Example #1
0
// Set tile properties for a map tile, such as picture to use
static void MapSetupTile(Map *map, const Vec2i pos, const Mission *m)
{
	const int floor = m->FloorStyle % FLOOR_STYLE_COUNT;
	const int wall = m->WallStyle % WALL_STYLE_COUNT;
	const int room = m->RoomStyle % ROOM_STYLE_COUNT;
	Tile *tAbove = MapGetTile(map, Vec2iNew(pos.x, pos.y - 1));
	bool canSeeTileAbove = !(tAbove != NULL && !TileCanSee(tAbove));
	Tile *t = MapGetTile(map, pos);
	if (!t)
	{
		return;
	}
	switch (IMapGet(map, pos) & MAP_MASKACCESS)
	{
	case MAP_FLOOR:
	case MAP_SQUARE:
		t->pic = PicManagerGetMaskedStylePic(
			&gPicManager, "floor", floor,
			canSeeTileAbove ? FLOOR_NORMAL : FLOOR_SHADOW,
			m->FloorMask, m->AltMask);
		if (canSeeTileAbove)
		{
			// Normal floor tiles can be replaced randomly with
			// special floor tiles such as drainage
			t->flags |= MAPTILE_IS_NORMAL_FLOOR;
		}
		break;

	case MAP_ROOM:
	case MAP_DOOR:
		t->pic = PicManagerGetMaskedStylePic(
			&gPicManager, "room", room,
			canSeeTileAbove ? ROOMFLOOR_NORMAL : ROOMFLOOR_SHADOW,
			m->RoomMask, m->AltMask);
		break;

	case MAP_WALL:
		t->pic = PicManagerGetMaskedStylePic(
			&gPicManager, "wall", wall, MapGetWallPic(map, pos),
			m->WallMask, m->AltMask);
		t->flags =
			MAPTILE_NO_WALK | MAPTILE_NO_SHOOT |
			MAPTILE_NO_SEE | MAPTILE_IS_WALL;
		break;

	case MAP_NOTHING:
		t->pic = NULL;
		t->flags =
			MAPTILE_NO_WALK | MAPTILE_IS_NOTHING;
		break;
	}
}
Example #2
0
void CollideAllItems(
	const TTileItem *item, const Vec2i pos,
	const int mask, const CollisionTeam team, const bool isPVP,
	CollideItemFunc func, void *data)
{
	const Vec2i tv = Vec2iToTile(pos);
	Vec2i dv;
	// Check collisions with all other items on this tile, in all 8 directions
	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;
			}
			CArray *tileThings = &MapGetTile(&gMap, dtv)->things;
			for (int i = 0; i < (int)tileThings->size; i++)
			{
				TTileItem *ti = ThingIdGetTileItem(CArrayGet(tileThings, i));
				// Don't collide if items are on the same team
				if (!CollisionIsOnSameTeam(ti, team, isPVP))
				{
					if (item != ti &&
						(ti->flags & mask) &&
						ItemsCollide(item, ti, pos))
					{
						func(ti, data);
					}
				}
			}
		}
	}
}
Example #3
0
static bool IsTileWalkable(Map *map, Vec2i pos)
{
    if (!IsTileWalkableOrOpenable(map, pos))
    {
        return false;
    }
    // Check if tile has a dangerous (explosive) item on it
    // For AI, we don't want to shoot it, so just walk around
    Tile *t = MapGetTile(map, pos);
    for (int i = 0; i < (int)t->things.size; i++)
    {
        ThingId *tid = CArrayGet(&t->things, i);
        // Only look for explosive objects
        if (tid->Kind != KIND_OBJECT)
        {
            continue;
        }
        TObject *o = CArrayGet(&gObjs, tid->Id);
        if (o->flags & OBJFLAG_DANGEROUS)
        {
            return false;
        }
    }
    return true;
}
Example #4
0
TTileItem *GetItemOnTileInCollision(
	TTileItem *item, Vec2i pos, int mask, CollisionTeam team, int isDogfight)
{
	Vec2i tv = Vec2iToTile(pos);
	Vec2i dv;
	if (!MapIsTileIn(&gMap, tv))
	{
		return NULL;
	}

	// Check collisions with all other items on this tile, in all 8 directions
	for (dv.y = -1; dv.y <= 1; dv.y++)
	{
		for (dv.x = -1; dv.x <= 1; dv.x++)
		{
			CArray *tileThings = &MapGetTile(&gMap, Vec2iAdd(tv, dv))->things;
			for (int i = 0; i < (int)tileThings->size; i++)
			{
				TTileItem *ti = ThingIdGetTileItem(CArrayGet(tileThings, i));
				// Don't collide if items are on the same team
				if (!CollisionIsOnSameTeam(ti, team, isDogfight))
				{
					if (item != ti &&
						(ti->flags & mask) &&
						ItemsCollide(item, ti, pos))
					{
						return ti;
					}
				}
			}
		}
	}

	return NULL;
}
Example #5
0
static void ActionRun(Action *a, CArray *mapTriggers)
{
	int i;
	switch (a->action)
	{
	case ACTION_NULL:
		return;

	case ACTION_SOUND:
		SoundPlayAt(&gSoundDevice, a->a.Sound, a->u.pos);
		break;

	case ACTION_SETTRIGGER:
		for (i = 0; i < (int)mapTriggers->size; i++)
		{
			Trigger *tr = *(Trigger **)CArrayGet(mapTriggers, i);
			if (tr->id == a->u.index)
			{
				tr->isActive = 1;
				break;
			}
		}
		break;

	case ACTION_CLEARTRIGGER:
		for (i = 0; i < (int)mapTriggers->size; i++)
		{
			Trigger *tr = *(Trigger **)CArrayGet(mapTriggers, i);
			if (tr->id == a->u.index)
			{
				tr->isActive = 0;
				break;
			}
		}
		break;

	case ACTION_CHANGETILE:
		{
			Tile *t= MapGetTile(&gMap, a->u.pos);
			t->flags = a->a.tileFlags;
			t->pic = a->tilePic;
			t->picAlt = a->tilePicAlt;
		}
		break;

	case ACTION_ACTIVATEWATCH:
		ActivateWatch(a->u.index);
		break;

	case ACTION_DEACTIVATEWATCH:
		DeactivateWatch(a->u.index);
		break;
	}
}
Example #6
0
void MapGenerateRandomExitArea(Map *map)
{
	const Tile *t = NULL;
	for (int i = 0; i < 10000 && (t == NULL ||!TileCanWalk(t)); i++)
	{
		map->ExitStart.x = (rand() % (abs(map->Size.x) - EXIT_WIDTH - 1));
		map->ExitEnd.x = map->ExitStart.x + EXIT_WIDTH + 1;
		map->ExitStart.y = (rand() % (abs(map->Size.y) - EXIT_HEIGHT - 1));
		map->ExitEnd.y = map->ExitStart.y + EXIT_HEIGHT + 1;
		// Check that the exit area is walkable
		const Vec2i center = Vec2iNew(
			(map->ExitStart.x + map->ExitEnd.x) / 2,
			(map->ExitStart.y + map->ExitEnd.y) / 2);
		t = MapGetTile(map, center);
	}
}
Example #7
0
static void DrawObjectiveCompass(
	GraphicsDevice *g, Vec2i playerPos, Rect2i r, bool showExit)
{
	// Draw exit position
	if (showExit)
	{
		DrawCompassArrow(
			g, r, MapGetExitPos(&gMap), playerPos, colorGreen, "Exit");
	}

	// Draw objectives
	Map *map = &gMap;
	Vec2i tilePos;
	for (tilePos.y = 0; tilePos.y < map->Size.y; tilePos.y++)
	{
		for (tilePos.x = 0; tilePos.x < map->Size.x; tilePos.x++)
		{
			Tile *tile = MapGetTile(map, tilePos);
			for (int i = 0; i < (int)tile->things.size; i++)
			{
				TTileItem *ti =
					ThingIdGetTileItem(CArrayGet(&tile->things, i));
				if (!(ti->flags & TILEITEM_OBJECTIVE))
				{
					continue;
				}
				int objective = ObjectiveFromTileItem(ti->flags);
				MissionObjective *mo =
					CArrayGet(&gMission.missionData->Objectives, objective);
				if (mo->Flags & OBJECTIVE_HIDDEN)
				{
					continue;
				}
				if (!(mo->Flags & OBJECTIVE_POSKNOWN) &&
					!tile->isVisited)
				{
					continue;
				}
				const ObjectiveDef *o =
					CArrayGet(&gMission.Objectives, objective);
				color_t color = o->color;
				DrawCompassArrow(
					g, r, Vec2iNew(ti->x, ti->y), playerPos, color, NULL);
			}
		}
	}
}
Example #8
0
void DrawBufferSetFromMap(
	DrawBuffer *buffer, Map *map, Vec2i origin, int width)
{
	int x, y;
	Tile *bufTile;

	buffer->Size = Vec2iNew(width, buffer->OrigSize.y);

	buffer->xTop = origin.x - TILE_WIDTH * width / 2;
	//buffer->yTop = y_origin - 100;
	buffer->yTop = origin.y - TILE_HEIGHT * buffer->OrigSize.y / 2;

	buffer->xStart = buffer->xTop / TILE_WIDTH;
	buffer->yStart = buffer->yTop / TILE_HEIGHT;
	if (buffer->xTop < 0)
	{
		buffer->xStart--;
	}
	if (buffer->yTop < 0)
	{
		buffer->yStart--;
	}

	buffer->dx = buffer->xStart * TILE_WIDTH - buffer->xTop;
	buffer->dy = buffer->yStart * TILE_HEIGHT - buffer->yTop;

	bufTile = &buffer->tiles[0][0];
	for (y = buffer->yStart; y < buffer->yStart + buffer->Size.y; y++)
	{
		for (x = buffer->xStart;
			x < buffer->xStart + buffer->Size.x;
			x++, bufTile++)
		{
			if (x >= 0 && x < map->Size.x && y >= 0 && y < map->Size.y)
			{
				*bufTile = *MapGetTile(map, Vec2iNew(x, y));
			}
			else
			{
				*bufTile = TileNone();
			}
		}
		bufTile += buffer->OrigSize.x - buffer->Size.x;
	}
}
Example #9
0
static int ConditionsMet(CArray *conditions)
{
	int i;
	for (i = 0; i < (int)conditions->size; i++)
	{
		Condition *c = CArrayGet(conditions, i);
		switch (c->condition)
		{
		case CONDITION_TILECLEAR:
			if (!TileIsClear(MapGetTile(&gMap, c->pos)))
			{
				return 0;
			}
			break;
		}
	}
	return 1;
}
Example #10
0
static bool IsTileWalkableAroundObjects(Map *map, Vec2i pos)
{
    if (!IsTileWalkableOrOpenable(map, pos))
    {
        return false;
    }
    // Check if tile has any item on it
    Tile *t = MapGetTile(map, pos);
    for (int i = 0; i < (int)t->things.size; i++)
    {
        ThingId *tid = CArrayGet(&t->things, i);
        if (tid->Kind == KIND_OBJECT)
        {
            // Check that the object is not a pickup type
            TObject *o = CArrayGet(&gObjs, tid->Id);
            if (o->Type == OBJ_NONE && !(o->tileItem.flags & TILEITEM_IS_WRECK))
            {
                return false;
            }
        }
        else if (tid->Kind == KIND_CHARACTER)
        {
            switch (gConfig.Game.AllyCollision)
            {
            case ALLYCOLLISION_NORMAL:
                return false;
            case ALLYCOLLISION_REPEL:
                // TODO: implement
                // Need to know collision team of player
                // to know if collision will result in repelling
                break;
            case ALLYCOLLISION_NONE:
                continue;
            default:
                CASSERT(false, "unknown collision type");
                break;
            }
        }
    }
    return true;
}
Example #11
0
static bool IsTileWalkableOrOpenable(Map *map, Vec2i pos)
{
    int tileFlags = MapGetTile(map, pos)->flags;
    if (!(tileFlags & MAPTILE_NO_WALK))
    {
        return true;
    }
    if (tileFlags & MAPTILE_OFFSET_PIC)
    {
        // A door; check if we can open it
        int keycard = MapGetDoorKeycardFlag(map, pos);
        if (!keycard)
        {
            // Unlocked door
            return true;
        }
        return !!(keycard & gMission.flags);
    }
    // Otherwise, we cannot walk over this tile
    return false;
}
Example #12
0
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;
}
Example #13
0
void MapSetupTilesAndWalls(Map *map, const Mission *m)
{
	// Pre-load the tile pics that this map will use
	// TODO: multiple styles and colours
	// Walls
	for (int i = 0; i < WALL_TYPES; i++)
	{
		PicManagerGenerateMaskedStylePic(
			&gPicManager, "wall", m->WallStyle, i, m->WallMask, m->AltMask);
	}
	// Floors
	for (int i = 0; i < FLOOR_TYPES; i++)
	{
		PicManagerGenerateMaskedStylePic(
			&gPicManager, "floor", m->FloorStyle, i, m->FloorMask, m->AltMask);
	}
	// Rooms
	for (int i = 0; i < ROOMFLOOR_TYPES; i++)
	{
		PicManagerGenerateMaskedStylePic(
			&gPicManager, "room", m->RoomStyle, i, m->RoomMask, m->AltMask);
	}

	Vec2i v;
	for (v.x = 0; v.x < map->Size.x; v.x++)
	{
		for (v.y = 0; v.y < map->Size.y; v.y++)
		{
			MapSetupTile(map, v, m);
		}
	}

	// Randomly change normal floor tiles to drainage tiles
	for (int i = 0; i < 50; i++)
	{
		// Make sure drain tiles aren't next to each other
		Tile *t = MapGetTile(map, Vec2iNew(
			(rand() % map->Size.x) & 0xFFFFFE,
			(rand() % map->Size.y) & 0xFFFFFE));
		if (TileIsNormalFloor(t))
		{
			TileSetAlternateFloor(t, PicManagerGetFromOld(
				&gPicManager, PIC_DRAINAGE));
			t->flags |= MAPTILE_IS_DRAINAGE;
		}
	}

	int floor = m->FloorStyle % FLOOR_STYLE_COUNT;
	// Randomly change normal floor tiles to alternative floor tiles
	for (int i = 0; i < 100; i++)
	{
		Tile *t = MapGetTile(
			map, Vec2iNew(rand() % map->Size.x, rand() % map->Size.y));
		if (TileIsNormalFloor(t))
		{
			TileSetAlternateFloor(t, PicManagerGetMaskedStylePic(
				&gPicManager, "floor", floor, FLOOR_1,
				m->FloorMask, m->AltMask));
		}
	}
	for (int i = 0; i < 150; i++)
	{
		Tile *t = MapGetTile(
			map, Vec2iNew(rand() % map->Size.x, rand() % map->Size.y));
		if (TileIsNormalFloor(t))
		{
			TileSetAlternateFloor(t, PicManagerGetMaskedStylePic(
				&gPicManager, "floor", floor, FLOOR_2,
				m->FloorMask, m->AltMask));
		}
	}
}
Example #14
0
static void HandleGameEvent(
	GameEvent *e,
	HUD *hud,
	ScreenShake *shake,
	HealthPickups *hp,
	EventHandlers *eventHandlers)
{
	switch (e->Type)
	{
		case GAME_EVENT_SCORE:
			Score(&gPlayerDatas[e->u.Score.PlayerIndex], e->u.Score.Score);
			HUDAddScoreUpdate(hud, e->u.Score.PlayerIndex, e->u.Score.Score);
			break;
		case GAME_EVENT_SOUND_AT:
			if (e->u.SoundAt.Sound)
			{
				SoundPlayAt(
					&gSoundDevice, e->u.SoundAt.Sound, e->u.SoundAt.Pos);
			}
			break;
		case GAME_EVENT_SCREEN_SHAKE:
			*shake = ScreenShakeAdd(
				*shake, e->u.ShakeAmount, gConfig.Graphics.ShakeMultiplier);
			break;
		case GAME_EVENT_SET_MESSAGE:
			HUDDisplayMessage(
				hud, e->u.SetMessage.Message, e->u.SetMessage.Ticks);
			break;
		case GAME_EVENT_GAME_START:
			if (eventHandlers->netInput.channel.state ==
				CHANNEL_STATE_CONNECTED)
			{
				NetInputSendMsg(
					&eventHandlers->netInput, SERVER_MSG_GAME_START);
			}
			break;
		case GAME_EVENT_ADD_HEALTH_PICKUP:
			MapPlaceHealth(e->u.AddPos);
			break;
		case GAME_EVENT_TAKE_HEALTH_PICKUP:
			if (IsPlayerAlive(e->u.PickupPlayer))
			{
				TActor *player = CArrayGet(
					&gActors, gPlayerIds[e->u.PickupPlayer]);
				ActorHeal(player, HEALTH_PICKUP_HEAL_AMOUNT);
				HealthPickupsRemoveOne(hp);
				HUDAddHealthUpdate(
					hud, e->u.PickupPlayer, HEALTH_PICKUP_HEAL_AMOUNT);
			}
			break;
		case GAME_EVENT_MOBILE_OBJECT_REMOVE:
			MobObjDestroy(e->u.MobileObjectRemoveId);
			break;
		case GAME_EVENT_ADD_BULLET:
			BulletAdd(
				e->u.AddBullet.Bullet,
				e->u.AddBullet.MuzzlePos, e->u.AddBullet.MuzzleHeight,
				e->u.AddBullet.Angle, e->u.AddBullet.Direction,
				e->u.AddBullet.Flags, e->u.AddBullet.PlayerIndex);
			break;
		case GAME_EVENT_ADD_MUZZLE_FLASH:
			AddMuzzleFlash(
				e->u.AddMuzzleFlash.FullPos,
				e->u.AddMuzzleFlash.MuzzleHeight,
				e->u.AddMuzzleFlash.Sprites,
				e->u.AddMuzzleFlash.Direction,
				e->u.AddMuzzleFlash.Color,
				e->u.AddMuzzleFlash.Duration);
			break;
		case GAME_EVENT_ADD_FIREBALL:
			AddFireball(e->u.AddFireball);
			break;
		case GAME_EVENT_HIT_CHARACTER:
			HitCharacter(
				e->u.HitCharacter.Flags,
				e->u.HitCharacter.PlayerIndex,
				CArrayGet(&gActors, e->u.HitCharacter.TargetId),
				e->u.HitCharacter.Special,
				e->u.HitCharacter.HasHitSound);
			break;
		case GAME_EVENT_ACTOR_IMPULSE:
			{
				TActor *a = CArrayGet(&gActors, e->u.ActorImpulse.Id);
				a->Vel = Vec2iAdd(a->Vel, e->u.ActorImpulse.Vel);
			}
			break;
		case GAME_EVENT_DAMAGE_CHARACTER:
			DamageCharacter(
				e->u.DamageCharacter.Power,
				e->u.DamageCharacter.PlayerIndex,
				CArrayGet(&gActors, e->u.DamageCharacter.TargetId));
			if (e->u.DamageCharacter.Power != 0)
			{
				HUDAddHealthUpdate(
					hud,
					e->u.DamageCharacter.TargetPlayerIndex,
					-e->u.DamageCharacter.Power);
			}
			break;
		case GAME_EVENT_TRIGGER:
			{
				const Tile *t = MapGetTile(&gMap, e->u.Trigger.TilePos);
				for (int i = 0; i < (int)t->triggers.size; i++)
				{
					Trigger **tp = CArrayGet(&t->triggers, i);
					if ((*tp)->id == e->u.Trigger.Id)
					{
						TriggerActivate(*tp, &gMap.triggers);
						break;
					}
				}
			}
			break;
		case GAME_EVENT_UPDATE_OBJECTIVE:
			{
				struct Objective *o = CArrayGet(
					&gMission.Objectives, e->u.UpdateObjective.ObjectiveIndex);
				o->done += e->u.UpdateObjective.Update;
				MissionObjective *mo = CArrayGet(
					&gMission.missionData->Objectives,
					e->u.UpdateObjective.ObjectiveIndex);
				switch (mo->Type)
				{
				case OBJECTIVE_COLLECT:
					{
						GameEvent e1;
						e1.Type = GAME_EVENT_SCORE;
						e1.u.Score.PlayerIndex =
							e->u.UpdateObjective.PlayerIndex;
						e1.u.Score.Score = PICKUP_SCORE;
						HandleGameEvent(&e1, hud, shake, hp, eventHandlers);
					}
					break;
				case OBJECTIVE_DESTROY:
					{
						GameEvent e1;
						e1.Type = GAME_EVENT_SCORE;
						e1.u.Score.PlayerIndex =
							e->u.UpdateObjective.PlayerIndex;
						e1.u.Score.Score = OBJECT_SCORE;
						HandleGameEvent(&e1, hud, shake, hp, eventHandlers);
					}
					break;
				default:
					// No other special objective handling
					break;
				}
				// Display a text update effect for the objective
				HUDAddObjectiveUpdate(
					hud,
					e->u.UpdateObjective.ObjectiveIndex,
					e->u.UpdateObjective.Update);
				MissionSetMessageIfComplete(&gMission);
			}
			break;
		case GAME_EVENT_MISSION_COMPLETE:
			HUDDisplayMessage(hud, "Mission complete", -1);
			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;
	}
}
Example #15
0
static bool IsPosNoSee(void *data, Vec2i pos)
{
	const Tile *t = MapGetTile(data, Vec2iToTile(pos));
	return t != NULL && (t->flags & MAPTILE_NO_SEE);
}
Example #16
0
/*
 * Pathfind takes ownership of _Path and then releases ownership of _Path when it calls _Path's callback.
 */
