/*
Copyright (C) 2015-2019 Parallel Realities

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "fighters.h"

static void separate(void);
static void die(void);
static void immediateDie(void);
static void spinDie(void);
static void straightDie(void);
static void simpleDie(void);
static void randomizeDart(Entity *dart);
static void randomizeDartGuns(Entity *dart);
static void loadFighterDef(char *filename);
static void loadFighterDefList(char *filename);
static Entity *getFighterDef(char *name);
static void addFighterStat(char *name);
static void incFighterStat(char *key);

static Entity defHead, *defTail;

Entity *spawnFighter(char *name, int x, int y, int side)
{
	Entity *e, *def;

	e = spawnEntity();

	def = getFighterDef(name);

	memcpy(e, def, sizeof(Entity));

	e->next = NULL;

	e->x = x;
	e->y = y;
	e->side = side;

	switch (side)
	{
		case SIDE_ALLIES:
			e->aiAggression = 2 + rand() % 2;
			if (!(e->aiFlags & AIF_FOLLOWS_PLAYER))
			{
				e->aiFlags |= AIF_MOVES_TO_PLAYER;
			}
			break;

		case SIDE_PIRATE:
			e->aiAggression = rand() % 3;
			break;

		case SIDE_PANDORAN:
			e->aiAggression = 3 + rand() % 2;
			break;

		case SIDE_TZAC:
			e->aiAggression = 1 + rand() % 3;
			break;
	}

	if (strcmp(name, "ATAF") == 0)
	{
		e->aiAggression = 4;
	}

	if (strcmp(name, "Dart") == 0)
	{
		randomizeDart(e);
	}

	if (strcmp(name, "Civilian") == 0 && rand() % 2 == 0)
	{
		e->texture = getAtlasImage("gfx/craft/civilian02.png");
	}

	if (e->aiFlags & AIF_AGGRESSIVE)
	{
		e->aiAggression = 4;
	}

	e->action = doAI;
	e->die = die;

	if (game.currentMission->challengeData.isDeathMatch)
	{
		e->side = SDL_GetTicks();
	}

	return e;
}

static void randomizeDart(Entity *dart)
{
	char texture[MAX_DESCRIPTION_LENGTH];

	if (rand() % 5 == 0)
	{
		dart->health = dart->maxHealth = 5 + (rand() % 21);
	}

	if (rand() % 5 == 0)
	{
		dart->shield = dart->maxShield = 1 + (rand() % 16);
		dart->shieldRechargeRate = 30 + (rand() % 90);
	}

	if (rand() % 5 == 0)
	{
		dart->speed = 2 + (rand() % 3);
	}

	if (rand() % 5 == 0)
	{
		dart->reloadTime = 24 + (rand() % 11);
	}

	randomizeDartGuns(dart);

	dart->missiles = rand() % 3;

	sprintf(texture, "gfx/fighters/dart0%d.png", 1 + rand() % 7);

	dart->texture = getAtlasImage(texture);
}

static void randomizeDartGuns(Entity *dart)
{
	int i;

	switch (rand() % 4)
	{
		/* Single plasma gun */
		case 0:
			dart->guns[0].type = BT_PLASMA;
			dart->guns[0].x = dart->guns[0].y = 0;

			for (i = 1 ; i < MAX_FIGHTER_GUNS ; i++)
			{
				if (dart->guns[i].type)
				{
					dart->guns[i].type = BT_NONE;
				}
			}
			break;

		/* Dual plasma guns */
		case 1:
			dart->guns[0].type = BT_PLASMA;
			dart->guns[1].type = BT_PLASMA;
			break;

		/* Triple particle guns */
		case 2:
			dart->guns[2].type = BT_PARTICLE;
			dart->guns[2].y = -10;
			break;


		/* Plasma / Laser cannons */
		case 3:
			dart->guns[0].type = BT_PLASMA;
			dart->guns[0].x = dart->guns[0].y = 0;

			dart->guns[1].type = BT_LASER;
			dart->guns[1].x = dart->guns[1].y = 0;
			break;

		/* Dual Laser cannons */
		case 4:
			dart->guns[0].type = BT_LASER;
			dart->guns[1].type = BT_LASER;
			break;
	}
}

