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