static void SetupQuickPlayEnemies( Mission *mission, const int numEnemies, CharacterStore *store) { int i; for (i = 0; i < numEnemies; i++) { const GunDescription *gun; CArrayPushBack(&mission->Enemies, &i); for (;;) { gun = CArrayGet( &gGunDescriptions.Guns, rand() % (int)gGunDescriptions.Guns.size); if (!gun->IsRealGun) { continue; } // make at least one of each type of enemy: // - Short range weapon // - Long range weapon // - High explosive weapon if (i == 0 && !IsShortRange(gun)) { continue; } if (i == 1 && !IsLongRange(gun)) { continue; } if (i == 2 && ConfigGetBool(&gConfig, "QuickPlay.EnemiesWithExplosives") && !IsHighDPS(gun)) { continue; } if (!ConfigGetBool(&gConfig, "QuickPlay.EnemiesWithExplosives") && IsHighDPS(gun)) { continue; } break; } Character *ch = CharacterStoreAddOther(store); SetupQuickPlayEnemy(ch, gun); } }
static void DrawFloor(DrawBuffer *b, struct vec2i offset) { int x, y; struct vec2i pos; const Tile *tile = &b->tiles[0][0]; const bool useFog = ConfigGetBool(&gConfig, "Game.Fog"); for (y = 0, pos.y = b->dy + offset.y; y < Y_TILES; y++, pos.y += TILE_HEIGHT) { for (x = 0, pos.x = b->dx + offset.x; x < b->Size.x; x++, tile++, pos.x += TILE_WIDTH) { if (tile->Class != NULL && tile->Class->Pic != NULL && tile->Class->Pic->Data != NULL && tile->Class->Type != TILE_CLASS_WALL) { DrawLOSPic(tile, tile->Class->Pic, pos, useFog); } } tile += X_TILES - b->Size.x; } }
static void DoDamageCharacter( const Vec2i hitVector, const int power, const int flags, const int playerUID, const int uid, TActor *actor, const special_damage_e special) { // Create events: hit, damage, score CASSERT(actor->isInUse, "Cannot damage nonexistent player"); CASSERT(CanHitCharacter(flags, uid, actor), "damaging undamageable actor"); if (ConfigGetBool(&gConfig, "Game.ShotsPushback")) { GameEvent ei = GameEventNew(GAME_EVENT_ACTOR_IMPULSE); ei.u.ActorImpulse.UID = actor->uid; ei.u.ActorImpulse.Vel = Vec2i2Net(Vec2iScaleDiv( Vec2iScale(hitVector, power), SHOT_IMPULSE_DIVISOR)); ei.u.ActorImpulse.Pos = Vec2i2Net(actor->Pos); GameEventsEnqueue(&gGameEvents, ei); } const bool canDamage = CanDamageCharacter(flags, playerUID, uid, actor, special); GameEvent e = GameEventNew(GAME_EVENT_ACTOR_HIT); e.u.ActorHit.UID = actor->uid; e.u.ActorHit.PlayerUID = actor->PlayerUID; e.u.ActorHit.HitterPlayerUID = playerUID; e.u.ActorHit.Special = special; e.u.ActorHit.Power = canDamage ? power : 0; e.u.ActorHit.Vel = Vec2i2Net(hitVector); GameEventsEnqueue(&gGameEvents, e); if (canDamage) { // Don't score for friendly or player hits const bool isFriendly = (actor->flags & FLAGS_GOOD_GUY) || (!IsPVP(gCampaign.Entry.Mode) && actor->PlayerUID >= 0); if (playerUID >= 0 && power != 0 && !isFriendly) { // Calculate score based on // if they hit a penalty character e = GameEventNew(GAME_EVENT_SCORE); e.u.Score.PlayerUID = playerUID; if (actor->flags & FLAGS_PENALTY) { e.u.Score.Score = PENALTY_MULTIPLIER * power; } else { e.u.Score.Score = power; } GameEventsEnqueue(&gGameEvents, e); } } }
void WeaponUpdate( Weapon *w, const int ticks, const Vec2i fullPos, const direction_e d) { // Reload sound if (ConfigGetBool(&gConfig, "Sound.Reloads") && w->lock > w->Gun->ReloadLead && w->lock - ticks <= w->Gun->ReloadLead && w->lock > 0 && w->Gun->ReloadSound != NULL) { SoundPlayAtPlusDistance( &gSoundDevice, w->Gun->ReloadSound, Vec2iFull2Real(fullPos), RELOAD_DISTANCE_PLUS); // Brass shells if (w->Gun->Brass) { AddBrass(w->Gun, d, fullPos); } } w->lock -= ticks; if (w->lock < 0) { w->lock = 0; } w->soundLock -= ticks; if (w->soundLock < 0) { w->soundLock = 0; } w->clickLock -= ticks; if (w->clickLock < 0) { w->clickLock = 0; } if (w->stateCounter >= 0) { w->stateCounter = MAX(0, w->stateCounter - ticks); if (w->stateCounter == 0) { switch (w->state) { case GUNSTATE_FIRING: WeaponSetState(w, GUNSTATE_RECOIL); break; case GUNSTATE_RECOIL: WeaponSetState(w, GUNSTATE_READY); break; default: assert(0); break; } } } }
static void AddPickupAtObject(const TObject *o, const PickupType type) { GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP); switch (type) { case PICKUP_JEWEL: CASSERT(false, "unexpected pickup type"); break; case PICKUP_HEALTH: if (!ConfigGetBool(&gConfig, "Game.HealthPickups")) { return; } strcpy(e.u.AddPickup.PickupClass, "health"); break; case PICKUP_AMMO: if (!ConfigGetBool(&gConfig, "Game.Ammo")) { return; } // Pick a random ammo type and spawn it { const int ammoId = rand() % AmmoGetNumClasses(&gAmmo); const Ammo *a = AmmoGetById(&gAmmo, ammoId); sprintf(e.u.AddPickup.PickupClass, "ammo_%s", a->Name); } break; case PICKUP_KEYCARD: CASSERT(false, "unexpected pickup type"); break; case PICKUP_GUN: // Pick a random mission gun type and spawn it { const int gunId = (int)(rand() % gMission.Weapons.size); const WeaponClass **wc = CArrayGet(&gMission.Weapons, gunId); sprintf(e.u.AddPickup.PickupClass, "gun_%s", (*wc)->name); } break; default: CASSERT(false, "unexpected pickup type"); break; } e.u.AddPickup.UID = PickupsGetNextUID(); e.u.AddPickup.Pos = Vec2ToNet(o->thing.Pos); e.u.AddPickup.IsRandomSpawned = true; e.u.AddPickup.SpawnerUID = -1; e.u.AddPickup.ThingFlags = 0; GameEventsEnqueue(&gGameEvents, e); }
void GraphicsConfigSetFromConfig(GraphicsConfig *c) { GraphicsConfigSet( c, Vec2iNew( ConfigGetInt(&gConfig, "Graphics.ResolutionWidth"), ConfigGetInt(&gConfig, "Graphics.ResolutionHeight")), ConfigGetBool(&gConfig, "Graphics.Fullscreen"), ConfigGetInt(&gConfig, "Graphics.ScaleFactor")); }
static void DrawWallsAndThings(DrawBuffer *b, struct vec2i offset) { struct vec2i pos; Tile *tile = &b->tiles[0][0]; pos.y = b->dy + WALL_OFFSET_Y + offset.y; const bool useFog = ConfigGetBool(&gConfig, "Game.Fog"); for (int y = 0; y < Y_TILES; y++, pos.y += TILE_HEIGHT) { CArrayClear(&b->displaylist); pos.x = b->dx + offset.x; for (int x = 0; x < b->Size.x; x++, tile++, pos.x += TILE_WIDTH) { if (tile->Class->Type == TILE_CLASS_WALL) { DrawLOSPic(tile, tile->Class->Pic, pos, useFog); } else if (tile->Class->Type == TILE_CLASS_DOOR && tile->ClassAlt && tile->ClassAlt->Pic) { // Drawing doors // Doors may be offset; vertical doors are drawn centered // horizontal doors are bottom aligned struct vec2i doorPos = pos; const Pic *pic = tile->ClassAlt->Pic; doorPos.x += (TILE_WIDTH - pic->size.x) / 2; if (pic->size.y > 16) { doorPos.y += TILE_HEIGHT - (pic->size.y % TILE_HEIGHT); } DrawLOSPic(tile, pic, doorPos, useFog); } // Draw the items that are in LOS if (tile->outOfSight) { continue; } CA_FOREACH(ThingId, tid, tile->things) const Thing *ti = ThingIdGetThing(tid); // Drawn later if (ThingDrawLast(ti)) { continue; } CArrayPushBack(&b->displaylist, &ti); CA_FOREACH_END() } DrawBufferSortDisplayList(b); CA_FOREACH(const Thing *, tp, b->displaylist) DrawThing(b, *tp, offset); CA_FOREACH_END() tile += X_TILES - b->Size.x; } }
static void DrawWeaponStatus( HUD *hud, const TActor *actor, Vec2i pos, const FontAlign hAlign, const FontAlign vAlign) { const Weapon *weapon = ActorGetGun(actor); // Draw gun icon, and allocate padding to draw the gun icon const GunDescription *g = ActorGetGun(actor)->Gun; const Vec2i iconPos = Vec2iAligned( Vec2iNew(pos.x - 2, pos.y - 2), g->Icon->size, hAlign, vAlign, gGraphicsDevice.cachedConfig.Res); Blit(&gGraphicsDevice, g->Icon, iconPos); // don't draw gauge if not reloading if (weapon->lock > 0) { const Vec2i gaugePos = Vec2iAdd(pos, Vec2iNew(-1 + GUN_ICON_PAD, -1)); const Vec2i size = Vec2iNew(GAUGE_WIDTH - GUN_ICON_PAD, FontH() + 2); const color_t barColor = { 0, 0, 255, 255 }; const int maxLock = weapon->Gun->Lock; int innerWidth; color_t backColor = { 128, 128, 128, 255 }; if (maxLock == 0) { innerWidth = 0; } else { innerWidth = MAX(1, size.x * (maxLock - weapon->lock) / maxLock); } DrawGauge( hud->device, gaugePos, size, innerWidth, barColor, backColor, hAlign, vAlign); } FontOpts opts = FontOptsNew(); opts.HAlign = hAlign; opts.VAlign = vAlign; opts.Area = gGraphicsDevice.cachedConfig.Res; opts.Pad = Vec2iNew(pos.x + GUN_ICON_PAD, pos.y); char buf[128]; if (ConfigGetBool(&gConfig, "Game.Ammo") && weapon->Gun->AmmoId >= 0) { // Include ammo counter sprintf(buf, "%s %d/%d", weapon->Gun->name, ActorGunGetAmmo(actor, weapon), AmmoGetById(&gAmmo, weapon->Gun->AmmoId)->Max); } else { strcpy(buf, weapon->Gun->name); } FontStrOpt(buf, Vec2iZero(), opts); }
void GraphicsConfigSetFromConfig(GraphicsConfig *gc, Config *c) { GraphicsConfigSet( gc, Vec2iNew( ConfigGetInt(c, "Graphics.ResolutionWidth"), ConfigGetInt(c, "Graphics.ResolutionHeight")), ConfigGetBool(c, "Graphics.Fullscreen"), ConfigGetInt(c, "Graphics.ScaleFactor"), (ScaleMode)ConfigGetEnum(c, "Graphics.ScaleMode"), ConfigGetInt(c, "Graphics.Brightness")); }
void LoadPic( const Pic **value, json_t *node, const char *name, const char *oldPicName) { if (json_find_first_label(node, name)) { char *tmp = GetString(node, name); *value = PicManagerGetPic(&gPicManager, tmp); CFREE(tmp); } if ((*value == NULL || ConfigGetBool(&gConfig, "Graphics.OriginalPics")) && json_find_first_label(node, oldPicName)) { int oldPic; LoadInt(&oldPic, node, oldPicName); *value = PicManagerGetFromOld(&gPicManager, oldPic); } }
static void DrawChatter( const TTileItem *ti, DrawBuffer *b, const Vec2i offset) { if (!ConfigGetBool(&gConfig, "Graphics.ShowHUD")) { return; } const TActor *a = CArrayGet(&gActors, ti->id); // Draw character text if (strlen(a->Chatter) > 0) { const Vec2i textPos = Vec2iNew( a->tileItem.x - b->xTop + offset.x - FontStrW(a->Chatter) / 2, a->tileItem.y - b->yTop + offset.y - ACTOR_HEIGHT); FontStr(a->Chatter, textPos); } }
// Three types of tile drawing, based on line of sight: // Unvisited: black // Out of sight: dark, or if fog disabled, black // In sight: full color static color_t GetTileLOSMask(Tile *tile) { if (!tile->isVisited) { return colorBlack; } if (tile->flags & MAPTILE_OUT_OF_SIGHT) { if (ConfigGetBool(&gConfig, "Game.Fog")) { color_t mask = { 96, 96, 96, 255 }; return mask; } else { return colorBlack; } } return colorWhite; }
void DrawObjectiveHighlights(DrawBuffer *b, const Vec2i offset) { if (!ConfigGetBool(&gConfig, "Graphics.ShowHUD")) { return; } Tile *tile = &b->tiles[0][0]; for (int y = 0; y < Y_TILES; y++) { for (int x = 0; x < b->Size.x; x++, tile++) { // Draw the items that are in LOS CA_FOREACH(ThingId, tid, tile->things) TTileItem *ti = ThingIdGetTileItem(tid); DrawObjectiveHighlight(ti, tile, b, offset); CA_FOREACH_END() } tile += X_TILES - b->Size.x; } }
static void DrawFloor(DrawBuffer *b, Vec2i offset) { int x, y; Vec2i pos; const Tile *tile = &b->tiles[0][0]; const bool useFog = ConfigGetBool(&gConfig, "Game.Fog"); for (y = 0, pos.y = b->dy + offset.y; y < Y_TILES; y++, pos.y += TILE_HEIGHT) { for (x = 0, pos.x = b->dx + offset.x; x < b->Size.x; x++, tile++, pos.x += TILE_WIDTH) { if (tile->pic != NULL && tile->pic->pic.Data != NULL && !(tile->flags & MAPTILE_IS_WALL)) { switch (GetTileLOS(tile, useFog)) { case TILE_LOS_NORMAL: Blit(&gGraphicsDevice, &tile->pic->pic, pos); break; case TILE_LOS_FOG: BlitMasked( &gGraphicsDevice, &tile->pic->pic, pos, colorFog, false); break; case TILE_LOS_NONE: default: // don't draw break; } } } tile += X_TILES - b->Size.x; } }
void DrawShadow(GraphicsDevice *device, Vec2i pos, Vec2i size) { Vec2i drawPos; HSV tint = { -1.0, 1.0, 0.0 }; if (!ConfigGetBool(&gConfig, "Game.Shadows")) { return; } for (drawPos.y = pos.y - size.y; drawPos.y < pos.y + size.y; drawPos.y++) { if (drawPos.y >= device->clipping.bottom) { break; } if (drawPos.y < device->clipping.top) { continue; } for (drawPos.x = pos.x - size.x; drawPos.x < pos.x + size.x; drawPos.x++) { // Calculate value tint based on distance from center Vec2i scaledPos; int distance2; if (drawPos.x >= device->clipping.right) { break; } if (drawPos.x < device->clipping.left) { continue; } scaledPos.x = drawPos.x; scaledPos.y = (drawPos.y - pos.y) * size.x / size.y + pos.y; distance2 = DistanceSquared(scaledPos, pos); // Maximum distance is x, so scale distance squared by x squared tint.v = CLAMP(distance2 * 1.0 / (size.x*size.x), 0.0, 1.0); DrawPointTint(device, drawPos, tint); } } }
void DrawWallColumn(int y, Vec2i pos, Tile *tile) { const bool useFog = ConfigGetBool(&gConfig, "Game.Fog"); while (y >= 0 && (tile->flags & MAPTILE_IS_WALL)) { switch (GetTileLOS(tile, useFog)) { case TILE_LOS_NORMAL: Blit(&gGraphicsDevice, &tile->pic->pic, pos); break; case TILE_LOS_FOG: BlitMasked(&gGraphicsDevice, &tile->pic->pic, pos, colorFog, false); break; case TILE_LOS_NONE: default: // don't draw anything break; } pos.y -= TILE_HEIGHT; tile -= X_TILES; y--; } }
void EventPoll(EventHandlers *handlers, Uint32 ticks) { SDL_Event e; handlers->HasResolutionChanged = false; handlers->HasLostFocus = false; KeyPrePoll(&handlers->keyboard); MousePrePoll(&handlers->mouse); JoyPrePoll(&handlers->joysticks); SDL_free(handlers->DropFile); handlers->DropFile = NULL; // Don't process mouse events if focus just regained this cycle // This is to prevent bogus click events outside the window, e.g. in the // title bar bool regainedFocus = false; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_KEYDOWN: if (e.key.repeat) { break; } KeyOnKeyDown(&handlers->keyboard, e.key.keysym); break; case SDL_KEYUP: KeyOnKeyUp(&handlers->keyboard, e.key.keysym); break; case SDL_TEXTINPUT: strcpy(handlers->keyboard.Typed, e.text.text); break; case SDL_CONTROLLERDEVICEADDED: { const SDL_JoystickID jid = JoyAdded(e.cdevice.which); if (jid == -1) { break; } // If there are players with unset devices, // set this controller to them CA_FOREACH(PlayerData, p, gPlayerDatas) if (p->inputDevice == INPUT_DEVICE_UNSET) { PlayerTrySetInputDevice(p, INPUT_DEVICE_JOYSTICK, jid); LOG(LM_INPUT, LL_INFO, "Joystick %d assigned to player %d", jid, p->UID); break; } CA_FOREACH_END() } break; case SDL_CONTROLLERDEVICEREMOVED: JoyRemoved(e.cdevice.which); // If there was a player using this joystick, // set their input device to nothing CA_FOREACH(PlayerData, p, gPlayerDatas) if (p->inputDevice == INPUT_DEVICE_JOYSTICK && p->deviceIndex == e.cdevice.which) { PlayerTrySetInputDevice(p, INPUT_DEVICE_UNSET, 0); LOG(LM_INPUT, LL_WARN, "Joystick for player %d removed", p->UID); break; } CA_FOREACH_END() break; case SDL_CONTROLLERBUTTONDOWN: JoyOnButtonDown(e.cbutton); break; case SDL_CONTROLLERBUTTONUP: JoyOnButtonUp(e.cbutton); break; case SDL_CONTROLLERAXISMOTION: JoyOnAxis(e.caxis); break; case SDL_MOUSEBUTTONDOWN: if (regainedFocus) break; MouseOnButtonDown(&handlers->mouse, e.button.button); break; case SDL_MOUSEBUTTONUP: if (regainedFocus) break; MouseOnButtonUp(&handlers->mouse, e.button.button); break; case SDL_MOUSEWHEEL: if (regainedFocus) break; MouseOnWheel(&handlers->mouse, e.wheel.x, e.wheel.y); break; case SDL_WINDOWEVENT: switch (e.window.event) { case SDL_WINDOWEVENT_FOCUS_GAINED: regainedFocus = true; MusicSetPlaying(&gSoundDevice, true); break; case SDL_WINDOWEVENT_FOCUS_LOST: if (!gCampaign.IsClient && !ConfigGetBool(&gConfig, "StartServer")) { MusicSetPlaying(&gSoundDevice, false); handlers->HasLostFocus = true; } // Reset input handlers EventReset( handlers, handlers->mouse.cursor, handlers->mouse.trail); break; case SDL_WINDOWEVENT_SIZE_CHANGED: handlers->HasResolutionChanged = true; if (gGraphicsDevice.cachedConfig.IsEditor) { const int scale = ConfigGetInt(&gConfig, "Graphics.ScaleFactor"); GraphicsConfigSet( &gGraphicsDevice.cachedConfig, Vec2iScaleDiv( Vec2iNew(e.window.data1, e.window.data2), scale), false, scale, gGraphicsDevice.cachedConfig.ScaleMode, gGraphicsDevice.cachedConfig.Brightness); GraphicsInitialize(&gGraphicsDevice); } break; default: // do nothing break; } break; case SDL_QUIT: handlers->HasQuit = true; break; case SDL_DROPFILE: handlers->DropFile = e.drop.file; break; default: break; } } KeyPostPoll(&handlers->keyboard, ticks); MousePostPoll(&handlers->mouse, ticks); }
static void DrawWallsAndThings(DrawBuffer *b, Vec2i offset) { Vec2i pos; Tile *tile = &b->tiles[0][0]; pos.y = b->dy + WALL_OFFSET_Y + offset.y; const bool useFog = ConfigGetBool(&gConfig, "Game.Fog"); for (int y = 0; y < Y_TILES; y++, pos.y += TILE_HEIGHT) { CArrayClear(&b->displaylist); pos.x = b->dx + offset.x; for (int x = 0; x < b->Size.x; x++, tile++, pos.x += TILE_WIDTH) { if (tile->flags & MAPTILE_IS_WALL) { if (!(tile->flags & MAPTILE_DELAY_DRAW)) { DrawWallColumn(y, pos, tile); } } else if (tile->flags & MAPTILE_OFFSET_PIC) { // Drawing doors // Doors may be offset; vertical doors are drawn centered // horizontal doors are bottom aligned Vec2i doorPos = pos; doorPos.x += (TILE_WIDTH - tile->picAlt->pic.size.x) / 2; if (tile->picAlt->pic.size.y > 16) { doorPos.y += TILE_HEIGHT - (tile->picAlt->pic.size.y % TILE_HEIGHT); } switch (GetTileLOS(tile, useFog)) { case TILE_LOS_NORMAL: Blit(&gGraphicsDevice, &tile->picAlt->pic, doorPos); break; case TILE_LOS_FOG: BlitMasked( &gGraphicsDevice, &tile->picAlt->pic, doorPos, colorFog, false); break; case TILE_LOS_NONE: default: // don't draw anything break; } } // Draw the items that are in LOS if (tile->flags & MAPTILE_OUT_OF_SIGHT) { continue; } CA_FOREACH(ThingId, tid, tile->things) const TTileItem *ti = ThingIdGetTileItem(tid); // Drawn later if (TileItemDrawLast(ti)) { continue; } CArrayPushBack(&b->displaylist, &ti); CA_FOREACH_END() } DrawBufferSortDisplayList(b); CA_FOREACH(const TTileItem *, tp, b->displaylist) DrawThing(b, *tp, offset); CA_FOREACH_END() tile += X_TILES - b->Size.x; } }
static void LoadBullet( BulletClass *b, json_t *node, const BulletClass *defaultBullet) { memset(b, 0, sizeof *b); if (defaultBullet != NULL) { memcpy(b, defaultBullet, sizeof *b); if (defaultBullet->HitSound.Object != NULL) { CSTRDUP(b->HitSound.Object, defaultBullet->HitSound.Object); } if (defaultBullet->HitSound.Flesh != NULL) { CSTRDUP(b->HitSound.Flesh, defaultBullet->HitSound.Flesh); } if (defaultBullet->HitSound.Wall != NULL) { CSTRDUP(b->HitSound.Wall, defaultBullet->HitSound.Wall); } // TODO: enable default bullet guns? memset(&b->Falling.DropGuns, 0, sizeof b->Falling.DropGuns); memset(&b->OutOfRangeGuns, 0, sizeof b->OutOfRangeGuns); memset(&b->HitGuns, 0, sizeof b->HitGuns); memset(&b->ProximityGuns, 0, sizeof b->ProximityGuns); } char *tmp; LoadStr(&b->Name, node, "Name"); if (json_find_first_label(node, "Pic")) { json_t *pic = json_find_first_label(node, "Pic")->child; tmp = GetString(pic, "Type"); b->CPic.Type = StrPicType(tmp); CFREE(tmp); bool picLoaded = false; switch (b->CPic.Type) { case PICTYPE_NORMAL: tmp = GetString(pic, "Pic"); b->CPic.u.Pic = PicManagerGetPic(&gPicManager, tmp); CFREE(tmp); picLoaded = b->CPic.u.Pic != NULL; break; case PICTYPE_DIRECTIONAL: tmp = GetString(pic, "Sprites"); b->CPic.u.Sprites = &PicManagerGetSprites(&gPicManager, tmp)->pics; CFREE(tmp); picLoaded = b->CPic.u.Sprites != NULL; break; case PICTYPE_ANIMATED: // fallthrough case PICTYPE_ANIMATED_RANDOM: tmp = GetString(pic, "Sprites"); b->CPic.u.Animated.Sprites = &PicManagerGetSprites(&gPicManager, tmp)->pics; CFREE(tmp); LoadInt(&b->CPic.u.Animated.Count, pic, "Count"); LoadInt(&b->CPic.u.Animated.TicksPerFrame, pic, "TicksPerFrame"); // Set safe default ticks per frame 1; // if 0 then this leads to infinite loop when animating b->CPic.u.Animated.TicksPerFrame = MAX( b->CPic.u.Animated.TicksPerFrame, 1); picLoaded = b->CPic.u.Animated.Sprites != NULL; break; default: CASSERT(false, "unknown pic type"); break; } b->CPic.UseMask = true; b->CPic.u1.Mask = colorWhite; if (json_find_first_label(pic, "Mask")) { tmp = GetString(pic, "Mask"); b->CPic.u1.Mask = StrColor(tmp); CFREE(tmp); } else if (json_find_first_label(pic, "Tint")) { b->CPic.UseMask = false; json_t *tint = json_find_first_label(pic, "Tint")->child->child; b->CPic.u1.Tint.h = atof(tint->text); tint = tint->next; b->CPic.u1.Tint.s = atof(tint->text); tint = tint->next; b->CPic.u1.Tint.v = atof(tint->text); } if ((json_find_first_label(pic, "OldPic") && ConfigGetBool(&gConfig, "Graphics.OriginalPics")) || !picLoaded) { int oldPic = PIC_UZIBULLET; LoadInt(&oldPic, pic, "OldPic"); b->CPic.Type = PICTYPE_NORMAL; b->CPic.u.Pic = PicManagerGetFromOld(&gPicManager, oldPic); } } LoadVec2i(&b->ShadowSize, node, "ShadowSize"); LoadInt(&b->Delay, node, "Delay"); if (json_find_first_label(node, "Speed")) { LoadInt(&b->SpeedLow, node, "Speed"); b->SpeedHigh = b->SpeedLow; } if (json_find_first_label(node, "SpeedLow")) { LoadInt(&b->SpeedLow, node, "SpeedLow"); } if (json_find_first_label(node, "SpeedHigh")) { LoadInt(&b->SpeedHigh, node, "SpeedHigh"); } b->SpeedLow = MIN(b->SpeedLow, b->SpeedHigh); b->SpeedHigh = MAX(b->SpeedLow, b->SpeedHigh); LoadBool(&b->SpeedScale, node, "SpeedScale"); LoadInt(&b->Friction, node, "Friction"); if (json_find_first_label(node, "Range")) { LoadInt(&b->RangeLow, node, "Range"); b->RangeHigh = b->RangeLow; } if (json_find_first_label(node, "RangeLow")) { LoadInt(&b->RangeLow, node, "RangeLow"); } if (json_find_first_label(node, "RangeHigh")) { LoadInt(&b->RangeHigh, node, "RangeHigh"); } b->RangeLow = MIN(b->RangeLow, b->RangeHigh); b->RangeHigh = MAX(b->RangeLow, b->RangeHigh); LoadInt(&b->Power, node, "Power"); LoadVec2i(&b->Size, node, "Size"); if (json_find_first_label(node, "Special")) { tmp = GetString(node, "Special"); b->Special = StrSpecialDamage(tmp); CFREE(tmp); } LoadBool(&b->HurtAlways, node, "HurtAlways"); LoadBool(&b->Persists, node, "Persists"); if (json_find_first_label(node, "Spark")) { tmp = GetString(node, "Spark"); b->Spark = StrParticleClass(&gParticleClasses, tmp); CFREE(tmp); } if (json_find_first_label(node, "HitSounds")) { json_t *hitSounds = json_find_first_label(node, "HitSounds")->child; CFREE(b->HitSound.Object); b->HitSound.Object = NULL; LoadStr(&b->HitSound.Object, hitSounds, "Object"); CFREE(b->HitSound.Flesh); b->HitSound.Flesh = NULL; LoadStr(&b->HitSound.Flesh, hitSounds, "Flesh"); CFREE(b->HitSound.Wall); b->HitSound.Wall = NULL; LoadStr(&b->HitSound.Wall, hitSounds, "Wall"); } LoadBool(&b->WallBounces, node, "WallBounces"); LoadBool(&b->HitsObjects, node, "HitsObjects"); if (json_find_first_label(node, "Falling")) { json_t *falling = json_find_first_label(node, "Falling")->child; LoadInt(&b->Falling.GravityFactor, falling, "GravityFactor"); LoadBool(&b->Falling.FallsDown, falling, "FallsDown"); LoadBool(&b->Falling.DestroyOnDrop, falling, "DestroyOnDrop"); LoadBool(&b->Falling.Bounces, falling, "Bounces"); } LoadInt(&b->SeekFactor, node, "SeekFactor"); LoadBool(&b->Erratic, node, "Erratic"); b->node = node; }
FEATURE(1, "Load default config") SCENARIO("Load a default config") { Config config1, config2; GIVEN("two configs") GIVEN_END WHEN("I load them both with defaults") // Note: default loaded before loading from file config1 = ConfigLoad(NULL); config2 = ConfigLoad(NULL); WHEN_END THEN("the two configs should have the same values") SHOULD_INT_EQUAL( ConfigGetBool(&config1, "Game.FriendlyFire"), ConfigGetBool(&config2, "Game.FriendlyFire")); SHOULD_INT_EQUAL( ConfigGetInt(&config1, "Graphics.Brightness"), ConfigGetInt(&config2, "Graphics.Brightness")); THEN_END } SCENARIO_END FEATURE_END FEATURE(2, "Save and load") SCENARIO("Save and load a JSON config file") { Config config1, config2; GIVEN("a config file with some values, and I save the config to a JSON file") config1 = ConfigLoad(NULL);
// Draw player's score, health etc. static void DrawPlayerStatus( HUD *hud, const PlayerData *data, const TActor *p, const int flags, const Rect2i r) { if (p != NULL) { DrawObjectiveCompass( hud->device, Vec2iFull2Real(p->Pos), r, hud->showExit); } Vec2i pos = Vec2iNew(5, 5); FontOpts opts = FontOptsNew(); if (flags & HUDFLAGS_PLACE_RIGHT) { opts.HAlign = ALIGN_END; } if (flags & HUDFLAGS_PLACE_BOTTOM) { opts.VAlign = ALIGN_END; pos.y += BOTTOM_PADDING; } opts.Area = gGraphicsDevice.cachedConfig.Res; opts.Pad = pos; FontStrOpt(data->name, Vec2iZero(), opts); const int rowHeight = 1 + FontH(); pos.y += rowHeight; char s[50]; if (IsScoreNeeded(gCampaign.Entry.Mode)) { if (ConfigGetBool(&gConfig, "Game.Ammo")) { // Display money instead of ammo sprintf(s, "Cash: $%d", data->score); } else { sprintf(s, "Score: %d", data->score); } } else { s[0] = 0; } if (p) { // Score/money opts.Pad = pos; FontStrOpt(s, Vec2iZero(), opts); // Health pos.y += rowHeight; DrawHealth(hud->device, p, pos, opts.HAlign, opts.VAlign); // Lives pos.y += rowHeight; DrawLives(hud->device, data, pos, opts.HAlign, opts.VAlign); // Weapon pos.y += rowHeight + LIVES_ROW_EXTRA_Y; DrawWeaponStatus(hud, p, pos, opts.HAlign, opts.VAlign); } else { opts.Pad = pos; FontStrOpt(s, Vec2iZero(), opts); } if (ConfigGetBool(&gConfig, "Interface.ShowHUDMap") && !(flags & HUDFLAGS_SHARE_SCREEN) && IsAutoMapEnabled(gCampaign.Entry.Mode)) { DrawRadar(hud->device, p, RADAR_SCALE, flags, hud->showExit); } }
static void Campaign(GraphicsDevice *graphics, CampaignOptions *co) { if (co->IsClient) { // If connecting to a server, we've already received the mission index // Do nothing } else if (IsPasswordAllowed(co->Entry.Mode)) { MissionSave m; AutosaveLoadMission(&gAutosave, &m, co->Entry.Path); co->MissionIndex = EnterPassword(graphics, &m); } else { co->MissionIndex = 0; } bool run = false; bool gameOver = true; do { // Unready all the players for (int i = 0; i < (int)gPlayerDatas.size; i++) { PlayerData *p = CArrayGet(&gPlayerDatas, i); p->Ready = false; } CampaignAndMissionSetup(1, co, &gMission); if (IsGameOptionsNeeded(co->Entry.Mode)) { debug(D_NORMAL, ">> Game options\n"); if (!GameOptions(co->Entry.Mode)) { run = false; goto bail; } co->OptionsSet = true; // If enabled, start net server if (!co->IsClient && ConfigGetBool(&gConfig, "StartServer")) { NetServerOpen(&gNetServer); } } // Mission briefing if (IsMissionBriefingNeeded(co->Entry.Mode)) { if (!ScreenMissionBriefing(&gMission)) { run = false; goto bail; } } // Equip guns if (!PlayerEquip()) { run = false; goto bail; } if (co->IsClient) { if (!ScreenWaitForGameStart()) { run = false; goto bail; } } run = RunGame(co, &gMission, &gMap); // Don't quit if all players died, that's normal for PVP modes if (IsPVP(co->Entry.Mode) && GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, false) == 0) { run = true; } const int survivingPlayers = GetNumPlayers(PLAYER_ALIVE, false, false); // In co-op (non-PVP) modes, at least one player must survive if (!IsPVP(co->Entry.Mode)) { gameOver = survivingPlayers == 0 || co->MissionIndex == (int)gCampaign.Setting.Missions.size - 1; } int maxScore = 0; for (int i = 0; i < (int)gPlayerDatas.size; i++) { PlayerData *p = CArrayGet(&gPlayerDatas, i); p->survived = IsPlayerAlive(p); if (IsPlayerAlive(p)) { TActor *player = ActorGetByUID(p->ActorUID); p->hp = player->health; p->RoundsWon++; maxScore = MAX(maxScore, p->RoundsWon); } } if (IsPVP(co->Entry.Mode)) { gameOver = maxScore == ModeMaxRoundsWon(co->Entry.Mode); CASSERT(maxScore <= ModeMaxRoundsWon(co->Entry.Mode), "score exceeds max rounds won"); } MissionEnd(); MusicPlayMenu(&gSoundDevice); if (run) { switch (co->Entry.Mode) { case GAME_MODE_DOGFIGHT: ScreenDogfightScores(); break; case GAME_MODE_DEATHMATCH: ScreenDeathmatchFinalScores(); break; default: ScreenMissionSummary(&gCampaign, &gMission); // Note: must use cached value because players get cleaned up // in CleanupMission() if (gameOver && survivingPlayers > 0) { ScreenVictory(&gCampaign); } break; } } // Check if any scores exceeded high scores, if we're not a PVP mode if (!IsPVP(co->Entry.Mode)) { bool allTime = false; bool todays = false; for (int i = 0; i < (int)gPlayerDatas.size; i++) { PlayerData *p = CArrayGet(&gPlayerDatas, i); if (((run && !p->survived) || gameOver) && p->IsLocal) { EnterHighScore(p); allTime |= p->allTime >= 0; todays |= p->today >= 0; } if (!p->survived) { p->totalScore = 0; p->missions = 0; } else { p->missions++; } p->lastMission = co->MissionIndex; } if (allTime) { DisplayAllTimeHighScores(graphics); } if (todays) { DisplayTodaysHighScores(graphics); } } if (!HasRounds(co->Entry.Mode)) { co->MissionIndex++; } bail: // Need to terminate the mission later as it is used in calculating scores MissionOptionsTerminate(&gMission); } while (run && !gameOver); // Final screen if (run) { switch (co->Entry.Mode) { case GAME_MODE_DOGFIGHT: ScreenDogfightFinalScores(); break; default: // no end screen break; } } NetServerClose(&gNetServer); }
void HUDDraw(HUD *hud, const input_device_e pausingDevice) { char s[50]; int flags = 0; const int numPlayersAlive = GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, false); const int numLocalPlayers = GetNumPlayers(PLAYER_ANY, false, true); const int numLocalPlayersAlive = GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, true); Rect2i r; r.Size = Vec2iNew( hud->device->cachedConfig.Res.x, hud->device->cachedConfig.Res.y); if (numLocalPlayersAlive <= 1) { flags = 0; } else if ( ConfigGetEnum(&gConfig, "Interface.Splitscreen") == SPLITSCREEN_NEVER) { flags |= HUDFLAGS_SHARE_SCREEN; } else if (numLocalPlayers == 2) { r.Size.x /= 2; flags |= HUDFLAGS_HALF_SCREEN; } else if (numLocalPlayers == 3 || numLocalPlayers == 4) { r.Size.x /= 2; r.Size.y /= 2; flags |= HUDFLAGS_QUARTER_SCREEN; } else { assert(0 && "not implemented"); } int idx = 0; for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++) { const PlayerData *p = CArrayGet(&gPlayerDatas, i); if (!p->IsLocal) { idx--; continue; } int drawFlags = flags; r.Pos = Vec2iZero(); if (idx & 1) { r.Pos.x = r.Size.x; drawFlags |= HUDFLAGS_PLACE_RIGHT; } if (idx >= 2) { r.Pos.y = r.Size.y; drawFlags |= HUDFLAGS_PLACE_BOTTOM; } TActor *player = NULL; if (IsPlayerAlive(p)) { player = ActorGetByUID(p->ActorUID); } DrawPlayerStatus(hud, p, player, drawFlags, r); DrawScoreUpdate(&hud->scoreUpdates[idx], drawFlags); DrawHealthUpdate(&hud->healthUpdates[idx], drawFlags); DrawAmmoUpdate(&hud->ammoUpdates[idx], drawFlags); } // Only draw radar once if shared if (ConfigGetBool(&gConfig, "Interface.ShowHUDMap") && (flags & HUDFLAGS_SHARE_SCREEN) && IsAutoMapEnabled(gCampaign.Entry.Mode)) { DrawSharedRadar(hud->device, RADAR_SCALE, hud->showExit); } if (numPlayersAlive == 0) { if (AreAllPlayersDeadAndNoLives()) { if (!IsPVP(gCampaign.Entry.Mode)) { FontStrCenter("Game Over!"); } else { FontStrCenter("All Kill!"); } } } else if (hud->mission->state == MISSION_STATE_PICKUP) { int timeLeft = gMission.pickupTime + PICKUP_LIMIT - gMission.time; sprintf(s, "Pickup in %d seconds\n", (timeLeft + (FPS_FRAMELIMIT - 1)) / FPS_FRAMELIMIT); FontStrCenter(s); } if (pausingDevice != INPUT_DEVICE_UNSET) { Vec2i pos = Vec2iScaleDiv(Vec2iMinus( gGraphicsDevice.cachedConfig.Res, FontStrSize("Foo\nPress foo or bar to unpause\nBaz")), 2); const int x = pos.x; FontStr("<Paused>", pos); pos.y += FontH(); pos = FontStr("Press ", pos); color_t c = colorWhite; const char *buttonName = InputGetButtonNameColor(pausingDevice, 0, CMD_ESC, &c); pos = FontStrMask(buttonName, pos, c); FontStr(" again to quit", pos); pos.x = x; pos.y += FontH(); pos = FontStr("Press ", pos); buttonName = InputGetButtonNameColor( pausingDevice, 0, CMD_BUTTON1, &c); pos = FontStrMask(buttonName, pos, c); pos = FontStr(" or ", pos); buttonName = InputGetButtonNameColor( pausingDevice, 0, CMD_BUTTON2, &c); pos = FontStrMask(buttonName, pos, c); FontStr(" to unpause", pos); } if (hud->messageTicks > 0 || hud->messageTicks == -1) { // Draw the message centered, and just below the automap Vec2i pos = Vec2iNew( (hud->device->cachedConfig.Res.x - FontStrW(hud->message)) / 2, AUTOMAP_SIZE + AUTOMAP_PADDING + AUTOMAP_PADDING); FontStrMask(hud->message, pos, colorCyan); } if (ConfigGetBool(&gConfig, "Interface.ShowFPS")) { FPSCounterDraw(&hud->fpsCounter); } if (ConfigGetBool(&gConfig, "Interface.ShowTime")) { WallClockDraw(&hud->clock); } DrawKeycards(hud); // Draw elapsed mission time as MM:SS int missionTimeSeconds = gMission.time / FPS_FRAMELIMIT; sprintf(s, "%d:%02d", missionTimeSeconds / 60, missionTimeSeconds % 60); FontOpts opts = FontOptsNew(); opts.HAlign = ALIGN_CENTER; opts.Area = hud->device->cachedConfig.Res; opts.Pad.y = 5; FontStrOpt(s, Vec2iZero(), opts); if (HasObjectives(gCampaign.Entry.Mode)) { DrawObjectiveCounts(hud); } }
static void HandleGameEvent( const GameEvent e, Camera *camera, PowerupSpawner *healthSpawner, CArray *ammoSpawners) { switch (e.Type) { case GAME_EVENT_PLAYER_DATA: PlayerDataAddOrUpdate(e.u.PlayerData); break; case GAME_EVENT_TILE_SET: { Tile *t = MapGetTile(&gMap, Net2Vec2i(e.u.TileSet.Pos)); t->flags = e.u.TileSet.Flags; t->pic = PicManagerGetNamedPic( &gPicManager, e.u.TileSet.PicName); t->picAlt = PicManagerGetNamedPic( &gPicManager, e.u.TileSet.PicAltName); } break; case GAME_EVENT_MAP_OBJECT_ADD: ObjAdd(e.u.MapObjectAdd); break; case GAME_EVENT_MAP_OBJECT_DAMAGE: DamageObject(e.u.MapObjectDamage); break; case GAME_EVENT_SCORE: { PlayerData *p = PlayerDataGetByUID(e.u.Score.PlayerUID); PlayerScore(p, e.u.Score.Score); HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_SCORE, e.u.Score.PlayerUID, e.u.Score.Score); } break; case GAME_EVENT_SOUND_AT: if (!e.u.SoundAt.IsHit || ConfigGetBool(&gConfig, "Sound.Hits")) { SoundPlayAt( &gSoundDevice, StrSound(e.u.SoundAt.Sound), Net2Vec2i(e.u.SoundAt.Pos)); } break; case GAME_EVENT_SCREEN_SHAKE: camera->shake = ScreenShakeAdd( camera->shake, e.u.ShakeAmount, ConfigGetInt(&gConfig, "Graphics.ShakeMultiplier")); break; case GAME_EVENT_SET_MESSAGE: HUDDisplayMessage( &camera->HUD, e.u.SetMessage.Message, e.u.SetMessage.Ticks); break; case GAME_EVENT_GAME_START: gMission.HasStarted = true; break; case GAME_EVENT_ACTOR_ADD: ActorAdd(e.u.ActorAdd); break; case GAME_EVENT_ACTOR_MOVE: ActorMove(e.u.ActorMove); break; case GAME_EVENT_ACTOR_STATE: { TActor *a = ActorGetByUID(e.u.ActorState.UID); if (!a->isInUse) break; ActorSetState(a, (ActorAnimation)e.u.ActorState.State); } break; case GAME_EVENT_ACTOR_DIR: { TActor *a = ActorGetByUID(e.u.ActorDir.UID); if (!a->isInUse) break; a->direction = (direction_e)e.u.ActorDir.Dir; } break; case GAME_EVENT_ACTOR_SLIDE: { TActor *a = ActorGetByUID(e.u.ActorSlide.UID); if (!a->isInUse) break; a->Vel = Net2Vec2i(e.u.ActorSlide.Vel); // Slide sound if (ConfigGetBool(&gConfig, "Sound.Footsteps")) { SoundPlayAt( &gSoundDevice, gSoundDevice.slideSound, Vec2iNew(a->tileItem.x, a->tileItem.y)); } } break; case GAME_EVENT_ACTOR_IMPULSE: { TActor *a = ActorGetByUID(e.u.ActorImpulse.UID); if (!a->isInUse) break; a->Vel = Vec2iAdd(a->Vel, Net2Vec2i(e.u.ActorImpulse.Vel)); const Vec2i pos = Net2Vec2i(e.u.ActorImpulse.Pos); if (!Vec2iIsZero(pos)) { a->Pos = pos; } } break; case GAME_EVENT_ACTOR_SWITCH_GUN: ActorSwitchGun(e.u.ActorSwitchGun); break; case GAME_EVENT_ACTOR_PICKUP_ALL: { TActor *a = ActorGetByUID(e.u.ActorPickupAll.UID); if (!a->isInUse) break; a->PickupAll = e.u.ActorPickupAll.PickupAll; } break; case GAME_EVENT_ACTOR_REPLACE_GUN: ActorReplaceGun(e.u.ActorReplaceGun); break; case GAME_EVENT_ACTOR_HEAL: { TActor *a = ActorGetByUID(e.u.Heal.UID); if (!a->isInUse || a->dead) break; ActorHeal(a, e.u.Heal.Amount); // Sound of healing SoundPlayAt( &gSoundDevice, gSoundDevice.healthSound, Vec2iFull2Real(a->Pos)); // Tell the spawner that we took a health so we can // spawn more (but only if we're the server) if (e.u.Heal.IsRandomSpawned && !gCampaign.IsClient) { PowerupSpawnerRemoveOne(healthSpawner); } if (e.u.Heal.PlayerUID >= 0) { HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_HEALTH, e.u.Heal.PlayerUID, e.u.Heal.Amount); } } break; case GAME_EVENT_ACTOR_ADD_AMMO: { TActor *a = ActorGetByUID(e.u.AddAmmo.UID); if (!a->isInUse || a->dead) break; ActorAddAmmo(a, e.u.AddAmmo.AmmoId, e.u.AddAmmo.Amount); // Tell the spawner that we took ammo so we can // spawn more (but only if we're the server) if (e.u.AddAmmo.IsRandomSpawned && !gCampaign.IsClient) { PowerupSpawnerRemoveOne( CArrayGet(ammoSpawners, e.u.AddAmmo.AmmoId)); } if (e.u.AddAmmo.PlayerUID >= 0) { HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_AMMO, e.u.AddAmmo.PlayerUID, e.u.AddAmmo.Amount); } } break; case GAME_EVENT_ACTOR_USE_AMMO: { TActor *a = ActorGetByUID(e.u.UseAmmo.UID); if (!a->isInUse || a->dead) break; ActorAddAmmo(a, e.u.UseAmmo.AmmoId, -(int)e.u.UseAmmo.Amount); if (e.u.UseAmmo.PlayerUID >= 0) { HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_AMMO, e.u.UseAmmo.PlayerUID, -(int)e.u.UseAmmo.Amount); } } break; case GAME_EVENT_ACTOR_DIE: { TActor *a = ActorGetByUID(e.u.ActorDie.UID); // Check if the player has lives to revive PlayerData *p = PlayerDataGetByUID(a->PlayerUID); if (p != NULL) { p->Lives--; CASSERT(p->Lives >= 0, "Player has died too many times"); if (p->Lives > 0 && !gCampaign.IsClient) { // Find the closest player alive; try to spawn next to that position // if no other suitable position exists Vec2i defaultSpawnPosition = Vec2iZero(); const TActor *closestActor = AIGetClosestPlayer(a->Pos); if (closestActor != NULL) defaultSpawnPosition = closestActor->Pos; PlacePlayer(&gMap, p, defaultSpawnPosition, false); } } ActorDestroy(a); } break; case GAME_EVENT_ACTOR_MELEE: { const TActor *a = ActorGetByUID(e.u.Melee.UID); if (!a->isInUse) break; const BulletClass *b = StrBulletClass(e.u.Melee.BulletClass); if ((HitType)e.u.Melee.HitType != HIT_NONE && HasHitSound(b->Power, a->flags, a->PlayerUID, (TileItemKind)e.u.Melee.TargetKind, e.u.Melee.TargetUID, SPECIAL_NONE, false)) { PlayHitSound( &b->HitSound, (HitType)e.u.Melee.HitType, Vec2iFull2Real(a->Pos)); } if (!gCampaign.IsClient) { Damage( Vec2iZero(), b->Power, a->flags, a->PlayerUID, a->uid, (TileItemKind)e.u.Melee.TargetKind, e.u.Melee.TargetUID, SPECIAL_NONE); } } break; case GAME_EVENT_ADD_PICKUP: PickupAdd(e.u.AddPickup); // Play a spawn sound SoundPlayAt( &gSoundDevice, StrSound("spawn_item"), Net2Vec2i(e.u.AddPickup.Pos)); break; case GAME_EVENT_REMOVE_PICKUP: PickupDestroy(e.u.RemovePickup.UID); if (e.u.RemovePickup.SpawnerUID >= 0) { TObject *o = ObjGetByUID(e.u.RemovePickup.SpawnerUID); o->counter = AMMO_SPAWNER_RESPAWN_TICKS; } break; case GAME_EVENT_BULLET_BOUNCE: { TMobileObject *o = MobObjGetByUID(e.u.BulletBounce.UID); if (o == NULL || !o->isInUse) break; const Vec2i pos = Net2Vec2i(e.u.BulletBounce.BouncePos); PlayHitSound( &o->bulletClass->HitSound, (HitType)e.u.BulletBounce.HitType, Vec2iFull2Real(pos)); if (e.u.BulletBounce.Spark && o->bulletClass->Spark != NULL) { GameEvent s = GameEventNew(GAME_EVENT_ADD_PARTICLE); s.u.AddParticle.Class = o->bulletClass->Spark; s.u.AddParticle.FullPos = pos; s.u.AddParticle.Z = o->z; GameEventsEnqueue(&gGameEvents, s); } o->x = pos.x; o->y = pos.y; o->vel = Net2Vec2i(e.u.BulletBounce.BounceVel); } break; case GAME_EVENT_REMOVE_BULLET: { TMobileObject *o = MobObjGetByUID(e.u.RemoveBullet.UID); if (o == NULL || !o->isInUse) break; MobObjDestroy(o); } break; case GAME_EVENT_PARTICLE_REMOVE: ParticleDestroy(&gParticles, e.u.ParticleRemoveId); break; case GAME_EVENT_GUN_FIRE: { const GunDescription *g = StrGunDescription(e.u.GunFire.Gun); const Vec2i fullPos = Net2Vec2i(e.u.GunFire.MuzzleFullPos); // Add bullets if (g->Bullet && !gCampaign.IsClient) { // Find the starting angle of the spread (clockwise) // Keep in mind the fencepost problem, i.e. spread of 3 means a // total spread angle of 2x width const double spreadStartAngle = g->AngleOffset - (g->Spread.Count - 1) * g->Spread.Width / 2; for (int i = 0; i < g->Spread.Count; i++) { const double recoil = ((double)rand() / RAND_MAX * g->Recoil) - g->Recoil / 2; const double finalAngle = e.u.GunFire.Angle + spreadStartAngle + i * g->Spread.Width + recoil; GameEvent ab = GameEventNew(GAME_EVENT_ADD_BULLET); ab.u.AddBullet.UID = MobObjsObjsGetNextUID(); strcpy(ab.u.AddBullet.BulletClass, g->Bullet->Name); ab.u.AddBullet.MuzzlePos = Vec2i2Net(fullPos); ab.u.AddBullet.MuzzleHeight = e.u.GunFire.Z; ab.u.AddBullet.Angle = (float)finalAngle; ab.u.AddBullet.Elevation = RAND_INT(g->ElevationLow, g->ElevationHigh); ab.u.AddBullet.Flags = e.u.GunFire.Flags; ab.u.AddBullet.PlayerUID = e.u.GunFire.PlayerUID; ab.u.AddBullet.ActorUID = e.u.GunFire.UID; GameEventsEnqueue(&gGameEvents, ab); } } // Add muzzle flash if (GunHasMuzzle(g)) { GameEvent ap = GameEventNew(GAME_EVENT_ADD_PARTICLE); ap.u.AddParticle.Class = g->MuzzleFlash; ap.u.AddParticle.FullPos = fullPos; ap.u.AddParticle.Z = e.u.GunFire.Z; ap.u.AddParticle.Angle = e.u.GunFire.Angle; GameEventsEnqueue(&gGameEvents, ap); } // Sound if (e.u.GunFire.Sound && g->Sound) { SoundPlayAt(&gSoundDevice, g->Sound, Vec2iFull2Real(fullPos)); } // Screen shake if (g->ShakeAmount > 0) { GameEvent s = GameEventNew(GAME_EVENT_SCREEN_SHAKE); s.u.ShakeAmount = g->ShakeAmount; GameEventsEnqueue(&gGameEvents, s); } // Brass shells // If we have a reload lead, defer the creation of shells until then if (g->Brass && g->ReloadLead == 0) { const direction_e d = RadiansToDirection(e.u.GunFire.Angle); const Vec2i muzzleOffset = GunGetMuzzleOffset(g, d); GunAddBrass(g, d, Vec2iMinus(fullPos, muzzleOffset)); } } break; case GAME_EVENT_GUN_RELOAD: { const GunDescription *g = StrGunDescription(e.u.GunReload.Gun); const Vec2i fullPos = Net2Vec2i(e.u.GunReload.FullPos); SoundPlayAtPlusDistance( &gSoundDevice, g->ReloadSound, Vec2iFull2Real(fullPos), RELOAD_DISTANCE_PLUS); // Brass shells if (g->Brass) { GunAddBrass(g, (direction_e)e.u.GunReload.Direction, fullPos); } } break; case GAME_EVENT_GUN_STATE: { const TActor *a = ActorGetByUID(e.u.GunState.ActorUID); if (!a->isInUse) break; WeaponSetState(ActorGetGun(a), (gunstate_e)e.u.GunState.State); } break; case GAME_EVENT_ADD_BULLET: BulletAdd(e.u.AddBullet); break; case GAME_EVENT_ADD_PARTICLE: ParticleAdd(&gParticles, e.u.AddParticle); break; case GAME_EVENT_ACTOR_HIT: { TActor *a = ActorGetByUID(e.u.ActorHit.UID); if (!a->isInUse) break; ActorTakeHit(a, e.u.ActorHit.Special); if (e.u.ActorHit.Power > 0) { DamageActor( a, e.u.ActorHit.Power, e.u.ActorHit.HitterPlayerUID); if (e.u.ActorHit.PlayerUID >= 0) { HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_HEALTH, e.u.ActorHit.PlayerUID, -e.u.ActorHit.Power); } AddBloodSplatter( a->Pos, e.u.ActorHit.Power, Net2Vec2i(e.u.ActorHit.Vel)); } } break; case GAME_EVENT_TRIGGER: { const Tile *t = MapGetTile(&gMap, Net2Vec2i(e.u.TriggerEvent.Tile)); CA_FOREACH(Trigger *, tp, t->triggers) if ((*tp)->id == (int)e.u.TriggerEvent.ID) { TriggerActivate(*tp, &gMap.triggers); break; } CA_FOREACH_END() } break; case GAME_EVENT_EXPLORE_TILES: // Process runs of explored tiles for (int i = 0; i < (int)e.u.ExploreTiles.Runs_count; i++) { Vec2i tile = Net2Vec2i(e.u.ExploreTiles.Runs[i].Tile); for (int j = 0; j < e.u.ExploreTiles.Runs[i].Run; j++) { MapMarkAsVisited(&gMap, tile); tile.x++; if (tile.x == gMap.Size.x) { tile.x = 0; tile.y++; } } } break; case GAME_EVENT_RESCUE_CHARACTER: { TActor *a = ActorGetByUID(e.u.Rescue.UID); if (!a->isInUse) break; a->flags &= ~FLAGS_PRISONER; SoundPlayAt( &gSoundDevice, StrSound("rescue"), Vec2iFull2Real(a->Pos)); } break; case GAME_EVENT_OBJECTIVE_UPDATE: { ObjectiveDef *o = CArrayGet( &gMission.Objectives, e.u.ObjectiveUpdate.ObjectiveId); o->done += e.u.ObjectiveUpdate.Count; // Display a text update effect for the objective HUDAddUpdate( &camera->HUD, NUMBER_UPDATE_OBJECTIVE, e.u.ObjectiveUpdate.ObjectiveId, e.u.ObjectiveUpdate.Count); MissionSetMessageIfComplete(&gMission); } break; case GAME_EVENT_ADD_KEYS: gMission.KeyFlags |= e.u.AddKeys.KeyFlags; SoundPlayAt( &gSoundDevice, gSoundDevice.keySound, Net2Vec2i(e.u.AddKeys.Pos)); // Clear cache since we may now have new paths PathCacheClear(&gPathCache); break; case GAME_EVENT_MISSION_COMPLETE: if (e.u.MissionComplete.ShowMsg) { HUDDisplayMessage(&camera->HUD, "Mission complete", -1); } camera->HUD.showExit = true; MapShowExitArea(&gMap); break; case GAME_EVENT_MISSION_INCOMPLETE: gMission.state = MISSION_STATE_PLAY; break; case GAME_EVENT_MISSION_PICKUP: gMission.state = MISSION_STATE_PICKUP; gMission.pickupTime = gMission.time; SoundPlay(&gSoundDevice, StrSound("whistle")); break; case GAME_EVENT_MISSION_END: gMission.isDone = true; break; default: assert(0 && "unknown game event"); break; } }