#include "Entity.h"

#include <SDL.h>

#include "Audio.h"
#include "ExMath.h"
#include "Inventory.h"
#include "Game.h"
#include "Level.h"
#include "Log.h"
#include "Particle.h"
#include "Player.h"
#include "RPK.h"
#include "VM.h"
#include "World.h"
#include "Render.h"

int SpecialEntities[NUM_SPECIAL_DEFS];
static struct _EntityNameAssoc
{
	char name[256];
	int entityId;
} *entityNames;
static int numEntityTypes = 0;

struct ObjType *TYPES;
struct ObjState *STATES;
static Bmp_t *entitySprites;

static int swordHitSoundId;

struct EntityDef
{
	uint16_t health;
	uint16_t xp;
	uint16_t initState;
	uint16_t wallHitState;
	uint16_t hurtState;
	uint16_t deadState;
	uint16_t useState;
	uint16_t touchAction;
	uint16_t hitAction;
	uint16_t painThresh;
	float    size;
	char     spawnTile;
	uint8_t  team;
	int16_t  initialData;
	enum ParticleType
	{
		PARTICLES_SPRITE,	/* Particle colour is picked from sprite */
		PARTICLES_BLOOD,	/* Particles are red */
		PARTICLES_NONE		/* No particles on damage */
	} particleType;
};

static void AI_LoadEntities(const RPKFile *rpak)
{
	int i;
	size_t datalen;
	struct EntityDef *defs;

	datalen = RPK_GetEntryData(rpak, "entities", (void **)&defs);
	if (datalen % sizeof(struct EntityDef) != 0)
	{
		Msg_Error("Entity definition data doesn't have a round number of entity definitions.");
	}

	numEntityTypes = datalen / sizeof(struct EntityDef);
	TYPES = SDL_calloc(numEntityTypes, sizeof(struct ObjType));

	for (i = 0; i < numEntityTypes; i++)
	{
		TYPES[i].maxHealth = defs[i].health;
		TYPES[i].xpReward = defs[i].xp;
		TYPES[i].states.base = defs[i].initState;
		TYPES[i].states.wallhit = defs[i].wallHitState;
		TYPES[i].states.hurt = defs[i].hurtState;
		TYPES[i].states.die = defs[i].deadState;
		TYPES[i].states.use = defs[i].useState;
		TYPES[i].touchBytecodeOffs = defs[i].touchAction;
		TYPES[i].hitBytecodeOffs = defs[i].hitAction;
		TYPES[i].hitboxRadius = defs[i].size / 2.0f;
		TYPES[i].spawnTile = defs[i].spawnTile;
		TYPES[i].team = defs[i].team;
		TYPES[i].initialData = defs[i].initialData;
		TYPES[i].particleType = defs[i].particleType;
	}

	SDL_free(defs);
}

struct StateDef
{
	char     spritename[RPK_NAME_LENGTH];
	char     soundname[RPK_NAME_LENGTH];
	uint16_t actionoffs;
	uint16_t flags;
	uint16_t nextid;
	uint16_t delay;
};

static void AI_LoadStates(const RPKFile *rpak)
{
	int numEntries, i;
	size_t datalen;
	struct StateDef *defs;

	datalen = RPK_GetEntryData(rpak, "states", (void **)&defs);
	if (datalen % sizeof(struct StateDef) != 0)
	{
		Msg_Error("State definition data doesn't have a round number of state definitions.");
	}

	numEntries = datalen / sizeof(struct StateDef);
	STATES = SDL_calloc(numEntries, sizeof(struct ObjState));
	// TODO: No need to load the same bitmap multiple times
	entitySprites = SDL_calloc(numEntries, sizeof(Bmp_t));

	for (i = 0; i < numEntries; i++)
	{
		STATES[i].sprite = Gfx_LoadSprite(rpak, defs[i].spritename);
		STATES[i].bytecodeOffs = defs[i].actionoffs;
		STATES[i].soundId = Audio_GetIDForName(defs[i].soundname);
		STATES[i].flags = defs[i].flags;
		STATES[i].next = defs[i].nextid;
		STATES[i].delay = defs[i].delay;
	}

	SDL_free(defs);
}

