summaryrefslogtreecommitdiff
path: root/game/server/tf/bot/tf_bot.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/bot/tf_bot.cpp')
-rw-r--r--game/server/tf/bot/tf_bot.cpp4644
1 files changed, 4644 insertions, 0 deletions
diff --git a/game/server/tf/bot/tf_bot.cpp b/game/server/tf/bot/tf_bot.cpp
new file mode 100644
index 0000000..6bafab4
--- /dev/null
+++ b/game/server/tf/bot/tf_bot.cpp
@@ -0,0 +1,4644 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot.cpp
+// Team Fortress NextBot
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_obj_sentrygun.h"
+#include "team_control_point_master.h"
+#include "tf_weapon_pipebomblauncher.h"
+#include "team_train_watcher.h"
+#include "tf_bot.h"
+#include "tf_bot_manager.h"
+#include "tf_bot_vision.h"
+#include "tf_team.h"
+#include "bot/map_entities/tf_bot_generator.h"
+#include "trigger_area_capture.h"
+#include "GameEventListener.h"
+#include "NextBotUtil.h"
+#include "tier3/tier3.h"
+#include "vgui/ILocalize.h"
+#include "econ_item_system.h"
+#include "bot/behavior/tf_bot_use_item.h"
+#include "tf_wearable_item_demoshield.h"
+#include "tf_weapon_buff_item.h"
+#include "tf_weapon_lunchbox.h"
+#include "func_respawnroom.h"
+#include "soundenvelope.h"
+
+#include "econ_entity_creation.h"
+
+#include "player_vs_environment/tf_population_manager.h"
+
+#include "bot/behavior/tf_bot_behavior.h"
+#include "bot/map_entities/tf_bot_generator.h"
+#include "bot/map_entities/tf_bot_hint_entity.h"
+
+ConVar tf_bot_force_class( "tf_bot_force_class", "", FCVAR_GAMEDLL, "If set to a class name, all TFBots will respawn as that class" );
+
+ConVar tf_bot_notice_gunfire_range( "tf_bot_notice_gunfire_range", "3000", FCVAR_GAMEDLL );
+ConVar tf_bot_notice_quiet_gunfire_range( "tf_bot_notice_quiet_gunfire_range", "500", FCVAR_GAMEDLL );
+ConVar tf_bot_sniper_personal_space_range( "tf_bot_sniper_personal_space_range", "1000", FCVAR_CHEAT, "Enemies beyond this range don't worry the Sniper" );
+ConVar tf_bot_pyro_deflect_tolerance( "tf_bot_pyro_deflect_tolerance", "0.5", FCVAR_CHEAT );
+ConVar tf_bot_keep_class_after_death( "tf_bot_keep_class_after_death", "0", FCVAR_GAMEDLL );
+ConVar tf_bot_prefix_name_with_difficulty( "tf_bot_prefix_name_with_difficulty", "0", FCVAR_GAMEDLL, "Append the skill level of the bot to the bot's name" );
+ConVar tf_bot_near_point_travel_distance( "tf_bot_near_point_travel_distance", "750", FCVAR_CHEAT, "If within this travel distance to the current point, bot is 'near' it" );
+ConVar tf_bot_pyro_shove_away_range( "tf_bot_pyro_shove_away_range", "250", FCVAR_CHEAT, "If a Pyro bot's target is closer than this, compression blast them away" );
+ConVar tf_bot_pyro_always_reflect( "tf_bot_pyro_always_reflect", "0", FCVAR_CHEAT, "Pyro bots will always reflect projectiles fired at them. For tesing/debugging purposes." );
+
+ConVar tf_bot_sniper_spot_min_range( "tf_bot_sniper_spot_min_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_sniper_spot_max_count( "tf_bot_sniper_spot_max_count", "10", FCVAR_CHEAT, "Stop searching for sniper spots when each side has found this many" );
+ConVar tf_bot_sniper_spot_search_count( "tf_bot_sniper_spot_search_count", "10", FCVAR_CHEAT, "Search this many times per behavior update frame" );
+ConVar tf_bot_sniper_spot_point_tolerance( "tf_bot_sniper_spot_point_tolerance", "750", FCVAR_CHEAT );
+ConVar tf_bot_sniper_spot_epsilon( "tf_bot_sniper_spot_epsilon", "100", FCVAR_CHEAT );
+
+ConVar tf_bot_sniper_goal_entity_move_tolerance( "tf_bot_sniper_goal_entity_move_tolerance", "500", FCVAR_CHEAT );
+
+ConVar tf_bot_suspect_spy_touch_interval( "tf_bot_suspect_spy_touch_interval", "5", FCVAR_CHEAT, "How many seconds back to look for touches against suspicious spies" );
+ConVar tf_bot_suspect_spy_forget_cooldown( "tf_bot_suspect_spy_forget_cooldown", "5", FCVAR_CHEAT, "How long to consider a suspicious spy as suspicious" );
+
+ConVar tf_bot_debug_tags( "tf_bot_debug_tags", "0", FCVAR_CHEAT, "ent_text will only show tags on bots" );
+
+extern ConVar tf_bot_sniper_spot_max_count;
+extern ConVar tf_bot_fire_weapon_min_time;
+extern ConVar tf_bot_sniper_misfire_chance;
+extern ConVar tf_bot_difficulty;
+extern ConVar tf_bot_farthest_visible_theater_sample_count;
+extern ConVar tf_bot_sniper_spot_min_range;
+extern ConVar tf_bot_sniper_spot_epsilon;
+extern ConVar tf_mvm_miniboss_min_health;
+extern ConVar tf_bot_path_lookahead_range;
+
+extern ConVar tf_mvm_miniboss_scale;
+
+
+//-----------------------------------------------------------------------------------------------------
+bool IsPlayerClassname( const char *string )
+{
+ for ( int i = TF_CLASS_SCOUT; i < TF_CLASS_COUNT_ALL; ++i )
+ {
+ if ( !stricmp( string, GetPlayerClassData( i )->m_szClassName ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool IsTeamName( const char *string )
+{
+ if ( !stricmp( string, "red" ) )
+ return true;
+
+ if ( !stricmp( string, "blue" ) )
+ return true;
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::DifficultyType StringToDifficultyLevel( const char *string )
+{
+ if ( !stricmp( string, "easy" ) )
+ return CTFBot::EASY;
+
+ if ( !stricmp( string, "normal" ) )
+ return CTFBot::NORMAL;
+
+ if ( !stricmp( string, "hard" ) )
+ return CTFBot::HARD;
+
+ if ( !stricmp( string, "expert" ) )
+ return CTFBot::EXPERT;
+
+ return CTFBot::UNDEFINED;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+const char *DifficultyLevelToString( CTFBot::DifficultyType skill )
+{
+ switch( skill )
+ {
+ case CTFBot::EASY: return "Easy ";
+ case CTFBot::NORMAL: return "Normal ";
+ case CTFBot::HARD: return "Hard ";
+ case CTFBot::EXPERT: return "Expert ";
+ }
+
+ return "Undefined ";
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+const char *GetRandomBotName( void )
+{
+ static const char *nameList[] =
+ {
+ "Chucklenuts",
+ "CryBaby",
+ "WITCH",
+ "ThatGuy",
+ "Still Alive",
+ "Hat-Wearing MAN",
+ "Me",
+ "Numnutz",
+ "H@XX0RZ",
+ "The G-Man",
+ "Chell",
+ "The Combine",
+ "Totally Not A Bot",
+ "Pow!",
+ "Zepheniah Mann",
+ "THEM",
+ "LOS LOS LOS",
+ "10001011101",
+ "DeadHead",
+ "ZAWMBEEZ",
+ "MindlessElectrons",
+ "TAAAAANK!",
+ "The Freeman",
+ "Black Mesa",
+ "Soulless",
+ "CEDA",
+ "BeepBeepBoop",
+ "NotMe",
+ "CreditToTeam",
+ "BoomerBile",
+ "Someone Else",
+ "Mann Co.",
+ "Dog",
+ "Kaboom!",
+ "AmNot",
+ "0xDEADBEEF",
+ "HI THERE",
+ "SomeDude",
+ "GLaDOS",
+ "Hostage",
+ "Headful of Eyeballs",
+ "CrySomeMore",
+ "Aperture Science Prototype XR7",
+ "Humans Are Weak",
+ "AimBot",
+ "C++",
+ "GutsAndGlory!",
+ "Nobody",
+ "Saxton Hale",
+ "RageQuit",
+ "Screamin' Eagles",
+
+ "Ze Ubermensch",
+ "Maggot",
+ "CRITRAWKETS",
+ "Herr Doktor",
+ "Gentlemanne of Leisure",
+ "Companion Cube",
+ "Target Practice",
+ "One-Man Cheeseburger Apocalypse",
+ "Crowbar",
+ "Delicious Cake",
+ "IvanTheSpaceBiker",
+ "I LIVE!",
+ "Cannon Fodder",
+
+ "trigger_hurt",
+ "Nom Nom Nom",
+ "Divide by Zero",
+ "GENTLE MANNE of LEISURE",
+ "MoreGun",
+ "Tiny Baby Man",
+ "Big Mean Muther Hubbard",
+ "Force of Nature",
+
+ "Crazed Gunman",
+ "Grim Bloody Fable",
+ "Poopy Joe",
+ "A Professional With Standards",
+ "Freakin' Unbelievable",
+ "SMELLY UNFORTUNATE",
+ "The Administrator",
+ "Mentlegen",
+
+ "Archimedes!",
+ "Ribs Grow Back",
+ "It's Filthy in There!",
+ "Mega Baboon",
+ "Kill Me",
+ "Glorified Toaster with Legs",
+
+#ifdef STAGING_ONLY
+ "John Spartan",
+ "Leeloo Dallas Multipass",
+ "Sho'nuff",
+ "Bruce Leroy",
+ "CAN YOUUUUUUUUU DIG IT?!?!?!?!",
+ "Big Gulp, Huh?",
+ "Stupid Hot Dog",
+ "I'm your huckleberry",
+ "The Crocketeer",
+#endif
+ NULL
+ };
+ static int nameCount = 0;
+ static int nameIndex = 0;
+
+ if ( nameCount == 0 )
+ {
+ for( ; nameList[ nameCount ]; ++nameCount );
+
+ // randomize the initial index
+ nameIndex = RandomInt( 0, nameCount-1 );
+ }
+
+ const char *name = nameList[ nameIndex++ ];
+
+ if ( nameIndex >= nameCount )
+ nameIndex = 0;
+
+ return name;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize )
+{
+ char szBotNameBuffer[256];
+ char szEnemyOrFriendlyString[256];
+
+ const char *pBotName = "";
+ const char *pFriendlyOrEnemyTitle = "";
+
+ // @note (Tom Bui): it is okay to get localized name in training, since we should be on a listen server
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // get the friendly/enemy title
+ const char *pBotTitle = NULL;
+ if ( iTeam != TEAM_UNASSIGNED )
+ {
+ int iHumanTeam = TFGameRules()->GetAssignedHumanTeam();
+ if ( iHumanTeam != TEAM_ANY )
+ {
+ if ( iHumanTeam == iTeam )
+ {
+ pBotTitle = "#TF_Bot_Title_Friendly";
+ }
+ else
+ {
+ pBotTitle = "#TF_Bot_Title_Enemy";
+ }
+ }
+ }
+ wchar_t *pLocalizedTitle = pBotTitle ? g_pVGuiLocalize->Find( pBotTitle ) : NULL;
+ if ( pLocalizedTitle )
+ {
+ g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedTitle, szEnemyOrFriendlyString, sizeof( szEnemyOrFriendlyString ) );
+ pFriendlyOrEnemyTitle = szEnemyOrFriendlyString;
+ }
+
+ // get the class name
+ wchar_t *pLocalizedName = NULL;
+ if ( iClassIndex >= TF_FIRST_NORMAL_CLASS && iClassIndex < TF_LAST_NORMAL_CLASS )
+ {
+ pLocalizedName = g_pVGuiLocalize->Find( g_aPlayerClassNames[ iClassIndex ] );
+ }
+ else
+ {
+ pLocalizedName = g_pVGuiLocalize->Find( "#TF_Bot_Generic_ClassName" );
+ }
+ g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedName, szBotNameBuffer, sizeof( szBotNameBuffer ) );
+ pBotName = szBotNameBuffer;
+ }
+ else
+ {
+ pBotName = GetRandomBotName();
+ }
+
+ const char *pDifficultyString = tf_bot_prefix_name_with_difficulty.GetBool() ? DifficultyLevelToString( skill ) : "";
+
+ // we use this as our formatting, because we don't know the language of the downstream clients
+ CFmtStr name( "%s%s%s",
+ pDifficultyString, pFriendlyOrEnemyTitle, pBotName );
+ Q_strncpy( pBuffer, name.Access(), iBufferSize );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_add, "Add a bot.", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ bool bQuotaManaged = true;
+ int botCount = 1;
+ const char *classname = NULL;
+ const char *teamname = "auto";
+ const char *pszBotNameViaArg = NULL;
+ CTFBot::DifficultyType skill = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT );
+
+ int i;
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ CTFBot::DifficultyType trySkill = StringToDifficultyLevel( args.Arg(i) );
+ int nArgAsInteger = atoi( args.Arg(i) );
+
+ // each argument could be a classname, a team, a difficulty level, a count, or a name
+ if ( IsPlayerClassname( args.Arg(i) ) )
+ {
+ classname = args.Arg(i);
+ }
+ else if ( IsTeamName( args.Arg(i) ) )
+ {
+ teamname = args.Arg(i);
+ }
+ else if ( !stricmp( args.Arg( i ), "noquota" ) )
+ {
+ bQuotaManaged = false;
+ }
+ else if ( trySkill != CTFBot::UNDEFINED )
+ {
+ skill = trySkill;
+ }
+ else if ( nArgAsInteger > 0 )
+ {
+ botCount = nArgAsInteger;
+ pszBotNameViaArg = NULL; // can't have a custom name if spawning multiple bots
+ }
+ else if ( botCount == 1 )
+ {
+ pszBotNameViaArg = args.Arg( i );
+ }
+ else
+ {
+ Warning( "Invalid argument '%s'\n", args.Arg(i) );
+ }
+ }
+
+ // cvar can override classname
+ classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? classname : tf_bot_force_class.GetString();
+ int iClassIndex = classname ? GetClassIndexFromString( classname ) : TF_CLASS_UNDEFINED;
+
+ int iTeam = TEAM_UNASSIGNED;
+ if ( FStrEq( teamname, "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( teamname, "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ skill = CTFBot::EASY;
+ }
+
+ char name[256];
+ int iNumAdded = 0;
+ for( i=0; i<botCount; ++i )
+ {
+ CTFBot *pBot = NULL;
+ const char *pszBotName = NULL;
+
+ if ( !pszBotNameViaArg )
+ {
+ CreateBotName( iTeam, iClassIndex, skill, name, sizeof(name) );
+ pszBotName = name;
+ }
+ else
+ {
+ pszBotName = pszBotNameViaArg;
+ }
+
+ pBot = NextBotCreatePlayerBot< CTFBot >( pszBotName );
+
+ if ( pBot )
+ {
+ if ( bQuotaManaged )
+ {
+ pBot->SetAttribute( CTFBot::QUOTA_MANANGED );
+ }
+
+ pBot->HandleCommand_JoinTeam( teamname );
+
+ pBot->SetDifficulty( skill );
+
+ // if no class is set, auto-select one
+ const char *thisClassname = classname ? classname : pBot->GetNextSpawnClassname();
+ pBot->HandleCommand_JoinClass( thisClassname );
+
+ // set up a proper name now that we are in training
+ if ( TFGameRules()->IsInTraining() )
+ {
+ CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof(name) );
+ engine->SetFakeClientConVarValue( pBot->edict(), "name", name );
+ }
+
+ ++iNumAdded;
+ }
+ }
+
+ if ( bQuotaManaged )
+ {
+ TheTFBots().OnForceAddedBots( iNumAdded );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_kick, "Remove a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ {
+ DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) );
+ return;
+ }
+
+ bool bMoveToSpectatorTeam = false;
+ int iTeam = TEAM_UNASSIGNED;
+ int i;
+ const char *pPlayerName = "";
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ // each argument could be a classname, a team, or a count
+ if ( FStrEq( args.Arg(i), "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( args.Arg(i), "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+ else if ( FStrEq( args.Arg(i), "all" ) )
+ {
+ iTeam = TEAM_ANY;
+ }
+ else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) )
+ {
+ bMoveToSpectatorTeam = true;
+ }
+ else
+ {
+ pPlayerName = args.Arg(i);
+ }
+ }
+
+ int iNumKicked = 0;
+ for( int i=1; i<=gpGlobals->maxClients; ++i )
+ {
+ CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) );
+
+ if ( !player )
+ continue;
+
+ if ( FNullEnt( player->edict() ) )
+ continue;
+
+ if ( player->MyNextBotPointer() )
+ {
+ if ( iTeam == TEAM_ANY ||
+ FStrEq( pPlayerName, player->GetPlayerName() ) ||
+ ( player->GetTeamNumber() == iTeam ) ||
+ ( player->GetTeamNumber() == iTeam ) )
+ {
+ if ( bMoveToSpectatorTeam )
+ {
+ player->ChangeTeam( TEAM_SPECTATOR, false, true );
+ }
+ else
+ {
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
+ }
+ CTFBot* pBot = dynamic_cast< CTFBot* >( player );
+ if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
+ {
+ ++iNumKicked;
+ }
+ }
+ }
+ }
+ TheTFBots().OnForceKickedBots( iNumKicked );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_kill, "Kill a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ {
+ DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) );
+ return;
+ }
+
+ int iTeam = TEAM_UNASSIGNED;
+ int i;
+ const char *pPlayerName = "";
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ // each argument could be a classname, a team, or a count
+ if ( FStrEq( args.Arg(i), "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( args.Arg(i), "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+ else if ( FStrEq( args.Arg(i), "all" ) )
+ {
+ iTeam = TEAM_ANY;
+ }
+ else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) )
+ {
+ // bMoveToSpectatorTeam = true;
+ }
+ else
+ {
+ pPlayerName = args.Arg(i);
+ }
+ }
+
+ for( int i=1; i<=gpGlobals->maxClients; ++i )
+ {
+ CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) );
+
+ if ( !player )
+ continue;
+
+ if ( FNullEnt( player->edict() ) )
+ continue;
+
+ if ( player->MyNextBotPointer() )
+ {
+ if ( iTeam == TEAM_ANY ||
+ FStrEq( pPlayerName, player->GetPlayerName() ) ||
+ ( player->GetTeamNumber() == iTeam ) ||
+ ( player->GetTeamNumber() == iTeam ) )
+ {
+ CTakeDamageInfo info( player, player, 9999999.9f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );
+ player->TakeDamage( info );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CMD_BotWarpTeamToMe( void )
+{
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ if ( !player )
+ return;
+
+ CTeam *myTeam = player->GetTeam();
+ for( int i=0; i<myTeam->GetNumPlayers(); ++i )
+ {
+ if ( !myTeam->GetPlayer(i)->IsAlive() )
+ continue;
+
+ myTeam->GetPlayer(i)->SetAbsOrigin( player->GetAbsOrigin() );
+ }
+}
+static ConCommand tf_bot_warp_team_to_me( "tf_bot_warp_team_to_me", CMD_BotWarpTeamToMe, "", FCVAR_GAMEDLL | FCVAR_CHEAT );
+
+
+//-----------------------------------------------------------------------------------------------------
+IMPLEMENT_INTENTION_INTERFACE( CTFBot, CTFBotMainAction );
+
+
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( tf_bot, CTFBot );
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Allocate a bot and bind it to the edict
+ */
+CBasePlayer *CTFBot::AllocatePlayerEntity( edict_t *edict, const char *playerName )
+{
+ CBasePlayer::s_PlayerEdict = edict;
+ return static_cast< CBasePlayer * >( CreateEntityByName( "tf_bot" ) );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseFireButton();
+ return;
+ }
+
+ BaseClass::PressFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressAltFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseAltFireButton();
+ return;
+ }
+
+ BaseClass::PressAltFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressSpecialFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseAltFireButton();
+ return;
+ }
+
+ BaseClass::PressSpecialFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+class CCountClassMembers
+{
+public:
+ CCountClassMembers( const CTFBot *me, int teamID )
+ {
+ m_me = me;
+ m_myTeam = teamID;
+ m_teamSize = 0;
+
+ for( int i=0; i<TF_LAST_NORMAL_CLASS; ++i )
+ m_count[i] = 0;
+ }
+
+ bool operator() ( CBasePlayer *basePlayer )
+ {
+ CTFPlayer *player = (CTFPlayer *)basePlayer;
+
+ if ( player->GetTeamNumber() != m_myTeam )
+ return true;
+
+ ++m_teamSize;
+
+ if ( m_me->IsSelf( player ) )
+ return true;
+
+ ++m_count[ player->GetDesiredPlayerClassIndex() ];
+
+ return true;
+ }
+
+ const CTFBot *m_me;
+ int m_myTeam;
+ int m_count[ TF_LAST_NORMAL_CLASS+1 ];
+ int m_teamSize;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * NOTE: Assumes bot's difficulty has been set, and the bot is on a team.
+ */
+const char *CTFBot::GetNextSpawnClassname( void ) const
+{
+ struct ClassSelectionInfo
+ {
+ int m_class;
+ int m_minTeamSizeToSelect; // team must have this many members to choose this class
+ int m_countPerTeamSize; // must have 1 Medic for each 4 team members, for example
+ int m_minLimit; // minimum that must be present (once other constraints are met)
+ int m_maxLimit[ NUM_DIFFICULTY_LEVELS ]; // maximum that can be present (-1 for infinite)
+ };
+
+ const int NoLimit = -1;
+
+ static ClassSelectionInfo defenseRoster[] =
+ {
+ { TF_CLASS_ENGINEER, 0, 4, 1, { 1, 2, 3, 3 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } },
+ { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } },
+ { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } },
+ { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } },
+ { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ static ClassSelectionInfo offenseRoster[] =
+ {
+ { TF_CLASS_SCOUT, 0, 0, 1, { 3, 3, 3, 3 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns
+ { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } },
+ { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } },
+ { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } },
+ { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } },
+ { TF_CLASS_ENGINEER, 5, 0, 0, { 1, 1, 1, 1 } },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ static ClassSelectionInfo compRoster[] =
+ {
+ { TF_CLASS_SCOUT, 0, 0, 0, { 0, 0, 2, 2 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { 0, 0, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 0, 0, 2, 2 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns
+ { TF_CLASS_PYRO, 0, -1 },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 0, 0, 2, 2 } },
+ { TF_CLASS_MEDIC, 1, 0, 1, { 0, 0, 1, 1 } },
+ { TF_CLASS_SNIPER, 0, -1 },
+ { TF_CLASS_SPY, 0, -1 },
+ { TF_CLASS_ENGINEER, 0, -1 },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ // if we are an engineer with an active sentry or teleporters, don't switch
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ if ( const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_SENTRYGUN ) ||
+ const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) )
+ {
+ return "engineer";
+ }
+ }
+
+ // count classes in use by my team, not including me
+ CCountClassMembers currentRoster( this, GetTeamNumber() );
+ ForEachPlayer( currentRoster );
+
+ // assume offense
+ ClassSelectionInfo *desiredRoster = offenseRoster;
+
+ if ( TFGameRules()->IsMatchTypeCompetitive() )
+ {
+ desiredRoster = compRoster;
+ }
+ else if ( TFGameRules()->IsInKothMode() )
+ {
+ CTeamControlPoint *point = GetMyControlPoint();
+ if ( point )
+ {
+ if ( GetTeamNumber() == ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) )
+ {
+ // defend our point
+ desiredRoster = defenseRoster;
+ }
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector );
+
+ CUtlVector< CTeamControlPoint * > defendVector;
+ TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector );
+
+ // if we have any points we can capture, try to do so
+ if ( captureVector.Count() > 0 || defendVector.Count() == 0 )
+ {
+ desiredRoster = offenseRoster;
+ }
+ else
+ {
+ desiredRoster = defenseRoster;
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ if ( GetTeamNumber() == TF_TEAM_RED )
+ {
+ desiredRoster = defenseRoster;
+ }
+ }
+
+ // build vector of classes we can pick from
+ CUtlVector< int > desiredClassVector;
+ CUtlVector< int > allowedClassForBotRosterVector;
+
+ for( int i=0; desiredRoster[ i ].m_class != TF_CLASS_UNDEFINED; ++i )
+ {
+ ClassSelectionInfo *desiredClassInfo = &desiredRoster[ i ];
+
+ if ( TFGameRules()->CanBotChooseClass( const_cast< CTFBot * >( this ), desiredClassInfo->m_class ) == false )
+ {
+ // not allowed to use this class
+ continue;
+ }
+ // just in case we hit the class limits, we want to make sure we select a class that is allowed
+ allowedClassForBotRosterVector.AddToTail( desiredClassInfo->m_class );
+
+ if ( currentRoster.m_teamSize < desiredClassInfo->m_minTeamSizeToSelect )
+ {
+ // team is too small to choose this class
+ continue;
+ }
+
+ // check limits
+ if ( currentRoster.m_count[ desiredClassInfo->m_class ] < desiredClassInfo->m_minLimit )
+ {
+ // below required limit - choose only this class
+ desiredClassVector.RemoveAll();
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ break;
+ }
+
+ int maxLimit = desiredClassInfo->m_maxLimit[ (int)clamp( GetDifficulty(), CTFBot::EASY, CTFBot::EXPERT ) ];
+
+ if ( maxLimit > NoLimit && currentRoster.m_count[ desiredClassInfo->m_class ] >= maxLimit )
+ {
+ // at or above limit for this class
+ continue;
+ }
+
+ if ( desiredClassInfo->m_countPerTeamSize > 0 )
+ {
+ // how many of this class should there be at the given "per" count
+ int maxCountPer = currentRoster.m_teamSize / desiredClassInfo->m_countPerTeamSize;
+ if ( currentRoster.m_count[ desiredClassInfo->m_class ] - desiredClassInfo->m_minTeamSizeToSelect < maxCountPer )
+ {
+ // below required limit - choose only this class
+ desiredClassVector.RemoveAll();
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ break;
+ }
+ }
+
+ // valid class to choose
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ }
+
+ if ( desiredClassVector.Count() == 0 )
+ {
+ if ( allowedClassForBotRosterVector.Count() == 0 )
+ {
+ // nothing available
+ Warning( "TFBot unable to choose a class, defaulting to 'auto'\n" );
+ return "auto";
+ }
+ else
+ {
+ desiredClassVector = allowedClassForBotRosterVector;
+ }
+ }
+
+ int which = RandomInt( 0, desiredClassVector.Count()-1 );
+
+ // if we need to destroy a sentry, pick a class that can do so
+ if ( GetEnemySentry() )
+ {
+ // best sentry demolitions
+ int demoman = desiredClassVector.Find( TF_CLASS_DEMOMAN );
+ if ( demoman >= 0 )
+ {
+ which = demoman;
+ }
+ else
+ {
+ // next best sentry demolitions
+ int spy = desiredClassVector.Find( TF_CLASS_SPY );
+ if ( spy >= 0 )
+ {
+ which = spy;
+ }
+ else
+ {
+ // good sentry demolitions
+ int soldier = desiredClassVector.Find( TF_CLASS_SOLDIER );
+ if ( soldier >= 0 )
+ {
+ which = soldier;
+ }
+ }
+ }
+ }
+
+ TFPlayerClassData_t *classData = GetPlayerClassData( desiredClassVector[ which ] );
+ if ( classData )
+ {
+ return classData->m_szClassName;
+ }
+
+ Warning( "TFBot unable to get data for desired class, defaulting to 'auto'\n" );
+ return "auto";
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::CTFBot()
+{
+ m_body = new CTFBotBody( this );
+ m_locomotor = new CTFBotLocomotion( this );
+ m_vision = new CTFBotVision( this );
+ ALLOCATE_INTENTION_INTERFACE( CTFBot );
+
+ m_spawnArea = NULL;
+ m_weaponRestrictionFlags = 0;
+ m_attributeFlags = 0;
+ m_homeArea = NULL;
+ m_squad = NULL;
+ m_didReselectClass = false;
+ m_enemySentry = NULL;
+ m_spotWhereEnemySentryLastInjuredMe = vec3_origin;
+ m_isLookingAroundForEnemies = true;
+ m_behaviorFlags = 0;
+ m_attentionFocusEntity = NULL;
+ m_noisyTimer.Invalidate();
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ m_difficulty = CTFBot::EASY;
+ }
+ else
+ {
+ m_difficulty = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT );
+ }
+
+ m_actionPoint = NULL;
+ m_proxy = NULL;
+ m_spawner = NULL;
+
+ m_myControlPoint = NULL;
+
+ SetMission( NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ SetMissionTarget( NULL );
+ m_missionString.Clear();
+
+ m_fModelScaleOverride = -1.0f;
+ m_maxVisionRangeOverride = -1.0f;
+ m_squadFormationError = 0.0f;
+
+ m_hFollowingFlagTarget = NULL;
+
+ SetShouldQuickBuild( false );
+ SetAutoJump( 0.f, 0.f );
+
+ ClearSniperSpots();
+
+ ListenForGameEvent( "teamplay_point_startcapture" );
+ ListenForGameEvent( "teamplay_point_captured" );
+ ListenForGameEvent( "teamplay_round_win" );
+ ListenForGameEvent( "teamplay_flag_event" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::~CTFBot()
+{
+ // delete Intention first, since destruction of Actions may access other components
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_body )
+ delete m_body;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+
+ if ( m_vision )
+ delete m_vision;
+
+ m_suspectedSpyVector.PurgeAndDeleteElements();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Spawn()
+{
+ BaseClass::Spawn();
+
+ m_spawnArea = NULL;
+ m_justLostPointTimer.Invalidate();
+ m_squad = NULL;
+ m_didReselectClass = false;
+ m_isLookingAroundForEnemies = true;
+ m_attentionFocusEntity = NULL;
+
+ m_suspectedSpyVector.PurgeAndDeleteElements();
+ m_knownSpyVector.RemoveAll();
+ m_delayedNoticeVector.RemoveAll();
+
+ m_myControlPoint = NULL;
+ ClearSniperSpots();
+ ClearTags();
+
+ m_hFollowingFlagTarget = NULL;
+
+ m_requiredWeaponStack.Clear();
+ SetShouldQuickBuild( false );
+
+ SetSquadFormationError( 0.0f );
+ SetBrokenFormation( false );
+
+ GetVisionInterface()->ForgetAllKnownEntities();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::SetMission( MissionType mission, bool resetBehaviorSystem )
+{
+ SetPrevMission( m_mission );
+ m_mission = mission;
+
+ if ( resetBehaviorSystem )
+ {
+ // reset the behavior system to start the given mission
+ GetIntentionInterface()->Reset();
+ }
+
+ // Temp hack - some missions play an idle loop
+ if ( m_mission > NO_MISSION )
+ {
+ StartIdleSound();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PhysicsSimulate( void )
+{
+ BaseClass::PhysicsSimulate();
+
+ if ( m_spawnArea == NULL )
+ {
+ m_spawnArea = GetLastKnownArea();
+ }
+
+ if ( HasAttribute( CTFBot::ALWAYS_CRIT ) && !m_Shared.InCond( TF_COND_CRITBOOSTED_USER_BUFF ) )
+ {
+ m_Shared.AddCond( TF_COND_CRITBOOSTED_USER_BUFF );
+ }
+
+ // force my speed to be recalculated to keep squad together and restore speed afterwards
+ TeamFortress_SetSpeed();
+
+ if ( IsInASquad() )
+ {
+ if ( GetSquad()->GetMemberCount() <= 1 || GetSquad()->GetLeader() == NULL )
+ {
+ // squad has collapsed - disband it
+ LeaveSquad();
+ }
+ }
+
+
+ // If we're dead, choose a new class.
+ // We need to do this outside of the behavior system, since changing class can
+ // sometimes force an immediate respawn, which will destroy the bot's existing actions out from under it.
+ if ( !IsAlive() && !m_didReselectClass && tf_bot_keep_class_after_death.GetBool() == false && TFGameRules()->CanBotChangeClass( this ) )
+ {
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ return;
+
+ const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? GetNextSpawnClassname() : tf_bot_force_class.GetString();
+
+ HandleCommand_JoinClass( classname );
+
+ m_didReselectClass = true;
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Touch( CBaseEntity *pOther )
+{
+ BaseClass::Touch( pOther );
+
+ CTFPlayer *them = ToTFPlayer( pOther );
+ if ( them && IsEnemy( them ) )
+ {
+ if ( them->m_Shared.IsStealthed() || them->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ // bumped a spy - they are discovered!
+ if ( TFGameRules()->IsMannVsMachineMode() ) // we have to build up to knowing that they are a spy in MvM
+ {
+ SuspectSpy( them );
+ }
+ else
+ {
+ RealizeSpy( them );
+ }
+ }
+
+ // always notice if we bump an enemy
+ TheNextBots().OnWeaponFired( them, them->GetActiveTFWeapon() );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Avoid penetrating teammates
+void CTFBot::AvoidPlayers( CUserCmd *pCmd )
+{
+ // Turn off the avoid player code.
+ if ( !tf_avoidteammates.GetBool() || !tf_avoidteammates_pushaway.GetBool() )
+ return;
+
+ Vector forward, right;
+ EyeVectors( &forward, &right );
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ Vector avoidVector = vec3_origin;
+
+ float tooClose = 50.0f;
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ // bots stay farther apart in MvM mode
+ tooClose = 150.0f;
+ }
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *them = playerVector[i];
+
+ if ( IsSelf( them ) )
+ {
+ continue;
+ }
+
+ if ( HasTheFlag() )
+ {
+ // Don't push around the flag (bomb) carrier.
+ // We need this for MvM mode so friendly bots don't
+ // move the bomb jumper and cause him to restart.
+ continue;
+ }
+
+ if ( IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ if ( !them->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // medics only avoid other medics, so they stay with their patient
+ continue;
+ }
+ }
+ else if ( IsInASquad() )
+ {
+ // if I'm a non-Medic in a Squad, I'm part of a formation
+ continue;
+ }
+
+ Vector between = GetAbsOrigin() - them->GetAbsOrigin();
+ if ( between.IsLengthLessThan( tooClose ) )
+ {
+ float range = between.NormalizeInPlace();
+
+ avoidVector += ( 1.0f - ( range / tooClose ) ) * between;
+ }
+ }
+
+ if ( avoidVector.IsZero() )
+ {
+ m_Shared.SetSeparation( false );
+ m_Shared.SetSeparationVelocity( vec3_origin );
+ return;
+ }
+
+ avoidVector.NormalizeInPlace();
+
+ m_Shared.SetSeparation( true );
+
+ const float maxSpeed = 50.0f;
+ m_Shared.SetSeparationVelocity( avoidVector * maxSpeed );
+
+ float ahead = maxSpeed * DotProduct( forward, avoidVector );
+ float side = maxSpeed * DotProduct( right, avoidVector );
+
+ pCmd->forwardmove += ahead;
+ pCmd->sidemove += side;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::UpdateOnRemove( void )
+{
+ StopIdleSound();
+
+ BaseClass::UpdateOnRemove();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+int CTFBot::ShouldTransmit( const CCheckTransmitInfo *pInfo )
+{
+ if ( HasAttribute( USE_BOSS_HEALTH_BAR ) )
+ {
+ return FL_EDICT_ALWAYS;
+ }
+
+ return BaseClass::ShouldTransmit( pInfo );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance /*= false*/ )
+{
+ BaseClass::ChangeTeam( iTeamNum, bAutoTeam, bSilent, bAutoBalance );
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ SetPrevMission( CTFBot::NO_MISSION );
+ ClearAllAttributes();
+ // Clear Sound
+ StopIdleSound();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::ShouldGib( const CTakeDamageInfo &info )
+{
+ // only gib giant/miniboss
+ if ( TFGameRules()->IsMannVsMachineMode() && ( IsMiniBoss() || GetModelScale() > 1.f ) )
+ {
+ return true;
+ }
+
+ return BaseClass::ShouldGib( info );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsAllowedToPickUpFlag( void ) const
+{
+ if ( !BaseClass::IsAllowedToPickUpFlag() )
+ {
+ return false;
+ }
+
+ // only the leader of a squad can pick up the flag
+ if ( IsInASquad() && !GetSquad()->IsLeader( const_cast< CTFBot * >( this ) ) )
+ return false;
+
+ // mission bots can't pick up the flag
+ return !IsOnAnyMission();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::InitClass( void )
+{
+ BaseClass::InitClass();
+}
+
+void CTFBot::ModifyMaxHealth( int nNewMaxHealth, bool bSetCurrentHealth /*= true*/, bool bAllowModelScaling /*= true*/ )
+{
+ if ( GetMaxHealth() != nNewMaxHealth )
+ {
+ static CSchemaAttributeDefHandle pAttrDef_HiddenMaxHealthNonBuffed( "hidden maxhealth non buffed" );
+ if ( !pAttrDef_HiddenMaxHealthNonBuffed )
+ {
+ Warning( "TFBotSpawner: Invalid attribute 'hidden maxhealth non buffed'\n" );
+ }
+ else
+ {
+ CAttributeList *pAttrList = GetAttributeList();
+ if ( pAttrList )
+ {
+ pAttrList->SetRuntimeAttributeValue( pAttrDef_HiddenMaxHealthNonBuffed, nNewMaxHealth - GetMaxHealth() );
+ }
+ }
+ }
+
+ if ( bSetCurrentHealth )
+ {
+ SetHealth( nNewMaxHealth );
+ }
+
+ if ( bAllowModelScaling && IsMiniBoss() )
+ {
+ SetModelScale( m_fModelScaleOverride > 0.0f ? m_fModelScaleOverride : tf_mvm_miniboss_scale.GetFloat() );
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Invoked when a game event occurs
+ */
+void CTFBot::FireGameEvent( IGameEvent *event )
+{
+ const char *eventName = event->GetName();
+
+ if ( FStrEq( eventName, "teamplay_point_captured" ) )
+ {
+ ClearMyControlPoint();
+
+ int whoCapped = event->GetInt( "team" );
+ int pointID = event->GetInt( "cp" );
+
+ if ( whoCapped == GetTeamNumber() )
+ {
+ OnTerritoryCaptured( pointID );
+ }
+ else
+ {
+ OnTerritoryLost( pointID );
+
+ m_justLostPointTimer.Start( RandomFloat( 10.0f, 20.0f ) );
+ }
+ }
+ else if ( FStrEq( eventName, "teamplay_point_startcapture" ) )
+ {
+ int pointID = event->GetInt( "cp" );
+
+ OnTerritoryContested( pointID );
+ }
+ else if ( FStrEq( eventName, "teamplay_flag_event" ) )
+ {
+ if ( event->GetInt( "eventtype" ) == TF_FLAGEVENT_PICKUP )
+ {
+ int iPlayer = event->GetInt( "player" );
+ if ( iPlayer == entindex() )
+ {
+ // I just picked up the flag
+ OnPickUp( NULL, NULL );
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Event_Killed( const CTakeDamageInfo &info )
+{
+ BaseClass::Event_Killed( info );
+
+ if ( HasProxy() )
+ {
+ GetProxy()->OnKilled();
+ }
+
+ // announce Spies
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS );
+
+ int spyCount = 0;
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ ++spyCount;
+ }
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mission_update" );
+ if ( event )
+ {
+ event->SetInt( "class", TF_CLASS_SPY );
+ event->SetInt( "count", spyCount );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ else if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // in MVM, when an engineer dies, we need to decouple his objects so they stay alive when his bot slot gets recycled
+ while ( GetObjectCount() > 0 )
+ {
+ // set to not have owner
+ CBaseObject *pObject = GetObject( 0 );
+ if ( pObject )
+ {
+ pObject->SetOwnerEntity( NULL );
+ pObject->SetBuilder( NULL );
+ }
+ RemoveObject( pObject );
+ }
+
+ // unown engineer nest if owned any
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity* pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] );
+ if ( pHint->GetOwnerEntity() == this )
+ {
+ pHint->SetOwnerEntity( NULL );
+ }
+ }
+
+ CUtlVector< CTFPlayer* > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS );
+ bool bShouldAnnounceLastEngineerBotDeath = HasAttribute( CTFBot::TELEPORT_TO_HINT );
+ if ( bShouldAnnounceLastEngineerBotDeath )
+ {
+ for ( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( playerVector[i] != this && playerVector[i]->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ bShouldAnnounceLastEngineerBotDeath = false;
+ break;
+ }
+ }
+ }
+
+ if ( bShouldAnnounceLastEngineerBotDeath )
+ {
+ bool bEngineerTeleporterInTheWorld = false;
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->GetType() == OBJ_TELEPORTER && pObj->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ bEngineerTeleporterInTheWorld = true;
+ }
+ }
+
+ if ( bEngineerTeleporterInTheWorld )
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead_But_Not_Teleporter" );
+ }
+ else
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead" );
+ }
+ }
+ }
+
+ // remove this bot from following flag
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+ flag->RemoveFollower( this );
+ }
+ }
+ } // MvM
+
+ if ( HasSpawner() )
+ {
+ GetSpawner()->OnBotKilled( this );
+ }
+
+ if ( IsInASquad() )
+ {
+ LeaveSquad();
+ }
+
+ CTFNavArea *lastArea = (CTFNavArea *)GetLastKnownArea();
+ if ( lastArea )
+ {
+ // remove us from old visible set
+ NavAreaCollector wasVisible;
+ lastArea->ForAllPotentiallyVisibleAreas( wasVisible );
+
+ int i;
+ for( i=0; i<wasVisible.m_area.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)wasVisible.m_area[i];
+ area->RemovePotentiallyVisibleActor( this );
+ }
+ }
+
+
+ if ( info.GetInflictor() && info.GetInflictor()->GetTeamNumber() != GetTeamNumber() )
+ {
+ CObjectSentrygun *sentrygun = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
+
+ if ( sentrygun )
+ {
+ // we were killed by an enemy sentry - remember it
+ RememberEnemySentry( sentrygun, GetAbsOrigin() );
+ }
+ }
+
+ StopIdleSound();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTeamControlPoint *CTFBot::SelectPointToCapture( CUtlVector< CTeamControlPoint * > *captureVector ) const
+{
+ if ( !captureVector || captureVector->Count() == 0 )
+ {
+ return NULL;
+ }
+
+ if ( captureVector->Count() == 1 )
+ {
+ // only one choice
+ return captureVector->Element(0);
+ }
+
+ // if we're capturing a point, stay on it
+ if ( const_cast< CTFBot * >( this )->IsCapturingPoint() )
+ {
+ CTriggerAreaCapture *trigger = const_cast< CTFBot * >( this )->GetControlPointStandingOn();
+ if ( trigger )
+ {
+ return trigger->GetControlPoint();
+ }
+ }
+
+ // if we're near a point that is being captured, go help (in the event multiple points are being simultaneously captured)
+ CTeamControlPoint *closestPoint = SelectClosestControlPointByTravelDistance( captureVector );
+ if ( closestPoint )
+ {
+ bool alwaysUseClosest = false;
+
+#ifdef STAGING_ONLY
+ alwaysUseClosest = TFGameRules() && TFGameRules()->IsBountyMode();
+#endif // STAGING_ONLY
+
+ if ( IsPointBeingCaptured( closestPoint ) || alwaysUseClosest )
+ {
+ return closestPoint;
+ }
+ }
+
+ // if any point is being captured by our team, go help
+ for( int i=0; i<captureVector->Count(); ++i )
+ {
+ CTeamControlPoint *point = captureVector->Element(i);
+
+ if ( IsPointBeingCaptured( point ) )
+ {
+ return point;
+ }
+ }
+
+ // no points are currently being captured - pick the point with the least combat
+ CTeamControlPoint *safestPoint = NULL;
+ float safestPointCombat = FLT_MAX;
+ bool areAllPointsCombatFree = true;
+
+ for( int i=0; i<captureVector->Count(); ++i )
+ {
+ CTeamControlPoint *point = captureVector->Element(i);
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ if ( !pointArea )
+ {
+ continue;
+ }
+
+ float combat = pointArea->GetCombatIntensity();
+
+ const float minCombat = 0.1f;
+ if ( combat > minCombat )
+ {
+ areAllPointsCombatFree = false;
+ }
+
+ if ( combat < safestPointCombat )
+ {
+ safestPoint = point;
+ safestPointCombat = combat;
+ }
+ }
+
+ // if no points are in combat, pick a random point
+ if ( areAllPointsCombatFree )
+ {
+ const float decisionPeriod = 60.0f;
+ int which = captureVector->Count() * TransientlyConsistentRandomValue( decisionPeriod );
+ which = clamp( which, 0, captureVector->Count()-1 );
+
+ return captureVector->Element( which );
+ }
+
+ // choose the point with the least combat
+ return safestPoint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTeamControlPoint *CTFBot::SelectPointToDefend( CUtlVector< CTeamControlPoint * > *defendVector ) const
+{
+ if ( defendVector && defendVector->Count() > 0 )
+ {
+ if ( HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) )
+ {
+ return SelectClosestControlPointByTravelDistance( defendVector );
+ }
+
+ return defendVector->Element( RandomInt( 0, defendVector->Count()-1 ) );
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the point we have decided to capture or defend
+ */
+CTeamControlPoint *CTFBot::GetMyControlPoint( void ) const
+{
+ if ( m_myControlPoint != NULL && !m_evaluateControlPointTimer.IsElapsed() )
+ {
+ return m_myControlPoint;
+ }
+
+ m_evaluateControlPointTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector );
+
+ CUtlVector< CTeamControlPoint * > defendVector;
+ TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector );
+
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) || IsPlayerClass( TF_CLASS_SNIPER ) || HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) )
+ {
+ // engineers always try to defend first
+ if ( defendVector.Count() > 0 )
+ {
+ m_myControlPoint = SelectPointToDefend( &defendVector );
+ return m_myControlPoint;
+ }
+ }
+
+ // if we have a point we can capture - do it
+ m_myControlPoint = SelectPointToCapture( &captureVector );
+
+ if ( m_myControlPoint == NULL )
+ {
+ // otherwise, defend our point(s) from capture
+ m_myControlPoint = SelectPointToDefend( &defendVector );
+ }
+
+ return m_myControlPoint;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return flag we want to fetch
+CCaptureFlag *CTFBot::GetFlagToFetch( void ) const
+{
+ CUtlVector<CCaptureFlag *> flagsVector;
+ int nCarriedFlags = 0;
+
+ // MvM Engineer bot never pick up a flag
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS && IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return NULL;
+ }
+
+ if( HasAttribute( CTFBot::IGNORE_FLAG ) )
+ {
+ return NULL;
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() && HasFlagTaget() )
+ {
+ return GetFlagTarget();
+ }
+ }
+
+ // Collect flags
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+
+ if ( flag->IsDisabled() )
+ continue;
+
+ // If I'm carrying a flag, look for mine and early-out
+ if ( HasTheFlag() )
+ {
+ if ( flag->GetOwnerEntity() == this )
+ {
+ return flag;
+ }
+ }
+
+ switch( flag->GetType() )
+ {
+ case TF_FLAGTYPE_CTF:
+ if ( flag->GetTeamNumber() == GetEnemyTeam( GetTeamNumber() ) )
+ {
+ // we want to steal the other team's flag
+ flagsVector.AddToTail( flag );
+ }
+ break;
+
+ case TF_FLAGTYPE_ATTACK_DEFEND:
+ case TF_FLAGTYPE_TERRITORY_CONTROL:
+ case TF_FLAGTYPE_INVADE:
+ if ( flag->GetTeamNumber() != GetEnemyTeam( GetTeamNumber() ) )
+ {
+ // we want to move our team's flag or a neutral flag
+ flagsVector.AddToTail( flag );
+ }
+ break;
+ }
+
+ if ( flag->IsStolen() )
+ {
+ nCarriedFlags++;
+ }
+ }
+
+ CCaptureFlag *pClosestFlag = NULL;
+ float flClosestFlagDist = FLT_MAX;
+ CCaptureFlag *pClosestUncarriedFlag = NULL;
+ float flClosestUncarriedFlagDist = FLT_MAX;
+
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ int nMinFollower = INT_MAX;
+
+ FOR_EACH_VEC( flagsVector, i )
+ {
+ CCaptureFlag *pFlag = flagsVector[i];
+ if ( pFlag )
+ {
+ // find the one which needs the most love
+ if ( pFlag->GetNumFollowers() < nMinFollower )
+ {
+ nMinFollower = pFlag->GetNumFollowers();
+
+ pClosestFlag = NULL;
+ flClosestFlagDist = FLT_MAX;
+ pClosestUncarriedFlag = NULL;
+ flClosestUncarriedFlagDist = FLT_MAX;
+ }
+
+ if ( pFlag->GetNumFollowers() == nMinFollower )
+ {
+ // Find the closest
+ float flDist = ( pFlag->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( flDist < flClosestFlagDist )
+ {
+ pClosestFlag = pFlag;
+ flClosestFlagDist = flDist;
+ }
+
+ // Find the closest uncarried
+ if ( nCarriedFlags < flagsVector.Count() && !pFlag->IsStolen() )
+ {
+ if ( flDist < flClosestUncarriedFlagDist )
+ {
+ pClosestUncarriedFlag = flagsVector[i];
+ flClosestUncarriedFlagDist = flDist;
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ FOR_EACH_VEC( flagsVector, i )
+ {
+ if ( flagsVector[i] )
+ {
+ // Find the closest
+ float flDist = ( flagsVector[i]->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( flDist < flClosestFlagDist )
+ {
+ pClosestFlag = flagsVector[i];
+ flClosestFlagDist = flDist;
+ }
+
+ // Find the closest uncarried
+ if ( nCarriedFlags < flagsVector.Count() && !flagsVector[i]->IsStolen() )
+ {
+ if ( flDist < flClosestUncarriedFlagDist )
+ {
+ pClosestUncarriedFlag = flagsVector[i];
+ flClosestUncarriedFlagDist = flDist;
+ }
+ }
+ }
+ }
+ }
+
+ // If we have an uncarried flag, prioritize
+ if ( pClosestUncarriedFlag )
+ return pClosestUncarriedFlag;
+
+ return pClosestFlag;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return capture zone for our flag(s)
+CCaptureZone *CTFBot::GetFlagCaptureZone( void ) const
+{
+ for( int i=0; i<ICaptureZoneAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureZone *zone = static_cast< CCaptureZone* >( ICaptureZoneAutoList::AutoList()[i] );
+ if ( zone->GetTeamNumber() == GetTeamNumber() )
+ {
+ return zone;
+ }
+ }
+
+ return NULL;
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ClearMyControlPoint( void )
+{
+ m_myControlPoint = NULL;
+ m_evaluateControlPointTimer.Invalidate();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return true if no enemy has contested any point yet
+ */
+bool CTFBot::AreAllPointsUncontestedSoFar( void ) const
+{
+ CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( master )
+ {
+ for( int i=0; i<master->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = master->GetControlPoint( i );
+
+ if ( point && point->HasBeenContested() )
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if the given point is being captured
+bool CTFBot::IsPointBeingCaptured( CTeamControlPoint *point ) const
+{
+ if ( point == NULL )
+ return false;
+
+ if ( point->LastContestedAt() > 0.0f && ( gpGlobals->curtime - point->LastContestedAt() ) < 5.0f )
+ {
+ // the point is, or was very recently, contested
+ return true;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if any point is being captured
+bool CTFBot::IsAnyPointBeingCaptured( void ) const
+{
+ CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( master )
+ {
+ for( int i=0; i<master->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = master->GetControlPoint( i );
+
+ if ( IsPointBeingCaptured( point ) )
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if we are within a short travel distance of the current point
+bool CTFBot::IsNearPoint( CTeamControlPoint *point ) const
+{
+ CTFNavArea *myArea = GetLastKnownArea();
+
+ if ( !myArea || !point )
+ {
+ return false;
+ }
+
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ if ( !pointArea )
+ {
+ return false;
+ }
+
+ float travelToPoint = fabs( myArea->GetIncursionDistance( GetTeamNumber() ) - pointArea->GetIncursionDistance( GetTeamNumber() ) );
+
+ return travelToPoint < tf_bot_near_point_travel_distance.GetFloat();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return time left to capture the point before we lose the game
+float CTFBot::GetTimeLeftToCapture( void ) const
+{
+ if ( TFGameRules()->IsInKothMode() )
+ {
+ if ( TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) ) )
+ {
+ return TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) )->GetTimeRemaining();
+ }
+ }
+ else if ( TFGameRules()->GetActiveRoundTimer() )
+ {
+ return TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining();
+ }
+
+ return 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Do internal setup when control point changes
+void CTFBot::SetupSniperSpotAccumulation( void )
+{
+ VPROF_BUDGET( "CTFBot::SetupSniperSpotAccumulation", "NextBot" );
+
+ CBaseEntity *goalEntity = NULL;
+
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ // try to find a payload cart to guard
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToPush( GetTeamNumber() );
+
+ if ( !trainWatcher )
+ {
+ trainWatcher = TFGameRules()->GetPayloadToBlock( GetTeamNumber() );
+ }
+
+ if ( trainWatcher )
+ {
+ goalEntity = trainWatcher->GetTrainEntity();
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ goalEntity = GetMyControlPoint();
+ }
+
+ if ( !goalEntity )
+ {
+ ClearSniperSpots();
+ return;
+ }
+
+ if ( goalEntity == m_snipingGoalEntity )
+ {
+ // if goal has moved too much (ie: payload cart), recompute our spots
+ Vector toGoal = m_snipingGoalEntity->WorldSpaceCenter() - m_lastSnipingGoalEntityPosition;
+
+ if ( toGoal.IsLengthLessThan( tf_bot_sniper_goal_entity_move_tolerance.GetFloat() ) )
+ {
+ // already set up
+ return;
+ }
+ }
+
+ ClearSniperSpots();
+
+ int myTeam = GetTeamNumber();
+ int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE;
+
+ bool isDefendingPoint = false;
+ CTFNavArea *goalEntityArea = NULL;
+
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ // the cart is owned by the invaders
+ isDefendingPoint = ( goalEntity->GetTeamNumber() != myTeam );
+ goalEntityArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( goalEntity->WorldSpaceCenter(), GETNAVAREA_CHECK_GROUND, 500.0f );
+ }
+ else
+ {
+ isDefendingPoint = ( GetMyControlPoint()->GetOwner() == myTeam );
+ goalEntityArea = TheTFNavMesh()->GetControlPointCenterArea( GetMyControlPoint()->GetPointIndex() );
+ }
+
+ // we are sniping a different control point - setup for new point accumulation
+ m_sniperVantageAreaVector.RemoveAll();
+ m_sniperTheaterAreaVector.RemoveAll();
+
+ if ( !goalEntityArea )
+ {
+ return;
+ }
+
+ for( int i=0; i<TheNavAreas.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheNavAreas[i];
+
+ if ( !area->IsReachableByTeam( myTeam ) || !area->IsReachableByTeam( enemyTeam ) )
+ {
+ continue;
+ }
+
+ if ( area->GetIncursionDistance( enemyTeam ) <= goalEntityArea->GetIncursionDistance( enemyTeam ) )
+ {
+ m_sniperTheaterAreaVector.AddToTail( area );
+ }
+
+ // if this is my point, I can stand on it, or go a bit beyond it
+ float myIncursionTolerance = tf_bot_sniper_spot_point_tolerance.GetFloat();
+
+ if ( !isDefendingPoint )
+ {
+ // not my point, keep back from it a bit
+ myIncursionTolerance *= -1.0f;
+ }
+
+ if ( area->GetIncursionDistance( myTeam ) <= goalEntityArea->GetIncursionDistance( myTeam ) + myIncursionTolerance )
+ {
+ m_sniperVantageAreaVector.AddToTail( area );
+ }
+ }
+
+ m_snipingGoalEntity = goalEntity;
+ m_lastSnipingGoalEntityPosition = goalEntity->WorldSpaceCenter();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Randomly sample points within candidate areas to find good sniping positions
+void CTFBot::AccumulateSniperSpots( void )
+{
+ VPROF_BUDGET( "CTFBot::AccumulateSniperSpots", "NextBot" );
+
+ SetupSniperSpotAccumulation();
+
+ if ( m_sniperVantageAreaVector.Count() == 0 || m_sniperTheaterAreaVector.Count() == 0 )
+ {
+ // retry every so often to catch cases where the incursion data is invalid during setup time
+ // due to blocked/closed off areas, etc.
+ if ( m_retrySniperSpotSetupTimer.IsElapsed() )
+ {
+ // retry
+ ClearSniperSpots();
+ }
+
+ return;
+ }
+
+ SniperSpotInfo info;
+
+ for( int count=0; count<tf_bot_sniper_spot_search_count.GetInt(); ++count )
+ {
+ // pick a random vantage area to sample
+ int which = RandomInt( 0, m_sniperVantageAreaVector.Count()-1 );
+ info.m_vantageArea = m_sniperVantageAreaVector[ which ];
+ info.m_vantageSpot = info.m_vantageArea->GetRandomPoint();
+
+ // pick a random theater area to sample
+ which = RandomInt( 0, m_sniperTheaterAreaVector.Count()-1 );
+ info.m_theaterArea = m_sniperTheaterAreaVector[ which ];
+ info.m_theaterSpot = info.m_theaterArea->GetRandomPoint();
+
+ info.m_range = ( info.m_vantageSpot - info.m_theaterSpot ).Length();
+ if ( info.m_range < tf_bot_sniper_spot_min_range.GetFloat() )
+ {
+ // not long enough sightline
+ continue;
+ }
+
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ if ( ( info.m_vantageSpot - m_sniperSpotVector[i].m_vantageSpot ).IsLengthLessThan( tf_bot_sniper_spot_epsilon.GetFloat() ) )
+ {
+ // too close to existing spot
+ continue;
+ }
+ }
+
+ Vector eyeOffset( 0, 0, 60.0f );
+ if ( IsLineOfFireClear( info.m_vantageSpot + eyeOffset, info.m_theaterSpot + eyeOffset ) )
+ {
+ // valid spot
+
+ // maximize the time it takes the enemy to get to us
+ info.m_advantage = info.m_vantageArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) ) - info.m_theaterArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) );
+
+ // if we have already maxxed out our sniper spots, replace the worst one if this is better
+ if ( m_sniperSpotVector.Count() >= tf_bot_sniper_spot_max_count.GetInt() )
+ {
+ int worst = -1;
+
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ if ( worst < 0 || m_sniperSpotVector[i].m_advantage < m_sniperSpotVector[ worst ].m_advantage )
+ {
+ worst = i;
+ }
+ }
+
+ // if our new spot is better, replace it
+ if ( info.m_advantage > m_sniperSpotVector[ worst ].m_advantage )
+ {
+ m_sniperSpotVector[ worst ] = info;
+ }
+ }
+ else
+ {
+ m_sniperSpotVector.AddToTail( info );
+ }
+ }
+ }
+
+ if ( IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ NDebugOverlay::Cross3D( m_sniperSpotVector[i].m_vantageSpot, 5.0f, 255, 0, 255, true, 0.1f );
+ NDebugOverlay::Line( m_sniperSpotVector[i].m_vantageSpot, m_sniperSpotVector[i].m_theaterSpot, 0, 200, 0, true, 0.1f );
+ }
+ }
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ClearSniperSpots( void )
+{
+ m_sniperSpotVector.RemoveAll();
+ m_sniperVantageAreaVector.RemoveAll();
+ m_sniperTheaterAreaVector.RemoveAll();
+ m_snipingGoalEntity = NULL;
+ m_retrySniperSpotSetupTimer.Start( RandomFloat( 5.0f, 10.0f ) );
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+class CCollectReachableObjects : public ISearchSurroundingAreasFunctor
+{
+public:
+ CCollectReachableObjects( const CTFBot *me, float maxRange, const CUtlVector< CHandle< CBaseEntity > > &potentialVector, CUtlVector< CHandle< CBaseEntity > > *collectionVector ) : m_potentialVector( potentialVector )
+ {
+ m_me = me;
+ m_maxRange = maxRange;
+ m_collectionVector = collectionVector;
+ }
+
+ virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ // do any of the potential objects overlap this area?
+ FOR_EACH_VEC( m_potentialVector, it )
+ {
+ CBaseEntity *obj = m_potentialVector[ it ];
+
+ if ( obj && area->Contains( obj->WorldSpaceCenter() ) )
+ {
+ // reachable - keep it
+ if ( !m_collectionVector->HasElement( obj ) )
+ {
+ m_collectionVector->AddToTail( obj );
+ }
+ }
+ }
+ return true;
+ }
+
+ virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar )
+ {
+ if ( adjArea->IsBlocked( m_me->GetTeamNumber() ) )
+ {
+ return false;
+ }
+
+ if ( travelDistanceSoFar > m_maxRange )
+ {
+ // too far away
+ return false;
+ }
+
+ return currentArea->IsContiguous( adjArea );
+ }
+
+ const CTFBot *m_me;
+ float m_maxRange;
+ const CUtlVector< CHandle< CBaseEntity > > &m_potentialVector;
+ CUtlVector< CHandle< CBaseEntity > > *m_collectionVector;
+};
+
+
+//
+// Search outwards from startSearchArea and collect all reachable objects from the given list that pass the given filter
+// Items in selectedObjectVector will be approximately sorted in nearest-to-farthest order (because of SearchSurroundingAreas)
+//
+void CTFBot::SelectReachableObjects( const CUtlVector< CHandle< CBaseEntity > > &candidateObjectVector,
+ CUtlVector< CHandle< CBaseEntity > > *selectedObjectVector,
+ const INextBotFilter &filter,
+ CNavArea *startSearchArea,
+ float maxRange ) const
+{
+ if ( startSearchArea == NULL || selectedObjectVector == NULL )
+ return;
+
+ selectedObjectVector->RemoveAll();
+
+ // filter candidate objects
+ CUtlVector< CHandle< CBaseEntity > > filteredObjectVector;
+ for( int i=0; i<candidateObjectVector.Count(); ++i )
+ {
+ if ( filter.IsSelected( candidateObjectVector[i] ) )
+ {
+ filteredObjectVector.AddToTail( candidateObjectVector[i] );
+ }
+ }
+
+ // only keep those that are reachable by us
+ CCollectReachableObjects collector( this, maxRange, filteredObjectVector, selectedObjectVector );
+ SearchSurroundingAreas( startSearchArea, collector );
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAmmoLow( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ if ( myWeapon->GetWeaponID() == TF_WEAPON_WRENCH )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) <= 0 );
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ // we never run out of ammo with a melee weapon
+ return false;
+ }
+
+ // no projectile, no ammo needed
+ const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() );
+ if ( weaponAlias )
+ {
+ WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
+ if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() )
+ {
+ CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );
+ if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE )
+ {
+ // we don't shoot anything, so we don't need ammo
+ return false;
+ }
+ }
+ }
+
+ float ratio = (float)GetAmmoCount( TF_AMMO_PRIMARY ) / (float)( const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY ) );
+
+ if ( ratio < 0.2f )
+ {
+ return true;
+ }
+ //if ( !myWeapon->HasPrimaryAmmo() && myWeapon->GetWeaponID() != TF_WEAPON_BUILDER && myWeapon->GetWeaponID() != TF_WEAPON_MEDIGUN )
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsAmmoFull( void ) const
+{
+ bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY );
+ bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY );
+
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 ) && isPrimaryFull && isSecondaryFull;
+ }
+
+ return isPrimaryFull && isSecondaryFull;
+
+/*
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 );
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ // we never run out of ammo with a melee weapon
+ return true;
+ }
+
+ // no projectile, no ammo needed
+ const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() );
+ if ( weaponAlias )
+ {
+ WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
+ if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() )
+ {
+ CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );
+ if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE )
+ {
+ // we don't shoot anything, so we don't need ammo
+ return true;
+ }
+ }
+ }
+
+ bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY );
+ bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY );
+
+ return isPrimaryFull && isSecondaryFull;
+ }
+
+ return false;
+*/
+}
+
+
+bool CTFBot::IsDormantWhenDead( void ) const
+{
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * When someone fires their weapon
+ */
+void CTFBot::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon )
+{
+ VPROF_BUDGET( "CTFBot::OnWeaponFired", "NextBot" );
+
+ BaseClass::OnWeaponFired( whoFired, weapon );
+
+ if ( !whoFired || !whoFired->IsAlive() )
+ return;
+
+ if ( IsRangeGreaterThan( whoFired, tf_bot_notice_gunfire_range.GetFloat() ) )
+ return;
+
+ int noticeChance = 100;
+
+ if ( IsQuietWeapon( (CTFWeaponBase *)weapon ) )
+ {
+ if ( IsRangeGreaterThan( whoFired, tf_bot_notice_quiet_gunfire_range.GetFloat() ) )
+ {
+ // too far away to hear in any event
+ return;
+ }
+
+ switch( GetDifficulty() )
+ {
+ case EASY:
+ noticeChance = 10;
+ break;
+
+ case NORMAL:
+ noticeChance = 30;
+ break;
+
+ case HARD:
+ noticeChance = 60;
+ break;
+
+ default:
+ case EXPERT:
+ noticeChance = 90;
+ break;
+ }
+
+ if ( IsEnvironmentNoisy() )
+ {
+ // less likely to notice with all the noise
+ noticeChance /= 2;
+ }
+ }
+ else if ( IsRangeLessThan( whoFired, 1000.0f ) )
+ {
+ // loud gunfire in our area - it's now "noisy" for a bit
+ m_noisyTimer.Start( 3.0f );
+ }
+
+ if ( RandomInt( 1, 100 ) > noticeChance )
+ {
+ return;
+ }
+
+ // notice the gunfire
+ GetVisionInterface()->AddKnownEntity( whoFired );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we match the given debug symbol
+bool CTFBot::IsDebugFilterMatch( const char *name ) const
+{
+ // player classname
+ if ( !Q_strnicmp( name, const_cast< CTFBot * >( this )->GetPlayerClass()->GetName(), Q_strlen( name ) ) )
+ {
+ return true;
+ }
+
+ return BaseClass::IsDebugFilterMatch( name );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+class CFindClosestPotentiallyVisibleAreaToPos
+{
+public:
+ CFindClosestPotentiallyVisibleAreaToPos( const Vector &pos )
+ {
+ m_pos = pos;
+ m_closeArea = NULL;
+ m_closeRangeSq = FLT_MAX;
+ }
+
+ bool operator() ( CNavArea *baseArea )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ Vector close;
+ area->GetClosestPointOnArea( m_pos, &close );
+
+ float rangeSq = ( close - m_pos ).LengthSqr();
+ if ( rangeSq < m_closeRangeSq )
+ {
+ m_closeArea = area;
+ m_closeRangeSq = rangeSq;
+ }
+
+ return true;
+ }
+
+ Vector m_pos;
+ CTFNavArea *m_closeArea;
+ float m_closeRangeSq;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+// Update our view to watch where members of the given team will be coming from
+void CTFBot::UpdateLookingAroundForIncomingPlayers( bool lookForEnemies )
+{
+ if ( !m_lookAtEnemyInvasionAreasTimer.IsElapsed() )
+ return;
+
+ const float maxLookInterval = 1.0f;
+ m_lookAtEnemyInvasionAreasTimer.Start( RandomFloat( 0.333f, maxLookInterval ) );
+
+ float minGazeRange = m_Shared.InCond( TF_COND_ZOOMED ) ? 750.0f : 150.0f;
+
+ CTFNavArea *myArea = GetLastKnownArea();
+ if ( myArea )
+ {
+ int team = GetTeamNumber();
+
+ // if we want to look where teammates come from, we need to pass in
+ // the *enemy* team, since the method collects *enemy* invasion areas
+ if ( !lookForEnemies )
+ {
+ team = GetEnemyTeam( team );
+ }
+
+ const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( team );
+
+ if ( invasionAreaVector.Count() > 0 )
+ {
+ // try to not look directly at walls
+ const int retryCount = 20.0f;
+ for( int r=0; r<retryCount; ++r )
+ {
+ int which = RandomInt( 0, invasionAreaVector.Count()-1 );
+ Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight );
+
+ if ( IsRangeGreaterThan( gazeSpot, minGazeRange ) && GetVisionInterface()->IsLineOfSightClear( gazeSpot ) )
+ {
+ // use maxLookInterval so these looks override body aiming from path following
+ GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::INTERESTING, maxLookInterval, NULL, "Looking toward enemy invasion areas" );
+ break;
+ }
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Update our view to keep an eye on areas where the enemy will be coming from
+ */
+void CTFBot::UpdateLookingAroundForEnemies( void )
+{
+ if ( !m_isLookingAroundForEnemies )
+ return;
+
+ if ( HasAttribute( CTFBot::IGNORE_ENEMIES ) )
+ return;
+
+ if ( m_Shared.IsControlStunned() )
+ return;
+
+ const float maxLookInterval = 1.0f;
+
+ const CKnownEntity *known = GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( known )
+ {
+ if ( known->IsVisibleInFOVNow() )
+ {
+ if ( IsPlayerClass( TF_CLASS_SPY ) &&
+ GetDifficulty() >= CTFBot::HARD &&
+ m_Shared.InCond( TF_COND_DISGUISED ) &&
+ !m_Shared.IsStealthed() )
+ {
+ // smart Spies don't look at their victims until it's too late...
+ // look around at where *teammates* will be coming from to fool the enemy
+ UpdateLookingAroundForIncomingPlayers( LOOK_FOR_FRIENDS );
+ return;
+ }
+
+ // I see you!
+ GetBodyInterface()->AimHeadTowards( known->GetEntity(), IBody::CRITICAL, 1.0f, NULL, "Aiming at a visible threat" );
+ return;
+ }
+
+/* apparently sounds update last known position...
+ if ( known->WasEverVisible() && known->GetTimeSinceLastSeen() < 3.0f )
+ {
+ // I saw you just a moment ago...
+ GetBodyInterface()->AimHeadTowards( known->GetLastKnownPosition() + GetClassEyeHeight(), IBody::IMPORTANT, 1.0f, NULL, "Aiming at a last known threat position" );
+ return;
+ }
+*/
+
+ // known but not currently visible (I know you're around here somewhere)
+
+ // if there is unobstructed space between us, turn around
+ if ( IsLineOfSightClear( known->GetEntity(), IGNORE_ACTORS ) )
+ {
+ Vector toThreat = known->GetEntity()->GetAbsOrigin() - GetAbsOrigin();
+ float threatRange = toThreat.NormalizeInPlace();
+
+ float aimError = M_PI/6.0f;
+
+ float s, c;
+ FastSinCos( aimError, &s, &c );
+
+ float error = threatRange * s;
+ Vector imperfectAimSpot = known->GetEntity()->WorldSpaceCenter();
+ imperfectAimSpot.x += RandomFloat( -error, error );
+ imperfectAimSpot.y += RandomFloat( -error, error );
+
+ GetBodyInterface()->AimHeadTowards( imperfectAimSpot, IBody::IMPORTANT, 1.0f, NULL, "Turning around to find threat out of our FOV" );
+ return;
+ }
+
+ if ( !IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ // look toward potentially visible area nearest the last known position
+ CTFNavArea *myArea = GetLastKnownArea();
+ if ( myArea )
+ {
+ const CTFNavArea *closeArea = NULL;
+ CFindClosestPotentiallyVisibleAreaToPos find( known->GetLastKnownPosition() );
+ myArea->ForAllPotentiallyVisibleAreas( find );
+
+ closeArea = find.m_closeArea;
+
+ if ( closeArea )
+ {
+ // try to not look directly at walls
+ const int retryCount = 10.0f;
+ for( int r=0; r<retryCount; ++r )
+ {
+ Vector gazeSpot = closeArea->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight );
+
+ if ( GetVisionInterface()->IsLineOfSightClear( gazeSpot ) )
+ {
+ // use maxLookInterval so these looks override body aiming from path following
+ GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::IMPORTANT, maxLookInterval, NULL, "Looking toward potentially visible area near known but hidden threat" );
+ return;
+ }
+ }
+
+ // can't find a clear line to look along
+ if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) )
+ {
+ ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s can't find clear line to look at potentially visible near known but hidden entity %s(#%d)\n",
+ gpGlobals->curtime,
+ GetDebugIdentifier(),
+ known->GetEntity()->GetClassname(),
+ known->GetEntity()->entindex() );
+ }
+ }
+ else if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) )
+ {
+ ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s no potentially visible area to look toward known but hidden entity %s(#%d)\n",
+ gpGlobals->curtime,
+ GetDebugIdentifier(),
+ known->GetEntity()->GetClassname(),
+ known->GetEntity()->entindex() );
+ }
+ }
+
+ return;
+ }
+ }
+
+ // no known threat - look toward where enemies will come from
+ UpdateLookingAroundForIncomingPlayers( LOOK_FOR_ENEMIES );
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CFindVantagePoint : public ISearchSurroundingAreasFunctor
+{
+public:
+ CFindVantagePoint( int enemyTeamIndex )
+ {
+ m_enemyTeamIndex = enemyTeamIndex;
+ m_vantageArea = NULL;
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ CTeam *enemyTeam = GetGlobalTeam( m_enemyTeamIndex );
+ for( int i=0; i<enemyTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *enemy = (CTFPlayer *)enemyTeam->GetPlayer(i);
+
+ if ( !enemy->IsAlive() || !enemy->GetLastKnownArea() )
+ continue;
+
+ CTFNavArea *enemyArea = (CTFNavArea *)enemy->GetLastKnownArea();
+ if ( enemyArea->IsCompletelyVisible( area ) )
+ {
+ // nearby area from which we can see the enemy team
+ m_vantageArea = area;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ int m_enemyTeamIndex;
+ CTFNavArea *m_vantageArea;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return a nearby area where we can see a member of the enemy team
+CTFNavArea *CTFBot::FindVantagePoint( float maxTravelDistance ) const
+{
+ CFindVantagePoint find( GetTeamNumber() == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE );
+ SearchSurroundingAreas( GetLastKnownArea(), find, maxTravelDistance );
+ return find.m_vantageArea;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return perceived danger of threat (0=none, 1=immediate deadly danger)
+ * @todo: Move this to contextual query
+ * @todo: Differentiate between potential threats (that sentry up ahead along our route) and immediate threats (the sentry I'm in range of)
+ */
+float CTFBot::GetThreatDanger( CBaseCombatCharacter *who ) const
+{
+ if ( who == NULL )
+ return 0.0f;
+
+ if ( IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ if ( IsRangeGreaterThan( who, tf_bot_sniper_personal_space_range.GetFloat() ) )
+ {
+ // far away enemies are no threat to a Sniper
+ return 0.0f;
+ }
+ }
+
+ if ( who->IsPlayer() )
+ {
+ CTFPlayer *player = ToTFPlayer( who );
+
+ // ubers are scary
+ if ( player->m_Shared.IsInvulnerable() )
+ return 1.0f;
+
+ switch( player->GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_MEDIC:
+ return 0.2f; // 1/5
+
+ case TF_CLASS_ENGINEER:
+ case TF_CLASS_SNIPER:
+ return 0.4f; // 2/5
+
+ case TF_CLASS_SCOUT:
+ case TF_CLASS_SPY:
+ case TF_CLASS_DEMOMAN:
+ return 0.6f; // 3/5
+
+ case TF_CLASS_SOLDIER:
+ case TF_CLASS_HEAVYWEAPONS:
+ return 0.8f; // 4/5
+
+ case TF_CLASS_PYRO:
+ return 1.0f; // 5/5
+ }
+
+ }
+ else
+ {
+ // sentry gun
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( who );
+ if ( sentry )
+ {
+ if ( !sentry->IsAlive() || sentry->IsPlacing() || sentry->HasSapper() || sentry->IsPlasmaDisabled() || sentry->IsUpgrading() || sentry->IsBuilding() )
+ return 0.0f;
+
+ switch( sentry->GetUpgradeLevel() )
+ {
+ case 3: return 1.0f;
+ case 2: return 0.8f;
+ default: return 0.6f;
+ }
+ }
+ }
+
+ return 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the max range at which we can effectively attack
+ */
+float CTFBot::GetMaxAttackRange( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( !myWeapon )
+ return 0.0f;
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ return 100.0f;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
+ {
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ const float flameRange = 350.0f;
+
+ static CSchemaItemDefHandle pItemDef_GiantFlamethrower( "MVM Giant Flamethrower" );
+
+ if ( IsActiveTFWeapon( pItemDef_GiantFlamethrower ) )
+ {
+ return 2.5f * flameRange;
+ }
+
+ return flameRange;
+ }
+
+ return 250.0f;
+ }
+
+ if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ // infinite
+ return FLT_MAX;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) )
+ {
+ return 3000.0f;
+ }
+
+ // bullet spray weapons, grenades, etc
+ // for now, default to infinite so bot always returns fire and doesn't look dumb
+ return FLT_MAX;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the ideal range at which we can effectively attack
+ */
+float CTFBot::GetDesiredAttackRange( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( !myWeapon )
+ return 0.0f;
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_KNIFE ) )
+ {
+ // get very close and stab
+ return 70.0f; // 60
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ return 100.0f;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
+ {
+ return 100.0f;
+ }
+
+ if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ // infinite
+ return FLT_MAX;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) && !TFGameRules()->IsMannVsMachineMode() )
+ {
+ return 1250.0f;
+ }
+
+ // bullet spray weapons, grenades, etc
+ return 500.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// If we're required to equip a specific weapon, do it.
+bool CTFBot::EquipRequiredWeapon( void )
+{
+ // if we have a required weapon on our stack, it takes precedence (items, etc)
+ if ( m_requiredWeaponStack.Count() )
+ {
+ CBaseCombatWeapon *pWeapon = m_requiredWeaponStack.Top().Get();
+ return Weapon_Switch( pWeapon );
+ }
+
+ if ( TheTFBots().IsMeleeOnly() || TFGameRules()->IsInMedievalMode() || HasWeaponRestriction( MELEE_ONLY ) )
+ {
+ // force use of melee weapons
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) );
+ return true;
+ }
+
+ if ( HasWeaponRestriction( PRIMARY_ONLY ) )
+ {
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) );
+ return true;
+ }
+
+ if ( HasWeaponRestriction( SECONDARY_ONLY ) )
+ {
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return true;
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Equip the best weapon we have to attack the given threat
+void CTFBot::EquipBestWeaponForThreat( const CKnownEntity *threat )
+{
+ if ( EquipRequiredWeapon() )
+ return;
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ // mobs never equip other weapons
+ return;
+ }
+
+ if ( GetPlayerClass()->GetClassIndex() == TF_CLASS_DEMOMAN && !IsInASquad() )
+ {
+ // wandering demomen use stickies only
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return;
+ }
+ }
+#endif // TF_RAID_MODE
+
+ CTFWeaponBase *primary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) );
+ if ( !IsCombatWeapon( primary ) )
+ {
+ primary = NULL;
+ }
+
+ CTFWeaponBase *secondary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ if ( !IsCombatWeapon( secondary ) )
+ {
+ secondary = NULL;
+ }
+
+ // no secondary weapons in MvM
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( IsPlayerClass( TF_CLASS_MEDIC ) && IsInASquad() && GetSquad() && !GetSquad()->IsLeader( this ) )
+ {
+ // always try to heal leader
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return;
+ }
+
+ secondary = NULL;
+ }
+
+ CTFWeaponBase *melee = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) );
+ if ( !IsCombatWeapon( melee ) )
+ {
+ melee = NULL;
+ }
+
+ CTFWeaponBase *gun = NULL;
+ if ( primary )
+ {
+ gun = primary;
+ }
+ else if ( secondary )
+ {
+ gun = secondary;
+ }
+ else
+ {
+ gun = melee;
+ }
+
+ if ( IsDifficulty( CTFBot::EASY ) )
+ {
+ // easy bots always use their primary weapon if they have one
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+
+ return;
+ }
+
+ if ( !threat || !threat->WasEverVisible() || threat->GetTimeSinceLastSeen() > 5.0f )
+ {
+ // no threat - go back to primary weapon so it has a chance to reload
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+
+ return;
+ }
+
+ // now filter weapons by available ammo
+ if ( GetAmmoCount( TF_AMMO_PRIMARY ) <= 0 )
+ {
+ primary = NULL;
+ }
+
+ if ( GetAmmoCount( TF_WPN_TYPE_SECONDARY ) <= 0 )
+ {
+ secondary = NULL;
+ }
+
+ // modify our gun choice based on threat situation (range, etc)
+ switch( GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_DEMOMAN:
+ case TF_CLASS_HEAVYWEAPONS:
+ case TF_CLASS_SPY:
+ case TF_CLASS_MEDIC:
+ case TF_CLASS_ENGINEER:
+ // primary
+ break;
+
+ case TF_CLASS_SCOUT:
+ {
+ if ( secondary )
+ {
+ if ( gun && !gun->Clip1() )
+ {
+ gun = secondary;
+ }
+ }
+ }
+ break;
+
+ case TF_CLASS_SOLDIER:
+ {
+ // if we've emptied our rocket launcher clip and are fighting a nearby threat, switch to our secondary if it is ready to fire
+ if ( gun && !gun->Clip1() )
+ {
+ if ( secondary && secondary->Clip1() )
+ {
+ const float closeSoldierRange = 500.0f;
+ if ( IsRangeLessThan( threat->GetLastKnownPosition(), closeSoldierRange ) )
+ {
+ gun = secondary;
+ }
+ }
+ }
+ }
+ break;
+
+ case TF_CLASS_SNIPER:
+ {
+ const float closeSniperRange = 750.0f;
+ if ( secondary && IsRangeLessThan( threat->GetLastKnownPosition(), closeSniperRange ) )
+ gun = secondary;
+ }
+ break;
+
+ case TF_CLASS_PYRO:
+ {
+ const float flameRange = 750.0f;
+ if ( secondary && IsRangeGreaterThan( threat->GetLastKnownPosition(), flameRange ) )
+ {
+ gun = secondary;
+ }
+
+ // keep flamethrower out to reflect projectiles
+ if ( threat->GetEntity() && threat->GetEntity()->IsPlayer() )
+ {
+ CTFPlayer *enemy = ToTFPlayer( threat->GetEntity() );
+
+ if ( enemy->IsPlayerClass( TF_CLASS_SOLDIER ) || enemy->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ gun = primary;
+ }
+ }
+ }
+ break;
+ }
+
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// NOTE: This assumes default weapon loadouts
+bool CTFBot::EquipLongRangeWeapon( void )
+{
+ // no secondary weapons in MvM
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ return false;
+
+ if ( IsPlayerClass( TF_CLASS_SOLDIER ) ||
+ IsPlayerClass( TF_CLASS_DEMOMAN ) ||
+ IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ||
+ IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ CBaseCombatWeapon *primary = Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( primary )
+ {
+ if ( GetAmmoCount( TF_AMMO_PRIMARY ) > 0 )
+ {
+ Weapon_Switch( primary );
+ return true;
+ }
+ }
+ }
+
+ // fall back to our secondary (or go right to it if its the only thing we have that has reach)
+ CBaseCombatWeapon *secondary = Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
+ if ( secondary )
+ {
+ if ( GetAmmoCount( TF_AMMO_SECONDARY ) > 0 )
+ {
+ Weapon_Switch( secondary );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Force us to equip and use this weapon until popped off the required stack
+void CTFBot::PushRequiredWeapon( CTFWeaponBase *weapon )
+{
+ m_requiredWeaponStack.Push( weapon );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Pop top required weapon off of stack and discard
+void CTFBot::PopRequiredWeapon( void )
+{
+ m_requiredWeaponStack.Pop();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon can be used to attack
+bool CTFBot::IsCombatWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_MEDIGUN:
+ case TF_WEAPON_PDA:
+ case TF_WEAPON_PDA_ENGINEER_BUILD:
+ case TF_WEAPON_PDA_ENGINEER_DESTROY:
+ case TF_WEAPON_PDA_SPY:
+ case TF_WEAPON_BUILDER:
+ case TF_WEAPON_DISPENSER:
+ case TF_WEAPON_INVIS:
+ case TF_WEAPON_LUNCHBOX:
+ case TF_WEAPON_BUFF_ITEM:
+ case TF_WEAPON_PUMPKIN_BOMB:
+ return false;
+ };
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon is a "hitscan" weapon
+bool CTFBot::IsHitScanWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_SHOTGUN_PRIMARY:
+ case TF_WEAPON_SHOTGUN_SOLDIER:
+ case TF_WEAPON_SHOTGUN_HWG:
+ case TF_WEAPON_SHOTGUN_PYRO:
+ case TF_WEAPON_SCATTERGUN:
+ case TF_WEAPON_SNIPERRIFLE:
+ case TF_WEAPON_MINIGUN:
+ case TF_WEAPON_SMG:
+ case TF_WEAPON_CHARGED_SMG:
+ case TF_WEAPON_PISTOL:
+ case TF_WEAPON_PISTOL_SCOUT:
+ case TF_WEAPON_REVOLVER:
+ case TF_WEAPON_SENTRY_BULLET:
+ case TF_WEAPON_SENTRY_ROCKET:
+ case TF_WEAPON_SENTRY_REVENGE:
+ case TF_WEAPON_HANDGUN_SCOUT_PRIMARY:
+ case TF_WEAPON_HANDGUN_SCOUT_SECONDARY:
+ case TF_WEAPON_SODA_POPPER:
+ case TF_WEAPON_SNIPERRIFLE_DECAP:
+ case TF_WEAPON_PEP_BRAWLER_BLASTER:
+ case TF_WEAPON_SNIPERRIFLE_CLASSIC:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon "sprays" bullets/fire/etc continuously (ie: not individual rockets/etc)
+bool CTFBot::IsContinuousFireWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( !IsCombatWeapon( weapon ) )
+ return false;
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_PISTOL:
+ case TF_WEAPON_PISTOL_SCOUT:
+ case TF_WEAPON_FLAREGUN:
+ case TF_WEAPON_JAR:
+ case TF_WEAPON_COMPOUND_BOW:
+ return false;
+ };
+ }
+
+ return true;
+
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon launches explosive projectiles with splash damage
+bool CTFBot::IsExplosiveProjectileWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_JAR:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon has small clip and long reload cost (ie: rocket launcher, etc)
+bool CTFBot::IsBarrageAndReloadWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_SCATTERGUN:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if given weapon doesn't make much sound when used (ie: spy knife, etc)
+bool CTFBot::IsQuietWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_KNIFE:
+ case TF_WEAPON_FISTS:
+ case TF_WEAPON_PDA:
+ case TF_WEAPON_PDA_ENGINEER_BUILD:
+ case TF_WEAPON_PDA_ENGINEER_DESTROY:
+ case TF_WEAPON_PDA_SPY:
+ case TF_WEAPON_BUILDER:
+ case TF_WEAPON_MEDIGUN:
+ case TF_WEAPON_DISPENSER:
+ case TF_WEAPON_INVIS:
+ case TF_WEAPON_FLAREGUN:
+ case TF_WEAPON_LUNCHBOX:
+ case TF_WEAPON_JAR:
+ case TF_WEAPON_COMPOUND_BOW:
+ case TF_WEAPON_SWORD:
+ case TF_WEAPON_CROSSBOW:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line between the given points
+bool CTFBot::IsLineOfFireClear( const Vector &from, const Vector &to ) const
+{
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE );
+ CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() );
+ CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter );
+
+ UTIL_TraceLine( from, to, MASK_SOLID_BRUSHONLY, &filter, &trace );
+
+ return !trace.DidHit();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line from our eye to the given position
+bool CTFBot::IsLineOfFireClear( const Vector &where ) const
+{
+ return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), where );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line between the given point and entity
+bool CTFBot::IsLineOfFireClear( const Vector &from, CBaseEntity *who ) const
+{
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE );
+ CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() );
+ CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter );
+
+ UTIL_TraceLine( from, who->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, &filter, &trace );
+
+ return !trace.DidHit() || trace.m_pEnt == who;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line from our eye to the given entity
+bool CTFBot::IsLineOfFireClear( CBaseEntity *who ) const
+{
+ return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), who );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsEntityBetweenTargetAndSelf( CBaseEntity *other, CBaseEntity *target )
+{
+ Vector toTarget = target->GetAbsOrigin() - GetAbsOrigin();
+ float rangeToTarget = toTarget.NormalizeInPlace();
+
+ Vector toOther = other->GetAbsOrigin() - GetAbsOrigin();
+ float rangeToOther = toOther.NormalizeInPlace();
+
+ return rangeToOther < rangeToTarget && DotProduct( toTarget, toOther ) > 0.7071f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we are sure this player actually is an enemy spy
+bool CTFBot::IsKnownSpy( CTFPlayer *player ) const
+{
+ for( int i=0; i<m_knownSpyVector.Count(); ++i )
+ {
+ CTFPlayer *spy = m_knownSpyVector[i];
+ if ( spy && player->entindex() == spy->entindex() )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we suspect this player might be an enemy spy
+CTFBot::SuspectedSpyInfo_t* CTFBot::IsSuspectedSpy( CTFPlayer *pPlayer )
+{
+ for( int i=0; i<m_suspectedSpyVector.Count(); ++i )
+ {
+ SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i];
+ CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy;
+ if ( pSpy && pPlayer->entindex() == pSpy->entindex() )
+ {
+ return pSpyInfo;
+ }
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Note that this player might be a spy
+void CTFBot::SuspectSpy( CTFPlayer *pPlayer )
+{
+ SuspectedSpyInfo_t* pSpyInfo = IsSuspectedSpy( pPlayer );
+
+ // Start suspecting this spy if we're not aware of them until now
+ if( pSpyInfo == NULL )
+ {
+ // add to head for LRU effect
+ pSpyInfo = new SuspectedSpyInfo_t;
+ pSpyInfo->m_suspectedSpy = pPlayer;
+ m_suspectedSpyVector.AddToHead( pSpyInfo );
+ }
+
+ // Suspicious!
+ pSpyInfo->Suspect();
+
+ // Too suspicious?
+ if( pSpyInfo->TestForRealizing() )
+ {
+ RealizeSpy( pPlayer );
+ }
+}
+
+void CTFBot::SuspectedSpyInfo_t::Suspect()
+{
+ int nCurTime = floor(gpGlobals->curtime);
+
+ // Add our new entry
+ m_touchTimes.AddToHead( nCurTime );
+}
+
+bool CTFBot::SuspectedSpyInfo_t::TestForRealizing()
+{
+ // Remove any old entries
+ int nCurTime = floor(gpGlobals->curtime);
+ int nCutoffTime = nCurTime - tf_bot_suspect_spy_touch_interval.GetInt();
+
+ FOR_EACH_VEC_BACK( m_touchTimes, i )
+ {
+ if( m_touchTimes[i] <= nCutoffTime )
+ m_touchTimes.Remove( i );
+ }
+
+ // Add our new entry
+ m_touchTimes.AddToHead( nCurTime );
+
+ // Setup an array of bools representing the past few seconds that we want
+ // to look for suspicious activity
+ CUtlVector<bool> vecSeconds;
+ vecSeconds.SetSize( tf_bot_suspect_spy_touch_interval.GetInt() );
+ FOR_EACH_VEC( vecSeconds, i )
+ {
+ vecSeconds[i] = false;
+ }
+
+ // Go through each time chunk and mark if there was suspicious activity
+ FOR_EACH_VEC( m_touchTimes, i )
+ {
+ int nTouchTime = m_touchTimes[i];
+ int nTimeSlot = nCurTime - nTouchTime;
+
+ if( nTimeSlot >= 0 && nTimeSlot < vecSeconds.Count() )
+ {
+ vecSeconds[nTimeSlot] = true;
+ }
+ }
+
+ // If all are true, then the spy has been suspicious enough to warrant being realized
+ FOR_EACH_VEC( vecSeconds, i )
+ {
+ if( vecSeconds[i] == false )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+bool CTFBot::SuspectedSpyInfo_t::IsCurrentlySuspected()
+{
+ float flCutoffTime = gpGlobals->curtime - tf_bot_suspect_spy_forget_cooldown.GetFloat();
+ if( m_touchTimes.Count() && m_touchTimes.Head() > flCutoffTime )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------------------------------
+// Note that this player *IS* a spy
+void CTFBot::RealizeSpy( CTFPlayer *pPlayer )
+{
+ // We already know about this spy
+ if ( IsKnownSpy( pPlayer ) )
+ return;
+
+ // add to head for LRU effect
+ m_knownSpyVector.AddToHead( pPlayer );
+
+ // inform my teammates
+ SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_CLOAKEDSPY );
+
+ // If I am suspicious of this spy, make everyone around me know that
+ // they should be suspicious too
+ SuspectedSpyInfo_t* pSuspectInfo = IsSuspectedSpy( pPlayer );
+ if( pSuspectInfo && pSuspectInfo->IsCurrentlySuspected() )
+ {
+ // Tell others around us we've realized there's a spy
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ CTFPlayer* pOther = playerVector[i];
+
+ if( !pOther->IsBot() )
+ continue;
+
+ //Make sure they're close by
+ Vector vecBetween = EyePosition() - pOther->EyePosition();
+ if( vecBetween.IsLengthLessThan( 512.f ) )
+ {
+ // If they dont know about this spy
+ CTFBot* pOtherBot = static_cast<CTFBot*>( pOther );
+ if( !pOtherBot->IsKnownSpy( pPlayer ) )
+ {
+ // I was suspicious that they were a spy, make my friend suspicious as well.
+ // This will cause them to attack a disguised spy in MvM for a bit.
+ pOtherBot->SuspectSpy( pPlayer );
+
+ // Tell them about it
+ pOtherBot->RealizeSpy( pPlayer );
+ }
+ }
+ }
+ }
+
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Remove player from spy suspect system
+void CTFBot::ForgetSpy( CTFPlayer *pPlayer )
+{
+ StopSuspectingSpy( pPlayer );
+ m_knownSpyVector.FindAndFastRemove( pPlayer );
+}
+
+void CTFBot::StopSuspectingSpy( CTFPlayer *pPlayer )
+{
+ // Find the entry matching this spy
+ for( int i=0; i<m_suspectedSpyVector.Count(); ++i )
+ {
+ SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i];
+ CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy;
+ if ( pSpy && pPlayer->entindex() == pSpy->entindex() )
+ {
+ delete pSpyInfo;
+ m_suspectedSpyVector.Remove(i);
+ break;
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return the nearest human player on the given team who is looking directly at me
+CTFPlayer *CTFBot::GetClosestHumanLookingAtMe( int team ) const
+{
+ CUtlVector< CTFPlayer * > otherVector;
+ CollectPlayers( &otherVector, team, COLLECT_ONLY_LIVING_PLAYERS );
+
+ float closeRange = FLT_MAX;
+ CTFPlayer *close = NULL;
+
+ for( int i=0; i<otherVector.Count(); ++i )
+ {
+ CTFPlayer *other = otherVector[i];
+
+ if ( other->IsBot() )
+ continue;
+
+ Vector otherEye, otherForward;
+ other->EyePositionAndVectors( &otherEye, &otherForward, NULL, NULL );
+
+ Vector toMe = const_cast< CTFBot * >( this )->EyePosition() - otherEye;
+ float range = toMe.NormalizeInPlace();
+
+ if ( range < closeRange )
+ {
+ const float cosTolerance = 0.98f;
+ if ( DotProduct( toMe, otherForward ) > cosTolerance )
+ {
+ // a human is looking toward me - check LOS
+ if ( IsLineOfSightClear( otherEye, IGNORE_NOTHING, other ) )
+ {
+ close = other;
+ closeRange = range;
+ }
+ }
+ }
+ }
+
+ return close;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// become a member of the given squad
+void CTFBot::JoinSquad( CTFBotSquad *squad )
+{
+ if ( squad )
+ {
+ squad->Join( this );
+ m_squad = squad;
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// leave our current squad
+void CTFBot::LeaveSquad( void )
+{
+ if ( m_squad )
+ {
+ m_squad->Leave( this );
+ m_squad = NULL;
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+// leave our current squad
+void CTFBot::DeleteSquad( void )
+{
+ if ( m_squad )
+ {
+ m_squad = NULL;
+ }
+}
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsWeaponRestricted( CTFWeaponBase *weapon ) const
+{
+ if ( !weapon )
+ {
+ return false;
+ }
+
+ // Get the weapon's loadout slot
+ CEconItemView *pEconItemView = weapon->GetAttributeContainer()->GetItem();
+ if ( !pEconItemView )
+ return false;
+ CTFItemDefinition *pItemDef = pEconItemView->GetStaticData();
+ if ( !pItemDef )
+ return false;
+ int iLoadoutSlot = pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() );
+
+ if ( HasWeaponRestriction( MELEE_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_MELEE);
+ }
+
+ if ( HasWeaponRestriction( PRIMARY_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_PRIMARY);
+ }
+
+ if ( HasWeaponRestriction( SECONDARY_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_SECONDARY);
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Return true if there is something we want to reflect directly ahead of us
+//
+bool CTFBot::ShouldFireCompressionBlast( void )
+{
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // no reflection in training mode
+ return false;
+ }
+
+ if ( !tf_bot_pyro_always_reflect.GetBool() )
+ {
+ if ( IsDifficulty( CTFBot::EASY ) )
+ {
+ // easy bots can't reflect at all
+ return false;
+ }
+
+ if ( IsDifficulty( CTFBot::NORMAL ) )
+ {
+ // normal bots reflect some of the time
+ if ( TransientlyConsistentRandomValue( 1.0f ) < 0.5f )
+ {
+ return false;
+ }
+ }
+
+ if ( IsDifficulty( CTFBot::HARD ) )
+ {
+ // hard bots reflect most of the time
+ if ( TransientlyConsistentRandomValue( 1.0f ) < 0.1f )
+ {
+ return false;
+ }
+ }
+ }
+
+ bool shouldPushPlayers = !TFGameRules()->IsMannVsMachineMode();
+
+ if ( shouldPushPlayers )
+ {
+ const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat( true );
+ if ( threat && threat->GetEntity() && threat->GetEntity()->IsPlayer() )
+ {
+ CTFPlayer *pushVictim = ToTFPlayer( threat->GetEntity() );
+
+ if ( IsRangeLessThan( pushVictim, tf_bot_pyro_shove_away_range.GetFloat() ) )
+ {
+ // our threat is very close - shove them!
+
+ // always shove ubers
+ if ( pushVictim && pushVictim->m_Shared.IsInvulnerable() )
+ {
+ return true;
+ }
+
+ if ( pushVictim->GetGroundEntity() == NULL )
+ {
+ // they are in the air - juggle them some of the time
+ return ( TransientlyConsistentRandomValue( 0.5f ) < 0.5f );
+ }
+
+ if ( pushVictim->IsCapturingPoint() )
+ {
+ // push them off the point!
+ return true;
+ }
+
+ // be pushy sometimes
+ if ( TransientlyConsistentRandomValue( 3.0f ) < 0.5f )
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+
+ Vector vecEye = EyePosition();
+ Vector vecForward, vecRight, vecUp;
+
+ AngleVectors( EyeAngles(), &vecForward, &vecRight, &vecUp );
+
+ Vector vecCenter = vecEye + vecForward * 128;
+ Vector vecSize = Vector( 128, 128, 64 );
+
+ const int maxCollectedEntities = 128;
+ CBaseEntity *pObjects[ maxCollectedEntities ];
+ int count = UTIL_EntitiesInBox( pObjects, maxCollectedEntities, vecCenter - vecSize, vecCenter + vecSize, FL_CLIENT | FL_GRENADE );
+
+ for ( int i = 0; i < count; i++ )
+ {
+ CBaseEntity *pObject = pObjects[i];
+ if ( pObject == this )
+ continue;
+
+ if ( pObject->GetTeamNumber() == GetTeamNumber() )
+ continue;
+
+ // should air blast player logic is already done before this loop
+ if ( pObject->IsPlayer() )
+ continue;
+
+ // is this something I want to deflect?
+ if ( !pObject->IsDeflectable() )
+ continue;
+
+ if ( FClassnameIs( pObject, "tf_projectile_rocket" ) || FClassnameIs( pObject, "tf_projectile_energy_ball" ) )
+ {
+ // is it headed right for me?
+ Vector vecThemUnitVel = pObject->GetAbsVelocity();
+ vecThemUnitVel.z = 0.0f;
+ vecThemUnitVel.NormalizeInPlace();
+
+ Vector horzForward( vecForward.x, vecForward.y, 0.0f );
+ horzForward.NormalizeInPlace();
+
+ if ( DotProduct( horzForward, vecThemUnitVel ) > -tf_bot_pyro_deflect_tolerance.GetFloat() )
+ continue;
+ }
+
+ // can I see it?
+ if ( !GetVisionInterface()->IsLineOfSightClear( pObject->WorldSpaceCenter() ) )
+ continue;
+
+ // bounce it!
+ return true;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Compute a pseudo random value (0-1) that stays consistent for the
+// given period of time, but changes unpredictably each period.
+float CTFBot::TransientlyConsistentRandomValue( float period, int seedValue ) const
+{
+ CNavArea *area = GetLastKnownArea();
+ if ( !area )
+ {
+ return 0.0f;
+ }
+
+ // this term stays stable for 'period' seconds, then changes in an unpredictable way
+ int timeMod = (int)( gpGlobals->curtime / period ) + 1;
+ return fabs( FastCos( (float)( seedValue + ( entindex() * area->GetID() * timeMod ) ) ) );
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Given a target entity, find a target within 'maxSplashRadius' that has clear line of fire
+// to both the target entity and to me.
+bool CTFBot::FindSplashTarget( CBaseEntity *target, float maxSplashRadius, Vector *splashTarget ) const
+{
+ if ( !target || !splashTarget )
+ return false;
+
+ *splashTarget = target->WorldSpaceCenter();
+
+ const int retryCount = 50;
+ for( int i=0; i<retryCount; ++i )
+ {
+ Vector probe = target->WorldSpaceCenter() + RandomVector( -maxSplashRadius, maxSplashRadius );
+
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( target->WorldSpaceCenter(), probe, MASK_SOLID_BRUSHONLY, &filter, &trace );
+ if ( trace.DidHitWorld() )
+ {
+ // can we shoot this spot?
+ if ( IsLineOfFireClear( trace.endpos ) )
+ {
+ // yes, found a corner-sticky target
+ *splashTarget = trace.endpos;
+
+ NDebugOverlay::Line( target->WorldSpaceCenter(), trace.endpos, 255, 0, 0, true, 60.0f );
+ NDebugOverlay::Cross3D( trace.endpos, 5.0f, 255, 255, 0, true, 60.0f );
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Restrict bot's attention to only this entity (or radius around this entity) to the exclusion of everything else
+void CTFBot::SetAttentionFocus( CBaseEntity *focusOn )
+{
+ m_attentionFocusEntity = focusOn;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Remove attention focus restrictions
+void CTFBot::ClearAttentionFocus( void )
+{
+ m_attentionFocusEntity = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAttentionFocused( void ) const
+{
+ return m_attentionFocusEntity != NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAttentionFocusedOn( CBaseEntity *who ) const
+{
+ if ( m_attentionFocusEntity == NULL || who == NULL )
+ {
+ return false;
+ }
+
+ if ( m_attentionFocusEntity->entindex() == who->entindex() )
+ {
+ // specifically focused on this entity
+ return true;
+ }
+
+ CTFBotActionPoint *actionPoint = dynamic_cast< CTFBotActionPoint * >( m_attentionFocusEntity.Get() );
+ if ( actionPoint )
+ {
+ // we attend to everything within the action point's radius
+ return actionPoint->IsWithinRange( who );
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Notice the given threat after the given number of seconds have elapsed
+void CTFBot::DelayedThreatNotice( CHandle< CBaseEntity > who, float noticeDelay )
+{
+ float when = gpGlobals->curtime + noticeDelay;
+
+ // if we already have a delayed notice for this threat, ignore the new one unless the delay is less
+ for( int i=0; i<m_delayedNoticeVector.Count(); ++i )
+ {
+ if ( m_delayedNoticeVector[i].m_who == who )
+ {
+ if ( m_delayedNoticeVector[i].m_when > when )
+ {
+ // update delay to shorter time
+ m_delayedNoticeVector[i].m_when = when;
+ }
+ return;
+ }
+ }
+
+ // new notice
+ DelayedNoticeInfo delay;
+ delay.m_who = who;
+ delay.m_when = when;
+ m_delayedNoticeVector.AddToTail( delay );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::UpdateDelayedThreatNotices( void )
+{
+ for( int i=0; i<m_delayedNoticeVector.Count(); ++i )
+ {
+ if ( m_delayedNoticeVector[i].m_when <= gpGlobals->curtime )
+ {
+ // delay is up - notice this threat
+ CBaseEntity *who = m_delayedNoticeVector[i].m_who;
+
+ if ( who )
+ {
+ if ( who->IsPlayer() )
+ {
+ CTFPlayer *player = ToTFPlayer( who );
+ if ( player->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ RealizeSpy( player );
+ }
+ }
+
+ GetVisionInterface()->AddKnownEntity( who );
+ }
+
+ m_delayedNoticeVector.Remove( i );
+ --i;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::GiveRandomItem( loadout_positions_t loadoutPosition )
+{
+ CUtlVector< const CEconItemDefinition * > itemVector;
+
+ const CEconItemSchema::ItemDefinitionMap_t& mapItemDefs = ItemSystem()->GetItemSchema()->GetItemDefinitionMap();
+ FOR_EACH_MAP_FAST( mapItemDefs, i )
+ {
+ const CTFItemDefinition *pItemDef = dynamic_cast< const CTFItemDefinition * >( mapItemDefs[i] );
+
+ if ( pItemDef && pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ) == loadoutPosition )
+ {
+ itemVector.AddToTail( pItemDef );
+ }
+ }
+
+ if ( itemVector.Count() > 0 )
+ {
+ int which = RandomInt( 0, itemVector.Count()-1 );
+
+/*
+ CBaseCombatWeapon *myMelee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ me->Weapon_Detach( myMelee );
+ UTIL_Remove( myMelee );
+*/
+
+ const char *itemName = itemVector[ which ]->GetDefinitionName();
+ BotGenerateAndWearItem( this, itemName );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsSquadmate( CTFPlayer *who ) const
+{
+ if ( !m_squad || !who || !who->IsBotOfType( TF_BOT_TYPE ) )
+ return false;
+
+ return GetSquad() == ToTFBot( who )->GetSquad();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Set Spy disguise to be a class that someone on the enemy team is actually using
+void CTFBot::DisguiseAsMemberOfEnemyTeam( void )
+{
+ CUtlVector< CTFPlayer * > enemyVector;
+ CollectPlayers( &enemyVector, GetEnemyTeam( GetTeamNumber() ) );
+
+ int disguise = RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 );
+
+ if ( enemyVector.Count() > 0 )
+ {
+ disguise = enemyVector[ RandomInt( 0, enemyVector.Count()-1 ) ]->GetPlayerClass()->GetClassIndex();
+ }
+
+ m_Shared.Disguise( GetEnemyTeam( GetTeamNumber() ), disguise );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::ClearTags( void )
+{
+ m_tags.RemoveAll();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::AddTag( const char *tag )
+{
+ if ( !HasTag( tag ) )
+ {
+ m_tags.AddToTail( CFmtStr( "%s", tag ) );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::RemoveTag( const char *tag )
+{
+ for ( int i=0; i<m_tags.Count(); ++i )
+ {
+ if ( FStrEq( tag, m_tags[i] ) )
+ {
+ m_tags.Remove(i);
+ return;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+// TODO: Make this an efficient lookup/match
+bool CTFBot::HasTag( const char *tag )
+{
+ for( int i=0; i<m_tags.Count(); ++i )
+ {
+ if ( FStrEq( tag, m_tags[i] ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CBaseObject *CTFBot::GetNearestKnownSappableTarget( void )
+{
+ CUtlVector< CKnownEntity > knownVector;
+ GetVisionInterface()->CollectKnownEntities( &knownVector );
+
+ CBaseObject *closeObject = NULL;
+ float closeObjectRangeSq = 500.0f * 500.0f;
+
+ for( int i=0; i<knownVector.Count(); ++i )
+ {
+ CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( knownVector[i].GetEntity() );
+ if ( enemyObject && !enemyObject->HasSapper() && IsEnemy( enemyObject ) )
+ {
+ float rangeSq = GetRangeSquaredTo( enemyObject );
+ if ( rangeSq < closeObjectRangeSq )
+ {
+ closeObjectRangeSq = rangeSq;
+ closeObject = enemyObject;
+ }
+ }
+ }
+
+ return closeObject;
+}
+
+
+//-----------------------------------------------------------------------------------------
+Action< CTFBot > *CTFBot::OpportunisticallyUseWeaponAbilities( void )
+{
+ if ( !m_opportunisticTimer.IsElapsed() )
+ {
+ return NULL;
+ }
+
+ m_opportunisticTimer.Start( RandomFloat( 0.1f, 0.2f ) );
+
+
+ // if I'm wearing a charge shield, use it!
+ if ( IsPlayerClass( TF_CLASS_DEMOMAN ) && m_Shared.IsShieldEquipped() )
+ {
+ Vector forward;
+ EyeVectors( &forward );
+ bool bShouldCharge = GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + 100.0f * forward, ILocomotion::IMMEDIATELY );
+ if ( HasAttribute( CTFBot::AIR_CHARGE_ONLY ) && ( GetGroundEntity() || GetAbsVelocity().z > 0 ) )
+ {
+ bShouldCharge = false;
+ }
+
+ if ( bShouldCharge )
+ {
+ PressAltFireButton();
+ }
+ }
+ // if I'm wearing parachute, check if I should activate my parachute
+ else if ( m_Shared.IsParachuteEquipped() )
+ {
+ bool bIsBurning = m_Shared.InCond( TF_COND_BURNING );
+ float flHealthPercent = (float)GetHealth() / GetMaxHealth();
+ const float flHealthThreshold = 0.5f;
+ // should I activate parachute?
+ if ( !m_Shared.InCond( TF_COND_PARACHUTE_DEPLOYED ) )
+ {
+ float flMinParachuteGroundDistance = 300.f;
+ // check if I'm falling, high enough off the ground to deploy parachute, and not burning
+ if ( flHealthPercent >= flHealthThreshold && !bIsBurning && GetAbsVelocity().z < 0 && GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flMinParachuteGroundDistance ), ILocomotion::IMMEDIATELY ) )
+ {
+ PressJumpButton();
+ }
+ }
+ // should I deactivate parachute?
+ else
+ {
+ float flCancelParachuteDistance = 150.f;
+ // if I'm burning or close enough to landing, deactivate the parachute or health less than some threshold
+ if ( flHealthPercent < flHealthThreshold || bIsBurning || !GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flCancelParachuteDistance ), ILocomotion::IMMEDIATELY ) )
+ {
+ PressJumpButton();
+ }
+ }
+ }
+
+ // don't use items if we have the flag, since most of them are unusable (unless we're a bomb carrier in MvM)
+ if ( HasTheFlag() && !TFGameRules()->IsMannVsMachineMode() )
+ {
+ return NULL;
+ }
+
+ for ( int w=0; w<MAX_WEAPONS; ++w )
+ {
+ CTFWeaponBase *weapon = ( CTFWeaponBase * )GetWeapon( w );
+ if ( !weapon )
+ continue;
+
+ // if I have some kind of buff banner - use it!
+ if ( weapon->GetWeaponID() == TF_WEAPON_BUFF_ITEM )
+ {
+ CTFBuffItem *buff = (CTFBuffItem *)weapon;
+ if ( buff->IsFull() )
+ {
+ return new CTFBotUseItem( buff );
+ }
+ }
+ else if ( weapon->GetWeaponID() == TF_WEAPON_LUNCHBOX )
+ {
+ // if we have an eatable (drink, sandvich, etc) - eat it!
+ CTFLunchBox *lunchbox = (CTFLunchBox *)weapon;
+ if ( lunchbox->HasAmmo() )
+ {
+ // scout lunchboxes are also gated by their energy drink meter
+ if ( !IsPlayerClass( TF_CLASS_SCOUT ) || m_Shared.GetScoutEnergyDrinkMeter() >= 100 )
+ {
+ return new CTFBotUseItem( lunchbox );
+ }
+ }
+ }
+ else if ( weapon->GetWeaponID() == TF_WEAPON_BAT_WOOD )
+ {
+ // sandman
+ if ( GetAmmoCount( TF_AMMO_GRENADES1 ) > 0 )
+ {
+ const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ // hit a stunball
+ PressAltFireButton();
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------
+// mostly for MvM - pick a random enemy player that is not in their spawn room
+CTFPlayer *CTFBot::SelectRandomReachableEnemy( void )
+{
+ CUtlVector< CTFPlayer * > livePlayerVector;
+ CollectPlayers( &livePlayerVector, GetEnemyTeam( GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ // only consider players who have left their spawn
+ CUtlVector< CTFPlayer * > playerVector;
+ for( int i=0; i<livePlayerVector.Count(); ++i )
+ {
+ CTFPlayer *player = livePlayerVector[i];
+ if ( !PointInRespawnRoom( player, player->WorldSpaceCenter() ) )
+ {
+ playerVector.AddToTail( player );
+ }
+ }
+
+ if ( playerVector.Count() > 0 )
+ {
+ return playerVector[ RandomInt( 0, playerVector.Count()-1 ) ];
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Different sized bots used different lookahead distances
+float CTFBot::GetDesiredPathLookAheadRange( void ) const
+{
+ return tf_bot_path_lookahead_range.GetFloat() * GetModelScale();
+}
+
+//-----------------------------------------------------------------------------------------
+// Hack to apply idle loop sounds in MvM
+void CTFBot::StartIdleSound( void )
+{
+ StopIdleSound();
+
+ if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() )
+ return;
+
+ // SHIELD YOUR EYES MIKEB!!!
+ if ( IsMiniBoss() )
+ {
+ const char *pszSoundName = NULL;
+
+ int iClass = GetPlayerClass()->GetClassIndex();
+ switch ( iClass )
+ {
+ case TF_CLASS_HEAVYWEAPONS:
+ {
+ pszSoundName = "MVM.GiantHeavyLoop";
+ break;
+ }
+ case TF_CLASS_SOLDIER:
+ {
+ pszSoundName = "MVM.GiantSoldierLoop";
+ break;
+ }
+ case TF_CLASS_DEMOMAN:
+ {
+ if ( m_mission == MISSION_DESTROY_SENTRIES )
+ {
+ pszSoundName = "MVM.SentryBusterLoop";
+ }
+ else
+ {
+ pszSoundName = "MVM.GiantDemomanLoop";
+ }
+ break;
+ }
+ case TF_CLASS_SCOUT:
+ {
+ pszSoundName = "MVM.GiantScoutLoop";
+ break;
+ }
+ case TF_CLASS_PYRO:
+ {
+ pszSoundName = "MVM.GiantPyroLoop";
+ break;
+ }
+ }
+
+ if ( pszSoundName )
+ {
+ CReliableBroadcastRecipientFilter filter;
+ CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
+ m_pIdleSound = controller.SoundCreate( filter, entindex(), pszSoundName );
+ controller.Play( m_pIdleSound, 1.0, 100 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------------------
+void CTFBot::StopIdleSound( void )
+{
+ if ( m_pIdleSound )
+ {
+ CSoundEnvelopeController::GetController().SoundDestroy( m_pIdleSound );
+ m_pIdleSound = NULL;
+ }
+}
+
+bool CTFBot::ShouldAutoJump()
+{
+ if ( !HasAttribute( CTFBot::AUTO_JUMP ) )
+ return false;
+
+ if ( !m_autoJumpTimer.HasStarted() )
+ {
+ m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) );
+ return true;
+ }
+ else if ( m_autoJumpTimer.IsElapsed() )
+ {
+ m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) );
+ return true;
+ }
+
+ return false;
+}
+
+
+void CTFBot::SetFlagTarget( CCaptureFlag* pFlag )
+{
+ if ( m_hFollowingFlagTarget != pFlag )
+ {
+ if ( m_hFollowingFlagTarget )
+ {
+ m_hFollowingFlagTarget->RemoveFollower( this );
+ }
+
+ m_hFollowingFlagTarget = pFlag;
+ if ( m_hFollowingFlagTarget )
+ {
+ m_hFollowingFlagTarget->AddFollower( this );
+ }
+ }
+}
+
+
+int CTFBot::DrawDebugTextOverlays(void)
+{
+ int offset = tf_bot_debug_tags.GetBool() ? 1 : BaseClass::DrawDebugTextOverlays();
+
+ CUtlString strTags = "Tags : ";
+ for( int i=0; i<m_tags.Count(); ++i )
+ {
+ strTags.Append( m_tags[i] );
+ strTags.Append( " " );
+ }
+
+ EntityText( offset, strTags.Get(), 0 );
+ offset++;
+
+ return offset;
+}
+
+
+void CTFBot::AddEventChangeAttributes( const CTFBot::EventChangeAttributes_t* newEvent )
+{
+ m_eventChangeAttributes.AddToTail( newEvent );
+}
+
+
+const CTFBot::EventChangeAttributes_t* CTFBot::GetEventChangeAttributes( const char* pszEventName ) const
+{
+ for ( int i=0; i<m_eventChangeAttributes.Count(); ++i )
+ {
+ if ( FStrEq( m_eventChangeAttributes[i]->m_eventName, pszEventName ) )
+ {
+ return m_eventChangeAttributes[i];
+ }
+ }
+ return NULL;
+}
+
+
+void CTFBot::OnEventChangeAttributes( const CTFBot::EventChangeAttributes_t* pEvent )
+{
+ if ( pEvent )
+ {
+ SetDifficulty( pEvent->m_skill );
+
+ ClearWeaponRestrictions();
+ SetWeaponRestriction( pEvent->m_weaponRestriction );
+
+ SetMission( pEvent->m_mission );
+
+ ClearAllAttributes();
+ SetAttribute( pEvent->m_attributeFlags );
+
+ SetMaxVisionRangeOverride( pEvent->m_maxVisionRange );
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ SetAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH );
+ SetAttribute( CTFBot::RETAIN_BUILDINGS );
+ }
+
+ // cache off health value before we clear attribute because ModifyMaxHealth adds new attribute and reset the health
+ int nHealth = GetHealth();
+ int nMaxHealth = GetMaxHealth();
+
+ // remove any player attributes
+ RemovePlayerAttributes( false );
+ // and add ones that we want specifically
+ FOR_EACH_VEC( pEvent->m_characterAttributes, i )
+ {
+ const CEconItemAttributeDefinition *pDef = pEvent->m_characterAttributes[i].GetAttributeDefinition();
+ if ( pDef )
+ {
+ Assert( GetAttributeList() );
+ GetAttributeList()->SetRuntimeAttributeValue( pDef, pEvent->m_characterAttributes[i].m_value.asFloat );
+ }
+ }
+ NetworkStateChanged();
+
+ // set health back to what it was before we clear bot's attributes
+ ModifyMaxHealth( nMaxHealth );
+ SetHealth( nHealth );
+
+ // give items to bot before apply attribute changes
+ FOR_EACH_VEC( pEvent->m_items, i )
+ {
+ AddItem( pEvent->m_items[i] );
+ }
+
+ // add attributes to equipped items
+ FOR_EACH_VEC( pEvent->m_itemsAttributes, i )
+ {
+ const CTFBot::EventChangeAttributes_t::item_attributes_t& itemAttributes = pEvent->m_itemsAttributes[i];
+ CSchemaItemDefHandle itemDef( itemAttributes.m_itemName );
+ if ( !itemDef )
+ {
+ Warning( "Unable to find item %s to update attribute.\n", itemAttributes.m_itemName.Get() );
+ }
+
+ for ( int iItemSlot = LOADOUT_POSITION_PRIMARY ; iItemSlot < CLASS_LOADOUT_POSITION_COUNT ; iItemSlot++ )
+ {
+ CEconEntity* pEntity = NULL;
+ CEconItemView *pCurItemData = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( this, iItemSlot, &pEntity );
+ if ( pCurItemData && itemDef && ( pCurItemData->GetItemDefIndex() == itemDef->GetDefinitionIndex() ) )
+ {
+ for ( int iAtt=0; iAtt<itemAttributes.m_attributes.Count(); ++iAtt )
+ {
+ const static_attrib_t& attrib = itemAttributes.m_attributes[iAtt];
+ CAttributeList *pAttribList = pCurItemData->GetAttributeList();
+ if ( pAttribList )
+ {
+ pAttribList->SetRuntimeAttributeValue( attrib.GetAttributeDefinition(), attrib.m_value.asFloat );
+ }
+ }
+
+ if ( pEntity )
+ {
+ // update model incase we change style
+ pEntity->UpdateModelToClass();
+ }
+
+ // move on to the next set of attributes
+ break;
+ }
+ } // for each slot
+ } // for each set of attributes
+
+ // tags
+ ClearTags();
+ for( int g=0; g<pEvent->m_tags.Count(); ++g )
+ {
+ AddTag( pEvent->m_tags[g] );
+ }
+ }
+}
+
+
+void CTFBot::AddItem( const char* pszItemName )
+{
+ CItemSelectionCriteria criteria;
+ criteria.SetQuality( AE_USE_SCRIPT_VALUE );
+ criteria.BAddCondition( "name", k_EOperator_String_EQ, pszItemName, true );
+
+ CBaseEntity *pItem = ItemGeneration()->GenerateRandomItem( &criteria, WorldSpaceCenter(), vec3_angle );
+ if ( pItem )
+ {
+ CEconItemView *pScriptItem = static_cast< CBaseCombatWeapon * >( pItem )->GetAttributeContainer()->GetItem();
+
+ // If we already have an item in that slot, remove it
+ int iClass = GetPlayerClass()->GetClassIndex();
+ int iSlot = pScriptItem->GetStaticData()->GetLoadoutSlot( iClass );
+ equip_region_mask_t unNewItemRegionMask = pScriptItem->GetItemDefinition() ? pScriptItem->GetItemDefinition()->GetEquipRegionConflictMask() : 0;
+
+ if ( IsWearableSlot( iSlot ) )
+ {
+ // Remove any wearable that has a conflicting equip_region
+ for ( int wbl = 0; wbl < GetNumWearables(); wbl++ )
+ {
+ CEconWearable *pWearable = GetWearable( wbl );
+ if ( !pWearable )
+ continue;
+
+ equip_region_mask_t unWearableRegionMask = 0;
+ if ( pWearable->GetAttributeContainer()->GetItem() )
+ {
+ unWearableRegionMask = pWearable->GetAttributeContainer()->GetItem()->GetItemDefinition()->GetEquipRegionConflictMask();
+ }
+
+ if ( unWearableRegionMask & unNewItemRegionMask )
+ {
+ RemoveWearable( pWearable );
+ }
+ }
+ }
+ else
+ {
+ CBaseEntity *pEntity = GetEntityForLoadoutSlot( iSlot );
+ if ( pEntity )
+ {
+ CBaseCombatWeapon *pWpn = dynamic_cast< CBaseCombatWeapon * >( pEntity );
+ Weapon_Detach( pWpn );
+ UTIL_Remove( pEntity );
+ }
+ }
+
+ // Fake global id
+ pScriptItem->SetItemID( 1 );
+
+ DispatchSpawn( pItem );
+
+ CEconEntity *pNewItem = assert_cast<CEconEntity*>( pItem );
+ if ( pNewItem )
+ {
+ pNewItem->GiveTo( this );
+ }
+
+ PostInventoryApplication();
+ }
+ else
+ {
+ if ( pszItemName && pszItemName[0] )
+ {
+ DevMsg( "CTFBotSpawner::AddItemToBot: Invalid item %s.\n", pszItemName );
+ }
+ }
+}
+
+
+int CTFBot::GetUberHealthThreshold()
+{
+ int iUberHealthThreshold = 0;
+ CALL_ATTRIB_HOOK_INT( iUberHealthThreshold, bot_medic_uber_health_threshold );
+ if ( iUberHealthThreshold > 0 )
+ {
+ return iUberHealthThreshold;
+ }
+
+ return 50;
+}
+
+
+float CTFBot::GetUberDeployDelayDuration()
+{
+ float flDelayUberDuration = 0;
+ CALL_ATTRIB_HOOK_INT( flDelayUberDuration, bot_medic_uber_deploy_delay_duration );
+ if ( flDelayUberDuration > 0 )
+ {
+ return flDelayUberDuration;
+ }
+
+ return -1.f;
+}