Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

4644 lines
121 KiB

//========= 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;
}