// Helper functions for bots

#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\bots\_bots_personality;
#include maps\mp\bots\_bots_strategy;

/*
=============
///ScriptDocBegin
"Name: bot_get_nodes_in_cone( <max_dist> , <vector_dot> , <only_visible_nodes> )"
"Summary: Gets all nodes in the cone in front of the bot, up to max dist and obeying vector_dot"
"CallOn: A bot player"
"MandatoryArg: <min_dist>: Min dist of cone"
"MandatoryArg: <max_dist>: Max dist of cone"
"MandatoryArg: <vector_dot>: Nodes within this vector dot of the bot's direction will be in cluded"
"MandatoryArg: <only_visible_nodes>: Only return nodes visible to the bot"
"Example: nodes_in_cone = self bot_get_nodes_in_cone( 750, 0.6, true );"
///ScriptDocEnd
=============
*/
bot_get_nodes_in_cone( min_dist, max_dist, vector_dot, only_visible_nodes )
{
	nodes_around_bot = GetNodesInRadius( self.origin, max_dist, min_dist );
	nodes_in_cone = [];
	
	nearest_node_to_bot = self GetNearestNode();
	bot_dir = AnglesToForward( self GetPlayerAngles() );
	bot_dir_norm = VectorNormalize( bot_dir * (1,1,0) );
	
	foreach( node in nodes_around_bot )
	{
		bot_to_node_norm = VectorNormalize( (node.origin - self.origin) * (1,1,0) );
		dot = VectorDot( bot_to_node_norm, bot_dir_norm );
		if ( dot > vector_dot )
		{
			if ( !only_visible_nodes || (IsDefined(nearest_node_to_bot) && NodesVisible( node, nearest_node_to_bot, true )) )
				nodes_in_cone[nodes_in_cone.size] = node;
		}
	}
	
	return nodes_in_cone;
}

/* 
=============
///ScriptDocBegin
"Name: bot_goal_can_override( <goal_type_1>, <goal_type_2> )"
"Summary: Returns true if goal_type_1 can override goal_type_2"
"CallOn: A bot player"
"MandatoryArg: <goal_type_1> : The goal to test"
"MandatoryArg: <goal_type_2> : The goal to test against"
"Example: can_override_goal = bot_goal_can_override( goal_type, current_goal_type );"
///ScriptDocEnd
============
 */
bot_goal_can_override( goal_type_1, goal_type_2 )
{
	if ( goal_type_1 == "none" )
	{
		// "none" can only override "none"
		return (goal_type_2 == "none");
	}
	else if ( goal_type_1 == "hunt" )
	{
		// "hunt" can override "hunt" or "none"
		return (goal_type_2 == "hunt" || goal_type_2 == "none");
	}
	else if ( goal_type_1 == "guard" )
	{
		// "guard" can override "guard", "hunt", or "none"
		return (goal_type_2 == "guard" || goal_type_2 == "hunt" || goal_type_2 == "none");
	}
	else if ( goal_type_1 == "objective" )
	{
		// "objective" can override "objective", "guard", "hunt", or "none"
		return (goal_type_2 == "objective" || goal_type_2 == "guard" || goal_type_2 == "hunt" || goal_type_2 == "none");
	}
	else if ( goal_type_1 == "critical" )
	{
		// "critical" can override "critical", "objective", "guard", "hunt", or "none"
		return (goal_type_2 == "critical" || goal_type_2 == "objective" || goal_type_2 == "guard" || goal_type_2 == "hunt" || goal_type_2 == "none");
	}
	else if ( goal_type_1 == "tactical" )
	{
		// "tactical" can override everything
		return true;
	}
	
	AssertEx( false, "Unsupported parameter <goal_type_1> passed in to bot_goal_can_override()" );
}

/* 
=============
///ScriptDocBegin
"Name: bot_set_personality( <personality> )"
"Summary: Sets the personality for this bot.  Necessary because script needs to re-assign function pointers, etc."
"CallOn: A bot player"
"MandatoryArg: <personality> : The personality to set"
"Example: self bot_set_personality("camper")"
///ScriptDocEnd
============
 */
bot_set_personality(personality)
{
	self BotSetPersonality( personality );
	self bot_assign_personality_functions();
	self BotClearScriptGoal();
}

/* 
=============
///ScriptDocBegin
"Name: bot_set_difficulty( <difficulty>, <force_follow_dev_dvar> )"
"Summary: Sets the difficulty for this bot."
"CallOn: A bot player"
"MandatoryArg: <personality> : The difficulty to set"
"OptionalArg: <force_follow_dev_dvar> : If set, will force the bot/agent to follow the bot_DebugDifficulty dvar if it is set"
"Example: self bot_set_difficulty("hardened")"
///ScriptDocEnd
============
 */
bot_set_difficulty( difficulty, force_follow_dev_dvar )
{
	assert( IsAI( self ) );
	
/#
	if ( IsTeamParticipant(self) || (IsDefined(force_follow_dev_dvar) && force_follow_dev_dvar) )
	{
		// Difficulty cannot be changed when using bot_DebugDifficulty dvar override
	    debugDifficulty = GetDvar( "bot_DebugDifficulty" );
	    if ( debugDifficulty != "default" )
	    {
	        difficulty = debugDifficulty;
	    }
	}
#/
	
	// Choose difficulty if need be
	if ( difficulty == "default" )
		difficulty = self bot_choose_difficulty_for_default();
	
	previous_difficulty = self BotGetDifficulty();
	self BotSetDifficulty( difficulty );
	
	if ( IsPlayer( self ) && previous_difficulty != difficulty )
	{
		self set_rank_xp_and_prestige_for_bot();
		rankID = self maps\mp\gametypes\_rank::getRankForXp( self maps\mp\gametypes\_rank::getTotalXP() );
		self.pers[ "rank" ] = rankID;
		prestige = self.pers["prestige"];
		self setRank( rankID, prestige );
	}
}


/* 
=============
///ScriptDocBegin
"Name: bot_choose_difficulty_for_default()"
"Summary: Chooses a difficulty when difficulty is set to "default""
"CallOn: A bot player"
"Example: difficulty = self bot_choose_difficulty_for_default();"
///ScriptDocEnd
============
 */
bot_choose_difficulty_for_default( )
{
	if ( !IsDefined( level.bot_difficulty_defaults ) )
	{
		level.bot_difficulty_defaults = [];
		level.bot_difficulty_defaults[level.bot_difficulty_defaults.size] = "recruit";
		level.bot_difficulty_defaults[level.bot_difficulty_defaults.size] = "regular";
		level.bot_difficulty_defaults[level.bot_difficulty_defaults.size] = "hardened";	
	}
		
	difficulty = self.pers["bot_chosen_difficulty"];
		
	if ( !IsDefined( difficulty ) )
	{
		inUseCount = [];

		team = self.team;
		if ( !IsDefined( team ) )
			team = self.bot_team;
		if ( !IsDefined( team ) )
			team = self.pers["team"];
		
		if ( !IsDefined( team ) )
			team = "allies";
		
		foreach ( player in level.players )
		{
			if ( player == self )
				continue;
			
			if ( !isAI( player ) )
				continue;
			
			usedDifficulty = player BotGetDifficulty();
			if ( usedDifficulty == "default" )
				continue;
			
			otherTeam = player.team;
			if ( !IsDefined( otherTeam ) )
				otherTeam = player.bot_team;
			if ( !IsDefined( otherTeam ) )
				otherTeam = player.pers["team"];
			
			if ( !IsDefined( otherTeam ) )
				continue;
			
			if ( !IsDefined( inUseCount[otherTeam] ) )
			    inUseCount[otherTeam] = [];

			if ( !IsDefined( inUseCount[otherTeam][usedDifficulty] ) )
				inUseCount[otherTeam][usedDifficulty] = 1;
			else
				inUseCount[otherTeam][usedDifficulty]++;
		}

		lowest = -1;
		
		foreach ( choice in level.bot_difficulty_defaults )	
		{
			if ( !IsDefined( inUseCount[team] ) || !IsDefined( inUseCount[team][choice] ) )
			{
				difficulty = choice;
				break;
			}
			else if ( lowest == -1 || inUseCount[team][choice] < lowest )
			{
				lowest = inUseCount[team][choice];
				difficulty = choice;
			}
		}
	}
		
	if ( IsDefined( difficulty ) )
		self.pers["bot_chosen_difficulty"] = difficulty;
	
	return difficulty;
}


/* 
=============
///ScriptDocBegin
"Name: bot_is_capturing()"
"Summary: Checks if this bot is capturing a point or zone"
"CallOn: A bot player"
"Example: if ( self bot_is_capturing() )"
///ScriptDocEnd
============
 */
