#include "World.h"

#include "inv_gui.h"
#include "ExMath.h"
#include "Game.h"
#include "Level.h"
#include "Player.h"
#include "Render.h"
#include "LevelGen.h"
#include "Save.h"
#include "Log.h"
#include "Menus.h"

player_t World_Players[WORLD_MAX_PLAYERS];

Obj_t *objects;
unsigned char *hasobj;

sprite_t World_Sprites[WORLD_MAX_SPRITES];
sprite_t World_LastSprites[WORLD_MAX_SPRITES];

struct ItemSprite
{
	Item_t item;
	float x, y, z;
	float xa, ya, za;
};
struct ItemSprite wlditems[WORLD_MAX_ITEMS];
unsigned char hasitem[WORLD_MAX_ITEMS];

static enum { WORLD_SINGLEPLAYER, WORLD_MP_HOST, WORLD_MP_CLIENT } multiplayer;
static bool clientLoaded = false;

static Bmp_t ladderTex;
static Bmp_t grassTex;

/*
#define NUM_GRASS 512
static struct Sprite grass[NUM_GRASS];
*/
static float deathViewAngle = 0.0f;
static float timeSinceDeath = 0.0f;

void World_Init(RPKFile *rpk)
{
	objects = SDL_malloc(WORLD_MAX_ENTITIES * sizeof(Obj_t));
	hasobj = SDL_malloc(WORLD_MAX_ENTITIES);

	ladderTex = Bmp_CreateFromPAKEntry(rpk, "ladder");
	grassTex = Bmp_CreateFromPAKEntry(rpk, "grass");

	Net_Init();
}

void World_Destroy(void)
{
	Bmp_Destroy(&ladderTex);
	Bmp_Destroy(&grassTex);
	SDL_free(objects);
	SDL_free(hasobj);
	Net_Cleanup();
}

static void World_PostLevelLoad(void)
{
	Lvl_ComputeLightmap();
	Lvl_FindLadders();
	Audio_SetMusic(Level.songName);
	World_RandomizeGrass();
	clearParticles();
	EnvironmentParticles();
}

void World_LoadOrGenerateFloor(int floor, int startX, int startY)
{
	SDL_memset(Level.vismap, 0, LEVEL_SIZE * LEVEL_SIZE);
	Level.boss = NULL;
	if (!Save_LoadLevel(floor))
	{
		struct SpawnedEntity *spawnedEntities;
		int numSpawnedEntities;

		numSpawnedEntities = LvlGen_Generate(floor, startX, startY, &spawnedEntities);

		struct SpawnedEntity *end = &spawnedEntities[numSpawnedEntities];
		for (struct SpawnedEntity *entity = spawnedEntities; entity != end; entity++)
		{
			Obj_t *spawned = World_NewObject(entity->type, entity->x, entity->y);
			spawned->z = entity->z;
			if (entity->isBoss)
			{
				Level.boss = spawned;
				spawned->team = ENTITY_TEAM_BOSS;
			}
		}
	}
	World_PostLevelLoad();
}