void resetFighter(Entity *fighter)
{
	Entity *e;

	e = spawnFighter(fighter->defName, fighter->x, fighter->y, fighter->side);

	e->x += (rand() % 7500) - (rand() % 7500);
	e->y += (rand() % 7500) - (rand() % 7500);

	e->active = 0;

	if (rand() % 4)
	{
		e->aiFlags |= AIF_TARGET_FOCUS;
	}
}

void doFighter(void)
{
	if (self->alive == ALIVE_ALIVE)
	{
		if (self != player)
		{
			separate();
		}

		if (!(self->flags & EF_DISABLED))
		{
			attachRope();
		}

		if (self->thrust > 0.25)
		{
			addEngineEffect();
		}

		if (self->health <= 0)
		{
			self->health = 0;
			self->alive = ALIVE_DYING;
			self->die();

			if (self == battle.missionTarget)
			{
				battle.missionTarget = NULL;
			}
		}
		else if (self->systemPower <= 0 || (self->flags & EF_DISABLED))
		{
			self->dx *= 0.99;
			self->dy *= 0.99;
			self->thrust = 0;
			self->shield = self->maxShield = 0;
			self->action = NULL;

			if ((self->flags & EF_DISABLED) == 0)
			{
				playBattleSound(SND_POWER_DOWN, self->x, self->y);

				self->flags |= EF_DISABLED;
				self->flags |= EF_SECONDARY_TARGET;

				if (self->aiFlags & AIF_SURRENDERING)
				{
					self->aiFlags |= AIF_SURRENDERED;
					self->aiFlags &= ~AIF_SURRENDERING;
				}

				battle.stats[STAT_ENEMIES_DISABLED]++;

				updateObjective(self->name, TT_DISABLE);
				updateObjective(self->groupName, TT_DISABLE);

				if (self->side != player->side)
				{
					runScriptFunction("ENEMIES_DISABLED %d", battle.stats[STAT_ENEMIES_DISABLED]);
				}
			}
		}

		if (self->target != NULL && self->target->alive != ALIVE_ALIVE)
		{
			self->target = NULL;

			if (self != player)
			{
				self->action = doAI;
			}
		}

		if (self->aiFlags & AIF_SUSPICIOUS)
		{
			checkSuspicionLevel();
		}

		if (self->aiFlags & AIF_ZAK_SUSPICIOUS)
		{
			checkZackariaSuspicionLevel();
		}
	}

	if (self->alive == ALIVE_ESCAPED)
	{
		if (self == player && !game.currentMission->challengeData.isChallenge)
		{
			updateObjective("Player", TT_ESCAPED);

			completeMission();
		}

		if (self->side != player->side && (!(self->flags & EF_DISABLED)))
		{
			addHudMessage(colors.red, _("Mission target has escaped."));
			battle.stats[STAT_ENEMIES_ESCAPED]++;
		}

		if (strcmp(self->defName, "Civilian") == 0)
		{
			battle.stats[STAT_CIVILIANS_RESCUED]++;
		}

		/* if you did not escape under your own volition, or with the aid of a friend, you've been stolen */
		if (!self->owner || self->side == self->owner->side)
		{
			updateObjective(self->name, TT_ESCAPED);
			updateCondition(self->name, TT_ESCAPED);
		}
		else
		{
			updateObjective(self->name, TT_STOLEN);
			updateCondition(self->name, TT_STOLEN);
		}
	}

	if (self->alive == ALIVE_DEAD)
	{
		if (player->alive == ALIVE_ALIVE && self != player)
		{
			if (self->side != player->side)
			{
				if (!(self->flags & EF_NO_KILL_INC))
				{
					battle.stats[STAT_ENEMIES_KILLED]++;

					runScriptFunction("ENEMIES_KILLED %d", battle.stats[STAT_ENEMIES_KILLED]);
				}
			}
			else
			{
				if (strcmp(self->name, "Civilian") == 0)
				{
					battle.stats[STAT_CIVILIANS_KILLED]++;
					if (!battle.isEpic || game.currentMission->challengeData.isChallenge)
					{
						addHudMessage(colors.red, _("Civilian has been killed"));
					}

					runScriptFunction("CIVILIANS_KILLED %d", battle.stats[STAT_CIVILIANS_KILLED]);
				}
				else
				{
					battle.stats[STAT_ALLIES_KILLED]++;
					if (!battle.isEpic && !game.currentMission->challengeData.isChallenge)
					{
						addHudMessage(colors.red, _("Ally has been killed"));
					}

					runScriptFunction("ALLIES_KILLED %d", battle.stats[STAT_ALLIES_KILLED]);
				}
			}

			updateObjective(self->name, TT_DESTROY);
			updateObjective(self->groupName, TT_DESTROY);

			if (battle.isEpic && self->killedBy == player)
			{
				updateObjective("EPIC_PLAYER_KILLS", TT_DESTROY);
			}

			adjustObjectiveTargetValue(self->name, TT_ESCAPED, -1);

			updateCondition(self->name, TT_DESTROY);
			updateCondition(self->groupName, TT_DESTROY);

			/* don't fire if the opposing side is responsible */
			if (self->aiFlags & AIF_SURRENDERED && self->killedBy->side == player->side)
			{
				updateCondition("SURRENDERED", TT_DESTROY);
			}
		}
	}
}

