diff --git a/data/scripts/mp/bots/_bot.gsc b/data/scripts/mp/bots/_bot.gsc new file mode 100644 index 00000000..a13be6c0 Binary files /dev/null and b/data/scripts/mp/bots/_bot.gsc differ diff --git a/data/scripts/mp/bots/_bot.gsc_raw b/data/scripts/mp/bots/_bot.gsc_raw new file mode 100644 index 00000000..82b1e956 --- /dev/null +++ b/data/scripts/mp/bots/_bot.gsc_raw @@ -0,0 +1,906 @@ +#using scripts\codescripts\struct; +#using scripts\shared\array_shared; +#using scripts\shared\callbacks_shared; +#using scripts\shared\killstreaks_shared; +#using scripts\shared\math_shared; +#using scripts\shared\rank_shared; +#using scripts\shared\system_shared; +#using scripts\shared\util_shared; +#using scripts\shared\weapons_shared; +#using scripts\shared\weapons\_weapons; + +#using scripts\shared\bots\_bot; +#using scripts\shared\bots\_bot_combat; +#using scripts\shared\bots\bot_traversals; +#using scripts\shared\bots\bot_buttons; +#using scripts\mp\bots\_bot_ball; +#using scripts\mp\bots\_bot_clean; +#using scripts\mp\bots\_bot_combat; +#using scripts\mp\bots\_bot_conf; +#using scripts\mp\bots\_bot_ctf; +#using scripts\mp\bots\_bot_dem; +#using scripts\mp\bots\_bot_dom; +#using scripts\mp\bots\_bot_escort; +#using scripts\mp\bots\_bot_hq; +#using scripts\mp\bots\_bot_koth; +#using scripts\mp\bots\_bot_loadout; +#using scripts\mp\bots\_bot_sd; +#using scripts\mp\killstreaks\_killstreakrules; +#using scripts\mp\killstreaks\_killstreaks; +#using scripts\mp\killstreaks\_ai_tank; +#using scripts\mp\killstreaks\_airsupport; +#using scripts\mp\killstreaks\_combat_robot; +#using scripts\mp\killstreaks\_counteruav; +#using scripts\mp\killstreaks\_dart; +#using scripts\mp\killstreaks\_dogs; +#using scripts\mp\killstreaks\_drone_strike; +#using scripts\mp\killstreaks\_emp; +#using scripts\mp\killstreaks\_flak_drone; +#using scripts\mp\killstreaks\_helicopter; +#using scripts\mp\killstreaks\_helicopter_gunner; +#using scripts\mp\killstreaks\_killstreak_bundles; +#using scripts\mp\killstreaks\_killstreak_detect; +#using scripts\mp\killstreaks\_killstreak_hacking; +#using scripts\mp\killstreaks\_killstreakrules; +#using scripts\mp\killstreaks\_killstreaks; +#using scripts\mp\killstreaks\_microwave_turret; +#using scripts\mp\killstreaks\_planemortar; +#using scripts\mp\killstreaks\_qrdrone; +#using scripts\mp\killstreaks\_raps; +#using scripts\mp\killstreaks\_rcbomb; +#using scripts\mp\killstreaks\_remote_weapons; +#using scripts\mp\killstreaks\_remotemissile; +#using scripts\mp\killstreaks\_satellite; +#using scripts\mp\killstreaks\_sentinel; +#using scripts\mp\killstreaks\_supplydrop; +#using scripts\mp\killstreaks\_turret; +#using scripts\mp\killstreaks\_uav; + +#using scripts\mp\teams\_teams; +#using scripts\mp\_util; + +#insert scripts\shared\shared.gsh; +#insert scripts\mp\bots\_bot.gsh; + +#define MAX_LOCAL_PLAYERS 10 +#define MAX_ONLINE_PLAYERS 18 +#define MAX_ONLINE_PLAYERS_PER_TEAM 6 + +#define RESPAWN_DELAY 0 +#define RESPAWN_INTERVAL 0.2 + +#namespace bot; + +REGISTER_SYSTEM( "bot_mp", &__init__, undefined ) + +function __init__() +{ + callback::on_start_gametype( &init ); + + level.getBotSettings = &get_bot_settings; + + level.onBotConnect = &on_bot_connect; + level.onBotSpawned = &on_bot_spawned; + level.onBotKilled = &on_bot_killed; + + level.botIdle = &bot_idle; + + level.botThreatLost = &bot_combat::chase_threat; + + level.botPreCombat = &bot_combat::mp_pre_combat; + level.botCombat = &bot_combat::combat_think; + level.botPostCombat = &bot_combat::mp_post_combat; + + level.botIgnoreThreat = &bot_combat::bot_ignore_threat; + + level.enemyEmpActive = &emp::EnemyEmpActive; +} + +function init() +{ + level endon( "game_ended" ); + + level.botSoak = is_bot_soak(); + + init_bot_gametype(); + + wait_for_host(); + + level thread populate_bots(); +} + +// Init Utils +//======================================== + +function is_bot_soak() +{ + return IsDedicated() && GetDvarInt( "sv_botsoak", 0 ); +} + +function wait_for_host() +{ + level endon( "game_ended" ); + + if ( level.botSoak ) + { + return; + } + + host = util::getHostPlayerForBots(); + + while ( !isdefined( host ) ) + { + wait( 0.25 ); + host = util::getHostPlayerForBots(); + } +} + +function get_host_team() +{ + host = util::getHostPlayerForBots(); + + if ( !isdefined( host ) || host.team == "spectator" ) + { + return "allies"; + } + + return host.team; +} + +function is_bot_comp_stomp() +{ + return false; +} + + +// Bot Events +//======================================== + +function on_bot_connect() +{ + self endon( "disconnect" ); + level endon( "game_ended" ); + + if ( IS_TRUE( level.disableClassSelection ) ) + { + self set_rank(); + + // Doesn't work if we don't do it in this order + self bot_loadout::pick_hero_gadget(); + self bot_loadout::pick_killstreaks(); + + return; + } + + if ( !IS_TRUE( self.pers["bot_loadout"] ) ) + { + self set_rank(); + + // Doesn't work if we don't do it in this order + self bot_loadout::build_classes(); + self bot_loadout::pick_hero_gadget(); + self bot_loadout::pick_killstreaks(); + + self.pers["bot_loadout"] = true; + } + + self bot_loadout::pick_classes(); + self choose_class(); +} + +function on_bot_spawned() +{ + self.bot.goalTag = undefined; +} + +function on_bot_killed() +{ + self endon("disconnect"); + level endon( "game_ended" ); + self endon( "spawned" ); + self waittill ( "death_delay_finished" ); + + wait RESPAWN_DELAY; + + if ( self choose_class() && level.playerForceRespawn ) + { + return; + } + + self thread respawn(); +} + +function respawn() +{ + self endon( "spawned" ); + self endon( "disconnect" ); + level endon( "game_ended" ); + + while( 1 ) + { + self bot::tap_use_button(); + + wait RESPAWN_INTERVAL; + } +} + +function bot_idle() +{ + if ( self do_supplydrop() ) + { + return; + } + + // TODO: Look for an enemy radar blip + // TODO: Get points on navmesh and feed into the spawn system to see if an enemy is likely to spawn there + self bot::navmesh_wander(); + self bot::sprint_to_goal(); +} + +// Crate maxs: 23.1482 +#define CRATE_GOAL_RADIUS 39 +#define CRATE_USE_RADIUS 62 // Wild guess on usable radius + +function do_supplydrop( maxRange = 1400 ) // A little under minimap width +{ + crates = GetEntArray( "care_package", "script_noteworthy" ); + + maxRangeSq = maxRange * maxRange; + + useRadiusSq = CRATE_USE_RADIUS * CRATE_USE_RADIUS; + + closestCrate = undefined; + closestCrateDistSq = undefined; + + foreach( crate in crates ) + { + if ( !crate IsOnGround() ) + { + continue; + } + + crateDistSq = Distance2DSquared( self.origin, crate.origin ); + + if ( crateDistSq > maxRangeSq ) + { + continue; + } + + inUse = isdefined( crate.useEnt ) && IS_TRUE( crate.useEnt.inUse ); + + if ( crateDistSq <= useRadiusSq ) + { + if ( inUse && !self useButtonPressed() ) + { + continue; + } + + self bot::press_use_button(); + return true; + } + + if ( !self has_minimap() && !self BotSightTracePassed( crate ) ) + { + continue; + } + + if ( !isdefined( closestCrate ) || crateDistSq < closestCrateDistSq ) + { + closestCrate = crate; + closestCrateDistSq = crateDistSq; + } + } + + if ( isdefined( closestCrate ) ) + { + randomAngle = ( 0, RandomInt( 360 ), 0 ); + randomVec = AnglesToForward( randomAngle ); + + point = closestCrate.origin + randomVec * CRATE_GOAL_RADIUS; + + if ( self BotSetGoal( point ) ) + { + self thread watch_crate( closestCrate ); + return true; + } + } + + return false; +} + +function watch_crate( crate ) +{ + self endon( "death" ); + self endon( "bot_goal_reached" ); + level endon( "game_ended" ); + + while ( isdefined( crate ) && !self bot_combat::has_threat() ) + { + wait level.botSettings.thinkInterval; + } + + self BotSetGoal( self.origin ); +} + +// Bot Team Population +//======================================== + +function populate_bots() +{ + level endon( "game_ended" ); + + if ( level.teambased ) + { + maxAllies = GetDvarInt( "bot_maxAllies", 0 ); + maxAxis = GetDvarInt( "bot_maxAxis", 0 ); + + level thread monitor_bot_team_population( maxAllies, maxAxis ); + } + else + { + maxFree = GetDvarInt( "bot_maxFree", 0 ); + + level thread monitor_bot_population( maxFree ); + } +} + +function monitor_bot_team_population( maxAllies, maxAxis ) +{ + level endon( "game_ended" ); + + if ( !maxAllies && !maxAxis ) + { + return; + } + + fill_balanced_teams( maxAllies, maxAxis ); + + while ( 1 ) + { + wait 3; + + // TODO: Get a player count that includes 'CON_CONNECTING' players + allies = GetPlayers( "allies" ); + axis = GetPlayers( "axis" ); + + if ( allies.size > maxAllies && + remove_best_bot( allies ) ) + { + continue; + } + + if ( axis.size > maxAxis && + remove_best_bot( axis ) ) + { + continue; + } + + if ( allies.size < maxAllies || axis.size < maxAxis ) + { + add_balanced_bot( allies, maxAllies, axis, maxAxis ); + } + } +} + +function fill_balanced_teams( maxAllies, maxAxis ) +{ + allies = GetPlayers( "allies" ); + axis = GetPlayers( "axis" ); + + while ( ( allies.size < maxAllies || axis.size < maxAxis ) && + add_balanced_bot( allies, maxAllies, axis, maxAxis ) ) + { + WAIT_SERVER_FRAME; + + allies = GetPlayers( "allies" ); + axis = GetPlayers( "axis" ); + } +} + +function add_balanced_bot( allies, maxAllies, axis, maxAxis ) +{ + bot = undefined; + + if ( allies.size < maxAllies && + ( allies.size <= axis.size || axis.size >= maxAxis ) ) + { + bot = add_bot( "allies" ); + } + else if ( axis.size < maxAxis ) + { + bot = add_bot( "axis" ); + } + + return isdefined( bot ); +} + +function monitor_bot_population( maxFree ) +{ + level endon( "game_ended" ); + + if ( !maxFree ) + { + return; + } + + // Initial Fill + players = GetPlayers( ); + while ( players.size < maxFree ) + { + add_bot(); + WAIT_SERVER_FRAME; + players = GetPlayers( ); + } + + while ( 1 ) + { + wait 3; + + // TODO: Get a player count that includes 'CON_CONNECTING' players + players = GetPlayers( ); + + if ( players.size < maxFree ) + { + add_bot(); + } + else if ( players.size > maxFree ) + { + remove_best_bot( players ); + } + } +} + +function remove_best_bot( players ) +{ + bots = filter_bots( players ); + + if ( !bots.size ) + { + return false; + } + + // Prefer non-combat bots + bestBots = []; + + foreach( bot in bots ) + { + // Don't kick bots in the process of connecting + if ( bot.sessionstate == "spectator" ) + { + continue; + } + + if ( bot.sessionstate == "dead" || !bot bot_combat::has_threat() ) + { + bestBots[bestBots.size] = bot; + } + } + + if ( bestBots.size ) + { + remove_bot( bestBots[RandomInt( bestBots.size )] ); + } + else + { + remove_bot( bots[RandomInt( bots.size )] ); + } + + return true; +} + +// Bot Loadouts +//======================================== + +function choose_class() +{ + if ( IS_TRUE( level.disableClassSelection ) ) + { + return false; + } + + currClass = self bot_loadout::get_current_class(); + + if ( !isdefined( currClass ) || RandomInt( 100 ) < VAL( level.botSettings.changeClassWeight, 0 ) ) + { + classIndex = RandomInt( self.loadoutClasses.size ); + className = self.loadoutClasses[classIndex].name; + } + + if ( !isdefined(className) || className === currClass ) + { + return false; + } + + self notify( "menuresponse", MENU_CHANGE_CLASS, className ); + + return true; +} + +// Killstreaks +//======================================== + +function use_killstreak() +{ + if ( !level.loadoutKillstreaksEnabled || + self emp::EnemyEMPActive() ) + { + return; + } + + weapons = self GetWeaponsList(); + inventoryWeapon = self GetInventoryWeapon(); + + foreach( weapon in weapons ) + { + killstreak = killstreaks::get_killstreak_for_weapon( weapon ); + + if ( !isdefined( killstreak ) ) + { + continue; + } + + if ( weapon != inventoryWeapon && !self GetWeaponAmmoClip( weapon ) ) + { + continue; + } + + if ( self killstreakrules::isKillstreakAllowed( killstreak, self.team ) ) + { + useWeapon = weapon; + break; + } + } + + if ( !isdefined( useWeapon ) ) + { + return; + } + + killstreak_ref = killstreaks::get_menu_name( killstreak ); + + switch( killstreak_ref ) + { + case "killstreak_uav": + case "killstreak_counteruav": + case "killstreak_remote_missile": + { + self switchtoweapon( weapon ); + self waittill( "weapon_change_complete" ); + wait 1.5; + self bot::press_attack_button(); + } + return; + case "killstreak_satellite": + case "killstreak_helicopter_player_gunner": + case "killstreak_ai_tank_drop": + self use_supply_drop( weapon ); + break; + case "killstreak_raps": + case "killstreak_sentinel": + { + self switchtoweapon(useweapon); + break; + } + } +} + +function get_closest_enemy( origin, on_radar ) +{ + enemies = self get_enemies( on_radar ); + enemies = arraysort( enemies, origin ); + + if ( enemies.size ) + return enemies[0]; + + return undefined; +} + +function use_supply_drop( weapon ) +{ + if ( weapon == "inventory_supplydrop_mp" || weapon == "supplydrop_mp" ) + { + if ( gettime() - self.spawntime > 5000 ) + return; + } + + yaw = ( 0, self.angles[1], 0 ); + dir = anglestoforward( yaw ); + dir = vectornormalize( dir ); + drop_point = self.origin + vectorscale( dir, 384 ); + end = drop_point + vectorscale( ( 0, 0, 1 ), 2048.0 ); + + if ( !sighttracepassed( drop_point, end, 0, undefined ) ) + return; + + if ( !sighttracepassed( self.origin, end, 0, undefined ) ) + return; + + end = drop_point - vectorscale( ( 0, 0, 1 ), 32.0 ); + + if ( bullettracepassed( drop_point, end, 0, undefined ) ) + return; + + self addgoal( self.origin, 24, 4, "killstreak" ); + + if ( weapon == "missile_drone_mp" || weapon == "inventory_missile_drone_mp" ) + self lookat( drop_point + vectorscale( ( 0, 0, 1 ), 384.0 ) ); + else + self lookat( drop_point ); + + wait 0.5; + + if ( self getcurrentweapon() != weapon ) + { + self thread weapon_switch_failsafe(); + self switchtoweapon( weapon ); + + self waittill( "weapon_change_complete" ); + } + + use_item( weapon ); + self switchtoweapon( self.lastnonkillstreakweapon ); + self clearlookat(); + self cancelgoal( "killstreak" ); +} + +function use_item( weapon ) +{ + self bot::press_attack_button(); + wait 0.5; + + for ( i = 0; i < 10; i++ ) + { + if ( self getcurrentweapon() == weapon || self getcurrentweapon() == "none" ) + self bot::press_attack_button(); + else + return; + + wait 0.5; + } +} + +function weapon_switch_failsafe() +{ + self endon( "death" ); + self endon( "disconnect" ); + self endon( "weapon_change_complete" ); + wait 10; + self notify( "weapon_change_complete" ); +} + + +function has_radar() +{ + if ( level.teambased ) + { + return ( uav::HasUAV( self.team ) || satellite::HasSatellite( self.team ) ); + } + + return ( uav::HasUAV( self.entnum ) || satellite::HasSatellite( self.entnum ) ); +} + +function has_minimap() +{ + if ( self IsEmpJammed() ) + { + return false; + } + + if ( IS_TRUE( level.hardcoreMode ) ) + { + return self has_radar(); + } + + return true; +} + +function get_enemies( on_radar ) +{ + if ( !isdefined( on_radar ) ) + { + on_radar = false; + } + + enemies = self GetEnemies(); + + if ( on_radar && !self has_radar() ) + { + for ( i = 0; i < enemies.size; i++ ) + { + if ( !isdefined( enemies[i].lastFireTime ) ) + { + ArrayRemoveIndex( enemies, i ); + i--; + } + else if ( GetTime() - enemies[i].lastFireTime > 2000 ) + { + ArrayRemoveIndex( enemies, i ); + i--; + } + } + } + + return enemies; +} + +function set_rank() +{ + players = GetPlayers(); + + ranks = []; + bot_ranks = []; + human_ranks = []; + + for ( i = 0; i < players.size; i++ ) + { + if ( players[i] == self ) + continue; + + if ( isdefined( players[i].pers[ "rank" ] ) ) + { + if ( players[i] util::is_bot() ) + { + bot_ranks[ bot_ranks.size ] = players[i].pers[ "rank" ]; + } + else + { + human_ranks[ human_ranks.size ] = players[i].pers[ "rank" ]; + } + } + } + + if( !human_ranks.size ) + human_ranks[ human_ranks.size ] = 10; + + human_avg = math::array_average( human_ranks ); + + while ( bot_ranks.size + human_ranks.size < 5 ) + { + // add some random ranks for better random number distribution + r = human_avg + RandomIntRange( -5, 5 ); + rank = math::clamp( r, 0, level.maxRank ); + human_ranks[ human_ranks.size ] = rank; + } + + ranks = ArrayCombine( human_ranks, bot_ranks, true, false ); + + avg = math::array_average( ranks ); + s = math::array_std_deviation( ranks, avg ); + + rank = Int( math::random_normal_distribution( avg, s, 0, level.maxRank ) ); + + while ( !isdefined( self.pers["codpoints"] ) ) + { + wait 0.1; + } + + self.pers[ "rank" ] = rank; + self.pers[ "rankxp" ] = rank::getRankInfoMinXP( rank ); + + self setRank( rank ); + self rank::syncXPStat(); +} + +function init_bot_gametype() +{ + switch(level.gametype) + { + case "ball": + { + bot_ball::init(); + return true; + } + case "conf": + { + bot_conf::init(); + return true; + } + case "ctf": + { + bot_ctf::init(); + return true; + } + case "dem": + { + bot_dem::init(); + return true; + } + case "dm": + { + return true; + } + case "dom": + { + bot_dom::init(); + return true; + } + case "escort": + { + bot_escort::init(); + return true; + } + case "infect": + { + return true; + } + case "gun": + { + return true; + } + case "koth": + { + bot_koth::init(); + return true; + } + case "sd": + { + bot_sd::init(); + return true; + } + case "clean": + { + bot_clean::init(); + return true; + } + case "tdm": + { + return true; + } + } + + return false; +} + +function get_bot_settings() +{ + switch ( GetDvarInt( "bot_difficulty", 1 ) ) + { + case 0: + bundleName = "bot_mp_easy"; + break; + + case 1: + bundleName = "bot_mp_normal"; + break; + case 2: + bundleName = "bot_mp_hard"; + break; + case 3: + default: + bundleName = "bot_mp_veteran"; + break; + } + + return struct::get_script_bundle( "botsettings", bundleName ); +} + +function friend_goal_in_radius( goal_name, origin, radius ) +{ + return 0; +} + +function friend_in_radius( goal_name, origin, radius ) +{ + return false; +} + +function get_friends() +{ + return []; +} + +function bot_vehicle_weapon_ammo( weaponName ) +{ + return false; +} + +function navmesh_points_visible( origin, point ) +{ + return false; +} + +function dive_to_prone( exit_stance ) +{ + +} + diff --git a/data/scripts/mp/bots/_bot_loadout.gsc b/data/scripts/mp/bots/_bot_loadout.gsc new file mode 100644 index 00000000..1dce3345 Binary files /dev/null and b/data/scripts/mp/bots/_bot_loadout.gsc differ diff --git a/data/scripts/mp/bots/_bot_loadout.gsc_raw b/data/scripts/mp/bots/_bot_loadout.gsc_raw new file mode 100644 index 00000000..3b48b7f1 --- /dev/null +++ b/data/scripts/mp/bots/_bot_loadout.gsc_raw @@ -0,0 +1,854 @@ +#using scripts\shared\array_shared; +#using scripts\shared\rank_shared; + +#insert scripts\shared\shared.gsh; +#insert scripts\shared\statstable_shared.gsh; + +#using scripts\mp\killstreaks\_killstreaks; + +#using scripts\mp\bots\_bot; + +#define BOT_ALLOCATION_MAX 100 +#define BOT_ALLOCATION_UNLOCK_MAX 3 +#define BOT_RANK_ALL_OPTIONS_AVAILABLE 20 +#define BOT_RANK_OPTIONS_MULTIPLIER 4 + +#namespace bot_loadout; + +// Item Whitelist +//======================================== + +function in_whitelist( itemName ) +{ + if ( !isdefined( itemName ) ) + return false; + + switch( itemName ) + { + // Secondaries + case "WEAPON_KNIFE_LOADOUT": + case "WEAPON_PISTOL_STANDARD": + case "WEAPON_PISTOL_BURST": + case "WEAPON_PISTOL_FULLAUTO": + case "WEAPON_LAUNCHER_STANDARD": + case "WEAPON_LAUNCHER_LOCKONLY": + + // Primaries + case "WEAPON_SMG_STANDARD": + case "WEAPON_SMG_BURST": + case "WEAPON_SMG_FASTFIRE": + case "WEAPON_SMG_LONGRANGE": + case "WEAPON_SMG_VERSATILE": + case "WEAPON_SMG_CAPACITY": + case "WEAPON_AR_STANDARD": + case "WEAPON_AR_ACCURATE": + case "WEAPON_AR_CQB": + case "WEAPON_AR_DAMAGE": + case "WEAPON_AR_FASTBURST": + case "WEAPON_AR_LONGBURST": + case "WEAPON_AR_MARKSMAN": + case "WEAPON_LMG_CQB": + case "WEAPON_LMG_HEAVY": + case "WEAPON_LMG_LIGHT": + case "WEAPON_LMG_SLOWFIRE": + case "WEAPON_SNIPER_FASTBOLT": + case "WEAPON_SNIPER_FASTSEMI": + case "WEAPON_SNIPER_POWERBOLT": + case "WEAPON_SNIPER_CHARGESHOT": + case "WEAPON_SHOTGUN_FULLAUTO": + case "WEAPON_SHOTGUN_PRECISION": + case "WEAPON_SHOTGUN_PUMP": + case "WEAPON_SHOTGUN_SEMIAUTO": + + // Lethals + case "WEAPON_FRAGGRENADE": + case "WEAPON_HATCHET": + case "WEAPON_STICKY_GRENADE": + case "WEAPON_SATCHEL_CHARGE": + case "WEAPON_BOUNCINGBETTY": + case "WEAPON_INCENDIARY_GRENADE": + + // Tacticals + case "WEAPON_WILLY_PETE": + case "WEAPON_STUN_GRENADE": + case "WEAPON_EMPGRENADE": + case "WEAPON_FLASHBANG": + case "WEAPON_PROXIMITY_GRENADE": + case "WEAPON_PDA_HACK": + case "WEAPON_TROPHY_SYSTEM": + + // Killstreaks + //case "KILLSTREAK_RCBOMB": + case "KILLSTREAK_RECON": + case "KILLSTREAK_COUNTER_UAV": + //case "KILLSTREAK_SUPPLY_DROP": + //case "KILLSTREAK_MICROWAVE_TURRET": + case "KILLSTREAK_REMOTE_MISSILE": + //case "KILLSTREAK_PLANEMORTAR": + //case "KILLSTREAK_AUTO_TURRET": + case "KILLSTREAK_AI_TANK_DROP": + //case "KILLSTREAK_HELICOPTER_COMLINK": + case "KILLSTREAK_SATELLITE": + //case "KILLSTREAK_EMP": + //case "KILLSTREAK_HELICOPTER_GUNNER": + case "KILLSTREAK_RAPS": + //case "KILLSTREAK_DRONE_STRIKE": + //case "KILLSTREAK_DART": + case "KILLSTREAK_SENTINEL": + + // TU Something Weapons + case "WEAPON_MELEE_KNUCKLES": + case "WEAPON_MELEE_BUTTERFLY": + case "WEAPON_MELEE_WRENCH": + + // TU 6 Weapons + case "WEAPON_PISTOL_SHOTGUN": + case "WEAPON_AR_GARAND": + case "WEAPON_SPECIAL_CROSSBOW": + case "WEAPON_MELEE_CROWBAR": + case "WEAPON_MELEE_SWORD": + case "WEAPON_MELEE_BOXING": + case "WEAPON_SMG_AK74U": + case "WEAPON_SMG_MP40": + case "WEAPON_SMG_RECHAMBER": + case "WEAPON_SMG_NAILGUN": + case "WEAPON_AR_AN94": + case "WEAPON_AR_FAMAS": + case "WEAPON_SMG_MSMC": + case "WEAPON_LMG_INFINITE": + case "WEAPON_AR_PULSE": + case "WEAPON_AR_M16": + case "WEAPON_SMG_PPSH": + case "WEAPON_LAUNCHER_EX41": + case "WEAPON_SHOTGUN_OLYMPIA": + case "WEAPON_SNIPER_QUICKSCOPE": + case "WEAPON_SNIPER_DOUBLE": + case "WEAPON_SMG_STEN": + case "WEAPON_AR_GALIL": + case "WEAPON_LMG_RPK": + case "WEAPON_AR_M14": + case "WEAPON_SHOTGUN_ENERGY": + case "WEAPON_SPECIAL_CROSSBOW_DW": + case "WEAPON_AR_PEACEKEEPER": + case "WEAPON_MELEE_CHAINSAW": + case "WEAPON_SPECIAL_KNIFE_BALLISTIC": + case "WEAPON_MELEE_CRESCENT": + case "WEAPON_SPECIAL_DISCGUN": + return true; + } + + return false; +} + +// Classes +//======================================== + +function build_classes() +{ + primaryWeapons = self get_available_items( undefined, "primary" ); + secondaryWeapons = self get_available_items( undefined, "secondary" ); + lethals = self get_available_items( undefined, "primarygadget" ); + tacticals = self get_available_items( undefined, "secondarygadget" ); + if ( IS_TRUE( level.perksEnabled ) ) + { + specialties1 = self get_available_items( undefined, "specialty1" ); + specialties2 = self get_available_items( undefined, "specialty2" ); + specialties3 = self get_available_items( undefined, "specialty3" ); + } + + foreach( className, classValue in level.classMap ) + { + if ( !isSubstr( className, "custom" ) ) + { + continue; + } + + classIndex = int( className[className.size-1] ); + + pickedItems = []; + + pick_item( pickedItems, primaryWeapons ); + + if ( RandomInt( 100 ) < 95 ) // 5% chance to be a boxer for Scronce + { + pick_item( pickedItems, secondaryWeapons ); + } + + // Shuffle these selections around a bit so the classes don't all look the same when the allocation is low + otherItems = Array ( lethals, tacticals, specialties1, specialties2, specialties3 ); + otherItems = array::randomize( otherItems ); + + for ( i = 0; i < otherItems.size; i ++ ) + { + pick_item( pickedItems, otherItems[i] ); + } + + // Add items up to the max allocation + for ( i = 0; i < pickedItems.size && i < level.maxAllocation; i++ ) + { + self BotClassAddItem( classIndex, pickedItems[i] ); + } + + // TODO: Pick primary/secondary attachments, extra perks, extra lethal, extra tactical, overkill +/* + primaryWeapon = self GetLoadoutWeapon( classIndex, "primary" ); + + if ( primaryWeapon != level.weaponNone && primaryWeapon.supportedAttachments.size ) + { + attachment = array::random( primaryWeapon.supportedAttachments ); + self BotClassAddAttachment( classIndex, primaryWeapon, attachment, "primary" ); + } + + secondaryWeapon = self GetLoadoutWeapon( classIndex, "secondary" ); + + if ( secondaryWeapon != level.weaponNone && secondaryWeapon.supportedAttachments.size ) + { + attachment = array::random( secondaryWeapon.supportedAttachments ); + self BotClassAddAttachment( classIndex, secondaryWeapon, attachment, "secondary" ); + } +*/ + } +} + +function pick_item( &pickedItems, items ) +{ + if ( !isdefined( items ) || items.size <= 0 ) + { + return; + } + + pickedItems[pickedItems.size] = array::random( items ); +} + +function pick_classes() +{ + self.loadoutClasses = []; + self.launcherClassCount = 0; + + foreach( className, classValue in level.classMap ) + { + if ( isSubstr( className, "custom" ) ) + { + if ( level.disableCAC ) + { + continue; + } + + classIndex = int( className[className.size-1] ); + } + else + { + // Things bots could use better in the default classes: + // C4, Trophy System, Lock on only launcher + classIndex = level.classToClassNum[ classValue ]; + } + + primary = self GetLoadoutWeapon( classIndex, "primary" ); + secondary = self GetLoadoutWeapon( classIndex, "secondary" ); + + botClass = SpawnStruct(); + botClass.name = className; + botClass.index = classIndex; + botClass.value = classValue; + botClass.primary = primary; + botClass.secondary = secondary; + + if ( botClass.secondary.isRocketLauncher ) + { + self.launcherClassCount++; + } + + self.loadoutClasses[ self.loadoutClasses.size ] = botClass; + } +} + +function get_current_class() +{ + currValue = self.pers["class"]; + if ( !isdefined( currValue ) ) + { + return undefined; + } + + foreach( botClass in self.loadoutClasses ) + { + if ( botClass.value == currValue ) + { + return botClass; + } + } + + return undefined; +} + +// Specialists +//======================================== + +function pick_hero_gadget() +{ + if ( RandomInt( 2 ) < 1 || !self pick_hero_ability() ) + { + self pick_hero_weapon(); + } +} + +function pick_hero_weapon() +{ + heroWeaponRef = self GetHeroWeaponName(); + + if ( IsItemRestricted( heroWeaponRef ) ) + { + return false; + } + + heroWeaponName = self get_item_name( heroWeaponRef ); + self BotClassAddItem( 0, heroWeaponName ); + + return true; +} + +function pick_hero_ability() +{ + heroAbilityRef = self GetHeroAbilityName(); + + if ( IsItemRestricted( heroAbilityRef ) ) + { + return false; + } + + heroAbilityName = self get_item_name( heroAbilityRef ); + self BotClassAddItem( 0, heroAbilityName ); + + return true; +} + +// Killstreaks +//======================================== + +function pick_killstreaks() +{ + killstreaks = array::randomize( self get_available_items( "killstreak" ) ); + + for( i = 0; i < 3 && i < killstreaks.size; i++ ) + { + self BotClassAddItem( 0, killstreaks[i] ); + } +} + + +// Get Items +//======================================== + +function get_available_items( filterGroup, filterSlot ) +{ + // Get unlocked and unrestricted items + items = []; + + for( i = 0; i < STATS_TABLE_MAX_ITEMS; i++ ) + { + row = tableLookupRowNum( level.statsTableID, STATS_TABLE_COL_NUMBERING, i ); + + if ( row < 0 ) + { + continue; + } + + name = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_NAME ); + + if ( name == "" || !in_whitelist( name ) ) + { + continue; + } + + allocation = Int( tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_ALLOCATION ) ); + + if ( allocation < 0 ) + { + continue; + } + + ref = tableLookupColumnForRow( level.statsTableId, row, STATS_TABLE_COL_REFERENCE ); + + if ( IsItemRestricted( ref ) ) + { + continue; + } + + number = Int( tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_NUMBERING ) ); +/* + if ( SessionModeIsPrivate() && self IsItemLocked( number ) ) + { + continue; + } +*/ + if ( isdefined( filterGroup ) ) + { + group = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_GROUP ); + + if ( group != filterGroup ) + { + continue; + } + } + + if ( isdefined( filterSlot ) ) + { + slot = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_SLOT ); + + if ( slot != filterSlot ) + { + continue; + } + } + + items[items.size] = name; + } + + return items; +} + +function get_item_name( itemReference ) +{ + for( i = 0; i < STATS_TABLE_MAX_ITEMS; i++ ) + { + row = tableLookupRowNum( level.statsTableID, STATS_TABLE_COL_NUMBERING, i ); + + if ( row < 0 ) + { + continue; + } + + reference = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_REFERENCE ); + + if ( reference != itemReference ) + { + continue; + } + + name = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_NAME ); + + return name; + } + + return undefined; +} + +// Not in use + +function init() +{ + level endon( "game_ended" ); + + level.bot_banned_killstreaks = Array ( "KILLSTREAK_RCBOMB", + "KILLSTREAK_QRDRONE", + /* "KILLSTREAK_REMOTE_MISSILE",*/ + "KILLSTREAK_REMOTE_MORTAR", + "KILLSTREAK_HELICOPTER_GUNNER" ); + for ( ;; ) + { + level waittill( "connected", player ); + + if ( !player IsTestClient() ) + { + continue; + } + + player thread on_bot_connect(); + } +} + +function on_bot_connect() +{ + self endon( "disconnect" ); + + if ( isdefined( self.pers[ "bot_loadout" ] ) ) + { + return; + } + + wait( 0.10 ); + + if ( self GetEntityNumber() % 2 == 0 ) + { + WAIT_SERVER_FRAME; + } + + self bot::set_rank(); + + self BotSetRandomCharacterCustomization(); + + + max_allocation = BOT_ALLOCATION_MAX; +/* + if ( SessionModeIsPrivate() ) + { + for ( i = 1; i <= BOT_ALLOCATION_UNLOCK_MAX; i++ ) + { + if ( self IsItemLocked( rank::GetItemIndex( "feature_allocation_slot_" + i ) ) ) + { + max_allocation--; + } + } + } +*/ + self construct_loadout( max_allocation ); + self.pers[ "bot_loadout" ] = true; +} + +function construct_loadout( allocation_max ) +{ +/* if ( SessionModeIsPrivate() && self IsItemLocked( rank::GetItemIndex( "feature_cac" ) ) ) + { + // cac still locked + return; + } +*/ + pixbeginevent( "bot_construct_loadout" ); + + item_list = build_item_list(); + +// item_list["primary"] = []; +// item_list["primary"][0] = "WEAPON_RIOTSHIELD"; + + construct_class( 0, item_list, allocation_max ); + construct_class( 1, item_list, allocation_max ); + construct_class( 2, item_list, allocation_max ); + construct_class( 3, item_list, allocation_max ); + construct_class( 4, item_list, allocation_max ); + + killstreaks = item_list["killstreak1"]; + + if ( isdefined( item_list["killstreak2"] ) ) + { + killstreaks = ArrayCombine( killstreaks, item_list["killstreak2"], true, false ); + } + + if ( isdefined( item_list["killstreak3"] ) ) + { + killstreaks = ArrayCombine( killstreaks, item_list["killstreak3"], true, false ); + } + + if ( isdefined( killstreaks ) && killstreaks.size ) + { + choose_weapon( 0, killstreaks ); + choose_weapon( 0, killstreaks ); + choose_weapon( 0, killstreaks ); + } + + self.claimed_items = undefined; + pixendevent(); +} + +function construct_class( constructclass, items, allocation_max ) +{ + allocation = 0; + + claimed_count = build_claimed_list( items ); + self.claimed_items = []; + + // primary + weapon = choose_weapon( constructclass, items["primary"] ); + claimed_count["primary"]++; + allocation++; + + // secondary + weapon = choose_weapon( constructclass, items["secondary"] ); + choose_weapon_option( constructclass, "camo", 1 ); +} + +function make_choice( chance, claimed, max_claim ) +{ + return ( claimed < max_claim && RandomInt( 100 ) < chance ); +} + +function chose_action( action1, chance1, action2, chance2, action3, chance3, action4, chance4 ) +{ + chance1 = Int( chance1 / 10 ); + chance2 = Int( chance2 / 10 ); + chance3 = Int( chance3 / 10 ); + chance4 = Int( chance4 / 10 ); + + actions = []; + + for( i = 0; i < chance1; i++ ) + { + actions[ actions.size ] = action1; + } + + for( i = 0; i < chance2; i++ ) + { + actions[ actions.size ] = action2; + } + + for( i = 0; i < chance3; i++ ) + { + actions[ actions.size ] = action3; + } + + for( i = 0; i < chance4; i++ ) + { + actions[ actions.size ] = action4; + } + + return array::random( actions ); +} + +function item_is_claimed( item ) +{ + foreach( claim in self.claimed_items ) + { + if ( claim == item ) + { + return true; + } + } + + return false; +} + +function choose_weapon( weaponclass, items ) +{ + if ( !isdefined( items ) || !items.size ) + { + return undefined; + } + + start = RandomInt( items.size ); + + for( i = 0; i < items.size; i++ ) + { + weapon = items[ start ]; + + if ( !item_is_claimed( weapon ) ) + { + break; + } + + start = ( start + 1 ) % items.size; + } + + self.claimed_items[ self.claimed_items.size ] = weapon; + + self BotClassAddItem( weaponclass, weapon ); + return weapon; +} + +function build_weapon_options_list( optionType ) +{ + level.botWeaponOptionsId[optionType] = []; + level.botWeaponOptionsProb[optionType] = []; + + csv_filename = "gamedata/weapons/common/attachmentTable.csv"; + prob = 0; + for ( row = 0 ; row < 255 ; row++ ) + { + if ( tableLookupColumnForRow( csv_filename, row, ATTACHMENT_TABLE_COL_TYPE ) == optionType ) + { + index = level.botWeaponOptionsId[optionType].size; + level.botWeaponOptionsId[optionType][index] = Int( tableLookupColumnForRow( csv_filename, row, ATTACHMENT_TABLE_COL_NUMBERING ) ); + prob += Int( tableLookupColumnForRow( csv_filename, row, ATTACHMENT_TABLE_COL_BOT_PROB ) ); + level.botWeaponOptionsProb[optionType][index] = prob; + } + } +} + +function choose_weapon_option( weaponclass, optionType, primary ) +{ + if ( !isdefined( level.botWeaponOptionsId ) ) + { + level.botWeaponOptionsId = []; + level.botWeaponOptionsProb = []; + + build_weapon_options_list( "camo" ); + build_weapon_options_list( "reticle" ); + } + + // weapon options cannot be set in local matches + if ( !level.onlineGame && !level.systemLink ) + return; + + // Increase the range of the probability to reduce the chances of picking the option when the bot's level is less than BOT_RANK_ALL_OPTIONS_AVAILABLE + // (in system link all options are available) + numOptions = level.botWeaponOptionsProb[optionType].size; + maxProb = level.botWeaponOptionsProb[optionType][numOptions-1]; + if ( !level.systemLink && self.pers[ "rank" ] < BOT_RANK_ALL_OPTIONS_AVAILABLE ) + maxProb += BOT_RANK_OPTIONS_MULTIPLIER * maxProb * ( ( BOT_RANK_ALL_OPTIONS_AVAILABLE - self.pers[ "rank" ] ) / BOT_RANK_ALL_OPTIONS_AVAILABLE ); + + rnd = RandomInt( Int( maxProb ) ); + for (i=0 ; i rnd ) + { + self BotClassSetWeaponOption( weaponclass, primary, optionType, level.botWeaponOptionsId[optionType][i] ); + break; + } + } +} + +function choose_primary_attachments( weaponclass, weapon, allocation, allocation_max ) +{ + attachments = weapon.supportedAttachments; + remaining = allocation_max - allocation; + + if ( !attachments.size || !remaining ) + { + return 0; + } + + attachment_action = chose_action( "3_attachments", 25, "2_attachments", 65, "1_attachments", 10, "none", 5 ); + + if ( remaining >= 4 && attachment_action == "3_attachments" ) + { + a1 = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a1, "primaryattachment1" ); + count = 1; + + attachments = GetWeaponAttachments( weapon, a1 ); + + if ( attachments.size ) + { + a2 = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a2, "primaryattachment2" ); + count++; + + attachments = GetWeaponAttachments( weapon, a1, a2 ); + + if ( attachments.size ) + { + a3 = array::random( attachments ); + self BotClassAddItem( weaponclass, "BONUSCARD_PRIMARY_GUNFIGHTER" ); + self BotClassAddAttachment( weaponclass, weapon, a3, "primaryattachment3" ); + return 4; + } + } + + return count; + } + else if ( remaining >= 2 && attachment_action == "2_attachments" ) + { + a1 = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a1, "primaryattachment1" ); + + attachments = GetWeaponAttachments( weapon, a1 ); + + if ( attachments.size ) + { + a2 = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a2, "primaryattachment2" ); + return 2; + } + + return 1; + } + else if ( remaining >= 1 && attachment_action == "1_attachments" ) + { + a = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a, "primaryattachment1" ); + return 1; + } + + return 0; +} + +function choose_secondary_attachments( weaponclass, weapon, allocation, allocation_max ) +{ + attachments = weapon.supportedAttachments ; + remaining = allocation_max - allocation; + + if ( !attachments.size || !remaining ) + { + return 0; + } + + attachment_action = chose_action( "2_attachments", 10, "1_attachments", 40, "none", 50, "none", 0 ); + + if ( remaining >= 3 && attachment_action == "2_attachments" ) + { + a1 = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a1, "secondaryattachment1" ); + + attachments = GetWeaponAttachments( weapon, a1 ); + + if ( attachments.size ) + { + a2 = array::random( attachments ); + self BotClassAddItem( weaponclass, "BONUSCARD_SECONDARY_GUNFIGHTER" ); + self BotClassAddAttachment( weaponclass, weapon, a2, "secondaryattachment2" ); + return 3; + } + + return 1; + } + else if ( remaining >= 1 && attachment_action == "1_attachments" ) + { + a = array::random( attachments ); + self BotClassAddAttachment( weaponclass, weapon, a, "secondaryattachment1" ); + return 1; + } + + return 0; +} + +function build_item_list() +{ + items = []; + + for( i = 0; i < STATS_TABLE_MAX_ITEMS; i++ ) + { + row = tableLookupRowNum( level.statsTableID, STATS_TABLE_COL_NUMBERING, i ); + + if ( row > -1 ) + { + slot = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_SLOT ); + + if ( slot == "" ) + { + continue; + } + + number = Int( tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_NUMBERING ) ); +/* + if ( SessionModeIsPrivate() && self IsItemLocked( number ) ) + { + continue; + } +*/ + allocation = Int( tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_ALLOCATION ) ); + + if ( allocation < 0 ) + { + continue; + } + + name = tableLookupColumnForRow( level.statsTableID, row, STATS_TABLE_COL_NAME ); +/* + if ( item_is_banned( slot, name ) ) + { + continue; + } +*/ + if ( !isdefined( items[slot] ) ) + { + items[slot] = []; + } + + items[ slot ][ items[slot].size ] = name; + } + } + + return items; +} + + +function build_claimed_list( items ) +{ + claimed = []; + keys = GetArrayKeys( items ); + + foreach( key in keys ) + { + claimed[ key ] = 0; + } + + return claimed; +} \ No newline at end of file