示例#1
0
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);
	}
}
示例#2
0
文件: draw.c 项目: cxong/cdogs-sdl
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;
	}
}
示例#3
0
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);
		}
	}
}
示例#4
0
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;
			}
		}
	}
}
示例#5
0
文件: objs.c 项目: cxong/cdogs-sdl
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);
}
示例#6
0
void GraphicsConfigSetFromConfig(GraphicsConfig *c)
{
	GraphicsConfigSet(
		c,
		Vec2iNew(
			ConfigGetInt(&gConfig, "Graphics.ResolutionWidth"),
			ConfigGetInt(&gConfig, "Graphics.ResolutionHeight")),
		ConfigGetBool(&gConfig, "Graphics.Fullscreen"),
		ConfigGetInt(&gConfig, "Graphics.ScaleFactor"));
}
示例#7
0
文件: draw.c 项目: cxong/cdogs-sdl
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;
	}
}
示例#8
0
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);
}
示例#9
0
文件: grafx.c 项目: cxong/cdogs-sdl
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"));
}
示例#10
0
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);
	}
}
示例#11
0
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);
	}
}
示例#12
0
文件: draw.c 项目: NSYXin/cdogs-sdl
// 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;
}
示例#13
0
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;
	}
}
示例#14
0
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;
	}
}
示例#15
0
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);
		}
	}
}
示例#16
0
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--;
	}
}
示例#17
0
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);
}
示例#18
0
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;
	}
}
示例#19
0
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;
}
示例#20
0
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);
示例#21
0
// 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);
	}
}
示例#22
0
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);
}
示例#23
0
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);
	}
}
示例#24
0
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;
	}
}