int PathfindNext(struct PathData* _Path, void* _None) {
	const struct Tile* _StartTile = MapGetTile(g_GameWorld.MapRenderer, &_Path->Start);
	const struct Tile* _Goal = MapGetTile(g_GameWorld.MapRenderer, &_Path->End);
	struct Tile* _Neighbors[TILE_SIZE];
	/*
	 * TODO: Remove LinkedList from PathfindNext to remove malloc calls from PathfindNext.
	 */
	struct LinkedList _ClosedList = {0, NULL, NULL};
	struct LnkLst_Node* _Itr = NULL;
	struct BinaryHeap _OpenList = {NULL, PATHFIND_OPENSIZE, 0, PathNodeScoreCmp};
	struct PathNodeScore* _Current = NULL;

	_OpenList.Table = PathStackNext();
	BinaryHeapInsert(&_OpenList, CreatePathNodeScore(_StartTile, 0, _Path->Heuristic(_StartTile, _Goal), TILE_SIZE, NULL));
	while(_OpenList.Size > 0 && _OpenList.Size <= _OpenList.TblSz) {
		_Current = BinaryHeapTop(&_OpenList);
		LnkLstPushBack(&_ClosedList, BinaryHeapPop(&_OpenList));
		if(_Current->Node->TilePos.x == _Goal->TilePos.x && _Current->Node->TilePos.y == _Goal->TilePos.y)
			break;
		/*
		 * Fill the open list with _Tile's neighbors.
		 */
		TileGetAdjTiles(g_GameWorld.MapRenderer, _Current->Node, _Neighbors);
		for(int i = 0; i < TILE_SIZE; ++i) {
			if(_Neighbors[i] != NULL) {
				const struct LnkLst_Node* CloseItr = _ClosedList.Front;
				while(CloseItr != NULL) {
					const struct PathNodeScore* _Node = (struct PathNodeScore*)CloseItr->Data;
					if(_Node->Node == _Neighbors[i]) {
						goto loop_end;
					}
					CloseItr = CloseItr->Next;
				}
				/*
				 * Check if neighbors are already in open list.
				 */
				for(int j = 0; j < _OpenList.Size; ++j)  {
					struct PathNodeScore* _OpenTemp = (struct PathNodeScore*)_OpenList.Table[j];
					if(_OpenTemp->Node == _Neighbors[i]) {
						int _gCost = _Current->g + _Path->Heuristic(_OpenTemp->Node, _Neighbors[i]);
						if(_gCost < _OpenTemp->g) {
							_OpenTemp->g = _gCost;
							BinaryHeapIncrease(&_OpenList, j);
						}
						goto loop_end;
					}
				}
					BinaryHeapInsert(&_OpenList, CreatePathNodeScore(_Neighbors[i], _Current->g + 1, _Path->Heuristic(_Neighbors[i], _Goal), i, _Current));
					loop_end:
					continue;
			}
		}
	}
	while(_OpenList.Size > 0) {
		DestroyPathNodeScore((struct PathNodeScore*)BinaryHeapPop(&_OpenList));
	}
	PathStackFree(_OpenList.Table);
	_Itr = _ClosedList.Front;
	//ListToPath(&_ClosedList, _Path->Path);
	const struct PathNodeScore* _Temp = _Current;
	struct LinkedList _TempList = {0, NULL, NULL};
	while(_Temp != NULL) {
		LnkLstPushFront(&_TempList, (struct PathNodeScore*)_Temp);
		_Temp = _Temp->Parent;
	}
	ListToPath(&_TempList, _Path->Path);
	while(_Itr != NULL) {
		DestroyPathNodeScore((struct PathNodeScore*)_Itr->Data);
		_Itr = _Itr->Next;
	}
	LnkLstClear(&_ClosedList);
	_Path->Callback(_Path->Data, _Path->Path);
	MemPoolFree(g_PathDataPool, _Path);
	return 0;
}
Example #17
0
static bool IsPosNoSee(void *data, Vec2i pos)
{
    return MapGetTile(data, Vec2iToTile(pos))->flags & MAPTILE_NO_SEE;
}
Example #18
0
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;
}
Example #19
0
//update homie
PUBLIC RETCODE HomieUpdate(hHOMIE homie, hGMAP map)
{
	int lastIndX = homie->indX;
	int lastIndY = homie->indY;

	if((TESTFLAGS(homie->status, HOMIE_MOVING) || TESTFLAGS(homie->status, HOMIE_STUCK))
		&& !TESTFLAGS(homie->status, HOMIE_DEAD))
	{
		float orient[eMaxPt]; OBJGetOrientation(homie->obj, orient);
		orient[eX] *= -1;

		int nextIndX = homie->indX + (orient[eX] <= 0 ? (orient[eX] < 0 ? -1 : 0) : 1);
		int nextIndY = homie->indY + (orient[eZ] <= 0 ? (orient[eZ] < 0 ? -1 : 0) : 1);

		hTILE nextTile;

		//check to see if next ind is out of bound
		if(nextIndX < 0 || nextIndX >= map->indSizeX ||
			nextIndY < 0 || nextIndY >= map->indSizeY)
			return HomieStop(homie, map);

		//get next tile
		nextTile = &CELL(map->tiles, nextIndY, nextIndX, map->indSizeX); assert(nextTile);

		//check if occupied
		if(TESTFLAGS(nextTile->status, TILE_OCCUPIED))
			return HomieStop(homie, map);

		//check if next tile is a wall or etc...
		switch(nextTile->type)
		{
		case eTILE_WALL:
			return HomieStop(homie, map);
		case eTILE_BOUNCE:
			//inverse direction
			orient[eZ] *= -1;
			OBJSetOrientation(homie->obj, orient[eX],orient[eY],orient[eZ]);
			homie->indX = nextIndX;
			homie->indY = nextIndY;
			return RETCODE_SUCCESS;
		case eTILE_DOOR:
			if(nextTile->homieType == homie->type) //destroy this door (change to floor)
			{
				float tileLoc[eMaxPt]; OBJGetLoc(nextTile->obj, tileLoc);
				float floorLoc[eMaxPt];

				OBJDestroy(&nextTile->obj);

				//look for a floor tile
				hTILE theTile = MapGetTile(map, eTILE_FLOOR);

				if(theTile)
				{
					OBJGetLoc(theTile->obj, floorLoc);
					nextTile->tileID = theTile->tileID;
					nextTile->obj = OBJDuplicate(theTile->obj, OBJGetID(theTile->obj), tileLoc[eX], floorLoc[eY], tileLoc[eZ]);
				}
				else
				{
					OBJGetLoc(map->floor.obj, floorLoc);
					nextTile->tileID = map->floor.tileID;
					nextTile->obj = OBJDuplicate(map->floor.obj, OBJGetID(map->floor.obj), tileLoc[eX], floorLoc[eY], tileLoc[eZ]);
				}

				nextTile->homieType = map->floor.homieType;
				nextTile->status = map->floor.status;
				nextTile->type = map->floor.type;
			}
			else
				return HomieStop(homie, map);
			break;
		}

		//otherwise, move homie!
		OBJMovLoc(homie->obj, orient[eX], 0, orient[eZ]);
		
		if(TESTFLAGS(homie->status, HOMIE_STUCK))
		{
			SETFLAG(homie->status, HOMIE_MOVING);
			CLEARFLAG(homie->status, HOMIE_STUCK);

			hTILE tile = &CELL(map->tiles, homie->indY, homie->indX, map->indSizeX);
			CLEARFLAG(tile->status, TILE_OCCUPIED);
		}

		float homieLoc[eMaxPt]; OBJGetLoc(homie->obj, homieLoc);
		float tileLoc[eMaxPt]; OBJGetLoc(nextTile->obj, tileLoc);

		//check to see if homie has reached the next tile
		if((orient[eX] > 0 && homieLoc[eX] >= tileLoc[eX]) ||
			(orient[eX] < 0 && homieLoc[eX] <= tileLoc[eX]) ||
			(orient[eZ] > 0 && homieLoc[eZ] >= tileLoc[eZ]) ||
			(orient[eZ] < 0 && homieLoc[eZ] <= tileLoc[eZ]))
		{
			homie->indX = nextIndX;
			homie->indY = nextIndY;

			//determine course of action with specific tile
			if(nextTile->homieType == eHOMIE_NOCOLOR || 
				nextTile->homieType == homie->type)
			{
				switch(nextTile->type)
				{
				case eTILE_ARROW_N:
					OBJSetLoc(homie->obj, tileLoc[eX], homieLoc[eY], tileLoc[eZ]);
					OBJSetOrientation(homie->obj, 0, 0, -GCFGGetHomieSpd());
					return RETCODE_SUCCESS;
				case eTILE_ARROW_S:
					OBJSetLoc(homie->obj, tileLoc[eX], homieLoc[eY], tileLoc[eZ]);
					OBJSetOrientation(homie->obj, 0, 0, GCFGGetHomieSpd());
					return RETCODE_SUCCESS;
				case eTILE_ARROW_E:
					OBJSetLoc(homie->obj, tileLoc[eX], homieLoc[eY], tileLoc[eZ]);
					OBJSetOrientation(homie->obj, -GCFGGetHomieSpd(), 0, 0);
					return RETCODE_SUCCESS;
				case eTILE_ARROW_W:
					OBJSetLoc(homie->obj, tileLoc[eX], homieLoc[eY], tileLoc[eZ]);
					OBJSetOrientation(homie->obj, GCFGGetHomieSpd(), 0, 0);
					return RETCODE_SUCCESS;
				case eTILE_TELEPORT:
					{
						hTILE theTile, lastTile;
						//look for another teleport tile similar to this
						for(int row = 0; row < map->indSizeY; row++)
						{
							for(int col = 0; col < map->indSizeX; col++)
							{
								theTile = &CELL(map->tiles, row, col, map->indSizeX);

								if((col != nextIndX || row != nextIndY) 
									&& !TESTFLAGS(theTile->status, TILE_OCCUPIED)
									&& theTile->type == eTILE_TELEPORT
									&& (theTile->homieType == nextTile->homieType))
								{
									//teleport homie here
									OBJGetLoc(theTile->obj, tileLoc);
									OBJSetLoc(homie->obj, tileLoc[eX], homieLoc[eY], tileLoc[eZ]);

									lastTile = &CELL(map->tiles, lastIndY, lastIndX, map->indSizeX);
									CLEARFLAG(lastTile->status, TILE_OCCUPIED);

									homie->indX = col;
									homie->indY = row;
									return RETCODE_SUCCESS;
								}
							}
						}
					}
					return RETCODE_SUCCESS;
				case eTILE_HOLE:
					{
						CLEARFLAG(homie->status, HOMIE_MOVING);
						SETFLAG(homie->status, HOMIE_DEAD);

						float tileLoc[eMaxPt]; OBJGetLoc(nextTile->obj, tileLoc);
						float floorLoc[eMaxPt];

						OBJDestroy(&nextTile->obj);

						//look for a floor tile
						hTILE theTile = MapGetTile(map, eTILE_FLOOR);

						if(theTile)
						{
							OBJGetLoc(theTile->obj, floorLoc);
							nextTile->tileID = theTile->tileID;
							nextTile->obj = OBJDuplicate(theTile->obj, OBJGetID(theTile->obj), tileLoc[eX], floorLoc[eY], tileLoc[eZ]);
						}
						else
						{
							OBJGetLoc(map->floor.obj, floorLoc);
							nextTile->tileID = map->floor.tileID;
							nextTile->obj = OBJDuplicate(map->floor.obj, OBJGetID(map->floor.obj), tileLoc[eX], floorLoc[eY], tileLoc[eZ]);
						}

						nextTile->homieType = map->floor.homieType;
						nextTile->status = map->floor.status;
						nextTile->type = map->floor.type;
					}
					return RETCODE_SUCCESS;
				}
			}
		}
	}

	return RETCODE_SUCCESS;
}
Example #20
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;
	}
}