static void AI_LoadEntityNames(const RPKFile *rpak)
{
	SDL_RWops *io;
	void *data;
	size_t len;
	int i;

	len = RPK_GetEntryData(rpak, "ENTNAMES", &data);
	if (len == 0)
	{
		Msg_Fatal("Missing entity name definition entry");
	}
	io = SDL_RWFromConstMem(data, len);

	entityNames = SDL_calloc(numEntityTypes, sizeof(struct _EntityNameAssoc));

	for (i = 0; i < numEntityTypes; i++)
	{
		int namelen = SDL_ReadU8(io);
		io->read(io, entityNames[i].name, 1, namelen);
		entityNames[i].name[namelen] = '\0';
		entityNames[i].entityId = SDL_ReadLE16(io);
	}

	io->close(io);
	SDL_free(data);
}

void AI_Init(const RPKFile *rpak)
{
	AI_LoadEntities(rpak);
	AI_LoadStates(rpak);
	AI_LoadEntityNames(rpak);

	Player_EntityTypeId = Obj_EntityIdByName("player");
	if (Player_EntityTypeId == -1)
	{
		Msg_Fatal("Could not find player entity type!");
	}
	Player_StateBaseOffset = TYPES[Player_EntityTypeId].states.base;

	SpecialEntities[SPECIAL_DEF_CHEST] = Obj_EntityIdByName("chest");
	SpecialEntities[SPECIAL_DEF_MIMIC] = Obj_EntityIdByName("mimic");
	SpecialEntities[SPECIAL_DEF_LANTERN] = Obj_EntityIdByName("lantern");

	swordHitSoundId = Audio_GetIDForName("SfxSwordHit");
}

void AI_Cleanup(void)
{
	SDL_free(TYPES);
	SDL_free(STATES);
	SDL_free(entityNames);
	SDL_free(entitySprites);
}

bool AI_IsOpposingTeam(int teamA, int teamB)
{
	switch (teamA)
	{
	case ENTITY_TEAM_ENEMIES:
		return teamB == ENTITY_TEAM_PLAYERS;
	case ENTITY_TEAM_PLAYERS:
		return teamB == ENTITY_TEAM_ENEMIES || teamB == ENTITY_TEAM_BOSS;
	case ENTITY_TEAM_BOSS:
		return teamB == ENTITY_TEAM_PLAYERS;
	default:
		return false;
	}
}

struct SeesTeamData
{
	float xOrig, yOrig;
	int team;
	bool foundOne;
};

static void SeesEnemy(Obj_t *obj, void *pData)
{
	struct SeesTeamData *data = (struct SeesTeamData *)pData;
	if (data->foundOne || obj->health <= 0 || !AI_IsOpposingTeam(obj->team, data->team))
		return;
	data->foundOne = Lvl_HasLineOfSight(data->xOrig, data->yOrig, obj->x, obj->y);
}

bool AI_CanSeeAnEnemy(Obj_t *obj)
{
	struct SeesTeamData data;
	data.xOrig = obj->x;
	data.yOrig = obj->y;
	data.team = obj->team;
	data.foundOne = false;

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

	return data.foundOne;
}

static void SeesAlly(Obj_t *obj, void *pData)
{
	struct SeesTeamData *data = (struct SeesTeamData *)pData;
	if (data->foundOne || obj->health <= 0 || obj->team != data->team)
		return;
	data->foundOne = Lvl_HasLineOfSight(data->xOrig, data->yOrig, obj->x, obj->y);
}

bool AI_CanSeeAnAlly(Obj_t *obj)
{
	struct SeesTeamData data;
	data.xOrig = obj->x;
	data.yOrig = obj->y;
	data.team = obj->team;
	data.foundOne = false;

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

	return data.foundOne;
}

