// Personality functions for bots

#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\bots\_bots_util;
#include maps\mp\bots\_bots_loadout;
#include maps\mp\bots\_bots_strategy;

//=======================================================
//			setup_personalities
//=======================================================
setup_personalities()
{
	level.bot_personality = [];
	level.bot_personality_list = [];
	level.bot_personality["active"][0] = "default";
	level.bot_personality["active"][1] = "run_and_gun";
	level.bot_personality["active"][2] = "cqb";
	level.bot_personality["stationary"][0] = "camper";
	
	level.bot_personality_type = [];
	foreach( index, personality_array in level.bot_personality )
	{
		foreach( personality in personality_array )
		{
			level.bot_personality_type[personality] = index;
			level.bot_personality_list[level.bot_personality_list.size] = personality;
		}
	}
	
	// Desired ratio of personalities
	// Currently we would like 4 active bots (run and gun, cqb, etc) for every 1 camper-type bot
	level.bot_personality_types_desired = [];
	level.bot_personality_types_desired["active"] = 4;
	level.bot_personality_types_desired["stationary"] = 1;

	level.bot_pers_init = [];
	level.bot_pers_init["default"] = ::init_personality_default;
	level.bot_pers_init["camper"] = ::init_personality_camper;
	
	level.bot_pers_update["default"] = ::update_personality_default;
	level.bot_pers_update["camper"] = ::update_personality_camper;
}

//=======================================================
//			assign_personality_functions
//=======================================================
bot_assign_personality_functions()
{
	self.personality = self BotGetPersonality();
	self.pers["personality"] = self.personality;
	
	self.personality_init_function = level.bot_pers_init[self.personality];
	if ( !IsDefined(self.personality_init_function) )
	{
		self.personality_init_function = level.bot_pers_init["default"];
	}
	
	// Call the init function now
	self [[ self.personality_init_function ]]();
	
	self.personality_update_function = level.bot_pers_update[self.personality];
	if ( !IsDefined(self.personality_update_function) )
	{
		self.personality_update_function = level.bot_pers_update["default"];
	}
}

//=======================================================
//				bot_balance_personality
//=======================================================
bot_balance_personality()
{
	if( IsDefined(self.personalityManuallySet) && self.personalityManuallySet )
	{
		return;
	}
	
	if ( IsDefined(self.pers["personality"]) )
	{
		self BotSetPersonality( self.pers["personality"] );		// Already set my personality in a previous round of this game, so keep that
		return;
	}
	
	my_team = self.team;
	if ( !IsDefined(my_team) && !IsDefined(self.bot_team) )
		my_team = self.pers["team"];	// Not yet spawned, but we know which team he will choose when he does spawn (for second rounds of dom, ctf, etc)
	
	Assert(level.bot_personality.size == level.bot_personality_types_desired.size);
	
	persCounts = [];
	persCountsByType = [];
	foreach( personality_type, personality_array in level.bot_personality )
	{
		persCountsByType[personality_type] = 0;
		foreach( personality in personality_array )
			persCounts[personality] = 0;		
	}
	
	// Count up the number of personalities on my team (not including myself), and also the types
	foreach( bot in level.participants )
	{
		if ( IsTeamParticipant(bot) && IsDefined(bot.team) && (bot.team == my_team) && (bot != self) && IsDefined(bot.has_balanced_personality) )
		{
			personality = bot BotGetPersonality();
			personality_type = level.bot_personality_type[personality];
			persCounts[personality] = persCounts[personality] + 1;
			persCountsByType[personality_type] = persCountsByType[personality_type] + 1;
		}
	}
	
	// Determine which personality type is needed by looking at level.bot_personality_types_desired, which is the desired ratio of personalities
	type_needed = undefined;
	while( !IsDefined(type_needed) )
	{
		personality_types_desired_in_progress = level.bot_personality_types_desired;
		while ( personality_types_desired_in_progress.size > 0 )
		{
			pers_type_picked = bot_get_string_index_for_integer( personality_types_desired_in_progress, RandomInt(personality_types_desired_in_progress.size) );
			
			persCountsByType[pers_type_picked] -= level.bot_personality_types_desired[pers_type_picked];
			if ( persCountsByType[pers_type_picked] < 0 )
			{
				type_needed = pers_type_picked;
				break;
			}
			
			personality_types_desired_in_progress[pers_type_picked] = undefined;
		}
	}
	
	// Now that we know which personality type is needed, figure out which personality is needed within that type
	personality_needed = undefined;
	least_common_personality = undefined;
	least_common_personality_uses = 9999;
	most_common_personality = undefined;
	most_common_personality_uses = -9999;
	randomized_personalities_needed = array_randomize(level.bot_personality[type_needed]);
	foreach ( personality in randomized_personalities_needed )
	{
		if ( persCounts[personality] < least_common_personality_uses )
		{
			least_common_personality = personality;
			least_common_personality_uses = persCounts[personality];
		}
		
		if ( persCounts[personality] > most_common_personality_uses )
		{
			most_common_personality = personality;
			most_common_personality_uses = persCounts[personality];
		}
	}
	
	if ( most_common_personality_uses - least_common_personality_uses >= 2 )
		personality_needed = least_common_personality;
	else
		personality_needed = Random(level.bot_personality[type_needed]);
	
	if ( self BotGetPersonality() != personality_needed )
		self BotSetPersonality( personality_needed );
	
	self.has_balanced_personality = true;
}
	
