#include common_scripts\utility; //#include common_scripts\shared; #include maps\mp\_utility; #include maps\mp\gametypes\_gamelogic; #include maps\mp\bots\_bots_ks; #include maps\mp\bots\_bots_util; #include maps\mp\bots\_bots_strategy; #include maps\mp\bots\_bots_personality; #include maps\mp\bots\_bots_fireteam; //======================================================== // main //======================================================== main() { if ( is_aliens() ) return; if( IsDefined( level.createFX_enabled ) && level.createFX_enabled ) return; if ( level.script == "mp_character_room" ) return; // This is called directly from native code on game startup // The particular gametype's main() is called from native code afterward setup_callbacks(); setup_personalities(); // Enable badplaces in destructibles level.badplace_cylinder_func = ::badplace_cylinder; level.badplace_delete_func = ::badplace_delete; // Init bot killstreak script maps\mp\bots\_bots_ks::bot_killstreak_setup(); // Init bot loadout data // Needs to be after _bots_ks::bot_killstreak_setup maps\mp\bots\_bots_loadout::init(); // Needs to be after _bots_loadout::init() level thread init(); /* /# maps\mp\bots\_bots_gametype_dom::empty_function_to_force_script_dev_compile(); maps\mp\bots\_bots_gametype_sr::empty_function_to_force_script_dev_compile(); maps\mp\bots\_bots_gametype_mugger::empty_function_to_force_script_dev_compile(); maps\mp\bots\_bots_gametype_conf::empty_function_to_force_script_dev_compile(); maps\mp\bots\_bots_gametype_sd::empty_function_to_force_script_dev_compile(); maps\mp\bots\_bots_gametype_blitz::empty_function_to_force_script_dev_compile(); #/ */ } //======================================================== // setup_callbacks //======================================================== setup_callbacks() { // Setup level.bot_funcs callback function table level.bot_funcs = []; // Bot System functions level.bot_funcs["bots_spawn"] = ::spawn_bots; level.bot_funcs["bots_add_scavenger_bag"] = ::bot_add_scavenger_bag; level.bot_funcs["bots_add_to_level_targets"] = ::bot_add_to_bot_level_targets; level.bot_funcs["bots_remove_from_level_targets"] = ::bot_remove_from_bot_level_targets; level.bot_funcs["bots_make_entity_sentient"] = ::bot_make_entity_sentient; // Bot entity functions level.bot_funcs["think"] = ::bot_think; level.bot_funcs["on_killed"] = ::on_bot_killed; level.bot_funcs["should_do_killcam"] = ::bot_should_do_killcam; level.bot_funcs["get_attacker_ent"] = ::bot_get_known_attacker; level.bot_funcs["should_pickup_weapons"] = ::bot_should_pickup_weapons; level.bot_funcs["on_damaged"] = ::bot_damage_callback; level.bot_funcs["gametype_think"] = ::default_gametype_think; level.bot_funcs["leader_dialog"] = ::bot_leader_dialog; level.bot_funcs["player_spawned"] = ::bot_player_spawned; level.bot_funcs["should_start_cautious_approach"] = ::should_start_cautious_approach_default; level.bot_funcs["know_enemies_on_start"] = ::bot_know_enemies_on_start; level.bot_funcs["bot_get_rank_xp"] = ::bot_get_rank_xp; level.bot_funcs["ai_3d_sighting_model"] = ::bot_3d_sighting_model; level.bot_funcs["dropped_weapon_think"] = ::bot_think_seek_dropped_weapons; level.bot_funcs["dropped_weapon_cancel"] = ::should_stop_seeking_weapon; level.bot_funcs["crate_can_use"] = ::crate_can_use_always; level.bot_funcs["crate_low_ammo_check"] = ::crate_low_ammo_check; level.bot_funcs["crate_should_claim"] = ::crate_should_claim; level.bot_funcs["crate_wait_use"] = ::crate_wait_use; level.bot_funcs["crate_in_range"] = ::crate_in_range; level.bot_funcs["post_teleport"] = ::bot_post_teleport; level.bot_random_path_function = []; level.bot_random_path_function["allies"] = ::bot_random_path_default; level.bot_random_path_function["axis"] = ::bot_random_path_default; level.bot_can_use_box_by_type["deployable_vest"] = ::bot_should_use_ballistic_vest_crate; level.bot_can_use_box_by_type["deployable_ammo"] = ::bot_should_use_ammo_crate; level.bot_can_use_box_by_type["scavenger_bag"] = ::bot_should_use_scavenger_bag; level.bot_can_use_box_by_type["deployable_grenades"]= ::bot_should_use_grenade_crate; level.bot_can_use_box_by_type["deployable_juicebox"]= ::bot_should_use_juicebox_crate; level.bot_pre_use_box_of_type["deployable_ammo"] = ::bot_pre_use_ammo_crate; level.bot_post_use_box_of_type["deployable_ammo"] = ::bot_post_use_ammo_crate; level.bot_find_defend_node_func["capture"] = ::find_defend_node_capture; level.bot_find_defend_node_func["capture_zone"] = ::find_defend_node_capture_zone; level.bot_find_defend_node_func["protect"] = ::find_defend_node_protect; level.bot_find_defend_node_func["bodyguard"] = ::find_defend_node_bodyguard; level.bot_find_defend_node_func["patrol"] = ::find_defend_node_patrol; // War (TDM) gametype serves as the default, so we use it to setup default gametype callbacks maps\mp\bots\_bots_gametype_war::setup_callbacks(); // Should be after everything else, so fireteam mode can override specific callbacks if necessary if ( bot_is_fireteam_mode() ) { bot_fireteam_setup_callbacks(); } } //======================================================== // CodeCallback_LeaderDialog //======================================================== CodeCallback_LeaderDialog( dialog, location ) { if ( IsDefined( level.bot_funcs ) && IsDefined( level.bot_funcs["leader_dialog"] ) ) { self [[ level.bot_funcs["leader_dialog"] ]]( dialog, location ); } } //======================================================== // init //======================================================== init() { thread monitor_smoke_grenades(); thread bot_triggers(); initBotLevelVariables(); /# level thread bot_debug_drawing(); #/ if( !shouldSpawnBots() ) return; refresh_existing_bots(); botAutoConnectValue = BotAutoConnectEnabled(); if ( botAutoConnectValue > 0 ) { setMatchData( "hasBots", true ); if ( bot_is_fireteam_mode() ) { // Fireteam mode : spawn bots on each team with specific loadouts level thread bot_fireteam_init(); // Init Fireteam commander logic - this needs to be called after Callback_StartGameType's init() functions. level thread maps\mp\bots\_bots_fireteam_commander::init(); } else// if ( botAutoConnectValue == 1 ) { // "fill_open" : drop and spawn bots as needed level thread bot_connect_monitor(); } } } //======================================================== // initVariables //======================================================== initBotLevelVariables() { if ( !IsDefined(level.crateOwnerUseTime) ) { level.crateOwnerUseTime = 500; } if ( !IsDefined(level.crateNonOwnerUseTime) ) { level.crateNonOwnerUseTime = 3000; } // Time it takes (after losing track of an enemy) for a bot to consider himself "out of combat" level.bot_out_of_combat_time = 3000; // Weapon that bots will use to shoot down helicopters level.bot_respawn_launcher_name = "iw6_panzerfaust3"; // Weapon to use as absolute fallback level.bot_fallback_weapon = "iw6_knifeonly"; level.zoneCount = GetZoneCount(); initBotMapExtents(); } initBotMapExtents() { if ( IsDefined(level.teleportGetActiveNodesFunc) ) all_nodes = [[level.teleportGetActiveNodesFunc]](); else all_nodes = GetAllNodes(); level.bot_map_min_x = 0; level.bot_map_max_x = 0; level.bot_map_min_y = 0; level.bot_map_max_y = 0; level.bot_map_min_z = 0; level.bot_map_max_z = 0; if ( all_nodes.size > 1 ) { level.bot_map_min_x = all_nodes[0].origin[0]; level.bot_map_max_x = all_nodes[0].origin[0]; level.bot_map_min_y = all_nodes[0].origin[1]; level.bot_map_max_y = all_nodes[0].origin[1]; level.bot_map_min_z = all_nodes[0].origin[2]; level.bot_map_max_z = all_nodes[0].origin[2]; for ( i = 1; i < all_nodes.size; i++ ) { node_origin = all_nodes[i].origin; if ( node_origin[0] < level.bot_map_min_x ) level.bot_map_min_x = node_origin[0]; if ( node_origin[0] > level.bot_map_max_x ) level.bot_map_max_x = node_origin[0]; if ( node_origin[1] < level.bot_map_min_y ) level.bot_map_min_y = node_origin[1]; if ( node_origin[1] > level.bot_map_max_y ) level.bot_map_max_y = node_origin[1]; if ( node_origin[2] < level.bot_map_min_z ) level.bot_map_min_z = node_origin[2]; if ( node_origin[2] > level.bot_map_max_z ) level.bot_map_max_z = node_origin[2]; } } level.bot_map_center = ( (level.bot_map_min_x + level.bot_map_max_x)/2 , (level.bot_map_min_y + level.bot_map_max_y)/2 , (level.bot_map_min_z + level.bot_map_max_z)/2 ); // Needs to be the last line of initBotLevelVariables level.bot_variables_initialized = true; } bot_post_teleport() { level.bot_variables_initialized = undefined; level.bot_initialized_remote_vehicles = undefined; initBotMapExtents(); maps\mp\bots\_bots_ks_remote_vehicle::remote_vehicle_setup(); } //======================================================== // shouldSpawnBots //======================================================== shouldSpawnBots() { return true; } refresh_existing_bots() { wait 1; // give level.players a chance to initialize between rounds // If we switched sides, the bots will still exist in game but will have lost their think threads. So we need to restart them foreach ( player in level.players ) { if ( IsBot( player ) ) { player.equipment_enabled = true; player.bot_team = player.team; player.bot_spawned_before = true; player thread [[ level.bot_funcs["think"] ]](); } } } /# bot_debug_drawing() { level endon( "game_ended" ); while( !bot_bots_enabled_or_added() ) wait(1.0); level.defense_debug_structs = []; for( i=0; i<10; i++ ) { level.defense_debug_structs[level.defense_debug_structs.size] = SpawnStruct(); } level.cur_num_defense_debug_structs = 0; SetDevDvarIfUninitialized("bot_DebugPathToPlayer",""); for(;;) { draw_debug_type = GetDvar("bot_DrawDebugSpecial"); clear_defense_draw(); if ( draw_debug_type == "defend" ) { bot_debug_draw_defense(); } else if ( draw_debug_type == "entrances" ) { bot_debug_draw_watch_nodes(); } else if ( draw_debug_type == "defend_with_entrances" ) { bot_debug_draw_defense(); bot_debug_draw_watch_nodes(); } else if ( draw_debug_type == "key_entry_points" ) { bot_debug_draw_cached_entrances(); } else if ( draw_debug_type == "key_cached_paths" ) { bot_debug_draw_cached_paths(); } if ( GetDvar( "bot_DebugPathToPlayer" ) != "" && level.gameType == "war" ) { SetDevDvar( "bot_DebugPathToPlayer", "" ); human = get_all_humans()[0]; if ( IsAlive(human) && human.team != "spectator" ) { level.bot_debug_force_path_location = human.origin; } } /* // For testing bullet trace collision if ( IsDefined(level.players[0]) && IsAlive(level.players[0]) ) { start = level.players[0] GetEye(); player_forward = AnglesToForward(level.players[0] GetPlayerAngles()); player_right = AnglesToRight(level.players[0] GetPlayerAngles()); player_up = AnglesToUp(level.players[0] GetPlayerAngles()); end = start + player_forward * 100; traceResult = BulletTrace( start, end, false, level.players[0], false, false, true ); bot_draw_cylinder(end - (0,0,2.5), 4, 5, 0.05, undefined, (0,1,0), true, 4); // draw line from start to frac as blue, and frac to end as red line_start = start - player_up; line_end = end - player_up; line_midpoint = line_start + (line_end - line_start) * traceResult["fraction"]; Line( line_start, line_midpoint, (0,0,1), 1.0, false ); Line( line_midpoint, line_end, (1,0,0), 1.0, false ); } */ wait(0.05); } } clear_defense_draw() { for( i=0; i 0 && IsAI( player ) && player bot_is_defending() ) { struct_for_defense = undefined; for( i=0; i 0 && IsAI( player ) && IsDefined(player.watch_nodes) ) { node_offset = (0,0,player GetPlayerViewHeight()); foreach( node in player.watch_nodes ) { // green means "high priority watch" // white means "low priority watch" // color lerps between the two entrance_color = (1 - node.watch_node_chance[player.entity_number], 1, 1 - node.watch_node_chance[player.entity_number]); line( player.origin + node_offset, node.origin + (0,0,30), entrance_color, 1, true ); bot_draw_cylinder(node.origin + (0,0,30), 10, 10, 0.05, undefined, entrance_color, true, 4); } } } } bot_debug_draw_cached_entrances() { if ( !IsDefined( level.entrance_indices ) || !IsDefined(level.entrance_points) ) return; node_offset = (0,0,11); standing_offset = (0,0,55); crouching_offset = (0,0,40); prone_offset = (0,0,15); color_visible = (0,1,0); color_not_visible = (1,0,0); node_height = 13; for ( i = 0; i < level.entrance_indices.size; i++ ) { entrance_collection = level.entrance_points[level.entrance_indices[i]]; for ( j = 0; j < entrance_collection.size; j++ ) { bot_draw_cylinder(entrance_collection[j].origin + node_offset, 10, node_height, 0.05, undefined, (0,1,0), true, 4); // standing line( level.entrance_origin_points[i] + standing_offset, entrance_collection[j].origin + node_offset + (0,0,node_height/2), color_visible, 1, true ); // crouching color_to_use = color_not_visible; if ( entrance_collection[j].crouch_visible_from[level.entrance_indices[i]] ) color_to_use = color_visible; line( level.entrance_origin_points[i] + crouching_offset, entrance_collection[j].origin + node_offset + (0,0,node_height/2), color_to_use, 1, true ); // prone color_to_use = color_not_visible; if ( entrance_collection[j].prone_visible_from[level.entrance_indices[i]] ) color_to_use = color_visible; line( level.entrance_origin_points[i] + prone_offset, entrance_collection[j].origin + node_offset + (0,0,node_height/2), color_to_use, 1, true ); } // Note: this white cylinder is the botTarget, if it exists bot_draw_cylinder(level.entrance_origin_points[i], 10, 75, 0.05, undefined, (1,1,1), true, 8); } } bot_debug_draw_cached_paths() { if ( !IsDefined( level.entrance_indices ) ) return; node_colors = [ (1,0,0), (0,1,0), (0,0,1), (1,0,1), (1,1,0), (0,1,1), (1,0.6,0.6), (0.6,1,0.6), (0.6,0.6,1), (0.1,0.1,0.1) ]; if ( !IsDefined(level.next_path_time) ) { level.bot_debug_cur_first_index = level.entrance_indices.size - 2; level.bot_debug_cur_second_index = level.entrance_indices.size - 1; level.next_path_time = GetTime() - 1; } if ( GetTime() > level.next_path_time ) { keep_trying = true; while(keep_trying) { if ( level.bot_debug_cur_second_index == level.entrance_indices.size - 1 ) { if ( level.bot_debug_cur_first_index == level.entrance_indices.size - 2 ) { level.bot_debug_cur_first_index = 0; level.bot_debug_cur_node_color = -1; } else { level.bot_debug_cur_first_index++; } level.bot_debug_cur_second_index = level.bot_debug_cur_first_index + 1; } else { level.bot_debug_cur_second_index++; } keep_trying = !IsDefined(level.precalculated_paths[level.entrance_indices[level.bot_debug_cur_first_index]][level.entrance_indices[level.bot_debug_cur_second_index]]); } level.bot_debug_cur_node_color = (level.bot_debug_cur_node_color + 1) % node_colors.size; level.next_path_time = GetTime() + (15 * 1000); } // Note: these white cylinders are the botTargets, if they exist bot_draw_cylinder(level.entrance_origin_points[level.bot_debug_cur_first_index], 10, 75, 0.05, undefined, (1,1,1), true, 8); bot_draw_cylinder(level.entrance_origin_points[level.bot_debug_cur_second_index], 10, 75, 0.05, undefined, (1,1,1), true, 8); foreach( node in level.precalculated_paths[level.entrance_indices[level.bot_debug_cur_first_index]][level.entrance_indices[level.bot_debug_cur_second_index]] ) { bot_draw_cylinder(node.origin, 10, 13, 0.05, undefined, node_colors[level.bot_debug_cur_node_color], true, 4); } } #/ //======================================================== // bot_player_spawned //======================================================== bot_player_spawned() { self bot_set_loadout_class(); if( IsDefined( self.prev_personality ) ) { self bot_set_personality( self.prev_personality ); self.prev_personality = undefined; } } bot_set_loadout_class() { if ( !IsDefined( self.bot_class ) ) { if ( !bot_gametype_chooses_class() ) { while( !IsDefined(level.bot_loadouts_initialized) ) wait(0.05); /# debugClass = GetDvar( "bot_debugClass", "" ); if ( IsDefined( debugClass ) && debugClass != "" ) { self.bot_class = debugClass; return; } #/ if ( IsDefined( self.override_class_function ) ) { self.bot_class = [[ self.override_class_function ]](); } else { self.bot_class = bot_setup_callback_class(); } } else { self.bot_class = self.class; } } } watch_players_connecting() { while(1) { level waittill("connected", player); if ( !IsAI( player ) && (level.players.size > 0) ) // If playing a local listen server, ignore this for the first player to join { level.players_waiting_to_join = array_add(level.players_waiting_to_join, player); childthread bots_notify_on_spawn(player); childthread bots_notify_on_disconnect(player); childthread bots_remove_from_array_on_notify(player); } } } bots_notify_on_spawn(player) { player endon("bots_human_disconnected"); while( !array_contains(level.players,player) ) { wait(0.05); } player notify("bots_human_spawned"); } bots_notify_on_disconnect(player) { player endon("bots_human_spawned"); player waittill("disconnect"); player notify("bots_human_disconnected"); } bots_remove_from_array_on_notify(player) { player waittill_any("bots_human_spawned","bots_human_disconnected"); level.players_waiting_to_join = array_remove(level.players_waiting_to_join,player); } monitor_pause_spawning() { // The purpose of this function (and all the childthreads) is to pause bot spawning while a human player is in the process of joining the game // So when a human connects, we add him to the "waiting" array, and he stays in there until he spawns in the game or disconnects // As long as there are any humans in the queue, we don't want bots to be spawning and taking up the human players' spots level.players_waiting_to_join = []; childthread watch_players_connecting(); while(1) { if ( level.players_waiting_to_join.size > 0 ) level.pausing_bot_connect_monitor = true; else level.pausing_bot_connect_monitor = false; wait(0.5); } } //======================================================== // bot_can_join_team //======================================================== bot_can_join_team( team ) { // Checks ahead of time if _menus.gsc::setTeam() is going to approve of the bot's team choice // Only necessary to return false if we are in a private match with team balancing (or SS/SL) and game mode has team limits active if ( matchMakingGame() ) return true; if( GetDvar( "squad_vs_squad" ) == "1" ) return true; if ( !level.teamBased ) return true; if ( maps\mp\gametypes\_teams::getJoinTeamPermissions( team ) ) return true; return false; } //======================================================== // bot_connect_monitor //======================================================== bot_connect_monitor( num_ally_bots, num_enemy_bots ) { level endon( "game_ended" ); self notify( "bot_connect_monitor" ); self endon( "bot_connect_monitor" ); level.pausing_bot_connect_monitor = false; childthread monitor_pause_spawning(); maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( 0.5 ); bot_connect_monitor_update_time = 1.5; if ( !IsDefined( level.bot_cm_spawned_bots ) ) level.bot_cm_spawned_bots = false; if ( !IsDefined( level.bot_cm_waited_players_time ) ) level.bot_cm_waited_players_time = 0; if ( !IsDefined( level.bot_cm_human_picked ) ) level.bot_cm_human_picked = false; if( ( GetDvar( "squad_vs_squad" ) == "1" ) ) { while ( !( isDefined( level.squad_vs_squad_allies_client ) && isDefined( level.squad_vs_squad_axis_client ) ) ) { wait 0.05; } wait 2.0; // wait a bit for additional humans to connect } if( ( GetDvar( "squad_match" ) == "1" ) ) { while ( !isDefined( level.squad_match_client ) ) { wait 0.05; } wait 2.0; // wait a bit for additional humans to connect } if( ( GetDvar( "squad_use_hosts_squad" ) == "1" ) ) { while ( !isDefined( level.wargame_client ) ) { wait 0.05; } wait 2.0; // wait a bit for additional humans to connect } for(;;) { if ( level.pausing_bot_connect_monitor ) { maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( bot_connect_monitor_update_time ); continue; } ignore_team_balance = IsDefined(level.bots_ignore_team_balance) || !level.teamBased; // NOTE: if level.bots_ignore_team_balance is defined, these variables don't control the number of bots per team, // but combined they do control the absolute number of bots. For example if level.bots_ignore_team_balance is defined, and // max_ally_bots_absolute is 2 and max_enemy_bots_absolute is 8, the total number of bots you can have in the match is 10, but the // ally team could have all 10 of them. max_ally_bots_absolute = BotGetTeamLimit( 0 ); max_enemy_bots_absolute = BotGetTeamLimit( 1 ); difficultyAlly = BotGetTeamDifficulty( 0 ); difficultyEnemy = BotGetTeamDifficulty( 1 ); /# // dont kick/spawn bots for team balance while this is on (for example if test clients have been added via the scr_testclients dvar) if ( GetDvarInt("bot_DisableAutoConnect") ) { maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( bot_connect_monitor_update_time ); continue; } #/ team_ally = "allies"; // team_ally is the team that the human player is on team_enemy = "axis"; // team_enemy is the enemy team (opposed to the team the human player is on) clientCounts = bot_client_counts(); numHumans = cat_array_get( clientCounts, "humans" ); if ( numHumans > 1 ) { hostPlayerTeam = bot_get_host_team(); if ( !matchMakingGame() && IsDefined(hostPlayerTeam) && hostPlayerTeam != "spectator" ) { team_ally = hostPlayerTeam; team_enemy = getOtherTeam( hostPlayerTeam ); } else { cur_num_allies_players = cat_array_get( clientCounts, "humans_" + "allies"); cur_num_axis_players = cat_array_get( clientCounts, "humans_" + "axis"); if ( cur_num_axis_players > cur_num_allies_players ) { // If there are more axis players, consider that to be team_ally (team that human players are on) team_ally = "axis"; team_enemy = "allies"; } } } else { humanPlayer = get_human_player(); if ( IsDefined( humanPlayer ) ) { humanPlayer_team = humanPlayer bot_get_player_team(); if ( IsDefined(humanPlayer_team) && humanPlayer_team != "spectator" ) { team_ally = humanPlayer_team; team_enemy = getOtherTeam( humanPlayer_team ); } } } // Count the max size of each team (in terms of client limits) ally_team_size = bot_get_team_limit(); enemy_team_size = bot_get_team_limit(); if ( ally_team_size + enemy_team_size < bot_get_client_limit() ) { // The client limit is odd, so add 1 to a team so we are still at the client limit if ( ally_team_size < max_ally_bots_absolute ) ally_team_size++; else if ( enemy_team_size < max_enemy_bots_absolute ) enemy_team_size++; } // Count current number of humans cur_num_ally_humans = cat_array_get( clientCounts, "humans_" + team_ally); cur_num_enemy_humans = cat_array_get( clientCounts, "humans_" + team_enemy); cur_num_humans = cur_num_ally_humans + cur_num_enemy_humans; // Count current number of spectators and try predict which team they will join cur_num_spectators = cat_array_get( clientCounts, "spectator" ); cur_num_ally_spectators = 0; cur_num_enemy_spectators = 0; while( cur_num_spectators > 0 ) { ally_team_has_room_for_another_spectator = (cur_num_ally_humans + cur_num_ally_spectators + 1 <= ally_team_size); enemy_team_has_room_for_another_spectator = (cur_num_enemy_humans + cur_num_enemy_spectators + 1 <= enemy_team_size); if ( ally_team_has_room_for_another_spectator && !enemy_team_has_room_for_another_spectator ) { cur_num_ally_spectators++; } else if ( !ally_team_has_room_for_another_spectator && enemy_team_has_room_for_another_spectator ) { cur_num_enemy_spectators++; } else if ( ally_team_has_room_for_another_spectator && enemy_team_has_room_for_another_spectator ) { if ( (cur_num_spectators % 2) == 1 ) cur_num_ally_spectators++; else cur_num_enemy_spectators++; } cur_num_spectators--; } // Count current number of bots cur_num_ally_bots = cat_array_get( clientCounts, "bots_" + team_ally); cur_num_enemy_bots = cat_array_get( clientCounts, "bots_" + team_enemy); cur_num_bots = cur_num_ally_bots + cur_num_enemy_bots; if ( cur_num_bots > 0 ) level.bot_cm_spawned_bots = true; waitingForHumanToChoose = false; if ( !level.bot_cm_human_picked ) { waitingForHumanToChoose = !bot_get_human_picked_team(); /# // Exception for developer dvar bot_autoconnectdefault which is used to get default BotAutoConnectEnabled() in command line runs if ( GetDvarInt( "bot_autoconnectdefault" ) && waitingForHumanToChoose ) waitingForHumanToChoose = false; #/ if ( !waitingForHumanToChoose ) level.bot_cm_human_picked = true; } if ( waitingForHumanToChoose ) { // In split screen mode (Local Play as its called now) never spawn in until a human has made a choice splitScreenWait = !getDvarInt( "systemlink" ) && !getDvarInt( "onlinegame" ); // With lopsided team sizes dont spawn ANY bots until a human has picked their team or 10 seconds has passed and prematch is over lopsidedWait = !ignore_team_balance && (max_enemy_bots_absolute != max_ally_bots_absolute) && !level.bot_cm_spawned_bots && ((level.bot_cm_waited_players_time < 10) || !gameFlag("prematch_done")); if ( splitScreenWait || lopsidedWait ) { level.bot_cm_waited_players_time += bot_connect_monitor_update_time; maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( bot_connect_monitor_update_time ); continue; } } // Open bot slots per team is either the number of available spots (team size - number of humans) or the hard limit, whichever is lower ally_bot_slots = INT(min(ally_team_size - cur_num_ally_humans - cur_num_ally_spectators,max_ally_bots_absolute)); enemy_bot_slots = INT(min(enemy_team_size - cur_num_enemy_humans - cur_num_enemy_spectators,max_enemy_bots_absolute)); // Make sure we are not leaving one team short because of reduced absolute limits being imbalanced fillTeam = 1; slotCount = (ally_bot_slots + enemy_bot_slots + numHumans); slotLimit = (max_ally_bots_absolute + max_enemy_bots_absolute + numHumans); lastSlotCount = [-1, -1]; while ( (slotCount < bot_get_client_limit()) && (slotCount < slotLimit) ) { if ( fillTeam && (ally_bot_slots < max_ally_bots_absolute) && bot_can_join_team( team_ally ) ) ally_bot_slots++; else if ( !fillTeam && (enemy_bot_slots < max_enemy_bots_absolute) && bot_can_join_team( team_enemy ) ) enemy_bot_slots++; slotCount = (ally_bot_slots + enemy_bot_slots + numHumans); // if we have come back to the same team again and not added any new slots we are done if ( lastSlotCount[fillTeam] == slotCount ) break; lastSlotCount[fillTeam] = slotCount; fillTeam = !fillTeam; } // When teams are not deliberately unbalanced by request, keep waiting up to 10 seconds for human to pick a team before spawning last bot when there is only one human playing if ( (max_ally_bots_absolute == max_enemy_bots_absolute) && !ignore_team_balance && (cur_num_ally_spectators == 1) && (cur_num_enemy_spectators == 0) && (enemy_bot_slots > 0) ) { if ( !IsDefined( level.bot_prematchDoneTime ) && gameFlag("prematch_done") ) level.bot_prematchDoneTime = GetTime(); if ( waitingForHumanToChoose && (!IsDefined( level.bot_prematchDoneTime ) || ((GetTime() - level.bot_prematchDoneTime) < 10000)) ) enemy_bot_slots--; } // Number of bots wanted right now is the number of available slots (from above) minus the number of bots currently in the game ally_bots_wanted = ally_bot_slots - cur_num_ally_bots; enemy_bots_wanted = enemy_bot_slots - cur_num_enemy_bots; need_to_spawn_or_drop = true; if ( ignore_team_balance ) { // Don't move bots between teams, but maybe spawn or drop them if necessary total_team_size = ally_team_size + enemy_team_size; max_total_bots_absolute = max_ally_bots_absolute + max_enemy_bots_absolute; cur_num_total_humans = cur_num_ally_humans + cur_num_enemy_humans; cur_num_total_bots = cur_num_ally_bots + cur_num_enemy_bots; total_bot_slots_open = INT(min(total_team_size - cur_num_total_humans,max_total_bots_absolute)); total_num_bots_wanted = total_bot_slots_open - cur_num_total_bots; if ( total_num_bots_wanted == 0 ) { // No changes needed need_to_spawn_or_drop = false; } else if ( total_num_bots_wanted > 0 ) { // Need to add bots. Just even them out between teams (doesn't really matter though) ally_bots_wanted = INT(total_num_bots_wanted/2) + (total_num_bots_wanted % 2 ); enemy_bots_wanted = INT(total_num_bots_wanted/2); } else if ( total_num_bots_wanted < 0 ) { // Need to remove bots. First try to remove them from the ally team, then if that doesn't do it, the enemy team as well num_of_bots_to_drop = total_num_bots_wanted * -1; ally_bots_wanted = -1 * INT(min(num_of_bots_to_drop,cur_num_ally_bots)); enemy_bots_wanted = -1 * (num_of_bots_to_drop + ally_bots_wanted); } } else if ( !matchMakingGame() && (ally_bots_wanted * enemy_bots_wanted < 0 && gameFlag("prematch_done") && !IsDefined(level.bots_disable_team_switching)) ) { // ally_bots_wanted and enemy_bots_wanted are both nonzero and have opposite signs. // This means one team needs to gain players and one needs to lose them. So move bots from one team to the other. difference = INT(min(abs(ally_bots_wanted),abs(enemy_bots_wanted))); if ( ally_bots_wanted > 0 ) move_bots_from_team_to_team( difference, team_enemy, team_ally, difficultyAlly ); else if ( enemy_bots_wanted > 0 ) move_bots_from_team_to_team( difference, team_ally, team_enemy, difficultyEnemy ); need_to_spawn_or_drop = false; } if ( need_to_spawn_or_drop ) { // Spawn or drop bots for teams that are under / over the limit if ( enemy_bots_wanted < 0 ) drop_bots( enemy_bots_wanted * -1, team_enemy ); if ( ally_bots_wanted < 0 ) drop_bots( ally_bots_wanted * -1, team_ally ); if ( enemy_bots_wanted > 0 ) level thread spawn_bots( enemy_bots_wanted, team_enemy, undefined, undefined, "spawned_enemies", difficultyEnemy ); if ( ally_bots_wanted > 0 ) level thread spawn_bots( ally_bots_wanted, team_ally, undefined, undefined, "spawned_allies", difficultyAlly ); if ( enemy_bots_wanted > 0 && ally_bots_wanted > 0 ) level waittill_multiple( "spawned_enemies", "spawned_allies" ); else if ( enemy_bots_wanted > 0 ) level waittill( "spawned_enemies" ); else if ( ally_bots_wanted > 0 ) level waittill( "spawned_allies" ); } if ( difficultyEnemy != difficultyAlly ) { bots_update_difficulty( team_enemy, difficultyEnemy ); bots_update_difficulty( team_ally, difficultyAlly ); } maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( bot_connect_monitor_update_time ); } } bot_get_player_team() { if ( IsDefined(self.team) ) return self.team; if ( IsDefined(self.pers["team"]) ) return self.pers["team"]; return undefined; } bot_get_host_team() { foreach( player in level.players ) { if ( !isAI( player ) && player isHost() ) return player bot_get_player_team(); } return "spectator"; } bot_get_human_picked_team() { haveHost = false; humanChose = false; hostChose = false; foreach ( player in level.players ) { if ( !isAI( player ) ) { if ( player isHost() ) haveHost = true; if ( player_picked_team( player ) ) { humanChose = true; if ( player IsHost() ) hostChose = true; } } } return ( hostChose || (humanChose && !haveHost) ); } player_picked_team( player ) { if ( IsDefined( player.team ) && player.team != "spectator" ) return true; if ( IsDefined( player.spectating_actively ) && player.spectating_actively ) return true; if ( player IsMLGSpectator() && IsDefined( player.team ) && player.team == "spectator" ) return true; return false; } bot_client_counts() { clientCounts = []; for ( i = 0; i < level.players.size; i++ ) { player = level.players[i]; if ( IsDefined(player) && IsDefined(player.team) ) { clientCounts = cat_array_add( clientCounts, "all" ); clientCounts = cat_array_add( clientCounts, player.team ); if ( IsBot( player ) ) { clientCounts = cat_array_add( clientCounts, "bots" ); clientCounts = cat_array_add( clientCounts, "bots_" + player.team ); } else { clientCounts = cat_array_add( clientCounts, "humans" ); clientCounts = cat_array_add( clientCounts, "humans_" + player.team ); } } } return clientCounts; } cat_array_add( arrayCounts, category ) { if ( !IsDefined( arrayCounts ) ) { arrayCounts = []; } if ( !IsDefined( arrayCounts[ category ] ) ) { arrayCounts[ category ] = 0; } arrayCounts[ category ] = arrayCounts[ category ] + 1; return arrayCounts; } cat_array_get( arrayCounts, category ) { if ( !IsDefined( arrayCounts ) ) { return 0; } if ( !IsDefined( arrayCounts[ category ] ) ) { return 0; } return arrayCounts[ category ]; } //======================================================== // move_bots_from_team_to_team //======================================================== move_bots_from_team_to_team( count, teamFrom, teamTo, difficulty ) { foreach ( player in level.players ) { if ( !IsDefined( player.team ) ) continue; if ( IsDefined( player.connected ) && player.connected && IsBot( player ) && player.team == teamFrom ) { player.bot_team = teamTo; if ( IsDefined( difficulty ) ) player bot_set_difficulty( difficulty ); player notify( "luinotifyserver", "team_select", bot_lui_convert_team_to_int(teamTo) ); wait(0.05); // Wait for the team change player notify( "luinotifyserver", "class_select", player.bot_class ); count--; if ( count <= 0 ) break; else wait(0.1); } } } //======================================================== // bots_update_difficulty //======================================================== bots_update_difficulty( team, difficulty ) { foreach ( player in level.players ) { if ( !IsDefined( player.team ) ) continue; if ( IsDefined( player.connected ) && player.connected && IsBot( player ) && player.team == team ) { if ( difficulty != (player BotGetDifficulty()) ) player bot_set_difficulty( difficulty ); } } } //======================================================== // bot_drop //======================================================== bot_drop() // self = bot { assert( isBot( self ) ); assert( self.connected ); kick( self.entity_number, "EXE_PLAYERKICKED_BOT_BALANCE" ); wait 0.1; } //======================================================== // drop_bots //======================================================== drop_bots( count, team ) { bots = []; foreach ( player in level.players ) { if ( IsDefined( player.connected ) && player.connected && IsBot( player ) && (!IsDefined( team ) || (IsDefined( player.team ) && player.team == team)) ) bots[bots.size] = player; } // First try to drop any bots who are dead (to avoid distrupting a game of S&R for example) for ( i = bots.size - 1; i >= 0; i-- ) { if ( count <= 0 ) break; if ( !isReallyAlive( bots[i] ) ) { bots[i] bot_drop(); bots = array_remove( bots, bots[i] ); count--; } } // Then drop any who are still remaining for ( i = bots.size - 1; i >= 0; i-- ) { if ( count <= 0 ) break; bots[i] bot_drop(); count--; } } bot_lui_convert_team_to_int( team_name ) { if ( team_name == "axis" ) return 0; else if ( team_name == "allies" ) return 1; else if ( team_name == "autoassign" || team_name == "random" ) return 2; else // if ( team_name == "spectator" ) return 3; } spawn_bot_latent( team, botCallback, connecting ) { function_wait_time = GetTime() + 60000; // Wait for spawn to be allowed. This is typically an indication that the models need to be streamed in. // This loop could be enhanced by adding all the test clients first, then waiting on the spawn, or threading off the wait while ( !self CanSpawnTestClient() ) { // This could block forever in cases when some client's can't sync. Ideally the client itself would be // dropped, this is temporary until such a system is implemented. if ( GetTime() >= function_wait_time ) { kick( self.entity_number, "EXE_PLAYERKICKED_BOT_BALANCE" ); connecting.abort = true; return; } wait( 0.05 ); // Entity may have been removed in the meantime if ( !IsDefined( self ) ) { connecting.abort = true; return; } } // Randomize a bit to simulate bot selecting team/loadout maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause ( RandomFloatRange( 0.25, 2.0 ) ); if ( !IsDefined( self ) ) { connecting.abort = true; return; } self SpawnTestClient(); self.pers["isBot"] = true; self.equipment_enabled = true; self.bot_team = team; if ( IsDefined( connecting.difficulty ) ) self bot_set_difficulty( connecting.difficulty ); if( IsDefined( botCallback ) ) { self [[botCallback]](); } self thread [[ level.bot_funcs["think"] ]](); connecting.ready = true; } find_squad_member_index( client, team ) { active_squad_member = client GetRankedPlayerData( "activeSquadMember" ); found = false; count = 0; while ( count < 10 ) //this needs to be kept in sync with max squads in player data { found = false; index = client GetRankedPlayerData( "squadHQ", "aiSquadMembers", count ); if ( index == active_squad_member ) { count++; continue; } if ( !IsDefined( level.human_team_bot_added ) || !IsDefined( level.human_team_bot_added[ index ] ) || level.human_team_bot_added[ index ] == false ) { return index; } count++; } return -1; } //======================================================== // spawn_bots //======================================================== spawn_bots( num_bots, team, botCallback, haltWhenFull, notifyWhenDone, difficulty ) { function_wait_time = GetTime() + 10000; connectingArray = []; squad_index = connectingArray.size; // First get all the bots connected while ( (level.players.size < bot_get_client_limit()) && (connectingArray.size < num_bots) && (GetTime() < function_wait_time) ) // don't want to be stuck in this function forever { maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( 0.05 ); bot = undefined; if( ( GetDvar( "squad_vs_squad" ) == "1" ) ) { leader = level.squad_vs_squad_axis_client; if ( team == "allies" ) { leader = level.squad_vs_squad_allies_client; } if ( IsDefined( leader ) ) { index_needed = squad_index; active_squad_member = leader GetRankedPlayerData( "activeSquadMember" ); slot = 0; index = 0; for ( squad_member = 0; squad_member < bot_get_team_limit(); squad_member++ ) { index = leader GetRankedPlayerData( "squadHQ", "aiSquadMembers", squad_member ); if ( index == active_squad_member ) continue; if ( index_needed == slot ) break; slot++; } name = leader GetRankedPlayerData( "squadMembers", index, "name" ); head = leader GetRankedPlayerData( "squadMembers", index, "head" ); body = leader GetRankedPlayerData( "squadMembers", index, "body" ); helmet = leader GetRankedPlayerData( "squadMembers", index, "helmet" ); bot = AddBot( name, head, body, helmet ); if ( IsDefined( bot ) ) { bot.pers[ "squadSlot" ] = index; } } else { if ( !IsDefined( level.squad_vs_squad_has_forfeited ) ) { if ( IsDefined( level.squad_vs_squad_axis_client ) ) { level.finalKillCam_winner = "axis"; thread maps\mp\gametypes\_gamelogic::endGame( "axis", game[ "end_reason" ][ "allies_forfeited" ] ); } else { level.finalKillCam_winner = "allies"; thread maps\mp\gametypes\_gamelogic::endGame( "allies", game[ "end_reason" ][ "axis_forfeited" ] ); } level.squad_vs_squad_has_forfeited = true; } } } else if( ( GetDvar( "squad_use_hosts_squad" ) == "1" ) ) { if ( level.wargame_client.team == team ) { if ( matchMakingGame() ) { index = find_squad_member_index( level.wargame_client, team ); name = level.wargame_client GetRankedPlayerData( "squadMembers", index, "name" ); head = level.wargame_client GetRankedPlayerData( "squadMembers", index, "head" ); body = level.wargame_client GetRankedPlayerData( "squadMembers", index, "body" ); helmet = level.wargame_client GetRankedPlayerData( "squadMembers", index, "helmet" ); bot = AddBot( name, head, body, helmet ); if ( IsDefined( bot ) ) { level.human_team_bot_added[ index ] = true; bot.pers[ "squadSlot" ] = index; } } else { index_needed = squad_index; active_squad_member = level.wargame_client GetPrivatePlayerData( "privateMatchActiveSquadMember" ); slot = 0; index = 0; for ( squad_member = 0; squad_member < bot_get_team_limit(); squad_member++ ) { index = squad_member; if ( squad_member == active_squad_member ) continue; if ( index_needed == slot ) break; slot++; } name = level.wargame_client GetPrivatePlayerData( "privateMatchSquadMembers", index, "name" ); head = level.wargame_client GetPrivatePlayerData( "privateMatchSquadMembers", index, "head" ); body = level.wargame_client GetPrivatePlayerData( "privateMatchSquadMembers", index, "body" ); helmet = level.wargame_client GetPrivatePlayerData( "privateMatchSquadMembers", index, "helmet" ); bot = AddBot( name, head, body, helmet ); if ( IsDefined( bot ) ) { bot.pers[ "squadSlot" ] = index; } } } else { bot = AddBot( "", 0, 0, 0 ); } } else if( ( GetDvar( "squad_match" ) == "1" ) ) { if ( team == "axis" ) { index = GetEnemySquadData( "squadHQ", "aiSquadMembers", squad_index ); name = GetEnemySquadData( "squadMembers", index, "name" ); head = GetEnemySquadData( "squadMembers", index, "head" ); body = GetEnemySquadData( "squadMembers", index, "body" ); helmet = GetEnemySquadData( "squadMembers", index, "helmet" ); bot = AddBot( name, head, body, helmet ); if ( IsDefined( bot ) ) { bot.pers[ "squadSlot" ] = index; } } else { index = find_squad_member_index( level.squad_match_client, team ); if ( index > -1 ) // no spare slots at the moment { name = level.squad_match_client GetRankedPlayerData( "squadMembers", index, "name" ); head = level.squad_match_client GetRankedPlayerData( "squadMembers", index, "head" ); body = level.squad_match_client GetRankedPlayerData( "squadMembers", index, "body" ); helmet = level.squad_match_client GetRankedPlayerData( "squadMembers", index, "helmet" ); bot = AddBot( name, head, body, helmet ); if ( IsDefined( bot ) ) { level.human_team_bot_added[ index ] = true; bot.pers[ "squadSlot" ] = index; } } } } else { bot = AddBot( "", 0, 0, 0 ); } if ( !IsDefined( bot ) ) { if ( IsDefined( haltWhenFull ) && haltWhenFull ) { if ( IsDefined( notifyWhenDone ) ) self notify( notifyWhenDone ); return; } maps\mp\gametypes\_hostmigration::waitLongDurationWithHostMigrationPause( 1 ); continue; } else { connecting = SpawnStruct(); connecting.bot = bot; connecting.ready = false; connecting.abort = false; connecting.index = squad_index; connecting.difficulty = difficulty; connectingArray[connectingArray.size] = connecting; connecting.bot thread spawn_bot_latent( team, botCallback, connecting ); squad_index++; } } // Wait for all the bots to complete their spawn process before returning connectedComplete = 0; function_wait_time = GetTime() + 60000; while ( (connectedComplete < connectingArray.size) && (GetTime() < function_wait_time) ) { connectedComplete = 0; foreach ( connecting in connectingArray ) { if ( connecting.ready || connecting.abort ) connectedComplete++; } wait 0.05; } if ( IsDefined( notifyWhenDone ) ) self notify( notifyWhenDone ); } bot_gametype_chooses_team() { if ( !level.teamBased ) return true; if ( IsDefined(level.bots_gametype_handles_team_choice) && level.bots_gametype_handles_team_choice ) return true; return false; } bot_gametype_chooses_class() { return ( IsDefined(level.bots_gametype_handles_class_choice) && level.bots_gametype_handles_class_choice ); } //======================================================== // bot_think //======================================================== bot_think( ) { self notify( "bot_think" ); self endon( "bot_think" ); self endon( "disconnect" ); while( !IsDefined( self.pers["team"] ) ) { wait( 0.05 ); } level.hasbots = true; if ( bot_gametype_chooses_team() ) self.bot_team = self.pers["team"]; team = self.bot_team; if ( !IsDefined( team ) ) team = self.pers["team"]; maps\mp\bots\_bots_ks::bot_killstreak_setup(); self.entity_number = self GetEntityNumber(); firstSpawn = false; if ( !isDefined( self.bot_spawned_before ) ) { firstSpawn = true; self.bot_spawned_before = true; if ( !bot_gametype_chooses_team() ) { self notify( "luinotifyserver", "team_select", bot_lui_convert_team_to_int(team) ); wait( 0.5 ); // if we are still on team spectator something is preventing us from joining the team we want to be on. Drop and try to reconnect again later. if ( self.pers["team"] == "spectator" ) { self bot_drop(); return; } } } while( true ) { // Make sure we pick a difficulty if its set to "default" self bot_set_difficulty( self BotGetDifficulty() ); // Balance personalities unless we are restricting them based on difficulty allowAdvPersonality = self BotGetDifficultySetting( "advancedPersonality" ); if ( firstSpawn && IsDefined( allowAdvPersonality ) && allowAdvPersonality != 0 ) self bot_balance_personality(); /# self bot_set_personality_from_dev_dvar(); #/ self bot_assign_personality_functions(); if ( firstSpawn ) { self bot_set_loadout_class(); if ( !bot_gametype_chooses_class() ) self notify( "luinotifyserver", "class_select", self.bot_class ); if ( self.health == 0 ) self waittill( "spawned_player" ); // Don't wait here if we have health (i.e. we've already spawned) if ( IsDefined( level.bot_funcs ) && IsDefined( level.bot_funcs["know_enemies_on_start"] ) ) self thread [[ level.bot_funcs["know_enemies_on_start"] ]](); firstSpawn = false; } self bot_restart_think_threads(); wait( 0.10 ); self waittill( "death" ); self respawn_watcher(); self waittill( "spawned_player" ); } } /# bot_set_personality_from_dev_dvar() { debug_personality = GetDvar( "bot_debugPersonality", "default" ); if( debug_personality != "default" ) self bot_set_personality( debug_personality ); } #/ respawn_watcher() { self endon( "started_spawnPlayer" ); // If for whatever reason spawnPlayer starts, then immediately end this // First wait till the bot is actually waiting to spawn while( !self.waitingToSpawn ) wait(0.05); // Now that he is waiting to spawn, push the Respawn button if necessary if ( self maps\mp\gametypes\_playerlogic::needsButtonToRespawn() ) { while( self.waitingToSpawn ) { if ( self.sessionstate == "spectator" ) { // Can attempt to spawn if the gamemode has unlimited lives or if the bot has lives remaining if ( GetDvarInt("numlives") == 0 || self.pers["lives"] > 0 ) self BotPressButton( "use", 0.5 ); } wait(1.0); } } } bot_get_rank_xp() { if ( self bot_israndom() == false ) { if ( !IsDefined(self.pers[ "rankxp" ]) ) self.pers[ "rankxp" ] = 0; return self.pers[ "rankxp" ]; } difficulty = self BotGetDifficulty(); persName = "bot_rank_" + difficulty; if ( IsDefined(self.pers[persName]) && self.pers[persName] > 0 ) return self.pers[persName]; desiredRanks = bot_random_ranks_for_difficulty( difficulty ); rank = desiredRanks[ "rank" ]; prestige = desiredRanks[ "prestige" ]; minXP = maps\mp\gametypes\_rank::getRankInfoMinXP( rank ); maxXP = minXP + maps\mp\gametypes\_rank::getRankInfoXPAmt( rank ) ; rankXP = RandomIntRange( minXP, maxXP + 1 ); self.pers[persName] = rankXP; return rankXP; } bot_3d_sighting_model( associatedEnt ) { self thread bot_3d_sighting_model_thread( associatedEnt ); } bot_3d_sighting_model_thread( associatedEnt ) { associatedEnt endon("disconnect"); self endon( "disconnect" ); level endon( "game_ended" ); while( 1 ) { if ( IsAlive( self ) && !(self BotCanSeeEntity( associatedEnt )) && within_fov( self.origin, self.angles, associatedEnt.origin, self BotGetFovDot() ) ) self BotGetImperfectEnemyInfo( associatedEnt, associatedEnt.origin ); wait 0.1; } } bot_random_ranks_for_difficulty( difficulty ) { result = []; result["rank"] = 0; result["prestige"] = 0; if ( difficulty == "default" ) return result; // Rank: 1 - 8, 10 - 38, 40 - 58, 60 (never set to N9 so there is no chance of jumping bracket by gained XP during the match) if ( !isDefined( level.bot_rnd_rank ) ) { level.bot_rnd_rank = []; level.bot_rnd_rank["recruit"][0] = 0; level.bot_rnd_rank["recruit"][1] = 7; level.bot_rnd_rank["regular"][0] = 9; level.bot_rnd_rank["regular"][1] = 37; level.bot_rnd_rank["hardened"][0] = 39; level.bot_rnd_rank["hardened"][1] = 57; level.bot_rnd_rank["veteran"][0] = 59; level.bot_rnd_rank["veteran"][1] = 59; } // Prestige: 2 - 9 only at veteran if ( !isDefined( level.bot_rnd_prestige ) ) { level.bot_rnd_prestige = []; level.bot_rnd_prestige["recruit"][0] = 0; level.bot_rnd_prestige["recruit"][1] = 0; level.bot_rnd_prestige["regular"][0] = 0; level.bot_rnd_prestige["regular"][1] = 0; level.bot_rnd_prestige["hardened"][0] = 0; level.bot_rnd_prestige["hardened"][1] = 0; level.bot_rnd_prestige["veteran"][0] = 0; level.bot_rnd_prestige["veteran"][1] = 9; } if ( IsDefined( level.bot_rnd_rank[difficulty][0] ) && IsDefined( level.bot_rnd_rank[difficulty][1] ) ) result["rank"] = RandomIntRange( level.bot_rnd_rank[difficulty][0], level.bot_rnd_rank[difficulty][1] + 1 ); if ( IsDefined( level.bot_rnd_prestige[difficulty][0] ) && IsDefined( level.bot_rnd_prestige[difficulty][1] ) ) result["prestige"] = RandomIntRange( level.bot_rnd_prestige[difficulty][0], level.bot_rnd_prestige[difficulty][1] + 1 ); return result; } crate_can_use_always( crate ) { // Agents can only pickup boxes normally if ( IsAgent(self) && !IsDefined( crate.boxType ) ) return false; return true; } //======================================================== // get_human_player //======================================================== get_human_player() { result = undefined; players = getEntArray( "player", "classname" ); if ( IsDefined( players ) ) { for( index = 0; index < players.size; index++ ) { if( IsDefined( players[index] ) && IsDefined( players[index].connected ) && players[index].connected && !IsAI( players[index] ) && (!IsDefined( result ) || result.team == "spectator") ) { result = players[index]; } } } return result; } /# get_all_humans() { humans = []; foreach ( player in level.players ) { if ( player.connected && !IsAI( player ) ) humans = array_add( humans, player ); } return humans; } spectators_exist() { humans = get_all_humans(); foreach( player in humans ) { if ( player.team == "spectator" ) return true; } return false; } #/ //======================================================== // bot_damage_callback //======================================================== bot_damage_callback( eAttacker, iDamage, sMeansOfDeath, sWeapon, eInflictor, sHitLoc ) { if( !IsDefined( self ) || !IsAlive( self ) ) { return; } if( sMeansOfDeath == "MOD_FALLING" || sMeansOfDeath == "MOD_SUICIDE" ) { return; } if( iDamage <= 0 ) { return; } if ( !IsDefined( eInflictor ) ) { if ( !IsDefined( eAttacker ) ) return; eInflictor = eAttacker; } if ( IsDefined( eInflictor ) ) { if ( level.teamBased ) { if ( IsDefined( eInflictor.team ) && eInflictor.team == self.team ) return; else if ( IsDefined( eAttacker ) && IsDefined( eAttacker.team ) && eAttacker.team == self.team ) return; } attacker_ent = bot_get_known_attacker( eAttacker, eInflictor ); if ( IsDefined(attacker_ent) ) self BotSetAttacker( attacker_ent ); } } //======================================================== // on_bot_killed //======================================================== on_bot_killed( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration, killId ) { self BotClearScriptEnemy(); self BotClearScriptGoal(); attacker_ent = bot_get_known_attacker( attacker, eInflictor ); if ( !bot_is_fireteam_mode() && GetDvar( "squad_match" ) != "1" && GetDvar( "squad_vs_squad" ) != "1" && IsDefined(attacker_ent) && attacker_ent.classname == "script_vehicle" && IsDefined(attacker_ent.helitype) ) { respawn_chance = self BotGetDifficultySetting("launcherRespawnChance"); if ( RandomFloat(1.0) < respawn_chance ) self.respawn_with_launcher = true; } } //======================================================== // bot_should_do_killcam //======================================================== bot_should_do_killcam() { /# if ( GetDvar("scr_game_spectatetype") == "2" ) { if ( spectators_exist() ) { return false; } } #/ if ( bot_is_fireteam_mode() ) return false; skip_killcam_chance = 0.0; bot_difficulty = self BotGetDifficulty(); if ( bot_difficulty == "recruit" ) { skip_killcam_chance = 0.1; } else if ( bot_difficulty == "regular" ) { skip_killcam_chance = 0.4; } else if ( bot_difficulty == "hardened" ) { skip_killcam_chance = 0.7; } else if ( bot_difficulty == "veteran" ) { skip_killcam_chance = 1.0; } return (RandomFloat(1.0) < (1.0-skip_killcam_chance)); } //======================================================== // bot_should_pickup_weapons //======================================================== bot_should_pickup_weapons() { if ( self isJuggernaut() ) return false; return true; } //======================================================== // bot_restart_think_threads //======================================================== bot_restart_think_threads() { self thread bot_think_watch_enemy(); self thread bot_think_tactical_goals(); self thread [[ level.bot_funcs["dropped_weapon_think"] ]](); self thread bot_think_level_actions(); self thread bot_think_crate(); self thread bot_think_crate_blocking_path(); //self thread bot_think_revive(); self thread bot_think_killstreak(); self thread bot_think_watch_aerial_killstreak(); self thread bot_think_gametype(); /# self thread bot_think_debug(); #/ } /# bot_think_debug() { self notify( "bot_think_debug" ); self endon( "bot_think_debug" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); while(1) { if ( IsDefined(level.bot_debug_force_path_location) ) { self bot_disable_tactical_goals(); self.ignoreall = true; self BotSetStance("stand"); if ( !self BotHasScriptGoal() ) { self BotSetScriptGoal(level.bot_debug_force_path_location,0,"tactical"); } else { goal = self BotGetScriptGoal(); if ( !bot_vectors_are_equal(goal, level.bot_debug_force_path_location) ) self BotSetScriptGoal(level.bot_debug_force_path_location,0,"tactical"); } } wait(0.05); } } #/ //======================================================== // bot_think_watch_enemy //======================================================== bot_think_watch_enemy( bEndOnDeath ) { endMessage = "spawned_player"; if( IsDefined(bEndOnDeath) && bEndOnDeath ) endMessage = "death"; self notify( "bot_think_watch_enemy" ); self endon( "bot_think_watch_enemy" ); self endon( endMessage ); self endon( "disconnect" ); level endon( "game_ended" ); // This function is for any logic that needs to be updated each frame regarding the enemy self.last_enemy_sight_time = GetTime(); while( true ) { if ( IsDefined( self.enemy ) ) { if ( self BotCanSeeEntity( self.enemy ) ) { self.last_enemy_sight_time = GetTime(); } } wait(0.05); } } //======================================================== // bot_think_dropped_weapons //======================================================== bot_think_seek_dropped_weapons() { self notify( "bot_think_seek_dropped_weapons" ); self endon( "bot_think_seek_dropped_weapons" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); throwing_knife_name = "throwingknife_mp"; throwing_knife_jugg_name = "throwingknifejugg_mp"; while( true ) { still_seeking_weapon = false; if ( self bot_out_of_ammo() && self [[level.bot_funcs["should_pickup_weapons"]]]() && !self bot_is_remote_or_linked() ) { dropped_weapons = GetEntArray("dropped_weapon","targetname"); dropped_weapons_sorted = get_array_of_closest(self.origin,dropped_weapons); if ( dropped_weapons_sorted.size > 0 ) { dropped_weapon = dropped_weapons_sorted[0]; self bot_seek_dropped_weapon( dropped_weapon ); } } if ( !self bot_in_combat() && !self bot_is_remote_or_linked() && self BotGetDifficultySetting("strategyLevel") > 0 ) { has_knife_normal = self HasWeapon( throwing_knife_name ); has_knife_jugg = self HasWeapon( throwing_knife_jugg_name ); knife_thrown = (has_knife_normal && self GetAmmoCount( throwing_knife_name ) == 0) || (has_knife_jugg && self GetAmmoCount( throwing_knife_jugg_name ) == 0); if ( knife_thrown ) { if ( IsDefined(self.going_for_knife) ) { wait(5.0); // Already set a knife destination continue; } dropped_knives = GetEntArray("dropped_knife","targetname"); dropped_knives_sorted = get_array_of_closest(self.origin,dropped_knives); foreach( knife in dropped_knives_sorted ) { if ( !IsDefined(knife) ) { // May have been deleted while we were waiting for the bot_queued_process below on the previous knife in the array continue; } if ( !IsDefined(knife.calculated_closest_point) ) { result = bot_queued_process( "BotGetClosestNavigablePoint", ::func_bot_get_closest_navigable_point, knife.origin, 32, self ); // since bot_queued_process waits, it's possible that the knife gets removed if ( IsDefined( knife ) ) { knife.closest_point_on_grid = result; knife.calculated_closest_point = true; } else { continue; } } if ( IsDefined(knife.closest_point_on_grid) ) { self.going_for_knife = true; self bot_seek_dropped_weapon( knife ); } } } else if ( has_knife_normal || has_knife_jugg ) { // Has a knife and has not thrown it yet self.going_for_knife = undefined; } } wait( RandomFloatRange(0.25, 0.75) ); } } bot_seek_dropped_weapon( dropped_weapon ) { if ( self bot_has_tactical_goal( "seek_dropped_weapon", dropped_weapon ) == false ) { action_thread = undefined; if ( dropped_weapon.targetname == "dropped_weapon" ) { needs_to_pickup_weapon = true; heldweapons = self GetWeaponsListPrimaries(); foreach ( held_weapon in heldweapons ) { if ( dropped_weapon.model == GetWeaponModel(held_weapon) ) needs_to_pickup_weapon = false; } if ( needs_to_pickup_weapon ) action_thread = ::bot_pickup_weapon; } extra_params = SpawnStruct(); extra_params.object = dropped_weapon; extra_params.script_goal_radius = 12; extra_params.should_abort = level.bot_funcs["dropped_weapon_cancel"]; extra_params.action_thread = action_thread; self bot_new_tactical_goal( "seek_dropped_weapon", dropped_weapon.origin, 100, extra_params ); } } bot_pickup_weapon( goal ) { self BotPressButton( "use", 2 ); wait(2); } should_stop_seeking_weapon( goal ) { // goal.object is the dropped weapon if ( !IsDefined( goal.object ) ) return true; if ( goal.object.targetname == "dropped_weapon" ) { if ( self bot_get_total_gun_ammo() > 0 ) return true; } else if ( goal.object.targetname == "dropped_knife" ) { if ( self bot_in_combat() ) { self.going_for_knife = undefined; return true; } } return false; } //======================================================== // bot_think_level_actions //======================================================== bot_think_level_actions( override_radius ) { self notify( "bot_think_level_actions" ); self endon( "bot_think_level_actions" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); while( true ) { waittill_notify_or_timeout( "calculate_new_level_targets", randomfloatrange( 2, 10 ) ); if ( !IsDefined(level.level_specific_bot_targets) || level.level_specific_bot_targets.size == 0 ) continue; if ( self bot_has_tactical_goal( "map_interactive_object" ) ) continue; if ( self bot_in_combat() || self bot_is_remote_or_linked() ) continue; target_picked = undefined; foreach( level_target in level.level_specific_bot_targets ) { if ( array_contains( level_target.high_priority_for, self ) ) { target_picked = level_target; break; } } if ( !IsDefined(target_picked) ) { if ( RandomInt(100) > 25 ) continue; level_triggers_sorted = get_array_of_closest( self.origin, level.level_specific_bot_targets ); max_dist = 256; if ( IsDefined(override_radius) ) max_dist = override_radius; else if ( self BotGetScriptGoalType() == "hunt" && self BotPursuingScriptGoal() ) max_dist = 512; // If bot is not hunting, or bot is hunting but has an enemy targeted, // then only use one of these if the bot is relatively close to it if ( DistanceSquared( self.origin, level_triggers_sorted[0].origin ) > max_dist*max_dist ) continue; target_picked = level_triggers_sorted[0]; } assert( IsDefined(target_picked) ); should_melee_target = false; if ( target_picked.bot_interaction_type == "damage" ) { should_melee_target = self bot_should_melee_level_damage_target( target_picked ); if ( should_melee_target ) { height_diff_to_node_0 = target_picked.origin[2] - (target_picked.bot_targets[0].origin[2] + 55); height_diff_to_node_1 = target_picked.origin[2] - (target_picked.bot_targets[1].origin[2] + 55); if ( (height_diff_to_node_0 > 55 && height_diff_to_node_1 > 55) ) { if ( array_contains( target_picked.high_priority_for, self ) ) target_picked.high_priority_for = array_remove(target_picked.high_priority_for, self); continue; } } weapon_class = WeaponClass( self GetCurrentWeapon() ); if ( weapon_class == "spread" ) { vec_to_node_0 = target_picked.bot_targets[0].origin - target_picked.origin; vec_to_node_1 = target_picked.bot_targets[1].origin - target_picked.origin; dist_to_node_0_sq = LengthSquared(vec_to_node_0); dist_to_node_1_sq = LengthSquared(vec_to_node_1); if ( (dist_to_node_0_sq > 150*150 && dist_to_node_1_sq > 150*150) ) { if ( array_contains( target_picked.high_priority_for, self ) ) target_picked.high_priority_for = array_remove(target_picked.high_priority_for, self); continue; } } } extra_params = SpawnStruct(); extra_params.object = target_picked; if ( target_picked.bot_interaction_type == "damage" ) { if ( should_melee_target ) extra_params.should_abort = ::level_trigger_should_abort_melee; else extra_params.should_abort = ::level_trigger_should_abort_ranged; } if ( target_picked.bot_interaction_type == "use" ) { extra_params.action_thread = ::use_use_trigger; extra_params.should_abort = ::level_trigger_should_abort; extra_params.script_goal_yaw = VectorToAngles(target_picked.origin - target_picked.bot_target.origin)[1]; self bot_new_tactical_goal( "map_interactive_object", target_picked.bot_target.origin, 10, extra_params ); } else if ( target_picked.bot_interaction_type == "damage" ) { Assert(target_picked.bot_targets.size == 2); if ( should_melee_target ) { extra_params.action_thread = ::melee_damage_trigger; extra_params.script_goal_radius = 20; } else { extra_params.action_thread = ::attack_damage_trigger; extra_params.script_goal_radius = 50; } node_target = undefined; path_dist_0 = bot_queued_process( "GetPathDistLevelAction", ::func_get_path_dist, self.origin, target_picked.bot_targets[0].origin ); path_dist_1 = bot_queued_process( "GetPathDistLevelAction", ::func_get_path_dist, self.origin, target_picked.bot_targets[1].origin ); if ( !IsDefined(target_picked) ) continue; // Could have gone undefined during the queued process wait if ( path_dist_0 <= 0 && path_dist_1 <= 0 ) continue; if ( path_dist_0 > 0 ) { Assert(IsDefined(target_picked.bot_targets[0])); if ( path_dist_1 < 0 || path_dist_0 <= path_dist_1 ) node_target = target_picked.bot_targets[0]; } if ( path_dist_1 > 0 ) { Assert(IsDefined(target_picked.bot_targets[1])); if ( path_dist_0 < 0 || path_dist_1 <= path_dist_0 ) node_target = target_picked.bot_targets[1]; } Assert(IsDefined(node_target)); /# if ( !IsDefined(node_target) ) continue; // In non-ship, bail to avoid SRE spam #/ if ( !should_melee_target ) self childthread monitor_node_visible( node_target ); self bot_new_tactical_goal( "map_interactive_object", node_target.origin, 10, extra_params ); } } } bot_should_melee_level_damage_target( level_target ) { Assert(level_target.bot_interaction_type == "damage" ); current_weapon = self GetCurrentWeapon(); should_melee_target = self bot_out_of_ammo() || self.hasRiotShieldEquipped || ( IsDefined( self.isJuggernautManiac ) && self.isJuggernautManiac == true ) || WeaponClass(current_weapon) == "grenade" || current_weapon == "iw6_knifeonly_mp" || current_weapon == "iw6_knifeonlyfast_mp"; /# should_melee_target = should_melee_target || (GetDvarInt("bot_SimulateNoAmmo") == 1); #/ return should_melee_target; } monitor_node_visible( node_target ) { self endon("goal"); wait(0.1); // give two frames for the tactical goal to start before we are allowed to notify "goal" while(1) { if ( WeaponClass( self GetCurrentWeapon() ) == "spread" ) { if ( DistanceSquared(self.origin,node_target.origin) > 300 * 300 ) { wait(0.05); continue; } } nearest_node_self = self GetNearestNode(); if ( IsDefined(nearest_node_self) ) { if ( NodesVisible( nearest_node_self, node_target ) ) { if ( SightTracePassed( self.origin + (0,0,55), node_target.origin + (0,0,55), false, self ) ) self notify("goal"); } } wait(0.05); } } SCR_CONST_DAMAGE_TRIGGER_TIMEOUT_TIME = 5000; attack_damage_trigger( goal ) { // goal.object is the trigger Assert(goal.object.bot_interaction_type == "damage" ); if ( goal.object.origin[2] - self GetEye()[2] > 55 ) { // Object is a lot higher than me, don't target it if I end up directly under it if ( Distance2DSquared(goal.object.origin, self.origin) < 15*15 ) return; } self BotSetFlag("disable_movement", true); self look_at_damage_trigger( goal.object, 0.30 ); self BotPressButton( "ads", 0.30 ); wait(0.25); time_started = GetTime(); while( IsDefined( goal.object ) && !IsDefined(goal.object.already_used) && GetTime() - time_started < SCR_CONST_DAMAGE_TRIGGER_TIMEOUT_TIME ) { self look_at_damage_trigger( goal.object, 0.15 ); self BotPressButton( "ads", 0.15 ); self BotPressButton( "attack" ); wait(0.1); } self BotSetFlag("disable_movement", false); } melee_damage_trigger( goal ) { // goal.object is the trigger Assert(goal.object.bot_interaction_type == "damage" ); self BotSetFlag("disable_movement", true); self look_at_damage_trigger( goal.object, 0.30 ); wait(0.25); time_started = GetTime(); while( IsDefined( goal.object ) && !IsDefined(goal.object.already_used) && GetTime() - time_started < SCR_CONST_DAMAGE_TRIGGER_TIMEOUT_TIME ) { self look_at_damage_trigger( goal.object, 0.15 ); self BotPressButton( "melee" ); wait(0.1); } self BotSetFlag("disable_movement", false); } look_at_damage_trigger( damage_trigger, time ) { look_origin = damage_trigger.origin; if ( Distance2DSquared( self.origin, look_origin ) < 10*10 ) look_origin = ( look_origin[0], look_origin[1], self GetEye()[2] ); // Ensure we're still looking forward when really close to the object, so we don't try to look directly up or down self BotLookAtPoint( look_origin, time, "script_forced" ); } use_use_trigger( goal ) { // goal.object is the trigger Assert(goal.object.bot_interaction_type == "use" ); if ( IsAgent(self) ) { self _enableUsability(); goal.object EnablePlayerUse( self ); wait(0.05); } time = goal.object.use_time; self BotPressButton( "use", time ); wait( time ); if ( IsAgent(self) ) { self _disableUsability(); if ( IsDefined(goal.object) ) goal.object DisablePlayerUse( self ); } } level_trigger_should_abort_melee( goal ) { // goal.object is the damage_trigger Assert( !IsDefined( goal.object ) || goal.object.bot_interaction_type == "damage" ); if ( level_trigger_should_abort(goal) ) return true; if ( !self bot_should_melee_level_damage_target(goal.object) ) return true; return false; } level_trigger_should_abort_ranged( goal ) { // goal.object is the damage_trigger Assert( !IsDefined( goal.object ) || goal.object.bot_interaction_type == "damage" ); if ( level_trigger_should_abort(goal) ) return true; if ( self bot_should_melee_level_damage_target(goal.object) ) return true; return false; } level_trigger_should_abort( goal ) { // goal.object is the trigger if ( !IsDefined( goal.object ) ) return true; if ( IsDefined( goal.object.already_used ) ) return true; if ( self bot_in_combat() ) return true; return false; } crate_in_range( crate ) { if ( !IsDefined( crate.owner ) || (crate.owner != self) ) { // I didn't call in this crate... // Ignore it if it is greater than 2048 distance away if ( DistanceSquared( self.origin, crate.origin ) > 2048 * 2048 ) return false; } return true; } bot_crate_valid( crate ) { if ( !IsDefined( crate ) ) return false; if ( !(self [[ level.bot_funcs["crate_can_use"] ]]( crate )) ) return false; if ( !crate_landed_and_on_path_grid( crate ) ) return false; // Ignore any crate that is a trap for the other team if ( level.teamBased && IsDefined( crate.bomb ) && IsDefined( crate.team ) && (crate.team == self.team) ) return false; if ( !( self [[ level.bot_funcs["crate_in_range"] ]]( crate ) ) ) { return false; } if ( IsDefined(crate.boxType) ) { if ( IsDefined( level.boxSettings[crate.boxType] ) && ![[level.boxSettings[crate.boxType].canUseCallback]]() ) return false; if ( IsDefined( crate.disabled_use_for ) && IsDefined(crate.disabled_use_for[self GetEntityNumber()]) && crate.disabled_use_for[self GetEntityNumber()] ) return false; /# if ( !IsDefined(level.bot_can_use_box_by_type[crate.boxType]) ) { AssertMsg( "Crate type <" + crate.boxType + "> is not supported for bots" ); return false; } #/ if ( !self [[level.bot_can_use_box_by_type[crate.boxType]]]( crate ) ) return false; } return isDefined( crate ); } crate_landed_and_on_path_grid( crate ) { Assert(IsDefined(crate)); if ( !crate_has_landed(crate) ) return false; Assert(IsDefined(crate)); if ( !crate_is_on_path_grid(crate) ) return false; return isDefined( crate ); } crate_has_landed( crate ) { if ( IsDefined(crate.boxType) ) { return ( GetTime() > (crate.birthtime + 1000) ); } else { return (IsDefined(crate.droppingToGround) && !crate.droppingToGround); } } crate_is_on_path_grid( crate ) { Assert( crate_has_landed(crate) ); // This can't be called on a crate still in the air if ( !IsDefined(crate.on_path_grid) ) crate_calculate_on_path_grid( crate ); return (IsDefined(crate) && crate.on_path_grid); } node_within_use_radius_of_crate( node, crate ) { if ( IsDefined(crate.boxtype) && crate.boxtype == "scavenger_bag" ) { return ( abs(node.origin[0] - crate.origin[0]) < 36 && abs(node.origin[0] - crate.origin[0]) < 36 && abs(node.origin[0] - crate.origin[0]) < 18 ); } else { player_use_radius = GetDvarFloat( "player_useRadius" ); dist_to_nearest_node_sq = DistanceSquared(crate.origin, node.origin + (0,0,40)); // Assume crouched at node return ( dist_to_nearest_node_sq <= (player_use_radius*player_use_radius) ); } } crate_calculate_on_path_grid( crate ) { Assert(!IsDefined(crate.nearest_nodes)); Assert(!IsDefined(crate.on_path_grid)); crate thread crate_monitor_position(); crate.on_path_grid = false; prev_forceDisconnectUntil = undefined; time_to_disconnect_until = undefined; if ( IsDefined(crate.forceDisconnectUntil) ) { prev_forceDisconnectUntil = crate.forceDisconnectUntil; time_to_disconnect_until = GetTime() + 30000; crate.forceDisconnectUntil = time_to_disconnect_until; crate notify( "path_disconnect" ); } wait(0.05); // Wait for the disconnect to happen if ( !IsDefined( crate ) ) return; nearest_nodes = crate_get_nearest_valid_nodes( crate ); if ( !IsDefined( crate ) ) return; if ( IsDefined(nearest_nodes) && nearest_nodes.size > 0 ) { crate.nearest_nodes = nearest_nodes; crate.on_path_grid = true; } else { player_use_radius = GetDvarFloat( "player_useRadius" ); closest_node = GetNodesInRadiusSorted( crate.origin, player_use_radius * 2, 0 )[0]; crate_loc_on_ground = crate GetPointInBounds(0,0,-1); // get the point on the ground (where the pathnode origin would be) nearest_point = undefined; if ( IsDefined(crate.boxtype) && crate.boxtype == "scavenger_bag" ) { if ( bot_point_is_on_pathgrid(crate.origin) ) nearest_point = crate.origin; } else { nearest_point = BotGetClosestNavigablePoint(crate.origin, player_use_radius); } if ( IsDefined(closest_node) && !closest_node NodeIsDisconnected() && IsDefined(nearest_point) && abs(closest_node.origin[2] - crate_loc_on_ground[2]) < 30 ) { crate.nearest_points = [nearest_point]; crate.nearest_nodes = [closest_node]; // Needed for vis checks even though its not our destination crate.on_path_grid = true; } } if ( IsDefined(crate.forceDisconnectUntil) ) { if ( crate.forceDisconnectUntil == time_to_disconnect_until ) crate.forceDisconnectUntil = prev_forceDisconnectUntil; } } crate_get_nearest_valid_nodes( crate ) { nodes = GetNodesInRadiusSorted( crate.origin, 256, 0 ); for ( i = nodes.size; i > 0; i-- ) nodes[i] = nodes[i-1]; nodes[0] = GetClosestNodeInSight( crate.origin ); all_nodes = undefined; if ( IsDefined(crate.forceDisconnectUntil) ) all_nodes = GetAllNodes(); nodes_to_return = []; nodes_wanted = 1; if ( !IsDefined(crate.boxType) ) nodes_wanted = 2; for ( i = 0; i < nodes.size; i++ ) { node = nodes[i]; if ( !IsDefined(node) || !IsDefined( crate ) ) continue; if ( node NodeIsDisconnected() ) continue; if ( !node_within_use_radius_of_crate( node, crate ) ) { // If i > 0, then we know the nodes are sorted by distance, so if this node is not within the radius than none of the rest will be either if ( i == 0 ) continue; else break; } wait(0.05); // Wait a frame in between sight traces if ( !IsDefined( crate ) ) break; if ( SightTracePassed( crate.origin, node.origin + (0,0,55), false, crate ) ) { wait(0.05); // Wait a frame in between path generations if ( !IsDefined( crate ) ) break; if ( !IsDefined(crate.forceDisconnectUntil) ) { // If this is not defined, then this crate doesn't disconnect paths and we don't need to check for that nodes_to_return[nodes_to_return.size] = node; if ( nodes_to_return.size == nodes_wanted ) return nodes_to_return; else continue; } // Find another node relatively far away and test path to that node other_node_to_test = undefined; num_tested = 0; while( !IsDefined(other_node_to_test) && num_tested < 100 ) { num_tested++; node_trying = Random(all_nodes); if ( DistanceSquared( node.origin, node_trying.origin ) > 500 * 500 ) other_node_to_test = node_trying; } if ( IsDefined(other_node_to_test) ) { path = bot_queued_process( "GetNodesOnPathCrate", ::func_get_nodes_on_path, node.origin, other_node_to_test.origin ); if ( IsDefined(path) ) { nodes_to_return[nodes_to_return.size] = node; if ( nodes_to_return.size == nodes_wanted ) return nodes_to_return; else continue; } } } } return undefined; } crate_get_bot_target( crate ) { if ( IsDefined(crate.nearest_points) ) return crate.nearest_points[0]; // ESU - 5/9/14 - If only one node exists it will cause an assert, since BotNodeScoreMultiple will return undefined. So we just return that single node origin directly. if ( IsDefined(crate.nearest_nodes) ) { if ( crate.nearest_nodes.size > 1 ) { nodes_sorted = array_reverse(self BotNodeScoreMultiple( crate.nearest_nodes, "node_exposed" )); return random_weight_sorted(nodes_sorted).origin; } else return crate.nearest_nodes[0].origin; } AssertMsg("unreachable"); } crate_get_bot_target_check_distance( crate, use_radius ) { crateStandPos = crate_get_bot_target( crate ); testRadiusSq = use_radius * 0.9; testRadiusSq *= testRadiusSq; if ( DistanceSquared( crate.origin, crateStandPos ) <= testRadiusSq ) return crateStandPos; else return undefined; } //======================================================== // bot_think_crate //======================================================== bot_think_crate() { self notify( "bot_think_crate" ); self endon( "bot_think_crate" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); player_use_radius = GetDvarFloat( "player_useRadius" ); while( true ) { wait_time = RandomFloatRange(2,4); self waittill_notify_or_timeout("new_crate_to_take", wait_time); if ( IsDefined( self.boxes ) && self.boxes.size == 0 ) self.boxes = undefined; all_crates = level.carePackages; if ( !(self bot_in_combat()) && IsDefined(self.boxes) ) all_crates = array_combine(all_crates, self.boxes); if ( IsDefined( level.bot_scavenger_bags ) && self _hasPerk( "specialty_scavenger" ) ) all_crates = array_combine(all_crates, level.bot_scavenger_bags); // Early out if we didn't find any crates all_crates = array_removeUndefined( all_crates ); if ( all_crates.size == 0 ) continue; if ( bot_has_tactical_goal( "airdrop_crate" ) || self BotGetScriptGoalType() == "tactical" || self bot_is_remote_or_linked() ) { continue; } all_valid_crates = []; foreach ( crate in all_crates ) { if ( self bot_crate_valid( crate ) ) all_valid_crates[all_valid_crates.size] = crate; } // bot_crate_valid() has a wait in it and crates could have been removed after being added to all_valid_crates all_valid_crates = array_remove_duplicates( all_valid_crates ); // We didn't find any valid crates to take if ( all_valid_crates.size == 0 ) continue; // Sort the array all_valid_crates = get_array_of_closest( self.origin, all_valid_crates ); // First check for the closest crate the bot can see (ignoring current FOV) nearest_node_bot = self GetNearestNode(); if ( !IsDefined(nearest_node_bot) ) continue; ammoLow = self [[ level.bot_funcs["crate_low_ammo_check"] ]](); can_take_any_crate_on_radar = (ammoLow || (RandomInt(100) < 50)) && !(self isEMPed()); crate_to_take = undefined; foreach ( crate in all_valid_crates ) { player_has_claimed_crate = false; if ( ( !IsDefined(crate.owner) || crate.owner != self ) && !IsDefined(crate.boxType) ) { human_allies_near_crate = []; foreach( player in level.players ) { if ( !IsDefined( player.team ) ) continue; if ( !IsAI(player) && level.teamBased && player.team == self.team ) { if ( DistanceSquared(player.origin,crate.origin) < 700*700 ) human_allies_near_crate[human_allies_near_crate.size] = player; } } if ( human_allies_near_crate.size > 0 ) { // Check if the human ally has a line of sight to the crate nearest_node_human = human_allies_near_crate[0] GetNearestNode(); if ( IsDefined(nearest_node_human) ) { player_has_claimed_crate = false; foreach ( node in crate.nearest_nodes ) { player_has_claimed_crate = player_has_claimed_crate | NodesVisible( nearest_node_human, node, true ); } } } } if ( !player_has_claimed_crate ) { bot_has_claimed_crate = IsDefined( crate.bots ) && IsDefined( crate.bots[self.team] ) && crate.bots[self.team] > 0; i_can_see_crate = false; foreach ( node in crate.nearest_nodes ) { i_can_see_crate = i_can_see_crate | NodesVisible( nearest_node_bot, node, true ); } // Either take a crate that I can see, or 50% chance to take the closest one pointed out on HUD to me that isnt claimed if ( i_can_see_crate || (can_take_any_crate_on_radar && !bot_has_claimed_crate) ) { crate_to_take = crate; break; } } } if ( IsDefined( crate_to_take ) ) { // Claim this crate if ( self [[ level.bot_funcs["crate_should_claim"] ]] () ) { if ( !IsDefined(crate_to_take.boxType) ) { if ( !IsDefined( crate_to_take.bots ) ) { crate_to_take.bots = []; } crate_to_take.bots[self.team] = 1; } } extra_params = SpawnStruct(); extra_params.object = crate_to_take; extra_params.start_thread = ::watch_bot_died_during_crate; extra_params.should_abort = ::crate_picked_up; crate_dest = undefined; if ( IsDefined(crate_to_take.boxType) ) { if ( IsDefined( crate_to_take.boxTouchOnly ) && crate_to_take.boxTouchOnly ) { extra_params.script_goal_radius = 16; extra_params.action_thread = undefined; crate_dest = crate_to_take.origin; } else { extra_params.script_goal_radius = 50; extra_params.action_thread = ::use_box; vec_crate_to_nearest_node = self crate_get_bot_target_check_distance( crate_to_take, player_use_radius ); if ( !IsDefined( vec_crate_to_nearest_node ) ) { continue; } vec_crate_to_nearest_node -= crate_to_take.origin; scale = Length(vec_crate_to_nearest_node) * RandomFloat(1.0); crate_dest = (crate_to_take.origin + VectorNormalize(vec_crate_to_nearest_node) * scale) + (0,0,12); } } else { extra_params.action_thread = ::use_crate; extra_params.end_thread = ::stop_using_crate; crate_dest = self crate_get_bot_target_check_distance(crate_to_take, player_use_radius ); if ( !IsDefined( crate_dest ) ) { continue; } extra_params.script_goal_radius = (player_use_radius - Distance(crate_to_take.origin, crate_dest + (0,0,40))); crate_dest = crate_dest + (0,0,24); } if ( IsDefined(extra_params.script_goal_radius) ) Assert(extra_params.script_goal_radius >= 0); crate_to_take notify( "path_disconnect" ); wait 0.05; if ( !IsDefined( crate_to_take ) ) continue; self bot_new_tactical_goal( "airdrop_crate", crate_dest, 30, extra_params ); } } } bot_should_use_ballistic_vest_crate( crate ) { return true; } crate_should_claim() { return true; } crate_low_ammo_check() { return false; } bot_should_use_ammo_crate( crate ) { if ( self GetCurrentWeapon() == level.boxSettings[crate.boxType].minigunWeapon ) return false; return true; } bot_pre_use_ammo_crate( crate ) { self SwitchToWeapon(self.secondaryWeapon); wait(1.0); } bot_post_use_ammo_crate( crate ) { self SwitchToWeapon("none"); self.secondaryWeapon = self GetCurrentWeapon(); // Make sure we're only ever switching out our secondary from gun boxes, not our primary } bot_should_use_scavenger_bag( crate ) { if ( self bot_get_low_on_ammo( 0.66 ) ) { // Scavenger bag must be in sight nearest_node_bot = self GetNearestNode(); if ( IsDefined( crate.nearest_nodes ) && IsDefined( crate.nearest_nodes[0] ) && IsDefined( nearest_node_bot ) ) { if ( NodesVisible(nearest_node_bot, crate.nearest_nodes[0], true) ) { if ( within_fov( self.origin, self.angles, crate.origin, self BotGetFovDot() ) ) return true; } } } return false; } bot_should_use_grenade_crate( crate ) { offhand_list = self GetWeaponsListOffhands(); foreach( weapon in offhand_list ) { if ( self GetWeaponAmmoStock(weapon) == 0 ) return true; } return false; } bot_should_use_juicebox_crate( crate ) { return true; } crate_monitor_position() { self notify("crate_monitor_position"); self endon("crate_monitor_position"); self endon("death"); level endon("game_ended"); while(1) { lastPos = self.origin; wait(0.5); if ( !IsAlive( self ) ) return; if ( !bot_vectors_are_equal( self.origin, lastPos ) ) { self.on_path_grid = undefined; self.nearest_nodes = undefined; self.nearest_points = undefined; } } } crate_wait_use() { } crate_picked_up( goal ) { // goal.object is the crate if ( !IsDefined( goal.object ) ) return true; return false; } use_crate( goal ) { // goal.object is the crate if ( IsAgent(self) ) { self _enableUsability(); goal.object EnablePlayerUse( self ); wait(0.05); } self [[ level.bot_funcs["crate_wait_use"] ]](); // crate.owner doesn't have to exist. But if it does, and this bot is the owner, use the shorter amount of time if ( IsDefined(goal.object.owner) && goal.object.owner == self ) { time = level.crateOwnerUseTime / 1000 + 0.5; } else { time = level.crateNonOwnerUseTime / 1000 + 1.0; } self BotPressButton( "use", time ); wait( time ); if ( IsAgent(self) ) { self _disableUsability(); if ( IsDefined(goal.object) ) goal.object DisablePlayerUse( self ); } if( isDefined( goal.object ) ) { if ( !IsDefined( goal.object.bots_used ) ) goal.object.bots_used = []; goal.object.bots_used[goal.object.bots_used.size] = self; } } use_box( goal ) { // goal.object is the box if ( IsAgent(self) ) { self _enableUsability(); goal.object EnablePlayerUse( self ); wait(0.05); } if ( isDefined( goal.object ) && isDefined( goal.object.boxType ) ) { boxType = goal.object.boxType; if ( IsDefined(level.bot_pre_use_box_of_type[boxType]) ) self [[ level.bot_pre_use_box_of_type[boxType] ]](goal.object); if ( IsDefined(goal.object) ) // Might have been picked up during a wait in the pre_use function { time = (level.boxSettings[goal.object.boxType].useTime / 1000) + 0.5; self BotPressButton( "use", time ); wait( time ); if ( IsDefined(level.bot_post_use_box_of_type[boxType]) ) self [[ level.bot_post_use_box_of_type[boxType] ]](goal.object); } } if ( IsAgent(self) ) { self _disableUsability(); if ( IsDefined(goal.object) ) goal.object DisablePlayerUse( self ); } } watch_bot_died_during_crate( goal ) { // goal.object is the crate self thread bot_watch_for_death( goal.object ); } stop_using_crate( goal ) { // goal.object is the crate if ( IsDefined( goal.object ) ) { goal.object.bots[self.team] = 0; } } //======================================================== // bot_watch_for_death //======================================================== bot_watch_for_death( object ) { object endon( "death" ); object endon( "revived" ); object endon( "disconnect" ); level endon( "game_ended" ); prev_team = self.team; self waittill_any( "death", "disconnect" ); if ( IsDefined(object) ) { object.bots[prev_team] = 0; } } //======================================================== // bot_think_crate_blocking_path //======================================================== bot_think_crate_blocking_path() { self notify( "bot_think_crate_blocking_path" ); self endon( "bot_think_crate_blocking_path" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); radius = GetDvarFloat( "player_useRadius" ); // ensure bots don't get stuck on crates while( true ) { wait( 3 ); if( self UseButtonPressed() ) { continue; } if ( self isUsingRemote() ) continue; crates = level.carePackages; for ( i = 0; i < crates.size; i++ ) { crate = crates[i]; if ( !IsDefined( crate ) ) continue; if( DistanceSquared( self.origin, crate.origin ) < radius * radius ) { if ( crate.owner == self ) { self BotPressButton( "use", level.crateOwnerUseTime / 1000 + 0.5 ); } else { self BotPressButton( "use", level.crateNonOwnerUseTime / 1000 + 0.5 ); } } } } } //======================================================== // bot_think_revive //======================================================== bot_think_revive() { self notify( "bot_think_revive" ); self endon( "bot_think_revive" ); self endon( "death" ); self endon( "disconnect" ); level endon ( "game_ended" ); if( !level.teamBased ) { return; } while( true ) { waitTime = 2.0; revive_triggers = GetEntArray( "revive_trigger", "targetname" ); if( revive_triggers.size > 0 ) waitTime = 0.05; level waittill_notify_or_timeout( "player_last_stand", waitTime ); if( !self bot_can_revive() ) { continue; } revive_triggers = GetEntArray( "revive_trigger", "targetname" ); // sort the players in last stand if( revive_triggers.size > 1 ) { revive_triggers = SortByDistance( revive_triggers, self.origin ); // put the agent's owner at the begining of the list if( IsDefined(self.owner) ) { for( i = 0; i < revive_triggers.size; i++ ) { if( revive_triggers[i].owner != self.owner ) continue; // agent's owner is already at the front of the array if( i == 0 ) break; agent_owner_trigger = revive_triggers[i]; revive_triggers[i] = revive_triggers[0]; revive_triggers[0] = agent_owner_trigger; break; } } } for( i = 0; i < revive_triggers.size; i++ ) { revive_trigger = revive_triggers[i]; player = revive_trigger.owner; if( !IsDefined( player) ) { continue; } if( player == self ) { continue; } if( !IsAlive( player ) ) { continue; } if( player.team != self.team ) { continue; } if( !IsDefined( player.inLastStand ) || !player.inLastStand ) { continue; } if ( IsDefined( player.bots ) && IsDefined( player.bots[self.team] ) && player.bots[self.team] > 0 ) { continue; } if( DistanceSquared( self.origin, player.origin ) < 2048 * 2048 ) { extra_params = SpawnStruct(); extra_params.object = revive_trigger; extra_params.script_goal_radius = 64; if ( IsDefined(self.last_revive_fail_time) && GetTime() - self.last_revive_fail_time < 1000 ) extra_params.script_goal_radius = 32; extra_params.start_thread = ::watch_bot_died_during_revive; extra_params.end_thread = ::stop_reviving; extra_params.should_abort = ::player_revived_or_dead; extra_params.action_thread = ::revive_player; self bot_new_tactical_goal( "revive", player.origin, 60, extra_params ); break; } } } } watch_bot_died_during_revive( goal ) { // goal.object is the revive trigger of the player to revive self thread bot_watch_for_death( goal.object.owner ); } stop_reviving( goal ) { // goal.object is the revive trigger of the player to revive if ( IsDefined( goal.object.owner ) ) { goal.object.owner.bots[self.team] = 0; } } player_revived_or_dead( goal ) { // goal.object is the revive trigger of the player to revive if ( !IsDefined( goal.object.owner ) || goal.object.owner.health <= 0 ) return true; if ( !IsDefined( goal.object.owner.inLastStand ) || !goal.object.owner.inLastStand ) return true; return false; } revive_player( goal ) { // goal.object is the revive trigger of the player to revive if ( DistanceSquared(self.origin,goal.object.owner.origin) > 64 * 64 ) { self.last_revive_fail_time = GetTime(); return; // player crawled away, try again } if ( IsAgent(self) ) { self _enableUsability(); goal.object EnablePlayerUse( self ); wait(0.05); } prev_team = self.team; self BotPressButton( "use", level.lastStandUseTime / 1000 + 0.5 ); wait( level.lastStandUseTime / 1000 + 1.5 ); if ( IsDefined(goal.object.owner) ) goal.object.bots[prev_team] = 0; if ( IsAgent(self) ) { self _disableUsability(); if ( IsDefined(goal.object) ) goal.object DisablePlayerUse( self ); } } //======================================================== // bot_can_revive //======================================================== bot_can_revive() { if ( IsDefined( self.laststand ) && self.laststand == true ) return false; if ( self bot_has_tactical_goal( "revive" ) ) return false; if ( self bot_is_remote_or_linked() ) return false; // Bodyguards can always revive no matter what their goal type is if ( self bot_is_bodyguarding() ) return true; goalType = self BotGetScriptGoalType(); if ( goalType == "none" || goalType == "hunt" || goalType == "guard" ) return true; return false; } //======================================================== // revive_watch_for_finished //======================================================== revive_watch_for_finished( player ) { self endon( "death" ); self endon( "disconnect" ); self endon( "bad_path" ); self endon( "goal" ); player waittill_any( "death", "revived" ); self notify( "bad_path" ); } //======================================================== // bot_know_enemies_on_start //======================================================== bot_know_enemies_on_start() { self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); // Wait till grace period is over, then let this bot know where enemies are // (this is intended for the beginning of a match to get them seeking out enemies based on "knowledge" of the map start spots) if ( GetTime() > 15000 ) return; while ( !gameHasStarted() || !gameFlag( "prematch_done" ) ) { wait 0.05; } chosenEnemy = undefined; chosenEnemyKnowSelf = undefined; for ( enemyIdx = 0; enemyIdx < level.players.size; enemyIdx++ ) { otherPlayer = level.players[enemyIdx]; if ( IsDefined( otherPlayer ) && IsDefined( self.team ) && IsDefined( otherPlayer.team ) && IsEnemyTeam( self.team, otherPlayer.team ) ) { if ( !IsDefined( otherPlayer.bot_start_known_by_enemy ) ) chosenEnemy = otherPlayer; if ( IsAI( otherPlayer ) && !IsDefined( otherPlayer.bot_start_know_enemy ) ) chosenEnemyKnowSelf = otherPlayer; } } if ( IsDefined( chosenEnemy ) ) { self.bot_start_know_enemy = true; chosenEnemy.bot_start_known_by_enemy = true; self GetEnemyInfo( chosenEnemy ); } if ( IsDefined( chosenEnemyKnowSelf ) ) { chosenEnemyKnowSelf.bot_start_know_enemy = true; self.bot_start_known_by_enemy = true; chosenEnemyKnowSelf GetEnemyInfo( self ); } } //======================================================== // bot_make_entity_sentient //======================================================== bot_make_entity_sentient( team, expendable ) { if ( IsDefined(expendable) ) return self MakeEntitySentient( team, expendable ); else return self MakeEntitySentient( team ); } //======================================================== // bot_think_gametype //======================================================== bot_think_gametype() { self notify( "bot_think_gametype" ); self endon( "bot_think_gametype" ); self endon( "death" ); self endon( "disconnect" ); level endon( "game_ended" ); gameFlagWait( "prematch_done" ); self thread [[ level.bot_funcs["gametype_think"] ]](); } default_gametype_think() { // do nothing } monitor_smoke_grenades() { while(1) { level waittill("smoke", smoke_grenade, smoke_grenade_weaponName); if ( smoke_grenade_weaponName == "smoke_grenade_mp" || smoke_grenade_weaponName == "smoke_grenadejugg_mp" || smoke_grenade_weaponName == "odin_projectile_smoke_mp" ) smoke_grenade thread handle_smoke( 9.0 ); else if ( smoke_grenade_weaponName == "odin_projectile_large_rod_mp" ) smoke_grenade thread handle_smoke( 2.5 ); } } handle_smoke( final_wait_time ) { self waittill("explode", explosion_location ); new_sight_clip_origin = spawn_tag_origin(); new_sight_clip_origin show(); new_sight_clip_origin.origin = explosion_location; next_wait_time = 0.8; wait(next_wait_time); next_wait_time = 0.5; smoke_sight_clip_collision_64_short = GetEnt( "smoke_grenade_sight_clip_64_short", "targetname" ); if ( IsDefined(smoke_sight_clip_collision_64_short) ) { new_sight_clip_origin CloneBrushmodelToScriptmodel( smoke_sight_clip_collision_64_short ); //draw_entity_bounds( new_sight_clip_origin, next_wait_time, (1,0,0) ); } wait(next_wait_time); next_wait_time = 0.6; smoke_sight_clip_collision_64_tall = GetEnt( "smoke_grenade_sight_clip_64_tall", "targetname" ); if ( IsDefined(smoke_sight_clip_collision_64_tall) ) { new_sight_clip_origin CloneBrushmodelToScriptmodel( smoke_sight_clip_collision_64_tall ); //draw_entity_bounds( new_sight_clip_origin, next_wait_time, (1,0,0) ); } wait(next_wait_time); next_wait_time = final_wait_time; smoke_sight_clip_collision_256 = GetEnt( "smoke_grenade_sight_clip_256", "targetname" ); if ( IsDefined(smoke_sight_clip_collision_256) ) { new_sight_clip_origin CloneBrushmodelToScriptmodel( smoke_sight_clip_collision_256 ); //draw_entity_bounds( new_sight_clip_origin, next_wait_time, (1,0,0) ); } wait(next_wait_time); new_sight_clip_origin delete(); } bot_add_scavenger_bag( dropBag ) { added = false; dropBag.boxType = "scavenger_bag"; dropBag.boxTouchOnly = true; if ( !IsDefined( level.bot_scavenger_bags ) ) level.bot_scavenger_bags = []; // First fill any empty slot found foreach( index, existingBag in level.bot_scavenger_bags ) { if ( !IsDefined( existingBag ) ) { added = true; level.bot_scavenger_bags[index] = dropBag; break; } } if ( !added ) level.bot_scavenger_bags[level.bot_scavenger_bags.size] = dropBag; // Notify all scavengers that this bag is now available foreach( participant in level.participants ) { if ( isAI( participant ) && participant _hasPerk( "specialty_scavenger" ) ) participant notify( "new_crate_to_take" ); } } //======================================================== // bot_triggers //======================================================== bot_triggers() { bot_flag_set_triggers = GetEntArray("bot_flag_set", "targetname"); foreach(trigger in bot_flag_set_triggers) { if(!IsDefined(trigger.script_noteworthy)) { AssertMsg("Bot Flag trigger at " + trigger.origin + " is missing script_noteworthy flag name."); continue; } trigger thread bot_flag_trigger(trigger.script_noteworthy); } } bot_flag_trigger(flag_name) { self endon("death"); while(1) { self waittill("trigger", bot); if(IsAIGameParticipant(bot)) { bot notify("flag_trigger_set_" + flag_name); bot BotSetFlag(flag_name, true); bot thread bot_flag_trigger_clear(flag_name); } } } bot_flag_trigger_clear(flag_name) { self endon("flag_trigger_set_" + flag_name); self endon("death"); self endon("disconnect"); level endon("game_ended"); waitframe(); waittillframeend; self BotSetFlag(flag_name, false); }