void AI_CalcEnemyDirVector(Obj_t *obj, float *xd, float *yd)
{
	Obj_t *playerTarget;
	float dx, dy;
	float length;

	playerTarget = World_GetNearestEnemyObj(obj->team, obj->x, obj->y);

	if (obj->typeId == Player_EntityTypeId || playerTarget == NULL)
	{
		*xd = Math_Sin(obj->rot);
		*yd = Math_Cos(obj->rot);
		return;
	}

	dx = playerTarget->x - obj->x;
	dy = playerTarget->y - obj->y;

	length = Math_Sqrt(dx * dx + dy * dy);
	if (length == 0.0f)
	{
		//Msg_Warning("Attempted to calculate player dir vector with length of zero");
		*xd = 1.0f;
		*yd = 0.0f;
		return;
	}

	*xd = dx / length;
	*yd = dy / length;
}

void AI_CalcAllyDirVector(Obj_t *obj, float *xd, float *yd)
{
	Obj_t *playerTarget;
	float dx, dy;
	float length;

	playerTarget = World_GetNearestAllyObj(obj, obj->team, obj->x, obj->y);

	if (obj->typeId == Player_EntityTypeId || playerTarget == NULL)
	{
		*xd = Math_Sin(obj->rot);
		*yd = Math_Cos(obj->rot);
		return;
	}

	dx = playerTarget->x - obj->x;
	dy = playerTarget->y - obj->y;

	length = Math_Sqrt(dx * dx + dy * dy);
	if (length == 0.0f)
	{
		//Msg_Warning("Attempted to calculate ally dir vector with length of zero");
		*xd = 1.0f;
		*yd = 0.0f;
		return;
	}

	*xd = dx / length;
	*yd = dy / length;
}

void AI_OffsetDirVector(float *xd, float *yd, float offs)
{
	float angle = Math_Atan2(*xd, *yd) + offs;
	*xd = Math_Sin(angle);
	*yd = Math_Cos(angle);
}

void AI_SetMovingDir(Obj_t *obj, float speed, float accel, float xa, float ya)
{
	float speedMod = Obj_GetModifierValue(obj, STATMOD_SPEED);
	// Prevent modifier from making the movement go the opposite direction
	if (speed > 0.0f && (speed + speedMod) < 0.0f)
		speed = 0.0f;
	else
		speed = speed + speedMod;
	obj->xd = xa * speed;
	obj->yd = ya * speed;
	obj->accel = accel;
	obj->rot = Math_Atan2(xa, ya);
}

struct MeleeAtkData
{
	const Obj_t *srcobj;
	int damage;
	int numHits;
	float radius; /* Only used by AI_RadiusHit */
};

static void DoSimpleDamageOn(Obj_t *obj, void *dataPtr)
{
	struct MeleeAtkData *data = (struct MeleeAtkData *)dataPtr;
	if (!Lvl_HasLineOfSight(data->srcobj->x, data->srcobj->y, obj->x, obj->y))
		return;
	int dmgDealt = Obj_Hurt(obj, data->srcobj, data->damage);
	if (dmgDealt > 0)
		data->numHits++;
}

int AI_HitscanAttack(Obj_t *obj, int damage, int maxHits, float dir, float maxRange, bool playSound)
{
	struct MeleeAtkData data = { .srcobj = obj, .damage = damage, .numHits = 0 };
	World_HitscanObjects(OF_ALL, DoSimpleDamageOn, obj, dir, maxRange, maxHits, &data);
	if (playSound && data.numHits > 0)
	{
		World_PlayEntitySound(obj, swordHitSoundId);
	}
	return data.numHits;
}

int AI_SweepHit(Obj_t *obj, float radius, int damage, bool playSound)
{
	struct MeleeAtkData data;
	struct Rect hitZone;
	float centerX, centerY;
	float xd, yd;

	AI_CalcEnemyDirVector(obj, &xd, &yd);

	centerX = obj->x + xd;
	centerY = obj->y + yd;

	hitZone.x0 = centerX - radius;
	hitZone.x1 = centerX + radius;
	hitZone.y0 = centerY - radius;
	hitZone.y1 = centerY + radius;

	data.srcobj = obj;
	data.damage = damage;
	data.numHits = 0;

	World_IterateObjects(OF_ALL, DoSimpleDamageOn, &hitZone, &data);

	if (playSound && data.numHits > 0)
	{
		World_PlayEntitySound(obj, swordHitSoundId);
	}

	return data.numHits;
}

