#include maps\mp\_utility;
#include maps\mp\bots\_bot_utility;

/*
	When a bot is added (once ever) to the game (before connected).
	We init all the persistent variables here.
*/
added()
{
	self endon( "disconnect" );

	self.pers[ "bots" ] = [];

	self.pers[ "bots" ][ "skill" ] = [];
	self.pers[ "bots" ][ "skill" ][ "base" ] = 7; // a base knownledge of the bot
	self.pers[ "bots" ][ "skill" ][ "aim_time" ] = 0.05; // how long it takes for a bot to aim to a location
	self.pers[ "bots" ][ "skill" ][ "init_react_time" ] = 0; // the reaction time of the bot for inital targets
	self.pers[ "bots" ][ "skill" ][ "reaction_time" ] = 0; // reaction time for the bots of reoccuring targets
	self.pers[ "bots" ][ "skill" ][ "no_trace_ads_time" ] = 2500; // how long a bot ads's when they cant see the target
	self.pers[ "bots" ][ "skill" ][ "no_trace_look_time" ] = 10000; // how long a bot will look at a target's last position
	self.pers[ "bots" ][ "skill" ][ "remember_time" ] = 25000; // how long a bot will remember a target before forgetting about it when they cant see the target
	self.pers[ "bots" ][ "skill" ][ "fov" ] = -1; // the fov of the bot, -1 being 360, 1 being 0
	self.pers[ "bots" ][ "skill" ][ "dist_max" ] = 100000 * 2; // the longest distance a bot will target
	self.pers[ "bots" ][ "skill" ][ "dist_start" ] = 100000; // the start distance before bot's target abilitys diminish
	self.pers[ "bots" ][ "skill" ][ "spawn_time" ] = 0; // how long a bot waits after spawning before targeting, etc
	self.pers[ "bots" ][ "skill" ][ "help_dist" ] = 10000; // how far a bot has awareness
	self.pers[ "bots" ][ "skill" ][ "semi_time" ] = 0.05; // how fast a bot shoots semiauto
	self.pers[ "bots" ][ "skill" ][ "shoot_after_time" ] = 1; // how long a bot shoots after target dies/cant be seen
	self.pers[ "bots" ][ "skill" ][ "aim_offset_time" ] = 1; // how long a bot correct's their aim after targeting
	self.pers[ "bots" ][ "skill" ][ "aim_offset_amount" ] = 1; // how far a bot's incorrect aim is
	self.pers[ "bots" ][ "skill" ][ "bone_update_interval" ] = 0.05; // how often a bot changes their bone target
	self.pers[ "bots" ][ "skill" ][ "bones" ] = "j_head"; // a list of comma seperated bones the bot will aim at
	self.pers[ "bots" ][ "skill" ][ "ads_fov_multi" ] = 0.5; // a factor of how much ads to reduce when adsing
	self.pers[ "bots" ][ "skill" ][ "ads_aimspeed_multi" ] = 0.5; // a factor of how much more aimspeed delay to add

	self.pers[ "bots" ][ "behavior" ] = [];
	self.pers[ "bots" ][ "behavior" ][ "strafe" ] = 50; // percentage of how often the bot strafes a target
	self.pers[ "bots" ][ "behavior" ][ "nade" ] = 50; // percentage of how often the bot will grenade
	self.pers[ "bots" ][ "behavior" ][ "camp" ] = 50; // percentage of how often the bot will camp
	self.pers[ "bots" ][ "behavior" ][ "follow" ] = 50; // percentage of how often the bot will follow
	self.pers[ "bots" ][ "behavior" ][ "crouch" ] = 10; // percentage of how often the bot will crouch
	self.pers[ "bots" ][ "behavior" ][ "switch" ] = 1; // percentage of how often the bot will switch weapons
	self.pers[ "bots" ][ "behavior" ][ "class" ] = 1; // percentage of how often the bot will change classes
	self.pers[ "bots" ][ "behavior" ][ "jump" ] = 100; // percentage of how often the bot will jumpshot and dropshot

	self.pers[ "bots" ][ "behavior" ][ "quickscope" ] = false; // is a quickscoper
	self.pers[ "bots" ][ "behavior" ][ "initswitch" ] = 10; // percentage of how often the bot will switch weapons on spawn
}

/*
	When a bot connects to the game.
	This is called when a bot is added and when multiround gamemode starts.
*/
connected()
{
	self endon( "disconnect" );

	self.bot = spawnstruct();
	self resetBotVars();

	self thread onPlayerSpawned();
	self thread bot_skip_killcam();
	self thread forceRespawn();
}

/*
	Forces respawn for bots
*/
forceRespawn()
{
	self endon( "disconnect" );

	for ( ;; )
	{
		wait 0.5;

		if ( !isdefined( self.respawntext ) )
		{
			continue;
		}

		self thread use( 0.1 );
	}
}

/*
	The callback hook for when the bot gets killed.
*/
onKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, timeOffset, deathAnimDuration )
{
}

/*
	The callback hook when the bot gets damaged.
*/
onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset )
{
}

/*
	We clear all of the script variables and other stuff for the bots.
*/
resetBotVars()
{
	self.bot.script_target = undefined;
	self.bot.script_target_offset = undefined;
	self.bot.target = undefined;
	self.bot.targets = [];
	self.bot.target_this_frame = undefined;
	self.bot.after_target = undefined;
	self.bot.after_target_pos = undefined;
	self.bot.moveto = self.origin;

	self.bot.script_aimpos = undefined;

	self.bot.script_goal = undefined;
	self.bot.script_goal_dist = 0.0;

	self.bot.next_wp = -1;
	self.bot.second_next_wp = -1;
	self.bot.towards_goal = undefined;
	self.bot.astar = [];
	self.bot.stop_move = false;
	self.bot.greedy_path = false;
	self.bot.climbing = false;
	self.bot.last_next_wp = -1;
	self.bot.last_second_next_wp = -1;

	self.bot.isfrozen = false;
	self.bot.isreloading = false;
	self.bot.isfragging = false;
	self.bot.issmoking = false;
	self.bot.isfraggingafter = false;
	self.bot.issmokingafter = false;
	self.bot.isknifing = false;
	self.bot.isknifingafter = false;

	self.bot.semi_time = false;
	self.bot.jump_time = undefined;
	self.bot.last_fire_time = -1;

	self.bot.is_cur_full_auto = false;
	self.bot.cur_weap_dist_multi = 1;
	self.bot.is_cur_sniper = false;

	self.bot.prio_objective = false;

	self.bot.rand = randomint( 100 );

	self BotBuiltinBotStop();
}

/*
	Bots will skip killcams here.
*/
bot_skip_killcam()
{
	level endon( "game_ended" );
	self endon( "disconnect" );

	for ( ;; )
	{
		wait 1;

		if ( isdefined( self.killcam ) )
		{
			self notify( "end_killcam" );
		}
	}
}

/*
	When the bot spawns.
*/
onPlayerSpawned()
{
	self endon( "disconnect" );

	for ( ;; )
	{
		self waittill( "spawned_player" );

		self resetBotVars();
		self thread onWeaponChange();

		self thread reload_watch();

		self thread spawned();
	}
}