static void separate(void)
{
	int angle;
	int distance;
	float dx, dy, force;
	int count;
	Entity *e, **candidates;
	int i;

	dx = dy = 0;
	count = 0;
	force = 0;

	candidates = getAllEntsInRadius(self->x, self->y, self->separationRadius, self);

	for (i = 0, e = candidates[i] ; e != NULL ; e = candidates[++i])
	{
		if ((e->flags & EF_TAKES_DAMAGE) && (!(e->flags & EF_NON_SOLID)))
		{
			distance = getDistance(e->x, e->y, self->x, self->y);

			if (distance > 0 && distance < self->separationRadius)
			{
				angle = getAngle(self->x, self->y, e->x, e->y);

				dx += sin(TO_RAIDANS(angle));
				dy += -cos(TO_RAIDANS(angle));
				force += (self->separationRadius - distance) * 0.005;

				count++;
			}
		}
	}

	if (count > 0)
	{
		dx /= count;
		dy /= count;

		dx *= force;
		dy *= force;

		self->dx -= dx;
		self->dy -= dy;
	}
}

void applyFighterThrust(void)
{
	float v;

	self->dx += sin(TO_RAIDANS(self->angle)) * 0.1;
	self->dy += -cos(TO_RAIDANS(self->angle)) * 0.1;
	self->thrust = sqrt((self->dx * self->dx) + (self->dy * self->dy));

	if (self->thrust > self->speed * self->speed)
	{
		v = (self->speed / sqrt(self->thrust));
		self->dx = v * self->dx;
		self->dy = v * self->dy;
		self->thrust = sqrt((self->dx * self->dx) + (self->dy * self->dy));
	}
}

void applyFighterBrakes(void)
{
	self->dx *= 0.95;
	self->dy *= 0.95;

	self->thrust = sqrt((self->dx * self->dx) + (self->dy * self->dy));
}

