From d9787cda5acb30ad2952b85978e467407d015788 Mon Sep 17 00:00:00 2001 From: joseupp Date: Mon, 13 Mar 2023 09:39:59 +0100 Subject: [PATCH] Enable bots in ranked matches --- data/scripts/mp/bots/_bot.gsc | Bin 0 -> 14640 bytes data/scripts/mp/bots/_bot.gsc_raw | 906 ++++++++++++++++++++++ data/scripts/mp/bots/_bot_loadout.gsc | Bin 0 -> 15500 bytes data/scripts/mp/bots/_bot_loadout.gsc_raw | 854 ++++++++++++++++++++ 4 files changed, 1760 insertions(+) create mode 100644 data/scripts/mp/bots/_bot.gsc create mode 100644 data/scripts/mp/bots/_bot.gsc_raw create mode 100644 data/scripts/mp/bots/_bot_loadout.gsc create mode 100644 data/scripts/mp/bots/_bot_loadout.gsc_raw diff --git a/data/scripts/mp/bots/_bot.gsc b/data/scripts/mp/bots/_bot.gsc new file mode 100644 index 0000000000000000000000000000000000000000..a13be6c09bb219cef5652d9160a0701be4bbbf4b GIT binary patch literal 14640 zcma)C34Bw<)}NcEr7aLDi|`QPX5aU%O>PpF(n9G%f!4MZ(llvlA#GxlmTuHO7DX0W zL_kEUD4X~Ik;Mg3T<|G8sECS)%2PxIMHC+oc?jP*Nv@QB_I+P|o!#c6@*Mt6M}h6{~AF^Ey(GKggi!~zf{5@ zA#YuEFdWi)1HKA29}3sid&7UQLRFr+3ZK?fSLbPRM?rU<*W>q>d%UwkJ@98$`~9G` z!qcNfjVD~yL*Y74?W`VHp{7u{qNWFCeYo1+18a7LCm5*hskJe#)*Z`q?`?Si2Fwlr zdt|u1ftqs9pYx%dJ6z{!sHh8h{GmTpfg>iXPQh)C7CvRr)=%+z~B3 zid0qjtG$6B^x^*@;+|1oTl-%mZ+8t=Nv-ey_Y&Q{if~02tJZypyC|&!OIBSwqsNxk zRD0_Jvtb3h!}WD_6?aU*+iGA61^u4diW+$Po}RmXf8Bo{xjIj<=T}$fEr&(jV?*np z@^HofGGpDl9u=#p4uz`yJZlUPXVg_x5DyZ36&@cMQxyn=D#rLdp^$rg zZKkKDg4`YqH+G~8^alP|;9aP(_s71OE}YvhqYLr&BEy;2R8hw+q%JZsi-pTI*tG$X zY1Tz9#JR{{t7wc$_% zHU>##i;HYaUf6+fv+=O(&8go4;m8$V&$FBIG{_gnS9< z4MJxH4&JqW1=QVt|7 zq=AtB4c_1xNQWSGKzaa@7t%OLdPtF9Uz$gs(WR!HVa#roshEH9)I&>`G%u!WjT;w6WLsOu8C9Bu5 z9n4T!M%DcV>H7sjr2k^etDL@h!O%sL9j4qusprU%-*zJ1t)8H~sOR0)nYu)y_eipX zzZY*sda2z~WJ=gp8811*F!f_wF7)nfKzhBV)l!+hAhRU(0bS)3f$=G0{u-qJViQ*- z^lsT5FWGN4`17j2(fV_d-k~W8WBwIk3fN!u^49`+$UO~|9=DiT~%{`wfi8&y2|1;y7i zDJ#mYl+_f{2X=2kx?h(iVLM6Os;8}~a zTmkLC&I!oBi|_N~TQajTkH0t6RPc$9_Pjnl@uY^C^eX2TTU)&a`tyqY zM<$W1a*Tw0NYr5_MGhk+4i=(JNY;`ctp&$r6F(%&G~UMXsXOP`d>8Cy)lyAkW%M0w zch1x|OP3zoI4@-XR*^tHcVK>_EFH%1qVZWhVXA(3XC>-*+Qg%kuD2v4+i6w?tMu(J z_5B;t@0G$jOAFA%&}j5aW$w8zSM!mc>P(@7MxQkk+9}a^9R&~nCw-qQPQF3apNum3 zbCAAHG5S8Fr#L3+8lc8AX&*_6QdVXcw57B4_SW<0IV!iD1mtoen zucwJ|MrCfWVk+Anq#G1FC1jSmH6uq}&bH?nL$aD;Hi@y`yIo!%);q>k39UCX&`)o` zVBXH^O|T?eNWNf7Q=?QXobQON*Nv(ay16k>jxzY`OHVhK+Qiq+NXF6=hhKRrg|=o$ zre!iwF7gMeaJ-eSt@`pJ?AMGEzgF6I>N9b4K3X&lmX~Oixj%59-$#m? zYUcO%Tq1K+1&i0__aVBBs%M0gza;k;|O|B@3lb5`*P`f+UUa(>^bgKdi#UfMPtZ_+yF za(`-pc5(|=Wnqj$~0P)s8j-CC+

5i+o(n5ak&1`$)1(8dqAD)*n~5 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1dce334526baf75b8b04aa93e58387a34dae01fe GIT binary patch literal 15500 zcmZWv34D~*wZGpi*$5G3ky@15_kGQ*VX}wJl1vs7NJv74ERv9sgcy=QalumH3AU)T zB2Wd9ih>A&lp=MZ77?)c6n7s+i)&G^MXTif@8tWEA4XeA2sy>VbzVYLdO~Ix386Usn=2*cVU#Z;zaZUY6J(%rnIJ>Ju8 z>~3pXvM^?BTC${RS;Gi1zHCX;;zi?&I^)Jg?HwK6JxdlgE$TKlBoU+4A+w+hT{Pf7 znNtubi-a3W!zG0U4Z%ntKT=i6G0RFSDkH&$ipoGZKM>6y$Ih*aR*V-dtO^DLRh1D= z6ht_$xFGtoHbx7Ak-XAKI9SWos0bDPOqlYn>5Vc90~M8pCDDT9ior;@C>jVCUBj#{ zh*ktDOM=%h^8#goypqb%ZUa9bB3OaEysBuRaC zsK)a`foN$(C=lk_3Sve{O;fVCAW%IzKFSD|6cvvyIAv4>BXh1kGNkvIybW)=Q zRiU!cURhx}l4MOOj0TE|q6OhV{^)8K2P&aEW8GJl5NgpdnOJQ9)HZsY0Nn0R~&zkdH2s`BM5Qc;l`nZpT=GzspU+(7hNX2qPym`@s!%f;%CZ3gucfl{ZfimOK%p)#8(nZ|k- z2ST|8qm5IcaG)dzjppPq+CbQ4OCk$l_oJ(kXtpAh$CXplED0Bugy~wd{uND>RRt@O z>5$pMmC_c<$|^<|ZPYs!)Y#0ULagOPg0-QtlE5f?#JbC?U>y~Ck+RV)So?-U&%M_7 zLFJOCRul-9jP5rbd$ep+O)>0{d355OJr{%vqD7;wY5dkTh30Ehbc+6nP^^a zB$S)XDUZVV!q*xaYp;=LB@{Wy0^z($ux-@DF$1hD!Li)X(bW{|TH2GqF%_KWOPj~I zV*@cK4a$;)W8_VpU8J?Cb78Ehr^$Hx!lpQyF*bK~c6Kc`Hudy0HMeyxT-;OHw4h_5 zp}D)4ELqso-rTW}(6EA78^%PsiRBu~d=0g@Yw<#EmB;Fsf6lUy+(@{pA`g)Sj*1eT zfbf!4uoIkWm^HgmV@+7cc594jPHLorx=I8l*EBP_^J8_ep=?l<9QkldY-sInCXXTj z`xX9T_$$VrD~f-KpI*tde)B_+Dq>Or|N{M2>dBKV_NcD-Cc%^Z@ zqnfSr|tRC3u7#- zV__u=gDlKvp_hd=7V20iW8n-IPGMmt3k59v@#@HYFSGDV77nrSJr=&f!jmj~iG@d4 zxSxf)S-72rn_0M#h3i;&Hwy<@*u%mO7RFdu$HGb$23eTTLN5z#EYz`3#=;paoWjCP z77AGS<4^4TS@QLEX)UdS6pdqtO>WUs!L@0>r^vN6~0M2t4ma$-+7oc+O@qcRplg9Azi+z zx|%p^XQxfLDWBwY#q#>a&yuEc$2-HfY#>F!_y>nR`;_!$$ZwjpteZIWga3VZ@y(>( zV|)Hs@jWEwR(`VQ-94nc&X9BF)XT(O9dPyue;}>S=(GQx^LNslC3?`jV(C4pguLr< z`@5Z_CSE;Ip8NSXxJePRDJQFFz?~&3GFkiXh0N{NOw*56(!!OON+?l-a#?A`;`uw@cHV zPkFOMi?z+ZlPH%M?)9MG(fae1X`!{Q<&J}Er@WmZ7Ucd}b=4!7bBB4KD^+`+xJsL< zYSaG1w2wI=N^QI(7I;pn7uixIYfZi0)D=fWg8Zv4v0m7<)|DmtO7<4oKdVbsD;z|9 zV#V|Iblq0E1mwIf#$<`E=u#~$zIs0sPxGR+;x5Yt2bsHV*`f(lZjp^|tu_A1HRR#7 z(sfGrWVM_V@xtppB))LoBK^vYX%A94CFU~Q6B`7-Fr;4K<=>}PP=24dp?QfoS2-Q4 zb<{aAcGbgc-eVx;T)mr>S)!$yG|6U7dga&RjAeu734KpFGL;ysvA?ft!AqGB-c8#P z>KJFIIZJe2#uTcZMDg$cSni_yXEaRx8QBZ6NlH?*MzKpwyi=u2$7l90)jo*e=J^N}GRmlE^S{dn=Wpzf2X+OZA>wQR(0ks`@l@kauwCdM{)#_`@~!Af|eUJC(Ws0n`xyy zwYp84nX`pa%TtxDCPC9@-UkdRrDtSLeX4esSYUoG=WVsH=?O!YXs4U%q}LY6H}Ne5&KO@01xl1*A( z_np?Iszf@{`B(2FuIiHfiRVja8Y5PIW37hRb)uspZD`NM-ubo01&Wl4bPtjCdk*-V zwV8+8ez#txRr6*;<8pMOU52h0-3z%XVr81niAcFlv_kDDqWcn%o%WMp+;fvO-bd+} z^#RB4+nLM9%^75;B12C1W|sY^lQ`b?&^`01#R=Fh?KJC*Lj7;8se@-^UukQge{j7t zph^6W_=WCc+Tn+Zvb!#^?*?~u%4dQm5@>a1KApOoM0;0!_TyV-vh)kv2UwqHR5kIc z-~2cKI`bKcp!%?MsYWoMb(fh0@PgCz&!}kMDH^7HtxIKPihg6h(aW>$Gc_uBeSGx} z|7^0=&TG-Bnf`B$8^nBNmszLgc{gjAATjQ<5&sj0u!3)&C$3b}wb^MFXrJ{@Ri+T9 zo*(~{8!OVbUCJ}h&0go)J)G`u)LZ(vc}#?D%r^3@+vTJ~scMLE{zqwNoW0T%AN4=? zdf@q+eI23PIof;NAKpmo7RlC&rl4$x?hZduHF~ZHmCsSW*)vHbS&`pGar!%L;Hfun z`|uo5TjCdI?YxH+XNfjtXF17SYimn5zOZF&&rB+}UCPXp`LaZOIkV_39nl-NPVJ}s zsY(GkY?}d1b7nJx&wG`|5kEQ4`c)k*hv#W7R%eN{Y7%>2c31-~Z7!EoF5~>kUA_Y< zrteh|m8HLbyLrlN;nHuZm(&fYGe}pKNZ_5JAPby@iuxjN;ah2HD)*Ry>7afSUQq*^ zS;mcL*ef6**PZab10E)OrwnIi!ZUK?S!pMMeem~<3L+m;y8U|;q}gn581--8$#G)X zZkEtK=x>vXS3CFViT_K)kx=5?ZqLGwd?X)&&b+5tk2?sQV};YhoqswPp>EL2`SHmz zreSXO4l(*&KGV}HCy}dWzSgCjsL3QMSVz8`sY`d>>3&zP!fs(y^;dL^`MB+{MX7mF zL1NrGg(S~tNUO)44YbryVS)zJ>W)A$L z>^Ti>^R$$7bM2Mbh?BZJ&Yz0)g5~##o!Nq0&pP^LRC^CNUp}Ife~hjK6K^FQyPbSwEjaTV%H*u z%8RX0RB1Ds_^QmDY}AL9m!FrVI1UIlzUw8+ucrm zdfhgE%pxF<%5NaG8lhp8Da}nhQ?k>lzsasOPL`zx?r`6$$hxXCrnTNHal%tBt)Xj9 zf1g-|@tL=7Jxvg+CiJVw+LzF;mVRYrZ^ui-8}uji%s$z()4|uSH952S@%>7|^viyw ze+;9%>fyJ2=(?mbTj&@+Q4>U$N)@L!4;Xk2UFP#LUTmsyntHBOKvG;SsKqx5oba*D z#U5V=t@pf&PryHu#RelRac;jHr>gb5_`EGR{I&eHoP_(EH!^ENzECiWGS0tTPxMPHACy1w$}RGr zL|LK@Dg!RWou0wMsenptNX-M>>0aQy2;GIAG;?v@K5?x1nX@lG)hYUqyLXc{t;7D2 z^mi6go{3fl%|w!GAeGDdl*Vk&sYHA^3-|8teK%qJttQg{gOMO+q#p438%w{YV`987 zIrqa8aO2BVF-1!bpZkz_osK=r-7%$Rjx0^up((|9{-9ZwP0zO7>Y)Chk;YqZJ6}6_ z()T^?4=E1B1n`j8pStun+E#|1*Z-(}hHXIes49(o=6UU8&^H_L{{4z8a)S7>?aP;c zOXU_@5bek^93$S-R{MU&%+r)V&3>mMCVnV~M9yT>Gl7KcUijd4^}(xe&~l}BgN<4A zp^A>{?>XM3bX*5q_lYNos24R1P#OAb)$niMSa!}%TD=X@+vYz>mPfP+Ka8;m&2{y2 zP(Q9B=B6LCFHre4(kv1DLo8*oZw{r`E11>-j3C{^Gr?;7As1rgP>aU*rs{oKzF(dt z+GXPDhoLcX1>=IfmcnI5xpD?^|4}Con!#v4R`bH}Q-~y&>$q|=Vy0RRud3I}n3Q&Y zp;AsgX{3cKcVRq;fWk*aO!b2@ffD6SrCM_nSMD~(t(};|Ry(8c$i#XQT$yvw<)~Q( z9>(NtR`1eZmx%P6RTI3-QlGD2fR;giu*Fy{CJuPEmP*NkLvs_kn-d0Nm77f7Rgl?f zX5=T#FS=}r`d9R*-}biO(m~5=HB96U@g*^7zv`v&B3ECiBRK=^Dj$*U)=$Q5a5Fr` zBLA?r7#fIE$=T(F<-n0H|Lx^OY(m!?_lMR)3V1felN^6KbP8T(!C@QZ5m9pdZ2$M( zQaR$wza8v{)=5Z<3ODXXd-gruo4l&0-xqXIOYT=dN zfxG!eNs*Pt*6=ueC(H{J!j|vUq+baSXxDP}qjC~osml@__cLvG=**V;um=MkVINk2 zy1jkSLA)(~kb`LF$JRe&3lT zQp?i{`@|WFGp<)P1hH8M{0w)#?R2F1XntbAlLGzCnBAzx+Tw!Bm2Xg{$f<|u^$MEb z&&e=9k_~G{9LAM@trytl>*)=NSBrA-+Y;z|B-GB8Ka|5;^ptmx>YRr0K4Ex4R*6V{ zj|+C8tMa8)gxp&^Gsw$1=}xVh@l;8^(lCZ+ba&Zt3J!d#{@n29uu%dZb-TfMLPA=5 zy(wOL|D8dudQuw@#V>!rg!4ACO}WpsK}@tJcZ%!H;lq~f9Fo&%#$BWiIg9#x#gD3H zkhN-|dA*W}RY~@_nAVHVwXPq1#PmnI)AZ)>RO3@J8vCo|B$RI9wH#ATRn8zsM10N5 zIgI`r=W#LPdCX=oV(hLv+;>~v9H#SSO4T~z=I+67nBkOgm*w`cRU@paxv=+T0%z3y z)}g7GuW9rBpU)tLZMQw}^uikme9)YJ#S7 z9g;t)%EcXR89oUb#IMtCCo+spU=i;nSo zL=*#J30bP)X|I}@kj?X)pI`A?-$v{-PueL$9O1mxdDgUjc!7e{-sGigc%9L1r|^J_ zX0~u1&)y*wGfh zzJbQ$I~_cCi}I{{0B4I(p_4Iz*~a14-#ceW--AEM&@;|8UM31X>!r8)rHQ;E7w?F4 zf}#f9KFQseRXX`de11161y^sR@zW{H9(v2DP;!@=vLZ8T?JXT?d?(d*?aaXZ7km1_ra8@yach-+t#U##cl)*4rt zcyo{m(B3H(sIEvFJ#_BB(>P@^A2e9-n2JTXbo} zVvB}fyHD<1Y@pHBF0rs{zHfn(wk`86S0XMhywUxMp06nJGxd|a&-sN#msH*kTCSHI z@*{Vn*=KseNZW|||EZZqzObD@e_R~*t?_x!6yI-bVS|mpKdLr~1hb!VAa2t(XHQvd z@>1DueLzlQD~<17>4UN<&9}IhDW_N_DS2jkYo>meXUAQ&f!l{6FVWd*#SzZVK6Wu7 zq2j1wzLK=h*TaJ?suuIT$5am)W3X1T^paSpYminX=0p@?9d_Guhjg*Q3k^iZ$dE4= z4$Sx6-b&Z@J$Hcz@|dTf;pLo^<)3?yRg7qJ>gvrg{)@kRjjpTNV|8WFY(eWC?&rn# zChoWh*YFk5$j_$8^d$BhdA@}IfLlz&0q~89xB$`FAH(X|G|yLh;La1oRM=)L`)m$D zP5+YKi9l9-?e2J@mckc7j+;+#`LMD1wfwe_!-Hso84J@V=*5;$B+t)pPgH zW~`|LulshH(gr`#r#tNWlg^YDfpLd4wK7W_^A?*@$Z1`QWR*pNe8U#?bevRQ=!MpR z?R%`f(w-p~n%gX20PhnAI*KKc_Voa=vsm}#S#*LscFB18XQizoD5)#1e<5naWc zoPXYd7lXU9n|%SuAZG74?av5>6%%q#xo_|g+#|X1sr>ik2pVzwGx0q)OAczQnf1yP z$#Fk##jv>Dn?-h+g!v|KmzwV9uzafW6^u-0OjRQSvcLsdxpDpI6SSUzKYh^L5~llt zh#`Jfm$6W*o-nU3=V1d`gun|i7uPSnPYiej-m9LBX84>Kw+<%^sm%j!oh-F{y-MOO zLvITdsS-p8iQ8g-AJ-qwDYqdzU-b{UK`V$*{SvnP+HV#8%aDSoTo86jk0YP6RlZG= zCGt5Kd$p3<^L-Xe(_U$*mg4v1sLTfpO$xd$8*CZD18PSN)dfWUH2pQm*>ky;?>Hz?j}E@FL%s@EWctx1Lk2G$P@(ZndW^#~FPqr#FA~2^BLv$dE5+P`oRr_M?^y zS0GDAll-`QasB-uVXCSB2-r#*+tHshJKg?IFS4TaGKP#5XFtWrs2)(~bghQWQ704I zAx&#SCaL_N^mgwk3UcEKNoE`Q@u&oP)#sG_(Z85#e#U(kM*e{6KhUcxtc%dBAEfJd zqw;SFy~6od5Bo{n*SK+|;QmSDw}7qAK-U2`Q}-HgTD8uY9{8sh_Jem+nwR4Gy;=Ew z4in$yZSdKYNKF-SdZkyUS`%?-UrT=?{)GQX#9z3hCGwo{Hgh8XiATN4bxC(lW1Z-UoPn4nb1;GF8bjY-==sYK?e@h@%0&JQ5jlOnbaL$=&MsW*>Xq{q zt)`Uv_eHz(hjX&XtLl6?zo=B2;!U&vS*4Sa4(z)|q)S^dEN#cUqEx2jF4_Qd|d=_KyKh*4f!r#nwV?ThZ*T>2fXPS?_8 zdy^o0lvX|UEX}?P(>!gYb+5~=5qjq$AAp=_19HO1Am!A(bHh{@)s0HCp#BL1Uh~X& zV~pMCz9eQ^<&q~|JkwIYQ_u7BRa-n>oTj`G?oKW5ix7KjR_5^KBN?L~C3qimb(vRK zcAEL6XCy**yA%<*j?6mgmY5F@SNVj|K3QsR&(JyOFU?yE-`*f&(D$?7-Fe$9 znS^}1f95TeN5|7|-TSy`65g-z?o4?+@cRC5)n|R+by@6BaB?#{KKW?d4TM}i`Jba4 zuUQ^m)HWG)4^4cM8cjt5MVvd)2 z#rfUpsd#I}w+NI+$MUpi`j#trS)%VKltAcW?_D36c!KAJQ2Qsd2-uXd<_<2kk;pS%}5>Q^Zb zZi8I@@yeP$L@D%6$F=vz|Gf3lbP*vm??QQW%zxM~JuDIvLSuAJPS8;O%asyBXx@eL zXup4XMeO>gdIWjNInXq&=gKdRE1z;5Av8}tQZAHagQo9Sl*bT;R|z_~y?a5^bAjz&b^$c? z+oST(+2rwwuD^DCHK6JHGgtq&vGiXi^c=(>@ zanMU{8Ec2id<0(dTz(wah6py!wSm2tm_T#mp!FM*XgaR#pdW*cC6B!rJ|Vfz%5n4| z&|~^0kjUUK=(`8E-epPt1N$jo44S@AaOEdJ(|3qExBLpq1>b_E?+;vg$~C@*k?BCw zcY={}Mshc3&W2H6vJo`rhv*#ljib+uqc0}WbPXo`k`NRAXfA!E4}s*)ar6tI$9xk{ z@-1iy{2#}ocA5d2=EK=}Nc^D3{0bx4HctM{ar89!NzSj)dCUTxY&Z9UPTt!r=teSa z8AnG!b9N1V;?=?Du25dvW{W7<4>OY8pp*4l0NqH|g(1+%eO??_u7Pjn%Bc=#gI*22 zAM@di;_Zdnf&%o}D%uhWxd!8^PO)^V9Hc#sB20_!fl9PW7 zG>xlAXrAQMIQf5%lULvcZ%m#q34`X=k+$CpI(dz@fKKl3M3OvRcYFgz%5d&*GCU%X z`N3NO-dI08Nga5c3vW}Z-l=R_-R&VHE!*^U(fPq0e_Jle=+d2 ztV|j3l4bEf={fYa}pDK0>KOut>GxC}V`rV~>n)6w4Dpy~I5wCp7AVe~sNiW9_d^xjV8 z>a>K!5LeM}LugqV;uHFP2*oD>=dt*7;Pg8Y$`=7I1)tWRh4|ue(DWMzTDB52{Z@eD z`G_0ncK}qb2Y3zs=(|3p_XFRAKl*M#=^+XB0Q0ABN3?ES3>{%{B1MeO%54T7#~;ma z(RI6^f?nZox*XrVfJfK(f)f9zzt><*z^6GZ$|vAwg73wj34EHXrhRt+_u`M{1ZkT( z$W2i}Z@{DZNh-6M@)T$fG#%eY;50u=@k79A-Y3x(`i!aJJJ_-}fYUq_mAeA`{{edQ BP&@zt literal 0 HcmV?d00001 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