Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
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.
 
 
 
 
 
 

659 lines
20 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
// tf_bot_tactical_monitor.cpp
// Behavior layer that interrupts for ammo/health/retreat/etc
// Michael Booth, June 2009
#include "cbase.h"
#include "fmtstr.h"
#include "tf_gamerules.h"
#include "tf_weapon_pipebomblauncher.h"
#include "NextBot/NavMeshEntities/func_nav_prerequisite.h"
#include "bot/tf_bot.h"
#include "bot/tf_bot_manager.h"
#include "bot/behavior/tf_bot_tactical_monitor.h"
#include "bot/behavior/tf_bot_scenario_monitor.h"
#include "bot/behavior/tf_bot_seek_and_destroy.h"
#include "bot/behavior/tf_bot_retreat_to_cover.h"
#include "bot/behavior/tf_bot_taunt.h"
#include "bot/behavior/tf_bot_get_health.h"
#include "bot/behavior/tf_bot_get_ammo.h"
#include "bot/behavior/tf_bot_destroy_enemy_sentry.h"
#include "bot/behavior/tf_bot_use_teleporter.h"
#include "bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h"
#include "bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h"
#include "bot/behavior/nav_entities/tf_bot_nav_ent_wait.h"
#include "bot/behavior/engineer/tf_bot_engineer_building.h"
#include "bot/behavior/squad/tf_bot_escort_squad_leader.h"
#include "bot/behavior/training/tf_bot_training.h"
#include "bot/map_entities/tf_bot_hint_sentrygun.h"
#include "tf_obj_sentrygun.h"
#include "tf_item_system.h"
extern ConVar tf_bot_health_ok_ratio;
extern ConVar tf_bot_health_critical_ratio;
ConVar tf_bot_force_jump( "tf_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" );
Action< CTFBot > *CTFBotTacticalMonitor::InitialContainedAction( CTFBot *me )
{
return new CTFBotScenarioMonitor;
}
//-----------------------------------------------------------------------------------------
ActionResult< CTFBot > CTFBotTacticalMonitor::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
{
return Continue();
}
//-----------------------------------------------------------------------------------------
void CTFBotTacticalMonitor::MonitorArmedStickyBombs( CTFBot *me )
{
if ( m_stickyBombCheckTimer.IsElapsed() )
{
m_stickyBombCheckTimer.Start( RandomFloat( 0.3f, 1.0f ) );
// are there any enemies on/near my sticky bombs?
CTFPipebombLauncher *gun = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
if ( gun )
{
const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeBombVector = gun->GetPipeBombVector();
if ( pipeBombVector.Count() > 0 )
{
CUtlVector< CKnownEntity > knownVector;
me->GetVisionInterface()->CollectKnownEntities( &knownVector );
for( int p=0; p<pipeBombVector.Count(); ++p )
{
CTFGrenadePipebombProjectile *sticky = pipeBombVector[p];
if ( !sticky )
{
continue;
}
for( int k=0; k<knownVector.Count(); ++k )
{
if ( knownVector[k].IsObsolete() )
{
continue;
}
if ( knownVector[k].GetEntity()->IsBaseObject() )
{
// we want to put several stickies on a sentry and det at once
continue;
}
if ( sticky->GetTeamNumber() != GetEnemyTeam( knownVector[k].GetEntity()->GetTeamNumber() ) )
{
// "known" is either a spectator, or on our team
continue;
}
const float closeRange = 150.0f;
if ( ( knownVector[k].GetLastKnownPosition() - sticky->GetAbsOrigin() ).IsLengthLessThan( closeRange ) )
{
// they are close - blow it!
me->PressAltFireButton();
return;
}
}
}
}
}
}
}
//-----------------------------------------------------------------------------------------
void CTFBotTacticalMonitor::AvoidBumpingEnemies( CTFBot *me )
{
if ( me->GetDifficulty() < CTFBot::HARD )
return;
const float avoidRange = 200.0f;
CUtlVector< CTFPlayer * > enemyVector;
CollectPlayers( &enemyVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
CTFPlayer *closestEnemy = NULL;
float closestRangeSq = avoidRange * avoidRange;
for( int i=0; i<enemyVector.Count(); ++i )
{
CTFPlayer *enemy = enemyVector[i];
if ( enemy->m_Shared.IsStealthed() || enemy->m_Shared.InCond( TF_COND_DISGUISED ) )
continue;
float rangeSq = ( enemy->GetAbsOrigin() - me->GetAbsOrigin() ).LengthSqr();
if ( rangeSq < closestRangeSq )
{
closestEnemy = enemy;
closestRangeSq = rangeSq;
}
}
if ( !closestEnemy )
return;
// avoid unless hindrance returns a definitive "no"
if ( me->GetIntentionInterface()->IsHindrance( me, closestEnemy ) == ANSWER_UNDEFINED )
{
me->ReleaseForwardButton();
me->ReleaseLeftButton();
me->ReleaseRightButton();
me->ReleaseBackwardButton();
Vector away = me->GetAbsOrigin() - closestEnemy->GetAbsOrigin();
me->GetLocomotionInterface()->Approach( me->GetLocomotionInterface()->GetFeet() + away );
}
}
//-----------------------------------------------------------------------------------------
ActionResult< CTFBot > CTFBotTacticalMonitor::Update( CTFBot *me, float interval )
{
if ( TFGameRules()->RoundHasBeenWon() )
{
#ifdef TF_RAID_MODE
if ( TFGameRules()->IsBossBattleMode() )
{
return Continue();
}
#endif // TF_RAID_MODE
if ( TFGameRules()->GetWinningTeam() == me->GetTeamNumber() )
{
// we won - kill all losers we see
return SuspendFor( new CTFBotSeekAndDestroy, "Get the losers!" );
}
else
{
// lost - run and hide
if ( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) )
{
return SuspendFor( new CTFBotRetreatToCover, "Run away from threat!" );
}
me->PressCrouchButton();
}
return Continue();
}
if ( tf_bot_force_jump.GetBool() )
{
if ( !me->GetLocomotionInterface()->IsClimbingOrJumping() )
{
me->GetLocomotionInterface()->Jump();
}
}
if ( TFGameRules()->State_Get() == GR_STATE_PREROUND )
{
// clear stuck monitor so we dont jump when the preround elapses
me->GetLocomotionInterface()->ClearStuckStatus( "In preround" );
}
Action< CTFBot > *result = me->OpportunisticallyUseWeaponAbilities();
if ( result )
{
return SuspendFor( result, "Opportunistically using buff item" );
}
if ( TFGameRules()->InSetup() )
{
// if a human is staring at us, face them and taunt
if ( m_acknowledgeRetryTimer.IsElapsed() )
{
CTFPlayer *watcher = me->GetClosestHumanLookingAtMe();
if ( watcher )
{
if ( !m_attentionTimer.HasStarted() )
m_attentionTimer.Start( 0.5f );
if ( m_attentionTimer.HasStarted() && m_attentionTimer.IsElapsed() )
{
// a human has been staring at us - acknowledge them
if ( !m_acknowledgeAttentionTimer.HasStarted() )
{
// look toward them
me->GetBodyInterface()->AimHeadTowards( watcher, IBody::IMPORTANT, 3.0f, NULL, "Acknowledging friendly human attention" );
m_acknowledgeAttentionTimer.Start( RandomFloat( 0.0f, 2.0f ) );
}
else if ( m_acknowledgeAttentionTimer.IsElapsed() )
{
m_acknowledgeAttentionTimer.Invalidate();
// don't ack again for awhile
m_acknowledgeRetryTimer.Start( RandomFloat( 10.0f, 20.0f ) );
return SuspendFor( new CTFBotTaunt, "Acknowledging friendly human attention" );
}
}
}
else
{
// no-one is looking at me
m_attentionTimer.Invalidate();
}
}
}
// check if we need to get to cover
QueryResultType shouldRetreat = me->GetIntentionInterface()->ShouldRetreat( me );
if ( TFGameRules()->IsMannVsMachineMode() )
{
// never retreat in MvM mode
shouldRetreat = ANSWER_NO;
}
if ( shouldRetreat == ANSWER_YES )
{
return SuspendFor( new CTFBotRetreatToCover, "Backing off" );
}
else if ( shouldRetreat != ANSWER_NO )
{
// retreat if we need to do a full reload (ie: soldiers shot all their rockets)
if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) )
{
if ( me->IsDifficulty( CTFBot::HARD ) || me->IsDifficulty( CTFBot::EXPERT ) )
{
CTFWeaponBase *myPrimary = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
if ( myPrimary && me->GetAmmoCount( TF_AMMO_PRIMARY ) > 0 && me->IsBarrageAndReloadWeapon( myPrimary ) )
{
if ( myPrimary->Clip1() <= 1 )
{
return SuspendFor( new CTFBotRetreatToCover, "Moving to cover to reload" );
}
}
}
}
}
bool isAvailable = ( me->GetIntentionInterface()->ShouldHurry( me ) != ANSWER_YES );
if ( TFGameRules()->IsMannVsMachineMode() && me->HasTheFlag() )
{
isAvailable = false;
}
// collect ammo and health kits, unless we're in a big hurry
if ( isAvailable && m_maintainTimer.IsElapsed() )
{
m_maintainTimer.Start( RandomFloat( 0.3f, 0.5f ) );
bool isHurt = false;
if ( me->IsInCombat() || me->IsPlayerClass( TF_CLASS_SNIPER ) )
{
// stay in the fight until we're nearly dead
isHurt = ( (float)me->GetHealth() / (float)me->GetMaxHealth() ) < tf_bot_health_critical_ratio.GetFloat();
}
else
{
isHurt = me->m_Shared.InCond( TF_COND_BURNING ) || ( (float)me->GetHealth() / (float)me->GetMaxHealth() ) < tf_bot_health_ok_ratio.GetFloat();
}
if ( isHurt && CTFBotGetHealth::IsPossible( me ) )
{
return SuspendFor( new CTFBotGetHealth, "Grabbing nearby health" );
}
if ( me->IsAmmoLow() && CTFBotGetAmmo::IsPossible( me ) )
{
return SuspendFor( new CTFBotGetAmmo, "Grabbing nearby ammo" );
}
bool shouldDestroySentries = true;
if ( TFGameRules()->IsMannVsMachineMode() )
{
shouldDestroySentries = false;
}
// destroy enemy sentry guns we've encountered
if ( shouldDestroySentries && me->GetEnemySentry() && CTFBotDestroyEnemySentry::IsPossible( me ) )
{
return SuspendFor( new CTFBotDestroyEnemySentry, "Going after an enemy sentry to destroy it" );
}
}
// opportunistically use nearby teleporters
if ( ShouldOpportunisticallyTeleport( me ) )
{
CObjectTeleporter *nearbyTeleporter = FindNearbyTeleporter( me );
if ( nearbyTeleporter )
{
CTFNavArea *teleporterArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( nearbyTeleporter );
CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea();
// only use teleporter if it is ahead of us
if ( teleporterArea && myArea && myArea->GetIncursionDistance( me->GetTeamNumber() ) < 350.0f + teleporterArea->GetIncursionDistance( me->GetTeamNumber() ) )
{
return SuspendFor( new CTFBotUseTeleporter( nearbyTeleporter ), "Using nearby teleporter" );
}
}
}
// detonate sticky bomb traps when victims are near
MonitorArmedStickyBombs( me );
// if we're a Spy, avoid bumping into enemies and giving ourselves away
if ( me->IsPlayerClass( TF_CLASS_SPY ) )
{
AvoidBumpingEnemies( me );
}
me->UpdateDelayedThreatNotices();
// if I'm a squad leader, wait for out of position squadmates
if ( me->IsInASquad() && me->GetSquad()->IsLeader( me ) && me->GetSquad()->ShouldSquadLeaderWaitForFormation() )
{
return SuspendFor( new CTFBotWaitForOutOfPositionSquadMember, "Waiting for squadmates to get back into formation" );
}
return Continue();
}
//-----------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
{
return TryContinue();
}
//-----------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea )
{
// does the area we are entering have a prerequisite?
if ( newArea && newArea->HasPrerequisite( me ) && !me->HasAttribute( CTFBot::AGGRESSIVE ) )
{
const CUtlVector< CHandle< CFuncNavPrerequisite > > &prereqVector = newArea->GetPrerequisiteVector();
for( int i=0; i<prereqVector.Count(); ++i )
{
const CFuncNavPrerequisite *prereq = prereqVector[i];
if ( prereq && prereq->IsEnabled() && const_cast< CFuncNavPrerequisite * >( prereq )->PassesTriggerFilters( me ) )
{
// this prerequisite applies to me
if ( prereq->IsTask( CFuncNavPrerequisite::TASK_WAIT ) )
{
return TrySuspendFor( new CTFBotNavEntWait( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to wait" );
}
else if ( prereq->IsTask( CFuncNavPrerequisite::TASK_MOVE_TO_ENTITY ) )
{
return TrySuspendFor( new CTFBotNavEntMoveTo( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to move to an entity" );
}
}
}
}
return TryContinue();
}
//-----------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnCommandString( CTFBot *me, const char *command )
{
if ( FStrEq( command, "goto action point" ) )
{
return TrySuspendFor( new CTFGotoActionPoint(), RESULT_IMPORTANT, "Received command to go to action point" );
}
else if ( FStrEq( command, "despawn" ) )
{
return TrySuspendFor( new CTFDespawn(), RESULT_CRITICAL, "Received command to go to de-spawn" );
}
else if ( FStrEq( command, "taunt" ) )
{
return TrySuspendFor( new CTFBotTaunt(), RESULT_TRY, "Received command to taunt" );
}
else if ( FStrEq( command, "cloak" ) )
{
if ( me->IsPlayerClass( TF_CLASS_SPY ) && me->m_Shared.IsStealthed() == false )
{
me->PressAltFireButton();
}
}
else if ( FStrEq( command, "uncloak" ) )
{
if ( me->IsPlayerClass( TF_CLASS_SPY ) && me->m_Shared.IsStealthed() == true )
{
me->PressAltFireButton();
}
}
else if ( FStrEq( command, "disguise") )
{
if ( me->IsPlayerClass( TF_CLASS_SPY ) )
{
if ( me->CanDisguise() )
{
me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 ) );
}
}
}
else if ( FStrEq( command, "build sentry at nearest sentry hint" ) )
{
if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
{
CTFBotHintSentrygun *bestSentryHint = NULL;
float bestDist2 = FLT_MAX;
CTFBotHintSentrygun *sentryHint;
for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) );
sentryHint;
sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) )
{
// clear the previous owner if it is us
if ( sentryHint->GetPlayerOwner() == me )
{
sentryHint->SetPlayerOwner( NULL );
}
if ( sentryHint->IsAvailableForSelection( me ) )
{
Vector toMe = me->GetAbsOrigin() - sentryHint->GetAbsOrigin();
float dist2 = toMe.LengthSqr();
if ( dist2 < bestDist2 )
{
bestSentryHint = sentryHint;
bestDist2 = dist2;
}
}
}
if ( bestSentryHint )
{
bestSentryHint->SetPlayerOwner( me );
return TrySuspendFor( new CTFBotEngineerBuilding( bestSentryHint ), RESULT_CRITICAL, "Building a Sentry at a hint location" );
}
}
}
else if ( FStrEq( command, "attack sentry at next action point" ) )
{
return TrySuspendFor( new CTFTrainingAttackSentryActionPoint(), RESULT_CRITICAL, "Received command to attack sentry gun at next action point" );
}
#ifdef STAGING_ONLY
// !!! BountyMode prototype evaluation hacks below - this code will most likely be deleted soon
else if ( FStrEq( command, "become raider" ) )
{
me->SetIsMiniBoss( true );
me->SetScaleOverride( 1.75f );
me->ModifyMaxHealth( 5000 );
me->SetWeaponRestriction( CTFBot::PRIMARY_ONLY );
me->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ me->GetPlayerClass()->GetClassIndex() ], USE_CLASS_ANIMATIONS );
me->UpdateModel();
me->SetBloodColor( DONT_BLEED );
engine->SetFakeClientConVarValue( me->edict(), "name", "Raider" );
// Custom attribs
struct botAttribs_t
{
char szName[MAX_ATTRIBUTE_DESCRIPTION_LENGTH];
float flValue;
};
botAttribs_t sAttribs[] =
{
{ "move speed bonus", 0.5f },
{ "damage bonus", 1.5f },
{ "damage force reduction", 0.3f },
{ "airblast vulnerability multiplier", 0.3f },
{ "override footstep sound set", 2.f },
};
CAttributeList *pAttribList = me->GetAttributeList();
if ( pAttribList )
{
for ( int i = 0; i < ARRAYSIZE( sAttribs ); i++ )
{
const CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinitionByName( sAttribs[i].szName );
if ( pDef )
{
pAttribList->SetRuntimeAttributeValue( pDef, sAttribs[i].flValue );
}
}
me->NetworkStateChanged();
}
}
// !!! BountyMode prototype evaluation hacks below - this code will most likely be deleted soon
else if ( FStrEq( command, "become guardian" ) )
{
me->SetIsMiniBoss( true );
me->SetScaleOverride( 1.75f );
me->ModifyMaxHealth( 3300 );
me->SetWeaponRestriction( CTFBot::PRIMARY_ONLY );
me->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ me->GetPlayerClass()->GetClassIndex() ], USE_CLASS_ANIMATIONS );
me->UpdateModel();
me->SetBloodColor( DONT_BLEED );
engine->SetFakeClientConVarValue( me->edict(), "name", "Guardian" );
me->SetAttribute( CTFBot::PRIORITIZE_DEFENSE );
// Custom attribs
struct botAttribs_t
{
char szName[MAX_ATTRIBUTE_DESCRIPTION_LENGTH];
float flValue;
};
botAttribs_t sAttribs[] =
{
{ "move speed bonus", 0.5f },
{ "faster reload rate", -0.4f },
{ "fire rate bonus", 0.75f },
{ "damage force reduction", 0.5f },
{ "airblast vulnerability multiplier", 0.5f },
{ "override footstep sound set", 4.f },
};
CAttributeList *pAttribList = me->GetAttributeList();
if ( pAttribList )
{
for ( int i = 0; i < ARRAYSIZE( sAttribs ); i++ )
{
const CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinitionByName( sAttribs[i].szName );
if ( pDef )
{
pAttribList->SetRuntimeAttributeValue( pDef, sAttribs[i].flValue );
}
}
me->NetworkStateChanged();
}
}
#endif // STAGING_ONLY
return TryContinue();
}
//-----------------------------------------------------------------------------------------
bool CTFBotTacticalMonitor::ShouldOpportunisticallyTeleport( CTFBot *me ) const
{
// if I'm an engineer who hasn't placed his teleport entrance yet, don't use friend's teleporter
if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
{
CBaseObject *teleporterEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
return ( teleporterEntrance != NULL );
}
// Medics don't automatically take teleporters unless they actively decide to follow their patient through
if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
{
return false;
}
return true;
}
//-----------------------------------------------------------------------------------------
CObjectTeleporter *CTFBotTacticalMonitor::FindNearbyTeleporter( CTFBot *me )
{
if ( !m_findTeleporterTimer.IsElapsed() )
{
return NULL;
}
m_findTeleporterTimer.Start( RandomFloat( 1.0f, 2.0f ) );
CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea();
if ( myArea == NULL )
{
return NULL;
}
CUtlVector< CNavArea * > nearbyAreaVector;
CUtlVector< CBaseObject * > objVector;
CUtlVector< CObjectTeleporter * > nearbyTeleporterEntranceVector;
CollectSurroundingAreas( &nearbyAreaVector, myArea, 1000.0f );
TheTFNavMesh()->CollectBuiltObjects( &objVector, me->GetTeamNumber() );
for( int j=0; j<objVector.Count(); ++j )
{
if ( objVector[j]->GetType() == OBJ_TELEPORTER )
{
CObjectTeleporter *teleporter = (CObjectTeleporter *)objVector[j];
teleporter->UpdateLastKnownArea();
CNavArea *teleporterArea = teleporter->GetLastKnownArea();
if ( teleporter->IsEntrance() && teleporter->IsReady() && teleporterArea )
{
// we've found a functional teleporter entrance - is it in our nearby area set?
for( int i=0; i<nearbyAreaVector.Count(); ++i )
{
CNavArea *nearbyArea = nearbyAreaVector[i];
if ( nearbyArea->GetID() == teleporterArea->GetID() )
{
// yes, it's nearby
nearbyTeleporterEntranceVector.AddToTail( teleporter );
break;
}
}
}
}
}
if ( nearbyTeleporterEntranceVector.Count() > 0 )
{
int which = RandomInt( 0, nearbyTeleporterEntranceVector.Count()-1 );
return nearbyTeleporterEntranceVector[ which ];
}
return NULL;
}