static void DoRadiusHitOn(Obj_t *obj, void *dataPtr)
{
	struct MeleeAtkData *data = (struct MeleeAtkData *)dataPtr;
	if (obj == data->srcobj)
		return;
	if (!Lvl_HasLineOfSight(data->srcobj->x, data->srcobj->y, obj->x, obj->y))
		return;
	float xx = obj->x - data->srcobj->x;
	float yy = obj->y - data->srcobj->y;
	float dist = Math_Sqrt(xx * xx + yy * yy);
	if (dist > data->radius)
		return;
	float distFactor = 1.0f - (dist / data->radius / 1.5f);
	int dmg = data->damage * distFactor;
	int dmgDealt = Obj_Hurt(obj, data->srcobj, dmg);
	if (dmgDealt > 0)
		data->numHits++;
}

int AI_RadiusHit(Obj_t *obj, float radius, int damage, bool playSound)
{
	struct MeleeAtkData data;
	struct Rect hitZone;

	hitZone.x0 = obj->x - radius;
	hitZone.x1 = obj->x + radius;
	hitZone.y0 = obj->y - radius;
	hitZone.y1 = obj->y + radius;

	data.srcobj = obj;
	data.damage = damage;
	data.numHits = 0;
	data.radius = radius;

	World_IterateObjects(OF_ALL, DoRadiusHitOn, &hitZone, &data);

	if (playSound && data.numHits > 0)
	{
		World_PlayEntitySound(obj, swordHitSoundId);
	}

	return data.numHits;
}

void AI_FireProjectile(Obj_t *obj, int entityId, float speed, int initialData, float angleOffs)
{
	float xd, yd;
	float xSpawn, ySpawn;

	AI_CalcEnemyDirVector(obj, &xd, &yd);
	if (angleOffs != 0.0f)
		AI_OffsetDirVector(&xd, &yd, angleOffs);

	xSpawn = obj->x;
	ySpawn = obj->y;

	// Spawn player projectiles a little further away so that they don't appear as close to the camera
	if (obj->typeId == Player_EntityTypeId)
	{
		xSpawn += xd * 0.5f;
		ySpawn += yd * 0.5f;
	}

	/* Quick hack to prevent bombs from spawning inside the player */
	if (TYPES[entityId].touchBytecodeOffs == 0)
	{
		struct Rect entityRect = {
			xSpawn - TYPES[entityId].hitboxRadius,
			ySpawn - TYPES[entityId].hitboxRadius,
			xSpawn + TYPES[entityId].hitboxRadius,
			ySpawn + TYPES[entityId].hitboxRadius,
		};

		for (int i = 0; i < WORLD_MAX_PLAYERS; i++)
		{
			player_t *player = &World_Players[i];
			if (player->entity == NULL)
				continue;

			struct Rect playerRect = {
			   player->entity->x - player->entity->type->hitboxRadius,
			   player->entity->y - player->entity->type->hitboxRadius,
			   player->entity->x + player->entity->type->hitboxRadius,
			   player->entity->y + player->entity->type->hitboxRadius,
			};

			/* Keep pushing the entity away until it no longer intersects with the player hitbox */
			while (RECT_INTERSECTS(playerRect, entityRect))
			{
				xSpawn += xd * 0.1f;
				ySpawn += yd * 0.1f;
				entityRect.x0 = xSpawn - TYPES[entityId].hitboxRadius;
				entityRect.y0 = ySpawn - TYPES[entityId].hitboxRadius;
				entityRect.x1 = xSpawn + TYPES[entityId].hitboxRadius;
				entityRect.y1 = ySpawn + TYPES[entityId].hitboxRadius;
			}
		}
	}

	Obj_t *projectile = World_NewObject(entityId, xSpawn, ySpawn);
	if (projectile == NULL)
		return;

	projectile->xd = xd * speed;
	projectile->yd = yd * speed;
	projectile->z = obj->z;
	projectile->data = initialData;
	projectile->owner = obj;
	projectile->rot = obj->rot;
	/* Decelerate non-flying entities */
	if (!(STATES[TYPES[entityId].states.base].flags & OF_FLYING))
		projectile->accel = 0.90f;

	// Make non-players face their target
	if (obj->typeId != Player_EntityTypeId)
		obj->rot = Math_Atan2(xd, yd);
}