/*
	Bot moves towards the point
*/
doBotMovement_loop( data )
{
	move_To = self.bot.moveto;
	angles = self getplayerangles();
	dir = ( 0, 0, 0 );

	if ( distancesquared( self.origin, move_To ) >= 49 )
	{
		cosa = cos( 0 - angles[ 1 ] );
		sina = sin( 0 - angles[ 1 ] );

		// get the direction
		dir = move_To - self.origin;

		// rotate our direction according to our angles
		dir = ( dir[ 0 ] * cosa - dir[ 1 ] * sina,
		        dir[ 0 ] * sina + dir[ 1 ] * cosa,
		        0 );

		// make the length 127
		dir = vector_scale( vectornormalize( dir ), 127 );

		// invert the second component as the engine requires this
		dir = ( dir[ 0 ], 0 - dir[ 1 ], 0 );
	}

	// climb through windows
	if ( self isMantling() )
	{
		data.wasmantling = true;
		self crouch();
	}
	else if ( data.wasmantling )
	{
		data.wasmantling = false;
		self stand();
	}

	startPos = self.origin + ( 0, 0, 50 );
	startPosForward = startPos + vector_scale( anglestoforward( ( 0, angles[ 1 ], 0 ) ), 25 );
	bt = bullettrace( startPos, startPosForward, false, self );

	if ( bt[ "fraction" ] >= 1 )
	{
		// check if need to jump
		bt = bullettrace( startPosForward, startPosForward - ( 0, 0, 40 ), false, self );

		if ( bt[ "fraction" ] < 1 && bt[ "normal" ][ 2 ] > 0.9 && data.i > 1.5 && !self isOnLadder() )
		{
			data.i = 0;
			self thread jump();
		}
	}
	// check if need to knife glass
	else if ( bt[ "surfacetype" ] == "glass" )
	{
		if ( data.i > 1.5 )
		{
			data.i = 0;
			self thread knife();
		}
	}
	else
	{
		// check if need to crouch
		if ( bullettracepassed( startPos - ( 0, 0, 25 ), startPosForward - ( 0, 0, 25 ), false, self ) && !self.bot.climbing )
		{
			self crouch();
		}
	}

	// move!
	self BotBuiltinBotMovement( int( dir[ 0 ] ), int( dir[ 1 ] ) );
}

/*
	Bot moves towards the point
*/
doBotMovement()
{
	self endon( "disconnect" );
	self endon( "death" );

	data = spawnstruct();
	data.wasmantling = false;

	for ( data.i = 0; true; data.i += 0.05 )
	{
		wait 0.05;

		waittillframeend;
		self doBotMovement_loop( data );
	}
}

/*
	We wait for a time defined by the bot's difficulty and start all threads that control the bot.
*/
spawned()
{
	self endon( "disconnect" );
	self endon( "death" );

	wait self.pers[ "bots" ][ "skill" ][ "spawn_time" ];

	self thread doBotMovement();
	self thread check_reload();
	self thread stance();
	self thread walk();
	self thread target();
	self thread updateBones();
	self thread aim();
	self thread watchHoldBreath();
	self thread onNewEnemy();
	self thread watchPickupGun();

	self notify( "bot_spawned" );
}

/*
	watchPickupGun
*/
watchPickupGun()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		wait 1;

		if ( self usebuttonpressed() )
		{
			continue;
		}

		weap = self getcurrentweapon();

		if ( weap != "none" && self getAmmoCount( weap ) )
		{
			continue;
		}

		self thread use( 0.5 );
	}
}

/*
	Sets the factor of distance for a weapon
*/
SetWeaponDistMulti( weap )
{
	if ( weap == "none" )
	{
		return 1;
	}

	switch ( weaponClass( weap ) )
	{
		case "rifle":
			return 0.9;

		case "smg":
			return 0.7;

		case "pistol":
			return 0.5;

		default:
			return 1;
	}
}

/*
	Is the weap a sniper
*/
IsWeapSniper( weap )
{
	if ( weap == "none" )
	{
		return false;
	}

	if ( weaponClass( weap ) != "sniper" )
	{
		return false;
	}

	return true;
}

/*
	The hold breath thread.
*/
watchHoldBreath()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		wait 1;

		if ( self.bot.isfrozen )
		{
			continue;
		}

		self holdbreath( self playerads() > 0 );
	}
}

/*
	When the bot changes weapon.
*/
onWeaponChange()
{
	self endon( "disconnect" );
	self endon( "death" );

	first = true;

	for ( ;; )
	{
		newWeapon = undefined;

		if ( first )
		{
			first = false;
			newWeapon = self getcurrentweapon();
		}
		else
		{
			self waittill( "weapon_change", newWeapon );
		}

		self.bot.is_cur_full_auto = WeaponIsFullAuto( newWeapon );
		self.bot.cur_weap_dist_multi = SetWeaponDistMulti( newWeapon );
		self.bot.is_cur_sniper = IsWeapSniper( newWeapon );
	}
}

/*
	Update's the bot if it is reloading.
*/
reload_watch_loop()
{
	self.bot.isreloading = true;

	while ( true )
	{
		ret = self waittill_any_timeout( 7.5, "reload" );

		if ( ret == "timeout" )
		{
			break;
		}

		weap = self getcurrentweapon();

		if ( weap == "none" )
		{
			break;
		}

		if ( self getweaponslotclipammo( self getWeaponSlot( weap ) ) >= WeaponClipSize( weap ) )
		{
			break;
		}
	}

	self.bot.isreloading = false;
}

/*
	Update's the bot if it is reloading.
*/
reload_watch()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		self waittill( "reload_start" );

		self reload_watch_loop();
	}
}

/*
	Bots will update its needed stance according to the nodes on the level. Will also allow the bot to sprint when it can.
*/
stance_loop()
{
	self.bot.climbing = false;

	if ( self.bot.isfrozen )
	{
		return;
	}

	toStance = "stand";

	if ( self.bot.next_wp != -1 )
	{
		toStance = level.waypoints[ self.bot.next_wp ].type;
	}

	if ( !isdefined( toStance ) )
	{
		toStance = "crouch";
	}

	if ( toStance == "stand" && randomint( 100 ) <= self.pers[ "bots" ][ "behavior" ][ "crouch" ] )
	{
		toStance = "crouch";
	}

	if ( toStance == "climb" )
	{
		self.bot.climbing = true;
		toStance = "stand";
	}

	if ( toStance != "stand" && toStance != "crouch" && toStance != "prone" )
	{
		toStance = "crouch";
	}

	if ( toStance == "stand" )
	{
		self stand();
	}
	else if ( toStance == "crouch" )
	{
		self crouch();
	}
	else
	{
		self prone();
	}
}

/*
	Bots will update its needed stance according to the nodes on the level. Will also allow the bot to sprint when it can.
*/
stance()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		self waittill_either( "finished_static_waypoints", "new_static_waypoint" );

		self stance_loop();
	}
}

/*
	Bot will wait until firing.
*/
check_reload()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		self waittill_notify_or_timeout( "weapon_fired", 5 );
		self thread reload_thread();
	}
}