//=======================================================
//				init_camper_data
//=======================================================
init_personality_camper()
{
	clear_camper_data();
}

//=======================================================
//				init_personality_default
//=======================================================
init_personality_default()
{
	// clear camper data here too in case bot was changing personality from camper	
	clear_camper_data();
}

SCR_CONST_CAMPER_HUNT_TIME = 10000;

//=======================================================
//				update_personality_camper
//=======================================================
update_personality_camper()
{
	if ( should_select_new_ambush_point() && !self bot_is_defending() && !self bot_is_remote_or_linked() )
	{	
		// If we took a detour to "hunt" then wait till that goal gets cleared before camping again
		goalType = self BotGetScriptGoalType();
		foundCampNode = false;
		
		if ( !IsDefined(self.camper_time_started_hunting) )
			self.camper_time_started_hunting = 0;
		
		is_hunting = (goalType == "hunt");
		time_to_stop_hunting = GetTime() > self.camper_time_started_hunting + SCR_CONST_CAMPER_HUNT_TIME;
		if ( ( !is_hunting || time_to_stop_hunting ) && !self bot_out_of_ammo() )	// Don't look for a camp node if we're out of ammo
		{
			if ( !(self BotHasScriptGoal()) )
			{
				// If we dont currently have a script goal act like a run and gunner while we wait for a camping goal
				self bot_random_path();
			}
			
			foundCampNode = self find_camp_node();
			if ( !foundCampNode )
				self.camper_time_started_hunting = GetTime();
		}
				
		if ( IsDefined( foundCampNode ) && foundCampNode )
		{
			self.ambush_entrances = self bot_queued_process( "bot_find_ambush_entrances", ::bot_find_ambush_entrances, self.node_ambushing_from, true );

			// If applicable, plant a trap to cover our rear first
			trap_item = self bot_get_ambush_trap_item( "trap_directional", "trap", "c4" );
			if ( IsDefined( trap_item ) )
			{
				trapTime = GetTime();
				self bot_set_ambush_trap( trap_item, self.ambush_entrances, self.node_ambushing_from, self.ambush_yaw );
				trapTime = GetTime() - trapTime;
				if ( trapTime > 0 && IsDefined( self.ambush_end ) && IsDefined( self.node_ambushing_from ) )
				{
					self.ambush_end += trapTime;
					self.node_ambushing_from.bot_ambush_end = self.ambush_end + 10000;
				}
			}

			if ( !(self bot_has_tactical_goal()) && !self bot_is_defending() && IsDefined(self.node_ambushing_from) )
			{	
				// Go to the ambush point
				self BotSetScriptGoalNode( self.node_ambushing_from, "camp", self.ambush_yaw );			
				self thread clear_script_goal_on( "bad_path", "node_relinquished", "out_of_ammo" );
				self thread watch_out_of_ammo();
			
				// When we get there add in the travel time to our camping timer
				self thread bot_add_ambush_time_delayed( "clear_camper_data", "goal" );
				
				// When we get there look toward each of the entrances periodically
				self thread bot_watch_entrances_delayed( "clear_camper_data", "bot_add_ambush_time_delayed", self.ambush_entrances, self.ambush_yaw );
				
				self childthread bot_try_trap_follower( "clear_camper_data", "goal" );
			}
		}
		else
		{
			// Hunt until we can find a reasonable camping spot to use
			if ( goalType == "camp" )
				self BotClearScriptGoal();
			update_personality_default();
		}
	}
}