void damageFighter(Entity *e, int amount, long flags)
{
	int prevShield = e->shield;

	e->aiDamageTimer = FPS;
	e->aiDamagePerSec += amount;

	if (flags & BF_SYSTEM_DAMAGE)
	{
		if (e->shield > 0)
		{
			amount /= 2;

			e->shield -= amount;

			if (e->shield < 0)
			{
				amount = -e->shield;
			}
		}

		if (amount >= 0)
		{
			e->systemPower = MAX(0, e->systemPower - amount);

			e->systemHit = 255;

			if (e->systemPower == 0)
			{
				e->shield = e->maxShield = 0;
				e->action = NULL;
			}

			playBattleSound(SND_MAG_HIT, e->x, e->y);
		}
	}
	else if (flags & BF_SHIELD_DAMAGE)
	{
		e->shield -= amount;

		if (e->shield <= 0 && prevShield > 0)
		{
			playBattleSound(SND_SHIELD_BREAK, e->x, e->y);
			addShieldSplinterEffect(e);
			e->shield = -(FPS * 10);
		}

		e->shield = MAX(-(FPS * 10), e->shield);
	}
	else
	{
		if (e->shield > 0)
		{
			e->shield -= amount;

			if (e->shield <= 0)
			{
				e->armourHit = 255;
				e->health += e->shield;
				e->shield = 0;

				playBattleSound(SND_ARMOUR_HIT, e->x, e->y);
			}
		}
		else
		{
			e->health -= amount;
			e->armourHit = 255;

			playBattleSound(SND_ARMOUR_HIT, e->x, e->y);
		}
	}

	if (e->shield > 0)
	{
		e->shieldHit = 255;

		playBattleSound(SND_SHIELD_HIT, e->x, e->y);
	}

	/* don't allow the shield to recharge immediately after taking a hit */
	e->shieldRecharge = e->shieldRechargeRate;

	/*
	 * Sometimes run away if you take too much damage in a short space of time
	 */
	if (e->type == ET_FIGHTER && e->alive == ALIVE_ALIVE && (!(e->aiFlags & AIF_EVADE)) && e != player && e->aiDamagePerSec >= (e->maxHealth + e->maxShield) * 0.1)
	{
		if ((rand() % 10) > 7)
		{
			e->action = doAI;
			e->aiFlags |= AIF_EVADE;
			e->aiActionTime = e->aiEvadeTimer = FPS * (1 + (rand() % 3));
		}
		else
		{
			e->aiDamagePerSec = 0;
		}
	}
}

static void die(void)
{
	int n = rand() % 3;

	switch (self->deathType)
	{
		case DT_ANY:
			n = rand() % 3;
			break;
		case DT_NO_SPIN:
			n = 1 + rand() % 2;
			break;
		case DT_INSTANT:
			n = 2;
			break;
		case DT_SIMPLE:
			n = 3;
			break;
	}

	if (self == player && battle.isEpic)
	{
		n = 1;
	}

	switch (n)
	{
		case 0:
			self->action = spinDie;
			break;
		case 1:
			self->action = straightDie;
			break;
		case 2:
			self->action = immediateDie;
			break;
		case 3:
			self->action = simpleDie;
			break;
	}

	if (self->killedBy == player && (!(self->flags & EF_NO_KILL_INC)))
	{
		battle.stats[STAT_ENEMIES_KILLED_PLAYER]++;

		if (self->flags & EF_COMMON_FIGHTER)
		{
			incFighterStat(self->defName);
		}

		if (battle.isEpic && player->flags & EF_COMMON_FIGHTER)
		{
			battle.stats[STAT_EPIC_KILL_STREAK]++;
		}
	}

	if (self->flags & EF_DROPS_ITEMS)
	{
		addRandomItem(self->x, self->y);
	}
}

static void immediateDie(void)
{
	self->alive = ALIVE_DEAD;
	addSmallExplosion();
	playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y);
	addDebris(self->x, self->y, 3 + rand() % 6);
}

static void spinDie(void)
{
	self->health--;
	self->thinkTime = 0;
	self->armourHit = 0;
	self->shieldHit = 0;
	self->systemHit = 0;

	self->angle += 8;

	if (rand() % 2 == 0)
	{
		addSmallFighterExplosion();
	}

	if (self->health <= -(FPS * 1.5))
	{
		self->alive = ALIVE_DEAD;
		addSmallExplosion();
		playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y);
		addDebris(self->x, self->y, 3 + rand() % 6);
	}
}