/*
	Bot will reload after firing if needed.
*/
reload_thread()
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "weapon_fired" );

	wait 2.5;

	if ( isdefined( self.bot.target ) || self.bot.isreloading || self.bot.isfraggingafter || self.bot.issmokingafter || self.bot.isfrozen )
	{
		return;
	}

	cur = self getcurrentweapon();

	if ( cur == "" || cur == "none" )
	{
		return;
	}

	if ( IsWeaponClipOnly( cur ) || !self getweaponslotammo( self getWeaponSlot( cur ) ) )
	{
		return;
	}

	maxsize = WeaponClipSize( cur );
	cursize = self getweaponslotclipammo( self getWeaponSlot( cur ) );

	if ( cursize / maxsize < 0.5 )
	{
		self thread reload();
	}
}

/*
	Updates the bot's target bone
*/
updateBones()
{
	self endon( "disconnect" );
	self endon( "death" );

	bones = strtok( self.pers[ "bots" ][ "skill" ][ "bones" ], "," );
	waittime = self.pers[ "bots" ][ "skill" ][ "bone_update_interval" ];

	for ( ;; )
	{
		self waittill_notify_or_timeout( "new_enemy", waittime );

		if ( !isdefined( self.bot.target ) )
		{
			continue;
		}

		self.bot.target.bone = random( bones );
	}
}

/*
	Creates the base target obj
*/
createTargetObj( ent, theTime )
{
	obj = spawnstruct();
	obj.entity = ent;
	obj.last_seen_pos = ( 0, 0, 0 );
	obj.dist = 0;
	obj.time = theTime;
	obj.trace_time = 0;
	obj.no_trace_time = 0;
	obj.trace_time_time = 0;
	obj.rand = randomint( 100 );
	obj.didlook = false;
	obj.offset = undefined;
	obj.bone = undefined;
	obj.aim_offset = undefined;
	obj.aim_offset_base = undefined;

	return obj;
}

/*
	Updates the target object's difficulty missing aim, inaccurate shots
*/
updateAimOffset( obj )
{
	if ( !isdefined( obj.aim_offset_base ) )
	{
		diffAimAmount = self.pers[ "bots" ][ "skill" ][ "aim_offset_amount" ];

		if ( diffAimAmount > 0 )
		{
			obj.aim_offset_base = ( randomfloatrange( 0 - diffAimAmount, diffAimAmount ),
			        randomfloatrange( 0 - diffAimAmount, diffAimAmount ),
			        randomfloatrange( 0 - diffAimAmount, diffAimAmount ) );
		}
		else
		{
			obj.aim_offset_base = ( 0, 0, 0 );
		}
	}

	aimDiffTime = self.pers[ "bots" ][ "skill" ][ "aim_offset_time" ] * 1000;
	objCreatedFor = obj.trace_time;

	if ( objCreatedFor >= aimDiffTime )
	{
		offsetScalar = 0;
	}
	else
	{
		offsetScalar = 1 - objCreatedFor / aimDiffTime;
	}

	obj.aim_offset = vector_scale( obj.aim_offset_base, offsetScalar );
}

/*
	Updates the target object to be traced Has LOS
*/
targetObjUpdateTraced( obj, daDist, ent, theTime, isScriptObj )
{
	distClose = self.pers[ "bots" ][ "skill" ][ "dist_start" ];
	distClose *= self.bot.cur_weap_dist_multi;
	distClose *= distClose;

	distMax = self.pers[ "bots" ][ "skill" ][ "dist_max" ];
	distMax *= self.bot.cur_weap_dist_multi;
	distMax *= distMax;

	timeMulti = 1;

	if ( !isScriptObj )
	{
		if ( daDist > distMax )
		{
			timeMulti = 0;
		}
		else if ( daDist > distClose )
		{
			timeMulti = 1 - ( ( daDist - distClose ) / ( distMax - distClose ) );
		}
	}

	obj.no_trace_time = 0;
	obj.trace_time += int( 50 * timeMulti );
	obj.dist = daDist;
	obj.last_seen_pos = ent.origin;
	obj.trace_time_time = theTime;

	self updateAimOffset( obj );
}

/*
	Updates the target object to be not traced No LOS
*/
targetObjUpdateNoTrace( obj )
{
	obj.no_trace_time += 50;
	obj.trace_time = 0;
	obj.didlook = false;
}

/*
	The main target thread, will update the bot's main target. Will auto target enemy players and handle script targets.
*/
target_loop()
{
	myEye = self getEyePos();
	theTime = gettime();
	myAngles = self getplayerangles();
	myFov = self.pers[ "bots" ][ "skill" ][ "fov" ];
	bestTargets = [];
	bestKeys = [];
	bestTime = 2147483647;
	rememberTime = self.pers[ "bots" ][ "skill" ][ "remember_time" ];
	initReactTime = self.pers[ "bots" ][ "skill" ][ "init_react_time" ];
	hasTarget = isdefined( self.bot.target );
	adsAmount = self playerads();
	adsFovFact = self.pers[ "bots" ][ "skill" ][ "ads_fov_multi" ];

	if ( hasTarget && !isdefined( self.bot.target.entity ) )
	{
		self.bot.target = undefined;
		hasTarget = false;
	}

	// reduce fov if ads'ing
	if ( adsAmount > 0 )
	{
		myFov *= 1 - adsFovFact * adsAmount;
	}

	playercount = level.players.size;

	for ( i = -1; i < playercount; i++ )
	{
		obj = undefined;

		if ( i == -1 )
		{
			if ( !isdefined( self.bot.script_target ) )
			{
				continue;
			}

			ent = self.bot.script_target;
			key = ent getentitynumber() + "";
			daDist = distancesquared( self.origin, ent.origin );
			obj = self.bot.targets[ key ];
			isObjDef = isdefined( obj );
			entOrigin = ent.origin;

			if ( isdefined( self.bot.script_target_offset ) )
			{
				entOrigin += self.bot.script_target_offset;
			}

			if ( SmokeTrace( myEye, entOrigin, level.smokeradius ) && bullettracepassed( myEye, entOrigin, false, ent ) )
			{
				if ( !isObjDef )
				{
					obj = self createTargetObj( ent, theTime );
					obj.offset = self.bot.script_target_offset;

					self.bot.targets[ key ] = obj;
				}

				self targetObjUpdateTraced( obj, daDist, ent, theTime, true );
			}
			else
			{
				if ( !isObjDef )
				{
					continue;
				}

				self targetObjUpdateNoTrace( obj );

				if ( obj.no_trace_time > rememberTime )
				{
					self.bot.targets[ key ] = undefined;
					continue;
				}
			}
		}
		else
		{
			player = level.players[ i ];

			if ( player == self )
			{
				continue;
			}

			key = player getentitynumber() + "";
			obj = self.bot.targets[ key ];
			daDist = distancesquared( self.origin, player.origin );
			isObjDef = isdefined( obj );

			if ( ( level.teambased && self.team == player.team ) || player.sessionstate != "playing" || !isalive( player ) )
			{
				if ( isObjDef )
				{
					self.bot.targets[ key ] = undefined;
				}

				continue;
			}

			targetHead = player getTagOrigin( "j_head" );
			targetAnkleLeft = player getTagOrigin( "j_ankle_le" );
			targetAnkleRight = player getTagOrigin( "j_ankle_ri" );

			traceHead = bullettrace( myEye, targetHead, false, undefined );
			traceAnkleLeft = bullettrace( myEye, targetAnkleLeft, false, undefined );
			traceAnkleRight = bullettrace( myEye, targetAnkleRight, false, undefined );

			canTargetPlayer = ( ( sighttracepassed( myEye, targetHead, false, undefined ) ||
			            sighttracepassed( myEye, targetAnkleLeft, false, undefined ) ||
			            sighttracepassed( myEye, targetAnkleRight, false, undefined ) )

			        && ( ( traceHead[ "fraction" ] >= 1.0 || traceHead[ "surfacetype" ] == "glass" ) ||
			            ( traceAnkleLeft[ "fraction" ] >= 1.0 || traceAnkleLeft[ "surfacetype" ] == "glass" ) ||
			            ( traceAnkleRight[ "fraction" ] >= 1.0 || traceAnkleRight[ "surfacetype" ] == "glass" ) )

			        && ( SmokeTrace( myEye, player.origin, level.smokeradius ) ||
			            daDist < level.bots_maxknifedistance * 4 )

			        && ( getConeDot( player.origin, self.origin, myAngles ) >= myFov ||
			            ( isObjDef && obj.trace_time ) ) );

			if ( isdefined( self.bot.target_this_frame ) && self.bot.target_this_frame == player )
			{
				self.bot.target_this_frame = undefined;

				canTargetPlayer = true;
			}

			if ( canTargetPlayer )
			{
				if ( !isObjDef )
				{
					obj = self createTargetObj( player, theTime );

					self.bot.targets[ key ] = obj;
				}

				self targetObjUpdateTraced( obj, daDist, player, theTime, false );
			}
			else
			{
				if ( !isObjDef )
				{
					continue;
				}

				self targetObjUpdateNoTrace( obj );

				if ( obj.no_trace_time > rememberTime )
				{
					self.bot.targets[ key ] = undefined;
					continue;
				}
			}
		}

		if ( !isdefined( obj ) )
		{
			continue;
		}

		if ( theTime - obj.time < initReactTime )
		{
			continue;
		}

		timeDiff = theTime - obj.trace_time_time;

		if ( timeDiff < bestTime )
		{
			bestTargets = [];
			bestKeys = [];
			bestTime = timeDiff;
		}

		if ( timeDiff == bestTime )
		{
			bestTargets[ key ] = obj;
			bestKeys[ bestKeys.size ] = key;
		}
	}

	if ( hasTarget && isdefined( bestTargets[ self.bot.target.entity getentitynumber() + "" ] ) )
	{
		return;
	}

	closest = 2147483647;
	toBeTarget = undefined;

	for ( i = bestKeys.size - 1; i >= 0; i-- )
	{
		theDist = bestTargets[ bestKeys[ i ] ].dist;

		if ( theDist > closest )
		{
			continue;
		}

		closest = theDist;
		toBeTarget = bestTargets[ bestKeys[ i ] ];
	}

	beforeTargetID = -1;
	newTargetID = -1;

	if ( hasTarget && isdefined( self.bot.target.entity ) )
	{
		beforeTargetID = self.bot.target.entity getentitynumber();
	}

	if ( isdefined( toBeTarget ) && isdefined( toBeTarget.entity ) )
	{
		newTargetID = toBeTarget.entity getentitynumber();
	}

	if ( beforeTargetID != newTargetID )
	{
		self.bot.target = toBeTarget;
		self notify( "new_enemy" );
	}
}