//=======================================================
//			update_personality_default
//=======================================================
update_personality_default()
{
	script_goal = undefined;
	has_script_goal = self BotHasScriptGoal();
	if ( has_script_goal )
		script_goal = self BotGetScriptGoal();
	
	if ( GetTime() - self.lastSpawnTime > 5000 )
		self bot_try_trap_follower();
	
	if ( !(self bot_has_tactical_goal()) && !self bot_is_remote_or_linked() )
	{	
		distSq = undefined;
		goalRadius = undefined;
		
		if ( has_script_goal ) 
		{
			distSq = DistanceSquared(self.origin,script_goal);
			goalRadius = self BotGetScriptGoalRadius();
			goalRadiusDbl = goalRadius * 2;
			
			if ( IsDefined( self.bot_memory_goal ) && distSq < goalRadiusDbl*goalRadiusDbl )
			{
				flagInvestigated = BotMemoryFlags( "investigated" );
				BotFlagMemoryEvents( 0, GetTime() - self.bot_memory_goal_time, 1, self.bot_memory_goal, goalRadiusDbl, "kill", flagInvestigated, self );
				BotFlagMemoryEvents( 0, GetTime() - self.bot_memory_goal_time, 1, self.bot_memory_goal, goalRadiusDbl, "death", flagInvestigated, self );
				self.bot_memory_goal = undefined;
				self.bot_memory_goal_time = undefined;
			}
		}
		
		if ( !has_script_goal || (distSq < goalRadius * goalRadius) )
		{
			set_random_path = self bot_random_path();
			
			// Sometimes plant a trap if we have applicable gear
			if ( set_random_path && (RandomFloat( 100 ) < 25) )
			{
				trap_item = self bot_get_ambush_trap_item( "trap_directional", "trap" );
				if ( IsDefined( trap_item ) )
				{
					ambush_point = self BotGetScriptGoal();
					if ( IsDefined( ambush_point ) )
					{
						ambush_node = GetClosestNodeInSight( ambush_point );
						if ( IsDefined( ambush_node ) ) 
						{
							ambush_entrances = self bot_find_ambush_entrances( ambush_node, false );
							set_trap = self bot_set_ambush_trap( trap_item, ambush_entrances, ambush_node );
							if( !IsDefined( set_trap ) || set_trap )
							{
								self BotClearScriptGoal();
								set_random_path = self bot_random_path();
							}
						}
					}
				}
			}
				
			if ( set_random_path )
			{
				self thread clear_script_goal_on( "enemy", "bad_path", "goal", "node_relinquished", "search_end" );
			}
		}
	}
}

