summaryrefslogtreecommitdiff
path: root/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp')
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp412
1 files changed, 412 insertions, 0 deletions
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp
new file mode 100644
index 0000000..8eab11c
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp
@@ -0,0 +1,412 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_attack.cpp
+// Backstab or pistol, as appropriate
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_attack.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/spy/tf_bot_spy_sap.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_spy_knife_range( "tf_bot_spy_knife_range", "300", FCVAR_CHEAT, "If threat is closer than this, prefer our knife" );
+ConVar tf_bot_spy_change_target_range_threshold( "tf_bot_spy_change_target_range_threshold", "300", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotSpyAttack::CTFBotSpyAttack( CTFPlayer *victim ) : m_path( ChasePath::LEAD_SUBJECT )
+{
+ m_victim = victim;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_isCoverBlown = false;
+
+ if ( m_victim.Get() )
+ {
+ me->GetVisionInterface()->AddKnownEntity( m_victim );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyAttack::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetKnown( m_victim );
+
+ // opportunistically attack closer threat if they are much closer to us than our existing threat
+ const CKnownEntity *closestThreat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( !threat )
+ {
+ threat = closestThreat;
+ m_isCoverBlown = false;
+ if ( closestThreat )
+ {
+ m_victim = ToTFPlayer( closestThreat->GetEntity() );
+ }
+ }
+ else if ( closestThreat &&
+ closestThreat->GetEntity() &&
+ closestThreat != threat )
+ {
+ float rangeToCurrentThreat = me->GetRangeTo( threat->GetLastKnownPosition() );
+ float rangeToNewThreat = me->GetRangeTo( closestThreat->GetLastKnownPosition() );
+
+ if ( rangeToCurrentThreat - rangeToNewThreat > tf_bot_spy_change_target_range_threshold.GetFloat() )
+ {
+ if ( closestThreat->GetEntity()->IsPlayer() )
+ {
+ threat = closestThreat;
+ m_victim = ToTFPlayer( closestThreat->GetEntity() );
+ m_isCoverBlown = false;
+ }
+ }
+ }
+
+ if ( !threat || threat->IsObsolete() )
+ {
+ return Done( "No threat" );
+ }
+
+
+ CBaseObject *sapTarget = me->GetNearestKnownSappableTarget();
+ if ( sapTarget && me->IsEntityBetweenTargetAndSelf( sapTarget, threat->GetEntity() ) )
+ {
+ return ChangeTo( new CTFBotSpySap( sapTarget ), "Opportunistically sapping an enemy object between my victim and I" );
+ }
+
+ if ( me->IsAnyEnemySentryAbleToAttackMe() )
+ {
+ m_isCoverBlown = true;
+
+ CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( TF_WPN_TYPE_PRIMARY );
+ me->Weapon_Switch( myGun );
+
+ return ChangeTo( new CTFBotRetreatToCover, "Escaping sentry fire!" );
+ }
+
+ CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() );
+ if ( !playerThreat )
+ {
+ return Done( "Current 'threat' is not a player or a building?" );
+ }
+
+ // remember who we are attacking (in case we changed our minds)
+ m_victim = playerThreat;
+
+ // uncloak so we can attack
+ if ( me->m_Shared.IsStealthed() && m_decloakTimer.IsElapsed() )
+ {
+ me->PressAltFireButton();
+ m_decloakTimer.Start( 1.0f );
+ }
+
+ bool isKnifeFight = false;
+
+ if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ||
+ me->m_Shared.InCond( TF_COND_DISGUISING ) ||
+ me->m_Shared.IsStealthed() )
+ {
+ isKnifeFight = true;
+ }
+
+ Vector playerThreatForward;
+ playerThreat->EyeVectors( &playerThreatForward );
+
+ Vector toPlayerThreat = playerThreat->GetAbsOrigin() - me->GetAbsOrigin();
+ float threatRange = toPlayerThreat.NormalizeInPlace();
+
+ float behindTolerance = 0.0f;
+
+ switch( me->GetDifficulty() )
+ {
+ case CTFBot::EASY: behindTolerance = 0.9f; break;
+ case CTFBot::NORMAL: behindTolerance = 0.7071f; break;
+ case CTFBot::HARD: behindTolerance = 0.2f; break;
+ case CTFBot::EXPERT: behindTolerance = 0.0f; break;
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ behindTolerance = 0.7071f;
+ }
+
+ bool isBehindVictim = DotProduct( playerThreatForward, toPlayerThreat ) > behindTolerance;
+
+ // easy Spies always think they're in position to backstab
+ if ( me->GetDifficulty() == CTFBot::EASY )
+ {
+ isBehindVictim = true;
+ }
+
+ if ( threatRange < tf_bot_spy_knife_range.GetFloat() )
+ {
+ isKnifeFight = true;
+ }
+ else if ( threat->IsVisibleInFOVNow() && isBehindVictim )
+ {
+ // they are facing away from us - go for the backstab
+ isKnifeFight = true;
+ }
+
+ // does my threat know I'm a Spy?
+ if ( me->IsThreatAimingTowardMe( playerThreat, 0.99f ) && me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f )
+ {
+ m_isCoverBlown |= ( playerThreat->GetTimeSinceWeaponFired() < 0.25f );
+ }
+
+ if ( m_isCoverBlown ||
+ me->m_Shared.InCond( TF_COND_BURNING ) ||
+ me->m_Shared.InCond( TF_COND_URINE ) ||
+ me->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
+ me->m_Shared.InCond( TF_COND_BLEEDING ) )
+ {
+ isKnifeFight = false;
+ }
+
+ CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( isKnifeFight ? TF_WPN_TYPE_MELEE : TF_WPN_TYPE_PRIMARY );
+ me->Weapon_Switch( myGun );
+
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+
+ bool isMovingTowardVictim = true;
+
+ if ( myWeapon && myWeapon->IsMeleeWeapon() )
+ {
+ if ( threat->IsVisibleInFOVNow() )
+ {
+ const float circleStrafeRange = 250.0f;
+
+ if ( threatRange < circleStrafeRange )
+ {
+ // we're close - aim our stab attack
+ me->GetBodyInterface()->AimHeadTowards( playerThreat, IBody::MANDATORY, 0.1f, NULL, "Aiming my stab!" );
+
+ if ( !isBehindVictim )
+ {
+ // circle around our victim to get behind them
+ Vector myForward;
+ me->EyeVectors( &myForward );
+
+ Vector cross;
+ CrossProduct( playerThreatForward, myForward, cross );
+
+ if ( cross.z < 0.0f )
+ {
+ me->PressRightButton();
+ }
+ else
+ {
+ me->PressLeftButton();
+ }
+
+ // don't continue to close in if we're already very close so we don't bump them and give ourselves away
+ if ( threatRange < 100.0f )
+ {
+ isMovingTowardVictim = false;
+ }
+ }
+ else if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( m_chuckleTimer.IsElapsed() )
+ {
+ m_chuckleTimer.Start( 1.0f );
+ me->EmitSound( "Spy.MVM_Chuckle" );
+ }
+ }
+ }
+
+ if ( threatRange < me->GetDesiredAttackRange() )
+ {
+ // if we're still disguised, go for the backstab
+ if ( me->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ if ( isBehindVictim || m_isCoverBlown )
+ {
+ // we're behind them (or they're onto us) - backstab!
+ me->PressFireButton();
+ }
+ }
+ else
+ {
+ // we're exposed - stab! stab! stab!
+ me->PressFireButton();
+ }
+ }
+ }
+ }
+ else
+ {
+ // aim our pistol
+ me->GetBodyInterface()->AimHeadTowards( playerThreat, IBody::MANDATORY, 0.1f, NULL, "Aiming my pistol" );
+ }
+
+ if ( isMovingTowardVictim )
+ {
+ // pursue the threat. if not visible, go to the last known position
+ if ( !threat->IsVisibleRecently() ||
+ me->IsRangeGreaterThan( threat->GetEntity()->GetAbsOrigin(), me->GetDesiredAttackRange() ) ||
+ !me->IsLineOfFireClear( threat->GetEntity()->EyePosition() ) )
+ {
+ // if we're at the threat's last known position and he's still not visible, we lost him
+ if ( !threat->IsVisibleRecently() )
+ {
+ if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), 20.0f ) )
+ {
+ me->GetVisionInterface()->ForgetEntity( threat->GetEntity() );
+ return Done( "I lost my target!" );
+ }
+ }
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Update( me, threat->GetEntity(), cost );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_victim = NULL;
+ m_path.Invalidate();
+ m_isCoverBlown = false;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyAttack::OnStuck( CTFBot *me )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyAttack::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ if ( me->IsEnemy( info.GetAttacker() ) )
+ {
+ if ( !me->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ // hurt by an enemy and exposed as a spy - flee!
+ m_isCoverBlown = true;
+
+ CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( TF_WPN_TYPE_PRIMARY );
+ me->Weapon_Switch( myGun );
+
+ return TryChangeTo( new CTFBotRetreatToCover, RESULT_IMPORTANT, "Time to get out of here!" );
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyAttack::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ if ( me->IsEnemy( other ) && other->MyCombatCharacterPointer() )
+ {
+ if ( other->MyCombatCharacterPointer()->IsLookingTowards( me ) )
+ {
+ m_isCoverBlown = true;
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyAttack::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyAttack::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyAttack::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const
+{
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+
+ if ( m_isCoverBlown ||
+ me->m_Shared.InCond( TF_COND_BURNING ) ||
+ me->m_Shared.InCond( TF_COND_URINE ) ||
+ me->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
+ me->m_Shared.InCond( TF_COND_BLEEDING ) )
+ {
+ // our cover is blown anyway
+ return ANSWER_YES;
+ }
+
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Use this to signal the enemy we are focusing on, so we dont avoid them
+QueryResultType CTFBotSpyAttack::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const
+{
+ if ( blocker != IS_ANY_HINDRANCE_POSSIBLE )
+ {
+ if ( blocker && m_victim.Get() && blocker->entindex() == m_victim->entindex() )
+ {
+ // don't avoid this guy
+ return ANSWER_NO;
+ }
+ }
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+const CKnownEntity * CTFBotSpyAttack::SelectMoreDangerousThreat( const INextBot *meBot,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const
+{
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+
+ if ( me->IsSelf( subject ) )
+ {
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon && myWeapon->IsMeleeWeapon() )
+ {
+ // attack the closest victim with my knife
+ if ( me->GetRangeSquaredTo( threat1->GetEntity() ) < me->GetRangeSquaredTo( threat2->GetEntity() ) )
+ {
+ return threat1;
+ }
+
+ return threat2;
+ }
+ }
+
+ return NULL;
+}
+