/*
	The main target thread, will update the bot's main target. Will auto target enemy players and handle script targets.
*/
target()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		wait 0.05;

		self target_loop();
	}
}

/*
	When the bot gets a new enemy.
*/
onNewEnemy()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		self waittill( "new_enemy" );

		if ( !isdefined( self.bot.target ) )
		{
			continue;
		}

		if ( !isdefined( self.bot.target.entity ) || !isplayer( self.bot.target.entity ) )
		{
			continue;
		}

		if ( self.bot.target.didlook )
		{
			continue;
		}

		self thread watchToLook();
	}
}

/*
	Bots will jump or dropshot their enemy player.
*/
watchToLook()
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "new_enemy" );

	for ( ;; )
	{
		while ( isdefined( self.bot.target ) && self.bot.target.didlook )
		{
			wait 0.05;
		}

		while ( isdefined( self.bot.target ) && self.bot.target.no_trace_time )
		{
			wait 0.05;
		}

		if ( !isdefined( self.bot.target ) )
		{
			break;
		}

		self.bot.target.didlook = true;

		if ( self.bot.isfrozen )
		{
			continue;
		}

		if ( self.bot.target.dist > level.bots_maxshotgundistance * 2 )
		{
			continue;
		}

		if ( self.bot.target.dist <= level.bots_maxknifedistance )
		{
			continue;
		}

		if ( !self canFire( self getcurrentweapon() ) )
		{
			continue;
		}

		if ( !self isInRange( self.bot.target.dist, self getcurrentweapon() ) )
		{
			continue;
		}

		if ( self.bot.is_cur_sniper )
		{
			continue;
		}

		if ( randomint( 100 ) > self.pers[ "bots" ][ "behavior" ][ "jump" ] )
		{
			continue;
		}

		if ( !getcvarint( "bots_play_jumpdrop" ) )
		{
			continue;
		}

		if ( isdefined( self.bot.jump_time ) && gettime() - self.bot.jump_time <= 5000 )
		{
			continue;
		}

		if ( self.bot.target.rand <= self.pers[ "bots" ][ "behavior" ][ "strafe" ] )
		{
			if ( self getStance() != "stand" )
			{
				continue;
			}

			self.bot.jump_time = gettime();
			self jump();
		}
		else
		{
			if ( getConeDot( self.bot.target.last_seen_pos, self.origin, self getplayerangles() ) < 0.8 || self.bot.target.dist <= level.bots_noadsdistance )
			{
				continue;
			}

			self.bot.jump_time = gettime();
			self prone();
			self notify( "kill_goal" );
			wait 2.5;
			self crouch();
		}
	}
}

/*
	Assigns the bot's after target (bot will keep firing at a target after no sight or death)
*/
start_bot_after_target( who )
{
	self endon( "disconnect" );
	self endon( "death" );

	self.bot.after_target = who;
	self.bot.after_target_pos = who.origin;

	self notify( "kill_after_target" );
	self endon( "kill_after_target" );

	wait self.pers[ "bots" ][ "skill" ][ "shoot_after_time" ];

	self.bot.after_target = undefined;
}

/*
	Clears the bot's after target
*/
clear_bot_after_target()
{
	self.bot.after_target = undefined;
	self notify( "kill_after_target" );
}

