/*
	server/ai/ai_core.qc

	ai stuff

	Copyright (C) 2021-2022 NZ:P Team

	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:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/

void(float what) play_sound_z;
void() LinkZombiesHitbox;
entity() Dog_FindEnemy;

void() path_corner_touch =
{
	self.origin_z = self.origin_z + 32;
	setorigin(self, self.origin);
	self.classname = "path_corner";
	self.movetype = MOVETYPE_NONE;
	self.solid = SOLID_NOT;
	self.touch = SUB_Null;
	setsize(self, '0 0 0 ', '0 0 0');
	if(!self.target)
	{
		if (self.spawnflags & 1)
			return;
		bprint(PRINT_HIGH, "path_corner with name: ");
		bprint(PRINT_HIGH, self.targetname);
		bprint(PRINT_HIGH, " has no target!\n");
	}
}

//We want the path corner to drop to the ground and then we set it up 32 units so it's exact
void() path_corner =
{
	self.classname = "path_corner_set";
	self.movetype = MOVETYPE_BOUNCE;
	self.solid = SOLID_BBOX;
	self.touch = path_corner_touch;
	setsize(self, '0 0 0 ', '0 0 0');
};

void() Respawn = 
{
	// cypress (20 dec 2023) -- whoever wrote this originally wasn't considering
	// many factors.. if a zombie respawns there's a chance it'll be in a 
	// completely different playspace, where the initial window isn't visible
	// at all, and removeZombie() does not clear that field (rightfully so).
	// so i've replaced this with the same function i use to respawn zombies
	// inside of the playable area, which calls self.th_die() and fibs to the
	// zombie counter a little better, fixes that issue.

	// Kill it.
	self.th_die();

	// This is a pseudo-hack that just tells the rounds core that we should
	// spawn another.
	Current_Zombies--;
	Remaining_Zombies++;
};

entity(entity blarg) find_new_enemy =
{
	entity  targets;
	entity  best_target;
	float 	best_distance;
	float 	distance;
	
	best_distance = 10000;
	best_target = world;

	if (blarg.classname == "ai_zombie" || blarg.classname == "ai_dog") {
		// Monkey Bomb (TODO -- if multiple, target first one thrown)
		targets = find(world, classname, "monkey_bomb");
		if (targets != world && blarg.classname != "ai_dog") {
			best_target = targets;
			return best_target;
		}

		// Now, try and find a viable player
		targets = find(world, classname, "player");

		while(targets != world) {
			// Don't target downed players.
			if (targets.downed == true || targets.isspec == true) {
				targets = find(targets, classname, "player");
				continue;
			}

			// Found one, let's see if it's closer than our last ideal target.
			distance = vlen(blarg.origin - targets.origin);

			if (distance < best_distance) {
				best_target = targets;
				best_distance = distance;
			}

			// Continue iterating
			targets = find(targets, classname, "player");
		}

		// Return a good player if we found one.
		if (best_target != world)
			return best_target;

		// We couldn't find a good player. How about a horde point?
		targets = find(world, classname, "zombie_horde_point");

		while(targets != world) {
			// Found one, let's see if it's closer than our last ideal target.
			distance = vlen(blarg.origin - targets.origin);

			if (distance < best_distance) {
				best_target = targets;
				best_distance = distance;
			}

			// Continue iterating
			targets = find(targets, classname, "zombie_horde_point");
		}

		// Last chance -- try a player spawn point.
		targets = find(world, classname, "info_player_1_spawn");

		while(targets != world) {
			// Found one, let's see if it's closer than our last ideal target.
			distance = vlen(blarg.origin - targets.origin);

			if (distance < best_distance) {
				best_target = targets;
				best_distance = distance;
			}

			// Continue iterating
			targets = find(targets, classname, "info_player_1_spawn");
		}

		// Return a horde point if we found one.
		if (best_target != world)
			return best_target;
	}

	// We didn't have much luck, just return the world.
	return best_target;
};

float() avoid_zombies =
{
	local entity ent;
	ent = findradius(self.origin,23);//22.67
	makevectors(self.angles);
	float left_amount, right_amount;
	left_amount = right_amount = 0;
	while(ent)
	{
		if(ent.classname == "ai_zombie" && ent != self)
		{
			local vector vec_b;
			local float dot;
			//vec_b = normalize(self.origin - ent.origin);
			//dot = v_right * vec_b;
			//dot = self.angles_y - (dot > 0.5) ? 90 :  270;
			
			vec_b = (self.origin - ent.origin);
			dot = (v_right_x * vec_b_x) + (v_right_y * vec_b_y);//dot product
			if(dot > 0)// on right
				right_amount++;
			else// on left
				left_amount++;
		}
		ent = ent.chain;
	}
	if(left_amount + right_amount == 0)
		return 0;
	
	return (left_amount > right_amount) ? 15 : -15;
};

float() push_away_zombies =
{
	local entity ent;
	ent = findradius(self.origin,11);
	float return_value;
	return_value = 0;
	while(ent)
	{
		if(ent.aistatus == "1" && ent != self)
		{
			vector push;
			push = ent.origin - self.origin;
			push_z = 0;
			push = normalize(push) * 10;
			
			ent.velocity += push;
			return_value = 1;
		}
		ent = ent.chain;
	}
	return return_value;
}


void(float dist, vector vec) do_walk_to_vec =
{
	if(dist == 0)
		return;
	
	self.ideal_yaw = vectoyaw(vec - self.origin);
	if(self.outside == false)
	{
		// this is a performance net negative for like 0 gain in horde space.
		push_away_zombies();
	}
	ChangeYaw();
	vector new_velocity;
	
	float len;
	len = vlen(self.origin - vec);
	
	if(dist > len)//if we're moving past our goal position
	{
		dist = len;
	}
	//This movement method is moving directly towards the goal, regardless of where our yaw is facing (fixes several issues)
	new_velocity = normalize(vec - self.origin) * dist * 10;
	
	new_velocity_z = self.velocity_z;
	self.velocity = new_velocity;
};

void(float dist) do_walk =
{
	do_walk_to_vec(dist,self.goalentity.origin);
};

void(float dist) walk_to_window =
{
	do_walk_to_vec(dist,self.goalorigin);
};

// unused
void(vector org, float scale) interpolateToVector =
{
	self.origin_x += (org_x - self.origin_x) * scale;
	self.origin_y += (org_y - self.origin_y) * scale;
	setorigin(self,self.origin);
	self.zoom = 1;//value to let engine know to not check for collisions
}

float(vector where) nearby = 
{
	if(self.classname == "ai_zombie" || self.classname == "ai_dog")
	{
		float xdist;
		float ydist;
		float zdist;
		xdist = fabs(self.origin_x - where_x);
		ydist = fabs(self.origin_y - where_y);
		zdist = fabs(self.origin_z - where_z);
		
		if(xdist < 4 && ydist < 4)//horizontal distance is fairly close
		{
			if(zdist < 50)//vertical distance just has to be arbitrarily close
			{
				return 1;
			}
		}
		return 0;
	}
	return 0;
};

void(float dist) Window_Walk =
{
	if(self.reload_delay < time)
		Respawn();
	
	if(self.hop_step == 0)//following path corners
	{		
		if(self.goalentity == world)
		{
			if((!self.target) && (self.outside == TRUE))
			{
				bprint(PRINT_HIGH, "Error: Outside zombie spawn point has no target.\n");
				Respawn();
			}
			self.goalentity = find(world,targetname, self.target);
			if(!self.goalentity)
			{
				bprint(PRINT_HIGH, "Error: Outside zombie spawn point target does not exist.\n");
				Respawn();
			}
		}
		
		if(self.goalentity.classname == "path_corner" && nearby(self.goalentity.origin))
		{
			if (self.goalentity.spawnflags & 1) //this path corner sets zombie on inside.
			{
				self.outside = FALSE;
				self.goalentity = world;
				self.enemy = find_new_enemy(self);
				self.th_walk();
				return;
			}
			self.reload_delay = time + 30;
			self.goalentity = find(world,targetname,self.goalentity.target);
			
			//Assumption is that when the zombie is outside, we can always walk from one path_corner to the next in a straight line, any devation should be corrected by the mapper
		}

		do_walk(dist);
		
		if(self.goalentity.classname == "window")
		{
			// cypress (17 dec 2023) -- fixed a nasty race condition here.
			// essentially, if a new zombie spawns in fast enough after
			// a zombie is currently/just finished hopping a spot, they will
			// end up always taking box1owner (the hoppable zombie)'s place,
			// regardless of whether or not there was someone else waiting.
			// apparently this is an issue i've created rather recently,
			// which does not make sense to me considering this is a pretty
			// glaring oversight that's been here since 2014. not my whoops?
			if(!self.goalentity.box1owner && !self.goalentity.box2owner && !self.goalentity.box3owner)
			{
				//self.used = WBOX1;
				self.goalentity.box1owner = self;
				self.goalorigin = self.goalentity.box1;
				self.hop_step = 3;
				self.reload_delay = time + 30;
			}
			else if(!self.goalentity.box2owner)
			{
				//self.used = WBOX2;
				self.goalentity.box2owner = self;
				self.goalorigin = self.goalentity.box2;
				self.hop_step = 3;
				self.reload_delay = time + 30;
			}
			else if(!self.goalentity.box3owner)
			{
				//self.used = WBOX3;
				self.goalentity.box3owner = self;
				self.goalorigin = self.goalentity.box3;
				self.hop_step = 3;
				self.reload_delay = time + 30;
			}
			else if(vlen(self.origin - self.goalentity.origin) < 150)
			{	
				//we don't claim the idlebox
				//self.used = WIDLEBOX;
				self.goalorigin = self.goalentity.idlebox;
				self.hop_step = 1;
				self.reload_delay = time + 30;
			}
			//else we continue walking to window until we either find one that's good, or we are close enough to chase idle_spot
		}
	}
	else if(self.hop_step == 1)//walking to the window's idle location
	{
		if(nearby(self.goalorigin))
		{
			self.hop_step = 2;
			self.reload_delay = time + 30;
			self.th_idle();
		}
		else
		{
			walk_to_window(dist);
		}
	}
	else if(self.hop_step == 2)//we're at idle box, waiting for a window attack box to be free...
	{
		if(self.goalentity.box1owner == world)
		{
			//self.used = WBOX1;
			self.goalentity.box1owner = self;
			self.goalorigin = self.goalentity.box1;
			self.hop_step = 3;
			self.reload_delay = time + 30;
			self.th_walk();
		}
		else if(self.goalentity.box2owner == world)
		{
			//self.used = WBOX2;
			self.goalentity.box2owner = self;
			self.goalorigin = self.goalentity.box2;
			self.hop_step = 3;
			self.reload_delay = time + 30;
			self.th_walk();
		}
		else if(self.goalentity.box3owner == world)
		{
			//self.used = WBOX3;
			self.goalentity.box3owner = self;
			self.goalorigin = self.goalentity.box3;
			self.hop_step = 3;
			self.reload_delay = time + 30;
			self.th_walk();
		}
	}
	else if(self.hop_step == 3)//walking to window attack box
	{
		// sometimes, we've assigned a zombie a waiting-position
		// while another zombie is actively hopping. this can cause
		// a bit of gameplay slowdown since the zombie has to walk
		// to that waiting position, then to the hop spot (approx 1sec),
		// so let's do continuous checks here to see if box1 is free,
		// and claim it if so, in order to smooth the path and claim
		// that time. only do this if there isn't anyone else in the queue,
		// though.
		if (!self.goalentity.box1owner) {
			// Claim it as ours mid-walk, if there isn't someone else waiting.
			if ((self.goalentity.box2owner == self && !self.goalentity.box3owner) ||
			(self.goalentity.box3owner == self && !self.goalentity.box2owner)) {
				self.goalentity.box1owner = self;
				self.goalorigin = self.goalentity.box1;

				// Free up the spot we were walking to before.
				if (self.goalentity.box2owner == self) self.goalentity.box2owner = world;
				if (self.goalentity.box3owner == self) self.goalentity.box3owner = world;
			}
		}

		if(nearby(self.goalorigin))
		{
			self.hop_step = 4;
			self.reload_delay = time + 30;
			self.th_idle();
		}
		else
		{
			walk_to_window(dist);
		}
	}
	else if(self.hop_step == 4)//attacking box
	{
		if(self.chase_time < time)
		{
			if(self.angles_z != self.goalentity.angles_z)
			{
				self.ideal_yaw = self.goalentity.angles_z;
				ChangeYaw();
				return;
			}
			if(self.goalentity.health > 0 && !self.goalentity.owner)
			{
				self.reload_delay = time + 30;
				self.th_melee();				
				if(rounds <= 5)
					self.chase_time = time + 1.5;
				else
					self.chase_time = time + 0.75;
				return;
			} else if (self.goalentity.owner) {
				self.chase_time = time + 0.05;
				return;
			}
		}
		if(self.goalentity.health <= 0 && !self.goalentity.owner)
		{
			self.outside = 2;
			self.chase_time = 0;
			self.hop_step = 0;
		}
		else return;
	}
};

void(float dist) Window_Hop =
{
	if(self.hop_step == 0) {
		if(self.goalentity.box1owner == self) {//we're at center box.
			self.hop_step = 4;
		} else {
			self.hop_step = 1;//wait for box1 to be free so we can claim it and walk to it.
			self.th_idle();
		}
	}
	if(self.hop_step == 1) {//waiting idly for box1 to be free, when free, we will claim it.
		if(!self.goalentity.box1owner || self.goalentity.box1owner == self) {
			self.goalentity.box1owner = self;
			
			if(self.goalentity.box2owner == self)
				self.goalentity.box2owner = world;
			if(self.goalentity.box3owner == self)
				self.goalentity.box3owner = world;
				
			//self.used = WBOX1;
			
			self.goalorigin = self.goalentity.box1;
			self.hop_step = 2;
			self.th_walk();
		}
	}
	if(self.hop_step == 2) {//we've claimed it, walk to box1
		if(nearby(self.goalorigin)) {
			self.hop_step = 4;
			self.angles = self.goalentity.angles;
		} else {
			walk_to_window(dist);
		}
	}
	
	if(self.hop_step == 4 && self.chase_time < time) {//we're at this step because we already own box1, so don't even check if window is busy...
		if(!self.goalentity.usedent) {
			self.hop_step = 5;
			self.angles = self.goalentity.angles;
			self.goalentity.box1owner = world;//free box1
			self.goalentity.usedent = self;//we own the window
			//don't need to set goalorigin here
			//self.used = WWINDOW;
			self.chase_time = 0;
			self.th_windowhop();
			return;
		} else {
			self.tries++;
			self.chase_time = time + 0.2;
			if(self.tries > 10) {
				if (!self.goalentity.owner)
					self.goalentity.usedent = world;//free up the window if we've been waiting to hop
			}
		}
	}
	
	if(self.hop_step == 6) {
		self.outside = FALSE;
		self.goalentity.usedent = world;//free up the window, we're done hopping it
		self.goalentity = world;
		self.enemy = find_new_enemy(self);
		//self.th_die();
		self.th_walk();
		//LinkZombiesHitbox();
		//bprint (PRINT_HIGH, "Linked hitboxes");
	}
}


//
// FTE's custom "tracemove" -- no way in hell I'm reimplementing SV_Move
// in QuakeC. So this is just a really bad traceline hack. We can't even
// use tracebox since that's limited by BSP hulls,
//
#ifdef FTE
inline float(vector start, vector min, vector max, vector end, float nomonsters, entity forent) tracemove =
#else
float(vector start, vector min, vector max, vector end, float nomonsters, entity forent) tracemove_fake =
#endif // FTE
{
	makevectors(forent.angles);

	// Top left of the box
	traceline(start + '0 0 32' + v_right * -18, end, nomonsters, forent);

	// Results Check
	if (trace_ent != forent && trace_endpos != end)
		return 0;

	// Top right of the box
	traceline(start + '0 0 32' + v_right * 18, end, nomonsters, forent);

	// Results Check
	if (trace_ent != forent && trace_endpos != end)
		return 0;

	// Bottom left of the box
	traceline(start - '0 0 24' + v_right * -18, end, nomonsters, forent);

	// Results Check
	if (trace_ent != forent && trace_endpos != end)
		return 0;

	// Bottom right of the box
	traceline(start - '0 0 24' + v_right * 18, end, nomonsters, forent);

	// Results Check
	if (trace_ent != forent && trace_endpos != end)
		return 0;

	return 1;		
}

float() TryWalkToEnemy =
{

	// Early out hack for FTE -- if there's tons of z-diff, GTFO!
	float z_axis_diff = fabs(self.origin_z - self.enemy.origin_z);
	if (z_axis_diff >= 30)
		return 0;

	// This has been a headache...
	// TryWalkToEnemy is a system that attempts to ignore waypoints from a
	// certain distance to simulate proper player-targeting. It does this
	// using the custom builtin tracemove, which calls SV_Move to determine
	// if it's possible for the enemy to walk directly to the target. This
	// is problematic, however -- in that FTE does not feature this builtin
	// since it's non-standard and was written by blubs. This means there
	// needs to be improvisation, and as a result there is disparity here.
	// See the custom tracemove() function for details on that.
	// -- cypress (28 Nov 2023)
#ifdef FTE
	float TraceResult = tracemove(self.origin, VEC_HULL_MIN, VEC_HULL_MAX, self.enemy.origin, TRUE, self);
#else
	float TraceResult = tracemove_fake(self.origin, VEC_HULL_MIN, VEC_HULL_MAX, self.enemy.origin, TRUE, self);
#endif // FTE

	if (TraceResult == 1) {
		self.goalentity = self.enemy;
		self.chase_time = time + 7;
		return 1;
	} else {
		return 0;
	}
};

void() PathfindToEnemy =
{
	float path_result;
	float path_failure;

	//just to stop any warns.
	path_failure = 0;

#ifndef FTE

	path_result = Do_Pathfind_psp(self, self.enemy);

#else

	path_result = Do_Pathfind(self, self.enemy);

#endif // FTE

	if (path_result >= 1) {

#ifndef FTE

		self.goaldummy.origin = Get_First_Waypoint(self, self.origin, VEC_HULL_MIN, VEC_HULL_MAX);
		setorigin(self.goaldummy, self.goaldummy.origin);
		path_failure = path_result;

#else

		self.goalway = path_result;
		setorigin(self.goaldummy,waypoints[self.goalway].org);
		path_failure = self.goalway;

#endif // FTE

		self.goalentity = self.goaldummy;
		self.chase_time = time + 7;
		
	} else if (path_failure == -1) {
		self.goalentity = self.enemy;
		self.chase_time = time + 6;
	} else {

#ifdef FTE

		bprint(PRINT_HIGH, "FirstPathfind Failure\n");

#endif // FTE

	}
}

void() NextPathfindToEnemy {
	// same as PathfindToEnemy on non-FTE platforms

#ifndef FTE

	float path_success;
	path_success = Do_Pathfind_psp(self,self.enemy);
	if(path_success ==1) {
		self.goaldummy.origin = Get_Next_Waypoint(self,self.origin,VEC_HULL_MIN,VEC_HULL_MAX);
		setorigin(self.goaldummy,self.goaldummy.origin);
		self.goalentity = self.goaldummy;
		self.chase_time = time + 7;
	} else if(path_success == -1){
		self.goalentity = self.enemy;
		self.chase_time = time + 6;
	} else {
		bprint(PRINT_HIGH, "NextPathfind Failure\n");		// this lags like hell
	}

#else

	self.way_cur++;

	if (self.way_cur < 40 && self.way_path[self.way_cur] != -1) {
		self.goalway = self.way_path[self.way_cur];
		setorigin(self.goaldummy,waypoints[self.goalway-1].org);
	} else {
		self.way_cur = 0;
	}

#endif // FTE

}

void(float dist) Inside_Walk = {
	// Hellhounds should only change targets if current one is in Last Stand
	if (self.classname == "ai_dog" && self.enemy.downed == true) {
		self.enemy = Dog_FindEnemy();
	} 
	// Normal Zombie time-out re-targeting
	else if (self.classname != "ai_dog") {
		if(self.enemy_timeout < time || self.enemy == world || self.enemy.downed == true) {
			self.enemy_timeout = time + 5;
			local entity oldEnemy;
			oldEnemy = self.enemy;
			self.enemy = find_new_enemy(self);
		}
	}
	//================Check for proximity to player ===========
	if(vlen(self.enemy.origin - self.origin) < 60) {
		if(self.enemy.classname == "monkey_bomb" && self.classname != "ai_dog") {
			self.th_idle();
		}
		
		if(self.attack_delay < time) {
			self.attack_delay = time + 1 + (1 * random());

			if (self.enemy.health)
				self.th_melee(); 

			if (self.enemy.downed == true) {
				if (self.classname == "ai_dog")
					self.enemy = Dog_FindEnemy();
				else
					self.enemy = find_new_enemy(self);
			}
			self.goalentity = self.enemy;
			self.chase_time = time + 5;
		}
		return;
	}
	
	if(vlen(self.enemy.origin - self.origin) < 600) {//50 feet
		if(self.goalentity == self.enemy && self.chase_enemy_time > time) {
			return;
		}
		if(TryWalkToEnemy())
		{
			self.chase_enemy_time = time + 0.5;
			return;
		}
	}

	if(self.goalentity == self.enemy) {
		self.goalentity = self.goaldummy;
		self.chase_time = 0;
	}
	//============= No Target ====================
	if(self.goalentity == world) {//not sure when this would ever occur... but whatever.
		self.goalentity = self.goaldummy;
	}
	//============ GoalDummy is Target ============
	if(self.goalentity == self.goaldummy) {
		if(nearby(self.goaldummy.origin)) {

#ifndef FTE

			NextPathfindToEnemy();

#else

			PathfindToEnemy();

#endif // FTE

		}

		if(self.chase_time < time) {
			if(self.goaldummy.origin != world.origin && tracemove(self.origin,VEC_HULL_MIN,VEC_HULL_MAX,self.goalentity.origin,TRUE,self) == 1) {
				self.chase_time = time + 7;
			} else {
				PathfindToEnemy();
			}
		}
	}
}

.float droptime;
void(float dist) Zombie_Walk =  {
	//Resetting velocity from last frame (except for vertical)
	self.velocity_x = 0;
	self.velocity_y = 0;
	//self.flags = self.flags | FL_PARTIALGROUND;

	if (!(self.flags & FL_ONGROUND) && self.watertype != CONTENT_WATER) {

		if (!self.droptime) {
			self.droptime = time + 1;
		} else if (self.droptime < time) {
			self.droptime = 0;
			//bprint(PRINT_HIGH, "not on ground\n");
			if (self.classname != "ai_dog")
				self.th_fall();
			return;
		}
	}
	
	if(self.outside == TRUE) {
		//handle special walk case for walking to org
		Window_Walk(dist);
		return;
	}
	
	if(self.outside == 2) {
		//play_sound_z(2);
		Window_Hop(dist);
		//handle special walk case for walking to org
		return;
	}
	if(self.outside == FALSE) {
		if(self.goalentity == self.enemy) {
			if(vlen(self.origin - self.enemy.origin) < 60) {
				return;
			}
		}
	}
	do_walk(dist);
}

void() Zombie_AI = {
	//dist = 0;
	float dist = 0;
	self.flags = self.flags | FL_PARTIALGROUND;
	//check_onfire();
	
	if(self.outside == TRUE) {
		play_sound_z(2);
		//self.calc_time = time + (0.3 * random());
		//Window_Walk(dist);
		return;
	} else if(self.outside == 2) {
		play_sound_z(2);
		//Window_Hop(0);
		return;
	} else if(self.outside == FALSE) {
		play_sound_z(2);
		//self.calc_time = time + (0.25 + (0.15 * random()));
		Inside_Walk(dist);
	}
}

void() Hellhound_AI =
{
	Inside_Walk(0);
}

//
// Do_Zombie_AI()
// Behaves differently based on platform -- in FTE,
// all AI can afford to be executed at once. However,
// on every other platform there is a timed delay in
// place and only one zombie AI can be updated at a time.
//
void() Do_Zombie_AI = 
{
	entity z;
	entity old_self;
	
#ifndef FTE

	z = find(lastzombie,aistatus,"1");
	if(z == world) {
		z = find(world,aistatus,"1");
		if(z == world) {
			return;//no zombies alive.
		}
	}

	old_self = self;
	self = z;

	if(z.classname == "ai_zombie")
		Zombie_AI();
	else if (z.classname == "ai_dog")
		Hellhound_AI();

	self = old_self;
	lastzombie = z;

#else

	z = find(world, aistatus, "1");

	while(z != world) {
		old_self = self;
		self = z;

		// Execute AI
		if (z.classname == "ai_zombie")
			Zombie_AI();
		else if (z.classname == "ai_dog")
			Hellhound_AI();

		self = old_self;
		z = find(z, aistatus, "1");
	}

#endif // FTE

}

#ifdef FTE

void() AI_SetAllEnemiesBBOX =
{
	entity zombies = find(world, classname, "ai_zombie");

	while(zombies != world) {
		if (zombies.aistatus == "1") {
			zombies.last_solid = zombies.solid;
			zombies.solid = SOLID_BBOX;

			if (zombies.head) {
				zombies.head.last_solid = zombies.head.solid;
				zombies.head.solid = SOLID_BBOX;
			}

			if (zombies.larm) {
				zombies.larm.last_solid = zombies.larm.solid;
				zombies.larm.solid = SOLID_BBOX;
			}

			if (zombies.rarm) {
				zombies.rarm.last_solid = zombies.rarm.solid;
				zombies.rarm.solid = SOLID_BBOX;
			}
		}
		
		zombies = find(zombies, classname, "ai_zombie");
	}
}

void() AI_RevertEnemySolidState =
{
	entity zombies = find(world, classname, "ai_zombie");

	while(zombies != world) {
		if (zombies.aistatus == "1") {
			zombies.solid = zombies.last_solid;

			if (zombies.head)
				zombies.head.solid = zombies.head.last_solid;
			if (zombies.larm)
				zombies.larm.solid = zombies.larm.last_solid;
			if (zombies.rarm)
				zombies.rarm.solid = zombies.rarm.last_solid;
		}

		zombies = find(zombies, classname, "ai_zombie");
	}
}

#endif // FTE