void AI_SummonBurst(Obj_t *obj, int entityId, int count, float radius, int initialData)
{
	for (int i = 0; i < count; i++)
	{
		float xSpawn = obj->x + (Math_RandomFloat() * 2.0f - 1.0f) * radius;
		float ySpawn = obj->y + (Math_RandomFloat() * 2.0f - 1.0f) * radius;
		// Entities that would be inside walls are not spawned
		if (Lvl_IsWall((int)xSpawn, (int)ySpawn))
			continue;
		Obj_t *newEntity = World_NewObject(entityId, xSpawn, ySpawn);
		if (newEntity == NULL)
			return;
		newEntity->data = initialData;
		//particleBurst(xSpawn, ySpawn, 0.0f, 0.0f, 0.0f, 1.0f, 0x47d5e8, 20);
	}
}

int Obj_EntityIdByName(const char *name)
{
	for (int i = 0; i < numEntityTypes; i++)
	{
		if (SDL_strcmp(entityNames[i].name, name) == 0)
			return entityNames[i].entityId;
	}
	Msg_Warning("Unable to find entity with name '%s'", name);
	return -1;
}

// TODO: A global types array would be a lot cooler.
char Obj_SpawnTileForType(int type)
{
	return TYPES[type].spawnTile;
}

Obj_t Obj_Init(int type, float x, float y)
{
	Obj_t obj;
	obj.x = x;
	obj.y = y;
	obj.z = 0.0f;
	obj.xd = 0.0f;
	obj.yd = 0.0f;
	obj.zd = 0.0f;
	obj.accel = 1.0f;
	obj.rot = 0.0f;
	obj.type = &TYPES[type];
	obj.health = obj.type->maxHealth;
	obj.maxHealth = obj.type->maxHealth;
	obj.invulnerableTime = 0;
	obj.waterLerp = 0;
	obj.isSwimming = 0;
	obj.data = obj.type->initialData;
	obj.dmgsrc = NULL;
	obj.stateId = -1;
	obj.typeId = type;
	obj.owner = NULL;
	obj.defense = 0;
	obj.team = obj.type->team;
	Obj_SetState(&obj, obj.type->states.base);
	SDL_memset(obj.modifiers, 0, sizeof(obj.modifiers));
	return obj;
}

enum Axis { AXIS_X, AXIS_Y };

struct ObjCollideData
{
	Obj_t *obj;
	enum Axis axis;
	float delta;
	bool hadEntityHit;
	bool shouldStopMovement;
};

void CheckObjCollideAxis(Obj_t *other, void *data)
{
	struct ObjCollideData *collideData = (struct ObjCollideData *)data;
	Obj_t *obj = collideData->obj;
	float dd;

	if (obj == other)
		return;

	/* Entities in their initial state will not collide with their owner */
	if ((other == obj->owner && obj->stateId == obj->type->states.base)
		|| (other->owner == obj && other->stateId == other->type->states.base))
		return;

	// Check whether the object is moving towards or away from the colliding object.
	// If it's moving away, allow it to pass so it doesn't get stuck.
	if (collideData->axis == AXIS_X)
		dd = obj->x - other->x;
	else
		dd = obj->y - other->y;
	if ((dd < 0) == (collideData->delta < 0))
		return;

	if (!collideData->hadEntityHit && obj->type->touchBytecodeOffs != 0)
	{
		struct VMContext ctx;
		ctx.obj = obj;
		ctx.other = other;
		ctx.item = NULL;
		ctx.player = NULL;
		VM_RunBytecode(obj->type->touchBytecodeOffs, &ctx);
		collideData->hadEntityHit = true;
	}

	if (obj->state->flags & OF_CANBEBLOCKED)
		collideData->shouldStopMovement = true;
}