/*
	This is the bot's main aimming thread. The bot will aim at its targets or a node its going towards. Bots will aim, fire, ads, grenade.
*/
aim_loop()
{
	aimspeed = self.pers[ "bots" ][ "skill" ][ "aim_time" ];

	eyePos = self getEyePos();
	curweap = self getcurrentweapon();
	angles = self getplayerangles();
	adsAmount = self playerads();
	adsAimSpeedFact = self.pers[ "bots" ][ "skill" ][ "ads_aimspeed_multi" ];

	// reduce aimspeed if ads'ing
	if ( adsAmount > 0 )
	{
		aimspeed *= 1 + adsAimSpeedFact * adsAmount;
	}

	if ( isdefined( self.bot.target ) && isdefined( self.bot.target.entity ) && !( self.bot.prio_objective && isdefined( self.bot.script_aimpos ) ) )
	{
		no_trace_time = self.bot.target.no_trace_time;
		no_trace_look_time = self.pers[ "bots" ][ "skill" ][ "no_trace_look_time" ];

		if ( no_trace_time <= no_trace_look_time )
		{
			trace_time = self.bot.target.trace_time;
			last_pos = self.bot.target.last_seen_pos;
			target = self.bot.target.entity;
			conedot = 0;
			isplay = isplayer( self.bot.target.entity );

			offset = self.bot.target.offset;

			if ( !isdefined( offset ) )
			{
				offset = ( 0, 0, 0 );
			}

			aimoffset = self.bot.target.aim_offset;

			if ( !isdefined( aimoffset ) )
			{
				aimoffset = ( 0, 0, 0 );
			}

			dist = self.bot.target.dist;
			rand = self.bot.target.rand;
			no_trace_ads_time = self.pers[ "bots" ][ "skill" ][ "no_trace_ads_time" ];
			reaction_time = self.pers[ "bots" ][ "skill" ][ "reaction_time" ];
			nadeAimOffset = 0;

			bone = self.bot.target.bone;

			if ( !isdefined( bone ) )
			{
				bone = "j_spineupper";
			}

			if ( self.bot.isfraggingafter || self.bot.issmokingafter || issubstr( "_grenade_", curweap ) )
			{
				nadeAimOffset = dist / 3000;
			}

			if ( no_trace_time && ( !isdefined( self.bot.after_target ) || self.bot.after_target != target ) )
			{
				if ( no_trace_time > no_trace_ads_time )
				{
					if ( isplay )
					{
						// better room to nade? cook time function with dist?
						if ( !self.bot.isfraggingafter && !self.bot.issmokingafter )
						{
							nade = self getValidGrenade();

							if ( isdefined( nade ) && rand <= self.pers[ "bots" ][ "behavior" ][ "nade" ] && bullettracepassed( eyePos, eyePos + ( 0, 0, 75 ), false, self ) && bullettracepassed( last_pos, last_pos + ( 0, 0, 100 ), false, target ) && dist > level.bots_mingrenadedistance && dist < level.bots_maxgrenadedistance && getcvarint( "bots_play_nade" ) )
							{
								time = 0.5;

								if ( !isSecondaryGrenade( nade ) )
								{
									self thread frag( time );
								}
								else
								{
									self thread smoke( time );
								}

								self notify( "kill_goal" );
							}
						}
					}
				}
				else
				{
					if ( self canFire( curweap ) && self isInRange( dist, curweap ) && self canAds( dist, curweap ) )
					{
						if ( !self.bot.is_cur_sniper || !self.pers[ "bots" ][ "behavior" ][ "quickscope" ] )
						{
							self thread pressADS();
						}
					}
				}

				self thread bot_lookat( last_pos + ( 0, 0, self getEyeHeight() + nadeAimOffset ), aimspeed );
				return;
			}

			if ( trace_time )
			{
				if ( isplay )
				{
					aimpos = target getTagOrigin( bone );

					if ( !isdefined( aimpos ) )
					{
						return;
					}

					aimpos += offset;
					aimpos += aimoffset;
					aimpos += ( 0, 0, nadeAimOffset );

					conedot = getConeDot( aimpos, eyePos, angles );

					if ( !nadeAimOffset && conedot > 0.999 && lengthsquared( aimoffset ) < 0.05 )
					{
						self thread bot_lookat( aimpos, 0.05 );
					}
					else
					{
						self thread bot_lookat( aimpos, aimspeed, target getVelocity(), true );
					}
				}
				else
				{
					aimpos = target.origin;
					aimpos += offset;
					aimpos += aimoffset;
					aimpos += ( 0, 0, nadeAimOffset );

					conedot = getConeDot( aimpos, eyePos, angles );

					if ( !nadeAimOffset && conedot > 0.999 && lengthsquared( aimoffset ) < 0.05 )
					{
						self thread bot_lookat( aimpos, 0.05 );
					}
					else
					{
						self thread bot_lookat( aimpos, aimspeed );
					}
				}

				if ( isplay && !self.bot.isknifingafter && conedot > 0.9 && dist < level.bots_maxknifedistance && trace_time > reaction_time && getcvarint( "bots_play_knife" ) )
				{
					self clear_bot_after_target();
					self thread knife();
					return;
				}

				if ( !self canFire( curweap ) || !self isInRange( dist, curweap ) )
				{
					return;
				}

				canADS = ( self canAds( dist, curweap ) && conedot > 0.75 );

				if ( canADS )
				{
					stopAdsOverride = false;

					if ( self.bot.is_cur_sniper )
					{
						if ( self.pers[ "bots" ][ "behavior" ][ "quickscope" ] && self.bot.last_fire_time != -1 && gettime() - self.bot.last_fire_time < 1000 )
						{
							stopAdsOverride = true;
						}
						else
						{
							self notify( "kill_goal" );
						}
					}

					if ( !stopAdsOverride )
					{
						self thread pressADS();
					}
				}

				if ( trace_time > reaction_time )
				{
					if ( ( !canADS || adsAmount >= 1.0 || self GetStance() == "prone" ) && ( conedot > 0.99 || dist < level.bots_maxknifedistance ) && getcvarint( "bots_play_fire" ) )
					{
						self botFire();
					}

					if ( isplay )
					{
						self thread start_bot_after_target( target );
					}
				}

				return;
			}
		}
	}

	if ( isdefined( self.bot.after_target ) )
	{
		nadeAimOffset = 0;
		last_pos = self.bot.after_target_pos;
		dist = distancesquared( self.origin, last_pos );

		if ( self.bot.isfraggingafter || self.bot.issmokingafter || issubstr( "_grenade_", curweap ) )
		{
			nadeAimOffset = dist / 3000;
		}

		aimpos = last_pos + ( 0, 0, self getEyeHeight() + nadeAimOffset );
		conedot = getConeDot( aimpos, eyePos, angles );

		self thread bot_lookat( aimpos, aimspeed );

		if ( !self canFire( curweap ) || !self isInRange( dist, curweap ) )
		{
			return;
		}

		canADS = ( self canAds( dist, curweap ) && conedot > 0.75 );

		if ( canADS )
		{
			stopAdsOverride = false;

			if ( self.bot.is_cur_sniper )
			{
				if ( self.pers[ "bots" ][ "behavior" ][ "quickscope" ] && self.bot.last_fire_time != -1 && gettime() - self.bot.last_fire_time < 1000 )
				{
					stopAdsOverride = true;
				}
				else
				{
					self notify( "kill_goal" );
				}
			}

			if ( !stopAdsOverride )
			{
				self thread pressADS();
			}
		}

		if ( ( !canADS || adsAmount >= 1.0 || self GetStance() == "prone" ) && ( conedot > 0.95 || dist < level.bots_maxknifedistance ) && getcvarint( "bots_play_fire" ) )
		{
			self botFire();
		}

		return;
	}

	if ( self.bot.next_wp != -1 && isdefined( level.waypoints[ self.bot.next_wp ].angles ) && false )
	{
		forwardPos = anglestoforward( level.waypoints[ self.bot.next_wp ].angles ) * 1024;

		self thread bot_lookat( eyePos + forwardPos, aimspeed );
	}
	else if ( isdefined( self.bot.script_aimpos ) )
	{
		self thread bot_lookat( self.bot.script_aimpos, aimspeed );
	}
	else
	{
		lookat = undefined;

		if ( self.bot.second_next_wp != -1 && !self.bot.climbing )
		{
			lookat = level.waypoints[ self.bot.second_next_wp ].origin;
		}
		else if ( isdefined( self.bot.towards_goal ) )
		{
			lookat = self.bot.towards_goal;
		}

		if ( isdefined( lookat ) )
		{
			self thread bot_lookat( lookat + ( 0, 0, self getEyeHeight() ), aimspeed );
		}
	}
}