// Game.depth should not be updated after this function is called, because the game's floor number needs to be set if a save is loaded.
// The start depth may be ignored if loading from a save file.
void World_OnNewGame(enum GameStartType type, int startDepth)
{
	Inv_Clear(&World_Players[0].inventory);
	Lvl_Reset();
	for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
		Player_Reset(&World_Players[i]);

	SDL_memset(objects, 0, WORLD_MAX_ENTITIES * sizeof(Obj_t));
	SDL_memset(World_Sprites, 0xff, sizeof(World_Sprites));
	SDL_memset(World_LastSprites, 0xff, sizeof(World_LastSprites));
	SDL_memset(hasobj, 0, WORLD_MAX_ENTITIES);

	switch (type)
	{
	case GAME_START_NEW:
	{
		int startPosX = RNG_RANGE(1, LEVEL_SIZE - 1);
		int startPosY = RNG_RANGE(1, LEVEL_SIZE - 1);
		Save_RemoveSaves();
		World_LoadOrGenerateFloor(startDepth, startPosX, startPosY);
		World_Players[0].entity = World_NewObject(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
		World_Players[0].sprite = &World_Sprites[World_Players[0].entity->id];
		Player_ApplyStatsToEntity(&World_Players[0]);
		multiplayer = WORLD_SINGLEPLAYER;
		clientLoaded = true;
	}
	break;
	case GAME_START_CONTINUE:
		if (!Save_LoadGame())
		{
			int startPosX = RNG_RANGE(1, LEVEL_SIZE - 1);
			int startPosY = RNG_RANGE(1, LEVEL_SIZE - 1);
			World_LoadOrGenerateFloor(startDepth, startPosX, startPosY);
			World_Players[0].entity = World_NewObject(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
			World_Players[0].sprite = &World_Sprites[World_Players[0].entity->id];
		}
		World_Players[0].maxMana = World_Players[0].itl * PLAYER_MANA_PER_ITL;
		Player_ApplyStatsToEntity(&World_Players[0]);
		World_PostLevelLoad();
		multiplayer = WORLD_SINGLEPLAYER;
		clientLoaded = true;
		break;
	case GAME_START_HOST:
		multiplayer = WORLD_MP_HOST;
		Netcode_Sv_Init();
		int startPosX = RNG_RANGE(1, LEVEL_SIZE - 1);
		int startPosY = RNG_RANGE(1, LEVEL_SIZE - 1);
		Save_RemoveSaves(); // TODO: This is Evil. Wicked. Nefarious. Despicable.
		World_LoadOrGenerateFloor(GAME_START_FLOOR, startPosX, startPosY);
		World_Players[0].entity = World_NewObject(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
		World_Players[0].sprite = &World_Sprites[World_Players[0].entity->id];
		Player_ApplyStatsToEntity(&World_Players[0]);
		clientLoaded = true;
		break;
	case GAME_START_JOIN:
		multiplayer = WORLD_MP_CLIENT;
		Netcode_Cl_Init();
		clientLoaded = false;
		break;
	}
}

void World_QuitGame(void)
{
	Msg_Info("Quitting world");
	if (multiplayer == WORLD_MP_HOST)
	{
		Netcode_Sv_Close();
	}
	else if (multiplayer == WORLD_MP_CLIENT)
	{
		Netcode_Cl_Close();
	}
}

// Only called when the level is changing in an existing game, not when starting a new game.
// Game.depth should refer to the currently loaded floor, while the floor parameter refers to the floor to be loaded.
void World_OnLevelChange(int floor)
{
	if (multiplayer == WORLD_MP_CLIENT)
	{
		clientLoaded = false;
		return;
	}

	if (multiplayer == WORLD_SINGLEPLAYER)
		Save_SaveGame();

	// Save player properties so they can be restored when the new player object is spawned.
	// I don't give a damn about the modifiers
	for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
	{
		player_t *player = &World_Players[i];
		if (player->entity == NULL)
		{
			player->savedHealth = -1;
			player->savedMaxHealth = -1;
		}
		else
		{
			player->savedHealth = player->entity->health;
			player->savedMaxHealth = player->entity->maxHealth;
		}
	}

	SDL_memset(objects, 0, WORLD_MAX_ENTITIES * sizeof(Obj_t));
	SDL_memset(World_Sprites, 0xff, sizeof(World_Sprites));
	SDL_memset(World_LastSprites, 0xff, sizeof(World_LastSprites));
	SDL_memset(hasobj, 0, WORLD_MAX_ENTITIES);

	int startPosX, startPosY;
	if (Game.depth > floor) // Going up
	{
		startPosX = Level.exitX;
		startPosY = Level.exitY;
	}
	else // Going down
	{
		startPosX = Level.enterX;
		startPosY = Level.enterY;
	}

	World_LoadOrGenerateFloor(floor, startPosX, startPosY);

	// Spawn host player at start pos and restore properties.
	// Properties for client players will be restored when they spawn into the world again.
	World_Players[0].entity = World_NewObject(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
	World_Players[0].entity->health = World_Players[0].savedHealth;
	World_Players[0].entity->maxHealth = World_Players[0].savedMaxHealth;
	World_Players[0].sprite = &World_Sprites[World_Players[0].entity->id];
	Player_ApplyStatsToEntity(&World_Players[0]);

	if (multiplayer == WORLD_MP_HOST)
	{
		Netcode_Sv_ChangeLevel(floor);
	}
}

void World_Tick(void)
{
	const float deltaTime = 1.0f / GAME_TICKS_PER_SECOND;

	if (multiplayer == WORLD_SINGLEPLAYER && Game.state != GAME_STATE_PLAY)
		return;

	SDL_memcpy(World_LastSprites, World_Sprites, sizeof(World_Sprites));

	if (multiplayer == WORLD_MP_HOST)
	{
		Netcode_Sv_ReadPackets();
	}
	else if (multiplayer == WORLD_MP_CLIENT)
	{
		Netcode_Cl_ReadPackets();
	}

	if (Game.state == GAME_STATE_DEAD && World_Players[0].entity->health > 0)
	{
		Game.state = GAME_STATE_PLAY;
		deathViewAngle = 0.f;
		timeSinceDeath = -1.f;
	}

	if (multiplayer != WORLD_MP_CLIENT)
	{
		for (int i = 0; i < WORLD_MAX_ENTITIES; i++)
		{
			if (!hasobj[i])
			{
				World_Sprites[i].id = -i;
				continue;
			}
			Obj_t *entity = &objects[i];
			if (objects[i].state->flags & OF_TICKER)
			{
				Obj_Tick(entity, NULL);
			}
			World_SpriteFromEntity(entity, &World_Sprites[i]);
		}

		for (int i = 0; i < WORLD_MAX_ITEMS; i++)
		{
			if (!hasitem[i])
				continue;
			struct ItemSprite *item = &wlditems[i];
			sprite_t *sprite = &World_Sprites[i + WORLD_ITEM_BASEID];

			// Collide with and bounce off of walls
			int xt = (int)(item->x + item->xa * deltaTime);
			int yt = (int)(item->y + item->ya * deltaTime);
			if (Lvl_IsWall(xt, (int)item->y))
			{
				item->ya = item->ya * -0.9f;
			}
			if (Lvl_IsWall((int)item->x, yt))
			{
				item->xa = item->xa * -0.9f;
			}

			item->x += item->xa * deltaTime;
			item->y += item->ya * deltaTime;
			item->z += item->za * deltaTime;
			item->za -= 9.81f * deltaTime;

			// Stop moving on ground hit
			if (item->z <= 0.0f)
			{
				if (Math_Abs(item->za) > 0.5f)
				{
					item->xa *= 0.5f;
					item->ya *= 0.5f;
					item->za *= -0.4f;
				}
				else
				{
					item->xa = 0.0f;
					item->ya = 0.0f;
					item->za = 0.0f;
					item->z = 0.0f;
				}
			}

			sprite->x = TOFIXED(item->x);
			sprite->y = TOFIXED(item->y);
			sprite->z = TOFIXED(item->z);
		}
	}

	for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
	{
		player_t *player = &World_Players[i];
		if (player->entity == NULL)
			continue;
		if (player->itemCooldown >= 0)
		{
			const Item_t *handItem = Inv_GetItem(&player->inventory, player->handSlot, NULL);
			if (handItem != NULL)
			{
				player->itemCooldown--;
				if (player->itemCooldown <= 0)
				{
					if (ItemTypes[handItem->type].autoFire && Game.input.keys[INPUT_KEY_ATTACK])
						Player_UseItem(player);
				}
			}
		}
	}

	if (multiplayer == WORLD_MP_HOST)
	{
		for (int i = 1; i < WORLD_MAX_PLAYERS; i++)
		{
			if (World_Players[i].entity && World_Players[i].entity->stateId == Player_StateBaseOffset)
				Player_DoMoveTick(&World_Players[i], deltaTime);
		}
	}

	if (multiplayer == WORLD_MP_HOST)
	{
		Netcode_Sv_Tick();
	}
	else if (multiplayer == WORLD_MP_CLIENT)
	{
		Netcode_Cl_Tick();
		if (clientLoaded && Game.state != GAME_STATE_DEAD && World_Players[0].entity->health <= 0)
		{
			World_OnPlayerDeath(World_Players[0].entity);
		}
	}

	Lvl_ComputeLightOverlay();
}

void World_Update(float deltaTime)
{
	if (Game.state == GAME_STATE_DEAD)
	{
		timeSinceDeath += 1.6f * deltaTime;
		deathViewAngle += 0.3f * deltaTime;
	}

	if (clientLoaded)
	{
		if (Game.state == GAME_STATE_PLAY)
		{
			World_Players[0].input = Game.input.command;
			if (World_Players[0].entity->stateId == Player_StateBaseOffset)
				Player_DoMoveTick(&World_Players[0], deltaTime);
			updateParticles(deltaTime);
		}
		Audio_UpdateListener();
	}
}

static float LerpAngle(uint16_t a, uint16_t b, float t)
{
	const int whole = UINT16_MAX;
	const int half = whole / 2;
	const int triple = whole + whole / 2;
	int diff = ((int)b - (int)a) % whole;
	int shortest = ((diff + triple) % whole) - half;
	return ANGLE2FLOAT(a) + ANGLE2FLOAT(shortest) * t;
}

void World_Render(Bmp_t *screen, float delta)
{
	if (!clientLoaded)
	{
		Bmp_Clear(screen, 0xff110000);
		Bmp_DrawText(screen, "Loading world...", 0xffffffff, 10, 10, screen->w);
		return;
	}

	/* Darken fog of war */
	for (int i = 0; i < LEVEL_SIZE * LEVEL_SIZE; i++)
	{
		if (Level.vismap[i] > 0) Level.vismap[i] = 1;
	}

	Gfx_ClearViewport();

	if (Game.state == GAME_STATE_DEAD)
	{
		sprite_t *sprite = &World_Sprites[World_Players[0].entity->id];
		float distance = 1.8f;
		float aSin = Math_Sin(deathViewAngle);
		float aCos = Math_Cos(deathViewAngle);
		float wallDist = Lvl_Raycast(TOFLOAT(sprite->x), TOFLOAT(sprite->y), aSin, aCos, false).distance;
		if (timeSinceDeath < 1.8f)
			distance = timeSinceDeath;
		if (distance > wallDist - 0.01f)
			distance = wallDist - 0.01f;
		Gfx_SetCameraPos(TOFLOAT(sprite->x) + aSin * distance, TOFLOAT(sprite->y) + aCos * distance, World_Players[0].viewBob, deathViewAngle + MATH_PI);
	}
	else
	{
		sprite_t *current = &World_Sprites[World_Players[0].entity->id];
		sprite_t *last = &World_LastSprites[World_Players[0].entity->id];
		if (Game.state == GAME_STATE_PLAY)
		{
			float x = MATH_LERP(TOFLOAT(last->x), TOFLOAT(current->x), delta);
			float y = MATH_LERP(TOFLOAT(last->y), TOFLOAT(current->y), delta);
			//float z = MATH_LERP(lastWorld_PlayerZ, World_Players[0].entity->z, delta);
			float r;
			if (multiplayer == WORLD_MP_CLIENT)
				r = LerpAngle(last->angle, current->angle, delta);
			else
				r = World_Players[0].entity->rot;
			Gfx_SetCameraPos(x, y, World_Players[0].viewBob, r);
		}
		else
		{
			Gfx_SetCameraPos(TOFLOAT(current->x), TOFLOAT(current->y), World_Players[0].viewBob, ANGLE2FLOAT(current->angle));
		}
	}

	if (Level.wallHeight < 0)
		Gfx_DrawSky();

	Gfx_DrawWalls();
	renderParticles();
	Gfx_DrawFloor();
	for (int i = 0; i < WORLD_MAX_SPRITES; i++)
	{
		sprite_t *current = &World_Sprites[i];
		sprite_t *last = &World_LastSprites[i];
		if (current->id < 0)
			continue;
		if (last->id < 0 || Game.state != GAME_STATE_PLAY)
		{
			Bmp_t *spriteTex = Gfx_GetSprite(current->sprite, ANGLE2FLOAT(current->angle));
			Gfx_DrawSprite(TOFLOAT(current->x), TOFLOAT(current->y), TOFLOAT(current->z), current->fx, spriteTex);
		}
		else
		{
			float x = MATH_LERP(TOFLOAT(last->x), TOFLOAT(current->x), delta);
			float y = MATH_LERP(TOFLOAT(last->y), TOFLOAT(current->y), delta);
			float z = MATH_LERP(TOFLOAT(last->z), TOFLOAT(current->z), delta);
			float r = LerpAngle(last->angle, current->angle, delta);
			Bmp_t *spriteTex = Gfx_GetSprite(last->sprite, r);
			Gfx_DrawSprite(x, y, z, last->fx, spriteTex);
		}
	}

	/*
	if (Level.flags & LEVEL_HASGRASS)
	{
		for (int i = 0; i < NUM_GRASS; i++)
		{
			sprite_t *spr = &grass[i];
			Gfx_DrawSprite(spr->x, spr->y, spr->z, SPRITEFX_NONE, &grassTex);
		}
	}
	*/

	if (!Level.boss && Level.flags & LEVEL_HASLADDERDOWN)
	{
		Gfx_DrawSprite(Level.exitX + 0.5f, Level.exitY + 0.5f, 0.0f, SPRITEFX_NONE, &ladderTex);
	}

	if (Game.depth > 1 && Level.flags & LEVEL_HASLADDERUP)
	{
		Gfx_DrawSprite(Level.enterX + 0.5f, Level.enterY + 0.5f, Level.wallHeight - 0.6f, SPRITEFX_NONE, &ladderTex);
	}

	if (multiplayer == WORLD_SINGLEPLAYER)
	{
		if (World_Players[0].entity->health <= 0 && Game.activeMenu == NULL)
		{
			Menu_Dead.layoutProc(&Menu_Dead, screen); /* Force a relayout of the death screen to update text */
			Game_SetMenu(&Menu_Dead);
		}
	}
}

static void ChooseSpawnPos(int playerNum, int *spawnX, int *spawnY)
{
	static const int offsets[WORLD_MAX_PLAYERS][2] = {
		{ -1, -1, },
		{ -1, 0, },
		{ -1, 1, },
		{ 0, -1, },
		{ 0, 1, },
		{ 1, -1, },
		{ 1, 0, },
		{ 1, 1, },
	};
	if (multiplayer == WORLD_SINGLEPLAYER)
	{
		*spawnX = Level.enterX;
		*spawnY = Level.enterY;
	}
	else
	{
		*spawnX = Level.enterX + offsets[playerNum][0];
		*spawnY = Level.enterY + offsets[playerNum][1];
	}
}

int World_NumPlayers(void)
{
	int result = 0;
	for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
	{
		if (World_Players[i].entity != NULL)
			result++;
	}
	return result;
}

int World_SpawnPlayer(int num, int entityId)
{
	int startPosX, startPosY;
	Obj_t *obj;
	player_t *player = &World_Players[num];

	ChooseSpawnPos(num, &startPosX, &startPosY);

	if (entityId < 0)
	{
		obj = World_NewObject(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
		entityId = obj->id;
	}
	else
	{
		hasobj[entityId] = 1;
		World_Sprites[entityId].id = entityId;
		World_Sprites[entityId].sprite = 0; // TODO: why should i

		obj = &objects[entityId];
		*obj = Obj_Init(Player_EntityTypeId, (float)startPosX + 0.5f, (float)startPosY + 0.5f);
		obj->id = entityId;
	}

	if (player->savedHealth == -1)
	{
		// No saved health, this player is joining for the first time.
		Msg_Info("%d joined world with entity id %d", num, entityId);
		Player_Reset(player);
		player->entity = obj;
		player->sprite = &World_Sprites[obj->id];
	}
	else
	{
		// Health was saved from the previous floor, load the player's properties from before.
		Msg_Info("%d followed to next level with entity id %d", num, entityId);
		player->entity = obj;
		player->entity->health = player->savedHealth;
		player->entity->maxHealth = player->savedMaxHealth;
		player->sprite = &World_Sprites[obj->id];
		Player_ApplyStatsToEntity(player);
	}

	// Now's a good time to compute the lightmap, since all the sprites should be loaded by now.
	World_PostLevelLoad();

	clientLoaded = true;
	return entityId;
}

void World_PlayerButtonPress(int playerId, int button)
{
	player_t *player = &World_Players[playerId];

	if (multiplayer == WORLD_MP_CLIENT && playerId == 0)
	{
		Netcode_Cl_SendButton(button);
		return;
	}

	switch (button)
	{
	case INPUT_KEY_ATTACK:
		if (player->entity->stateId == Player_StateBaseOffset)
			Player_UseItem(player);
		break;
	case INPUT_KEY_USE:
		if (!World_UseEntities(player))
		{
			World_PickUpItems(player);
			Player_UseDoors(player->entity->x, player->entity->y, player->entity->rot);
		}
		break;
	case INPUT_KEY_DROP:
	{
		int slot;
		Item_t *item = Inv_GetItem(&player->inventory, player->handSlot, &slot);
		if (item != NULL)
		{
			World_DropItem(*item, player->entity->x, player->entity->y, 0.5f, player->entity->rot);
			Inv_ClearItem(&player->inventory, slot);
		}
	}
	break;
	case INPUT_KEY_HOTBARPREV:
		player->handSlot--;
		if (player->handSlot < INV_HAND_SLOT)
			player->handSlot = INV_HAND_SLOT + 4;
		break;
	case INPUT_KEY_HOTBARNEXT:
		player->handSlot++;
		if (player->handSlot > INV_HAND_SLOT + 4)
			player->handSlot = INV_HAND_SLOT;
		break;
	case INPUT_KEY_HOTBAR1:
	case INPUT_KEY_HOTBAR2:
	case INPUT_KEY_HOTBAR3:
	case INPUT_KEY_HOTBAR4:
	case INPUT_KEY_HOTBAR5:
		player->handSlot = INV_HAND_SLOT + button - INPUT_KEY_HOTBAR1;
		break;
	default:
		break;
	}
}

void World_AwardKillToPlayer(const Obj_t *playerEntity, int xpReward)
{
	player_t *player = NULL;
	if (multiplayer == WORLD_SINGLEPLAYER)
	{
		player = &World_Players[0];
	}
	else
	{
		for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
		{
			if (World_Players[i].entity == playerEntity)
			{
				player = &World_Players[i];
				break;
			}
		}
		if (player == NULL)
		{
			Msg_Warning("Attempted to award XP to nonexistent player");
			return;
		}
	}
	player->deathStatistics.kills++;
	Player_AwardXP(player, xpReward);
}

void World_OnPlayerDeath(Obj_t *playerEntity)
{
	playerEntity->xd = 0;
	playerEntity->yd = 0;

	if (playerEntity == World_Players[0].entity || multiplayer != WORLD_MP_HOST)
	{
		if (multiplayer == WORLD_SINGLEPLAYER)
			Save_RemoveSaves();

		Game.state = GAME_STATE_DEAD;
		Game.mapOpen = false;
		Game.inventoryOpen = false;

		deathViewAngle = playerEntity->rot + MATH_PI;
		timeSinceDeath = 0.0f;
	}

	World_PlayGlobalSound(Audio_GetIDForName("SfxRovgLaugh"));
}

void World_InventoryClick(int x, int y)
{
	int interact = Inv_Click(&World_Players[0].inventory, x, y);
	if (interact != INV_INTERACT_NONE)
	{
		if (multiplayer == WORLD_MP_CLIENT)
		{
			Netcode_Cl_GuiInteract(interact);
		}
		else
		{
			Inv_Interact(&World_Players[0].inventory, &World_Players[0], interact);
		}
	}
}

void World_SpriteFromEntity(Obj_t *entity, sprite_t *sprite)
{
	sprite->id = entity->id;
	sprite->x = TOFIXED(entity->x);
	sprite->y = TOFIXED(entity->y);
	sprite->z = TOFIXED(entity->z);
	sprite->angle = ANGLE2FIXED(entity->rot);
	sprite->sprite = entity->state->sprite;
	sprite->fx = SPRITEFX_NONE;
	if (entity->state->flags & OF_GLOW || entity->state->flags & OF_SHINE)
		sprite->fx = SPRITEFX_GLOW;
	if (entity->state->flags & OF_INVERT)
		sprite->fx = SPRITEFX_INVERT;
	if (entity->state->flags & OF_BLUR)
		sprite->fx = SPRITEFX_BLUR;
	if (entity->invulnerableTime > 0)
		sprite->fx = SPRITEFX_DMGRED;
	if (entity->state->flags & OF_GLOW)
		sprite->light = entity->state->flags & OF_TICKER ? SPRITE_LIGHT_DYNAMIC : SPRITE_LIGHT_STATIC;
	else
		sprite->light = SPRITE_LIGHT_NONE;
}

Obj_t *World_NewObject(int type, float x, float y)
{
	int i;
	for (i = 0; i < WORLD_MAX_ENTITIES; i++)
	{
		if (hasobj[i])
			continue;
		hasobj[i] = 1;

		Obj_t *obj = &objects[i];
		*obj = Obj_Init(type, x, y);
		obj->id = i;

		World_SpriteFromEntity(obj, &World_Sprites[i]);
		return obj;
	}
	return NULL;
}

void World_RemoveObject(Obj_t *obj)
{
	hasobj[obj->id] = 0;
	if (obj == Level.boss)
		Level.boss = NULL;
	World_Sprites[obj->id].id = -obj->id;
}

void World_IterateObjects(enum ObjFlags predicate, IterateObjFunc_t callback, struct Rect *bounds, void *data)
{
	int i;
	for (i = 0; i < WORLD_MAX_ENTITIES; i++)
	{
		if (!hasobj[i])
			continue;
		Obj_t *obj = &objects[i];

		if (predicate != OF_ALL && !(obj->state->flags & predicate))
			continue;

		if (bounds != NULL)
		{
			struct Rect r;
			r.x0 = obj->x - obj->type->hitboxRadius;
			r.x1 = obj->x + obj->type->hitboxRadius;
			r.y0 = obj->y - obj->type->hitboxRadius;
			r.y1 = obj->y + obj->type->hitboxRadius;
			if (RECT_INTERSECTS(*bounds, r))
				callback(obj, data);;
		}
		else
		{
			callback(obj, data);
		}
	}
}

struct FindTeamData // TODO: merge FindAllyObj & FindEnemyObj functions
{
	float nearestDistSqr;
	Obj_t *sourceObj;
	Obj_t *nearestObj;
	int team;
	float xOrig, yOrig;
};

static void FindEnemyObj(Obj_t *obj, void *pData)
{
	struct FindTeamData *data = (struct FindTeamData *)pData;
	float xx, yy, dd;

	if ((obj->state && obj->state->flags & OF_NOTARGET) || !AI_IsOpposingTeam(obj->team, data->team))
		return;

	xx = obj->x - data->xOrig;
	yy = obj->y - data->yOrig;
	dd = Math_Abs(xx * xx + yy * yy);
	if (dd < data->nearestDistSqr)
	{
		data->nearestDistSqr = dd;
		data->nearestObj = obj;
	}
}

static void FindAllyObj(Obj_t *obj, void *pData)
{
	struct FindTeamData *data = (struct FindTeamData *)pData;
	float xx, yy, dd;

	if (obj == data->sourceObj || (obj->state && obj->state->flags & OF_NOTARGET) || obj->team != data->team)
		return;

	xx = obj->x - data->xOrig;
	yy = obj->y - data->yOrig;
	dd = Math_Abs(xx * xx + yy * yy);
	if (dd < data->nearestDistSqr)
	{
		data->nearestDistSqr = dd;
		data->nearestObj = obj;
	}
}

Obj_t *World_GetNearestEnemyObj(int team, float x, float y)
{
	struct FindTeamData data;
	data.nearestDistSqr = 9999.0f;
	data.nearestObj = NULL;
	data.team = team;
	data.xOrig = x;
	data.yOrig = y;

	World_IterateObjects(OF_ALL, FindEnemyObj, NULL, &data);

	return data.nearestObj;
}

Obj_t *World_GetNearestAllyObj(Obj_t *obj, int team, float x, float y)
{
	struct FindTeamData data;
	data.nearestDistSqr = 9999.0f;
	data.sourceObj = obj;
	data.nearestObj = NULL;
	data.team = team;
	data.xOrig = x;
	data.yOrig = y;

	World_IterateObjects(OF_ALL, FindAllyObj, NULL, &data);

	return data.nearestObj;
}

struct HitscanData
{
	const Obj_t *src;
	float x, y;
	int numHits;
	IterateObjFunc_t callback;
	void *data;
	Obj_t *lastHit;
};

void OnHitscanHit(Obj_t *obj, void *pdata)
{
	struct HitscanData *data = (struct HitscanData *)pdata;
	struct Rect objRect;

	if (obj == data->src || obj == data->lastHit)
		return;

	objRect.x0 = obj->x - obj->type->hitboxRadius;
	objRect.y0 = obj->y - obj->type->hitboxRadius;
	objRect.x1 = obj->x + obj->type->hitboxRadius;
	objRect.y1 = obj->y + obj->type->hitboxRadius;

	if ((data->x >= objRect.x0 && data->x <= objRect.x1) && (data->y >= objRect.y0 && data->y <= objRect.y1))
	{
		data->lastHit = obj;
		data->callback(obj, data->data);
		data->numHits++;
	}
}

void World_HitscanObjects(enum ObjFlags predicate, IterateObjFunc_t callback, const Obj_t *src, float dir, float maxDist, int maxHits, void *data)
{
	struct HitscanData dataStruct;

	float dd = 0.1f;
	float xd = Math_Sin(dir) * dd;
	float yd = Math_Cos(dir) * dd;

	float distance = 0.0f;

	dataStruct.src = src;
	dataStruct.x = src->x;
	dataStruct.y = src->y;
	dataStruct.numHits = 0;
	dataStruct.data = data;
	dataStruct.callback = callback;
	dataStruct.lastHit = NULL;

	do
	{
		dataStruct.x += xd;
		dataStruct.y += yd;
		distance += dd;
		World_IterateObjects(predicate, OnHitscanHit, NULL, &dataStruct);
	} while (dataStruct.numHits < maxHits && distance < maxDist && !Lvl_IsWall((int)dataStruct.x, (int)dataStruct.y));
}

void World_DropItem(Item_t item, float x, float y, float z, float angle)
{
	int i;
	for (i = 0; i < WORLD_MAX_ITEMS; i++)
	{
		if (hasitem[i])
			continue;
		hasitem[i] = 1;
		wlditems[i].item = item;
		wlditems[i].x = x;
		wlditems[i].y = y;
		wlditems[i].z = z;
		wlditems[i].xa = Math_Sin(angle) * 2.0f;
		wlditems[i].ya = Math_Cos(angle) * 2.0f;
		wlditems[i].za = 0.8f;
		World_Sprites[i + WORLD_ITEM_BASEID].id = i + WORLD_ITEM_BASEID;
		World_Sprites[i + WORLD_ITEM_BASEID].x = x;
		World_Sprites[i + WORLD_ITEM_BASEID].y = y;
		World_Sprites[i + WORLD_ITEM_BASEID].z = z;
		World_Sprites[i + WORLD_ITEM_BASEID].sprite = ItemTypes[item.type].worldSprite;
		World_Sprites[i + WORLD_ITEM_BASEID].fx = SPRITEFX_NONE;
		World_Sprites[i + WORLD_ITEM_BASEID].angle = 0;
		return;
	}
	// RUH ROH!!!
}

void World_RandomizeGrass(void)
{
#if 0
	//	if (!(Level.flags & LEVEL_HASGRASS))
	//		return;
	for (int i = 0; i < NUM_GRASS; i++)
	{
		float x, y;
		int tries = 0;
		do
		{
			x = Math_RandomFloat() * LEVEL_SIZE;
			y = Math_RandomFloat() * LEVEL_SIZE;
			tries++;
		} while (Level.tilemap[(int)x + (int)y * LEVEL_SIZE] != TILE_GROUND && tries < 30);
		grass[i].x = x;
		grass[i].y = y;
	}
#endif
}

static void UseObject(Obj_t *obj, void *didGrab)
{
	Obj_SetState(obj, obj->type->states.use);
	*(bool *)didGrab = true;
}

bool World_CanUseLadderUp(player_t *player)
{
	return Game.depth > 1 && Level.flags & LEVEL_HASLADDERUP && (int)(player->sprite->x >> 8) == Level.enterX && (int)(player->sprite->y >> 8) == Level.enterY;
}

bool World_CanUseLadderDown(player_t *player)
{
	return !Level.boss && Level.flags & LEVEL_HASLADDERDOWN && (int)(player->sprite->x >> 8) == Level.exitX && (int)(player->sprite->y >> 8) == Level.exitY;
}

bool World_UseEntities(player_t *player)
{
	struct Rect pickupZone;
	bool didGrab = false;
	const float zoneRadius = 0.5f;

	float zoneCenterX = player->entity->x + Math_Sin(player->entity->rot);
	float zoneCenterY = player->entity->y + Math_Cos(player->entity->rot);

	pickupZone.x0 = zoneCenterX - zoneRadius;
	pickupZone.x1 = zoneCenterX + zoneRadius;
	pickupZone.y0 = zoneCenterY - zoneRadius;
	pickupZone.y1 = zoneCenterY + zoneRadius;

	if (World_CanUseLadderDown(player))
	{
		Game_ChangeFloor(Game.depth + 1, -1, -1);
		return true;
	}
	else if (World_CanUseLadderUp(player))
	{
		Game_ChangeFloor(Game.depth - 1, -1, -1);
		return true;
	}

	// TODO: Only affect the first matching object
	World_IterateObjects(OF_CANUSE, UseObject, &pickupZone, &didGrab);

	return didGrab;
}

void World_PickUpItems(player_t *player)
{
	struct Rect pickupZone;
	const float zoneRadius = 0.5f;
	int i;

	float zoneCenterX = player->entity->x + Math_Sin(player->entity->rot);
	float zoneCenterY = player->entity->y + Math_Cos(player->entity->rot);

	pickupZone.x0 = zoneCenterX - zoneRadius;
	pickupZone.x1 = zoneCenterX + zoneRadius;
	pickupZone.y0 = zoneCenterY - zoneRadius;
	pickupZone.y1 = zoneCenterY + zoneRadius;

	for (i = 0; i < WORLD_MAX_ITEMS; i++)
	{
		struct Rect itemBounds;
		const float itemRadius = 0.25f;
		if (!hasitem[i])
			continue;

		struct ItemSprite *item = &wlditems[i];
		itemBounds.x0 = item->x - itemRadius;
		itemBounds.x1 = item->x + itemRadius;
		itemBounds.y0 = item->y - itemRadius;
		itemBounds.y1 = item->y + itemRadius;

		if (RECT_INTERSECTS(pickupZone, itemBounds))
		{
			if (Inv_AutoPlace(&player->inventory, item->item))
			{
				static int pickupSound = 0;
				if (!pickupSound)
					pickupSound = Audio_GetIDForName("SfxItemPickup");
				hasitem[i] = 0;
				World_Sprites[i + WORLD_MAX_ENTITIES].id = -1;
				player->deathStatistics.items++;
				Audio_PlaySound(pickupSound);
			}
			else
			{
				Game_ShowStatusMsg("You don't have room for this item");
			}
			return;
		}
	}
}

void World_PlayGlobalSound(int soundId)
{
	if (multiplayer == WORLD_MP_HOST)
		Netcode_Sv_PlaySound(soundId);
	Audio_PlaySound(soundId);
}

void World_PlayTileSound(int x, int y, int soundId)
{
	float srcx = (float)x + 0.5f;
	float srcy = (float)y + 0.5f;
	if (multiplayer == WORLD_MP_HOST)
		Netcode_Sv_PlaySound3D(soundId, -1, srcx, srcy, 0.f, 0.f);
	Audio_PlaySound3D(soundId, srcx, srcy, 0.f, 0.f);
}

void World_PlayEntitySound(Obj_t *source, int soundId)
{
	if (multiplayer == WORLD_MP_HOST)
		Netcode_Sv_PlaySound3D(soundId, source->id, source->x, source->y, source->xd, source->yd);
	if (source == World_Players[0].entity)
		Audio_PlaySound(soundId);
	else
		Audio_PlaySound3D(soundId, source->x, source->y, source->xd, source->yd);
}

void World_SetTile(int x, int y, char tile)
{
	Level.tilemap[x + y * LEVEL_SIZE] = tile;
	if (multiplayer == WORLD_MP_HOST)
	{
		Netcode_Sv_SetTile(x, y, tile);
	}
}