bool CheckCollisionAxis(Obj_t *obj, enum Axis axis, float delta, bool *hadWallHit, bool *hadEntityHit)
{
	struct Rect collider;
	struct ObjCollideData collideData;
	bool didCollide = false;

	if (axis == AXIS_X)
	{
		collider.x0 = (obj->x + delta) - obj->type->hitboxRadius;
		collider.x1 = (obj->x + delta) + obj->type->hitboxRadius;
		collider.y0 = obj->y - obj->type->hitboxRadius;
		collider.y1 = obj->y + obj->type->hitboxRadius;
	}
	else
	{
		collider.x0 = obj->x - obj->type->hitboxRadius;
		collider.x1 = obj->x + obj->type->hitboxRadius;
		collider.y0 = (obj->y + delta) - obj->type->hitboxRadius;
		collider.y1 = (obj->y + delta) + obj->type->hitboxRadius;
	}

	if (axis == AXIS_X)
	{
		if (delta < 0.0f)
		{
			// check top-left and bottom-left corners
			if (Lvl_IsWall((int)collider.x0, (int)collider.y0)) didCollide = *hadWallHit = true;
			if (Lvl_IsWall((int)collider.x0, (int)collider.y1)) didCollide = *hadWallHit = true;
		}
		else if (delta > 0.0f)
		{
			// check top-right and bottom-right corners
			if (Lvl_IsWall((int)collider.x1, (int)collider.y0)) didCollide = *hadWallHit = true;
			if (Lvl_IsWall((int)collider.x1, (int)collider.y1)) didCollide = *hadWallHit = true;
		}
	}
	else
	{
		if (delta < 0.0f)
		{
			// check top-left and top-right corners
			if (Lvl_IsWall((int)collider.x0, (int)collider.y0)) didCollide = *hadWallHit = true;
			if (Lvl_IsWall((int)collider.x1, (int)collider.y0)) didCollide = *hadWallHit = true;
		}
		else if (delta > 0.0f)
		{
			// check bottom-left and bottom-right corners
			if (Lvl_IsWall((int)collider.x0, (int)collider.y1)) didCollide = *hadWallHit = true;
			if (Lvl_IsWall((int)collider.x1, (int)collider.y1)) didCollide = *hadWallHit = true;
		}
	}

	collideData.obj = obj;
	collideData.axis = axis;
	collideData.delta = delta;
	collideData.hadEntityHit = *hadEntityHit;
	collideData.shouldStopMovement = didCollide;

	World_IterateObjects(OF_BLOCKMOVEMENT, CheckObjCollideAxis, &collider, &collideData);

	*hadEntityHit = collideData.hadEntityHit;
	didCollide = collideData.shouldStopMovement;

	return didCollide;
}