bot_try_trap_follower( endEvent, waitEvent )
{
	self notify( "bot_try_trap_follower" );
	self endon( "bot_try_trap_follower" );
	self endon( "death" );
	self endon( "disconnect" );

	if ( IsDefined( endEvent ) )
		self endon( endEvent );
	self endon( "node_relinquished" );
	self endon( "bad_path" );
	
	if ( IsDefined(waitEvent) )
		self waittill( waitEvent );
	
	trap_item = self bot_get_ambush_trap_item( "trap_follower" );
	if ( IsDefined( trap_item ) && self IsOnGround() )
	{
		nodes_in_cone = self bot_get_nodes_in_cone( 300, 600, 0.7, true );
		if ( nodes_in_cone.size > 0 )
		{
			self BotPressButton( trap_item["item_action"] );
			self waittill_any_timeout( 5, "grenade_fire", "missile_fire" );
		}
	}
}

//=======================================================
//			clear_script_goal_on
//=======================================================
clear_script_goal_on( event1, event2, event3, event4, event5 )
{
	self notify("clear_script_goal_on");
	self endon ("clear_script_goal_on");
	self endon( "death" );
	self endon( "disconnect" );
	self endon( "start_tactical_goal" );
	
	goal_at_start = self BotGetScriptGoal();
	
	keep_looping = true;
	while( keep_looping )
	{
		result = self waittill_any_return( event1, event2, event3, event4, event5, "script_goal_changed" );
		
		keep_looping = false;
		should_clear_script_goal = true;
		if ( result == "node_relinquished" || result == "goal" || result == "script_goal_changed" )
		{
			// For these notifies, we only clear the script goal if it was our original goal
			if ( !self BotHasScriptGoal() )
			{
				should_clear_script_goal = false;
			}
			else
			{
				goal_at_end = self BotGetScriptGoal();
				should_clear_script_goal = bot_vectors_are_equal(goal_at_start,goal_at_end);
			}
		}
		
		if ( result == "enemy" && IsDefined(self.enemy) )
		{
			// Switched from one enemy to another, so don't clear script goal
			should_clear_script_goal = false;
			keep_looping = true;	// Continue to wait for a better notify
		}
		
		if ( should_clear_script_goal )
			self BotClearScriptGoal();
	}
}

//=======================================================
//			watch_out_of_ammo
//=======================================================
watch_out_of_ammo()
{
	self notify("watch_out_of_ammo");
	self endon ("watch_out_of_ammo");
	self endon( "death" );
	self endon( "disconnect" );
	
	while(!self bot_out_of_ammo())
		wait(0.5);
	
	self notify("out_of_ammo");
}

//=======================================================
//			bot_add_ambush_time_delayed
//=======================================================
bot_add_ambush_time_delayed( endEvent, waitFor )
{
	self notify( "bot_add_ambush_time_delayed" );
	self endon( "bot_add_ambush_time_delayed" );
	self endon( "death" );
	self endon( "disconnect" );

	if ( IsDefined( endEvent ) )
		self endon( endEvent );
	self endon( "node_relinquished" );
	self endon( "bad_path" );
	
	// Add in how long we waited to the self.ambush_end time
	startTime = GetTime();
	
	if ( IsDefined( waitFor ) )
		self waittill( waitFor );
	
	if ( IsDefined( self.ambush_end ) && IsDefined( self.node_ambushing_from ) )
	{
		self.ambush_end += GetTime() - startTime;
		self.node_ambushing_from.bot_ambush_end = self.ambush_end + 10000;
	}
	self notify( "bot_add_ambush_time_delayed" );
}

//=======================================================
//			bot_watch_entrances_delayed
//=======================================================
bot_watch_entrances_delayed( endEvent, waitFor, entrances, yaw )
{
	self notify( "bot_watch_entrances_delayed" );
	
	if ( entrances.size > 0 )
	{
		self endon( "bot_watch_entrances_delayed" );
		self endon( "death" );
		self endon( "disconnect" );
	
		self endon( endEvent );
		self endon( "node_relinquished" );
		self endon( "bad_path" );
		
		if ( IsDefined( waitFor ) ) 
			self waittill( waitFor );
		
		self endon("path_enemy");
		self childthread bot_watch_nodes( entrances, yaw, 0, self.ambush_end );
		self childthread bot_monitor_watch_entrances_camp();
	}
}