static void straightDie(void)
{
	self->health--;
	self->thinkTime = 0;
	self->armourHit = 0;
	self->shieldHit = 0;
	self->systemHit = 0;

	if (rand() % 2 == 0)
	{
		addSmallFighterExplosion();
	}

	if (self->health <= -(FPS * 1.5))
	{
		self->alive = ALIVE_DEAD;
		addSmallExplosion();
		playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y);
		addDebris(self->x, self->y, 3 + rand() % 6);
	}
}

static void simpleDie(void)
{
	self->alive = ALIVE_DEAD;
	addSmallExplosion();
	playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y);
}

void retreatEnemies(void)
{
	Entity *e;

	for (e = battle.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_FIGHTER && e->side != player->side)
		{
			e->flags |= EF_RETREATING;

			e->aiFlags |= AIF_AVOIDS_COMBAT;
			e->aiFlags |= AIF_UNLIMITED_RANGE;
			e->aiFlags &= ~AIF_MOVES_TO_LEADER;
			e->aiFlags &= ~AIF_WANDERS;

			e->aiActionTime = MIN(e->aiActionTime, FPS);

			if (!game.currentMission->challengeData.isChallenge)
			{
				e->aiFlags |= AIF_GOAL_JUMPGATE;
			}
		}
	}
}

void retreatAllies(void)
{
	Entity *e;

	for (e = battle.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_FIGHTER && e->side == player->side)
		{
			e->flags |= EF_RETREATING;

			e->aiFlags |= AIF_AVOIDS_COMBAT;
			e->aiFlags |= AIF_UNLIMITED_RANGE;
			e->aiFlags &= ~AIF_FOLLOWS_PLAYER;
			e->aiFlags &= ~AIF_MOVES_TO_PLAYER;
			e->aiFlags &= ~AIF_MOVES_TO_LEADER;
			e->aiFlags &= ~AIF_WANDERS;

			e->aiActionTime = MIN(e->aiActionTime, FPS);

			if (!game.currentMission->challengeData.isChallenge)
			{
				e->aiFlags |= AIF_GOAL_JUMPGATE;
			}
		}
	}
}

static Entity *getFighterDef(char *name)
{
	Entity *e;

	for (e = defHead.next ; e != NULL ; e = e->next)
	{
		if (strcmp(e->name, name) == 0)
		{
			return e;
		}
	}

	printf("Error: no such fighter '%s'\n", name);
	exit(1);
}

Entity **getDBFighters(int *num)
{
	Entity *e, **dbFighters;
	int i;

	i = *num = 0;

	for (e = defHead.next ; e != NULL ; e = e->next)
	{
		if (e->description != NULL)
		{
			*num = *num + 1;
		}
	}

	dbFighters = malloc(sizeof(Entity*) * *num);

	for (e = defHead.next ; e != NULL ; e = e->next)
	{
		if (e->description != NULL)
		{
			dbFighters[i] = e;

			i++;
		}
	}

	return dbFighters;
}

void loadFighterDefs(void)
{
	memset(&defHead, 0, sizeof(Entity));
	defTail = &defHead;

	loadFighterDefList("data/fighters");
	loadFighterDefList("data/craft");
	loadFighterDefList("data/turrets");
}

static void loadFighterDefList(char *dir)
{
	char **filenames;
	char path[MAX_FILENAME_LENGTH];
	int count, i;

	filenames = getFileList(dir, &count);

	for (i = 0 ; i < count ; i++)
	{
		sprintf(path, "%s/%s", dir, filenames[i]);

		loadFighterDef(path);

		free(filenames[i]);
	}

	free(filenames);
}