void Obj_Tick(Obj_t *obj, void *_unused)
{
	obj->stateTimer--;

	if (obj->invulnerableTime > 0)
	{
		obj->invulnerableTime--;
	}

	if (obj->stateTimer < 0)
	{
		Obj_SetState(obj, obj->state->next);
	}

	if (!(obj->state->flags & OF_FLYING))
	{
		char tile = TILE_GROUND;
		if (obj->x >= 0 && obj->x < LEVEL_SIZE && obj->y >= 0 && obj->y < LEVEL_SIZE)
			tile = Level.tilemap[(int)obj->x + (int)obj->y * LEVEL_SIZE];
		if (!(obj->state->flags & OF_LAVAIMMUNE))
		{
			if ((tile == TILE_HURT || tile == TILE_LAVA) && (Game.levelTime % GAME_TICKS_PER_SECOND == 0))
				Obj_Hurt(obj, NULL, 6);
		}
	}

	for (int i = 0; i < MAX_STATMODS; i++)
	{
		if (obj->modifiers[i].duration > 0)
		{
			obj->modifiers[i].duration--;
			if (obj->modifiers[i].target == STATMOD_REGEN)
			{
				int healthTime = (int)obj->modifiers[i].modifier;
				if ((obj->modifiers[i].duration % healthTime) == 0)
				{
					if (healthTime > 0)
					{
						obj->health++;
						if (obj->health > obj->maxHealth)
							obj->health = obj->maxHealth;
					}
					else
					{
						Obj_Hurt(obj, NULL, 1);
					}
				}
			}
		}
	}

	const float deltaTime = 1.0 / GAME_TICKS_PER_SECOND;

	bool didCollideX = false, didCollideY = false;
	bool wallHitX = false, wallHitY = false;
	bool didDoEntityCollide = false; // Prevents the entity touch callback from being called twice.

	float xd = obj->xd * deltaTime;
	float yd = obj->yd * deltaTime;
	float zd = obj->zd * deltaTime;

	if (!(obj->state->flags & OF_FLYING))
	{
		char tile = '.';
		if (obj->x >= 0 && obj->x < LEVEL_SIZE && obj->y >= 0 && obj->y < LEVEL_SIZE)
			tile = Level.tilemap[(int)obj->x + (int)obj->y * LEVEL_SIZE];

		if (tile == TILE_WATER || tile == TILE_LAVA)
		{
			xd *= 0.45f;
			yd *= 0.45f;
			if (obj->waterLerp <= 0)
			{
				if (!obj->isSwimming)
				{
					//particleBurst(obj->x, obj->y, obj->z, 0, 0, 0.45f, tile == TILE_WATER ? 0xff1a1af2 : 0xffef4115, 24);
					obj->waterLerp = 8;
				}
			}
			else
			{
				obj->waterLerp--;
			}

			obj->z = -((float)(8 - obj->waterLerp) / 8.0f) * 0.35f;
			obj->isSwimming = 1;
		}
		else
		{
			obj->z = 0.0f;

			if (obj->waterLerp <= 0)
			{
				if (obj->isSwimming) obj->waterLerp = 8;
			}
			else
			{
				obj->waterLerp--;
			}

			obj->z = -((float)obj->waterLerp / 8.0f) * 0.35f;
			obj->isSwimming = 0;
		}
	}

	didCollideX = CheckCollisionAxis(obj, AXIS_X, xd, &wallHitX, &didDoEntityCollide);
	didCollideY = CheckCollisionAxis(obj, AXIS_Y, yd, &wallHitY, &didDoEntityCollide);

	if (obj == World_Players[0].entity && Game.cheats & GAME_CHEAT_NOCLIP)
	{
		wallHitX = false;
		wallHitY = false;
		didCollideX = false;
		didCollideY = false;
	}

	if (wallHitX || wallHitY) Obj_SetState(obj, obj->type->states.wallhit);
	if (didCollideX) obj->xd = 0.0f;
	if (didCollideY) obj->yd = 0.0f;

	if (!didCollideX) obj->x += xd;
	if (!didCollideY) obj->y += yd;
	obj->z += zd;

	obj->xd *= obj->accel;
	obj->yd *= obj->accel;
	obj->zd *= obj->accel;
}

void Obj_Knockback(Obj_t *obj, float fromx, float fromy, int damage)
{
	float length, speed;

	float dx = obj->x - fromx;
	float dy = obj->y - fromy;

	length = Math_Sqrt(dx * dx + dy * dy);
	if (length == 0.0f)
		return;

	dx /= length;
	dy /= length;

	speed = damage / 3.2f;
	obj->xd = dx * speed;
	obj->yd = dy * speed;
	obj->accel = 0.8f;
}

#define ENTITY_INVULNERABILITY_TIME 2

void Obj_Heal(Obj_t *obj, int amt)
{
	obj->health += amt;
	if (obj->health > obj->maxHealth)
		obj->health = obj->maxHealth;
	if (obj->health < 0) /* In case 'amt' is a negative value */
		obj->health = 0;
	obj->invulnerableTime = ENTITY_INVULNERABILITY_TIME;
}