bot_monitor_watch_entrances_camp()
{
	self notify( "bot_monitor_watch_entrances_camp" );
	self endon( "bot_monitor_watch_entrances_camp" );
	self notify( "bot_monitor_watch_entrances" );
	self endon( "bot_monitor_watch_entrances" );
	self endon( "disconnect" );
	self endon( "death" );
	
	while(!IsDefined(self.watch_nodes))
		wait(0.05);
	
	while( IsDefined( self.watch_nodes ) )
	{
		foreach( node in self.watch_nodes )
			node.watch_node_chance[self.entity_number] = 1.0;
		
		prioritize_watch_nodes_toward_enemies(0.5);

		wait(RandomFloatRange(0.5,0.75));
	}
}

//=======================================================
//			bot_find_ambush_entrances
//=======================================================
bot_find_ambush_entrances( ambush_node, to_be_occupied )
{
	self endon("disconnect");
	
	// Get entrances and filter out any nodes that are too close or don't have exposure to the node in the crouching stance
	useEntrances = [];
	
	entrances = FindEntrances( ambush_node.origin );

	AssertEx( entrances.size > 0, "Entrance points for node at location " + ambush_node.origin + " could not be calculated.  Check pathgrid around that area" );
	
	if ( IsDefined( entrances ) && entrances.size > 0 )
	{
		wait(0.05);
		crouching = ( ambush_node.type != "Cover Stand" && ambush_node.type != "Conceal Stand" );
		
		if ( crouching && to_be_occupied ) 
			entrances = self BotNodeScoreMultiple( entrances, "node_exposure_vis", ambush_node.origin, "crouch" );
	
		foreach ( node in entrances )
		{
			if ( DistanceSquared( self.origin, node.origin ) < (300 * 300) )
				continue;
					
			if ( crouching && to_be_occupied )
			{
				wait 0.05;
		
				if ( !entrance_visible_from( node.origin, ambush_node.origin, "crouch" ) )
					continue;
			}
			
			useEntrances[useEntrances.size] = node;
		}
	}

	return useEntrances;
}

//=======================================================
//			bot_filter_ambush_inuse
//=======================================================
bot_filter_ambush_inuse( nodes )
{
	Assert(IsDefined(nodes));
	resultNodes = [];
	
	now = GetTime();
	nodesSize = nodes.size;
	
	for( i = 0; i < nodesSize; i++ )
	{
		node = nodes[i];
		if ( !IsDefined( node.bot_ambush_end ) || (now > node.bot_ambush_end) )
			resultNodes[resultNodes.size] = node;
	}
	
	return resultNodes;
}
	
//=======================================================
//			bot_filter_ambush_vicinity
//=======================================================
bot_filter_ambush_vicinity( nodes, bot, radius )
{
	resultNodes = [];
	checkPoints = [];
	
	radiusSq = (radius * radius);
	
	if ( level.teamBased )
	{
		foreach( player in level.participants )
		{
			if ( !isReallyAlive( player ) )
				continue;

			if ( !IsDefined( player.team ) )
				continue;
			
			if ( (player.team == bot.team) && (player != bot) && IsDefined( player.node_ambushing_from ) )
				checkPoints[checkPoints.size] = player.node_ambushing_from.origin;
		}
	}
	
	checkpointsSize = checkPoints.size;
	nodesSize = nodes.size;

	for( i = 0; i < nodesSize; i++ )
	{
		tooClose = false;
		node = nodes[i];

		for ( j = 0; !tooClose && j < checkpointsSize; j++ )
		{
			distSq = DistanceSquared( checkPoints[j], node.origin );
			tooClose = ( distSq < radiusSq );
		}
		
		if ( !tooClose )
			resultNodes[resultNodes.size] = node;
	}
	
	return resultNodes;
}

