static void DrawEditorTiles(DrawBuffer *b, Vec2i offset) { Vec2i pos; Tile *tile = &b->tiles[0][0]; pos.y = b->dy + offset.y; for (int y = 0; y < Y_TILES; y++, pos.y += TILE_HEIGHT) { pos.x = b->dx + offset.x; for (int x = 0; x < b->Size.x; x++, tile++, pos.x += TILE_WIDTH) { if (gMission.missionData->Type == MAPTYPE_STATIC) { Vec2i start = gMission.missionData->u.Static.Start; if (!Vec2iIsZero(start) && Vec2iEqual(start, Vec2iNew(x + b->xStart, y + b->yStart))) { // mission start BlitMasked( &gGraphicsDevice, PicManagerGetPic(&gPicManager, "editor/start"), pos, colorWhite, 1); } } } tile += X_TILES - b->Size.x; } }
void UIButtonSetPic(UIObject *o, Pic *pic) { assert(o->Type == UITYPE_BUTTON && "invalid UI type"); o->u.Button.Pic = pic; if (Vec2iIsZero(o->Size)) { o->Size = o->u.Button.Pic->size; } }
Vec2i PlacePlayer( Map *map, const PlayerData *p, const Vec2i firstPos, const bool pumpEvents) { NetMsgActorAdd aa = NetMsgActorAdd_init_default; aa.Id = ActorsGetFreeIndex(); aa.Health = p->Char.maxHealth; aa.PlayerId = p->playerIndex; if (IsPVP(gCampaign.Entry.Mode)) { // In a PVP mode, always place players apart aa.FullPos = PlaceActor(&gMap); } else if (gMission.missionData->Type == MAPTYPE_STATIC && !Vec2iIsZero(gMission.missionData->u.Static.Start)) { // place players near the start point Vec2i startPoint = Vec2iReal2Full(Vec2iCenterOfTile( gMission.missionData->u.Static.Start)); aa.FullPos = PlaceActorNear(map, startPoint, true); } else if (gConfig.Interface.Splitscreen == SPLITSCREEN_NEVER && !Vec2iIsZero(firstPos)) { // If never split screen, try to place players near the first player aa.FullPos = PlaceActorNear(map, firstPos, true); } else { aa.FullPos = PlaceActor(map); } GameEvent e = GameEventNew(GAME_EVENT_ACTOR_ADD); e.u.ActorAdd = aa; GameEventsEnqueue(&gGameEvents, e); if (pumpEvents) { // Process the events that actually place the players HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers); } return Vec2iNew(aa.FullPos.x, aa.FullPos.y); }
Vec2i GetWallBounceFullPos( const Vec2i startFull, const Vec2i newFull, Vec2i *velFull) { CASSERT(velFull != NULL, "need velocity for wall bouncing"); Vec2i newReal = Vec2iFull2Real(newFull); if (!ShootWall(newReal.x, newReal.y)) { return newFull; } Vec2i startRealPos = Vec2iFull2Real(startFull); Vec2i bounceFull = startFull; if (!ShootWall(startRealPos.x, newReal.y)) { bounceFull.y = newFull.y; velFull->x *= -1; } else if (!ShootWall(newReal.x, startRealPos.y)) { bounceFull.x = newFull.x; velFull->y *= -1; } else { *velFull = Vec2iScale(*velFull, -1); // Keep bouncing back if it's inside a wall // However, do not bounce more than half a tile's size if (!Vec2iIsZero(*velFull)) { Vec2i bounceReal = newReal; const int maxBounces = MAX( velFull->x / 256 / TILE_WIDTH, velFull->y / 256 / TILE_HEIGHT); for (int i = 0; i < maxBounces && MapIsRealPosIn(&gMap, bounceReal) && ShootWall(bounceReal.x, bounceReal.y); i++) { bounceFull = Vec2iAdd(bounceFull, *velFull); bounceReal = Vec2iFull2Real(bounceFull); } // If still colliding wall or outside map, // can't recover from this point; zero velocity and return if (!MapIsRealPosIn(&gMap, bounceReal) || ShootWall(bounceReal.x, bounceReal.y)) { *velFull = Vec2iZero(); return startFull; } } } return bounceFull; }
static bool TryPlacePickup(HealthPickups *h) { Vec2i size = Vec2iNew(HEALTH_W, HEALTH_H); // Attempt to place one in unexplored area for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); if (!Vec2iIsZero(v) && !MapGetTile(h->map, Vec2iToTile(v))->isVisited) { MapPlaceHealth(v); return true; } } // Attempt to place one in out-of-sight area for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); const Vec2i fullpos = Vec2iReal2Full(v); const TActor *closestPlayer = AIGetClosestPlayer(fullpos); if (!Vec2iIsZero(v) && (!closestPlayer || CHEBYSHEV_DISTANCE( fullpos.x, fullpos.y, closestPlayer->Pos.x, closestPlayer->Pos.y) >= 256 * 150)) { MapPlaceHealth(v); return true; } } // Attempt to place one anyway for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); if (!Vec2iIsZero(v)) { MapPlaceHealth(v); return true; } } return false; }
static void DrawThing(DrawBuffer *b, const TTileItem *t, const Vec2i offset) { const Vec2i picPos = Vec2iNew( t->x - b->xTop + offset.x, t->y - b->yTop + offset.y); if (!Vec2iIsZero(t->ShadowSize)) { DrawShadow(&gGraphicsDevice, picPos, t->ShadowSize); } if (t->CPicFunc) { CPicDrawContext c = t->CPicFunc(t->id); CPicDraw(b->g, &t->CPic, picPos, &c); } else if (t->getPicFunc) { Vec2i picOffset; const Pic *pic = t->getPicFunc(t->id, &picOffset); Blit(&gGraphicsDevice, pic, Vec2iAdd(picPos, picOffset)); } else if (t->kind == KIND_CHARACTER) { TActor *a = CArrayGet(&gActors, t->id); ActorPics pics = GetCharacterPicsFromActor(a); DrawActorPics(&pics, picPos); // Draw weapon indicators DrawLaserSight(&pics, a, picPos); } else { (*(t->drawFunc))(picPos, &t->drawData); } #ifdef DEBUG_DRAW_HITBOXES const int pulsePeriod = ConfigGetInt(&gConfig, "Game.FPS"); int alphaUnscaled = (gMission.time % pulsePeriod) * 255 / (pulsePeriod / 2); if (alphaUnscaled > 255) { alphaUnscaled = 255 * 2 - alphaUnscaled; } color_t color = colorPurple; color.a = (Uint8)alphaUnscaled; DrawRectangle( &gGraphicsDevice, Vec2iMinus(picPos, Vec2iScaleDiv(t->size, 2)), t->size, color, DRAW_FLAG_LINE); #endif }
static Vec2i SeekTowards( const Vec2i pos, const Vec2i vel, const double speedMin, const Vec2i targetPos, const int seekFactor) { // Compensate for bullet's velocity const Vec2i targetVel = Vec2iMinus(Vec2iMinus(targetPos, pos), vel); // Don't seek if the coordinates are too big if (abs(targetVel.x) > 10000 || abs(targetVel.y) > 10000 || Vec2iIsZero(targetVel)) { return vel; } const double targetMag = sqrt( targetVel.x*targetVel.x + targetVel.y*targetVel.y); const double magnitude = MAX(speedMin, Vec2iEqual(vel, Vec2iZero()) ? speedMin : sqrt(vel.x*vel.x + vel.y*vel.y)); const double combinedX = vel.x / magnitude * seekFactor + targetVel.x / targetMag; const double combinedY = vel.y / magnitude * seekFactor + targetVel.y / targetMag; return Vec2iNew( (int)round(combinedX * magnitude / (seekFactor + 1)), (int)round(combinedY * magnitude / (seekFactor + 1))); }
static void DrawThing(DrawBuffer *b, const TTileItem *t, const Vec2i offset) { const Vec2i picPos = Vec2iNew( t->x - b->xTop + offset.x, t->y - b->yTop + offset.y); #ifdef DEBUG_DRAW_BOUNDS Draw_Box( picPos.x - t->size.x / 2, picPos.y - t->size.y / 2, picPos.x + t->size.x / 2, picPos.y + t->size.y / 2, colorGray); #endif if (!Vec2iIsZero(t->ShadowSize)) { DrawShadow(&gGraphicsDevice, picPos, t->ShadowSize); } if (t->CPicFunc) { CPicDrawContext c = t->CPicFunc(t->id); CPicDraw(b->g, &t->CPic, picPos, &c); } else if (t->getPicFunc) { Vec2i picOffset; const Pic *pic = t->getPicFunc(t->id, &picOffset); Blit(&gGraphicsDevice, pic, Vec2iAdd(picPos, picOffset)); } else if (t->getActorPicsFunc) { DrawActorPics(t, picPos); } else { (*(t->drawFunc))(picPos, &t->drawData); } }
static void DrawRadar( GraphicsDevice *device, const TActor *p, const int scale, const int flags, const bool showExit) { Vec2i pos = Vec2iZero(); int w = device->cachedConfig.Res.x; int h = device->cachedConfig.Res.y; if (!p) { return; } // Possible map positions: // top-right (player 1 only) // top-left (player 2 only) // bottom-right (player 3 only) // bottom-left (player 4 only) // top-left-of-middle (player 1 when two players) // top-right-of-middle (player 2 when two players) // bottom-left-of-middle (player 3) // bottom-right-of-middle (player 4) // top (shared screen) if (flags & HUDFLAGS_HALF_SCREEN) { // two players pos.y = AUTOMAP_PADDING; if (flags & HUDFLAGS_PLACE_RIGHT) { // player 2 pos.x = w / 2 + AUTOMAP_PADDING; } else { // player 1 pos.x = w / 2 - AUTOMAP_SIZE - AUTOMAP_PADDING; } } else if (flags & HUDFLAGS_QUARTER_SCREEN) { // four players if (flags & HUDFLAGS_PLACE_RIGHT) { // player 2 or 4 pos.x = w / 2 + AUTOMAP_PADDING; } else { // player 1 or 3 pos.x = w / 2 - AUTOMAP_SIZE - AUTOMAP_PADDING; } if (flags & HUDFLAGS_PLACE_BOTTOM) { // player 3 or 4 pos.y = h - AUTOMAP_SIZE - AUTOMAP_PADDING; } else { // player 1 or 2 pos.y = AUTOMAP_PADDING; } } else { // one player if (flags & HUDFLAGS_PLACE_RIGHT) { // player 2 or 4 pos.x = AUTOMAP_PADDING; } else { // player 1 or 3 pos.x = w - AUTOMAP_SIZE - AUTOMAP_PADDING; } if (flags & HUDFLAGS_PLACE_BOTTOM) { // player 3 or 4 pos.y = h - AUTOMAP_SIZE - AUTOMAP_PADDING; } else { // player 1 or 2 pos.y = AUTOMAP_PADDING; } } if (!Vec2iIsZero(pos)) { Vec2i playerPos = Vec2iNew( p->tileItem.x / TILE_WIDTH, p->tileItem.y / TILE_HEIGHT); AutomapDrawRegion( &gMap, pos, Vec2iNew(AUTOMAP_SIZE, AUTOMAP_SIZE), playerPos, scale, AUTOMAP_FLAGS_MASK, showExit); } }
bool UpdateBullet(TMobileObject *obj, const int ticks) { obj->count += ticks; obj->soundLock = MAX(0, obj->soundLock - ticks); obj->specialLock = MAX(0, obj->specialLock - ticks); if (obj->count < obj->bulletClass->Delay) { return true; } if (obj->range >= 0 && obj->count > obj->range) { if (!gCampaign.IsClient) { FireGuns(obj, &obj->bulletClass->OutOfRangeGuns); } return false; } const Vec2i objPos = Vec2iNew(obj->x, obj->y); if (obj->bulletClass->SeekFactor > 0) { // Find the closest target to this bullet and steer towards it const TActor *owner = ActorGetByUID(obj->ActorUID); if (owner == NULL) { return false; } const TActor *target = AIGetClosestEnemy(objPos, owner, obj->flags); if (target && !target->dead) { for (int i = 0; i < ticks; i++) { obj->vel = SeekTowards( objPos, obj->vel, obj->bulletClass->SpeedLow, target->Pos, obj->bulletClass->SeekFactor); } } } Vec2i pos = Vec2iScale(Vec2iAdd(objPos, obj->vel), ticks); HitType hitItem = HIT_NONE; if (!gCampaign.IsClient) { hitItem = HitItem(obj, pos, obj->bulletClass->Persists); } const Vec2i realPos = Vec2iFull2Real(pos); // Falling (grenades) if (obj->bulletClass->Falling.GravityFactor != 0) { bool hasDropped = obj->z <= 0; for (int i = 0; i < ticks; i++) { obj->z += obj->dz; if (obj->z <= 0) { obj->z = 0; if (obj->bulletClass->Falling.Bounces) { obj->dz = -obj->dz / 2; } else { obj->dz = 0; } if (!hasDropped) { if (!gCampaign.IsClient) { FireGuns(obj, &obj->bulletClass->Falling.DropGuns); } } hasDropped = true; if (obj->bulletClass->Falling.DestroyOnDrop) { return false; } SoundPlayAt( &gSoundDevice, StrSound(obj->bulletClass->HitSound.Wall), realPos); } else { obj->dz -= obj->bulletClass->Falling.GravityFactor; } if (!obj->bulletClass->Falling.FallsDown) { obj->dz = MAX(0, obj->dz); } } } // Friction const bool isDiagonal = obj->vel.x != 0 && obj->vel.y != 0; int frictionComponent = isDiagonal ? (int)round(obj->bulletClass->Friction / sqrt(2)) : obj->bulletClass->Friction; for (int i = 0; i < ticks; i++) { if (obj->vel.x > 0) { obj->vel.x -= frictionComponent; } else if (obj->vel.x < 0) { obj->vel.x += frictionComponent; } if (obj->vel.y > 0) { obj->vel.y -= frictionComponent; } else if (obj->vel.y < 0) { obj->vel.y += frictionComponent; } } bool hitWall = false; if (!gCampaign.IsClient && hitItem == HIT_NONE) { hitWall = MapIsRealPosIn(&gMap, realPos) && ShootWall(realPos.x, realPos.y); } if (hitWall || hitItem != HIT_NONE) { GameEvent b = GameEventNew(GAME_EVENT_BULLET_BOUNCE); b.u.BulletBounce.UID = obj->UID; if (hitWall && !Vec2iIsZero(obj->vel)) { b.u.BulletBounce.HitType = (int)HIT_WALL; } else { b.u.BulletBounce.HitType = (int)hitItem; } bool alive = true; if ((hitWall && !obj->bulletClass->WallBounces) || ((hitItem != HIT_NONE) && obj->bulletClass->HitsObjects)) { b.u.BulletBounce.Spark = true; CASSERT(!gCampaign.IsClient, "Cannot process bounces as client"); FireGuns(obj, &obj->bulletClass->HitGuns); if (hitWall || !obj->bulletClass->Persists) { alive = false; } } b.u.BulletBounce.BouncePos = Vec2i2Net(pos); b.u.BulletBounce.BounceVel = Vec2i2Net(obj->vel); if (hitWall && !Vec2iIsZero(obj->vel)) { // Bouncing Vec2i bounceVel = obj->vel; pos = GetWallBounceFullPos(objPos, pos, &bounceVel); b.u.BulletBounce.BouncePos = Vec2i2Net(pos); b.u.BulletBounce.BounceVel = Vec2i2Net(bounceVel); obj->vel = bounceVel; } GameEventsEnqueue(&gGameEvents, b); if (!alive) { return false; } } if (!MapTryMoveTileItem(&gMap, &obj->tileItem, realPos)) { obj->count = obj->range; return false; } obj->x = pos.x; obj->y = pos.y; if (obj->bulletClass->Erratic) { for (int i = 0; i < ticks; i++) { obj->vel.x += ((rand() % 3) - 1) * 128; obj->vel.y += ((rand() % 3) - 1) * 128; } } // Proximity function, destroy // Only check proximity every now and then if (obj->bulletClass->ProximityGuns.size > 0 && !(obj->count & 3)) { if (!gCampaign.IsClient) { // Detonate the mine if there are characters in the tiles around it const Vec2i tv = Vec2iToTile(Vec2iFull2Real(pos)); Vec2i dv; for (dv.y = -1; dv.y <= 1; dv.y++) { for (dv.x = -1; dv.x <= 1; dv.x++) { const Vec2i dtv = Vec2iAdd(tv, dv); if (!MapIsTileIn(&gMap, dtv)) { continue; } if (TileHasCharacter(MapGetTile(&gMap, dtv))) { FireGuns(obj, &obj->bulletClass->ProximityGuns); return false; } } } } } return true; }
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; } }