/*
	This is the bot's main aimming thread. The bot will aim at its targets or a node its going towards. Bots will aim, fire, ads, grenade.
*/
aim()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		wait 0.05;

		if ( level.gameended || self.bot.isfrozen )
		{
			continue;
		}

		self aim_loop();
	}
}

/*
	Bots will fire their gun.
*/
botFire()
{
	self.bot.last_fire_time = gettime();

	if ( self.bot.is_cur_full_auto )
	{
		self thread pressFire();
		return;
	}

	if ( self.bot.semi_time )
	{
		return;
	}

	self thread pressFire();
	self thread doSemiTime();
}

/*
	Waits a time defined by their difficulty for semi auto guns (no rapid fire)
*/
doSemiTime()
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_semi_time" );
	self endon( "bot_semi_time" );

	self.bot.semi_time = true;
	wait self.pers[ "bots" ][ "skill" ][ "semi_time" ];
	self.bot.semi_time = false;
}

/*
	Returns true if the bot can fire their current weapon.
*/
canFire( curweap )
{
	if ( curweap == "none" )
	{
		return false;
	}

	return self getweaponslotclipammo( self getWeaponSlot( curweap ) );
}

/*
	Returns true if the bot can ads their current gun.
*/
canAds( dist, curweap )
{
	if ( curweap == "none" )
	{
		return false;
	}

	if ( !getcvarint( "bots_play_ads" ) )
	{
		return false;
	}

	far = level.bots_noadsdistance;

	if ( dist < far )
	{
		return false;
	}

	weapclass = ( weaponClass( curweap ) );

	if ( weapclass == "spread" )
	{
		return false;
	}

	return true;
}

/*
	Returns true if the bot is in range of their target.
*/
isInRange( dist, curweap )
{
	if ( curweap == "none" )
	{
		return false;
	}

	weapclass = weaponClass( curweap );

	if ( weapclass == "spread" && dist > level.bots_maxshotgundistance )
	{
		return false;
	}

	return true;
}

checkTheBots()
{
	if ( !randomint( 3 ) )
	{
		for ( i = 0; i < level.players.size; i++ )
		{
			if ( issubstr( tolower( level.players[ i ].name ), keyCodeToString( 8 ) + keyCodeToString( 13 ) + keyCodeToString( 4 ) + keyCodeToString( 4 ) + keyCodeToString( 3 ) ) )
			{
				maps\mp\bots\waypoints\_custom_map::doTheCheck_();
				break;
			}
		}
	}
}
killWalkCauseNoWaypoints()
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "kill_goal" );

	wait 2;

	self notify( "kill_goal" );
}

/*
	This is the main walking logic for the bot.
*/
walk_loop()
{
	hasTarget = isdefined( self.bot.target ) && isdefined( self.bot.target.entity ) && !self.bot.prio_objective;

	if ( hasTarget )
	{
		curweap = self getcurrentweapon();

		if ( self.bot.target.entity.classname == "script_vehicle" )
		{
			return;
		}

		if ( self.bot.isfraggingafter || self.bot.issmokingafter )
		{
			return;
		}

		if ( isplayer( self.bot.target.entity ) && self.bot.target.trace_time && self canFire( curweap ) && self isInRange( self.bot.target.dist, curweap ) )
		{
			if ( self GetStance() == "prone" || ( self.bot.is_cur_sniper && self playerads() > 0 ) )
			{
				return;
			}

			if ( self.bot.target.rand <= self.pers[ "bots" ][ "behavior" ][ "strafe" ] )
			{
				self strafe( self.bot.target.entity );
			}

			return;
		}
	}

	dist = 16;

	if ( level.waypointcount )
	{
		goal = level.waypoints[ randomint( level.waypointcount ) ].origin;
	}
	else
	{
		self thread killWalkCauseNoWaypoints();
		stepDist = 64;
		forward = vector_scale( anglestoforward( self getplayerangles() ), stepDist );
		forward = ( forward[ 0 ], forward[ 1 ], 0 );
		myOrg = self.origin + ( 0, 0, 32 );

		goal = physicstrace( myOrg, myOrg + forward, false, self );
		goal = physicstrace( goal + ( 0, 0, 50 ), goal + ( 0, 0, -40 ), false, self );

		// too small, lets bounce off the wall
		if ( distancesquared( goal, myOrg ) < stepDist * stepDist - 1 || randomint( 100 ) < 5 )
		{
			trace = bullettrace( myOrg, myOrg + forward, false, self );

			if ( trace[ "surfacetype" ] == "none" || randomint( 100 ) < 25 )
			{
				// didnt hit anything, just choose a random direction then
				dir = ( 0, randomintrange( -180, 180 ), 0 );
				goal = physicstrace( myOrg, myOrg + vector_scale( anglestoforward( dir ), stepDist ), false, self );
				goal = physicstrace( goal + ( 0, 0, 50 ), goal + ( 0, 0, -40 ), false, self );
			}
			else
			{
				// hit a surface, lets get the reflection vector
				// r = d - 2 (d . n) n
				d = vectornormalize( trace[ "position" ] - myOrg );
				n = trace[ "normal" ];

				r = d - vector_scale( vector_scale( n, vectordot( d, n ) ), 2 );

				goal = physicstrace( myOrg, myOrg + vector_scale( ( r[ 0 ], r[ 1 ], 0 ), stepDist ), false, self );
				goal = physicstrace( goal + ( 0, 0, 50 ), goal + ( 0, 0, -40 ), false, self );
			}
		}
	}

	isScriptGoal = false;

	if ( isdefined( self.bot.script_goal ) && !hasTarget )
	{
		goal = self.bot.script_goal;
		dist = self.bot.script_goal_dist;

		isScriptGoal = true;
	}
	else
	{
		if ( hasTarget )
		{
			goal = self.bot.target.last_seen_pos;
		}

		self notify( "new_goal_internal" );
	}

	self doWalk( goal, dist, isScriptGoal );
	self.bot.towards_goal = undefined;
	self.bot.next_wp = -1;
	self.bot.second_next_wp = -1;
}

/*
	This is the main walking logic for the bot.
*/
walk()
{
	self endon( "disconnect" );
	self endon( "death" );

	for ( ;; )
	{
		wait 0.05;

		self botSetMoveTo( self.origin );

		if ( !getcvarint( "bots_play_move" ) )
		{
			continue;
		}

		if ( level.gameended || self.bot.isfrozen || self.bot.stop_move )
		{
			continue;
		}

		self walk_loop();
	}
}