int Obj_Hurt(Obj_t *obj, const Obj_t *dmgsrc, int damage)
{
	bool fromPlayer = dmgsrc &&
		(dmgsrc->typeId == Player_EntityTypeId
			|| (dmgsrc->owner && dmgsrc->owner->typeId == Player_EntityTypeId));

	if (obj == dmgsrc) // Stop hitting yourself
		return 0;

	if (obj->type->hitBytecodeOffs > 0)
	{
		struct VMContext ctx;
		ctx.obj = obj;
		ctx.other = dmgsrc;
		ctx.item = NULL;
		ctx.player = NULL;
		VM_RunBytecode(obj->type->hitBytecodeOffs, &ctx);
	}

	if (obj == World_Players[0].entity && Game.cheats & GAME_CHEAT_GODMODE)
		return 0;

	if (obj->invulnerableTime > 0 || !(obj->state->flags & OF_CANHURT))
		return 0;

	/* Apply defense stat to damage */
	if (obj->defense > 0)
	{
		damage -= RNG_RANGE(obj->defense / 4, obj->defense);
		if (damage < 0) damage = 0;
		if (obj == World_Players[0].entity)
			World_Players[0].deathStatistics.dmgTaken += damage;
	}

	obj->health -= damage;
	obj->invulnerableTime = ENTITY_INVULNERABILITY_TIME;
	obj->dmgsrc = dmgsrc;

	if (obj->health <= 0)
	{
		Obj_SetState(obj, obj->type->states.die);
		obj->health = 0;

		if (obj->typeId == Player_EntityTypeId)
			World_OnPlayerDeath(obj);

		if (fromPlayer)
		{
			const Obj_t *playerObj = dmgsrc->typeId == Player_EntityTypeId ? dmgsrc : dmgsrc->owner;
			World_AwardKillToPlayer(playerObj, obj->type->xpReward);
		}

		if (Level.boss == obj)
		{
			Level.boss = NULL;
			Lvl_ComputeLightmap(); /* make newly revealed ladder glow */
		}
	}
	else
	{
		Obj_SetState(obj, obj->type->states.hurt);
	}

	if (dmgsrc != NULL && obj->state->flags & OF_CANKNOCKBACK)
		Obj_Knockback(obj, dmgsrc->x, dmgsrc->y, damage);

	if (fromPlayer)
		World_Players[0].deathStatistics.dmgDealt += damage;

	Bmp_t *sprite = Gfx_GetSprite(obj->state->sprite, 0.f);
	float height = sprite->h / 64.0f;
	switch (obj->type->particleType)
	{

	case PARTICLES_SPRITE:
		for (int i = 0; i < damage * 2; i++)
		{
			int randomCol = 0;
			while (randomCol == 0)
			{
				randomCol = sprite->pixels[RNG_RANGE(0, sprite->w * sprite->w)];
			}
			particleBurst(obj->x, obj->y, height, obj->xd * 0.5f, obj->yd * 0.5f, damage * 0.05f, randomCol, 1);
		}
		break;
	case PARTICLES_BLOOD:
		particleBurst(obj->x, obj->y, height, obj->xd * 0.5f, obj->yd * 0.5f, damage * 0.05f, 0xffa01010, damage * 2);
		break;
	default:
		break;
	}

	return damage;
}

void Obj_SetState(Obj_t *obj, int i)
{
	if (i == STATE_NONE)
		return;

	obj->stateId = i;
	obj->state = &STATES[i];
	obj->stateTimer = STATES[i].delay;

	if (STATES[i].bytecodeOffs != 0)
	{
		struct VMContext ctx;
		ctx.obj = obj;
		ctx.other = NULL;
		ctx.item = NULL;
		ctx.player = &World_Players[0];
		VM_RunBytecode(STATES[i].bytecodeOffs, &ctx);
	}

	if (STATES[i].soundId != SOUND_NONE)
		World_PlayEntitySound(obj, STATES[i].soundId);
}

void Obj_AddStatModifier(Obj_t *obj, enum StatModifierTarget target, float value, time duration)
{
	for (int i = 0; i < MAX_STATMODS; i++)
	{
		if (obj->modifiers[i].duration <= 0)
		{
			obj->modifiers[i].target = target;
			obj->modifiers[i].modifier = value;
			obj->modifiers[i].duration = duration;
			return;
		}
	}
}

float Obj_GetModifierValue(Obj_t *obj, enum StatModifierTarget target)
{
	float totalModifier = 0.0f;
	for (int i = 0; i < MAX_STATMODS; i++)
	{
		if (obj->modifiers[i].duration <= 0 || obj->modifiers[i].target != target)
		{
			continue;
		}
		totalModifier += obj->modifiers[i].modifier;
	}
	return totalModifier;
}
