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