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.
413 lines
11 KiB
413 lines
11 KiB
4 years ago
|
//========= 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;
|
||
|
}
|
||
|
|