//=======================================================
//			clear_camper_data
//=======================================================
clear_camper_data()
{
	self notify( "clear_camper_data" );
	
	if ( IsDefined( self.node_ambushing_from ) && IsDefined( self.node_ambushing_from.bot_ambush_end ) )
		self.node_ambushing_from.bot_ambush_end = undefined;
	
	self.node_ambushing_from 	= undefined;
	self.point_to_ambush		= undefined;
	self.ambush_yaw				= undefined;
	self.ambush_entrances		= undefined;
	self.ambush_duration 		= RandomIntRange( 20000, 30000 );
	self.ambush_end				= -1;	
}

//=======================================================
//			should_select_new_ambush_point
//======================================================
should_select_new_ambush_point()
{
	if ( self bot_has_tactical_goal() )
		return false;

	if ( GetTime() > self.ambush_end )
		return true;
	
	if ( !self BotHasScriptGoal() )
		return true;
	
	return false;
}


//=======================================================
//					find_camp_node
//=======================================================
find_camp_node( )
{
	self notify( "find_camp_node" );
	self endon( "find_camp_node" );
	
	return self bot_queued_process( "find_camp_node_worker", ::find_camp_node_worker );
}

//=======================================================
//					find_camp_node_worker
//=======================================================
find_camp_node_worker( )
{
	self notify( "find_camp_node_worker" );
	self endon( "find_camp_node_worker" );
	
	self endon( "death" );
	self endon( "disconnect" );
	level endon( "game_ended" );

	self clear_camper_data();
	
	if ( level.zoneCount <= 0 )
		return false;
		
	myZone = GetZoneNearest( self.origin );
	targetZone = undefined;
	nextZone = undefined;
	faceAngles = self.angles;
	
	if ( IsDefined( myZone ) )
	{
		// Get nearest zone with predicted enemies but no allies
		zoneEnemies = BotZoneNearestCount( myZone, self.team, -1, "enemy_predict", ">", 0, "ally", "<", 1 );

		// Fallback to just nearest zone with enemies if that came back empty
		if ( !IsDefined( zoneEnemies ) )
			zoneEnemies = BotZoneNearestCount( myZone, self.team, -1, "enemy_predict", ">", 0 );
		
		if ( IsDefined(zoneEnemies) )
		{
			special_zone_node = GetZoneNodeForIndex(zoneEnemies);
			linked_zones = GetLinkedNodes(special_zone_node);
			if ( linked_zones.size == 0 )
				zoneEnemies = undefined;	// Enemy zone isn't linked to any other zones, so ignore it
		}

		// If we have no idea where enemies are then pick the zone furthest from me
		if ( !IsDefined( zoneEnemies ) )
		{
			furthestDist = -1;
			furthestZone = -1;
			for ( z = 0; z < level.zoneCount; z++ )
			{
				special_zone_node = GetZoneNodeForIndex(z);
				linked_zones = GetLinkedNodes(special_zone_node);
				if ( linked_zones.size > 0 )
				{
					random_node_in_zone = Random(GetZoneNodes(z));
					node_not_valid_for_random_path = IsDefined(random_node_in_zone.targetname) && random_node_in_zone.targetname == "no_bot_random_path";
					if ( !node_not_valid_for_random_path )
					{
						dist = Distance2DSquared( GetZoneOrigin( z ), self.origin );
						if ( dist > furthestDist )
						{
							furthestDist = dist;
							furthestZone = z;
						}
					}
				}
			}
			
			Assert(furthestZone >= 0);
			zoneEnemies = furthestZone;
		}

		Assert( IsDefined( zoneEnemies ) );
		
		zonePath = GetZonePath( myZone, zoneEnemies );
		if ( !IsDefined(zonePath) || zonePath.size == 0 )
		{
			AssertMsg("Bot unable to find path from zone " + myZone + " at origin " + GetZoneOrigin( myZone ) + " to zone " + zoneEnemies + " at origin " + GetZoneOrigin( zoneEnemies ) );
			return false;
		}
		
		index = 0;
			
		// Pick a point along the path adjacent to predicted enemies but no further than the midpoint
		while ( index <= int(zonePath.size / 2) )
		{
			targetZone = zonePath[index];
			nextZone = zonePath[int(min(index+1, zonePath.size - 1))];

			if ( BotZoneGetCount( nextZone, self.team, "enemy_predict" ) != 0 )
				break;
			
			index++;
		}

		if ( IsDefined( targetZone ) && IsDefined( nextZone ) && targetZone != nextZone )
		{
			faceAngles = GetZoneOrigin( nextZone ) - GetZoneOrigin( targetZone );
			faceAngles = VectorToAngles( faceAngles );
		}
	}
		
	node_to_camp = undefined;
	
	if ( IsDefined( targetZone ) )
	{
		keep_searching = true;
		zone_steps = 1;
		use_lenient_flag = false;
		while( keep_searching )
		{
			// get set of nodes in the region we want to camp
			nodes_to_select_from = GetZoneNodesByDist( targetZone, 800 * zone_steps, true );
			if ( nodes_to_select_from.size > 1024 )
				nodes_to_select_from = GetZoneNodes( targetZone, 0 );
			
			wait 0.05;		
			
			// get direction we want to face in that region
			randomRoll = RandomInt( 100 );
			if ( randomRoll < 66 && randomRoll >= 33 )
				faceAngles = (faceAngles[0], faceAngles[1] + 45, 0);
			else if ( randomRoll < 33 )
				faceAngles = (faceAngles[0], faceAngles[1] - 45, 0);	
					
			// Choose from only the BEST camp spots from within those nodes facing that direction
			if ( nodes_to_select_from.size > 0 )
			{
				while( nodes_to_select_from.size > 1024 )
				{
					// This is probably because zones haven't been set up so there's one giant zone in the map
					// BotNodePickMultiple can't handle an array greater than 1024, so need to truncate it
					nodes_to_select_from[nodes_to_select_from.size-1] = undefined;
				}
				
				// Only want to pick from the best of the best
				selectCount = int( clamp( nodes_to_select_from.size * 0.15, 1, 10 ) );
				
				if ( use_lenient_flag )
					nodes_to_select_from = self BotNodePickMultiple( nodes_to_select_from, selectCount, selectCount, "node_camp", AnglesToForward( faceAngles ), "lenient" );
				else
					nodes_to_select_from = self BotNodePickMultiple( nodes_to_select_from, selectCount, selectCount, "node_camp", AnglesToForward( faceAngles ) );

				nodes_to_select_from = bot_filter_ambush_inuse( nodes_to_select_from );
				if ( !IsDefined(self.can_camp_near_others) || !self.can_camp_near_others )
				{
					vicinity_radius = 800;
					nodes_to_select_from = bot_filter_ambush_vicinity( nodes_to_select_from, self, vicinity_radius );
				}
				
				if ( nodes_to_select_from.size > 0 )
					node_to_camp = random_weight_sorted( nodes_to_select_from );
			}
			
			if ( IsDefined(node_to_camp) )
			{
				keep_searching = false;
			}
			else
			{
				if ( IsDefined(self.camping_needs_fallback_camp_location) )
				{
					if ( zone_steps == 1 && !use_lenient_flag )
					{
						// First try 3 steps away instead of 1
						zone_steps = 3;
					}
					else if ( zone_steps == 3 && !use_lenient_flag )
					{
						// 3 steps failed, so try using the lenient flag
						use_lenient_flag = true;
					}
					else if ( zone_steps == 3 && use_lenient_flag )
					{
						// 3 zone steps AND the lenient flag didn't do it, so just bail
						keep_searching = false;
					}
				}
				else
				{
					keep_searching = false;
				}
			}
			
			if ( keep_searching )
				wait 0.05;		
		}
	}
		
	if ( !IsDefined( node_to_camp ) || !self BotNodeAvailable( node_to_camp ) )
		return false;
	
	self.node_ambushing_from = node_to_camp;
	self.ambush_end = GetTime() + self.ambush_duration;
	self.node_ambushing_from.bot_ambush_end = self.ambush_end;
	self.ambush_yaw = faceAngles[1];
		
	return true;
}