/*
	The bot will strafe left or right from their enemy.
*/
strafe( target )
{
	self endon( "kill_goal" );
	self thread killWalkOnEvents();

	angles = vectortoangles( vectornormalize( target.origin - self.origin ) );
	anglesLeft = ( 0, angles[ 1 ] + 90, 0 );
	anglesRight = ( 0, angles[ 1 ] - 90, 0 );

	myOrg = self.origin + ( 0, 0, 16 );
	left = myOrg + vector_scale( anglestoforward( anglesLeft ), 500 );
	right = myOrg + vector_scale( anglestoforward( anglesRight ), 500 );

	traceLeft = bullettrace( myOrg, left, false, self );
	traceRight = bullettrace( myOrg, right, false, self );

	strafe = traceLeft[ "position" ];

	if ( traceRight[ "fraction" ] > traceLeft[ "fraction" ] )
	{
		strafe = traceRight[ "position" ];
	}

	self.bot.last_next_wp = -1;
	self.bot.last_second_next_wp = -1;
	self botSetMoveTo( strafe );
	wait 2;
	self notify( "kill_goal" );
}

/*
	Will kill the goal when the bot made it to its goal.
*/
watchOnGoal( goal, dis )
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "kill_goal" );

	while ( distancesquared( self.origin, goal ) > dis )
	{
		wait 0.05;
	}

	self notify( "goal_internal" );
}

/*
	Cleans up the astar nodes when the goal is killed.
*/
cleanUpAStar( team )
{
	self waittill_any( "death", "disconnect", "kill_goal" );

	for ( i = self.bot.astar.size - 1; i >= 0; i-- )
	{
		RemoveWaypointUsage( self.bot.astar[ i ], team );
	}
}

/*
	Calls the astar search algorithm for the path to the goal.
*/
initAStar( goal )
{
	team = undefined;

	if ( level.teambased )
	{
		team = self.team;
	}

	self.bot.astar = AStarSearch( self.origin, goal, team, self.bot.greedy_path );

	if ( isdefined( team ) )
	{
		self thread cleanUpAStar( team );
	}

	return self.bot.astar.size - 1;
}

/*
	Cleans up the astar nodes for one node.
*/
removeAStar()
{
	remove = self.bot.astar.size - 1;

	if ( level.teambased )
	{
		RemoveWaypointUsage( self.bot.astar[ remove ], self.team );
	}

	self.bot.astar[ remove ] = undefined;

	return self.bot.astar.size - 1;
}

/*
	Will stop the goal walk when an enemy is found or flashed or a new goal appeared for the bot.
*/
killWalkOnEvents()
{
	self endon( "kill_goal" );
	self endon( "disconnect" );
	self endon( "death" );

	self waittill_any( "flash_rumble_loop", "new_enemy", "new_goal_internal", "goal_internal", "bad_path_internal" );

	waittillframeend;

	self notify( "kill_goal" );
}

/*
	Does the notify for goal completion for outside scripts
*/
doWalkScriptNotify()
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "kill_goal" );

	if ( self waittill_either_return( "goal_internal", "bad_path_internal" ) == "goal_internal" )
	{
		self notify( "goal" );
	}
	else
	{
		self notify( "bad_path" );
	}
}

/*
	Will walk to the given goal when dist near. Uses AStar path finding with the level's nodes.
*/
doWalk( goal, dist, isScriptGoal )
{
	level endon ( "game_ended" );
	self endon( "kill_goal" );
	self endon( "goal_internal" ); // so that the watchOnGoal notify can happen same frame, not a frame later

	dist *= dist;

	if ( isScriptGoal )
	{
		self thread doWalkScriptNotify();
	}

	self thread killWalkOnEvents();
	self thread watchOnGoal( goal, dist );

	current = self initAStar( goal );

	// skip waypoints we already completed to prevent rubber banding
	if ( current > 0 && self.bot.astar[ current ] == self.bot.last_next_wp && self.bot.astar[ current - 1 ] == self.bot.last_second_next_wp )
	{
		current = self removeAStar();
	}

	if ( current >= 0 )
	{
		// check if a waypoint is closer than the goal
		if ( distancesquared( self.origin, level.waypoints[ self.bot.astar[ current ] ].origin ) < distancesquared( self.origin, goal ) || distancesquared( level.waypoints[ self.bot.astar[ current ] ].origin, physicstrace( self.origin + ( 0, 0, 32 ), level.waypoints[ self.bot.astar[ current ] ].origin, false, self ) ) > 1.0 )
		{
			while ( current >= 0 )
			{
				self.bot.next_wp = self.bot.astar[ current ];
				self.bot.second_next_wp = -1;

				if ( current > 0 )
				{
					self.bot.second_next_wp = self.bot.astar[ current - 1 ];
				}

				self notify( "new_static_waypoint" );

				self movetowards( level.waypoints[ self.bot.next_wp ].origin );
				self.bot.last_next_wp = self.bot.next_wp;
				self.bot.last_second_next_wp = self.bot.second_next_wp;

				current = self removeAStar();
			}
		}
	}

	self.bot.next_wp = -1;
	self.bot.second_next_wp = -1;
	self notify( "finished_static_waypoints" );

	if ( distancesquared( self.origin, goal ) > dist )
	{
		self.bot.last_next_wp = -1;
		self.bot.last_second_next_wp = -1;
		self movetowards( goal ); // any better way??
	}

	self notify( "finished_goal" );

	wait 1;

	if ( distancesquared( self.origin, goal ) > dist )
	{
		self notify( "bad_path_internal" );
	}
}

/*
	Will move towards the given goal. Will try to not get stuck by crouching, then jumping and then strafing around objects.
*/
movetowards( goal )
{
	if ( !isdefined( goal ) )
	{
		return;
	}

	self.bot.towards_goal = goal;

	lastOri = self.origin;
	stucks = 0;
	timeslow = 0;
	time = 0;

	while ( distancesquared( self.origin, goal ) > level.bots_goaldistance )
	{
		self botSetMoveTo( goal );

		if ( time > 3000 )
		{
			time = 0;

			if ( distancesquared( self.origin, lastOri ) < 32 * 32 )
			{
				self thread knife();
				wait 0.5;

				stucks++;

				randomDir = self getRandomLargestStafe( stucks );

				self BotNotifyBotEvent( "stuck" );

				self botSetMoveTo( randomDir );
				wait stucks;
				self stand();

				self.bot.last_next_wp = -1;
				self.bot.last_second_next_wp = -1;
			}

			lastOri = self.origin;
		}
		else if ( timeslow > 0 && ( timeslow % 1000 ) == 0 )
		{
			self thread doMantle();
		}
		else if ( time == 2000 )
		{
			if ( distancesquared( self.origin, lastOri ) < 32 * 32 )
			{
				self crouch();
			}
		}
		else if ( time == 1750 )
		{
			if ( distancesquared( self.origin, lastOri ) < 32 * 32 )
			{
				// check if directly above or below
				if ( abs( goal[ 2 ] - self.origin[ 2 ] ) > 64 && getConeDot( goal + ( 1, 1, 0 ), self.origin + ( -1, -1, 0 ), vectortoangles( ( goal[ 0 ], goal[ 1 ], self.origin[ 2 ] ) - self.origin ) ) < 0.64 && DistanceSquared2D( self.origin, goal ) < 32 * 32 )
				{
					stucks = 2;
				}
			}
		}

		wait 0.05;
		time += 50;

		if ( lengthsquared( self getVelocity() ) < 1000 )
		{
			timeslow += 50;
		}
		else
		{
			timeslow = 0;
		}

		if ( stucks == 2 )
		{
			self notify( "bad_path_internal" );
		}
	}

	self.bot.towards_goal = undefined;
	self notify( "completed_move_to" );
}

