From db823277b2b068afa253671fba7ecc613936f824 Mon Sep 17 00:00:00 2001 From: JezuzLizard Date: Sun, 20 Nov 2022 21:18:30 -0800 Subject: [PATCH] Initiali bot manager. --- scripts/sp/T4ZM_zbots_main.gsc | 423 +++++++++ scripts/sp/bots/_bot_utility.gsc | 1385 ++++++++++++++++++++++++++++++ 2 files changed, 1808 insertions(+) create mode 100644 scripts/sp/T4ZM_zbots_main.gsc create mode 100644 scripts/sp/bots/_bot_utility.gsc diff --git a/scripts/sp/T4ZM_zbots_main.gsc b/scripts/sp/T4ZM_zbots_main.gsc new file mode 100644 index 0000000..2891e7e --- /dev/null +++ b/scripts/sp/T4ZM_zbots_main.gsc @@ -0,0 +1,423 @@ +#include common_scripts\utility; +#include maps\_utility; +#include scripts\sp\bots\_bot_utility; + +/* + Initiates the whole bot scripts. +*/ +init() +{ + level.bw_VERSION = "2.1.0"; + + if ( getDvar( "bots_main" ) == "" ) + setDvar( "bots_main", true ); + + if ( !getDvarInt( "bots_main" ) ) + return; + + thread load_waypoints(); + thread hook_callbacks(); + + if ( getDvar( "bots_main_GUIDs" ) == "" ) + setDvar( "bots_main_GUIDs", "" ); //guids of players who will be given host powers, comma seperated + + if ( getDvar( "bots_main_firstIsHost" ) == "" ) + setDvar( "bots_main_firstIsHost", true ); //first player to connect is a host + + if ( getDvar( "bots_main_waitForHostTime" ) == "" ) + setDvar( "bots_main_waitForHostTime", 10.0 ); //how long to wait to wait for the host player + + if ( getDvar( "bots_main_kickBotsAtEnd" ) == "" ) + setDvar( "bots_main_kickBotsAtEnd", false ); //kicks the bots at game end + + if ( getDvar( "bots_manage_add" ) == "" ) + setDvar( "bots_manage_add", 0 ); //amount of bots to add to the game + + if ( getDvar( "bots_manage_fill" ) == "" ) + setDvar( "bots_manage_fill", 0 ); //amount of bots to maintain + + if ( getDvar( "bots_manage_fill_mode" ) == "" ) + setDvar( "bots_manage_fill_mode", 0 ); //fill mode, 0 adds everyone, 1 just bots, 2 maintains at maps, 3 is 2 with 1 + + if ( getDvar( "bots_manage_fill_kick" ) == "" ) + setDvar( "bots_manage_fill_kick", false ); //kick bots if too many + + if ( getDvar( "bots_skill" ) == "" ) + setDvar( "bots_skill", 0 ); //0 is random, 1 is easy 7 is hard, 8 is custom, 9 is completely random + + if ( getDvar( "bots_skill_hard" ) == "" ) + setDvar( "bots_skill_hard", 0 ); //amount of hard bots on axis team + + if ( getDvar( "bots_skill_med" ) == "" ) + setDvar( "bots_skill_med", 0 ); + + if ( getDvar( "bots_loadout_rank" ) == "" ) // what rank the bots should be around, -1 is around the players, 0 is all random + setDvar( "bots_loadout_rank", -1 ); + + if ( getDvar( "bots_loadout_prestige" ) == "" ) // what pretige the bots will be, -1 is the players, -2 is random + setDvar( "bots_loadout_prestige", -1 ); + + if ( getDvar( "bots_play_move" ) == "" ) //bots move + setDvar( "bots_play_move", true ); + + if ( getDvar( "bots_play_knife" ) == "" ) //bots knife + setDvar( "bots_play_knife", true ); + + if ( getDvar( "bots_play_fire" ) == "" ) //bots fire + setDvar( "bots_play_fire", true ); + + if ( getDvar( "bots_play_nade" ) == "" ) //bots grenade + setDvar( "bots_play_nade", true ); + + if ( getDvar( "bots_play_ads" ) == "" ) //bot ads + setDvar( "bots_play_ads", true ); + + if ( getDvar( "bots_play_aim" ) == "" ) + setDvar( "bots_play_aim", true ); + + if ( !isDefined( game["botWarfare"] ) ) + game["botWarfare"] = true; + + level.bots_minSprintDistance = 315; + level.bots_minSprintDistance *= level.bots_minSprintDistance; + level.bots_minGrenadeDistance = 256; + level.bots_minGrenadeDistance *= level.bots_minGrenadeDistance; + level.bots_maxGrenadeDistance = 1024; + level.bots_maxGrenadeDistance *= level.bots_maxGrenadeDistance; + level.bots_maxKnifeDistance = 80; + level.bots_maxKnifeDistance *= level.bots_maxKnifeDistance; + level.bots_goalDistance = 27.5; + level.bots_goalDistance *= level.bots_goalDistance; + level.bots_noADSDistance = 200; + level.bots_noADSDistance *= level.bots_noADSDistance; + level.bots_maxShotgunDistance = 500; + level.bots_maxShotgunDistance *= level.bots_maxShotgunDistance; + + level.players = []; + level.bots = []; + + level.bots_fullautoguns = []; + level.bots_fullautoguns["thompson"] = true; + level.bots_fullautoguns["mp40"] = true; + level.bots_fullautoguns["type100smg"] = true; + level.bots_fullautoguns["ppsh"] = true; + level.bots_fullautoguns["stg44"] = true; + level.bots_fullautoguns["30cal"] = true; + level.bots_fullautoguns["mg42"] = true; + level.bots_fullautoguns["dp28"] = true; + level.bots_fullautoguns["bar"] = true; + level.bots_fullautoguns["fg42"] = true; + level.bots_fullautoguns["type99lmg"] = true; + + level thread onPlayerConnect(); + level thread handleBots(); +} + +/* + Starts the threads for bots. +*/ +handleBots() +{ + level thread diffBots(); + level addBots(); + + while ( !level.intermission ) + wait 0.05; + + setDvar( "bots_manage_add", getBotArray().size ); + + if ( !getDvarInt( "bots_main_kickBotsAtEnd" ) ) + return; + + bots = getBotArray(); + + for ( i = 0; i < bots.size; i++ ) + { + bots[i] RemoveTestClient(); + } +} + +/* + The hook callback for when any player becomes damaged. +*/ +onPlayerDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ) +{ + if ( self is_bot() ) + { + //self maps\mp\bots\_bot_internal::onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ); + //self maps\mp\bots\_bot_script::onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ); + } + + self [[level.prevCallbackPlayerDamage]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ); +} + +/* + Starts the callbacks. +*/ +hook_callbacks() +{ + wait 0.05; + level.prevCallbackPlayerDamage = level.callbackPlayerDamage; + level.callbackPlayerDamage = ::onPlayerDamage; +} + +/* + Thread when any player connects. Starts the threads needed. +*/ +onPlayerConnect() +{ + for ( ;; ) + { + level waittill( "connected", player ); + + player thread connected(); + } +} + +/* + When a bot disconnects. +*/ +onDisconnectAll() +{ + self waittill( "disconnect" ); + + level.players = array_remove( level.players, self ); +} + +/* + When a bot disconnects. +*/ +onDisconnect() +{ + self waittill( "disconnect" ); + + level.bots = array_remove( level.bots, self ); +} + +/* + Called when a player connects. +*/ +connected() +{ + self endon( "disconnect" ); + + if ( !isDefined( self.pers["bot_host"] ) ) + self thread doHostCheck(); + + level.players[level.players.size] = self; + self thread onDisconnectAll(); + + if ( !self is_bot() ) + return; + + if ( !isDefined( self.pers["isBot"] ) ) + { + // fast restart... + self.pers["isBot"] = true; + } + + if ( !isDefined( self.pers["isBotWarfare"] ) ) + { + self.pers["isBotWarfare"] = true; + self thread added(); + } + + //self thread maps\mp\bots\_bot_internal::connected(); + //self thread maps\mp\bots\_bot_script::connected(); + + level.bots[level.bots.size] = self; + self thread onDisconnect(); + + level notify( "bot_connected", self ); + + self thread watchBotDebugEvent(); +} + +/* + DEBUG +*/ +watchBotDebugEvent() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + self waittill( "bot_event", msg, str, b, c, d, e, f, g ); + + if ( msg == "debug" && GetDvarInt( "bots_main_debug" ) ) + { + PrintConsole( "Bot Warfare debug: " + self.name + ": " + str + "\n" ); + } + } +} + +/* + When a bot gets added into the game. +*/ +added() +{ + self endon( "disconnect" ); + + //self thread maps\mp\bots\_bot_internal::added(); + //self thread maps\mp\bots\_bot_script::added(); +} + +/* + Adds a bot to the game. +*/ +add_bot() +{ + bot = addtestclient(); + + if ( isdefined( bot ) ) + { + bot.pers["isBot"] = true; + bot.pers["isBotWarfare"] = true; + bot thread added(); + } +} + +/* + A server thread for monitoring all bot's difficulty levels for custom server settings. +*/ +diffBots_loop() +{ + var_hard = getDVarInt( "bots_skill_hard" ); + var_med = getDVarInt( "bots_skill_med" ); + var_skill = getDvarInt( "bots_skill" ); + + hard = 0; + med = 0; + + if ( var_skill == 8 ) + { + playercount = level.players.size; + + for ( i = 0; i < playercount; i++ ) + { + player = level.players[i]; + + if ( !isDefined( player.pers["team"] ) ) + continue; + + if ( !player is_bot() ) + continue; + + if ( hard < var_hard ) + { + hard++; + player.pers["bots"]["skill"]["base"] = 7; + } + else if ( med < var_med ) + { + med++; + player.pers["bots"]["skill"]["base"] = 4; + } + else + player.pers["bots"]["skill"]["base"] = 1; + } + } + else if ( var_skill != 0 && var_skill != 9 ) + { + playercount = level.players.size; + + for ( i = 0; i < playercount; i++ ) + { + player = level.players[i]; + + if ( !player is_bot() ) + continue; + + player.pers["bots"]["skill"]["base"] = var_skill; + } + } +} + +/* + A server thread for monitoring all bot's difficulty levels for custom server settings. +*/ +diffBots() +{ + for ( ;; ) + { + wait 1.5; + + diffBots_loop(); + } +} + +/* + A server thread for monitoring all bot's in game. Will add and kick bots according to server settings. +*/ +addBots_loop() +{ + botsToAdd = GetDvarInt( "bots_manage_add" ); + + if ( botsToAdd > 0 ) + { + SetDvar( "bots_manage_add", 0 ); + + if ( botsToAdd > 4 ) + botsToAdd = 4; + + for ( ; botsToAdd > 0; botsToAdd-- ) + { + level add_bot(); + wait 0.25; + } + } + + fillMode = getDVarInt( "bots_manage_fill_mode" ); + + if ( fillMode == 2 || fillMode == 3 ) + setDvar( "bots_manage_fill", getGoodMapAmount() ); + + fillAmount = getDvarInt( "bots_manage_fill" ); + + players = 0; + bots = 0; + + playercount = level.players.size; + + for ( i = 0; i < playercount; i++ ) + { + player = level.players[i]; + + if ( player is_bot() ) + bots++; + else + players++; + } + + amount = bots; + + if ( fillMode == 0 || fillMode == 2 ) + amount += players; + + if ( amount < fillAmount ) + setDvar( "bots_manage_add", 1 ); + else if ( amount > fillAmount && getDvarInt( "bots_manage_fill_kick" ) ) + { + tempBot = PickRandom( getBotArray() ); + + if ( isDefined( tempBot ) ) + tempBot RemoveTestClient(); + } +} + +/* + A server thread for monitoring all bot's in game. Will add and kick bots according to server settings. +*/ +addBots() +{ + level endon( "game_ended" ); + + bot_wait_for_host(); + + for ( ;; ) + { + wait 1.5; + + addBots_loop(); + } +} diff --git a/scripts/sp/bots/_bot_utility.gsc b/scripts/sp/bots/_bot_utility.gsc new file mode 100644 index 0000000..9238d30 --- /dev/null +++ b/scripts/sp/bots/_bot_utility.gsc @@ -0,0 +1,1385 @@ +#include common_scripts\utility; +#include maps\_utility; + +/* + Returns if player is the host +*/ +is_host() +{ + return ( isDefined( self.pers["bot_host"] ) && self.pers["bot_host"] ); +} + +/* + Setups the host variable on the player +*/ +doHostCheck() +{ + self.pers["bot_host"] = false; + + if ( self is_bot() ) + return; + + result = false; + + if ( getDvar( "bots_main_firstIsHost" ) != "0" ) + { + PrintConsole( "WARNING: bots_main_firstIsHost is enabled\n" ); + + if ( getDvar( "bots_main_firstIsHost" ) == "1" ) + { + setDvar( "bots_main_firstIsHost", self getguid() ); + } + + if ( getDvar( "bots_main_firstIsHost" ) == self getguid() + "" ) + result = true; + } + + DvarGUID = getDvar( "bots_main_GUIDs" ); + + if ( DvarGUID != "" ) + { + guids = strtok( DvarGUID, "," ); + + for ( i = 0; i < guids.size; i++ ) + { + if ( self getguid() + "" == guids[i] ) + result = true; + } + } + + if ( !result ) + return; + + self.pers["bot_host"] = true; +} + +/* + Returns if the player is a bot. +*/ +is_bot() +{ + return self isBot(); +} + +/* + Set the bot's stance +*/ +BotSetStance( stance ) +{ + switch ( stance ) + { + case "stand": + //self maps\mp\bots\_bot_internal::stand(); + break; + + case "crouch": + //self maps\mp\bots\_bot_internal::crouch(); + break; + + case "prone": + //self maps\mp\bots\_bot_internal::prone(); + break; + } +} + +/* + Bot changes to the weap +*/ +BotChangeToWeapon( weap ) +{ + //self maps\mp\bots\_bot_internal::changeToWeap( weap ); +} + +/* + Bot presses the button for time. +*/ +BotPressAttack( time ) +{ + //self maps\mp\bots\_bot_internal::pressFire( time ); +} + +/* + Bot presses the ads button for time. +*/ +BotPressADS( time ) +{ + //self maps\mp\bots\_bot_internal::pressADS( time ); +} + +/* + Bot presses the use button for time. +*/ +BotPressUse( time ) +{ + //self maps\mp\bots\_bot_internal::use( time ); +} + +/* + Bot presses the frag button for time. +*/ +BotPressFrag( time ) +{ + //self maps\mp\bots\_bot_internal::frag( time ); +} + +/* + Bot presses the smoke button for time. +*/ +BotPressSmoke( time ) +{ + //self maps\mp\bots\_bot_internal::smoke( time ); +} + +/* + Returns the bot's random assigned number. +*/ +BotGetRandom() +{ + return self.bot.rand; +} + +/* + Returns a random number thats different everytime it changes target +*/ +BotGetTargetRandom() +{ + if ( !isDefined( self.bot.target ) ) + return undefined; + + return self.bot.target.rand; +} + +/* + Returns if the bot is fragging. +*/ +IsBotFragging() +{ + return self.bot.isfraggingafter; +} + +/* + Returns if the bot is pressing smoke button. +*/ +IsBotSmoking() +{ + return self.bot.issmokingafter; +} + +/* + Returns if the bot is sprinting. +*/ +IsBotSprinting() +{ + return self.bot.issprinting; +} + +/* + Returns if the bot is reloading. +*/ +IsBotReloading() +{ + return self.bot.isreloading; +} + +/* + Is bot knifing +*/ +IsBotKnifing() +{ + return self.bot.isknifingafter; +} + +/* + Freezes the bot's controls. +*/ +BotFreezeControls( what ) +{ + self.bot.isfrozen = what; + + if ( what ) + self notify( "kill_goal" ); +} + +/* + Returns if the bot is script frozen. +*/ +BotIsFrozen() +{ + return self.bot.isfrozen; +} + +/* + Bot will stop moving +*/ +BotStopMoving( what ) +{ + self.bot.stop_move = what; + + if ( what ) + self notify( "kill_goal" ); +} + +/* + Notify the bot chat message +*/ +BotNotifyBotEvent( msg, a, b, c, d, e, f, g ) +{ + self notify( "bot_event", msg, a, b, c, d, e, f, g ); +} + +/* + Returns if the bot has a script goal. + (like t5 gsc bot) +*/ +HasScriptGoal() +{ + return ( isDefined( self GetScriptGoal() ) ); +} + +/* + Returns the pos of the bot's goal +*/ +GetScriptGoal() +{ + return self.bot.script_goal; +} + +/* + Sets the bot's goal, will acheive it when dist away from it. +*/ +SetScriptGoal( goal, dist ) +{ + if ( !isDefined( dist ) ) + dist = 16; + + self.bot.script_goal = goal; + self.bot.script_goal_dist = dist; + waittillframeend; + self notify( "new_goal_internal" ); + self notify( "new_goal" ); +} + +/* + Clears the bot's goal. +*/ +ClearScriptGoal() +{ + self SetScriptGoal( undefined, 0 ); +} + +/* + Sets the aim position of the bot +*/ +SetScriptAimPos( pos ) +{ + self.bot.script_aimpos = pos; +} + +/* + Clears the aim position of the bot +*/ +ClearScriptAimPos() +{ + self SetScriptAimPos( undefined ); +} + +/* + Returns the aim position of the bot +*/ +GetScriptAimPos() +{ + return self.bot.script_aimpos; +} + +/* + Returns if the bot has a aim pos +*/ +HasScriptAimPos() +{ + return isDefined( self GetScriptAimPos() ); +} + +/* + Sets the bot's target to be this ent. +*/ +SetAttacker( att ) +{ + self.bot.target_this_frame = att; +} + +/* + Sets the script enemy for a bot. +*/ +SetScriptEnemy( enemy, offset ) +{ + self.bot.script_target = enemy; + self.bot.script_target_offset = offset; +} + +/* + Removes the script enemy of the bot. +*/ +ClearScriptEnemy() +{ + self SetScriptEnemy( undefined, undefined ); +} + +/* + Returns the entity of the bot's target. +*/ +GetThreat() +{ + if ( !isdefined( self.bot.target ) ) + return undefined; + + return self.bot.target.entity; +} + +/* + Returns if the bot has a script enemy. +*/ +HasScriptEnemy() +{ + return ( isDefined( self.bot.script_target ) ); +} + +/* + Returns if the bot has a threat. +*/ +HasThreat() +{ + return ( isDefined( self GetThreat() ) ); +} + +/* + Returns a valid grenade launcher weapon +*/ +getValidTube() +{ + weaps = self getweaponslist(); + + for ( i = 0; i < weaps.size; i++ ) + { + weap = weaps[i]; + + if ( !self getAmmoCount( weap ) ) + continue; + + if ( isSubStr( weap, "gl_" ) && !isSubStr( weap, "_gl_" ) ) + return weap; + } + + return undefined; +} + +/* + Returns a random grenade in the bot's inventory. +*/ +getValidGrenade() +{ + grenadeTypes = []; + grenadeTypes[grenadeTypes.size] = "frag_grenade_mp"; + grenadeTypes[grenadeTypes.size] = "molotov_mp"; + grenadeTypes[grenadeTypes.size] = "m8_white_smoke_mp"; + grenadeTypes[grenadeTypes.size] = "tabun_gas_mp"; + grenadeTypes[grenadeTypes.size] = "sticky_grenade_mp"; + grenadeTypes[grenadeTypes.size] = "signal_flare_mp"; + + possibles = []; + + for ( i = 0; i < grenadeTypes.size; i++ ) + { + if ( !self hasWeapon( grenadeTypes[i] ) ) + continue; + + if ( !self getAmmoCount( grenadeTypes[i] ) ) + continue; + + possibles[possibles.size] = grenadeTypes[i]; + } + + return PickRandom( possibles ); +} + +/* + Picks a random thing +*/ +PickRandom( arr ) +{ + if ( !arr.size ) + return undefined; + + return arr[randomInt( arr.size )]; +} + +/* + If weap is a secondary gnade +*/ +isSecondaryGrenade( gnade ) +{ + return ( gnade == "tabun_gas_mp" || gnade == "m8_white_smoke_mp" || gnade == "signal_flare_mp" ); +} + +/* + CoD4 +*/ +getBaseWeaponName( weap ) +{ + return strtok( weap, "_" )[0]; +} + +/* + Returns if the given weapon is full auto. +*/ +WeaponIsFullAuto( weap ) +{ + weaptoks = strtok( weap, "_" ); + + return isDefined( weaptoks[0] ) && isString( weaptoks[0] ) && isdefined( level.bots_fullautoguns[weaptoks[0]] ); +} + +/* + Returns what our eye height is. +*/ +GetEyeHeight() +{ + myEye = self GetEyePos(); + + return myEye[2] - self.origin[2]; +} + +/* + Returns (iw4) eye pos. +*/ +GetEyePos() +{ + return self getTagOrigin( "tag_eye" ); +} + +/* + helper +*/ +waittill_either_return_( str1, str2 ) +{ + self endon( str1 ); + self waittill( str2 ); + return true; +} + +/* + Returns which string gets notified first +*/ +waittill_either_return( str1, str2 ) +{ + if ( !isDefined( self waittill_either_return_( str1, str2 ) ) ) + return str1; + + return str2; +} + +/* + Taken from iw4 script +*/ +waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ) +{ + if ( ( !isdefined( string1 ) || string1 != "death" ) && + ( !isdefined( string2 ) || string2 != "death" ) && + ( !isdefined( string3 ) || string3 != "death" ) && + ( !isdefined( string4 ) || string4 != "death" ) && + ( !isdefined( string5 ) || string5 != "death" ) ) + self endon( "death" ); + + ent = spawnstruct(); + + if ( isdefined( string1 ) ) + self thread waittill_string( string1, ent ); + + if ( isdefined( string2 ) ) + self thread waittill_string( string2, ent ); + + if ( isdefined( string3 ) ) + self thread waittill_string( string3, ent ); + + if ( isdefined( string4 ) ) + self thread waittill_string( string4, ent ); + + if ( isdefined( string5 ) ) + self thread waittill_string( string5, ent ); + + ent thread _timeout( timeOut ); + + ent waittill( "returned", msg ); + ent notify( "die" ); + return msg; +} + +/* + Used for waittill_any_timeout +*/ +_timeout( delay ) +{ + self endon( "die" ); + + wait( delay ); + self notify( "returned", "timeout" ); +} + +/* + Gets a player who is host +*/ +GetHostPlayer() +{ + for ( i = 0; i < level.players.size; i++ ) + { + player = level.players[i]; + + if ( !player is_host() ) + continue; + + return player; + } + + return undefined; +} + +/* + Waits for a host player +*/ +bot_wait_for_host() +{ + host = undefined; + + while ( !isDefined( level ) || !isDefined( level.players ) ) + wait 0.05; + + for ( i = getDvarFloat( "bots_main_waitForHostTime" ); i > 0; i -= 0.05 ) + { + host = GetHostPlayer(); + + if ( isDefined( host ) ) + break; + + wait 0.05; + } + + if ( !isDefined( host ) ) + return; + + for ( i = getDvarFloat( "bots_main_waitForHostTime" ); i > 0; i -= 0.05 ) + { + if ( IsDefined( host.pers[ "team" ] ) ) + break; + + wait 0.05; + } + + if ( !IsDefined( host.pers[ "team" ] ) ) + return; + + for ( i = getDvarFloat( "bots_main_waitForHostTime" ); i > 0; i -= 0.05 ) + { + if ( host.pers[ "team" ] == "allies" || host.pers[ "team" ] == "axis" ) + break; + + wait 0.05; + } +} + +/* + Returns the cone dot (like fov, or distance from the center of our screen). 1.0 = directly looking at, 0.0 = completely right angle, -1.0, completely 180 +*/ +getConeDot( to, from, dir ) +{ + dirToTarget = VectorNormalize( to - from ); + forward = AnglesToForward( dir ); + return vectordot( dirToTarget, forward ); +} + +/* + Returns the distance squared in a 2d space +*/ +DistanceSquared2D( to, from ) +{ + to = ( to[0], to[1], 0 ); + from = ( from[0], from[1], 0 ); + + return DistanceSquared( to, from ); +} + +/* + Rounds to the nearest whole number. +*/ +Round( x ) +{ + y = int( x ); + + if ( abs( x ) - abs( y ) > 0.5 ) + { + if ( x < 0 ) + return y - 1; + else + return y + 1; + } + else + return y; +} + +/* + Rounds up the given value. +*/ +RoundUp( floatVal ) +{ + i = int( floatVal ); + + if ( i != floatVal ) + return i + 1; + else + return i; +} + +/* + Clamps between value +*/ +Clamp( a, minv, maxv ) +{ + return max( min( a, maxv ), minv ); +} + +/* + converts a string into a float +*/ +float( num ) +{ + setdvar( "temp_dvar_bot_util", num ); + + return GetDvarFloat( "temp_dvar_bot_util" ); +} + +/* + Tokenizes a string (strtok has limits...) (only one char tok) +*/ +tokenizeLine( line, tok ) +{ + tokens = []; + + token = ""; + + for ( i = 0; i < line.size; i++ ) + { + c = line[i]; + + if ( c == tok ) + { + tokens[tokens.size] = token; + token = ""; + continue; + } + + token += c; + } + + tokens[tokens.size] = token; + + return tokens; +} + +/* + Parses tokens into a waypoint obj +*/ +parseTokensIntoWaypoint( tokens ) +{ + waypoint = spawnStruct(); + + orgStr = tokens[0]; + orgToks = strtok( orgStr, " " ); + waypoint.origin = ( float( orgToks[0] ), float( orgToks[1] ), float( orgToks[2] ) ); + + childStr = tokens[1]; + childToks = strtok( childStr, " " ); + waypoint.children = []; + + for ( j = 0; j < childToks.size; j++ ) + waypoint.children[j] = int( childToks[j] ); + + type = tokens[2]; + waypoint.type = type; + + anglesStr = tokens[3]; + + if ( isDefined( anglesStr ) && anglesStr != "" ) + { + anglesToks = strtok( anglesStr, " " ); + waypoint.angles = ( float( anglesToks[0] ), float( anglesToks[1] ), float( anglesToks[2] ) ); + } + + return waypoint; +} + +/* + Returns an array of each line +*/ +getWaypointLinesFromFile( filename ) +{ + result = spawnStruct(); + result.lines = []; + + waypointStr = fileRead( filename ); + + if ( !isDefined( waypointStr ) ) + return result; + + line = ""; + + for ( i = 0; i < waypointStr.size; i++ ) + { + c = waypointStr[i]; + + if ( c == "\n" ) + { + result.lines[result.lines.size] = line; + + line = ""; + continue; + } + + line += c; + } + + result.lines[result.lines.size] = line; + + return result; +} + +/* + Read from file a csv, and returns an array of waypoints +*/ +readWpsFromFile( mapname ) +{ + waypoints = []; + filename = "waypoints/" + mapname + "_wp.csv"; + + res = getWaypointLinesFromFile( filename ); + + if ( !res.lines.size ) + return waypoints; + + PrintConsole( "Attempting to read waypoints from " + filename + "\n" ); + + waypointCount = int( res.lines[0] ); + + for ( i = 1; i <= waypointCount; i++ ) + { + tokens = tokenizeLine( res.lines[i], "," ); + + waypoint = parseTokensIntoWaypoint( tokens ); + + waypoints[i - 1] = waypoint; + } + + return waypoints; +} + +/* + Loads the waypoints. Populating everything needed for the waypoints. +*/ +load_waypoints() +{ + mapname = getDvar( "mapname" ); + + level.waypointCount = 0; + level.waypoints = []; + + wps = readWpsFromFile( mapname ); + + if ( wps.size ) + { + level.waypoints = wps; + PrintConsole( "Loaded " + wps.size + " waypoints from file.\n" ); + } + else + { + switch ( mapname ) + { + default: + maps\mp\bots\waypoints\_custom_map::main( mapname ); + break; + } + + if ( level.waypoints.size ) + PrintConsole( "Loaded " + level.waypoints.size + " waypoints from script.\n" ); + } + + level.waypointCount = level.waypoints.size; + + for ( i = 0; i < level.waypointCount; i++ ) + { + if ( !isDefined( level.waypoints[i].children ) || !isDefined( level.waypoints[i].children.size ) ) + level.waypoints[i].children = []; + + if ( !isDefined( level.waypoints[i].origin ) ) + level.waypoints[i].origin = ( 0, 0, 0 ); + + if ( !isDefined( level.waypoints[i].type ) ) + level.waypoints[i].type = "crouch"; + + level.waypoints[i].childCount = undefined; + } +} + +/* + Is bot near any of the given waypoints +*/ +nearAnyOfWaypoints( dist, waypoints ) +{ + dist *= dist; + + for ( i = 0; i < waypoints.size; i++ ) + { + waypoint = level.waypoints[waypoints[i]]; + + if ( DistanceSquared( waypoint.origin, self.origin ) > dist ) + continue; + + return true; + } + + return false; +} + +/* + Returns the waypoints that are near +*/ +waypointsNear( waypoints, dist ) +{ + dist *= dist; + + answer = []; + + for ( i = 0; i < waypoints.size; i++ ) + { + wp = level.waypoints[waypoints[i]]; + + if ( DistanceSquared( wp.origin, self.origin ) > dist ) + continue; + + answer[answer.size] = waypoints[i]; + } + + return answer; +} + +/* + Returns nearest waypoint of waypoints +*/ +getNearestWaypointOfWaypoints( waypoints ) +{ + answer = undefined; + closestDist = 2147483647; + + for ( i = 0; i < waypoints.size; i++ ) + { + waypoint = level.waypoints[waypoints[i]]; + thisDist = DistanceSquared( self.origin, waypoint.origin ); + + if ( isDefined( answer ) && thisDist > closestDist ) + continue; + + answer = waypoints[i]; + closestDist = thisDist; + } + + return answer; +} + +/* + Returns all waypoints of type +*/ +getWaypointsOfType( type ) +{ + answer = []; + + for ( i = 0; i < level.waypointCount; i++ ) + { + wp = level.waypoints[i]; + + if ( type == "camp" ) + { + if ( wp.type != "crouch" ) + continue; + + if ( wp.children.size != 1 ) + continue; + } + else if ( type != wp.type ) + continue; + + answer[answer.size] = i; + } + + return answer; +} + +/* + Returns the waypoint for index +*/ +getWaypointForIndex( i ) +{ + if ( !isDefined( i ) ) + return undefined; + + return level.waypoints[i]; +} + +/* + Returns a good amount of players. +*/ +getGoodMapAmount() +{ + return 2; +} + +/* + Returns the friendly user name for a given map's codename +*/ +getMapName( map ) +{ + return map; +} + +/* + Returns an array of all the bots in the game. +*/ +getBotArray() +{ + result = []; + playercount = level.players.size; + + for ( i = 0; i < playercount; i++ ) + { + player = level.players[i]; + + if ( !player is_bot() ) + continue; + + result[result.size] = player; + } + + return result; +} + +/* + A heap invarient comparitor, used for numbers, numbers with the highest number will be first in the heap. +*/ +Heap( item, item2 ) +{ + return item > item2; +} + +/* + A heap invarient comparitor, used for numbers, numbers with the lowest number will be first in the heap. +*/ +ReverseHeap( item, item2 ) +{ + return item < item2; +} + +/* + A heap invarient comparitor, used for traces. Wanting the trace with the largest length first in the heap. +*/ +HeapTraceFraction( item, item2 ) +{ + return item["fraction"] > item2["fraction"]; +} + +/* + Returns a new heap. +*/ +NewHeap( compare ) +{ + heap_node = spawnStruct(); + heap_node.data = []; + heap_node.compare = compare; + + return heap_node; +} + +/* + Inserts the item into the heap. Called on a heap. +*/ +HeapInsert( item ) +{ + insert = self.data.size; + self.data[insert] = item; + + current = insert + 1; + + while ( current > 1 ) + { + last = current; + current = int( current / 2 ); + + if ( ![[self.compare]]( item, self.data[current - 1] ) ) + break; + + self.data[last - 1] = self.data[current - 1]; + self.data[current - 1] = item; + } +} + +/* + Helper function to determine what is the next child of the bst. +*/ +_HeapNextChild( node, hsize ) +{ + left = node * 2; + right = left + 1; + + if ( left > hsize ) + return -1; + + if ( right > hsize ) + return left; + + if ( [[self.compare]]( self.data[left - 1], self.data[right - 1] ) ) + return left; + else + return right; +} + +/* + Removes an item from the heap. Called on a heap. +*/ +HeapRemove() +{ + remove = self.data.size; + + if ( !remove ) + return remove; + + move = self.data[remove - 1]; + self.data[0] = move; + self.data[remove - 1] = undefined; + remove--; + + if ( !remove ) + return remove; + + last = 1; + next = self _HeapNextChild( 1, remove ); + + while ( next != -1 ) + { + if ( [[self.compare]]( move, self.data[next - 1] ) ) + break; + + self.data[last - 1] = self.data[next - 1]; + self.data[next - 1] = move; + + last = next; + next = self _HeapNextChild( next, remove ); + } + + return remove; +} + +/* + A heap invarient comparitor, used for the astar's nodes, wanting the node with the lowest f to be first in the heap. +*/ +ReverseHeapAStar( item, item2 ) +{ + return item.f < item2.f; +} + +/* + Will linearly search for the nearest waypoint to pos that has a direct line of sight. +*/ +GetNearestWaypointWithSight( pos ) +{ + candidate = undefined; + dist = 2147483647; + + for ( i = 0; i < level.waypointCount; i++ ) + { + if ( !bulletTracePassed( pos + ( 0, 0, 15 ), level.waypoints[i].origin + ( 0, 0, 15 ), false, undefined ) ) + continue; + + curdis = DistanceSquared( level.waypoints[i].origin, pos ); + + if ( curdis > dist ) + continue; + + dist = curdis; + candidate = i; + } + + return candidate; +} + +/* + Will linearly search for the nearest waypoint +*/ +GetNearestWaypoint( pos ) +{ + candidate = undefined; + dist = 2147483647; + + for ( i = 0; i < level.waypointCount; i++ ) + { + curdis = DistanceSquared( level.waypoints[i].origin, pos ); + + if ( curdis > dist ) + continue; + + dist = curdis; + candidate = i; + } + + return candidate; +} + +/* + Modified Pezbot astar search. + This makes use of sets for quick look up and a heap for a priority queue instead of simple lists which require to linearly search for elements everytime. + It is also modified to make paths with bots already on more expensive and will try a less congested path first. Thus spliting up the bots onto more paths instead of just one (the smallest). +*/ +AStarSearch( start, goal, team, greedy_path ) +{ + open = NewHeap( ::ReverseHeapAStar ); //heap + openset = [];//set for quick lookup + closed = [];//set for quick lookup + + + startWp = getNearestWaypoint( start ); + + if ( !isDefined( startWp ) ) + return []; + + _startwp = undefined; + + if ( !bulletTracePassed( start + ( 0, 0, 15 ), level.waypoints[startWp].origin + ( 0, 0, 15 ), false, undefined ) ) + _startwp = GetNearestWaypointWithSight( start ); + + if ( isDefined( _startwp ) ) + startWp = _startwp; + + + goalWp = getNearestWaypoint( goal ); + + if ( !isDefined( goalWp ) ) + return []; + + _goalWp = undefined; + + if ( !bulletTracePassed( goal + ( 0, 0, 15 ), level.waypoints[goalWp].origin + ( 0, 0, 15 ), false, undefined ) ) + _goalwp = GetNearestWaypointWithSight( goal ); + + if ( isDefined( _goalwp ) ) + goalWp = _goalwp; + + + node = spawnStruct(); + node.g = 0; //path dist so far + node.h = DistanceSquared( level.waypoints[startWp].origin, level.waypoints[goalWp].origin ); //herustic, distance to goal for path finding + node.f = node.h + node.g; // combine path dist and heru, use reverse heap to sort the priority queue by this attru + node.index = startWp; + node.parent = undefined; //we are start, so we have no parent + + //push node onto queue + openset[node.index + ""] = node; + open HeapInsert( node ); + + //while the queue is not empty + while ( open.data.size ) + { + //pop bestnode from queue + bestNode = open.data[0]; + open HeapRemove(); + openset[bestNode.index + ""] = undefined; + wp = level.waypoints[bestNode.index]; + + //check if we made it to the goal + if ( bestNode.index == goalWp ) + { + path = []; + + while ( isDefined( bestNode ) ) + { + //construct path + path[path.size] = bestNode.index; + + bestNode = bestNode.parent; + } + + return path; + } + + //for each child of bestnode + for ( i = wp.children.size - 1; i >= 0; i-- ) + { + child = wp.children[i]; + childWp = level.waypoints[child]; + + penalty = 1; + + // have certain types of nodes more expensive + if ( childWp.type == "climb" || childWp.type == "prone" ) + penalty += 4; + + //calc the total path we have took + newg = bestNode.g + DistanceSquared( wp.origin, childWp.origin ) * penalty; //bots on same team's path are more expensive + + //check if this child is in open or close with a g value less than newg + inopen = isDefined( openset[child + ""] ); + + if ( inopen && openset[child + ""].g <= newg ) + continue; + + inclosed = isDefined( closed[child + ""] ); + + if ( inclosed && closed[child + ""].g <= newg ) + continue; + + node = undefined; + + if ( inopen ) + node = openset[child + ""]; + else if ( inclosed ) + node = closed[child + ""]; + else + node = spawnStruct(); + + node.parent = bestNode; + node.g = newg; + node.h = DistanceSquared( childWp.origin, level.waypoints[goalWp].origin ); + node.f = node.g + node.h; + node.index = child; + + //check if in closed, remove it + if ( inclosed ) + closed[child + ""] = undefined; + + //check if not in open, add it + if ( !inopen ) + { + open HeapInsert( node ); + openset[child + ""] = node; + } + } + + //done with children, push onto closed + closed[bestNode.index + ""] = bestNode; + } + + return []; +} + +/* + Returns the natural log of x using harmonic series. +*/ +Log( x ) +{ + /* if (!isDefined(level.log_cache)) + level.log_cache = []; + + key = x + ""; + + if (isDefined(level.log_cache[key])) + return level.log_cache[key];*/ + + //thanks Bob__ at stackoverflow + old_sum = 0.0; + xmlxpl = ( x - 1 ) / ( x + 1 ); + xmlxpl_2 = xmlxpl * xmlxpl; + denom = 1.0; + frac = xmlxpl; + sum = frac; + + while ( sum != old_sum ) + { + old_sum = sum; + denom += 2.0; + frac *= xmlxpl_2; + sum += frac / denom; + } + + answer = 2.0 * sum; + + //level.log_cache[key] = answer; + return answer; +} + +/* + Taken from t5 gsc. + Returns an array of number's average. +*/ +array_average( array ) +{ + assert( array.size > 0 ); + total = 0; + + for ( i = 0; i < array.size; i++ ) + { + total += array[i]; + } + + return ( total / array.size ); +} + +/* + Taken from t5 gsc. + Returns an array of number's standard deviation. +*/ +array_std_deviation( array, mean ) +{ + assert( array.size > 0 ); + tmp = []; + + for ( i = 0; i < array.size; i++ ) + { + tmp[i] = ( array[i] - mean ) * ( array[i] - mean ); + } + + total = 0; + + for ( i = 0; i < tmp.size; i++ ) + { + total = total + tmp[i]; + } + + return Sqrt( total / array.size ); +} + +/* + Taken from t5 gsc. + Will produce a random number between lower_bound and upper_bound but with a bell curve distribution (more likely to be close to the mean). +*/ +random_normal_distribution( mean, std_deviation, lower_bound, upper_bound ) +{ + x1 = 0; + x2 = 0; + w = 1; + y1 = 0; + + while ( w >= 1 ) + { + x1 = 2 * RandomFloatRange( 0, 1 ) - 1; + x2 = 2 * RandomFloatRange( 0, 1 ) - 1; + w = x1 * x1 + x2 * x2; + } + + w = Sqrt( ( -2.0 * Log( w ) ) / w ); + y1 = x1 * w; + number = mean + y1 * std_deviation; + + if ( IsDefined( lower_bound ) && number < lower_bound ) + { + number = lower_bound; + } + + if ( IsDefined( upper_bound ) && number > upper_bound ) + { + number = upper_bound; + } + + return ( number ); +}