//=======================================================
//					find_ambush_node
//=======================================================
find_ambush_node( optional_point_to_ambush, optional_ambush_radius )
{
	self clear_camper_data();
	
	if ( IsDefined(optional_point_to_ambush) )
	{
		self.point_to_ambush = optional_point_to_ambush;
	}
	else
	{
		// get all the high traffic nodes near the bot
		node_to_ambush = undefined;
		nodes_around_bot = GetNodesInRadius( self.origin, 5000, 0, 2000 );
		if ( nodes_around_bot.size > 0 )
		{
			node_to_ambush = self BotNodePick( nodes_around_bot, nodes_around_bot.size * 0.25, "node_traffic" );
		}
		
		if ( IsDefined( node_to_ambush ) )
		{
			self.point_to_ambush = node_to_ambush.origin;
		}
		else
		{
			return false;
		}
	}
	
	ambush_radius = 2000;
	if ( IsDefined(optional_ambush_radius) )
		ambush_radius = optional_ambush_radius;

	nodes_around_ambush_point = GetNodesInRadius( self.point_to_ambush, ambush_radius, 0, 1000 );
	ambush_node_trying = undefined;
	Assert(IsDefined(nodes_around_ambush_point));
	if ( nodes_around_ambush_point.size > 0 )
	{
		selectCount = int(max( 1, int( nodes_around_ambush_point.size * 0.15 ) ));
		nodes_around_ambush_point = self BotNodePickMultiple( nodes_around_ambush_point, selectCount, selectCount, "node_ambush", self.point_to_ambush );
	}
	
	Assert(IsDefined(nodes_around_ambush_point));
	nodes_around_ambush_point = bot_filter_ambush_inuse( nodes_around_ambush_point );
	
	if ( nodes_around_ambush_point.size > 0 )
		ambush_node_trying = random_weight_sorted( nodes_around_ambush_point );
	
	if( !IsDefined( ambush_node_trying ) || !self BotNodeAvailable( ambush_node_trying )  )
		return false;
	
	self.node_ambushing_from = ambush_node_trying;
	self.ambush_end = GetTime() + self.ambush_duration;
	self.node_ambushing_from.bot_ambush_end = self.ambush_end;
	
	node_to_ambush_point = VectorNormalize( self.point_to_ambush - self.node_ambushing_from.origin );
	node_to_ambush_point_angles = VectorToAngles (node_to_ambush_point );

	self.ambush_yaw = node_to_ambush_point_angles[1];
	
	return true;
}