static void loadFighterDef(char *filename)
{
	cJSON *root, *node;
	char *text;
	Entity *e;
	int i, len;

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Loading %s", filename);

	text = readFile(filename);

	root = cJSON_Parse(text);

	if (root)
	{
		e = malloc(sizeof(Entity));
		memset(e, 0, sizeof(Entity));
		defTail->next = e;
		defTail = e;

		e->type = ET_FIGHTER;
		e->active = 1;

		STRNCPY(e->name, cJSON_GetObjectItem(root, "name")->valuestring, MAX_NAME_LENGTH);
		STRNCPY(e->defName, e->name, MAX_NAME_LENGTH);
		STRNCPY(e->affiliation, cJSON_GetObjectItem(root, "affiliation")->valuestring, MAX_NAME_LENGTH);
		e->health = e->maxHealth = cJSON_GetObjectItem(root, "health")->valueint;
		e->shield = e->maxShield = getJSONValue(root, "shield", 0);
		e->speed = cJSON_GetObjectItem(root, "speed")->valuedouble;
		e->reloadTime = getJSONValue(root, "reloadTime", 0);
		e->shieldRechargeRate = getJSONValue(root, "shieldRechargeRate", 0);
		e->texture = getAtlasImage(cJSON_GetObjectItem(root, "texture")->valuestring);

		if (strlen(cJSON_GetObjectItem(root, "description")->valuestring) > 0)
		{
			len = strlen(_(cJSON_GetObjectItem(root, "description")->valuestring)) + 1;
			e->description = malloc(len);
			strcpy(e->description, _(cJSON_GetObjectItem(root, "description")->valuestring));
		}

		e->w = e->texture->rect.w;
		e->h = e->texture->rect.h;

		if (cJSON_GetObjectItem(root, "guns"))
		{
			i = 0;

			for (node = cJSON_GetObjectItem(root, "guns")->child ; node != NULL ; node = node->next)
			{
				e->guns[i].type = lookup(cJSON_GetObjectItem(node, "type")->valuestring);
				e->guns[i].x = cJSON_GetObjectItem(node, "x")->valueint;
				e->guns[i].y = cJSON_GetObjectItem(node, "y")->valueint;

				i++;

				if (i >= MAX_FIGHTER_GUNS)
				{
					printf("ERROR: cannot assign more than %d guns to a fighter\n", MAX_FIGHTER_GUNS);
					exit(1);
				}
			}

			e->combinedGuns = getJSONValue(root, "combinedGuns", 0);
		}

		e->selectedGunType = e->guns[0].type;

		e->missiles = getJSONValue(root, "missiles", 0);

		if (cJSON_GetObjectItem(root, "flags"))
		{
			e->flags = flagsToLong(cJSON_GetObjectItem(root, "flags")->valuestring, NULL);
		}

		if (cJSON_GetObjectItem(root, "aiFlags"))
		{
			e->aiFlags = flagsToLong(cJSON_GetObjectItem(root, "aiFlags")->valuestring, NULL);
		}

		if (cJSON_GetObjectItem(root, "deathType"))
		{
			e->deathType = lookup(cJSON_GetObjectItem(root, "deathType")->valuestring);
		}

		if (e->flags & EF_COMMON_FIGHTER)
		{
			addFighterStat(e->name);
		}

		e->separationRadius = MAX(e->w, e->h) * 3;

		e->systemPower = MAX_SYSTEM_POWER;

		cJSON_Delete(root);
	}
	else
	{
		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Failed to load '%s'", filename);
	}

	free(text);
}

static void addFighterStat(char *key)
{
	Tuple *t, *tail;

	tail = &game.fighterStatHead;

	for (t = game.fighterStatHead.next ; t != NULL ; t = t->next)
	{
		if (strcmp(t->key, key) == 0)
		{
			return;
		}

		tail = t;
	}

	t = malloc(sizeof(Tuple));
	memset(t, 0, sizeof(Tuple));
	tail->next = t;

	STRNCPY(t->key, key, MAX_NAME_LENGTH);
	t->value = 0;

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Added '%s' to fighter stats", key);
}