bot_is_capturing()
{
	if ( self bot_is_defending() )
	{
		if ( self.bot_defending_type == "capture" || self.bot_defending_type == "capture_zone" )
		{
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_patrolling()"
"Summary: Checks if this bot is patrolling a point"
"CallOn: A bot player"
"Example: if ( self bot_is_patrolling() )"
///ScriptDocEnd
============
 */
bot_is_patrolling()
{
	if ( self bot_is_defending() )
	{
		if ( self.bot_defending_type == "patrol" )
		{
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_protecting()"
"Summary: Checks if this bot is protecting a point"
"CallOn: A bot player"
"Example: if ( self bot_is_protecting() )"
///ScriptDocEnd
============
 */
bot_is_protecting()
{
	if ( self bot_is_defending() )
	{
		if ( self.bot_defending_type == "protect" )
		{
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_bodyguarding()"
"Summary: Checks if this bot is a bodyguard"
"CallOn: A bot player"
"Example: if ( self bot_is_bodyguarding() )"
///ScriptDocEnd
============
 */
bot_is_bodyguarding()
{
	if ( self bot_is_defending() )
	{
		if ( self.bot_defending_type == "bodyguard" )
		{
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_defending()"
"Summary: Checks if this bot is defending"
"CallOn: A bot player"
"Example: if ( self bot_is_defending() )"
///ScriptDocEnd
============
 */
bot_is_defending()
{
	return ( IsDefined( self.bot_defending ) );
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_defending_point( <point> )"
"Summary: Checks if this bot is defending a specific point"
"CallOn: A bot player"
"MandatoryArg: <point> : The point to check"
"Example: if ( !self bot_is_defending_point(level.sdBombModel.origin) )"
///ScriptDocEnd
============
 */
bot_is_defending_point(point)
{
	if ( self bot_is_defending() )
	{
		if ( bot_vectors_are_equal(self.bot_defending_center,point) )
		{
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_guarding_player( <player> )"
"Summary: Checks if this bot is guarding a specific player"
"CallOn: A bot player"
"MandatoryArg: <player> : The player to check"
"Example: if ( !self bot_is_guarding_player( self.owner ) )"
///ScriptDocEnd
============
 */
bot_is_guarding_player( player )
{
	if ( self bot_is_bodyguarding() && self.bot_defend_player_guarding == player )
		return true;
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: entrance_visible_from( <entrance_origin>, <from_origin>, <stance> )"
"Summary: Checks if the specified <entrance_origin> is visible from the <from_origin> with the given <stance>"
"MandatoryArg: <entrance_origin> : Origin of the entrance node"
"MandatoryArg: <from_origin> : Origin to check visibility from"
"MandatoryArg: <stance> : The stance at the <from_origin> to check"
"Example: entrance.prone_visible_from[index] = entrance_visible_from( entrance.origin, origin_array[i], "prone" );"
///ScriptDocEnd
============
 */
entrance_visible_from( entrance_origin, from_origin, stance )
{
	assert( (stance == "stand") || (stance == "crouch") || stance == ("prone") );
	
	prone_offset = (0,0,11);
	crouch_offset = (0,0,40);
	
	offset = undefined;
	if ( stance == "stand" )
		return true;
	else if ( stance == "crouch" )
		offset = crouch_offset;
	else if ( stance == "prone" )
		offset = prone_offset;
	
	return SightTracePassed( from_origin+offset, entrance_origin+offset, false, undefined );
}

/* 
=============
///ScriptDocBegin
"Name: get_extended_path( <start>, <end> )"
"Summary: Gets a "wider" path from start to end.  Includes all the nodes in the direct path, plus any nodes that are linked along the way"
"MandatoryArg: <start> : The start location"
"MandatoryArg: <end> : The end location"
"Example: path = get_extended_path( start, end );"
///ScriptDocEnd
============
 */
get_extended_path( start, end )
{
	path = func_get_nodes_on_path( start, end );
	if ( IsDefined( path ) )
	{
		path = remove_ends_from_path( path );
		path = get_all_connected_nodes( path );
	}
	
	return path;
}

/* 
=============
///ScriptDocBegin
"Name: func_get_path_dist( <start>, <end> )"
"Summary: threadable call to GetPathDist() native function"
"MandatoryArg: <start> : The start location"
"MandatoryArg: <end> : The end location"
"Example: path = func_get_path_dist( start, end );"
///ScriptDocEnd
============
 */
func_get_path_dist( start, end )
{
	return GetPathDist( start, end );
}
	
/* 
=============
///ScriptDocBegin
"Name: func_get_nodes_on_path( <start>, <end> )"
"Summary: threadable call to GetNodesOnPath() native function"
"MandatoryArg: <start> : The start location"
"MandatoryArg: <end> : The end location"
"Example: path = func_get_nodes_on_path( start, end );"
///ScriptDocEnd
============
 */
func_get_nodes_on_path( start, end )
{
	return GetNodesOnPath( start, end );
}

/* 
=============
///ScriptDocBegin
"Name: func_bot_get_closest_navigable_point( <origin>, <radius>, <entity> )"
"Summary: threadable call to BotGetClosestNavigablePoint() native function"
"MandatoryArg: <origin> : The point to search around"
"MandatoryArg: <radius> : The max distance around the point to search"
"OptionalArg: <entity> The entity whose clip mask we will be using"
"Example: nearest_point = func_bot_get_closest_navigable_point(crate.origin, player_use_radius);"
///ScriptDocEnd
============
 */
func_bot_get_closest_navigable_point( origin, radius, entity )
{
	return BotGetClosestNavigablePoint( origin, radius, entity );
}

/* 
=============
///ScriptDocBegin
"Name: node_is_on_path_from_labels( <label1>, <label2> )"
"Summary: Checks if the node is contained in the path from label1 to label2"
"Summary: The labels correspond to elements in the level.entrance_indices, which match up with origins in level.entrance_origin_points"
"MandatoryArg: <label1> :  The first label"
"MandatoryArg: <label2> :  The second label"
"Example: if ( node node_is_on_path_from_labels(self.current_flag, flag_complete_label) )"
///ScriptDocEnd
============
 */
node_is_on_path_from_labels( label1, label2 )
{
	if ( !IsDefined( self.on_path_from ) )
		return false;
	
	if ( IsDefined( self.on_path_from[label1] ) && IsDefined( self.on_path_from[label1][label2] ) && self.on_path_from[label1][label2] )
	    return true;
	
	if ( IsDefined( self.on_path_from[label2] ) && IsDefined( self.on_path_from[label2][label1] ) && self.on_path_from[label2][label1] )
	    return true;

	return false;
}

/* 
=============
///ScriptDocBegin
"Name: get_all_connected_nodes( <nodes> )"
"Summary: Returns the array nodes and all nodes connected to them"
"MandatoryArg: <nodes> : The array of nodes"
"Example: all_nodes = get_all_connected_nodes(path);"
///ScriptDocEnd
============
 */
get_all_connected_nodes( nodes )
{
	all_nodes = nodes;
	
	for ( i = 0; i < nodes.size; i++ )
	{
		//bot_draw_cylinder(nodes[i].origin, 10, 20, 20, undefined, (0,0,1), true, 4);

		linked_nodes = GetLinkedNodes( nodes[i] );
		for ( j = 0; j < linked_nodes.size; j++ )
		{
			if ( !array_contains( all_nodes, linked_nodes[j] ) )
			{
				all_nodes = array_add( all_nodes, linked_nodes[j] );
				
				//line( nodes[i].origin, linked_nodes[j].origin, (0,1,0), 1.0, true, 20*20 );
				//bot_draw_cylinder(linked_nodes[j].origin, 10, 10, 20, undefined, (0,1,0), true, 4);
			}
		}
	}
	
	return all_nodes;
}

/* 
=============
///ScriptDocBegin
"Name: get_visible_nodes_array( <nodes>, <node_from> )"
"Summary: Returns all nodes in the <nodes> array that are visible from <node_from> using node visibility"
"MandatoryArg: <nodes> : The array of nodes"
"MandatoryArg: <node_from> : The node we're looking from"
"Example: visible_nodes = get_visible_nodes_array( nodes, current_nearest_node );"
///ScriptDocEnd
============
 */
get_visible_nodes_array( nodes, node_from )
{
	visible_nodes = [];
	
	foreach( node in nodes )
	{
		if ( NodesVisible( node, node_from, true ) )
			visible_nodes = array_add( visible_nodes, node );
	}
	
	return visible_nodes;
}

/* 
=============
///ScriptDocBegin
"Name: remove_ends_from_path( <path> )"
"Summary: Removes the first and last node from the path and returns the resulting array"
"MandatoryArg: <path> : The array of nodes in the path"
"Example: path = remove_ends_from_path(path);"
///ScriptDocEnd
============
 */
remove_ends_from_path( path )
{
	path[path.size-1] = undefined;
	path[0] = undefined;

	return array_removeUndefined( path );
}

/* 
=============
///ScriptDocBegin
"Name: bot_waittill_bots_enabled( <only_team_participants> )"
"Summary: Waits until bots are enabled (until bot_AutoConnectDefault is 1 or bots are added via the devgui)"
"OptionalArg: <only_team_participants> : Only count bots or agents that actually participate in team activities (planting bombs, etc)"
"Example: bot_waittill_bots_enabled();"
///ScriptDocEnd
============
 */
bot_waittill_bots_enabled( only_team_participants )
{
	keep_looping = true;
	while( !bot_bots_enabled_or_added( only_team_participants ) )
	{
		wait(0.5);
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_bots_enabled_or_added( <only_team_participants> )"
"Summary: Return true if bots are enabled (bot_AutoConnectDefault is 1 or bots have been added via the devgui)"
"OptionalArg: <only_team_participants> : Only count bots or agents that actually participate in team activities (planting bombs, etc)"
"Example: if ( bot_bots_enabled_or_added() )"
///ScriptDocEnd
============
 */
bot_bots_enabled_or_added( only_team_participants )
{
	if ( BotAutoConnectEnabled() )
		return true;
	
	if(IsDefined(level.ai_game_mode) && level.ai_game_mode)
		return true;
	
	if ( bots_exist( only_team_participants ) )
		return true;
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_waittill_out_of_combat_or_time( <time> )"
"Summary: Waits until this bot is out of combat (or the time limit is reached), then returns"
"OptionalArg: <time> : The time (in ms) before we return"
"Example: self bot_waittill_out_of_combat_or_time();"
///ScriptDocEnd
============
 */
bot_waittill_out_of_combat_or_time( time )
{
	start_time = GetTime();
	
	while( 1 )
	{
		if ( IsDefined( time ) )
		{
			if ( GetTime() > start_time + time )
				return;
		}
		
		if ( !IsDefined(self.enemy) )
		{
			return;
		}
		else
		{
			if ( !self bot_in_combat() )
				return;
		}
		
		wait(0.05);
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_in_combat( <optional_time> )"
"Summary: Checks if this bot is in combat (has seen an enemy recently)"
"OptionalArg: <optional_time> : How long the enemy has to be out of view to consider the bot out of combat"
"Example: if ( self bot_in_combat() )"
///ScriptDocEnd
============
 */
bot_in_combat( optional_time )
{
	time_since_last_saw_enemy = GetTime() - self.last_enemy_sight_time;
	
	check_time = level.bot_out_of_combat_time;
	if ( IsDefined(optional_time) )
		check_time = optional_time;
	
	return ( time_since_last_saw_enemy < check_time );
}

/* 
=============
///ScriptDocBegin
"Name: bot_waittill_goal_or_fail( <optional_time>, <optional_param_1>, <optional_param_2> )"
"Summary: Waits until this bot reaches his goal or is interrupted, and returns the result"
"Summary: By default this waits for "goal", "bad_path", "node_relinquished", and "script_goal_changed""
"OptionalArg: <optional_time> : The time (in seconds) before returning"
"OptionalArg: <optional_param_1> : An extra parameter to wait on"
"OptionalArg: <optional_param_2> : An extra parameter to wait on"
"Example: pathResult = self bot_waittill_goal_or_fail();"
///ScriptDocEnd
============
 */
bot_waittill_goal_or_fail( optional_time, optional_param_1, optional_param_2 )
{
	if ( !IsDefined(optional_param_1) && IsDefined(optional_param_2) )
	{
		AssertEx( false, "Error: Calling bot_waittill_goal_or_fail needs to define param 1 if using param 2" );
	}
	
	wait_array = [ "goal", "bad_path", "no_path", "node_relinquished", "script_goal_changed" ];
	if ( IsDefined(optional_param_1) )
		wait_array[wait_array.size] = optional_param_1;
	if ( IsDefined(optional_param_2) )
		wait_array[wait_array.size] = optional_param_2;
	
	if ( IsDefined( optional_time ) )
		result = self waittill_any_in_array_or_timeout( wait_array, optional_time );
	else
		result = self waittill_any_in_array_return( wait_array );
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_usebutton_wait( <time>, <self_notify_1>, <self_notify_2> )"
"Summary: Waits until this bot releases the use button, or the time expires, or the bot gets a notify.  Returns the notify that ended it"
"OptionalArg: <time> : The time (in ms) before we return"
"OptionalArg: <self_notify_1> : Return if this is notified on the self"
"OptionalArg: <self_notify_2> : Return if this is notified on the self"
"Example: self bot_usebutton_wait( time, "bomb_planted" );"
///ScriptDocEnd
============
 */
bot_usebutton_wait( time, self_notify_1, self_notify_2 )
{
	level endon( "game_ended" );
	
	self childthread use_button_stopped_notify();
	
	result = self waittill_any_timeout( time, self_notify_1, self_notify_2, "use_button_no_longer_pressed", "finished_use" );
	self notify("stop_usebutton_watcher");
	return result;
}
	
use_button_stopped_notify(self_notify_1, self_notify_2)
	{
	self endon("stop_usebutton_watcher");

	wait(0.05);	// Wait a frame for the use button to be pressed initially
	while(self UseButtonPressed())
	{
		wait(0.05);
	}
	self notify("use_button_no_longer_pressed");
}

/* 
=============
///ScriptDocBegin
"Name: bots_exist( <only_team_participants> )"
"Summary: Checks if bots exist in the level"
"OptionalArg: <only_team_participants> : Only count bots or agents that actually participate in team activities (planting bombs, etc)"
"Example: if ( bots_exist() )"
///ScriptDocEnd
============
 */
bots_exist( only_team_participants )
{
	foreach(player in level.participants)
	{
		if ( IsAI(player) )
		{
			if ( IsDefined(only_team_participants) && only_team_participants )
			{
				if ( !IsTeamParticipant(player) )
					continue;
			}
			
			return true;
		}
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_entrances_for_stance_and_index( <stance>, <index>, <searching_from_goal> )"
"Summary: Gets entrance points for the given stance and index.  Returns an array of nodes"
"OptionalArg: <stance> : "stand", "crouch", or "prone" "
"MandatoryArg: <index> : Index into level.entrance_points: "flag_a", "zone_1", etc."
"OptionalArg: <searching_from_goal> : If this search is coming from a bot at his goal position"
"Example: return bot_get_entrances_for_stance_and_index(self.cur_defend_stance,self.defend_entrance_index);"
///ScriptDocEnd
============
 */
bot_get_entrances_for_stance_and_index( stance, index, searching_from_goal )
{
	assert( !IsDefined(stance) || (stance == "stand") || (stance == "crouch") || stance == ("prone") );
	
	if ( !IsDefined(level.entrance_points_finished_caching) && !IsDefined( self.defense_override_entrances ) )
		return undefined;
	
	assert( IsDefined(index) );
	if ( IsArray(index) )
	{
		// We had an array of indices, instead of just one.  So we need to pick one
		if ( IsDefined(searching_from_goal) && searching_from_goal )
		{
			// If searching from the goal, pick the entrance index that corresponds to the closest entrance origin point
			closest_label = undefined;
			dist_to_closest_label = 999999999;
			foreach ( label in index )
			{
				index_into_origin_points = array_find( level.entrance_indices, label );
				origin_for_label = level.entrance_origin_points[index_into_origin_points];
				dist_to_label_sq = DistanceSquared(self.origin, origin_for_label);
				if ( dist_to_label_sq < dist_to_closest_label )
				{
					closest_label = label;
					dist_to_closest_label = dist_to_label_sq;
				}
			}
			
			index = closest_label;
			assert( IsDefined(index) );
		}
		else
		{
			// If not searching from the goal, just pick one at random (its not as important in this case)
			index = Random(index);
		}
	}
	assert( (IsDefined(level.entrance_points) && IsDefined(level.entrance_points[index])) || IsDefined(self.defense_override_entrances) );
	
	entrances = [];
	if ( IsDefined( self.defense_override_entrances ) )
		entrances = self.defense_override_entrances;
	else
		entrances = level.entrance_points[index];
	
	if ( !IsDefined( stance ) || (stance == "stand") )
	{
		return entrances;
	}
	else if ( stance == "crouch" )
	{
		acceptable_nodes = [];
		foreach( node in entrances )
		{
			if ( node.crouch_visible_from[index] )
				acceptable_nodes = array_add(acceptable_nodes, node );
		}
		
		return acceptable_nodes;
	}
	else if ( stance == "prone" )
	{
		acceptable_nodes = [];
		foreach( node in entrances )
		{
			if ( node.prone_visible_from[index] )
				acceptable_nodes = array_add(acceptable_nodes, node );
		}
		
		return acceptable_nodes;	
	}
	
	return undefined;
}

SCR_CONST_GUARD_NODE_TOO_CLOSE_DIST_SQ = 100 * 100;

/* 
=============
///ScriptDocBegin
"Name: bot_find_node_to_guard_player( <center_of_search>, <radius>, <opposite_side_of_player> )"
"Summary: Looks through the nodes in the radius and finds node that would be good to use to defend a player"
"CallOn: A bot player"
"MandatoryArg: <center_of_search> : The center point for the node search"
"MandatoryArg: <radius> : The radius of the node search"
"OptionalArg: <opposide_side_of_player> : If defined and true, will pick a node on the opposide side of the player"
"Example: node = self bot_find_node_to_guard_player( self.bot_defending_center, self.bot_defending_radius );"
///ScriptDocEnd
============
 */
bot_find_node_to_guard_player( center_of_search, radius, opposide_side_of_player )
{
	result = undefined;
	
	player_guarding_velocity = self.bot_defend_player_guarding GetVelocity();
	if ( LengthSquared( player_guarding_velocity ) > 100 )
	{
		// Player we're guarding is moving, so widen radius
		all_nodes_raw = GetNodesInRadius( center_of_search, radius * 1.75, radius * 0.5, 500 );
		
		// Cull nodes that aren't in the direction the player is moving
		all_nodes = [];
		normalized_velocity = VectorNormalize(player_guarding_velocity);
		for ( i = 0; i < all_nodes_raw.size; i++ )
		{
			player_to_node = VectorNormalize( all_nodes_raw[i].origin - self.bot_defend_player_guarding.origin );
			if ( VectorDot( player_to_node, normalized_velocity ) > 0.1 )
				all_nodes[all_nodes.size] = all_nodes_raw[i];
		}
	}
	else
	{
		all_nodes = GetNodesInRadius( center_of_search, radius, 0, 500 );
	}
	
	if ( IsDefined(opposide_side_of_player) && opposide_side_of_player )
	{
		bot_to_player = VectorNormalize( self.bot_defend_player_guarding.origin - self.origin );
		all_nodes_old = all_nodes;
		all_nodes = [];
		foreach( node in all_nodes_old )
		{
			player_to_node = VectorNormalize(node.origin - self.bot_defend_player_guarding.origin);
			if ( VectorDot(bot_to_player,player_to_node) > 0.2 )
				all_nodes[all_nodes.size] = node;
		}
	}
	
	// Remove any nodes that are really close to the center
	nodes_not_close = [];
	nodes_same_elevation = [];
	nodes_not_close_same_elevation = [];
	for ( i = 0; i < all_nodes.size; i++ )
	{
		add_to_nodes_not_close_array = DistanceSquared(all_nodes[i].origin,center_of_search) > SCR_CONST_GUARD_NODE_TOO_CLOSE_DIST_SQ;
		add_to_same_elevation_array = abs( all_nodes[i].origin[2] - self.bot_defend_player_guarding.origin[2] ) < 50;
		
		if ( add_to_nodes_not_close_array )
			nodes_not_close[nodes_not_close.size] = all_nodes[i];
		
		if ( add_to_same_elevation_array )
			nodes_same_elevation[nodes_same_elevation.size] = all_nodes[i];
		
		if ( add_to_nodes_not_close_array && add_to_same_elevation_array )
			nodes_not_close_same_elevation[nodes_not_close_same_elevation.size] = all_nodes[i];
		
		// Only process a max of 100 nodes per frame
		if ( i % 100 == 99 )
			wait(0.05);	
	}
	
	// First try the nodes that are on the same elevation and not near the center
	if ( nodes_not_close_same_elevation.size > 0 )
		result = self BotNodePick( nodes_not_close_same_elevation, nodes_not_close_same_elevation.size * 0.15, "node_capture", center_of_search, undefined, self.defense_score_flags );
	
	// If necessary, next try the nodes that are on the same elevation, regardless of their distance from the center
	if ( !IsDefined(result)  )
	{
		wait(0.05);
		if ( nodes_same_elevation.size > 0 )
			result = self BotNodePick( nodes_same_elevation, nodes_same_elevation.size * 0.15, "node_capture", center_of_search, undefined, self.defense_score_flags );
	
		// If necessary, finally try all the nodes, regardless of elevation, as long as they are not near the center
		if ( !IsDefined(result) && nodes_not_close.size > 0 )
		{
			wait(0.05);
			result = self BotNodePick( nodes_not_close, nodes_not_close.size * 0.15, "node_capture", center_of_search, undefined, self.defense_score_flags );
		}
	}
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_find_node_to_capture_point( <center_of_search>, <radius>, <point_to_face> )"
"Summary: Looks through the nodes in the radius and finds node that would be good to use to capture the point"
"CallOn: A bot player"
"MandatoryArg: <center_of_search> : The center point for the node search"
"MandatoryArg: <radius> : The radius of the node search"
"OptionalArg: <point_to_face> : The point that the node needs to face"
"Example: node = self bot_find_node_to_capture_point( self.bot_defending_center, max_node_dist, entrance_point );"
///ScriptDocEnd
============
 */
bot_find_node_to_capture_point( center_of_search, radius, point_to_face )
{
	result = undefined;
	
	all_nodes = GetNodesInRadius( center_of_search, radius, 0, 500 );
	if ( all_nodes.size > 0 )
		result = self BotNodePick( all_nodes, all_nodes.size * 0.15, "node_capture", center_of_search, point_to_face, self.defense_score_flags );
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_find_node_to_capture_zone( <nodes>, <point_to_face> )"
"Summary: Looks through the nodes in the zone and finds node that would be good to use to capture the zone"
"CallOn: A bot player"
"MandatoryArg: <nodes> : An array of nodes in the zone"
"OptionalArg: <point_to_face> : The point that the node needs to face"
"Example: node = self bot_find_node_to_capture_zone( nodes, entrance_point );"
///ScriptDocEnd
============
 */
bot_find_node_to_capture_zone( nodes, point_to_face )
{
	result = undefined;
	
	if ( nodes.size > 0 )
		result = self BotNodePick( nodes, nodes.size * 0.15, "node_capture", undefined, point_to_face, self.defense_score_flags );
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_find_node_that_protects_point( <center_of_search>, <radius> )"
"Summary: Looks through the nodes in the radius and finds node that would be good to use to protect the point"
"CallOn: A bot player"
"MandatoryArg: <center_of_search> : The center point for the node search"
"MandatoryArg: <radius> : The radius of the node search"
"Example: node = self bot_find_node_that_protects_point( self.bot_defending_center, max_node_dist );"
///ScriptDocEnd
============
 */
bot_find_node_that_protects_point( center_of_search, radius )
{
	result = undefined;
	
	all_nodes = GetNodesInRadius( center_of_search, radius, 0, 500 );
	if ( all_nodes.size > 0 )
		result = self BotNodePick( all_nodes, all_nodes.size * 0.15, "node_protect", center_of_search, self.defense_score_flags );
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_pick_random_point_in_radius(<center_point>, <radius>, <point_test_func>, <close_dist>, <far_dist>)"
"Summary: Finds a random point in the radius around center_point.  First tries to find a point halfway between valid nodes.  If that fails, finds a completely random point"
"CallOn: A bot player"
"MandatoryArg: <center_point> : The center point for the search"
"MandatoryArg: <radius> : The radius of the search"
"OptionalArg: <point_test_func> : Optional test function, to disqualify points that other bots might be using"
"OptionalArg: <close_dist> : When doing random search, don't find nodes closer than close_dist * radius"
"OptionalArg: <far_dist> : When doing random search, don't find nodes farther than far_dist * radius"
"Example: self.cur_defend_point_override = self bot_pick_random_point_in_radius(self.bot_defending_center,self.bot_defending_radius,::bot_can_use_point_in_defend,0.15,0.9);"
///ScriptDocEnd
============
 */
bot_pick_random_point_in_radius(center_point, node_radius, point_test_func, close_dist, far_dist)
{
	point_picked = undefined;
	
	// try picking two random nodes in the radius and finding the midpoint of them
	nodes = GetNodesInRadius( center_point, node_radius, 0, 500 );
	if ( IsDefined( nodes ) && nodes.size >= 2 )
		point_picked = bot_find_random_midpoint( nodes, point_test_func );
	
	if ( !IsDefined(point_picked) )
	{
		if ( !IsDefined(close_dist) )
			close_dist = 0;
		if ( !IsDefined(far_dist) )
			far_dist = 1;
		
		// Pick a point completely at random
		rand_dist = RandomFloatRange(self.bot_defending_radius*close_dist, self.bot_defending_radius*far_dist);
		rand_dir = AnglesToForward((0,RandomInt(360),0));
		point_picked = center_point + rand_dir*rand_dist;
	}
	
	return point_picked;
}

/* 
=============
///ScriptDocBegin
"Name: bot_pick_random_point_from_set(<center_point>, <node_set>, <point_test_func>)"
"Summary: Finds a random point in the radius around center_point.  First tries to find a point halfway between valid nodes.  If that fails, finds a completely random point"
"CallOn: A bot player"
"MandatoryArg: <center_point> : The center point for the search"
"MandatoryArg: <node_set> : The set of nodes to pick from"
"OptionalArg: <point_test_func> : Optional test function, to disqualify points that other bots might be using"
"Example: self.cur_defend_point_override = self bot_pick_random_point_from_set(self.bot_defending_center,nodes,::bot_can_use_point_in_defend);"
///ScriptDocEnd
============
 */
bot_pick_random_point_from_set(center_point, node_set, point_test_func)
{
	point_picked = undefined;
	
	// try picking two random nodes in the set and finding the midpoint of them
	if ( node_set.size >= 2 )
		point_picked = bot_find_random_midpoint( node_set, point_test_func );
	
	if ( !IsDefined(point_picked) )
	{
		// Pick a point completely at random
		rand_node_picked = Random(node_set);
		vec_to_rand_node = rand_node_picked.origin - center_point;
		
		point_picked = center_point + VectorNormalize(vec_to_rand_node) * Length(vec_to_rand_node) * RandomFloat(1.0);
	}
	
	return point_picked;
}

bot_find_random_midpoint( nodes, point_test_func )
{
	point_picked = undefined;
	nodes_randomized = array_randomize(nodes);
	
	for ( i=0; i<nodes_randomized.size; i++ )
	{
		for ( j=i+1; j<nodes_randomized.size; j++ )
		{
			node1 = nodes_randomized[i];
			node2 = nodes_randomized[j];
			if ( NodesVisible(node1,node2,true) )
			{
				point_picked = ((node1.origin[0] + node2.origin[0])*0.5,(node1.origin[1] + node2.origin[1])*0.5,(node1.origin[2] + node2.origin[2])*0.5);
				if ( IsDefined(point_test_func) && (self [[point_test_func]](point_picked) == true) )
					return point_picked;
			}
		}
	}
	
	return point_picked;
}

/* 
=============
///ScriptDocBegin
"Name: defend_valid_center()"
"Summary: Returns a center point for the defense that is on the path grid"
"Summary: This is either the absolute center or the optional node that was passed in to use as the center"
"CallOn: A bot player"
"Example: center = self defend_valid_center()"
///ScriptDocEnd
============
 */
defend_valid_center()
{
	if ( IsDefined(self.bot_defending_override_origin_node) )
		return self.bot_defending_override_origin_node.origin;
	else if ( IsDefined(self.bot_defending_center) )
		return self.bot_defending_center;
	
	return undefined;
}

/* 
=============
///ScriptDocBegin
"Name: bot_allowed_to_use_killstreaks()"
"Summary: Checks if a bot is allowed to use killstreaks"
"CallOn: A bot player"
"Example: if ( self bot_allowed_to_use_killstreaks() )"
///ScriptDocEnd
============
 */
bot_allowed_to_use_killstreaks()
{
	Assert(IsAlive( self ));
	
	if ( self isKillstreakDenied() )
	{
		// bots wont use killstreaks of any kind while EMPed
		return false;
	}
	
	if ( self bot_is_remote_or_linked() )
	{
		// shouldnt ever happen but just in case - dont use other killstreaks while using a remote
		return false;
	}
	
	if ( self isUsingTurret() )
		return false;

	if ( IsDefined(level.nukeIncoming) )
		return false;
	
	if ( IsDefined(self.underWater) && self.underWater )
		return false;
	
	if ( IsDefined(self.controlsFrozen) && self.controlsFrozen )
		return false;
	
	// corresponding to code change that we're no longer allowing player to 'queue up' a killstreak 
	// when they're cooking a grenade or holding up the underbarrel grenade launcher
	if ( self IsOffhandWeaponReadyToThrow() )
		return false;
	
	if ( getGametypeNumLives() > 0 )
	{
		all_enemies_are_dead = true;
		
		// If this is a gametype with limited lives, don't use a killstreak if all the enemies are dead
		foreach ( player in level.participants )
		{
			if ( IsAlive(player) && !IsAlliedSentient( self, player ) )
				all_enemies_are_dead = false;
		}
		
		if ( all_enemies_are_dead )
			return false;
	}
	
	if ( !self bot_in_combat(500) )
	{
		return true;
	}
	
	if ( !IsAlive( self.enemy ) )
	{
		return true;
	}
	
	return false;
}


/* 
=============
///ScriptDocBegin
"Name: bot_recent_point_of_interest()"
"Summary: Checks if there is a location this bot should investigate based on persistant memory"
"CallOn: A bot player"
"Example: goalPos = self bot_recent_point_of_interest()"
///ScriptDocEnd
============
 */
bot_recent_point_of_interest()
{
	result = undefined;
	
	deathExcludeFlags = BotMemoryFlags( "investigated", "killer_died" );
	killExcludeFlags = BotMemoryFlags( "investigated" );
	
	// Kill/Death position is always the killer position
	memoryHotSpot = random(BotGetMemoryEvents( 0, GetTime() - 10000, 1, "death", deathExcludeFlags, self ));
	if ( IsDefined(memoryHotSpot) )
	{
		// We were just killed, seek out that killer
		result = memoryHotSpot;
		self.bot_memory_goal_time = 10000;
	}
	else
	{
		// Randomly seek out a relatively recent kill or death location
		curScriptGoal = undefined;
		
		if ( self BotGetScriptGoalType() != "none" )
		{
			curScriptGoal = self BotGetScriptGoal();
		}
		
		bot_killed_someone_from = BotGetMemoryEvents( 0, GetTime() - 45000, 1, "kill", killExcludeFlags, self );
		bot_was_killed_from = BotGetMemoryEvents( 0, GetTime() - 45000, 1, "death", deathExcludeFlags, self );
		memoryHotSpot = random(array_combine(bot_killed_someone_from,bot_was_killed_from));
		if ( IsDefined(memoryHotSpot) > 0 && ( !IsDefined( curScriptGoal ) || DistanceSquared( curScriptGoal, memoryHotSpot ) > ( 1000 * 1000 ) ) )
		{
			result = memoryHotSpot;
			self.bot_memory_goal_time = 45000;
		}
	}
	
	if ( IsDefined( result ) )
	{
		hotSpotZone = GetZoneNearest( result );
		myZone = GetZoneNearest( self.origin );
		if ( IsDefined( hotSpotZone ) && IsDefined( myZone ) && myZone != hotSpotZone )
		{
			// Dont seek it out if there are multiple other allies already there or headed by/near there
			activity = BotZoneGetCount( hotSpotZone, self.team, "ally" ) + BotZoneGetCount( hotSpotZone, self.team, "path_ally" );
			if ( activity > 1 )
				result = undefined;
		}
	}

	if ( IsDefined( result ) )
		self.bot_memory_goal = result;
	
	return result;
}

bot_draw_cylinder( pos, rad, height, duration, stop_notify, color, depthTest, sides )
{
/#
	if ( !IsDefined( duration ) )
	{
		duration = 0;
	}
	
	level thread bot_draw_cylinder_think( pos, rad, height, duration, stop_notify, color, depthTest, sides );
#/
}

bot_draw_cylinder_think( pos, rad, height, seconds, stop_notify, color, depthTest, sides )
{
/#
	if ( IsDefined( stop_notify ) )
	{
		level endon( stop_notify );
	}
	
	if ( !IsDefined(color) )
	{
		color = (1,1,1);
	}
	
	if ( !IsDefined(depthTest) )
	{
		depthTest = false;	
	}
	
	if ( !IsDefined(sides) )
	{
		sides = 20;
	}

	stop_time = GetTime() + ( seconds * 1000 );

	currad = rad; 
	curheight = height; 

	for ( ;; )
	{
		if ( seconds > 0 && stop_time <= GetTime() )
		{
			return;
		}

		for( r = 0; r < sides; r++ )
		{
			theta = r / sides * 360; 
			theta2 = ( r + 1 ) / sides * 360; 

			line( pos +( cos( theta ) * currad, sin( theta ) * currad, 0 ), pos +( cos( theta2 ) * currad, sin( theta2 ) * currad, 0 ), color, 1.0, depthTest ); 
			line( pos +( cos( theta ) * currad, sin( theta ) * currad, curheight ), pos +( cos( theta2 ) * currad, sin( theta2 ) * currad, curheight ), color, 1.0, depthTest ); 
			line( pos +( cos( theta ) * currad, sin( theta ) * currad, 0 ), pos +( cos( theta ) * currad, sin( theta ) * currad, curheight ), color, 1.0, depthTest ); 
		}

		wait( 0.05 );
	}
#/
}

bot_draw_circle( center, radius, color, depthTest, segments )
{
/#
	if ( !isDefined( segments ) )
		segments = 16;
		
	angleFrac = 360/segments;
	circlepoints = [];
	
	for( i = 0; i < segments; i++ )
	{
		angle = (angleFrac * i);
		xAdd = cos(angle) * radius;
		yAdd = sin(angle) * radius;
		x = center[0] + xAdd;
		y = center[1] + yAdd;
		z = center[2];
		circlepoints[circlepoints.size] = ( x, y, z );
	}
	
	for( i = 0; i < circlepoints.size; i++ )
	{
		start = circlepoints[i];
		if (i + 1 >= circlepoints.size)
			end = circlepoints[0];
		else
			end = circlepoints[i + 1];
		
		line( start, end, color, 1.0, depthTest );
	}
#/
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_total_gun_ammo()"
"Summary: Returns the total amount of ammo this bot has"
"Example: if ( self bot_get_total_gun_ammo() == 0 )"
///ScriptDocEnd
============
 */
bot_get_total_gun_ammo()
{
	total_ammo = 0;
	
	weapon_list = undefined;
	if ( IsDefined(self.weaponlist) && self.weaponlist.size > 0 )
		weapon_list = self.weaponlist;
	else
		weapon_list = self GetWeaponsListPrimaries();
	
	foreach ( weapon in weapon_list )
	{
		total_ammo += self GetWeaponAmmoClip( weapon );
		total_ammo += self GetWeaponAmmoStock( weapon );
	}
	
	return total_ammo;
}

/* 
=============
///ScriptDocBegin
"Name: bot_out_of_ammo()"
"Summary: Returns true if this bot has no ammo remaining for any of his weapons"
"Example: if ( self bot_out_of_ammo() )"
///ScriptDocEnd
============
 */
bot_out_of_ammo()
{
	weapon_list = undefined;
	if ( IsDefined(self.weaponlist) && self.weaponlist.size > 0 )
		weapon_list = self.weaponlist;
	else
		weapon_list = self GetWeaponsListPrimaries();
	
	foreach ( weapon in weapon_list )
	{
		if ( self GetWeaponAmmoClip( weapon ) > 0 )
			return false;
		
		if ( self GetWeaponAmmoStock( weapon ) > 0 )
			return false;
	}
	
	return true;
}


/* 
=============
///ScriptDocBegin
"Name: bot_get_grenade_ammo()"
"Summary: Returns the amount of grenade ammo this bot has"
"Example: if ( self bot_get_grenade_ammo() > 0 )"
///ScriptDocEnd
============
 */
bot_get_grenade_ammo()
{
	total_grenades = 0;
	
	offhand_list = self GetWeaponsListOffhands();
	
	foreach( weapon in offhand_list )
	{
		total_grenades += self GetWeaponAmmoStock(weapon);
	}
	
	return total_grenades;
}


/* 
=============
///ScriptDocBegin
"Name: bot_grenade_matches_purpose( purpose, grenade )"
"Summary: Returns true if given weapon matches given purpose"
"CallOn: A bot player"
"Example: if ( bot_grenade_matches_purpose( "trap", grenadeWeap ) )"
///ScriptDocEnd
============
 */
bot_grenade_matches_purpose( purpose, grenade )
{
	Assert(IsDefined(grenade));
	
	switch ( purpose )
	{
		case "trap_directional":
			switch ( grenade )
			{
				case "claymore_mp":
					return true;
			}
			break;
		case "trap":
			switch ( grenade )
			{
				case "proximity_explosive_mp":
				case "motion_sensor_mp":
				case "trophy_mp":
				case "explosive_drone_mp":
					return true;
			}
			break;
		case "trap_follower":
			switch ( grenade )
			{
				case "tracking_drone_mp":
					return true;
			}
			break;
		case "c4":
			switch ( grenade )
			{
				case "c4_mp":
					return true;
			}
			break;
		case "tacticalinsertion":
			switch ( grenade )
			{
				case "s1_tactical_insertion_device_mp":
					return true;
			}
			break;
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_grenade_for_purpose( purpose )"
"Summary: Gets the grenade type of a useable item matching the requested type or undefined if nothing in inventory applicable"
"CallOn: A bot player"
"Example: grenadeType = self bot_get_grenade_for_purpose( "trap" );"
///ScriptDocEnd
============
 */
bot_get_grenade_for_purpose( purpose )
{
	if ( self BotGetDifficultySetting("allowGrenades") != 0 )
	{
		grenade = self BotFirstAvailableGrenade( "lethal" );
		if ( IsDefined(grenade) && (self GetWeaponAmmoClip( grenade ) > 0 || self GetWeaponAmmoStock( grenade ) > 0) && bot_grenade_matches_purpose( purpose, grenade ) )
			return "lethal";
		
		grenade = self BotFirstAvailableGrenade( "tactical" );
		if ( IsDefined(grenade) && (self GetWeaponAmmoClip( grenade ) > 0 || self GetWeaponAmmoStock( grenade ) > 0) && bot_grenade_matches_purpose( purpose, grenade ) )
			return "tactical";
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_watch_nodes(<nodes>, <yaw>, <end_time>, <end1>, <end2>, <end3> )"
"Summary: Bot will look at nodes and make sure to keep an eye on ones that have been out of sight the longest
"CallOn: A bot player"
"MandatoryArg: <nodes> : The array of nodes"
"OptionalArg: <yaw> : If provided, limits to nodes within yawCos arc of world yaw from bot"
"OptionalArg: <yaw_fov> : cosine of the FOV angle from yaw to include in watch list"
"OptionalArg: <end_time> : If provided, ends the thread when GetTime() >= end_time"
"OptionalArg: <end1-4> : If provided, ends the thread on any of the events named"
"Example: self thread bot_watch_nodes(entrances, threatDirYaw, Cos(45), 10, "node_relinquished", "enemy");"
"NoteLine: Nothing is done to test actual visibility to the nodes, only a within_fov() check is done"
"NoteLine: If you want to ensure the nodes are physically visible, exclude blocked nodes prior to calling this thread"
///ScriptDocEnd
============
 */
bot_watch_nodes( nodes, yaw, yaw_fov, end_time, end1, end2, end3, end4 )
{
	self notify( "bot_watch_nodes" );
	self endon( "bot_watch_nodes" );
	self endon( "bot_watch_nodes_stop" );
	self endon( "using_remote" );
	self endon( "disconnect" );
	self endon( "death" );
	
	if ( IsDefined( end1 ) )
		self endon( end1 );
	if ( IsDefined( end2 ) )
		self endon( end2 );
	if ( IsDefined( end3 ) )
		self endon( end3 );
	if ( IsDefined( end4 ) )
		self endon( end4 );
	
	wait(1.0);				// Wait a second for the bot to settle into his script goal before starting
	keep_waiting = true;
	radius_sq = squared(self BotGetScriptGoalRadius());
	while(keep_waiting)
	{
		if ( self BotHasScriptGoal() && self BotPursuingScriptGoal() )
		{
			if ( DistanceSquared(self BotGetScriptGoal(), self.origin) < radius_sq )
			{
				if ( Length( self GetVelocity() ) <= 1 )
					keep_waiting = false;
			}
		}
		
		if ( keep_waiting )
			wait(0.05);
	}
	
	origin_when_calculating = self.origin;
	node_height_offset = (0,0,self GetPlayerViewHeight());
	
	if ( IsDefined(nodes) )
	{
		self.watch_nodes = [];
		foreach( node in nodes )
		{
			node_invalid = false;
			
			if ( Distance2DSquared(self.origin,node.origin) <= 40 )
				node_invalid = true;
			
			self_eye = self GetEye();
			dot_to_node = VectorDot( (0,0,1), VectorNormalize((node.origin + node_height_offset) - self_eye) );
			if ( abs(dot_to_node) > 0.92 )
			{
				node_invalid = true;
				AssertEx( abs(node.origin[2]-self_eye[2]) < 1000, "bot_watch_nodes() error - Bot with eyes at location " + self_eye + " trying to watch invalid point " + node.origin );
			}
			
			if ( !node_invalid )
				self.watch_nodes[self.watch_nodes.size] = node;
		}
	}
	
	if ( !IsDefined(self.watch_nodes) )
		return;
	
	self thread watch_nodes_aborted();

	// Randomize the array to avoid doing the same thing in the same order each time
	self.watch_nodes = array_randomize( self.watch_nodes );
	
	foreach( node in self.watch_nodes )
		node.watch_node_chance[self.entity_number] = 1.0;
	
	startTime = GetTime();
	nextLookTime = startTime;
	node_vis_times = [];

	yawAngles = undefined;	
	if ( IsDefined( yaw ) )
		yawAngles = ( 0, yaw, 0 );
	has_yaw_angles_and_fov = IsDefined(yawAngles) && IsDefined(yaw_fov);
	
	lookingAtNode = undefined;

	for(;;)
	{
		now = GetTime();
		self notify("still_watching_nodes");
		bot_fov = self BotGetFovDot();
		
		if ( isDefined(end_time) && (now >= end_time) )
			return;
		
		if ( self bot_has_tactical_goal() )
		{
			// Not allowed to watch nodes when bot has a tactical goal
			self BotLookAtPoint( undefined );
			wait(0.2);
			continue;
		}
		
		if ( !self BotHasScriptGoal() || !self BotPursuingScriptGoal() )
		{
			// If bot isn't pursuing his script goal, then don't look at any nodes since they are based around the script goal position
			wait(0.2);
			continue;
		}
		
		if ( IsDefined(lookingAtNode) && lookingAtNode.watch_node_chance[self.entity_number] == 0.0 )
		{
			// If the node we're looking at is no longer valid, then make sure to pick a new target
			nextLookTime = now;
		}
		
		if ( self.watch_nodes.size > 0 )
		{
			lookingTowardEnemy = false;
			
			if ( IsDefined( self.enemy ) )
			{
				// If I have an enemy, watch the node closest to where I knew them to be last
				enemyKnownPos = self LastKnownPos( self.enemy );
				enemyKnownTime = self LastKnownTime( self.enemy );
				if ( enemyKnownTime && ((now - enemyKnownTime) < 5000) )
				{
					dirEnemy = VectorNormalize( enemyKnownPos - self.origin );
					maxDot = 0;
					for ( i = 0; i < self.watch_nodes.size; i++ )
					{
						dirNode = VectorNormalize( self.watch_nodes[i].origin - self.origin );
						dot = VectorDot( dirEnemy, dirNode );
						
						if ( dot > maxDot )
						{
							maxDot = dot;
							lookingAtNode = self.watch_nodes[i];
							lookingTowardEnemy = true;
						}
					}
				}
			}
			
			if ( !lookingTowardEnemy && (now >= nextLookTime) )
			{
				watch_nodes_oldest_to_newest = [];
				for ( i = 0; i < self.watch_nodes.size; i++ )
				{
					node = self.watch_nodes[i];
					node_num = node GetNodeNumber();
					
					if ( has_yaw_angles_and_fov && !within_fov( self.origin, yawAngles, node.origin, yaw_fov ) )
					{
						// Only pay attention to nodes within the yaw arc, if defined
						continue;
					}
					
					if ( Distance2DSquared(self.origin,node.origin) <= 10 )
					{
						// Somehow the bot got too close to this node, so ignore it
						continue;
					}
					
					if ( !IsDefined(node_vis_times[node_num]) )
						node_vis_times[node_num] = 0;
					
					// Mark the last time each node was visible by the bot
					if ( within_fov( self.origin, self.angles, node.origin, bot_fov ) )
						node_vis_times[node_num] = now;
					
					// fit this node into the array of oldest to newest seen nodes
					index = 0;
					for( ; index<watch_nodes_oldest_to_newest.size; index++ )
					{
						if ( node_vis_times[watch_nodes_oldest_to_newest[index] GetNodeNumber()] > node_vis_times[node_num] )
							break;
					}
					
					watch_nodes_oldest_to_newest = array_insert( watch_nodes_oldest_to_newest, node, index );
				}
				
				lookingAtNode = undefined;
				for( i=0; i<watch_nodes_oldest_to_newest.size; i++ )
				{
					if ( RandomFloat(1) > watch_nodes_oldest_to_newest[i].watch_node_chance[self.entity_number] )
						continue;
					
					lookingAtNode = watch_nodes_oldest_to_newest[i];
					nextLookTime = now + RandomIntRange( 3000, 5000 );
					break;
				}
			}
			
			if ( isDefined( lookingAtNode ) )
			{
				look_at_point = lookingAtNode.origin + node_height_offset;
				if ( Distance2DSquared(self.origin,look_at_point) <= 10 )
				{
					// Somehow we ended up looking at a node right on top of ourself.  So just clear it and try again
					self BotLookAtPoint( undefined );
					lookingAtNode = undefined;
					nextLookTime = 0;
				}
				else
				{
					self BotLookAtPoint( look_at_point, 0.4, "script_search" );
				}
			}
		}
		
		wait(0.2);
	}
}

watch_nodes_stop()
{
	self notify("bot_watch_nodes_stop");
	
	if ( IsDefined(self.watch_nodes) )
	{
		foreach(node in self.watch_nodes)
			node.watch_node_chance[self.entity_number] = undefined;
	}
	
	self.watch_nodes = undefined;
}

watch_nodes_aborted()
{
	self notify("watch_nodes_aborted");
	self endon("watch_nodes_aborted");
	self endon( "disconnect" );
	
	while(1)
	{
		msg = self waittill_any_timeout( 0.5, "still_watching_nodes" );
		if ( !IsDefined(msg) || msg != "still_watching_nodes" )
		{
			self watch_nodes_stop();
			return;
		}
	}
}
	
/* 
=============
///ScriptDocBegin
"Name: bot_leader_dialog( <dialog>, <location> )"
"Summary: Bot handler for playLeaderDialogOnPlayer()
"CallOn: A bot player"
"MandatoryArg: <dialog> : The dialog type string"
"OptionalArg: <location> : If provided, the location the event happened"
///ScriptDocEnd
============
 */
bot_leader_dialog( dialog, location )
{
	// Game events notified to players come in through here such as flags being captured or killstreak hardware destroyed
	
	// Are we being notified that something happened at a specific location?
	if ( IsDefined( location ) && (location != (0, 0, 0)) )
	{
		Assert(IsDefined(self));
		Assert(IsDefined(self.origin));
		
		// If we are not currently looking at that spot but can potentially see it, look there for a few seconds to see whats going on
		if ( !within_fov( self.origin, self.angles, location, self BotGetFovDot() ) )
		{
			lookAtLoc = self BotPredictSeePoint( location );
			if ( IsDefined( lookAtLoc ) )
				self BotLookAtPoint( lookAtLoc + (0,0,40), 1.0, "script_seek" );
		}

		// Mark it in the bot's memory as well
		self BotMemoryEvent( "known_enemy", undefined, location );
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_known_attacker()"
"Summary: Returns the actual attacker when something damaged or killed a bot"
"Summary: The actual attacker is the attacker that the bot knows about - i.e. when killed by a gun or a grenade, he knows who was behind it and where they are located."
"Summary: But when killed by something like a helicopter / bouncing betty / etc., he doesn't know about the entity behind the attack"
"Example: attacker_ent = bot_get_known_attacker( eAttacker, eInflictor );"
///ScriptDocEnd
============
 */
bot_get_known_attacker( attacker, inflictor )
{
	if ( IsDefined(inflictor) && IsDefined(inflictor.classname) )
	{
		if ( inflictor.classname == "grenade" )
		{
			if ( !bot_ent_is_anonymous_mine(inflictor) )
				return attacker;
		}
		else if ( inflictor.classname == "rocket" )
		{
			if ( IsDefined(inflictor.vehicle_fired_from) )
			    return inflictor.vehicle_fired_from;
			if ( IsDefined(inflictor.type) && (inflictor.type == "remote" || inflictor.type == "odin") )
				return inflictor;
			
			// Needs to be last - only if all else fails do we return the .owner
			if ( IsDefined(inflictor.owner) )
				return inflictor.owner;
		}
		else if ( inflictor.classname == "worldspawn" || inflictor.classname == "trigger_hurt" )
		{
			// falling damage, environmental damage, etc.
			return undefined;
		}
		
		return inflictor;
	}
	
	return attacker;
}

bot_ent_is_anonymous_mine( ent )
{
	// True if this ent is a mine where you wouldn't have knowledge of who placed it
	if ( !IsDefined(ent.weapon_name) )
		return false;
	
	if ( ent.weapon_name == "c4_mp" )
		return true;
	
	if ( ent.weapon_name == "proximity_explosive_mp" )
		return true;
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_vectors_are_equal( <vec1>, <vec2> )"
"Summary: Returns true if the two vectors are equal
"MandatoryArg: <vec1> : A vector"
"MandatoryArg: <vec2> : Another vector"
"Example: if ( bot_vectors_are_equal( struct.defense_center, player.bot_defending_center ) )"
///ScriptDocEnd
============
 */
bot_vectors_are_equal( vec1, vec2 )
{
	return (vec1[0] == vec2[0] && vec1[1] == vec2[1] && vec1[2] == vec2[2]);
}

/* 
=============
///ScriptDocBegin
"Name: bot_add_to_bot_level_targets( <target_to_add> )"
"Summary: Adds a trigger to the level-specific array that bots look at to determine level-specific actions"
"Summary: This trigger needs to have trigger.bot_interaction_type set"
"MandatoryArg: <target_to_add> : A trigger"
"Example: bot_add_to_bot_level_targets( target );"
///ScriptDocEnd
============
 */
bot_add_to_bot_level_targets( target_to_add )
{
	target_to_add.high_priority_for = [];
	
	if ( target_to_add.bot_interaction_type == "use" )
		bot_add_to_bot_use_targets( target_to_add );
	else if ( target_to_add.bot_interaction_type == "damage" )
		bot_add_to_bot_damage_targets( target_to_add );
	else
		AssertMsg("bot_add_to_bot_level_targets needs a trigger with bot_interaction_type set");
}

/* 
=============
///ScriptDocBegin
"Name: bot_remove_from_bot_level_targets( <target_to_remove> )"
"Summary: Removes a trigger from the level-specific array that bots look at to determine level-specific damage actions"
"MandatoryArg: <target_to_remove> : A trigger"
"Example: bot_remove_from_bot_level_targets( target );"
///ScriptDocEnd
============
 */
bot_remove_from_bot_level_targets( target_to_remove )
{
	target_to_remove.already_used = true;
	level.level_specific_bot_targets = array_remove( level.level_specific_bot_targets, target_to_remove );	
}

bot_add_to_bot_use_targets( new_use_target )
{
	if ( !IsSubStr( new_use_target.code_classname, "trigger_use" ) )
	{
		AssertMsg("bot_add_to_bot_use_targets can only be used with a trigger_use");
		return;
	}
	
	if ( !IsDefined(new_use_target.target) )
	{
		AssertMsg("bot_add_to_bot_use_targets needs a trigger with a target");
		return;
	}
	
	if ( IsDefined(new_use_target.bot_target) )
	{
		AssertMsg("bot_add_to_bot_use_targets has already been processed for this trigger");
		return;
	}
	
	if ( !IsDefined(new_use_target.use_time ) )
	{
		AssertMsg("bot_add_to_bot_use_targets needs .use_time set");
		return;
	}
	
	use_trigger_targets = GetNodeArray( new_use_target.target, "targetname" );
	if ( use_trigger_targets.size != 1 )
	{
		AssertMsg("bot_add_to_bot_use_targets needs to target exactly one node");
		return;
	}
	
	new_use_target.bot_target = use_trigger_targets[0];
	
	if ( !IsDefined(level.level_specific_bot_targets) )
		level.level_specific_bot_targets = [];
		
	level.level_specific_bot_targets = array_add(level.level_specific_bot_targets, new_use_target );
}

bot_add_to_bot_damage_targets( new_damage_target )
{
	if ( !IsSubStr( new_damage_target.code_classname, "trigger_damage" ) )
	{
		AssertMsg("bot_add_to_bot_damage_targets can only be used with a trigger_damage");
		return;
	}
	
	damage_trigger_targets = GetNodeArray( new_damage_target.target, "targetname" );
	if ( damage_trigger_targets.size != 2 )
	{
		AssertMsg("bot_add_to_bot_use_targets needs to target exactly two nodes");
		return;
	}
	
	new_damage_target.bot_targets = damage_trigger_targets;
	
	if ( !IsDefined(level.level_specific_bot_targets) )
		level.level_specific_bot_targets = [];
		
	level.level_specific_bot_targets = array_add(level.level_specific_bot_targets, new_damage_target );
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_string_index_for_integer( <array>, <integer_index> )"
"Summary: Returns the nth string index into <array>, where n = <integer_index>"
"MandatoryArg: <array> : The array to look through"
"MandatoryArg: <integer_index> : The nth array index to return"
"Example: pers_type_found = bot_get_string_index_for_integer( personality_types_desired_in_progress, random_index_picked );"
///ScriptDocEnd
============
 */
bot_get_string_index_for_integer( array, integer_index )
{
	current_index = 0;
	foreach( string_index, array_value in array )
	{
		if ( current_index == integer_index )
		{
			return string_index;
		}
		current_index++;
	}
	
	return undefined;
}


/* 
=============
///ScriptDocBegin
"Name: bot_get_zones_within_dist( <target_zone_index>, <max_dist> )"
"Summary: Returns an array of zone indices for all zones that are within max_dist of target_zone_index"
"Summary: It uses the zone connectivity / zone paths, not straight-line distance"
"MandatoryArg: <target_zone_index> : The zone to start at"
"MandatoryArg: <max_dist> : How far to search for zones"
"Example: zones = bot_get_zones_within_dist( targetZone, 800 * zone_steps );"
///ScriptDocEnd
============
 */
bot_get_zones_within_dist( target_zone_index, max_dist )
{
	for ( z = 0; z < level.zoneCount; z++ )
	{
		zone_node = GetZoneNodeForIndex( z );
		zone_node.visited = false;
	}
	
	target_zone_node = GetZoneNodeForIndex( target_zone_index );
	return bot_get_zones_within_dist_recurs( target_zone_node, max_dist );
}

bot_get_zones_within_dist_recurs( target_zone_node, max_dist )
{
	all_zones = [];
	all_zones[0] = GetNodeZone( target_zone_node );
	
	target_zone_node.visited = true;
	target_zone_linked_nodes = GetLinkedNodes( target_zone_node );
	
	foreach ( node in target_zone_linked_nodes )
	{
		if ( !node.visited )
		{
			distance_to_zone = Distance( target_zone_node.origin, node.origin );
			if ( distance_to_zone < max_dist )
			{
				new_zones = bot_get_zones_within_dist_recurs( node, (max_dist - distance_to_zone) );
				all_zones = array_combine(new_zones, all_zones);
			}
		}
	}

	return all_zones;
}

/* 
=============
///ScriptDocBegin
"Name: bot_crate_is_command_goal( <crate> )"
"Summary: Returns true if crate is flagged as a command goal"
"MandatoryArg: <crate> : The airdrop crate or deployable box"
"Example: if( bot_crate_is_command_goal( crate ) )"
///ScriptDocEnd
============
 */
bot_crate_is_command_goal( crate )
{
	return ( IsDefined( crate ) && IsDefined( crate.command_goal ) && crate.command_goal );
}


/* 
=============
///ScriptDocBegin
"Name: bot_get_max_players_on_team( <team> )"
"Summary: Returns the max number of players on this team (respecting dev dvars, private match limits, etc)"
"MandatoryArg: <team> : Team to check"
"Example: team_limit = bot_get_max_players_on_team( self.team );"
///ScriptDocEnd
============
 */
bot_get_max_players_on_team( team )
{
	return level.bot_max_players_on_team[team];
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_team_limit()"
"Summary: Returns the theoretical max number of clients on a team"
"Example: ally_team_size = bot_get_team_limit();"
///ScriptDocEnd
============
 */
bot_get_team_limit()
{
	return INT(bot_get_client_limit()/2);
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_client_limit()"
"Summary: Returns the max number of clients in the game"
"Example: if ( ally_team_size + enemy_team_size < bot_get_client_limit() )"
///ScriptDocEnd
============
 */
bot_get_client_limit()
{
	maxPlayers = GetDvarInt( "party_maxplayers", 0 );	
	maxPlayers = max( maxPlayers, GetDvarInt( "party_maxPrivatePartyPlayers", 0 ) );
	
	if ( maxPlayers > level.maxClients )
		return level.maxClients;

	return maxPlayers;
}

bot_queued_process_level_thread( )
{
	self notify( "bot_queued_process_level_thread" );
	self endon( "bot_queued_process_level_thread" );
	
	// Need to wait a frame here - if this is the first time this function is started, then without the wait would finish its call and then
	// notify the process.owner before he was waiting, and so he would never return from whatever called bot_queued_process
	wait(0.05);
	
	while ( 1 )
	{
		if ( IsDefined( level.bot_queued_process_queue ) && level.bot_queued_process_queue.size > 0 )
		{
			process = level.bot_queued_process_queue[0];
			
			if ( IsDefined( process ) && IsDefined( process.owner ) )
			{
				assert( isdefined( process.func ) );
				result = undefined;
				if ( IsDefined( process.parm4 ) )
					result = process.owner [[process.func]]( process.parm1, process.parm2, process.parm3, process.parm4 );
				else if ( IsDefined( process.parm3 ) )
					result = process.owner [[process.func]]( process.parm1, process.parm2, process.parm3 );
				else if( IsDefined( process.parm2 ) ) 
					result = process.owner [[process.func]]( process.parm1, process.parm2 );
				else if ( IsDefined( process.parm1 ) ) 
					result = process.owner [[process.func]]( process.parm1 );
				else
					result = process.owner [[process.func]]( );
				process.owner notify( process.name_complete, result );
			}
			
			new_queue = [];
			for( i = 1; i < level.bot_queued_process_queue.size; i++ )
				new_queue[i-1] = level.bot_queued_process_queue[i];
			level.bot_queued_process_queue = new_queue;
		}
		
		wait 0.05;
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_queued_process( <process_name>, <process_func>, <optional_parm1>, <optional_parm2>, <optional_parm3>, <optional_parm4> )"
"Summary: Queues a process in the level bot process queue and returns result when complete"
"MandatoryArg: <process_name> : Process Name"
"OptionalArg: <process_func> : Script function to run"
"OptionalArg: <optional_parm1> : Optional parameter to pass to the process"
"OptionalArg: <optional_parm2> : Optional parameter to pass to the process"
"OptionalArg: <optional_parm3> : Optional parameter to pass to the process"
"OptionalArg: <optional_parm4> : Optional parameter to pass to the process"
"Example: result = bot_queued_process( "find_camp_node_worker", ::find_camp_node_worker );
///ScriptDocEnd
============
 */
bot_queued_process( process_name, process_func, optional_parm1, optional_parm2, optional_parm3, optional_parm4 )
{
	if ( !IsDefined( level.bot_queued_process_queue ) )
		level.bot_queued_process_queue = [];
	
	// Stop any existing processes of this name for this bot
	foreach( index, process in level.bot_queued_process_queue )
	{
		if ( process.owner == self && process.name == process_name )
		{
			self notify( process.name );
			level.bot_queued_process_queue[index] = undefined;
		}
	}

	process = SpawnStruct();
	process.owner = self;
	process.name = process_name;
	process.name_complete = process.name + "_done";
	process.func = process_func;
	process.parm1 = optional_parm1;
	process.parm2 = optional_parm2;
	process.parm3 = optional_parm3;
	process.parm4 = optional_parm4;
	
	level.bot_queued_process_queue[level.bot_queued_process_queue.size] = process;
	
	if ( !IsDefined( level.bot_queued_process_level_thread_active ) )
	{
		level.bot_queued_process_level_thread_active = true;
		level thread bot_queued_process_level_thread();
	}
	
	self waittill( process.name_complete, result );
	
	return result;
}

/* 
=============
///ScriptDocBegin
"Name: bot_is_remote_or_linked()"
"Summary: Returns true if the bot is using a remote or is linked to something (cases where he shouldn't be pathing, etc)"
"Example: if ( self bot_is_remote_or_linked() )"
///ScriptDocEnd
============
 */
bot_is_remote_or_linked()
{
	return ( self isUsingRemote() || self IsLinked() );
}

/* 
=============
///ScriptDocBegin
"Name: bot_get_low_on_ammo( <minFrac> )"
"Summary: Returns true if the bot's ammo for any carried weapon is below the fraction passed in (ratio of held ammo to max ammo, from 0-1)"
"MandatoryArg: <minFrac> : The threshhold under which the bot is considered low on ammo"
"Example: if ( self bot_get_low_on_ammo(0.25) )"
///ScriptDocEnd
============
 */
bot_get_low_on_ammo( minFrac )
{
	weapon_list = undefined;
	if ( IsDefined(self.weaponlist) && self.weaponlist.size > 0 )
		weapon_list = self.weaponlist;
	else
		weapon_list = self GetWeaponsListPrimaries();
	
	foreach ( weapon in weapon_list )
	{
		max_clip_ammo = WeaponClipSize( weapon );
		stock_ammo = self GetWeaponAmmoStock( weapon );
		
		if ( stock_ammo <= max_clip_ammo )
			return true;
		
		if ( self GetFractionMaxAmmo( weapon ) <= minFrac )
			return true;
	}
	
	return false;
}

/* 
=============
///ScriptDocBegin
"Name: bot_point_is_on_pathgrid( <point>, <radius> )"
"Summary: Returns true if the point is on the pathgrid (i.e. the pathgrid is accessible within the radius given)"
"MandatoryArg: <point> : The point to check"
"MandatoryArg: <radius> : The radius around the point to search for the pathgrid"
"MandatoryArg: <height> : The height around the point to search for the pathgrid"
"Example: if ( bot_point_is_on_pathgrid( self.odin.targeting_marker.origin, 200 ) )"
///ScriptDocEnd
============
 */
bot_point_is_on_pathgrid( point, radius, height )
{
	if ( !IsDefined(radius) )
		radius = 256;
	
	if ( !IsDefined(height) )
		height = 50;
	
	nodes = GetNodesInRadiusSorted( point, radius, 0, height, "Path" );
	foreach( node in nodes )
	{
		start = point + (0,0,30);
		end = node.origin + (0,0,30);
		trace_end = PhysicsTrace( start, end );
		if ( bot_vectors_are_equal(trace_end, end) )
			return true;
		
		wait(0.05);
	}
	
	return false;
}


/* 
=============
///ScriptDocBegin
"Name: bot_monitor_enemy_camp_spots()"
"Summary: Thread that keeps self.enemy_camp_spots["team"] array updated"
"Example: level thread bot_monitor_enemy_camp_spots();"
///ScriptDocEnd
============
 */
bot_monitor_enemy_camp_spots( validateFunc )
{
	level endon("game_ended");
	self notify( "bot_monitor_enemy_camp_spots" );
	self endon( "bot_monitor_enemy_camp_spots" );

	level.enemy_camp_spots = [];
	level.enemy_camp_assassin_goal = [];
	level.enemy_camp_assassin = [];
	
	while( 1 )
	{
		wait 1.0;
		
		updated = [];
		
		if ( !IsDefined( validateFunc ) )
			continue;
		
		foreach( participant in level.participants )
		{
			if ( !IsDefined( participant.team ) )
				continue;
	
			if ( participant [[validateFunc]]() && !IsDefined( updated[participant.team] ) )
			{
				level.enemy_camp_assassin[participant.team] = undefined;
				level.enemy_camp_spots[participant.team] = participant BotPredictEnemyCampSpots( true );
				
				if ( IsDefined( level.enemy_camp_spots[participant.team] ) )
				{
					if ( !IsDefined( level.enemy_camp_assassin_goal[participant.team] ) || 
					     !array_contains( level.enemy_camp_spots[participant.team], level.enemy_camp_assassin_goal[participant.team] ) )
					    level.enemy_camp_assassin_goal[participant.team] = random( level.enemy_camp_spots[participant.team] );
					
					if ( isDefined( level.enemy_camp_assassin_goal[participant.team] ) )
					{
						aiAllies = [];
						foreach( otherParticipant in level.participants )
						{
							if ( !IsDefined( otherParticipant.team ) )
								continue;
							if ( otherParticipant [[validateFunc]]() && (otherParticipant.team == participant.team) )
								aiAllies[aiAllies.size] = otherParticipant;
						}
						aiAllies = SortByDistance( aiAllies, level.enemy_camp_assassin_goal[participant.team] );
						
						if ( aiAllies.size > 0 )
							level.enemy_camp_assassin[participant.team] = aiAllies[0];						
					}
				}
				
				updated[participant.team] = true;
			}
		}
	}
}

/* 
=============
///ScriptDocBegin
"Name: bot_valid_camp_assassin()"
"Summary: Default camp assassin validator"
"Example: bot bot_update_camp_assassin( ::bot_valid_camp_assassin ) )"
///ScriptDocEnd
============
 */
bot_valid_camp_assassin() // self = bot
{
	if ( !IsDefined( self ) )
		return false;
	
	if ( !isAI( self ) )
		return false;
	
	if ( !IsDefined( self.team ) )
		return false;

	if ( self.team == "spectator" )
		return false;

	if ( !IsAlive( self ) )
		return false;		
	
	if ( !IsAITeamParticipant( self ) )
		return false;
	
	if ( self.personality == "camper" )
		return false;
	
	return true;
}

/* 
=============
///ScriptDocBegin
"Name: bot_update_camp_assassin()"
"Summary: Interjecting logic that assigns a non-camper to seek out predicted enemy camp spots"
"Example: if ( !(self bot_update_camp_assassin()) )"
///ScriptDocEnd
============
 */
bot_update_camp_assassin() // self = bot
{	
	if ( !IsDefined( level.enemy_camp_assassin ) )
		return;
	
	if ( !IsDefined( level.enemy_camp_assassin[self.team] ) )
		return;

	if ( level.enemy_camp_assassin[self.team] == self )
	{
		self bot_defend_stop();
		self BotSetScriptGoal( level.enemy_camp_assassin_goal[self.team], 128, "objective", undefined, 256 );
		self bot_waittill_goal_or_fail();
	}
}


/* 
=============
///ScriptDocBegin
"Name: bot_force_stance_for_time( <stance>, <seconds> )"
"Summary: Forces a bot to a stance for the next N seconds"
"MandatoryArg: <stance> : The stance to force ("crouch", "prone", "stand")"
"MandatoryArg: <seconds> : The seconds to keep forcing it"
"Example: self bot_force_stand( "stand", 1.0 );"
///ScriptDocEnd
============
 */
bot_force_stance_for_time( stance, seconds ) // self = bot
{	
	self notify( "bot_force_stance_for_time" );
	self endon( "bot_force_stance_for_time" );
	self endon( "death" );
	self endon( "disconnect" );
	level endon( "game_ended" );
	
	self BotSetStance( stance );
	wait seconds;
	self BotSetStance( "none" );
}