summaryrefslogtreecommitdiff
path: root/game/server/tf/bot/behavior
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/bot/behavior
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/bot/behavior')
-rw-r--r--game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp310
-rw-r--r--game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h45
-rw-r--r--game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp342
-rw-r--r--game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h51
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp123
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h29
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp105
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h27
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp662
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h55
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp95
-rw-r--r--game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h28
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp118
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h31
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp212
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h31
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp194
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h44
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp112
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h23
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp190
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h36
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp497
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h64
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp504
-rw-r--r--game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h43
-rw-r--r--game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp1108
-rw-r--r--game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h69
-rw-r--r--game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp144
-rw-r--r--game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h29
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp104
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h30
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp377
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h53
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp400
-rw-r--r--game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h49
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp156
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h35
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp110
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h35
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp56
-rw-r--r--game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h28
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp231
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h36
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp442
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h48
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp247
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h40
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp126
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h34
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp422
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h63
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp142
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h31
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp128
-rw-r--r--game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h35
-rw-r--r--game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp240
-rw-r--r--game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h57
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp151
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h38
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp283
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h43
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp155
-rw-r--r--game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h33
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp217
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h57
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp261
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h39
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp164
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h42
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp150
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h47
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp175
-rw-r--r--game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h51
-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
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp412
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h49
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp31
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h24
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp31
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h24
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp236
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h45
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp335
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h42
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp160
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h26
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp91
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h26
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp250
-rw-r--r--game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h42
-rw-r--r--game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp336
-rw-r--r--game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h55
-rw-r--r--game/server/tf/bot/behavior/tf_bot_approach_object.cpp69
-rw-r--r--game/server/tf/bot/behavior/tf_bot_approach_object.h29
-rw-r--r--game/server/tf/bot/behavior/tf_bot_attack.cpp157
-rw-r--r--game/server/tf/bot/behavior/tf_bot_attack.h38
-rw-r--r--game/server/tf/bot/behavior/tf_bot_behavior.cpp1649
-rw-r--r--game/server/tf/bot/behavior/tf_bot_behavior.h76
-rw-r--r--game/server/tf/bot/behavior/tf_bot_dead.cpp58
-rw-r--r--game/server/tf/bot/behavior/tf_bot_dead.h23
-rw-r--r--game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp959
-rw-r--r--game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h79
-rw-r--r--game/server/tf/bot/behavior/tf_bot_escort.cpp130
-rw-r--r--game/server/tf/bot/behavior/tf_bot_escort.h40
-rw-r--r--game/server/tf/bot/behavior/tf_bot_get_ammo.cpp339
-rw-r--r--game/server/tf/bot/behavior/tf_bot_get_ammo.h38
-rw-r--r--game/server/tf/bot/behavior/tf_bot_get_health.cpp324
-rw-r--r--game/server/tf/bot/behavior/tf_bot_get_health.h34
-rw-r--r--game/server/tf/bot/behavior/tf_bot_melee_attack.cpp67
-rw-r--r--game/server/tf/bot/behavior/tf_bot_melee_attack.h26
-rw-r--r--game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp90
-rw-r--r--game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h33
-rw-r--r--game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp190
-rw-r--r--game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h27
-rw-r--r--game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp317
-rw-r--r--game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h41
-rw-r--r--game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp368
-rw-r--r--game/server/tf/bot/behavior/tf_bot_scenario_monitor.h27
-rw-r--r--game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp250
-rw-r--r--game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h51
-rw-r--r--game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp659
-rw-r--r--game/server/tf/bot/behavior/tf_bot_tactical_monitor.h47
-rw-r--r--game/server/tf/bot/behavior/tf_bot_taunt.cpp53
-rw-r--r--game/server/tf/bot/behavior/tf_bot_taunt.h26
-rw-r--r--game/server/tf/bot/behavior/tf_bot_use_item.cpp73
-rw-r--r--game/server/tf/bot/behavior/tf_bot_use_item.h27
-rw-r--r--game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp123
-rw-r--r--game/server/tf/bot/behavior/tf_bot_use_teleporter.h40
-rw-r--r--game/server/tf/bot/behavior/training/tf_bot_training.cpp133
-rw-r--r--game/server/tf/bot/behavior/training/tf_bot_training.h54
134 files changed, 20930 insertions, 0 deletions
diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp
new file mode 100644
index 0000000..721bd35
--- /dev/null
+++ b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp
@@ -0,0 +1,310 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_prepare_stickybomb_trap.cpp
+// Place stickybombs to create a deadly trap
+// Michael Booth, July 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+#include "tf_weapon_pipebomblauncher.h"
+
+#define MAX_STICKYBOMB_COUNT 8
+
+ConVar tf_bot_stickybomb_density( "tf_bot_stickybomb_density", "0.0001", FCVAR_CHEAT, "Number of stickies to place per square inch" );
+
+
+//---------------------------------------------------------------------------------------------
+class PlaceStickyBombReply : public INextBotReply
+{
+public:
+ virtual void OnSuccess( INextBot *bot ) // invoked when process completed successfully
+ {
+ CTFBot *me = ToTFBot( bot->GetEntity() );
+
+ CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myCurrentWeapon && myCurrentWeapon->GetWeaponID() == TF_WEAPON_PIPEBOMBLAUNCHER )
+ {
+ // launch the sticky
+ me->PressFireButton( 0.1f );
+
+ // increase the bomb count for this target area
+ if ( m_bombTargetArea )
+ {
+ m_bombTargetArea->m_count++;
+ }
+
+ if( m_pLaunchWaitTimer )
+ {
+ // release the latch
+ m_pLaunchWaitTimer->Start( 0.15f );
+ }
+ }
+ }
+
+ virtual void OnFail( INextBot *bot, FailureReason reason )// invoked when process failed
+ {
+ // retry aim immediately
+ m_pLaunchWaitTimer->Invalidate();
+ }
+
+ void ClearData()
+ {
+ // Be sure to clear all members here, as we can potentially get an OnSuccess() call
+ // after the ~CTFBotPrepareStickybombTrap.
+ m_bombTargetArea = NULL;
+ m_pLaunchWaitTimer = NULL;
+ }
+
+ CTFBotPrepareStickybombTrap::BombTargetArea *m_bombTargetArea;
+ CountdownTimer *m_pLaunchWaitTimer;
+};
+
+
+static PlaceStickyBombReply bombReply;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotPrepareStickybombTrap::CTFBotPrepareStickybombTrap( void )
+{
+ m_myArea = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotPrepareStickybombTrap::~CTFBotPrepareStickybombTrap( )
+{
+ bombReply.ClearData();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if this Action has what it needs to perform right now
+bool CTFBotPrepareStickybombTrap::IsPossible( CTFBot *me )
+{
+ // don't lay a trap if we're in the midst of fighting
+ if ( /*me->IsInCombat() || */ me->GetTimeSinceLastInjury() < 1.0f )
+ {
+ return false;
+ }
+
+ if ( !me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ return false;
+ }
+
+ CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ if ( stickyLauncher && !me->IsWeaponRestricted( stickyLauncher ) )
+ {
+ if ( stickyLauncher->GetPipeBombCount() >= MAX_STICKYBOMB_COUNT || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotPrepareStickybombTrap::InitBombTargetAreas( CTFBot *me )
+{
+ const CUtlVector< CTFNavArea * > &invasionAreaVector = m_myArea->GetEnemyInvasionAreaVector( me->GetTeamNumber() );
+
+ // randomly shuffle the target areas
+ CUtlVector< CTFNavArea * > shuffleVector;
+ shuffleVector = invasionAreaVector;
+ int n = shuffleVector.Count();
+ while( n > 1 )
+ {
+ int k = RandomInt( 0, n-1 );
+ n--;
+
+ CTFNavArea *tmp = shuffleVector[n];
+ shuffleVector[n] = shuffleVector[k];
+ shuffleVector[k] = tmp;
+ }
+
+ // initialize each target area to zero sticky bombs
+ m_bombTargetAreaVector.RemoveAll();
+
+ for( int i=0; i<shuffleVector.Count(); ++i )
+ {
+ BombTargetArea target;
+ target.m_area = shuffleVector[i];
+ target.m_count = 0;
+
+ m_bombTargetAreaVector.AddToTail( target );
+ }
+
+ m_launchWaitTimer.Invalidate();
+
+ // Clean up any in-flight AimHeadTowards() replies, since changing m_bombTargetAreaVector
+ // might move memory and invalidate the current reply pointer.
+ me->GetBodyInterface()->ClearPendingAimReply();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPrepareStickybombTrap::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // detonate old set of stickies
+ // me->PressAltFireButton();
+
+ // reload entire clip before laying sticky trap
+ CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ if ( stickyLauncher )
+ {
+ m_isFullReloadNeeded = ( me->GetAmmoCount( TF_AMMO_SECONDARY ) >= stickyLauncher->GetMaxClip1() && stickyLauncher->Clip1() < stickyLauncher->GetMaxClip1() );
+ }
+ else
+ {
+ m_isFullReloadNeeded = false;
+ }
+
+ m_myArea = me->GetLastKnownArea();
+ if ( !m_myArea )
+ {
+ return Done( "No nav mesh" );
+ }
+
+ InitBombTargetAreas( me );
+
+ // own our view updating so we can aim
+ me->StopLookingAroundForEnemies();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPrepareStickybombTrap::Update( CTFBot *me, float interval )
+{
+ if ( !TFGameRules()->InSetup() )
+ {
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat )
+ {
+ const float giveUpRange = 500.0f;
+ if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), giveUpRange ) )
+ {
+ return Done( "Enemy nearby - giving up" );
+ }
+ }
+ }
+
+ if ( me->GetLastKnownArea() && me->GetLastKnownArea() != m_myArea )
+ {
+ // we've moved
+ m_myArea = me->GetLastKnownArea();
+ InitBombTargetAreas( me );
+ }
+
+ CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon();
+ CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+
+ if ( !myCurrentWeapon || !stickyLauncher )
+ {
+ return Done( "Missing weapon" );
+ }
+
+ if ( myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER )
+ {
+ me->Weapon_Switch( stickyLauncher );
+ }
+
+ // reload fully
+ if ( m_isFullReloadNeeded )
+ {
+ int maxClip = MIN( stickyLauncher->GetMaxClip1(), me->GetAmmoCount( TF_AMMO_SECONDARY ) );
+
+ if ( stickyLauncher->Clip1() >= maxClip )
+ {
+ // fully reloaded
+ m_isFullReloadNeeded = false;
+ }
+
+ me->PressReloadButton();
+
+ return Continue();
+ }
+
+
+ if ( stickyLauncher->GetPipeBombCount() >= MAX_STICKYBOMB_COUNT || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ return Done( "Max sticky bombs reached" );
+ }
+
+
+ // aim towards areas where enemy will come from
+ if ( m_launchWaitTimer.IsElapsed() )
+ {
+ // find next target that needs bombs
+ int i;
+ for( i=0; i<m_bombTargetAreaVector.Count(); ++i )
+ {
+ CTFNavArea *targetArea = m_bombTargetAreaVector[i].m_area;
+
+ int desiredCount = tf_bot_stickybomb_density.GetFloat() * targetArea->GetSizeX() * targetArea->GetSizeY();
+ if ( desiredCount < 1 )
+ {
+ desiredCount = 1;
+ }
+
+ if ( m_bombTargetAreaVector[i].m_count < desiredCount )
+ {
+ // place a sticky on this area
+ bombReply.m_bombTargetArea = &m_bombTargetAreaVector[i];
+
+ // this timer causes us to wait until the aim finishes and launched before we start another aim
+ m_launchWaitTimer.Start( 2.0f );
+ bombReply.m_pLaunchWaitTimer = &m_launchWaitTimer;
+
+ Vector bombSpot = targetArea->GetRandomPoint();
+
+ me->GetBodyInterface()->AimHeadTowards( bombSpot, IBody::IMPORTANT, 5.0f, &bombReply, "Aiming a sticky bomb" );
+
+ break;
+ }
+ }
+
+ if ( i == m_bombTargetAreaVector.Count() )
+ {
+ return Done( "Exhausted bomb target areas" );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotPrepareStickybombTrap::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ // clean up any in-flight AimHeadTowards() replies
+ me->GetBodyInterface()->ClearPendingAimReply();
+
+ me->StartLookingAroundForEnemies();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPrepareStickybombTrap::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ // this behavior is transitory - if we need to do something else, just give up
+ return Done();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPrepareStickybombTrap::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ return TryDone( RESULT_IMPORTANT, "Ouch!" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPrepareStickybombTrap::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h
new file mode 100644
index 0000000..0abded3
--- /dev/null
+++ b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h
@@ -0,0 +1,45 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_prepare_stickybomb_trap.h
+// Place stickybombs to create a deadly trap
+// Michael Booth, July 2010
+
+#ifndef TF_BOT_PREPARE_STICKYBOMB_TRAP_H
+#define TF_BOT_PREPARE_STICKYBOMB_TRAP_H
+
+class CTFBotPrepareStickybombTrap : public Action< CTFBot >
+{
+public:
+ CTFBotPrepareStickybombTrap( void );
+ virtual ~CTFBotPrepareStickybombTrap( );
+
+ 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 );
+ virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction );
+
+ virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "PrepareStickybombTrap"; };
+
+ struct BombTargetArea
+ {
+ CTFNavArea *m_area;
+ int m_count;
+ };
+
+private:
+ bool m_isFullReloadNeeded;
+
+ CTFNavArea *m_myArea;
+
+ CUtlVector< BombTargetArea > m_bombTargetAreaVector;
+ void InitBombTargetAreas( CTFBot *me );
+ CountdownTimer m_launchWaitTimer;
+};
+
+#endif // TF_BOT_PREPARE_STICKYBOMB_TRAP_H
diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp
new file mode 100644
index 0000000..d788892
--- /dev/null
+++ b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp
@@ -0,0 +1,342 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_stickybomb_sentrygun.cpp
+// Destroy the given sentrygun with stickybombs
+// Michael Booth, August 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h"
+#include "tf_weapon_pipebomblauncher.h"
+#include "tf_obj_sentrygun.h"
+#include "NextBotUtil.h"
+
+ConVar tf_bot_sticky_base_range( "tf_bot_sticky_base_range", "800", FCVAR_CHEAT );
+ConVar tf_bot_sticky_charge_rate( "tf_bot_sticky_charge_rate", "0.01", FCVAR_CHEAT, "Seconds of charge per unit range beyond base" );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotStickybombSentrygun::CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun )
+{
+ m_sentrygun = sentrygun;
+ m_hasGivenAim = false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotStickybombSentrygun::CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun, float aimYaw, float aimPitch, float aimCharge )
+{
+ m_sentrygun = sentrygun;
+ m_hasGivenAim = true;
+ m_givenYaw = aimYaw;
+ m_givenPitch = aimPitch;
+ m_givenCharge = aimCharge;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotStickybombSentrygun::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // detonate old set of stickies
+ me->PressAltFireButton();
+
+ // own our view updating so we can aim
+ me->StopLookingAroundForEnemies();
+
+ m_isFullReloadNeeded = true;
+
+ // STOP
+ me->SetAbsVelocity( vec3_origin );
+
+ m_searchPitch = 0.0f;
+ m_hasTarget = false;
+ m_searchTimer.Start( 3.0f );
+
+ m_isChargingShot = false;
+
+ if ( m_hasGivenAim )
+ {
+ m_hasTarget = true;
+
+ // remember where we are standing - if we move for any reason, we'll need to re-search
+ m_launchSpot = me->GetAbsOrigin();
+
+ // start charging up the sticky launch
+ m_chargeToLaunch = m_givenCharge;
+ m_isChargingShot = true;
+
+ // aim along given pitch/yaw
+ QAngle angles;
+ angles.x = m_givenPitch;
+ angles.y = m_givenYaw;
+ angles.z = 0.0f;
+
+ Vector aimForward;
+ AngleVectors( angles, &aimForward );
+
+ m_eyeAimTarget = me->EyePosition() + 1500.0f * aimForward;
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotStickybombSentrygun::IsAimOnTarget( CTFBot *me, float pitch, float yaw, float charge )
+{
+ // estimate impact spot
+ Vector impactSpot = me->EstimateStickybombProjectileImpactPosition( pitch, yaw, charge );
+
+ // check if impactSpot landed near sentry
+ const float explosionRadius = 75.0f;
+ if ( ( m_sentrygun->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) )
+ {
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( m_sentrygun->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace );
+ if ( !trace.DidHit() )
+ {
+ // NDebugOverlay::Cross3D( impactSpot, 10.0f, 100, 255, 0, true, 60.0f );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotStickybombSentrygun::Update( CTFBot *me, float interval )
+{
+ CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon();
+ CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+
+ if ( !myCurrentWeapon || !stickyLauncher )
+ {
+ return Done( "Missing weapon" );
+ }
+
+ if ( myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER )
+ {
+ me->Weapon_Switch( stickyLauncher );
+ }
+
+ if ( m_sentrygun == NULL || !m_sentrygun->IsAlive() )
+ {
+ return Done( "Sentry destroyed" );
+ }
+
+ if ( !m_hasTarget && m_searchTimer.IsElapsed() )
+ {
+ return Done( "Can't find aim" );
+ }
+
+ // reload fully
+ if ( m_isFullReloadNeeded )
+ {
+ int maxClip = MIN( stickyLauncher->GetMaxClip1(), me->GetAmmoCount( TF_AMMO_SECONDARY ) );
+
+ if ( stickyLauncher->Clip1() >= maxClip )
+ {
+ // fully reloaded
+ m_isFullReloadNeeded = false;
+ }
+
+ me->PressReloadButton();
+
+ return Continue();
+ }
+
+ int requiredStickyBombs = 3;
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ // launch more stickies to make sure we take out beefed-up sentries
+ requiredStickyBombs = 5;
+ }
+
+ if ( stickyLauncher->GetPipeBombCount() >= requiredStickyBombs || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ // stickies laid - detonate them once they are on the ground
+ const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeVector = stickyLauncher->GetPipeBombVector();
+
+ int i;
+ for( i=0; i<pipeVector.Count(); ++i )
+ {
+ if ( pipeVector[i].Get() && !pipeVector[i]->m_bTouched )
+ {
+ break;
+ }
+ }
+
+ if ( i == pipeVector.Count() )
+ {
+ // stickies are on the ground
+ me->PressAltFireButton();
+
+ if ( me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ return Done( "Out of ammo" );
+ }
+ }
+ }
+ else if ( m_isChargingShot )
+ {
+ // fudge charge time a bit longer - better to overshoot
+ float stickyChargeTime = 1.1f * m_chargeToLaunch * TF_PIPEBOMB_MAX_CHARGE_TIME;
+
+ me->GetBodyInterface()->AimHeadTowards( m_eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Aiming a sticky bomb at a sentrygun" );
+
+ if ( gpGlobals->curtime - stickyLauncher->GetChargeBeginTime() >= stickyChargeTime )
+ {
+ // let go
+ me->ReleaseFireButton();
+ m_isChargingShot = false;
+ }
+ else
+ {
+ me->PressFireButton();
+ }
+ }
+ else if ( stickyLauncher->m_flNextPrimaryAttack < gpGlobals->curtime )
+ {
+ // if we've moved, we need to re-search
+ if ( m_hasTarget )
+ {
+ const float tolerance = 1.0f;
+ if ( me->IsRangeGreaterThan( m_launchSpot, tolerance ) )
+ {
+ m_hasTarget = false;
+ m_searchTimer.Reset();
+ }
+ }
+
+ if ( !m_hasTarget )
+ {
+ // search for angle to land sticky near sentry
+ Vector toSentry = m_sentrygun->WorldSpaceCenter() - me->EyePosition();
+
+ QAngle angles;
+ VectorAngles( toSentry, angles );
+
+ float bestYaw = 0.0f;
+ float bestPitch = 0.0f;
+ float bestCharge = 1.0f;
+
+ const int trials = 100;
+ for( int t=0; t<trials; ++t )
+ {
+ float yaw = angles.y + RandomFloat( -30.0f, 30.0f );
+ // float pitch = ( trials & 0x1 ) ? m_searchPitch : -m_searchPitch;
+ float pitch = RandomFloat( -85.0f, 85.0f );
+
+ float charge = 0.0f;
+ if ( toSentry.IsLengthGreaterThan( tf_bot_sticky_base_range.GetBool() ) )
+ {
+ charge = RandomFloat( 0.1f, 1.0f );
+
+ // skew towards zero - full charge shots are seldom required
+ charge *= charge;
+ }
+
+ if ( IsAimOnTarget( me, pitch, yaw, charge ) )
+ {
+ // found target aim - keep one we find with least required
+ // charge, because we need to be fast in combat
+ if ( charge < bestCharge )
+ {
+ m_hasTarget = true;
+
+ bestCharge = charge;
+ m_chargeToLaunch = bestCharge;
+
+ bestYaw = yaw;
+ bestPitch = pitch;
+
+ if ( bestCharge < 0.01 )
+ {
+ // as quick as possible - no need to search further
+ break;
+ }
+ }
+ }
+ }
+
+ // aim along yaw/pitch to reach impact spot
+ angles.x = bestPitch;
+ angles.y = bestYaw;
+ angles.z = 0.0f;
+
+ Vector aimForward;
+ AngleVectors( angles, &aimForward );
+
+ // always recompute eye aim target so we can update our view
+ m_eyeAimTarget = me->EyePosition() + 500.0f * aimForward;
+ me->GetBodyInterface()->AimHeadTowards( m_eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Searching for aim..." );
+ }
+
+ if ( m_hasTarget )
+ {
+ // remember where we are standing - if we move for any reason, we'll need to re-search
+ m_launchSpot = me->GetAbsOrigin();
+
+ // start charging up the sticky launch
+ me->PressFireButton();
+ m_isChargingShot = true;
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotStickybombSentrygun::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ // detonate any stickes left out there
+ me->PressAltFireButton();
+
+ me->StartLookingAroundForEnemies();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotStickybombSentrygun::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ // detonate any stickes left out there
+ me->PressAltFireButton();
+
+ // this behavior is transitory - if we need to do something else, just give up
+ return Done();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotStickybombSentrygun::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ return TryDone( RESULT_IMPORTANT, "Ouch!" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotStickybombSentrygun::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotStickybombSentrygun::ShouldHurry( const INextBot *me ) const
+{
+ // while killing a sentry we're "hurrying" so we don't dodge
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotStickybombSentrygun::ShouldRetreat( const INextBot *me ) const
+{
+ // stay stuck in to try to kill that gun!
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h
new file mode 100644
index 0000000..901e541
--- /dev/null
+++ b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h
@@ -0,0 +1,51 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_stickybomb_sentrygun.h
+// Destroy the given sentrygun with stickybombs
+// Michael Booth, August 2010
+
+#ifndef TF_BOT_STICKYBOMB_SENTRY_H
+#define TF_BOT_STICKYBOMB_SENTRY_H
+
+class CObjectSentrygun;
+
+
+class CTFBotStickybombSentrygun : public Action< CTFBot >
+{
+public:
+ CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun );
+ CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun, float aimYaw, float aimPitch, float aimCharge );
+
+ 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 EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const;
+ 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 "StickybombSentrygun"; };
+
+private:
+ float m_givenYaw, m_givenPitch, m_givenCharge;
+ bool m_hasGivenAim;
+
+ bool m_isFullReloadNeeded;
+
+ CHandle< CObjectSentrygun > m_sentrygun;
+
+ bool m_isChargingShot;
+
+ CountdownTimer m_searchTimer;
+ bool m_hasTarget;
+ Vector m_eyeAimTarget;
+ Vector m_launchSpot;
+ float m_chargeToLaunch;
+ float m_searchPitch;
+ bool IsAimOnTarget( CTFBot *me, float pitch, float yaw, float charge );
+};
+
+#endif // TF_BOT_STICKYBOMB_SENTRY_H
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp
new file mode 100644
index 0000000..c86318f
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp
@@ -0,0 +1,123 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"
+#include "string_t.h"
+#include "tf_fx.h"
+
+extern ConVar tf_bot_engineer_mvm_building_health_multiplier;
+
+//---------------------------------------------------------------------------------------------
+CTFBotMvMEngineerBuildSentryGun::CTFBotMvMEngineerBuildSentryGun( CTFBotHintSentrygun* pSentryHint )
+{
+ m_sentryBuildHint = pSentryHint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerBuildSentryGun::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ me->StartBuildingObjectOfType( OBJ_SENTRYGUN );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerBuildSentryGun::Update( CTFBot *me, float interval )
+{
+ if ( m_sentryBuildHint == NULL )
+ return Done( "No hint entity" );
+
+ float rangeToBuildSpot = me->GetRangeTo( m_sentryBuildHint->GetAbsOrigin() );
+
+ if ( rangeToBuildSpot < 200.0f )
+ {
+ // crouch as we get close so we don't overshoot
+ me->PressCrouchButton();
+
+ me->GetBodyInterface()->AimHeadTowards( m_sentryBuildHint->GetAbsOrigin(), IBody::MANDATORY, 0.1f, NULL, "Placing sentry" );
+ }
+
+ // various interruptions could mean we're away from our build location - move to it
+ if ( rangeToBuildSpot > 25.0f )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_sentryBuildHint->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+
+ if ( !m_path.IsValid() )
+ {
+ return Done( "Path failed" );
+ }
+
+ return Continue();
+ }
+
+ if ( !m_delayBuildTime.HasStarted() )
+ {
+ m_delayBuildTime.Start( 0.1f );
+ TFGameRules()->PushAllPlayersAway( m_sentryBuildHint->GetAbsOrigin(), 400, 500, TF_TEAM_RED );
+ }
+ else if ( m_delayBuildTime.HasStarted() && m_delayBuildTime.IsElapsed() )
+ {
+ // destroy previous object
+ me->DetonateObjectOfType( OBJ_SENTRYGUN, MODE_SENTRYGUN_NORMAL, true );
+
+ // directly create a sentry gun at the precise position and orientation desired
+ m_sentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" );
+ if ( m_sentry )
+ {
+ m_sentry->SetName( m_sentryBuildHint->GetEntityName() );
+
+ m_sentryBuildHint->IncrementUseCount();
+ m_sentry->m_nDefaultUpgradeLevel = 2;
+
+ m_sentry->SetAbsOrigin( m_sentryBuildHint->GetAbsOrigin() );
+ m_sentry->SetAbsAngles( QAngle( 0, m_sentryBuildHint->GetAbsAngles().y, 0 ) );
+ m_sentry->Spawn();
+
+ m_sentry->StartPlacement( me );
+ m_sentry->StartBuilding( me );
+
+ // the sentry owns this hint now
+ m_sentryBuildHint->SetOwnerEntity( m_sentry );
+
+ m_sentry = NULL;
+ }
+
+ return Done( "Built a sentry" );
+ }
+
+ return Continue();
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMvMEngineerBuildSentryGun::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( m_sentry.Get() )
+ {
+ m_sentry->DropCarriedObject( me );
+ UTIL_Remove( m_sentry );
+ m_sentry = NULL;
+ }
+}
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h
new file mode 100644
index 0000000..199492f
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h
@@ -0,0 +1,29 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#ifndef TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H
+#define TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H
+
+class CTFBotHintSentrygun;
+
+class CTFBotMvMEngineerBuildSentryGun : public Action< CTFBot >
+{
+public:
+ CTFBotMvMEngineerBuildSentryGun( CTFBotHintSentrygun* pSentryHint );
+
+ 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 const char *GetName( void ) const { return "MvMEngineerBuildSentryGun"; };
+
+private:
+ CHandle< CTFBotHintSentrygun > m_sentryBuildHint;
+ CHandle< CObjectSentrygun > m_sentry;
+
+ CountdownTimer m_delayBuildTime;
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+};
+
+#endif // TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp
new file mode 100644
index 0000000..024b981
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp
@@ -0,0 +1,105 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#include "cbase.h"
+#include "string_t.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h"
+#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"
+
+ConVar tf_bot_engineer_mvm_building_health_multiplier( "tf_bot_engineer_building_health_multiplier", "2", FCVAR_CHEAT );
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotMvMEngineerBuildTeleportExit::CTFBotMvMEngineerBuildTeleportExit( CTFBotHintTeleporterExit *hint )
+{
+ m_teleporterBuildHint = hint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerBuildTeleportExit::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerBuildTeleportExit::Update( CTFBot *me, float interval )
+{
+ if ( m_teleporterBuildHint == NULL )
+ return Done( "No hint entity" );
+
+ // various interruptions could mean we're away from our build location - move to it
+ if ( me->IsRangeGreaterThan( m_teleporterBuildHint->GetAbsOrigin(), 25.0f ) )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_teleporterBuildHint->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+
+ if ( !m_path.IsValid() )
+ {
+ return Done( "Path failed" );
+ }
+
+ return Continue();
+ }
+
+ if ( !m_delayBuildTime.HasStarted() )
+ {
+ m_delayBuildTime.Start( 0.1f );
+ TFGameRules()->PushAllPlayersAway( m_teleporterBuildHint->GetAbsOrigin(), 400, 500, TF_TEAM_RED );
+ }
+ else if ( m_delayBuildTime.IsElapsed() )
+ {
+ // destroy previous object
+ me->DetonateObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT, true );
+
+ // directly create at the precise position and orientation desired
+ CObjectTeleporter* myTeleporter = (CObjectTeleporter *)CreateEntityByName( "obj_teleporter" );
+ if ( myTeleporter )
+ {
+ myTeleporter->SetAbsOrigin( m_teleporterBuildHint->GetAbsOrigin() );
+ myTeleporter->SetAbsAngles( QAngle( 0, m_teleporterBuildHint->GetAbsAngles().y, 0 ) );
+ myTeleporter->SetObjectMode( MODE_TELEPORTER_EXIT );
+ myTeleporter->Spawn();
+
+ myTeleporter->SetTeleportWhere( me->GetTeleportWhere() );
+
+ if ( me->ShouldQuickBuild() )
+ {
+ myTeleporter->ForceQuickBuild();
+ }
+
+ myTeleporter->StartPlacement( me );
+ myTeleporter->StartBuilding( me );
+
+ int iHealth = myTeleporter->GetMaxHealthForCurrentLevel() * tf_bot_engineer_mvm_building_health_multiplier.GetFloat();
+ myTeleporter->SetMaxHealth( iHealth );
+ myTeleporter->SetHealth( iHealth );
+
+ m_teleporterBuildHint->SetOwnerEntity( myTeleporter );
+
+ me->EmitSound( "Engineer.MVM_AutoBuildingTeleporter02" );
+
+ return Done( "Teleport exit built" );
+ }
+ }
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h
new file mode 100644
index 0000000..7acb0b6
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h
@@ -0,0 +1,27 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#ifndef TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H
+#define TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H
+
+class CTFBotHintTeleporterExit;
+
+class CTFBotMvMEngineerBuildTeleportExit : public Action< CTFBot >
+{
+public:
+ CTFBotMvMEngineerBuildTeleportExit( CTFBotHintTeleporterExit *hint );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "MvMEngineerBuildTeleportExit"; };
+
+private:
+ CHandle< CTFBotHintTeleporterExit > m_teleporterBuildHint;
+
+ CountdownTimer m_delayBuildTime;
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+};
+
+#endif // TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp
new file mode 100644
index 0000000..c6157dc
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp
@@ -0,0 +1,662 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#include "cbase.h"
+#include "nav_mesh/tf_nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_teleporter.h"
+#include "bot/tf_bot.h"
+#include "bot/map_entities/tf_bot_hint_engineer_nest.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"
+
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+
+
+ConVar tf_bot_engineer_mvm_sentry_hint_bomb_forward_range( "tf_bot_engineer_mvm_sentry_hint_bomb_forward_range", "0", FCVAR_CHEAT );
+ConVar tf_bot_engineer_mvm_sentry_hint_bomb_backward_range( "tf_bot_engineer_mvm_sentry_hint_bomb_backward_range", "3000", FCVAR_CHEAT );
+ConVar tf_bot_engineer_mvm_hint_min_distance_from_bomb( "tf_bot_engineer_mvm_hint_min_distance_from_bomb", "1300", FCVAR_CHEAT );
+
+struct BombInfo_t
+{
+ Vector m_vPosition;
+ float m_flMinBattleFront;
+ float m_flMaxBattleFront;
+};
+
+
+bool GetBombInfo( BombInfo_t* pBombInfo = NULL )
+{
+ // find the incursion distance of the current "front" (the location of the bomb)
+
+ // first find farthest bomb delivery distance of invading team since maps
+ // have different spawn room sizes and geometries
+ float battlefront = 0.0f;
+
+ for( int n=0; n<TheNavAreas.Count(); ++n )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheNavAreas[n];
+
+ if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ continue;
+ }
+
+ float areaDistanceToTarget = area->GetTravelDistanceToBombTarget();
+ if ( areaDistanceToTarget > battlefront && areaDistanceToTarget > 0.0f )
+ {
+ battlefront = areaDistanceToTarget;
+ }
+ }
+
+
+ // find the travel distance from the bomb to the delivery target and use it as the front
+ CCaptureFlag *flag = NULL;
+ Vector vBombSpot(0, 0, 0);
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *pTempFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+ Vector vTempBombSpot;
+ CTFPlayer *carrier = ToTFPlayer( pTempFlag->GetOwnerEntity() );
+ if ( carrier )
+ {
+ vTempBombSpot = carrier->GetAbsOrigin();
+ }
+ else
+ {
+ vTempBombSpot = pTempFlag->WorldSpaceCenter();
+ }
+
+ CTFNavArea *flagArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( vTempBombSpot, false, 1000.0f );
+ if ( flagArea )
+ {
+ float flagDistanceToTarget = flagArea->GetTravelDistanceToBombTarget();
+
+ if ( flagDistanceToTarget < battlefront && flagDistanceToTarget >= 0.0f )
+ {
+ battlefront = flagDistanceToTarget;
+ flag = pTempFlag;
+ vBombSpot = vTempBombSpot;
+ }
+ }
+ }
+
+ float flMaxBattlefront = battlefront + tf_bot_engineer_mvm_sentry_hint_bomb_backward_range.GetFloat();
+ float flMinBattlefront = battlefront - tf_bot_engineer_mvm_sentry_hint_bomb_forward_range.GetFloat();
+
+ if ( pBombInfo )
+ {
+ pBombInfo->m_vPosition = vBombSpot;
+ pBombInfo->m_flMinBattleFront = flMinBattlefront;
+ pBombInfo->m_flMaxBattleFront = flMaxBattlefront;
+ }
+
+ return flag ? true : false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerIdle::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ me->StopLookingAroundForEnemies();
+
+ m_sentryHint = NULL;
+ m_teleporterHint = NULL;
+ m_nestHint = NULL;
+ m_nTeleportedCount = 0;
+ m_bTeleportedToHint = false;
+ m_bTriedToDetonateStaleNest = false;
+
+ return Continue();
+}
+
+
+void CTFBotMvMEngineerIdle::TakeOverStaleNest( CBaseTFBotHintEntity* pHint, CTFBot *me )
+{
+ if ( pHint != NULL && pHint->OwnerObjectHasNoOwner() )
+ {
+ CBaseObject* pObj = static_cast< CBaseObject* >( pHint->GetOwnerEntity() );
+ pObj->SetOwnerEntity( me );
+ pObj->SetBuilder( me );
+ me->AddObject( pObj );
+ }
+}
+
+
+bool CTFBotMvMEngineerIdle::ShouldAdvanceNestSpot( CTFBot *me )
+{
+ if ( !m_nestHint )
+ {
+ return false;
+ }
+
+ if ( !m_reevaluateNestTimer.HasStarted() )
+ {
+ m_reevaluateNestTimer.Start( 5.f );
+ return false;
+ }
+
+ for ( int i=0; i<me->GetObjectCount(); ++i )
+ {
+ CBaseObject *pObj = me->GetObject( i );
+ if ( pObj && pObj->GetHealth() < pObj->GetMaxHealth() )
+ {
+ // if the nest is under attack, don't advance the nest
+ m_reevaluateNestTimer.Start( 5.f );
+ return false;
+ }
+ }
+
+ if ( m_reevaluateNestTimer.IsElapsed() )
+ {
+ m_reevaluateNestTimer.Invalidate();
+ }
+
+ BombInfo_t bombInfo;
+ if ( GetBombInfo( &bombInfo ) )
+ {
+ if ( m_nestHint )
+ {
+ CTFNavArea *hintArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( m_nestHint->GetAbsOrigin(), false, 1000.0f );
+ if ( hintArea )
+ {
+ float hintDistanceToTarget = hintArea->GetTravelDistanceToBombTarget();
+
+ bool bShouldAdvance = ( hintDistanceToTarget > bombInfo.m_flMaxBattleFront );
+
+ return bShouldAdvance;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+void CTFBotMvMEngineerIdle::TryToDetonateStaleNest()
+{
+ if ( m_bTriedToDetonateStaleNest )
+ return;
+
+ // wait until the engy finish building his nest
+ if ( ( m_sentryHint && !m_sentryHint->OwnerObjectFinishBuilding() ) ||
+ ( m_teleporterHint && !m_teleporterHint->OwnerObjectFinishBuilding() ) )
+ return;
+
+ // collect all existing and active teleporter hints
+ CUtlVector< CTFBotHintEngineerNest* > activeEngineerNest;
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity *pHint = static_cast<CBaseTFBotHintEntity*>( ITFBotHintEntityAutoList::AutoList()[i] );
+ if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_ENGINEER_NEST ) && pHint->IsEnabled() && pHint->GetOwnerEntity() == NULL )
+ {
+ activeEngineerNest.AddToTail( static_cast< CTFBotHintEngineerNest* >( pHint ) );
+ }
+ }
+
+ // try to detonate stale nest that's out of range, when engineer finished building his nest
+ for ( int i=0; i<activeEngineerNest.Count(); ++i )
+ {
+ CTFBotHintEngineerNest *pNest = activeEngineerNest[i];
+ if ( pNest->IsStaleNest() )
+ {
+ pNest->DetonateStaleNest();
+ }
+ }
+
+ m_bTriedToDetonateStaleNest = true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerIdle::Update( CTFBot *me, float interval )
+{
+ if ( !me->IsAlive() )
+ {
+ // don't do anything when I'm dead
+ return Done();
+ }
+
+ // Always equip my wrench
+ CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( wrench )
+ {
+ me->Weapon_Switch( wrench );
+ }
+
+ if ( m_nestHint == NULL || ShouldAdvanceNestSpot( me ) )
+ {
+ if ( m_findHintTimer.HasStarted() && !m_findHintTimer.IsElapsed() )
+ {
+ // too soon
+ return Continue();
+ }
+
+ m_findHintTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ // figure out where to teleport into the map
+ bool bShouldTeleportToHint = me->HasAttribute( CTFBot::TELEPORT_TO_HINT );
+ bool bShouldCheckForBlockingObject = !m_bTeleportedToHint && bShouldTeleportToHint;
+ CHandle< CTFBotHintEngineerNest > newNest = NULL;
+ if ( !CTFBotMvMEngineerHintFinder::FindHint( bShouldCheckForBlockingObject, !bShouldTeleportToHint, &newNest ) )
+ {
+ // try again next time
+ return Continue();
+ }
+
+ // unown the old nest
+ if ( m_nestHint )
+ {
+ m_nestHint->SetOwnerEntity( NULL );
+ }
+
+ m_nestHint = newNest;
+ m_nestHint->SetOwnerEntity( me );
+ m_sentryHint = m_nestHint->GetSentryHint();
+ TakeOverStaleNest( m_sentryHint, me );
+
+ if ( me->GetTeleportWhere().Count() > 0 )
+ {
+ m_teleporterHint = m_nestHint->GetTeleporterHint();
+ TakeOverStaleNest( m_teleporterHint, me );
+ }
+ }
+
+ if ( !m_bTeleportedToHint && me->HasAttribute( CTFBot::TELEPORT_TO_HINT ) )
+ {
+ m_nTeleportedCount++;
+ bool bFirstTeleportSpawn = m_nTeleportedCount == 1;
+ m_bTeleportedToHint = true;
+ return SuspendFor( new CTFBotMvMEngineerTeleportSpawn( m_nestHint, bFirstTeleportSpawn ), "In spawn area - teleport to the teleporter hint" );
+ }
+
+ const float rebuildInterval = 3.0f;
+ CObjectSentrygun *mySentry = NULL;
+ if ( m_sentryHint )
+ {
+ if ( m_sentryHint->GetOwnerEntity() && m_sentryHint->GetOwnerEntity()->IsBaseObject() )
+ {
+ mySentry = assert_cast< CObjectSentrygun* >( m_sentryHint->GetOwnerEntity() );
+ }
+
+ if ( mySentry )
+ {
+ // force an interval between sentry being destroyed and me trying to rebuild it
+ m_sentryRebuildTimer.Start( rebuildInterval );
+ }
+ else
+ {
+ // check if there's a stale object on the hint
+ if ( m_sentryHint->GetOwnerEntity() && m_sentryHint->GetOwnerEntity()->IsBaseObject() )
+ {
+ mySentry = assert_cast< CObjectSentrygun* >( m_sentryHint->GetOwnerEntity() );
+ me->AddObject( mySentry );
+ mySentry->SetOwnerEntity( me );
+ }
+ else
+ {
+ if ( m_sentryRebuildTimer.IsElapsed() )
+ {
+ return SuspendFor( new CTFBotMvMEngineerBuildSentryGun( m_sentryHint ), "No sentry - building a new one" );
+ }
+ else
+ {
+ // run away!
+ return SuspendFor( new CTFBotRetreatToCover( 1.0f ), "Lost my sentry - retreat!" );
+ }
+ }
+ }
+ }
+
+ if ( mySentry && mySentry->GetHealth() < mySentry->GetMaxHealth() && !mySentry->IsBuilding() )
+ {
+ // track when sentry was last hurt
+ m_sentryInjuredTimer.Start( 3.0f );
+ }
+
+
+ CObjectTeleporter *myTeleporter = NULL;
+ if ( m_teleporterHint && m_sentryInjuredTimer.IsElapsed() )
+ {
+ if ( m_teleporterHint->GetOwnerEntity() && m_teleporterHint->GetOwnerEntity()->IsBaseObject() )
+ {
+ // force an interval between teleporter being destroyed and me trying to rebuild it
+ myTeleporter = assert_cast< CObjectTeleporter* >( m_teleporterHint->GetOwnerEntity() );
+ m_teleporterRebuildTimer.Start( rebuildInterval );
+ }
+ else if ( m_teleporterRebuildTimer.IsElapsed() )
+ {
+ return SuspendFor( new CTFBotMvMEngineerBuildTeleportExit( m_teleporterHint ), "Sentry is safe - building a teleport exit" );
+ }
+ }
+
+ // fix teleporter if sentry is not hurt
+ if ( myTeleporter && m_sentryInjuredTimer.IsElapsed() && myTeleporter->GetHealth() < myTeleporter->GetMaxHealth() && !myTeleporter->IsBuilding() )
+ {
+ float rangeToTeleporter = me->GetDistanceBetween( myTeleporter );
+
+ const float nearTeleporterRange = 75.0f;
+
+ if ( rangeToTeleporter < 1.2f * nearTeleporterRange )
+ {
+ // crouch as I get close
+ me->PressCrouchButton();
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ Vector toTeleporter = myTeleporter->GetAbsOrigin() - me->GetAbsOrigin();
+ Vector hittingTeleporterSpot = myTeleporter->GetAbsOrigin() - 50.0f * toTeleporter.Normalized();
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, hittingTeleporterSpot, cost );
+ }
+
+ m_path.Update( me );
+
+ if ( rangeToTeleporter < nearTeleporterRange )
+ {
+ // we are in position - hit sentry with wrench
+ me->GetBodyInterface()->AimHeadTowards( myTeleporter->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Teleporter" );
+ me->PressFireButton();
+ }
+ }
+ else if ( mySentry )
+ {
+ float rangeToSentry = me->GetDistanceBetween( mySentry );
+
+ const float nearSentryRange = 75.0f;
+
+ if ( rangeToSentry < 1.2f * nearSentryRange )
+ {
+ // crouch as I get close
+ me->PressCrouchButton();
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ Vector mySentryForward;
+ AngleVectors( mySentry->GetTurretAngles(), &mySentryForward );
+
+ Vector behindSentrySpot = mySentry->GetAbsOrigin() - 50.0f * mySentryForward;
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, behindSentrySpot, cost );
+ }
+
+ m_path.Update( me );
+
+ if ( rangeToSentry < nearSentryRange )
+ {
+ // we are in position - hit sentry with wrench
+ me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Sentry" );
+ me->PressFireButton();
+ }
+ }
+
+ TryToDetonateStaleNest();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMvMEngineerIdle::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMvMEngineerIdle::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMvMEngineerIdle::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+CTFBotHintEngineerNest* SelectOutOfRangeNest( const CUtlVector< CTFBotHintEngineerNest* >& nestVector )
+{
+ if ( nestVector.Count() )
+ {
+ for ( int i=0; i<nestVector.Count(); ++i )
+ {
+ if ( nestVector[i]->IsStaleNest() )
+ {
+ return nestVector[i];
+ }
+ }
+
+ int which = RandomInt( 0, nestVector.Count() - 1 );
+ return nestVector[which];
+ }
+
+ return NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotMvMEngineerHintFinder::FindHint( bool bShouldCheckForBlockingObjects, bool bAllowOutOfRangeNest, CHandle< CTFBotHintEngineerNest >* pFoundNest /*= NULL*/ )
+{
+ // collect all existing and active teleporter hints
+ CUtlVector< CTFBotHintEngineerNest* > activeEngineerNest;
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity *pHint = static_cast<CBaseTFBotHintEntity*>( ITFBotHintEntityAutoList::AutoList()[i] );
+ if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_ENGINEER_NEST ) && pHint->IsEnabled() && pHint->GetOwnerEntity() == NULL )
+ {
+ activeEngineerNest.AddToTail( static_cast< CTFBotHintEngineerNest* >( pHint ) );
+ }
+ }
+
+ if ( activeEngineerNest.Count() == 0 )
+ {
+ if ( pFoundNest )
+ {
+ *pFoundNest = NULL;
+ }
+
+ return false;
+ }
+
+ BombInfo_t bombInfo;
+ GetBombInfo( &bombInfo );
+
+ CUtlVector< CTFBotHintEngineerNest* > forwardOutOfRangeHintVector;
+ CUtlVector< CTFBotHintEngineerNest* > backwardOutOfRangeHintVector;
+
+ CUtlVector< CTFBotHintEngineerNest* > freeAtFrontHintVector;
+ CUtlVector< CTFBotHintEngineerNest* > staleAtFrontHintVector;
+ for( int i=0; i<activeEngineerNest.Count(); ++i )
+ {
+ CTFBotHintEngineerNest* pCurrentNest = activeEngineerNest[i];
+ const Vector& vNestPosition = pCurrentNest->GetAbsOrigin();
+ CTFNavArea *hintArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( vNestPosition, false, 1000.0f );
+ if ( !hintArea )
+ {
+ Warning( "Sentry hint has NULL nav area!\n" );
+ continue;
+ }
+
+
+ float hintDistanceToTarget = hintArea->GetTravelDistanceToBombTarget();
+ if ( hintDistanceToTarget > bombInfo.m_flMinBattleFront && hintDistanceToTarget < bombInfo.m_flMaxBattleFront )
+ {
+ if ( bShouldCheckForBlockingObjects )
+ {
+ // check for blocking players and objects
+ CBaseEntity *pList[256];
+ int count = UTIL_EntitiesInBox( pList, ARRAYSIZE( pList ), vNestPosition + VEC_HULL_MIN, vNestPosition + VEC_HULL_MAX, FL_CLIENT|FL_OBJECT );
+ if ( count > 0 )
+ {
+ continue;
+ }
+ }
+
+ // this hint is in range of the front
+ if ( pCurrentNest->IsStaleNest() )
+ {
+ // some dead engineer was here and left his object(s) behind. I should take over
+ staleAtFrontHintVector.AddToTail( pCurrentNest );
+ }
+ else
+ {
+ if ( VectorLength( bombInfo.m_vPosition - vNestPosition ) < tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat() )
+ {
+ // the hint is too close to the bomb, don't go there
+ continue;
+ }
+ // this hint is also unowned
+ freeAtFrontHintVector.AddToTail( pCurrentNest );
+ }
+ }
+ else if ( hintDistanceToTarget > bombInfo.m_flMaxBattleFront )
+ {
+ forwardOutOfRangeHintVector.AddToTail( pCurrentNest );
+ }
+ else
+ {
+ backwardOutOfRangeHintVector.AddToTail( pCurrentNest );
+ }
+ }
+
+ CTFBotHintEngineerNest *hint = NULL;
+ if ( freeAtFrontHintVector.Count() == 0 && staleAtFrontHintVector.Count() == 0 )
+ {
+ if ( bAllowOutOfRangeNest )
+ {
+ // try to advance forward before falling backward
+ hint = SelectOutOfRangeNest( forwardOutOfRangeHintVector );
+ if ( !hint )
+ {
+ hint = SelectOutOfRangeNest( backwardOutOfRangeHintVector );
+ }
+ }
+
+ // no hints are in range, or they are all in use
+ if ( pFoundNest )
+ {
+ *pFoundNest = hint;
+ }
+ }
+ else
+ {
+ // try to pick stale nest in range first
+ if ( staleAtFrontHintVector.Count() )
+ {
+ int whichHint = RandomInt( 0, staleAtFrontHintVector.Count()-1 );
+ hint = staleAtFrontHintVector[ whichHint ];
+ }
+ // if I didn't find any stale nest, try to find a free one
+ else if ( freeAtFrontHintVector.Count() )
+ {
+ int whichHint = RandomInt( 0, freeAtFrontHintVector.Count()-1 );
+ hint = freeAtFrontHintVector[ whichHint ];
+ }
+
+ if ( pFoundNest )
+ {
+ *pFoundNest = hint;
+ }
+ }
+
+ return hint != NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_mvm_show_engineer_hint_region, "Show the nav areas MvM engineer bots will consider when selecting sentry and teleporter hints", FCVAR_CHEAT )
+{
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ CBasePlayer *pPlayer = UTIL_GetCommandClient();
+
+ trace_t result;
+ Vector forward;
+ pPlayer->EyeVectors( &forward );
+
+ UTIL_TraceLine( pPlayer->EyePosition(),
+ pPlayer->EyePosition() + forward * 10000.0f, MASK_SOLID,
+ pPlayer, COLLISION_GROUP_NONE, &result );
+
+ float flDrawTime = 5.0f;
+
+ if ( result.DidHit() )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( result.endpos );
+
+ if ( area )
+ {
+ float battlefront = area->GetTravelDistanceToBombTarget();
+
+ float maxBattlefront = battlefront + tf_bot_engineer_mvm_sentry_hint_bomb_backward_range.GetFloat();
+ float minBattlefront = battlefront - tf_bot_engineer_mvm_sentry_hint_bomb_forward_range.GetFloat();
+
+ CUtlVector< CTFNavArea * > battlefrontAreaVector;
+ TheTFNavMesh()->CollectAreaWithinBombTravelRange( &battlefrontAreaVector, minBattlefront, maxBattlefront );
+
+ CUtlVector< CTFNavArea * > hintAreaVector;
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] );
+ hintAreaVector.AddToTail( (CTFNavArea*)TheNavMesh->GetNearestNavArea( pHint ) );
+ }
+
+ for( int i=0; i<battlefrontAreaVector.Count(); ++i )
+ {
+ CTFNavArea *fillArea = battlefrontAreaVector[i];
+
+ if ( fillArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE || TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ continue;
+ }
+
+ fillArea->DrawFilled( 255, 100, 0, 0, flDrawTime );
+
+ for ( int j=0; j<hintAreaVector.Count(); ++j )
+ {
+ if ( fillArea == hintAreaVector[j] )
+ {
+ CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[j] );
+ Color color;
+ if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_SENTRYGUN ) )
+ {
+ color = Color( 0, 255, 0 );
+ }
+ else if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_TELEPORTER_EXIT ) )
+ {
+ color = Color( 0, 0, 255 );
+ }
+ else
+ {
+ bool bTooCloseToBomb = VectorLength( result.endpos - pHint->GetAbsOrigin() ) < tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat();
+ color = bTooCloseToBomb ? Color( 255, 0, 0 ) : Color( 255, 255, 0 );
+ }
+ NDebugOverlay::Sphere( pHint->GetAbsOrigin(), 50, color.r(), color.g(), color.b(), true, flDrawTime );
+ }
+ }
+ }
+
+ NDebugOverlay::Sphere( result.endpos, tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat(), 255, 255, 0, false, flDrawTime );
+ }
+ }
+}
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h
new file mode 100644
index 0000000..13ea69f
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h
@@ -0,0 +1,55 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// Michael Booth, September 2012
+
+#ifndef TF_BOT_MVM_ENGINEER_IDLE_H
+#define TF_BOT_MVM_ENGINEER_IDLE_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CBaseTFBotHintEntity;
+class CTFBotHintSentrygun;
+class CTFBotHintTeleporterExit;
+class CTFBotHintEngineerNest;
+
+class CTFBotMvMEngineerIdle : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const;
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "MvMEngineerIdle"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ CountdownTimer m_sentryInjuredTimer;
+ CountdownTimer m_sentryRebuildTimer;
+ CountdownTimer m_teleporterRebuildTimer;
+ CountdownTimer m_findHintTimer;
+ CountdownTimer m_reevaluateNestTimer;
+
+ int m_nTeleportedCount;
+ bool m_bTeleportedToHint;
+ CHandle< CTFBotHintTeleporterExit > m_teleporterHint;
+ CHandle< CTFBotHintSentrygun > m_sentryHint;
+ CHandle< CTFBotHintEngineerNest > m_nestHint;
+
+ void TakeOverStaleNest( CBaseTFBotHintEntity* pHint, CTFBot *me );
+ bool ShouldAdvanceNestSpot( CTFBot *me );
+
+ void TryToDetonateStaleNest();
+ bool m_bTriedToDetonateStaleNest;
+};
+
+class CTFBotMvMEngineerHintFinder
+{
+public:
+ static bool FindHint( bool bShouldCheckForBlockingObjects, bool bAllowOutOfRangeNest, CHandle< CTFBotHintEngineerNest >* pFoundNest = NULL );
+};
+
+
+#endif // TF_BOT_MVM_ENGINEER_IDLE_H
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp
new file mode 100644
index 0000000..5b439e6
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp
@@ -0,0 +1,95 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_gamerules.h"
+#include "bot/tf_bot.h"
+#include "tf_obj_sentrygun.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h"
+#include "bot/map_entities/tf_bot_hint_entity.h"
+#include "string_t.h"
+#include "tf_fx.h"
+#include "player_vs_environment/tf_population_manager.h"
+
+//---------------------------------------------------------------------------------------------
+CTFBotMvMEngineerTeleportSpawn::CTFBotMvMEngineerTeleportSpawn( CBaseTFBotHintEntity* pHint, bool bFirstTeleportSpawn )
+{
+ m_hintEntity = pHint;
+ m_bFirstTeleportSpawn = bFirstTeleportSpawn;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerTeleportSpawn::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( !me->HasAttribute( CTFBot::TELEPORT_TO_HINT ) )
+ {
+ return Done( "Cannot teleport to hint with out Attributes TeleportToHint" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMEngineerTeleportSpawn::Update( CTFBot *me, float interval )
+{
+ if ( !m_teleportDelay.HasStarted() )
+ {
+ m_teleportDelay.Start( 0.1f );
+ if ( m_hintEntity )
+ TFGameRules()->PushAllPlayersAway( m_hintEntity->GetAbsOrigin(), 400, 500, TF_TEAM_RED );
+ }
+ else if ( m_teleportDelay.IsElapsed() )
+ {
+ if ( !m_hintEntity )
+ return Done( "Cannot teleport to hint as m_hintEntity is NULL" );
+
+ // teleport the engineer to the sentry spawn point
+ QAngle angles = m_hintEntity->GetAbsAngles();
+ Vector origin = m_hintEntity->GetAbsOrigin();
+ origin.z += 10.f; // move up off the around a little bit to prevent the engineer from getting stuck in the ground
+
+ me->Teleport( &origin, &angles, NULL );
+
+ CPVSFilter filter( origin );
+ TE_TFParticleEffect( filter, 0.0, "teleported_blue", origin, vec3_angle );
+ TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", origin, vec3_angle );
+
+ if ( m_bFirstTeleportSpawn )
+ {
+ // notify players that engineer's teleported into the map
+ TE_TFParticleEffect( filter, 0.0, "teleported_mvm_bot", origin, vec3_angle );
+ me->EmitSound( "Engineer.MVM_BattleCry07" );
+ m_hintEntity->EmitSound( "MVM.Robot_Engineer_Spawn" );
+
+ if ( g_pPopulationManager )
+ {
+ CWave *pWave = g_pPopulationManager->GetCurrentWave();
+ if ( pWave )
+ {
+ if ( pWave->NumEngineersTeleportSpawned() == 0 )
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_First_Engineer_Teleport_Spawned" );
+ }
+ else
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Another_Engineer_Teleport_Spawned" );
+ }
+
+ pWave->IncrementEngineerTeleportSpawned();
+ }
+ }
+ }
+
+ return Done( "Teleported" );
+ }
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h
new file mode 100644
index 0000000..cd2f32b
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h
@@ -0,0 +1,28 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+
+#ifndef TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H
+#define TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H
+
+class CBaseTFBotHintEntity;
+
+class CTFBotMvMEngineerTeleportSpawn : public Action< CTFBot >
+{
+public:
+ CTFBotMvMEngineerTeleportSpawn( CBaseTFBotHintEntity* pHint, bool bFirstTeleportSpawn );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "MvMEngineerTeleportSpawn"; };
+
+private:
+ CountdownTimer m_teleportDelay;
+ CHandle< CBaseTFBotHintEntity > m_hintEntity;
+ bool m_bFirstTeleportSpawn;
+};
+
+#endif // TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp
new file mode 100644
index 0000000..e6d586e
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp
@@ -0,0 +1,118 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build.cpp
+// Engineer building his buildings
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+
+
+#include "raid/tf_raid_logic.h"
+
+// this was useful when engineers build at their normal (slow) rate to make sure initial sentries get built in time
+ConVar tf_raid_engineer_infinte_metal( "tf_raid_engineer_infinte_metal", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
+
+
+//---------------------------------------------------------------------------------------------
+Action< CTFBot > *CTFBotEngineerBuild::InitialContainedAction( CTFBot *me )
+{
+ if ( TFGameRules()->IsPVEModeActive() )
+ {
+ return new CTFBotEngineerMoveToBuild;
+ }
+
+ return new CTFBotEngineerBuildTeleportEntrance;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuild::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuild::Update( CTFBot *me, float interval )
+{
+ if ( TFGameRules()->IsPVEModeActive() && tf_raid_engineer_infinte_metal.GetBool() )
+ {
+ // infinite ammo
+ me->GiveAmmo( 1000, TF_AMMO_METAL, true );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuild::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerBuild::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Hack to disable ammo/health gathering elsewhere
+QueryResultType CTFBotEngineerBuild::ShouldHurry( const INextBot *meBot ) const
+{
+ CTFBot *me = (CTFBot *)meBot->GetEntity();
+
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+ CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER );
+
+ if ( mySentry && myDispenser && !mySentry->IsBuilding() && !myDispenser->IsBuilding() && me->GetActiveTFWeapon() && me->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_WRENCH )
+ {
+ if ( me->IsAmmoLow() && myDispenser->GetAvailableMetal() <= 0 )
+ {
+ // we're totally out of metal - collect some nearby
+ return ANSWER_NO;
+ }
+
+ // by being in a "hurry" we wont collect health and ammo
+ return ANSWER_YES;
+ }
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotEngineerBuild::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const
+{
+ CTFBot *me = (CTFBot *)meBot->GetEntity();
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+
+ CTFPlayer *themPlayer = ToTFPlayer( them->GetEntity() );
+
+ if ( themPlayer && themPlayer->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ // Engineers hate Spies
+ return ANSWER_YES;
+ }
+
+ if ( mySentry && me->IsRangeLessThan( mySentry, 100.0f ) )
+ {
+ // focus on keeping our sentry alive
+ return ANSWER_NO;
+ }
+
+ return ANSWER_UNDEFINED;
+}
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h
new file mode 100644
index 0000000..2ff6265
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h
@@ -0,0 +1,31 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build.h
+// Engineer building his buildings
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_ENGINEER_BUILD_H
+#define TF_BOT_ENGINEER_BUILD_H
+
+class CTFBotHintTeleporterExit;
+
+
+class CTFBotEngineerBuild : public Action< CTFBot >
+{
+public:
+ virtual Action< CTFBot > *InitialContainedAction( CTFBot *me );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "EngineerBuild"; };
+};
+
+
+#endif // TF_BOT_ENGINEER_BUILD_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp
new file mode 100644
index 0000000..eabd9ba
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp
@@ -0,0 +1,212 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_dispenser.cpp
+// Engineer building his Dispenser near his Sentry
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_dispenser.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildDispenser::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_placementTriesLeft = 3;
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+class PressFireButtonIfValidBuildPositionReply : public INextBotReply
+{
+public:
+ PressFireButtonIfValidBuildPositionReply( void )
+ {
+ m_builder = NULL;
+ }
+
+ void SetBuilder( CTFWeaponBuilder *builder )
+ {
+ m_builder = builder;
+ }
+
+ // invoked when process completed successfully
+ virtual void OnSuccess( INextBot *bot )
+ {
+ if ( m_builder != NULL && m_builder->IsValidPlacement() )
+ {
+ INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() );
+ if ( playerInput )
+ {
+ playerInput->PressFireButton();
+ }
+ }
+ }
+
+ CTFWeaponBuilder *m_builder;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildDispenser::Update( CTFBot *me, float interval )
+{
+ if ( me->GetTimeSinceLastInjury() < 1.0f )
+ {
+ return Done( "Ouch! I'm under attack" );
+ }
+
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+ if ( !mySentry )
+ {
+ return Done( "No Sentry" );
+ }
+
+ if ( mySentry->GetTimeSinceLastInjury() < 1.0f || mySentry->GetHealth() < mySentry->GetMaxHealth() )
+ {
+ return Done( "Need to repair my Sentry" );
+ }
+
+ CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER );
+ if ( myDispenser )
+ {
+ return Done( "Dispenser built" );
+ }
+
+ if ( m_placementTriesLeft <= 0 )
+ {
+ return Done( "Can't find a place to build a Dispenser" );
+ }
+
+ if ( me->CanBuild( OBJ_DISPENSER ) == CB_NEED_RESOURCES )
+ {
+ if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) )
+ {
+ // need more metal - get some
+ m_getAmmoTimer.Start( 1.0f );
+ return SuspendFor( new CTFBotGetAmmo, "Need more metal to build" );
+ }
+/*
+ else
+ {
+ // work on my sentry while I wait for ammo to show up
+ me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on sentry while I wait for ammo to show up" );
+ me->PressFireButton();
+ return Continue();
+ }
+*/
+ }
+
+
+/*
+ // if my sentry is under attack, forgo building a dispenser - focus on keeping the sentry alive
+ if ( mySentry->GetTimeSinceLastInjury() < 1.0f )
+ {
+ CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( wrench )
+ {
+ me->Weapon_Switch( wrench );
+ }
+
+ me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Focusing on keeping my besieged sentry alive" );
+ me->PressFireButton();
+
+ return Continue();
+ }
+*/
+
+
+ // move behind the Sentry (our chosen build location)
+ Vector buildSpot = mySentry->GetAbsOrigin() - 75.0f * mySentry->BodyDirection2D();
+
+ // the ground might be steeply sloped (ie: stairs), so find the actual ground
+ buildSpot.z += HumanHeight;
+ TheNavMesh->GetSimpleGroundHeight( buildSpot, &buildSpot.z );
+
+ if ( me->IsDistanceBetweenLessThan( buildSpot, 100.0f ) )
+ {
+ // crouch as we get close so we slow down and hit our mark
+ me->PressCrouchButton();
+ }
+
+ // if too far away from our build location, move closer
+ if ( me->IsDistanceBetweenGreaterThan( buildSpot, 25.0f ) )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, buildSpot, cost );
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+ }
+
+ // we're at our build spot behind our sentry now - build a Dispenser
+ CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( me->GetActiveTFWeapon() );
+ if ( !builder || builder->GetType() != OBJ_DISPENSER || builder->m_hObjectBeingBuilt == NULL )
+ {
+ // at home position, build the object
+ me->StartBuildingObjectOfType( OBJ_DISPENSER );
+ }
+ else if ( m_searchTimer.IsElapsed() )
+ {
+ // rotate to find valid spot
+ Vector toSentry = mySentry->GetAbsOrigin() - me->GetAbsOrigin();
+ toSentry.NormalizeInPlace();
+
+ Vector forward = -toSentry;
+
+ float angle = RandomFloat( -M_PI/2.0f, M_PI/2.0f );
+ float s, c;
+ FastSinCos( angle, &s, &c );
+
+ forward.x = toSentry.x * c - toSentry.y * s;
+ forward.y = toSentry.x * s + toSentry.y * c;
+ forward.z = 0.0f;
+
+ static PressFireButtonIfValidBuildPositionReply buildReply;
+
+ buildReply.SetBuilder( builder );
+ me->GetBodyInterface()->AimHeadTowards( me->EyePosition() - 100.0f * forward, IBody::CRITICAL, 1.0f, &buildReply, "Trying to place my dispenser" );
+
+ m_searchTimer.Start( 1.0f );
+
+ --m_placementTriesLeft;
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotEngineerBuildDispenser::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->GetBodyInterface()->ClearPendingAimReply();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildDispenser::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ m_repathTimer.Invalidate();
+ me->GetBodyInterface()->ClearPendingAimReply();
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h
new file mode 100644
index 0000000..12e5dbf
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h
@@ -0,0 +1,31 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_dispenser.h
+// Engineer building his Dispenser near his Sentry
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_ENGINEER_BUILD_DISPENSER_H
+#define TF_BOT_ENGINEER_BUILD_DISPENSER_H
+
+
+class CTFBotEngineerBuildDispenser : 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 > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual const char *GetName( void ) const { return "EngineerBuildDispenser"; };
+
+private:
+ CountdownTimer m_searchTimer;
+ CountdownTimer m_getAmmoTimer;
+ CountdownTimer m_repathTimer;
+
+ int m_placementTriesLeft;
+ PathFollower m_path;
+};
+
+
+#endif // TF_BOT_ENGINEER_BUILD_DISPENSER_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp
new file mode 100644
index 0000000..b1beb35
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp
@@ -0,0 +1,194 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_sentrygun.cpp
+// Engineer building his Sentry gun
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuildSentryGun::CTFBotEngineerBuildSentryGun( void )
+{
+ m_sentryBuildHint = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuildSentryGun::CTFBotEngineerBuildSentryGun( CTFBotHintSentrygun *sentryBuildHint )
+{
+ m_sentryBuildHint = sentryBuildHint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_sentryTriesLeft = 5;
+ m_giveUpTimer.Invalidate();
+
+ m_searchTimer.Invalidate();
+ m_wanderWay = 1;
+ m_needToAimSentry = true;
+
+ m_sentryBuildLocation = ( m_sentryBuildHint == NULL ) ? me->GetAbsOrigin() : m_sentryBuildHint->GetAbsOrigin();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::Update( CTFBot *me, float interval )
+{
+ if ( me->GetTimeSinceLastInjury() < 1.0f )
+ {
+ return Done( "Ouch! I'm under attack" );
+ }
+
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+ if ( mySentry )
+ {
+ return Done( "Sentry built" );
+ }
+
+ // collect metal as we move to our build location
+ if ( me->CanBuild( OBJ_SENTRYGUN ) == CB_NEED_RESOURCES )
+ {
+ if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) )
+ {
+ // need more metal - get some
+ m_getAmmoTimer.Start( 1.0f );
+ return SuspendFor( new CTFBotGetAmmo, "Need more metal to build my Sentry" );
+ }
+ }
+
+ // various interruptions could mean we're away from our build location - move to it
+ if ( me->IsRangeGreaterThan( m_sentryBuildLocation, 25.0f ) )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_sentryBuildLocation, cost );
+ }
+
+ m_path.Update( me );
+
+ if ( !m_path.IsValid() )
+ {
+ return Done( "Path failed" );
+ }
+
+ return Continue();
+ }
+
+ // we are at our build location
+ if ( m_sentryTriesLeft <= 0 )
+ {
+ // couldn't build here
+ return Done( "Couldn't find a place to build" );
+ }
+
+ // attempt to build a Sentry here
+ if ( m_sentryBuildHint != NULL )
+ {
+ // directly create a sentry gun at the precise position and orientation desired
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" );
+ if ( mySentry )
+ {
+ m_sentryBuildHint->IncrementUseCount();
+
+ mySentry->SetAbsOrigin( m_sentryBuildHint->GetAbsOrigin() );
+ mySentry->SetAbsAngles( QAngle( 0, m_sentryBuildHint->GetAbsAngles().y, 0 ) );
+ mySentry->Spawn();
+
+ mySentry->StartPlacement( me );
+ mySentry->StartBuilding( me );
+ }
+ }
+ else
+ {
+ // no precise build location - go through the normal build process
+
+ CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( me->GetActiveTFWeapon() );
+ if ( !builder || builder->GetType() != OBJ_SENTRYGUN || builder->m_hObjectBeingBuilt == NULL )
+ {
+ // at home position, build a sentry (switch to the sentry builder "gun")
+ me->StartBuildingObjectOfType( OBJ_SENTRYGUN );
+ return Continue();
+ }
+
+ // orient sentry towards where enemies enter this region
+ if ( m_needToAimSentry )
+ {
+ CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea();
+ if ( myArea )
+ {
+ CUtlVector< CTFNavArea * > invasionVector;
+ myArea->GetEnemyInvasionAreaVector( me->GetTeamNumber() );
+
+ if ( invasionVector.Count() > 0 )
+ {
+ // orient sentry towards where enemies enter this region
+ int which = RandomInt( 0, invasionVector.Count()-1 );
+ me->GetBodyInterface()->AimHeadTowards( invasionVector[ which ]->GetCenter(), IBody::CRITICAL, 1.0f, NULL, "Sentry build orientation" );
+ m_needToAimSentry = false;
+ }
+ }
+ }
+
+ if ( me->GetBodyInterface()->IsHeadSteady() )
+ {
+ if ( builder->IsValidPlacement() )
+ {
+ // build the sentry
+ me->PressFireButton();
+ }
+ else
+ {
+ // move around a bit to find valid spot
+ if ( m_searchTimer.IsElapsed() )
+ {
+ m_wanderWay = RandomInt( 0, 3 );
+ m_needToAimSentry = true;
+ m_searchTimer.Start( RandomFloat( 0.1f, 0.25f ) );
+ --m_sentryTriesLeft;
+ }
+
+ switch( m_wanderWay )
+ {
+ case 0: me->PressForwardButton(); break;
+ case 1: me->PressBackwardButton(); break;
+ case 2: me->PressRightButton(); break;
+ case 3: me->PressLeftButton(); break;
+ }
+ }
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ m_repathTimer.Invalidate();
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h
new file mode 100644
index 0000000..774b7ca
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h
@@ -0,0 +1,44 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_sentrygun.h
+// Engineer building his Sentry gun
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_ENGINEER_BUILD_SENTRYGUN_H
+#define TF_BOT_ENGINEER_BUILD_SENTRYGUN_H
+
+class CTFBotHintSentrygun;
+
+
+class CTFBotEngineerBuildSentryGun : public Action< CTFBot >
+{
+public:
+ CTFBotEngineerBuildSentryGun( void );
+ CTFBotEngineerBuildSentryGun( CTFBotHintSentrygun *sentryBuildHint );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual const char *GetName( void ) const { return "EngineerBuildSentryGun"; };
+
+private:
+ CountdownTimer m_searchTimer;
+ CountdownTimer m_giveUpTimer;
+ CountdownTimer m_getAmmoTimer;
+ CountdownTimer m_repathTimer;
+ CountdownTimer m_buildTeleporterExitTimer;
+
+ int m_sentryTriesLeft;
+ PathFollower m_path;
+
+ CTFBotHintSentrygun *m_sentryBuildHint;
+ Vector m_sentryBuildLocation;
+
+ int m_wanderWay;
+ bool m_needToAimSentry;
+ Vector m_sentryBuildAimTarget;
+};
+
+
+#endif // TF_BOT_ENGINEER_BUILD_SENTRYGUN_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp
new file mode 100644
index 0000000..c6924aa
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp
@@ -0,0 +1,112 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_teleport_entrance.cpp
+// Engineer building a teleport entrance right outside of the spawn room
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_max_teleport_entrance_travel( "tf_bot_max_teleport_entrance_travel", "1500", FCVAR_CHEAT, "Don't plant teleport entrances farther than this travel distance from our spawn room" );
+ConVar tf_bot_teleport_build_surface_normal_limit( "tf_bot_teleport_build_surface_normal_limit", "0.99", FCVAR_CHEAT, "If the ground normal Z component is less that this value, Engineer bots won't place their entrance teleporter" );
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::Update( CTFBot *me, float interval )
+{
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( !point )
+ {
+ // wait until a control point becomes available
+ return Continue();
+ }
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+
+ if ( !myArea )
+ {
+ return Done( "No nav mesh!" );
+ }
+
+ if ( myArea->GetIncursionDistance( me->GetTeamNumber() ) > tf_bot_max_teleport_entrance_travel.GetFloat() )
+ {
+ return ChangeTo( new CTFBotEngineerMoveToBuild, "Too far from our spawn room to build teleporter entrance" );
+ }
+
+ // make sure we go back to our resupply cabinet after planting the teleporter entrance before we move on
+ if ( !me->IsAmmoFull() && CTFBotGetAmmo::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotGetAmmo, "Refilling ammo" );
+ }
+
+ CBaseObject *myTeleportEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
+ if ( myTeleportEntrance )
+ {
+ // successfully built
+ return ChangeTo( new CTFBotEngineerMoveToBuild, "Teleport entrance built" );
+ }
+
+ // head towards the control point and build as soon as we can
+ if ( !m_path.IsValid() )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, point->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+
+ // build
+ CTFWeaponBase *myGun = me->GetActiveTFWeapon();
+ if ( myGun )
+ {
+ CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( myGun );
+ if ( builder )
+ {
+ // don't build on slopes - causes player blockages
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ const float placementRange = 30.0f;
+ forward *= placementRange;
+
+ trace_t result;
+ UTIL_TraceLine( me->WorldSpaceCenter() + Vector( forward.x, forward.y, 0.0f ), me->WorldSpaceCenter() + Vector( forward.x, forward.y, -200.0f ), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result );
+
+ if ( builder->IsValidPlacement() && result.DidHit() && result.plane.normal.z > tf_bot_teleport_build_surface_normal_limit.GetFloat() )
+ {
+ // place it down
+ me->PressFireButton();
+ }
+ }
+ else
+ {
+ // switch to teleporter builder
+ me->StartBuildingObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+
+ return TryContinue();
+}
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h
new file mode 100644
index 0000000..cf59418
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h
@@ -0,0 +1,23 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_teleport_entrance.h
+// Engineer building a teleport entrance right outside of the spawn room
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H
+#define TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H
+
+class CTFBotEngineerBuildTeleportEntrance : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ virtual const char *GetName( void ) const { return "EngineerBuildTeleportEntrance"; };
+
+private:
+ PathFollower m_path;
+};
+
+#endif // TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp
new file mode 100644
index 0000000..4c621a5
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp
@@ -0,0 +1,190 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_teleport_exit.cpp
+// Engineer building a teleport exit
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_weapon_builder.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuildTeleportExit::CTFBotEngineerBuildTeleportExit( void )
+{
+ m_hasPreciseBuildLocation = false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuildTeleportExit::CTFBotEngineerBuildTeleportExit( const Vector &buildLocation, float buildAngle )
+{
+ m_hasPreciseBuildLocation = true;
+ m_buildLocation = buildLocation;
+ m_buildAngle = buildAngle;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( !m_hasPreciseBuildLocation )
+ {
+ // if no specific build location given, just build right where we are
+ m_buildLocation = me->GetAbsOrigin();
+ }
+
+ m_giveUpTimer.Start( 3.1f );
+ m_path.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::Update( CTFBot *me, float interval )
+{
+ if ( me->GetTimeSinceLastInjury() < 1.0f )
+ {
+ return Done( "Ouch! I'm under attack" );
+ }
+
+ CBaseObject *myTeleportEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT );
+ if ( myTeleportEntrance )
+ {
+ // successfully built
+ return Done( "Teleport exit built" );
+ }
+
+
+ // collect metal as we move to our build location
+ if ( me->CanBuild( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) == CB_NEED_RESOURCES )
+ {
+ if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) )
+ {
+ // need more metal - get some
+ m_getAmmoTimer.Start( 1.0f );
+ return SuspendFor( new CTFBotGetAmmo, "Need more metal to build my Teleporter Exit" );
+ }
+ }
+
+ // move near our build position
+ const float buildRange = 50.0f;
+ if ( me->IsRangeGreaterThan( m_buildLocation, buildRange ) )
+ {
+ // move into position
+ if ( !m_path.IsValid() || m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_buildLocation, cost );
+
+ m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) );
+ }
+
+ m_path.Update( me );
+
+ // don't give up until we've reached our build location
+ m_giveUpTimer.Reset();
+
+ return Continue();
+ }
+
+ // in position to build
+ if ( m_giveUpTimer.IsElapsed() )
+ {
+ return Done( "Taking too long - giving up" );
+ }
+
+ if ( m_hasPreciseBuildLocation )
+ {
+ me->GetBodyInterface()->AimHeadTowards( m_buildLocation, IBody::CRITICAL, 1.0f, NULL, "Looking toward my precise build location" );
+
+ // directly create a teleporter exit at the precise position and orientation desired
+ CObjectTeleporter *myTeleporterExit = (CObjectTeleporter *)CreateEntityByName( "obj_teleporter" );
+ if ( myTeleporterExit )
+ {
+ myTeleporterExit->SetObjectMode( MODE_TELEPORTER_EXIT );
+ myTeleporterExit->SetAbsOrigin( m_buildLocation );
+ myTeleporterExit->SetAbsAngles( QAngle( 0, m_buildAngle, 0 ) );
+ myTeleporterExit->Spawn();
+
+ myTeleporterExit->StartPlacement( me );
+ myTeleporterExit->StartBuilding( me );
+ myTeleporterExit->SetBuilder( me );
+
+ // teleporter exits are solid blockers - put engineer on top of exit or he'll be stuck
+ Vector myNewOrigin = me->GetAbsOrigin();
+ myNewOrigin.z += me->GetLocomotionInterface()->GetStepHeight();
+
+ me->SetAbsOrigin( myNewOrigin );
+
+ return Done( "Teleport exit built at precise location" );
+ }
+
+ return Continue();
+ }
+
+
+ // build exit roughly at this spot
+ CTFWeaponBase *myGun = me->GetActiveTFWeapon();
+ if ( myGun )
+ {
+ CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( myGun );
+ if ( builder )
+ {
+ if ( builder->IsValidPlacement() )
+ {
+ // place it down
+ me->PressFireButton();
+ }
+ else if ( m_searchTimer.IsElapsed() )
+ {
+ // rotate to find valid spot
+ Vector forward;
+ float angle = RandomFloat( -M_PI, M_PI );
+ FastSinCos( angle, &forward.y, &forward.x );
+
+ forward.z = 0.0f;
+
+ me->GetBodyInterface()->AimHeadTowards( me->EyePosition() - 100.0f * forward, IBody::CRITICAL, 1.0f, NULL, "Trying to place my teleport exit" );
+
+ m_searchTimer.Start( 1.0f );
+ }
+ }
+ else
+ {
+ // switch to teleporter builder
+ me->StartBuildingObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_giveUpTimer.Reset();
+ m_path.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+
+ return TryContinue();
+}
+
+
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h
new file mode 100644
index 0000000..d64f34a
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h
@@ -0,0 +1,36 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_build_teleport_exit.h
+// Engineer building a teleport exit
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H
+#define TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H
+
+class CTFBotEngineerBuildTeleportExit : public Action< CTFBot >
+{
+public:
+ CTFBotEngineerBuildTeleportExit( void );
+ CTFBotEngineerBuildTeleportExit( const Vector &buildLocation, float buildAngle );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ virtual const char *GetName( void ) const { return "EngineerBuildTeleportExit"; };
+
+private:
+ PathFollower m_path;
+
+ bool m_hasPreciseBuildLocation;
+ Vector m_buildLocation;
+ float m_buildAngle;
+
+ CountdownTimer m_giveUpTimer;
+ CountdownTimer m_repathTimer;
+ CountdownTimer m_getAmmoTimer;
+ CountdownTimer m_searchTimer;
+};
+
+#endif // TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp
new file mode 100644
index 0000000..a0f84c0
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp
@@ -0,0 +1,497 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_building.cpp
+// At building location, constructing buildings
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_builder.h"
+#include "team_train_watcher.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_building.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_dispenser.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+#include "NextBotUtil.h"
+
+
+ConVar tf_bot_engineer_retaliate_range( "tf_bot_engineer_retaliate_range", "750", FCVAR_CHEAT, "If attacker who destroyed sentry is closer than this, attack. Otherwise, retreat" );
+ConVar tf_bot_engineer_exit_near_sentry_range( "tf_bot_engineer_exit_near_sentry_range", "2500", FCVAR_CHEAT, "Maximum travel distance between a bot's Sentry gun and its Teleporter Exit" );
+ConVar tf_bot_engineer_max_sentry_travel_distance_to_point( "tf_bot_engineer_max_sentry_travel_distance_to_point", "2500", FCVAR_CHEAT, "Maximum travel distance between a bot's Sentry gun and the currently contested point" );
+
+extern ConVar tf_bot_path_lookahead_range;
+
+const int MaxPlacementAttempts = 5;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuilding::CTFBotEngineerBuilding( void )
+{
+ m_sentryBuildHint = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEngineerBuilding::CTFBotEngineerBuilding( CTFBotHintSentrygun *sentryBuildHint )
+{
+ m_sentryBuildHint = sentryBuildHint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuilding::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_sentryTriesLeft = MaxPlacementAttempts;
+
+ m_territoryRangeTimer.Invalidate();
+
+ m_hasBuiltSentry = false;
+ m_isSentryOutOfPosition = false;
+ m_nearbyMetalStatus = NEARBY_METAL_UNKNOWN;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Everything is built, upgrade/maintain it
+// TODO: Upgrade/maintain nearby friendly buildings, too.
+void CTFBotEngineerBuilding::UpgradeAndMaintainBuildings( CTFBot *me )
+{
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+ CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER );
+
+ if ( !mySentry )
+ {
+ return;
+ }
+
+ CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( wrench )
+ {
+ me->Weapon_Switch( wrench );
+ }
+
+ const float tooFarRange = 75.0f;
+
+ if ( !myDispenser )
+ {
+ // just work on our sentry
+ float rangeToSentry = me->GetDistanceBetween( mySentry );
+
+ if ( rangeToSentry < 1.2f * tooFarRange )
+ {
+ // crouch both for cover behind our buildings, but also to slow us down so we hit our move goal more accurately
+ me->PressCrouchButton();
+ }
+
+ if ( rangeToSentry > tooFarRange )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, mySentry->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+ }
+ else
+ {
+ // we are in position - work on our buildings
+ me->StopLookingAroundForEnemies();
+ me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Sentry" );
+ me->PressFireButton();
+ }
+
+ return;
+ }
+
+ // sit near both buildings
+ Vector betweenMyBuildings = ( mySentry->GetAbsOrigin() + myDispenser->GetAbsOrigin() ) / 2.0f;
+
+ // try to equalize distance between both
+ float rangeToSentry = me->GetDistanceBetween( mySentry );
+ float rangeToDispenser = me->GetDistanceBetween( myDispenser );
+
+ const float equalTolerance = 25.0f;
+
+ if ( rangeToSentry < 1.2f * tooFarRange && rangeToDispenser < 1.2f * tooFarRange )
+ {
+ // crouch both for cover behind our buildings, but also to slow us down so we hit our move goal more accurately
+ me->PressCrouchButton();
+ }
+
+ if ( fabs( rangeToDispenser - rangeToSentry ) > equalTolerance || rangeToSentry > tooFarRange || rangeToDispenser > tooFarRange )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, betweenMyBuildings, cost );
+ }
+
+ m_path.Update( me );
+ }
+
+ if ( rangeToSentry < tooFarRange || rangeToDispenser < tooFarRange )
+ {
+ // we are (nearly) in position - work on our buildings
+ m_searchTimer.Invalidate();
+
+ CBaseObject *workTarget = mySentry;
+
+ if ( mySentry->HasSapper() || mySentry->IsPlasmaDisabled() )
+ workTarget = mySentry;
+ else if ( myDispenser->HasSapper() || myDispenser->IsPlasmaDisabled() )
+ workTarget = myDispenser;
+ else if ( mySentry->GetTimeSinceLastInjury() < 1.0f || mySentry->GetHealth() < mySentry->GetMaxHealth() )
+ workTarget = mySentry;
+ else if ( mySentry->IsBuilding() )
+ workTarget = mySentry;
+ else if ( myDispenser->IsBuilding() )
+ workTarget = myDispenser;
+ else if ( mySentry->GetUpgradeLevel() < 3 )
+ workTarget = mySentry;
+ else if ( myDispenser->GetHealth() < myDispenser->GetMaxHealth() )
+ workTarget = myDispenser;
+ else if ( myDispenser->GetUpgradeLevel() < mySentry->GetUpgradeLevel() )
+ workTarget = myDispenser;
+
+ me->StopLookingAroundForEnemies();
+ me->GetBodyInterface()->AimHeadTowards( workTarget->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my buildings" );
+ me->PressFireButton();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotEngineerBuilding::IsMetalSourceNearby( CTFBot *me ) const
+{
+ CUtlVector< CNavArea * > nearbyVector;
+ CollectSurroundingAreas( &nearbyVector, me->GetLastKnownArea(), 2000.0f, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetStepHeight() );
+
+ for( int i=0; i<nearbyVector.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)nearbyVector[i];
+ if ( area->HasAttributeTF( TF_NAV_HAS_AMMO ) )
+ {
+ return true;
+ }
+
+ // this assumes all spawn rooms have resupply cabinets
+ if ( me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ return true;
+ }
+
+ if ( me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotEngineerBuilding::CheckIfSentryIsOutOfPosition( CTFBot *me ) const
+{
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+
+ if ( !mySentry )
+ {
+ return false;
+ }
+
+ // payload
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ CTeamTrainWatcher *trainWatcher;
+
+ if ( me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() );
+ }
+ else
+ {
+ trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
+ }
+
+ if ( trainWatcher )
+ {
+ float sentryDistanceAlongPath;
+ trainWatcher->ProjectPointOntoPath( mySentry->GetAbsOrigin(), NULL, &sentryDistanceAlongPath );
+
+ const float behindTrainTolerance = SENTRY_MAX_RANGE;
+ return ( trainWatcher->GetTrainDistanceAlongTrack() > sentryDistanceAlongPath + behindTrainTolerance );
+ }
+ }
+
+ // control points
+ mySentry->UpdateLastKnownArea();
+ CNavArea *sentryArea = mySentry->GetLastKnownArea();
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point )
+ {
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ if ( sentryArea && pointArea )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( NavAreaTravelDistance( sentryArea, pointArea, cost, tf_bot_engineer_max_sentry_travel_distance_to_point.GetFloat() ) < 0 &&
+ NavAreaTravelDistance( pointArea, sentryArea, cost, tf_bot_engineer_max_sentry_travel_distance_to_point.GetFloat() ) < 0 )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuilding::Update( CTFBot *me, float interval )
+{
+ CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN );
+ CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER );
+ CObjectTeleporter *myTeleportEntrance = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
+ CObjectTeleporter *myTeleportExit = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT );
+
+ bool isUnderAttack = ( me->GetTimeSinceLastInjury() < 1.0f );
+ isUnderAttack |= ( mySentry && ( mySentry->HasSapper() || mySentry->IsPlasmaDisabled() ) );
+ isUnderAttack |= ( myDispenser && ( myDispenser->HasSapper() || myDispenser->IsPlasmaDisabled() ) );
+
+ me->StartLookingAroundForEnemies();
+
+ // try to build a Sentry
+ if ( !mySentry )
+ {
+ m_nearbyMetalStatus = NEARBY_METAL_UNKNOWN;
+
+ // react to nearby threats if our sentry is down
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( !m_hasBuiltSentry && m_sentryTriesLeft > 0 )
+ {
+ --m_sentryTriesLeft;
+
+ if ( m_sentryBuildHint )
+ {
+ return SuspendFor( new CTFBotEngineerBuildSentryGun( m_sentryBuildHint ), "Building a Sentry at a hint location" );
+ }
+
+ return SuspendFor( new CTFBotEngineerBuildSentryGun, "Building a Sentry" );
+ }
+ else
+ {
+ // can't build a Sentry here - pick a new place
+ return ChangeTo( new CTFBotEngineerMoveToBuild, "Couldn't find a place to build" );
+ }
+ }
+
+ // I have a Sentry
+ m_hasBuiltSentry = true;
+
+ if ( m_sentryBuildHint != NULL && !m_sentryBuildHint->IsEnabled() )
+ {
+ // our hint has been disabled and no longer has influence on our behavior
+ m_sentryBuildHint = NULL;
+ }
+
+ // periodically check that our Sentry is still near the contested point
+ if ( m_sentryBuildHint == NULL || !m_sentryBuildHint->IsSticky() )
+ {
+ if ( !m_isSentryOutOfPosition && m_territoryRangeTimer.IsElapsed() )
+ {
+ m_territoryRangeTimer.Start( RandomFloat( 3.0f, 5.0f ) );
+
+ m_isSentryOutOfPosition = CheckIfSentryIsOutOfPosition( me );
+ }
+
+ m_isSentryOutOfPosition = false;
+
+ if ( m_isSentryOutOfPosition )
+ {
+ // the point has moved, only keep sentry as long as it keeps attacking
+ if ( mySentry->GetTimeSinceLastFired() > 10.0f )
+ {
+ mySentry->DetonateObject();
+
+ // if we built here because of a hint, disable that hint so we don't use it and rebuild here again
+ if ( m_sentryBuildHint != NULL )
+ {
+ inputdata_t dummy;
+ m_sentryBuildHint->InputDisable( dummy );
+
+ m_sentryBuildHint = NULL;
+ }
+
+ if ( myDispenser )
+ {
+ myDispenser->DetonateObject();
+ }
+
+ if ( myTeleportExit )
+ {
+ myTeleportExit->DetonateObject();
+ }
+
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_MOVEUP );
+
+ return ChangeTo( new CTFBotEngineerMoveToBuild, "Need to move my gear closer to the point!" );
+ }
+ }
+ }
+
+ // if my dispenser is too far away from my sentry, destroy and rebuild it next update
+ // @TODO: Flag hint-built entities for a larger range
+ const float maxSeparation = 500.0f;
+ if ( myDispenser )
+ {
+ if ( ( mySentry->GetAbsOrigin() - myDispenser->GetAbsOrigin() ).IsLengthGreaterThan( maxSeparation ) )
+ {
+ myDispenser->DestroyObject();
+ myDispenser = NULL;
+ }
+ }
+
+ // build up the sentry all the way if there is a metal source nearby
+ if ( mySentry->GetUpgradeLevel() < 3 )
+ {
+ if ( m_nearbyMetalStatus == NEARBY_METAL_UNKNOWN )
+ {
+ m_nearbyMetalStatus = IsMetalSourceNearby( me ) ? NEARBY_METAL_EXISTS : NEARBY_METAL_NONE;
+ }
+
+ if ( m_nearbyMetalStatus == NEARBY_METAL_EXISTS )
+ {
+ UpgradeAndMaintainBuildings( me );
+ return Continue();
+ }
+ }
+
+/*
+ if ( myTeleportExit )
+ {
+ // if my teleporter exit is too far away from my sentry, destroy and rebuild it next update
+ if ( ( mySentry->GetAbsOrigin() - myTeleportExit->GetAbsOrigin() ).IsLengthGreaterThan( maxSeparation ) )
+ {
+ myTeleportExit->DestroyObject();
+ myTeleportExit = NULL;
+ }
+ }
+*/
+
+ // try to build a Dispenser (build after tele exit in training)
+ if ( !TFGameRules()->IsInTraining() || myTeleportExit )
+ {
+ const float dispenserRebuildInterval = 10.0f;
+ if ( myDispenser )
+ {
+ // don't rebuild immediately after building is destroyed
+ m_dispenserRetryTimer.Start( dispenserRebuildInterval );
+ }
+ else if ( m_dispenserRetryTimer.IsElapsed() && !isUnderAttack )
+ {
+ m_dispenserRetryTimer.Start( dispenserRebuildInterval );
+
+ return SuspendFor( new CTFBotEngineerBuildDispenser, "Building a Dispenser" );
+ }
+ }
+
+ // try to build a Teleporter Exit
+ const float exitRebuildInterval = TFGameRules()->IsInTraining() ? 5.0f : 30.0f;
+ if ( myTeleportExit )
+ {
+ // don't rebuild immediately after building is destroyed
+ m_teleportExitRetryTimer.Start( exitRebuildInterval );
+ }
+ else if ( m_teleportExitRetryTimer.IsElapsed() && myTeleportEntrance && !isUnderAttack )
+ {
+ m_teleportExitRetryTimer.Start( exitRebuildInterval );
+
+ // we need to build a teleporter exit yet
+ if ( m_sentryBuildHint != NULL )
+ {
+ // if there are teleporter exit hints, find the closest one to our sentry and use it
+ CUtlVector< CBaseEntity * > hintVector;
+ CTFBotHintTeleporterExit *hint = NULL;
+ while( ( hint = (CTFBotHintTeleporterExit *)gEntList.FindEntityByClassname( hint, "bot_hint_teleporter_exit" ) ) != NULL )
+ {
+ if ( hint->IsEnabled() && hint->InSameTeam( me ) )
+ {
+ hintVector.AddToTail( hint );
+ }
+ }
+
+ if ( hintVector.Count() > 0 )
+ {
+ mySentry->UpdateLastKnownArea();
+ CBaseEntity *closeHint = SelectClosestEntityByTravelDistance( me, hintVector, mySentry->GetLastKnownArea(), tf_bot_engineer_exit_near_sentry_range.GetFloat() );
+
+ if ( closeHint )
+ {
+ return SuspendFor( new CTFBotEngineerBuildTeleportExit( closeHint->GetAbsOrigin(), closeHint->GetAbsAngles().y ), "Building teleporter exit at nearby hint" );
+ }
+ }
+ }
+ else if ( me->IsRangeLessThan( mySentry, 300.0f ) )
+ {
+ // drop a teleporter exit near our sentry
+ return SuspendFor( new CTFBotEngineerBuildTeleportExit(), "Building teleporter exit" );
+ }
+ }
+
+ // everything is built - maintain them
+ UpgradeAndMaintainBuildings( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotEngineerBuilding::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->StartLookingAroundForEnemies();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerBuilding::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerBuilding::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerBuilding::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h
new file mode 100644
index 0000000..e5928ce
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h
@@ -0,0 +1,64 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_building.h
+// At building location, constructing buildings
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_ENGINEER_BUILDING_H
+#define TF_BOT_ENGINEER_BUILDING_H
+
+class CTFBotHintSentrygun;
+
+
+class CTFBotEngineerBuilding : public Action< CTFBot >
+{
+public:
+ CTFBotEngineerBuilding( void );
+ CTFBotEngineerBuilding( CTFBotHintSentrygun *sentryBuildHint );
+
+ 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 > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+
+ virtual const char *GetName( void ) const { return "EngineerBuilding"; };
+
+private:
+ CountdownTimer m_searchTimer;
+ CountdownTimer m_getAmmoTimer;
+ CountdownTimer m_repathTimer;
+ CountdownTimer m_buildTeleporterExitTimer;
+
+ int m_sentryTriesLeft;
+
+ CountdownTimer m_dispenserRetryTimer;
+ CountdownTimer m_teleportExitRetryTimer;
+
+ PathFollower m_path;
+
+ CHandle< CTFBotHintSentrygun > m_sentryBuildHint;
+
+ bool m_hasBuiltSentry;
+
+ enum NearbyMetalType
+ {
+ NEARBY_METAL_UNKNOWN,
+ NEARBY_METAL_NONE,
+ NEARBY_METAL_EXISTS
+ };
+
+ NearbyMetalType m_nearbyMetalStatus;
+
+ CountdownTimer m_territoryRangeTimer;
+ bool m_isSentryOutOfPosition;
+ bool CheckIfSentryIsOutOfPosition( CTFBot *me ) const;
+
+ void UpgradeAndMaintainBuildings( CTFBot *me );
+ bool IsMetalSourceNearby( CTFBot *me ) const;
+};
+
+
+#endif // TF_BOT_ENGINEER_BUILDING_H
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp
new file mode 100644
index 0000000..4ef28d6
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp
@@ -0,0 +1,504 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_move_to_build.cpp
+// Engineer moving into position to build
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh/tf_nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_weapon_builder.h"
+#include "team_train_watcher.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
+#include "bot/behavior/engineer/tf_bot_engineer_building.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h"
+#include "trigger_area_capture.h"
+
+#include "raid/tf_raid_logic.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_debug_sentry_placement( "tf_bot_debug_sentry_placement", "0", FCVAR_CHEAT );
+ConVar tf_bot_max_teleport_exit_travel_to_point( "tf_bot_max_teleport_exit_travel_to_point", "2500", FCVAR_CHEAT, "In an offensive engineer bot's tele exit is farther from the point than this, destroy it" );
+ConVar tf_bot_min_teleport_travel( "tf_bot_min_teleport_travel", "3000", FCVAR_CHEAT, "Minimum travel distance between teleporter entrance and exit before engineer bot will build one" );
+
+//--------------------------------------------------------------------------------------------------------
+static Vector s_pointCentroid;
+
+int CompareRangeToPoint( CTFNavArea * const *area1, CTFNavArea * const *area2 )
+{
+ float d1 = ( (*area1)->GetCenter() - s_pointCentroid ).LengthSqr();
+ float d2 = ( (*area2)->GetCenter() - s_pointCentroid ).LengthSqr();
+
+ // reversed so farthest is sorted first in the vector
+ if ( d1 < d2 )
+ return 1;
+
+ if ( d1 > d2 )
+ return -1;
+
+ return 0;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotEngineerMoveToBuild::CollectBuildAreas( CTFBot *me )
+{
+ // if we have a predesignated build area, we're done
+ if ( me->GetHomeArea() )
+ return;
+
+ m_sentryAreaVector.RemoveAll();
+
+ CUtlVector< CTFNavArea * > pointAreaVector;
+ Vector pointCentroid = vec3_origin;
+ float pointEnemyIncursion = 0.0f;
+ int i;
+
+ int myTeam = me->GetTeamNumber();
+ int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE;
+
+ CCaptureZone *zone = me->GetFlagCaptureZone();
+ if ( zone )
+ {
+ // NOTE: Not strictly the right thing - should defend location of our team's flag
+ CTFNavArea *zoneArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( zone->WorldSpaceCenter(), false, 500.0f, true );
+ if ( zoneArea )
+ {
+ pointAreaVector.AddToTail( zoneArea );
+ pointCentroid += zoneArea->GetCenter();
+ pointEnemyIncursion += zoneArea->GetIncursionDistance( enemyTeam );
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ CTeamTrainWatcher *trainWatcher;
+
+ if ( myTeam == TF_TEAM_BLUE )
+ {
+ trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() );
+ }
+ else
+ {
+ trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
+ }
+
+ if ( trainWatcher )
+ {
+ Vector checkpointPos = trainWatcher->GetNextCheckpointPosition();
+
+ CTFNavArea *checkpointArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( checkpointPos, false, 500.0f, true );
+ if ( checkpointArea )
+ {
+ pointAreaVector.AddToTail( checkpointArea );
+ pointCentroid += checkpointArea->GetCenter();
+ pointEnemyIncursion += checkpointArea->GetIncursionDistance( enemyTeam );
+ }
+ }
+ }
+ else
+ {
+ // collect all areas overlapping the point
+ CTeamControlPoint *ctrlPoint = me->GetMyControlPoint();
+ if ( !ctrlPoint )
+ return;
+
+ const CUtlVector< CTFNavArea * > *ctrlPointAreaVector = TheTFNavMesh()->GetControlPointAreas( ctrlPoint->GetPointIndex() );
+
+ if ( ctrlPointAreaVector )
+ {
+ for( i=0; i<ctrlPointAreaVector->Count(); ++i )
+ {
+ CTFNavArea *area = ctrlPointAreaVector->Element(i);
+
+ pointAreaVector.AddToTail( area );
+ pointCentroid += area->GetCenter();
+ pointEnemyIncursion += area->GetIncursionDistance( enemyTeam );
+ }
+ }
+ }
+
+ if ( pointAreaVector.Count() == 0 )
+ return;
+
+ pointCentroid /= pointAreaVector.Count();
+ pointEnemyIncursion /= pointAreaVector.Count();
+
+
+ // collect all areas that can see the point
+ CUtlVector< CTFNavArea * > exposedAreaVector;
+ for( i=0; i<pointAreaVector.Count(); ++i )
+ {
+ CTFAreaCollector collect;
+ pointAreaVector[i]->ForAllPotentiallyVisibleAreas( collect );
+
+ for( int j=0; j<collect.m_vector.Count(); ++j )
+ {
+ CTFNavArea *visibleArea = collect.m_vector[j];
+
+
+ if ( visibleArea->GetIncursionDistance( myTeam ) < 0 || visibleArea->GetIncursionDistance( enemyTeam ) < 0 )
+ continue;
+
+ if ( TFGameRules()->IsInKothMode() )
+ {
+ // ignore areas the enemy can reach first
+ if ( visibleArea->GetIncursionDistance( myTeam ) >= visibleArea->GetIncursionDistance( enemyTeam ) )
+ continue;
+ }
+
+// incursion flow is badly behaved at cap #1, stage #2 in dustbowl
+// else
+// {
+// if ( pointEnemyIncursion > visibleArea->GetIncursionDistance( enemyTeam ) )
+// continue;
+// }
+
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ // don't build directly on the point
+ if ( visibleArea->HasAttributeTF( TF_NAV_CONTROL_POINT ) )
+ continue;
+
+ // ignore areas below the point
+ const float tooFarBelow = 150.0f;
+ if ( visibleArea->GetCenter().z < pointCentroid.z - tooFarBelow )
+ continue;
+
+ // ignore areas too far from the point for the sentry gun to reach
+ const float tolerance = 1.1f;
+ if ( ( visibleArea->GetCenter() - pointCentroid ).IsLengthGreaterThan( SENTRY_MAX_RANGE * tolerance ) )
+ continue;
+ }
+
+ // ignore areas that don't have clear line of FIRE (not sight)
+ const float sentryEyeHeight = 60.0f;
+ const float pointFlagHeight = 70.0f; // 100.0f;
+ if ( !me->IsLineOfFireClear( visibleArea->GetCenter() + Vector( 0, 0, sentryEyeHeight ), pointCentroid + Vector( 0, 0, pointFlagHeight ) ) )
+ continue;
+
+ if ( !exposedAreaVector.HasElement( visibleArea ) )
+ exposedAreaVector.AddToTail( visibleArea );
+ }
+ }
+
+ // keep the farthest away areas
+ const float keepRatio = 1.0f; // 0.5f;
+ s_pointCentroid = pointCentroid;
+ exposedAreaVector.Sort( CompareRangeToPoint );
+
+ for( i=0; i<exposedAreaVector.Count() * keepRatio; ++i )
+ {
+ CTFNavArea *usableArea = exposedAreaVector[i];
+
+ m_sentryAreaVector.AddToTail( usableArea );
+ }
+
+ // calculate total surface area
+ m_totalSurfaceArea = 0.0f;
+ FOR_EACH_VEC( m_sentryAreaVector, it )
+ {
+ CTFNavArea *area = m_sentryAreaVector[ it ];
+
+ m_totalSurfaceArea += area->GetSizeX() * area->GetSizeY();
+
+ if ( tf_bot_debug_sentry_placement.GetBool() )
+ {
+ TheNavMesh->AddToSelectedSet( area );
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Doesn't recompute the potential areas, just reselected from the list
+ */
+void CTFBotEngineerMoveToBuild::SelectBuildLocation( CTFBot *me )
+{
+ m_path.Invalidate();
+
+ m_sentryBuildHint = NULL;
+ m_sentryBuildLocation = vec3_origin;
+
+
+ // if we have a build spot, use it
+ if ( me->GetHomeArea() )
+ {
+ m_sentryBuildLocation = me->GetHomeArea()->GetCenter();
+ return;
+ }
+
+ // if we have a set of specific build locations, pick one of them
+ CUtlVector< CTFBotHintSentrygun * > sentryHintVector;
+
+ 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 ) )
+ {
+ sentryHintVector.AddToTail( sentryHint );
+ }
+ }
+
+ if ( sentryHintVector.Count() > 0 )
+ {
+ int which = RandomInt( 0, sentryHintVector.Count()-1 );
+
+ m_sentryBuildHint = sentryHintVector[ which ];
+ m_sentryBuildHint->SetPlayerOwner( me );
+ m_sentryBuildLocation = m_sentryBuildHint->GetAbsOrigin();
+
+ return;
+ }
+
+
+ // collect nav area candidates
+ CollectBuildAreas( me );
+
+ // choose based on surface area to avoid biasing finely subdivided areas of the mesh
+ float which = RandomFloat( 0.0f, m_totalSurfaceArea - 1.0f );
+ float soFar = 0.0f;
+ FOR_EACH_VEC( m_sentryAreaVector, sit )
+ {
+ CTFNavArea *area = m_sentryAreaVector[ sit ];
+
+ soFar += area->GetSizeX() * area->GetSizeY();
+
+ if ( which < soFar )
+ {
+ m_sentryBuildLocation = area->GetRandomPoint();
+ return;
+ }
+ }
+
+ if ( !HushAsserts() )
+ {
+ Assert( !"Failed to find a build location" );
+ }
+ m_sentryBuildLocation = me->GetAbsOrigin();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerMoveToBuild::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( me->GetHomeArea() && TFGameRules()->GetRaidLogic() )
+ {
+ // try to pick a new area
+ CTFNavArea *sentryArea = TFGameRules()->GetRaidLogic()->SelectRaidSentryArea();
+ if ( sentryArea )
+ {
+ me->SetHomeArea( sentryArea );
+ }
+ }
+ }
+#endif // TF_RAID_MODE
+
+ SelectBuildLocation( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEngineerMoveToBuild::Update( CTFBot *me, float interval )
+{
+ if ( me->WasPointJustLost() )
+ {
+ if ( m_fallBackTimer.HasStarted() )
+ {
+ if ( m_fallBackTimer.IsElapsed() )
+ {
+ SelectBuildLocation( me );
+ m_fallBackTimer.Invalidate();
+ }
+ else
+ {
+ // wait a moment while we decide where to build near fallback point
+ return Continue();
+ }
+ }
+ }
+
+ CBaseObject *mySentry = me->GetObjectOfType( OBJ_SENTRYGUN );
+ if ( mySentry )
+ {
+ // we already have a sentry from a previous life - continue what we were doing
+
+ // if we used a sentry hint last time, reuse it
+ CTFBotHintSentrygun *sentryHint;
+ for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) );
+ sentryHint;
+ sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) )
+ {
+ if ( sentryHint->GetPlayerOwner() == me )
+ {
+ return ChangeTo( new CTFBotEngineerBuilding( sentryHint ), "Going back to my existing sentry nest and reusing a sentry hint" );
+ }
+ }
+
+ return ChangeTo( new CTFBotEngineerBuilding, "Going back to my existing sentry nest" );
+ }
+
+ // offensive engineers need to place a forward teleporter
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP && !TFGameRules()->IsInKothMode() && me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ CObjectTeleporter *myTeleportExit = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT );
+ int myTeam = me->GetTeamNumber();
+
+ if ( myTeleportExit )
+ {
+ // if exit is too far from the point, destroy it and try again
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point )
+ {
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ myTeleportExit->UpdateLastKnownArea();
+ CTFNavArea *exitArea = (CTFNavArea *)myTeleportExit->GetLastKnownArea();
+
+ if ( pointArea && exitArea )
+ {
+ float travelToPoint = fabs( exitArea->GetIncursionDistance( myTeam ) - pointArea->GetIncursionDistance( myTeam ) );
+
+ if ( travelToPoint > tf_bot_max_teleport_exit_travel_to_point.GetFloat() )
+ {
+ // too far, destroy it
+ myTeleportExit->DestroyObject();
+ myTeleportExit = NULL;
+ }
+ }
+ }
+ }
+ else
+ {
+ CObjectTeleporter *myTeleportEntrance = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
+ CTFNavArea *myArea = me->GetLastKnownArea();
+
+ bool shouldBuildExit = true;
+
+ // if we have a teleporter entrance, don't place the exit too close to it
+ if ( myTeleportEntrance && myArea )
+ {
+ myTeleportEntrance->UpdateLastKnownArea();
+ CTFNavArea *enterArea = (CTFNavArea *)myTeleportEntrance->GetLastKnownArea();
+
+ if ( enterArea )
+ {
+ float travelBetween = fabs( enterArea->GetIncursionDistance( myTeam ) - myArea->GetIncursionDistance( myTeam ) );
+
+ if ( travelBetween < tf_bot_min_teleport_travel.GetFloat() )
+ {
+ shouldBuildExit = false;
+ }
+ }
+ }
+
+ if ( shouldBuildExit )
+ {
+ // no exit yet - need to place one
+ // when we see the enemy, retreat to cover and build the exit there
+ if ( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) )
+ {
+ if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) && ShouldRetreat( me ) != ANSWER_NO )
+ {
+ Action< CTFBot > *nextActionWhenInCover = new CTFBotEngineerBuildTeleportExit;
+ return SuspendFor( new CTFBotRetreatToCover( nextActionWhenInCover ), "Retreating to a safe place to build my teleporter exit" );
+ }
+ }
+ }
+ }
+ }
+
+ // move to build position
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_sentryBuildLocation, cost );
+ }
+
+ Vector forward;
+ me->EyeVectors( &forward );
+ forward.z = 0.0f;
+ forward.NormalizeInPlace();
+
+ Vector myBlueprintPosition = me->GetAbsOrigin() + 50.0f * forward;
+
+ const float closeToHome = 25.0f;
+ Vector toBuild = m_sentryBuildLocation - myBlueprintPosition;
+ Vector toMe = m_sentryBuildLocation - me->GetAbsOrigin();
+
+ if ( me->GetLocomotionInterface()->IsOnGround() )
+ {
+ // we need to wait until we're on the ground since the Build action assumes our position OnStart is where we are going to build
+ if ( toMe.AsVector2D().IsLengthLessThan( closeToHome ) || toBuild.AsVector2D().IsLengthLessThan( closeToHome ) )
+ {
+ if ( m_sentryBuildHint != NULL )
+ {
+ return ChangeTo( new CTFBotEngineerBuilding( m_sentryBuildHint ), "Reached my precise build location" );
+ }
+
+ return ChangeTo( new CTFBotEngineerBuilding, "Reached my build location" );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnStuck( CTFBot *me )
+{
+// SelectBuildLocation( me );
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ SelectBuildLocation( me );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ // we have to wait a moment until contested point changes to select a new build spot
+ m_fallBackTimer.Start( 0.2f );
+
+ return TryContinue();
+}
diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h
new file mode 100644
index 0000000..42f60c4
--- /dev/null
+++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h
@@ -0,0 +1,43 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_engineer_move_to_build.h
+// Engineer moving into position to build
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_ENGINEER_MOVE_TO_BUILD_H
+#define TF_BOT_ENGINEER_MOVE_TO_BUILD_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotHintSentrygun;
+
+
+class CTFBotEngineerMoveToBuild : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual const char *GetName( void ) const { return "EngineerMoveToBuild"; };
+
+private:
+ CHandle< CTFBotHintSentrygun > m_sentryBuildHint;
+ Vector m_sentryBuildLocation;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CUtlVector< CTFNavArea * > m_sentryAreaVector;
+ float m_totalSurfaceArea;
+ void CollectBuildAreas( CTFBot *me );
+
+ void SelectBuildLocation( CTFBot *me );
+ CountdownTimer m_fallBackTimer;
+};
+
+#endif // TF_BOT_ENGINEER_MOVE_TO_BUILD_H
diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp
new file mode 100644
index 0000000..74ac1d6
--- /dev/null
+++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp
@@ -0,0 +1,1108 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_heal.cpp
+// Heal a teammate
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "team.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_medigun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/medic/tf_bot_medic_retreat.h"
+#include "bot/behavior/tf_bot_use_teleporter.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h"
+#include "nav_mesh.h"
+#include "tier0/vprof.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_medic_stop_follow_range( "tf_bot_medic_stop_follow_range", "75", FCVAR_CHEAT ); // 100
+ConVar tf_bot_medic_start_follow_range( "tf_bot_medic_start_follow_range", "250", FCVAR_CHEAT ); // 300
+ConVar tf_bot_medic_max_heal_range( "tf_bot_medic_max_heal_range", "600", FCVAR_CHEAT );
+ConVar tf_bot_medic_debug( "tf_bot_medic_debug", "0", FCVAR_CHEAT );
+ConVar tf_bot_medic_max_call_response_range( "tf_bot_medic_max_call_response_range", "1000", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicHeal::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_chasePath.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_patient = NULL;
+ m_coverArea = NULL;
+ m_patientAnchorPos = vec3_origin;
+ m_isPatientRunningTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Choose a player as our "primary" patient. The guy we're going to tether ourselves to
+ * and keep alive as long as we can.
+ */
+class CSelectPrimaryPatient : public IVision::IForEachKnownEntity
+{
+public:
+ CSelectPrimaryPatient( CTFBot *me, CTFPlayer *currentPatient )
+ {
+ m_me = me;
+ m_medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
+
+ m_selected = currentPatient;
+ }
+
+ CTFPlayer *SelectPreferred( CTFPlayer *current, CTFPlayer *contender )
+ {
+ // in order of preference
+ static int preferredClass[] =
+ {
+ TF_CLASS_HEAVYWEAPONS,
+ TF_CLASS_SOLDIER,
+ TF_CLASS_PYRO,
+ TF_CLASS_DEMOMAN,
+
+// TF_CLASS_SCOUT,
+// TF_CLASS_ENGINEER,
+// TF_CLASS_SNIPER,
+// TF_CLASS_SPY,
+// TF_CLASS_MEDIC,
+
+ TF_CLASS_UNDEFINED
+ };
+
+ int i;
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // in training mode, stay on the human trainee
+ if ( !current || current->IsBot() )
+ return contender;
+
+ return current;
+ }
+
+ if ( !current )
+ {
+ return contender;
+ }
+ else if ( !contender )
+ {
+ return current;
+ }
+
+ // if we are in a squad, always heal the squad leader
+ if ( m_me->IsInASquad() && m_me->GetSquad()->GetLeader() )
+ {
+ if ( m_me->GetSquad()->GetLeader()->entindex() == current->entindex() )
+ {
+ return current;
+ }
+
+ if ( m_me->GetSquad()->GetLeader()->entindex() == contender->entindex() )
+ {
+ return contender;
+ }
+ }
+
+ // if current already has another medic (not a dispenser) on him, select contender
+ int numHealers = current->m_Shared.GetNumHealers();
+ for ( i=0; i<numHealers; ++i )
+ {
+ CBaseEntity *medic = current->m_Shared.GetHealerByIndex(i);
+
+ if ( medic && medic->IsPlayer() && !m_me->IsSelf( medic ) )
+ return contender;
+ }
+
+ // if contender already has another medic (not a dispenser) on him, ignore him
+ numHealers = contender->m_Shared.GetNumHealers();
+ for ( i=0; i<numHealers; ++i )
+ {
+ CBaseEntity *medic = contender->m_Shared.GetHealerByIndex(i);
+
+ if ( medic && medic->IsPlayer() && !m_me->IsSelf( medic ) )
+ return current;
+ }
+
+ // respond to calls for help
+ // NOTE: For now, only attend to HUMAN calls for help
+ CTFPlayer *currentCaller = NULL;
+ CTFPlayer *contenderCaller = NULL;
+ CTFBotPathCost cost( m_me, FASTEST_ROUTE );
+
+ if ( !current->IsBot() && current->IsCallingForMedic() && m_me->IsRangeLessThan( current, tf_bot_medic_max_call_response_range.GetFloat() ) )
+ {
+ // check actual travel range
+ if ( NavAreaTravelDistance( m_me->GetLastKnownArea(), current->GetLastKnownArea(), cost, 1.5f * tf_bot_medic_max_call_response_range.GetFloat() ) >= 0.0 )
+ {
+ currentCaller = current;
+ }
+ }
+
+ if ( !contender->IsBot() && contender->IsCallingForMedic() && m_me->IsRangeLessThan( contender, tf_bot_medic_max_call_response_range.GetFloat() ) )
+ {
+ // check actual travel range
+ if ( NavAreaTravelDistance( m_me->GetLastKnownArea(), contender->GetLastKnownArea(), cost, 1.5f * tf_bot_medic_max_call_response_range.GetFloat() ) >= 0.0 )
+ {
+ contenderCaller = contender;
+ }
+ }
+
+ if ( currentCaller )
+ {
+ if ( contenderCaller )
+ {
+ // both are calling for me, and in range - choose most recent caller
+ if ( currentCaller->GetTimeSinceCalledForMedic() < contender->GetTimeSinceCalledForMedic() )
+ {
+ return current;
+ }
+ else
+ {
+ return contender;
+ }
+ }
+ else
+ {
+ return current;
+ }
+ }
+ else if ( contenderCaller )
+ {
+ return contender;
+ }
+
+
+ int currentRank = 999, contenderRank = 999;
+ for( i=0; preferredClass[i] != TF_CLASS_UNDEFINED; ++i )
+ {
+ // for now, heavy, solider, and pyro are equivalent choices
+ if ( current->GetPlayerClass()->GetClassIndex() == preferredClass[i] )
+ currentRank = (i < 3) ? 0 : i;
+
+ if ( contender->GetPlayerClass()->GetClassIndex() == preferredClass[i] )
+ contenderRank = (i < 3) ? 0 : i;
+ }
+
+ if ( currentRank == contenderRank )
+ {
+ // unless contender is much closer, keep current guy
+ const float tolerance = 300.0f;
+ return ( m_me->GetDistanceBetween( current ) - m_me->GetDistanceBetween( contender ) > tolerance ) ? contender : current;
+ }
+
+ if ( currentRank > contenderRank )
+ {
+ // switch to contender unless he's far away
+ const float nearbyRange = 750.0f;
+ if ( m_me->GetDistanceBetween( contender ) < nearbyRange )
+ {
+ return contender;
+ }
+ }
+
+ return current;
+ }
+
+ bool Inspect( const CKnownEntity &known )
+ {
+ if ( !known.GetEntity() || !known.GetEntity()->IsPlayer() || !known.GetEntity()->IsAlive() || !m_me->IsFriend( known.GetEntity() ) )
+ return true;
+
+ CTFPlayer *player = dynamic_cast< CTFPlayer * >( known.GetEntity() );
+ if ( player == NULL )
+ return true;
+
+ if ( m_me->IsSelf( player ) )
+ return true;
+
+ // always heal the flag carrier, regardless of class
+ // squads always heal the leader
+ if ( !player->HasTheFlag() && !m_me->IsInASquad() )
+ {
+ if ( player->IsPlayerClass( TF_CLASS_MEDIC ) ||
+ player->IsPlayerClass( TF_CLASS_SNIPER ) ||
+ player->IsPlayerClass( TF_CLASS_ENGINEER ) ||
+ player->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ // these classes can't be our primary heal target (although they will get opportunistic healing
+ return true;
+ }
+ }
+
+ // select primary patient for long-term healing
+ m_selected = SelectPreferred( m_selected, player );
+
+ return true;
+ }
+
+ CTFBot *m_me;
+ CWeaponMedigun *m_medigun;
+ CTFPlayer *m_selected;
+};
+
+
+//---------------------------------------------------------------------------------------------
+CTFPlayer *CTFBotMedicHeal::SelectPatient( CTFBot *me, CTFPlayer *current )
+{
+ CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
+
+ if ( medigun )
+ {
+ if ( current == NULL || !current->IsAlive() )
+ {
+ current = ToTFPlayer( medigun->GetHealTarget() );
+ }
+
+ if ( medigun->IsReleasingCharge() )
+ {
+ // don't change targets when using uber
+ return current;
+ }
+
+ if ( IsReadyToDeployUber( medigun ) && current && IsGoodUberTarget( current ) )
+ {
+ // don't change targets if we're ready to uber and we have a good target
+ return current;
+ }
+ }
+
+ CSelectPrimaryPatient choose( me, current );
+
+ if ( TFGameRules()->IsPVEModeActive() )
+ {
+ // assume perfect knowledge
+ CUtlVector< CTFPlayer * > livePlayerVector;
+ CollectPlayers( &livePlayerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ for( int i=0; i<livePlayerVector.Count(); ++i )
+ {
+ CKnownEntity known( livePlayerVector[i] );
+ known.UpdatePosition();
+
+ choose.Inspect( known );
+ }
+ }
+ else
+ {
+ me->GetVisionInterface()->ForEachKnownEntity( choose );
+ }
+
+ return choose.m_selected;
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Return true if the given patient is healthy and safe for now
+ */
+bool CTFBotMedicHeal::IsStable( CTFPlayer *patient ) const
+{
+ const float safeTime = 3.0f;
+
+ // if they are in combat, they are not stable
+ if ( patient->GetTimeSinceLastInjury( GetEnemyTeam( patient->GetTeamNumber() ) ) < safeTime )
+ return false;
+
+ const float healthyRatio = 1.0f; // can be buffed higher
+ if ( ( (float)patient->GetHealth() / (float)patient->GetMaxHealth() ) < healthyRatio )
+ return false;
+
+ if ( patient->m_Shared.InCond( TF_COND_BURNING ) )
+ return false;
+
+ if ( patient->m_Shared.InCond( TF_COND_BLEEDING ) )
+ return false;
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CFindMostInjuredNeighbor : public IVision::IForEachKnownEntity
+{
+public:
+ CFindMostInjuredNeighbor( CTFBot *me, float maxRange, bool isInCombat )
+ {
+ m_me = me;
+ m_mostInjured = NULL;
+ m_injuredHealthRatio = 1.0f;
+ m_isOnFire = false;
+ m_maxRange = maxRange;
+ m_isInCombat = isInCombat;
+ }
+
+ bool Inspect( const CKnownEntity &known )
+ {
+ if ( known.GetEntity()->IsPlayer() )
+ {
+ CTFPlayer *player = ToTFPlayer( known.GetEntity() );
+
+ if ( m_me->IsRangeGreaterThan( player, m_maxRange ) )
+ return true;
+
+ if ( !m_me->IsLineOfFireClear( player->EyePosition() ) )
+ return true;
+
+ if ( !m_me->IsSelf( player ) && player->IsAlive() && player->InSameTeam( m_me ) )
+ {
+ // if we're not in combat, opportunistically overheal
+ float maxHealth = m_isInCombat ? player->GetMaxHealth() : player->m_Shared.GetMaxBuffedHealth();
+ float healthRatio = (float)player->GetHealth() / maxHealth;
+
+ if ( m_isOnFire )
+ {
+ // only others on fire who have less health can trump
+ if ( player->m_Shared.InCond( TF_COND_BURNING ) && healthRatio < m_injuredHealthRatio )
+ {
+ m_mostInjured = player;
+ m_injuredHealthRatio = healthRatio;
+ }
+ }
+ else
+ {
+ if ( player->m_Shared.InCond( TF_COND_BURNING ) )
+ {
+ // fire trumps
+ m_mostInjured = player;
+ m_injuredHealthRatio = healthRatio;
+ m_isOnFire = true;
+ }
+ else
+ {
+ if ( healthRatio < m_injuredHealthRatio )
+ {
+ m_mostInjured = player;
+ m_injuredHealthRatio = healthRatio;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ CTFBot *m_me;
+ CTFPlayer *m_mostInjured;
+ float m_injuredHealthRatio;
+ bool m_isOnFire;
+ float m_maxRange;
+ bool m_isInCombat;
+};
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotMedicHeal::CanDeployUber( CTFBot *me, const CWeaponMedigun* pMedigun ) const
+{
+#ifdef STAGING_ONLY
+ if ( TFGameRules()->IsMannVsMachineMode() &&
+ me && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) &&
+ pMedigun && ( pMedigun->GetMedigunShield() != NULL ) && pMedigun->HasPermanentShield() && ( ( pMedigun->GetMedigunType() == MEDIGUN_STANDARD ) || ( pMedigun->GetMedigunType() == MEDIGUN_UBER ) ) )
+ {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Return true if we our charge is full, and it is an appropriate time to release uber.
+// Don't use uber in setup.
+// We don't pay attention to our patient here, because we might need to pop uber to save ourselves.
+//
+bool CTFBotMedicHeal::IsReadyToDeployUber( const CWeaponMedigun* pMedigun ) const
+{
+ if( !pMedigun )
+ return false;
+
+ if ( pMedigun->GetChargeLevel() < pMedigun->GetMinChargeAmount() )
+ return false;
+
+ if ( TFGameRules()->InSetup() )
+ return false;
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotMedicHeal::IsGoodUberTarget( CTFPlayer *who ) const
+{
+ if ( who->IsPlayerClass( TF_CLASS_MEDIC ) ||
+ who->IsPlayerClass( TF_CLASS_SNIPER ) ||
+ who->IsPlayerClass( TF_CLASS_ENGINEER ) ||
+ who->IsPlayerClass( TF_CLASS_SCOUT ) ||
+ who->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return false;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicHeal::Update( CTFBot *me, float interval )
+{
+ // if we're in a squad, and the only other members are medics, disband the squad
+ if ( me->IsInASquad() )
+ {
+ CTFBotSquad *squad = me->GetSquad();
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && squad->IsLeader( me ) )
+ {
+ return ChangeTo( new CTFBotFetchFlag, "I'm now a squad leader! Going for the flag!" );
+ }
+
+ if ( !squad->ShouldPreserveSquad() )
+ {
+ CUtlVector< CTFBot * > memberVector;
+ squad->CollectMembers( &memberVector );
+
+ int i;
+ for( i=0; i<memberVector.Count(); ++i )
+ {
+ if ( !memberVector[i]->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ break;
+ }
+ }
+
+ if ( i == memberVector.Count() )
+ {
+ // squad is obsolete
+ for( i=0; i<memberVector.Count(); ++i )
+ {
+ memberVector[i]->LeaveSquad();
+ }
+ }
+ }
+ }
+ else
+ {
+ // not in a squad - for now, assume whatever mission I was on is over
+ me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ }
+
+ m_patient = SelectPatient( me, m_patient );
+
+ // prevent a group of medic healing each other in a loop. always heal the top guy in the chain
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && m_patient != NULL && m_patient->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ CUtlVector< CBaseEntity* > seenPatients;
+ seenPatients.AddToTail( m_patient );
+
+ while ( CBaseEntity* pTestPatient = m_patient->MedicGetHealTarget() )
+ {
+ if ( !pTestPatient->IsPlayer() || seenPatients.Find( pTestPatient ) != seenPatients.InvalidIndex() )
+ {
+ break;
+ }
+
+ seenPatients.AddToTail( pTestPatient );
+ m_patient = ToTFPlayer( pTestPatient );
+ }
+ }
+
+ if ( m_patient == NULL )
+ {
+ // no patients
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ // no-one is left to heal - get the flag!
+ return ChangeTo( new CTFBotFetchFlag, "Everyone is gone! Going for the flag" );
+ }
+
+ if ( TFGameRules()->IsPVEModeActive() )
+ {
+ // don't retreat, just wait
+ return Continue();
+ }
+
+ // no patients - retreat to spawn to find another one
+ return SuspendFor( new CTFBotMedicRetreat, "Retreating to find another patient to heal" );
+ }
+
+ const float anchorRadius = 200.0f;
+ if ( ( m_patient->GetAbsOrigin() - m_patientAnchorPos ).IsLengthGreaterThan( anchorRadius ) )
+ {
+ // our patient is on the move
+ m_patientAnchorPos = m_patient->GetAbsOrigin();
+ m_isPatientRunningTimer.Start( 3.0f );
+ }
+
+ // if our patient is teleporting away - follow them!
+ if ( m_patient->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ) )
+ {
+ // find closest teleporter entrance to patient's location
+ CObjectTeleporter *closeTeleporter = NULL;
+ float closeRangeSq = FLT_MAX;
+
+ CUtlVector< CBaseObject * > objVector;
+ TheTFNavMesh()->CollectBuiltObjects( &objVector, me->GetTeamNumber() );
+
+ for( int i=0; i<objVector.Count(); ++i )
+ {
+ if ( objVector[i]->GetType() == OBJ_TELEPORTER )
+ {
+ CObjectTeleporter *teleporter = (CObjectTeleporter *)objVector[i];
+
+ if ( teleporter->IsEntrance() && teleporter->IsReady() )
+ {
+ float rangeSq = ( teleporter->GetAbsOrigin() - m_patient->GetAbsOrigin() ).LengthSqr();
+
+ if ( rangeSq < closeRangeSq )
+ {
+ closeRangeSq = rangeSq;
+ closeTeleporter = teleporter;
+ }
+ }
+ }
+ }
+
+ if ( closeTeleporter )
+ {
+ return SuspendFor( new CTFBotUseTeleporter( closeTeleporter, CTFBotUseTeleporter::ALWAYS_USE ), "Following my patient through a teleporter" );
+ }
+ }
+
+
+ CTFPlayer *actualHealTarget = m_patient;
+ bool isHealTargetBlocked = true;
+ bool isActivelyHealing = false;
+ bool isUsingProjectileShield = false;
+ const CKnownEntity *knownThreat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
+ if ( medigun )
+ {
+ if( medigun->GetMedigunType() == MEDIGUN_RESIST )
+ {
+ // If I'm a Vaccinnator medic and am told to prefer a certain type of resist, then cycle to that resist
+ while( ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_BULLETS ) && medigun->GetResistType() != MEDIGUN_BULLET_RESIST )
+ || ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_BLAST ) && medigun->GetResistType() != MEDIGUN_BLAST_RESIST )
+ || ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_FIRE ) && medigun->GetResistType() != MEDIGUN_FIRE_RESIST ) )
+ {
+ medigun->CycleResistType();
+ }
+ }
+
+ // if our primary patient is healthy and safe, heal others in our immediate vicinity who need it
+ // No opportunistic healing in training - focus on the trainee
+ // No opportunistic healing if I'm in a squad - stay on the leader
+ if ( !medigun->IsReleasingCharge() && IsStable( m_patient ) && !TFGameRules()->IsInTraining() && !me->IsInASquad() )
+ {
+ bool isInCombat = actualHealTarget ? actualHealTarget->GetTimeSinceWeaponFired() < 1.0f : false;
+
+ CFindMostInjuredNeighbor neighbor( me, 0.9f * medigun->GetTargetRange(), isInCombat );
+ me->GetVisionInterface()->ForEachKnownEntity( neighbor );
+
+ float hurtRatio = isInCombat ? 0.5f : 1.0f;
+ if ( neighbor.m_mostInjured && neighbor.m_injuredHealthRatio < hurtRatio )
+ {
+ actualHealTarget = neighbor.m_mostInjured;
+ }
+ }
+
+ // juice 'em
+ me->GetBodyInterface()->AimHeadTowards( actualHealTarget, IBody::CRITICAL, 1.0f, NULL, "Aiming at my patient" );
+
+ if ( medigun->GetHealTarget() == NULL || medigun->GetHealTarget() == actualHealTarget )
+ {
+ // only hold fire button if we're healing who we think we're healing
+ me->PressFireButton();
+ isHealTargetBlocked = false;
+ isActivelyHealing = ( medigun->GetHealTarget() != NULL );
+ }
+ else
+ {
+ // we're not healing who we want to, but we don't want to spam the medigun on/off so much
+ if ( m_changePatientTimer.IsElapsed() )
+ {
+ // stop pressing fire for a moment to allow the medigun to select a new target
+ m_changePatientTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+ }
+ else
+ {
+ // keep building uber on wrong patient at least
+ me->PressFireButton();
+ }
+ }
+
+ // use uber if we've got it and we're under threat, or our patient was just hurt
+ bool useUber = false;
+ if ( IsReadyToDeployUber( medigun ) && CanDeployUber( me, medigun ) )
+ {
+ if( medigun->GetMedigunType() == MEDIGUN_RESIST )
+ {
+ // uber if I'm getting low and have recently taken damage
+ if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f )
+ {
+ useUber = true;
+ }
+
+ if( m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
+ {
+ useUber = true;
+ }
+ }
+ else
+ {
+ // use uber if our patient's health is getting low
+ const float healthyRatio = 0.5f;
+ useUber = ( ( (float)m_patient->GetHealth() / (float)m_patient->GetMaxHealth() ) < healthyRatio );
+
+ // don't uber our patient if he's already uber from some other source
+ if ( m_patient->m_Shared.InCond( TF_COND_INVULNERABLE ) || m_patient->m_Shared.InCond( TF_COND_MEGAHEAL ) )
+ {
+ useUber = false;
+ }
+
+ // uber if I'm getting low and have recently taken damage
+ if ( me->GetHealth() < me->GetUberHealthThreshold() )
+ {
+ if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || TFGameRules()->IsMannVsMachineMode() )
+ {
+ useUber = true;
+ }
+ }
+
+ // also uber if I'm about to die!
+ if ( me->GetHealth() < 25 )
+ {
+ useUber = true;
+ }
+
+ // special case for bots in mvm spawn zones
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( m_patient->m_Shared.InCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED ) &&
+ me->m_Shared.InCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED ) )
+ {
+ useUber = false;
+ }
+ }
+ }
+
+ if ( useUber )
+ {
+ if ( !m_delayUberTimer.HasStarted() )
+ {
+ m_delayUberTimer.Start( me->GetUberDeployDelayDuration() );
+ }
+
+ if ( m_delayUberTimer.IsElapsed() )
+ {
+ m_delayUberTimer.Invalidate();
+
+ // start the uber
+ me->PressAltFireButton();
+ }
+ }
+ }
+
+#ifdef STAGING_ONLY
+ // try to activate shield when I'm not using uber so I don't waste it
+ if ( TFGameRules()->IsMannVsMachineMode() && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) && medigun->GetMedigunShield() == NULL )
+ {
+ // activate shield ASAP for permanent shield medigun
+ if ( medigun->HasPermanentShield() )
+ {
+ me->PressSpecialFireButton();
+ isUsingProjectileShield = true;
+ }
+ else
+ {
+ isUsingProjectileShield = me->m_Shared.IsRageDraining();
+ // when the rage is ready to deploy and we're not using uber
+ if ( me->m_Shared.GetRageMeter() >= 100.f && !isUsingProjectileShield && !useUber )
+ {
+ // use shield if me or my patient is getting attacked
+ if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
+ {
+ me->PressSpecialFireButton();
+ isUsingProjectileShield = true;
+ }
+ }
+ }
+ }
+#else // remove this when we ship medic shield MVM update
+ // try to activate shield when I'm not using uber so I don't waste it
+ if ( TFGameRules()->IsMannVsMachineMode() && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) )
+ {
+ isUsingProjectileShield = me->m_Shared.IsRageDraining();
+ // when the rage is ready to deploy and we're not using uber
+ if ( me->m_Shared.GetRageMeter() >= 100.f && !isUsingProjectileShield && !useUber )
+ {
+ // use shield if me or my patient is getting attacked
+ if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
+ {
+ me->PressSpecialFireButton();
+ isUsingProjectileShield = true;
+ }
+ }
+ }
+#endif
+ }
+
+ bool isThreatened = false;
+ if ( knownThreat && knownThreat->IsVisibleRecently() && knownThreat->GetEntity() )
+ {
+ if ( actualHealTarget )
+ {
+ float patientRangeSq = me->GetRangeSquaredTo( actualHealTarget );
+ float threatRangeSq = me->GetRangeSquaredTo( knownThreat->GetEntity() );
+ isThreatened = threatRangeSq < patientRangeSq;
+ }
+ else
+ {
+ isThreatened = true;
+ }
+ }
+
+ bool outOfHealRange = me->IsRangeGreaterThan( actualHealTarget, 1.1f * tf_bot_medic_max_heal_range.GetFloat() );
+ bool isPatientObscured = actualHealTarget ? !me->IsLineOfFireClear( actualHealTarget->EyePosition() ) : true;
+
+ if ( !IsReadyToDeployUber( medigun ) && !me->m_Shared.InCond( TF_COND_INVULNERABLE ) && !isActivelyHealing && !isUsingProjectileShield && ( isThreatened || outOfHealRange || isPatientObscured ) )
+ {
+ // patient is too far to heal or obscured, equip combat weapon and defend ourselves while we move into position
+ me->EquipBestWeaponForThreat( knownThreat );
+
+ if ( knownThreat && knownThreat->GetEntity() )
+ {
+ me->GetBodyInterface()->AimHeadTowards( knownThreat->GetEntity(), IBody::IMPORTANT, 1.0f, NULL, "Aiming at an enemy" );
+ }
+ }
+ else
+ {
+ // equip the medigun and prepare to heal
+ CBaseCombatWeapon *gun = me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
+ if ( gun )
+ {
+ me->Weapon_Switch( gun );
+ }
+ }
+
+ // if we are ubering or are ready to uber (or lost our beam lock), stay close and locked on
+ if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) || IsReadyToDeployUber( medigun ) || isHealTargetBlocked )
+ {
+ // if we're not close or can't see our patient, move closer, otherwise we're good where we are
+ if ( me->IsRangeGreaterThan( m_patient, tf_bot_medic_stop_follow_range.GetFloat() ) || !me->IsAbleToSee( m_patient, CBaseCombatCharacter::DISREGARD_FOV ) )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_chasePath.Update( me, m_patient, cost );
+ }
+ }
+ else
+ {
+ // follow my patient (not my momentary heal target) and stay in cover
+ if ( m_coverTimer.IsElapsed() || IsVisibleToEnemy( me, me->EyePosition() ) )
+ {
+ m_coverTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ ComputeFollowPosition( me );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_coverPath.Compute( me, m_followGoal, cost );
+ }
+
+ m_coverPath.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicHeal::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_chasePath.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicHeal::OnStuck( CTFBot *me )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicHeal::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicHeal::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicHeal::OnActorEmoted( CTFBot *me, CBaseCombatCharacter *emoter, int emote )
+{
+ if ( !emoter->IsPlayer() )
+ return TryContinue();
+
+ CTFPlayer *emotingPlayer = ToTFPlayer( emoter );
+
+ switch( emote )
+ {
+ case MP_CONCEPT_PLAYER_MEDIC:
+ // emoter is calling to be healed by a Medic
+ // this is handled in SelectPatient()
+ break;
+
+ case MP_CONCEPT_PLAYER_GO:
+ case MP_CONCEPT_PLAYER_ACTIVATECHARGE:
+ // if our patient said this, and we have charge, deploy it!
+ if ( m_patient && emotingPlayer && m_patient->entindex() == emotingPlayer->entindex() )
+ {
+ CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
+ if ( IsReadyToDeployUber( medigun ) && CanDeployUber( me, medigun ) )
+ {
+ // start the uber
+ me->PressAltFireButton();
+ }
+ }
+ break;
+ }
+
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMedicHeal::ShouldHurry( const INextBot *me ) const
+{
+ // never abandon our patient
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMedicHeal::ShouldAttack( const INextBot *bot, const CKnownEntity *them ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ // only attack if we're not wielding the medigun
+ return me->IsCombatWeapon( MY_CURRENT_GUN ) ? ANSWER_YES : ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMedicHeal::ShouldRetreat( const INextBot *bot ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ // retreat if stunned
+ if ( me->m_Shared.IsControlStunned() || me->m_Shared.IsLoserStateStunned() )
+ return ANSWER_YES;
+
+ // never abandon our patient
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CKnownCollector: public IVision::IForEachKnownEntity
+{
+public:
+ virtual bool Inspect( const CKnownEntity &known )
+ {
+ m_vector.AddToTail( &known );
+ return true;
+ }
+
+ CUtlVector< const CKnownEntity * > m_vector;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ConVar tf_bot_medic_cover_test_resolution( "tf_bot_medic_cover_test_resolution", "8", FCVAR_CHEAT );
+
+void CTFBotMedicHeal::ComputeFollowPosition( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotMedicHeal::ComputeFollowPosition", "NextBot" );
+
+ m_followGoal = me->GetAbsOrigin();
+
+ if ( m_patient == NULL )
+ {
+ return;
+ }
+
+ bool isExposed;
+
+ if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ // robot medics in MvM don't care if the enemy sees them
+ isExposed = false;
+ }
+ else
+ {
+ isExposed = IsVisibleToEnemy( me, me->EyePosition() );
+ }
+
+ Vector patientForward;
+ m_patient->EyeVectors( &patientForward );
+ patientForward.z = 0.0f;
+ patientForward.NormalizeInPlace();
+
+ bool isNearPatient = me->IsRangeLessThan( m_patient, tf_bot_medic_start_follow_range.GetFloat() ) && me->IsAbleToSee( m_patient, CBaseCombatCharacter::DISREGARD_FOV );
+
+ if ( !isExposed )
+ {
+ // we're not currently visible to any enemies - try to stay that way
+ if ( isNearPatient )
+ {
+ // if we haven't been in combat for awhile, move behind our patient if we're in front of him
+ Vector toPatient = m_patient->GetAbsOrigin() - me->GetAbsOrigin();
+ if ( !TFGameRules()->InSetup() && m_patient->GetTimeSinceWeaponFired() > 5.0f && DotProduct( patientForward, toPatient ) < 0.0f )
+ {
+ m_followGoal = m_patient->GetAbsOrigin() - tf_bot_medic_stop_follow_range.GetFloat() * patientForward;
+ }
+ else
+ {
+ // we're good where we are
+ m_followGoal = me->GetAbsOrigin();
+ }
+ }
+ else
+ {
+ // get closer to our patient
+ m_followGoal = m_patient->GetAbsOrigin();
+ }
+
+ return;
+ }
+
+ // we are visible to one or more enemies - try to move to nearby cover while remaining close enough to heal
+ Vector closeSafety = me->GetAbsOrigin();
+ float closeSafetyRangeSq = FLT_MAX;
+
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors traceFilter( NULL, COLLISION_GROUP_NONE );
+
+ float angle;
+ float inc = M_PI / tf_bot_medic_cover_test_resolution.GetFloat();
+
+ float radius;
+ float radiusInc = 100.0f;
+ float maxRadius = tf_bot_medic_max_heal_range.GetFloat();
+ CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
+
+ if ( IsPatientRunning() || IsReadyToDeployUber( medigun ) )
+ {
+ // stay close if our patient is on the move, or we have an uber ready
+ maxRadius = tf_bot_medic_start_follow_range.GetFloat();
+ }
+
+ for( radius = tf_bot_medic_stop_follow_range.GetFloat() + RandomFloat( 0.0f, radiusInc );
+ radius <= maxRadius;
+ radius += radiusInc )
+ {
+ Vector offset = vec3_origin;
+
+ for( angle = 0.0f; angle <= 2.0f * M_PI; angle += inc )
+ {
+ SinCos( angle, &offset.y, &offset.x );
+ Vector pos = m_patient->WorldSpaceCenter() + radius * offset;
+
+ // find cover in this direction
+ UTIL_TraceLine( m_patient->WorldSpaceCenter(), pos, MASK_OPAQUE | CONTENTS_IGNORE_NODRAW_OPAQUE | CONTENTS_MONSTER, &traceFilter, &trace );
+
+ Vector actualPos = trace.endpos;
+ if ( trace.DidHit() )
+ {
+ // back up a bit if we hit something, so there is room for the medic to stand
+ actualPos -= 0.5f * me->GetBodyInterface()->GetHullWidth() * offset;
+ }
+
+ TheNavMesh->GetSimpleGroundHeight( actualPos, &actualPos.z );
+
+ // skip spots that are too low
+ if ( m_patient->GetAbsOrigin().z - actualPos.z > me->GetLocomotionInterface()->GetStepHeight() )
+ {
+ if ( tf_bot_medic_debug.GetBool() )
+ {
+ NDebugOverlay::Cross3D( actualPos, 5.0f, 255, 100, 0, true, 1.0f );
+ NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 255, 100, 0, true, 1.0f );
+ }
+
+ continue;
+ }
+
+ actualPos.z += HumanEyeHeight;
+
+ if ( IsVisibleToEnemy( me, actualPos ) )
+ {
+ // this spot is visible to a threat
+ if ( tf_bot_medic_debug.GetBool() )
+ {
+ //NDebugOverlay::Circle( actualPos, 5.0f, 255, 0, 0, 255, true, 1.0f );
+ NDebugOverlay::Cross3D( actualPos, 5.0f, 255, 0, 0, true, 1.0f );
+ NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 255, 0, 0, true, 1.0f );
+ }
+ }
+ else
+ {
+ // no threat can see this spot
+ // keep the closest safe position to our current position to minimize exposure
+ float rangeSq = ( me->EyePosition() - actualPos ).LengthSqr();
+ if ( rangeSq < closeSafetyRangeSq )
+ {
+ closeSafetyRangeSq = rangeSq;
+ closeSafety = actualPos;
+ }
+
+ if ( tf_bot_medic_debug.GetBool() )
+ {
+ //NDebugOverlay::Circle( actualPos, 5.0f, 0, 255, 0, 255, true, 1.0f );
+ NDebugOverlay::Cross3D( actualPos, 5.0f, 0, 255, 0, true, 1.0f );
+ NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 0, 255, 0, true, 1.0f );
+ }
+ }
+ }
+ }
+
+ m_followGoal = closeSafety;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotMedicHeal::IsVisibleToEnemy( CTFBot *me, const Vector &where ) const
+{
+ CKnownCollector known;
+ me->GetVisionInterface()->ForEachKnownEntity( known );
+
+ trace_t trace;
+
+ for( int i=0; i<known.m_vector.Count(); ++i )
+ {
+ CBaseCombatCharacter *threat = known.m_vector[i]->GetEntity()->MyCombatCharacterPointer();
+
+ if ( threat && me->IsEnemy( threat ) )
+ {
+ if ( threat->IsLineOfSightClear( where, CBaseCombatCharacter::IGNORE_ACTORS ) )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h
new file mode 100644
index 0000000..1ba30e7
--- /dev/null
+++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h
@@ -0,0 +1,69 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_medic_heal.h
+// Heal a teammate
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_MEDIC_HEAL_H
+#define TF_BOT_MEDIC_HEAL_H
+
+#include "Path/NextBotChasePath.h"
+
+class CWeaponMedigun;
+
+class CTFBotMedicHeal : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+ virtual EventDesiredResult< CTFBot > OnActorEmoted( CTFBot *me, CBaseCombatCharacter *emoter, int emote );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+ virtual QueryResultType ShouldRetreat( const INextBot *bot ) const;
+
+ virtual const char *GetName( void ) const { return "Heal"; };
+
+private:
+ ChasePath m_chasePath;
+
+ CTFPlayer *SelectPatient( CTFBot *me, CTFPlayer *current );
+ CountdownTimer m_changePatientTimer;
+
+ CountdownTimer m_delayUberTimer;
+
+ CHandle< CTFPlayer > m_patient;
+ Vector m_patientAnchorPos; // a spot where the patient was, to track if they are moving
+ CountdownTimer m_isPatientRunningTimer;
+ bool IsPatientRunning( void ) const;
+
+ bool IsStable( CTFPlayer *patient ) const; // return true if the given patient is healthy and safe for now
+
+ CTFNavArea *FindCoverArea( CTFBot *me );
+ CTFNavArea *m_coverArea;
+ CountdownTimer m_coverTimer;
+ PathFollower m_coverPath;
+
+ void ComputeFollowPosition( CTFBot *me );
+ Vector m_followGoal;
+
+ bool IsVisibleToEnemy( CTFBot *me, const Vector &where ) const;
+
+ bool IsReadyToDeployUber( const CWeaponMedigun* pMedigun ) const;
+
+ bool IsGoodUberTarget( CTFPlayer *who ) const;
+
+ bool CanDeployUber( CTFBot *me, const CWeaponMedigun* pMedigun ) const;
+};
+
+inline bool CTFBotMedicHeal::IsPatientRunning( void ) const
+{
+ return m_isPatientRunningTimer.IsElapsed() ? false : true;
+}
+
+
+#endif // TF_BOT_MEDIC_HEAL_H
diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp
new file mode 100644
index 0000000..d3d8ed6
--- /dev/null
+++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp
@@ -0,0 +1,144 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_retreat.cpp
+// Retreat towards our spawn to find another patient
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_weapon_medigun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/medic/tf_bot_medic_retreat.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_medic_follow_range;
+extern ConVar tf_bot_force_class;
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicRetreat::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ CTFNavArea *homeArea = me->GetSpawnArea();
+
+ if ( homeArea == NULL )
+ {
+ return Done( "No home area!" );
+ }
+
+ m_path.SetMinLookAheadDistance( tf_bot_path_lookahead_range.GetFloat() );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, homeArea->GetCenter(), cost );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CUsefulHealTargetFilter : public INextBotEntityFilter
+{
+public:
+ CUsefulHealTargetFilter( int team )
+ {
+ m_team = team;
+ }
+
+ virtual bool IsAllowed( CBaseEntity *entity ) const
+ {
+ if ( entity && entity->IsPlayer() && entity->GetTeamNumber() == m_team )
+ {
+ return !ToTFPlayer( entity )->IsPlayerClass( TF_CLASS_MEDIC ) && !ToTFPlayer( entity )->IsPlayerClass( TF_CLASS_SNIPER );
+ }
+ return false;
+ }
+
+ int m_team;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicRetreat::Update( CTFBot *me, float interval )
+{
+ // equip the syringegun and defend ourselves!
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ if ( myWeapon->GetWeaponID() != TF_WEAPON_SYRINGEGUN_MEDIC )
+ {
+ CBaseCombatWeapon *syringeGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+
+ if ( syringeGun )
+ {
+ me->Weapon_Switch( syringeGun );
+ }
+ }
+ }
+
+ m_path.Update( me );
+
+ // look around to try to spot a friend to heal
+ if ( m_lookAroundTimer.IsElapsed() )
+ {
+ m_lookAroundTimer.Start( RandomFloat( 0.33f, 1.0f ) );
+
+ QAngle angle;
+ angle.x = 0.0f;
+ angle.y = RandomFloat( -180.0f, 180.0f );
+ angle.z = 0.0f;
+
+ Vector forward;
+ AngleVectors( angle, &forward );
+
+ me->GetBodyInterface()->AimHeadTowards( me->EyePosition() + forward, IBody::IMPORTANT, 0.1f, NULL, "Looking for someone to heal" );
+ }
+
+ // if we see a friend, heal them
+ CUsefulHealTargetFilter filter( me->GetTeamNumber() );
+ const CKnownEntity *known = me->GetVisionInterface()->GetClosestKnown( filter );
+ if ( known )
+ {
+ return Done( "I know of a teammate" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMedicRetreat::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicRetreat::OnStuck( CTFBot *me )
+{
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMedicRetreat::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMedicRetreat::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ // defend ourselves!
+ return ANSWER_YES;
+}
diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h
new file mode 100644
index 0000000..8ec46a0
--- /dev/null
+++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h
@@ -0,0 +1,29 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_retreat.cpp
+// Retreat towards our spawn to find another patient
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_MEDIC_RETREAT_H
+#define TF_BOT_MEDIC_RETREAT_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotMedicRetreat : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const;
+
+ virtual const char *GetName( void ) const { return "Retreat"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_lookAroundTimer;
+};
+
+#endif // TF_BOT_MEDIC_RETREAT_H
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp
new file mode 100644
index 0000000..8f5c8c4
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp
@@ -0,0 +1,104 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_destroy_sentries.cpp
+// Seek and destroy enemy sentries and ignore everything else
+// Michael Booth, June 2011
+
+#include "cbase.h"
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/missions/tf_bot_mission_destroy_sentries.h"
+#include "bot/behavior/spy/tf_bot_spy_sap.h"
+#include "bot/behavior/tf_bot_destroy_enemy_sentry.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h"
+#include "tf_obj_sentrygun.h"
+
+//
+// NOTE: This behavior is deprecated and unused for now.
+// The only sentry destroying mission is the Sentry Buster right now (suicide bomber).
+//
+
+//---------------------------------------------------------------------------------------------
+CTFBotMissionDestroySentries::CTFBotMissionDestroySentries( CObjectSentrygun *goalSentry )
+{
+ m_goalSentry = goalSentry;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CObjectSentrygun *CTFBotMissionDestroySentries::SelectSentryTarget( CTFBot *me )
+{
+
+ return NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionDestroySentries::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return ChangeTo( new CTFBotMedicHeal, "My job is to heal/uber the others in the mission" );
+ }
+
+ // focus only on the mission
+ me->SetAttribute( CTFBot::IGNORE_ENEMIES );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionDestroySentries::Update( CTFBot *me, float interval )
+{
+ if ( m_goalSentry == NULL )
+ {
+ // first destroy the sentry we were assigned to, or any sentry we discovered or that is attacking us
+ m_goalSentry = me->GetEnemySentry();
+
+ if ( m_goalSentry == NULL )
+ {
+ // next destroy the most dangerous sentry
+ int iTeam = ( me->GetTeamNumber() == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED;
+
+ if ( TFGameRules() && TFGameRules()->IsPVEModeActive() )
+ {
+ iTeam = TF_TEAM_PVE_DEFENDERS;
+ }
+
+ m_goalSentry = TFGameRules()->FindSentryGunWithMostKills( iTeam );
+ }
+ }
+
+ // for suicide bombers, we never want them to revert to normal behavior even if there is no sentry to kill
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ return SuspendFor( new CTFBotMissionSuicideBomber, "On a suicide mission to blow up a sentry" );
+ }
+
+ if ( m_goalSentry == NULL )
+ {
+ // no sentries left to destroy - our mission is complete
+ me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ return ChangeTo( GetParentAction()->InitialContainedAction( me ), "Mission complete - reverting to normal behavior" );
+ }
+
+ if ( m_goalSentry != me->GetEnemySentry() )
+ {
+ me->RememberEnemySentry( m_goalSentry, m_goalSentry->WorldSpaceCenter() );
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return SuspendFor( new CTFBotSpySap( m_goalSentry ), "On a mission to sap a sentry" );
+ }
+
+ return SuspendFor( new CTFBotDestroyEnemySentry, "On a mission to destroy a sentry" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionDestroySentries::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->ClearAttribute( CTFBot::IGNORE_ENEMIES );
+}
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h
new file mode 100644
index 0000000..ad07721
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h
@@ -0,0 +1,30 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_destroy_sentries.h
+// Seek and destroy enemy sentries and ignore everything else
+// Michael Booth, June 2011
+
+#ifndef TF_BOT_MISSION_DESTROY_SENTRIES_H
+#define TF_BOT_MISSION_DESTROY_SENTRIES_H
+
+
+//-----------------------------------------------------------------------------
+class CTFBotMissionDestroySentries : public Action< CTFBot >
+{
+public:
+ CTFBotMissionDestroySentries( CObjectSentrygun *goalSentry = NULL );
+ virtual ~CTFBotMissionDestroySentries() { }
+
+ 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 const char *GetName( void ) const { return "MissionDestroySentries"; };
+
+private:
+ CHandle< CObjectSentrygun > m_goalSentry;
+
+ CObjectSentrygun *SelectSentryTarget( CTFBot *me );
+};
+
+
+#endif // TF_BOT_MISSION_DESTROY_SENTRIES_H
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp
new file mode 100644
index 0000000..07fe474
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp
@@ -0,0 +1,377 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_reprogrammed.cpp
+// Move to target and explode
+// Michael Booth, October 2011
+
+#include "cbase.h"
+#include "tf_team.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/missions/tf_bot_mission_reprogrammed.h"
+#include "particle_parse.h"
+#include "tf_obj_sentrygun.h"
+
+#ifdef STAGING_ONLY
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_suicide_bomb_range;
+
+ConVar tf_bot_reprogrammed_time( "tf_bot_reprogrammed_time", "8", FCVAR_CHEAT );
+
+//---------------------------------------------------------------------------------------------
+CTFBotMissionReprogrammed::CTFBotMissionReprogrammed( void )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionReprogrammed::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_detonateTimer.Invalidate();
+ m_detonateSeekTimer.Invalidate();
+ m_reprogrammedTimer.Invalidate();
+ m_hasDetonated = false;
+ m_consecutivePathFailures = 0;
+ m_wasSuccessful = false;
+
+ if ( me->HasTheFlag() )
+ me->DropFlag();
+ me->SetFlagTarget( NULL );
+
+ m_victim = me->GetMissionTarget();
+
+ // Find nearest, accessible teammate
+ if ( !m_victim )
+ {
+ CTFPlayer *pTarget = FindNearestEnemy( me );
+ if ( pTarget )
+ {
+ me->SetMissionTarget( pTarget );
+ m_victim = pTarget;
+ }
+ }
+
+ if ( m_victim )
+ {
+ m_lastKnownVictimPosition = m_victim->GetAbsOrigin();
+ }
+
+ // We're guaranteed to stay alive for a period of time
+ me->SetHealth( 1 );
+ me->m_takedamage = DAMAGE_NO;
+
+ // Duration
+ m_reprogrammedTimer.Start( tf_bot_reprogrammed_time.GetFloat() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionReprogrammed::Update( CTFBot *me, float interval )
+{
+ bool bDetonate = false;
+
+ // one we start detonating, there's no turning back
+ if ( m_detonateTimer.HasStarted() )
+ {
+ if ( m_detonateTimer.IsElapsed() )
+ {
+ m_vecDetLocation = me->GetAbsOrigin();
+ Detonate( me );
+
+ return Done( "KABOOM!" );
+ }
+
+ return Continue();
+ }
+
+ // Find nearest, accessible teammate
+ if ( !m_victim || !m_victim->IsAlive() )
+ {
+ m_victim.Set( NULL );
+
+ CTFPlayer *pTarget = FindNearestEnemy( me );
+ if ( pTarget )
+ {
+ me->SetMissionTarget( pTarget );
+ m_victim = pTarget;
+ }
+ }
+
+ if ( m_victim )
+ {
+ me->m_Shared.RemoveCond( TF_COND_STUNNED );
+
+ // update chase destination
+ if ( m_victim->IsAlive() && !m_victim->IsEffectActive( EF_NODRAW ) )
+ {
+ m_lastKnownVictimPosition = m_victim->GetAbsOrigin();
+ }
+ }
+
+ if ( m_talkTimer.IsElapsed() )
+ {
+ m_talkTimer.Start( 4.0f );
+ me->EmitSound( "MVM.SentryBusterIntro" );
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_lastKnownVictimPosition, cost );
+
+ if ( m_path.Compute( me, m_lastKnownVictimPosition, cost ) == false )
+ {
+ ++m_consecutivePathFailures;
+
+ if ( m_consecutivePathFailures >= 3 )
+ {
+ bDetonate = true;
+ }
+ }
+ }
+
+ // move to the victim
+ m_path.Update( me );
+
+ // Reprogramming time is up. Find nearest and detonate.
+ if ( m_reprogrammedTimer.IsElapsed() )
+ {
+ // Limit this mode to 5 seconds
+ if ( !m_detonateSeekTimer.HasStarted() )
+ {
+ m_detonateSeekTimer.Start( 5.f );
+ }
+ else if ( m_detonateSeekTimer.IsElapsed() )
+ {
+ bDetonate = true;
+ }
+
+ // Get to a third of the damage range before detonating
+ const float detonateRange = tf_bot_suicide_bomb_range.GetFloat() / 3.0f;
+ if ( me->IsDistanceBetweenLessThan( m_lastKnownVictimPosition, detonateRange ) )
+ {
+ if ( me->IsLineOfFireClear( m_lastKnownVictimPosition + Vector( 0, 0, StepHeight ) ) )
+ {
+ bDetonate = true;
+ }
+ }
+ }
+
+ if ( bDetonate )
+ {
+ StartDetonate( me, true );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionReprogrammed::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( me->IsAlive() )
+ {
+ me->ForceChangeTeam( TEAM_SPECTATOR );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMissionReprogrammed::OnKilled( CTFBot *me, const CTakeDamageInfo &info )
+{
+ // Keep us alive, but run to nearest enemy
+ if ( !m_hasDetonated )
+ {
+ if ( !m_detonateTimer.HasStarted() )
+ {
+ StartDetonate( me );
+ }
+ else
+ {
+ Detonate( me );
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMissionReprogrammed::OnStuck( CTFBot *me )
+{
+ // we're stuck, decide to detonate now!
+ if ( !m_hasDetonated && !m_detonateTimer.HasStarted() )
+ {
+ StartDetonate( me );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionReprogrammed::StartDetonate( CTFBot *me, bool wasSuccessful /* = false */ )
+{
+ if ( m_detonateTimer.HasStarted() )
+ return;
+
+ if ( !me->IsAlive() || me->GetHealth() < 1 )
+ {
+ if ( me->GetTeamNumber() != TEAM_SPECTATOR)
+ {
+ me->m_lifeState = LIFE_ALIVE;
+ me->SetHealth( 1 );
+ }
+ }
+
+ m_wasSuccessful = wasSuccessful;
+
+ me->Taunt( TAUNT_BASE_WEAPON );
+ m_detonateTimer.Start( 2.f );
+ me->EmitSound( "MvM.SentryBusterSpin" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionReprogrammed::Detonate( CTFBot *me )
+{
+ // BLAST!
+ m_hasDetonated = true;
+
+ DispatchParticleEffect( "explosionTrail_seeds_mvm", me->GetAbsOrigin(), me->GetAbsAngles() );
+ DispatchParticleEffect( "fluidSmokeExpl_ring_mvm", me->GetAbsOrigin(), me->GetAbsAngles() );
+
+ me->EmitSound( "MVM.SentryBusterExplode" );
+
+ UTIL_ScreenShake( me->GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START );
+
+ if ( !m_wasSuccessful )
+ {
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER_DOWN, TF_TEAM_PVE_DEFENDERS );
+ }
+ }
+
+ CUtlVector< CTFPlayer* > playerVector;
+ CUtlVector< CBaseCombatCharacter* > victimVector;
+
+ // Only damage our original team (reprogramming switches team)
+ CTFTeam *pTeam = me->GetOpposingTFTeam();
+ if ( pTeam )
+ {
+ CollectPlayers( &playerVector, pTeam->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ // objects
+ for ( int i = 0; i < pTeam->GetNumObjects(); ++i )
+ {
+ CBaseObject *object = pTeam->GetObject( i );
+ if ( object )
+ {
+ victimVector.AddToTail( object );
+ }
+ }
+ }
+
+ // players
+ for ( int i = 0; i < playerVector.Count(); ++i )
+ {
+ victimVector.AddToTail( playerVector[i] );
+ }
+
+ // non-player bots
+ CUtlVector< INextBot * > botVector;
+ TheNextBots().CollectAllBots( &botVector );
+ for( int i = 0; i < botVector.Count(); ++i )
+ {
+ CBaseCombatCharacter *bot = botVector[i]->GetEntity();
+
+ if ( !bot->IsPlayer() && bot->IsAlive() )
+ {
+ victimVector.AddToTail( bot );
+ }
+ }
+
+ // Clear my mission before we have everyone take damage so I will die with the rest
+ me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ me->m_takedamage = DAMAGE_YES;
+
+ // kill victims (including me)
+ for( int i = 0; i < victimVector.Count(); ++i )
+ {
+ CBaseCombatCharacter *victim = victimVector[i];
+
+ Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter();
+
+ if ( toVictim.IsLengthGreaterThan( tf_bot_suicide_bomb_range.GetFloat() ) )
+ continue;
+
+ if ( victim->IsPlayer() )
+ {
+ color32 colorHit = { 255, 255, 255, 255 };
+ UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN );
+ }
+
+ if ( me->IsLineOfFireClear( victim ) )
+ {
+ toVictim.NormalizeInPlace();
+
+ const int nDamage = 1000;
+ CTakeDamageInfo info( me, me, nDamage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
+
+ CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
+ victim->TakeDamage( info );
+ }
+ }
+
+ // make sure we're removed (in case we detonated in our spawn area where we are invulnerable)
+ me->ForceChangeTeam( TEAM_SPECTATOR );
+}
+
+//---------------------------------------------------------------------------------------------
+CTFPlayer *CTFBotMissionReprogrammed::FindNearestEnemy( CTFBot *me )
+{
+ CUtlVector< CTFPlayer* > playerVector;
+
+ CollectPlayers( &playerVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ CTFPlayer *pClosestPlayer = NULL;
+ float flClosestPlayerDist = FLT_MAX;
+
+ FOR_EACH_VEC( playerVector, i )
+ {
+ if ( !playerVector[i] )
+ continue;
+
+ if ( playerVector[i] == me )
+ continue;
+
+ me->GetDistanceBetween( playerVector[i] );
+
+ // Find closest
+ float flDist = me->GetDistanceBetween( playerVector[i] );
+ if ( flDist < flClosestPlayerDist )
+ {
+ pClosestPlayer = playerVector[i];
+ flClosestPlayerDist = flDist;
+ }
+ }
+
+ return pClosestPlayer;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMissionReprogrammed::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ( m_detonateTimer.HasStarted() ) ? ANSWER_NO : ANSWER_YES;
+}
+
+#endif // STAGING_ONLY
+
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h
new file mode 100644
index 0000000..7c63c7e
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h
@@ -0,0 +1,53 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_suicide_bomber.h
+// Move to target and explode
+// Michael Booth, October 2011
+
+#ifndef TF_BOT_MISSION_REPROGRAMMED_H
+#define TF_BOT_MISSION_REPROGRAMMED_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotMissionReprogrammed : public Action< CTFBot >
+{
+#ifdef STAGING_ONLY
+public:
+ CTFBotMissionReprogrammed( void );
+
+ 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 EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "MissionReprogrammed"; };
+
+private:
+ CHandle< CBaseEntity > m_victim; // the victim we are trying to destroy
+ Vector m_lastKnownVictimPosition;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CountdownTimer m_talkTimer;
+ CountdownTimer m_detonateTimer;
+ CountdownTimer m_detonateSeekTimer;
+ CountdownTimer m_reprogrammedTimer;
+
+ void StartDetonate( CTFBot *me, bool wasSuccessful = false );
+ void Detonate( CTFBot *me );
+ CTFPlayer *FindNearestEnemy( CTFBot *me );
+ bool m_hasDetonated;
+ bool m_wasSuccessful;
+
+ int m_consecutivePathFailures;
+
+ Vector m_vecDetLocation;
+#endif // STAGING_ONLY
+};
+
+
+#endif // TF_BOT_MISSION_REPROGRAMMED_H
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp
new file mode 100644
index 0000000..aac9d6c
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp
@@ -0,0 +1,400 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_suicide_bomber.cpp
+// Move to target and explode
+// Michael Booth, October 2011
+
+#include "cbase.h"
+#include "tf_team.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h"
+#include "particle_parse.h"
+#include "tf_obj_sentrygun.h"
+#include "player_vs_environment/tf_populators.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_suicide_bomb_range( "tf_bot_suicide_bomb_range", "300", FCVAR_CHEAT );
+ConVar tf_bot_suicide_bomb_friendly_fire( "tf_bot_suicide_bomb_friendly_fire", "1", FCVAR_CHEAT );
+
+//---------------------------------------------------------------------------------------------
+CTFBotMissionSuicideBomber::CTFBotMissionSuicideBomber( void )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionSuicideBomber::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_detonateTimer.Invalidate();
+ m_bHasDetonated = false;
+ m_consecutivePathFailures = 0;
+ m_bWasSuccessful = false;
+ m_bWasKilled = false;
+
+ m_victim = me->GetMissionTarget();
+
+ if ( m_victim != NULL )
+ {
+ m_lastKnownVictimPosition = m_victim->GetAbsOrigin();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMissionSuicideBomber::Update( CTFBot *me, float interval )
+{
+ // one we start detonating, there's no turning back
+ if ( m_detonateTimer.HasStarted() )
+ {
+ if ( m_detonateTimer.IsElapsed() )
+ {
+ m_vecDetLocation = me->GetAbsOrigin();
+ Detonate( me );
+
+ // Send out an event
+ if ( m_bWasSuccessful && m_victim && m_victim->IsBaseObject() )
+ {
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( m_victim.Get() );
+ if ( sentry && sentry->GetOwner() )
+ {
+ CTFPlayer *pOwner = ToTFPlayer( sentry->GetOwner() );
+ if ( pOwner )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_sentrybuster_detonate" );
+ if ( event )
+ {
+ event->SetInt( "player", pOwner->entindex() );
+ event->SetFloat( "det_x", m_vecDetLocation.x );
+ event->SetFloat( "det_y", m_vecDetLocation.y );
+ event->SetFloat( "det_z", m_vecDetLocation.z );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ }
+ }
+
+ return Done( "KABOOM!" );
+ }
+
+ return Continue();
+ }
+
+
+ if ( me->GetHealth() == 1 )
+ {
+ // low on health - detonate where we are!
+ StartDetonate( me, false, true );
+
+ return Continue();
+ }
+
+ if ( m_victim != NULL )
+ {
+ // update chase destination
+ if ( m_victim->IsAlive() && !m_victim->IsEffectActive( EF_NODRAW ) )
+ {
+ m_lastKnownVictimPosition = m_victim->GetAbsOrigin();
+ }
+
+ // if the engineer is carrying his sentry, he becomes the victim
+ if ( m_victim->IsBaseObject() )
+ {
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( m_victim.Get() );
+ if ( sentry && sentry->IsCarried() && sentry->GetOwner() )
+ {
+ // path to the engineer carrying the sentry
+ m_lastKnownVictimPosition = sentry->GetOwner()->GetAbsOrigin();
+ }
+ }
+ }
+
+ // Get to a third of the damage range before detonating
+ const float detonateRange = tf_bot_suicide_bomb_range.GetFloat() / 3.0f;
+ if ( me->IsDistanceBetweenLessThan( m_lastKnownVictimPosition, detonateRange ) )
+ {
+ if ( me->IsLineOfFireClear( m_lastKnownVictimPosition + Vector( 0, 0, StepHeight ) ) )
+ {
+ StartDetonate( me, true );
+ }
+ }
+
+ if ( m_talkTimer.IsElapsed() )
+ {
+ m_talkTimer.Start( 4.0f );
+ me->EmitSound( "MVM.SentryBusterIntro" );
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+
+ if ( m_path.Compute( me, m_lastKnownVictimPosition, cost ) == false )
+ {
+ ++m_consecutivePathFailures;
+
+ if ( m_consecutivePathFailures >= 3 )
+ {
+ // really can't reach my victim - detonate!
+ StartDetonate( me );
+ }
+ }
+ else
+ {
+ m_consecutivePathFailures = 0;
+ }
+ }
+
+ // move to the victim
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionSuicideBomber::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMissionSuicideBomber::OnKilled( CTFBot *me, const CTakeDamageInfo &info )
+{
+ if ( !m_bHasDetonated )
+ {
+ if ( !m_detonateTimer.HasStarted() )
+ {
+ StartDetonate( me );
+ }
+ else if ( m_detonateTimer.IsElapsed() )
+ {
+ Detonate( me );
+ }
+ else
+ {
+ // We're in detonate mode, and something's trying to kill us. Prevent it.
+ if ( me->GetTeamNumber() != TEAM_SPECTATOR )
+ {
+ me->m_lifeState = LIFE_ALIVE;
+ me->SetHealth( 1 );
+ }
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMissionSuicideBomber::OnStuck( CTFBot *me )
+{
+ // we're stuck, decide to detonate now!
+ if ( !m_bHasDetonated && !m_detonateTimer.HasStarted() )
+ {
+ StartDetonate( me );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionSuicideBomber::StartDetonate( CTFBot *me, bool bWasSuccessful /* = false */, bool bWasKilled /*= false*/ )
+{
+ if ( m_detonateTimer.HasStarted() )
+ return;
+
+ if ( !me->IsAlive() || me->GetHealth() < 1 )
+ {
+ if ( me->GetTeamNumber() != TEAM_SPECTATOR)
+ {
+ me->m_lifeState = LIFE_ALIVE;
+ me->SetHealth( 1 );
+ }
+ }
+
+ m_bWasSuccessful = bWasSuccessful;
+ m_bWasKilled = bWasKilled;
+
+ me->m_takedamage = DAMAGE_NO;
+
+ me->Taunt( TAUNT_BASE_WEAPON );
+ m_detonateTimer.Start( 2.0f );
+ me->EmitSound( "MvM.SentryBusterSpin" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMissionSuicideBomber::Detonate( CTFBot *me )
+{
+ // BLAST!
+ m_bHasDetonated = true;
+
+ DispatchParticleEffect( "explosionTrail_seeds_mvm", me->GetAbsOrigin(), me->GetAbsAngles() );
+ DispatchParticleEffect( "fluidSmokeExpl_ring_mvm", me->GetAbsOrigin(), me->GetAbsAngles() );
+
+ me->EmitSound( "MVM.SentryBusterExplode" );
+
+ UTIL_ScreenShake( me->GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START );
+
+ if ( !m_bWasSuccessful )
+ {
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER_DOWN, TF_TEAM_PVE_DEFENDERS );
+
+ // ACHIEVEMENT_TF_MVM_KILL_SENTRY_BUSTER
+ for ( int iDamager = 0 ; iDamager < MAX_ACHIEVEMENT_HISTORY_SLOTS ; iDamager ++ )
+ {
+ EntityHistory_t *damagerHistory = me->m_AchievementData.GetDamagerHistory( iDamager );
+ if ( damagerHistory )
+ {
+ if ( damagerHistory->hEntity && ( gpGlobals->curtime - damagerHistory->flTimeDamage <= 5.0f ) )
+ {
+ CTFPlayer *pRecentDamager = ToTFPlayer( damagerHistory->hEntity );
+ if ( pRecentDamager )
+ {
+ pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MVM_KILL_SENTRY_BUSTER );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
+
+ CUtlVector< CBaseCombatCharacter * > victimVector;
+
+ int i;
+
+ // players
+ for ( i=0; i<playerVector.Count(); ++i )
+ {
+ victimVector.AddToTail( playerVector[i] );
+ }
+
+ // objects
+ CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE );
+ if ( team )
+ {
+ for ( i=0; i<team->GetNumObjects(); ++i )
+ {
+ CBaseObject *object = team->GetObject( i );
+ if ( object )
+ {
+ victimVector.AddToTail( object );
+ }
+ }
+ }
+
+ team = GetGlobalTFTeam( TF_TEAM_RED );
+ if ( team )
+ {
+ for ( i=0; i<team->GetNumObjects(); ++i )
+ {
+ CBaseObject *object = team->GetObject( i );
+ if ( object )
+ {
+ victimVector.AddToTail( object );
+ }
+ }
+ }
+
+ // non-player bots
+ CUtlVector< INextBot * > botVector;
+ TheNextBots().CollectAllBots( &botVector );
+ for( i=0; i<botVector.Count(); ++i )
+ {
+ CBaseCombatCharacter *bot = botVector[i]->GetEntity();
+
+ if ( !bot->IsPlayer() && bot->IsAlive() )
+ {
+ victimVector.AddToTail( bot );
+ }
+ }
+
+ // Send out an event whenever players damaged us to the point where we had to detonate
+ if ( m_bWasKilled )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_sentrybuster_killed" );
+ if ( event )
+ {
+ event->SetInt( "sentry_buster", me->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+
+ // Clear my mission before we have everyone take damage so I will die with the rest
+ me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ me->m_takedamage = DAMAGE_YES;
+
+ // kill victims (including me)
+ for( int i=0; i<victimVector.Count(); ++i )
+ {
+ CBaseCombatCharacter *victim = victimVector[i];
+
+ Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter();
+
+ if ( toVictim.IsLengthGreaterThan( tf_bot_suicide_bomb_range.GetFloat() ) )
+ continue;
+
+ if ( victim->IsPlayer() )
+ {
+ color32 colorHit = { 255, 255, 255, 255 };
+ UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN );
+ }
+
+ if ( me->IsLineOfFireClear( victim ) )
+ {
+ toVictim.NormalizeInPlace();
+
+ int damage = MAX( victim->GetMaxHealth(), victim->GetHealth() );
+
+ CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
+ if ( tf_bot_suicide_bomb_friendly_fire.GetBool() )
+ {
+ info.SetForceFriendlyFire( true );
+ }
+
+ CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
+ victim->TakeDamage( info );
+ }
+ }
+
+ // make sure we're removed (in case we detonated in our spawn area where we are invulnerable)
+ me->CommitSuicide( false, true );
+ if ( me->IsAlive() )
+ {
+ me->ForceChangeTeam( TEAM_SPECTATOR );
+ }
+
+ if ( m_bWasKilled )
+ {
+ // increment num sentry killed this wave
+ CWave *pWave = g_pPopulationManager ? g_pPopulationManager->GetCurrentWave() : NULL;
+ if ( pWave )
+ {
+ pWave->IncrementSentryBustersKilled();
+ }
+ }
+}
+
+
+// Should we attack "them"?
+QueryResultType CTFBotMissionSuicideBomber::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ // buster never "attacks", just approaches and self-detonates
+ return ANSWER_NO;
+}
+
+
diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h
new file mode 100644
index 0000000..6105bea
--- /dev/null
+++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h
@@ -0,0 +1,49 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mission_suicide_bomber.h
+// Move to target and explode
+// Michael Booth, October 2011
+
+#ifndef TF_BOT_MISSION_SUICIDE_BOMBER_H
+#define TF_BOT_MISSION_SUICIDE_BOMBER_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotMissionSuicideBomber : public Action< CTFBot >
+{
+public:
+ CTFBotMissionSuicideBomber( void );
+
+ 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 EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "MissionSuicideBomber"; };
+
+private:
+ CHandle< CBaseEntity > m_victim; // the victim we are trying to destroy
+ Vector m_lastKnownVictimPosition;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CountdownTimer m_talkTimer;
+ CountdownTimer m_detonateTimer;
+
+ void StartDetonate( CTFBot *me, bool bWasSuccessful = false, bool bWasKilled = false );
+ void Detonate( CTFBot *me );
+ bool m_bHasDetonated;
+ bool m_bWasSuccessful;
+ bool m_bWasKilled;
+
+ int m_consecutivePathFailures;
+
+ Vector m_vecDetLocation;
+};
+
+
+#endif // TF_BOT_MISSION_SUICIDE_BOMBER_H
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp
new file mode 100644
index 0000000..4218c3d
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp
@@ -0,0 +1,156 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_destroy_entity.h
+// Destroy the given entity, under nav entity control
+// Michael Booth, September 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+CTFBotNavEntDestroyEntity::CTFBotNavEntDestroyEntity( const CFuncNavPrerequisite *prereq )
+{
+ m_prereq = prereq;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntDestroyEntity::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed before we started" );
+ }
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_wasIgnoringEnemies = me->HasAttribute( CTFBot::IGNORE_ENEMIES );
+
+ m_isReadyToLaunchSticky = true;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotNavEntDestroyEntity::DetonateStickiesWhenSet( CTFBot *me, CTFPipebombLauncher *stickyLauncher ) const
+{
+ if ( !stickyLauncher )
+ return;
+
+ if ( stickyLauncher->GetPipeBombCount() >= 8 || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ // stickies laid - detonate them once they are on the ground
+ const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeVector = stickyLauncher->GetPipeBombVector();
+
+ int i;
+ for( i=0; i<pipeVector.Count(); ++i )
+ {
+ if ( pipeVector[i].Get() && !pipeVector[i]->m_bTouched )
+ {
+ break;
+ }
+ }
+
+ if ( i == pipeVector.Count() )
+ {
+ // stickies are on the ground
+ me->PressAltFireButton();
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntDestroyEntity::Update( CTFBot *me, float interval )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed" );
+ }
+
+ CBaseEntity *target = m_prereq->GetTaskEntity();
+ if ( target == NULL )
+ {
+ return Done( "Target entity is NULL" );
+ }
+
+ float attackRange = me->GetMaxAttackRange();
+
+ if ( m_prereq->GetTaskValue() > 0.0f )
+ {
+ attackRange = MIN( attackRange, m_prereq->GetTaskValue() );
+ }
+
+ if ( me->IsDistanceBetweenLessThan( target, attackRange ) && me->GetVisionInterface()->IsLineOfSightClearToEntity( target ) )
+ {
+ me->SetAttribute( CTFBot::IGNORE_ENEMIES );
+
+ me->GetBodyInterface()->AimHeadTowards( target->WorldSpaceCenter(), IBody::CRITICAL, 0.2f, NULL, "Aiming at target we need to destroy to progress" );
+
+ if ( me->GetBodyInterface()->IsHeadAimingOnTarget() )
+ {
+ // attack
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ // demomen use stickybombs to destroy the barrier
+ CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon();
+ CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+
+ if ( myCurrentWeapon && myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER )
+ {
+ me->Weapon_Switch( stickyLauncher );
+ }
+
+ if ( m_isReadyToLaunchSticky )
+ {
+ me->PressFireButton();
+ }
+
+ m_isReadyToLaunchSticky = !m_isReadyToLaunchSticky;
+
+ DetonateStickiesWhenSet( me, stickyLauncher );
+
+ return Continue();
+ }
+
+ me->EquipBestWeaponForThreat( NULL );
+ me->PressFireButton();
+ }
+
+ return Continue();
+ }
+
+
+ if ( !m_wasIgnoringEnemies )
+ {
+ me->ClearAttribute( CTFBot::IGNORE_ENEMIES );
+ }
+
+ // move into view of our target
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, target->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotNavEntDestroyEntity::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( !m_wasIgnoringEnemies )
+ {
+ me->ClearAttribute( CTFBot::IGNORE_ENEMIES );
+ }
+}
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h
new file mode 100644
index 0000000..87dbc15
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h
@@ -0,0 +1,35 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_destroy_entity.h
+// Destroy the given entity, under nav entity control
+// Michael Booth, September 2009
+
+#ifndef TF_BOT_NAV_ENT_DESTROY_ENTITY_H
+#define TF_BOT_NAV_ENT_DESTROY_ENTITY_H
+
+#include "Path/NextBotPathFollow.h"
+#include "NextBot/NavMeshEntities/func_nav_prerequisite.h"
+#include "tf_weapon_pipebomblauncher.h"
+
+class CTFBotNavEntDestroyEntity : public Action< CTFBot >
+{
+public:
+ CTFBotNavEntDestroyEntity( const CFuncNavPrerequisite *prereq );
+
+ 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 const char *GetName( void ) const { return "NavEntDestroyEntity"; };
+
+private:
+ CHandle< CFuncNavPrerequisite > m_prereq;
+ PathFollower m_path; // how we get to the target
+ CountdownTimer m_repathTimer;
+ bool m_wasIgnoringEnemies;
+
+ void DetonateStickiesWhenSet( CTFBot *me, CTFPipebombLauncher *stickyLauncher ) const;
+ bool m_isReadyToLaunchSticky;
+};
+
+
+#endif // TF_BOT_NAV_ENT_DESTROY_ENTITY_H
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp
new file mode 100644
index 0000000..22b7f03
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp
@@ -0,0 +1,110 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_move_to.cpp
+// Move onto target and wait, as directed by nav entity
+// Michael Booth, September 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+CTFBotNavEntMoveTo::CTFBotNavEntMoveTo( const CFuncNavPrerequisite *prereq )
+{
+ m_prereq = prereq;
+ m_pGoalArea = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntMoveTo::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed before we started" );
+ }
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_waitTimer.Invalidate();
+
+ CBaseEntity *target = m_prereq->GetTaskEntity();
+ if ( target == NULL )
+ {
+ return Done( "Prerequisite target entity is NULL" );
+ }
+
+ Extent targetExtent;
+ targetExtent.Init( target );
+
+ // pick random ground position within target entity as move-to goal
+ m_goalPosition = targetExtent.lo + Vector( RandomFloat( 0.0f, targetExtent.SizeX() ), RandomFloat( 0.0f, targetExtent.SizeY() ), targetExtent.SizeZ() );
+
+ TheNavMesh->GetSimpleGroundHeight( m_goalPosition, &m_goalPosition.z );
+
+ m_pGoalArea = (CTFNavArea*)TheNavMesh->GetNavArea( m_goalPosition );
+ if ( !m_pGoalArea )
+ {
+ return Done( "There's no nav area for the goal position" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntMoveTo::Update( CTFBot *me, float interval )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed" );
+ }
+
+ if ( !m_prereq->IsEnabled() )
+ {
+ return Done( "Prerequisite has been disabled" );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( m_waitTimer.HasStarted() )
+ {
+ if ( m_waitTimer.IsElapsed() )
+ {
+ return Done( "Wait duration elapsed" );
+ }
+ }
+ else
+ {
+ // move to the goal area
+ if ( m_pGoalArea == me->GetLastKnownArea() )
+ {
+ // in area
+ m_waitTimer.Start( m_prereq->GetTaskValue() );
+ }
+ else
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_goalPosition, cost );
+ }
+
+ // move into position
+ m_path.Update( me );
+ }
+ }
+
+ return Continue();
+}
+
+
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h
new file mode 100644
index 0000000..53295af
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h
@@ -0,0 +1,35 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_move_to.h
+// Move onto target and wait, as directed by nav entity
+// Michael Booth, September 2009
+
+#ifndef TF_BOT_NAV_ENT_MOVE_TO_H
+#define TF_BOT_NAV_ENT_MOVE_TO_H
+
+#include "Path/NextBotPathFollow.h"
+#include "NextBot/NavMeshEntities/func_nav_prerequisite.h"
+
+
+class CTFBotNavEntMoveTo : public Action< CTFBot >
+{
+public:
+ CTFBotNavEntMoveTo( const CFuncNavPrerequisite *prereq );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "NavEntMoveTo"; };
+
+private:
+ CHandle< CFuncNavPrerequisite > m_prereq;
+ Vector m_goalPosition; // specific position within entity to move to
+ CTFNavArea* m_pGoalArea;
+
+ CountdownTimer m_waitTimer;
+
+ PathFollower m_path; // how we get to the loot
+ CountdownTimer m_repathTimer;
+};
+
+
+#endif // TF_BOT_NAV_ENT_MOVE_TO_H
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp
new file mode 100644
index 0000000..d55d7fd
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp
@@ -0,0 +1,56 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_wait.cpp
+// Wait for awhile, as directed by nav entity
+// Michael Booth, September 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/nav_entities/tf_bot_nav_ent_wait.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+CTFBotNavEntWait::CTFBotNavEntWait( const CFuncNavPrerequisite *prereq )
+{
+ m_prereq = prereq;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntWait::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed before we started" );
+ }
+
+ m_timer.Start( m_prereq->GetTaskValue() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotNavEntWait::Update( CTFBot *me, float interval )
+{
+ if ( m_prereq == NULL )
+ {
+ return Done( "Prerequisite has been removed" );
+ }
+
+ if ( !m_prereq->IsEnabled() )
+ {
+ return Done( "Prerequisite has been disabled" );
+ }
+
+ if ( m_timer.IsElapsed() )
+ {
+ return Done( "Wait time elapsed" );
+ }
+
+ return Continue();
+}
+
+
diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h
new file mode 100644
index 0000000..72de0e8
--- /dev/null
+++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h
@@ -0,0 +1,28 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_nav_ent_wait.h
+// Wait for awhile, as directed by nav entity
+// Michael Booth, September 2009
+
+#ifndef TF_BOT_NAV_ENT_WAIT_H
+#define TF_BOT_NAV_ENT_WAIT_H
+
+#include "NextBot/NavMeshEntities/func_nav_prerequisite.h"
+
+
+class CTFBotNavEntWait : public Action< CTFBot >
+{
+public:
+ CTFBotNavEntWait( const CFuncNavPrerequisite *prereq );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "NavEntWait"; };
+
+private:
+ CHandle< CFuncNavPrerequisite > m_prereq;
+ CountdownTimer m_timer;
+};
+
+
+#endif // TF_BOT_NAV_ENT_WAIT_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp
new file mode 100644
index 0000000..6d8648b
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp
@@ -0,0 +1,231 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_capture_point.cpp
+// Move to and try to capture the next point
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "trigger_area_capture.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h"
+#include "bot/behavior/tf_bot_seek_and_destroy.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+ConVar tf_bot_offense_must_push_time( "tf_bot_offense_must_push_time", "120", FCVAR_CHEAT, "If timer is less than this, bots will push hard to cap" );
+
+ConVar tf_bot_capture_seek_and_destroy_min_duration( "tf_bot_capture_seek_and_destroy_min_duration", "15", FCVAR_CHEAT, "If a capturing bot decides to go hunting, this is the min duration he will hunt for before reconsidering" );
+ConVar tf_bot_capture_seek_and_destroy_max_duration( "tf_bot_capture_seek_and_destroy_max_duration", "30", FCVAR_CHEAT, "If a capturing bot decides to go hunting, this is the max duration he will hunt for before reconsidering" );
+
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCapturePoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ VPROF_BUDGET( "CTFBotCapturePoint::OnStart", "NextBot" );
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_path.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCapturePoint::Update( CTFBot *me, float interval )
+{
+ if ( TFGameRules()->InSetup() )
+ {
+ // wait until the gates open, then path
+ m_path.Invalidate();
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ return Continue();
+ }
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+
+ if ( point == NULL )
+ {
+ const float roamTime = 10.0f;
+ return SuspendFor( new CTFBotSeekAndDestroy( roamTime ), "Seek and destroy until a point becomes available" );
+ }
+
+ if ( point->GetTeamNumber() == me->GetTeamNumber() )
+ {
+ return ChangeTo( new CTFBotDefendPoint, "We need to defend our point(s)" );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ bool isPushingToCapture = ( me->IsPointBeingCaptured( point ) && !me->IsInCombat() ) || // a friend is capturing
+ me->IsCapturingPoint() || // we're capturing
+ // me->m_Shared.InCond( TF_COND_INVULNERABLE ) || // we're ubered
+ TFGameRules()->InOvertime() || // the game is in overtime
+ me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() || // nearly out of tim
+ TFGameRules()->IsInTraining() || // teach newbies to capture
+ me->IsNearPoint( point );
+
+
+ // if we see an enemy at a good combat range, stop and engage them unless we're running out of time
+ if ( !isPushingToCapture )
+ {
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ return SuspendFor( new CTFBotSeekAndDestroy( RandomFloat( tf_bot_capture_seek_and_destroy_min_duration.GetFloat(), tf_bot_capture_seek_and_destroy_max_duration.GetFloat() ) ), "Too early to capture - hunting" );
+ }
+ }
+
+
+ if ( me->IsCapturingPoint() )
+ {
+ // move around on the point while we capture
+ const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() );
+ if ( controlPointAreas )
+ {
+ if ( controlPointAreas->Count() == 0 )
+ {
+ Assert( controlPointAreas->Count() );
+ Continue(); // this control point has no nav areas for bot to move around
+ }
+
+ // move to a random spot on this control point
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ int which = RandomInt( 0, controlPointAreas->Count() - 1 );
+ CTFNavArea *goalArea = controlPointAreas->Element( which );
+ if ( goalArea )
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, goalArea->GetRandomPoint(), cost );
+ }
+ }
+
+ m_path.Update( me );
+ }
+ }
+ else
+ {
+ // move toward the point, periodically repathing to account for changing situation
+ if ( m_repathTimer.IsElapsed() )
+ {
+ VPROF_BUDGET( "CTFBotCapturePoint::Update( repath )", "NextBot" );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, point->GetAbsOrigin(), cost );
+ m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) );
+ }
+
+ if ( TFGameRules()->IsInTraining() && !me->IsAnyPointBeingCaptured() )
+ {
+ // stop short of capturing until the human trainee starts it
+ if ( m_path.GetLength() < 1000.0f )
+ {
+ // hold here and yell at player to get on the point
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_GO );
+
+ return Continue();
+ }
+ }
+
+ // move towards next capture point
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCapturePoint::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_repathTimer.Invalidate();
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnStuck( CTFBot *me )
+{
+ m_repathTimer.Invalidate();
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_repathTimer.Invalidate();
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ // we got it, move on
+ m_repathTimer.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotCapturePoint::ShouldRetreat( const INextBot *bot ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ // if we're running out of time, we have to go for it
+ if ( me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() )
+ return ANSWER_NO;
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotCapturePoint::ShouldHurry( const INextBot *bot ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ // if we're running out of time, we have to go for it
+ if ( me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() )
+ return ANSWER_YES;
+
+ return ANSWER_UNDEFINED;
+}
+
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h
new file mode 100644
index 0000000..af80217
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h
@@ -0,0 +1,36 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_capture_point.h
+// Move to and try to capture the next point
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_CAPTURE_POINT_H
+#define TF_BOT_CAPTURE_POINT_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotCapturePoint : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "CapturePoint"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+};
+
+#endif // TF_BOT_CAPTURE_POINT_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp
new file mode 100644
index 0000000..20c090b
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp
@@ -0,0 +1,442 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_defend_point.h
+// Move to and defend current point from capture
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh/tf_nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "trigger_area_capture.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_seek_and_destroy.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h"
+#include "bot/behavior/sniper/tf_bot_sniper_attack.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_min_setup_gate_defend_range;
+extern ConVar tf_bot_max_setup_gate_defend_range;
+extern ConVar tf_bot_min_setup_gate_sniper_defend_range;
+extern ConVar tf_bot_offense_must_push_time;
+
+ConVar tf_bot_defense_must_defend_time( "tf_bot_defense_must_defend_time", "300", FCVAR_CHEAT, "If timer is less than this, bots will stay near point and guard" );
+ConVar tf_bot_max_point_defend_range( "tf_bot_max_point_defend_range", "1250", FCVAR_CHEAT, "How far (in travel distance) from the point defending bots will take up positions" );
+ConVar tf_bot_defense_debug( "tf_bot_defense_debug", "0", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_defenseArea = NULL;
+
+ // higher skilled bots prefer to seek and destroy until the time is almost up
+ static float roamChance[ CTFBot::NUM_DIFFICULTY_LEVELS ] = { 10.0f, 50.0f, 75.0f, 90.0f };
+ m_isAllowedToRoam = ( RandomFloat( 0.0f, 100.0f ) < roamChance[ (int)clamp( me->GetDifficulty(), CTFBot::EASY, CTFBot::EXPERT ) ] );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Return true if we're in immediate danger of losing the point
+ */
+bool CTFBotDefendPoint::IsPointThreatened( CTFBot *me )
+{
+ CTeamControlPoint *point = me->GetMyControlPoint();
+
+ 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;
+ }
+
+ // if we just lost a point, we should fall back and stand on the next point to defend against a rush
+ if ( me->WasPointJustLost() )
+ {
+ return true;
+ }
+
+/*
+ // if an enemy is closer to the point than we are, head them off
+ // TODO: Compare time to reach, not distance
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat )
+ {
+ const float tolerance = 100.0f;
+
+ float themRange = ( threat->GetLastKnownPosition() - point->GetAbsOrigin() ).Length();
+ float myRange = ( me->GetAbsOrigin() - point->GetAbsOrigin() ).Length();
+ if ( myRange + tolerance > themRange )
+ return true;
+ }
+*/
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Are we smart enough to get on the point to block the cap
+bool CTFBotDefendPoint::WillBlockCapture( CTFBot *me ) const
+{
+ if ( TFGameRules()->IsInTraining() )
+ return false;
+
+ if ( me->IsDifficulty( CTFBot::EASY ) )
+ return false;
+
+ if ( me->IsDifficulty( CTFBot::NORMAL ) )
+ {
+ // 50% chance of blocking cap
+ return me->TransientlyConsistentRandomValue() > 0.5f;
+ }
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPoint::Update( CTFBot *me, float interval )
+{
+ // King of the Hill logic
+ CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( master && master->GetNumPoints() == 1 )
+ {
+ // if we don't own the only point, switch to capture behavior
+ CTeamControlPoint *point = master->GetControlPoint( 0 );
+ if ( point && point->GetOwner() != me->GetTeamNumber() )
+ {
+ return ChangeTo( new CTFBotCapturePoint, "We need to capture the point!" );
+ }
+ }
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+
+ if ( point == NULL )
+ {
+ const float roamTime = 10.0f;
+ return SuspendFor( new CTFBotSeekAndDestroy( roamTime ), "Seek and destroy until a point becomes available" );
+ }
+
+ if ( point->GetTeamNumber() != me->GetTeamNumber() )
+ {
+ return ChangeTo( new CTFBotCapturePoint, "We need to capture our point(s)" );
+ }
+
+ // if point in is danger - get ON the point!
+ // Don't do this in training to keep things easy for the new trainee
+ if ( IsPointThreatened( me ) && WillBlockCapture( me ) )
+ {
+ // point is being captured - get on it!
+ return SuspendFor( new CTFBotDefendPointBlockCapture, "Moving to block point capture!" );
+ }
+
+ // point is safe for the moment
+
+ // if I'm uber'd, go get 'em!
+ if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) )
+ {
+ const float uberChargeTime = 6.0;
+ return SuspendFor( new CTFBotSeekAndDestroy( uberChargeTime ), "Attacking because I'm uber'd!" );
+ }
+
+ if ( point && point->IsLocked() )
+ {
+ return SuspendFor( new CTFBotSeekAndDestroy, "Seek and destroy until the point unlocks" );
+ }
+
+ if ( m_isAllowedToRoam && me->GetTimeLeftToCapture() > tf_bot_defense_must_defend_time.GetFloat() )
+ {
+ return SuspendFor( new CTFBotSeekAndDestroy( 15.0f ), "Seek and destroy - we have lots of time" );
+ }
+
+ if ( TFGameRules()->InSetup() )
+ {
+ // don't lose patience during setup time
+ m_idleTimer.Reset();
+ }
+
+ // if we see an enemy as we have a melee weapon equipped, chase them down
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ me->EquipBestWeaponForThreat( threat );
+
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // we're aware of an enemy
+ m_idleTimer.Reset();
+
+ if ( me->IsPlayerClass( TF_CLASS_PYRO ) )
+ {
+ // go get 'em
+ return SuspendFor( new CTFBotSeekAndDestroy( 15.0f ), "Going after an enemy" );
+ }
+
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon && ( myWeapon->IsMeleeWeapon() || myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) ) )
+ {
+ // TODO: Check if threat is visible and if not, move to last known position
+ CTFBotPathCost cost( me, me->IsPlayerClass( TF_CLASS_PYRO ) ? SAFEST_ROUTE : FASTEST_ROUTE );
+ m_chasePath.Update( me, threat->GetEntity(), cost );
+
+ return Continue();
+ }
+ }
+
+ // choose where we'll defend from
+ if ( m_defenseArea == NULL || m_idleTimer.IsElapsed() )
+ {
+ m_defenseArea = SelectAreaToDefendFrom( me );
+ }
+
+ if ( m_defenseArea )
+ {
+ if ( me->GetLastKnownArea() == m_defenseArea )
+ {
+ // at our defense position
+ if ( CTFBotPrepareStickybombTrap::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" );
+ }
+ }
+ else
+ {
+ // move to our desired defense position, repathing periodically to account for changing situation
+ VPROF_BUDGET( "CTFBotDefendPoint::Update( repath )", "NextBot" );
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) );
+
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, m_defenseArea->GetCenter(), cost );
+ }
+
+ m_path.Update( me );
+
+ // we're not idle while we're moving to our defend position
+ m_idleTimer.Reset();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPoint::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ // may have lost point - recheck
+ me->ClearMyControlPoint();
+ m_repathTimer.Invalidate();
+ m_path.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ m_defenseArea = SelectAreaToDefendFrom( me );
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_path.Invalidate();
+ m_defenseArea = SelectAreaToDefendFrom( me );
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ // handled in the Update() loop
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ // we lost it, fall back to next point
+ me->ClearMyControlPoint();
+ m_defenseArea = SelectAreaToDefendFrom( me );
+ m_repathTimer.Invalidate();
+ m_path.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CSelectDefenseAreaForPoint : public ISearchSurroundingAreasFunctor
+{
+public:
+ CSelectDefenseAreaForPoint( CTFNavArea *pointArea, int myTeam, CUtlVector< CTFNavArea * > *areaVector )
+ {
+ m_pointArea = pointArea;
+ m_myTeam = myTeam;
+
+ // don't select areas that are beyond the point
+ m_incursionFlowLimit = pointArea->GetIncursionDistance( m_myTeam ) + 250.0f;
+
+ m_areaVector = areaVector;
+ m_areaVector->RemoveAll();
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ if ( !TFGameRules()->IsInKothMode() )
+ {
+ // don't select areas that are beyond the point
+ if ( area->GetIncursionDistance( m_myTeam ) > m_incursionFlowLimit )
+ return true;
+ }
+
+ if ( area->IsPotentiallyVisible( m_pointArea ) )
+ {
+ // a bit of a hack here to avoid bots choosing to defend in bottom of ravine at stage 3 of dustbowl
+ const float tooLow = 220.0f;
+ if ( m_pointArea->GetCenter().z - area->GetCenter().z < tooLow )
+ {
+ // valid defense position
+ m_areaVector->AddToTail( area );
+ }
+ }
+
+ return true;
+ }
+
+ virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar )
+ {
+ if ( adjArea->IsBlocked( TFGameRules()->IsInKothMode() ? TEAM_ANY : m_myTeam ) )
+ {
+ return false;
+ }
+
+ if ( travelDistanceSoFar > tf_bot_max_point_defend_range.GetFloat() )
+ {
+ // too far away
+ return false;
+ }
+
+ const float maxHeightChange = 65.0f;
+ float deltaZ = currentArea->ComputeAdjacentConnectionHeightChange( adjArea );
+ return ( fabs( deltaZ ) < maxHeightChange );
+ }
+
+ CTFNavArea *m_pointArea;
+ CUtlVector< CTFNavArea * > *m_areaVector;
+ float m_incursionFlowLimit;
+ int m_myTeam;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Select the area where we will guard the point from
+ */
+CTFNavArea *CTFBotDefendPoint::SelectAreaToDefendFrom( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotDefendPoint::SelectAreaToDefendFrom", "NextBot" );
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( !point )
+ {
+ return NULL;
+ }
+
+ // decide where we will defend from
+ CUtlVector< CTFNavArea * > defenseAreas;
+
+/*
+ if ( !TFGameRules()->IsInKothMode() &&
+ point->GetTeamCapPercentage( me->GetTeamNumber() ) <= 0.0f && // point is currently safe
+ ( ObjectiveResource()->GetPreviousPointForPoint( point->GetPointIndex(), me->GetTeamNumber(), 0 ) < 0 || // this is the first cap point
+ me->IsPlayerClass( TF_CLASS_PYRO ) ) ) // pyros are skirmishers
+ {
+ if ( TheTFNavMesh()->GetSetupGateDefenseAreas() )
+ {
+ defenseAreas = *TheTFNavMesh()->GetSetupGateDefenseAreas();
+ }
+ }
+*/
+
+ if ( defenseAreas.Count() == 0 )
+ {
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+ if ( pointArea )
+ {
+ // search outwards from the point along walkable areas (not drop downs) to make sure we can get back to the point quickly
+ CSelectDefenseAreaForPoint defenseScan( pointArea, me->GetTeamNumber(), &defenseAreas );
+ SearchSurroundingAreas( pointArea, defenseScan );
+ }
+ }
+
+ // select a specific area from the potential defense set
+ if ( defenseAreas.Count() == 0 )
+ {
+ return NULL;
+ }
+
+ // how long will we wait if we don't see any action
+ m_idleTimer.Start( RandomFloat( 10.0f, 20.0f ) );
+
+ if ( tf_bot_defense_debug.GetBool() )
+ {
+ for( int i=0; i<defenseAreas.Count(); ++i )
+ {
+ defenseAreas[i]->DrawFilled( 0, 200, 200, 999.9f );
+ }
+ }
+
+ // select one of the defense areas
+ int which = RandomInt( 0, defenseAreas.Count()-1 );
+ return defenseAreas[ which ];
+}
+
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h
new file mode 100644
index 0000000..c68a55c
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h
@@ -0,0 +1,48 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_defend_point.h
+// Move to and defend current point from capture
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_DEFEND_POINT_H
+#define TF_BOT_DEFEND_POINT_H
+
+#include "Path/NextBotPathFollow.h"
+#include "Path/NextBotChasePath.h"
+
+class CTFBotDefendPoint : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual const char *GetName( void ) const { return "DefendPoint"; };
+
+private:
+ PathFollower m_path; // for moving to a defense position
+ ChasePath m_chasePath; // for chasing enemies
+
+ CountdownTimer m_repathTimer;
+ CountdownTimer m_lookAroundTimer;
+ CountdownTimer m_idleTimer;
+
+ CTFNavArea *m_defenseArea;
+ CTFNavArea *SelectAreaToDefendFrom( CTFBot *me );
+
+ bool IsPointThreatened( CTFBot *me );
+ bool WillBlockCapture( CTFBot *me ) const;
+ bool m_isAllowedToRoam;
+};
+
+
+#endif // TF_BOT_DEFEND_POINT_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp
new file mode 100644
index 0000000..6e5bbf7
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp
@@ -0,0 +1,247 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_defend_point_block_capture.h
+// Move to and defend current point from capture
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh/tf_nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "trigger_area_capture.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_defend_owned_point_percent( "tf_bot_defend_owned_point_percent", "0.5", FCVAR_CHEAT, "Stay on the contested point we own until enemy cap percent falls below this" );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPointBlockCapture::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_point = me->GetMyControlPoint();
+ if ( m_point == NULL )
+ {
+ return Done( "Point is NULL" );
+ }
+
+ m_defenseArea = static_cast< CTFNavArea * >( TheTFNavMesh()->GetNearestNavArea( m_point->GetAbsOrigin() ) );
+ if ( m_defenseArea == NULL )
+ {
+ return Done( "Can't find nav area on point" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotDefendPointBlockCapture::IsPointSafe( CTFBot *me )
+{
+ // if a point was just captured, defend this point for awhile
+ if ( me->WasPointJustLost() )
+ {
+ return false;
+ }
+
+ if ( m_point == NULL )
+ {
+ return true;
+ }
+
+ if ( m_point->GetTeamCapPercentage( me->GetTeamNumber() ) < tf_bot_defend_owned_point_percent.GetFloat() )
+ {
+ // we're not in complete control of this point yet
+ return false;
+ }
+
+ // is point is being contested, or was just being contested, its not safe
+ if ( m_point->HasBeenContested() && ( gpGlobals->curtime - m_point->LastContestedAt() ) < 5.0f )
+ {
+ return false;
+ }
+
+ // if we still see a near threat, stay put
+ const CKnownEntity *knownThreat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( knownThreat )
+ {
+ const float dangerRange = 500.0f;
+ if ( ( knownThreat->GetLastKnownPosition() - m_point->GetAbsOrigin() ).IsLengthLessThan( dangerRange ) )
+ return false;
+ }
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPointBlockCapture::Update( CTFBot *me, float interval )
+{
+ // if point is safe, we can move back to our defense positions
+ if ( IsPointSafe( me ) )
+ {
+ return Done( "Point is safe again" );
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // medics look ridiculous rushing to the point - they need to heal
+ return SuspendFor( new CTFBotMedicHeal );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ me->EquipBestWeaponForThreat( threat );
+
+ Extent pointExtent;
+ pointExtent.Init( m_point );
+
+ bool isStandingOnThePoint = pointExtent.Contains( me->GetAbsOrigin() );
+
+ const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( m_point->GetPointIndex() );
+ if ( controlPointAreas )
+ {
+ for( int i=0; i<controlPointAreas->Count(); ++i )
+ {
+ if ( me->GetLastKnownArea() && me->GetLastKnownArea()->GetID() == controlPointAreas->Element(i)->GetID() )
+ {
+ isStandingOnThePoint = true;
+ }
+ }
+ }
+
+ if ( isStandingOnThePoint && CTFBotPrepareStickybombTrap::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotPrepareStickybombTrap, "Placing stickies for defense" );
+ }
+
+ if ( controlPointAreas )
+ {
+ // move to a random spot on this control point
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ float totalArea = 0.0f;
+ int i;
+ for( i=0; i<controlPointAreas->Count(); ++i )
+ {
+ CTFNavArea *area = controlPointAreas->Element(i);
+ totalArea += area->GetSizeX() * area->GetSizeY();
+ }
+
+ float which = RandomFloat( 0.0f, totalArea - 1.0f );
+ CTFNavArea *goalArea = NULL;
+ for( i=0; i<controlPointAreas->Count(); ++i )
+ {
+ CTFNavArea *area = controlPointAreas->Element(i);
+ which -= area->GetSizeX() * area->GetSizeY();
+ if ( which <= 0.0f )
+ {
+ goalArea = area;
+ break;
+ }
+ }
+
+ if ( goalArea )
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, goalArea->GetRandomPoint(), cost );
+ }
+ }
+
+ m_path.Update( me );
+ }
+ else if ( !isStandingOnThePoint )
+ {
+ // get on the point!
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, ( pointExtent.lo + pointExtent.hi )/2.0f, cost );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDefendPointBlockCapture::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_path.Invalidate();
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ return TryToSustain();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ // we lost it, fall back
+ return TryDone( RESULT_CRITICAL, "Lost the point" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDefendPointBlockCapture::ShouldHurry( const INextBot *me ) const
+{
+ // hurry up and get on the point!
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDefendPointBlockCapture::ShouldRetreat( const INextBot *me ) const
+{
+ // get on the point!
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h
new file mode 100644
index 0000000..55ab7af
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h
@@ -0,0 +1,40 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_defend_point_block_capture.h
+// Move to and defend current point from capture
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H
+#define TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H
+
+
+class CTFBotDefendPointBlockCapture : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const;
+
+ virtual const char *GetName( void ) const { return "BlockCapture"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ CTeamControlPoint *m_point;
+ CTFNavArea *m_defenseArea;
+
+ bool IsPointSafe( CTFBot *me );
+};
+
+
+#endif // TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp
new file mode 100644
index 0000000..95f1480
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp
@@ -0,0 +1,126 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_attack_flag_defenders.cpp
+// Attack enemies that are preventing the flag from reaching its destination
+// Michael Booth, May 2011
+
+#include "cbase.h"
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h"
+
+ConVar tf_bot_flag_escort_range( "tf_bot_flag_escort_range", "500", FCVAR_CHEAT );
+
+extern ConVar tf_bot_flag_escort_max_count;
+
+extern int GetBotEscortCount( int team );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotAttackFlagDefenders::CTFBotAttackFlagDefenders( float minDuration )
+{
+ if ( minDuration > 0.0f )
+ {
+ m_minDurationTimer.Start( minDuration );
+ }
+ else
+ {
+ m_minDurationTimer.Invalidate();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotAttackFlagDefenders::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_chasePlayer = NULL;
+ return CTFBotAttack::OnStart( me, priorAction );
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotAttackFlagDefenders::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( m_watchFlagTimer.IsElapsed() && m_minDurationTimer.IsElapsed() )
+ {
+ m_watchFlagTimer.Start( RandomFloat( 1.0f, 3.0f ) );
+
+ CCaptureFlag *flag = me->GetFlagToFetch();
+
+ if ( !flag )
+ {
+ return Done( "No flag" );
+ }
+
+ // can't reach flag if it is at home
+ if ( !TFGameRules()->IsMannVsMachineMode() || !flag->IsHome() )
+ {
+ CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() );
+ if ( !carrier )
+ {
+ return Done( "Flag was dropped" );
+ }
+
+ if ( me->IsSelf( carrier ) )
+ {
+ return Done( "I picked up the flag!" );
+ }
+
+ // escort the flag carrier, unless the carrier is in a squad
+ CTFBot *botCarrier = ToTFBot( carrier );
+ if ( !botCarrier || !botCarrier->IsInASquad() )
+ {
+ if ( me->IsRangeLessThan( carrier, tf_bot_flag_escort_range.GetFloat() ) )
+ {
+ if ( GetBotEscortCount( me->GetTeamNumber() ) < tf_bot_flag_escort_max_count.GetInt() )
+ {
+ return ChangeTo( new CTFBotEscortFlagCarrier, "Near flag carrier - escorting" );
+ }
+ }
+ }
+ }
+ }
+
+ ActionResult< CTFBot > result = CTFBotAttack::Update( me, interval );
+
+ if ( result.IsDone() )
+ {
+ // nothing to attack, move towards a random player
+
+ if ( m_chasePlayer == NULL || !m_chasePlayer->IsAlive() )
+ {
+ m_chasePlayer = me->SelectRandomReachableEnemy();
+ }
+
+ if ( m_chasePlayer == NULL )
+ {
+ // everyone is dead or hiding in the spawn room - go escort the flag
+ return ChangeTo( new CTFBotEscortFlagCarrier, "No reachable victim - escorting flag" );
+ }
+
+ // cheat and "see" our victim so we know where to go
+ me->GetVisionInterface()->AddKnownEntity( m_chasePlayer );
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 3.0f ) );
+
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f;
+ m_path.Compute( me, m_chasePlayer, cost, maxPathLength );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h
new file mode 100644
index 0000000..662ef8a
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h
@@ -0,0 +1,34 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_attack_flag_defenders.h
+// Attack enemies that are preventing the flag from reaching its destination
+// Michael Booth, May 2011
+
+#ifndef TF_BOT_ATTACK_FLAG_DEFENDERS_H
+#define TF_BOT_ATTACK_FLAG_DEFENDERS_H
+
+#include "Path/NextBotPathFollow.h"
+#include "bot/behavior/tf_bot_attack.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotAttackFlagDefenders : public CTFBotAttack
+{
+public:
+ CTFBotAttackFlagDefenders( float minDuration = -1.0f );
+ virtual ~CTFBotAttackFlagDefenders() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "AttackFlagDefenders"; }
+
+private:
+ CountdownTimer m_minDurationTimer;
+ CountdownTimer m_watchFlagTimer;
+ CHandle< CTFPlayer > m_chasePlayer;
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+};
+
+
+#endif // TF_BOT_ATTACK_FLAG_DEFENDERS_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp
new file mode 100644
index 0000000..873c36e
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp
@@ -0,0 +1,422 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_deliver_flag.cpp
+// Take the flag we are holding to its destination
+// Michael Booth, May 2011
+
+#include "cbase.h"
+
+#include "tf_player_shared.h"
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h"
+#include "bot/behavior/tf_bot_taunt.h"
+#include "bot/behavior/tf_bot_mvm_deploy_bomb.h"
+
+#include "tf_objective_resource.h"
+#include "player_vs_environment/tf_population_manager.h"
+#include "econ_item_system.h"
+#include "tf_gamestats.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 "particle_parse.h"
+
+ConVar tf_mvm_bot_allow_flag_carrier_to_fight( "tf_mvm_bot_allow_flag_carrier_to_fight", "1", FCVAR_CHEAT );
+
+ConVar tf_mvm_bot_flag_carrier_interval_to_1st_upgrade( "tf_mvm_bot_flag_carrier_interval_to_1st_upgrade", "5", FCVAR_CHEAT );
+ConVar tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade( "tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade", "15", FCVAR_CHEAT );
+ConVar tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade( "tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade", "15", FCVAR_CHEAT );
+
+ConVar tf_mvm_bot_flag_carrier_health_regen( "tf_mvm_bot_flag_carrier_health_regen", "45.0f", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDeliverFlag::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_flTotalTravelDistance = -1.0f;
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ if ( tf_mvm_bot_allow_flag_carrier_to_fight.GetBool() == false )
+ {
+ me->SetAttribute( CTFBot::SUPPRESS_FIRE );
+ }
+
+ // mini-bosses don't upgrade - they are already tough
+ if ( me->IsMiniBoss() )
+ {
+ m_upgradeLevel = DONT_UPGRADE;
+ if ( TFObjectiveResource() )
+ {
+ // Set threat level to max
+ TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 4 );
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( -1 );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( -1 );
+ }
+ }
+ else
+ {
+ m_upgradeLevel = 0;
+ m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_1st_upgrade.GetFloat() );
+ if ( TFObjectiveResource() )
+ {
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() );
+ }
+
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// In Mann Vs Machine, the flag carrier gets stronger the longer he carries the flag
+bool CTFBotDeliverFlag::UpgradeOverTime( CTFBot *me )
+{
+ if ( TFGameRules()->IsMannVsMachineMode() && m_upgradeLevel != DONT_UPGRADE )
+ {
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
+
+ if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
+ {
+ // don't start counting down until we leave the spawn
+ m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_1st_upgrade.GetFloat() );
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() );
+ }
+
+ // do defensive buff effect ourselves (since we're not a soldier)
+ if ( m_upgradeLevel > 0 && m_buffPulseTimer.IsElapsed() )
+ {
+ m_buffPulseTimer.Start( 1.0f );
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ const float buffRadius = 450.0f;
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( me->IsRangeLessThan( playerVector[i], buffRadius ) )
+ {
+ playerVector[i]->m_Shared.AddCond( TF_COND_DEFENSEBUFF_NO_CRIT_BLOCK, 1.2f );
+ }
+ }
+ }
+
+ // the flag carrier gets stronger the longer he holds the flag
+ if ( m_upgradeTimer.IsElapsed() )
+ {
+ const int maxLevel = 3;
+
+ if ( m_upgradeLevel < maxLevel )
+ {
+ ++m_upgradeLevel;
+
+ TFGameRules()->BroadcastSound( 255, "MVM.Warning" );
+
+ switch( m_upgradeLevel )
+ {
+ //---------------------------------------
+ case 1:
+ m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade.GetFloat() );
+
+ // permanent buff banner effect (handled above)
+
+ // update the objective resource so clients have the information
+ if ( TFObjectiveResource() )
+ {
+ TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 1 );
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() );
+ TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE1, TF_TEAM_PVE_DEFENDERS );
+ DispatchParticleEffect( "mvm_levelup1", PATTACH_POINT_FOLLOW, me, "head" );
+ }
+ return true;
+
+ //---------------------------------------
+ case 2:
+ {
+ static CSchemaAttributeDefHandle pAttrDef_HealthRegen( "health regen" );
+
+ m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade.GetFloat() );
+
+ if ( !pAttrDef_HealthRegen )
+ {
+ Warning( "TFBotSpawner: Invalid attribute 'health regen'\n" );
+ }
+ else
+ {
+ CAttributeList *pAttrList = me->GetAttributeList();
+ if ( pAttrList )
+ {
+ pAttrList->SetRuntimeAttributeValue( pAttrDef_HealthRegen, tf_mvm_bot_flag_carrier_health_regen.GetFloat() );
+ }
+ }
+
+ // update the objective resource so clients have the information
+ if ( TFObjectiveResource() )
+ {
+ TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 2 );
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() );
+ TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE2, TF_TEAM_PVE_DEFENDERS );
+ DispatchParticleEffect( "mvm_levelup2", PATTACH_POINT_FOLLOW, me, "head" );
+ }
+ return true;
+ }
+
+ //---------------------------------------
+ case 3:
+ // add critz
+ me->m_Shared.AddCond( TF_COND_CRITBOOSTED );
+
+ // update the objective resource so clients have the information
+ if ( TFObjectiveResource() )
+ {
+ TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 3 );
+ TFObjectiveResource()->SetBaseMvMBombUpgradeTime( -1 );
+ TFObjectiveResource()->SetNextMvMBombUpgradeTime( -1 );
+ TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE3, TF_TEAM_PVE_DEFENDERS );
+ DispatchParticleEffect( "mvm_levelup3", PATTACH_POINT_FOLLOW, me, "head" );
+ }
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDeliverFlag::Update( CTFBot *me, float interval )
+{
+ CCaptureFlag *flag = me->GetFlagToFetch();
+
+ if ( !flag )
+ {
+ return Done( "No flag" );
+ }
+
+ CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() );
+ if ( !carrier || !me->IsSelf( carrier ) )
+ {
+ return Done( "I'm no longer carrying the flag" );
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ // let the bomb carrier use it's buff banners/etc
+ Action< CTFBot > *result = me->OpportunisticallyUseWeaponAbilities();
+ if ( result )
+ {
+ return SuspendFor( result, "Opportunistically using buff item" );
+ }
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ // deliver the flag
+ if ( m_repathTimer.IsElapsed() )
+ {
+ CCaptureZone *zone = me->GetFlagCaptureZone();
+
+ if ( !zone )
+ {
+ return Done( "No flag capture zone exists!" );
+ }
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, zone->WorldSpaceCenter(), cost );
+
+ float flOldTravelDistance = m_flTotalTravelDistance;
+
+ m_flTotalTravelDistance = NavAreaTravelDistance( me->GetLastKnownArea(), TheNavMesh->GetNavArea( zone->WorldSpaceCenter() ), cost );
+
+ if ( flOldTravelDistance != -1.0f && m_flTotalTravelDistance - flOldTravelDistance > 2000.0f )
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bomb_Reset" );
+
+ // Look for players that helped with the reset and send an event
+ CUtlVector<CTFPlayer *> playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ CTFPlayer *pPlayer = playerVector[i];
+ if ( !pPlayer )
+ continue;
+
+ if ( me->m_AchievementData.IsPusherInHistory( pPlayer, 3.f ) )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_reset_by_player" );
+ if ( event )
+ {
+ event->SetInt( "player", pPlayer->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayer, me, 100 );
+ }
+ }
+ }
+
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+ }
+
+ m_path.Update( me );
+
+ if ( UpgradeOverTime( me ) )
+ {
+ return SuspendFor( new CTFBotTaunt, "Taunting for our new upgrade" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotDeliverFlag::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->ClearAttribute( CTFBot::SUPPRESS_FIRE );
+
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ me->m_Shared.ResetRageBuffs();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDeliverFlag::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ if ( tf_mvm_bot_allow_flag_carrier_to_fight.GetBool() )
+ {
+ return ANSWER_UNDEFINED;
+ }
+
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// are we in a hurry?
+QueryResultType CTFBotDeliverFlag::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// is it time to retreat?
+QueryResultType CTFBotDeliverFlag::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotDeliverFlag::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ if ( TFGameRules()->IsMannVsMachineMode() && other && FClassnameIs( other, "func_capturezone" ) )
+ {
+ return TrySuspendFor( new CTFBotMvMDeployBomb, RESULT_CRITICAL, "Delivering the bomb!" );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CTFBotPushToCapturePoint::CTFBotPushToCapturePoint( Action< CTFBot > *nextAction )
+{
+ m_nextAction = nextAction;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPushToCapturePoint::Update( CTFBot *me, float interval )
+{
+ // flag collection and delivery is handled by our parent behavior, ScenarioMonitor
+
+ CCaptureZone *zone = me->GetFlagCaptureZone();
+
+ if ( !zone )
+ {
+ if ( m_nextAction )
+ {
+ return ChangeTo( m_nextAction, "No flag capture zone exists!" );
+ }
+
+ return Done( "No flag capture zone exists!" );
+ }
+
+ Vector toZone = zone->WorldSpaceCenter() - me->GetAbsOrigin();
+ if ( toZone.AsVector2D().IsLengthLessThan( 50.0f ) )
+ {
+ if ( m_nextAction )
+ {
+ return ChangeTo( m_nextAction, "At destination" );
+ }
+
+ return Done( "At destination" );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, zone->WorldSpaceCenter(), cost );
+
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+}
+
+//-----------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPushToCapturePoint::OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea )
+{
+ // does the area we are entering have a prerequisite?
+ if ( newArea && newArea->HasPrerequisite( me ) )
+ {
+ 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();
+}
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h
new file mode 100644
index 0000000..1e8dfdf
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h
@@ -0,0 +1,63 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_deliver_flag.h
+// Take the flag we are holding to its destination
+// Michael Booth, May 2011
+
+#ifndef TF_BOT_DELIVER_FLAG_H
+#define TF_BOT_DELIVER_FLAG_H
+
+#include "Path/NextBotPathFollow.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotDeliverFlag : 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 QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const;
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const;
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const;
+
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+
+ virtual const char *GetName( void ) const { return "DeliverFlag"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ float m_flTotalTravelDistance;
+
+ bool UpgradeOverTime( CTFBot *me );
+ CountdownTimer m_upgradeTimer;
+
+#define DONT_UPGRADE -1
+ int m_upgradeLevel;
+
+ CountdownTimer m_buffPulseTimer;
+};
+
+
+//-----------------------------------------------------------------------------
+class CTFBotPushToCapturePoint : public Action< CTFBot >
+{
+public:
+ CTFBotPushToCapturePoint( Action< CTFBot > *nextAction = NULL );
+ virtual ~CTFBotPushToCapturePoint() { }
+
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual EventDesiredResult< CTFBot > OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea );
+
+ virtual const char *GetName( void ) const { return "PushToCapturePoint"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ Action< CTFBot > *m_nextAction;
+};
+
+
+#endif // TF_BOT_DELIVER_FLAG_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp
new file mode 100644
index 0000000..ca48e19
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp
@@ -0,0 +1,142 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort_flag_carrier.cpp
+// Escort the flag carrier to their destination
+// Michael Booth, May 2011
+
+#include "cbase.h"
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h"
+
+extern ConVar tf_bot_flag_escort_range;
+
+ConVar tf_bot_flag_escort_give_up_range( "tf_bot_flag_escort_give_up_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_flag_escort_max_count( "tf_bot_flag_escort_max_count", "4", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Count the number of TFBots currently engaged in the "EscortFlagCarrier" behavior
+//
+int GetBotEscortCount( int team )
+{
+ int count = 0;
+
+ CUtlVector< CTFPlayer * > livePlayerVector;
+ CollectPlayers( &livePlayerVector, team, COLLECT_ONLY_LIVING_PLAYERS );
+
+ int i;
+ for( i=0; i<livePlayerVector.Count(); ++i )
+ {
+ CTFBot *bot = dynamic_cast< CTFBot * >( livePlayerVector[i] );
+ if ( bot )
+ {
+ Behavior< CTFBot > *behavior = (Behavior< CTFBot > *)bot->GetIntentionInterface()->FirstContainedResponder();
+ if ( behavior )
+ {
+ Action< CTFBot > *action = (Action< CTFBot > *)behavior->FirstContainedResponder();
+
+ while( action && action->GetActiveChildAction() )
+ {
+ action = action->GetActiveChildAction();
+ }
+
+ if ( action && action->IsNamed( "EscortFlagCarrier" ) )
+ {
+ ++count;
+ }
+ }
+ }
+ }
+
+ return count;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscortFlagCarrier::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscortFlagCarrier::Update( CTFBot *me, float interval )
+{
+ CCaptureFlag *flag = me->GetFlagToFetch();
+
+ if ( !flag )
+ {
+ return Done( "No flag" );
+ }
+
+ CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() );
+ if ( !carrier )
+ {
+ return Done( "Flag was dropped" );
+ }
+ else if ( me->IsSelf( carrier ) )
+ {
+ return Done( "I picked up the flag!" );
+ }
+
+ // stay near the carrier
+ if ( me->IsRangeGreaterThan( carrier, tf_bot_flag_escort_give_up_range.GetFloat() ) )
+ {
+ if ( me->SelectRandomReachableEnemy() )
+ {
+ // too far away - give up
+ return ChangeTo( new CTFBotAttackFlagDefenders, "Too far from flag carrier - attack defenders!" );
+ }
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon && myWeapon->IsMeleeWeapon() )
+ {
+ if ( me->IsRangeLessThan( carrier, tf_bot_flag_escort_range.GetFloat() ) && me->IsLineOfSightClear( carrier ) )
+ {
+ ActionResult< CTFBot > result = m_meleeAttackAction.Update( me, interval );
+
+ if ( result.IsContinue() )
+ {
+ // we have a melee target, and we're still reasonably close to the flag carrier
+ return Continue();
+ }
+ }
+ }
+
+ if ( me->IsRangeGreaterThan( carrier, 0.5f * tf_bot_flag_escort_range.GetFloat() ) )
+ {
+ // move near carrier
+ if ( m_repathTimer.IsElapsed() )
+ {
+ if ( GetBotEscortCount( me->GetTeamNumber() ) > tf_bot_flag_escort_max_count.GetInt() )
+ {
+ if ( me->SelectRandomReachableEnemy() )
+ {
+ return Done( "Too many flag escorts - giving up" );
+ }
+ }
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, carrier, cost );
+
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h
new file mode 100644
index 0000000..f8e5f9d
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h
@@ -0,0 +1,31 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort_flag_carrier.h
+// Escort the flag carrier to their destination
+// Michael Booth, May 2011
+
+#ifndef TF_BOT_ESCORT_FLAG_CARRIER_H
+#define TF_BOT_ESCORT_FLAG_CARRIER_H
+
+
+#include "Path/NextBotPathFollow.h"
+#include "bot/behavior/tf_bot_melee_attack.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotEscortFlagCarrier : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "EscortFlagCarrier"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CTFBotMeleeAttack m_meleeAttackAction;
+};
+
+
+#endif // TF_BOT_ESCORT_FLAG_CARRIER_H
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp
new file mode 100644
index 0000000..84e02ce
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp
@@ -0,0 +1,128 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_fetch_flag.cpp
+// Go get the flag!
+// Michael Booth, May 2011
+
+#include "cbase.h"
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h"
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotFetchFlag::CTFBotFetchFlag( bool isTemporary )
+{
+ m_isTemporary = isTemporary;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotFetchFlag::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotFetchFlag::Update( CTFBot *me, float interval )
+{
+ CCaptureFlag *flag = me->GetFlagToFetch();
+
+ if ( !flag )
+ {
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ return SuspendFor( new CTFBotAttackFlagDefenders, "Flag flag exists - Attacking the enemy flag defenders" );
+ }
+
+ return Done( "No flag" );
+ }
+
+ // uncloak so we can attack
+ if ( me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() && flag->IsHome() )
+ {
+ if ( gpGlobals->curtime - me->GetSpawnTime() < 1.0f && me->GetTeamNumber() != TEAM_SPECTATOR )
+ {
+ // we just spawned - give us the flag
+ flag->PickUp( me, true );
+ }
+ else
+ {
+ if ( m_isTemporary )
+ {
+ return Done( "Flag unreachable" );
+ }
+
+ // flag is at home and we're out in the world - can't reach it
+ return SuspendFor( new CTFBotAttackFlagDefenders, "Flag unreachable at home - Attacking the enemy flag defenders" );
+ }
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat )
+ {
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() );
+ if ( carrier )
+ {
+ if ( m_isTemporary )
+ {
+ return Done( "Someone else picked up the flag" );
+ }
+
+ // NOTE: if I've picked up the flag, the ScenarioMonitor will handle it
+ return SuspendFor( new CTFBotAttackFlagDefenders, "Someone has the flag - attacking the enemy defenders" );
+ }
+
+ // go pick up the flag
+ if ( m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f;
+ if ( m_path.Compute( me, flag->WorldSpaceCenter(), cost, maxPathLength ) == false )
+ {
+ if ( flag->IsDropped() )
+ {
+ // flag is unreachable - attack for awhile and hope someone else can dislodge it
+ return SuspendFor( new CTFBotAttackFlagDefenders( RandomFloat( 5.0f, 10.0f ) ), "Flag unreachable - Attacking" );
+
+ // just give it to me
+ // flag->PickUp( me, true );
+ }
+ }
+
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// are we in a hurry?
+QueryResultType CTFBotFetchFlag::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// is it time to retreat?
+QueryResultType CTFBotFetchFlag::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h
new file mode 100644
index 0000000..5ca8818
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h
@@ -0,0 +1,35 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_fetch_flag.h
+// Go get the flag!
+// Michael Booth, May 2011
+
+#ifndef TF_BOT_FETCH_FLAG_H
+#define TF_BOT_FETCH_FLAG_H
+
+#include "Path/NextBotPathFollow.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotFetchFlag : public Action< CTFBot >
+{
+public:
+ #define TEMPORARY_FLAG_FETCH true
+ CTFBotFetchFlag( bool isTemporary = false );
+ virtual ~CTFBotFetchFlag() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const;
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const;
+
+ virtual const char *GetName( void ) const { return "FetchFlag"; };
+
+private:
+ bool m_isTemporary;
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+};
+
+
+#endif // TF_BOT_FETCH_FLAG_H
diff --git a/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp
new file mode 100644
index 0000000..fb93882
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp
@@ -0,0 +1,240 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_creep_wave.cpp
+// Move in a "creep wave" to the next available control point to capture it
+// Michael Booth, August 2010
+
+#include "cbase.h"
+
+#ifdef TF_CREEP_MODE
+
+#include "team.h"
+#include "team_control_point_master.h"
+#include "bot/tf_bot_manager.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h"
+
+ConVar tf_creep_aggro_range( "tf_creep_aggro_range", "250" );
+ConVar tf_creep_give_up_range( "tf_creep_give_up_range", "300" );
+
+
+CTFPlayer *FindNearestEnemy( CTFBot *me, float maxRange )
+{
+ CBasePlayer *closest = NULL;
+ float closeRangeSq = maxRange * maxRange;
+
+ 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 ( me->IsFriend( player ) )
+ continue;
+
+ if ( !player->IsAlive() )
+ continue;
+
+ float rangeSq = me->GetRangeSquaredTo( player );
+ if ( rangeSq < closeRangeSq )
+ {
+ if ( me->IsLineOfFireClear( player ) )
+ {
+ closeRangeSq = rangeSq;
+ closest = player;
+ }
+ }
+ }
+
+ return (CTFPlayer *)closest;
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCreepWave::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ me->StopLookingAroundForEnemies();
+ m_stuckTimer.Invalidate();
+
+ me->GetPlayerClass()->SetCustomModel( "models/bots/bot_heavy.mdl", USE_CLASS_ANIMATIONS );
+ me->UpdateModel();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCreepWave::Update( CTFBot *me, float interval )
+{
+ if ( !me->IsAlive() && me->StateGet() != TF_STATE_DYING )
+ {
+ // remove dead creeps for now
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) );
+ }
+
+ CBaseCombatWeapon *melee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( melee )
+ {
+ me->Weapon_Switch( melee );
+ }
+
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( me, &captureVector );
+
+ if ( captureVector.Count() == 0 )
+ {
+ return Continue();
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, captureVector[0]->WorldSpaceCenter(), cost );
+ }
+
+ m_path.Update( me );
+
+ if ( m_stuckTimer.HasStarted() )
+ {
+ // juke and dodge to escape stuck situation
+ switch( RandomInt( 0, 3 ) )
+ {
+ case 0: me->PressBackwardButton(); break;
+ case 1: me->PressForwardButton(); break;
+ case 2: me->PressLeftButton(); break;
+ case 3: me->PressRightButton(); break;
+ }
+ }
+
+ CTFPlayer *enemy = FindNearestEnemy( me, tf_creep_aggro_range.GetFloat() );
+ if ( enemy )
+ {
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_INCOMING );
+ return SuspendFor( new CTFBotCreepAttack( enemy ), "Attacking nearby enemy" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCreepWave::OnKilled( CTFBot *me, const CTakeDamageInfo &info )
+{
+ if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) )
+ {
+ TheTFBots().OnCreepKilled( ToTFPlayer( info.GetAttacker() ) );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCreepWave::OnStuck( CTFBot *me )
+{
+ m_stuckTimer.Start();
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotCreepWave::OnUnStuck( CTFBot *me )
+{
+ m_stuckTimer.Invalidate();
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CTFBotCreepAttack::CTFBotCreepAttack( CTFPlayer *victim )
+{
+ m_victim = victim;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCreepAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCreepAttack::Update( CTFBot *me, float interval )
+{
+ if ( !me->IsAlive() && me->StateGet() != TF_STATE_DYING )
+ {
+ // remove dead creeps for now
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) );
+ }
+
+ if ( m_victim.Get() == NULL || !m_victim->IsAlive() )
+ {
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS );
+ return Done( "Killed victim" );
+ }
+
+ if ( me->IsRangeGreaterThan( m_victim, tf_creep_give_up_range.GetFloat() ) ||
+ !me->IsLineOfFireClear( m_victim ) )
+ {
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_NEGATIVE );
+ return Done( "Lost victim" );
+ }
+
+ CTFPlayer *newVictim = FindNearestEnemy( me, tf_creep_aggro_range.GetFloat() );
+ if ( newVictim )
+ {
+ float newRangeSq = me->GetRangeSquaredTo( newVictim );
+ float victimRangeSq = me->GetRangeSquaredTo( m_victim );
+
+ if ( newRangeSq < victimRangeSq )
+ {
+ // switch to closer target
+ m_victim = newVictim;
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY );
+ }
+ }
+
+ CBaseCombatWeapon *melee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( melee )
+ {
+ me->Weapon_Switch( melee );
+ }
+
+ me->GetBodyInterface()->AimHeadTowards( m_victim, IBody::CRITICAL, 0.2f, NULL, "Looking at enemy" );
+
+ // swing weapon
+ me->PressFireButton();
+
+ // beeline towards our victim
+ const float combatRange = 40.0f;
+ if ( me->IsRangeGreaterThan( m_victim, combatRange ) )
+ {
+ me->GetLocomotionInterface()->Approach( m_victim->GetAbsOrigin() );
+ }
+ else
+ {
+ // juke and dodge to avoid interpenetration
+ switch( RandomInt( 0, 3 ) )
+ {
+ case 0: me->PressBackwardButton(); break;
+ case 1: me->PressForwardButton(); break;
+ case 2: me->PressLeftButton(); break;
+ case 3: me->PressRightButton(); break;
+ }
+ }
+
+ return Continue();
+}
+
+
+
+
+#endif // TF_CREEP_MODE \ No newline at end of file
diff --git a/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h
new file mode 100644
index 0000000..6dedafe
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h
@@ -0,0 +1,57 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_creep_wave.h
+// Move in a "creep wave" to the next available control point to capture it
+// Michael Booth, August 2010
+
+#ifndef TF_BOT_CREEP_WAVE_H
+#define TF_BOT_CREEP_WAVE_H
+
+#ifdef TF_CREEP_MODE
+
+#include "Path/NextBotPathFollow.h"
+#include "Path/NextBotChasePath.h"
+
+
+CTFBot *FindNearestEnemyCreep( CTFBot *me );
+
+
+//-----------------------------------------------------------------------------
+class CTFBotCreepWave : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnUnStuck( CTFBot *me );
+
+ virtual const char *GetName( void ) const { return "CreepWave"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ IntervalTimer m_stuckTimer;
+};
+
+
+
+//-----------------------------------------------------------------------------
+class CTFBotCreepAttack : public Action< CTFBot >
+{
+public:
+ CTFBotCreepAttack( CTFPlayer *victim );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "CreepAttack"; };
+
+private:
+ CHandle< CTFPlayer > m_victim;
+};
+
+
+#endif // TF_CREEP_MODE
+
+#endif // TF_BOT_CREEP_WAVE_H
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp
new file mode 100644
index 0000000..3f34071
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp
@@ -0,0 +1,151 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_block.cpp
+// Prevent the other team from moving the cart
+// Michael Booth, April 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "team_train_watcher.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_block.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadBlock::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_path.Invalidate();
+
+ m_giveUpTimer.Start( RandomFloat( 3.0f, 5.0f ) );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadBlock::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( m_giveUpTimer.IsElapsed() )
+ {
+ return Done( "Been blocking long enough" );
+ }
+
+ // move toward the point, periodically repathing to account for changing situation
+ if ( m_repathTimer.IsElapsed() )
+ {
+ VPROF_BUDGET( "CTFBotPayloadBlock::Update( repath )", "NextBot" );
+
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
+ if ( !trainWatcher )
+ {
+ return Done( "Train Watcher is missing" );
+ }
+
+ CBaseEntity *cart = trainWatcher->GetTrainEntity();
+ if ( !cart )
+ {
+ return Done( "Cart is missing" );
+ }
+
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, cart->WorldSpaceCenter(), cost );
+ m_repathTimer.Start( RandomFloat( 0.2f, 0.4f ) );
+ }
+
+ // move towards next capture point
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadBlock::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ VPROF_BUDGET( "CTFBotPayloadBlock::OnResume", "NextBot" );
+
+ m_repathTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnStuck( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotPayloadBlock::OnStuck", "NextBot" );
+
+ m_repathTimer.Invalidate();
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ VPROF_BUDGET( "CTFBotPayloadBlock::OnMoveToFailure", "NextBot" );
+
+ m_repathTimer.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ return TryToSustain( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryToSustain( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryToSustain( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadBlock::ShouldRetreat( const INextBot *bot ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadBlock::ShouldHurry( const INextBot *bot ) const
+{
+ // hurry and block the cart - don't retreat, etc
+ return ANSWER_YES;
+}
+
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h
new file mode 100644
index 0000000..9b934f2
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h
@@ -0,0 +1,38 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_block.h
+// Prevent the other team from moving the cart
+// Michael Booth, April 2010
+
+#ifndef TF_BOT_PAYLOAD_BLOCK_H
+#define TF_BOT_PAYLOAD_BLOCK_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotPayloadBlock : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "PayloadBlock"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CountdownTimer m_giveUpTimer;
+};
+
+#endif // TF_BOT_PAYLOAD_BLOCK_H
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp
new file mode 100644
index 0000000..a09d186
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp
@@ -0,0 +1,283 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_guard.cpp
+// Guard the payload and keep the attackers from getting near it
+// Michael Booth, April 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "team_train_watcher.h"
+#include "trigger_area_capture.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_guard.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_block.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_payload_guard_range( "tf_bot_payload_guard_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_debug_payload_guard_vantage_points( "tf_bot_debug_payload_guard_vantage_points", 0, FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadGuard::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_path.Invalidate();
+
+ m_vantagePoint = me->GetAbsOrigin();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadGuard::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
+ if ( !trainWatcher )
+ {
+ return Continue();
+ }
+
+ CBaseEntity *cart = trainWatcher->GetTrainEntity();
+ if ( !cart )
+ {
+ return Continue();
+ }
+
+ if ( !trainWatcher->IsDisabled() && trainWatcher->GetCapturerCount() > 0 )
+ {
+ // the cart is being pushed ahead - block it
+ if ( !m_moveToBlockTimer.HasStarted() )
+ {
+ m_moveToBlockTimer.Start( RandomFloat( 0.5f, 3.0f ) );
+ }
+ }
+
+ if ( m_moveToBlockTimer.HasStarted() && m_moveToBlockTimer.IsElapsed() )
+ {
+ m_moveToBlockTimer.Invalidate();
+
+ if ( trainWatcher->GetCapturerCount() >= 0 )
+ {
+ // the cart is not yet blocked - move to block it!
+ return SuspendFor( new CTFBotPayloadBlock, "Moving to block the cart's forward motion" );
+ }
+ }
+
+ bool isMovingToVantagePoint = ( me->GetAbsOrigin() - m_vantagePoint ).AsVector2D().IsLengthGreaterThan( 25.0f );
+
+ if ( isMovingToVantagePoint )
+ {
+ // en route, don't change the point
+ m_vantagePointTimer.Start( RandomFloat( 3.0f, 15.0f ) );
+ }
+
+ if ( !me->IsLineOfFireClear( cart ) )
+ {
+ // cart is no longer visible from this area, find another one
+ m_vantagePointTimer.Invalidate();
+ }
+
+ if ( m_vantagePointTimer.IsElapsed() )
+ {
+ // find a new vantage point
+ m_vantagePoint = FindVantagePoint( me, cart );
+ m_repathTimer.Invalidate();
+ isMovingToVantagePoint = true;
+ }
+
+ if ( isMovingToVantagePoint )
+ {
+ // update our path periodically
+ if ( m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, m_vantagePoint, cost );
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+ }
+
+ // move towards our vantage point
+ m_path.Update( me );
+ }
+ else
+ {
+ // at vantage point
+ if ( CTFBotPrepareStickybombTrap::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadGuard::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ VPROF_BUDGET( "CTFBotPayloadGuard::OnResume", "NextBot" );
+
+ m_vantagePointTimer.Invalidate();
+ m_repathTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnStuck( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotPayloadGuard::OnStuck", "NextBot" );
+
+ m_repathTimer.Invalidate();
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ VPROF_BUDGET( "CTFBotPayloadGuard::OnMoveToFailure", "NextBot" );
+
+ m_vantagePointTimer.Invalidate();
+ m_repathTimer.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Invoked when cart is being pushed
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Invoked when cart hits a checkpoint
+EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadGuard::ShouldRetreat( const INextBot *bot ) const
+{
+ CTFBot *me = ToTFBot( bot->GetEntity() );
+
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
+ if ( trainWatcher && trainWatcher->IsTrainNearCheckpoint() )
+ {
+ // don't retreat if the cart is almost at the next checkpoint
+ return ANSWER_NO;
+ }
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadGuard::ShouldHurry( const INextBot *bot ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CCollectPayloadGuardVantagePoints : public ISearchSurroundingAreasFunctor
+{
+public:
+ CCollectPayloadGuardVantagePoints( CTFBot *me, CBaseEntity *cart )
+ {
+ m_me = me;
+ m_cart = cart;
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ // TODO: only use areas that are at/farther along than the payload
+
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE );
+
+ const int tryCount = 3;
+
+ for( int i=0; i<tryCount; ++i )
+ {
+ Vector spot = area->GetRandomPoint();
+ Vector eyeSpot = Vector( spot.x, spot.y, spot.z + HumanEyeHeight );
+
+ UTIL_TraceLine( eyeSpot, m_cart->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, &filter, &trace );
+
+ if ( !trace.DidHit() || trace.m_pEnt == m_cart )
+ {
+ m_vantagePointVector.AddToTail( spot );
+
+ if ( tf_bot_debug_payload_guard_vantage_points.GetBool() )
+ {
+ NDebugOverlay::Cross3D( spot, 5.0f, 255, 0, 255, true, 120.0f );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ CTFBot *m_me;
+ CBaseEntity *m_cart;
+ CUtlVector< Vector > m_vantagePointVector;
+};
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Find a tactically advantageous area where we can see the payload
+//
+Vector CTFBotPayloadGuard::FindVantagePoint( CTFBot *me, CBaseEntity *cart )
+{
+ CTFNavArea *cartArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( cart );
+
+ CCollectPayloadGuardVantagePoints collect( me, cart );
+ SearchSurroundingAreas( cartArea, collect, tf_bot_payload_guard_range.GetFloat() );
+
+ if ( collect.m_vantagePointVector.Count() == 0 )
+ return cart->WorldSpaceCenter();
+
+ int which = RandomInt( 0, collect.m_vantagePointVector.Count()-1 );
+ return collect.m_vantagePointVector[ which ];
+}
+
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h
new file mode 100644
index 0000000..2d1b03d
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h
@@ -0,0 +1,43 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_guard.h
+// Guard the payload and keep the attackers from getting near it
+// Michael Booth, April 2010
+
+#ifndef TF_BOT_PAYLOAD_GUARD_H
+#define TF_BOT_PAYLOAD_GUARD_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotPayloadGuard : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "PayloadGuard"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ Vector m_vantagePoint;
+ CountdownTimer m_vantagePointTimer;
+ Vector FindVantagePoint( CTFBot *me, CBaseEntity *cart );
+
+ CountdownTimer m_moveToBlockTimer;
+
+};
+
+#endif // TF_BOT_PAYLOAD_GUARD_H
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp
new file mode 100644
index 0000000..79fe667
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp
@@ -0,0 +1,155 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_push.cpp
+// Push the cartTrigger to the goal
+// Michael Booth, April 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "team_train_watcher.h"
+#include "trigger_area_capture.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_push.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+ConVar tf_bot_cart_push_radius( "tf_bot_cart_push_radius", "60", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadPush::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+ m_path.Invalidate();
+
+ m_hideAngle = 180.0f;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadPush::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( TFGameRules()->InSetup() )
+ {
+ // wait until the gates open, then path
+ m_path.Invalidate();
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ return Continue();
+ }
+
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() );
+ if ( !trainWatcher )
+ {
+ return Continue();
+ }
+
+ CBaseEntity *cart = trainWatcher->GetTrainEntity();
+ if ( !cart )
+ {
+ return Continue();
+ }
+
+
+ // move toward the point, periodically repathing to account for changing situation
+ if ( m_repathTimer.IsElapsed() )
+ {
+ VPROF_BUDGET( "CTFBotPayloadPush::Update( repath )", "NextBot" );
+
+ Vector cartForward;
+ cart->GetVectors( &cartForward, NULL, NULL );
+
+ // default push position is behind cart
+ Vector pushPos = cart->WorldSpaceCenter() - cartForward * tf_bot_cart_push_radius.GetFloat();
+
+ // try to hide from enemies on other side of cart
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat )
+ {
+ Vector enemyToCart = cart->WorldSpaceCenter() - threat->GetLastKnownPosition();
+ enemyToCart.z = 0.0f;
+ enemyToCart.NormalizeInPlace();
+
+ pushPos = cart->WorldSpaceCenter() + tf_bot_cart_push_radius.GetFloat() * enemyToCart;
+ }
+
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_path.Compute( me, pushPos, cost );
+
+ m_repathTimer.Start( RandomFloat( 0.2f, 0.4f ) );
+ }
+
+ // push the cartTrigger
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotPayloadPush::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ VPROF_BUDGET( "CTFBotPayloadPush::OnResume", "NextBot" );
+
+ m_repathTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadPush::OnStuck( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotPayloadPush::OnStuck", "NextBot" );
+
+ m_repathTimer.Invalidate();
+ me->GetLocomotionInterface()->ClearStuckStatus();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadPush::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotPayloadPush::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ VPROF_BUDGET( "CTFBotPayloadPush::OnMoveToFailure", "NextBot" );
+
+ m_repathTimer.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadPush::ShouldRetreat( const INextBot *bot ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotPayloadPush::ShouldHurry( const INextBot *bot ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h
new file mode 100644
index 0000000..4499c25
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h
@@ -0,0 +1,33 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_payload_push.h
+// Push the cart to the goal
+// Michael Booth, April 2010
+
+#ifndef TF_BOT_PAYLOAD_PUSH_H
+#define TF_BOT_PAYLOAD_PUSH_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotPayloadPush : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "PayloadPush"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ float m_hideAngle;
+};
+
+#endif // TF_BOT_PAYLOAD_PUSH_H
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp
new file mode 100644
index 0000000..1d78ff3
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp
@@ -0,0 +1,217 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_raid_companion.cpp
+// Teammate bots for Raid mode
+// Michael Booth, October 2009
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "team_control_point_master.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/scenario/raid/tf_bot_companion.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_move_to_vantage_point.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/sniper/tf_bot_sniper_lurk.h"
+
+#include "bot/map_entities/tf_bot_generator.h" // action point
+
+ConVar tf_raid_companion_follow_range( "tf_raid_companion_follow_range", "150", FCVAR_CHEAT );
+ConVar tf_raid_companion_allow_bot_leader( "tf_raid_companion_allow_bot_leader", "0", FCVAR_CHEAT );
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCompanion::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFPlayer *CTFBotCompanion::GetLeader( void )
+{
+ CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE );
+ CTFPlayer *leader = NULL;
+ float leaderSpeed = FLT_MAX;
+
+ for( int i=0; i<raidingTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i);
+
+ if ( player->IsBot() && !tf_raid_companion_allow_bot_leader.GetBool() )
+ continue;
+
+/*
+ if ( player->IsPlayerClass( TF_CLASS_ENGINEER ) ||
+ player->IsPlayerClass( TF_CLASS_SNIPER ) ||
+ player->IsPlayerClass( TF_CLASS_MEDIC ) )
+ continue;
+*/
+
+ if ( player->IsAlive() )
+ {
+ float speed = player->GetPlayerClass()->GetMaxSpeed();
+
+ if ( speed < leaderSpeed )
+ {
+ leader = player;
+ leaderSpeed = speed;
+ }
+ }
+ }
+
+ return leader;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCompanion::Update( CTFBot *me, float interval )
+{
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ const CKnownEntity *patient = me->GetVisionInterface()->GetClosestKnown( me->GetTeamNumber() );
+ if ( patient )
+ {
+ return SuspendFor( new CTFBotMedicHeal );
+ }
+ }
+
+ CTFPlayer *leader = GetLeader();
+ if ( !leader )
+ return Continue();
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( me->IsSelf( leader ) )
+ {
+ const float engageRange = 500.0f;
+ if ( threat && threat->IsVisibleRecently() && me->IsRangeLessThan( threat->GetEntity(), engageRange ) )
+ {
+ // stop pushing ahead and kill nearby threats
+ return SuspendFor( new CTFBotAttack, "Attacking nearby threats" );
+ }
+
+ // head toward next capture point
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point )
+ {
+ m_path.Update( me, point, cost );
+ }
+ }
+ else
+ {
+ if ( ( !threat || threat->GetTimeSinceLastSeen() > 3.0f ) && leader->GetTimeSinceLastInjury() < 1.0f )
+ {
+ // we don't see anything, but the leader is under attack - find a better vantage point
+ const float nearRange = 1000.0f;
+ return SuspendFor( new CTFBotMoveToVantagePoint( nearRange ), "Moving to where I can see the enemy" );
+ }
+
+ if ( leader && me->IsDistanceBetweenGreaterThan( leader, tf_raid_companion_follow_range.GetFloat() ) )
+ {
+ m_path.Update( me, leader, cost );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotCompanion::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ return Continue();
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGuardian::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGuardian::Update( CTFBot *me, float interval )
+{
+ if ( me->GetActionPoint() )
+ {
+ const float atHomeRange = 35.0f; // 25.0f;
+ const Vector &home = me->GetActionPoint()->GetAbsOrigin();
+
+ if ( me->IsRangeGreaterThan( home, atHomeRange ) )
+ {
+ if ( m_repathTimer.IsElapsed() && !m_path.IsValid() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, home, cost );
+ }
+
+ // move home
+ m_path.Update( me );
+
+ return Continue();
+ }
+ }
+
+ // at home
+ m_path.Invalidate();
+ me->SetHomeArea( me->GetLastKnownArea() );
+
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return SuspendFor( new CTFBotEngineerBuild );
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ return SuspendFor( new CTFBotSniperLurk );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGuardian::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardian::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ return TryContinue( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardian::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ m_path.Invalidate();
+ return TryContinue( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardian::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_path.Invalidate();
+ return TryContinue( RESULT_IMPORTANT );
+}
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h
new file mode 100644
index 0000000..e379b0b
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h
@@ -0,0 +1,57 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_companion.h
+// Teammate bots for Raid mode
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_COMPANION_H
+#define TF_BOT_COMPANION_H
+
+#ifdef TF_RAID_MODE
+
+#include "Path/NextBotPathFollow.h"
+#include "Path/NextBotChasePath.h"
+
+//
+// Friendly teammate bots
+//
+class CTFBotCompanion : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual const char *GetName( void ) const { return "Companion"; };
+
+private:
+ ChasePath m_path;
+ CTFPlayer *GetLeader( void );
+};
+
+
+//
+// Friendly defenders of the base
+//
+class CTFBotGuardian : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual const char *GetName( void ) const { return "Guardian"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+};
+
+#endif // TF_RAID_MODE
+
+#endif // TF_BOT_COMPANION_H
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp
new file mode 100644
index 0000000..32759d9
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp
@@ -0,0 +1,261 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_guard_area.cpp
+// Defend an area against intruders
+// Michael Booth, October 2009
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "team_control_point_master.h"
+#include "econ_entity_creation.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/sniper/tf_bot_sniper_attack.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/scenario/raid/tf_bot_wander.h"
+#include "bot/behavior/scenario/raid/tf_bot_guard_area.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+ConVar tf_bot_guard_aggro_range( "tf_bot_guard_aggro_range", "750", FCVAR_CHEAT );
+//ConVar tf_bot_guard_give_up_range( "tf_bot_guard_give_up_range", "1250", FCVAR_CHEAT );
+
+ConVar tf_raid_special_vocalize_min_interval( "tf_raid_special_vocalize_min_interval", "10", FCVAR_CHEAT );
+ConVar tf_raid_special_vocalize_max_interval( "tf_raid_special_vocalize_max_interval", "15", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGuardArea::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_chasePath.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+/*
+ // give this guy a hat!
+ randomitemcriteria_t criteria;
+ criteria.iItemLevel = AE_USE_SCRIPT_VALUE;
+ criteria.iItemQuality = AE_USE_SCRIPT_VALUE;
+ criteria.vecAbsOrigin = me->GetAbsOrigin();
+ criteria.vecAbsAngles = vec3_angle;
+
+ switch( me->GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_SCOUT: criteria.pszItemName = "Scout Hat 1"; break;
+ case TF_CLASS_SNIPER: criteria.pszItemName = "Sniper Hat 1"; break;
+ case TF_CLASS_SOLDIER: criteria.pszItemName = "Soldier Pot Hat"; break;
+ case TF_CLASS_DEMOMAN: criteria.pszItemName = "Demo Top Hat"; break;
+ case TF_CLASS_MEDIC: criteria.pszItemName = "Medic Hat 1"; break;
+ case TF_CLASS_HEAVYWEAPONS: criteria.pszItemName = "Heavy Ushanka Hat"; break;
+ case TF_CLASS_PYRO: criteria.pszItemName = "Pyro Chicken Hat"; break;
+ case TF_CLASS_SPY: criteria.pszItemName = "Spy Derby Hat"; break;
+ case TF_CLASS_ENGINEER: criteria.pszItemName = "Engineer Hat 1"; break;
+ default: criteria.pszItemName = ""; break;
+ }
+
+ CBaseEntity *hat = ItemGeneration()->GenerateRandomItem( &criteria );
+ if ( hat )
+ {
+ // Fake global id
+ static int s_nFakeID = 1;
+ static_cast< CEconEntity * >( hat )->GetAttributeContainer()->GetItem()->SetItemID( s_nFakeID++ );
+
+ DispatchSpawn( hat );
+ static_cast< CEconEntity * >( hat )->GetAttributeContainer()->GetItem()->GenerateAttributes();
+ static_cast< CEconEntity * >( hat )->GiveTo( me );
+ }
+ else
+ {
+ Msg( "Failed to create hat\n" );
+ }
+*/
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CFindVantagePoint : public ISearchSurroundingAreasFunctor
+{
+public:
+ CFindVantagePoint( void )
+ {
+ m_vantageArea = NULL;
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ if ( travelDistanceSoFar > 2000.0f )
+ return false;
+
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE );
+ for( int i=0; i<raidingTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i);
+
+ if ( !player->IsAlive() || !player->GetLastKnownArea() )
+ continue;
+
+ CTFNavArea *playerArea = (CTFNavArea *)player->GetLastKnownArea();
+ if ( playerArea->IsCompletelyVisible( area ) )
+ {
+ // nearby area from which we can see the enemy team
+ m_vantageArea = area;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ CTFNavArea *m_vantageArea;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGuardArea::Update( CTFBot *me, float interval )
+{
+ // emit vocalizations to warn players we're in the area
+ if ( m_vocalizeTimer.IsElapsed() )
+ {
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_special_vocalize_min_interval.GetFloat(), tf_raid_special_vocalize_max_interval.GetFloat() ) );
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS );
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return SuspendFor( new CTFBotMedicHeal );
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return SuspendFor( new CTFBotEngineerBuild );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ m_pathToVantageArea.Invalidate();
+
+ CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea();
+ CTFNavArea *threatArea = (CTFNavArea *)threat->GetLastKnownArea();
+ if ( myArea && threatArea )
+ {
+ if ( threatArea->GetIncursionDistance( TF_TEAM_BLUE ) < myArea->GetIncursionDistance( TF_TEAM_BLUE ) )
+ {
+ if ( me->IsRangeGreaterThan( threat->GetLastKnownPosition(), tf_bot_guard_aggro_range.GetFloat() ) )
+ {
+ // threat is far off and hasn't reached us yet - hide until they are closer
+ return SuspendFor( new CTFBotRetreatToCover, "Hiding until threat gets closer" );
+ }
+ }
+ }
+
+ // attack!
+ return SuspendFor( new CTFBotAttack, "Attacking nearby threat" );
+ }
+ else
+ {
+ // no enemy is visible
+ Vector moveTo = me->GetAbsOrigin();
+
+ // if point is being captured, move to it
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point && point->LastContestedAt() > 0.0f && ( gpGlobals->curtime - point->LastContestedAt() ) < 5.0f )
+ {
+ // the point is, or was very recently, contested - defend it!
+ moveTo = point->GetAbsOrigin();
+ }
+ else if ( me->GetHomeArea() )
+ {
+ // no enemy is visible - return to our home position
+ moveTo = me->GetHomeArea()->GetCenter();
+ }
+
+ if ( !m_pathToPoint.IsValid() || m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_pathToPoint.Compute( me, moveTo, cost );
+ m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) );
+ }
+
+ if ( ( me->GetAbsOrigin() - moveTo ).IsLengthGreaterThan( 25.0f ) )
+ {
+ m_pathToPoint.Update( me );
+ }
+
+ if ( me->GetHomeArea() == me->GetLastKnownArea() )
+ {
+ // at home
+ if ( CTFBotPrepareStickybombTrap::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" );
+ }
+ }
+
+/*
+ // no enemy is visible - move to where we can see them
+ if ( !m_pathToVantageArea.IsValid() )
+ {
+ CTFNavArea *vantageArea = me->FindVantagePoint();
+ if ( vantageArea )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_pathToVantageArea.Compute( me, vantageArea->GetCenter(), cost );
+ }
+ }
+
+ m_pathToVantageArea.Update( me );
+*/
+ }
+
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardArea::OnStuck( CTFBot *me )
+{
+ m_chasePath.Invalidate();
+ m_pathToPoint.Invalidate();
+ m_pathToVantageArea.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardArea::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardArea::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotGuardArea::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGuardArea::OnCommandApproach( CTFBot *me, const Vector &pos, float range )
+{
+ return TryContinue();
+}
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h
new file mode 100644
index 0000000..891dda7
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h
@@ -0,0 +1,39 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_guard_area.h
+// Defend an area against intruders
+// Michael Booth, October 2009
+
+#ifdef TF_RAID_MODE
+
+#ifndef TF_BOT_GUARD_AREA_H
+#define TF_BOT_GUARD_AREA_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotGuardArea : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+
+ virtual EventDesiredResult< CTFBot > OnCommandApproach( CTFBot *me, const Vector &pos, float range );
+
+ virtual const char *GetName( void ) const { return "GuardArea"; };
+
+private:
+ ChasePath m_chasePath;
+ PathFollower m_pathToPoint;
+ PathFollower m_pathToVantageArea;
+ CountdownTimer m_vocalizeTimer;
+ CountdownTimer m_repathTimer;
+};
+
+#endif // TF_RAID_MODE
+
+#endif // TF_BOT_GUARD_AREA_H
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp
new file mode 100644
index 0000000..f80117d
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp
@@ -0,0 +1,164 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mob_rush.cpp
+// A member of a rushing mob of melee attackers
+// Michael Booth, October 2009
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_taunt.h"
+#include "bot/behavior/scenario/raid/tf_bot_mob_rush.h"
+
+
+ConVar tf_bot_taunt_range( "tf_bot_taunt_range", "100", FCVAR_CHEAT );
+ConVar tf_raid_mob_rush_vocalize_min_interval( "tf_raid_mob_rush_vocalize_min_interval", "5", FCVAR_CHEAT );
+ConVar tf_raid_mob_rush_vocalize_max_interval( "tf_raid_mob_rush_vocalize_max_interval", "8", FCVAR_CHEAT );
+ConVar tf_raid_mob_avoid_range( "tf_raid_mob_avoid_range", "100", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CTFBotMobRush::CTFBotMobRush( CTFPlayer *victim, float reactionTime )
+{
+ m_victim = victim;
+
+ // this isn't strictly correct - we shouldn't start the timer until OnStart
+ m_reactionTimer.Start( reactionTime );
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMobRush::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_mob_rush_vocalize_min_interval.GetFloat(), tf_raid_mob_rush_vocalize_max_interval.GetFloat() ) );
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMobRush::Update( CTFBot *me, float interval )
+{
+ // mobs use only their melee weapons
+ CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( meleeWeapon )
+ {
+ me->Weapon_Switch( meleeWeapon );
+ }
+
+
+ if ( m_victim == NULL )
+ {
+ return Done( "No victim" );
+ }
+
+ me->GetBodyInterface()->AimHeadTowards( m_victim, IBody::CRITICAL, 1.0f, NULL, "Looking at our melee target" );
+
+ if ( m_reactionTimer.HasStarted() )
+ {
+ if ( m_reactionTimer.IsElapsed() )
+ {
+ // snap out of it!
+ me->DoAnimationEvent( PLAYERANIMEVENT_VOICE_COMMAND_GESTURE, ACT_MP_GESTURE_VC_FINGERPOINT_PRIMARY );
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY );
+ m_reactionTimer.Invalidate();
+ }
+ else
+ {
+ // wait for reaction time to elapse
+ return Continue();
+ }
+ }
+
+ if ( me->IsPlayingGesture( ACT_MP_GESTURE_VC_FINGERPOINT_PRIMARY ) )
+ {
+ // wait for "wake up" anim to finish
+ return Continue();
+ }
+
+ // just keep swinging
+ me->PressFireButton();
+
+ // chase them down
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Update( me, m_victim, cost );
+
+ // avoid friends
+ CTeam *team = GetGlobalTeam( TF_TEAM_RED );
+ for( int t=0; t<team->GetNumPlayers(); ++t )
+ {
+ CTFPlayer *teamMember = (CTFPlayer *)team->GetPlayer(t);
+
+ if ( !teamMember->IsAlive() )
+ continue;
+
+ Vector toBuddy = teamMember->GetAbsOrigin() - me->GetAbsOrigin();
+ if ( toBuddy.IsLengthLessThan( tf_raid_mob_avoid_range.GetFloat() ) )
+ {
+ float range = toBuddy.NormalizeInPlace();
+
+ me->GetLocomotionInterface()->Approach( me->GetAbsOrigin() - 100.0f * toBuddy, 1.0f - ( range / tf_raid_mob_avoid_range.GetFloat() ) );
+ }
+ }
+
+
+ if ( !m_victim->IsAlive() && me->IsRangeLessThan( m_victim, tf_bot_taunt_range.GetFloat() ) )
+ {
+ // we got 'em!
+ return ChangeTo( new CTFBotTaunt, "Taunt their corpse" );
+ }
+
+ if ( m_vocalizeTimer.IsElapsed() )
+ {
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_mob_rush_vocalize_min_interval.GetFloat(), tf_raid_mob_rush_vocalize_max_interval.GetFloat() ) );
+
+ if ( me->IsPlayerClass( TF_CLASS_SCOUT ) )
+ me->EmitSound( "Scout.MobJabber" );
+ else if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
+ me->EmitSound( "Heavy.MobJabber" );
+ else
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMobRush::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ return TryToSustain( RESULT_CRITICAL, "Ignoring contact" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMobRush::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ return TryToSustain( RESULT_CRITICAL, "Ignoring injury" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMobRush::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+{
+ return TryToSustain( RESULT_CRITICAL, "Ignoring friend death" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMobRush::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ return TryToSustain( RESULT_CRITICAL );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMobRush::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h
new file mode 100644
index 0000000..0331acc
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h
@@ -0,0 +1,42 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mob_rush.h
+// A member of a rushing mob of melee attackers
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_MOB_RUSH_H
+#define TF_BOT_MOB_RUSH_H
+
+#ifdef TF_RAID_MODE
+
+#include "Path/NextBotChasePath.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotMobRush : public Action< CTFBot >
+{
+public:
+ CTFBotMobRush( CTFPlayer *victim, float reactionTime = 0.0f );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+ virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ QueryResultType ShouldRetreat( const INextBot *me ) const;
+
+ virtual const char *GetName( void ) const { return "MobRush"; };
+
+private:
+ CHandle< CTFPlayer > m_victim;
+ CountdownTimer m_reactionTimer;
+ CountdownTimer m_tauntTimer;
+ CountdownTimer m_vocalizeTimer;
+ ChasePath m_path;
+};
+
+#endif // TF_RAID_MODE
+
+#endif // TF_BOT_MOB_RUSH_H
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp
new file mode 100644
index 0000000..95d4362
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp
@@ -0,0 +1,150 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_squad_attack.cpp
+// Move and attack as a small, cohesive, group
+// Michael Booth, October 2009
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "team.h"
+#include "raid/tf_raid_logic.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/raid/tf_bot_wander.h"
+#include "bot/behavior/scenario/raid/tf_bot_squad_attack.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/tf_bot_move_to_vantage_point.h"
+
+
+ConVar tf_squad_radius( "tf_squad_radius", "200", FCVAR_CHEAT );
+ConVar tf_squad_debug( "tf_squad_debug", "0", FCVAR_CHEAT );
+ConVar tf_raid_squad_vocalize_min_interval( "tf_raid_squad_vocalize_min_interval", "5", FCVAR_CHEAT );
+ConVar tf_raid_squad_vocalize_max_interval( "tf_raid_squad_vocalize_max_interval", "8", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSquadAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_squad_vocalize_min_interval.GetFloat(), tf_raid_squad_vocalize_max_interval.GetFloat() ) );
+ m_victim = NULL;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// the leader is the slowest member of the squad
+CTFBot *CTFBotSquadAttack::GetSquadLeader( CTFBot *me ) const
+{
+ CTFBot *leader = NULL;
+ float leaderSpeed = FLT_MAX;
+
+ CTFBotSquad *squad = me->GetSquad();
+ CTFBotSquad::Iterator it;
+ for( it = squad->GetFirstMember(); it != squad->InvalidIterator(); it = squad->GetNextMember( it ) )
+ {
+ CTFBot *bot = it();
+
+ float speed = bot->GetPlayerClass()->GetMaxSpeed();
+
+ if ( speed < leaderSpeed )
+ {
+ leader = bot;
+ leaderSpeed = speed;
+ }
+ }
+
+ return leader;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSquadAttack::Update( CTFBot *me, float interval )
+{
+ if ( !me->IsInASquad() )
+ return Done( "Not in a squad" );
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return SuspendFor( new CTFBotMedicHeal );
+ }
+
+ CTFBot *leader = GetSquadLeader( me );
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+
+ if ( m_victim == NULL || m_victimConsiderTimer.IsElapsed() )
+ {
+ m_victimConsiderTimer.Start( 3.0f );
+
+ m_victim = TFGameRules()->GetRaidLogic()->SelectRaiderToAttack();
+ }
+
+ if ( m_victim )
+ {
+ const float engageRange = 500.0f;
+ if ( me->IsPlayerClass( TF_CLASS_PYRO ) ||
+ me->IsRangeGreaterThan( m_victim->GetAbsOrigin(), engageRange ) ||
+ !me->GetVisionInterface()->IsAbleToSee( m_victim, IVision::DISREGARD_FOV ) )
+ {
+ if ( me->IsSelf( leader ) || me->IsRangeLessThan( leader, tf_squad_radius.GetFloat() ) )
+ {
+ // chase down the enemy
+ m_chasePath.Update( me, m_victim, cost );
+ }
+ }
+
+ if ( !me->IsSelf( leader ) && me->IsRangeGreaterThan( leader, 1.25f * tf_squad_radius.GetFloat() ) )
+ {
+ // too far from leader - return to him
+ m_chasePath.Update( me, leader, cost );
+ }
+
+ if ( tf_squad_debug.GetBool() && me->IsSelf( leader ) )
+ {
+ NDebugOverlay::Circle( me->GetAbsOrigin(), 20.0f, 255, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+
+ CTFBotSquad *squad = me->GetSquad();
+ CTFBotSquad::Iterator it;
+ for( it = squad->GetFirstMember(); it != squad->InvalidIterator(); it = squad->GetNextMember( it ) )
+ {
+ CTFBot *bot = it();
+
+ if ( me->IsSelf( bot ) )
+ continue;
+
+ NDebugOverlay::Line( me->WorldSpaceCenter(), bot->WorldSpaceCenter(), 0, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+ }
+ }
+
+ if ( m_vocalizeTimer.IsElapsed() )
+ {
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_squad_vocalize_min_interval.GetFloat(), tf_raid_squad_vocalize_max_interval.GetFloat() ) );
+
+ if ( me->IsPlayerClass( TF_CLASS_SCOUT ) )
+ me->EmitSound( "Scout.MobJabber" );
+ else if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
+ me->EmitSound( "Heavy.MobJabber" );
+ else
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSquadAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ return Continue();
+}
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSquadAttack::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ return TryContinue();
+}
+
+#endif // TF_RAID_MODE \ No newline at end of file
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h
new file mode 100644
index 0000000..a685c65
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h
@@ -0,0 +1,47 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_squad_attack.h
+// Move and attack as a small, cohesive, group
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_SQUAD_ATTACK_H
+#define TF_BOT_SQUAD_ATTACK_H
+
+#ifdef TF_RAID_MODE
+
+#include "Path/NextBotPathFollow.h"
+#include "Path/NextBotChasePath.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotSquadAttack : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ QueryResultType ShouldRetreat( const INextBot *me ) const;
+
+ virtual const char *GetName( void ) const { return "SquadPatrol"; };
+
+private:
+ CountdownTimer m_vocalizeTimer;
+ PathFollower m_path;
+ ChasePath m_chasePath;
+ CHandle< CTFPlayer > m_victim;
+ CountdownTimer m_victimConsiderTimer;
+
+ CTFBot *GetSquadLeader( CTFBot *me ) const;
+};
+
+inline QueryResultType CTFBotSquadAttack::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+#endif // TF_RAID_MODE
+
+#endif // TF_BOT_SQUAD_ATTACK_H
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp
new file mode 100644
index 0000000..ffd08f4
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp
@@ -0,0 +1,175 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_wander.cpp
+// Wanderering/idle enemies for Squad Co-op mode
+// Michael Booth, October 2009
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "team.h"
+#include "raid/tf_raid_logic.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/scenario/raid/tf_bot_wander.h"
+#include "bot/behavior/scenario/raid/tf_bot_mob_rush.h"
+
+
+ConVar tf_raid_wanderer_aggro_range( "tf_raid_wanderer_aggro_range", "500", FCVAR_CHEAT, "If wanderers see a threat closer than this, they attack" );
+ConVar tf_raid_wanderer_notice_friend_death_range( "tf_raid_wanderer_notice_friend_death_range", "1000", FCVAR_CHEAT, "If a friend dies within this radius of a wanderer, it wakes up and attacks the attacker" );
+ConVar tf_raid_wanderer_reaction_factor( "tf_raid_wanderer_reaction_factor", "1", FCVAR_CHEAT );
+ConVar tf_raid_wanderer_vocalize_min_interval( "tf_raid_wanderer_vocalize_min_interval", "20", FCVAR_CHEAT );
+ConVar tf_raid_wanderer_vocalize_max_interval( "tf_raid_wanderer_vocalize_max_interval", "30", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CTFBotWander::CTFBotWander( void )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotWander::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_wanderer_vocalize_min_interval.GetFloat(), tf_raid_wanderer_vocalize_max_interval.GetFloat() ) );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotWander::Update( CTFBot *me, float interval )
+{
+ // mobs use only their melee weapons
+ CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( meleeWeapon )
+ {
+ me->Weapon_Switch( meleeWeapon );
+ }
+
+
+ CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE );
+
+ if ( me->HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ // I'm a mob rusher - pick a random raider and attack them!
+ CTFPlayer *victim = TFGameRules()->GetRaidLogic()->SelectRaiderToAttack();
+ if ( victim )
+ {
+ return SuspendFor( new CTFBotMobRush( victim ), "Rushing a raider" );
+ }
+ }
+ else if ( m_visionTimer.IsElapsed() )
+ {
+ // I'm a wanderer - look for very nearby threats
+ m_visionTimer.Start( RandomFloat( 0.5f, 1.0f ) );
+
+ // find closest visible raider within aggro range
+ CTFPlayer *threat = NULL;
+ float closeThreatRangeSq = tf_raid_wanderer_aggro_range.GetFloat() * tf_raid_wanderer_aggro_range.GetFloat();
+
+ for( int i=0; i<raidingTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i);
+
+ if ( !player->IsAlive() )
+ continue;
+
+ float rangeSq = me->GetRangeSquaredTo( player );
+ if ( rangeSq < closeThreatRangeSq )
+ {
+ if ( me->GetVisionInterface()->IsLineOfSightClearToEntity( player ) )
+ {
+ threat = player;
+ closeThreatRangeSq = rangeSq;
+ }
+ }
+ }
+
+ if ( threat )
+ {
+ return SuspendFor( new CTFBotMobRush( threat ), "Attacking threat!" );
+ }
+ }
+
+ if ( m_vocalizeTimer.IsElapsed() )
+ {
+ m_vocalizeTimer.Start( RandomFloat( tf_raid_wanderer_vocalize_min_interval.GetFloat(), tf_raid_wanderer_vocalize_max_interval.GetFloat() ) );
+
+ // mouth off
+ if ( me->IsPlayerClass( TF_CLASS_SCOUT ) )
+ me->EmitSound( "Scout.WanderJabber" );
+ else
+ me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotWander::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ if ( other && other->IsPlayer() && me->IsEnemy( other ) )
+ {
+ return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)other ), RESULT_IMPORTANT, "Attacking threat who touched me!" );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotWander::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) )
+ {
+ return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)info.GetAttacker() ), RESULT_IMPORTANT, "Attacking threat who attacked me!" );
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotWander::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+{
+ if ( victim && me->IsFriend( victim ) )
+ {
+ if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) )
+ {
+ if ( me->IsRangeLessThan( victim, tf_raid_wanderer_notice_friend_death_range.GetFloat() ) )
+ {
+ if ( me->GetVisionInterface()->IsAbleToSee( victim, IVision::DISREGARD_FOV ) &&
+ me->GetVisionInterface()->IsAbleToSee( info.GetAttacker(), IVision::DISREGARD_FOV ) )
+ {
+ float rangeToAttacker = me->GetRangeTo( info.GetAttacker() );
+ float reactionTime;
+
+ if ( rangeToAttacker < tf_raid_wanderer_aggro_range.GetFloat() )
+ {
+ reactionTime = 0.0f;
+ }
+ else
+ {
+ reactionTime = tf_raid_wanderer_reaction_factor.GetFloat() * ( rangeToAttacker - tf_raid_wanderer_aggro_range.GetFloat() ) / tf_raid_wanderer_aggro_range.GetFloat();
+ }
+
+ return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)info.GetAttacker(), reactionTime ), RESULT_IMPORTANT, "Attacking my friend's attacker!" );
+ }
+ }
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotWander::OnCommandAttack( CTFBot *me, CBaseEntity *victim )
+{
+ return TryContinue();
+}
+
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h
new file mode 100644
index 0000000..0364903
--- /dev/null
+++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h
@@ -0,0 +1,51 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_wander.h
+// Wanderering/idle enemies for Squad Co-op mode
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_WANDER_H
+#define TF_BOT_WANDER_H
+
+#ifdef TF_RAID_MODE
+
+//-----------------------------------------------------------------------------
+class CTFBotWander : public Action< CTFBot >
+{
+public:
+ CTFBotWander( void );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+ virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info );
+
+ virtual EventDesiredResult< CTFBot > OnCommandAttack( CTFBot *me, CBaseEntity *victim );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+
+ virtual const char *GetName( void ) const { return "Wander"; };
+
+private:
+ CountdownTimer m_visionTimer;
+ CountdownTimer m_vocalizeTimer;
+};
+
+
+inline QueryResultType CTFBotWander::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+inline QueryResultType CTFBotWander::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+#endif TF_RAID_MODE
+
+#endif // TF_BOT_WANDER_H
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
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;
+}
+
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h
new file mode 100644
index 0000000..085d043
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h
@@ -0,0 +1,49 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_attack.h
+// Backstab or pistol, as appropriate
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_SPY_ATTACK_H
+#define TF_BOT_SPY_ATTACK_H
+
+#include "Path/NextBotChasePath.h"
+
+
+//-------------------------------------------------------------------------------
+class CTFBotSpyAttack : public Action< CTFBot >
+{
+public:
+ CTFBotSpyAttack( CTFPlayer *victim );
+ virtual ~CTFBotSpyAttack() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const;
+ virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // use this to signal the enemy we are focusing on, so we dont avoid them
+
+ 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 "SpyAttack"; };
+
+private:
+ CHandle< CTFPlayer > m_victim;
+ ChasePath m_path;
+ bool m_isCoverBlown;
+ CountdownTimer m_chuckleTimer;
+ CountdownTimer m_decloakTimer;
+};
+
+
+#endif // TF_BOT_SPY_ATTACK_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp
new file mode 100644
index 0000000..db5c69e
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp
@@ -0,0 +1,31 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_backstab.cpp
+// Chase behind a victim and backstab them
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_backstab.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyBackstab::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyBackstab::Update( CTFBot *me, float interval )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyBackstab::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h
new file mode 100644
index 0000000..1efd0a0
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h
@@ -0,0 +1,24 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_backstab.h
+// Chase behind a victim and backstab them
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_SPY_BACKSTAB_H
+#define TF_BOT_SPY_BACKSTAB_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyBackstab : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyBackstab"; };
+
+private:
+};
+
+#endif // TF_BOT_SPY_BACKSTAB_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp
new file mode 100644
index 0000000..828da5d
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp
@@ -0,0 +1,31 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_escape.cpp
+// Flee!
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_escape.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyEscape::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyEscape::Update( CTFBot *me, float interval )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyEscape::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h
new file mode 100644
index 0000000..cfd887e
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h
@@ -0,0 +1,24 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_escape.h
+// Flee!
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_SPY_ESCAPE_H
+#define TF_BOT_SPY_ESCAPE_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyEscape : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyEscape"; };
+
+private:
+};
+
+#endif // TF_BOT_SPY_ESCAPE_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp
new file mode 100644
index 0000000..2a35b31
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp
@@ -0,0 +1,236 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_hide.cpp
+// Move to a hiding spot
+// Michael Booth, September 2011
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_hide.h"
+#include "bot/behavior/spy/tf_bot_spy_lurk.h"
+#include "bot/behavior/spy/tf_bot_spy_attack.h"
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotSpyHide::CTFBotSpyHide( CTFPlayer *victim )
+{
+ m_initialVictim = victim;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyHide::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_hidingSpot = NULL;
+ m_findTimer.Invalidate();
+ m_isAtGoal = false;
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+
+ int enemyTeam = GetEnemyTeam( me->GetTeamNumber() );
+
+ m_incursionThreshold = myArea ? myArea->GetIncursionDistance( enemyTeam ) : FLT_MAX;
+ if ( m_incursionThreshold < 0.0f )
+ {
+ m_incursionThreshold = FLT_MAX;
+ }
+
+ m_talkTimer.Start( RandomFloat( 5.0f, 10.0f ) );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyHide::Update( CTFBot *me, float interval )
+{
+ if ( m_initialVictim != NULL && !me->GetVisionInterface()->IsIgnored( m_initialVictim ) )
+ {
+ return SuspendFor( new CTFBotSpyAttack( m_initialVictim ), "Going after our initial victim" );
+ }
+
+ // go after victims we've gotten behind
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->GetTimeSinceLastKnown() < 3.0f )
+ {
+ CTFPlayer *victim = ToTFPlayer( threat->GetEntity() );
+ if ( victim )
+ {
+ const float attackRange = 750.0f;
+ if ( me->IsRangeLessThan( victim, attackRange ) )
+ {
+ if ( !victim->IsLookingTowards( me ) || victim->IsFiringWeapon() )
+ {
+ return SuspendFor( new CTFBotSpyAttack( victim ), "Opportunistic attack or self defense!" );
+ }
+ }
+ }
+ }
+
+ if ( m_talkTimer.IsElapsed() )
+ {
+ m_talkTimer.Start( RandomFloat( 5.0f, 10.0f ) );
+ me->EmitSound( "Spy.TeaseVictim" );
+ }
+
+ if ( m_isAtGoal )
+ {
+ // Quiet everyone! We are hiding now!
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ if ( myArea )
+ {
+ int enemyTeam = GetEnemyTeam( me->GetTeamNumber() );
+
+ m_incursionThreshold = myArea->GetIncursionDistance( enemyTeam );
+ }
+
+ return SuspendFor( new CTFBotSpyLurk, "Reached hiding spot - lurking" );
+ }
+
+ if ( m_hidingSpot == NULL && m_findTimer.IsElapsed() )
+ {
+ FindHidingSpot( me );
+ }
+
+ // move to our hiding spot
+ m_path.Update( me );
+
+ // path following may invalidate our hiding spot (OnMoveToFailure())
+ if ( m_hidingSpot == NULL )
+ {
+ return Continue();
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_hidingSpot->GetPosition(), cost );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyHide::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_hidingSpot = NULL;
+ m_isAtGoal = false;
+ m_initialVictim = NULL;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyHide::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ m_isAtGoal = true;
+
+ return TryContinue( RESULT_CRITICAL );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyHide::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_hidingSpot = NULL;
+ m_isAtGoal = false;
+
+ return TryContinue( RESULT_IMPORTANT );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyHide::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
+
+struct IncursionEntry_t
+{
+ int team;
+ CTFNavArea *area;
+};
+
+//---------------------------------------------------------------------------------------------
+class SpyHideIncursionDistanceLess
+{
+public:
+ bool Less( const IncursionEntry_t &src1, const IncursionEntry_t &src2, void *pCtx )
+ {
+ return src1.area->GetIncursionDistance( src1.team ) < src2.area->GetIncursionDistance( src2.team );
+ }
+};
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSpyHide::FindHidingSpot( CTFBot *me )
+{
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ if ( !myArea )
+ {
+ return false;
+ }
+
+ m_hidingSpot = NULL;
+
+ // find a spot to hide
+ const float maxRange = 3500.0f;
+ CUtlVector< CNavArea * > nearbyVector;
+ CollectSurroundingAreas( &nearbyVector, me->GetLastKnownArea(), maxRange,
+ 500.0f, 500.0f );
+
+ CUtlSortVector< IncursionEntry_t, SpyHideIncursionDistanceLess > hidingSpotVector;
+
+ float maxIncursion = m_incursionThreshold + 1000.0f;
+
+ int enemyTeam = GetEnemyTeam( me->GetTeamNumber() );
+
+ // if we are standing in an area the defenders can't reach, don't limit
+ if ( myArea->GetIncursionDistance( enemyTeam ) < 0.0f )
+ {
+ maxIncursion = 9999999;
+ }
+
+ for( int i=0; i<nearbyVector.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)nearbyVector[i];
+
+ if ( area->GetHidingSpots()->Count() <= 0 )
+ {
+ continue;
+ }
+
+ if ( area->GetIncursionDistance( enemyTeam ) < 0 )
+ {
+ continue;
+ }
+
+ // keep pushing inwards towards defender's spawn
+ if ( area->GetIncursionDistance( enemyTeam ) > maxIncursion )
+ {
+ continue;
+ }
+
+ IncursionEntry_t entry = { enemyTeam, area };
+ hidingSpotVector.Insert( entry );
+ }
+
+ if ( hidingSpotVector.Count() <= 0 )
+ {
+ return false;
+ }
+
+ // penetrate as far as we can
+ int which = RandomInt( 0, hidingSpotVector.Count()/2 );
+ CTFNavArea *whichArea = hidingSpotVector[ which ].area;
+
+ const HidingSpotVector *hidingSpots = whichArea->GetHidingSpots();
+
+ m_hidingSpot = hidingSpots->Element( RandomInt( 0, hidingSpots->Count()-1 ) );
+
+ return true;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h
new file mode 100644
index 0000000..dedd670
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h
@@ -0,0 +1,45 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_hide.h
+// Move to a hiding spot
+// Michael Booth, September 2011
+
+#ifndef TF_BOT_SPY_HIDE
+#define TF_BOT_SPY_HIDE
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyHide : public Action< CTFBot >
+{
+public:
+ CTFBotSpyHide( CTFPlayer *victim = NULL );
+ virtual ~CTFBotSpyHide() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyHide"; };
+
+private:
+ CHandle< CTFPlayer > m_initialVictim;
+
+ HidingSpot *m_hidingSpot;
+ bool FindHidingSpot( CTFBot *me );
+ CountdownTimer m_findTimer;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ bool m_isAtGoal;
+
+ float m_incursionThreshold;
+
+ CountdownTimer m_talkTimer;
+};
+
+#endif // TF_BOT_SPY_HIDE
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp
new file mode 100644
index 0000000..ae0de20
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp
@@ -0,0 +1,335 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_infiltrate.cpp
+// Move into position behind enemy lines and wait for victims
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_obj_sentrygun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_infiltrate.h"
+#include "bot/behavior/spy/tf_bot_spy_sap.h"
+#include "bot/behavior/spy/tf_bot_spy_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_debug_spy( "tf_bot_debug_spy", "0", FCVAR_CHEAT );
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyInfiltrate::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_hideArea = NULL;
+
+ m_hasEnteredCombatZone = false;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyInfiltrate::Update( CTFBot *me, float interval )
+{
+ // switch to our pistol
+ CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myGun )
+ {
+ me->Weapon_Switch( myGun );
+ }
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+
+ if ( !myArea )
+ {
+ return Continue();
+ }
+
+ bool isInMySpawn = myArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED );
+ if ( myArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_EXIT ) )
+ {
+ // don't count exits so we cloak as we leave
+ isInMySpawn = false;
+ }
+
+ // cloak when we first enter an area of active combat
+ if ( !me->m_Shared.IsStealthed() &&
+ !isInMySpawn &&
+ myArea->IsInCombat() &&
+ !m_hasEnteredCombatZone )
+ {
+ m_hasEnteredCombatZone = true;
+ me->PressAltFireButton();
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->GetEntity() && threat->GetEntity()->IsBaseObject() )
+ {
+ CBaseObject *enemyObject = (CBaseObject *)threat->GetEntity();
+ if ( !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) )
+ {
+ return SuspendFor( new CTFBotSpySap( enemyObject ), "Sapping an enemy object" );
+ }
+ }
+
+ if ( me->GetEnemySentry() && !me->GetEnemySentry()->HasSapper() )
+ {
+ return SuspendFor( new CTFBotSpySap( me->GetEnemySentry() ), "Sapping a Sentry" );
+ }
+
+ if ( !m_hideArea && m_findHidingSpotTimer.IsElapsed() )
+ {
+ FindHidingSpot( me );
+ m_findHidingSpotTimer.Start( 3.0f );
+ }
+
+ if ( !TFGameRules()->InSetup() )
+ {
+ // go after victims we've gotten behind
+ if ( threat && threat->GetTimeSinceLastKnown() < 3.0f )
+ {
+ CTFPlayer *victim = ToTFPlayer( threat->GetEntity() );
+ if ( victim )
+ {
+ CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea();
+ if ( victimArea )
+ {
+ int victimTeam = victim->GetTeamNumber();
+
+ if ( victimArea->GetIncursionDistance( victimTeam ) > myArea->GetIncursionDistance( victimTeam ) )
+ {
+ if ( me->m_Shared.IsStealthed() )
+ {
+ return SuspendFor( new CTFBotRetreatToCover( new CTFBotSpyAttack( victim ) ), "Hiding to decloak before going after a backstab victim" );
+ }
+ else
+ {
+ return SuspendFor( new CTFBotSpyAttack( victim ), "Going after a backstab victim" );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( m_hideArea )
+ {
+ if ( tf_bot_debug_spy.GetBool() )
+ {
+ m_hideArea->DrawFilled( 255, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+
+ if ( myArea == m_hideArea )
+ {
+ // stay hidden during setup time
+ if ( TFGameRules()->InSetup() )
+ {
+ m_waitTimer.Start( RandomFloat( 0.0f, 5.0f ) );
+ }
+ else
+ {
+ // wait in our hiding spot for a bit, then try another
+ if ( !m_waitTimer.HasStarted() )
+ {
+ m_waitTimer.Start( RandomFloat( 5.0f, 10.0f ) );
+ }
+ else if ( m_waitTimer.IsElapsed() )
+ {
+ // time to find a new hiding spot
+ m_hideArea = NULL;
+ }
+ }
+ }
+ else
+ {
+ // move to our ambush position
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ // we may not be able to path to our hiding spot, but get as close as we can
+ // (dropdown mid spawn in cp_gorge)
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_hideArea->GetCenter(), cost );
+ }
+
+ m_path.Update( me );
+
+ m_waitTimer.Invalidate();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotSpyInfiltrate::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyInfiltrate::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyInfiltrate::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_repathTimer.Invalidate();
+ m_hideArea = NULL;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSpyInfiltrate::FindHidingSpot( CTFBot *me )
+{
+ m_hideArea = NULL;
+
+ if ( me->GetAliveDuration() < 5.0f && TFGameRules()->InSetup() )
+ {
+ // wait a bit until the nav mesh has updated itself
+ return false;
+ }
+
+ int myTeam = me->GetTeamNumber();
+ const CUtlVector< CTFNavArea * > *enemySpawnExitVector = TheTFNavMesh()->GetSpawnRoomExitAreas( GetEnemyTeam( myTeam ) );
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ // for now, just lurk where we are
+ return false;
+ }
+#endif
+
+ if ( !enemySpawnExitVector || enemySpawnExitVector->Count() == 0 )
+ {
+ if ( tf_bot_debug_spy.GetBool() )
+ {
+ DevMsg( "%3.2f: No enemy spawn room exit areas found\n", gpGlobals->curtime );
+ }
+ return false;
+ }
+
+ // find nearby place to hide hear enemy spawn exit(s)
+ CUtlVector< CNavArea * > nearbyAreaVector;
+ const float nearbyHideRange = 2500.0f;
+ for( int x=0; x<enemySpawnExitVector->Count(); ++x )
+ {
+ CTFNavArea *enemySpawnExitArea = enemySpawnExitVector->Element( x );
+
+ CUtlVector< CNavArea * > nearbyThisExitAreaVector;
+ CollectSurroundingAreas( &nearbyThisExitAreaVector, enemySpawnExitArea, nearbyHideRange, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetStepHeight() );
+
+ // concat vectors (assuming N^2 unique search would cost more than ripping through some duplicates)
+ nearbyAreaVector.AddVectorToTail( nearbyThisExitAreaVector );
+ }
+
+ // find area not visible to any enemy spawn exits
+ CUtlVector< CTFNavArea * > hideAreaVector;
+ int i;
+
+ for( i=0; i<nearbyAreaVector.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i];
+
+ if ( !me->GetLocomotionInterface()->IsAreaTraversable( area ) )
+ continue;
+
+ bool isHidden = true;
+ for( int j=0; j<enemySpawnExitVector->Count(); ++j )
+ {
+ if ( area->IsPotentiallyVisible( enemySpawnExitVector->Element(j) ) )
+ {
+ isHidden = false;
+ break;
+ }
+ }
+
+ if ( isHidden )
+ {
+ hideAreaVector.AddToTail( area );
+ }
+ }
+
+ if ( hideAreaVector.Count() == 0 )
+ {
+ if ( tf_bot_debug_spy.GetBool() )
+ {
+ DevMsg( "%3.2f: Can't find any non-visible hiding areas, trying for anything near the spawn exit...\n", gpGlobals->curtime );
+ }
+
+ for( i=0; i<nearbyAreaVector.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i];
+
+ if ( !me->GetLocomotionInterface()->IsAreaTraversable( area ) )
+ continue;
+
+ hideAreaVector.AddToTail( area );
+ }
+ }
+
+ if ( hideAreaVector.Count() == 0 )
+ {
+ if ( tf_bot_debug_spy.GetBool() )
+ {
+ DevMsg( "%3.2f: Can't find any areas near the enemy spawn exit - just heading to the enemy spawn and hoping...\n", gpGlobals->curtime );
+ }
+
+ m_hideArea = enemySpawnExitVector->Element( RandomInt( 0, enemySpawnExitVector->Count()-1 ) );
+
+ return false;
+ }
+
+ // pick a specific hiding spot
+ m_hideArea = hideAreaVector[ RandomInt( 0, hideAreaVector.Count()-1 ) ];
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnStuck( CTFBot *me )
+{
+ m_hideArea = NULL;
+ m_findHidingSpotTimer.Invalidate();
+
+ return TryContinue( RESULT_TRY );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ // enemy spawn likely changed - find new hiding spot after internal data has updated
+ m_hideArea = NULL;
+ m_findHidingSpotTimer.Start( 5.0f );
+
+ return TryContinue( RESULT_TRY );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ // enemy spawn likely changed - find new hiding spot after internal data has updated
+ m_hideArea = NULL;
+ m_findHidingSpotTimer.Start( 5.0f );
+
+ return TryContinue( RESULT_TRY );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyInfiltrate::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h
new file mode 100644
index 0000000..db0be5a
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h
@@ -0,0 +1,42 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_infiltrate.h
+// Move into position behind enemy lines and wait for victims
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_SPY_INFILTRATE_H
+#define TF_BOT_SPY_INFILTRATE_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyInfiltrate : 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 );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyInfiltrate"; };
+
+private:
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+
+ CTFNavArea *m_hideArea;
+ bool FindHidingSpot( CTFBot *me );
+ CountdownTimer m_findHidingSpotTimer;
+
+ CountdownTimer m_waitTimer;
+
+ bool m_hasEnteredCombatZone;
+};
+
+
+#endif // TF_BOT_SPY_INFILTRATE_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp
new file mode 100644
index 0000000..7ee3d9c
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp
@@ -0,0 +1,160 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_leave_spawn_room.cpp
+// Assume the enemy is watching our spawn - escape it
+// Michael Booth, September 2011
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_leave_spawn_room.h"
+#include "bot/behavior/spy/tf_bot_spy_hide.h"
+
+
+extern bool IsSpaceToSpawnHere( const Vector &where );
+
+//---------------------------------------------------------------------------------------------
+bool TeleportNearVictim( CTFBot *me, CTFPlayer *victim, int attempt )
+{
+ VPROF_BUDGET( "CTFBotSpyLeaveSpawnRoom::TeleportNearVictim", "NextBot" );
+
+ if ( !victim )
+ {
+ return false;
+ }
+
+ CUtlVector< CTFNavArea * > ambushVector; // vector of hidden but near-to-victim areas
+
+ if ( !victim->GetLastKnownArea() )
+ {
+ return false;
+ }
+
+ const float maxSurroundTravelRange = 6000.0f;
+
+ float surroundTravelRange = 1500.0f + 500.0f * attempt;
+ if ( surroundTravelRange > maxSurroundTravelRange )
+ {
+ surroundTravelRange = maxSurroundTravelRange;
+ }
+
+ CUtlVector< CNavArea * > areaVector;
+
+ // collect walkable areas surrounding this victim
+ CollectSurroundingAreas( &areaVector, victim->GetLastKnownArea(), surroundTravelRange, StepHeight, StepHeight );
+
+ // keep subset that isn't visible to the victim's team
+ for( int j=0; j<areaVector.Count(); ++j )
+ {
+ CTFNavArea *area = (CTFNavArea *)areaVector[j];
+
+ if ( !area->IsValidForWanderingPopulation() )
+ {
+ continue;
+ }
+
+ if ( area->IsPotentiallyVisibleToTeam( victim->GetTeamNumber() ) )
+ {
+ continue;
+ }
+
+ ambushVector.AddToTail( area );
+ }
+
+ if ( ambushVector.Count() == 0 )
+ {
+ return false;
+ }
+
+ int maxTries = MIN( 10, ambushVector.Count() );
+
+ for( int retry=0; retry<maxTries; ++retry )
+ {
+ int which = RandomInt( 0, ambushVector.Count()-1 );
+ Vector where = ambushVector[ which ]->GetCenter() + Vector( 0, 0, StepHeight );
+
+ if ( IsSpaceToSpawnHere( where ) )
+ {
+ me->Teleport( &where, &vec3_angle, &vec3_origin );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyLeaveSpawnRoom::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // disguise as enemy team
+ me->DisguiseAsMemberOfEnemyTeam();
+
+ // cloak
+ me->PressAltFireButton();
+
+ // wait a few moments to guarantee a minimum time between announcing Spies and their attack
+ m_waitTimer.Start( 2.0f + RandomFloat( 0.0f, 1.0f ) );
+
+ m_attempt = 0;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyLeaveSpawnRoom::Update( CTFBot *me, float interval )
+{
+ VPROF_BUDGET( "CTFBotSpyLeaveSpawnRoom::Update", "NextBot" );
+
+ if ( m_waitTimer.IsElapsed() )
+ {
+ CTFPlayer *victim = NULL;
+
+ CUtlVector< CTFPlayer * > enemyVector;
+ CollectPlayers( &enemyVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ // randomly shuffle our enemies
+ CUtlVector< CTFPlayer * > shuffleVector;
+ shuffleVector = enemyVector;
+ int n = shuffleVector.Count();
+ while( n > 1 )
+ {
+ int k = RandomInt( 0, n-1 );
+ n--;
+
+ CTFPlayer *tmp = shuffleVector[n];
+ shuffleVector[n] = shuffleVector[k];
+ shuffleVector[k] = tmp;
+ }
+
+ for( int i=0; i<shuffleVector.Count(); ++i )
+ {
+ if ( TeleportNearVictim( me, shuffleVector[i], m_attempt ) )
+ {
+ victim = shuffleVector[i];
+ break;
+ }
+ }
+
+ // if we didn't find a victim, try again in a bit
+ if ( !victim )
+ {
+ m_waitTimer.Start( 1.0f );
+
+ ++m_attempt;
+
+ return Continue();
+ }
+
+ return ChangeTo( new CTFBotSpyHide( victim ), "Hiding!" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyLeaveSpawnRoom::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h
new file mode 100644
index 0000000..3471749
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h
@@ -0,0 +1,26 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_leave_spawn_room.h
+// Assume the enemy is watching our spawn - escape it
+// Michael Booth, September 2011
+
+#ifndef TF_BOT_LEAVE_SPAWN_ROOM_H
+#define TF_BOT_LEAVE_SPAWN_ROOM_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyLeaveSpawnRoom : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyLeaveSpawnRoom"; };
+
+private:
+ CountdownTimer m_waitTimer;
+ int m_attempt;
+};
+
+#endif // TF_BOT_LEAVE_SPAWN_ROOM_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp
new file mode 100644
index 0000000..a9379be
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp
@@ -0,0 +1,91 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_lurk.cpp
+// Wait for victims
+// Michael Booth, September 2011
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_obj_sentrygun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_lurk.h"
+#include "bot/behavior/spy/tf_bot_spy_sap.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;
+extern ConVar tf_bot_debug_spy;
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyLurk::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // cloak
+ if ( !me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+
+ // disguise as the enemy team
+ me->DisguiseAsMemberOfEnemyTeam();
+
+ m_lurkTimer.Start( RandomFloat( 3.0f, 5.0f ) );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpyLurk::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->GetEntity() )
+ {
+ CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( threat->GetEntity() );
+ if ( enemyObject && !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) )
+ {
+ return SuspendFor( new CTFBotSpySap( enemyObject ), "Sapping an enemy object" );
+ }
+ }
+
+ if ( me->GetEnemySentry() != NULL && !me->GetEnemySentry()->HasSapper() )
+ {
+ return SuspendFor( new CTFBotSpySap( me->GetEnemySentry() ), "Sapping a Sentry" );
+ }
+
+ if ( m_lurkTimer.IsElapsed() )
+ {
+ return Done( "Lost patience with my hiding spot" );
+ }
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+
+ if ( !myArea )
+ {
+ return Continue();
+ }
+
+ // go after victims we've gotten behind
+ if ( threat && threat->GetTimeSinceLastKnown() < 3.0f )
+ {
+ CTFPlayer *victim = ToTFPlayer( threat->GetEntity() );
+ if ( victim )
+ {
+ if ( !victim->IsLookingTowards( me ) )
+ {
+ return ChangeTo( new CTFBotSpyAttack( victim ), "Going after a backstab victim" );
+ }
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpyLurk::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h
new file mode 100644
index 0000000..73e2d44
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h
@@ -0,0 +1,26 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_lurk.h
+// Wait for victims
+// Michael Booth, September 2011
+
+#ifndef TF_BOT_SPY_LURK_H
+#define TF_BOT_SPY_LURK_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpyLurk : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "SpyLurk"; };
+
+private:
+ CountdownTimer m_lurkTimer;
+};
+
+
+#endif // TF_BOT_SPY_LURK_H
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp
new file mode 100644
index 0000000..776339a
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp
@@ -0,0 +1,250 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_sap.cpp
+// Sap nearby enemy buildings
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_obj_sentrygun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/spy/tf_bot_spy_sap.h"
+#include "bot/behavior/tf_bot_approach_object.h"
+#include "bot/behavior/spy/tf_bot_spy_attack.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_debug_spy;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotSpySap::CTFBotSpySap( CBaseObject *sapTarget )
+{
+ m_sapTarget = sapTarget;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpySap::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ me->StopLookingAroundForEnemies();
+
+ // uncloak so we can sap
+ if ( me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpySap::Update( CTFBot *me, float interval )
+{
+ CBaseObject *newSapTarget = me->GetNearestKnownSappableTarget();
+
+ if ( newSapTarget )
+ {
+ m_sapTarget = newSapTarget;
+ }
+
+ if ( m_sapTarget == NULL )
+ {
+ return Done( "Sap target gone" );
+ }
+
+ CTFPlayer *victim = NULL;
+
+ CUtlVector< CKnownEntity > knownVector;
+ me->GetVisionInterface()->CollectKnownEntities( &knownVector );
+
+ for( int i=0; i<knownVector.Count(); ++i )
+ {
+ CTFPlayer *playerThreat = ToTFPlayer( knownVector[i].GetEntity() );
+ if ( playerThreat && me->IsEnemy( playerThreat ) )
+ {
+ victim = playerThreat;
+ break;
+ }
+ }
+
+ // opportunistic backstab if engineer is between me and my sap target
+ if ( victim && victim->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ const float nearbyRange = 150.0f;
+ if ( m_sapTarget->GetOwner() == victim && me->IsRangeLessThan( victim, nearbyRange ) )
+ {
+ if ( me->IsEntityBetweenTargetAndSelf( victim, m_sapTarget ) )
+ {
+ return SuspendFor( new CTFBotSpyAttack( victim ), "Backstabbing the engineer before I sap his buildings" );
+ }
+ }
+ }
+
+ const float sapRange = 40.0f;
+
+ if ( me->IsRangeLessThan( m_sapTarget, 2.0f * sapRange ) )
+ {
+ // switch to our sapper and spam it
+ CBaseCombatWeapon *mySapper = me->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING );
+ if ( !mySapper )
+ {
+ return Done( "I have no sapper" );
+ }
+
+ me->Weapon_Switch( mySapper );
+
+ // uncloak
+ if ( me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+
+ // sap our target
+ me->GetBodyInterface()->AimHeadTowards( m_sapTarget, IBody::MANDATORY, 0.1f, NULL, "Aiming my sapper" );
+
+ me->PressFireButton();
+ }
+
+ if ( me->IsRangeGreaterThan( m_sapTarget, sapRange ) )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( m_path.Compute( me, m_sapTarget, cost ) == false )
+ {
+ return Done( "No path to sap target!" );
+ }
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+ }
+
+ // if our target is sapped, look for other nearby buildings to sap
+ if ( m_sapTarget->HasSapper() )
+ {
+ CBaseObject *nextTarget = me->GetNearestKnownSappableTarget();
+ if ( nextTarget )
+ {
+ m_sapTarget = nextTarget;
+ }
+ else
+ {
+ // everything is sapped - explicitly attack nearby enemy Engineers
+ if ( victim && victim->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return SuspendFor( new CTFBotSpyAttack( victim ), "Attacking an engineer" );
+ }
+
+ return Done( "All targets sapped" );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotSpySap::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->StartLookingAroundForEnemies();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpySap::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ me->StartLookingAroundForEnemies();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSpySap::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ me->StopLookingAroundForEnemies();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSpySap::OnStuck( CTFBot *me )
+{
+ return TryDone( RESULT_CRITICAL, "I'm stuck, probably on a sapped building that hasn't exploded yet" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpySap::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const
+{
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+
+ if ( m_sapTarget && !m_sapTarget->HasSapper() )
+ {
+ // mission not accomplished
+ return ANSWER_NO;
+ }
+
+ if ( !me->m_Shared.InCond( TF_COND_DISGUISED ) &&
+ !me->m_Shared.InCond( TF_COND_DISGUISING ) &&
+ !me->m_Shared.IsStealthed() )
+ {
+ // our cover is blown!
+ return ANSWER_YES;
+ }
+
+ // if we've sapped, attack
+ return AreAllDangerousSentriesSapped( me ) ? ANSWER_YES : ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Don't avoid enemies when we're going in for the sap
+QueryResultType CTFBotSpySap::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const
+{
+ if ( m_sapTarget.Get() && me->IsRangeLessThan( m_sapTarget, 300.0f ) )
+ {
+ // we're almost to our sap target - don't avoid anyone
+ return ANSWER_NO;
+ }
+
+ // avoid everyone while we move to our sap target
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSpySap::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotSpySap::AreAllDangerousSentriesSapped( CTFBot *me ) const
+{
+ CUtlVector< CKnownEntity > knownVector;
+ me->GetVisionInterface()->CollectKnownEntities( &knownVector );
+
+ for( int i=0; i<knownVector.Count(); ++i )
+ {
+ CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( knownVector[i].GetEntity() );
+ if ( enemyObject && enemyObject->ObjectType() == OBJ_SENTRYGUN && !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) )
+ {
+ // this is an active enemy sentry, are we in range and line of fire?
+ if ( me->IsRangeLessThan( enemyObject, SENTRY_MAX_RANGE ) && me->IsLineOfFireClear( enemyObject ) )
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+
diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h
new file mode 100644
index 0000000..4c8fe77
--- /dev/null
+++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h
@@ -0,0 +1,42 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_spy_sap.h
+// Sap nearby enemy buildings
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_SPY_SAP_H
+#define TF_BOT_SPY_SAP_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotSpySap : public Action< CTFBot >
+{
+public:
+ CTFBotSpySap( CBaseObject *sapTarget );
+ virtual ~CTFBotSpySap() { }
+
+ 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 );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ 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 QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // use this to signal the enemy we are focusing on, so we dont avoid them
+
+ virtual const char *GetName( void ) const { return "SpySap"; };
+
+private:
+ CHandle< CBaseObject > m_sapTarget;
+
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+
+ CBaseObject *GetNearestKnownSappableTarget( CTFBot *me );
+ bool AreAllDangerousSentriesSapped( CTFBot *me ) const;
+};
+
+#endif // TF_BOT_SPY_SAP_H
diff --git a/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp
new file mode 100644
index 0000000..873762d
--- /dev/null
+++ b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp
@@ -0,0 +1,336 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort_squad_leader.cpp
+// Escort the squad leader to their destination
+// Michael Booth, Octoboer 2011
+
+#include "cbase.h"
+
+#include "bot/tf_bot.h"
+#include "bot/behavior/squad/tf_bot_escort_squad_leader.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h"
+
+ConVar tf_bot_squad_escort_range( "tf_bot_squad_escort_range", "500", FCVAR_CHEAT );
+ConVar tf_bot_formation_debug( "tf_bot_formation_debug", "0", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEscortSquadLeader::CTFBotEscortSquadLeader( Action< CTFBot > *actionToDoAfterSquadDisbands ) // : m_path( ChasePath::LEAD_SUBJECT )
+{
+ m_actionToDoAfterSquadDisbands = actionToDoAfterSquadDisbands;
+ m_formationPath.SetGoalTolerance( 0.0f );
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscortSquadLeader::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_formationForward = vec3_origin;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscortSquadLeader::Update( CTFBot *me, float interval )
+{
+ if ( interval <= 0.0f )
+ {
+ return Continue();
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ CTFBotSquad *squad = me->GetSquad();
+ if ( !squad )
+ {
+ if ( m_actionToDoAfterSquadDisbands )
+ {
+ return ChangeTo( m_actionToDoAfterSquadDisbands, "Not in a Squad" );
+ }
+
+ return Done( "Not in a Squad" );
+ }
+
+ // we need to update every tick to smoothly move in formation
+ me->FlagForUpdate();
+
+ CTFBot *leader = squad->GetLeader();
+ if ( !leader || !leader->IsAlive() )
+ {
+ me->LeaveSquad();
+
+ if ( m_actionToDoAfterSquadDisbands )
+ {
+ return ChangeTo( m_actionToDoAfterSquadDisbands, "Squad leader is dead" );
+ }
+
+ return Done( "Squad leader is dead" );
+ }
+
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && leader == me )
+ {
+ const char* pszNowLeader = "I'm now the squad leader! Going for the flag!";
+ if ( me->HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ // push for the point first, then attack
+ return ChangeTo( new CTFBotPushToCapturePoint( new CTFBotFetchFlag ), pszNowLeader );
+ }
+
+ // capture the flag
+ return ChangeTo( new CTFBotFetchFlag, pszNowLeader );
+ }
+
+ // if we're using a melee weapon, close and attack with it while staying near the leader
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon && myWeapon->IsMeleeWeapon() )
+ {
+ if ( me->IsRangeLessThan( leader, tf_bot_squad_escort_range.GetFloat() ) && me->IsLineOfSightClear( leader ) )
+ {
+ ActionResult< CTFBot > result = m_meleeAttackAction.Update( me, interval );
+
+ if ( result.IsContinue() )
+ {
+ // we have a melee target, and we're still reasonably close to the flag leader
+ return Continue();
+ }
+ }
+ }
+
+ CUtlVector< CTFBot * > rawMemberVector;
+ squad->CollectMembers( &rawMemberVector );
+
+ // cull out the medics - they do their own thing
+ CUtlVector< CTFBot * > memberVector;
+ for( int m=0; m<rawMemberVector.Count(); ++m )
+ {
+ if ( !rawMemberVector[m]->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ memberVector.AddToTail( rawMemberVector[m] );
+ }
+ }
+
+ const PathFollower *leaderPath = leader->GetCurrentPath();
+ if ( !leaderPath || !leaderPath->GetCurrentGoal() )
+ {
+ // no path, no formation
+ me->SetSquadFormationError( 0.0f );
+ me->SetBrokenFormation( false );
+ return Continue();
+ }
+
+ const Path::Segment *leaderSegment = leaderPath->GetCurrentGoal();
+
+ Vector leaderForward = leaderSegment->pos - leader->GetAbsOrigin();
+
+ // if the leader is very close to the goal, use the next goal to ensure
+ // the forward vector stays forward
+ const float atGoal = 25.0f;
+ if ( leaderForward.IsLengthLessThan( atGoal ) )
+ {
+ const Path::Segment *nextSegment = leaderPath->NextSegment( leaderSegment );
+ if ( nextSegment )
+ {
+ leaderForward = nextSegment->pos - leader->GetAbsOrigin();
+ }
+ }
+
+ leaderForward.NormalizeInPlace();
+
+ if ( m_formationForward.IsZero() )
+ {
+ m_formationForward = leaderForward;
+ }
+ else
+ {
+ // limit rate of change of leader forward vector to keep formation coherent
+ float maxRotation = 30.0f; // degrees/second
+
+ float leaderForwardYaw = UTIL_VecToYaw( leaderForward );
+ float formationYaw = UTIL_VecToYaw( m_formationForward );
+
+ float angleDiff = UTIL_AngleDiff( leaderForwardYaw, formationYaw );
+
+ float deltaYaw = maxRotation * interval;
+
+ if ( angleDiff < -deltaYaw )
+ {
+ formationYaw -= deltaYaw;
+ }
+ else if ( angleDiff > deltaYaw )
+ {
+ formationYaw += deltaYaw;
+ }
+ else
+ {
+ formationYaw += angleDiff;
+ }
+
+ FastSinCos( formationYaw * M_PI / 180.0f, &m_formationForward.y, &m_formationForward.x );
+ m_formationForward.z = 0.0f;
+ }
+
+
+ const float maxSeparationAngle = 30.0f * M_PI / 180.0f;
+
+ float formationRadius = 125.0f;
+ if ( squad->GetFormationSize() > 0.0f )
+ {
+ formationRadius = squad->GetFormationSize();
+ }
+
+ Vector myFormationSpot;
+ Vector formationForward = vec3_origin;
+ float s, c;
+
+ // where am I in the roster
+ int which;
+ for( which=0; which<memberVector.Count(); ++which )
+ {
+ if ( me->IsSelf( memberVector[which] ) )
+ {
+ break;
+ }
+ }
+
+ // subtract one since the leader is always first
+ --which;
+
+ // my formation spot is assigned via my position in the roster array
+ int slot = ( which + 1 ) /2;
+
+ float formationAngle = slot * maxSeparationAngle;
+
+ if ( which & 0x1 )
+ {
+ formationAngle = -formationAngle;
+ }
+
+ FastSinCos( formationAngle, &s, &c );
+ formationForward.x = m_formationForward.x * c - m_formationForward.y * s;
+ formationForward.y = m_formationForward.y * c + m_formationForward.x * s;
+
+ myFormationSpot = leader->GetAbsOrigin() + formationRadius * formationForward;
+
+ trace_t result;
+ CTraceFilterIgnoreTeammates filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() );
+ UTIL_TraceLine( leader->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ), myFormationSpot + Vector( 0, 0, HalfHumanHeight ), MASK_PLAYERSOLID, &filter, &result );
+
+ if ( result.DidHitWorld() )
+ {
+ myFormationSpot = result.endpos - Vector( 0, 0, HalfHumanHeight ) + 0.6f * me->GetBodyInterface()->GetHullWidth() * result.plane.normal;
+ }
+
+
+ if ( tf_bot_formation_debug.GetBool() )
+ {
+ NDebugOverlay::Circle( myFormationSpot, 16.0f, 0, 255, 0, 255, true, 0.1f );
+
+ CFmtStr msg;
+ NDebugOverlay::Text( myFormationSpot, msg.sprintf( "%d", which ), false, 0.1f );
+ }
+
+ // match speed with leader if I'm at/near my formation position
+ Vector to = myFormationSpot - me->GetAbsOrigin();
+ float error = to.Length2D();
+ const float maxError = 100.0f; // 50
+
+ float normalizedError = 1.0f;
+ if ( error < maxError )
+ {
+ normalizedError = error / maxError;
+ }
+
+ // this error term is used in CTFPlayer::TeamFortress_CalculateMaxSpeed() to
+ // modulate our speed
+ // 0 = in position (no error)
+ // 1 = far out of position (max error)
+ me->SetSquadFormationError( normalizedError );
+
+ // move to my formation spot
+ if ( error < 50.0f )
+ {
+ // if we're ahead of where we want to be, just wait
+ if ( DotProduct( to, formationForward ) > 0.0f )
+ {
+ // very close - just directly approach to avoid pathing jaggies
+ me->GetLocomotionInterface()->Approach( myFormationSpot );
+ }
+ else
+ {
+ // we're in position
+ me->SetSquadFormationError( 0.0f );
+ }
+ }
+ else
+ {
+ if ( m_pathTimer.IsElapsed() )
+ {
+ m_pathTimer.Start( RandomFloat( 0.1f, 0.2f ) );
+
+ me->SetBrokenFormation( false );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( m_formationPath.Compute( me, myFormationSpot, cost ) == false )
+ {
+ // no path back to formation
+ me->SetBrokenFormation( true );
+ }
+
+ // if we have a long path to get back in formation, we've broken ranks
+ const float tooFar = 750.0f;
+ if ( m_formationPath.GetLength() > tooFar )
+ {
+ me->SetBrokenFormation( true );
+ }
+ }
+
+ m_formationPath.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotEscortSquadLeader::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotWaitForOutOfPositionSquadMember::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_waitTimer.Start( 2.0f );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotWaitForOutOfPositionSquadMember::Update( CTFBot *me, float interval )
+{
+ if ( m_waitTimer.IsElapsed() )
+ {
+ return Done( "Timeout" );
+ }
+
+ if ( !me->IsInASquad() || !me->GetSquad()->IsLeader( me ) )
+ {
+ return Done( "No squad" );
+ }
+
+ if ( me->GetSquad()->IsInFormation() )
+ {
+ // Everyone is in position
+ return Done( "Everyone is in formation. Moving on." );
+ }
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h
new file mode 100644
index 0000000..c9bf2cb
--- /dev/null
+++ b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h
@@ -0,0 +1,55 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort_squad_leader.h
+// Escort the squad leader to their destination
+// Michael Booth, Octoboer 2011
+
+#ifndef TF_BOT_ESCORT_SQUAD_LEADER_H
+#define TF_BOT_ESCORT_SQUAD_LEADER_H
+
+
+#include "Path/NextBotPathFollow.h"
+#include "bot/behavior/tf_bot_melee_attack.h"
+
+
+//-----------------------------------------------------------------------------
+class CTFBotEscortSquadLeader : public Action< CTFBot >
+{
+public:
+ CTFBotEscortSquadLeader( Action< CTFBot > *actionToDoAfterSquadDisbands = NULL );
+ virtual ~CTFBotEscortSquadLeader() { }
+
+ 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 const char *GetName( void ) const { return "EscortSquadLeader"; };
+
+private:
+ Action< CTFBot > *m_actionToDoAfterSquadDisbands;
+ CTFBotMeleeAttack m_meleeAttackAction;
+
+ PathFollower m_formationPath;
+ CountdownTimer m_pathTimer;
+
+ const Vector &GetFormationForwardVector( CTFBot *me );
+ Vector m_formationForward;
+};
+
+
+//-----------------------------------------------------------------------------
+class CTFBotWaitForOutOfPositionSquadMember : public Action< CTFBot >
+{
+public:
+ virtual ~CTFBotWaitForOutOfPositionSquadMember() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "WaitForOutOfPositionSquadMember"; };
+
+private:
+ CountdownTimer m_waitTimer;
+};
+
+
+#endif // TF_BOT_ESCORT_SQUAD_LEADER_H
diff --git a/game/server/tf/bot/behavior/tf_bot_approach_object.cpp b/game/server/tf/bot/behavior/tf_bot_approach_object.cpp
new file mode 100644
index 0000000..d2ebf63
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_approach_object.cpp
@@ -0,0 +1,69 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_approach_object.h
+// Move near/onto an object
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_approach_object.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+CTFBotApproachObject::CTFBotApproachObject( CBaseEntity *loot, float range )
+{
+ m_loot = loot;
+ m_range = range;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotApproachObject::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotApproachObject::Update( CTFBot *me, float interval )
+{
+ if ( m_loot == NULL )
+ {
+ return Done( "Object is NULL" );
+ }
+
+ if ( m_loot->IsEffectActive( EF_NODRAW ) )
+ {
+ return Done( "Object is NODRAW" );
+ }
+
+ if ( me->GetLocomotionInterface()->GetGround() == m_loot )
+ {
+ return Done( "I'm standing on the object" );
+ }
+
+ if ( me->IsDistanceBetweenLessThan( m_loot->GetAbsOrigin(), m_range ) )
+ {
+ // in case we can't pick up the loot for some reason
+ return Done( "Reached object" );
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_loot->GetAbsOrigin(), cost );
+ }
+
+ // move to the loot
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
diff --git a/game/server/tf/bot/behavior/tf_bot_approach_object.h b/game/server/tf/bot/behavior/tf_bot_approach_object.h
new file mode 100644
index 0000000..d19f4bc
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_approach_object.h
@@ -0,0 +1,29 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_approach_object.h
+// Move near/onto an object
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_APPROACH_OBJECT_H
+#define TF_BOT_APPROACH_OBJECT_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotApproachObject : public Action< CTFBot >
+{
+public:
+ CTFBotApproachObject( CBaseEntity *loot, float range = 10.0f );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "ApproachObject"; };
+
+private:
+ CHandle< CBaseEntity > m_loot; // what we are collecting
+ float m_range; // how close should we get
+ PathFollower m_path; // how we get to the loot
+ CountdownTimer m_repathTimer;
+};
+
+
+#endif // TF_BOT_APPROACH_OBJECT_H
diff --git a/game/server/tf/bot/behavior/tf_bot_attack.cpp b/game/server/tf/bot/behavior/tf_bot_attack.cpp
new file mode 100644
index 0000000..98266b8
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_attack.cpp
@@ -0,0 +1,157 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_attack.cpp
+// Attack our threat
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_attack.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_offense_must_push_time;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotAttack::CTFBotAttack( void ) : m_chasePath( ChasePath::LEAD_SUBJECT )
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// head aiming and weapon firing is handled elsewhere - we just need to get into position to fight
+ActionResult< CTFBot > CTFBotAttack::Update( CTFBot *me, float interval )
+{
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ bool isUsingCloseRangeWeapon = ( myWeapon && ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) || myWeapon->IsMeleeWeapon() ) );
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat == NULL || threat->IsObsolete() || !me->GetIntentionInterface()->ShouldAttack( me, threat ) )
+ {
+ return Done( "No threat" );
+ }
+
+ me->EquipBestWeaponForThreat( threat );
+
+ if ( isUsingCloseRangeWeapon && threat->IsVisibleRecently() && me->IsRangeLessThan( threat->GetLastKnownPosition(), 1.1f * me->GetDesiredAttackRange() ) )
+ {
+ // circle around our victim
+ if ( me->TransientlyConsistentRandomValue( 3.0f ) < 0.5f )
+ {
+ me->PressLeftButton();
+ }
+ else
+ {
+ me->PressRightButton();
+ }
+ }
+
+
+ // 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 ( threat->IsVisibleRecently() )
+ {
+ if ( isUsingCloseRangeWeapon && !TFGameRules()->IsMannVsMachineMode() ) // all bots in MvM use the default route
+ {
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_chasePath.Update( me, threat->GetEntity(), cost );
+ }
+ else
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ m_chasePath.Update( me, threat->GetEntity(), cost );
+ }
+ }
+ else
+ {
+ // if we're at the threat's last known position and he's still not visible, we lost him
+ m_chasePath.Invalidate();
+
+ if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), 20.0f ) )
+ {
+ me->GetVisionInterface()->ForgetEntity( threat->GetEntity() );
+ return Done( "I lost my target!" );
+ }
+
+ // look where we last saw him as we approach
+ if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), me->GetMaxAttackRange() ) )
+ {
+ me->GetBodyInterface()->AimHeadTowards( threat->GetLastKnownPosition() + Vector( 0, 0, HumanEyeHeight ), IBody::IMPORTANT, 0.2f, NULL, "Looking towards where we lost sight of our victim" );
+ }
+
+ m_path.Update( me );
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ //m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) );
+ m_repathTimer.Start( RandomFloat( 3.0f, 5.0f ) );
+
+ if ( isUsingCloseRangeWeapon && !TFGameRules()->IsMannVsMachineMode() ) // all bots in MvM use the default route
+ {
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, threat->GetLastKnownPosition(), cost );
+ }
+ else
+ {
+ CTFBotPathCost cost( me, DEFAULT_ROUTE );
+ float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f;
+ m_path.Compute( me, threat->GetLastKnownPosition(), cost, maxPathLength );
+ }
+ }
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotAttack::OnStuck( CTFBot *me )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotAttack::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotAttack::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotAttack::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotAttack::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_attack.h b/game/server/tf/bot/behavior/tf_bot_attack.h
new file mode 100644
index 0000000..9f97acd
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_attack.h
@@ -0,0 +1,38 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_attack.h
+// Attack a threat
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_ATTACK_H
+#define TF_BOT_ATTACK_H
+
+#include "Path/NextBotChasePath.h"
+
+
+//-------------------------------------------------------------------------------
+class CTFBotAttack : public Action< CTFBot >
+{
+public:
+ CTFBotAttack( void );
+ virtual ~CTFBotAttack() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "Attack"; };
+
+private:
+ PathFollower m_path;
+ ChasePath m_chasePath;
+ CountdownTimer m_repathTimer;
+};
+
+
+#endif // TF_BOT_ATTACK_H
diff --git a/game/server/tf/bot/behavior/tf_bot_behavior.cpp b/game/server/tf/bot/behavior/tf_bot_behavior.cpp
new file mode 100644
index 0000000..08bf424
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_behavior.cpp
@@ -0,0 +1,1649 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_behavior.cpp
+// Team Fortress NextBot
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "fmtstr.h"
+
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_projectile_rocket.h"
+#include "tf_weaponbase_grenadeproj.h"
+#include "tf_obj.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_weapon_flamethrower.h"
+#include "tf_weapon_sniperrifle.h"
+#include "tf_weapon_compound_bow.h"
+#include "bot/tf_bot.h"
+#include "bot/tf_bot_manager.h"
+#include "bot/behavior/tf_bot_behavior.h"
+#include "bot/behavior/tf_bot_dead.h"
+#include "NextBot/NavMeshEntities/func_nav_prerequisite.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/tf_bot_tactical_monitor.h"
+#include "bot/behavior/tf_bot_taunt.h"
+#include "bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h"
+#include "player_vs_environment/tf_population_manager.h"
+
+
+extern ConVar tf_bot_health_ok_ratio;
+
+ConVar tf_bot_path_lookahead_range( "tf_bot_path_lookahead_range", "300" );
+ConVar tf_bot_sniper_aim_error( "tf_bot_sniper_aim_error", "0.01", FCVAR_CHEAT );
+ConVar tf_bot_sniper_aim_steady_rate( "tf_bot_sniper_aim_steady_rate", "10", FCVAR_CHEAT );
+ConVar tf_bot_debug_sniper( "tf_bot_debug_sniper", "0", FCVAR_CHEAT );
+ConVar tf_bot_fire_weapon_min_time( "tf_bot_fire_weapon_min_time", "1", FCVAR_CHEAT );
+ConVar tf_bot_taunt_victim_chance( "tf_bot_taunt_victim_chance", "20" ); // community requested this not be a cheat cvar
+
+ConVar tf_bot_notice_backstab_chance( "tf_bot_notice_backstab_chance", "25", FCVAR_CHEAT );
+ConVar tf_bot_notice_backstab_min_range( "tf_bot_notice_backstab_min_range", "100", FCVAR_CHEAT );
+ConVar tf_bot_notice_backstab_max_range( "tf_bot_notice_backstab_max_range", "750", FCVAR_CHEAT );
+
+ConVar tf_bot_arrow_elevation_rate( "tf_bot_arrow_elevation_rate", "0.0001", FCVAR_CHEAT, "When firing arrows at far away targets, this is the degree/range slope to raise our aim" );
+ConVar tf_bot_ballistic_elevation_rate( "tf_bot_ballistic_elevation_rate", "0.01", FCVAR_CHEAT, "When lobbing grenades at far away targets, this is the degree/range slope to raise our aim" );
+
+ConVar tf_bot_hitscan_range_limit( "tf_bot_hitscan_range_limit", "1800", FCVAR_CHEAT );
+
+ConVar tf_bot_always_full_reload( "tf_bot_always_full_reload", "0", FCVAR_CHEAT );
+
+ConVar tf_bot_fire_weapon_allowed( "tf_bot_fire_weapon_allowed", "1", FCVAR_CHEAT, "If zero, TFBots will not pull the trigger of their weapons (but will act like they did)" );
+
+#ifdef STAGING_ONLY
+ConVar tf_bot_use_items( "tf_bot_use_items", "0", FCVAR_CHEAT, "0-100: Chance bot will use random item." );
+#endif
+
+//---------------------------------------------------------------------------------------------
+Action< CTFBot > *CTFBotMainAction::InitialContainedAction( CTFBot *me )
+{
+ return new CTFBotTacticalMonitor;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMainAction::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_lastTouch = NULL;
+ m_lastTouchTime = 0.0f;
+ m_aimErrorRadius = 0.0f;
+ m_aimErrorAngle = 0.0f;
+ m_nextDisguise = TF_CLASS_UNDEFINED;
+
+ m_yawRate = 0.0f;
+ m_priorYaw = 0.0f;
+
+ m_isWaitingForFullReload = false;
+
+ // if bot is already dead at this point, make sure it's dead
+ // check for !IsAlive because bot could be DYING
+ if ( !me->IsAlive() )
+ {
+ return ChangeTo( new CTFBotDead, "I'm actually dead" );
+ }
+
+#ifdef TF_CREEP_MODE
+ if ( TFGameRules()->IsCreepWaveMode() )
+ {
+ return ChangeTo( new CTFBotCreepWave, "I'm a creep" );
+ }
+#endif // TF_CREEP_MODE
+
+
+#ifdef STAGING_ONLY
+ if ( tf_bot_use_items.GetInt() && ( RandomInt(0, 100) <= tf_bot_use_items.GetInt() ) )
+ {
+ me->GiveRandomItem( LOADOUT_POSITION_PRIMARY );
+ me->GiveRandomItem( LOADOUT_POSITION_SECONDARY );
+ me->GiveRandomItem( LOADOUT_POSITION_MELEE );
+
+ me->GiveRandomItem( LOADOUT_POSITION_HEAD );
+ me->GiveRandomItem( LOADOUT_POSITION_MISC );
+ me->GiveRandomItem( LOADOUT_POSITION_MISC2 );
+ }
+#endif // STAGING_ONLY
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMainAction::Update( CTFBot *me, float interval )
+{
+ VPROF_BUDGET( "CTFBotMainAction::Update", "NextBot" );
+
+ if ( me->GetTeamNumber() != TF_TEAM_BLUE && me->GetTeamNumber() != TF_TEAM_RED )
+ {
+ // not on a team - do nothing
+ return Done( "Not on a playing team" );
+ }
+
+ // Should I accept taunt from my partner?
+ if ( me->FindPartnerTauntInitiator() )
+ {
+ return SuspendFor( new CTFBotTaunt, "Responding to teammate partner taunt" );
+ }
+
+ // make sure our vision FOV matches the player's
+ me->GetVisionInterface()->SetFieldOfView( me->GetFOV() );
+
+ // teammates in training have infinite ammo
+ if ( TFGameRules()->IsInTraining() && me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ me->GiveAmmo( 1000, TF_AMMO_METAL, true );
+ }
+
+ // track aim velocity ourselves, since body aim "steady" is too loose
+ float deltaYaw = me->EyeAngles().y - m_priorYaw;
+ m_yawRate = fabs( deltaYaw / ( interval + 0.0001f ) );
+ m_priorYaw = me->EyeAngles().y;
+
+ if ( m_yawRate < tf_bot_sniper_aim_steady_rate.GetFloat() )
+ {
+ if ( !m_steadyTimer.HasStarted() )
+ m_steadyTimer.Start();
+
+// if ( tf_bot_debug_sniper.GetBool() )
+// {
+// DevMsg( "%3.2f: STEADY\n", gpGlobals->curtime );
+// }
+ }
+ else
+ {
+ m_steadyTimer.Invalidate();
+
+// if ( tf_bot_debug_sniper.GetBool() )
+// {
+// DevMsg( "%3.2f: Yaw rate = %3.2f\n", gpGlobals->curtime, m_yawRate );
+// }
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ // infinite ammo
+ // me->GiveAmmo( 100, TF_AMMO_PRIMARY, true );
+ // me->GiveAmmo( 100, TF_AMMO_SECONDARY, true );
+ // This resets the Sandman
+ //me->GiveAmmo( 100, TF_AMMO_GRENADES1, true );
+ // This resets the Bonk drink meter...
+ //me->GiveAmmo( 100, TF_AMMO_GRENADES2, true );
+ me->GiveAmmo( 100, TF_AMMO_METAL, true );
+
+ me->m_Shared.AddToSpyCloakMeter( 100.0f );
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
+
+ if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
+ {
+ // invading bots get uber while they leave their spawn so they don't drop their cash where players can't pick it up
+ me->m_Shared.AddCond( TF_COND_INVULNERABLE, 0.5f );
+ me->m_Shared.AddCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED, 0.5f );
+ me->m_Shared.AddCond( TF_COND_INVULNERABLE_WEARINGOFF, 0.5f );
+ }
+
+ // watch for bots that have fallen through the ground
+ if ( myArea && myArea->GetZ( me->GetAbsOrigin() ) - me->GetAbsOrigin().z > 100.0f )
+ {
+ if ( !m_undergroundTimer.HasStarted() )
+ {
+ m_undergroundTimer.Start();
+ }
+ else if ( m_undergroundTimer.IsGreaterThen( 3.0f ) )
+ {
+ UTIL_LogPrintf( "\"%s<%i><%s><%s>\" underground (position \"%3.2f %3.2f %3.2f\")\n",
+ me->GetPlayerName(),
+ me->GetUserID(),
+ me->GetNetworkIDString(),
+ me->GetTeam()->GetName(),
+ me->GetAbsOrigin().x, me->GetAbsOrigin().y, me->GetAbsOrigin().z );
+
+ // teleport bot to a reasonable place
+ me->SetAbsOrigin( myArea->GetCenter() );
+ }
+ }
+ else
+ {
+ m_undergroundTimer.Invalidate();
+ }
+
+ if ( me->ShouldAutoJump() )
+ {
+ me->GetLocomotionInterface()->Jump();
+ }
+ }
+
+ // spies always want to be disguised
+ if ( !me->IsFiringWeapon() && !me->m_Shared.InCond( TF_COND_DISGUISED ) && !me->m_Shared.InCond( TF_COND_DISGUISING ) )
+ {
+ if ( me->CanDisguise() )
+ {
+ if ( m_nextDisguise == TF_CLASS_UNDEFINED )
+ {
+ if ( me->IsDifficulty( CTFBot::EASY ) || me->IsDifficulty( CTFBot::NORMAL ) )
+ {
+ // disguise as a random class
+ me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 ) );
+ }
+ else
+ {
+ me->DisguiseAsMemberOfEnemyTeam();
+ }
+ }
+ else
+ {
+ // disguise as the class we just killed
+ me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), m_nextDisguise );
+ m_nextDisguise = TF_CLASS_UNDEFINED;
+ }
+ }
+ }
+
+ me->EquipRequiredWeapon();
+
+ me->UpdateLookingAroundForEnemies();
+ FireWeaponAtEnemy( me );
+ Dodge( me );
+
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ // dont auto reload, so we fire stickies fast
+ me->SetAutoReload( false );
+ }
+ else
+ {
+ // reload weapons
+ me->SetAutoReload( true );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult<CTFBot> CTFBotMainAction::OnKilled( CTFBot *me, const CTakeDamageInfo& info )
+{
+ return TryChangeTo( new CTFBotDead, RESULT_CRITICAL, "I died!" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMainAction::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
+{
+ CBaseObject *obj = dynamic_cast< CBaseObject * >( info.GetInflictor() );
+
+ // if an object hurt me, it must be a sentry
+ CBaseEntity *subject = obj ? obj : info.GetAttacker();
+
+ // notice the gunfire - needed for sentry guns, which don't go through the player OnWeaponFired() system
+ me->GetVisionInterface()->AddKnownEntity( subject );
+
+ if ( info.GetInflictor() && info.GetInflictor()->GetTeamNumber() != me->GetTeamNumber() )
+ {
+ CObjectSentrygun *sentrygun = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
+
+ if ( sentrygun )
+ {
+ // we were injured by an enemy sentry - remember it
+ me->RememberEnemySentry( sentrygun, me->GetAbsOrigin() );
+ }
+
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB )
+ {
+ // backstabs that don't kill me make me mad
+ me->DelayedThreatNotice( info.GetInflictor(), 0.5f );
+
+ // chance of nearby friends noticing the backstab
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ float minRange = tf_bot_notice_backstab_min_range.GetFloat();
+ float maxRange = tf_bot_notice_backstab_max_range.GetFloat();
+ float deltaRange = maxRange - minRange;
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFBot *bot = ToTFBot( playerVector[i] );
+ if ( bot )
+ {
+ if ( !me->IsSelf( bot ) )
+ {
+ float range = me->GetRangeTo( bot );
+
+ if ( range > maxRange )
+ {
+ // too far away to notice
+ continue;
+ }
+
+ int noticeChance = tf_bot_notice_backstab_chance.GetInt();
+
+ if ( range > minRange )
+ {
+ // scale notice chance down to zero at max range
+ noticeChance *= ( range - minRange ) / deltaRange;
+ }
+
+ if ( RandomInt( 0, 100 ) < noticeChance )
+ {
+ bot->DelayedThreatNotice( info.GetInflictor(), 0.5f );
+ }
+ }
+ }
+ }
+ }
+ else if ( info.GetAttacker() && ( info.GetDamageType() & DMG_CRITICAL ) && ( info.GetDamageType() & DMG_BURN ) )
+ {
+ // Notice anyone nearby hitting us with crit fire (i.e. Backburner)
+ if ( me->GetRangeTo( info.GetAttacker() ) < tf_bot_notice_backstab_max_range.GetFloat() )
+ {
+ me->DelayedThreatNotice( info.GetAttacker(), 0.5f );
+ }
+ }
+ }
+
+
+#ifdef UNNEEDED // known entity/listening to gunfire handles this without insta-turn
+
+ if ( false && !me->IsSelf( info.GetAttacker() ) )
+ {
+ // hack to stop engineers from looking away from healing their sentry
+ if ( !me->IsPlayerClass( TF_CLASS_ENGINEER ) && !me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ CBaseObject *obj = dynamic_cast< CBaseObject * >( info.GetInflictor() );
+
+ // if an object hurt me, it must be a sentry
+ CBaseEntity *subject = obj ? obj : info.GetAttacker();
+
+ if ( !me->GetVisionInterface()->IsInFieldOfView( subject ) )
+ {
+ // something out of my field of view hurt me - look around for it
+ // turn right or left, since player's damage indicators tell them which way
+ Vector forward, right;
+ me->EyeVectors( &forward, &right );
+
+ Vector toAttacker = subject->EyePosition() - me->EyePosition();
+ Vector newForward;
+ float error = 1.0f; RandomFloat( -1.0f, 1.0f );
+
+ if ( DotProduct( right, toAttacker ) > 0.0f )
+ {
+ newForward = error * forward + right;
+ }
+ else
+ {
+ newForward = error * forward - right;
+ }
+
+ me->GetBodyInterface()->AimHeadTowards( me->EyePosition() + 100.0f * newForward, IBody::IMPORTANT, RandomFloat( 0.5f, 1.0f ), NULL, "Something hurt me!" );
+ }
+ }
+ }
+#endif // _DEBUG
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMainAction::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ if ( other && !other->IsSolidFlagSet( FSOLID_NOT_SOLID ) && !other->IsWorld() && !other->IsPlayer() )
+ {
+ m_lastTouch = other;
+ m_lastTouchTime = gpGlobals->curtime;
+
+ // Mini-bosses destroy non-Sentrygun objects they bump into (ie: Dispensers)
+ if ( TFGameRules()->IsMannVsMachineMode() && me->IsMiniBoss() )
+ {
+ if ( other->IsBaseObject() )
+ {
+ CBaseObject *pObject = assert_cast< CBaseObject* >( other );
+ if ( pObject->GetType() != OBJ_SENTRYGUN || pObject->IsMiniBuilding() )
+ {
+ int damage = MAX( other->GetMaxHealth(), other->GetHealth() );
+
+ Vector toVictim = other->WorldSpaceCenter() - me->WorldSpaceCenter();
+
+ CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
+ CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
+ other->TakeDamage( info );
+ }
+ }
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+class BlockOverlappingAreaScan
+{
+public:
+ BlockOverlappingAreaScan( int teamID, CBaseEntity *blocker )
+ {
+ m_teamID = teamID;
+ m_blocker = blocker;
+ }
+
+ bool operator() ( CNavArea *baseArea )
+ {
+ CTFNavArea *area = static_cast< CTFNavArea * >( baseArea );
+
+ area->SetAttributeTF( TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE );
+
+ return true;
+ }
+
+ int m_teamID;
+ CBaseEntity *m_blocker;
+};
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMainAction::OnStuck( CTFBot *me )
+{
+/*
+ // if we are touching a func_door while stuck, assume the door is locked and block
+ // the nav areas underneath it until the next stage of the scenario
+ if ( m_lastTouch != NULL && gpGlobals->curtime - m_lastTouchTime < 2.0f )
+ {
+ if ( FClassnameIs( m_lastTouch, "func_door*" ) || FClassnameIs( m_lastTouch, "prop_door*" ) || FClassnameIs( m_lastTouch, "func_brush" ) )
+ {
+ Extent extent;
+ extent.Init( m_lastTouch );
+
+ BlockOverlappingAreaScan block( me->GetTeamNumber(), m_lastTouch );
+ TheNavMesh->ForAllAreasOverlappingExtent( block, extent );
+ }
+ }
+*/
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( me->m_Shared.InCond( TF_COND_MVM_BOT_STUN_RADIOWAVE ) )
+ {
+ // bot is stunned, not stuck
+ return TryContinue();
+ }
+
+ if ( m_lastTouch != NULL && gpGlobals->curtime - m_lastTouchTime < 2.0f )
+ {
+ if ( m_lastTouch->IsBaseObject() && dynamic_cast< CObjectSentrygun * >( m_lastTouch.Get() ) == NULL )
+ {
+ // we are stuck on a teleporter or dispenser - destroy it!
+ int damage = MAX( m_lastTouch->GetMaxHealth(), m_lastTouch->GetHealth() );
+
+ Vector toVictim = m_lastTouch->WorldSpaceCenter() - me->WorldSpaceCenter();
+
+ CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
+ CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
+ m_lastTouch->TakeDamage( info );
+ }
+ }
+ }
+
+ UTIL_LogPrintf( "\"%s<%i><%s><%s>\" stuck (position \"%3.2f %3.2f %3.2f\") (duration \"%3.2f\") ",
+ me->GetPlayerName(),
+ me->GetUserID(),
+ me->GetNetworkIDString(),
+ me->GetTeam()->GetName(),
+ me->GetAbsOrigin().x, me->GetAbsOrigin().y, me->GetAbsOrigin().z,
+ me->GetLocomotionInterface()->GetStuckDuration() );
+
+ const PathFollower *path = me->GetCurrentPath();
+ if ( path && path->GetCurrentGoal() )
+ {
+ UTIL_LogPrintf( " path_goal ( \"%3.2f %3.2f %3.2f\" )\n",
+ path->GetCurrentGoal()->pos.x,
+ path->GetCurrentGoal()->pos.y,
+ path->GetCurrentGoal()->pos.z );
+ }
+ else
+ {
+ UTIL_LogPrintf( " path_goal ( \"NULL\" )\n" );
+ }
+
+ me->GetLocomotionInterface()->Jump();
+
+ if ( RandomInt( 0, 100 ) < 50 )
+ {
+ me->PressLeftButton();
+ }
+ else
+ {
+ me->PressRightButton();
+ }
+
+/*
+ if ( me->GetLocomotionInterface()->GetStuckDuration() > 3.0f )
+ {
+ // stuck for too long, do something drastic
+ // warp to the our next path goal
+ if ( me->GetCurrentPath() && me->GetCurrentPath()->GetCurrentGoal() )
+ {
+ me->SetAbsOrigin( me->GetCurrentPath()->GetCurrentGoal()->pos + Vector( 0, 0, StepHeight ) );
+
+ UTIL_LogPrintf( "%3.2f: TFBot '%s' stuck for too long - slammed to goal position. Entindex = %d.\n", gpGlobals->curtime, me->GetPlayerName(), me->entindex() );
+ }
+ }
+*/
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMainAction::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+{
+ // make sure we forget about this guy
+ me->GetVisionInterface()->ForgetEntity( victim );
+
+ bool do_taunt = victim && victim->IsPlayer();
+
+#ifdef STAGING_ONLY
+ if ( !do_taunt )
+ {
+ // If bots are using items, go ahead and let bots taunt other bots.
+ do_taunt = victim && tf_bot_use_items.GetBool();
+ }
+#endif
+
+ if ( do_taunt )
+ {
+ CTFPlayer *playerVictim = ToTFPlayer( victim );
+
+ me->ForgetSpy( playerVictim );
+
+ if ( me->IsSelf( info.GetAttacker() ) && me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ // disguise as our victim
+ m_nextDisguise = playerVictim->GetPlayerClass()->GetClassIndex();
+ }
+
+ if ( !ToTFPlayer( victim )->IsBot() && me->IsEnemy( victim ) && me->IsSelf( info.GetAttacker() ) )
+ {
+ bool isTaunting = !me->HasTheFlag() && RandomFloat( 0.0f, 100.0f ) <= tf_bot_taunt_victim_chance.GetFloat();
+
+ if ( TFGameRules()->IsMannVsMachineMode() && me->IsMiniBoss() )
+ {
+ // Bosses don't taunt puny humans
+ isTaunting = false;
+ }
+
+ if ( isTaunting )
+ {
+ // we just killed a human - taunt!
+ return TrySuspendFor( new CTFBotTaunt, RESULT_IMPORTANT, "Taunting our victim" );
+ }
+ }
+ }
+
+ // if we saw a friend killed by a sentry, kill the sentry
+ if ( victim && victim->IsPlayer() && me->IsFriend( victim ) && info.GetInflictor() && me->IsEnemy( info.GetInflictor() ) && me->IsLineOfSightClear( victim->WorldSpaceCenter() ) )
+ {
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
+
+ if ( sentry && !me->GetEnemySentry() )
+ {
+ me->RememberEnemySentry( sentry, victim->GetAbsOrigin() );
+ }
+ }
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Given a subject, return the world space position we should aim at
+ */
+Vector CTFBotMainAction::SelectTargetPoint( const INextBot *meBot, const CBaseCombatCharacter *subject ) const
+{
+ CTFBot *me = (CTFBot *)meBot->GetEntity();
+
+ if ( subject )
+ {
+ // if our subject is a sentry gun, aim at it's "eye position", which is updated based on the sentry's level
+ if ( subject->IsBaseObject() )
+ {
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( const_cast< CBaseCombatCharacter * >( subject ) );
+ if ( sentry )
+ {
+ // Aim a bit lower than eye height to ensure we hit the body of the sentry
+ return sentry->GetAbsOrigin() + 0.5f * sentry->GetViewOffset();
+ }
+ }
+
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ // lead our target and aim for the feet with the rocket launcher
+ if ( !me->IsDifficulty( CTFBot::EASY ) )
+ {
+ if ( myWeapon->GetWeaponID() == TF_WEAPON_ROCKETLAUNCHER )
+ {
+ // if they are above us, don't aim for the feet
+ const float aboveTolerance = 30.0f;
+ if ( subject->GetAbsOrigin().z - aboveTolerance > me->GetAbsOrigin().z )
+ {
+ if ( me->GetVisionInterface()->IsAbleToSee( subject->GetAbsOrigin(), IVision::DISREGARD_FOV ) )
+ return subject->GetAbsOrigin();
+
+ if ( me->GetVisionInterface()->IsAbleToSee( subject->WorldSpaceCenter(), IVision::DISREGARD_FOV ) )
+ return subject->WorldSpaceCenter();
+
+ return subject->EyePosition();
+ }
+
+ // aim at the ground under the subject
+ if ( subject->GetGroundEntity() == NULL )
+ {
+ // they are airborne, find the ground underneath them, if they aren't too high
+ trace_t result;
+ UTIL_TraceLine( subject->GetAbsOrigin(), subject->GetAbsOrigin() + Vector( 0, 0, -200 ), MASK_SOLID, subject, COLLISION_GROUP_NONE, &result );
+ if ( result.DidHit() )
+ {
+ return result.endpos;
+ }
+ }
+
+ // aim at their feet
+
+ // lead our target
+ const float missileSpeed = 1100.0f;
+ float rangeBetween = me->GetRangeTo( subject->GetAbsOrigin() );
+
+ const float veryCloseRange = 150.0f;
+ if ( rangeBetween > veryCloseRange )
+ {
+ float timeToTravel = rangeBetween / missileSpeed;
+
+ Vector targetPos = subject->GetAbsOrigin() + timeToTravel * subject->GetAbsVelocity();
+
+ if ( me->GetVisionInterface()->IsAbleToSee( targetPos, IVision::DISREGARD_FOV ) )
+ return targetPos;
+
+ // try their head and hope
+ return subject->EyePosition() + timeToTravel * subject->GetAbsVelocity();
+ }
+
+ return subject->EyePosition();
+ }
+ else if ( myWeapon->GetWeaponID() == TF_WEAPON_COMPOUND_BOW )
+ {
+ // lead our target
+ const float missileSpeed = ( (CTFCompoundBow *)myWeapon )->GetProjectileSpeed();
+ float rangeBetween = me->GetRangeTo( subject->GetAbsOrigin() );
+
+ const float veryCloseRange = 150.0f;
+ if ( rangeBetween > veryCloseRange )
+ {
+ float timeToTravel = rangeBetween / missileSpeed;
+
+ Vector targetSpot = me->IsDifficulty( CTFBot::NORMAL ) ? subject->WorldSpaceCenter() : subject->EyePosition();
+
+ Vector leadTargetSpot = targetSpot + timeToTravel * subject->GetAbsVelocity();
+
+ // elevate our aim based on range
+ float elevationAngle = rangeBetween * tf_bot_arrow_elevation_rate.GetFloat();
+
+ if ( elevationAngle > 45.0f )
+ {
+ // ballistic range maximum at 45 degrees - aiming higher would decrease the range
+ elevationAngle = 45.0f;
+ }
+
+ float s, c;
+ FastSinCos( elevationAngle * M_PI / 180.0f, &s, &c );
+
+ if ( c > 0.0f )
+ {
+ float elevation = rangeBetween * s / c;
+ return leadTargetSpot + Vector( 0, 0, elevation );
+ }
+
+ return leadTargetSpot;
+ }
+
+ return subject->EyePosition();
+ }
+ }
+
+ if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ if ( m_aimAdjustTimer.IsElapsed() )
+ {
+ m_aimAdjustTimer.Start( RandomFloat( 0.5f, 1.5f ) );
+
+ m_aimErrorAngle = RandomFloat( -M_PI, M_PI );
+ m_aimErrorRadius = RandomFloat( 0.0f, tf_bot_sniper_aim_error.GetFloat() );
+ }
+
+ Vector toThreat = subject->GetAbsOrigin() - me->GetAbsOrigin();
+ float threatRange = toThreat.NormalizeInPlace();
+
+ float s1, c1;
+ FastSinCos( m_aimErrorRadius, &s1, &c1 );
+
+ float error = threatRange * s1;
+
+ Vector up( 0, 0, 1 );
+ Vector side;
+ CrossProduct( toThreat, up, side );
+
+ float s, c;
+ FastSinCos( m_aimErrorAngle, &s, &c );
+
+ // aim a bit lower than the head - the imperfections may yet give us a headshot
+ Vector desiredAimSpot;
+
+ switch( me->GetDifficulty() )
+ {
+ case CTFBot::EXPERT:
+ case CTFBot::HARD:
+ // aim for the head - reaction times will differentiate the skill levels
+ desiredAimSpot = subject->EyePosition();
+ break;
+
+ default:
+ Assert(0);
+ case CTFBot::NORMAL:
+ desiredAimSpot = ( subject->EyePosition() + subject->EyePosition() + subject->WorldSpaceCenter() ) / 3.0f;
+ break;
+
+ case CTFBot::EASY:
+ desiredAimSpot = subject->WorldSpaceCenter();
+ break;
+ }
+
+ Vector imperfectAimSpot = desiredAimSpot + error * s * up + error * c * side;
+
+ return imperfectAimSpot;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_GRENADELAUNCHER ) ||
+ myWeapon->IsWeapon( TF_WEAPON_PIPEBOMBLAUNCHER ) )
+ {
+ Vector toThreat = subject->GetAbsOrigin() - me->GetAbsOrigin();
+ float threatRange = toThreat.NormalizeInPlace();
+ float elevationAngle = threatRange * tf_bot_ballistic_elevation_rate.GetFloat();
+
+ if ( elevationAngle > 45.0f )
+ {
+ // ballistic range maximum at 45 degrees - aiming higher would decrease the range
+ elevationAngle = 45.0f;
+ }
+
+ float s, c;
+ FastSinCos( elevationAngle * M_PI / 180.0f, &s, &c );
+
+ if ( c > 0.0f )
+ {
+ float elevation = threatRange * s / c;
+ return subject->WorldSpaceCenter() + Vector( 0, 0, elevation );
+ }
+ }
+ }
+ }
+
+ // aim for the center of the object (ie: sentry gun)
+ return subject->WorldSpaceCenter();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Allow bot to approve of positions game movement tries to put him into.
+ * This is most useful for bots derived from CBasePlayer that go through
+ * the player movement system.
+ */
+QueryResultType CTFBotMainAction::IsPositionAllowed( const INextBot *me, const Vector &pos ) const
+{
+ return ANSWER_YES;
+
+ // This is causing bots to get hung up on drop-downs, particularly in MvM. MSB 6/11/2012
+ /*
+ if ( me->GetLocomotionInterface()->IsScrambling() )
+ {
+ // anything goes when we're in the air/etc
+ return ANSWER_YES;
+ }
+
+ // if we are at a DROP_DOWN segment of our path, allow us to drop
+ const PathFollower *path = me->GetCurrentPath();
+ if ( path && path->IsValid() )
+ {
+ const Path::Segment *goal = path->GetCurrentGoal();
+ if ( goal )
+ {
+ if ( goal->type == Path::DROP_DOWN || me->GetLocomotionInterface()->GetFeet().z - goal->pos.z >= me->GetLocomotionInterface()->GetMaxJumpHeight() )
+ {
+ // our goal requires us to drop down
+ return ANSWER_YES;
+ }
+ }
+ }
+
+ // do not fall off someplace we can't get back up from!
+ trace_t result;
+ NextBotTraceFilterIgnoreActors filter( me->GetEntity(), COLLISION_GROUP_PLAYER_MOVEMENT );
+ ILocomotion *mover = me->GetLocomotionInterface();
+ IBody *body = me->GetBodyInterface();
+
+ // slightly smaller to allow skirting the edge
+ float halfWidth = 0.4f * body->GetHullWidth();
+
+ mover->TraceHull( pos + Vector( 0, 0, mover->GetStepHeight() ), // start up a bit to handle rough terrain
+ pos + Vector( 0, 0, -mover->GetMaxJumpHeight() ),
+ Vector( -halfWidth, -halfWidth, 0 ),
+ Vector( halfWidth, halfWidth, body->GetHullHeight() ),
+ body->GetSolidMask(),
+ &filter,
+ &result );
+
+ if ( result.DidHit() )
+ {
+ // there is ground safe beneath us
+ return ANSWER_YES;
+ }
+
+ return ANSWER_NO;
+ */
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBotMainAction::IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const
+{
+ CTFBot *me = GetActor();
+
+ // the TFBot code assumes the subject is always "me"
+ if ( !me || !me->IsSelf( subject ) )
+ return false;
+
+ if ( me->InSameTeam( threat->GetEntity() ) )
+ return false;
+
+ if ( !threat->GetEntity()->IsAlive() )
+ return false;
+
+ if ( !threat->IsVisibleRecently() )
+ return false;
+
+ // if they can't hurt me, they aren't an immediate threat
+ if ( !me->IsLineOfFireClear( threat->GetEntity() ) )
+ return false;
+
+ CTFPlayer *threatPlayer = ToTFPlayer( threat->GetEntity() );
+
+ Vector to = me->GetAbsOrigin() - threat->GetLastKnownPosition();
+ float threatRange = to.NormalizeInPlace();
+
+ const float nearbyRange = 500.0f;
+ if ( threatRange < nearbyRange )
+ {
+ // very near threats are always immediately dangerous
+ return true;
+ }
+
+ // mid-to-far away threats
+
+ if ( me->IsThreatFiringAtMe( threat->GetEntity() ) )
+ {
+ // distant threat firing on me - an immediate threat whether in my FOV or not
+ return true;
+ }
+
+ if ( threatPlayer == NULL )
+ {
+ // non-player threat - sentry guns
+
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat->GetEntity() );
+ if ( sentry && !sentry->HasSapper() && !sentry->IsPlasmaDisabled() && !sentry->IsPlacing() )
+ {
+ // are we in range? (or will be very soon)
+ if ( threatRange < 1.5f * SENTRY_MAX_RANGE )
+ {
+ // is it pointing at us?
+ Vector sentryForward;
+ AngleVectors( sentry->GetTurretAngles(), &sentryForward );
+
+ if ( DotProduct( to, sentryForward ) > 0.8f )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // does a sniper have a shot on me?
+ if ( threatPlayer->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ Vector sniperForward;
+ threatPlayer->EyeVectors( &sniperForward );
+
+ if ( DotProduct( to, sniperForward ) > 0.0f )
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ if ( me->GetDifficulty() > CTFBot::NORMAL && threatPlayer->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // always try to kill these guys first
+ return true;
+ }
+
+ if ( me->GetDifficulty() > CTFBot::NORMAL && threatPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // take out engineers to let the team kill their sentry nests
+ return true;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+const CKnownEntity *CTFBotMainAction::SelectCloserThreat( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const
+{
+ float rangeSq1 = me->GetRangeSquaredTo( threat1->GetEntity() );
+ float rangeSq2 = me->GetRangeSquaredTo( threat2->GetEntity() );
+
+ if ( rangeSq1 < rangeSq2 )
+ return threat1;
+
+ return threat2;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// If the given threat is being healed by a Medic, return the Medic, otherwise just
+// return the threat.
+const CKnownEntity *CTFBotMainAction::GetHealerOfThreat( const CKnownEntity *threat ) const
+{
+ if ( !threat || !threat->GetEntity() )
+ return NULL;
+
+ CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() );
+ if ( playerThreat )
+ {
+ for( int i=0; i<playerThreat->m_Shared.GetNumHealers(); ++i )
+ {
+ CBaseEntity *healer = playerThreat->m_Shared.GetHealerByIndex( i );
+ CTFPlayer *playerHealer = ToTFPlayer( healer );
+
+ if ( playerHealer )
+ {
+ const CKnownEntity *knownHealer = GetActor()->GetVisionInterface()->GetKnown( playerHealer );
+
+ if ( knownHealer && knownHealer->IsVisibleInFOVNow() )
+ {
+ return knownHealer;
+ }
+ }
+ }
+ }
+
+ return threat;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+const CKnownEntity *CTFBotMainAction::SelectMoreDangerousThreat( const INextBot *meBot,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const
+{
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+
+ // determine the actual threat
+ const CKnownEntity *threat = SelectMoreDangerousThreatInternal( me, subject, threat1, threat2 );
+
+ if ( me->IsDifficulty( CTFBot::EASY ) )
+ {
+ return threat;
+ }
+
+ if ( me->IsDifficulty( CTFBot::NORMAL ) && me->TransientlyConsistentRandomValue() < 0.5f )
+ {
+ return threat;
+ }
+
+ // smarter bots first aim at the Medic healing our dangerous target
+ return GetHealerOfThreat( threat );
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Given a pair of enemy players, return the closest Spy of those two, or NULL if neither is a Spy
+const CKnownEntity *SelectClosestSpyToMe( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 )
+{
+ CTFPlayer *playerThreat1 = ToTFPlayer( threat1->GetEntity() );
+ CTFPlayer *playerThreat2 = ToTFPlayer( threat2->GetEntity() );
+
+ if ( playerThreat1 && playerThreat1->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( playerThreat2 && playerThreat2->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( me->GetRangeSquaredTo( playerThreat1 ) < me->GetRangeSquaredTo( playerThreat2 ) )
+ return threat1;
+
+ return threat2;
+ }
+
+ return threat1;
+ }
+ else if ( playerThreat2 && playerThreat2->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return threat2;
+ }
+
+ return NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
+const CKnownEntity *CTFBotMainAction::SelectMoreDangerousThreatInternal( const INextBot *meBot,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const
+{
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+ const CKnownEntity *closerThreat = SelectCloserThreat( me, threat1, threat2 );
+
+ if ( me->HasWeaponRestriction( CTFBot::MELEE_ONLY ) )
+ {
+ // melee only bots just use closest threat
+ return closerThreat;
+ }
+
+ // close range sentries are the most dangerous of all
+ bool shouldFearSentryGuns = true;
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ // MvM bots are not afraid of sentry guns and treat them like other enemy players
+ shouldFearSentryGuns = false;
+ }
+
+ if ( shouldFearSentryGuns )
+ {
+ CObjectSentrygun *sentry1 = NULL;
+ if ( threat1->IsVisibleRecently() && !threat1->GetEntity()->IsPlayer() )
+ {
+ sentry1 = dynamic_cast< CObjectSentrygun * >( threat1->GetEntity() );
+ }
+
+ CObjectSentrygun *sentry2 = NULL;
+ if ( threat2->IsVisibleRecently() && !threat2->GetEntity()->IsPlayer() )
+ {
+ sentry2 = dynamic_cast< CObjectSentrygun * >( threat2->GetEntity() );
+ }
+
+ if ( sentry1 && me->IsRangeLessThan( sentry1, SENTRY_MAX_RANGE ) && !sentry1->HasSapper() && !sentry1->IsPlasmaDisabled() && !sentry1->IsPlacing() )
+ {
+ // in range of a visible sentry!
+ if ( sentry2 && me->IsRangeLessThan( sentry2, SENTRY_MAX_RANGE ) && !sentry2->HasSapper() && !sentry2->IsPlasmaDisabled() && !sentry2->IsPlacing() )
+ {
+ // in range of two visible sentries! we're probably dead meat at this point.
+ // default is choose closest
+ return closerThreat;
+ }
+
+ return threat1;
+ }
+
+ if ( sentry2 && me->IsRangeLessThan( sentry2, SENTRY_MAX_RANGE ) && !sentry2->HasSapper() && !sentry2->IsPlasmaDisabled() && !sentry2->IsPlacing() )
+ {
+ // in range of a visible sentry!
+ return threat2;
+ }
+ }
+
+ // enforce Spy hatred in MvM mode
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ const float spyHateRadius = 1000.0f;
+
+ const CKnownEntity *spyThreat = SelectClosestSpyToMe( me, threat1, threat2 );
+ if ( spyThreat && me->IsRangeLessThan( spyThreat->GetEntity(), spyHateRadius ) )
+ {
+ return spyThreat;
+ }
+ }
+
+
+ bool isImmediateThreat1 = IsImmediateThreat( subject, threat1 );
+ bool isImmediateThreat2 = IsImmediateThreat( subject, threat2 );
+
+ if ( isImmediateThreat1 && !isImmediateThreat2 )
+ {
+ return threat1;
+ }
+ else if ( !isImmediateThreat1 && isImmediateThreat2 )
+ {
+ return threat2;
+ }
+ else if ( !isImmediateThreat1 && !isImmediateThreat2 )
+ {
+ // neither threat is immediately dangerous - use closest
+ return closerThreat;
+ }
+
+ // both threats are immediately dangerous!
+ // check if any are extremely dangerous
+
+ const CKnownEntity *spyThreat = SelectClosestSpyToMe( me, threat1, threat2 );
+ if ( spyThreat )
+ {
+ return spyThreat;
+ }
+
+ // choose most recent attacker (assume an enemy firing their weapon at us has attacked us)
+ if ( me->IsThreatFiringAtMe( threat1->GetEntity() ) )
+ {
+ if ( me->IsThreatFiringAtMe( threat2->GetEntity() ) )
+ {
+ // choose closest
+ return closerThreat;
+ }
+
+ return threat1;
+ }
+ else if ( me->IsThreatFiringAtMe( threat2->GetEntity() ) )
+ {
+ return threat2;
+ }
+
+ // choose closest
+ return closerThreat;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMainAction::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const
+{
+ if ( g_pPopulationManager )
+ {
+ // if I'm in my spawn room, obey the population manager's attack restrictions
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
+
+ if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
+ {
+ return g_pPopulationManager->CanBotsAttackWhileInSpawnRoom() ? ANSWER_YES : ANSWER_NO;
+ }
+ }
+
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotMainAction::ShouldHurry( const INextBot *meBot ) const
+{
+ if ( g_pPopulationManager )
+ {
+ // if I'm in my spawn room, obey the population manager's attack restrictions
+ CTFBot *me = ToTFBot( meBot->GetEntity() );
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
+
+ if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
+ {
+ if ( !g_pPopulationManager->CanBotsAttackWhileInSpawnRoom() )
+ {
+ // hurry to leave the spawn
+ return ANSWER_YES;
+ }
+ }
+ }
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMainAction::FireWeaponAtEnemy( CTFBot *me )
+{
+ if ( !me->IsAlive() )
+ return;
+
+ if ( me->HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ return;
+
+ if ( me->HasAttribute( CTFBot::IGNORE_ENEMIES ) )
+ return;
+
+ if ( me->m_Shared.InCond( TF_COND_TAUNTING ) )
+ return;
+
+ if ( !tf_bot_fire_weapon_allowed.GetBool() )
+ {
+ return;
+ }
+
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( !myWeapon )
+ return;
+
+ if ( me->IsBarrageAndReloadWeapon( myWeapon ) )
+ {
+ if ( me->HasAttribute( CTFBot::HOLD_FIRE_UNTIL_FULL_RELOAD ) || tf_bot_always_full_reload.GetBool() )
+ {
+ if ( myWeapon->Clip1() <= 0 )
+ {
+ m_isWaitingForFullReload = true;
+ }
+
+ if ( m_isWaitingForFullReload )
+ {
+ if ( myWeapon->Clip1() < myWeapon->GetMaxClip1() )
+ {
+ return;
+ }
+
+ // we are fully reloaded
+ m_isWaitingForFullReload = false;
+ }
+ }
+ }
+
+ if ( me->HasAttribute( CTFBot::ALWAYS_FIRE_WEAPON ) )
+ {
+ me->PressFireButton();
+ return;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ if ( myWeapon && myWeapon->IsWeapon( TF_WEAPON_MEDIGUN ) )
+ {
+ // don't interfere with medic healing behaviors
+ return;
+ }
+ }
+
+ // if we're a heavy and just saw a bad guy, keep the barrel spinning (unless we're in a hurry)
+ if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && !me->IsAmmoLow() && me->GetIntentionInterface()->ShouldHurry( me ) != ANSWER_YES )
+ {
+ const float spinTime = 3.0f;
+ if ( me->GetVisionInterface()->GetTimeSinceVisible( GetEnemyTeam( me->GetTeamNumber() ) ) < spinTime )
+ {
+ me->PressAltFireButton();
+ }
+ }
+
+ // shoot at bad guys
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ // ignore non-visible threats here so we don't force a premature weapon switch if we're doing something else
+ if ( threat == NULL || !threat->GetEntity() || !threat->IsVisibleRecently() )
+ return;
+
+ // don't shoot through windows/etc
+ if ( !me->IsLineOfFireClear( threat->GetEntity()->EyePosition() ) )
+ {
+ if ( !me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter() ) )
+ {
+ if ( !me->IsLineOfFireClear( threat->GetEntity()->GetAbsOrigin() ) )
+ return;
+ }
+ }
+
+ // if our target is uber'd, most weapons are useless - unless we're in MvM, where invuln tanking is valuable
+ if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() )
+ {
+ CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() );
+ if ( playerThreat && playerThreat->m_Shared.IsInvulnerable() )
+ {
+ if ( !myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) &&
+ !myWeapon->IsWeapon( TF_WEAPON_GRENADELAUNCHER ) &&
+ !myWeapon->IsWeapon( TF_WEAPON_PIPEBOMBLAUNCHER ) &
+ !myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT ) )
+ {
+ // firing would just waste ammo, so don't
+ return;
+ }
+ }
+ }
+
+ if ( me->GetIntentionInterface()->ShouldAttack( me, threat ) == ANSWER_NO )
+ return;
+
+ if ( TFGameRules()->InSetup() )
+ {
+ // wait until the gates open
+ return;
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ if ( me->IsRangeLessThan( threat->GetEntity(), 250.0f ) )
+ {
+ me->PressFireButton();
+ }
+ return;
+ }
+
+ // limit range of hitscan weapon fire in MvM
+ if ( TFGameRules()->IsMannVsMachineMode() && !me->IsPlayerClass( TF_CLASS_SNIPER ) && me->IsHitScanWeapon( myWeapon ) )
+ {
+ if ( me->IsRangeGreaterThan( threat->GetEntity(), tf_bot_hitscan_range_limit.GetFloat() ) )
+ {
+ return;
+ }
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
+ {
+ CTFFlameThrower *pFlamethrower = assert_cast< CTFFlameThrower* >( myWeapon );
+ // watch for enemy projectiles heading our way
+ if ( pFlamethrower->CanAirBlast() && me->ShouldFireCompressionBlast() )
+ {
+ // bounce missiles with compression blast
+ me->PressAltFireButton();
+ }
+ else if ( threat->GetTimeSinceLastSeen() < 1.0f &&
+ me->IsDistanceBetweenLessThan( threat->GetEntity(), me->GetMaxAttackRange() ) )
+ {
+ me->PressFireButton( tf_bot_fire_weapon_min_time.GetFloat() );
+ }
+
+ return;
+ }
+
+ float threatRange = ( threat->GetEntity()->GetAbsOrigin() - me->GetAbsOrigin() ).Length();
+
+ // actual head aiming is handled elsewhere, just check if we're on target
+ if ( me->GetBodyInterface()->IsHeadAimingOnTarget() && threatRange < me->GetMaxAttackRange() )
+ {
+ if ( myWeapon->IsWeapon( TF_WEAPON_COMPOUND_BOW ) )
+ {
+ CTFCompoundBow *myBow = (CTFCompoundBow *)myWeapon;
+
+ if ( myBow->GetCurrentCharge() < 0.95f || !me->IsLineOfFireClear( threat->GetEntity() ) )
+ {
+ me->PressFireButton();
+ }
+ }
+ else if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ // only fire if zoomed in
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ {
+ const float reactionTime = TFGameRules()->IsMannVsMachineMode() ? 0.5f : 0.1f; // just a moment to stop headshots when obviously panning too fast to see
+ if ( m_steadyTimer.HasStarted() && m_steadyTimer.IsGreaterThen( reactionTime ) )
+ {
+ trace_t trace;
+
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ // allow bot to see through projectile shield
+ CTraceFilterIgnoreFriendlyCombatItems filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() );
+ UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 9000.0f * forward, MASK_SHOT, &filter, &trace );
+
+ if ( trace.m_pEnt == threat->GetEntity() )
+ {
+ // we're on target - fire!
+ me->PressFireButton();
+ }
+ }
+ }
+ }
+ else if ( me->IsCombatWeapon( MY_CURRENT_GUN ) )
+ {
+ if ( me->IsContinuousFireWeapon( MY_CURRENT_GUN ) )
+ {
+ // spray for a bit
+ me->PressFireButton( tf_bot_fire_weapon_min_time.GetFloat() );
+ }
+ else
+ {
+ if ( me->IsExplosiveProjectileWeapon( MY_CURRENT_GUN ) )
+ {
+ // don't fire if we're going to hit a nearby wall
+ trace_t trace;
+
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ // allow bot to see through projectile shield
+ CTraceFilterIgnoreFriendlyCombatItems filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() );
+ UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 1.1f * threatRange * forward, MASK_SHOT, &filter, &trace );
+
+ float hitRange = trace.fraction * 1.1f * threatRange;
+
+ if ( hitRange < TF_ROCKET_RADIUS )
+ {
+ // shot will impact very near us
+ if ( !trace.m_pEnt || ( trace.m_pEnt && !trace.m_pEnt->MyCombatCharacterPointer() ) )
+ {
+ // don't fire, we'd only hit the world or a non-player or non-sentry
+ return;
+ }
+ }
+ }
+
+ me->PressFireButton();
+ }
+ }
+
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Compute nearby friends influence and visible enemy influence
+ */
+class CCompareFriendFoeInfluence : public IVision::IForEachKnownEntity
+{
+public:
+ CCompareFriendFoeInfluence( CTFBot *me )
+ {
+ m_me = me;
+ m_friendScore = 0;
+ m_foeScore = 0;
+ }
+
+ virtual bool Inspect( const CKnownEntity &known )
+ {
+ if ( known.GetEntity()->IsAlive() )
+ {
+ const float nearRange = 750.0f;
+ if ( m_me->IsRangeLessThan( known.GetEntity(), nearRange ) )
+ {
+ if ( m_me->IsFriend( known.GetEntity() ) )
+ {
+ m_friendScore += m_me->GetThreatDanger( known.GetEntity()->MyCombatCharacterPointer() );
+ }
+ else if ( known.WasEverVisible() && known.GetTimeSinceLastSeen() < 3.0f && m_me->IsEnemy( known.GetEntity() ) )
+ {
+ // ignore disguised spies, etc
+ if ( m_me->GetVisionInterface()->IsIgnored( known.GetEntity() ) )
+ return true;
+
+ // only count them if they are facing me
+ if ( UTIL_IsFacingWithinTolerance( known.GetEntity(), m_me->EyePosition(), 0.5f ) )
+ {
+ m_foeScore += m_me->GetThreatDanger( known.GetEntity()->MyCombatCharacterPointer() );
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ CTFBot *m_me;
+ float m_friendScore;
+ float m_foeScore;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * If we're outnumbered, retreat and wait for backup - unless we're ubered!
+ */
+QueryResultType CTFBotMainAction::ShouldRetreat( const INextBot *bot ) const
+{
+ CTFBot *me = (CTFBot *)bot->GetEntity();
+
+ // don't retreat if we're in "melee only" mode
+ if ( TheTFBots().IsMeleeOnly() )
+ return ANSWER_NO;
+
+ // don't retreat if ubered
+ if ( me->m_Shared.IsInvulnerable() )
+ return ANSWER_NO;
+
+ // don't retreat if we're ignoring enemies
+ if ( me->HasAttribute( CTFBot::IGNORE_ENEMIES ) )
+ return ANSWER_NO;
+
+ // retreat if stunned
+ if ( me->m_Shared.IsControlStunned() || me->m_Shared.IsLoserStateStunned() )
+ return ANSWER_YES;
+
+ // don't retreat during setup time, since we're always safe
+ if ( TFGameRules()->InSetup() )
+ return ANSWER_NO;
+
+ // if we're an undercover spy, don't blow our cover
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ||
+ me->m_Shared.InCond( TF_COND_DISGUISING ) ||
+ me->m_Shared.IsStealthed() )
+ {
+ return ANSWER_NO;
+ }
+ }
+
+ CCompareFriendFoeInfluence compare( me );
+ me->GetVisionInterface()->ForEachKnownEntity( compare );
+
+ if ( compare.m_friendScore < compare.m_foeScore )
+ {
+ return ANSWER_YES;
+ }
+
+ return ANSWER_NO;
+}
+
+
+//-----------------------------------------------------------------------------------------
+void CTFBotMainAction::Dodge( CTFBot *me )
+{
+ // low-skill bots don't dodge
+ if ( me->IsDifficulty( CTFBot::EASY ) )
+ return;
+
+ // no need to dodge if we're invulnerable
+ if ( me->m_Shared.IsInvulnerable() )
+ return;
+
+ // don't dodge if we're trying to snipe
+ if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ return;
+
+ // don't dodge if we are taunting
+ if ( me->m_Shared.InCond( TF_COND_TAUNTING ) )
+ return;
+
+ // don't dodge if that ability is "turned off"
+ if ( me->HasAttribute( CTFBot::DISABLE_DODGE ) )
+ return;
+
+ // don't dodge if we're not trying to fight back
+ if ( !me->IsCombatWeapon( MY_CURRENT_GUN ) )
+ return;
+
+ // don't waste time doding if we're in a hurry
+ if ( me->GetIntentionInterface()->ShouldHurry( me ) == ANSWER_YES )
+ return;
+
+ // for now, engies don't dodge
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ return;
+
+ // disguised/cloaked spies don't dodge
+ if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ||
+ me->m_Shared.InCond( TF_COND_DISGUISING ) ||
+ me->m_Shared.IsStealthed() )
+ {
+ return;
+ }
+
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ return;
+#endif // TF_RAID_MODE
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ bool isShotClear = true;
+
+ CTFWeaponBase *myGun = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myGun && myGun->IsWeapon( TF_WEAPON_COMPOUND_BOW ) )
+ {
+ CTFCompoundBow *myBow = (CTFCompoundBow *)myGun;
+ if ( myBow->GetCurrentCharge() > 0.0f )
+ {
+ // we're drawing back our bow - hold still
+ return;
+ }
+
+ // if we don't have a clear shot, dodge around until we do
+ isShotClear = true;
+ }
+ else
+ {
+ isShotClear = me->IsLineOfFireClear( threat->GetLastKnownPosition() );
+ }
+
+ // don't dodge if they can't hit us
+ if ( !isShotClear )
+ return;
+
+ Vector forward;
+ me->EyeVectors( &forward );
+ Vector left( -forward.y, forward.x, 0.0f );
+ left.NormalizeInPlace();
+
+ const float sideStepSize = 25.0f;
+
+ int rnd = RandomInt( 0, 100 );
+ if ( rnd < 33 )
+ {
+ if ( !me->GetLocomotionInterface()->HasPotentialGap( me->GetAbsOrigin(), me->GetAbsOrigin() + sideStepSize * left ) )
+ {
+ me->PressLeftButton();
+ }
+ }
+ else if ( rnd > 66 )
+ {
+ if ( !me->GetLocomotionInterface()->HasPotentialGap( me->GetAbsOrigin(), me->GetAbsOrigin() - sideStepSize * left ) )
+ {
+ me->PressRightButton();
+ }
+ }
+ }
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_behavior.h b/game/server/tf/bot/behavior/tf_bot_behavior.h
new file mode 100644
index 0000000..2a09159
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_behavior.h
@@ -0,0 +1,76 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_behavior.h
+// Team Fortress NextBot behaviors
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_BEHAVIOR_H
+#define TF_BOT_BEHAVIOR_H
+
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotMainAction : public Action< CTFBot >
+{
+public:
+ virtual Action< CTFBot > *InitialContainedAction( CTFBot *me );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info );
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+
+ virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info );
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const;
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at
+ virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const;
+
+ 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 "MainAction"; };
+
+private:
+ CountdownTimer m_reloadTimer;
+ mutable CountdownTimer m_aimAdjustTimer;
+ mutable float m_aimErrorRadius;
+ mutable float m_aimErrorAngle;
+
+ float m_yawRate;
+ float m_priorYaw;
+ IntervalTimer m_steadyTimer;
+
+ int m_nextDisguise;
+
+ bool m_isWaitingForFullReload;
+
+ void FireWeaponAtEnemy( CTFBot *me );
+
+ CHandle< CBaseEntity > m_lastTouch;
+ float m_lastTouchTime;
+
+ bool IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const;
+ const CKnownEntity *SelectCloserThreat( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const;
+ const CKnownEntity *GetHealerOfThreat( const CKnownEntity *threat ) const;
+
+ const CKnownEntity *SelectMoreDangerousThreatInternal( const INextBot *me,
+ const CBaseCombatCharacter *subject,
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const;
+
+
+ void Dodge( CTFBot *me );
+
+ IntervalTimer m_undergroundTimer;
+};
+
+
+
+#endif // TF_BOT_BEHAVIOR_H
diff --git a/game/server/tf/bot/behavior/tf_bot_dead.cpp b/game/server/tf/bot/behavior/tf_bot_dead.cpp
new file mode 100644
index 0000000..6e94d99
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_dead.cpp
@@ -0,0 +1,58 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_dead.cpp
+// Push up daisies
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_dead.h"
+#include "bot/behavior/tf_bot_behavior.h"
+
+#include "nav_mesh.h"
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDead::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_deadTimer.Start();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDead::Update( CTFBot *me, float interval )
+{
+ if ( me->IsAlive() )
+ {
+ // how did this happen?
+ return ChangeTo( new CTFBotMainAction, "This should not happen!" );
+ }
+
+ if ( m_deadTimer.IsGreaterThen( 5.0f ) )
+ {
+ if ( me->HasAttribute( CTFBot::REMOVE_ON_DEATH ) )
+ {
+ // remove dead bots
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) );
+ }
+ else if ( me->HasAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH ) )
+ {
+ me->ChangeTeam( TEAM_SPECTATOR, false, true );
+ return Done();
+ }
+ }
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() && me->GetTeamNumber() == TF_TEAM_RED )
+ {
+ // dead defenders go to spectator for recycling
+ me->ChangeTeam( TEAM_SPECTATOR, false, true );
+ }
+#endif // TF_RAID_MODE
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_dead.h b/game/server/tf/bot/behavior/tf_bot_dead.h
new file mode 100644
index 0000000..796db43
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_dead.h
@@ -0,0 +1,23 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_dead.h
+// Push up daisies
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_DEAD_H
+#define TF_BOT_DEAD_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotDead : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "Dead"; };
+
+private:
+ IntervalTimer m_deadTimer;
+};
+
+#endif // TF_BOT_DEAD_H
diff --git a/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp
new file mode 100644
index 0000000..e539436
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp
@@ -0,0 +1,959 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_destroy_enemy_sentry.cpp
+// Destroy an enemy sentry gun
+// Michael Booth, June 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_weaponbase_gun.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_destroy_enemy_sentry.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+#include "bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h"
+
+#include "nav_mesh.h"
+
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_sticky_base_range;
+
+ConVar tf_bot_debug_destroy_enemy_sentry( "tf_bot_debug_destroy_enemy_sentry", "0", FCVAR_CHEAT );
+ConVar tf_bot_max_grenade_launch_at_sentry_range( "tf_bot_max_grenade_launch_at_sentry_range", "1500", FCVAR_CHEAT );
+ConVar tf_bot_max_sticky_launch_at_sentry_range( "tf_bot_max_sticky_launch_at_sentry_range", "1500", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+// Search for angle to land grenade near target
+bool FindGrenadeAim( CTFBot *me, CBaseEntity *target, float *aimYaw, float *aimPitch )
+{
+ Vector toTarget = target->WorldSpaceCenter() - me->EyePosition();
+
+ if ( toTarget.IsLengthGreaterThan( tf_bot_max_grenade_launch_at_sentry_range.GetFloat() ) )
+ {
+ return false;
+ }
+
+ QAngle anglesToTarget;
+ VectorAngles( toTarget, anglesToTarget );
+
+ // start with current aim, in case we're already on target
+ const QAngle &eyeAngles = me->EyeAngles();
+ float yaw = eyeAngles.y;
+ float pitch = eyeAngles.x;
+
+ const int trials = 10;
+ for( int t=0; t<trials; ++t )
+ {
+ // estimate impact spot
+ const float pipebombInitVel = 900.0f;
+ Vector impactSpot = me->EstimateProjectileImpactPosition( pitch, yaw, pipebombInitVel );
+
+ // check if impactSpot landed near sentry
+ const float explosionRadius = 75.0f;
+ if ( ( target->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) )
+ {
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( target, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( target->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace );
+ if ( !trace.DidHit() )
+ {
+ *aimYaw = yaw;
+ *aimPitch = pitch;
+ return true;
+ }
+ }
+
+ yaw = anglesToTarget.y + RandomFloat( -30.0f, 30.0f );
+ pitch = RandomFloat( -85.0f, 85.0f );
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Search for angle to land sticky near target
+bool FindStickybombAim( CTFBot *me, CBaseEntity *target, float *aimYaw, float *aimPitch, float *aimCharge )
+{
+ Vector toTarget = target->WorldSpaceCenter() - me->EyePosition();
+
+ if ( toTarget.IsLengthGreaterThan( tf_bot_max_sticky_launch_at_sentry_range.GetFloat() ) )
+ {
+ return false;
+ }
+
+ QAngle anglesToTarget;
+ VectorAngles( toTarget, anglesToTarget );
+
+ // start with current aim, in case we're already on target
+ const QAngle &eyeAngles = me->EyeAngles();
+
+ float yaw = eyeAngles.y;
+ float pitch = eyeAngles.x;
+
+ *aimCharge = 1.0f;
+
+ bool hasTarget = false;
+
+ const int trials = 100;
+ for( int t=0; t<trials; ++t )
+ {
+ float charge = 0.0f;
+// if ( toTarget.IsLengthGreaterThan( tf_bot_sticky_base_range.GetBool() ) )
+// {
+// charge = RandomFloat( 0.1f, 1.0f );
+//
+// // skew towards zero - full charge shots are seldom required
+// charge *= charge;
+// }
+
+ // estimate impact spot
+ Vector impactSpot = me->EstimateStickybombProjectileImpactPosition( pitch, yaw, charge );
+
+ // check if impactSpot landed near target
+ const float explosionRadius = 75.0f;
+ if ( ( target->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) )
+ {
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( target, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( target->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace );
+ if ( !trace.DidHit() )
+ {
+ // found target aim - keep one we find with least required
+ // charge, because we need to be fast in combat
+ if ( charge < (*aimCharge) )
+ {
+ hasTarget = true;
+
+ *aimCharge = charge;
+ *aimYaw = yaw;
+ *aimPitch = pitch;
+
+ if ( *aimCharge < 0.01 )
+ {
+ // as quick as possible - no need to search further
+ break;
+ }
+ }
+ }
+ }
+
+ yaw = anglesToTarget.y + RandomFloat( -30.0f, 30.0f );
+ pitch = RandomFloat( -85.0f, 85.0f );
+ }
+
+ return hasTarget;
+}
+
+
+
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if this Action has what it needs to perform right now
+bool CTFBotDestroyEnemySentry::IsPossible( CTFBot *me )
+{
+ if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ||
+ me->IsPlayerClass( TF_CLASS_SNIPER ) ||
+ me->IsPlayerClass( TF_CLASS_MEDIC ) ||
+ me->IsPlayerClass( TF_CLASS_ENGINEER ) ||
+ me->IsPlayerClass( TF_CLASS_PYRO ) )
+ {
+ // these classes have no way to kill a sentry at long range
+ return false;
+ }
+
+ // don't go after a sentry if we're out of ammo
+ if ( me->GetAmmoCount( TF_AMMO_PRIMARY ) <= 0 || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 )
+ {
+ return false;
+ }
+
+ // if we're a spy, we have better ways of destroying sentries that shooting at it
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return false;
+ }
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ return false;
+ }
+ }
+#endif
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CFindSafeAttackArea : public ISearchSurroundingAreasFunctor
+{
+public:
+ CFindSafeAttackArea( CTFBot *me )
+ {
+ m_me = me;
+ m_attackSpot = me->GetAbsOrigin();
+ m_foundAttackSpot = false;
+
+ CObjectSentrygun *sentry = me->GetEnemySentry();
+ if ( sentry )
+ {
+ sentry->UpdateLastKnownArea();
+ m_sentryArea = (CTFNavArea *)sentry->GetLastKnownArea();
+ }
+ else
+ {
+ m_sentryArea = NULL;
+ }
+ }
+
+ virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ if ( !m_sentryArea )
+ {
+ return false;
+ }
+
+ if ( area->IsPotentiallyVisible( m_sentryArea ) )
+ {
+ // try the center first
+ m_attackSpot = area->GetCenter();
+
+ const int maxTries = 5;
+ for( int i=0; i<maxTries; ++i )
+ {
+ if ( m_me->IsLineOfFireClear( m_attackSpot + m_me->GetClassEyeHeight(), m_me->GetEnemySentry() ) )
+ {
+ if ( ( m_attackSpot - m_me->GetEnemySentry()->GetAbsOrigin() ).IsLengthGreaterThan( 1.1f * SENTRY_MAX_RANGE ) )
+ {
+ // found our attack spot
+ m_foundAttackSpot = true;
+ return false;
+ }
+ }
+
+ m_attackSpot = area->GetRandomPoint();
+ }
+ }
+
+ return true;
+ }
+
+
+ CTFBot *m_me;
+ CTFNavArea *m_sentryArea;
+ Vector m_attackSpot;
+ bool m_foundAttackSpot;
+
+ Vector m_splashFromSpot;
+ Vector m_splashToSpot;
+ bool m_foundSplashSpot;
+};
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotDestroyEnemySentry::ComputeSafeAttackSpot( CTFBot *me )
+{
+ m_hasSafeAttackSpot = false;
+
+ CObjectSentrygun *sentry = me->GetEnemySentry();
+ if ( sentry == NULL )
+ {
+ return;
+ }
+
+ sentry->UpdateLastKnownArea();
+
+ CTFNavArea *sentryArea = (CTFNavArea *)sentry->GetLastKnownArea();
+ if ( sentryArea == NULL )
+ {
+ return;
+ }
+
+ NavAreaCollector collector( true );
+ sentryArea->ForAllPotentiallyVisibleAreas( collector );
+
+ int i;
+ CUtlVector< CTFNavArea * > beyondSentryRangeVector;
+ for( i=0; i<collector.m_area.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)collector.m_area[i];
+
+ Vector wayOut = ( area->GetCenter() - sentryArea->GetCenter() ) + area->GetCenter();
+
+ Vector farthestFromSentry;
+ area->GetClosestPointOnArea( wayOut, &farthestFromSentry );
+
+ if ( ( farthestFromSentry - sentry->GetAbsOrigin() ).IsLengthGreaterThan( SENTRY_MAX_RANGE ) )
+ {
+ // at least some of this area is out of sentry range
+ beyondSentryRangeVector.AddToTail( area );
+
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ area->DrawFilled( 0, 255, 0, 255, 60.0f, true, 1.0f );
+ }
+ }
+ }
+
+
+ CUtlVector< CTFNavArea * > attackSentryVector;
+ for( i=0; i<beyondSentryRangeVector.Count(); ++i )
+ {
+ CTFNavArea *area = beyondSentryRangeVector[i];
+
+ Vector closestToSentry;
+ area->GetClosestPointOnArea( sentry->GetAbsOrigin(), &closestToSentry );
+
+ if ( ( closestToSentry - sentry->GetAbsOrigin() ).IsLengthLessThan( 1.5f * SENTRY_MAX_RANGE ) )
+ {
+ // good attack range
+ attackSentryVector.AddToTail( area );
+
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ area->DrawFilled( 100, 255, 0, 255, 60.0f );
+ }
+ }
+ }
+
+
+ if ( beyondSentryRangeVector.Count() == 0 )
+ {
+ // no safe areas at all
+ m_hasSafeAttackSpot = false;
+ return;
+ }
+
+ CUtlVector< CTFNavArea * > *safeAreaVector;
+
+ if ( attackSentryVector.Count() == 0 )
+ {
+ // no good close-in attack areas, choose from farther away set
+ safeAreaVector = &beyondSentryRangeVector;
+ }
+ else
+ {
+ // for now, just pick a random spot
+ safeAreaVector = &attackSentryVector;
+ }
+
+ // TODO: find closest and least combat-hot area
+ CTFNavArea *safeArea = safeAreaVector->Element( RandomInt( 0, safeAreaVector->Count()-1 ) );
+
+ m_safeAttackSpot = safeArea->GetRandomPoint();
+ m_hasSafeAttackSpot = true;
+
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ safeArea->DrawFilled( 255, 255, 0, 255, 60.0f );
+ NDebugOverlay::Cross3D( m_safeAttackSpot, 10.0f, 255, 0, 0, true, 60.0f );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+class FindSafeSentryApproachAreaScan : public ISearchSurroundingAreasFunctor
+{
+public:
+ FindSafeSentryApproachAreaScan( CTFBot *me )
+ {
+ m_me = me;
+
+ m_isEscaping = false;
+
+ CTFNavArea *myArea = me->GetLastKnownArea();
+ if ( myArea && myArea->IsTFMarked() )
+ {
+ // I'm standing in a danger area - escape!
+ m_isEscaping = true;
+ }
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ if ( m_isEscaping )
+ {
+ if ( !area->IsTFMarked() )
+ {
+ // found safe area - use it
+ m_approachAreaVector.AddToTail( area );
+ return false;
+ }
+ }
+ else
+ {
+ if ( area->IsTFMarked() && priorArea )
+ {
+ // we just stepped into sentry fire - keep the area one step prior
+ m_approachAreaVector.AddToTail( (CTFNavArea *)priorArea );
+ }
+ }
+
+ return true;
+ }
+
+ // return true if 'adjArea' should be included in the ongoing search
+ virtual bool ShouldSearch( CNavArea *baseAdjArea, CNavArea *baseCurrentArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseCurrentArea;
+
+ if ( !m_isEscaping )
+ {
+ // don't search beyond sentry danger areas (but step into them)
+ if ( area->IsTFMarked() )
+ {
+ return false;
+ }
+ }
+
+ return m_me->GetLocomotionInterface()->IsAreaTraversable( baseAdjArea );
+ }
+
+ // Invoked after the search has completed
+ virtual void PostSearch( void )
+ {
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ for( int i=0; i<m_approachAreaVector.Count(); ++i )
+ {
+ m_approachAreaVector[i]->DrawFilled( 0, 255, 0, 255, 60.0f );
+ }
+ }
+ }
+
+ CTFBot *m_me;
+ CUtlVector< CTFNavArea * > m_approachAreaVector;
+ bool m_isEscaping;
+};
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotDestroyEnemySentry::ComputeCornerAttackSpot( CTFBot *me )
+{
+ m_safeAttackSpot = vec3_origin;
+ m_hasSafeAttackSpot = false;
+
+ CObjectSentrygun *sentry = me->GetEnemySentry();
+ if ( !sentry )
+ {
+ return;
+ }
+
+ sentry->UpdateLastKnownArea();
+ CTFNavArea *sentryArea = (CTFNavArea *)sentry->GetLastKnownArea();
+
+ if ( !sentryArea )
+ {
+ return;
+ }
+
+ // mark all areas this sentry can potentially fire upon
+ // need to use completely visible so the partially visible areas are used as corner-fighting spots
+ NavAreaCollector sentryDanger;
+ sentryArea->ForAllCompletelyVisibleAreas( sentryDanger );
+
+ CTFNavArea::MakeNewTFMarker();
+ for( int i=0; i<sentryDanger.m_area.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)sentryDanger.m_area[i];
+
+ Vector close;
+ area->GetClosestPointOnArea( sentry->GetAbsOrigin(), &close );
+
+ if ( ( sentry->GetAbsOrigin() - close ).IsLengthLessThan( SENTRY_MAX_RANGE ) )
+ {
+ area->TFMark();
+
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ area->DrawFilled( 255, 0, 0, 255, 60.0f );
+ }
+ }
+ }
+
+
+ // find nearby area adjacent to area that is in enemy sentry fire field
+ FindSafeSentryApproachAreaScan scan( me );
+ SearchSurroundingAreas( me->GetLastKnownArea(), scan );
+
+ if ( scan.m_approachAreaVector.Count() > 0 )
+ {
+ CTFNavArea *safeArea = scan.m_approachAreaVector[ RandomInt( 0, scan.m_approachAreaVector.Count()-1 ) ];
+
+ // try to avoid picking a spot where sentry can attack us
+ const int retryCount = 25;
+ for( int r=0; r<retryCount; ++r )
+ {
+ m_safeAttackSpot = safeArea->GetRandomPoint();
+
+ if ( ( sentry->WorldSpaceCenter() - m_safeAttackSpot ).IsLengthGreaterThan( SENTRY_MAX_RANGE ) ||
+ !me->IsLineOfFireClear( sentry->WorldSpaceCenter(), m_safeAttackSpot ) )
+ {
+ break;
+ }
+ }
+
+ m_hasSafeAttackSpot = true;
+
+ if ( tf_bot_debug_destroy_enemy_sentry.GetBool() )
+ {
+ NDebugOverlay::Cross3D( m_safeAttackSpot, 5.0f, 255, 255, 0, true, 60.0f );
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDestroyEnemySentry::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_path.Invalidate();
+ m_repathTimer.Invalidate();
+
+ m_isAttackingSentry = false;
+ m_wasUber = false;
+
+/*
+ // find a spot to attack the sentry out of its range
+ CFindSafeAttackArea find( me );
+ SearchSurroundingAreas( me->GetLastKnownArea(), find, 1.5f * SENTRY_MAX_RANGE );
+
+ m_hasSafeAttackSpot = find.m_foundAttackSpot;
+ m_safeAttackSpot = find.m_attackSpot;
+*/
+
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ ComputeCornerAttackSpot( me );
+ }
+ else
+ {
+ ComputeSafeAttackSpot( me );
+ }
+
+/*
+ if ( !m_hasSafeAttackSpot )
+ {
+ return Done( "No safe attack spot found" );
+ }
+*/
+
+ m_targetSentry = me->GetEnemySentry();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDestroyEnemySentry::Update( CTFBot *me, float interval )
+{
+ if ( me->GetEnemySentry() == NULL )
+ {
+ return Done( "Enemy sentry is destroyed" );
+ }
+
+ // if the sentry changes, re-evaluate
+ if ( me->GetEnemySentry() != m_targetSentry )
+ {
+ return ChangeTo( new CTFBotDestroyEnemySentry, "Changed sentry target" );
+ }
+
+ if ( me->m_Shared.IsInvulnerable() )
+ {
+ if ( !m_wasUber )
+ {
+ m_wasUber = true;
+
+ // we just became uber - are we close enough to rush the sentry?
+ const float maxRushDistance = 500.0f;
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ float travelDistance = NavAreaTravelDistance( me->GetLastKnownArea(),
+ m_targetSentry->GetLastKnownArea(),
+ cost, maxRushDistance );
+
+ if ( travelDistance >= 0.0f )
+ {
+ return SuspendFor( new CTFBotUberAttackEnemySentry( m_targetSentry ), "Go get it!" );
+ }
+ }
+ }
+ else
+ {
+ m_wasUber = false;
+ }
+
+ if ( !me->HasAttribute( CTFBot::IGNORE_ENEMIES ) )
+ {
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ float threatRange = me->GetRangeTo( threat->GetLastKnownPosition() );
+ float sentryRange = me->GetRangeTo( me->GetEnemySentry() );
+
+ if ( threatRange < 0.5f * sentryRange )
+ {
+ return Done( "Enemy near" );
+ }
+ }
+ }
+
+ bool isSentryFiringOnMe = false;
+ if ( me->GetEnemySentry()->GetTimeSinceLastFired() < 1.0f )
+ {
+ Vector sentryForward;
+ AngleVectors( me->GetEnemySentry()->GetTurretAngles(), &sentryForward );
+
+ Vector to = me->GetAbsOrigin() - me->GetEnemySentry()->GetAbsOrigin();
+ to.NormalizeInPlace();
+
+ if ( DotProduct( to, sentryForward ) > 0.8f )
+ {
+ isSentryFiringOnMe = true;
+ }
+ }
+
+
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ // a demoman wants to get close to the sentry but just out of range or line of sight so
+ // he can pepper the area with stickies and destroy it
+ Vector attackSpot = m_hasSafeAttackSpot ? m_safeAttackSpot : m_targetSentry->GetAbsOrigin();
+
+ // move into position
+ if ( !m_path.IsValid() || m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( 1.0f );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, attackSpot, cost );
+ }
+
+ float aimPitch, aimYaw, aimCharge;
+ if ( isSentryFiringOnMe )
+ {
+ // the sentry is firing on me - might as well shoot back!
+ me->EquipLongRangeWeapon();
+ me->PressFireButton();
+ }
+ else if ( FindStickybombAim( me, m_targetSentry, &aimYaw, &aimPitch, &aimCharge ) )
+ {
+ // found an opportunistic spot to sticky the sentry from
+ return ChangeTo( new CTFBotStickybombSentrygun( me->GetEnemySentry(), aimYaw, aimPitch, aimCharge ), "Destroying sentry with opportunistic sticky shot" );
+ }
+
+ // move towards sentry
+ if ( m_canMove )
+ {
+ m_path.Update( me );
+ }
+
+ if ( ( me->IsRangeLessThan( attackSpot, 50.0f ) &&
+ ( me->GetAbsOrigin() - attackSpot ).AsVector2D().IsLengthLessThan( 25.0f ) ) ||
+ ( me->IsLineOfFireClear( me->GetEnemySentry() ) && me->IsRangeLessThan( m_targetSentry, 1000.0f ) ) ) // opportunistic shot
+ {
+ // reached attack spot
+ return ChangeTo( new CTFBotStickybombSentrygun( me->GetEnemySentry() ), "Destroying sentry with stickies" );
+ }
+
+ if ( me->IsRangeLessThan( attackSpot, 200.0f ) )
+ {
+#ifdef TF_CREEP_MODE
+ if ( m_creepTimer.IsElapsed() )
+ {
+ m_canMove = !m_canMove;
+
+ if ( m_canMove )
+ {
+ m_creepTimer.Start( 0.1f );
+ }
+ else
+ {
+ m_creepTimer.Start( RandomFloat( 0.2f, 0.5f ) );
+ }
+ }
+#endif
+ }
+ else
+ {
+ m_canMove = true;
+ }
+
+ return Continue();
+ }
+
+
+ bool isInAttackPosition = ( m_hasSafeAttackSpot && me->IsRangeLessThan( m_safeAttackSpot, 20.0f ) );
+
+ if ( isInAttackPosition || me->IsLineOfFireClear( me->GetEnemySentry() ) )
+ {
+ // must look at sentry entity to make use of SelectTargetPoint()
+ me->GetBodyInterface()->AimHeadTowards( me->GetEnemySentry(), IBody::MANDATORY, 1.0f, NULL, "Aiming at enemy sentry" );
+
+ // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades
+ Vector toSentry = me->GetEnemySentry()->WorldSpaceCenter() - me->EyePosition();
+ toSentry.NormalizeInPlace();
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ if ( ( forward.x * toSentry.x + forward.y * toSentry.y ) > 0.95f )
+ {
+ if ( me->EquipLongRangeWeapon() == false )
+ {
+ return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" );
+ }
+
+ me->PressFireButton();
+ m_isAttackingSentry = true;
+ }
+ else
+ {
+ m_isAttackingSentry = false;
+ }
+
+ if ( me->IsRangeGreaterThan( me->GetEnemySentry(), 1.1f * SENTRY_MAX_RANGE ) )
+ {
+ // safely out of range of the gun - hold here and fire at it
+ return Continue();
+ }
+
+ // we are in range of the gun - if it is pointed at us and firing, retreat to cover
+ if ( me->GetEnemySentry()->GetTimeSinceLastFired() < 1.0f )
+ {
+ Vector sentryForward;
+ AngleVectors( me->GetEnemySentry()->GetTurretAngles(), &sentryForward );
+
+ Vector to = me->GetAbsOrigin() - me->GetEnemySentry()->GetAbsOrigin();
+ to.NormalizeInPlace();
+
+ if ( DotProduct( to, sentryForward ) > 0.8f )
+ {
+ return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "Taking cover from sentry fire" );
+ }
+ }
+
+ if ( isInAttackPosition )
+ {
+ // we're at our attack position, hold here
+ return Continue();
+ }
+ }
+
+ // move into position
+ if ( !m_path.IsValid() || m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( 1.0f );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ Vector moveGoal = m_hasSafeAttackSpot ? m_safeAttackSpot : me->GetEnemySentry()->GetAbsOrigin();
+
+ if ( !m_path.Compute( me, moveGoal, cost ) )
+ {
+ return Done( "No path" );
+ }
+ }
+
+ // move along path to vantage point
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotDestroyEnemySentry::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ m_path.Invalidate();
+ m_repathTimer.Invalidate();
+
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ ComputeCornerAttackSpot( me );
+ }
+ else
+ {
+ ComputeSafeAttackSpot( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDestroyEnemySentry::ShouldHurry( const INextBot *me ) const
+{
+ // while killing a sentry we're "hurrying" so we don't dodge
+ return m_isAttackingSentry ? ANSWER_YES : ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDestroyEnemySentry::ShouldRetreat( const INextBot *me ) const
+{
+ // push in to kill the sentry
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotDestroyEnemySentry::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ // if we're in range to attack the sentry, we handle firing directly
+ return m_isAttackingSentry ? ANSWER_NO : ANSWER_UNDEFINED;
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CTFBotUberAttackEnemySentry::CTFBotUberAttackEnemySentry( CObjectSentrygun *sentryTarget )
+{
+ m_targetSentry = sentryTarget;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUberAttackEnemySentry::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_wasIgnoringEnemies = me->HasAttribute( CTFBot::IGNORE_ENEMIES );
+
+ me->SetAttribute( CTFBot::IGNORE_ENEMIES );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUberAttackEnemySentry::Update( CTFBot *me, float interval )
+{
+ if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) )
+ {
+ return Done( "No longer uber" );
+ }
+
+ if ( m_targetSentry == NULL )
+ {
+ return Done( "Target sentry destroyed" );
+ }
+
+ float aimYaw, aimPitch;
+ if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) && FindGrenadeAim( me, m_targetSentry, &aimYaw, &aimPitch ) )
+ {
+ QAngle aimAngles;
+ aimAngles.x = aimPitch;
+ aimAngles.y = aimYaw;
+ aimAngles.z = 0.0f;
+
+ Vector aimForward;
+ AngleVectors( aimAngles, &aimForward );
+
+ // always recompute eye aim target so we can update our view
+ Vector eyeAimTarget = me->EyePosition() + 5000.0f * aimForward;
+ me->GetBodyInterface()->AimHeadTowards( eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Aiming at opportunistic grenade shot" );
+
+ Vector eyeForward;
+ me->EyeVectors( &eyeForward );
+
+ if ( DotProduct( aimForward, eyeForward ) > 0.9f )
+ {
+ if ( me->EquipLongRangeWeapon() == false )
+ {
+ return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" );
+ }
+
+ me->PressFireButton();
+ }
+ }
+ else if ( me->IsLineOfFireClear( m_targetSentry ) )
+ {
+ // must look at sentry entity to make use of SelectTargetPoint()
+ me->GetBodyInterface()->AimHeadTowards( m_targetSentry, IBody::MANDATORY, 1.0f, NULL, "Aiming at target sentry" );
+
+ // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades
+ Vector toSentry = m_targetSentry->WorldSpaceCenter() - me->EyePosition();
+ toSentry.NormalizeInPlace();
+
+ Vector eyeForward;
+ me->EyeVectors( &eyeForward );
+
+ if ( ( eyeForward.x * toSentry.x + eyeForward.y * toSentry.y ) > 0.95f )
+ {
+ if ( me->EquipLongRangeWeapon() == false )
+ {
+ return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" );
+ }
+
+ me->PressFireButton();
+ }
+
+ if ( me->IsRangeLessThan( m_targetSentry, 100.0f ) )
+ {
+ // we have a clear line of fire and are close enough
+ return Continue();
+ }
+ }
+
+ // move into position
+ if ( !m_path.IsValid() || m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( 1.0f );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, m_targetSentry->WorldSpaceCenter(), cost );
+ }
+
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotUberAttackEnemySentry::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( !m_wasIgnoringEnemies )
+ {
+ me->ClearAttribute( CTFBot::IGNORE_ENEMIES );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotUberAttackEnemySentry::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotUberAttackEnemySentry::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotUberAttackEnemySentry::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_YES;
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h
new file mode 100644
index 0000000..f12d31a
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h
@@ -0,0 +1,79 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_destroy_enemy_sentry.h
+// Destroy an enemy sentry gun
+// Michael Booth, June 2010
+
+#ifndef TF_BOT_DESTROY_ENEMY_SENTRY_H
+#define TF_BOT_DESTROY_ENEMY_SENTRY_H
+
+#include "Path/NextBotChasePath.h"
+
+//---------------------------------------------------------------------------------
+class CTFBotDestroyEnemySentry : 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 );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "DestroyEnemySentry"; };
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ bool m_canMove;
+
+#ifdef TF_CREEP_MODE
+ CountdownTimer m_creepTimer;
+#endif
+
+ Vector m_safeAttackSpot;
+ bool m_hasSafeAttackSpot;
+ void ComputeSafeAttackSpot( CTFBot *me );
+ void ComputeCornerAttackSpot( CTFBot *me );
+
+ bool m_isAttackingSentry;
+ bool m_wasUber;
+
+ ActionResult< CTFBot > EquipLongRangeWeapon( CTFBot *me );
+
+ CHandle< CObjectSentrygun > m_targetSentry;
+};
+
+
+//---------------------------------------------------------------------------------
+class CTFBotUberAttackEnemySentry : public Action< CTFBot >
+{
+public:
+ CTFBotUberAttackEnemySentry( CObjectSentrygun *sentryTarget );
+ virtual ~CTFBotUberAttackEnemySentry() { }
+
+ 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 QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"?
+
+ virtual const char *GetName( void ) const { return "UberAttackEnemySentry"; };
+
+private:
+ bool m_wasIgnoringEnemies;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CHandle< CObjectSentrygun > m_targetSentry;
+};
+
+
+#endif // TF_BOT_DESTROY_ENEMY_SENTRY_H
diff --git a/game/server/tf/bot/behavior/tf_bot_escort.cpp b/game/server/tf/bot/behavior/tf_bot_escort.cpp
new file mode 100644
index 0000000..08a2f77
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_escort.cpp
@@ -0,0 +1,130 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort.cpp
+// Move near an entity and protect it
+// Michael Booth, April 2011
+
+#include "cbase.h"
+
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_escort.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h"
+#include "bot/behavior/tf_bot_destroy_enemy_sentry.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_escort_range( "tf_bot_escort_range", "300", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotEscort::CTFBotEscort( CBaseEntity *who )
+{
+ SetWho( who );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotEscort::SetWho( CBaseEntity *who )
+{
+ m_who = who;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CBaseEntity *CTFBotEscort::GetWho( void ) const
+{
+ return m_who;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscort::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_pathToWho.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotEscort::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ return SuspendFor( new CTFBotAttack, "Attacking nearby threat" );
+ }
+ else
+ {
+ // no enemy is visible - move near who we are escorting
+ if ( m_who != NULL )
+ {
+ if ( me->IsRangeGreaterThan( m_who, tf_bot_escort_range.GetFloat() ) )
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_pathToWho.Compute( me, m_who->GetAbsOrigin(), cost );
+ m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) );
+ }
+
+ m_pathToWho.Update( me );
+ }
+ else
+ {
+ if ( CTFBotPrepareStickybombTrap::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" );
+ }
+ }
+ }
+
+ // destroy enemy sentry guns we've encountered
+ if ( me->GetEnemySentry() && CTFBotDestroyEnemySentry::IsPossible( me ) )
+ {
+ return SuspendFor( new CTFBotDestroyEnemySentry, "Going after an enemy sentry to destroy it" );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEscort::OnStuck( CTFBot *me )
+{
+ m_repathTimer.Invalidate();
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEscort::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEscort::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotEscort::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_NO;
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotEscort::OnCommandApproach( CTFBot *me, const Vector &pos, float range )
+{
+ return TryContinue();
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_escort.h b/game/server/tf/bot/behavior/tf_bot_escort.h
new file mode 100644
index 0000000..53e4cc0
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_escort.h
@@ -0,0 +1,40 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_escort.cpp
+// Move near an entity and protect it
+// Michael Booth, April 2011
+
+#ifndef TF_BOT_ESCORT_H
+#define TF_BOT_ESCORT_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotEscort : public Action< CTFBot >
+{
+public:
+ CTFBotEscort( CBaseEntity *who );
+ virtual ~CTFBotEscort() { }
+
+ void SetWho( CBaseEntity *who );
+ CBaseEntity *GetWho( void ) const;
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+
+ virtual EventDesiredResult< CTFBot > OnCommandApproach( CTFBot *me, const Vector &pos, float range );
+
+ virtual const char *GetName( void ) const { return "Escort"; }
+
+private:
+ CHandle< CBaseEntity > m_who;
+ PathFollower m_pathToWho;
+ CountdownTimer m_vocalizeTimer;
+ CountdownTimer m_repathTimer;
+};
+
+#endif // TF_BOT_ESCORT_H
diff --git a/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp b/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp
new file mode 100644
index 0000000..302663f
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp
@@ -0,0 +1,339 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_get_ammo.h
+// Pick up any nearby ammo
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "tf_obj.h"
+#include "tf_gamerules.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_ammo_search_range( "tf_bot_ammo_search_range", "5000", FCVAR_CHEAT, "How far bots will search to find ammo around them" );
+ConVar tf_bot_debug_ammo_scavenging( "tf_bot_debug_ammo_scavenging", "0", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotGetAmmo::CTFBotGetAmmo( void )
+{
+ m_path.Invalidate();
+ m_ammo = NULL;
+ m_isGoalDispenser = false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CAmmoFilter : public INextBotFilter
+{
+public:
+ CAmmoFilter( CTFBot *me )
+ {
+ m_me = me;
+ m_ammoArea = NULL;
+ }
+
+ bool IsSelected( const CBaseEntity *constCandidate ) const
+ {
+ CBaseEntity *candidate = const_cast< CBaseEntity * >( constCandidate );
+
+ m_ammoArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( candidate->WorldSpaceCenter() );
+ if ( !m_ammoArea )
+ return false;
+
+ CClosestTFPlayer close( candidate );
+ ForEachPlayer( close );
+
+ // if the closest player to this candidate object is an enemy, don't use it
+ if ( close.m_closePlayer && !m_me->InSameTeam( close.m_closePlayer ) )
+ return false;
+
+ // resupply cabinets (not assigned a team)
+ if ( candidate->ClassMatches( "func_regenerate" ) )
+ {
+ if ( !m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ // Assume any resupply cabinets not in a teamed spawn room are inaccessible.
+ // Ex: pl_upward has forward spawn rooms that neither team can use until
+ // certain checkpoints are reached.
+ return false;
+ }
+
+ if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) ||
+ ( m_me->GetTeamNumber() == TF_TEAM_BLUE && m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) )
+ {
+ // the supply cabinet is in my spawn room, or not in any spawn room
+ return true;
+ }
+ return false;
+ }
+
+ // ignore non-existent ammo to ensure we collect nearby existing ammo
+ if ( candidate->IsEffectActive( EF_NODRAW ) )
+ return false;
+
+ if ( candidate->ClassMatches( "tf_ammo_pack" ) )
+ return true;
+
+ if ( candidate->ClassMatches( "item_ammopack*" ) )
+ return true;
+
+ if ( m_me->InSameTeam( candidate ) )
+ {
+ // friendly engineer's dispenser
+ if ( candidate->ClassMatches( "obj_dispenser*" ) )
+ {
+ // for now, assume Engineers want to go fetch ammo boxes unless their dispenser is fully upgraded
+ // unless we have no sentry yet, then we need to leech off of buddy's dispenser to get started
+ if ( !m_me->IsPlayerClass( TF_CLASS_ENGINEER ) || ( (CBaseObject *)candidate )->GetUpgradeLevel() >= 3 || !m_me->GetObjectOfType( OBJ_SENTRYGUN ) )
+ {
+ CBaseObject *dispenser = (CBaseObject *)candidate;
+ if ( !dispenser->IsBuilding() && !dispenser->IsDisabled() )
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ CTFBot *m_me;
+ mutable CTFNavArea *m_ammoArea;
+};
+
+
+//---------------------------------------------------------------------------------------------
+static CTFBot *s_possibleBot = NULL;
+static CHandle< CBaseEntity > s_possibleAmmo = NULL;
+static int s_possibleFrame = 0;
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Return true if this Action has what it needs to perform right now
+ */
+bool CTFBotGetAmmo::IsPossible( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotGetAmmo::IsPossible", "NextBot" );
+
+ int i;
+
+ CUtlVector< CNavArea * > nearbyAreaVector;
+ CollectSurroundingAreas( &nearbyAreaVector, me->GetLastKnownArea(), tf_bot_ammo_search_range.GetFloat(), me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetDeathDropHeight() );
+
+ CAmmoFilter ammoFilter( me );
+
+ const CUtlVector< CHandle< CBaseEntity > > &staticAmmoVector = TFGameRules()->GetAmmoEntityVector();
+ CBaseEntity *closestAmmo = NULL;
+ float closestAmmoTravelDistance = FLT_MAX;
+
+ for( i=0; i<staticAmmoVector.Count(); ++i )
+ {
+ CBaseEntity *ammo = staticAmmoVector[i];
+ if ( ammo )
+ {
+ if ( ammoFilter.IsSelected( ammo ) )
+ {
+ if ( ammoFilter.m_ammoArea && ammoFilter.m_ammoArea->IsMarked() )
+ {
+ // "cost so far" was computed during the breadth first search within CollectSurroundingAreas()
+ // and is the travel distance from to this area
+ if ( ammoFilter.m_ammoArea->GetCostSoFar() < closestAmmoTravelDistance )
+ {
+ closestAmmo = ammo;
+ closestAmmoTravelDistance = ammoFilter.m_ammoArea->GetCostSoFar();
+ }
+
+ if ( tf_bot_debug_ammo_scavenging.GetBool() )
+ {
+ NDebugOverlay::Cross3D( ammo->WorldSpaceCenter(), 5.0f, 255, 255, 0, true, 999.9 );
+ }
+ }
+ }
+ }
+ }
+
+ // append nearby dropped weapons
+ CBaseEntity *ammoPack = NULL;
+ while( ( ammoPack = gEntList.FindEntityByClassname( ammoPack, "tf_ammo_pack" ) ) != NULL )
+ {
+ if ( ammoFilter.IsSelected( ammoPack ) )
+ {
+ if ( ammoFilter.m_ammoArea && ammoFilter.m_ammoArea->IsMarked() )
+ {
+ if ( ammoFilter.m_ammoArea->GetCostSoFar() < closestAmmoTravelDistance )
+ {
+ closestAmmo = ammoPack;
+ closestAmmoTravelDistance = ammoFilter.m_ammoArea->GetCostSoFar();
+ }
+
+ if ( tf_bot_debug_ammo_scavenging.GetBool() )
+ {
+ NDebugOverlay::Cross3D( ammoPack->WorldSpaceCenter(), 5.0f, 255, 100, 0, true, 999.9 );
+ }
+ }
+ }
+ }
+
+ if ( !closestAmmo )
+ {
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ Warning( "%3.2f: No ammo nearby\n", gpGlobals->curtime );
+ }
+ return false;
+ }
+
+ s_possibleBot = me;
+ s_possibleAmmo = closestAmmo;
+ s_possibleFrame = gpGlobals->framecount;
+
+ return true;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGetAmmo::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ VPROF_BUDGET( "CTFBotGetAmmo::OnStart", "NextBot" );
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ // if IsPossible() has already been called, use its cached data
+ if ( s_possibleFrame != gpGlobals->framecount || s_possibleBot != me )
+ {
+ if ( !IsPossible( me ) || s_possibleAmmo == NULL )
+ {
+ return Done( "Can't get ammo" );
+ }
+ }
+
+ m_ammo = s_possibleAmmo;
+ m_isGoalDispenser = m_ammo->ClassMatches( "obj_dispenser*" );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( !m_path.Compute( me, m_ammo->WorldSpaceCenter(), cost ) )
+ {
+ return Done( "No path to ammo!" );
+ }
+
+ // if I'm a spy, cloak and disguise
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( !me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGetAmmo::Update( CTFBot *me, float interval )
+{
+ if ( me->IsAmmoFull() )
+ {
+ return Done( "My ammo is full" );
+ }
+
+ if ( m_ammo == NULL ) // || ( m_ammo->IsEffectActive( EF_NODRAW ) && !FClassnameIs( m_ammo, "func_regenerate" ) ) )
+ {
+/*
+ // engineers try to gather all the metal they can
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) && CTFBotGetAmmo::IsPossible( me ) )
+ {
+ // more ammo to be had
+ return ChangeTo( new CTFBotGetAmmo, "Not full yet - grabbing more ammo" );
+ }
+*/
+
+ return Done( "Ammo I was going for has been taken" );
+ }
+
+ if ( m_isGoalDispenser )
+ {
+ // we need to get near and wait, not try to run over
+ const float nearRange = 75.0f;
+ if ( ( me->GetAbsOrigin() - m_ammo->GetAbsOrigin() ).IsLengthLessThan( nearRange ) )
+ {
+ if ( me->GetVisionInterface()->IsLineOfSightClearToEntity( m_ammo ) )
+ {
+ if ( me->IsAmmoFull() )
+ {
+ return Done( "Ammo refilled by the Dispenser" );
+ }
+
+ // don't wait if I'm in combat
+ if ( !me->IsAmmoLow() && me->GetVisionInterface()->GetPrimaryKnownThreat() )
+ {
+ return Done( "No time to wait for more ammo, I must fight" );
+ }
+
+ // wait until the dispenser refills us
+ return Continue();
+ }
+ }
+ }
+
+ if ( !m_path.IsValid() )
+ {
+ return Done( "My path became invalid" );
+ }
+
+/* TODO: Rethink this. Currently creates zombie behavior loop.
+ // if the closest player to the item we're after is an enemy, give up
+ CClosestTFPlayer close( m_ammo );
+ ForEachPlayer( close );
+ if ( close.m_closePlayer && !me->InSameTeam( close.m_closePlayer ) )
+ return Done( "An enemy is closer to it" );
+*/
+
+ // may need to switch weapons due to out of ammo
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ me->EquipBestWeaponForThreat( threat );
+
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetAmmo::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetAmmo::OnStuck( CTFBot *me )
+{
+ return TryDone( RESULT_CRITICAL, "Stuck trying to reach ammo" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetAmmo::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetAmmo::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryDone( RESULT_CRITICAL, "Failed to reach ammo" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotGetAmmo::ShouldHurry( const INextBot *me ) const
+{
+ // if we need ammo, we best hustle
+ return ANSWER_YES;
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_get_ammo.h b/game/server/tf/bot/behavior/tf_bot_get_ammo.h
new file mode 100644
index 0000000..f77f293
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_get_ammo.h
@@ -0,0 +1,38 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_get_ammo.h
+// Pick up any nearby ammo
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_GET_AMMO_H
+#define TF_BOT_GET_AMMO_H
+
+#include "tf_powerup.h"
+
+class CTFBotGetAmmo : public Action< CTFBot >
+{
+public:
+ CTFBotGetAmmo( void );
+
+ 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 );
+
+ virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "GetAmmo"; };
+
+private:
+ PathFollower m_path;
+ CHandle< CBaseEntity > m_ammo;
+ bool m_isGoalDispenser;
+};
+
+
+#endif // TF_BOT_GET_AMMO_H
diff --git a/game/server/tf/bot/behavior/tf_bot_get_health.cpp b/game/server/tf/bot/behavior/tf_bot_get_health.cpp
new file mode 100644
index 0000000..91e51ef
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_get_health.cpp
@@ -0,0 +1,324 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_get_health.h
+// Pick up any nearby health kit
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "tf_gamerules.h"
+#include "tf_obj.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_get_health.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_health_critical_ratio( "tf_bot_health_critical_ratio", "0.3", FCVAR_CHEAT );
+ConVar tf_bot_health_ok_ratio( "tf_bot_health_ok_ratio", "0.8", FCVAR_CHEAT );
+ConVar tf_bot_health_search_near_range( "tf_bot_health_search_near_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_health_search_far_range( "tf_bot_health_search_far_range", "2000", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+class CHealthFilter : public INextBotFilter
+{
+public:
+ CHealthFilter( CTFBot *me )
+ {
+ m_me = me;
+ }
+
+ bool IsSelected( const CBaseEntity *constCandidate ) const
+ {
+ if ( !constCandidate )
+ return false;
+
+ CBaseEntity *candidate = const_cast< CBaseEntity * >( constCandidate );
+
+ CTFNavArea *area = (CTFNavArea *)TheNavMesh->GetNearestNavArea( candidate->WorldSpaceCenter() );
+ if ( !area )
+ return false;
+
+ CClosestTFPlayer close( candidate );
+ ForEachPlayer( close );
+
+ // if the closest player to this candidate object is an enemy, don't use it
+ if ( close.m_closePlayer && !m_me->InSameTeam( close.m_closePlayer ) )
+ return false;
+
+ // resupply cabinets (not assigned a team)
+ if ( candidate->ClassMatches( "func_regenerate" ) )
+ {
+ if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ // Assume any resupply cabinets not in a teamed spawn room are inaccessible.
+ // Ex: pl_upward has forward spawn rooms that neither team can use until
+ // certain checkpoints are reached.
+ return false;
+ }
+
+ if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) ||
+ ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) )
+ {
+ // the supply cabinet is in my spawn room
+ return true;
+ }
+
+ return false;
+ }
+
+ // ignore non-existent ammo to ensure we collect nearby existing ammo
+ if ( candidate->IsEffectActive( EF_NODRAW ) )
+ return false;
+
+ if ( candidate->ClassMatches( "item_healthkit*" ) )
+ return true;
+
+ if ( m_me->InSameTeam( candidate ) )
+ {
+ // friendly engineer's dispenser
+ if ( candidate->ClassMatches( "obj_dispenser*" ) )
+ {
+ CBaseObject *dispenser = (CBaseObject *)candidate;
+ if ( !dispenser->IsBuilding() && !dispenser->IsPlacing() && !dispenser->IsDisabled() )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ CTFBot *m_me;
+};
+
+
+//---------------------------------------------------------------------------------------------
+static CTFBot *s_possibleBot = NULL;
+static CHandle< CBaseEntity > s_possibleHealth = NULL;
+static int s_possibleFrame = 0;
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Return true if this Action has what it needs to perform right now
+ */
+bool CTFBotGetHealth::IsPossible( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotGetHealth::IsPossible", "NextBot" );
+
+ // don't move to fetch health if we have a healer
+ if ( me->m_Shared.GetNumHealers() > 0 )
+ return false;
+
+#ifdef TF_RAID_MODE
+ // mobs don't heal
+ if ( TFGameRules()->IsRaidMode() && me->HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ return false;
+ }
+#endif // TF_RAID_MODE
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ return false;
+ }
+
+ float healthRatio = (float)me->GetHealth() / (float)me->GetMaxHealth();
+
+ float t = ( healthRatio - tf_bot_health_critical_ratio.GetFloat() ) / ( tf_bot_health_ok_ratio.GetFloat() - tf_bot_health_critical_ratio.GetFloat() );
+ t = clamp( t, 0.0f, 1.0f );
+
+ if ( me->m_Shared.InCond( TF_COND_BURNING ) )
+ {
+ // on fire - get health now
+ t = 0.0f;
+ }
+
+ // the more we are hurt, the farther we'll travel to get health
+ float searchRange = tf_bot_health_search_far_range.GetFloat() + t * ( tf_bot_health_search_near_range.GetFloat() - tf_bot_health_search_far_range.GetFloat() );
+
+ CUtlVector< CHandle< CBaseEntity > > healthVector;
+ CHealthFilter healthFilter( me );
+
+ me->SelectReachableObjects( TFGameRules()->GetHealthEntityVector(), &healthVector, healthFilter, me->GetLastKnownArea(), searchRange );
+
+ if ( healthVector.Count() == 0 )
+ {
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ Warning( "%3.2f: No health nearby\n", gpGlobals->curtime );
+ }
+ return false;
+ }
+
+ // use the first item in the list, since it will be the closest to us (or nearly so)
+ CBaseEntity *health = healthVector[0];
+ for( int i=0; i<healthVector.Count(); ++i )
+ {
+ if ( healthVector[i]->GetTeamNumber() != GetEnemyTeam( me->GetTeamNumber() ) )
+ {
+ health = healthVector[i];
+ break;
+ }
+ }
+
+ if ( health == NULL )
+ {
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ Warning( "%3.2f: No health available to my team nearby\n", gpGlobals->curtime );
+ }
+ return false;
+ }
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ PathFollower path;
+ if ( !path.Compute( me, health->WorldSpaceCenter(), cost ) )
+ {
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ Warning( "%3.2f: No path to health!\n", gpGlobals->curtime );
+ }
+ return false;
+ }
+
+ s_possibleBot = me;
+ s_possibleHealth = health;
+ s_possibleFrame = gpGlobals->framecount;
+
+ return true;
+}
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGetHealth::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ VPROF_BUDGET( "CTFBotGetHealth::OnStart", "NextBot" );
+
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ // if IsPossible() has already been called, use its cached data
+ if ( s_possibleFrame != gpGlobals->framecount || s_possibleBot != me )
+ {
+ if ( !IsPossible( me ) || s_possibleHealth == NULL )
+ {
+ return Done( "Can't get health" );
+ }
+ }
+
+ m_healthKit = s_possibleHealth;
+ m_isGoalDispenser = m_healthKit->ClassMatches( "obj_dispenser*" );
+
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ if ( !m_path.Compute( me, m_healthKit->WorldSpaceCenter(), cost ) )
+ {
+ return Done( "No path to health!" );
+ }
+
+ // if I'm a spy, cloak and disguise
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( !me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotGetHealth::Update( CTFBot *me, float interval )
+{
+ if ( m_healthKit == NULL || ( m_healthKit->IsEffectActive( EF_NODRAW ) && !FClassnameIs( m_healthKit, "func_regenerate" ) ) )
+ {
+ return Done( "Health kit I was going for has been taken" );
+ }
+
+ // if a medic is healing us, give up on getting a kit
+ int i;
+ for( i=0; i<me->m_Shared.GetNumHealers(); ++i )
+ {
+ if ( !me->m_Shared.HealerIsDispenser( i ) )
+ break;
+ }
+
+ if ( i < me->m_Shared.GetNumHealers() )
+ {
+ return Done( "A Medic is healing me" );
+ }
+
+ if ( me->m_Shared.GetNumHealers() )
+ {
+ // a dispenser is healing me, don't wait if I'm in combat
+ const CKnownEntity *known = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( known && known->IsVisibleInFOVNow() )
+ {
+ return Done( "No time to wait for health, I must fight" );
+ }
+ }
+
+ if ( me->GetHealth() >= me->GetMaxHealth() )
+ {
+ return Done( "I've been healed" );
+ }
+
+ // if the closest player to the item we're after is an enemy, give up
+ CClosestTFPlayer close( m_healthKit );
+ ForEachPlayer( close );
+ if ( close.m_closePlayer && !me->InSameTeam( close.m_closePlayer ) )
+ return Done( "An enemy is closer to it" );
+
+ // un-zoom
+ CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
+ if ( myWeapon && myWeapon->IsWeapon( TF_WEAPON_SNIPERRIFLE ) && me->m_Shared.InCond( TF_COND_ZOOMED ) )
+ me->PressAltFireButton();
+
+ if ( !m_path.IsValid() )
+ {
+ // this can occur if we overshoot the health kit's location
+ // because it is momentarily gone
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ if ( !m_path.Compute( me, m_healthKit->WorldSpaceCenter(), cost ) )
+ {
+ return Done( "No path to health!" );
+ }
+ }
+
+ m_path.Update( me );
+
+ // may need to switch weapons (ie: engineer holding toolbox now needs to heal and defend himself)
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ me->EquipBestWeaponForThreat( threat );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetHealth::OnStuck( CTFBot *me )
+{
+ return TryDone( RESULT_CRITICAL, "Stuck trying to reach health kit" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetHealth::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotGetHealth::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryDone( RESULT_CRITICAL, "Failed to reach health kit" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+// We are always hurrying if we need to collect health
+QueryResultType CTFBotGetHealth::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_get_health.h b/game/server/tf/bot/behavior/tf_bot_get_health.h
new file mode 100644
index 0000000..99d4d1d
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_get_health.h
@@ -0,0 +1,34 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_get_health.h
+// Pick up any nearby health kit
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_GET_HEALTH_H
+#define TF_BOT_GET_HEALTH_H
+
+#include "tf_powerup.h"
+
+class CTFBotGetHealth : 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 );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "GetHealth"; };
+
+private:
+ PathFollower m_path;
+ CHandle< CTFPowerup > m_healthKit;
+ bool m_isGoalDispenser;
+};
+
+
+#endif // TF_BOT_GET_HEALTH_H
diff --git a/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp b/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp
new file mode 100644
index 0000000..c914a8c
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp
@@ -0,0 +1,67 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_melee_attack.h
+// Attack a threat with out melee weapon
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_melee_attack.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+ConVar tf_bot_melee_attack_abandon_range( "tf_bot_melee_attack_abandon_range", "500", FCVAR_CHEAT, "If threat is farther away than this, bot will switch back to its primary weapon and attack" );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotMeleeAttack::CTFBotMeleeAttack( float giveUpRange )
+{
+ m_giveUpRange = giveUpRange < 0.0f ? tf_bot_melee_attack_abandon_range.GetFloat() : giveUpRange;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMeleeAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMeleeAttack::Update( CTFBot *me, float interval )
+{
+ // bash the bad guys
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( threat == NULL )
+ {
+ return Done( "No threat" );
+ }
+
+ if ( me->IsDistanceBetweenGreaterThan( threat->GetLastKnownPosition(), m_giveUpRange ) )
+ {
+ // threat is too far away for melee
+ return Done( "Threat is too far away for a melee attack" );
+ }
+
+ // switch to our melee weapon
+ CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ if ( meleeWeapon )
+ {
+ me->Weapon_Switch( meleeWeapon );
+ }
+
+ // actual head aiming is handled elsewhere
+
+ // just keep swinging
+ me->PressFireButton();
+
+ // chase them down
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Update( me, threat->GetEntity(), cost );
+
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_melee_attack.h b/game/server/tf/bot/behavior/tf_bot_melee_attack.h
new file mode 100644
index 0000000..fc47fd0
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_melee_attack.h
@@ -0,0 +1,26 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_melee_attack.h
+// Attack a threat with out melee weapon
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_MELEE_ATTACK_H
+#define TF_BOT_MELEE_ATTACK_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotMeleeAttack : public Action< CTFBot >
+{
+public:
+ CTFBotMeleeAttack( float giveUpRange = -1.0f );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "MeleeAttack"; };
+
+private:
+ float m_giveUpRange; // if non-negative and if threat is farther than this, give up our melee attack
+ ChasePath m_path;
+};
+
+#endif // TF_BOT_MELEE_ATTACK_H
diff --git a/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp
new file mode 100644
index 0000000..9a620df
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp
@@ -0,0 +1,90 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_move_to_vantage_point.h
+// Move to a position where at least one enemy is visible
+// Michael Booth, November 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_move_to_vantage_point.h"
+
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotMoveToVantagePoint::CTFBotMoveToVantagePoint( float maxTravelDistance )
+{
+ m_maxTravelDistance = maxTravelDistance;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMoveToVantagePoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_vantageArea = me->FindVantagePoint( m_maxTravelDistance );
+ if ( !m_vantageArea )
+ {
+ return Done( "No vantage point found" );
+ }
+
+ m_path.Invalidate();
+ m_repathTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMoveToVantagePoint::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ return Done( "Enemy is visible" );
+ }
+
+ if ( !m_path.IsValid() && m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( 1.0f );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( !m_path.Compute( me, m_vantageArea->GetCenter(), cost ) )
+ {
+ return Done( "No path to vantage point exists" );
+ }
+ }
+
+ // move along path to vantage point
+ m_path.Update( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnStuck( CTFBot *me )
+{
+ m_path.Invalidate();
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryDone( RESULT_CRITICAL, "Vantage point reached" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ m_path.Invalidate();
+ return TryContinue();
+}
+
+
diff --git a/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h
new file mode 100644
index 0000000..b3cb51b
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h
@@ -0,0 +1,33 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_move_to_vantage_point.h
+// Move to a position where at least one enemy is visible
+// Michael Booth, November 2009
+
+#ifndef TF_BOT_MOVE_TO_VANTAGE_POINT_H
+#define TF_BOT_MOVE_TO_VANTAGE_POINT_H
+
+#include "Path/NextBotChasePath.h"
+
+class CTFBotMoveToVantagePoint : public Action< CTFBot >
+{
+public:
+ CTFBotMoveToVantagePoint( float maxTravelDistance = 2000.0f );
+ virtual ~CTFBotMoveToVantagePoint() { }
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual const char *GetName( void ) const { return "MoveToVantagePoint"; };
+
+private:
+ float m_maxTravelDistance;
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ CTFNavArea *m_vantageArea;
+};
+
+#endif // TF_BOT_MOVE_TO_VANTAGE_POINT_H
diff --git a/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp
new file mode 100644
index 0000000..87880fa
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp
@@ -0,0 +1,190 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mvm_deploy_bomb.cpp
+// Set us up the bomb!
+
+#include "cbase.h"
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_mvm_deploy_bomb.h"
+#include "econ_item_system.h"
+
+
+extern ConVar tf_deploying_bomb_delay_time;
+extern ConVar tf_deploying_bomb_time;
+
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMDeployBomb::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ me->SetDeployingBombState( TF_BOMB_DEPLOYING_DELAY );
+ m_timer.Start( tf_deploying_bomb_delay_time.GetFloat() );
+
+ // remember where we start deploying
+ m_anchorPos = me->GetAbsOrigin();
+ me->GetLocomotionInterface()->Stop();
+ me->SetAbsVelocity( Vector( 0.0f, 0.0f, 0.0f ) );
+
+ if ( me->IsMiniBoss() )
+ {
+ static CSchemaAttributeDefHandle pAttrDef_AirblastVerticalVulnerability( "airblast vertical vulnerability multiplier" );
+
+ // Minibosses can't be pushed once they start deploying
+ if ( !pAttrDef_AirblastVerticalVulnerability )
+ {
+ Warning( "TFBotSpawner: Invalid attribute 'airblast vertical vulnerability multiplier'\n" );
+ }
+ else
+ {
+ me->GetAttributeList()->SetRuntimeAttributeValue( pAttrDef_AirblastVerticalVulnerability, 0.0f );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotMvMDeployBomb::Update( CTFBot *me, float interval )
+{
+ CCaptureZone *pAreaTrigger = NULL;
+
+ if ( me->GetDeployingBombState() != TF_BOMB_DEPLOYING_COMPLETE )
+ {
+ pAreaTrigger = me->GetClosestCaptureZone();
+ if ( !pAreaTrigger )
+ {
+ return Done( "No capture zone!" );
+ }
+
+ // if we've been moved, give up and go back to normal behavior
+ const float movedRange = 20.0f;
+ if ( me->IsRangeGreaterThan( m_anchorPos, movedRange ) )
+ {
+ // Look for players that pushed me away and send an event
+ CUtlVector<CTFPlayer *> playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ CTFPlayer *pPlayer = playerVector[i];
+ if ( !pPlayer )
+ continue;
+
+ if ( me->m_AchievementData.IsPusherInHistory( pPlayer, 2.f ) )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_deploy_reset_by_player" );
+ if ( event )
+ {
+ event->SetInt( "player", pPlayer->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ }
+
+ return Done( "I've been pushed" );
+ }
+
+ // face the capture zone
+ me->GetBodyInterface()->AimHeadTowards( pAreaTrigger->WorldSpaceCenter(), IBody::CRITICAL, 0.5f, NULL, "Face point for bomb deploy" );
+
+ // slam facing towards bomb hole
+ Vector to = pAreaTrigger->WorldSpaceCenter() - me->WorldSpaceCenter();
+ to.NormalizeInPlace();
+
+ QAngle desiredAngles;
+ VectorAngles( to, desiredAngles );
+
+ me->SnapEyeAngles( desiredAngles );
+ }
+
+ switch ( me->GetDeployingBombState() )
+ {
+ case TF_BOMB_DEPLOYING_DELAY:
+ if ( m_timer.IsElapsed() )
+ {
+ me->PlaySpecificSequence( "primary_deploybomb" );
+ m_timer.Start( tf_deploying_bomb_time.GetFloat() );
+ me->SetDeployingBombState( TF_BOMB_DEPLOYING_ANIMATING );
+
+ const char *pszSoundName = me->IsMiniBoss() ? "MVM.DeployBombGiant" : "MVM.DeployBombSmall";
+ me->EmitSound( pszSoundName );
+
+ TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Bomb_Alert_Deploying", 5.0f );
+ }
+ break;
+
+ case TF_BOMB_DEPLOYING_ANIMATING:
+ if ( m_timer.IsElapsed() )
+ {
+ if ( pAreaTrigger )
+ {
+ pAreaTrigger->Capture( me );
+ }
+
+ m_timer.Start( 2.0f );
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Robots_Planted" );
+ me->SetDeployingBombState( TF_BOMB_DEPLOYING_COMPLETE );
+ me->m_takedamage = DAMAGE_NO;
+ me->AddEffects( EF_NODRAW );
+ me->RemoveAllWeapons();
+ }
+ break;
+
+ case TF_BOMB_DEPLOYING_COMPLETE:
+ if ( m_timer.IsElapsed() )
+ {
+ me->SetDeployingBombState( TF_BOMB_DEPLOYING_NONE );
+ me->m_takedamage = DAMAGE_YES;
+ me->TakeDamage( CTakeDamageInfo( me, me, 99999.9f, DMG_CRUSH ) );
+ return Done( "I've deployed successfully" );
+ }
+ break;
+ }
+
+ return Continue();
+}
+
+
+extern void TE_PlayerAnimEvent( CBasePlayer *pPlayer, PlayerAnimEvent_t event, int nData );
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotMvMDeployBomb::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ if ( me->GetDeployingBombState() == TF_BOMB_DEPLOYING_ANIMATING )
+ {
+ // reset the in-progress deploy animation
+ me->m_PlayerAnimState->DoAnimationEvent( PLAYERANIMEVENT_SPAWN );
+ TE_PlayerAnimEvent( me, PLAYERANIMEVENT_SPAWN, 0 ); // Send to any clients who can see this guy.
+ }
+
+ if ( me->IsMiniBoss() )
+ {
+ static CSchemaAttributeDefHandle pAttrDef_AirblastVerticalVulnerability( "airblast vertical vulnerability multiplier" );
+
+ // Minibosses can be pushed again
+ if ( !pAttrDef_AirblastVerticalVulnerability )
+ {
+ Warning( "TFBotSpawner: Invalid attribute 'airblast vertical vulnerability multiplier'\n" );
+ }
+ else
+ {
+ me->GetAttributeList()->RemoveAttribute( pAttrDef_AirblastVerticalVulnerability );
+ }
+ }
+
+ me->SetDeployingBombState( TF_BOMB_DEPLOYING_NONE );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotMvMDeployBomb::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
+{
+ // so event doesn't fall thru to buried action which will then redo transition to this state as we stay in contact with the zone
+ return TryToSustain( RESULT_CRITICAL );
+}
+
+QueryResultType CTFBotMvMDeployBomb::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_NO;
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h
new file mode 100644
index 0000000..ae3043c
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h
@@ -0,0 +1,27 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_mvm_deploy_bomb.h
+// Set us up the bomb!
+
+#ifndef TF_BOT_MVM_DEPLOY_BOMB_H
+#define TF_BOT_MVM_DEPLOY_BOMB_H
+
+//-----------------------------------------------------------------------------
+class CTFBotMvMDeployBomb : 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 );
+
+ EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result );
+ QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const;
+
+ virtual const char *GetName( void ) const { return "MvMDeployBomb"; };
+
+private:
+ CountdownTimer m_timer;
+ Vector m_anchorPos;
+};
+
+
+#endif // TF_BOT_MVM_DEPLOY_BOMB_H
diff --git a/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp
new file mode 100644
index 0000000..5dd970b
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp
@@ -0,0 +1,317 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_move_to_cover.cpp
+// Retreat to local cover from known threats
+// Michael Booth, June 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+ConVar tf_bot_retreat_to_cover_range( "tf_bot_retreat_to_cover_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_debug_retreat_to_cover( "tf_bot_debug_retreat_to_cover", "0", FCVAR_CHEAT );
+ConVar tf_bot_wait_in_cover_min_time( "tf_bot_wait_in_cover_min_time", "1", FCVAR_CHEAT );
+ConVar tf_bot_wait_in_cover_max_time( "tf_bot_wait_in_cover_max_time", "2", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotRetreatToCover::CTFBotRetreatToCover( float hideDuration )
+{
+ m_hideDuration = hideDuration;
+ m_actionToChangeToOnceCoverReached = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotRetreatToCover::CTFBotRetreatToCover( Action< CTFBot > *actionToChangeToOnceCoverReached )
+{
+ m_hideDuration = -1.0f;
+ m_actionToChangeToOnceCoverReached = actionToChangeToOnceCoverReached;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// for testing a given area's exposure to known threats
+class CTestAreaAgainstThreats : public IVision::IForEachKnownEntity
+{
+public:
+ CTestAreaAgainstThreats( CTFBot *me, CTFNavArea *area )
+ {
+ m_me = me;
+ m_area = area;
+ m_exposedThreatCount = 0;
+ }
+
+ virtual bool Inspect( const CKnownEntity &known )
+ {
+ VPROF_BUDGET( "CTestAreaAgainstThreats::Inspect", "NextBot" );
+
+ if ( m_me->IsEnemy( known.GetEntity() ) )
+ {
+ const CNavArea *threatArea = known.GetLastKnownArea();
+
+ if ( threatArea )
+ {
+ // is area visible by known threat
+ if ( m_area->IsPotentiallyVisible( threatArea ) )
+ ++m_exposedThreatCount;
+ }
+ }
+
+ return true;
+ }
+
+ CTFBot *m_me;
+ CTFNavArea *m_area;
+ int m_exposedThreatCount;
+};
+
+
+// collect nearby areas that provide cover from our known threats
+class CSearchForCover : public ISearchSurroundingAreasFunctor
+{
+public:
+ CSearchForCover( CTFBot *me )
+ {
+ m_me = me;
+ m_minExposureCount = 9999;
+
+ if ( tf_bot_debug_retreat_to_cover.GetBool() )
+ TheNavMesh->ClearSelectedSet();
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ VPROF_BUDGET( "CSearchForCover::operator()", "NextBot" );
+
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ CTestAreaAgainstThreats test( m_me, area );
+ m_me->GetVisionInterface()->ForEachKnownEntity( test );
+
+ if ( test.m_exposedThreatCount <= m_minExposureCount )
+ {
+ // this area is at least as good as already found cover
+ if ( test.m_exposedThreatCount < m_minExposureCount )
+ {
+ // this area is better than already found cover - throw out list and start over
+ m_coverAreaVector.RemoveAll();
+ m_minExposureCount = test.m_exposedThreatCount;
+ }
+
+ m_coverAreaVector.AddToTail( area );
+ }
+
+ return true;
+ }
+
+ // return true if 'adjArea' should be included in the ongoing search
+ virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar )
+ {
+ if ( travelDistanceSoFar > tf_bot_retreat_to_cover_range.GetFloat() )
+ return false;
+
+ // allow falling off ledges, but don't jump up - too slow
+ return ( currentArea->ComputeAdjacentConnectionHeightChange( adjArea ) < m_me->GetLocomotionInterface()->GetStepHeight() );
+ }
+
+ virtual void PostSearch( void )
+ {
+ if ( tf_bot_debug_retreat_to_cover.GetBool() )
+ {
+ for( int i=0; i<m_coverAreaVector.Count(); ++i )
+ TheNavMesh->AddToSelectedSet( m_coverAreaVector[i] );
+ }
+ }
+
+ CTFBot *m_me;
+ CUtlVector< CTFNavArea * > m_coverAreaVector;
+ int m_minExposureCount;
+};
+
+
+//---------------------------------------------------------------------------------------------
+CTFNavArea *CTFBotRetreatToCover::FindCoverArea( CTFBot *me )
+{
+ VPROF_BUDGET( "CTFBotRetreatToCover::FindCoverArea", "NextBot" );
+
+ CSearchForCover search( me );
+ SearchSurroundingAreas( me->GetLastKnownArea(), search );
+
+ if ( search.m_coverAreaVector.Count() == 0 )
+ {
+ return NULL;
+ }
+
+ // first in vector should be closest via travel distance
+ // pick from the closest 10 areas to avoid the whole team bunching up in one spot
+ int last = MIN( 10, search.m_coverAreaVector.Count() );
+ int which = RandomInt( 0, last-1 );
+ return search.m_coverAreaVector[ which ];
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotRetreatToCover::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ m_coverArea = FindCoverArea( me );
+
+ if ( m_coverArea == NULL )
+ return Done( "No cover available!" );
+
+ if ( m_hideDuration < 0.0f )
+ {
+ m_hideDuration = RandomFloat( tf_bot_wait_in_cover_min_time.GetFloat(), tf_bot_wait_in_cover_max_time.GetFloat() );
+ }
+
+ m_waitInCoverTimer.Start( m_hideDuration );
+
+ // if I'm a spy, cloak and disguise while I retreat
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( !me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotRetreatToCover::Update( CTFBot *me, float interval )
+{
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true );
+
+ if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) )
+ return Done( "I'm invulnerable - no need to retreat!" );
+
+ if ( ShouldRetreat( me ) == ANSWER_NO )
+ return Done( "No longer need to retreat" );
+
+ // attack while retreating
+ me->EquipBestWeaponForThreat( threat );
+
+ // reload while moving to cover
+ bool isDoingAFullReload = false;
+ CTFWeaponBase *myPrimary = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( myPrimary && me->GetAmmoCount( TF_AMMO_PRIMARY ) > 0 && me->IsBarrageAndReloadWeapon( myPrimary ) )
+ {
+ if ( myPrimary->Clip1() < myPrimary->GetMaxClip1() )
+ {
+ me->PressReloadButton();
+ isDoingAFullReload = true;
+ }
+ }
+
+
+ // move to cover, or stop if we've found opportunistic cover (no visible threats right now)
+ if ( me->GetLastKnownArea() == m_coverArea || !threat )
+ {
+ // we are now in cover
+
+ if ( threat )
+ {
+ // threats are still visible - find new cover
+ m_coverArea = FindCoverArea( me );
+
+ if ( m_coverArea == NULL )
+ {
+ return Done( "My cover is exposed, and there is no other cover available!" );
+ }
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) && !me->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ // don't leave cover until my disguise kicks in
+ return Continue();
+ }
+
+ // uncloak so we can attack when we leave cover
+ if ( me->m_Shared.IsStealthed() )
+ {
+ me->PressAltFireButton();
+ }
+
+ if ( m_actionToChangeToOnceCoverReached )
+ {
+ return ChangeTo( m_actionToChangeToOnceCoverReached, "Doing given action now that I'm in cover" );
+ }
+
+ // if I'm being healed by a medic who nearly has his charge built up, wait in cover until his charge is ready
+ int numHealers = me->m_Shared.GetNumHealers();
+ for ( int i=0; i<numHealers; ++i )
+ {
+ CTFPlayer *medic = ToTFPlayer( me->m_Shared.GetHealerByIndex( i ) );
+
+ if ( medic && medic->MedicGetChargeLevel() > 0.9f )
+ {
+ // wait for uber to finish
+ return Continue();
+ }
+ }
+
+ // stay in cover while we fully reload
+ if ( isDoingAFullReload )
+ {
+ return Continue();
+ }
+
+ if ( m_waitInCoverTimer.IsElapsed() )
+ {
+ return Done( "Been in cover long enough" );
+ }
+ }
+ else
+ {
+ // not in cover yet
+ m_waitInCoverTimer.Reset();
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) );
+
+ CTFBotPathCost cost( me, RETREAT_ROUTE );
+ m_path.Compute( me, m_coverArea->GetCenter(), cost );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnStuck( CTFBot *me )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Hustle yer butt to safety!
+QueryResultType CTFBotRetreatToCover::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_YES;
+}
+
+
diff --git a/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h
new file mode 100644
index 0000000..799a4af
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h
@@ -0,0 +1,41 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_retreat_to_cover.h
+// Retreat to local cover from known threats
+// Michael Booth, June 2009
+
+#ifndef TF_BOT_RETREAT_TO_COVER_H
+#define TF_BOT_RETREAT_TO_COVER_H
+
+class CTFBotRetreatToCover : public Action< CTFBot >
+{
+public:
+ CTFBotRetreatToCover( float hideDuration = -1.0f );
+ CTFBotRetreatToCover( Action< CTFBot > *actionToChangeToOnceCoverReached );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual const char *GetName( void ) const { return "RetreatToCover"; };
+
+private:
+ float m_hideDuration;
+ Action< CTFBot > *m_actionToChangeToOnceCoverReached;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ CTFNavArea *m_coverArea;
+ CountdownTimer m_waitInCoverTimer;
+
+ CTFNavArea *FindCoverArea( CTFBot *me );
+};
+
+
+
+#endif // TF_BOT_RETREAT_TO_COVER_H
diff --git a/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp
new file mode 100644
index 0000000..50ed460
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp
@@ -0,0 +1,368 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_scenario_monitor.h
+// Behavior layer that interrupts for scenario rules (picked up flag, drop what you're doing and capture, etc)
+// Michael Booth, May 2011
+
+#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/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/tf_bot_tactical_monitor.h"
+#include "bot/behavior/tf_bot_retreat_to_cover.h"
+#include "bot/behavior/tf_bot_get_health.h"
+#include "bot/behavior/tf_bot_get_ammo.h"
+#include "bot/behavior/sniper/tf_bot_sniper_lurk.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h"
+#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_guard.h"
+#include "bot/behavior/scenario/payload/tf_bot_payload_push.h"
+#include "bot/behavior/tf_bot_use_teleporter.h"
+#include "bot/behavior/training/tf_bot_training.h"
+#include "bot/behavior/tf_bot_destroy_enemy_sentry.h"
+#include "bot/behavior/engineer/tf_bot_engineer_building.h"
+#include "bot/behavior/spy/tf_bot_spy_infiltrate.h"
+#include "bot/behavior/spy/tf_bot_spy_leave_spawn_room.h"
+#include "bot/behavior/medic/tf_bot_medic_heal.h"
+#include "bot/behavior/engineer/tf_bot_engineer_build.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+
+#ifdef TF_RAID_MODE
+#include "bot/behavior/scenario/raid/tf_bot_wander.h"
+#include "bot/behavior/scenario/raid/tf_bot_companion.h"
+#include "bot/behavior/scenario/raid/tf_bot_squad_attack.h"
+#include "bot/behavior/scenario/raid/tf_bot_guard_area.h"
+#endif // TF_RAID_MODE
+
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_seek_and_destroy.h"
+#include "bot/behavior/tf_bot_taunt.h"
+#include "bot/behavior/tf_bot_escort.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h"
+#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h"
+
+#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h"
+#include "bot/behavior/squad/tf_bot_escort_squad_leader.h"
+#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h"
+#include "bot/behavior/missions/tf_bot_mission_reprogrammed.h"
+
+#include "bot/behavior/tf_bot_scenario_monitor.h"
+
+
+extern ConVar tf_bot_health_ok_ratio;
+extern ConVar tf_bot_health_critical_ratio;
+
+
+//-----------------------------------------------------------------------------------------
+// Returns the initial Action we will run concurrently as a child to us
+Action< CTFBot > *CTFBotScenarioMonitor::InitialContainedAction( CTFBot *me )
+{
+ if ( me->IsInASquad() )
+ {
+ if ( me->GetSquad()->IsLeader( me ) )
+ {
+ // I'm the leader of this Squad, so I can do what I want and the other Squaddies will support me
+ return DesiredScenarioAndClassAction( me );
+ }
+
+ // Medics are the exception - they always heal, and have special squad logic in their heal logic
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return new CTFBotMedicHeal;
+ }
+
+ // I'm in a Squad but not the leader, do "escort and support" Squad behavior
+ // until the Squad disbands, and then do my normal thing
+ return new CTFBotEscortSquadLeader( DesiredScenarioAndClassAction( me ) );
+ }
+
+ return DesiredScenarioAndClassAction( me );
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Returns Action specific to the scenario and my class
+Action< CTFBot > *CTFBotScenarioMonitor::DesiredScenarioAndClassAction( CTFBot *me )
+{
+ switch( me->GetMission() )
+ {
+ case CTFBot::MISSION_SEEK_AND_DESTROY:
+ break;
+
+ case CTFBot::MISSION_DESTROY_SENTRIES:
+ return new CTFBotMissionSuicideBomber;
+
+ case CTFBot::MISSION_SNIPER:
+ return new CTFBotSniperLurk;
+
+#ifdef STAGING_ONLY
+ case CTFBot::MISSION_REPROGRAMMED:
+ return new CTFBotMissionReprogrammed;
+#endif
+ }
+
+#ifdef TF_RAID_MODE
+ if ( me->HasAttribute( CTFBot::IS_NPC ) )
+ {
+ // map-spawned guardians
+ return new CTFBotGuardian;
+ }
+#endif // TF_RAID_MODE
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsBossBattleMode() )
+ {
+ if ( me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ // bot teammates
+ return new CTFBotCompanion;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ return new CTFBotSniperLurk;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return new CTFBotSpyInfiltrate;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return new CTFBotMedicHeal;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return new CTFBotEngineerBuild;
+ }
+
+ return new CTFBotEscort( TFGameRules()->GetActiveBoss() );
+ }
+ else if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ // bot teammates
+ return new CTFBotCompanion;
+ }
+
+ if ( me->IsInASquad() )
+ {
+ // squad behavior
+ return new CTFBotSquadAttack;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SCOUT ) || me->HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ return new CTFBotWander;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ return new CTFBotSniperLurk;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return new CTFBotSpyInfiltrate;
+ }
+
+ return new CTFBotGuardArea;
+ }
+#endif // TF_RAID_MODE
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return new CTFBotSpyLeaveSpawnRoom;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // if I'm being healed by another medic, I should do something else other than healing
+ bool bIsBeingHealedByAMedic = false;
+ int nNumHealers = me->m_Shared.GetNumHealers();
+ for ( int i=0; i<nNumHealers; ++i )
+ {
+ CBaseEntity *pHealer = me->m_Shared.GetHealerByIndex(i);
+ if ( pHealer && pHealer->IsPlayer() )
+ {
+ bIsBeingHealedByAMedic = true;
+ break;
+ }
+ }
+
+ if ( !bIsBeingHealedByAMedic )
+ {
+ return new CTFBotMedicHeal;
+ }
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return new CTFBotMvMEngineerIdle;
+ }
+
+ // NOTE: Snipers are intentionally left out so they go after the flag. Actual sniping behavior is done as a mission.
+
+ if ( me->HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ // push for the point first, then attack
+ return new CTFBotPushToCapturePoint( new CTFBotFetchFlag );
+ }
+
+ // capture the flag
+ return new CTFBotFetchFlag;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return new CTFBotSpyInfiltrate;
+ }
+
+ if ( !TheTFBots().IsMeleeOnly() )
+ {
+ if ( me->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ return new CTFBotSniperLurk;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ return new CTFBotMedicHeal;
+ }
+
+ if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return new CTFBotEngineerBuild;
+ }
+ }
+
+ if ( me->GetFlagToFetch() )
+ {
+ // capture the flag
+ return new CTFBotFetchFlag;
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ // push the cart
+ if ( me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ // blu is pushing
+ return new CTFBotPayloadPush;
+ }
+ else if ( me->GetTeamNumber() == TF_TEAM_RED )
+ {
+ // red is blocking
+ return new CTFBotPayloadGuard;
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ // if we have a point we can capture - do it
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( me, &captureVector );
+
+ if ( captureVector.Count() > 0 )
+ {
+ return new CTFBotCapturePoint;
+ }
+
+ // otherwise, defend our point(s) from capture
+ CUtlVector< CTeamControlPoint * > defendVector;
+ TFGameRules()->CollectDefendPoints( me, &defendVector );
+
+ if ( defendVector.Count() > 0 )
+ {
+ return new CTFBotDefendPoint;
+ }
+
+ // likely KotH mode and/or all points are locked - assume capture
+ DevMsg( "%3.2f: %s: Gametype is CP, but I can't find a point to capture or defend!\n", gpGlobals->curtime, me->GetDebugIdentifier() );
+ return new CTFBotCapturePoint;
+ }
+ else
+ {
+ // scenario not implemented yet - just fight
+ return new CTFBotSeekAndDestroy;
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotScenarioMonitor::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_ignoreLostFlagTimer.Start( 20.0f );
+ m_lostFlagTimer.Invalidate();
+ return Continue();
+}
+
+
+ConVar tf_bot_fetch_lost_flag_time( "tf_bot_fetch_lost_flag_time", "10", FCVAR_CHEAT, "How long busy TFBots will ignore the dropped flag before they give up what they are doing and go after it" );
+ConVar tf_bot_flag_kill_on_touch( "tf_bot_flag_kill_on_touch", "0", FCVAR_CHEAT, "If nonzero, any bot that picks up the flag dies. For testing." );
+
+
+//-----------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotScenarioMonitor::Update( CTFBot *me, float interval )
+{
+ // CTF Scenario
+ if ( me->HasTheFlag() )
+ {
+ if ( tf_bot_flag_kill_on_touch.GetBool() )
+ {
+ me->CommitSuicide( false, true );
+ return Done( "Flag kill" );
+ }
+
+ // we just picked up the flag - drop what we're doing and take it in
+ return SuspendFor( new CTFBotDeliverFlag, "I've picked up the flag! Running it in..." );
+ }
+
+ if ( me->HasMission( CTFBot::NO_MISSION ) && m_ignoreLostFlagTimer.IsElapsed() && me->IsAllowedToPickUpFlag() )
+ {
+ CCaptureFlag *flag = me->GetFlagToFetch();
+
+ if ( flag )
+ {
+ CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() );
+ if ( carrier )
+ {
+ m_lostFlagTimer.Invalidate();
+ }
+ else
+ {
+ // flag is loose
+ if ( !m_lostFlagTimer.HasStarted() )
+ {
+ m_lostFlagTimer.Start( tf_bot_fetch_lost_flag_time.GetFloat() );
+ }
+ else if ( m_lostFlagTimer.IsElapsed() )
+ {
+ m_lostFlagTimer.Invalidate();
+
+ // if we're a Medic an actively healing someone, don't interrupt
+ if ( !me->MedicGetHealTarget() )
+ {
+ // we better go get the flag
+ return SuspendFor( new CTFBotFetchFlag( TEMPORARY_FLAG_FETCH ), "Fetching lost flag..." );
+ }
+ }
+ }
+ }
+ }
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h
new file mode 100644
index 0000000..e0e4f89
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h
@@ -0,0 +1,27 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_scenario_monitor.h
+// Behavior layer that interrupts for scenario rules (picked up flag, drop what you're doing and capture, etc)
+// Michael Booth, May 2011
+
+#ifndef TF_BOT_SCENARIO_MONITOR_H
+#define TF_BOT_SCENARIO_MONITOR_H
+
+class CTFBotScenarioMonitor : public Action< CTFBot >
+{
+public:
+ virtual Action< CTFBot > *InitialContainedAction( CTFBot *me );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "ScenarioMonitor"; }
+
+private:
+ CountdownTimer m_ignoreLostFlagTimer;
+ CountdownTimer m_lostFlagTimer;
+
+ virtual Action< CTFBot > *DesiredScenarioAndClassAction( CTFBot *me );
+};
+
+
+#endif // TF_BOT_SCENARIO_MONITOR_H
diff --git a/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp
new file mode 100644
index 0000000..5e78ea3
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp
@@ -0,0 +1,250 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_seek_and_destroy.h
+// Roam the environment, attacking victims
+// Michael Booth, January 2010
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "team_control_point_master.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_attack.h"
+#include "bot/behavior/tf_bot_seek_and_destroy.h"
+#include "bot/behavior/sniper/tf_bot_sniper_attack.h"
+#include "nav_mesh.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+extern ConVar tf_bot_offense_must_push_time;
+extern ConVar tf_bot_defense_must_defend_time;
+
+ConVar tf_bot_debug_seek_and_destroy( "tf_bot_debug_seek_and_destroy", "0", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotSeekAndDestroy::CTFBotSeekAndDestroy( float duration )
+{
+ if ( duration > 0.0f )
+ {
+ m_giveUpTimer.Start( duration );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSeekAndDestroy::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ RecomputeSeekPath( me );
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ m_isPointLocked = ( point && point->IsLocked() );
+
+ // restart the timer if we have one
+ if ( m_giveUpTimer.HasStarted() )
+ {
+ m_giveUpTimer.Reset();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSeekAndDestroy::Update( CTFBot *me, float interval )
+{
+ if ( m_giveUpTimer.HasStarted() && m_giveUpTimer.IsElapsed() )
+ {
+ return Done( "Behavior duration elapsed" );
+ }
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // if the trainee has started capturing the point, assist them
+ if ( me->IsAnyPointBeingCaptured() )
+ {
+ return Done( "Assist trainee in capturing the point" );
+ }
+ }
+ else
+ {
+ if ( me->IsCapturingPoint() )
+ {
+ return Done( "Keep capturing point I happened to stumble upon" );
+ }
+
+ if ( m_isPointLocked )
+ {
+ CTeamControlPoint *point = me->GetMyControlPoint();
+
+ if ( point && !point->IsLocked() )
+ {
+ return Done( "The point just unlocked" );
+ }
+ }
+
+ if ( !TFGameRules()->RoundHasBeenWon() && me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() )
+ {
+ return Done( "Time to push for the objective" );
+ }
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat )
+ {
+ if ( TFGameRules()->RoundHasBeenWon() )
+ {
+ // hunt down the losers
+ return SuspendFor( new CTFBotAttack, "Chasing down the losers" );
+ }
+
+ const float engageRange = 1000.0f;
+ if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), engageRange ) )
+ {
+ return SuspendFor( new CTFBotAttack, "Going after an enemy" );
+ }
+ }
+
+ // move towards our seek goal
+ m_path.Update( me );
+
+ if ( !m_path.IsValid() && m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( 1.0f );
+
+ RecomputeSeekPath( me );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotSeekAndDestroy::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
+{
+ RecomputeSeekPath( me );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnStuck( CTFBot *me )
+{
+ RecomputeSeekPath( me );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnMoveToSuccess( CTFBot *me, const Path *path )
+{
+ RecomputeSeekPath( me );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
+{
+ RecomputeSeekPath( me );
+
+ return TryContinue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSeekAndDestroy::ShouldRetreat( const INextBot *meBot ) const
+{
+ CTFBot *me = (CTFBot *)meBot->GetEntity();
+
+ if ( me->IsPlayerClass( TF_CLASS_PYRO ) )
+ {
+ return ANSWER_NO;
+ }
+
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+QueryResultType CTFBotSeekAndDestroy::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTFNavArea *CTFBotSeekAndDestroy::ChooseGoalArea( CTFBot *me )
+{
+ CUtlVector< CTFNavArea * > goalVector;
+
+ TheTFNavMesh()->CollectSpawnRoomThresholdAreas( &goalVector, GetEnemyTeam( me->GetTeamNumber() ) );
+
+ CTeamControlPoint *point = me->GetMyControlPoint();
+ if ( point && !point->IsLocked() )
+ {
+ // add current control point as a seek goal
+ const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() );
+ if ( controlPointAreas && controlPointAreas->Count() > 0 )
+ {
+ goalVector.AddToTail( controlPointAreas->Element( RandomInt( 0, controlPointAreas->Count()-1 ) ) );
+ }
+ }
+
+ if ( tf_bot_debug_seek_and_destroy.GetBool() )
+ {
+ for( int i=0; i<goalVector.Count(); ++i )
+ {
+ TheNavMesh->AddToSelectedSet( goalVector[i] );
+ }
+ }
+
+ // pick a new goal
+ if ( goalVector.Count() > 0 )
+ {
+ return goalVector[ RandomInt( 0, goalVector.Count()-1 ) ];
+ }
+
+ return NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotSeekAndDestroy::RecomputeSeekPath( CTFBot *me )
+{
+ m_goalArea = ChooseGoalArea( me );
+ if ( m_goalArea )
+ {
+ CTFBotPathCost cost( me, SAFEST_ROUTE );
+ m_path.Compute( me, m_goalArea->GetCenter(), cost );
+ }
+ else
+ {
+ m_path.Invalidate();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryContested( CTFBot *me, int territoryID )
+{
+ return TryDone( RESULT_IMPORTANT, "Defending the point" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryCaptured( CTFBot *me, int territoryID )
+{
+ return TryDone( RESULT_IMPORTANT, "Giving up due to point capture" );
+}
+
+
+//---------------------------------------------------------------------------------------------
+EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryLost( CTFBot *me, int territoryID )
+{
+ return TryDone( RESULT_IMPORTANT, "Giving up due to point lost" );
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h
new file mode 100644
index 0000000..26dba48
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h
@@ -0,0 +1,51 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_seek_and_destroy.h
+// Roam the environment, attacking victims
+// Michael Booth, January 2010
+
+#ifndef TF_BOT_SEEK_AND_DESTROY_H
+#define TF_BOT_SEEK_AND_DESTROY_H
+
+#include "Path/NextBotChasePath.h"
+
+
+//
+// Roam around the map attacking enemies
+//
+class CTFBotSeekAndDestroy : public Action< CTFBot >
+{
+public:
+ CTFBotSeekAndDestroy( float duration = -1.0f );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction );
+
+ virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me );
+ virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path );
+ virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason );
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat?
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry?
+
+ virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID );
+ virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID );
+
+ virtual const char *GetName( void ) const { return "SeekAndDestroy"; };
+
+private:
+ CTFNavArea *m_goalArea;
+ CTFNavArea *ChooseGoalArea( CTFBot *me );
+ bool m_isPointLocked;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+ void RecomputeSeekPath( CTFBot *me );
+
+ CountdownTimer m_giveUpTimer;
+};
+
+
+#endif // TF_BOT_SEEK_AND_DESTROY_H
diff --git a/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp
new file mode 100644
index 0000000..e68cd9a
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp
@@ -0,0 +1,659 @@
+//========= 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;
+}
diff --git a/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h
new file mode 100644
index 0000000..1bbd658
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h
@@ -0,0 +1,47 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_tactical_monitor.h
+// Behavior layer that interrupts for ammo/health/retreat/etc
+// Michael Booth, June 2009
+
+#ifndef TF_BOT_TACTICAL_MONITOR_H
+#define TF_BOT_TACTICAL_MONITOR_H
+
+class CObjectTeleporter;
+
+class CTFBotTacticalMonitor : public Action< CTFBot >
+{
+public:
+ virtual Action< CTFBot > *InitialContainedAction( CTFBot *me );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual EventDesiredResult< CTFBot > OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea );
+ virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info );
+
+ // @note Tom Bui: Currently used for the training stuff, but once we get that interface down, we will turn that
+ // into a proper API
+ virtual EventDesiredResult< CTFBot > OnCommandString( CTFBot *me, const char *command );
+
+ virtual const char *GetName( void ) const { return "TacticalMonitor"; }
+
+private:
+ CountdownTimer m_maintainTimer;
+
+ CountdownTimer m_acknowledgeAttentionTimer;
+ CountdownTimer m_acknowledgeRetryTimer;
+ CountdownTimer m_attentionTimer;
+
+ CountdownTimer m_stickyBombCheckTimer;
+ void MonitorArmedStickyBombs( CTFBot *me );
+
+ bool ShouldOpportunisticallyTeleport( CTFBot *me ) const;
+ CObjectTeleporter *FindNearbyTeleporter( CTFBot *me );
+ CountdownTimer m_findTeleporterTimer;
+
+ void AvoidBumpingEnemies( CTFBot *me );
+};
+
+
+
+#endif // TF_BOT_TACTICAL_MONITOR_H
diff --git a/game/server/tf/bot/behavior/tf_bot_taunt.cpp b/game/server/tf/bot/behavior/tf_bot_taunt.cpp
new file mode 100644
index 0000000..b0034fa
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_taunt.cpp
@@ -0,0 +1,53 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_taunt.cpp
+// Stand still and play a taunt animation
+// Michael Booth, November 2009
+
+#include "cbase.h"
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_taunt.h"
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotTaunt::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // wait a short random time so entire mob doesn't taunt in unison
+ m_tauntTimer.Start( RandomFloat( 0, 1.0f ) );
+ m_didTaunt = false;
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotTaunt::Update( CTFBot *me, float interval )
+{
+ if ( m_tauntTimer.IsElapsed() )
+ {
+ if ( m_didTaunt )
+ {
+ // Stop taunting after a while
+ if ( m_tauntEndTimer.IsElapsed() && me->m_Shared.GetTauntIndex() == TAUNT_LONG )
+ {
+ me->EndLongTaunt();
+ }
+
+ if ( me->m_Shared.InCond( TF_COND_TAUNTING ) == false )
+ {
+ return Done( "Taunt finished" );
+ }
+ }
+ else
+ {
+ me->HandleTauntCommand();
+ // Start a timer to end our taunt in case we're still going after awhile
+ m_tauntEndTimer.Start( RandomFloat( 3.f, 5.f ) );
+
+ m_didTaunt = true;
+ }
+ }
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_taunt.h b/game/server/tf/bot/behavior/tf_bot_taunt.h
new file mode 100644
index 0000000..37a9074
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_taunt.h
@@ -0,0 +1,26 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_taunt.h
+// Stand still and play a taunt animation
+// Michael Booth, November 2009
+
+#ifndef TF_BOT_TAUNT_H
+#define TF_BOT_TAUNT_H
+
+
+//-----------------------------------------------------------------------------
+class CTFBotTaunt : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "Taunt"; };
+
+private:
+ CountdownTimer m_tauntTimer;
+ CountdownTimer m_tauntEndTimer;
+ bool m_didTaunt;
+};
+
+
+#endif // TF_BOT_TAUNT_H
diff --git a/game/server/tf/bot/behavior/tf_bot_use_item.cpp b/game/server/tf/bot/behavior/tf_bot_use_item.cpp
new file mode 100644
index 0000000..ee8e9ec
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_use_item.cpp
@@ -0,0 +1,73 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_use_item.h
+// Equip and consume an item
+// Michael Booth, July 2011
+
+#include "cbase.h"
+#include "tf_weaponbase.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_use_item.h"
+
+
+//---------------------------------------------------------------------------------------------
+CTFBotUseItem::CTFBotUseItem( CTFWeaponBase *item )
+{
+ m_item = item;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUseItem::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ // force-equip the item we're going to use
+ me->PushRequiredWeapon( m_item );
+
+ m_cooldownTimer.Start( m_item->m_flNextPrimaryAttack - gpGlobals->curtime + 0.25f );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUseItem::Update( CTFBot *me, float interval )
+{
+ if ( m_item == NULL )
+ {
+ return Done( "NULL item" );
+ }
+
+ CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon();
+
+ if ( !myCurrentWeapon )
+ {
+ return Done( "NULL weapon" );
+ }
+
+ if ( m_cooldownTimer.HasStarted() )
+ {
+ if ( m_cooldownTimer.IsElapsed() )
+ {
+ // use it
+ me->PressFireButton();
+ m_cooldownTimer.Invalidate();
+ }
+ }
+ else // used
+ {
+ // some items use the taunt system - wait for the taunt to end
+ if ( !me->IsTaunting() )
+ {
+ return Done( "Item used" );
+ }
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBotUseItem::OnEnd( CTFBot *me, Action< CTFBot > *nextAction )
+{
+ me->PopRequiredWeapon();
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_use_item.h b/game/server/tf/bot/behavior/tf_bot_use_item.h
new file mode 100644
index 0000000..33467a3
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_use_item.h
@@ -0,0 +1,27 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_use_item.h
+// Equip and consume an item
+// Michael Booth, July 2011
+
+#ifndef TF_BOT_USE_ITEM_H
+#define TF_BOT_USE_ITEM_H
+
+class CTFBotUseItem : public Action< CTFBot >
+{
+public:
+ CTFBotUseItem( CTFWeaponBase *item );
+ virtual ~CTFBotUseItem() { }
+
+ 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 const char *GetName( void ) const { return "UseItem"; };
+
+private:
+ CHandle< CTFWeaponBase > m_item;
+ CountdownTimer m_cooldownTimer;
+};
+
+
+#endif // TF_BOT_USE_ITEM_H
diff --git a/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp b/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp
new file mode 100644
index 0000000..fc249fb
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp
@@ -0,0 +1,123 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_use_teleporter.cpp
+// Ride a friendly teleporter
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "tf_player.h"
+#include "bot/tf_bot.h"
+#include "bot/behavior/tf_bot_use_teleporter.h"
+
+extern ConVar tf_bot_path_lookahead_range;
+
+//---------------------------------------------------------------------------------------------
+CTFBotUseTeleporter::CTFBotUseTeleporter( CObjectTeleporter *teleporter, UseHowType how )
+{
+ m_teleporter = teleporter;
+ m_how = how;
+ m_isInTransit = false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUseTeleporter::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// We could compute the time it would take to walk from the tele entrance to exit
+// and compare that to the time needed to wait for the tele to be ready to send us,
+// but players tend to use the tele only if it is ready NOW (or very soon).
+bool CTFBotUseTeleporter::IsTeleporterAvailable( void ) const
+{
+ if ( m_teleporter != NULL )
+ {
+ if ( !m_teleporter->IsReady() )
+ return false;
+
+ if ( m_teleporter->GetState() == TELEPORTER_STATE_READY )
+ return true;
+
+/* causes massive bot pileups
+ if ( m_teleporter->GetState() == TELEPORTER_STATE_SENDING ||
+ m_teleporter->GetState() == TELEPORTER_STATE_RECHARGING )
+ {
+ if ( m_teleporter->GetUpgradeLevel() == 3 )
+ {
+ // we'll wait for level 3 teleporters - they're really fast
+ return true;
+ }
+ }
+*/
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CTFBot > CTFBotUseTeleporter::Update( CTFBot *me, float interval )
+{
+ if ( m_teleporter == NULL )
+ {
+ return Done( "Teleporter is gone" );
+ }
+
+ CObjectTeleporter *teleporterExit = m_teleporter->GetMatchingTeleporter();
+ if ( !teleporterExit )
+ {
+ return Done( "Missing teleporter exit" );
+ }
+
+ if ( m_teleporter->IsSendingPlayer( me ) )
+ {
+ // note that we have been teleported, because it takes a few frames
+ // to actually relocate us, even after our teleporter leaves the "sending" state
+ m_isInTransit = true;
+ }
+
+ if ( m_isInTransit )
+ {
+ if ( me->IsRangeLessThan( teleporterExit, 25.0f ) )
+ {
+ return Done( "Successful teleport" );
+ }
+ }
+ else if ( !IsTeleporterAvailable() && m_how == USE_IF_READY )
+ {
+ return Done( "Teleporter is not available" );
+ }
+
+ const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleRecently() )
+ {
+ // prepare to fight
+ me->EquipBestWeaponForThreat( threat );
+ }
+
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ if ( m_path.Compute( me, m_teleporter->GetAbsOrigin(), cost ) == false )
+ {
+ // no path to teleporter
+ return Done( "Can't reach teleporter!" );
+ }
+ }
+
+ // move toward the teleporter until we're standing on it
+ if ( me->GetLocomotionInterface()->GetGround() != m_teleporter )
+ {
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
diff --git a/game/server/tf/bot/behavior/tf_bot_use_teleporter.h b/game/server/tf/bot/behavior/tf_bot_use_teleporter.h
new file mode 100644
index 0000000..6b36b33
--- /dev/null
+++ b/game/server/tf/bot/behavior/tf_bot_use_teleporter.h
@@ -0,0 +1,40 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_use_teleporter.h
+// Ride a friendly teleporter
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_USE_TELEPORTER_H
+#define TF_BOT_USE_TELEPORTER_H
+
+#include "tf_obj_teleporter.h"
+#include "Path/NextBotPathFollow.h"
+
+class CTFBotUseTeleporter : public Action< CTFBot >
+{
+public:
+ enum UseHowType
+ {
+ USE_IF_READY,
+ ALWAYS_USE
+ };
+ CTFBotUseTeleporter( CObjectTeleporter *teleporter, UseHowType how = USE_IF_READY );
+
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+
+ virtual const char *GetName( void ) const { return "UseTeleporter"; };
+
+private:
+ CHandle< CObjectTeleporter > m_teleporter; // the teleporter we're trying to use
+ UseHowType m_how;
+
+ PathFollower m_path;
+ CountdownTimer m_repathTimer;
+
+ bool m_isInTransit;
+
+ bool IsTeleporterAvailable( void ) const;
+};
+
+
+#endif // TF_BOT_USE_TELEPORTER_H
diff --git a/game/server/tf/bot/behavior/training/tf_bot_training.cpp b/game/server/tf/bot/behavior/training/tf_bot_training.cpp
new file mode 100644
index 0000000..7db948b
--- /dev/null
+++ b/game/server/tf/bot/behavior/training/tf_bot_training.cpp
@@ -0,0 +1,133 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// tf_bot_training.cpp
+//
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "cbase.h"
+#include "team.h"
+#include "bot/tf_bot.h"
+#include "bot/map_entities/tf_bot_generator.h"
+#include "bot/behavior/training/tf_bot_training.h"
+#include "tf_obj_sentrygun.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ActionResult< CTFBot > CTFDespawn::Update( CTFBot *me, float interval )
+{
+ // players need to be kicked, not deleted
+ if ( me->GetEntity()->IsPlayer() )
+ {
+ CBasePlayer *player = dynamic_cast< CBasePlayer * >( me->GetEntity() );
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
+ }
+ else
+ {
+ UTIL_Remove( me->GetEntity() );
+ }
+ return Continue();
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ActionResult< CTFBot > CTFTrainingAttackSentryActionPoint::Update( CTFBot *me, float interval )
+{
+ CTFBotActionPoint* pActionPoint = me->GetActionPoint();
+ if ( pActionPoint == NULL )
+ {
+ return Done();
+ }
+
+ if ( pActionPoint->IsWithinRange( me ) )
+ {
+ CObjectSentrygun *pSentrygun = me->GetEnemySentry();
+ if ( pSentrygun )
+ {
+ me->GetBodyInterface()->AimHeadTowards( pSentrygun, IBody::MANDATORY, 1.0f, NULL, "Aiming at enemy sentry" );
+
+ // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades
+ Vector toSentry = pSentrygun->WorldSpaceCenter() - me->EyePosition();
+ toSentry.NormalizeInPlace();
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ if ( ( forward.x * toSentry.x + forward.y * toSentry.y ) > 0.95f )
+ {
+ me->PressFireButton();
+ }
+ }
+ }
+ else
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, pActionPoint->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+ }
+
+ return Continue();
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ActionResult< CTFBot > CTFGotoActionPoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
+{
+ m_stayTimer.Invalidate();
+ m_wasTeleported = false;
+
+ return Continue();
+}
+
+ActionResult< CTFBot > CTFGotoActionPoint::Update( CTFBot *me, float interval )
+{
+ CTFBotActionPoint* pActionPoint = me->GetActionPoint();
+ if ( pActionPoint == NULL )
+ {
+ return Done();
+ }
+
+ if ( pActionPoint->IsWithinRange( me ) )
+ {
+ // track if we ever get teleported during this process
+ m_wasTeleported |= me->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT );
+
+ // we're at the action point
+ if ( m_stayTimer.HasStarted() == false )
+ {
+ // this method may cause us to become suspended for other actions
+ pActionPoint->ReachedActionPoint( me );
+
+ m_stayTimer.Start( pActionPoint->m_stayTime );
+ }
+ else if ( m_stayTimer.IsElapsed() )
+ {
+ me->SetActionPoint( dynamic_cast< CTFBotActionPoint * >( pActionPoint->m_moveGoal.Get() ) );
+ return ChangeTo( new CTFGotoActionPoint, "Reached point, going to next" );
+ }
+ }
+ else if ( m_wasTeleported )
+ {
+ // we reached our action point, but were teleported far away.
+ // presumably we've resumed, so just go to the next action point.
+ me->SetActionPoint( dynamic_cast< CTFBotActionPoint * >( pActionPoint->m_moveGoal.Get() ) );
+ return ChangeTo( new CTFGotoActionPoint, "Reached point, going to next" );
+ }
+ else
+ {
+ if ( m_repathTimer.IsElapsed() )
+ {
+ m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+ CTFBotPathCost cost( me, FASTEST_ROUTE );
+ m_path.Compute( me, pActionPoint->GetAbsOrigin(), cost );
+ }
+
+ m_path.Update( me );
+ }
+ return Continue();
+}
diff --git a/game/server/tf/bot/behavior/training/tf_bot_training.h b/game/server/tf/bot/behavior/training/tf_bot_training.h
new file mode 100644
index 0000000..d8d6cde
--- /dev/null
+++ b/game/server/tf/bot/behavior/training/tf_bot_training.h
@@ -0,0 +1,54 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// tf_bot_training.h
+//
+// Misc. training actions/behaviors. To be split up into separate files when we deem them "re-usable"
+//
+// Tom Bui, April 2010
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TF_BOT_TRAINING_H
+#define TF_BOT_TRAINING_H
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// Attempts to kick/despawn the bot in the Update()
+
+class CTFDespawn : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual const char *GetName( void ) const { return "Despawn"; };
+};
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// Simple behavior for training where the bot approaches action point and tries to fire at it (and anything there)
+
+class CTFTrainingAttackSentryActionPoint : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual const char *GetName( void ) const { return "Despawn"; };
+
+private:
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// Tells a bot to go an Action Point and run any command it has
+class CTFGotoActionPoint : public Action< CTFBot >
+{
+public:
+ virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction );
+ virtual ActionResult< CTFBot > Update( CTFBot *me, float interval );
+ virtual const char *GetName( void ) const { return "GotoActionPoint"; };
+
+private:
+ CountdownTimer m_stayTimer;
+ CountdownTimer m_repathTimer;
+ PathFollower m_path;
+ bool m_wasTeleported;
+};
+
+#endif // TF_BOT_TRAINING_H