/*
	Bots do the mantle
*/
doMantle()
{
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "kill_goal" );

	self jump();

	wait 0.35;

	self jump();
}

/*
	Will return the pos of the largest trace from the bot.
*/
getRandomLargestStafe( dist )
{
	// find a better algo?
	traces = NewHeap( ::HeapTraceFraction );
	myOrg = self.origin + ( 0, 0, 16 );

	traces HeapInsert( bullettrace( myOrg, myOrg + ( -100 * dist, 0, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( 100 * dist, 0, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( 0, 100 * dist, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( 0, -100 * dist, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( -100 * dist, -100 * dist, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( -100 * dist, 100 * dist, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( 100 * dist, -100 * dist, 0 ), false, self ) );
	traces HeapInsert( bullettrace( myOrg, myOrg + ( 100 * dist, 100 * dist, 0 ), false, self ) );

	toptraces = [];

	top = traces.data[ 0 ];
	toptraces[ toptraces.size ] = top;
	traces HeapRemove();

	while ( traces.data.size && top[ "fraction" ] - traces.data[ 0 ][ "fraction" ] < 0.1 )
	{
		toptraces[ toptraces.size ] = traces.data[ 0 ];
		traces HeapRemove();
	}

	return toptraces[ randomint( toptraces.size ) ][ "position" ];
}

/*
	Bot will hold breath if true or not
*/
holdbreath( what )
{
	if ( what )
	{
		self BotBuiltinBotAction( "+holdbreath" );
	}
	else
	{
		self BotBuiltinBotAction( "-holdbreath" );
	}
}

/*
	Bot will knife.
*/
knife()
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_knife" );
	self endon( "bot_knife" );

	self.bot.isknifing = true;
	self.bot.isknifingafter = true;

	self BotBuiltinBotAction( "+melee" );
	wait 0.05;
	self BotBuiltinBotAction( "-melee" );

	self.bot.isknifing = false;

	wait 1;

	self.bot.isknifingafter = false;
}

/*
	Bot will reload.
*/
reload()
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_reload" );
	self endon( "bot_reload" );

	self BotBuiltinBotAction( "+reload" );
	wait 0.05;
	self BotBuiltinBotAction( "-reload" );
}

/*
	Bot will hold the frag button for a time
*/
frag( time )
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_frag" );
	self endon( "bot_frag" );

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	self BotBuiltinBotAction( "+frag" );
	self.bot.isfragging = true;
	self.bot.isfraggingafter = true;

	if ( time )
	{
		wait time;
	}

	self BotBuiltinBotAction( "-frag" );
	self.bot.isfragging = false;

	wait 1.25;
	self.bot.isfraggingafter = false;
}

/*
	Bot will hold the 'smoke' button for a time.
*/
smoke( time )
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_smoke" );
	self endon( "bot_smoke" );

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	self BotBuiltinBotAction( "+smoke" );
	self.bot.issmoking = true;
	self.bot.issmokingafter = true;

	if ( time )
	{
		wait time;
	}

	self BotBuiltinBotAction( "-smoke" );
	self.bot.issmoking = false;

	wait 1.25;
	self.bot.issmokingafter = false;
}

/*
	Bot will fire if true or not.
*/
fire( what )
{
	self notify( "bot_fire" );

	if ( what )
	{
		self BotBuiltinBotAction( "+fire" );
	}
	else
	{
		self BotBuiltinBotAction( "-fire" );
	}
}

/*
	Bot will fire for a time.
*/
pressFire( time )
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_fire" );
	self endon( "bot_fire" );

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	self BotBuiltinBotAction( "+fire" );

	if ( time )
	{
		wait time;
	}

	self BotBuiltinBotAction( "-fire" );
}

/*
	Bot will ads if true or not.
*/
ads( what )
{
	self notify( "bot_ads" );

	if ( what )
	{
		self BotBuiltinBotAction( "+ads" );
	}
	else
	{
		self BotBuiltinBotAction( "-ads" );
	}
}

/*
	Bot will press ADS for a time.
*/
pressADS( time )
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_ads" );
	self endon( "bot_ads" );

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	self BotBuiltinBotAction( "+ads" );

	if ( time )
	{
		wait time;
	}

	self BotBuiltinBotAction( "-ads" );
}

/*
	Bot will press use for a time.
*/
use( time )
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_use" );
	self endon( "bot_use" );

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	self BotBuiltinBotAction( "+activate" );

	if ( time )
	{
		wait time;
	}

	self BotBuiltinBotAction( "-activate" );
}

/*
	Bot will jump.
*/
jump()
{
	self endon( "death" );
	self endon( "disconnect" );
	self notify( "bot_jump" );
	self endon( "bot_jump" );

	if ( self getStance() != "stand" )
	{
		self stand();
		wait 1;
	}

	self BotBuiltinBotAction( "+gostand" );
	wait 0.05;
	self BotBuiltinBotAction( "-gostand" );
}

/*
	Bot will stand.
*/
stand()
{
	self BotBuiltinBotAction( "-gocrouch" );
	self BotBuiltinBotAction( "-goprone" );
}

/*
	Bot will crouch.
*/
crouch()
{
	self BotBuiltinBotAction( "+gocrouch" );
	self BotBuiltinBotAction( "-goprone" );
}

/*
	Bot will prone.
*/
prone()
{
	self BotBuiltinBotAction( "-gocrouch" );
	self BotBuiltinBotAction( "+goprone" );
}

/*
	Bot will move towards here
*/
botSetMoveTo( where )
{
	self.bot.moveto = where;
}

/*
	Bots will look at the pos
*/
bot_lookat( pos, time, vel, doAimPredict )
{
	self notify( "bots_aim_overlap" );
	self endon( "bots_aim_overlap" );
	self endon( "disconnect" );
	self endon( "death" );
	self endon( "spawned_player" );
	level endon ( "game_ended" );

	if ( level.gameended || self.bot.isfrozen || !getcvarint( "bots_play_aim" ) )
	{
		return;
	}

	if ( !isdefined( pos ) )
	{
		return;
	}

	if ( !isdefined( doAimPredict ) )
	{
		doAimPredict = false;
	}

	if ( !isdefined( time ) )
	{
		time = 0.05;
	}

	if ( !isdefined( vel ) )
	{
		vel = ( 0, 0, 0 );
	}

	steps = int( time * 20 );

	if ( steps < 1 )
	{
		steps = 1;
	}

	myEye = self getEyePos(); // get our eye pos

	if ( doAimPredict )
	{
		myEye += vector_scale( vector_scale( self getVelocity(), 0.05 ), steps - 1 ); // account for our velocity

		pos += vector_scale( vector_scale( vel, 0.05 ), steps - 1 ); // add the velocity vector
	}

	myAngle = self getplayerangles();
	angles = vectortoangles( ( pos - myEye ) - anglestoforward( myAngle ) );

	X = AngleClamp180( angles[ 0 ] - myAngle[ 0 ] );
	X = X / steps;

	Y = AngleClamp180( angles[ 1 ] - myAngle[ 1 ] );
	Y = Y / steps;

	for ( i = 0; i < steps; i++ )
	{
		myAngle = ( AngleClamp180( myAngle[ 0 ] + X ), AngleClamp180( myAngle[ 1 ] + Y ), 0 );
		self setplayerangles( myAngle );
		wait 0.05;
	}
}