bot_random_path()
{
	if ( self bot_is_remote_or_linked() )
		return false;
	
	random_path_func = level.bot_random_path_function[self.team];
	
	return self [[random_path_func]]();
}


//=======================================================
//					bot_random_path_default
//=======================================================
bot_random_path_default()
{
	result = false;
	
	chance_to_seek_out_killer = 50;
	if ( self.personality == "camper" )
	{
		chance_to_seek_out_killer = 0;
	}
	
	goalPos = undefined;
	if ( RandomInt(100) < chance_to_seek_out_killer )
	{
		goalPos = bot_recent_point_of_interest();
	}
	
	if ( !IsDefined( goalPos ) )
	{
		// Find a random place to go
		randomNode = self BotFindNodeRandom();
		if ( IsDefined( randomNode ) )
		{
			goalPos = randomNode.origin;
		}
	}
	
	if ( IsDefined( goalPos ) )
	{
		result = self BotSetScriptGoal( goalPos, 128, "hunt" );
	}
	
	return result;
}


//=======================================================
//				bot_setup_callback_class
//=======================================================
bot_setup_callback_class()
{
	if ( practiceRoundGame() )
	{
		return "practice" + RandomIntRange(1,6);
	}
	if ( self bot_setup_loadout_callback() )
		return "callback";
	else
		return "class0";
}
