summaryrefslogtreecommitdiff
path: root/game/server/tf/bot/behavior/sniper
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/bot/behavior/sniper')
-rw-r--r--game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp253
-rw-r--r--game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h37
-rw-r--r--game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp628
-rw-r--r--game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h51
4 files changed, 969 insertions, 0 deletions
diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp
new file mode 100644
index 0000000..9cf7945
--- /dev/null
+++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp
@@ -0,0 +1,253 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_sniper_attack.h
+// Attack a threat as a Sniper
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_gamerules.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/sniper/tf_bot_sniper_attack.h"
+#include "bot/behavior/tf_bot_melee_attack.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_sniper_flee_range( "tf_bot_sniper_flee_range", "400", FCVAR_CHEAT, "If threat is closer than this, retreat" );
+ConVar tf_bot_sniper_melee_range( "tf_bot_sniper_melee_range", "200", FCVAR_CHEAT, "If threat is closer than this, attack with melee weapon" );
+ConVar tf_bot_sniper_linger_time( "tf_bot_sniper_linger_time", "5", FCVAR_CHEAT, "How long Sniper will wait around after losing his target before giving up" );
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSniperAttack::IsPossible( CTFBot *me )
+{
+ return me->IsPlayerClass( TF_CLASS_SNIPER ) && me->GetVisionInterface()->GetPrimaryKnownThreat() && me->GetVisionInterface()->GetPrimaryKnownThreat()->IsVisibleRecently();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperAttack::Update( CTFBot *me, float interval )
+{
+ // switch to our sniper rifle
+ CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myGun )
+ {
+ me->Weapon_Switch( myGun );
+ }
+
+ // shoot at bad guys
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( threat && !threat->GetEntity()->IsAlive() )
+ {
+ // he's dead
+ threat = NULL;
+ }
+
+ if ( threat == NULL || !threat->IsVisibleInFOVNow() )
+ {
+ if ( m_lingerTimer.IsElapsed() )
+ {
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ return Continue();
+ }
+
+ return Done( "No threat for awhile" );
+ }
+
+ return Continue();
+ }
+
+ me->EquipBestWeaponForThreat( threat );
+
+ if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), tf_bot_sniper_flee_range.GetFloat() ) )
+ {
+ return SuspendFor( new CTFBotRetreatToCover, "Retreating from nearby enemy" );
+ }
+
+ if ( me->GetTimeSinceLastInjury() < 1.0f )
+ {
+ return SuspendFor( new CTFBotRetreatToCover, "Retreating due to injury" );
+ }
+
+ // we have a target
+ m_lingerTimer.Start( RandomFloat( 0.75f, 1.25f ) * tf_bot_sniper_linger_time.GetFloat() );
+
+ if ( !me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ me->PressAltFireButton();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotSniperAttack::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ // we're leaving to do something else - unzoom
+ me->PressAltFireButton();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperAttack::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ // we're leaving to do something else - unzoom
+ me->PressAltFireButton();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// given a subject, return the world space position we should aim at
+Vector CTFBotSniperAttack::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const
+{
+ VPROF_BUDGET( "CTFBotSniperAttack::SelectTargetPoint", "NextBot" );
+
+ Vector visibleSpot;
+
+ trace_t result;
+ NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE );
+
+ // head, then chest, then feet for the Sniper
+
+ // headshot seems to be a bit higher that EyePosition()
+ Vector subjectHeadPos( subject->EyePosition() );
+ subjectHeadPos.z += 1.0f;
+
+ UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subjectHeadPos, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ if ( result.DidHit() )
+ {
+ UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+
+ if ( result.DidHit() )
+ {
+ UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subject->GetAbsOrigin(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ }
+ }
+
+ // even if they aren't visible, we have no way to communicate that out, so pick a reasonable spot
+ return result.endpos;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSniperAttack::IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const
+{
+ if ( subject->InSameTeam( threat->GetEntity() ) )
+ return false;
+
+ if ( !threat->GetEntity()->IsAlive() )
+ return false;
+
+ const float hiddenAwhile = 3.0f;
+ if ( !threat->WasEverVisible() || threat->GetTimeSinceLastSeen() > hiddenAwhile )
+ return false;
+
+ CTFPlayer *player = ToTFPlayer( threat->GetEntity() );
+
+ Vector to = subject->GetAbsOrigin() - threat->GetLastKnownPosition();
+ float threatRange = to.NormalizeInPlace();
+
+ if ( player == NULL )
+ {
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat->GetEntity() );
+ if ( sentry )
+ {
+ // are we in range?
+ if ( threatRange < SENTRY_MAX_RANGE )
+ {
+ // is it pointing at us?
+ Vector sentryForward;
+ AngleVectors( sentry->GetTurretAngles(), &sentryForward );
+
+ if ( DotProduct( to, sentryForward ) > 0.8f )
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ if ( player->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ // is the sniper pointing at me?
+ Vector sniperForward;
+ player->EyeVectors( &sniperForward );
+
+ if ( DotProduct( to, sniperForward ) > 0.8f )
+ {
+ return true;
+ }
+ }
+
+#ifdef TF_RAID_MODE
+ if ( !TFGameRules()->IsRaidMode() )
+ {
+ }
+ else
+#endif // TF_RAID_MODE
+ {
+ if ( player->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // always try to kill these guys first
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+const CKnownEntity *CTFBotSniperAttack::SelectMoreDangerousThreat( const INextBot *me,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const
+{
+ if ( threat1 && threat2 )
+ {
+ bool isImmediateThreat1 = IsImmediateThreat( subject, threat1 );
+ bool isImmediateThreat2 = IsImmediateThreat( subject, threat2 );
+
+ if ( isImmediateThreat1 && !isImmediateThreat2 )
+ {
+ return threat1;
+ }
+ else if ( !isImmediateThreat1 && isImmediateThreat2 )
+ {
+ return threat2;
+ }
+ }
+
+ // both or neither are immediate threats - no preference
+ return NULL;
+}
diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h
new file mode 100644
index 0000000..7a65102
--- /dev/null
+++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h
@@ -0,0 +1,37 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_sniper_attack.h
+// Attack a threat as a Sniper
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_SNIPER_ATTACK_H
+#define TF_BOT_SNIPER_ATTACK_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotSniperAttack : public Action< CTFBot >
+{
+public:
+ static bool IsPossible( CTFBot *me ); // return true if this Action has what it needs to perform right now
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ void OnEnd( CTFBot *me, Action< CTFBot > *nextAction );
+ virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at
+
+ virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+
+ virtual const char *GetName( void ) const { return "SniperAttack"; };
+
+private:
+ CountdownTimer m_lingerTimer;
+
+ bool IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const;
+};
+
+#endif // TF_BOT_SNIPER_ATTACK_H
diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp
new file mode 100644
index 0000000..88b5204
--- /dev/null
+++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp
@@ -0,0 +1,628 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_sniper_lurk.h
+// Move into position and wait for victims
+// Michael Booth, October 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+
+#ifdef TF_RAID_MODE
+#include "raid/tf_raid_logic.h"
+#endif // TF_RAID_MODE
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/sniper/tf_bot_sniper_lurk.h"
+#include "bot/behavior/sniper/tf_bot_sniper_attack.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/tf_bot_melee_attack.h"
+#include "bot/map_entities/tf_bot_hint.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_sniper_flee_range;
+extern ConVar tf_bot_sniper_melee_range;
+extern ConVar tf_bot_debug_sniper;
+
+extern float SkewedRandomValue( void );
+
+ConVar tf_bot_sniper_patience_duration( "tf_bot_sniper_patience_duration", "10", FCVAR_CHEAT, "How long a Sniper bot will wait without seeing an enemy before picking a new spot" );
+ConVar tf_bot_sniper_target_linger_duration( "tf_bot_sniper_target_linger_duration", "2", FCVAR_CHEAT, "How long a Sniper bot will keep toward at a target it just lost sight of" );
+ConVar tf_bot_sniper_allow_opportunistic( "tf_bot_sniper_allow_opportunistic", "1", FCVAR_NONE, "If set, Snipers will stop on their way to their preferred lurking spot to snipe at opportunistic targets" );
+
+ConVar tf_mvm_bot_sniper_target_by_dps( "tf_mvm_bot_sniper_target_by_dps", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "If set, Snipers in MvM mode target the victim that has the highest DPS" );
+
+#ifdef STAGING_ONLY
+extern ConVar tf_bot_use_items;
+#endif
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperLurk::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() );
+
+ m_homePosition = me->GetAbsOrigin();
+ m_isHomePositionValid = false;
+ m_isAtHome = false;
+ m_failCount = 0;
+
+ m_isOpportunistic = tf_bot_sniper_allow_opportunistic.GetBool();
+
+ CTFBotHint *hint = NULL;
+ while( ( hint = (CTFBotHint *)( gEntList.FindEntityByClassname( hint, "func_tfbot_hint" ) ) ) != NULL )
+ {
+ if ( hint->IsA( CTFBotHint::HINT_SNIPER_SPOT ) )
+ {
+ m_hintVector.AddToTail( hint );
+
+ // make sure we don't yet own any of these hints
+ if ( me->IsSelf( hint->GetOwnerEntity() ) )
+ {
+ hint->SetOwnerEntity( NULL );
+ }
+ }
+ }
+
+ m_priorHint = NULL;
+
+ if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ // mann vs machine snipers shouldn't stop until they reach their home
+ //m_isOpportunistic = false;
+
+ // mann vs machine snipers should ignore the scenario and just snipe
+ me->SetMission( CTFBot::MISSION_SNIPER, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ }
+
+#ifdef STAGING_ONLY
+ if ( tf_bot_use_items.GetInt() && ( RandomInt(0, 100) <= tf_bot_use_items.GetInt() ) )
+ {
+ CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ me->Weapon_Detach( myGun );
+ UTIL_Remove( myGun );
+
+ BotGenerateAndWearItem( me, "The Huntsman" );
+ }
+#endif // STAGING_ONLY
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperLurk::Update( CTFBot *me, float interval )
+{
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ }
+ else
+#endif
+ {
+ // continuously search for good sniping spots
+ me->AccumulateSniperSpots();
+
+ if ( !m_isHomePositionValid )
+ {
+ // just found our first sniper spot - update our home position
+ FindNewHome( me );
+ }
+ }
+
+ // aim at bad guys
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( threat && !threat->GetEntity()->IsAlive() )
+ {
+ // he's dead
+ threat = NULL;
+ }
+
+ if ( threat && me->GetIntentionInterface()->ShouldAttack( me, threat ) == ANSWER_NO )
+ {
+ threat = NULL;
+ }
+
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ m_failCount = 0;
+
+ if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), tf_bot_sniper_melee_range.GetFloat() ) )
+ {
+ const float giveUpRange = 1.25f * tf_bot_sniper_melee_range.GetFloat();
+ return SuspendFor( new CTFBotMeleeAttack( giveUpRange ), "Melee attacking nearby threat" );
+ }
+ }
+
+ bool isSightingRifle = false;
+
+ if ( threat &&
+ threat->GetTimeSinceLastSeen() < tf_bot_sniper_target_linger_duration.GetFloat() &&
+ me->IsLineOfFireClear( threat->GetEntity() ) )
+ {
+ // we see something...
+ if ( m_isOpportunistic )
+ {
+ // switch to our sniper rifle
+ CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myGun )
+ {
+ me->Weapon_Switch( myGun );
+ }
+
+ isSightingRifle = true;
+ m_boredTimer.Reset();
+
+ if ( !m_isHomePositionValid )
+ {
+ // make this our opportunistic home for awhile
+ m_homePosition = me->GetAbsOrigin();
+ m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() );
+ }
+ }
+ else
+ {
+ // switch to our SMG and fire while we run
+ CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
+ if ( myGun )
+ {
+ me->Weapon_Switch( myGun );
+ }
+ }
+ }
+
+ const float homeRange = 25.0f; // 100.0f;
+ m_isAtHome = ( me->GetAbsOrigin() - m_homePosition ).AsVector2D().IsLengthLessThan( homeRange );
+
+ if ( m_isAtHome )
+ {
+ isSightingRifle = true;
+
+ // once we've reached a good home spot, opportunistically attack from there
+ m_isOpportunistic = tf_bot_sniper_allow_opportunistic.GetBool();
+
+ if ( m_boredTimer.IsElapsed() )
+ {
+ ++m_failCount;
+
+ if ( FindNewHome( me ) )
+ {
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_NEGATIVE );
+ m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() );
+ }
+ else
+ {
+ // try again soon
+ m_boredTimer.Start( 1.0f );
+ }
+ }
+ }
+ else
+ {
+ // not yet at home - can't start to be bored
+ m_boredTimer.Reset();
+ }
+
+ if ( isSightingRifle )
+ {
+ // switch to our sniper rifle
+ CTFWeaponBase *myGun = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myGun )
+ {
+ me->Weapon_Switch( myGun );
+
+ if ( !me->m_Shared.InCond( TF_COND_ZOOMED ) && !myGun->IsWeapon( TF_WEAPON_COMPOUND_BOW ) )
+ {
+ // zoom in and stand still
+ me->PressAltFireButton();
+ }
+ }
+ }
+ else
+ {
+ // move to our home position
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_homePosition, cost );
+ }
+
+ m_path.Update( me );
+
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ me->PressAltFireButton();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotSniperLurk::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ // we're leaving to do something else - unzoom
+ me->PressAltFireButton();
+ }
+
+ if ( m_priorHint != NULL )
+ {
+ // release my hint
+ m_priorHint->SetOwnerEntity( NULL );
+
+ if ( tf_bot_debug_sniper.GetBool() )
+ {
+ DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() );
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperLurk::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ // we're leaving to do something else - unzoom
+ me->PressAltFireButton();
+ }
+
+ if ( m_priorHint != NULL )
+ {
+ // release my hint
+ m_priorHint->SetOwnerEntity( NULL );
+
+ if ( tf_bot_debug_sniper.GetBool() )
+ {
+ DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSniperLurk::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_repathTimer.Invalidate();
+ m_priorHint = NULL;
+
+ // we probably just fetched some health because the enemy shot us - pick a new place to lurk
+ FindNewHome( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSniperLurk::FindHint( CTFBot *me )
+{
+ // if any sniper spot hints exist, pick one of them
+ CUtlVector< CTFBotHint * > activeHintVector;
+ for( int i=0; i<m_hintVector.Count(); ++i )
+ {
+ if ( m_hintVector[i] != NULL && m_hintVector[i]->IsFor( me ) )
+ {
+ activeHintVector.AddToTail( m_hintVector[i] );
+ }
+ }
+
+ if ( activeHintVector.Count() == 0 )
+ {
+ return false;
+ }
+
+ if ( m_priorHint != NULL )
+ {
+ // release my hint
+ m_priorHint->SetOwnerEntity( NULL );
+
+ if ( tf_bot_debug_sniper.GetBool() )
+ {
+ DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() );
+ }
+ }
+
+ CTFBotHint *hint = NULL;
+
+ if ( m_priorHint != NULL && m_failCount < 2 )
+ {
+ // there used to be targets here - pick nearby hint
+ float nearRange = 500.0f;
+ CUtlVector< CTFBotHint * > nearHintVector;
+ for( int i=0; i<activeHintVector.Count(); ++i )
+ {
+ if ( activeHintVector[i] == m_priorHint )
+ continue;
+
+ if ( ( activeHintVector[i]->WorldSpaceCenter() - m_priorHint->WorldSpaceCenter() ).IsLengthGreaterThan( nearRange ) )
+ continue;
+
+ if ( activeHintVector[i]->GetOwnerEntity() != NULL )
+ continue;
+
+ nearHintVector.AddToTail( activeHintVector[i] );
+ }
+
+ if ( nearHintVector.Count() == 0 )
+ {
+ ++m_failCount;
+ return false;
+ }
+
+ int whichHint = RandomInt( 0, nearHintVector.Count()-1 );
+ hint = nearHintVector[ whichHint ];
+ }
+ else
+ {
+ // picking either our first hint, or we haven't seen a victim in a long time - pick a hint that can actually see someone
+ CUtlVector< CTFPlayer * > victimVector;
+ CollectPlayers( &victimVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ CUtlVector< CTFBotHint * > hotHintVector;
+ CUtlVector< CTFBotHint * > freeHintVector;
+
+ for( int i=0; i<activeHintVector.Count(); ++i )
+ {
+ if ( activeHintVector[i]->GetOwnerEntity() != NULL )
+ continue;
+
+ freeHintVector.AddToTail( activeHintVector[i] );
+
+ for( int p=0; p<victimVector.Count(); ++p )
+ {
+ if ( victimVector[p]->IsLineOfSightClear( activeHintVector[i]->WorldSpaceCenter(), CBaseCombatCharacter::IGNORE_ACTORS ) )
+ {
+ // at least one victim is visible from this hint
+ hotHintVector.AddToTail( activeHintVector[i] );
+ break;
+ }
+ }
+ }
+
+ if ( hotHintVector.Count() == 0 )
+ {
+ // no hints can see any victims - pick at random
+ if ( freeHintVector.Count() == 0 )
+ {
+ // all hints are owned by another sniper - double up
+ int whichHint = RandomInt( 0, activeHintVector.Count()-1 );
+ hint = activeHintVector[ whichHint ];
+
+ if ( tf_bot_debug_sniper.GetBool() )
+ {
+ DevMsg( "%3.2f: %s: No un-owned hints available! Doubling up.\n", gpGlobals->curtime, me->GetPlayerName() );
+ }
+ }
+ else
+ {
+ int whichHint = RandomInt( 0, freeHintVector.Count()-1 );
+ hint = freeHintVector[ whichHint ];
+ }
+ }
+ else
+ {
+ int whichHint = RandomInt( 0, hotHintVector.Count()-1 );
+ hint = hotHintVector[ whichHint ];
+ }
+ }
+
+ if ( hint == NULL )
+ {
+ return false;
+ }
+
+ Extent hintExtent;
+ hintExtent.Init( hint );
+
+ Vector hintSpot;
+ hintSpot.x = RandomFloat( hintExtent.lo.x, hintExtent.hi.x );
+ hintSpot.y = RandomFloat( hintExtent.lo.y, hintExtent.hi.y );
+ hintSpot.z = ( hintExtent.lo.z + hintExtent.hi.z ) / 2.0f;
+
+ TheNavMesh->GetSimpleGroundHeight( hintSpot, &hintSpot.z );
+
+ m_homePosition = hintSpot;
+ m_isHomePositionValid = true;
+ m_priorHint = hint;
+
+ // my hint
+ hint->SetOwnerEntity( me );
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSniperLurk::FindNewHome( CTFBot *me )
+{
+ if ( !m_findHomeTimer.IsElapsed() )
+ {
+ return false;
+ }
+
+ m_findHomeTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ // stay put for now
+ return true;
+ }
+ else
+#endif // TF_RAID_MODE
+ {
+ // if any sniper spot hints exist, pick one of them
+ if ( FindHint( me ) )
+ {
+ return true;
+ }
+
+ // pick a sniper spot from our ongoing search
+ const CUtlVector< CTFBot::SniperSpotInfo > *sniperSpotVector = me->GetSniperSpots();
+ if ( sniperSpotVector->Count() > 0 )
+ {
+ m_homePosition = sniperSpotVector->Element( RandomInt( 0, sniperSpotVector->Count()-1 ) ).m_vantageSpot;
+ m_isHomePositionValid = true;
+ return true;
+ }
+ }
+
+ // can't find a real sniper spot - pick another goal that will get us out into the fray
+ m_isHomePositionValid = false;
+
+ // head toward the point
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point && !point->IsLocked() )
+ {
+ const CUtlVector< CTFNavArea * > *pointAreaVector = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() );
+
+ if ( pointAreaVector && pointAreaVector->Count() > 0 )
+ {
+ int which = RandomInt( 0, pointAreaVector->Count()-1 );
+
+ m_homePosition = pointAreaVector->Element( which )->GetRandomPoint();
+
+ return false;
+ }
+ }
+
+ // no available point at the moment - head toward the enemy spawn room and opportunistically snipe
+ CUtlVector< CTFNavArea * > enemySpawnThresholdVector;
+ TheTFNavMesh()->CollectSpawnRoomThresholdAreas( &enemySpawnThresholdVector, GetEnemyTeam( me->GetTeamNumber() ) );
+
+ if ( enemySpawnThresholdVector.Count() > 0 )
+ {
+ m_homePosition = enemySpawnThresholdVector[ RandomInt( 0, enemySpawnThresholdVector.Count()-1 ) ]->GetCenter();
+ }
+ else
+ {
+ m_homePosition = me->GetAbsOrigin();
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSniperLurk::ShouldAttack( const INextBot *bot, const CKnownEntity *them ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ CTFNavArea *area = me->GetLastKnownArea();
+
+ if ( TFGameRules()->IsMannVsMachineMode() && area && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) )
+ {
+ // don't fire while in the spawn area
+ return ANSWER_NO;
+ }
+
+ // take the shot if you've got it
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSniperLurk::ShouldRetreat( const INextBot *me ) const
+{
+ if ( TFGameRules()->IsMannVsMachineMode() && me->GetEntity()->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ 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 *CTFBotSniperLurk::SelectMoreDangerousThreat( const INextBot *meBot,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const
+{
+ if ( TFGameRules()->IsMannVsMachineMode() && tf_mvm_bot_sniper_target_by_dps.GetBool() )
+ {
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+
+ // If one threat is visible and the other not, always pick the visible one
+ if ( !threat1->IsVisibleRecently() )
+ {
+ if ( threat2->IsVisibleRecently() )
+ {
+ return threat2;
+ }
+ }
+ else if ( !threat2->IsVisibleRecently() )
+ {
+ return threat1;
+ }
+
+ // At this point, threat1 and threat2 are either both visible, or both not
+
+ CTFPlayer *playerThreat1 = ToTFPlayer( threat1->GetEntity() );
+ CTFPlayer *playerThreat2 = ToTFPlayer( threat2->GetEntity() );
+
+ if ( playerThreat1 && playerThreat2 )
+ {
+ float rangeSq1 = me->GetRangeSquaredTo( playerThreat1 );
+ float rangeSq2 = me->GetRangeSquaredTo( playerThreat2 );
+
+ if ( me->HasWeaponRestriction( CTFBot::MELEE_ONLY ) )
+ {
+ // Melee-only bots just use closest threat
+ if ( rangeSq1 < rangeSq2 )
+ {
+ return threat1;
+ }
+ return threat2;
+ }
+
+ // Very near threats are always immediately dangerous
+ const float nearbyRangeSq = 500.0f * 500.0f;
+ if ( rangeSq1 < nearbyRangeSq )
+ {
+ if ( rangeSq2 > nearbyRangeSq )
+ {
+ return threat1;
+ }
+ }
+ else if ( rangeSq2 < nearbyRangeSq )
+ {
+ return threat2;
+ }
+
+ // At this point, both threats are either both very near or both "far"
+
+ // Choose the threat that has the highest DPS
+ const int equalTolerance = 50;
+
+ if ( playerThreat1->GetDamagePerSecond() > playerThreat2->GetDamagePerSecond() + equalTolerance )
+ {
+ return threat1;
+ }
+ else if ( playerThreat2->GetDamagePerSecond() > playerThreat1->GetDamagePerSecond() + equalTolerance )
+ {
+ return threat2;
+ }
+ else
+ {
+ // approximately equal DPS, choose closest
+ if ( rangeSq1 < rangeSq2 )
+ {
+ return threat1;
+ }
+ return threat2;
+ }
+ }
+ }
+
+ // Use normal threat selection
+ return NULL;
+}
diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h
new file mode 100644
index 0000000..fc5b914
--- /dev/null
+++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h
@@ -0,0 +1,51 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_sniper_lurk.h
+// Move into position and wait for victims
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_SNIPER_LURK_H
+#define TF_BOT_SNIPER_LURK_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotHint;
+
+class CTFBotSniperLurk : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction );
+ virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ // Snipers choose their targets a bit differently
+ virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+
+ virtual const char *GetName( void ) const { return "SniperLurk"; };
+
+private:
+ CountdownTimer m_boredTimer;
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+ int m_failCount;
+
+ Vector m_homePosition; // where we want to snipe from
+ bool m_isHomePositionValid;
+ bool m_isAtHome;
+ bool FindNewHome( CTFBot *me );
+ CountdownTimer m_findHomeTimer;
+ bool m_isOpportunistic;
+
+ CUtlVector< CHandle< CTFBotHint > > m_hintVector;
+ CHandle< CTFBotHint > m_priorHint;
+ bool FindHint( CTFBot *me );
+};
+
+#endif // TF_BOT_SNIPER_LURK_H