static void incFighterStat(char *key)
{
	Tuple *t;

	for (t = game.fighterStatHead.next ; t != NULL ; t = t->next)
	{
		if (strcmp(t->key, key) == 0)
		{
			t->value++;
			return;
		}
	}
}

void loadFighters(cJSON *node)
{
	Entity *e;
	char **types, *name, *groupName, *type, *strpos;
	int side, scatter, number, active;
	int i, numTypes, addFlags, addAIFlags, id;
	long flags, aiFlags;
	float x, y;

	if (node)
	{
		id = 0;

		node = node->child;

		while (node)
		{
			name = NULL;
			groupName = NULL;
			flags = -1;
			aiFlags = -1;

			types = toTypeArray(cJSON_GetObjectItem(node, "types")->valuestring, &numTypes);
			side = lookup(cJSON_GetObjectItem(node, "side")->valuestring);
			x = (cJSON_GetObjectItem(node, "x")->valuedouble / BATTLE_AREA_CELLS) * BATTLE_AREA_WIDTH;
			y = (cJSON_GetObjectItem(node, "y")->valuedouble / BATTLE_AREA_CELLS) * BATTLE_AREA_HEIGHT;
			name = getJSONValueStr(node, "name", NULL);
			groupName = getJSONValueStr(node, "groupName", NULL);
			number = getJSONValue(node, "number", 1);
			scatter = getJSONValue(node, "scatter", 1);
			active = getJSONValue(node, "active", 1);

			if (cJSON_GetObjectItem(node, "flags"))
			{
				flags = flagsToLong(cJSON_GetObjectItem(node, "flags")->valuestring, &addFlags);
			}

			if (cJSON_GetObjectItem(node, "aiFlags"))
			{
				aiFlags = flagsToLong(cJSON_GetObjectItem(node, "aiFlags")->valuestring, &addAIFlags);
			}

			for (i = 0 ; i < number ; i++)
			{
				type = types[rand() % numTypes];

				e = spawnFighter(type, x, y, side);

				if (scatter > 1)
				{
					e->x += (rand() % scatter) - (rand() % scatter);
					e->y += (rand() % scatter) - (rand() % scatter);
				}

				e->active = active;

				if (flags != -1)
				{
					if (addFlags)
					{
						e->flags |= flags;
					}
					else
					{
						e->flags = flags;

						SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Flags for '%s' (%s) replaced", e->name, e->defName);
					}

					if (e->flags & EF_DISABLED)
					{
						e->speed = 0;
					}
				}

				if (aiFlags != -1)
				{
					if (addAIFlags)
					{
						e->aiFlags |= aiFlags;
					}
					else
					{
						e->aiFlags = aiFlags;

						SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "AI Flags for '%s' (%s) replaced", e->name, e->defName);
					}
				}

				if (name)
				{
					STRNCPY(e->name, name, MAX_NAME_LENGTH);

					/* update 'name #?' to 'name #1', etc. */
					strpos = strstr(e->name, "#?");

					if (strpos)
					{
						*(++strpos) = ('0' + ++id);
					}
				}

				if (groupName)
				{
					STRNCPY(e->groupName, groupName, MAX_NAME_LENGTH);
				}
			}

			node = node->next;

			for (i = 0 ; i < numTypes ; i++)
			{
				free(types[i]);
			}

			free(types);
		}
	}
}

void destroyFighterDefs(void)
{
	Entity *e;

	while (defHead.next)
	{
		e = defHead.next;
		defHead.next = e->next;

		if (e->description)
		{
			free(e->description);
		}

		free(e);
	}
}

void destroyFighterStats(void)
{
	Tuple *t;

	while (game.fighterStatHead.next)
	{
		t = game.fighterStatHead.next;
		game.fighterStatHead.next = t->next;
		free(t);
	}
}
