summaryrefslogtreecommitdiff
path: root/game/server/tf/bot
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
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/bot')
-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
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_generator.cpp470
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_generator.h100
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint.cpp128
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint.h54
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp160
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h53
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp60
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_entity.h59
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp42
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h75
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp19
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h23
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_proxy.cpp167
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_proxy.h58
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_roster.cpp107
-rw-r--r--game/server/tf/bot/map_entities/tf_bot_roster.h39
-rw-r--r--game/server/tf/bot/map_entities/tf_spawner.cpp163
-rw-r--r--game/server/tf/bot/map_entities/tf_spawner.h66
-rw-r--r--game/server/tf/bot/map_entities/tf_spawner_boss.cpp167
-rw-r--r--game/server/tf/bot/map_entities/tf_spawner_boss.h54
-rw-r--r--game/server/tf/bot/tf_bot.cpp4644
-rw-r--r--game/server/tf/bot/tf_bot.h1080
-rw-r--r--game/server/tf/bot/tf_bot_body.cpp42
-rw-r--r--game/server/tf/bot/tf_bot_body.h24
-rw-r--r--game/server/tf/bot/tf_bot_locomotion.cpp137
-rw-r--r--game/server/tf/bot/tf_bot_locomotion.h50
-rw-r--r--game/server/tf/bot/tf_bot_manager.cpp872
-rw-r--r--game/server/tf/bot/tf_bot_manager.h147
-rw-r--r--game/server/tf/bot/tf_bot_squad.cpp294
-rw-r--r--game/server/tf/bot/tf_bot_squad.h121
-rw-r--r--game/server/tf/bot/tf_bot_vision.cpp482
-rw-r--r--game/server/tf/bot/tf_bot_vision.h44
166 files changed, 30931 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
diff --git a/game/server/tf/bot/map_entities/tf_bot_generator.cpp b/game/server/tf/bot/map_entities/tf_bot_generator.cpp
new file mode 100644
index 0000000..38fbc3b
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_generator.cpp
@@ -0,0 +1,470 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_generator.cpp
+// Entity to spawn a collection of TFBots
+// Michael Booth, September 2009
+
+#include "cbase.h"
+
+#include "tf_bot_generator.h"
+
+#include "bot/tf_bot.h"
+#include "bot/tf_bot_manager.h"
+#include "tf_gamerules.h"
+#include "tier3/tier3.h"
+#include "vgui/ILocalize.h"
+
+extern ConVar tf_bot_prefix_name_with_difficulty;
+extern ConVar tf_bot_difficulty;
+
+extern void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize );
+
+//------------------------------------------------------------------------------
+
+BEGIN_DATADESC( CTFBotGenerator )
+ DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ),
+ DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ),
+ DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ),
+ DEFINE_KEYFIELD( m_className, FIELD_STRING, "class" ),
+ DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ),
+ DEFINE_KEYFIELD( m_actionPointName, FIELD_STRING, "action_point" ),
+ DEFINE_KEYFIELD( m_initialCommand, FIELD_STRING, "initial_command" ),
+ DEFINE_KEYFIELD( m_bSuppressFire, FIELD_BOOLEAN, "suppressFire" ),
+ DEFINE_KEYFIELD( m_bDisableDodge, FIELD_BOOLEAN, "disableDodge" ),
+ DEFINE_KEYFIELD( m_iOnDeathAction, FIELD_INTEGER, "actionOnDeath" ),
+ DEFINE_KEYFIELD( m_bUseTeamSpawnpoint, FIELD_BOOLEAN, "useTeamSpawnPoint" ),
+ DEFINE_KEYFIELD( m_difficulty, FIELD_INTEGER, "difficulty" ),
+ DEFINE_KEYFIELD( m_bRetainBuildings, FIELD_BOOLEAN, "retainBuildings" ),
+ DEFINE_KEYFIELD( m_bSpawnOnlyWhenTriggered, FIELD_BOOLEAN, "spawnOnlyWhenTriggered" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetSuppressFire", InputSetSuppressFire ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetDisableDodge", InputSetDisableDodge ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetDifficulty", InputSetDifficulty ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "CommandGotoActionPoint", InputCommandGotoActionPoint ),
+
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetAttentionFocus", InputSetAttentionFocus ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "ClearAttentionFocus", InputClearAttentionFocus ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "SpawnBot", InputSpawnBot ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "RemoveBots", InputRemoveBots ),
+
+ DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ),
+ DEFINE_OUTPUT( m_onExpended, "OnExpended" ),
+ DEFINE_OUTPUT( m_onBotKilled, "OnBotKilled" ),
+
+ DEFINE_THINKFUNC( GeneratorThink ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_generator, CTFBotGenerator );
+
+enum
+{
+ kOnDeath_Respawn,
+ kOnDeath_RemoveSelf,
+ kOnDeath_MoveToSpectatorTeam,
+};
+
+//------------------------------------------------------------------------------
+CTFBotGenerator::CTFBotGenerator( void )
+ : m_bBotChoosesClass(false)
+ , m_bSuppressFire(false)
+ , m_bDisableDodge(false)
+ , m_bUseTeamSpawnpoint(false)
+ , m_bRetainBuildings(false)
+ , m_bExpended(false)
+ , m_iOnDeathAction(kOnDeath_RemoveSelf)
+ , m_difficulty(CTFBot::UNDEFINED)
+ , m_spawnCountRemaining(0)
+ , m_bSpawnOnlyWhenTriggered(false)
+ , m_bEnabled(true)
+{
+ SetThink( NULL );
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputEnable( inputdata_t &inputdata )
+{
+ m_bEnabled = true;
+
+ if ( m_bExpended )
+ {
+ return;
+ }
+
+ SetThink( &CTFBotGenerator::GeneratorThink );
+
+ if ( m_spawnCountRemaining )
+ {
+ // already generating - don't restart count
+ return;
+ }
+ SetNextThink( gpGlobals->curtime );
+ m_spawnCountRemaining = m_spawnCount;
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputDisable( inputdata_t &inputdata )
+{
+ m_bEnabled = false;
+
+ // just stop thinking
+ SetThink( NULL );
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputSetSuppressFire( inputdata_t &inputdata )
+{
+ m_bSuppressFire = inputdata.value.Bool();
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputSetDisableDodge( inputdata_t &inputdata )
+{
+ m_bDisableDodge = inputdata.value.Bool();
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputSetDifficulty( inputdata_t &inputdata )
+{
+ m_difficulty = clamp( inputdata.value.Int(), (int) CTFBot::UNDEFINED, (int) CTFBot::EXPERT );
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputCommandGotoActionPoint( inputdata_t &inputdata )
+{
+ CTFBotActionPoint *pActionPoint = dynamic_cast<CTFBotActionPoint *>( gEntList.FindEntityByName( NULL, inputdata.value.String() ) );
+ if ( pActionPoint == NULL )
+ {
+ return;
+ }
+ for ( int i = 0; i < m_spawnedBotVector.Count(); )
+ {
+ CHandle< CTFBot > hBot = m_spawnedBotVector[i];
+ if ( hBot == NULL )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+ if ( hBot->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+ hBot->SetActionPoint( pActionPoint );
+ hBot->OnCommandString( "goto action point" );
+ ++i;
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputSetAttentionFocus( inputdata_t &inputdata )
+{
+ CBaseEntity *focus = gEntList.FindEntityByName( NULL, inputdata.value.String() );
+
+ if ( focus == NULL )
+ {
+ return;
+ }
+
+ for( int i = 0; i < m_spawnedBotVector.Count(); )
+ {
+ CTFBot *bot = m_spawnedBotVector[i];
+
+ if ( !bot || bot->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+
+ bot->SetAttentionFocus( focus );
+
+ ++i;
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputClearAttentionFocus( inputdata_t &inputdata )
+{
+ for( int i = 0; i < m_spawnedBotVector.Count(); )
+ {
+ CTFBot *bot = m_spawnedBotVector[i];
+
+ if ( !bot || bot->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+
+ bot->ClearAttentionFocus();
+
+ ++i;
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputSpawnBot( inputdata_t &inputdata )
+{
+ if ( m_bEnabled )
+ {
+ SpawnBot();
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::InputRemoveBots( inputdata_t &inputdata )
+{
+ for( int i = 0; i < m_spawnedBotVector.Count(); i++ )
+ {
+ CTFBot *pBot = m_spawnedBotVector[i];
+ if ( pBot )
+ {
+ pBot->Remove();
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pBot->GetUserID() ) );
+ }
+
+ m_spawnedBotVector.FastRemove(i);
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::OnBotKilled( CTFBot *pBot )
+{
+ m_onBotKilled.FireOutput( pBot, this );
+}
+
+//------------------------------------------------------------------------------
+
+void CTFBotGenerator::Activate()
+{
+ BaseClass::Activate();
+ m_bBotChoosesClass = FStrEq( m_className.ToCStr(), "auto" );
+ m_moveGoal = gEntList.FindEntityByName( NULL, m_actionPointName.ToCStr() );
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::GeneratorThink( void )
+{
+ // still waiting for the real game to start?
+ gamerules_roundstate_t roundState = TFGameRules()->State_Get();
+ if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() )
+ {
+ SetNextThink( gpGlobals->curtime + 1.0f );
+ return;
+ }
+
+ // create the bot finally...
+ if ( !m_bSpawnOnlyWhenTriggered )
+ {
+ SpawnBot();
+ }
+}
+
+//------------------------------------------------------------------------------
+void CTFBotGenerator::SpawnBot( void )
+{
+ // did we exceed the max active count?
+ for ( int i = 0; i < m_spawnedBotVector.Count(); )
+ {
+ CHandle< CTFBot > hBot = m_spawnedBotVector[i];
+ if ( hBot == NULL )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+ if ( hBot->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+ ++i;
+ }
+
+ if ( m_spawnedBotVector.Count() >= m_maxActiveCount )
+ {
+ SetNextThink( gpGlobals->curtime + 0.1f );
+ return;
+ }
+
+ char name[256];
+ CTFBot *bot = TheTFBots().GetAvailableBotFromPool();
+ if ( bot == NULL )
+ {
+ CreateBotName( TEAM_UNASSIGNED, TF_CLASS_UNDEFINED, (CTFBot::DifficultyType)m_difficulty, name, sizeof(name) );
+ bot = NextBotCreatePlayerBot< CTFBot >( name );
+ }
+
+ if ( bot )
+ {
+ m_spawnedBotVector.AddToTail( bot );
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ bot->SetAttribute( CTFBot::IS_NPC );
+ }
+#endif // TF_RAID_MODE
+
+ bot->SetSpawner( this );
+
+ if ( m_bUseTeamSpawnpoint == false )
+ {
+ bot->SetSpawnPoint( this );
+ }
+
+ if ( m_bSuppressFire )
+ {
+ bot->SetAttribute( CTFBot::SUPPRESS_FIRE );
+ }
+
+ if ( m_bRetainBuildings )
+ {
+ bot->SetAttribute( CTFBot::RETAIN_BUILDINGS );
+ }
+
+ if ( m_bDisableDodge )
+ {
+ bot->SetAttribute( CTFBot::DISABLE_DODGE );
+ }
+
+ if ( m_difficulty != CTFBot::UNDEFINED )
+ {
+ bot->SetDifficulty( (CTFBot::DifficultyType )m_difficulty );
+ }
+
+ // propagate the generator's spawn flags into all bots generated
+ bot->ClearBehaviorFlag( TFBOT_ALL_BEHAVIOR_FLAGS );
+ bot->SetBehaviorFlag( m_spawnflags );
+
+ switch ( m_iOnDeathAction )
+ {
+ case kOnDeath_RemoveSelf:
+ bot->SetAttribute( CTFBot::REMOVE_ON_DEATH );
+ break;
+ case kOnDeath_MoveToSpectatorTeam:
+ bot->SetAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH );
+ break;
+ } // switch
+
+ bot->SetActionPoint( dynamic_cast<CTFBotActionPoint *>( m_moveGoal.Get() ) );
+
+ // pick a team and force the team change
+ // HandleCommand_JoinTeam() may fail, but this should always succeed
+ int iTeam = TEAM_UNASSIGNED;
+ if ( FStrEq( m_teamName.ToCStr(), "auto" ) )
+ {
+ iTeam = bot->GetAutoTeam();
+ }
+ else if ( FStrEq( m_teamName.ToCStr(), "spectate" ) )
+ {
+ iTeam = TEAM_SPECTATOR;
+ }
+ else
+ {
+ for ( int i = 0; i < TF_TEAM_COUNT; ++i )
+ {
+ COMPILE_TIME_ASSERT( TF_TEAM_COUNT == ARRAYSIZE( g_aTeamNames ) );
+ if ( FStrEq( m_teamName.ToCStr(), g_aTeamNames[i] ) )
+ {
+ iTeam = i;
+ break;
+ }
+ }
+ }
+ if ( iTeam == TEAM_UNASSIGNED )
+ {
+ iTeam = bot->GetAutoTeam();
+ }
+ bot->ChangeTeam( iTeam, false, false );
+
+ const char* pClassName = m_bBotChoosesClass ? bot->GetNextSpawnClassname() : m_className.ToCStr();
+ bot->HandleCommand_JoinClass( pClassName );
+
+ // in training, reset the after the bot joins the class
+ if ( TFGameRules()->IsInTraining() )
+ {
+ CTFBot::DifficultyType skill = bot->GetDifficulty();
+ CreateBotName( iTeam, bot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof(name) );
+ engine->SetFakeClientConVarValue( bot->edict(), "name", name );
+ }
+
+ if ( bot->IsAlive() == false )
+ {
+ bot->ForceRespawn();
+ }
+
+ // make sure the bot is facing the right way.
+ // @todo Tom Bui: for some reason it is still turning towards another direction...need to investigate
+ bot->SnapEyeAngles( GetAbsAngles() );
+
+ if ( FStrEq( m_initialCommand.ToCStr(), "" ) == false )
+ {
+ // @note Tom Bui: we call Update() once here to make sure the bot is ready to receive commands
+ bot->Update();
+ bot->OnCommandString( m_initialCommand.ToCStr() );
+ }
+ m_onSpawned.FireOutput( bot, this );
+
+ --m_spawnCountRemaining;
+ if ( m_spawnCountRemaining )
+ {
+ SetNextThink( gpGlobals->curtime + m_spawnInterval );
+ }
+ else
+ {
+ SetThink( NULL );
+ m_onExpended.FireOutput( this, this );
+ m_bExpended = true;
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+
+BEGIN_DATADESC( CTFBotActionPoint )
+ DEFINE_KEYFIELD( m_stayTime, FIELD_FLOAT, "stay_time" ),
+ DEFINE_KEYFIELD( m_desiredDistance, FIELD_FLOAT, "desired_distance" ),
+ DEFINE_KEYFIELD( m_nextActionPointName, FIELD_STRING, "next_action_point" ),
+ DEFINE_KEYFIELD( m_command, FIELD_STRING, "command" ),
+ DEFINE_OUTPUT( m_onReachedActionPoint, "OnBotReached" ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_action_point, CTFBotActionPoint );
+
+//------------------------------------------------------------------------------
+
+CTFBotActionPoint::CTFBotActionPoint()
+: m_stayTime( 0.0f )
+, m_desiredDistance( 1.0f )
+
+{
+
+}
+
+//------------------------------------------------------------------------------
+
+void CTFBotActionPoint::Activate()
+{
+ BaseClass::Activate();
+ m_moveGoal = gEntList.FindEntityByName( NULL, m_nextActionPointName.ToCStr() );
+}
+
+//------------------------------------------------------------------------------
+
+bool CTFBotActionPoint::IsWithinRange( CBaseEntity *entity )
+{
+ return ( entity->GetAbsOrigin() - GetAbsOrigin() ).IsLengthLessThan( m_desiredDistance );
+}
+
+//------------------------------------------------------------------------------
+
+void CTFBotActionPoint::ReachedActionPoint( CTFBot* pBot )
+{
+ if ( FStrEq( m_command.ToCStr(), "" ) == false )
+ {
+ pBot->OnCommandString( m_command.ToCStr() );
+ }
+ m_onReachedActionPoint.FireOutput( pBot, this );
+}
+
+//------------------------------------------------------------------------------
diff --git a/game/server/tf/bot/map_entities/tf_bot_generator.h b/game/server/tf/bot/map_entities/tf_bot_generator.h
new file mode 100644
index 0000000..dfbc52a
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_generator.h
@@ -0,0 +1,100 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_generator.h
+// Entity to spawn a collection of TFBots
+// Michael Booth, September 2009
+
+#ifndef TF_BOT_GENERATOR_H
+#define TF_BOT_GENERATOR_H
+
+#include "bot/tf_bot.h"
+
+
+class CTFBotGenerator : public CPointEntity
+{
+public:
+ DECLARE_CLASS( CTFBotGenerator, CPointEntity );
+ DECLARE_DATADESC();
+
+ CTFBotGenerator( void );
+ virtual ~CTFBotGenerator() { }
+
+ virtual void Activate();
+
+ void GeneratorThink( void );
+ void SpawnBot( void );
+
+ // Input.
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+ void InputSetSuppressFire( inputdata_t &inputdata );
+ void InputSetDisableDodge( inputdata_t &inputdata );
+ void InputSetDifficulty( inputdata_t &inputdata );
+ void InputCommandGotoActionPoint( inputdata_t &inputdata );
+ void InputSetAttentionFocus( inputdata_t &inputdata );
+ void InputClearAttentionFocus( inputdata_t &inputdata );
+ void InputSpawnBot( inputdata_t &inputdata );
+ void InputRemoveBots( inputdata_t &inputdata );
+
+ // Output
+ void OnBotKilled( CTFBot *pBot );
+
+private:
+ bool m_bBotChoosesClass;
+ bool m_bSuppressFire;
+ bool m_bDisableDodge;
+ bool m_bUseTeamSpawnpoint;
+ bool m_bRetainBuildings;
+ bool m_bExpended;
+ int m_iOnDeathAction;
+ int m_spawnCount;
+ int m_spawnCountRemaining;
+ int m_maxActiveCount;
+ float m_spawnInterval;
+ string_t m_className;
+ string_t m_teamName;
+ string_t m_actionPointName;
+ string_t m_initialCommand;
+ CHandle< CBaseEntity > m_moveGoal;
+ int m_difficulty;
+ bool m_bSpawnOnlyWhenTriggered;
+ bool m_bEnabled;
+
+ COutputEvent m_onSpawned;
+ COutputEvent m_onExpended;
+ COutputEvent m_onBotKilled;
+
+ CUtlVector< CHandle< CTFBot > > m_spawnedBotVector;
+};
+
+//---------------------------------------------------------------
+//
+// Bot generator may have one of these as an argument, which
+// means "tell the bot I created to move here and do what this node says".
+// Things like "stay here", "move to <next task point>", "face towards <X>", "shoot at <Y>", etc
+//
+class CTFBotActionPoint : public CPointEntity
+{
+ DECLARE_CLASS( CTFBotActionPoint, CPointEntity );
+public:
+ DECLARE_DATADESC();
+
+ CTFBotActionPoint( void );
+ virtual ~CTFBotActionPoint() { }
+
+ virtual void Activate();
+
+ bool IsWithinRange( CBaseEntity *entity );
+ void ReachedActionPoint( CTFBot* pBot );
+
+ CHandle< CBaseEntity > m_moveGoal;
+
+ // reflected
+ float m_stayTime;
+ float m_desiredDistance;
+ string_t m_nextActionPointName;
+ string_t m_command;
+
+ COutputEvent m_onReachedActionPoint;
+};
+
+#endif // TF_BOT_GENERATOR_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint.cpp b/game/server/tf/bot/map_entities/tf_bot_hint.cpp
new file mode 100644
index 0000000..b4dfa11
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint.cpp
@@ -0,0 +1,128 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint.cpp
+// Designer-placed hint for TFBots
+
+#include "cbase.h"
+#include "bot/tf_bot.h"
+#include "tf_bot_hint.h"
+
+BEGIN_DATADESC( CTFBotHint )
+ DEFINE_KEYFIELD( m_team, FIELD_INTEGER, "team" ),
+ DEFINE_KEYFIELD( m_hint, FIELD_INTEGER, "hint" ),
+ DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( func_tfbot_hint, CTFBotHint );
+
+//
+// NOTE: For simplicity and runtime efficiency, this will not
+// play nice with nav area hints stored in the mesh,
+// nor will overlapping hints of the same type work well.
+//
+
+//------------------------------------------------------------------------------
+CTFBotHint::CTFBotHint( void )
+{
+ m_isDisabled = false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+// Return true if this hint applies to the given entity
+bool CTFBotHint::IsFor( CTFBot *who ) const
+{
+ if ( m_isDisabled )
+ {
+ return false;
+ }
+
+ if ( m_team > 0 && who->GetTeamNumber() != m_team )
+ {
+ return false;
+ }
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CTFBotHint::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetSolid( SOLID_BSP );
+ AddSolidFlags( FSOLID_NOT_SOLID );
+
+ SetMoveType( MOVETYPE_NONE );
+ SetModel( STRING( GetModelName() ) );
+ AddEffects( EF_NODRAW );
+ SetCollisionGroup( COLLISION_GROUP_NONE );
+
+ VPhysicsInitShadow( false, false );
+
+ UpdateNavDecoration();
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CTFBotHint::UpdateOnRemove( void )
+{
+ BaseClass::UpdateOnRemove();
+
+ UpdateNavDecoration();
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CTFBotHint::InputEnable( inputdata_t &inputdata )
+{
+ m_isDisabled = false;
+ UpdateNavDecoration();
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CTFBotHint::InputDisable( inputdata_t &inputdata )
+{
+ m_isDisabled = true;
+ UpdateNavDecoration();
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CTFBotHint::UpdateNavDecoration( void )
+{
+ Extent extent;
+ extent.Init( this );
+
+ CUtlVector< CTFNavArea * > overlapVector;
+ TheNavMesh->CollectAreasOverlappingExtent( extent, &overlapVector );
+
+ int attributeBits = 0;
+ switch( m_hint )
+ {
+ case HINT_SNIPER_SPOT:
+ attributeBits = TF_NAV_SNIPER_SPOT;
+ break;
+
+ case HINT_SENTRY_SPOT:
+ attributeBits = TF_NAV_SENTRY_SPOT;
+ break;
+ }
+
+ for( int j=0; j<overlapVector.Count(); ++j )
+ {
+ if ( m_isDisabled )
+ {
+ overlapVector[j]->ClearAttributeTF( attributeBits );
+ }
+ else
+ {
+ overlapVector[j]->SetAttributeTF( attributeBits );
+ }
+ }
+}
+
+
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint.h b/game/server/tf/bot/map_entities/tf_bot_hint.h
new file mode 100644
index 0000000..4945145
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint.h
@@ -0,0 +1,54 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint.h
+// Designer-placed hint for TFBots
+
+#ifndef TF_BOT_HINT_H
+#define TF_BOT_HINT_H
+
+class CTFBot;
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * An entity that specifies TFBot behavior hints.
+ */
+class CTFBotHint : public CBaseEntity
+{
+public:
+ DECLARE_DATADESC();
+ DECLARE_CLASS( CTFBotHint, CBaseEntity );
+
+ CTFBotHint( void );
+ virtual ~CTFBotHint() { }
+
+ enum HintType
+ {
+ HINT_SNIPER_SPOT = 0,
+ HINT_SENTRY_SPOT = 1,
+ };
+
+ bool IsA( HintType type ) const;
+
+ bool IsFor( CTFBot *who ) const; // return true if this hint applies to the given entity
+
+ virtual void Spawn( void );
+ virtual void UpdateOnRemove( void );
+
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+ bool IsEnabled( void ) const { return !m_isDisabled; }
+
+protected:
+ int m_team;
+ int m_hint;
+ bool m_isDisabled;
+
+ void UpdateNavDecoration( void );
+};
+
+inline bool CTFBotHint::IsA( HintType type ) const
+{
+ return ( m_hint == type );
+}
+
+
+#endif // TF_BOT_HINT_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp
new file mode 100644
index 0000000..7f21a71
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp
@@ -0,0 +1,160 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+#include "cbase.h"
+#include "tf_bot_hint_engineer_nest.h"
+#include "tf_obj.h"
+#include "tf_obj_teleporter.h"
+
+IMPLEMENT_SERVERCLASS_ST( CTFBotHintEngineerNest, DT_TFBotHintEngineerNest )
+ SendPropBool( SENDINFO(m_bHasActiveTeleporter) ),
+END_SEND_TABLE()
+
+BEGIN_DATADESC( CTFBotHintEngineerNest )
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_hint_engineer_nest, CTFBotHintEngineerNest );
+
+//------------------------------------------------------------------------------
+CTFBotHintEngineerNest::CTFBotHintEngineerNest( void )
+{
+ m_bHasActiveTeleporter = false;
+}
+
+
+void CTFBotHintEngineerNest::Spawn()
+{
+ BaseClass::Spawn();
+
+ SetThink( &CTFBotHintEngineerNest::HintThink );
+ SetNextThink( gpGlobals->curtime + 0.1f );
+}
+
+
+void CTFBotHintEngineerNest::HintThink()
+{
+ // find sentry and teleporter hint
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] );
+ if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_SENTRYGUN ) && pHint->GetEntityName() == GetEntityName() )
+ {
+ m_sentries.AddToTail( pHint );
+ }
+ else if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_TELEPORTER_EXIT ) && pHint->GetEntityName() == GetEntityName() )
+ {
+ m_teleporters.AddToTail( pHint );
+ }
+ }
+
+ if ( m_sentries.Count() == 0 && m_teleporters.Count() == 0 )
+ {
+ AssertMsg( 0, "Must have a teleporter and/or a sentry hint with the same name." );
+ Warning( "Must have a teleporter and/or a sentry hint with the same name.\n" );
+ }
+
+ SetThink( &CTFBotHintEngineerNest::HintTeleporterThink );
+ SetNextThink( gpGlobals->curtime + 0.1f );
+}
+
+
+void CTFBotHintEngineerNest::HintTeleporterThink()
+{
+ bool bFoundActiveTeleporter = false;
+ for ( int i=0; i<m_teleporters.Count(); ++i )
+ {
+ CBaseEntity* pOwner = m_teleporters[i]->GetOwnerEntity();
+ if ( pOwner && pOwner->IsBaseObject() )
+ {
+ CObjectTeleporter *pTeleporter = assert_cast< CObjectTeleporter* >( pOwner );
+ if ( pTeleporter )
+ {
+ bFoundActiveTeleporter |= !pTeleporter->IsBuilding();
+ }
+ }
+ }
+
+ // update particle bool
+ m_bHasActiveTeleporter = bFoundActiveTeleporter;
+
+ SetNextThink( gpGlobals->curtime + 0.1f );
+}
+
+
+bool CTFBotHintEngineerNest::IsStaleNest() const
+{
+ for ( int i=0; i<m_sentries.Count(); ++i )
+ {
+ if ( m_sentries[i]->OwnerObjectHasNoOwner() )
+ {
+ return true;
+ }
+ }
+
+ for ( int i=0; i<m_teleporters.Count(); ++i )
+ {
+ if ( m_teleporters[i]->OwnerObjectHasNoOwner() )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+void CTFBotHintEngineerNest::DetonateStaleNest()
+{
+ DetonateObjectsFromHints( m_sentries );
+ DetonateObjectsFromHints( m_teleporters );
+}
+
+
+void CTFBotHintEngineerNest::DetonateObjectsFromHints( const HintVector_t& hints )
+{
+ for ( int i=0; i<hints.Count(); ++i )
+ {
+ if ( hints[i]->OwnerObjectHasNoOwner() )
+ {
+ CBaseObject* pObj = assert_cast< CBaseObject* >( hints[i]->GetOwnerEntity() );
+ if ( pObj )
+ {
+ pObj->DetonateObject();
+ }
+ }
+ }
+}
+
+
+CBaseTFBotHintEntity* CTFBotHintEngineerNest::GetHint( const HintVector_t& hints ) const
+{
+ if ( hints.Count() == 0 )
+ {
+ return NULL;
+ }
+
+ for ( int i=0; i<hints.Count(); ++i )
+ {
+ if ( hints[i]->OwnerObjectHasNoOwner() )
+ {
+ return hints[i];
+ }
+ }
+
+ int which = RandomInt( 0, hints.Count() - 1 );
+ return hints[ which ];
+}
+
+
+CTFBotHintSentrygun* CTFBotHintEngineerNest::GetSentryHint() const
+{
+ return (CTFBotHintSentrygun*)GetHint( m_sentries );
+}
+
+
+CTFBotHintTeleporterExit* CTFBotHintEngineerNest::GetTeleporterHint() const
+{
+ return (CTFBotHintTeleporterExit*)GetHint( m_teleporters );
+}
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h
new file mode 100644
index 0000000..9e5fdf5
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h
@@ -0,0 +1,53 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+#ifndef TF_BOT_HINT_ENGINEER_NEST_H
+#define TF_BOT_HINT_ENGINEER_NEST_H
+
+#include "tf_bot_hint_entity.h"
+
+typedef CUtlVector< CHandle< CBaseTFBotHintEntity > > HintVector_t;
+
+class CTFBotHintSentrygun;
+class CTFBotHintTeleporterExit;
+
+class CTFBotHintEngineerNest : public CBaseTFBotHintEntity
+{
+ DECLARE_CLASS( CTFBotHintEngineerNest, CBaseTFBotHintEntity );
+public:
+ DECLARE_SERVERCLASS();
+ DECLARE_DATADESC();
+
+ CTFBotHintEngineerNest( void );
+ virtual ~CTFBotHintEngineerNest() { }
+
+ virtual void Spawn() OVERRIDE;
+
+ virtual HintType GetHintType() const OVERRIDE { return HINT_ENGINEER_NEST; }
+
+ virtual int UpdateTransmitState()
+ {
+ return SetTransmitState( FL_EDICT_ALWAYS );
+ }
+
+ void HintThink();
+ void HintTeleporterThink();
+
+ bool IsStaleNest() const;
+ void DetonateStaleNest();
+
+ CTFBotHintSentrygun* GetSentryHint() const;
+ CTFBotHintTeleporterExit* GetTeleporterHint() const;
+private:
+ void DetonateObjectsFromHints( const HintVector_t& hints );
+ CBaseTFBotHintEntity* GetHint( const HintVector_t& hints ) const;
+
+ HintVector_t m_sentries;
+ HintVector_t m_teleporters;
+
+ CNetworkVar( bool, m_bHasActiveTeleporter );
+};
+
+#endif // TF_BOT_HINT_ENGINEER_NEST_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp
new file mode 100644
index 0000000..ffd2c74
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp
@@ -0,0 +1,60 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+#include "cbase.h"
+#include "tf_bot_hint_entity.h"
+#include "tf_obj.h"
+#include "tf_player.h"
+
+
+BEGIN_DATADESC( CBaseTFBotHintEntity )
+ DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+END_DATADESC()
+
+IMPLEMENT_AUTO_LIST( ITFBotHintEntityAutoList );
+
+//------------------------------------------------------------------------------
+CBaseTFBotHintEntity::CBaseTFBotHintEntity( void )
+ : m_isDisabled( false ),
+ m_hintType( HINT_INVALID )
+{
+}
+
+
+bool CBaseTFBotHintEntity::OwnerObjectHasNoOwner() const
+{
+ CBaseEntity* pOwner = GetOwnerEntity();
+ if ( pOwner && pOwner->IsBaseObject() )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( pOwner );
+ if ( pObj->GetBuilder() == NULL )
+ {
+ return true;
+ }
+ else
+ {
+ if ( !pObj->GetBuilder()->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ AssertMsg( 0, "Object has an owner that's not engineer." );
+ Warning( "Object has an owner that's not engineer." );
+ }
+ }
+ }
+ return false;
+}
+
+
+bool CBaseTFBotHintEntity::OwnerObjectFinishBuilding() const
+{
+ CBaseEntity* pOwner = GetOwnerEntity();
+ if ( pOwner && pOwner->IsBaseObject() )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( pOwner );
+ return !pObj->IsBuilding();
+ }
+ return false;
+}
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_entity.h b/game/server/tf/bot/map_entities/tf_bot_hint_entity.h
new file mode 100644
index 0000000..af0df70
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_entity.h
@@ -0,0 +1,59 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//
+//
+//=============================================================================
+#ifndef TF_BOT_HINT_ENTITY_H
+#define TF_BOT_HINT_ENTITY_H
+
+DECLARE_AUTO_LIST( ITFBotHintEntityAutoList );
+
+class CBaseTFBotHintEntity : public CPointEntity, public ITFBotHintEntityAutoList
+{
+ DECLARE_CLASS( CBaseTFBotHintEntity, CPointEntity );
+public:
+ DECLARE_DATADESC();
+
+ CBaseTFBotHintEntity( void );
+ virtual ~CBaseTFBotHintEntity() { }
+
+ enum HintType
+ {
+ HINT_INVALID = -1,
+ HINT_TELEPORTER_EXIT,
+ HINT_SENTRYGUN,
+ HINT_ENGINEER_NEST,
+ };
+ virtual HintType GetHintType() const = 0;
+ bool IsHintType( HintType hintType ) { return GetHintType() == hintType; }
+
+ bool OwnerObjectHasNoOwner() const;
+ bool OwnerObjectFinishBuilding() const;
+
+ bool IsEnabled() const;
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+
+private:
+
+ bool m_isDisabled;
+ HintType m_hintType;
+};
+
+
+inline void CBaseTFBotHintEntity::InputEnable( inputdata_t &inputdata )
+{
+ m_isDisabled = false;
+}
+
+inline void CBaseTFBotHintEntity::InputDisable( inputdata_t &inputdata )
+{
+ m_isDisabled = true;
+}
+
+inline bool CBaseTFBotHintEntity::IsEnabled() const
+{
+ return !m_isDisabled;
+}
+
+#endif // TF_BOT_HINT_ENTITY_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp
new file mode 100644
index 0000000..4cc804d
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp
@@ -0,0 +1,42 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint_sentrygun.cpp
+// Designer-placed hint for bot sentry placement
+// Michael Booth, October 2009
+
+#include "cbase.h"
+#include "bot/tf_bot.h"
+#include "tf_bot_hint_sentrygun.h"
+
+
+BEGIN_DATADESC( CTFBotHintSentrygun )
+ DEFINE_KEYFIELD( m_isSticky, FIELD_BOOLEAN, "sticky" ),
+ DEFINE_OUTPUT( m_outputOnSentryGunDestroyed, "OnSentryGunDestroyed" ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_hint_sentrygun, CTFBotHintSentrygun );
+
+//------------------------------------------------------------------------------
+CTFBotHintSentrygun::CTFBotHintSentrygun( void )
+ : m_isSticky( false )
+ , m_iUseCount( 0 )
+{
+}
+
+//------------------------------------------------------------------------------
+void CTFBotHintSentrygun::OnSentryGunDestroyed( CBaseEntity *pEntity )
+{
+ m_outputOnSentryGunDestroyed.FireOutput( pEntity, pEntity );
+}
+
+//------------------------------------------------------------------------------
+bool CTFBotHintSentrygun::IsAvailableForSelection( CTFPlayer *pRequestingPlayer ) const
+{
+ // sentry hint is eligible as long as there is no owner (or the owner is no longer an engineer)
+ // if the hint is enabled and the hint is not in use and it is on the same team as me
+ if ( ( GetPlayerOwner() == NULL || !GetPlayerOwner()->IsPlayerClass( TF_CLASS_ENGINEER ) ) &&
+ ( IsEnabled() && IsInUse() == false && InSameTeam( pRequestingPlayer ) ) )
+ {
+ return true;
+ }
+ return false;
+}
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h
new file mode 100644
index 0000000..e2b0440
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h
@@ -0,0 +1,75 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint_sentrygun.h
+// Designer-placed hint for bot sentry placement
+// Michael Booth, October 2009
+
+#ifndef TF_BOT_HINT_SENTRYGUN_H
+#define TF_BOT_HINT_SENTRYGUN_H
+
+#include "tf_bot_hint_entity.h"
+
+class CTFPlayer;
+
+class CTFBotHintSentrygun : public CBaseTFBotHintEntity
+{
+public:
+ DECLARE_CLASS( CTFBotHintSentrygun, CBaseTFBotHintEntity );
+ DECLARE_DATADESC();
+
+ CTFBotHintSentrygun( void );
+ virtual ~CTFBotHintSentrygun() { }
+
+ bool IsSticky() const;
+ bool IsInUse() const;
+
+ CTFPlayer *GetPlayerOwner() const;
+ void SetPlayerOwner( CTFPlayer *pPlayerOwner );
+
+ void IncrementUseCount();
+ void DecrementUseCount();
+
+ void OnSentryGunDestroyed( CBaseEntity *pBaseEntity );
+
+ bool IsAvailableForSelection( CTFPlayer *pRequestingPlayer ) const;
+
+ virtual HintType GetHintType() const OVERRIDE { return HINT_SENTRYGUN; }
+
+private:
+ bool m_isSticky;
+ int m_iUseCount;
+ COutputEvent m_outputOnSentryGunDestroyed;
+
+ CHandle< CTFPlayer > m_playerOwner;
+};
+
+inline bool CTFBotHintSentrygun::IsSticky() const
+{
+ return m_isSticky;
+}
+
+inline bool CTFBotHintSentrygun::IsInUse() const
+{
+ return m_iUseCount != 0;
+}
+
+inline CTFPlayer *CTFBotHintSentrygun::GetPlayerOwner() const
+{
+ return m_playerOwner;
+}
+
+inline void CTFBotHintSentrygun::SetPlayerOwner( CTFPlayer *pPlayerOwner )
+{
+ m_playerOwner = pPlayerOwner;
+}
+
+inline void CTFBotHintSentrygun::IncrementUseCount()
+{
+ ++m_iUseCount;
+}
+
+inline void CTFBotHintSentrygun::DecrementUseCount()
+{
+ --m_iUseCount;
+}
+
+#endif // TF_BOT_HINT_SENTRYGUN_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp
new file mode 100644
index 0000000..32beca2
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp
@@ -0,0 +1,19 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint_teleporter_exit.cpp
+// Designer-placed hint for bot teleporter exit placement
+// Michael Booth, May 2010
+
+#include "cbase.h"
+#include "tf_bot_hint_teleporter_exit.h"
+
+
+BEGIN_DATADESC( CTFBotHintTeleporterExit )
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_hint_teleporter_exit, CTFBotHintTeleporterExit );
+
+//------------------------------------------------------------------------------
+CTFBotHintTeleporterExit::CTFBotHintTeleporterExit( void )
+{
+}
+
diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h
new file mode 100644
index 0000000..a1af04a
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h
@@ -0,0 +1,23 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_hint_teleporter_exit.h
+// Designer-placed hint for bot teleporter exit placement
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_HINT_TELEPORTER_EXIT_H
+#define TF_BOT_HINT_TELEPORTER_EXIT_H
+
+#include "tf_bot_hint_entity.h"
+
+class CTFBotHintTeleporterExit : public CBaseTFBotHintEntity
+{
+ DECLARE_CLASS( CTFBotHintTeleporterExit, CBaseTFBotHintEntity );
+public:
+ DECLARE_DATADESC();
+
+ CTFBotHintTeleporterExit( void );
+ virtual ~CTFBotHintTeleporterExit() { }
+
+ virtual HintType GetHintType() const OVERRIDE { return HINT_TELEPORTER_EXIT; }
+};
+
+#endif // TF_BOT_HINT_TELEPORTER_EXIT_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_proxy.cpp b/game/server/tf/bot/map_entities/tf_bot_proxy.cpp
new file mode 100644
index 0000000..560b341
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_proxy.cpp
@@ -0,0 +1,167 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_proxy.cpp
+// A Hammer entity that spawns a TFBot and relays events to/from it
+// Michael Booth, November 2009
+
+#include "cbase.h"
+
+#include "bot/tf_bot.h"
+#include "tf_bot_proxy.h"
+#include "tf_bot_generator.h"
+
+
+BEGIN_DATADESC( CTFBotProxy )
+ DEFINE_KEYFIELD( m_botName, FIELD_STRING, "bot_name" ),
+ DEFINE_KEYFIELD( m_className, FIELD_STRING, "class" ),
+ DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ),
+ DEFINE_KEYFIELD( m_respawnInterval, FIELD_FLOAT, "respawn_interval" ),
+ DEFINE_KEYFIELD( m_actionPointName, FIELD_STRING, "action_point" ),
+ DEFINE_KEYFIELD( m_spawnOnStart, FIELD_STRING, "spawn_on_start" ),
+
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetTeam", InputSetTeam ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetClass", InputSetClass ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetMovementGoal", InputSetMovementGoal ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Spawn", InputSpawn ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Delete", InputDelete ),
+
+ DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ),
+ DEFINE_OUTPUT( m_onInjured, "OnInjured" ),
+ DEFINE_OUTPUT( m_onKilled, "OnKilled" ),
+ DEFINE_OUTPUT( m_onAttackingEnemy, "OnAttackingEnemy" ),
+ DEFINE_OUTPUT( m_onKilledEnemy, "OnKilledEnemy" ),
+
+ DEFINE_THINKFUNC( Think ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_proxy, CTFBotProxy );
+
+
+
+//------------------------------------------------------------------------------
+CTFBotProxy::CTFBotProxy( void )
+{
+ V_strcpy_safe( m_botName, "TFBot" );
+ V_strcpy_safe( m_teamName, "auto" );
+ V_strcpy_safe( m_className, "auto" );
+ m_bot = NULL;
+ m_moveGoal = NULL;
+ SetThink( NULL );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::Think( void )
+{
+
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::InputSetTeam( inputdata_t &inputdata )
+{
+ const char *teamName = inputdata.value.String();
+ if ( teamName && teamName[0] )
+ {
+ V_strcpy_safe( m_teamName, teamName );
+
+ // if m_bot exists, tell it to change team
+ if ( m_bot != NULL )
+ {
+ m_bot->HandleCommand_JoinTeam( m_teamName );
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::InputSetClass( inputdata_t &inputdata )
+{
+ const char *className = inputdata.value.String();
+ if ( className && className[0] )
+ {
+ V_strcpy_safe( m_className, className );
+
+ // if m_bot exists, tell it to change class
+ if ( m_bot != NULL )
+ {
+ m_bot->HandleCommand_JoinClass( m_className );
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::InputSetMovementGoal( inputdata_t &inputdata )
+{
+ const char *entityName = inputdata.value.String();
+ if ( entityName && entityName[0] )
+ {
+ m_moveGoal = dynamic_cast< CTFBotActionPoint * >( gEntList.FindEntityByName( NULL, entityName ) );
+
+ // if m_bot exists, tell it to move to the new action point
+ if ( m_bot != NULL )
+ {
+ m_bot->SetActionPoint( (CTFBotActionPoint *)m_moveGoal.Get() );
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::InputSpawn( inputdata_t &inputdata )
+{
+ m_bot = NextBotCreatePlayerBot< CTFBot >( m_botName );
+ if ( m_bot != NULL )
+ {
+ m_bot->SetSpawnPoint( this );
+ m_bot->SetAttribute( CTFBot::REMOVE_ON_DEATH );
+ m_bot->SetAttribute( CTFBot::IS_NPC );
+
+ m_bot->SetActionPoint( (CTFBotActionPoint *)m_moveGoal.Get() );
+
+ m_bot->HandleCommand_JoinTeam( m_teamName );
+ m_bot->HandleCommand_JoinClass( m_className );
+
+ m_onSpawned.FireOutput( m_bot, m_bot );
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::InputDelete( inputdata_t &inputdata )
+{
+ if ( m_bot != NULL )
+ {
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", m_bot->GetUserID() ) );
+ m_bot = NULL;
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::OnInjured( void )
+{
+ m_onInjured.FireOutput( this, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::OnKilled( void )
+{
+ m_onKilled.FireOutput( this, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::OnAttackingEnemy( void )
+{
+ m_onAttackingEnemy.FireOutput( this, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFBotProxy::OnKilledEnemy( void )
+{
+ m_onKilledEnemy.FireOutput( this, this );
+}
+
diff --git a/game/server/tf/bot/map_entities/tf_bot_proxy.h b/game/server/tf/bot/map_entities/tf_bot_proxy.h
new file mode 100644
index 0000000..6cdfbf4
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_proxy.h
@@ -0,0 +1,58 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_proxy.h
+// A Hammer entity that spawns a TFBot and relays events to/from it
+// Michael Booth, November 2009
+
+#ifndef TF_BOT_PROXY_H
+#define TF_BOT_PROXY_H
+
+
+class CTFBot;
+class CTFBotActionPoint;
+
+
+class CTFBotProxy : public CPointEntity
+{
+ DECLARE_CLASS( CTFBotProxy, CPointEntity );
+public:
+ DECLARE_DATADESC();
+
+ CTFBotProxy( void );
+ virtual ~CTFBotProxy() { }
+
+ void Think( void );
+
+ // Input
+ void InputSetTeam( inputdata_t &inputdata );
+ void InputSetClass( inputdata_t &inputdata );
+ void InputSetMovementGoal( inputdata_t &inputdata );
+ void InputSpawn( inputdata_t &inputdata );
+ void InputDelete( inputdata_t &inputdata );
+
+ void OnInjured( void );
+ void OnKilled( void );
+ void OnAttackingEnemy( void );
+ void OnKilledEnemy( void );
+
+protected:
+ // Output
+ COutputEvent m_onSpawned;
+ COutputEvent m_onInjured;
+ COutputEvent m_onKilled;
+ COutputEvent m_onAttackingEnemy;
+ COutputEvent m_onKilledEnemy;
+
+ char m_botName[64];
+ char m_className[64];
+ char m_teamName[64];
+
+ string_t m_spawnOnStart;
+ string_t m_actionPointName;
+ float m_respawnInterval;
+
+ CHandle< CTFBot > m_bot;
+ CHandle< CTFBotActionPoint > m_moveGoal;
+};
+
+
+#endif // TF_BOT_PROXY_H
diff --git a/game/server/tf/bot/map_entities/tf_bot_roster.cpp b/game/server/tf/bot/map_entities/tf_bot_roster.cpp
new file mode 100644
index 0000000..d07fc23
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_roster.cpp
@@ -0,0 +1,107 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_roster.cpp
+// entity that dictates what classes a bot can choose when spawning
+// Tom Bui, April 2010
+
+#include "cbase.h"
+
+#include "tf_shareddefs.h"
+#include "bot/map_entities/tf_bot_roster.h"
+
+//------------------------------------------------------------------------------
+
+BEGIN_DATADESC( CTFBotRoster )
+ DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ),
+ DEFINE_KEYFIELD( m_bAllowClassChanges, FIELD_BOOLEAN, "allowClassChanges" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SCOUT], FIELD_BOOLEAN, "allowScout" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SNIPER], FIELD_BOOLEAN, "allowSniper" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SOLDIER], FIELD_BOOLEAN, "allowSoldier" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_DEMOMAN], FIELD_BOOLEAN, "allowDemoman" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_MEDIC], FIELD_BOOLEAN, "allowMedic" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_HEAVYWEAPONS], FIELD_BOOLEAN, "allowHeavy" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_PYRO], FIELD_BOOLEAN, "allowPyro" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SPY], FIELD_BOOLEAN, "allowSpy" ),
+ DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_ENGINEER], FIELD_BOOLEAN, "allowEngineer" ),
+
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetTeam", InputSetTeam ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowScout", InputSetAllowScout ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSniper", InputSetAllowSniper ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSoldier", InputSetAllowSoldier ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowDemoman", InputSetAllowDemoman ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowMedic", InputSetAllowMedic ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowHeavy", InputSetAllowHeavy ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowPyro", InputSetAllowPyro ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSpy", InputSetAllowSpy ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowEngineer", InputSetAllowEngineer ),
+
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( bot_roster, CTFBotRoster );
+
+//------------------------------------------------------------------------------
+
+CTFBotRoster::CTFBotRoster()
+{
+ memset( m_bAllowedClasses, 0, sizeof( m_bAllowedClasses ) );
+}
+
+//------------------------------------------------------------------------------
+
+void CTFBotRoster::InputSetAllowScout( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_SCOUT] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowSniper( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_SNIPER] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowSoldier( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_SOLDIER] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowDemoman( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_DEMOMAN] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowMedic( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_MEDIC] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowHeavy( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_HEAVYWEAPONS] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowPyro( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_PYRO] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowSpy( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_SPY] = inputdata.value.Bool();
+}
+
+void CTFBotRoster::InputSetAllowEngineer( inputdata_t &inputdata )
+{
+ m_bAllowedClasses[TF_CLASS_ENGINEER] = inputdata.value.Bool();
+}
+
+//------------------------------------------------------------------------------
+
+bool CTFBotRoster::IsClassAllowed( int iBotClass ) const
+{
+ return iBotClass > TF_CLASS_UNDEFINED && iBotClass < TF_LAST_NORMAL_CLASS && m_bAllowedClasses[iBotClass];
+}
+
+//------------------------------------------------------------------------------
+
+bool CTFBotRoster::IsClassChangeAllowed() const
+{
+ return m_bAllowClassChanges;
+}
diff --git a/game/server/tf/bot/map_entities/tf_bot_roster.h b/game/server/tf/bot/map_entities/tf_bot_roster.h
new file mode 100644
index 0000000..e3931ba
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_bot_roster.h
@@ -0,0 +1,39 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_roster.h
+// entity that dictates what classes a bot can choose when spawning
+// Tom Bui, April 2010
+
+#ifndef TF_BOT_ROSTER_H
+#define TF_BOT_ROSTER_H
+
+class CTFBotRoster : public CPointEntity
+{
+ DECLARE_CLASS( CTFBotRoster, CPointEntity );
+public:
+ DECLARE_DATADESC();
+
+ CTFBotRoster( void );
+ virtual ~CTFBotRoster() {}
+
+ // input
+ void InputSetAllowScout( inputdata_t &inputdata );
+ void InputSetAllowSniper( inputdata_t &inputdata );
+ void InputSetAllowSoldier( inputdata_t &inputdata );
+ void InputSetAllowDemoman( inputdata_t &inputdata );
+ void InputSetAllowMedic( inputdata_t &inputdata );
+ void InputSetAllowHeavy( inputdata_t &inputdata );
+ void InputSetAllowPyro( inputdata_t &inputdata );
+ void InputSetAllowSpy( inputdata_t &inputdata );
+ void InputSetAllowEngineer( inputdata_t &inputdata );
+
+ // misc.
+ bool IsClassAllowed( int iBotClass ) const;
+ bool IsClassChangeAllowed() const;
+
+public:
+ string_t m_teamName;
+ bool m_bAllowClassChanges;
+ bool m_bAllowedClasses[TF_LAST_NORMAL_CLASS];
+};
+
+#endif // TF_BOT_ROSTER_H
diff --git a/game/server/tf/bot/map_entities/tf_spawner.cpp b/game/server/tf/bot/map_entities/tf_spawner.cpp
new file mode 100644
index 0000000..fa4f7c7
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_spawner.cpp
@@ -0,0 +1,163 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_spawner.cpp
+// Entity to spawn one or more templatized entities
+// Michael Booth, April 2011
+
+#include "cbase.h"
+
+#include "tf_gamerules.h"
+#include "bot/map_entities/tf_spawner.h"
+
+
+//------------------------------------------------------------------------------
+BEGIN_DATADESC( CTFSpawner )
+ DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ),
+ DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ),
+ DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ),
+ DEFINE_KEYFIELD( m_templateName, FIELD_STRING, "template" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "Reset", InputReset ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+
+ DEFINE_OUTPUT( m_onExpended, "OnExpended" ),
+ DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ),
+ DEFINE_OUTPUT( m_onKilled, "OnKilled" ),
+
+ DEFINE_THINKFUNC( SpawnerThink ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( tf_spawner, CTFSpawner );
+
+
+//------------------------------------------------------------------------------
+CTFSpawner::CTFSpawner( void )
+{
+ Reset();
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::Reset( void )
+{
+ m_bExpended = false;
+ m_spawnCountRemaining = 0;
+ m_spawnedVector.RemoveAll();
+ SetThink( NULL );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::InputReset( inputdata_t &inputdata )
+{
+ Reset();
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::InputEnable( inputdata_t &inputdata )
+{
+ if ( m_bExpended )
+ {
+ return;
+ }
+
+ SetThink( &CTFSpawner::SpawnerThink );
+
+ if ( m_spawnCountRemaining )
+ {
+ // already generating - don't restart count
+ return;
+ }
+
+ SetNextThink( gpGlobals->curtime );
+ m_spawnCountRemaining = m_spawnCount;
+
+ m_template = dynamic_cast< CTFSpawnTemplate * >( gEntList.FindEntityByName( NULL, m_templateName ) );
+ if ( m_template == NULL )
+ {
+ Warning( "%s failed to find template named '%s'\n", GetClassname(), STRING( m_templateName ) );
+ }
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::InputDisable( inputdata_t &inputdata )
+{
+ // just stop thinking
+ SetThink( NULL );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::OnKilled( CBaseEntity *dead )
+{
+ m_onKilled.FireOutput( dead, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawner::SpawnerThink( void )
+{
+ // still waiting for the real game to start?
+ gamerules_roundstate_t roundState = TFGameRules()->State_Get();
+ if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() )
+ {
+ SetNextThink( gpGlobals->curtime + 1.0f );
+ return;
+ }
+
+ // clean up destroyed children
+ for ( int i = 0; i < m_spawnedVector.Count(); )
+ {
+ CHandle< CBaseEntity > child = m_spawnedVector[i];
+
+ if ( child == NULL )
+ {
+ m_spawnedVector.FastRemove(i);
+ m_onKilled.FireOutput( this, this );
+ continue;
+ }
+
+ ++i;
+ }
+
+ if ( m_spawnedVector.Count() >= m_maxActiveCount )
+ {
+ // reached max simultanous active count
+ SetNextThink( gpGlobals->curtime + 0.1f );
+ return;
+ }
+
+ if ( m_template == NULL )
+ {
+ // nothing to spawn!
+ return;
+ }
+
+ // spawn the entity
+ CBaseEntity *child = m_template->Instantiate();
+ if ( child )
+ {
+ m_spawnedVector.AddToTail( child );
+
+ child->SetAbsOrigin( GetAbsOrigin() );
+ child->SetAbsAngles( GetAbsAngles() );
+ child->SetOwnerEntity( this );
+
+ DispatchSpawn( child );
+ m_onSpawned.FireOutput( child, this );
+
+ --m_spawnCountRemaining;
+ if ( m_spawnCountRemaining )
+ {
+ SetNextThink( gpGlobals->curtime + m_spawnInterval );
+ }
+ else
+ {
+ SetThink( NULL );
+ m_onExpended.FireOutput( this, this );
+ m_bExpended = true;
+ }
+ }
+}
diff --git a/game/server/tf/bot/map_entities/tf_spawner.h b/game/server/tf/bot/map_entities/tf_spawner.h
new file mode 100644
index 0000000..f57db89
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_spawner.h
@@ -0,0 +1,66 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_spawner.h
+// Entity to spawn one or more templatized entities
+// Michael Booth, April 2011
+
+#ifndef TF_SPAWNER_H
+#define TF_SPAWNER_H
+
+//--------------------------------------------------------
+/**
+ * Each particular type of entity the tf_spawner can create
+ * has an associated template (derived from this class)
+ * which defines its spawning location and initial properties.
+ */
+class CTFSpawnTemplate : public CPointEntity
+{
+public:
+ DECLARE_CLASS( CTFSpawnTemplate, CPointEntity );
+
+ virtual ~CTFSpawnTemplate() { }
+
+ virtual CBaseEntity *Instantiate( void ) const = 0; // spawn an instance of this template
+};
+
+
+//--------------------------------------------------------
+class CTFSpawner : public CPointEntity
+{
+public:
+ DECLARE_CLASS( CTFSpawner, CPointEntity );
+ DECLARE_DATADESC();
+
+ CTFSpawner( void );
+ virtual ~CTFSpawner() { }
+
+ void SpawnerThink( void );
+
+ // Input.
+ void InputReset( inputdata_t &inputdata );
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+
+ // Output
+ void OnKilled( CBaseEntity *dead );
+
+private:
+ void Reset( void );
+
+ bool m_bExpended;
+ int m_spawnCount;
+ int m_spawnCountRemaining;
+ int m_maxActiveCount;
+ float m_spawnInterval;
+
+ string_t m_templateName;
+ CHandle< CTFSpawnTemplate > m_template;
+
+ COutputEvent m_onSpawned;
+ COutputEvent m_onExpended;
+ COutputEvent m_onKilled;
+
+ CUtlVector< CHandle< CBaseEntity > > m_spawnedVector;
+};
+
+
+#endif // TF_SPAWNER_H
diff --git a/game/server/tf/bot/map_entities/tf_spawner_boss.cpp b/game/server/tf/bot/map_entities/tf_spawner_boss.cpp
new file mode 100644
index 0000000..4381dac
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_spawner_boss.cpp
@@ -0,0 +1,167 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_spawner_boss.cpp
+// Entity to spawn a Boss
+// Michael Booth, February 2011
+
+#include "cbase.h"
+
+#ifdef OBSOLETE_USE_BOSS_ALPHA
+
+#ifdef TF_RAID_MODE
+
+#include "tf_gamerules.h"
+#include "tf_spawner_boss.h"
+#include "bot_npc/bot_npc.h"
+
+
+//------------------------------------------------------------------------------
+
+BEGIN_DATADESC( CTFSpawnerBoss )
+ DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ),
+ DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ),
+ DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ),
+ DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+
+ DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ),
+ DEFINE_OUTPUT( m_onExpended, "OnExpended" ),
+ DEFINE_OUTPUT( m_onBotKilled, "OnBotKilled" ),
+ DEFINE_OUTPUT( m_onBotStunned, "OnBotStunned" ),
+
+ DEFINE_THINKFUNC( SpawnerThink ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( tf_spawner_boss, CTFSpawnerBoss );
+
+
+//------------------------------------------------------------------------------
+CTFSpawnerBoss::CTFSpawnerBoss( void )
+{
+ m_isExpended = false;
+ m_spawnCountRemaining = 0;
+
+ SetThink( NULL );
+}
+
+//------------------------------------------------------------------------------
+void CTFSpawnerBoss::InputEnable( inputdata_t &inputdata )
+{
+ if ( m_isExpended )
+ {
+ return;
+ }
+
+ SetThink( &CTFSpawnerBoss::SpawnerThink );
+
+ if ( m_spawnCountRemaining )
+ {
+ // already generating - don't restart count
+ return;
+ }
+ SetNextThink( gpGlobals->curtime );
+ m_spawnCountRemaining = m_spawnCount;
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawnerBoss::InputDisable( inputdata_t &inputdata )
+{
+ // just stop thinking
+ SetThink( NULL );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawnerBoss::OnBotKilled( CBotNPC *pBot )
+{
+ m_onBotKilled.FireOutput( pBot, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawnerBoss::OnBotStunned( CBotNPC *pBot )
+{
+ m_onBotStunned.FireOutput( pBot, this );
+}
+
+
+//------------------------------------------------------------------------------
+void CTFSpawnerBoss::SpawnerThink( void )
+{
+ // still waiting for the real game to start?
+ gamerules_roundstate_t roundState = TFGameRules()->State_Get();
+ if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() )
+ {
+ SetNextThink( gpGlobals->curtime + 1.0f );
+ return;
+ }
+
+ // remove invalid handles from our collection
+ int i = 0;
+ while( i < m_spawnedBotVector.Count() )
+ {
+ CHandle< CBotNPC > hBot = m_spawnedBotVector[i];
+ if ( hBot == NULL )
+ {
+ m_spawnedBotVector.FastRemove(i);
+ continue;
+ }
+
+ ++i;
+ }
+
+ if ( m_spawnedBotVector.Count() >= m_maxActiveCount )
+ {
+ // maximum count reached - can't spawn any more
+ SetNextThink( gpGlobals->curtime + 0.1f );
+ return;
+ }
+
+ // spawn a bot
+ CBotNPC *bot = (CBotNPC *)CreateEntityByName( "bot_boss" );
+ if ( bot )
+ {
+ m_spawnedBotVector.AddToTail( bot );
+
+ int iTeam = TEAM_UNASSIGNED;
+ if ( FStrEq( m_teamName.ToCStr(), "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( m_teamName.ToCStr(), "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+ bot->ChangeTeam( iTeam );
+
+ // match bot facing to that of spawner
+ bot->SetAbsAngles( GetAbsAngles() );
+
+ bot->SetAbsOrigin( GetAbsOrigin() );
+
+ bot->SetSpawner( this );
+
+ DispatchSpawn( bot );
+
+ m_onSpawned.FireOutput( bot, this );
+
+ --m_spawnCountRemaining;
+ if ( m_spawnCountRemaining )
+ {
+ SetNextThink( gpGlobals->curtime + m_spawnInterval );
+ }
+ else
+ {
+ SetThink( NULL );
+ m_onExpended.FireOutput( this, this );
+ m_isExpended = true;
+ }
+ }
+}
+
+#endif // TF_RAID_MODE
+
+#endif // #ifdef OBSOLETE_USE_BOSS_ALPHA
+
diff --git a/game/server/tf/bot/map_entities/tf_spawner_boss.h b/game/server/tf/bot/map_entities/tf_spawner_boss.h
new file mode 100644
index 0000000..13f247a
--- /dev/null
+++ b/game/server/tf/bot/map_entities/tf_spawner_boss.h
@@ -0,0 +1,54 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_spawner_boss.h
+// Entity to spawn a Boss
+// Michael Booth, February 2011
+
+#ifndef TF_SPAWNER_BOSS_H
+#define TF_SPAWNER_BOSS_H
+
+#ifdef OBSOLETE_USE_BOSS_ALPHA
+
+#ifdef TF_RAID_MODE
+
+class CBotNPC;
+
+class CTFSpawnerBoss : public CPointEntity
+{
+public:
+ DECLARE_CLASS( CTFSpawnerBoss, CPointEntity );
+ DECLARE_DATADESC();
+
+ CTFSpawnerBoss( void );
+ virtual ~CTFSpawnerBoss() { }
+
+ void SpawnerThink( void );
+
+ // Input.
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+
+ // Output
+ void OnBotKilled( CBotNPC *pBot );
+ void OnBotStunned( CBotNPC *pBot );
+
+private:
+ bool m_isExpended;
+ int m_spawnCount;
+ int m_spawnCountRemaining;
+ int m_maxActiveCount;
+ float m_spawnInterval;
+ string_t m_teamName;
+
+ COutputEvent m_onSpawned;
+ COutputEvent m_onExpended;
+ COutputEvent m_onBotKilled;
+ COutputEvent m_onBotStunned;
+
+ CUtlVector< CHandle< CBotNPC > > m_spawnedBotVector;
+};
+
+#endif // TF_RAID_MODE
+
+#endif // OBSOLETE_USE_BOSS_ALPHA
+
+#endif // TF_SPAWNER_BOSS_H
diff --git a/game/server/tf/bot/tf_bot.cpp b/game/server/tf/bot/tf_bot.cpp
new file mode 100644
index 0000000..6bafab4
--- /dev/null
+++ b/game/server/tf/bot/tf_bot.cpp
@@ -0,0 +1,4644 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot.cpp
+// Team Fortress NextBot
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_obj_sentrygun.h"
+#include "team_control_point_master.h"
+#include "tf_weapon_pipebomblauncher.h"
+#include "team_train_watcher.h"
+#include "tf_bot.h"
+#include "tf_bot_manager.h"
+#include "tf_bot_vision.h"
+#include "tf_team.h"
+#include "bot/map_entities/tf_bot_generator.h"
+#include "trigger_area_capture.h"
+#include "GameEventListener.h"
+#include "NextBotUtil.h"
+#include "tier3/tier3.h"
+#include "vgui/ILocalize.h"
+#include "econ_item_system.h"
+#include "bot/behavior/tf_bot_use_item.h"
+#include "tf_wearable_item_demoshield.h"
+#include "tf_weapon_buff_item.h"
+#include "tf_weapon_lunchbox.h"
+#include "func_respawnroom.h"
+#include "soundenvelope.h"
+
+#include "econ_entity_creation.h"
+
+#include "player_vs_environment/tf_population_manager.h"
+
+#include "bot/behavior/tf_bot_behavior.h"
+#include "bot/map_entities/tf_bot_generator.h"
+#include "bot/map_entities/tf_bot_hint_entity.h"
+
+ConVar tf_bot_force_class( "tf_bot_force_class", "", FCVAR_GAMEDLL, "If set to a class name, all TFBots will respawn as that class" );
+
+ConVar tf_bot_notice_gunfire_range( "tf_bot_notice_gunfire_range", "3000", FCVAR_GAMEDLL );
+ConVar tf_bot_notice_quiet_gunfire_range( "tf_bot_notice_quiet_gunfire_range", "500", FCVAR_GAMEDLL );
+ConVar tf_bot_sniper_personal_space_range( "tf_bot_sniper_personal_space_range", "1000", FCVAR_CHEAT, "Enemies beyond this range don't worry the Sniper" );
+ConVar tf_bot_pyro_deflect_tolerance( "tf_bot_pyro_deflect_tolerance", "0.5", FCVAR_CHEAT );
+ConVar tf_bot_keep_class_after_death( "tf_bot_keep_class_after_death", "0", FCVAR_GAMEDLL );
+ConVar tf_bot_prefix_name_with_difficulty( "tf_bot_prefix_name_with_difficulty", "0", FCVAR_GAMEDLL, "Append the skill level of the bot to the bot's name" );
+ConVar tf_bot_near_point_travel_distance( "tf_bot_near_point_travel_distance", "750", FCVAR_CHEAT, "If within this travel distance to the current point, bot is 'near' it" );
+ConVar tf_bot_pyro_shove_away_range( "tf_bot_pyro_shove_away_range", "250", FCVAR_CHEAT, "If a Pyro bot's target is closer than this, compression blast them away" );
+ConVar tf_bot_pyro_always_reflect( "tf_bot_pyro_always_reflect", "0", FCVAR_CHEAT, "Pyro bots will always reflect projectiles fired at them. For tesing/debugging purposes." );
+
+ConVar tf_bot_sniper_spot_min_range( "tf_bot_sniper_spot_min_range", "1000", FCVAR_CHEAT );
+ConVar tf_bot_sniper_spot_max_count( "tf_bot_sniper_spot_max_count", "10", FCVAR_CHEAT, "Stop searching for sniper spots when each side has found this many" );
+ConVar tf_bot_sniper_spot_search_count( "tf_bot_sniper_spot_search_count", "10", FCVAR_CHEAT, "Search this many times per behavior update frame" );
+ConVar tf_bot_sniper_spot_point_tolerance( "tf_bot_sniper_spot_point_tolerance", "750", FCVAR_CHEAT );
+ConVar tf_bot_sniper_spot_epsilon( "tf_bot_sniper_spot_epsilon", "100", FCVAR_CHEAT );
+
+ConVar tf_bot_sniper_goal_entity_move_tolerance( "tf_bot_sniper_goal_entity_move_tolerance", "500", FCVAR_CHEAT );
+
+ConVar tf_bot_suspect_spy_touch_interval( "tf_bot_suspect_spy_touch_interval", "5", FCVAR_CHEAT, "How many seconds back to look for touches against suspicious spies" );
+ConVar tf_bot_suspect_spy_forget_cooldown( "tf_bot_suspect_spy_forget_cooldown", "5", FCVAR_CHEAT, "How long to consider a suspicious spy as suspicious" );
+
+ConVar tf_bot_debug_tags( "tf_bot_debug_tags", "0", FCVAR_CHEAT, "ent_text will only show tags on bots" );
+
+extern ConVar tf_bot_sniper_spot_max_count;
+extern ConVar tf_bot_fire_weapon_min_time;
+extern ConVar tf_bot_sniper_misfire_chance;
+extern ConVar tf_bot_difficulty;
+extern ConVar tf_bot_farthest_visible_theater_sample_count;
+extern ConVar tf_bot_sniper_spot_min_range;
+extern ConVar tf_bot_sniper_spot_epsilon;
+extern ConVar tf_mvm_miniboss_min_health;
+extern ConVar tf_bot_path_lookahead_range;
+
+extern ConVar tf_mvm_miniboss_scale;
+
+
+//-----------------------------------------------------------------------------------------------------
+bool IsPlayerClassname( const char *string )
+{
+ for ( int i = TF_CLASS_SCOUT; i < TF_CLASS_COUNT_ALL; ++i )
+ {
+ if ( !stricmp( string, GetPlayerClassData( i )->m_szClassName ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool IsTeamName( const char *string )
+{
+ if ( !stricmp( string, "red" ) )
+ return true;
+
+ if ( !stricmp( string, "blue" ) )
+ return true;
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::DifficultyType StringToDifficultyLevel( const char *string )
+{
+ if ( !stricmp( string, "easy" ) )
+ return CTFBot::EASY;
+
+ if ( !stricmp( string, "normal" ) )
+ return CTFBot::NORMAL;
+
+ if ( !stricmp( string, "hard" ) )
+ return CTFBot::HARD;
+
+ if ( !stricmp( string, "expert" ) )
+ return CTFBot::EXPERT;
+
+ return CTFBot::UNDEFINED;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+const char *DifficultyLevelToString( CTFBot::DifficultyType skill )
+{
+ switch( skill )
+ {
+ case CTFBot::EASY: return "Easy ";
+ case CTFBot::NORMAL: return "Normal ";
+ case CTFBot::HARD: return "Hard ";
+ case CTFBot::EXPERT: return "Expert ";
+ }
+
+ return "Undefined ";
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+const char *GetRandomBotName( void )
+{
+ static const char *nameList[] =
+ {
+ "Chucklenuts",
+ "CryBaby",
+ "WITCH",
+ "ThatGuy",
+ "Still Alive",
+ "Hat-Wearing MAN",
+ "Me",
+ "Numnutz",
+ "H@XX0RZ",
+ "The G-Man",
+ "Chell",
+ "The Combine",
+ "Totally Not A Bot",
+ "Pow!",
+ "Zepheniah Mann",
+ "THEM",
+ "LOS LOS LOS",
+ "10001011101",
+ "DeadHead",
+ "ZAWMBEEZ",
+ "MindlessElectrons",
+ "TAAAAANK!",
+ "The Freeman",
+ "Black Mesa",
+ "Soulless",
+ "CEDA",
+ "BeepBeepBoop",
+ "NotMe",
+ "CreditToTeam",
+ "BoomerBile",
+ "Someone Else",
+ "Mann Co.",
+ "Dog",
+ "Kaboom!",
+ "AmNot",
+ "0xDEADBEEF",
+ "HI THERE",
+ "SomeDude",
+ "GLaDOS",
+ "Hostage",
+ "Headful of Eyeballs",
+ "CrySomeMore",
+ "Aperture Science Prototype XR7",
+ "Humans Are Weak",
+ "AimBot",
+ "C++",
+ "GutsAndGlory!",
+ "Nobody",
+ "Saxton Hale",
+ "RageQuit",
+ "Screamin' Eagles",
+
+ "Ze Ubermensch",
+ "Maggot",
+ "CRITRAWKETS",
+ "Herr Doktor",
+ "Gentlemanne of Leisure",
+ "Companion Cube",
+ "Target Practice",
+ "One-Man Cheeseburger Apocalypse",
+ "Crowbar",
+ "Delicious Cake",
+ "IvanTheSpaceBiker",
+ "I LIVE!",
+ "Cannon Fodder",
+
+ "trigger_hurt",
+ "Nom Nom Nom",
+ "Divide by Zero",
+ "GENTLE MANNE of LEISURE",
+ "MoreGun",
+ "Tiny Baby Man",
+ "Big Mean Muther Hubbard",
+ "Force of Nature",
+
+ "Crazed Gunman",
+ "Grim Bloody Fable",
+ "Poopy Joe",
+ "A Professional With Standards",
+ "Freakin' Unbelievable",
+ "SMELLY UNFORTUNATE",
+ "The Administrator",
+ "Mentlegen",
+
+ "Archimedes!",
+ "Ribs Grow Back",
+ "It's Filthy in There!",
+ "Mega Baboon",
+ "Kill Me",
+ "Glorified Toaster with Legs",
+
+#ifdef STAGING_ONLY
+ "John Spartan",
+ "Leeloo Dallas Multipass",
+ "Sho'nuff",
+ "Bruce Leroy",
+ "CAN YOUUUUUUUUU DIG IT?!?!?!?!",
+ "Big Gulp, Huh?",
+ "Stupid Hot Dog",
+ "I'm your huckleberry",
+ "The Crocketeer",
+#endif
+ NULL
+ };
+ static int nameCount = 0;
+ static int nameIndex = 0;
+
+ if ( nameCount == 0 )
+ {
+ for( ; nameList[ nameCount ]; ++nameCount );
+
+ // randomize the initial index
+ nameIndex = RandomInt( 0, nameCount-1 );
+ }
+
+ const char *name = nameList[ nameIndex++ ];
+
+ if ( nameIndex >= nameCount )
+ nameIndex = 0;
+
+ return name;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize )
+{
+ char szBotNameBuffer[256];
+ char szEnemyOrFriendlyString[256];
+
+ const char *pBotName = "";
+ const char *pFriendlyOrEnemyTitle = "";
+
+ // @note (Tom Bui): it is okay to get localized name in training, since we should be on a listen server
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // get the friendly/enemy title
+ const char *pBotTitle = NULL;
+ if ( iTeam != TEAM_UNASSIGNED )
+ {
+ int iHumanTeam = TFGameRules()->GetAssignedHumanTeam();
+ if ( iHumanTeam != TEAM_ANY )
+ {
+ if ( iHumanTeam == iTeam )
+ {
+ pBotTitle = "#TF_Bot_Title_Friendly";
+ }
+ else
+ {
+ pBotTitle = "#TF_Bot_Title_Enemy";
+ }
+ }
+ }
+ wchar_t *pLocalizedTitle = pBotTitle ? g_pVGuiLocalize->Find( pBotTitle ) : NULL;
+ if ( pLocalizedTitle )
+ {
+ g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedTitle, szEnemyOrFriendlyString, sizeof( szEnemyOrFriendlyString ) );
+ pFriendlyOrEnemyTitle = szEnemyOrFriendlyString;
+ }
+
+ // get the class name
+ wchar_t *pLocalizedName = NULL;
+ if ( iClassIndex >= TF_FIRST_NORMAL_CLASS && iClassIndex < TF_LAST_NORMAL_CLASS )
+ {
+ pLocalizedName = g_pVGuiLocalize->Find( g_aPlayerClassNames[ iClassIndex ] );
+ }
+ else
+ {
+ pLocalizedName = g_pVGuiLocalize->Find( "#TF_Bot_Generic_ClassName" );
+ }
+ g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedName, szBotNameBuffer, sizeof( szBotNameBuffer ) );
+ pBotName = szBotNameBuffer;
+ }
+ else
+ {
+ pBotName = GetRandomBotName();
+ }
+
+ const char *pDifficultyString = tf_bot_prefix_name_with_difficulty.GetBool() ? DifficultyLevelToString( skill ) : "";
+
+ // we use this as our formatting, because we don't know the language of the downstream clients
+ CFmtStr name( "%s%s%s",
+ pDifficultyString, pFriendlyOrEnemyTitle, pBotName );
+ Q_strncpy( pBuffer, name.Access(), iBufferSize );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_add, "Add a bot.", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ bool bQuotaManaged = true;
+ int botCount = 1;
+ const char *classname = NULL;
+ const char *teamname = "auto";
+ const char *pszBotNameViaArg = NULL;
+ CTFBot::DifficultyType skill = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT );
+
+ int i;
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ CTFBot::DifficultyType trySkill = StringToDifficultyLevel( args.Arg(i) );
+ int nArgAsInteger = atoi( args.Arg(i) );
+
+ // each argument could be a classname, a team, a difficulty level, a count, or a name
+ if ( IsPlayerClassname( args.Arg(i) ) )
+ {
+ classname = args.Arg(i);
+ }
+ else if ( IsTeamName( args.Arg(i) ) )
+ {
+ teamname = args.Arg(i);
+ }
+ else if ( !stricmp( args.Arg( i ), "noquota" ) )
+ {
+ bQuotaManaged = false;
+ }
+ else if ( trySkill != CTFBot::UNDEFINED )
+ {
+ skill = trySkill;
+ }
+ else if ( nArgAsInteger > 0 )
+ {
+ botCount = nArgAsInteger;
+ pszBotNameViaArg = NULL; // can't have a custom name if spawning multiple bots
+ }
+ else if ( botCount == 1 )
+ {
+ pszBotNameViaArg = args.Arg( i );
+ }
+ else
+ {
+ Warning( "Invalid argument '%s'\n", args.Arg(i) );
+ }
+ }
+
+ // cvar can override classname
+ classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? classname : tf_bot_force_class.GetString();
+ int iClassIndex = classname ? GetClassIndexFromString( classname ) : TF_CLASS_UNDEFINED;
+
+ int iTeam = TEAM_UNASSIGNED;
+ if ( FStrEq( teamname, "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( teamname, "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ skill = CTFBot::EASY;
+ }
+
+ char name[256];
+ int iNumAdded = 0;
+ for( i=0; i<botCount; ++i )
+ {
+ CTFBot *pBot = NULL;
+ const char *pszBotName = NULL;
+
+ if ( !pszBotNameViaArg )
+ {
+ CreateBotName( iTeam, iClassIndex, skill, name, sizeof(name) );
+ pszBotName = name;
+ }
+ else
+ {
+ pszBotName = pszBotNameViaArg;
+ }
+
+ pBot = NextBotCreatePlayerBot< CTFBot >( pszBotName );
+
+ if ( pBot )
+ {
+ if ( bQuotaManaged )
+ {
+ pBot->SetAttribute( CTFBot::QUOTA_MANANGED );
+ }
+
+ pBot->HandleCommand_JoinTeam( teamname );
+
+ pBot->SetDifficulty( skill );
+
+ // if no class is set, auto-select one
+ const char *thisClassname = classname ? classname : pBot->GetNextSpawnClassname();
+ pBot->HandleCommand_JoinClass( thisClassname );
+
+ // set up a proper name now that we are in training
+ if ( TFGameRules()->IsInTraining() )
+ {
+ CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof(name) );
+ engine->SetFakeClientConVarValue( pBot->edict(), "name", name );
+ }
+
+ ++iNumAdded;
+ }
+ }
+
+ if ( bQuotaManaged )
+ {
+ TheTFBots().OnForceAddedBots( iNumAdded );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_kick, "Remove a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ {
+ DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) );
+ return;
+ }
+
+ bool bMoveToSpectatorTeam = false;
+ int iTeam = TEAM_UNASSIGNED;
+ int i;
+ const char *pPlayerName = "";
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ // each argument could be a classname, a team, or a count
+ if ( FStrEq( args.Arg(i), "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( args.Arg(i), "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+ else if ( FStrEq( args.Arg(i), "all" ) )
+ {
+ iTeam = TEAM_ANY;
+ }
+ else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) )
+ {
+ bMoveToSpectatorTeam = true;
+ }
+ else
+ {
+ pPlayerName = args.Arg(i);
+ }
+ }
+
+ int iNumKicked = 0;
+ 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 ( player->MyNextBotPointer() )
+ {
+ if ( iTeam == TEAM_ANY ||
+ FStrEq( pPlayerName, player->GetPlayerName() ) ||
+ ( player->GetTeamNumber() == iTeam ) ||
+ ( player->GetTeamNumber() == iTeam ) )
+ {
+ if ( bMoveToSpectatorTeam )
+ {
+ player->ChangeTeam( TEAM_SPECTATOR, false, true );
+ }
+ else
+ {
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
+ }
+ CTFBot* pBot = dynamic_cast< CTFBot* >( player );
+ if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
+ {
+ ++iNumKicked;
+ }
+ }
+ }
+ }
+ TheTFBots().OnForceKickedBots( iNumKicked );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_kill, "Kill a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ {
+ DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) );
+ return;
+ }
+
+ int iTeam = TEAM_UNASSIGNED;
+ int i;
+ const char *pPlayerName = "";
+ for( i=1; i<args.ArgC(); ++i )
+ {
+ // each argument could be a classname, a team, or a count
+ if ( FStrEq( args.Arg(i), "red" ) )
+ {
+ iTeam = TF_TEAM_RED;
+ }
+ else if ( FStrEq( args.Arg(i), "blue" ) )
+ {
+ iTeam = TF_TEAM_BLUE;
+ }
+ else if ( FStrEq( args.Arg(i), "all" ) )
+ {
+ iTeam = TEAM_ANY;
+ }
+ else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) )
+ {
+ // bMoveToSpectatorTeam = true;
+ }
+ else
+ {
+ pPlayerName = args.Arg(i);
+ }
+ }
+
+ 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 ( player->MyNextBotPointer() )
+ {
+ if ( iTeam == TEAM_ANY ||
+ FStrEq( pPlayerName, player->GetPlayerName() ) ||
+ ( player->GetTeamNumber() == iTeam ) ||
+ ( player->GetTeamNumber() == iTeam ) )
+ {
+ CTakeDamageInfo info( player, player, 9999999.9f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );
+ player->TakeDamage( info );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CMD_BotWarpTeamToMe( void )
+{
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ if ( !player )
+ return;
+
+ CTeam *myTeam = player->GetTeam();
+ for( int i=0; i<myTeam->GetNumPlayers(); ++i )
+ {
+ if ( !myTeam->GetPlayer(i)->IsAlive() )
+ continue;
+
+ myTeam->GetPlayer(i)->SetAbsOrigin( player->GetAbsOrigin() );
+ }
+}
+static ConCommand tf_bot_warp_team_to_me( "tf_bot_warp_team_to_me", CMD_BotWarpTeamToMe, "", FCVAR_GAMEDLL | FCVAR_CHEAT );
+
+
+//-----------------------------------------------------------------------------------------------------
+IMPLEMENT_INTENTION_INTERFACE( CTFBot, CTFBotMainAction );
+
+
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( tf_bot, CTFBot );
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Allocate a bot and bind it to the edict
+ */
+CBasePlayer *CTFBot::AllocatePlayerEntity( edict_t *edict, const char *playerName )
+{
+ CBasePlayer::s_PlayerEdict = edict;
+ return static_cast< CBasePlayer * >( CreateEntityByName( "tf_bot" ) );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseFireButton();
+ return;
+ }
+
+ BaseClass::PressFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressAltFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseAltFireButton();
+ return;
+ }
+
+ BaseClass::PressAltFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PressSpecialFireButton( float duration )
+{
+ // can't fire if stunned
+ // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire
+ if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) )
+ {
+ ReleaseAltFireButton();
+ return;
+ }
+
+ BaseClass::PressSpecialFireButton( duration );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+class CCountClassMembers
+{
+public:
+ CCountClassMembers( const CTFBot *me, int teamID )
+ {
+ m_me = me;
+ m_myTeam = teamID;
+ m_teamSize = 0;
+
+ for( int i=0; i<TF_LAST_NORMAL_CLASS; ++i )
+ m_count[i] = 0;
+ }
+
+ bool operator() ( CBasePlayer *basePlayer )
+ {
+ CTFPlayer *player = (CTFPlayer *)basePlayer;
+
+ if ( player->GetTeamNumber() != m_myTeam )
+ return true;
+
+ ++m_teamSize;
+
+ if ( m_me->IsSelf( player ) )
+ return true;
+
+ ++m_count[ player->GetDesiredPlayerClassIndex() ];
+
+ return true;
+ }
+
+ const CTFBot *m_me;
+ int m_myTeam;
+ int m_count[ TF_LAST_NORMAL_CLASS+1 ];
+ int m_teamSize;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * NOTE: Assumes bot's difficulty has been set, and the bot is on a team.
+ */
+const char *CTFBot::GetNextSpawnClassname( void ) const
+{
+ struct ClassSelectionInfo
+ {
+ int m_class;
+ int m_minTeamSizeToSelect; // team must have this many members to choose this class
+ int m_countPerTeamSize; // must have 1 Medic for each 4 team members, for example
+ int m_minLimit; // minimum that must be present (once other constraints are met)
+ int m_maxLimit[ NUM_DIFFICULTY_LEVELS ]; // maximum that can be present (-1 for infinite)
+ };
+
+ const int NoLimit = -1;
+
+ static ClassSelectionInfo defenseRoster[] =
+ {
+ { TF_CLASS_ENGINEER, 0, 4, 1, { 1, 2, 3, 3 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } },
+ { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } },
+ { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } },
+ { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } },
+ { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ static ClassSelectionInfo offenseRoster[] =
+ {
+ { TF_CLASS_SCOUT, 0, 0, 1, { 3, 3, 3, 3 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns
+ { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } },
+ { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } },
+ { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } },
+ { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } },
+ { TF_CLASS_ENGINEER, 5, 0, 0, { 1, 1, 1, 1 } },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ static ClassSelectionInfo compRoster[] =
+ {
+ { TF_CLASS_SCOUT, 0, 0, 0, { 0, 0, 2, 2 } },
+ { TF_CLASS_SOLDIER, 0, 0, 0, { 0, 0, NoLimit, NoLimit } },
+ { TF_CLASS_DEMOMAN, 0, 0, 0, { 0, 0, 2, 2 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns
+ { TF_CLASS_PYRO, 0, -1 },
+ { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 0, 0, 2, 2 } },
+ { TF_CLASS_MEDIC, 1, 0, 1, { 0, 0, 1, 1 } },
+ { TF_CLASS_SNIPER, 0, -1 },
+ { TF_CLASS_SPY, 0, -1 },
+ { TF_CLASS_ENGINEER, 0, -1 },
+
+ { TF_CLASS_UNDEFINED, 0, -1 },
+ };
+
+ // if we are an engineer with an active sentry or teleporters, don't switch
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ if ( const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_SENTRYGUN ) ||
+ const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) )
+ {
+ return "engineer";
+ }
+ }
+
+ // count classes in use by my team, not including me
+ CCountClassMembers currentRoster( this, GetTeamNumber() );
+ ForEachPlayer( currentRoster );
+
+ // assume offense
+ ClassSelectionInfo *desiredRoster = offenseRoster;
+
+ if ( TFGameRules()->IsMatchTypeCompetitive() )
+ {
+ desiredRoster = compRoster;
+ }
+ else if ( TFGameRules()->IsInKothMode() )
+ {
+ CTeamControlPoint *point = GetMyControlPoint();
+ if ( point )
+ {
+ if ( GetTeamNumber() == ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) )
+ {
+ // defend our point
+ desiredRoster = defenseRoster;
+ }
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector );
+
+ CUtlVector< CTeamControlPoint * > defendVector;
+ TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector );
+
+ // if we have any points we can capture, try to do so
+ if ( captureVector.Count() > 0 || defendVector.Count() == 0 )
+ {
+ desiredRoster = offenseRoster;
+ }
+ else
+ {
+ desiredRoster = defenseRoster;
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ if ( GetTeamNumber() == TF_TEAM_RED )
+ {
+ desiredRoster = defenseRoster;
+ }
+ }
+
+ // build vector of classes we can pick from
+ CUtlVector< int > desiredClassVector;
+ CUtlVector< int > allowedClassForBotRosterVector;
+
+ for( int i=0; desiredRoster[ i ].m_class != TF_CLASS_UNDEFINED; ++i )
+ {
+ ClassSelectionInfo *desiredClassInfo = &desiredRoster[ i ];
+
+ if ( TFGameRules()->CanBotChooseClass( const_cast< CTFBot * >( this ), desiredClassInfo->m_class ) == false )
+ {
+ // not allowed to use this class
+ continue;
+ }
+ // just in case we hit the class limits, we want to make sure we select a class that is allowed
+ allowedClassForBotRosterVector.AddToTail( desiredClassInfo->m_class );
+
+ if ( currentRoster.m_teamSize < desiredClassInfo->m_minTeamSizeToSelect )
+ {
+ // team is too small to choose this class
+ continue;
+ }
+
+ // check limits
+ if ( currentRoster.m_count[ desiredClassInfo->m_class ] < desiredClassInfo->m_minLimit )
+ {
+ // below required limit - choose only this class
+ desiredClassVector.RemoveAll();
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ break;
+ }
+
+ int maxLimit = desiredClassInfo->m_maxLimit[ (int)clamp( GetDifficulty(), CTFBot::EASY, CTFBot::EXPERT ) ];
+
+ if ( maxLimit > NoLimit && currentRoster.m_count[ desiredClassInfo->m_class ] >= maxLimit )
+ {
+ // at or above limit for this class
+ continue;
+ }
+
+ if ( desiredClassInfo->m_countPerTeamSize > 0 )
+ {
+ // how many of this class should there be at the given "per" count
+ int maxCountPer = currentRoster.m_teamSize / desiredClassInfo->m_countPerTeamSize;
+ if ( currentRoster.m_count[ desiredClassInfo->m_class ] - desiredClassInfo->m_minTeamSizeToSelect < maxCountPer )
+ {
+ // below required limit - choose only this class
+ desiredClassVector.RemoveAll();
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ break;
+ }
+ }
+
+ // valid class to choose
+ desiredClassVector.AddToTail( desiredClassInfo->m_class );
+ }
+
+ if ( desiredClassVector.Count() == 0 )
+ {
+ if ( allowedClassForBotRosterVector.Count() == 0 )
+ {
+ // nothing available
+ Warning( "TFBot unable to choose a class, defaulting to 'auto'\n" );
+ return "auto";
+ }
+ else
+ {
+ desiredClassVector = allowedClassForBotRosterVector;
+ }
+ }
+
+ int which = RandomInt( 0, desiredClassVector.Count()-1 );
+
+ // if we need to destroy a sentry, pick a class that can do so
+ if ( GetEnemySentry() )
+ {
+ // best sentry demolitions
+ int demoman = desiredClassVector.Find( TF_CLASS_DEMOMAN );
+ if ( demoman >= 0 )
+ {
+ which = demoman;
+ }
+ else
+ {
+ // next best sentry demolitions
+ int spy = desiredClassVector.Find( TF_CLASS_SPY );
+ if ( spy >= 0 )
+ {
+ which = spy;
+ }
+ else
+ {
+ // good sentry demolitions
+ int soldier = desiredClassVector.Find( TF_CLASS_SOLDIER );
+ if ( soldier >= 0 )
+ {
+ which = soldier;
+ }
+ }
+ }
+ }
+
+ TFPlayerClassData_t *classData = GetPlayerClassData( desiredClassVector[ which ] );
+ if ( classData )
+ {
+ return classData->m_szClassName;
+ }
+
+ Warning( "TFBot unable to get data for desired class, defaulting to 'auto'\n" );
+ return "auto";
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::CTFBot()
+{
+ m_body = new CTFBotBody( this );
+ m_locomotor = new CTFBotLocomotion( this );
+ m_vision = new CTFBotVision( this );
+ ALLOCATE_INTENTION_INTERFACE( CTFBot );
+
+ m_spawnArea = NULL;
+ m_weaponRestrictionFlags = 0;
+ m_attributeFlags = 0;
+ m_homeArea = NULL;
+ m_squad = NULL;
+ m_didReselectClass = false;
+ m_enemySentry = NULL;
+ m_spotWhereEnemySentryLastInjuredMe = vec3_origin;
+ m_isLookingAroundForEnemies = true;
+ m_behaviorFlags = 0;
+ m_attentionFocusEntity = NULL;
+ m_noisyTimer.Invalidate();
+
+ if ( TFGameRules()->IsInTraining() )
+ {
+ m_difficulty = CTFBot::EASY;
+ }
+ else
+ {
+ m_difficulty = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT );
+ }
+
+ m_actionPoint = NULL;
+ m_proxy = NULL;
+ m_spawner = NULL;
+
+ m_myControlPoint = NULL;
+
+ SetMission( NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
+ SetMissionTarget( NULL );
+ m_missionString.Clear();
+
+ m_fModelScaleOverride = -1.0f;
+ m_maxVisionRangeOverride = -1.0f;
+ m_squadFormationError = 0.0f;
+
+ m_hFollowingFlagTarget = NULL;
+
+ SetShouldQuickBuild( false );
+ SetAutoJump( 0.f, 0.f );
+
+ ClearSniperSpots();
+
+ ListenForGameEvent( "teamplay_point_startcapture" );
+ ListenForGameEvent( "teamplay_point_captured" );
+ ListenForGameEvent( "teamplay_round_win" );
+ ListenForGameEvent( "teamplay_flag_event" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTFBot::~CTFBot()
+{
+ // delete Intention first, since destruction of Actions may access other components
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_body )
+ delete m_body;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+
+ if ( m_vision )
+ delete m_vision;
+
+ m_suspectedSpyVector.PurgeAndDeleteElements();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Spawn()
+{
+ BaseClass::Spawn();
+
+ m_spawnArea = NULL;
+ m_justLostPointTimer.Invalidate();
+ m_squad = NULL;
+ m_didReselectClass = false;
+ m_isLookingAroundForEnemies = true;
+ m_attentionFocusEntity = NULL;
+
+ m_suspectedSpyVector.PurgeAndDeleteElements();
+ m_knownSpyVector.RemoveAll();
+ m_delayedNoticeVector.RemoveAll();
+
+ m_myControlPoint = NULL;
+ ClearSniperSpots();
+ ClearTags();
+
+ m_hFollowingFlagTarget = NULL;
+
+ m_requiredWeaponStack.Clear();
+ SetShouldQuickBuild( false );
+
+ SetSquadFormationError( 0.0f );
+ SetBrokenFormation( false );
+
+ GetVisionInterface()->ForgetAllKnownEntities();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::SetMission( MissionType mission, bool resetBehaviorSystem )
+{
+ SetPrevMission( m_mission );
+ m_mission = mission;
+
+ if ( resetBehaviorSystem )
+ {
+ // reset the behavior system to start the given mission
+ GetIntentionInterface()->Reset();
+ }
+
+ // Temp hack - some missions play an idle loop
+ if ( m_mission > NO_MISSION )
+ {
+ StartIdleSound();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::PhysicsSimulate( void )
+{
+ BaseClass::PhysicsSimulate();
+
+ if ( m_spawnArea == NULL )
+ {
+ m_spawnArea = GetLastKnownArea();
+ }
+
+ if ( HasAttribute( CTFBot::ALWAYS_CRIT ) && !m_Shared.InCond( TF_COND_CRITBOOSTED_USER_BUFF ) )
+ {
+ m_Shared.AddCond( TF_COND_CRITBOOSTED_USER_BUFF );
+ }
+
+ // force my speed to be recalculated to keep squad together and restore speed afterwards
+ TeamFortress_SetSpeed();
+
+ if ( IsInASquad() )
+ {
+ if ( GetSquad()->GetMemberCount() <= 1 || GetSquad()->GetLeader() == NULL )
+ {
+ // squad has collapsed - disband it
+ LeaveSquad();
+ }
+ }
+
+
+ // If we're dead, choose a new class.
+ // We need to do this outside of the behavior system, since changing class can
+ // sometimes force an immediate respawn, which will destroy the bot's existing actions out from under it.
+ if ( !IsAlive() && !m_didReselectClass && tf_bot_keep_class_after_death.GetBool() == false && TFGameRules()->CanBotChangeClass( this ) )
+ {
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ return;
+
+ const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? GetNextSpawnClassname() : tf_bot_force_class.GetString();
+
+ HandleCommand_JoinClass( classname );
+
+ m_didReselectClass = true;
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Touch( CBaseEntity *pOther )
+{
+ BaseClass::Touch( pOther );
+
+ CTFPlayer *them = ToTFPlayer( pOther );
+ if ( them && IsEnemy( them ) )
+ {
+ if ( them->m_Shared.IsStealthed() || them->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ // bumped a spy - they are discovered!
+ if ( TFGameRules()->IsMannVsMachineMode() ) // we have to build up to knowing that they are a spy in MvM
+ {
+ SuspectSpy( them );
+ }
+ else
+ {
+ RealizeSpy( them );
+ }
+ }
+
+ // always notice if we bump an enemy
+ TheNextBots().OnWeaponFired( them, them->GetActiveTFWeapon() );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Avoid penetrating teammates
+void CTFBot::AvoidPlayers( CUserCmd *pCmd )
+{
+ // Turn off the avoid player code.
+ if ( !tf_avoidteammates.GetBool() || !tf_avoidteammates_pushaway.GetBool() )
+ return;
+
+ Vector forward, right;
+ EyeVectors( &forward, &right );
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+
+ Vector avoidVector = vec3_origin;
+
+ float tooClose = 50.0f;
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ // bots stay farther apart in MvM mode
+ tooClose = 150.0f;
+ }
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *them = playerVector[i];
+
+ if ( IsSelf( them ) )
+ {
+ continue;
+ }
+
+ if ( HasTheFlag() )
+ {
+ // Don't push around the flag (bomb) carrier.
+ // We need this for MvM mode so friendly bots don't
+ // move the bomb jumper and cause him to restart.
+ continue;
+ }
+
+ if ( IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ if ( !them->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // medics only avoid other medics, so they stay with their patient
+ continue;
+ }
+ }
+ else if ( IsInASquad() )
+ {
+ // if I'm a non-Medic in a Squad, I'm part of a formation
+ continue;
+ }
+
+ Vector between = GetAbsOrigin() - them->GetAbsOrigin();
+ if ( between.IsLengthLessThan( tooClose ) )
+ {
+ float range = between.NormalizeInPlace();
+
+ avoidVector += ( 1.0f - ( range / tooClose ) ) * between;
+ }
+ }
+
+ if ( avoidVector.IsZero() )
+ {
+ m_Shared.SetSeparation( false );
+ m_Shared.SetSeparationVelocity( vec3_origin );
+ return;
+ }
+
+ avoidVector.NormalizeInPlace();
+
+ m_Shared.SetSeparation( true );
+
+ const float maxSpeed = 50.0f;
+ m_Shared.SetSeparationVelocity( avoidVector * maxSpeed );
+
+ float ahead = maxSpeed * DotProduct( forward, avoidVector );
+ float side = maxSpeed * DotProduct( right, avoidVector );
+
+ pCmd->forwardmove += ahead;
+ pCmd->sidemove += side;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::UpdateOnRemove( void )
+{
+ StopIdleSound();
+
+ BaseClass::UpdateOnRemove();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+int CTFBot::ShouldTransmit( const CCheckTransmitInfo *pInfo )
+{
+ if ( HasAttribute( USE_BOSS_HEALTH_BAR ) )
+ {
+ return FL_EDICT_ALWAYS;
+ }
+
+ return BaseClass::ShouldTransmit( pInfo );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance /*= false*/ )
+{
+ BaseClass::ChangeTeam( iTeamNum, bAutoTeam, bSilent, bAutoBalance );
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ SetPrevMission( CTFBot::NO_MISSION );
+ ClearAllAttributes();
+ // Clear Sound
+ StopIdleSound();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::ShouldGib( const CTakeDamageInfo &info )
+{
+ // only gib giant/miniboss
+ if ( TFGameRules()->IsMannVsMachineMode() && ( IsMiniBoss() || GetModelScale() > 1.f ) )
+ {
+ return true;
+ }
+
+ return BaseClass::ShouldGib( info );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsAllowedToPickUpFlag( void ) const
+{
+ if ( !BaseClass::IsAllowedToPickUpFlag() )
+ {
+ return false;
+ }
+
+ // only the leader of a squad can pick up the flag
+ if ( IsInASquad() && !GetSquad()->IsLeader( const_cast< CTFBot * >( this ) ) )
+ return false;
+
+ // mission bots can't pick up the flag
+ return !IsOnAnyMission();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::InitClass( void )
+{
+ BaseClass::InitClass();
+}
+
+void CTFBot::ModifyMaxHealth( int nNewMaxHealth, bool bSetCurrentHealth /*= true*/, bool bAllowModelScaling /*= true*/ )
+{
+ if ( GetMaxHealth() != nNewMaxHealth )
+ {
+ static CSchemaAttributeDefHandle pAttrDef_HiddenMaxHealthNonBuffed( "hidden maxhealth non buffed" );
+ if ( !pAttrDef_HiddenMaxHealthNonBuffed )
+ {
+ Warning( "TFBotSpawner: Invalid attribute 'hidden maxhealth non buffed'\n" );
+ }
+ else
+ {
+ CAttributeList *pAttrList = GetAttributeList();
+ if ( pAttrList )
+ {
+ pAttrList->SetRuntimeAttributeValue( pAttrDef_HiddenMaxHealthNonBuffed, nNewMaxHealth - GetMaxHealth() );
+ }
+ }
+ }
+
+ if ( bSetCurrentHealth )
+ {
+ SetHealth( nNewMaxHealth );
+ }
+
+ if ( bAllowModelScaling && IsMiniBoss() )
+ {
+ SetModelScale( m_fModelScaleOverride > 0.0f ? m_fModelScaleOverride : tf_mvm_miniboss_scale.GetFloat() );
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Invoked when a game event occurs
+ */
+void CTFBot::FireGameEvent( IGameEvent *event )
+{
+ const char *eventName = event->GetName();
+
+ if ( FStrEq( eventName, "teamplay_point_captured" ) )
+ {
+ ClearMyControlPoint();
+
+ int whoCapped = event->GetInt( "team" );
+ int pointID = event->GetInt( "cp" );
+
+ if ( whoCapped == GetTeamNumber() )
+ {
+ OnTerritoryCaptured( pointID );
+ }
+ else
+ {
+ OnTerritoryLost( pointID );
+
+ m_justLostPointTimer.Start( RandomFloat( 10.0f, 20.0f ) );
+ }
+ }
+ else if ( FStrEq( eventName, "teamplay_point_startcapture" ) )
+ {
+ int pointID = event->GetInt( "cp" );
+
+ OnTerritoryContested( pointID );
+ }
+ else if ( FStrEq( eventName, "teamplay_flag_event" ) )
+ {
+ if ( event->GetInt( "eventtype" ) == TF_FLAGEVENT_PICKUP )
+ {
+ int iPlayer = event->GetInt( "player" );
+ if ( iPlayer == entindex() )
+ {
+ // I just picked up the flag
+ OnPickUp( NULL, NULL );
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::Event_Killed( const CTakeDamageInfo &info )
+{
+ BaseClass::Event_Killed( info );
+
+ if ( HasProxy() )
+ {
+ GetProxy()->OnKilled();
+ }
+
+ // announce Spies
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS );
+
+ int spyCount = 0;
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ ++spyCount;
+ }
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mission_update" );
+ if ( event )
+ {
+ event->SetInt( "class", TF_CLASS_SPY );
+ event->SetInt( "count", spyCount );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ else if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // in MVM, when an engineer dies, we need to decouple his objects so they stay alive when his bot slot gets recycled
+ while ( GetObjectCount() > 0 )
+ {
+ // set to not have owner
+ CBaseObject *pObject = GetObject( 0 );
+ if ( pObject )
+ {
+ pObject->SetOwnerEntity( NULL );
+ pObject->SetBuilder( NULL );
+ }
+ RemoveObject( pObject );
+ }
+
+ // unown engineer nest if owned any
+ for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i )
+ {
+ CBaseTFBotHintEntity* pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] );
+ if ( pHint->GetOwnerEntity() == this )
+ {
+ pHint->SetOwnerEntity( NULL );
+ }
+ }
+
+ CUtlVector< CTFPlayer* > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS );
+ bool bShouldAnnounceLastEngineerBotDeath = HasAttribute( CTFBot::TELEPORT_TO_HINT );
+ if ( bShouldAnnounceLastEngineerBotDeath )
+ {
+ for ( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( playerVector[i] != this && playerVector[i]->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ bShouldAnnounceLastEngineerBotDeath = false;
+ break;
+ }
+ }
+ }
+
+ if ( bShouldAnnounceLastEngineerBotDeath )
+ {
+ bool bEngineerTeleporterInTheWorld = false;
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->GetType() == OBJ_TELEPORTER && pObj->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ bEngineerTeleporterInTheWorld = true;
+ }
+ }
+
+ if ( bEngineerTeleporterInTheWorld )
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead_But_Not_Teleporter" );
+ }
+ else
+ {
+ TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead" );
+ }
+ }
+ }
+
+ // remove this bot from following flag
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+ flag->RemoveFollower( this );
+ }
+ }
+ } // MvM
+
+ if ( HasSpawner() )
+ {
+ GetSpawner()->OnBotKilled( this );
+ }
+
+ if ( IsInASquad() )
+ {
+ LeaveSquad();
+ }
+
+ CTFNavArea *lastArea = (CTFNavArea *)GetLastKnownArea();
+ if ( lastArea )
+ {
+ // remove us from old visible set
+ NavAreaCollector wasVisible;
+ lastArea->ForAllPotentiallyVisibleAreas( wasVisible );
+
+ int i;
+ for( i=0; i<wasVisible.m_area.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)wasVisible.m_area[i];
+ area->RemovePotentiallyVisibleActor( this );
+ }
+ }
+
+
+ if ( info.GetInflictor() && info.GetInflictor()->GetTeamNumber() != GetTeamNumber() )
+ {
+ CObjectSentrygun *sentrygun = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
+
+ if ( sentrygun )
+ {
+ // we were killed by an enemy sentry - remember it
+ RememberEnemySentry( sentrygun, GetAbsOrigin() );
+ }
+ }
+
+ StopIdleSound();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CTeamControlPoint *CTFBot::SelectPointToCapture( CUtlVector< CTeamControlPoint * > *captureVector ) const
+{
+ if ( !captureVector || captureVector->Count() == 0 )
+ {
+ return NULL;
+ }
+
+ if ( captureVector->Count() == 1 )
+ {
+ // only one choice
+ return captureVector->Element(0);
+ }
+
+ // if we're capturing a point, stay on it
+ if ( const_cast< CTFBot * >( this )->IsCapturingPoint() )
+ {
+ CTriggerAreaCapture *trigger = const_cast< CTFBot * >( this )->GetControlPointStandingOn();
+ if ( trigger )
+ {
+ return trigger->GetControlPoint();
+ }
+ }
+
+ // if we're near a point that is being captured, go help (in the event multiple points are being simultaneously captured)
+ CTeamControlPoint *closestPoint = SelectClosestControlPointByTravelDistance( captureVector );
+ if ( closestPoint )
+ {
+ bool alwaysUseClosest = false;
+
+#ifdef STAGING_ONLY
+ alwaysUseClosest = TFGameRules() && TFGameRules()->IsBountyMode();
+#endif // STAGING_ONLY
+
+ if ( IsPointBeingCaptured( closestPoint ) || alwaysUseClosest )
+ {
+ return closestPoint;
+ }
+ }
+
+ // if any point is being captured by our team, go help
+ for( int i=0; i<captureVector->Count(); ++i )
+ {
+ CTeamControlPoint *point = captureVector->Element(i);
+
+ if ( IsPointBeingCaptured( point ) )
+ {
+ return point;
+ }
+ }
+
+ // no points are currently being captured - pick the point with the least combat
+ CTeamControlPoint *safestPoint = NULL;
+ float safestPointCombat = FLT_MAX;
+ bool areAllPointsCombatFree = true;
+
+ for( int i=0; i<captureVector->Count(); ++i )
+ {
+ CTeamControlPoint *point = captureVector->Element(i);
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ if ( !pointArea )
+ {
+ continue;
+ }
+
+ float combat = pointArea->GetCombatIntensity();
+
+ const float minCombat = 0.1f;
+ if ( combat > minCombat )
+ {
+ areAllPointsCombatFree = false;
+ }
+
+ if ( combat < safestPointCombat )
+ {
+ safestPoint = point;
+ safestPointCombat = combat;
+ }
+ }
+
+ // if no points are in combat, pick a random point
+ if ( areAllPointsCombatFree )
+ {
+ const float decisionPeriod = 60.0f;
+ int which = captureVector->Count() * TransientlyConsistentRandomValue( decisionPeriod );
+ which = clamp( which, 0, captureVector->Count()-1 );
+
+ return captureVector->Element( which );
+ }
+
+ // choose the point with the least combat
+ return safestPoint;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CTeamControlPoint *CTFBot::SelectPointToDefend( CUtlVector< CTeamControlPoint * > *defendVector ) const
+{
+ if ( defendVector && defendVector->Count() > 0 )
+ {
+ if ( HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) )
+ {
+ return SelectClosestControlPointByTravelDistance( defendVector );
+ }
+
+ return defendVector->Element( RandomInt( 0, defendVector->Count()-1 ) );
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the point we have decided to capture or defend
+ */
+CTeamControlPoint *CTFBot::GetMyControlPoint( void ) const
+{
+ if ( m_myControlPoint != NULL && !m_evaluateControlPointTimer.IsElapsed() )
+ {
+ return m_myControlPoint;
+ }
+
+ m_evaluateControlPointTimer.Start( RandomFloat( 1.0f, 2.0f ) );
+
+
+ CUtlVector< CTeamControlPoint * > captureVector;
+ TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector );
+
+ CUtlVector< CTeamControlPoint * > defendVector;
+ TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector );
+
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) || IsPlayerClass( TF_CLASS_SNIPER ) || HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) )
+ {
+ // engineers always try to defend first
+ if ( defendVector.Count() > 0 )
+ {
+ m_myControlPoint = SelectPointToDefend( &defendVector );
+ return m_myControlPoint;
+ }
+ }
+
+ // if we have a point we can capture - do it
+ m_myControlPoint = SelectPointToCapture( &captureVector );
+
+ if ( m_myControlPoint == NULL )
+ {
+ // otherwise, defend our point(s) from capture
+ m_myControlPoint = SelectPointToDefend( &defendVector );
+ }
+
+ return m_myControlPoint;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return flag we want to fetch
+CCaptureFlag *CTFBot::GetFlagToFetch( void ) const
+{
+ CUtlVector<CCaptureFlag *> flagsVector;
+ int nCarriedFlags = 0;
+
+ // MvM Engineer bot never pick up a flag
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS && IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ return NULL;
+ }
+
+ if( HasAttribute( CTFBot::IGNORE_FLAG ) )
+ {
+ return NULL;
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() && HasFlagTaget() )
+ {
+ return GetFlagTarget();
+ }
+ }
+
+ // Collect flags
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+
+ if ( flag->IsDisabled() )
+ continue;
+
+ // If I'm carrying a flag, look for mine and early-out
+ if ( HasTheFlag() )
+ {
+ if ( flag->GetOwnerEntity() == this )
+ {
+ return flag;
+ }
+ }
+
+ switch( flag->GetType() )
+ {
+ case TF_FLAGTYPE_CTF:
+ if ( flag->GetTeamNumber() == GetEnemyTeam( GetTeamNumber() ) )
+ {
+ // we want to steal the other team's flag
+ flagsVector.AddToTail( flag );
+ }
+ break;
+
+ case TF_FLAGTYPE_ATTACK_DEFEND:
+ case TF_FLAGTYPE_TERRITORY_CONTROL:
+ case TF_FLAGTYPE_INVADE:
+ if ( flag->GetTeamNumber() != GetEnemyTeam( GetTeamNumber() ) )
+ {
+ // we want to move our team's flag or a neutral flag
+ flagsVector.AddToTail( flag );
+ }
+ break;
+ }
+
+ if ( flag->IsStolen() )
+ {
+ nCarriedFlags++;
+ }
+ }
+
+ CCaptureFlag *pClosestFlag = NULL;
+ float flClosestFlagDist = FLT_MAX;
+ CCaptureFlag *pClosestUncarriedFlag = NULL;
+ float flClosestUncarriedFlagDist = FLT_MAX;
+
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ int nMinFollower = INT_MAX;
+
+ FOR_EACH_VEC( flagsVector, i )
+ {
+ CCaptureFlag *pFlag = flagsVector[i];
+ if ( pFlag )
+ {
+ // find the one which needs the most love
+ if ( pFlag->GetNumFollowers() < nMinFollower )
+ {
+ nMinFollower = pFlag->GetNumFollowers();
+
+ pClosestFlag = NULL;
+ flClosestFlagDist = FLT_MAX;
+ pClosestUncarriedFlag = NULL;
+ flClosestUncarriedFlagDist = FLT_MAX;
+ }
+
+ if ( pFlag->GetNumFollowers() == nMinFollower )
+ {
+ // Find the closest
+ float flDist = ( pFlag->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( flDist < flClosestFlagDist )
+ {
+ pClosestFlag = pFlag;
+ flClosestFlagDist = flDist;
+ }
+
+ // Find the closest uncarried
+ if ( nCarriedFlags < flagsVector.Count() && !pFlag->IsStolen() )
+ {
+ if ( flDist < flClosestUncarriedFlagDist )
+ {
+ pClosestUncarriedFlag = flagsVector[i];
+ flClosestUncarriedFlagDist = flDist;
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ FOR_EACH_VEC( flagsVector, i )
+ {
+ if ( flagsVector[i] )
+ {
+ // Find the closest
+ float flDist = ( flagsVector[i]->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( flDist < flClosestFlagDist )
+ {
+ pClosestFlag = flagsVector[i];
+ flClosestFlagDist = flDist;
+ }
+
+ // Find the closest uncarried
+ if ( nCarriedFlags < flagsVector.Count() && !flagsVector[i]->IsStolen() )
+ {
+ if ( flDist < flClosestUncarriedFlagDist )
+ {
+ pClosestUncarriedFlag = flagsVector[i];
+ flClosestUncarriedFlagDist = flDist;
+ }
+ }
+ }
+ }
+ }
+
+ // If we have an uncarried flag, prioritize
+ if ( pClosestUncarriedFlag )
+ return pClosestUncarriedFlag;
+
+ return pClosestFlag;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return capture zone for our flag(s)
+CCaptureZone *CTFBot::GetFlagCaptureZone( void ) const
+{
+ for( int i=0; i<ICaptureZoneAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureZone *zone = static_cast< CCaptureZone* >( ICaptureZoneAutoList::AutoList()[i] );
+ if ( zone->GetTeamNumber() == GetTeamNumber() )
+ {
+ return zone;
+ }
+ }
+
+ return NULL;
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ClearMyControlPoint( void )
+{
+ m_myControlPoint = NULL;
+ m_evaluateControlPointTimer.Invalidate();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return true if no enemy has contested any point yet
+ */
+bool CTFBot::AreAllPointsUncontestedSoFar( void ) const
+{
+ CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( master )
+ {
+ for( int i=0; i<master->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = master->GetControlPoint( i );
+
+ if ( point && point->HasBeenContested() )
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if the given point is being captured
+bool CTFBot::IsPointBeingCaptured( CTeamControlPoint *point ) const
+{
+ 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;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if any point is being captured
+bool CTFBot::IsAnyPointBeingCaptured( void ) const
+{
+ CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( master )
+ {
+ for( int i=0; i<master->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = master->GetControlPoint( i );
+
+ if ( IsPointBeingCaptured( point ) )
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return true if we are within a short travel distance of the current point
+bool CTFBot::IsNearPoint( CTeamControlPoint *point ) const
+{
+ CTFNavArea *myArea = GetLastKnownArea();
+
+ if ( !myArea || !point )
+ {
+ return false;
+ }
+
+ CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );
+
+ if ( !pointArea )
+ {
+ return false;
+ }
+
+ float travelToPoint = fabs( myArea->GetIncursionDistance( GetTeamNumber() ) - pointArea->GetIncursionDistance( GetTeamNumber() ) );
+
+ return travelToPoint < tf_bot_near_point_travel_distance.GetFloat();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Return time left to capture the point before we lose the game
+float CTFBot::GetTimeLeftToCapture( void ) const
+{
+ if ( TFGameRules()->IsInKothMode() )
+ {
+ if ( TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) ) )
+ {
+ return TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) )->GetTimeRemaining();
+ }
+ }
+ else if ( TFGameRules()->GetActiveRoundTimer() )
+ {
+ return TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining();
+ }
+
+ return 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Do internal setup when control point changes
+void CTFBot::SetupSniperSpotAccumulation( void )
+{
+ VPROF_BUDGET( "CTFBot::SetupSniperSpotAccumulation", "NextBot" );
+
+ CBaseEntity *goalEntity = NULL;
+
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ // try to find a payload cart to guard
+ CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToPush( GetTeamNumber() );
+
+ if ( !trainWatcher )
+ {
+ trainWatcher = TFGameRules()->GetPayloadToBlock( GetTeamNumber() );
+ }
+
+ if ( trainWatcher )
+ {
+ goalEntity = trainWatcher->GetTrainEntity();
+ }
+ }
+ else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
+ {
+ goalEntity = GetMyControlPoint();
+ }
+
+ if ( !goalEntity )
+ {
+ ClearSniperSpots();
+ return;
+ }
+
+ if ( goalEntity == m_snipingGoalEntity )
+ {
+ // if goal has moved too much (ie: payload cart), recompute our spots
+ Vector toGoal = m_snipingGoalEntity->WorldSpaceCenter() - m_lastSnipingGoalEntityPosition;
+
+ if ( toGoal.IsLengthLessThan( tf_bot_sniper_goal_entity_move_tolerance.GetFloat() ) )
+ {
+ // already set up
+ return;
+ }
+ }
+
+ ClearSniperSpots();
+
+ int myTeam = GetTeamNumber();
+ int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE;
+
+ bool isDefendingPoint = false;
+ CTFNavArea *goalEntityArea = NULL;
+
+ if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ // the cart is owned by the invaders
+ isDefendingPoint = ( goalEntity->GetTeamNumber() != myTeam );
+ goalEntityArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( goalEntity->WorldSpaceCenter(), GETNAVAREA_CHECK_GROUND, 500.0f );
+ }
+ else
+ {
+ isDefendingPoint = ( GetMyControlPoint()->GetOwner() == myTeam );
+ goalEntityArea = TheTFNavMesh()->GetControlPointCenterArea( GetMyControlPoint()->GetPointIndex() );
+ }
+
+ // we are sniping a different control point - setup for new point accumulation
+ m_sniperVantageAreaVector.RemoveAll();
+ m_sniperTheaterAreaVector.RemoveAll();
+
+ if ( !goalEntityArea )
+ {
+ return;
+ }
+
+ for( int i=0; i<TheNavAreas.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheNavAreas[i];
+
+ if ( !area->IsReachableByTeam( myTeam ) || !area->IsReachableByTeam( enemyTeam ) )
+ {
+ continue;
+ }
+
+ if ( area->GetIncursionDistance( enemyTeam ) <= goalEntityArea->GetIncursionDistance( enemyTeam ) )
+ {
+ m_sniperTheaterAreaVector.AddToTail( area );
+ }
+
+ // if this is my point, I can stand on it, or go a bit beyond it
+ float myIncursionTolerance = tf_bot_sniper_spot_point_tolerance.GetFloat();
+
+ if ( !isDefendingPoint )
+ {
+ // not my point, keep back from it a bit
+ myIncursionTolerance *= -1.0f;
+ }
+
+ if ( area->GetIncursionDistance( myTeam ) <= goalEntityArea->GetIncursionDistance( myTeam ) + myIncursionTolerance )
+ {
+ m_sniperVantageAreaVector.AddToTail( area );
+ }
+ }
+
+ m_snipingGoalEntity = goalEntity;
+ m_lastSnipingGoalEntityPosition = goalEntity->WorldSpaceCenter();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Randomly sample points within candidate areas to find good sniping positions
+void CTFBot::AccumulateSniperSpots( void )
+{
+ VPROF_BUDGET( "CTFBot::AccumulateSniperSpots", "NextBot" );
+
+ SetupSniperSpotAccumulation();
+
+ if ( m_sniperVantageAreaVector.Count() == 0 || m_sniperTheaterAreaVector.Count() == 0 )
+ {
+ // retry every so often to catch cases where the incursion data is invalid during setup time
+ // due to blocked/closed off areas, etc.
+ if ( m_retrySniperSpotSetupTimer.IsElapsed() )
+ {
+ // retry
+ ClearSniperSpots();
+ }
+
+ return;
+ }
+
+ SniperSpotInfo info;
+
+ for( int count=0; count<tf_bot_sniper_spot_search_count.GetInt(); ++count )
+ {
+ // pick a random vantage area to sample
+ int which = RandomInt( 0, m_sniperVantageAreaVector.Count()-1 );
+ info.m_vantageArea = m_sniperVantageAreaVector[ which ];
+ info.m_vantageSpot = info.m_vantageArea->GetRandomPoint();
+
+ // pick a random theater area to sample
+ which = RandomInt( 0, m_sniperTheaterAreaVector.Count()-1 );
+ info.m_theaterArea = m_sniperTheaterAreaVector[ which ];
+ info.m_theaterSpot = info.m_theaterArea->GetRandomPoint();
+
+ info.m_range = ( info.m_vantageSpot - info.m_theaterSpot ).Length();
+ if ( info.m_range < tf_bot_sniper_spot_min_range.GetFloat() )
+ {
+ // not long enough sightline
+ continue;
+ }
+
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ if ( ( info.m_vantageSpot - m_sniperSpotVector[i].m_vantageSpot ).IsLengthLessThan( tf_bot_sniper_spot_epsilon.GetFloat() ) )
+ {
+ // too close to existing spot
+ continue;
+ }
+ }
+
+ Vector eyeOffset( 0, 0, 60.0f );
+ if ( IsLineOfFireClear( info.m_vantageSpot + eyeOffset, info.m_theaterSpot + eyeOffset ) )
+ {
+ // valid spot
+
+ // maximize the time it takes the enemy to get to us
+ info.m_advantage = info.m_vantageArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) ) - info.m_theaterArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) );
+
+ // if we have already maxxed out our sniper spots, replace the worst one if this is better
+ if ( m_sniperSpotVector.Count() >= tf_bot_sniper_spot_max_count.GetInt() )
+ {
+ int worst = -1;
+
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ if ( worst < 0 || m_sniperSpotVector[i].m_advantage < m_sniperSpotVector[ worst ].m_advantage )
+ {
+ worst = i;
+ }
+ }
+
+ // if our new spot is better, replace it
+ if ( info.m_advantage > m_sniperSpotVector[ worst ].m_advantage )
+ {
+ m_sniperSpotVector[ worst ] = info;
+ }
+ }
+ else
+ {
+ m_sniperSpotVector.AddToTail( info );
+ }
+ }
+ }
+
+ if ( IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ for( int i=0; i<m_sniperSpotVector.Count(); ++i )
+ {
+ NDebugOverlay::Cross3D( m_sniperSpotVector[i].m_vantageSpot, 5.0f, 255, 0, 255, true, 0.1f );
+ NDebugOverlay::Line( m_sniperSpotVector[i].m_vantageSpot, m_sniperSpotVector[i].m_theaterSpot, 0, 200, 0, true, 0.1f );
+ }
+ }
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+void CTFBot::ClearSniperSpots( void )
+{
+ m_sniperSpotVector.RemoveAll();
+ m_sniperVantageAreaVector.RemoveAll();
+ m_sniperTheaterAreaVector.RemoveAll();
+ m_snipingGoalEntity = NULL;
+ m_retrySniperSpotSetupTimer.Start( RandomFloat( 5.0f, 10.0f ) );
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+class CCollectReachableObjects : public ISearchSurroundingAreasFunctor
+{
+public:
+ CCollectReachableObjects( const CTFBot *me, float maxRange, const CUtlVector< CHandle< CBaseEntity > > &potentialVector, CUtlVector< CHandle< CBaseEntity > > *collectionVector ) : m_potentialVector( potentialVector )
+ {
+ m_me = me;
+ m_maxRange = maxRange;
+ m_collectionVector = collectionVector;
+ }
+
+ virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ // do any of the potential objects overlap this area?
+ FOR_EACH_VEC( m_potentialVector, it )
+ {
+ CBaseEntity *obj = m_potentialVector[ it ];
+
+ if ( obj && area->Contains( obj->WorldSpaceCenter() ) )
+ {
+ // reachable - keep it
+ if ( !m_collectionVector->HasElement( obj ) )
+ {
+ m_collectionVector->AddToTail( obj );
+ }
+ }
+ }
+ return true;
+ }
+
+ virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar )
+ {
+ if ( adjArea->IsBlocked( m_me->GetTeamNumber() ) )
+ {
+ return false;
+ }
+
+ if ( travelDistanceSoFar > m_maxRange )
+ {
+ // too far away
+ return false;
+ }
+
+ return currentArea->IsContiguous( adjArea );
+ }
+
+ const CTFBot *m_me;
+ float m_maxRange;
+ const CUtlVector< CHandle< CBaseEntity > > &m_potentialVector;
+ CUtlVector< CHandle< CBaseEntity > > *m_collectionVector;
+};
+
+
+//
+// Search outwards from startSearchArea and collect all reachable objects from the given list that pass the given filter
+// Items in selectedObjectVector will be approximately sorted in nearest-to-farthest order (because of SearchSurroundingAreas)
+//
+void CTFBot::SelectReachableObjects( const CUtlVector< CHandle< CBaseEntity > > &candidateObjectVector,
+ CUtlVector< CHandle< CBaseEntity > > *selectedObjectVector,
+ const INextBotFilter &filter,
+ CNavArea *startSearchArea,
+ float maxRange ) const
+{
+ if ( startSearchArea == NULL || selectedObjectVector == NULL )
+ return;
+
+ selectedObjectVector->RemoveAll();
+
+ // filter candidate objects
+ CUtlVector< CHandle< CBaseEntity > > filteredObjectVector;
+ for( int i=0; i<candidateObjectVector.Count(); ++i )
+ {
+ if ( filter.IsSelected( candidateObjectVector[i] ) )
+ {
+ filteredObjectVector.AddToTail( candidateObjectVector[i] );
+ }
+ }
+
+ // only keep those that are reachable by us
+ CCollectReachableObjects collector( this, maxRange, filteredObjectVector, selectedObjectVector );
+ SearchSurroundingAreas( startSearchArea, collector );
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAmmoLow( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ if ( myWeapon->GetWeaponID() == TF_WEAPON_WRENCH )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) <= 0 );
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ // we never run out of ammo with a melee weapon
+ return false;
+ }
+
+ // no projectile, no ammo needed
+ const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() );
+ if ( weaponAlias )
+ {
+ WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
+ if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() )
+ {
+ CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );
+ if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE )
+ {
+ // we don't shoot anything, so we don't need ammo
+ return false;
+ }
+ }
+ }
+
+ float ratio = (float)GetAmmoCount( TF_AMMO_PRIMARY ) / (float)( const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY ) );
+
+ if ( ratio < 0.2f )
+ {
+ return true;
+ }
+ //if ( !myWeapon->HasPrimaryAmmo() && myWeapon->GetWeaponID() != TF_WEAPON_BUILDER && myWeapon->GetWeaponID() != TF_WEAPON_MEDIGUN )
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsAmmoFull( void ) const
+{
+ bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY );
+ bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY );
+
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 ) && isPrimaryFull && isSecondaryFull;
+ }
+
+ return isPrimaryFull && isSecondaryFull;
+
+/*
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( myWeapon )
+ {
+ if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // wrench is special. it's a melee weapon that wants ammo - metal
+ return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 );
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ // we never run out of ammo with a melee weapon
+ return true;
+ }
+
+ // no projectile, no ammo needed
+ const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() );
+ if ( weaponAlias )
+ {
+ WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
+ if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() )
+ {
+ CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );
+ if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE )
+ {
+ // we don't shoot anything, so we don't need ammo
+ return true;
+ }
+ }
+ }
+
+ bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY );
+ bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY );
+
+ return isPrimaryFull && isSecondaryFull;
+ }
+
+ return false;
+*/
+}
+
+
+bool CTFBot::IsDormantWhenDead( void ) const
+{
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * When someone fires their weapon
+ */
+void CTFBot::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon )
+{
+ VPROF_BUDGET( "CTFBot::OnWeaponFired", "NextBot" );
+
+ BaseClass::OnWeaponFired( whoFired, weapon );
+
+ if ( !whoFired || !whoFired->IsAlive() )
+ return;
+
+ if ( IsRangeGreaterThan( whoFired, tf_bot_notice_gunfire_range.GetFloat() ) )
+ return;
+
+ int noticeChance = 100;
+
+ if ( IsQuietWeapon( (CTFWeaponBase *)weapon ) )
+ {
+ if ( IsRangeGreaterThan( whoFired, tf_bot_notice_quiet_gunfire_range.GetFloat() ) )
+ {
+ // too far away to hear in any event
+ return;
+ }
+
+ switch( GetDifficulty() )
+ {
+ case EASY:
+ noticeChance = 10;
+ break;
+
+ case NORMAL:
+ noticeChance = 30;
+ break;
+
+ case HARD:
+ noticeChance = 60;
+ break;
+
+ default:
+ case EXPERT:
+ noticeChance = 90;
+ break;
+ }
+
+ if ( IsEnvironmentNoisy() )
+ {
+ // less likely to notice with all the noise
+ noticeChance /= 2;
+ }
+ }
+ else if ( IsRangeLessThan( whoFired, 1000.0f ) )
+ {
+ // loud gunfire in our area - it's now "noisy" for a bit
+ m_noisyTimer.Start( 3.0f );
+ }
+
+ if ( RandomInt( 1, 100 ) > noticeChance )
+ {
+ return;
+ }
+
+ // notice the gunfire
+ GetVisionInterface()->AddKnownEntity( whoFired );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we match the given debug symbol
+bool CTFBot::IsDebugFilterMatch( const char *name ) const
+{
+ // player classname
+ if ( !Q_strnicmp( name, const_cast< CTFBot * >( this )->GetPlayerClass()->GetName(), Q_strlen( name ) ) )
+ {
+ return true;
+ }
+
+ return BaseClass::IsDebugFilterMatch( name );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+class CFindClosestPotentiallyVisibleAreaToPos
+{
+public:
+ CFindClosestPotentiallyVisibleAreaToPos( const Vector &pos )
+ {
+ m_pos = pos;
+ m_closeArea = NULL;
+ m_closeRangeSq = FLT_MAX;
+ }
+
+ bool operator() ( CNavArea *baseArea )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ Vector close;
+ area->GetClosestPointOnArea( m_pos, &close );
+
+ float rangeSq = ( close - m_pos ).LengthSqr();
+ if ( rangeSq < m_closeRangeSq )
+ {
+ m_closeArea = area;
+ m_closeRangeSq = rangeSq;
+ }
+
+ return true;
+ }
+
+ Vector m_pos;
+ CTFNavArea *m_closeArea;
+ float m_closeRangeSq;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+// Update our view to watch where members of the given team will be coming from
+void CTFBot::UpdateLookingAroundForIncomingPlayers( bool lookForEnemies )
+{
+ if ( !m_lookAtEnemyInvasionAreasTimer.IsElapsed() )
+ return;
+
+ const float maxLookInterval = 1.0f;
+ m_lookAtEnemyInvasionAreasTimer.Start( RandomFloat( 0.333f, maxLookInterval ) );
+
+ float minGazeRange = m_Shared.InCond( TF_COND_ZOOMED ) ? 750.0f : 150.0f;
+
+ CTFNavArea *myArea = GetLastKnownArea();
+ if ( myArea )
+ {
+ int team = GetTeamNumber();
+
+ // if we want to look where teammates come from, we need to pass in
+ // the *enemy* team, since the method collects *enemy* invasion areas
+ if ( !lookForEnemies )
+ {
+ team = GetEnemyTeam( team );
+ }
+
+ const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( team );
+
+ if ( invasionAreaVector.Count() > 0 )
+ {
+ // try to not look directly at walls
+ const int retryCount = 20.0f;
+ for( int r=0; r<retryCount; ++r )
+ {
+ int which = RandomInt( 0, invasionAreaVector.Count()-1 );
+ Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight );
+
+ if ( IsRangeGreaterThan( gazeSpot, minGazeRange ) && GetVisionInterface()->IsLineOfSightClear( gazeSpot ) )
+ {
+ // use maxLookInterval so these looks override body aiming from path following
+ GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::INTERESTING, maxLookInterval, NULL, "Looking toward enemy invasion areas" );
+ break;
+ }
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Update our view to keep an eye on areas where the enemy will be coming from
+ */
+void CTFBot::UpdateLookingAroundForEnemies( void )
+{
+ if ( !m_isLookingAroundForEnemies )
+ return;
+
+ if ( HasAttribute( CTFBot::IGNORE_ENEMIES ) )
+ return;
+
+ if ( m_Shared.IsControlStunned() )
+ return;
+
+ const float maxLookInterval = 1.0f;
+
+ const CKnownEntity *known = GetVisionInterface()->GetPrimaryKnownThreat();
+
+ if ( known )
+ {
+ if ( known->IsVisibleInFOVNow() )
+ {
+ if ( IsPlayerClass( TF_CLASS_SPY ) &&
+ GetDifficulty() >= CTFBot::HARD &&
+ m_Shared.InCond( TF_COND_DISGUISED ) &&
+ !m_Shared.IsStealthed() )
+ {
+ // smart Spies don't look at their victims until it's too late...
+ // look around at where *teammates* will be coming from to fool the enemy
+ UpdateLookingAroundForIncomingPlayers( LOOK_FOR_FRIENDS );
+ return;
+ }
+
+ // I see you!
+ GetBodyInterface()->AimHeadTowards( known->GetEntity(), IBody::CRITICAL, 1.0f, NULL, "Aiming at a visible threat" );
+ return;
+ }
+
+/* apparently sounds update last known position...
+ if ( known->WasEverVisible() && known->GetTimeSinceLastSeen() < 3.0f )
+ {
+ // I saw you just a moment ago...
+ GetBodyInterface()->AimHeadTowards( known->GetLastKnownPosition() + GetClassEyeHeight(), IBody::IMPORTANT, 1.0f, NULL, "Aiming at a last known threat position" );
+ return;
+ }
+*/
+
+ // known but not currently visible (I know you're around here somewhere)
+
+ // if there is unobstructed space between us, turn around
+ if ( IsLineOfSightClear( known->GetEntity(), IGNORE_ACTORS ) )
+ {
+ Vector toThreat = known->GetEntity()->GetAbsOrigin() - GetAbsOrigin();
+ float threatRange = toThreat.NormalizeInPlace();
+
+ float aimError = M_PI/6.0f;
+
+ float s, c;
+ FastSinCos( aimError, &s, &c );
+
+ float error = threatRange * s;
+ Vector imperfectAimSpot = known->GetEntity()->WorldSpaceCenter();
+ imperfectAimSpot.x += RandomFloat( -error, error );
+ imperfectAimSpot.y += RandomFloat( -error, error );
+
+ GetBodyInterface()->AimHeadTowards( imperfectAimSpot, IBody::IMPORTANT, 1.0f, NULL, "Turning around to find threat out of our FOV" );
+ return;
+ }
+
+ if ( !IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ // look toward potentially visible area nearest the last known position
+ CTFNavArea *myArea = GetLastKnownArea();
+ if ( myArea )
+ {
+ const CTFNavArea *closeArea = NULL;
+ CFindClosestPotentiallyVisibleAreaToPos find( known->GetLastKnownPosition() );
+ myArea->ForAllPotentiallyVisibleAreas( find );
+
+ closeArea = find.m_closeArea;
+
+ if ( closeArea )
+ {
+ // try to not look directly at walls
+ const int retryCount = 10.0f;
+ for( int r=0; r<retryCount; ++r )
+ {
+ Vector gazeSpot = closeArea->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight );
+
+ if ( GetVisionInterface()->IsLineOfSightClear( gazeSpot ) )
+ {
+ // use maxLookInterval so these looks override body aiming from path following
+ GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::IMPORTANT, maxLookInterval, NULL, "Looking toward potentially visible area near known but hidden threat" );
+ return;
+ }
+ }
+
+ // can't find a clear line to look along
+ if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) )
+ {
+ ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s can't find clear line to look at potentially visible near known but hidden entity %s(#%d)\n",
+ gpGlobals->curtime,
+ GetDebugIdentifier(),
+ known->GetEntity()->GetClassname(),
+ known->GetEntity()->entindex() );
+ }
+ }
+ else if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) )
+ {
+ ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s no potentially visible area to look toward known but hidden entity %s(#%d)\n",
+ gpGlobals->curtime,
+ GetDebugIdentifier(),
+ known->GetEntity()->GetClassname(),
+ known->GetEntity()->entindex() );
+ }
+ }
+
+ return;
+ }
+ }
+
+ // no known threat - look toward where enemies will come from
+ UpdateLookingAroundForIncomingPlayers( LOOK_FOR_ENEMIES );
+}
+
+
+//---------------------------------------------------------------------------------------------
+class CFindVantagePoint : public ISearchSurroundingAreasFunctor
+{
+public:
+ CFindVantagePoint( int enemyTeamIndex )
+ {
+ m_enemyTeamIndex = enemyTeamIndex;
+ m_vantageArea = NULL;
+ }
+
+ virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
+ {
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ CTeam *enemyTeam = GetGlobalTeam( m_enemyTeamIndex );
+ for( int i=0; i<enemyTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *enemy = (CTFPlayer *)enemyTeam->GetPlayer(i);
+
+ if ( !enemy->IsAlive() || !enemy->GetLastKnownArea() )
+ continue;
+
+ CTFNavArea *enemyArea = (CTFNavArea *)enemy->GetLastKnownArea();
+ if ( enemyArea->IsCompletelyVisible( area ) )
+ {
+ // nearby area from which we can see the enemy team
+ m_vantageArea = area;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ int m_enemyTeamIndex;
+ CTFNavArea *m_vantageArea;
+};
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return a nearby area where we can see a member of the enemy team
+CTFNavArea *CTFBot::FindVantagePoint( float maxTravelDistance ) const
+{
+ CFindVantagePoint find( GetTeamNumber() == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE );
+ SearchSurroundingAreas( GetLastKnownArea(), find, maxTravelDistance );
+ return find.m_vantageArea;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return perceived danger of threat (0=none, 1=immediate deadly danger)
+ * @todo: Move this to contextual query
+ * @todo: Differentiate between potential threats (that sentry up ahead along our route) and immediate threats (the sentry I'm in range of)
+ */
+float CTFBot::GetThreatDanger( CBaseCombatCharacter *who ) const
+{
+ if ( who == NULL )
+ return 0.0f;
+
+ if ( IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ if ( IsRangeGreaterThan( who, tf_bot_sniper_personal_space_range.GetFloat() ) )
+ {
+ // far away enemies are no threat to a Sniper
+ return 0.0f;
+ }
+ }
+
+ if ( who->IsPlayer() )
+ {
+ CTFPlayer *player = ToTFPlayer( who );
+
+ // ubers are scary
+ if ( player->m_Shared.IsInvulnerable() )
+ return 1.0f;
+
+ switch( player->GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_MEDIC:
+ return 0.2f; // 1/5
+
+ case TF_CLASS_ENGINEER:
+ case TF_CLASS_SNIPER:
+ return 0.4f; // 2/5
+
+ case TF_CLASS_SCOUT:
+ case TF_CLASS_SPY:
+ case TF_CLASS_DEMOMAN:
+ return 0.6f; // 3/5
+
+ case TF_CLASS_SOLDIER:
+ case TF_CLASS_HEAVYWEAPONS:
+ return 0.8f; // 4/5
+
+ case TF_CLASS_PYRO:
+ return 1.0f; // 5/5
+ }
+
+ }
+ else
+ {
+ // sentry gun
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( who );
+ if ( sentry )
+ {
+ if ( !sentry->IsAlive() || sentry->IsPlacing() || sentry->HasSapper() || sentry->IsPlasmaDisabled() || sentry->IsUpgrading() || sentry->IsBuilding() )
+ return 0.0f;
+
+ switch( sentry->GetUpgradeLevel() )
+ {
+ case 3: return 1.0f;
+ case 2: return 0.8f;
+ default: return 0.6f;
+ }
+ }
+ }
+
+ return 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the max range at which we can effectively attack
+ */
+float CTFBot::GetMaxAttackRange( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( !myWeapon )
+ return 0.0f;
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ return 100.0f;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
+ {
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ const float flameRange = 350.0f;
+
+ static CSchemaItemDefHandle pItemDef_GiantFlamethrower( "MVM Giant Flamethrower" );
+
+ if ( IsActiveTFWeapon( pItemDef_GiantFlamethrower ) )
+ {
+ return 2.5f * flameRange;
+ }
+
+ return flameRange;
+ }
+
+ return 250.0f;
+ }
+
+ if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ // infinite
+ return FLT_MAX;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) )
+ {
+ return 3000.0f;
+ }
+
+ // bullet spray weapons, grenades, etc
+ // for now, default to infinite so bot always returns fire and doesn't look dumb
+ return FLT_MAX;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return the ideal range at which we can effectively attack
+ */
+float CTFBot::GetDesiredAttackRange( void ) const
+{
+ CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon();
+ if ( !myWeapon )
+ return 0.0f;
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_KNIFE ) )
+ {
+ // get very close and stab
+ return 70.0f; // 60
+ }
+
+ if ( myWeapon->IsMeleeWeapon() )
+ {
+ return 100.0f;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
+ {
+ return 100.0f;
+ }
+
+ if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
+ {
+ // infinite
+ return FLT_MAX;
+ }
+
+ if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) && !TFGameRules()->IsMannVsMachineMode() )
+ {
+ return 1250.0f;
+ }
+
+ // bullet spray weapons, grenades, etc
+ return 500.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// If we're required to equip a specific weapon, do it.
+bool CTFBot::EquipRequiredWeapon( void )
+{
+ // if we have a required weapon on our stack, it takes precedence (items, etc)
+ if ( m_requiredWeaponStack.Count() )
+ {
+ CBaseCombatWeapon *pWeapon = m_requiredWeaponStack.Top().Get();
+ return Weapon_Switch( pWeapon );
+ }
+
+ if ( TheTFBots().IsMeleeOnly() || TFGameRules()->IsInMedievalMode() || HasWeaponRestriction( MELEE_ONLY ) )
+ {
+ // force use of melee weapons
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) );
+ return true;
+ }
+
+ if ( HasWeaponRestriction( PRIMARY_ONLY ) )
+ {
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) );
+ return true;
+ }
+
+ if ( HasWeaponRestriction( SECONDARY_ONLY ) )
+ {
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return true;
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Equip the best weapon we have to attack the given threat
+void CTFBot::EquipBestWeaponForThreat( const CKnownEntity *threat )
+{
+ if ( EquipRequiredWeapon() )
+ return;
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( HasAttribute( CTFBot::AGGRESSIVE ) )
+ {
+ // mobs never equip other weapons
+ return;
+ }
+
+ if ( GetPlayerClass()->GetClassIndex() == TF_CLASS_DEMOMAN && !IsInASquad() )
+ {
+ // wandering demomen use stickies only
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return;
+ }
+ }
+#endif // TF_RAID_MODE
+
+ CTFWeaponBase *primary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) );
+ if ( !IsCombatWeapon( primary ) )
+ {
+ primary = NULL;
+ }
+
+ CTFWeaponBase *secondary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ if ( !IsCombatWeapon( secondary ) )
+ {
+ secondary = NULL;
+ }
+
+ // no secondary weapons in MvM
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( IsPlayerClass( TF_CLASS_MEDIC ) && IsInASquad() && GetSquad() && !GetSquad()->IsLeader( this ) )
+ {
+ // always try to heal leader
+ Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) );
+ return;
+ }
+
+ secondary = NULL;
+ }
+
+ CTFWeaponBase *melee = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) );
+ if ( !IsCombatWeapon( melee ) )
+ {
+ melee = NULL;
+ }
+
+ CTFWeaponBase *gun = NULL;
+ if ( primary )
+ {
+ gun = primary;
+ }
+ else if ( secondary )
+ {
+ gun = secondary;
+ }
+ else
+ {
+ gun = melee;
+ }
+
+ if ( IsDifficulty( CTFBot::EASY ) )
+ {
+ // easy bots always use their primary weapon if they have one
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+
+ return;
+ }
+
+ if ( !threat || !threat->WasEverVisible() || threat->GetTimeSinceLastSeen() > 5.0f )
+ {
+ // no threat - go back to primary weapon so it has a chance to reload
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+
+ return;
+ }
+
+ // now filter weapons by available ammo
+ if ( GetAmmoCount( TF_AMMO_PRIMARY ) <= 0 )
+ {
+ primary = NULL;
+ }
+
+ if ( GetAmmoCount( TF_WPN_TYPE_SECONDARY ) <= 0 )
+ {
+ secondary = NULL;
+ }
+
+ // modify our gun choice based on threat situation (range, etc)
+ switch( GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_DEMOMAN:
+ case TF_CLASS_HEAVYWEAPONS:
+ case TF_CLASS_SPY:
+ case TF_CLASS_MEDIC:
+ case TF_CLASS_ENGINEER:
+ // primary
+ break;
+
+ case TF_CLASS_SCOUT:
+ {
+ if ( secondary )
+ {
+ if ( gun && !gun->Clip1() )
+ {
+ gun = secondary;
+ }
+ }
+ }
+ break;
+
+ case TF_CLASS_SOLDIER:
+ {
+ // if we've emptied our rocket launcher clip and are fighting a nearby threat, switch to our secondary if it is ready to fire
+ if ( gun && !gun->Clip1() )
+ {
+ if ( secondary && secondary->Clip1() )
+ {
+ const float closeSoldierRange = 500.0f;
+ if ( IsRangeLessThan( threat->GetLastKnownPosition(), closeSoldierRange ) )
+ {
+ gun = secondary;
+ }
+ }
+ }
+ }
+ break;
+
+ case TF_CLASS_SNIPER:
+ {
+ const float closeSniperRange = 750.0f;
+ if ( secondary && IsRangeLessThan( threat->GetLastKnownPosition(), closeSniperRange ) )
+ gun = secondary;
+ }
+ break;
+
+ case TF_CLASS_PYRO:
+ {
+ const float flameRange = 750.0f;
+ if ( secondary && IsRangeGreaterThan( threat->GetLastKnownPosition(), flameRange ) )
+ {
+ gun = secondary;
+ }
+
+ // keep flamethrower out to reflect projectiles
+ if ( threat->GetEntity() && threat->GetEntity()->IsPlayer() )
+ {
+ CTFPlayer *enemy = ToTFPlayer( threat->GetEntity() );
+
+ if ( enemy->IsPlayerClass( TF_CLASS_SOLDIER ) || enemy->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ gun = primary;
+ }
+ }
+ }
+ break;
+ }
+
+ if ( gun )
+ {
+ Weapon_Switch( gun );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// NOTE: This assumes default weapon loadouts
+bool CTFBot::EquipLongRangeWeapon( void )
+{
+ // no secondary weapons in MvM
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ return false;
+
+ if ( IsPlayerClass( TF_CLASS_SOLDIER ) ||
+ IsPlayerClass( TF_CLASS_DEMOMAN ) ||
+ IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ||
+ IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ CBaseCombatWeapon *primary = Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ if ( primary )
+ {
+ if ( GetAmmoCount( TF_AMMO_PRIMARY ) > 0 )
+ {
+ Weapon_Switch( primary );
+ return true;
+ }
+ }
+ }
+
+ // fall back to our secondary (or go right to it if its the only thing we have that has reach)
+ CBaseCombatWeapon *secondary = Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
+ if ( secondary )
+ {
+ if ( GetAmmoCount( TF_AMMO_SECONDARY ) > 0 )
+ {
+ Weapon_Switch( secondary );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Force us to equip and use this weapon until popped off the required stack
+void CTFBot::PushRequiredWeapon( CTFWeaponBase *weapon )
+{
+ m_requiredWeaponStack.Push( weapon );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Pop top required weapon off of stack and discard
+void CTFBot::PopRequiredWeapon( void )
+{
+ m_requiredWeaponStack.Pop();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon can be used to attack
+bool CTFBot::IsCombatWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_MEDIGUN:
+ case TF_WEAPON_PDA:
+ case TF_WEAPON_PDA_ENGINEER_BUILD:
+ case TF_WEAPON_PDA_ENGINEER_DESTROY:
+ case TF_WEAPON_PDA_SPY:
+ case TF_WEAPON_BUILDER:
+ case TF_WEAPON_DISPENSER:
+ case TF_WEAPON_INVIS:
+ case TF_WEAPON_LUNCHBOX:
+ case TF_WEAPON_BUFF_ITEM:
+ case TF_WEAPON_PUMPKIN_BOMB:
+ return false;
+ };
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon is a "hitscan" weapon
+bool CTFBot::IsHitScanWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_SHOTGUN_PRIMARY:
+ case TF_WEAPON_SHOTGUN_SOLDIER:
+ case TF_WEAPON_SHOTGUN_HWG:
+ case TF_WEAPON_SHOTGUN_PYRO:
+ case TF_WEAPON_SCATTERGUN:
+ case TF_WEAPON_SNIPERRIFLE:
+ case TF_WEAPON_MINIGUN:
+ case TF_WEAPON_SMG:
+ case TF_WEAPON_CHARGED_SMG:
+ case TF_WEAPON_PISTOL:
+ case TF_WEAPON_PISTOL_SCOUT:
+ case TF_WEAPON_REVOLVER:
+ case TF_WEAPON_SENTRY_BULLET:
+ case TF_WEAPON_SENTRY_ROCKET:
+ case TF_WEAPON_SENTRY_REVENGE:
+ case TF_WEAPON_HANDGUN_SCOUT_PRIMARY:
+ case TF_WEAPON_HANDGUN_SCOUT_SECONDARY:
+ case TF_WEAPON_SODA_POPPER:
+ case TF_WEAPON_SNIPERRIFLE_DECAP:
+ case TF_WEAPON_PEP_BRAWLER_BLASTER:
+ case TF_WEAPON_SNIPERRIFLE_CLASSIC:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon "sprays" bullets/fire/etc continuously (ie: not individual rockets/etc)
+bool CTFBot::IsContinuousFireWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( !IsCombatWeapon( weapon ) )
+ return false;
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_PISTOL:
+ case TF_WEAPON_PISTOL_SCOUT:
+ case TF_WEAPON_FLAREGUN:
+ case TF_WEAPON_JAR:
+ case TF_WEAPON_COMPOUND_BOW:
+ return false;
+ };
+ }
+
+ return true;
+
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon launches explosive projectiles with splash damage
+bool CTFBot::IsExplosiveProjectileWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_JAR:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// return true if given weapon has small clip and long reload cost (ie: rocket launcher, etc)
+bool CTFBot::IsBarrageAndReloadWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_ROCKETLAUNCHER:
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
+ case TF_WEAPON_GRENADELAUNCHER:
+ case TF_WEAPON_PIPEBOMBLAUNCHER:
+ case TF_WEAPON_SCATTERGUN:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if given weapon doesn't make much sound when used (ie: spy knife, etc)
+bool CTFBot::IsQuietWeapon( CTFWeaponBase *weapon ) const
+{
+ if ( weapon == MY_CURRENT_GUN )
+ {
+ weapon = m_Shared.GetActiveTFWeapon();
+ }
+
+ if ( weapon )
+ {
+ switch ( weapon->GetWeaponID() )
+ {
+ case TF_WEAPON_KNIFE:
+ case TF_WEAPON_FISTS:
+ case TF_WEAPON_PDA:
+ case TF_WEAPON_PDA_ENGINEER_BUILD:
+ case TF_WEAPON_PDA_ENGINEER_DESTROY:
+ case TF_WEAPON_PDA_SPY:
+ case TF_WEAPON_BUILDER:
+ case TF_WEAPON_MEDIGUN:
+ case TF_WEAPON_DISPENSER:
+ case TF_WEAPON_INVIS:
+ case TF_WEAPON_FLAREGUN:
+ case TF_WEAPON_LUNCHBOX:
+ case TF_WEAPON_JAR:
+ case TF_WEAPON_COMPOUND_BOW:
+ case TF_WEAPON_SWORD:
+ case TF_WEAPON_CROSSBOW:
+ return true;
+ };
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line between the given points
+bool CTFBot::IsLineOfFireClear( const Vector &from, const Vector &to ) const
+{
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE );
+ CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() );
+ CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter );
+
+ UTIL_TraceLine( from, to, MASK_SOLID_BRUSHONLY, &filter, &trace );
+
+ return !trace.DidHit();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line from our eye to the given position
+bool CTFBot::IsLineOfFireClear( const Vector &where ) const
+{
+ return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), where );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line between the given point and entity
+bool CTFBot::IsLineOfFireClear( const Vector &from, CBaseEntity *who ) const
+{
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE );
+ CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() );
+ CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter );
+
+ UTIL_TraceLine( from, who->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, &filter, &trace );
+
+ return !trace.DidHit() || trace.m_pEnt == who;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if a weapon has no obstructions along the line from our eye to the given entity
+bool CTFBot::IsLineOfFireClear( CBaseEntity *who ) const
+{
+ return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), who );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool CTFBot::IsEntityBetweenTargetAndSelf( CBaseEntity *other, CBaseEntity *target )
+{
+ Vector toTarget = target->GetAbsOrigin() - GetAbsOrigin();
+ float rangeToTarget = toTarget.NormalizeInPlace();
+
+ Vector toOther = other->GetAbsOrigin() - GetAbsOrigin();
+ float rangeToOther = toOther.NormalizeInPlace();
+
+ return rangeToOther < rangeToTarget && DotProduct( toTarget, toOther ) > 0.7071f;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we are sure this player actually is an enemy spy
+bool CTFBot::IsKnownSpy( CTFPlayer *player ) const
+{
+ for( int i=0; i<m_knownSpyVector.Count(); ++i )
+ {
+ CTFPlayer *spy = m_knownSpyVector[i];
+ if ( spy && player->entindex() == spy->entindex() )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return true if we suspect this player might be an enemy spy
+CTFBot::SuspectedSpyInfo_t* CTFBot::IsSuspectedSpy( CTFPlayer *pPlayer )
+{
+ for( int i=0; i<m_suspectedSpyVector.Count(); ++i )
+ {
+ SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i];
+ CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy;
+ if ( pSpy && pPlayer->entindex() == pSpy->entindex() )
+ {
+ return pSpyInfo;
+ }
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Note that this player might be a spy
+void CTFBot::SuspectSpy( CTFPlayer *pPlayer )
+{
+ SuspectedSpyInfo_t* pSpyInfo = IsSuspectedSpy( pPlayer );
+
+ // Start suspecting this spy if we're not aware of them until now
+ if( pSpyInfo == NULL )
+ {
+ // add to head for LRU effect
+ pSpyInfo = new SuspectedSpyInfo_t;
+ pSpyInfo->m_suspectedSpy = pPlayer;
+ m_suspectedSpyVector.AddToHead( pSpyInfo );
+ }
+
+ // Suspicious!
+ pSpyInfo->Suspect();
+
+ // Too suspicious?
+ if( pSpyInfo->TestForRealizing() )
+ {
+ RealizeSpy( pPlayer );
+ }
+}
+
+void CTFBot::SuspectedSpyInfo_t::Suspect()
+{
+ int nCurTime = floor(gpGlobals->curtime);
+
+ // Add our new entry
+ m_touchTimes.AddToHead( nCurTime );
+}
+
+bool CTFBot::SuspectedSpyInfo_t::TestForRealizing()
+{
+ // Remove any old entries
+ int nCurTime = floor(gpGlobals->curtime);
+ int nCutoffTime = nCurTime - tf_bot_suspect_spy_touch_interval.GetInt();
+
+ FOR_EACH_VEC_BACK( m_touchTimes, i )
+ {
+ if( m_touchTimes[i] <= nCutoffTime )
+ m_touchTimes.Remove( i );
+ }
+
+ // Add our new entry
+ m_touchTimes.AddToHead( nCurTime );
+
+ // Setup an array of bools representing the past few seconds that we want
+ // to look for suspicious activity
+ CUtlVector<bool> vecSeconds;
+ vecSeconds.SetSize( tf_bot_suspect_spy_touch_interval.GetInt() );
+ FOR_EACH_VEC( vecSeconds, i )
+ {
+ vecSeconds[i] = false;
+ }
+
+ // Go through each time chunk and mark if there was suspicious activity
+ FOR_EACH_VEC( m_touchTimes, i )
+ {
+ int nTouchTime = m_touchTimes[i];
+ int nTimeSlot = nCurTime - nTouchTime;
+
+ if( nTimeSlot >= 0 && nTimeSlot < vecSeconds.Count() )
+ {
+ vecSeconds[nTimeSlot] = true;
+ }
+ }
+
+ // If all are true, then the spy has been suspicious enough to warrant being realized
+ FOR_EACH_VEC( vecSeconds, i )
+ {
+ if( vecSeconds[i] == false )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+bool CTFBot::SuspectedSpyInfo_t::IsCurrentlySuspected()
+{
+ float flCutoffTime = gpGlobals->curtime - tf_bot_suspect_spy_forget_cooldown.GetFloat();
+ if( m_touchTimes.Count() && m_touchTimes.Head() > flCutoffTime )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------------------------------
+// Note that this player *IS* a spy
+void CTFBot::RealizeSpy( CTFPlayer *pPlayer )
+{
+ // We already know about this spy
+ if ( IsKnownSpy( pPlayer ) )
+ return;
+
+ // add to head for LRU effect
+ m_knownSpyVector.AddToHead( pPlayer );
+
+ // inform my teammates
+ SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_CLOAKEDSPY );
+
+ // If I am suspicious of this spy, make everyone around me know that
+ // they should be suspicious too
+ SuspectedSpyInfo_t* pSuspectInfo = IsSuspectedSpy( pPlayer );
+ if( pSuspectInfo && pSuspectInfo->IsCurrentlySuspected() )
+ {
+ // Tell others around us we've realized there's a spy
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ CTFPlayer* pOther = playerVector[i];
+
+ if( !pOther->IsBot() )
+ continue;
+
+ //Make sure they're close by
+ Vector vecBetween = EyePosition() - pOther->EyePosition();
+ if( vecBetween.IsLengthLessThan( 512.f ) )
+ {
+ // If they dont know about this spy
+ CTFBot* pOtherBot = static_cast<CTFBot*>( pOther );
+ if( !pOtherBot->IsKnownSpy( pPlayer ) )
+ {
+ // I was suspicious that they were a spy, make my friend suspicious as well.
+ // This will cause them to attack a disguised spy in MvM for a bit.
+ pOtherBot->SuspectSpy( pPlayer );
+
+ // Tell them about it
+ pOtherBot->RealizeSpy( pPlayer );
+ }
+ }
+ }
+ }
+
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Remove player from spy suspect system
+void CTFBot::ForgetSpy( CTFPlayer *pPlayer )
+{
+ StopSuspectingSpy( pPlayer );
+ m_knownSpyVector.FindAndFastRemove( pPlayer );
+}
+
+void CTFBot::StopSuspectingSpy( CTFPlayer *pPlayer )
+{
+ // Find the entry matching this spy
+ for( int i=0; i<m_suspectedSpyVector.Count(); ++i )
+ {
+ SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i];
+ CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy;
+ if ( pSpy && pPlayer->entindex() == pSpy->entindex() )
+ {
+ delete pSpyInfo;
+ m_suspectedSpyVector.Remove(i);
+ break;
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// Return the nearest human player on the given team who is looking directly at me
+CTFPlayer *CTFBot::GetClosestHumanLookingAtMe( int team ) const
+{
+ CUtlVector< CTFPlayer * > otherVector;
+ CollectPlayers( &otherVector, team, COLLECT_ONLY_LIVING_PLAYERS );
+
+ float closeRange = FLT_MAX;
+ CTFPlayer *close = NULL;
+
+ for( int i=0; i<otherVector.Count(); ++i )
+ {
+ CTFPlayer *other = otherVector[i];
+
+ if ( other->IsBot() )
+ continue;
+
+ Vector otherEye, otherForward;
+ other->EyePositionAndVectors( &otherEye, &otherForward, NULL, NULL );
+
+ Vector toMe = const_cast< CTFBot * >( this )->EyePosition() - otherEye;
+ float range = toMe.NormalizeInPlace();
+
+ if ( range < closeRange )
+ {
+ const float cosTolerance = 0.98f;
+ if ( DotProduct( toMe, otherForward ) > cosTolerance )
+ {
+ // a human is looking toward me - check LOS
+ if ( IsLineOfSightClear( otherEye, IGNORE_NOTHING, other ) )
+ {
+ close = other;
+ closeRange = range;
+ }
+ }
+ }
+ }
+
+ return close;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// become a member of the given squad
+void CTFBot::JoinSquad( CTFBotSquad *squad )
+{
+ if ( squad )
+ {
+ squad->Join( this );
+ m_squad = squad;
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// leave our current squad
+void CTFBot::LeaveSquad( void )
+{
+ if ( m_squad )
+ {
+ m_squad->Leave( this );
+ m_squad = NULL;
+ }
+}
+
+//-----------------------------------------------------------------------------------------------------
+// leave our current squad
+void CTFBot::DeleteSquad( void )
+{
+ if ( m_squad )
+ {
+ m_squad = NULL;
+ }
+}
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsWeaponRestricted( CTFWeaponBase *weapon ) const
+{
+ if ( !weapon )
+ {
+ return false;
+ }
+
+ // Get the weapon's loadout slot
+ CEconItemView *pEconItemView = weapon->GetAttributeContainer()->GetItem();
+ if ( !pEconItemView )
+ return false;
+ CTFItemDefinition *pItemDef = pEconItemView->GetStaticData();
+ if ( !pItemDef )
+ return false;
+ int iLoadoutSlot = pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() );
+
+ if ( HasWeaponRestriction( MELEE_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_MELEE);
+ }
+
+ if ( HasWeaponRestriction( PRIMARY_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_PRIMARY);
+ }
+
+ if ( HasWeaponRestriction( SECONDARY_ONLY ) )
+ {
+ return (iLoadoutSlot != LOADOUT_POSITION_SECONDARY);
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Return true if there is something we want to reflect directly ahead of us
+//
+bool CTFBot::ShouldFireCompressionBlast( void )
+{
+ if ( TFGameRules()->IsInTraining() )
+ {
+ // no reflection in training mode
+ return false;
+ }
+
+ if ( !tf_bot_pyro_always_reflect.GetBool() )
+ {
+ if ( IsDifficulty( CTFBot::EASY ) )
+ {
+ // easy bots can't reflect at all
+ return false;
+ }
+
+ if ( IsDifficulty( CTFBot::NORMAL ) )
+ {
+ // normal bots reflect some of the time
+ if ( TransientlyConsistentRandomValue( 1.0f ) < 0.5f )
+ {
+ return false;
+ }
+ }
+
+ if ( IsDifficulty( CTFBot::HARD ) )
+ {
+ // hard bots reflect most of the time
+ if ( TransientlyConsistentRandomValue( 1.0f ) < 0.1f )
+ {
+ return false;
+ }
+ }
+ }
+
+ bool shouldPushPlayers = !TFGameRules()->IsMannVsMachineMode();
+
+ if ( shouldPushPlayers )
+ {
+ const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat( true );
+ if ( threat && threat->GetEntity() && threat->GetEntity()->IsPlayer() )
+ {
+ CTFPlayer *pushVictim = ToTFPlayer( threat->GetEntity() );
+
+ if ( IsRangeLessThan( pushVictim, tf_bot_pyro_shove_away_range.GetFloat() ) )
+ {
+ // our threat is very close - shove them!
+
+ // always shove ubers
+ if ( pushVictim && pushVictim->m_Shared.IsInvulnerable() )
+ {
+ return true;
+ }
+
+ if ( pushVictim->GetGroundEntity() == NULL )
+ {
+ // they are in the air - juggle them some of the time
+ return ( TransientlyConsistentRandomValue( 0.5f ) < 0.5f );
+ }
+
+ if ( pushVictim->IsCapturingPoint() )
+ {
+ // push them off the point!
+ return true;
+ }
+
+ // be pushy sometimes
+ if ( TransientlyConsistentRandomValue( 3.0f ) < 0.5f )
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+
+ Vector vecEye = EyePosition();
+ Vector vecForward, vecRight, vecUp;
+
+ AngleVectors( EyeAngles(), &vecForward, &vecRight, &vecUp );
+
+ Vector vecCenter = vecEye + vecForward * 128;
+ Vector vecSize = Vector( 128, 128, 64 );
+
+ const int maxCollectedEntities = 128;
+ CBaseEntity *pObjects[ maxCollectedEntities ];
+ int count = UTIL_EntitiesInBox( pObjects, maxCollectedEntities, vecCenter - vecSize, vecCenter + vecSize, FL_CLIENT | FL_GRENADE );
+
+ for ( int i = 0; i < count; i++ )
+ {
+ CBaseEntity *pObject = pObjects[i];
+ if ( pObject == this )
+ continue;
+
+ if ( pObject->GetTeamNumber() == GetTeamNumber() )
+ continue;
+
+ // should air blast player logic is already done before this loop
+ if ( pObject->IsPlayer() )
+ continue;
+
+ // is this something I want to deflect?
+ if ( !pObject->IsDeflectable() )
+ continue;
+
+ if ( FClassnameIs( pObject, "tf_projectile_rocket" ) || FClassnameIs( pObject, "tf_projectile_energy_ball" ) )
+ {
+ // is it headed right for me?
+ Vector vecThemUnitVel = pObject->GetAbsVelocity();
+ vecThemUnitVel.z = 0.0f;
+ vecThemUnitVel.NormalizeInPlace();
+
+ Vector horzForward( vecForward.x, vecForward.y, 0.0f );
+ horzForward.NormalizeInPlace();
+
+ if ( DotProduct( horzForward, vecThemUnitVel ) > -tf_bot_pyro_deflect_tolerance.GetFloat() )
+ continue;
+ }
+
+ // can I see it?
+ if ( !GetVisionInterface()->IsLineOfSightClear( pObject->WorldSpaceCenter() ) )
+ continue;
+
+ // bounce it!
+ return true;
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Compute a pseudo random value (0-1) that stays consistent for the
+// given period of time, but changes unpredictably each period.
+float CTFBot::TransientlyConsistentRandomValue( float period, int seedValue ) const
+{
+ CNavArea *area = GetLastKnownArea();
+ if ( !area )
+ {
+ return 0.0f;
+ }
+
+ // this term stays stable for 'period' seconds, then changes in an unpredictable way
+ int timeMod = (int)( gpGlobals->curtime / period ) + 1;
+ return fabs( FastCos( (float)( seedValue + ( entindex() * area->GetID() * timeMod ) ) ) );
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Given a target entity, find a target within 'maxSplashRadius' that has clear line of fire
+// to both the target entity and to me.
+bool CTFBot::FindSplashTarget( CBaseEntity *target, float maxSplashRadius, Vector *splashTarget ) const
+{
+ if ( !target || !splashTarget )
+ return false;
+
+ *splashTarget = target->WorldSpaceCenter();
+
+ const int retryCount = 50;
+ for( int i=0; i<retryCount; ++i )
+ {
+ Vector probe = target->WorldSpaceCenter() + RandomVector( -maxSplashRadius, maxSplashRadius );
+
+ trace_t trace;
+ NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( target->WorldSpaceCenter(), probe, MASK_SOLID_BRUSHONLY, &filter, &trace );
+ if ( trace.DidHitWorld() )
+ {
+ // can we shoot this spot?
+ if ( IsLineOfFireClear( trace.endpos ) )
+ {
+ // yes, found a corner-sticky target
+ *splashTarget = trace.endpos;
+
+ NDebugOverlay::Line( target->WorldSpaceCenter(), trace.endpos, 255, 0, 0, true, 60.0f );
+ NDebugOverlay::Cross3D( trace.endpos, 5.0f, 255, 255, 0, true, 60.0f );
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Restrict bot's attention to only this entity (or radius around this entity) to the exclusion of everything else
+void CTFBot::SetAttentionFocus( CBaseEntity *focusOn )
+{
+ m_attentionFocusEntity = focusOn;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Remove attention focus restrictions
+void CTFBot::ClearAttentionFocus( void )
+{
+ m_attentionFocusEntity = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAttentionFocused( void ) const
+{
+ return m_attentionFocusEntity != NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsAttentionFocusedOn( CBaseEntity *who ) const
+{
+ if ( m_attentionFocusEntity == NULL || who == NULL )
+ {
+ return false;
+ }
+
+ if ( m_attentionFocusEntity->entindex() == who->entindex() )
+ {
+ // specifically focused on this entity
+ return true;
+ }
+
+ CTFBotActionPoint *actionPoint = dynamic_cast< CTFBotActionPoint * >( m_attentionFocusEntity.Get() );
+ if ( actionPoint )
+ {
+ // we attend to everything within the action point's radius
+ return actionPoint->IsWithinRange( who );
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Notice the given threat after the given number of seconds have elapsed
+void CTFBot::DelayedThreatNotice( CHandle< CBaseEntity > who, float noticeDelay )
+{
+ float when = gpGlobals->curtime + noticeDelay;
+
+ // if we already have a delayed notice for this threat, ignore the new one unless the delay is less
+ for( int i=0; i<m_delayedNoticeVector.Count(); ++i )
+ {
+ if ( m_delayedNoticeVector[i].m_who == who )
+ {
+ if ( m_delayedNoticeVector[i].m_when > when )
+ {
+ // update delay to shorter time
+ m_delayedNoticeVector[i].m_when = when;
+ }
+ return;
+ }
+ }
+
+ // new notice
+ DelayedNoticeInfo delay;
+ delay.m_who = who;
+ delay.m_when = when;
+ m_delayedNoticeVector.AddToTail( delay );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::UpdateDelayedThreatNotices( void )
+{
+ for( int i=0; i<m_delayedNoticeVector.Count(); ++i )
+ {
+ if ( m_delayedNoticeVector[i].m_when <= gpGlobals->curtime )
+ {
+ // delay is up - notice this threat
+ CBaseEntity *who = m_delayedNoticeVector[i].m_who;
+
+ if ( who )
+ {
+ if ( who->IsPlayer() )
+ {
+ CTFPlayer *player = ToTFPlayer( who );
+ if ( player->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ RealizeSpy( player );
+ }
+ }
+
+ GetVisionInterface()->AddKnownEntity( who );
+ }
+
+ m_delayedNoticeVector.Remove( i );
+ --i;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::GiveRandomItem( loadout_positions_t loadoutPosition )
+{
+ CUtlVector< const CEconItemDefinition * > itemVector;
+
+ const CEconItemSchema::ItemDefinitionMap_t& mapItemDefs = ItemSystem()->GetItemSchema()->GetItemDefinitionMap();
+ FOR_EACH_MAP_FAST( mapItemDefs, i )
+ {
+ const CTFItemDefinition *pItemDef = dynamic_cast< const CTFItemDefinition * >( mapItemDefs[i] );
+
+ if ( pItemDef && pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ) == loadoutPosition )
+ {
+ itemVector.AddToTail( pItemDef );
+ }
+ }
+
+ if ( itemVector.Count() > 0 )
+ {
+ int which = RandomInt( 0, itemVector.Count()-1 );
+
+/*
+ CBaseCombatWeapon *myMelee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ me->Weapon_Detach( myMelee );
+ UTIL_Remove( myMelee );
+*/
+
+ const char *itemName = itemVector[ which ]->GetDefinitionName();
+ BotGenerateAndWearItem( this, itemName );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CTFBot::IsSquadmate( CTFPlayer *who ) const
+{
+ if ( !m_squad || !who || !who->IsBotOfType( TF_BOT_TYPE ) )
+ return false;
+
+ return GetSquad() == ToTFBot( who )->GetSquad();
+}
+
+
+//---------------------------------------------------------------------------------------------
+// Set Spy disguise to be a class that someone on the enemy team is actually using
+void CTFBot::DisguiseAsMemberOfEnemyTeam( void )
+{
+ CUtlVector< CTFPlayer * > enemyVector;
+ CollectPlayers( &enemyVector, GetEnemyTeam( GetTeamNumber() ) );
+
+ int disguise = RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 );
+
+ if ( enemyVector.Count() > 0 )
+ {
+ disguise = enemyVector[ RandomInt( 0, enemyVector.Count()-1 ) ]->GetPlayerClass()->GetClassIndex();
+ }
+
+ m_Shared.Disguise( GetEnemyTeam( GetTeamNumber() ), disguise );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::ClearTags( void )
+{
+ m_tags.RemoveAll();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::AddTag( const char *tag )
+{
+ if ( !HasTag( tag ) )
+ {
+ m_tags.AddToTail( CFmtStr( "%s", tag ) );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CTFBot::RemoveTag( const char *tag )
+{
+ for ( int i=0; i<m_tags.Count(); ++i )
+ {
+ if ( FStrEq( tag, m_tags[i] ) )
+ {
+ m_tags.Remove(i);
+ return;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+// TODO: Make this an efficient lookup/match
+bool CTFBot::HasTag( const char *tag )
+{
+ for( int i=0; i<m_tags.Count(); ++i )
+ {
+ if ( FStrEq( tag, m_tags[i] ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//---------------------------------------------------------------------------------------------
+CBaseObject *CTFBot::GetNearestKnownSappableTarget( void )
+{
+ CUtlVector< CKnownEntity > knownVector;
+ GetVisionInterface()->CollectKnownEntities( &knownVector );
+
+ CBaseObject *closeObject = NULL;
+ float closeObjectRangeSq = 500.0f * 500.0f;
+
+ for( int i=0; i<knownVector.Count(); ++i )
+ {
+ CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( knownVector[i].GetEntity() );
+ if ( enemyObject && !enemyObject->HasSapper() && IsEnemy( enemyObject ) )
+ {
+ float rangeSq = GetRangeSquaredTo( enemyObject );
+ if ( rangeSq < closeObjectRangeSq )
+ {
+ closeObjectRangeSq = rangeSq;
+ closeObject = enemyObject;
+ }
+ }
+ }
+
+ return closeObject;
+}
+
+
+//-----------------------------------------------------------------------------------------
+Action< CTFBot > *CTFBot::OpportunisticallyUseWeaponAbilities( void )
+{
+ if ( !m_opportunisticTimer.IsElapsed() )
+ {
+ return NULL;
+ }
+
+ m_opportunisticTimer.Start( RandomFloat( 0.1f, 0.2f ) );
+
+
+ // if I'm wearing a charge shield, use it!
+ if ( IsPlayerClass( TF_CLASS_DEMOMAN ) && m_Shared.IsShieldEquipped() )
+ {
+ Vector forward;
+ EyeVectors( &forward );
+ bool bShouldCharge = GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + 100.0f * forward, ILocomotion::IMMEDIATELY );
+ if ( HasAttribute( CTFBot::AIR_CHARGE_ONLY ) && ( GetGroundEntity() || GetAbsVelocity().z > 0 ) )
+ {
+ bShouldCharge = false;
+ }
+
+ if ( bShouldCharge )
+ {
+ PressAltFireButton();
+ }
+ }
+ // if I'm wearing parachute, check if I should activate my parachute
+ else if ( m_Shared.IsParachuteEquipped() )
+ {
+ bool bIsBurning = m_Shared.InCond( TF_COND_BURNING );
+ float flHealthPercent = (float)GetHealth() / GetMaxHealth();
+ const float flHealthThreshold = 0.5f;
+ // should I activate parachute?
+ if ( !m_Shared.InCond( TF_COND_PARACHUTE_DEPLOYED ) )
+ {
+ float flMinParachuteGroundDistance = 300.f;
+ // check if I'm falling, high enough off the ground to deploy parachute, and not burning
+ if ( flHealthPercent >= flHealthThreshold && !bIsBurning && GetAbsVelocity().z < 0 && GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flMinParachuteGroundDistance ), ILocomotion::IMMEDIATELY ) )
+ {
+ PressJumpButton();
+ }
+ }
+ // should I deactivate parachute?
+ else
+ {
+ float flCancelParachuteDistance = 150.f;
+ // if I'm burning or close enough to landing, deactivate the parachute or health less than some threshold
+ if ( flHealthPercent < flHealthThreshold || bIsBurning || !GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flCancelParachuteDistance ), ILocomotion::IMMEDIATELY ) )
+ {
+ PressJumpButton();
+ }
+ }
+ }
+
+ // don't use items if we have the flag, since most of them are unusable (unless we're a bomb carrier in MvM)
+ if ( HasTheFlag() && !TFGameRules()->IsMannVsMachineMode() )
+ {
+ return NULL;
+ }
+
+ for ( int w=0; w<MAX_WEAPONS; ++w )
+ {
+ CTFWeaponBase *weapon = ( CTFWeaponBase * )GetWeapon( w );
+ if ( !weapon )
+ continue;
+
+ // if I have some kind of buff banner - use it!
+ if ( weapon->GetWeaponID() == TF_WEAPON_BUFF_ITEM )
+ {
+ CTFBuffItem *buff = (CTFBuffItem *)weapon;
+ if ( buff->IsFull() )
+ {
+ return new CTFBotUseItem( buff );
+ }
+ }
+ else if ( weapon->GetWeaponID() == TF_WEAPON_LUNCHBOX )
+ {
+ // if we have an eatable (drink, sandvich, etc) - eat it!
+ CTFLunchBox *lunchbox = (CTFLunchBox *)weapon;
+ if ( lunchbox->HasAmmo() )
+ {
+ // scout lunchboxes are also gated by their energy drink meter
+ if ( !IsPlayerClass( TF_CLASS_SCOUT ) || m_Shared.GetScoutEnergyDrinkMeter() >= 100 )
+ {
+ return new CTFBotUseItem( lunchbox );
+ }
+ }
+ }
+ else if ( weapon->GetWeaponID() == TF_WEAPON_BAT_WOOD )
+ {
+ // sandman
+ if ( GetAmmoCount( TF_AMMO_GRENADES1 ) > 0 )
+ {
+ const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat();
+ if ( threat && threat->IsVisibleInFOVNow() )
+ {
+ // hit a stunball
+ PressAltFireButton();
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------
+// mostly for MvM - pick a random enemy player that is not in their spawn room
+CTFPlayer *CTFBot::SelectRandomReachableEnemy( void )
+{
+ CUtlVector< CTFPlayer * > livePlayerVector;
+ CollectPlayers( &livePlayerVector, GetEnemyTeam( GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ // only consider players who have left their spawn
+ CUtlVector< CTFPlayer * > playerVector;
+ for( int i=0; i<livePlayerVector.Count(); ++i )
+ {
+ CTFPlayer *player = livePlayerVector[i];
+ if ( !PointInRespawnRoom( player, player->WorldSpaceCenter() ) )
+ {
+ playerVector.AddToTail( player );
+ }
+ }
+
+ if ( playerVector.Count() > 0 )
+ {
+ return playerVector[ RandomInt( 0, playerVector.Count()-1 ) ];
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Different sized bots used different lookahead distances
+float CTFBot::GetDesiredPathLookAheadRange( void ) const
+{
+ return tf_bot_path_lookahead_range.GetFloat() * GetModelScale();
+}
+
+//-----------------------------------------------------------------------------------------
+// Hack to apply idle loop sounds in MvM
+void CTFBot::StartIdleSound( void )
+{
+ StopIdleSound();
+
+ if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() )
+ return;
+
+ // SHIELD YOUR EYES MIKEB!!!
+ if ( IsMiniBoss() )
+ {
+ const char *pszSoundName = NULL;
+
+ int iClass = GetPlayerClass()->GetClassIndex();
+ switch ( iClass )
+ {
+ case TF_CLASS_HEAVYWEAPONS:
+ {
+ pszSoundName = "MVM.GiantHeavyLoop";
+ break;
+ }
+ case TF_CLASS_SOLDIER:
+ {
+ pszSoundName = "MVM.GiantSoldierLoop";
+ break;
+ }
+ case TF_CLASS_DEMOMAN:
+ {
+ if ( m_mission == MISSION_DESTROY_SENTRIES )
+ {
+ pszSoundName = "MVM.SentryBusterLoop";
+ }
+ else
+ {
+ pszSoundName = "MVM.GiantDemomanLoop";
+ }
+ break;
+ }
+ case TF_CLASS_SCOUT:
+ {
+ pszSoundName = "MVM.GiantScoutLoop";
+ break;
+ }
+ case TF_CLASS_PYRO:
+ {
+ pszSoundName = "MVM.GiantPyroLoop";
+ break;
+ }
+ }
+
+ if ( pszSoundName )
+ {
+ CReliableBroadcastRecipientFilter filter;
+ CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
+ m_pIdleSound = controller.SoundCreate( filter, entindex(), pszSoundName );
+ controller.Play( m_pIdleSound, 1.0, 100 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------------------
+void CTFBot::StopIdleSound( void )
+{
+ if ( m_pIdleSound )
+ {
+ CSoundEnvelopeController::GetController().SoundDestroy( m_pIdleSound );
+ m_pIdleSound = NULL;
+ }
+}
+
+bool CTFBot::ShouldAutoJump()
+{
+ if ( !HasAttribute( CTFBot::AUTO_JUMP ) )
+ return false;
+
+ if ( !m_autoJumpTimer.HasStarted() )
+ {
+ m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) );
+ return true;
+ }
+ else if ( m_autoJumpTimer.IsElapsed() )
+ {
+ m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) );
+ return true;
+ }
+
+ return false;
+}
+
+
+void CTFBot::SetFlagTarget( CCaptureFlag* pFlag )
+{
+ if ( m_hFollowingFlagTarget != pFlag )
+ {
+ if ( m_hFollowingFlagTarget )
+ {
+ m_hFollowingFlagTarget->RemoveFollower( this );
+ }
+
+ m_hFollowingFlagTarget = pFlag;
+ if ( m_hFollowingFlagTarget )
+ {
+ m_hFollowingFlagTarget->AddFollower( this );
+ }
+ }
+}
+
+
+int CTFBot::DrawDebugTextOverlays(void)
+{
+ int offset = tf_bot_debug_tags.GetBool() ? 1 : BaseClass::DrawDebugTextOverlays();
+
+ CUtlString strTags = "Tags : ";
+ for( int i=0; i<m_tags.Count(); ++i )
+ {
+ strTags.Append( m_tags[i] );
+ strTags.Append( " " );
+ }
+
+ EntityText( offset, strTags.Get(), 0 );
+ offset++;
+
+ return offset;
+}
+
+
+void CTFBot::AddEventChangeAttributes( const CTFBot::EventChangeAttributes_t* newEvent )
+{
+ m_eventChangeAttributes.AddToTail( newEvent );
+}
+
+
+const CTFBot::EventChangeAttributes_t* CTFBot::GetEventChangeAttributes( const char* pszEventName ) const
+{
+ for ( int i=0; i<m_eventChangeAttributes.Count(); ++i )
+ {
+ if ( FStrEq( m_eventChangeAttributes[i]->m_eventName, pszEventName ) )
+ {
+ return m_eventChangeAttributes[i];
+ }
+ }
+ return NULL;
+}
+
+
+void CTFBot::OnEventChangeAttributes( const CTFBot::EventChangeAttributes_t* pEvent )
+{
+ if ( pEvent )
+ {
+ SetDifficulty( pEvent->m_skill );
+
+ ClearWeaponRestrictions();
+ SetWeaponRestriction( pEvent->m_weaponRestriction );
+
+ SetMission( pEvent->m_mission );
+
+ ClearAllAttributes();
+ SetAttribute( pEvent->m_attributeFlags );
+
+ SetMaxVisionRangeOverride( pEvent->m_maxVisionRange );
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ SetAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH );
+ SetAttribute( CTFBot::RETAIN_BUILDINGS );
+ }
+
+ // cache off health value before we clear attribute because ModifyMaxHealth adds new attribute and reset the health
+ int nHealth = GetHealth();
+ int nMaxHealth = GetMaxHealth();
+
+ // remove any player attributes
+ RemovePlayerAttributes( false );
+ // and add ones that we want specifically
+ FOR_EACH_VEC( pEvent->m_characterAttributes, i )
+ {
+ const CEconItemAttributeDefinition *pDef = pEvent->m_characterAttributes[i].GetAttributeDefinition();
+ if ( pDef )
+ {
+ Assert( GetAttributeList() );
+ GetAttributeList()->SetRuntimeAttributeValue( pDef, pEvent->m_characterAttributes[i].m_value.asFloat );
+ }
+ }
+ NetworkStateChanged();
+
+ // set health back to what it was before we clear bot's attributes
+ ModifyMaxHealth( nMaxHealth );
+ SetHealth( nHealth );
+
+ // give items to bot before apply attribute changes
+ FOR_EACH_VEC( pEvent->m_items, i )
+ {
+ AddItem( pEvent->m_items[i] );
+ }
+
+ // add attributes to equipped items
+ FOR_EACH_VEC( pEvent->m_itemsAttributes, i )
+ {
+ const CTFBot::EventChangeAttributes_t::item_attributes_t& itemAttributes = pEvent->m_itemsAttributes[i];
+ CSchemaItemDefHandle itemDef( itemAttributes.m_itemName );
+ if ( !itemDef )
+ {
+ Warning( "Unable to find item %s to update attribute.\n", itemAttributes.m_itemName.Get() );
+ }
+
+ for ( int iItemSlot = LOADOUT_POSITION_PRIMARY ; iItemSlot < CLASS_LOADOUT_POSITION_COUNT ; iItemSlot++ )
+ {
+ CEconEntity* pEntity = NULL;
+ CEconItemView *pCurItemData = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( this, iItemSlot, &pEntity );
+ if ( pCurItemData && itemDef && ( pCurItemData->GetItemDefIndex() == itemDef->GetDefinitionIndex() ) )
+ {
+ for ( int iAtt=0; iAtt<itemAttributes.m_attributes.Count(); ++iAtt )
+ {
+ const static_attrib_t& attrib = itemAttributes.m_attributes[iAtt];
+ CAttributeList *pAttribList = pCurItemData->GetAttributeList();
+ if ( pAttribList )
+ {
+ pAttribList->SetRuntimeAttributeValue( attrib.GetAttributeDefinition(), attrib.m_value.asFloat );
+ }
+ }
+
+ if ( pEntity )
+ {
+ // update model incase we change style
+ pEntity->UpdateModelToClass();
+ }
+
+ // move on to the next set of attributes
+ break;
+ }
+ } // for each slot
+ } // for each set of attributes
+
+ // tags
+ ClearTags();
+ for( int g=0; g<pEvent->m_tags.Count(); ++g )
+ {
+ AddTag( pEvent->m_tags[g] );
+ }
+ }
+}
+
+
+void CTFBot::AddItem( const char* pszItemName )
+{
+ CItemSelectionCriteria criteria;
+ criteria.SetQuality( AE_USE_SCRIPT_VALUE );
+ criteria.BAddCondition( "name", k_EOperator_String_EQ, pszItemName, true );
+
+ CBaseEntity *pItem = ItemGeneration()->GenerateRandomItem( &criteria, WorldSpaceCenter(), vec3_angle );
+ if ( pItem )
+ {
+ CEconItemView *pScriptItem = static_cast< CBaseCombatWeapon * >( pItem )->GetAttributeContainer()->GetItem();
+
+ // If we already have an item in that slot, remove it
+ int iClass = GetPlayerClass()->GetClassIndex();
+ int iSlot = pScriptItem->GetStaticData()->GetLoadoutSlot( iClass );
+ equip_region_mask_t unNewItemRegionMask = pScriptItem->GetItemDefinition() ? pScriptItem->GetItemDefinition()->GetEquipRegionConflictMask() : 0;
+
+ if ( IsWearableSlot( iSlot ) )
+ {
+ // Remove any wearable that has a conflicting equip_region
+ for ( int wbl = 0; wbl < GetNumWearables(); wbl++ )
+ {
+ CEconWearable *pWearable = GetWearable( wbl );
+ if ( !pWearable )
+ continue;
+
+ equip_region_mask_t unWearableRegionMask = 0;
+ if ( pWearable->GetAttributeContainer()->GetItem() )
+ {
+ unWearableRegionMask = pWearable->GetAttributeContainer()->GetItem()->GetItemDefinition()->GetEquipRegionConflictMask();
+ }
+
+ if ( unWearableRegionMask & unNewItemRegionMask )
+ {
+ RemoveWearable( pWearable );
+ }
+ }
+ }
+ else
+ {
+ CBaseEntity *pEntity = GetEntityForLoadoutSlot( iSlot );
+ if ( pEntity )
+ {
+ CBaseCombatWeapon *pWpn = dynamic_cast< CBaseCombatWeapon * >( pEntity );
+ Weapon_Detach( pWpn );
+ UTIL_Remove( pEntity );
+ }
+ }
+
+ // Fake global id
+ pScriptItem->SetItemID( 1 );
+
+ DispatchSpawn( pItem );
+
+ CEconEntity *pNewItem = assert_cast<CEconEntity*>( pItem );
+ if ( pNewItem )
+ {
+ pNewItem->GiveTo( this );
+ }
+
+ PostInventoryApplication();
+ }
+ else
+ {
+ if ( pszItemName && pszItemName[0] )
+ {
+ DevMsg( "CTFBotSpawner::AddItemToBot: Invalid item %s.\n", pszItemName );
+ }
+ }
+}
+
+
+int CTFBot::GetUberHealthThreshold()
+{
+ int iUberHealthThreshold = 0;
+ CALL_ATTRIB_HOOK_INT( iUberHealthThreshold, bot_medic_uber_health_threshold );
+ if ( iUberHealthThreshold > 0 )
+ {
+ return iUberHealthThreshold;
+ }
+
+ return 50;
+}
+
+
+float CTFBot::GetUberDeployDelayDuration()
+{
+ float flDelayUberDuration = 0;
+ CALL_ATTRIB_HOOK_INT( flDelayUberDuration, bot_medic_uber_deploy_delay_duration );
+ if ( flDelayUberDuration > 0 )
+ {
+ return flDelayUberDuration;
+ }
+
+ return -1.f;
+}
diff --git a/game/server/tf/bot/tf_bot.h b/game/server/tf/bot/tf_bot.h
new file mode 100644
index 0000000..62be222
--- /dev/null
+++ b/game/server/tf/bot/tf_bot.h
@@ -0,0 +1,1080 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot.h
+// Team Fortress NextBot
+// Michael Booth, February 2009
+
+#ifndef TF_BOT_H
+#define TF_BOT_H
+
+#include "Player/NextBotPlayer.h"
+#include "../nav_mesh/tf_nav_mesh.h"
+#include "tf_bot_vision.h"
+#include "tf_bot_body.h"
+#include "tf_bot_locomotion.h"
+#include "tf_player.h"
+#include "tf_bot_squad.h"
+#include "bot/map_entities/tf_bot_proxy.h"
+#include "tf_gamerules.h"
+#include "entity_capture_flag.h"
+#include "func_capture_zone.h"
+#include "nav_entities.h"
+#include "utlstack.h"
+
+#define TF_BOT_TYPE 1337
+
+class CTriggerAreaCapture;
+class CTFBotActionPoint;
+class CObjectSentrygun;
+class CTFBotGenerator;
+
+extern void BotGenerateAndWearItem( CTFPlayer *pBot, const char *itemName );
+
+//----------------------------------------------------------------------------
+// These must remain in sync with the bot_generator's spawnflags in tf.fgd:
+#define TFBOT_IGNORE_ENEMY_SCOUTS 0x0001
+#define TFBOT_IGNORE_ENEMY_SOLDIERS 0x0002
+#define TFBOT_IGNORE_ENEMY_PYROS 0x0004
+#define TFBOT_IGNORE_ENEMY_DEMOMEN 0x0008
+#define TFBOT_IGNORE_ENEMY_HEAVIES 0x0010
+#define TFBOT_IGNORE_ENEMY_MEDICS 0x0020
+#define TFBOT_IGNORE_ENEMY_ENGINEERS 0x0040
+#define TFBOT_IGNORE_ENEMY_SNIPERS 0x0080
+#define TFBOT_IGNORE_ENEMY_SPIES 0x0100
+#define TFBOT_IGNORE_ENEMY_SENTRY_GUNS 0x0200
+#define TFBOT_IGNORE_SCENARIO_GOALS 0x0400
+
+#define TFBOT_ALL_BEHAVIOR_FLAGS 0xFFFF
+
+#define TFBOT_MVM_MAX_PATH_LENGTH 0.0f // 7000.0f // in MvM, all pathfinds are limited to this (0 == no limit)
+
+
+//----------------------------------------------------------------------------
+class CTFBot: public NextBotPlayer< CTFPlayer >, public CGameEventListener
+{
+public:
+ DECLARE_CLASS( CTFBot, NextBotPlayer< CTFPlayer > );
+
+ CTFBot();
+ virtual ~CTFBot();
+
+ virtual void Spawn();
+ virtual void FireGameEvent( IGameEvent *event );
+ virtual void Event_Killed( const CTakeDamageInfo &info );
+ virtual void PhysicsSimulate( void );
+ virtual void Touch( CBaseEntity *pOther );
+ virtual void AvoidPlayers( CUserCmd *pCmd ); // some game types allow players to pass through each other, this method pushes them apart
+ virtual void UpdateOnRemove( void );
+ virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ) OVERRIDE;
+ virtual void ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance = false ) OVERRIDE;
+ virtual bool ShouldGib( const CTakeDamageInfo &info ) OVERRIDE;
+
+ virtual int DrawDebugTextOverlays(void);
+
+ virtual bool IsAllowedToPickUpFlag( void ) const;
+
+ virtual void InitClass( void ); // set health/etc
+ void ModifyMaxHealth( int nNewMaxHealth, bool bSetCurrentHealth = true, bool bAllowModelScaling = true );
+
+ virtual int GetBotType( void ) const; // return a unique int representing the type of bot instance this is
+
+ virtual CTFNavArea *GetLastKnownArea( void ) const { return static_cast< CTFNavArea * >( BaseClass::GetLastKnownArea() ); } // return the last nav area the player occupied - NULL if unknown
+
+ // NextBotPlayer
+ static CBasePlayer *AllocatePlayerEntity( edict_t *pEdict, const char *playerName );
+
+ virtual void PressFireButton( float duration = -1.0f ) OVERRIDE;
+ virtual void PressAltFireButton( float duration = -1.0f ) OVERRIDE;
+ virtual void PressSpecialFireButton( float duration = -1.0f ) OVERRIDE;
+
+ // INextBot
+ virtual CTFBotLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+ virtual CTFBotBody *GetBodyInterface( void ) const { return m_body; }
+ virtual CTFBotVision *GetVisionInterface( void ) const { return m_vision; }
+ DECLARE_INTENTION_INTERFACE( CTFBot );
+
+ virtual bool IsDormantWhenDead( void ) const; // should this player-bot continue to update itself when dead (respawn logic, etc)
+
+ virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires their weapon
+
+ virtual bool IsDebugFilterMatch( const char *name ) const; // return true if we match the given debug symbol
+
+ virtual int GetAllowedTauntPartnerTeam() const OVERRIDE { return GetTeamNumber(); }
+
+ // CTFBot specific
+ CTeamControlPoint *GetMyControlPoint( void ) const; // return point we want to capture, or need to defend
+ void ClearMyControlPoint( void );
+ bool WasPointJustLost( void ) const; // return true if we just lost territory recently
+ bool AreAllPointsUncontestedSoFar( void ) const; // return true if no enemy has contested any point yet
+ bool IsPointBeingCaptured( CTeamControlPoint *point ) const; // return true if the given point is being captured
+ bool IsAnyPointBeingCaptured( void ) const; // return true if any point is being captured
+ bool IsNearPoint( CTeamControlPoint *point ) const; // return true if we are within a short travel distance of the current point
+ float GetTimeLeftToCapture( void ) const; // return time left to capture the point before we lose the game
+
+ CCaptureFlag *GetFlagToFetch( void ) const; // return flag we want to fetch
+ CCaptureZone *GetFlagCaptureZone( void ) const; // return capture zone for our flag(s)
+
+ struct SniperSpotInfo
+ {
+ CTFNavArea *m_vantageArea;
+ Vector m_vantageSpot;
+
+ CTFNavArea *m_theaterArea;
+ Vector m_theaterSpot;
+
+ float m_range;
+ float m_advantage; // the difference in how long it takes us to reach our vantage spot vs them to reach the theater spot
+ };
+
+ void AccumulateSniperSpots( void ); // find good sniping spots and store them
+ const CUtlVector< SniperSpotInfo > *GetSniperSpots( void ) const; // return vector of good sniping positions
+ bool HasSniperSpots( void ) const;
+ void ClearSniperSpots( void );
+
+ // search outwards from startSearchArea and collect all reachable objects from the given list that pass the given filter
+ void SelectReachableObjects( const CUtlVector< CHandle< CBaseEntity > > &candidateObjectVector, CUtlVector< CHandle< CBaseEntity > > *selectedObjectVector, const INextBotFilter &filter, CNavArea *startSearchArea, float maxRange = 2000.0f ) const;
+ CBaseEntity *FindClosestReachableObject( const char *objectName, CNavArea *from, float maxRange = 2000.0f ) const;
+
+ CTFNavArea *GetSpawnArea( void ) const; // get area where we spawned in
+
+ bool IsAmmoLow( void ) const;
+ bool IsAmmoFull( void ) const;
+
+ void UpdateLookingAroundForEnemies( void ); // update our view to keep an eye on enemies, and where enemies come from
+
+ #define LOOK_FOR_FRIENDS false
+ #define LOOK_FOR_ENEMIES true
+ void UpdateLookingAroundForIncomingPlayers( bool lookForEnemies ); // update our view to watch where friends or enemies will be coming from
+ void StartLookingAroundForEnemies( void ); // enable updating view for enemy searching
+ void StopLookingAroundForEnemies( void ); // disable updating view for enemy searching
+
+ void SetAttentionFocus( CBaseEntity *focusOn ); // restrict bot's attention to only this entity (or radius around this entity) to the exclusion of everything else
+ void ClearAttentionFocus( void ); // remove attention focus restrictions
+ bool IsAttentionFocused( void ) const;
+ bool IsAttentionFocusedOn( CBaseEntity *who ) const;
+
+ void DelayedThreatNotice( CHandle< CBaseEntity > who, float noticeDelay ); // notice the given threat after the given number of seconds have elapsed
+ void UpdateDelayedThreatNotices( void );
+
+ CTFNavArea *FindVantagePoint( float maxTravelDistance = 2000.0f ) const; // return a nearby area where we can see a member of the enemy team
+
+ const char *GetNextSpawnClassname( void ) const;
+
+ float GetThreatDanger( CBaseCombatCharacter *who ) const; // return perceived danger of threat (0=none, 1=immediate deadly danger)
+ float GetMaxAttackRange( void ) const; // return the max range at which we can effectively attack
+ float GetDesiredAttackRange( void ) const; // return the ideal range at which we can effectively attack
+
+ bool EquipRequiredWeapon( void ); // if we're required to equip a specific weapon, do it.
+ void EquipBestWeaponForThreat( const CKnownEntity *threat ); // equip the best weapon we have to attack the given threat
+ bool EquipLongRangeWeapon( void ); // equip a weapon that can damage far-away targets
+
+ void PushRequiredWeapon( CTFWeaponBase *weapon ); // force us to equip and use this weapon until popped off the required stack
+ void PopRequiredWeapon( void ); // pop top required weapon off of stack and discard
+
+ #define MY_CURRENT_GUN NULL // can be passed as weapon to following queries
+ bool IsCombatWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon can be used to attack
+ bool IsHitScanWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon is a "hitscan" weapon (scattered tracelines with instant damage)
+ bool IsContinuousFireWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon "sprays" bullets/fire/etc continuously (ie: not individual rockets/etc)
+ bool IsExplosiveProjectileWeapon( CTFWeaponBase *weapon ) const;// return true if given weapon launches explosive projectiles with splash damage
+ bool IsBarrageAndReloadWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon has small clip and long reload cost (ie: rocket launcher, etc)
+ bool IsQuietWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon doesn't make much sound when used (ie: spy knife, etc)
+
+ bool IsEnvironmentNoisy( void ) const; // return true if there are/have been loud noises (ie: non-quiet weapons) nearby very recently
+
+ enum WeaponRestrictionType
+ {
+ ANY_WEAPON = 0,
+ MELEE_ONLY = 0x0001,
+ PRIMARY_ONLY = 0x0002,
+ SECONDARY_ONLY = 0x0004,
+ };
+ void ClearWeaponRestrictions( void );
+ void SetWeaponRestriction( int restrictionFlags );
+ bool HasWeaponRestriction( int restrictionFlags ) const;
+ bool IsWeaponRestricted( CTFWeaponBase *weapon ) const;
+
+ bool ShouldFireCompressionBlast( void );
+
+ bool IsLineOfFireClear( const Vector &where ) const; // return true if a weapon has no obstructions along the line from our eye to the given position
+ bool IsLineOfFireClear( CBaseEntity *who ) const; // return true if a weapon has no obstructions along the line from our eye to the given entity
+ bool IsLineOfFireClear( const Vector &from, const Vector &to ) const; // return true if a weapon has no obstructions along the line between the given points
+ bool IsLineOfFireClear( const Vector &from, CBaseEntity *who ) const; // return true if a weapon has no obstructions along the line between the given point and entity
+
+ bool IsEntityBetweenTargetAndSelf( CBaseEntity *other, CBaseEntity *target ); // return true if "other" is positioned inbetween us and "target"
+
+ class SuspectedSpyInfo_t
+ {
+ public:
+ bool IsCurrentlySuspected();
+ void Suspect(); // The verb form of the word, not the noun.
+ bool TestForRealizing();
+ CHandle< CTFPlayer > m_suspectedSpy;
+
+ private:
+
+
+ CUtlVector< int > m_touchTimes;
+ };
+
+ bool IsKnownSpy( CTFPlayer *player ) const; // return true if we are sure this player actually is an enemy spy
+ SuspectedSpyInfo_t* IsSuspectedSpy( CTFPlayer *player ); // return true if we suspect this player might be an enemy spy
+ void SuspectSpy( CTFPlayer *player ); // note that this player might be a spy
+ void RealizeSpy( CTFPlayer *player ); // note that this player *IS* a spy
+ void ForgetSpy( CTFPlayer *player ); // remove player from spy suspect system
+ void StopSuspectingSpy( CTFPlayer *pPlayer );
+
+ CTFPlayer *GetClosestHumanLookingAtMe( int team = TEAM_ANY ) const; // return the nearest human player on the given team who is looking directly at me
+
+ enum AttributeType
+ {
+ REMOVE_ON_DEATH = 1<<0, // kick bot from server when killed
+ AGGRESSIVE = 1<<1, // in MvM mode, push for the cap point
+ IS_NPC = 1<<2, // a non-player support character
+ SUPPRESS_FIRE = 1<<3,
+ DISABLE_DODGE = 1<<4,
+ BECOME_SPECTATOR_ON_DEATH = 1<<5, // move bot to spectator team when killed
+ QUOTA_MANANGED = 1<<6, // managed by the bot quota in CTFBotManager
+ RETAIN_BUILDINGS = 1<<7, // don't destroy this bot's buildings when it disconnects
+ SPAWN_WITH_FULL_CHARGE = 1<<8, // all weapons start with full charge (ie: uber)
+ ALWAYS_CRIT = 1<<9, // always fire critical hits
+ IGNORE_ENEMIES = 1<<10,
+ HOLD_FIRE_UNTIL_FULL_RELOAD = 1<<11, // don't fire our barrage weapon until it is full reloaded (rocket launcher, etc)
+ PRIORITIZE_DEFENSE = 1<<12, // bot prioritizes defending when possible
+ ALWAYS_FIRE_WEAPON = 1<<13, // constantly fire our weapon
+ TELEPORT_TO_HINT = 1<<14, // bot will teleport to hint target instead of walking out from the spawn point
+ MINIBOSS = 1<<15, // is miniboss?
+ USE_BOSS_HEALTH_BAR = 1<<16, // should I use boss health bar?
+ IGNORE_FLAG = 1<<17, // don't pick up flag/bomb
+ AUTO_JUMP = 1<<18, // auto jump
+ AIR_CHARGE_ONLY = 1<<19, // demo knight only charge in the air
+ PREFER_VACCINATOR_BULLETS = 1<<20, // When using the vaccinator, prefer to use the bullets shield
+ PREFER_VACCINATOR_BLAST = 1<<21, // When using the vaccinator, prefer to use the blast shield
+ PREFER_VACCINATOR_FIRE = 1<<22, // When using the vaccinator, prefer to use the fire shield
+ BULLET_IMMUNE = 1<<23, // Has a shield that makes the bot immune to bullets
+ BLAST_IMMUNE = 1<<24, // "" blast
+ FIRE_IMMUNE = 1<<25, // "" fire
+ PARACHUTE = 1<<26, // demo/soldier parachute when falling
+ PROJECTILE_SHIELD = 1<<27, // medic projectile shield
+ };
+ void SetAttribute( int attributeFlag );
+ void ClearAttribute( int attributeFlag );
+ void ClearAllAttributes();
+ bool HasAttribute( int attributeFlag ) const;
+
+ enum DifficultyType
+ {
+ UNDEFINED = -1,
+ EASY = 0,
+ NORMAL = 1,
+ HARD = 2,
+ EXPERT = 3,
+
+ NUM_DIFFICULTY_LEVELS
+ };
+ DifficultyType GetDifficulty( void ) const;
+ void SetDifficulty( DifficultyType difficulty );
+ bool IsDifficulty( DifficultyType skill ) const;
+
+ void SetHomeArea( CTFNavArea *area );
+ CTFNavArea *GetHomeArea( void ) const;
+
+ CObjectSentrygun *GetEnemySentry( void ) const; // if we've been attacked/killed by an enemy sentry, this will return it, otherwise NULL
+ void RememberEnemySentry( CObjectSentrygun *sentry, const Vector &injurySpot );
+ const Vector &GetSpotWhereEnemySentryLastInjuredMe( void ) const;
+
+ void SetActionPoint( CTFBotActionPoint *point );
+ CTFBotActionPoint *GetActionPoint( void ) const;
+
+ bool HasProxy( void ) const;
+ void SetProxy( CTFBotProxy *proxy ); // attach this bot to a bot_proxy entity for map I/O communications
+ CTFBotProxy *GetProxy( void ) const;
+
+ bool HasSpawner( void ) const;
+ void SetSpawner( CTFBotGenerator *spawner );
+ CTFBotGenerator *GetSpawner( void ) const;
+
+ void JoinSquad( CTFBotSquad *squad ); // become a member of the given squad
+ void LeaveSquad( void ); // leave our current squad
+ void DeleteSquad( void );
+ bool IsInASquad( void ) const;
+ bool IsSquadmate( CTFPlayer *who ) const; // return true if given bot is in my squad
+ CTFBotSquad *GetSquad( void ) const; // return squad we are in, or NULL
+ float GetSquadFormationError( void ) const; // return normalized error term where 0 = in formation position and 1 = completely out of position
+ void SetSquadFormationError( float error );
+ bool HasBrokenFormation( void ) const; // return true if this bot is far out of formation, or has no path back
+ void SetBrokenFormation( bool state );
+
+ float TransientlyConsistentRandomValue( float period = 10.0f, int seedValue = 0 ) const; // compute a pseudo random value (0-1) that stays consistent for the given period of time, but changes unpredictably each period
+
+ void SetBehaviorFlag( unsigned int flags );
+ void ClearBehaviorFlag( unsigned int flags );
+ bool IsBehaviorFlagSet( unsigned int flags ) const;
+
+ bool FindSplashTarget( CBaseEntity *target, float maxSplashRadius, Vector *splashTarget ) const;
+
+ void GiveRandomItem( loadout_positions_t loadoutPosition );
+
+ enum MissionType
+ {
+ NO_MISSION = 0,
+ MISSION_SEEK_AND_DESTROY, // focus on finding and killing enemy players
+ MISSION_DESTROY_SENTRIES, // focus on finding and destroying enemy sentry guns (and buildings)
+ MISSION_SNIPER, // maintain teams of snipers harassing the enemy
+ MISSION_SPY, // maintain teams of spies harassing the enemy
+ MISSION_ENGINEER, // maintain engineer nests for harassing the enemy
+ MISSION_REPROGRAMMED, // MvM: robot has been hacked and will do bad things to their team
+ };
+ #define MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM false
+ void SetMission( MissionType mission, bool resetBehaviorSystem = true );
+ void SetPrevMission( MissionType mission );
+ MissionType GetMission( void ) const;
+ MissionType GetPrevMission( void ) const;
+ bool HasMission( MissionType mission ) const;
+ bool IsOnAnyMission( void ) const;
+ void SetMissionTarget( CBaseEntity *target );
+ CBaseEntity *GetMissionTarget( void ) const;
+ void SetMissionString( CUtlString string );
+ CUtlString *GetMissionString( void );
+
+ void SetTeleportWhere( const CUtlStringList& teleportWhereName );
+ const CUtlStringList& GetTeleportWhere();
+ void ClearTeleportWhere();
+
+ void SetScaleOverride( float fScale );
+
+ void SetMaxVisionRangeOverride( float range );
+ float GetMaxVisionRangeOverride( void ) const;
+
+ void DisguiseAsMemberOfEnemyTeam( void ); // set Spy disguise to be a class that someone on the enemy team is actually using
+ CBaseObject *GetNearestKnownSappableTarget( void );
+
+ void ClearTags( void );
+ void AddTag( const char *tag );
+ void RemoveTag( const char *tag );
+ bool HasTag( const char *tag );
+
+ Action< CTFBot > *OpportunisticallyUseWeaponAbilities( void );
+
+ CTFPlayer *SelectRandomReachableEnemy( void ); // mostly for MvM - pick a random enemy player that is not in their spawn room
+
+ float GetDesiredPathLookAheadRange( void ) const; // different sized bots used different lookahead distances
+
+ void StartIdleSound( void );
+ void StopIdleSound( void );
+
+ bool ShouldQuickBuild() const { return m_bForceQuickBuild; }
+ void SetShouldQuickBuild( bool bShouldQuickBuild ) { m_bForceQuickBuild = bShouldQuickBuild; }
+
+ void SetAutoJump( float flAutoJumpMin, float flAutoJumpMax ) { m_flAutoJumpMin = flAutoJumpMin; m_flAutoJumpMax = flAutoJumpMax; }
+ bool ShouldAutoJump();
+
+ void SetFlagTarget( CCaptureFlag* pFlag );
+ CCaptureFlag* GetFlagTarget() const { return m_hFollowingFlagTarget; }
+ bool HasFlagTaget() const { return m_hFollowingFlagTarget != NULL; }
+
+ struct EventChangeAttributes_t
+ {
+ EventChangeAttributes_t()
+ {
+ Reset();
+ }
+
+ EventChangeAttributes_t( const EventChangeAttributes_t& copy )
+ {
+ Reset();
+
+ m_eventName = copy.m_eventName;
+
+ m_skill = copy.m_skill;
+ m_weaponRestriction = copy.m_weaponRestriction;
+ m_mission = copy.m_mission;
+ m_prevMission = copy.m_prevMission;
+ m_attributeFlags = copy.m_attributeFlags;
+ m_maxVisionRange = copy.m_maxVisionRange;
+
+ for ( int i=0; i<copy.m_items.Count(); ++i )
+ {
+ m_items.CopyAndAddToTail( copy.m_items[i] );
+ }
+
+ m_itemsAttributes = copy.m_itemsAttributes;
+ m_characterAttributes = copy.m_characterAttributes;
+
+ for ( int i=0; i<copy.m_tags.Count(); ++i )
+ {
+ m_tags.CopyAndAddToTail( copy.m_tags[i] );
+ }
+ }
+
+ void Reset()
+ {
+ m_eventName = "default";
+
+ m_skill = CTFBot::EASY;
+ m_weaponRestriction = CTFBot::ANY_WEAPON;
+ m_mission = CTFBot::NO_MISSION;
+ m_prevMission = m_mission;
+ m_attributeFlags = 0;
+ m_maxVisionRange = -1.f;
+
+ m_items.RemoveAll();
+
+ m_itemsAttributes.RemoveAll();
+ m_characterAttributes.RemoveAll();
+ m_tags.RemoveAll();
+ }
+
+ CUtlString m_eventName;
+
+ DifficultyType m_skill;
+ WeaponRestrictionType m_weaponRestriction;
+ MissionType m_mission;
+ MissionType m_prevMission;
+ int m_attributeFlags;
+ float m_maxVisionRange;
+
+ CUtlStringList m_items;
+
+ struct item_attributes_t
+ {
+ CUtlString m_itemName;
+ CCopyableUtlVector< static_attrib_t > m_attributes;
+ };
+ CUtlVector< item_attributes_t > m_itemsAttributes;
+ CUtlVector< static_attrib_t > m_characterAttributes;
+ CUtlStringList m_tags;
+ };
+ void ClearEventChangeAttributes() { m_eventChangeAttributes.RemoveAll(); }
+ void AddEventChangeAttributes( const EventChangeAttributes_t* newEvent );
+ const EventChangeAttributes_t* GetEventChangeAttributes( const char* pszEventName ) const;
+ void OnEventChangeAttributes( const CTFBot::EventChangeAttributes_t* pEvent );
+
+ void AddItem( const char* pszItemName );
+
+ int GetUberHealthThreshold();
+ float GetUberDeployDelayDuration();
+
+private:
+ CTFBotLocomotion *m_locomotor;
+ CTFBotBody *m_body;
+ CTFBotVision *m_vision;
+
+ CountdownTimer m_lookAtEnemyInvasionAreasTimer;
+
+ CTFNavArea *m_spawnArea; // where we spawned
+ CountdownTimer m_justLostPointTimer;
+
+ int m_weaponRestrictionFlags;
+ int m_attributeFlags;
+ DifficultyType m_difficulty;
+
+ CTFNavArea *m_homeArea;
+
+ CHandle< CTFBotActionPoint > m_actionPoint;
+ CHandle< CTFBotProxy > m_proxy;
+ CHandle< CTFBotGenerator > m_spawner;
+
+ CTFBotSquad *m_squad;
+ bool m_didReselectClass;
+
+ CHandle< CObjectSentrygun > m_enemySentry;
+ Vector m_spotWhereEnemySentryLastInjuredMe; // the last position where I was injured by an enemy sentry
+
+ CUtlVector< SuspectedSpyInfo_t* > m_suspectedSpyVector;
+ CUtlVector< CHandle< CTFPlayer > > m_knownSpyVector;
+
+ CUtlVector< SniperSpotInfo > m_sniperSpotVector; // collection of good sniping spots for the current objective
+
+ CUtlVector< CTFNavArea * > m_sniperVantageAreaVector;
+ CUtlVector< CTFNavArea * > m_sniperTheaterAreaVector;
+
+ CBaseEntity *m_snipingGoalEntity; // the entity we are guarding (control point, payload cart)
+ Vector m_lastSnipingGoalEntityPosition;
+
+ void SetupSniperSpotAccumulation( void ); // do internal setup when control point changes
+ CountdownTimer m_retrySniperSpotSetupTimer;
+
+ bool m_isLookingAroundForEnemies;
+
+ unsigned int m_behaviorFlags; // spawnflags from the bot_generator that spawned us
+ CUtlVector< CFmtStr > m_tags;
+
+ CHandle< CBaseEntity > m_attentionFocusEntity;
+
+ CTeamControlPoint *SelectPointToCapture( CUtlVector< CTeamControlPoint * > *captureVector ) const;
+ CTeamControlPoint *SelectPointToDefend( CUtlVector< CTeamControlPoint * > *defendVector ) const;
+ mutable CHandle< CTeamControlPoint > m_myControlPoint;
+ mutable CountdownTimer m_evaluateControlPointTimer;
+
+ float m_fModelScaleOverride;
+
+ MissionType m_mission;
+ MissionType m_prevMission;
+
+ CHandle< CBaseEntity > m_missionTarget;
+ CUtlString m_missionString;
+
+ CUtlStack< CHandle<CTFWeaponBase> > m_requiredWeaponStack; // if non-empty, bot must equip the weapon on top of the stack
+
+ CountdownTimer m_noisyTimer;
+
+ struct DelayedNoticeInfo
+ {
+ CHandle< CBaseEntity > m_who;
+ float m_when;
+ };
+ CUtlVector< DelayedNoticeInfo > m_delayedNoticeVector;
+
+ float m_maxVisionRangeOverride;
+
+ CountdownTimer m_opportunisticTimer;
+
+ CSoundPatch *m_pIdleSound;
+
+ float m_squadFormationError;
+ bool m_hasBrokenFormation;
+
+ CUtlStringList m_teleportWhereName; // spawn name an engineer mission teleporter will override
+ bool m_bForceQuickBuild;
+
+ float m_flAutoJumpMin;
+ float m_flAutoJumpMax;
+ CountdownTimer m_autoJumpTimer;
+
+ CHandle< CCaptureFlag > m_hFollowingFlagTarget;
+
+ CUtlVector< const EventChangeAttributes_t* > m_eventChangeAttributes;
+};
+
+
+inline void CTFBot::SetTeleportWhere( const CUtlStringList& teleportWhereName )
+{
+ // deep copy strings
+ for ( int i=0; i<teleportWhereName.Count(); ++i )
+ {
+ m_teleportWhereName.CopyAndAddToTail( teleportWhereName[i] );
+ }
+}
+
+inline const CUtlStringList& CTFBot::GetTeleportWhere()
+{
+ return m_teleportWhereName;
+}
+
+inline void CTFBot::ClearTeleportWhere()
+{
+ m_teleportWhereName.RemoveAll();
+}
+
+inline void CTFBot::SetMissionString( CUtlString string )
+{
+ m_missionString = string;
+}
+
+inline CUtlString *CTFBot::GetMissionString( void )
+{
+ return &m_missionString;
+}
+
+inline void CTFBot::SetMissionTarget( CBaseEntity *target )
+{
+ m_missionTarget = target;
+}
+
+inline CBaseEntity *CTFBot::GetMissionTarget( void ) const
+{
+ return m_missionTarget;
+}
+
+inline float CTFBot::GetSquadFormationError( void ) const
+{
+ return m_squadFormationError;
+}
+
+inline void CTFBot::SetSquadFormationError( float error )
+{
+ m_squadFormationError = error;
+}
+
+inline bool CTFBot::HasBrokenFormation( void ) const
+{
+ return m_hasBrokenFormation;
+}
+
+inline void CTFBot::SetBrokenFormation( bool state )
+{
+ m_hasBrokenFormation = state;
+}
+
+inline void CTFBot::SetMaxVisionRangeOverride( float range )
+{
+ m_maxVisionRangeOverride = range;
+}
+
+inline float CTFBot::GetMaxVisionRangeOverride( void ) const
+{
+ return m_maxVisionRangeOverride;
+}
+
+inline void CTFBot::SetBehaviorFlag( unsigned int flags )
+{
+ m_behaviorFlags |= flags;
+}
+
+inline void CTFBot::ClearBehaviorFlag( unsigned int flags )
+{
+ m_behaviorFlags &= ~flags;
+}
+
+inline bool CTFBot::IsBehaviorFlagSet( unsigned int flags ) const
+{
+ return ( m_behaviorFlags & flags ) ? true : false;
+}
+
+inline void CTFBot::StartLookingAroundForEnemies( void )
+{
+ m_isLookingAroundForEnemies = true;
+}
+
+inline void CTFBot::StopLookingAroundForEnemies( void )
+{
+ m_isLookingAroundForEnemies = false;
+}
+
+inline int CTFBot::GetBotType( void ) const
+{
+ return TF_BOT_TYPE;
+}
+
+inline void CTFBot::RememberEnemySentry( CObjectSentrygun *sentry, const Vector &injurySpot )
+{
+ m_enemySentry = sentry;
+ m_spotWhereEnemySentryLastInjuredMe = injurySpot;
+}
+
+inline CObjectSentrygun *CTFBot::GetEnemySentry( void ) const
+{
+ return m_enemySentry;
+}
+
+inline const Vector &CTFBot::GetSpotWhereEnemySentryLastInjuredMe( void ) const
+{
+ return m_spotWhereEnemySentryLastInjuredMe;
+}
+
+inline CTFBot::DifficultyType CTFBot::GetDifficulty( void ) const
+{
+ return m_difficulty;
+}
+
+inline void CTFBot::SetDifficulty( CTFBot::DifficultyType difficulty )
+{
+ m_difficulty = difficulty;
+
+ m_nBotSkill = m_difficulty;
+}
+
+inline bool CTFBot::IsDifficulty( DifficultyType skill ) const
+{
+ return skill == m_difficulty;
+}
+
+inline bool CTFBot::HasProxy( void ) const
+{
+ return m_proxy == NULL ? false : true;
+}
+
+inline void CTFBot::SetProxy( CTFBotProxy *proxy )
+{
+ m_proxy = proxy;
+}
+
+inline CTFBotProxy *CTFBot::GetProxy( void ) const
+{
+ return m_proxy;
+}
+
+inline bool CTFBot::HasSpawner( void ) const
+{
+ return m_spawner == NULL ? false : true;
+}
+
+inline void CTFBot::SetSpawner( CTFBotGenerator *spawner )
+{
+ m_spawner = spawner;
+}
+
+inline CTFBotGenerator *CTFBot::GetSpawner( void ) const
+{
+ return m_spawner;
+}
+
+inline void CTFBot::SetActionPoint( CTFBotActionPoint *point )
+{
+ m_actionPoint = point;
+}
+
+inline CTFBotActionPoint *CTFBot::GetActionPoint( void ) const
+{
+ return m_actionPoint;
+}
+
+inline bool CTFBot::IsInASquad( void ) const
+{
+ return m_squad == NULL ? false : true;
+}
+
+inline CTFBotSquad *CTFBot::GetSquad( void ) const
+{
+ return m_squad;
+}
+
+inline void CTFBot::SetHomeArea( CTFNavArea *area )
+{
+ m_homeArea = area;
+}
+
+inline CTFNavArea *CTFBot::GetHomeArea( void ) const
+{
+ return m_homeArea;
+}
+
+inline void CTFBot::ClearWeaponRestrictions( void )
+{
+ m_weaponRestrictionFlags = 0;
+}
+
+inline void CTFBot::SetWeaponRestriction( int restrictionFlags )
+{
+ m_weaponRestrictionFlags |= restrictionFlags;
+}
+
+inline bool CTFBot::HasWeaponRestriction( int restrictionFlags ) const
+{
+ return m_weaponRestrictionFlags & restrictionFlags ? true : false;
+}
+
+inline void CTFBot::SetAttribute( int attributeFlag )
+{
+ m_attributeFlags |= attributeFlag;
+}
+
+inline void CTFBot::ClearAttribute( int attributeFlag )
+{
+ m_attributeFlags &= ~attributeFlag;
+}
+
+inline void CTFBot::ClearAllAttributes()
+{
+ m_attributeFlags = 0;
+}
+
+inline bool CTFBot::HasAttribute( int attributeFlag ) const
+{
+ return m_attributeFlags & attributeFlag ? true : false;
+}
+
+inline CTFNavArea *CTFBot::GetSpawnArea( void ) const
+{
+ return m_spawnArea;
+}
+
+inline bool CTFBot::WasPointJustLost( void ) const
+{
+ return m_justLostPointTimer.HasStarted() && !m_justLostPointTimer.IsElapsed();
+}
+
+inline const CUtlVector< CTFBot::SniperSpotInfo > *CTFBot::GetSniperSpots( void ) const
+{
+ return &m_sniperSpotVector;
+}
+
+inline bool CTFBot::HasSniperSpots( void ) const
+{
+ return m_sniperSpotVector.Count() > 0 ? true : false;
+}
+
+inline CTFBot::MissionType CTFBot::GetMission( void ) const
+{
+ return m_mission;
+}
+
+inline void CTFBot::SetPrevMission( MissionType mission )
+{
+ m_prevMission = mission;
+}
+
+inline CTFBot::MissionType CTFBot::GetPrevMission( void ) const
+{
+ return m_prevMission;
+}
+
+inline bool CTFBot::HasMission( MissionType mission ) const
+{
+ return m_mission == mission ? true : false;
+}
+
+inline bool CTFBot::IsOnAnyMission( void ) const
+{
+ return m_mission == NO_MISSION ? false : true;
+}
+
+inline void CTFBot::SetScaleOverride( float fScale )
+{
+ m_fModelScaleOverride = fScale;
+
+ SetModelScale( m_fModelScaleOverride > 0.0f ? m_fModelScaleOverride : 1.0f );
+}
+
+inline bool CTFBot::IsEnvironmentNoisy( void ) const
+{
+ return !m_noisyTimer.IsElapsed();
+}
+
+//---------------------------------------------------------------------------------------------
+inline CTFBot *ToTFBot( CBaseEntity *pEntity )
+{
+ if ( !pEntity || !pEntity->IsPlayer() || !ToTFPlayer( pEntity )->IsBotOfType( TF_BOT_TYPE ) )
+ return NULL;
+
+ Assert( "***IMPORTANT!!! DONT IGNORE ME!!!***" && dynamic_cast< CTFBot * >( pEntity ) != 0 );
+
+ return static_cast< CTFBot * >( pEntity );
+}
+
+
+//---------------------------------------------------------------------------------------------
+inline const CTFBot *ToTFBot( const CBaseEntity *pEntity )
+{
+ if ( !pEntity || !pEntity->IsPlayer() || !ToTFPlayer( const_cast< CBaseEntity * >( pEntity ) )->IsBotOfType( TF_BOT_TYPE ) )
+ return NULL;
+
+ Assert( "***IMPORTANT!!! DONT IGNORE ME!!!***" && dynamic_cast< const CTFBot * >( pEntity ) != 0 );
+
+ return static_cast< const CTFBot * >( pEntity );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Functor used with NavAreaBuildPath()
+ */
+class CTFBotPathCost : public IPathCost
+{
+public:
+ CTFBotPathCost( CTFBot *me, RouteType routeType )
+ {
+ m_me = me;
+ m_routeType = routeType;
+ m_stepHeight = me->GetLocomotionInterface()->GetStepHeight();
+ m_maxJumpHeight = me->GetLocomotionInterface()->GetMaxJumpHeight();
+ m_maxDropHeight = me->GetLocomotionInterface()->GetDeathDropHeight();
+ }
+
+ virtual float operator()( CNavArea *baseArea, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const
+ {
+ VPROF_BUDGET( "CTFBotPathCost::operator()", "NextBot" );
+
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ if ( fromArea == NULL )
+ {
+ // first area in path, no cost
+ return 0.0f;
+ }
+ else
+ {
+ if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) )
+ {
+ return -1.0f;
+ }
+
+ // in training, avoid capturing the point until the human trainee does so
+ if ( TFGameRules()->IsInTraining() &&
+ area->HasAttributeTF( TF_NAV_CONTROL_POINT ) &&
+ !m_me->IsAnyPointBeingCaptured() &&
+ !m_me->IsPlayerClass( TF_CLASS_ENGINEER ) ) // allow engineers to path so they can test travel distance for sentry placement
+ {
+ return -1.0f;
+ }
+
+ // don't path through enemy spawn rooms
+ if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) ||
+ ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) )
+ {
+ if ( !TFGameRules()->RoundHasBeenWon() )
+ {
+ return -1.0f;
+ }
+ }
+
+ // compute distance traveled along path so far
+ float dist;
+
+ if ( ladder )
+ {
+ dist = ladder->m_length;
+ }
+ else if ( length > 0.0 )
+ {
+ dist = length;
+ }
+ else
+ {
+ dist = ( area->GetCenter() - fromArea->GetCenter() ).Length();
+ }
+
+
+ // check height change
+ float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area );
+
+ if ( deltaZ >= m_stepHeight )
+ {
+ if ( deltaZ >= m_maxJumpHeight )
+ {
+ // too high to reach
+ return -1.0f;
+ }
+
+ // jumping is slower than flat ground
+ const float jumpPenalty = 2.0f;
+ dist *= jumpPenalty;
+ }
+ else if ( deltaZ < -m_maxDropHeight )
+ {
+ // too far to drop
+ return -1.0f;
+ }
+
+ // add a random penalty unique to this character so they choose different routes to the same place
+ float preference = 1.0f;
+
+ if ( m_routeType == DEFAULT_ROUTE && !m_me->IsMiniBoss() )
+ {
+ // this term causes the same bot to choose different routes over time,
+ // but keep the same route for a period in case of repaths
+ int timeMod = (int)( gpGlobals->curtime / 10.0f ) + 1;
+ preference = 1.0f + 50.0f * ( 1.0f + FastCos( (float)( m_me->GetEntity()->entindex() * area->GetID() * timeMod ) ) );
+ }
+
+ if ( m_routeType == SAFEST_ROUTE )
+ {
+ // avoid combat areas
+ if ( area->IsInCombat() )
+ {
+ const float combatDangerCost = 4.0f;
+ dist *= combatDangerCost * area->GetCombatIntensity();
+ }
+
+ // if this area exposes us to enemy sentry fire, avoid it
+ const float sentryDangerCost = 5.0f;
+ if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER ) ) ||
+ ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_RED_SENTRY_DANGER ) ) )
+ {
+ dist *= sentryDangerCost;
+ }
+ }
+
+ if ( m_me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ int enemyTeam = GetEnemyTeam( m_me->GetTeamNumber() );
+
+ // Since spies can get right up to enemy buildings, avoid them.
+ for ( int oit = 0; oit < IBaseObjectAutoList::AutoList().Count(); ++oit )
+ {
+ CBaseObject *enemyObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[ oit ] );
+
+ if ( ( enemyObj->ObjectType() == OBJ_SENTRYGUN ) &&
+ ( enemyObj->GetTeamNumber() == enemyTeam ) )
+ {
+ enemyObj->UpdateLastKnownArea();
+
+ if ( enemyObj->GetLastKnownArea() == area )
+ {
+ // There is an enemy building in this area - avoid it as a spy.
+ const float enemyBuildingCost = 10.0f;
+ dist *= enemyBuildingCost;
+ }
+ }
+ }
+
+ // Spies avoid teammates, since they draw attention and gunfire.
+ const float teammateCost = 10.0f;
+ dist += dist * teammateCost * area->GetPlayerCount( m_me->GetTeamNumber() );
+
+ // We shouldn't be getting NaNs here. It will be handled when we return, but ideally
+ // it should be fixed here and not just worked around in NavAreaBuildPath.
+ DebuggerBreakOnNaN_StagingOnly( dist );
+ }
+
+ float cost = ( dist * preference );
+
+ if ( area->HasAttributes( NAV_MESH_FUNC_COST ) )
+ {
+ cost *= area->ComputeFuncNavCost( m_me );
+ DebuggerBreakOnNaN_StagingOnly( cost );
+ }
+
+ return cost + fromArea->GetCostSoFar();
+ }
+ }
+
+ CTFBot *m_me;
+ RouteType m_routeType;
+ float m_stepHeight;
+ float m_maxJumpHeight;
+ float m_maxDropHeight;
+};
+
+
+//---------------------------------------------------------------------------------------------
+class CClosestTFPlayer
+{
+public:
+ CClosestTFPlayer( const Vector &where, int team = TEAM_ANY )
+ {
+ m_where = where;
+ m_closeRangeSq = FLT_MAX;
+ m_closePlayer = NULL;
+ m_team = team;
+ }
+
+ CClosestTFPlayer( CBaseEntity *entity, int team = TEAM_ANY )
+ {
+ m_where = entity->WorldSpaceCenter();
+ m_closeRangeSq = FLT_MAX;
+ m_closePlayer = NULL;
+ m_team = team;
+ }
+
+ bool operator() ( CBasePlayer *player )
+ {
+ if ( !player->IsAlive() )
+ return true;
+
+ if ( player->GetTeamNumber() != TF_TEAM_RED && player->GetTeamNumber() != TF_TEAM_BLUE )
+ return true;
+
+ if ( m_team != TEAM_ANY && player->GetTeamNumber() != m_team )
+ return true;
+
+ CTFBot *bot = ToTFBot( player );
+ if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) )
+ return true;
+
+ float rangeSq = ( m_where - player->GetAbsOrigin() ).LengthSqr();
+ if ( rangeSq < m_closeRangeSq )
+ {
+ m_closeRangeSq = rangeSq;
+ m_closePlayer = player;
+ }
+ return true;
+ }
+
+ Vector m_where;
+ float m_closeRangeSq;
+ CBasePlayer *m_closePlayer;
+ int m_team;
+};
+
+
+#endif // TF_BOT_H
diff --git a/game/server/tf/bot/tf_bot_body.cpp b/game/server/tf/bot/tf_bot_body.cpp
new file mode 100644
index 0000000..b1b87c5
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_body.cpp
@@ -0,0 +1,42 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_body.cpp
+// Team Fortress NextBot body interface
+// Michael Booth, May 2010
+
+#include "cbase.h"
+
+#include "tf_bot.h"
+#include "tf_bot_body.h"
+
+
+//
+// Return how often we should sample our target's position and
+// velocity to update our aim tracking, to allow realistic slop in tracking
+//
+float CTFBotBody::GetHeadAimTrackingInterval( void ) const
+{
+ CTFBot *me = (CTFBot *)GetBot();
+
+ // don't let Spies in MvM mode aim too precisely
+ if ( TFGameRules()->IsMannVsMachineMode() && me->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ return 0.25f;
+ }
+
+ switch( me->GetDifficulty() )
+ {
+ case CTFBot::EXPERT:
+ return 0.05f;
+
+ case CTFBot::HARD:
+ return 0.1f;
+
+ case CTFBot::NORMAL:
+ return 0.25f;
+
+ case CTFBot::EASY:
+ return 1.0f;
+ }
+
+ return 0.0f;
+}
diff --git a/game/server/tf/bot/tf_bot_body.h b/game/server/tf/bot/tf_bot_body.h
new file mode 100644
index 0000000..3c18d89
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_body.h
@@ -0,0 +1,24 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_body.h
+// Team Fortress NextBot body interface
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_BODY_H
+#define TF_BOT_BODY_H
+
+#include "NextBot/Player/NextBotPlayerBody.h"
+
+//----------------------------------------------------------------------------
+class CTFBotBody : public PlayerBody
+{
+public:
+ CTFBotBody( INextBot *bot ) : PlayerBody( bot )
+ {
+ }
+
+ virtual ~CTFBotBody() { }
+
+ virtual float GetHeadAimTrackingInterval( void ) const; // return how often we should sample our target's position and velocity to update our aim tracking, to allow realistic slop in tracking
+};
+
+#endif // TF_BOT_BODY_H \ No newline at end of file
diff --git a/game/server/tf/bot/tf_bot_locomotion.cpp b/game/server/tf/bot/tf_bot_locomotion.cpp
new file mode 100644
index 0000000..8ff19c1
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_locomotion.cpp
@@ -0,0 +1,137 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_locomotion.cpp
+// Team Fortress NextBot locomotion interface
+// Michael Booth, May 2010
+
+#include "cbase.h"
+
+#include "tf_bot.h"
+#include "tf_bot_locomotion.h"
+#include "particle_parse.h"
+
+
+//-----------------------------------------------------------------------------------------
+void CTFBotLocomotion::Update( void )
+{
+ BaseClass::Update();
+
+ CTFBot *me = ToTFBot( GetBot()->GetEntity() );
+ if ( !me )
+ {
+ return;
+ }
+
+ // always 'crouch jump'
+ if ( IsOnGround() )
+ {
+ if ( !me->IsPlayerClass( TF_CLASS_ENGINEER ) )
+ {
+ // engineers need to crouch behind their guns
+ me->ReleaseCrouchButton();
+ }
+ }
+ else
+ {
+ me->PressCrouchButton( 0.3f );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Move directly towards the given position
+void CTFBotLocomotion::Approach( const Vector &pos, float goalWeight )
+{
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ if ( !IsOnGround() && !IsClimbingOrJumping() )
+ {
+ // no air control
+ return;
+ }
+ }
+
+ BaseClass::Approach( pos, goalWeight );
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Distance at which we will die if we fall
+float CTFBotLocomotion::GetDeathDropHeight( void ) const
+{
+ return 1000.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Get maximum running speed
+float CTFBotLocomotion::GetRunSpeed( void ) const
+{
+ CTFBot *me = (CTFBot *)GetBot()->GetEntity();
+ return me->GetPlayerClass()->GetMaxSpeed();
+}
+
+
+//-----------------------------------------------------------------------------------------
+// Return true if given area can be used for navigation
+bool CTFBotLocomotion::IsAreaTraversable( const CNavArea *baseArea ) const
+{
+ CTFBot *me = (CTFBot *)GetBot()->GetEntity();
+ CTFNavArea *area = (CTFNavArea *)baseArea;
+
+ if ( area->IsBlocked( me->GetTeamNumber() ) )
+ {
+ return false;
+ }
+
+ if ( !TFGameRules()->RoundHasBeenWon() || TFGameRules()->GetWinningTeam() != me->GetTeamNumber() )
+ {
+ if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) && me->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ return false;
+ }
+
+ if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) && me->GetTeamNumber() == TF_TEAM_RED )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------
+bool CTFBotLocomotion::IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when ) const
+{
+ // assume all players are "traversable" in that they will move or can be killed
+ if ( obstacle && obstacle->IsPlayer() )
+ {
+ return true;
+ }
+
+ return PlayerLocomotion::IsEntityTraversable( obstacle, when );
+}
+
+
+void CTFBotLocomotion::Jump( void )
+{
+ BaseClass::Jump();
+
+ CTFBot *me = ToTFBot( GetBot()->GetEntity() );
+ if ( !me )
+ {
+ return;
+ }
+
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ int iCustomJumpParticle = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( me, iCustomJumpParticle, bot_custom_jump_particle );
+ if ( iCustomJumpParticle )
+ {
+ const char *pEffectName = "rocketjump_smoke";
+ DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, me, "foot_L" );
+ DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, me, "foot_R" );
+ }
+ }
+}
diff --git a/game/server/tf/bot/tf_bot_locomotion.h b/game/server/tf/bot/tf_bot_locomotion.h
new file mode 100644
index 0000000..40cad56
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_locomotion.h
@@ -0,0 +1,50 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_locomotion.h
+// Team Fortress NextBot locomotion interface
+// Michael Booth, May 2010
+
+#ifndef TF_BOT_LOCOMOTION_H
+#define TF_BOT_LOCOMOTION_H
+
+#include "NextBot/Player/NextBotPlayerLocomotion.h"
+
+//----------------------------------------------------------------------------
+class CTFBotLocomotion : public PlayerLocomotion
+{
+public:
+ DECLARE_CLASS( CTFBotLocomotion, PlayerLocomotion );
+
+ CTFBotLocomotion( INextBot *bot ) : PlayerLocomotion( bot )
+ {
+ }
+
+ virtual ~CTFBotLocomotion() { }
+
+ virtual void Update( void ); // (EXTEND) update internal state
+
+ virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position
+
+ virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump
+ virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall
+
+ virtual float GetRunSpeed( void ) const; // get maximum running speed
+
+ virtual bool IsAreaTraversable( const CNavArea *baseArea ) const; // return true if given area can be used for navigation
+ virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const;
+
+ //
+ // ILocomotion modifiers
+ //
+ virtual void Jump( void ) OVERRIDE; // initiate a simple undirected jump in the air
+
+protected:
+ virtual void AdjustPosture( const Vector &moveGoal ) { } // never crouch to navigate
+};
+
+inline float CTFBotLocomotion::GetMaxJumpHeight( void ) const
+{
+ // http://developer.valvesoftware.com/wiki/TF2/Team_Fortress_2_Mapper%27s_Reference
+ return 72.0f;
+}
+
+#endif // TF_BOT_LOCOMOTION_H
diff --git a/game/server/tf/bot/tf_bot_manager.cpp b/game/server/tf/bot/tf_bot_manager.cpp
new file mode 100644
index 0000000..6ead184
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_manager.cpp
@@ -0,0 +1,872 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//----------------------------------------------------------------------------------------------------------------
+// tf_bot_manager.cpp
+// Team Fortress NextBotManager
+// Tom Bui, February 2010
+//----------------------------------------------------------------------------------------------------------------
+
+#include "cbase.h"
+#include "tf_bot_manager.h"
+
+#include "Player/NextBotPlayer.h"
+#include "team.h"
+#include "tf_bot.h"
+#include "tf_gamerules.h"
+#include "bot/map_entities/tf_bot_hint.h"
+#include "bot/map_entities/tf_bot_hint_sentrygun.h"
+#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"
+
+
+//----------------------------------------------------------------------------------------------------------------
+
+// Creates and sets CTFBotManager as the NextBotManager singleton
+static CTFBotManager sTFBotManager;
+
+extern ConVar tf_bot_force_class;
+ConVar tf_bot_difficulty( "tf_bot_difficulty", "1", FCVAR_NONE, "Defines the skill of bots joining the game. Values are: 0=easy, 1=normal, 2=hard, 3=expert." );
+ConVar tf_bot_quota( "tf_bot_quota", "0", FCVAR_NONE, "Determines the total number of tf bots in the game." );
+ConVar tf_bot_quota_mode( "tf_bot_quota_mode", "normal", FCVAR_NONE, "Determines the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is bot_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is bot_quota." );
+ConVar tf_bot_join_after_player( "tf_bot_join_after_player", "1", FCVAR_NONE, "If nonzero, bots wait until a player joins before entering the game." );
+ConVar tf_bot_auto_vacate( "tf_bot_auto_vacate", "1", FCVAR_NONE, "If nonzero, bots will automatically leave to make room for human players." );
+ConVar tf_bot_offline_practice( "tf_bot_offline_practice", "0", FCVAR_NONE, "Tells the server that it is in offline practice mode." );
+ConVar tf_bot_melee_only( "tf_bot_melee_only", "0", FCVAR_GAMEDLL, "If nonzero, TFBots will only use melee weapons" );
+
+extern const char *GetRandomBotName( void );
+extern void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize );
+
+static bool UTIL_KickBotFromTeam( int kickTeam )
+{
+ int i;
+
+ // try to kick a dead bot first
+ for ( i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);
+
+ if (pBot == NULL)
+ continue;
+
+ if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false )
+ continue;
+
+ if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 )
+ continue;
+
+ if ( !pPlayer->IsAlive() && pPlayer->GetTeamNumber() == kickTeam )
+ {
+ // its a bot on the right team - kick it
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) );
+
+ return true;
+ }
+ }
+
+ // no dead bots, kick any bot on the given team
+ for ( i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);
+
+ if (pBot == NULL)
+ continue;
+
+ if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false )
+ continue;
+
+ if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 )
+ continue;
+
+ if (pPlayer->GetTeamNumber() == kickTeam)
+ {
+ // its a bot on the right team - kick it
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) );
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//----------------------------------------------------------------------------------------------------------------
+
+CTFBotManager::CTFBotManager()
+ : NextBotManager()
+ , m_flNextPeriodicThink( 0 )
+{
+ NextBotManager::SetInstance( this );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+CTFBotManager::~CTFBotManager()
+{
+ NextBotManager::SetInstance( NULL );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::OnMapLoaded( void )
+{
+ NextBotManager::OnMapLoaded();
+
+ ClearStuckBotData();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::OnRoundRestart( void )
+{
+ NextBotManager::OnRoundRestart();
+
+ // clear all hint ownership
+ CTFBotHint *hint = NULL;
+ while( ( hint = (CTFBotHint *)( gEntList.FindEntityByClassname( hint, "func_tfbot_hint" ) ) ) != NULL )
+ {
+ hint->SetOwnerEntity( NULL );
+ }
+
+ CTFBotHintSentrygun *sentryHint = NULL;
+ while( ( sentryHint = (CTFBotHintSentrygun *)( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) != NULL )
+ {
+ sentryHint->SetOwnerEntity( NULL );
+ }
+
+ CTFBotHintTeleporterExit *teleporterHint = NULL;
+ while( ( teleporterHint = (CTFBotHintTeleporterExit *)( gEntList.FindEntityByClassname( teleporterHint, "bot_hint_teleporter_exit" ) ) ) != NULL )
+ {
+ teleporterHint->SetOwnerEntity( NULL );
+ }
+
+
+#ifdef TF_CREEP_MODE
+ m_creepExperience[ TF_TEAM_RED ] = 0;
+ m_creepExperience[ TF_TEAM_BLUE ] = 0;
+#endif
+
+ m_isMedeivalBossScenarioSetup = false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::Update()
+{
+ MaintainBotQuota();
+
+ DrawStuckBotData();
+
+#ifdef TF_CREEP_MODE
+ UpdateCreepWaves();
+#endif
+
+ NextBotManager::Update();
+}
+
+
+#ifdef TF_CREEP_MODE
+ConVar tf_creep_initial_delay( "tf_creep_initial_delay", "30" );
+ConVar tf_creep_wave_interval( "tf_creep_wave_interval", "30" );
+ConVar tf_creep_wave_count( "tf_creep_wave_count", "3" );
+ConVar tf_creep_class( "tf_creep_class", "heavyweapons" );
+ConVar tf_creep_level_up( "tf_creep_level_up", "6" );
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::UpdateCreepWaves()
+{
+ if ( !TFGameRules()->IsCreepWaveMode() )
+ return;
+
+ if ( TFGameRules()->RoundHasBeenWon() )
+ {
+ // no more creep waves - game is over
+ return;
+ }
+
+ if ( TFGameRules()->InSetup() || TFGameRules()->State_Get() == GR_STATE_STARTGAME || TFGameRules()->State_Get() == GR_STATE_PREROUND )
+ {
+ // no creeps at start of round
+ m_creepWaveTimer.Start( tf_creep_initial_delay.GetFloat() );
+
+ // delete all creeps
+ for( int i=1; i<=gpGlobals->maxClients; ++i )
+ {
+ CBasePlayer *player = static_cast< CBasePlayer * >( UTIL_PlayerByIndex( i ) );
+
+ if ( !player )
+ continue;
+
+ if ( FNullEnt( player->edict() ) )
+ continue;
+
+ CTFBot *creep = ToTFBot( player );
+ if ( !creep || !creep->HasAttribute( CTFBot::IS_NPC ) )
+ continue;
+
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
+ }
+
+ return;
+ }
+
+ if ( m_creepWaveTimer.IsElapsed() )
+ {
+ m_creepWaveTimer.Start( tf_creep_wave_interval.GetFloat() );
+
+ SpawnCreepWave( TF_TEAM_RED );
+ SpawnCreepWave( TF_TEAM_BLUE );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::SpawnCreepWave( int team )
+{
+ CTFBotSquad *squad = new CTFBotSquad;
+
+ for( int i=0; i<tf_creep_wave_count.GetInt(); ++i )
+ {
+ SpawnCreep( team, squad );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::SpawnCreep( int team, CTFBotSquad *squad )
+{
+ CTFBot *bot = NextBotCreatePlayerBot< CTFBot >( "Creep" );
+
+ if ( !bot )
+ return;
+
+ bot->SetAttribute( CTFBot::IS_NPC );
+ bot->HandleCommand_JoinTeam( team == TF_TEAM_RED ? "red" : "blue" );
+ bot->SetDifficulty( CTFBot::NORMAL );
+ bot->HandleCommand_JoinClass( tf_creep_class.GetString() );
+ bot->JoinSquad( squad );
+ bot->AddGlowEffect();
+ //BotGenerateAndWearItem( bot, "Honest Halo" );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::OnCreepKilled( CTFPlayer *killer )
+{
+ CTFBot *bot = ToTFBot( killer );
+ if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) )
+ return;
+
+ ++m_creepExperience[ killer->GetTeamNumber() ];
+
+/*
+ int xp = m_creepExperience[ killer->GetTeamNumber() ];
+ int level = xp / tf_creep_level_up.GetInt();
+ int left = xp % tf_creep_level_up.GetInt();
+
+ char text[256];
+ Q_snprintf( text, sizeof(text), "%s killed a creep. %s team LVL = %d+%d/%d\n",
+ killer->GetPlayerName(),
+ killer->GetTeamNumber() == TF_TEAM_RED ? "Red" : "Blue",
+ level+1, left, tf_creep_level_up.GetInt() );
+
+ UTIL_ClientPrintAll( HUD_PRINTTALK, text );
+*/
+
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "%s killed a creep" );
+}
+
+#endif // TF_CREEP_MODE
+
+//----------------------------------------------------------------------------------------------------------------
+bool CTFBotManager::RemoveBotFromTeamAndKick( int nTeam )
+{
+ CUtlVector< CTFPlayer* > vecCandidates;
+
+ // Gather potential candidates
+ for ( int i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer == NULL )
+ continue;
+
+ if ( FNullEnt( pPlayer->edict() ) )
+ continue;
+
+ if ( !pPlayer->IsConnected() )
+ continue;
+
+ CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer );
+ if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
+ {
+ if ( pBot->GetTeamNumber() == nTeam )
+ {
+ vecCandidates.AddToTail( pPlayer );
+ }
+ }
+ }
+
+ CTFPlayer *pVictim = NULL;
+ if ( vecCandidates.Count() > 0 )
+ {
+ // first look for bots that are currently dead
+ FOR_EACH_VEC( vecCandidates, i )
+ {
+ CTFPlayer *pPlayer = vecCandidates[i];
+ if ( pPlayer && !pPlayer->IsAlive() )
+ {
+ pVictim = pPlayer;
+ break;
+ }
+ }
+
+ // if we didn't fine one, try to kick anyone on the team
+ if ( !pVictim )
+ {
+ FOR_EACH_VEC( vecCandidates, i )
+ {
+ CTFPlayer *pPlayer = vecCandidates[i];
+ if ( pPlayer )
+ {
+ pVictim = pPlayer;
+ break;
+ }
+ }
+ }
+ }
+
+ if ( pVictim )
+ {
+ if ( pVictim->IsAlive() )
+ {
+ pVictim->CommitSuicide();
+ }
+ pVictim->ForceChangeTeam( TEAM_UNASSIGNED ); // skipping TEAM_SPECTATOR because some servers don't allow spectators
+ UTIL_KickBotFromTeam( TEAM_UNASSIGNED );
+ return true;
+ }
+
+ return false;
+}
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::MaintainBotQuota()
+{
+ if ( TheNavMesh->IsGenerating() )
+ return;
+
+ if ( g_fGameOver )
+ return;
+
+ // new players can't spawn immediately after the round has been going for some time
+ if ( !TFGameRules() )
+ return;
+
+ // training mode controls the bots
+ if ( TFGameRules()->IsInTraining() )
+ return;
+
+ // if it is not time to do anything...
+ if ( gpGlobals->curtime < m_flNextPeriodicThink )
+ return;
+
+ // think every quarter second
+ m_flNextPeriodicThink = gpGlobals->curtime + 0.25f;
+
+ // don't add bots until local player has been registered, to make sure he's player ID #1
+ if ( !engine->IsDedicatedServer() )
+ {
+ CBasePlayer *pPlayer = UTIL_GetListenServerHost();
+ if ( !pPlayer )
+ return;
+ }
+
+ // We want to balance based on who's playing on game teams not necessary who's on team spectator, etc.
+ int nConnectedClients = 0;
+ int nTFBots = 0;
+ int nTFBotsOnGameTeams = 0;
+ int nNonTFBotsOnGameTeams = 0;
+ int nSpectators = 0;
+ for ( int i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer == NULL )
+ continue;
+
+ if ( FNullEnt( pPlayer->edict() ) )
+ continue;
+
+ if ( !pPlayer->IsConnected() )
+ continue;
+
+ CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer );
+ if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
+ {
+ nTFBots++;
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ nTFBotsOnGameTeams++;
+ }
+ }
+ else
+ {
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ nNonTFBotsOnGameTeams++;
+ }
+ else if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ nSpectators++;
+ }
+ }
+
+ nConnectedClients++;
+ }
+
+ int desiredBotCount = tf_bot_quota.GetInt();
+ int nTotalNonTFBots = nConnectedClients - nTFBots;
+
+ if ( FStrEq( tf_bot_quota_mode.GetString(), "fill" ) )
+ {
+ desiredBotCount = MAX( 0, desiredBotCount - nNonTFBotsOnGameTeams );
+ }
+ else if ( FStrEq( tf_bot_quota_mode.GetString(), "match" ) )
+ {
+ // If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans
+ desiredBotCount = (int)MAX( 0, tf_bot_quota.GetFloat() * nNonTFBotsOnGameTeams );
+ }
+
+ // wait for a player to join, if necessary
+ if ( tf_bot_join_after_player.GetBool() )
+ {
+ if ( ( nNonTFBotsOnGameTeams == 0 ) && ( nSpectators == 0 ) )
+ {
+ desiredBotCount = 0;
+ }
+ }
+
+ // if bots will auto-vacate, we need to keep one slot open to allow players to join
+ if ( tf_bot_auto_vacate.GetBool() )
+ {
+ desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots - 1 );
+ }
+ else
+ {
+ desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots );
+ }
+
+ // add bots if necessary
+ if ( desiredBotCount > nTFBotsOnGameTeams )
+ {
+ // don't try to add a bot if it would unbalance
+ if ( !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_BLUE, TEAM_UNASSIGNED ) ||
+ !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_RED, TEAM_UNASSIGNED ) )
+ {
+ CTFBot *pBot = GetAvailableBotFromPool();
+ if ( pBot == NULL )
+ {
+ pBot = NextBotCreatePlayerBot< CTFBot >( GetRandomBotName() );
+ }
+ if ( pBot )
+ {
+ pBot->SetAttribute( CTFBot::QUOTA_MANANGED );
+
+ // join a team before we pick our class, since we use our teammates to decide what class to be
+ pBot->HandleCommand_JoinTeam( "auto" );
+
+ const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? pBot->GetNextSpawnClassname() : tf_bot_force_class.GetString();
+ pBot->HandleCommand_JoinClass( classname );
+
+ // give the bot a proper name
+ char name[256];
+ CTFBot::DifficultyType skill = pBot->GetDifficulty();
+ CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof( name ) );
+ engine->SetFakeClientConVarValue( pBot->edict(), "name", name );
+
+ // Keep track of any bots we add during a match
+ CMatchInfo *pMatchInfo = GTFGCClientSystem()->GetMatch();
+ if ( pMatchInfo )
+ {
+ pMatchInfo->m_nBotsAdded++;
+ }
+ }
+ }
+ }
+ else if ( desiredBotCount < nTFBotsOnGameTeams )
+ {
+ // kick a bot to maintain quota
+
+ // first remove any unassigned bots
+ if ( UTIL_KickBotFromTeam( TEAM_UNASSIGNED ) )
+ return;
+
+ int kickTeam;
+
+ CTeam *pRed = GetGlobalTeam( TF_TEAM_RED );
+ CTeam *pBlue = GetGlobalTeam( TF_TEAM_BLUE );
+
+ // remove from the team that has more players
+ if ( pBlue->GetNumPlayers() > pRed->GetNumPlayers() )
+ {
+ kickTeam = TF_TEAM_BLUE;
+ }
+ else if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() )
+ {
+ kickTeam = TF_TEAM_RED;
+ }
+ // remove from the team that's winning
+ else if ( pBlue->GetScore() > pRed->GetScore() )
+ {
+ kickTeam = TF_TEAM_BLUE;
+ }
+ else if ( pBlue->GetScore() < pRed->GetScore() )
+ {
+ kickTeam = TF_TEAM_RED;
+ }
+ else
+ {
+ // teams and scores are equal, pick a team at random
+ kickTeam = (RandomInt( 0, 1 ) == 0) ? TF_TEAM_BLUE : TF_TEAM_RED;
+ }
+
+ // attempt to kick a bot from the given team
+ if ( UTIL_KickBotFromTeam( kickTeam ) )
+ return;
+
+ // if there were no bots on the team, kick a bot from the other team
+ UTIL_KickBotFromTeam( kickTeam == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool CTFBotManager::IsAllBotTeam( int iTeam )
+{
+ CTeam *pTeam = GetGlobalTeam( iTeam );
+ if ( pTeam == NULL )
+ {
+ return false;
+ }
+
+ // check to see if any players on the team are humans
+ for ( int i = 0, n = pTeam->GetNumPlayers(); i < n; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
+ if ( pPlayer == NULL )
+ {
+ continue;
+ }
+ if ( pPlayer->IsBot() == false )
+ {
+ return false;
+ }
+ }
+
+ // if we made it this far, then they must all be bots!
+ if ( pTeam->GetNumPlayers() != 0 )
+ {
+ return true;
+ }
+
+ // okay, this is a bit trickier...
+ // if there are no people on this team, then we need to check the "assigned" human team
+ return TFGameRules()->GetAssignedHumanTeam() != iTeam;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::SetIsInOfflinePractice(bool bIsInOfflinePractice)
+{
+ tf_bot_offline_practice.SetValue( bIsInOfflinePractice ? 1 : 0 );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool CTFBotManager::IsInOfflinePractice() const
+{
+ return tf_bot_offline_practice.GetInt() != 0;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool CTFBotManager::IsMeleeOnly() const
+{
+ return tf_bot_melee_only.GetBool();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::RevertOfflinePracticeConvars()
+{
+ tf_bot_quota.Revert();
+ tf_bot_quota_mode.Revert();
+ tf_bot_auto_vacate.Revert();
+ tf_bot_difficulty.Revert();
+ tf_bot_offline_practice.Revert();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::LevelShutdown()
+{
+ m_flNextPeriodicThink = 0.0f;
+ if ( IsInOfflinePractice() )
+ {
+ RevertOfflinePracticeConvars();
+ SetIsInOfflinePractice( false );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+CTFBot* CTFBotManager::GetAvailableBotFromPool()
+{
+ for ( int i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);
+
+ if (pBot == NULL)
+ continue;
+
+ if ( ( pBot->GetFlags() & FL_FAKECLIENT ) == 0 )
+ continue;
+
+ if ( pBot->GetTeamNumber() == TEAM_SPECTATOR || pBot->GetTeamNumber() == TEAM_UNASSIGNED )
+ {
+ pBot->ClearAttribute( CTFBot::QUOTA_MANANGED );
+ return pBot;
+ }
+ }
+ return NULL;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::OnForceAddedBots( int iNumAdded )
+{
+ tf_bot_quota.SetValue( tf_bot_quota.GetInt() + iNumAdded );
+ m_flNextPeriodicThink = gpGlobals->curtime + 1.0f;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::OnForceKickedBots( int iNumKicked )
+{
+ tf_bot_quota.SetValue( MAX( tf_bot_quota.GetInt() - iNumKicked, 0 ) );
+ // allow time for the bots to be kicked
+ m_flNextPeriodicThink = gpGlobals->curtime + 2.0f;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+CTFBotManager &TheTFBots( void )
+{
+ return static_cast<CTFBotManager&>( TheNextBots() );
+}
+
+
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_debug_stuck_log, "Given a server logfile, visually display bot stuck locations.", FCVAR_GAMEDLL | FCVAR_CHEAT )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ {
+ DevMsg( "%s <logfilename>\n", args.Arg(0) );
+ return;
+ }
+
+ FileHandle_t file = filesystem->Open( args.Arg(1), "r", "GAME" );
+
+ const int maxBufferSize = 1024;
+ char buffer[ maxBufferSize ];
+
+ char logMapName[ maxBufferSize ];
+ logMapName[0] = '\000';
+
+ TheTFBots().ClearStuckBotData();
+
+ if ( file )
+ {
+ int line = 0;
+ while( !filesystem->EndOfFile( file ) )
+ {
+ filesystem->ReadLine( buffer, maxBufferSize, file );
+ ++line;
+
+ strtok( buffer, ":" );
+ strtok( NULL, ":" );
+ strtok( NULL, ":" );
+ char *first = strtok( NULL, " " );
+
+ if ( !first )
+ continue;
+
+ if ( !strcmp( first, "Loading" ) )
+ {
+ // L 08/08/2012 - 15:10:47: Loading map "mvm_coaltown"
+ strtok( NULL, " " );
+ char *mapname = strtok( NULL, "\"" );
+
+ if ( mapname )
+ {
+ strcpy( logMapName, mapname );
+ Warning( "*** Log file from map '%s'\n", mapname );
+ }
+ }
+ else if ( first[0] == '\"' )
+ {
+ // might be a player ID
+
+ char *playerClassname = &first[1];
+
+ char *nameEnd = playerClassname;
+ while( *nameEnd != '\000' && *nameEnd != '<' )
+ ++nameEnd;
+ *nameEnd = '\000';
+
+ char *botIDString = ++nameEnd;
+ char *IDEnd = botIDString;
+ while( *IDEnd != '\000' && *IDEnd != '>' )
+ ++IDEnd;
+ *IDEnd = '\000';
+
+ int botID = atoi( botIDString );
+
+ char *second = strtok( NULL, " " );
+ if ( second && !strcmp( second, "stuck" ) )
+ {
+ CStuckBot *stuckBot = TheTFBots().FindOrCreateStuckBot( botID, playerClassname );
+
+ CStuckBotEvent *stuckEvent = new CStuckBotEvent;
+
+
+ // L 08/08/2012 - 15:15:05: "Scout<53><BOT><Blue>" stuck (position "-180.61 2471.29 216.04") (duration "2.52") L 08/08/2012 - 15:15:05: path_goal ( "-180.61 2471.29 216.04" )
+ strtok( NULL, " (\"" ); // (position
+
+ stuckEvent->m_stuckSpot.x = (float)atof( strtok( NULL, " )\"" ) );
+ stuckEvent->m_stuckSpot.y = (float)atof( strtok( NULL, " )\"" ) );
+ stuckEvent->m_stuckSpot.z = (float)atof( strtok( NULL, " )\"" ) );
+
+ strtok( NULL, ") (\"" );
+ stuckEvent->m_stuckDuration = (float)atof( strtok( NULL, "\"" ) );
+
+ strtok( NULL, ") (\"-L0123456789/:" ); // path_goal
+
+ char *goal = strtok( NULL, ") (\"" );
+
+ if ( goal && strcmp( goal, "NULL" ) )
+ {
+ stuckEvent->m_isGoalValid = true;
+
+ stuckEvent->m_goalSpot.x = (float)atof( goal );
+ stuckEvent->m_goalSpot.y = (float)atof( strtok( NULL, ") (\"" ) );
+ stuckEvent->m_goalSpot.z = (float)atof( strtok( NULL, ") (\"" ) );
+ }
+ else
+ {
+ stuckEvent->m_isGoalValid = false;
+ }
+
+ stuckBot->m_stuckEventVector.AddToTail( stuckEvent );
+ }
+ }
+ }
+
+ filesystem->Close( file );
+ }
+ else
+ {
+ Warning( "Can't open file '%s'\n", args.Arg(1) );
+ }
+
+ //TheTFBots().DrawStuckBotData();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+CON_COMMAND_F( tf_bot_debug_stuck_log_clear, "Clear currently loaded bot stuck data", FCVAR_GAMEDLL | FCVAR_CHEAT )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ TheTFBots().ClearStuckBotData();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+// for parsing and debugging stuck bot server logs
+void CTFBotManager::ClearStuckBotData()
+{
+ m_stuckBotVector.PurgeAndDeleteElements();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+// for parsing and debugging stuck bot server logs
+CStuckBot *CTFBotManager::FindOrCreateStuckBot( int id, const char *playerClass )
+{
+ for( int i=0; i<m_stuckBotVector.Count(); ++i )
+ {
+ CStuckBot *stuckBot = m_stuckBotVector[i];
+
+ if ( stuckBot->IsMatch( id, playerClass ) )
+ {
+ return stuckBot;
+ }
+ }
+
+ // new instance of a stuck bot
+ CStuckBot *newStuckBot = new CStuckBot( id, playerClass );
+ m_stuckBotVector.AddToHead( newStuckBot );
+
+ return newStuckBot;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void CTFBotManager::DrawStuckBotData( float deltaT )
+{
+ if ( engine->IsDedicatedServer() )
+ return;
+
+ if ( !m_stuckDisplayTimer.IsElapsed() )
+ return;
+
+ m_stuckDisplayTimer.Start( deltaT );
+
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ if ( player == NULL )
+ return;
+
+// Vector forward;
+// AngleVectors( player->EyeAngles(), &forward );
+
+ for( int i=0; i<m_stuckBotVector.Count(); ++i )
+ {
+ for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count(); ++j )
+ {
+ m_stuckBotVector[i]->m_stuckEventVector[j]->Draw( deltaT );
+ }
+
+ for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count()-1; ++j )
+ {
+ NDebugOverlay::HorzArrow( m_stuckBotVector[i]->m_stuckEventVector[j]->m_stuckSpot,
+ m_stuckBotVector[i]->m_stuckEventVector[j+1]->m_stuckSpot,
+ 3, 100, 0, 255, 255, true, deltaT );
+ }
+
+ NDebugOverlay::Text( m_stuckBotVector[i]->m_stuckEventVector[0]->m_stuckSpot, CFmtStr( "%s(#%d)", m_stuckBotVector[i]->m_name, m_stuckBotVector[i]->m_id ), false, deltaT );
+ }
+}
+
+
diff --git a/game/server/tf/bot/tf_bot_manager.h b/game/server/tf/bot/tf_bot_manager.h
new file mode 100644
index 0000000..4f5ec5f
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_manager.h
@@ -0,0 +1,147 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_manager.h
+// Team Fortress NextBotManager
+// Tom Bui, May 2010
+
+#ifndef TF_BOT_MANAGER_H
+#define TF_BOT_MANAGER_H
+
+#include "NextBotManager.h"
+#include "tf_team.h"
+
+class CTFBot;
+class CTFPlayer;
+class CTFBotSquad;
+class CStuckBotEvent;
+
+
+
+//----------------------------------------------------------------------------------------------
+// For parsing and displaying stuck events from server logs.
+class CStuckBot
+{
+public:
+ CStuckBot( int id, const char *name )
+ {
+ m_id = id;
+ Q_strncpy( m_name, name, 256 );
+ }
+
+ bool IsMatch( int id, const char *name )
+ {
+ return ( id == m_id && FStrEq( name, m_name ) );
+ }
+
+ char m_name[256];
+ int m_id;
+
+ CUtlVector< CStuckBotEvent * > m_stuckEventVector;
+};
+
+
+
+//----------------------------------------------------------------------------------------------
+// For parsing and displaying stuck events from server logs.
+class CStuckBotEvent
+{
+public:
+ Vector m_stuckSpot;
+ float m_stuckDuration;
+ Vector m_goalSpot;
+ bool m_isGoalValid;
+
+ void Draw( float deltaT = 0.1f )
+ {
+ NDebugOverlay::Cross3D( m_stuckSpot, 5.0f, 255, 255, 0, true, deltaT );
+
+ if ( m_isGoalValid )
+ {
+ if ( m_stuckDuration > 6.0f )
+ {
+ NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 255, 0, 0, 255, true, deltaT );
+ }
+ else if ( m_stuckDuration > 3.0f )
+ {
+ NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 255, 255, 0, 255, true, deltaT );
+ }
+ else
+ {
+ NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 0, 255, 0, 255, true, deltaT );
+ }
+ }
+ }
+};
+
+
+//----------------------------------------------------------------------------------------------
+class CTFBotManager : public NextBotManager
+{
+public:
+ CTFBotManager();
+ virtual ~CTFBotManager();
+
+ virtual void Update();
+ void LevelShutdown();
+
+ virtual void OnMapLoaded( void ); // when the server has changed maps
+ virtual void OnRoundRestart( void ); // when the scenario restarts
+
+ bool IsAllBotTeam( int iTeam );
+ bool IsInOfflinePractice() const;
+ bool IsMeleeOnly() const;
+
+ CTFBot* GetAvailableBotFromPool();
+
+ void OnForceAddedBots( int iNumAdded );
+ void OnForceKickedBots( int iNumKicked );
+
+ void ClearStuckBotData();
+ CStuckBot *FindOrCreateStuckBot( int id, const char *playerClass ); // for parsing and debugging stuck bot server logs
+ void DrawStuckBotData( float deltaT = 0.1f );
+
+#ifdef TF_CREEP_MODE
+ void OnCreepKilled( CTFPlayer *killer );
+#endif
+
+ bool RemoveBotFromTeamAndKick( int nTeam );
+
+protected:
+ void MaintainBotQuota();
+ void SetIsInOfflinePractice( bool bIsInOfflinePractice );
+ void RevertOfflinePracticeConvars();
+
+ float m_flNextPeriodicThink;
+
+#ifdef TF_CREEP_MODE
+ void UpdateCreepWaves();
+ CountdownTimer m_creepWaveTimer;
+
+ void SpawnCreep( int team, CTFBotSquad *squad );
+ void SpawnCreepWave( int team );
+
+ int m_creepExperience[ TF_TEAM_COUNT ];
+#endif
+
+ void UpdateMedievalBossScenario();
+ bool m_isMedeivalBossScenarioSetup;
+ void SetupMedievalBossScenario();
+
+ CUtlVector< CBaseEntity * > m_archerSpawnVector;
+
+ struct ArcherAssignmentInfo
+ {
+ CHandle< CBaseCombatCharacter > m_archer;
+ CHandle< CBaseEntity > m_mark;
+ };
+ CUtlVector< ArcherAssignmentInfo > m_archerMarkVector;
+
+ CountdownTimer m_archerTimer;
+
+ CUtlVector< CStuckBot * > m_stuckBotVector;
+ CountdownTimer m_stuckDisplayTimer;
+};
+
+// singleton accessor
+CTFBotManager &TheTFBots( void );
+
+#endif // TF_BOT_MANAGER_H
diff --git a/game/server/tf/bot/tf_bot_squad.cpp b/game/server/tf/bot/tf_bot_squad.cpp
new file mode 100644
index 0000000..91cc914
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_squad.cpp
@@ -0,0 +1,294 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_squad.h
+// Small groups of TFBot, managed as a unit
+// Michael Booth, November 2009
+
+#include "cbase.h"
+#include "tf_bot.h"
+#include "tf_bot_squad.h"
+
+
+//----------------------------------------------------------------------
+CTFBotSquad::CTFBotSquad( void )
+{
+ m_leader = NULL;
+ m_formationSize = -1.0f;
+ m_bShouldPreserveSquad = false;
+}
+
+
+//----------------------------------------------------------------------
+void CTFBotSquad::Join( CTFBot *bot )
+{
+ // first member is the leader
+ if ( m_roster.Count() == 0 )
+ {
+ m_leader = bot;
+ }
+ else if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ bot->SetFlagTarget( NULL );
+ }
+
+ m_roster.AddToTail( bot );
+}
+
+
+//----------------------------------------------------------------------
+void CTFBotSquad::Leave( CTFBot *bot )
+{
+ m_roster.FindAndRemove( bot );
+
+ if ( bot == m_leader.Get() )
+ {
+ m_leader = NULL;
+
+ // pick the next living leader that's left in the squad
+ if ( m_bShouldPreserveSquad )
+ {
+ CUtlVector< CTFBot* > members;
+ CollectMembers( &members );
+ if ( members.Count() )
+ {
+ m_leader = members[0];
+ }
+ }
+ }
+ else if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ AssertMsg( !bot->HasFlagTaget(), "Squad member shouldn't have a flag target. Always follow the leader." );
+ CCaptureFlag *pFlag = bot->GetFlagToFetch();
+ if ( pFlag )
+ {
+ bot->SetFlagTarget( pFlag );
+ }
+ }
+
+ if ( GetMemberCount() == 0 )
+ {
+ DisbandAndDeleteSquad();
+ }
+}
+
+
+//----------------------------------------------------------------------
+INextBotEventResponder *CTFBotSquad::FirstContainedResponder( void ) const
+{
+ return m_roster.Count() ? m_roster[0] : NULL;
+}
+
+
+//----------------------------------------------------------------------
+INextBotEventResponder *CTFBotSquad::NextContainedResponder( INextBotEventResponder *current ) const
+{
+ CTFBot *currentBot = (CTFBot *)current;
+
+ int i = m_roster.Find( currentBot );
+
+ if ( i == m_roster.InvalidIndex() )
+ return NULL;
+
+ if ( ++i >= m_roster.Count() )
+ return NULL;
+
+ return (CTFBot *)m_roster[i];
+}
+
+
+//----------------------------------------------------------------------
+CTFBot *CTFBotSquad::GetLeader( void ) const
+{
+ return m_leader;
+}
+
+
+//----------------------------------------------------------------------
+void CTFBotSquad::CollectMembers( CUtlVector< CTFBot * > *memberVector ) const
+{
+ for( int i=0; i<m_roster.Count(); ++i )
+ {
+ if ( m_roster[i] != NULL && m_roster[i]->IsAlive() )
+ {
+ memberVector->AddToTail( m_roster[i] );
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------
+CTFBotSquad::Iterator CTFBotSquad::GetFirstMember( void ) const
+{
+ // find first non-NULL member
+ for( int i=0; i<m_roster.Count(); ++i )
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ return Iterator( m_roster[i], i );
+
+ return InvalidIterator();
+}
+
+
+//----------------------------------------------------------------------
+CTFBotSquad::Iterator CTFBotSquad::GetNextMember( const Iterator &it ) const
+{
+ // find next non-NULL member
+ for( int i=it.m_index+1; i<m_roster.Count(); ++i )
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ return Iterator( m_roster[i], i );
+
+ return InvalidIterator();
+}
+
+
+//----------------------------------------------------------------------
+int CTFBotSquad::GetMemberCount( void ) const
+{
+ // count the non-NULL members
+ int count = 0;
+ for( int i=0; i<m_roster.Count(); ++i )
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ ++count;
+
+ return count;
+}
+
+
+//----------------------------------------------------------------------
+// Return the speed of the slowest member of the squad
+float CTFBotSquad::GetSlowestMemberSpeed( bool includeLeader ) const
+{
+ float speed = FLT_MAX;
+
+ int i = includeLeader ? 0 : 1;
+
+ for( ; i<m_roster.Count(); ++i )
+ {
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ {
+ float memberSpeed = m_roster[i]->MaxSpeed();
+ if ( memberSpeed < speed )
+ {
+ speed = memberSpeed;
+ }
+ }
+ }
+
+ return speed;
+}
+
+
+//----------------------------------------------------------------------
+// Return the speed of the slowest member of the squad,
+// considering their ideal class speed.
+float CTFBotSquad::GetSlowestMemberIdealSpeed( bool includeLeader ) const
+{
+ float speed = FLT_MAX;
+
+ int i = includeLeader ? 0 : 1;
+
+ for( ; i<m_roster.Count(); ++i )
+ {
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ {
+ float memberSpeed = m_roster[i]->GetPlayerClass()->GetMaxSpeed();
+ if ( memberSpeed < speed )
+ {
+ speed = memberSpeed;
+ }
+ }
+ }
+
+ return speed;
+}
+
+
+//----------------------------------------------------------------------
+// Return the maximum formation error of the squad's memebers.
+float CTFBotSquad::GetMaxSquadFormationError( void ) const
+{
+ float maxError = 0.0f;
+
+ // skip the leader since he's what the formation forms around
+ for( int i=1; i<m_roster.Count(); ++i )
+ {
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ {
+ float error = m_roster[i]->GetSquadFormationError();
+ if ( error > maxError )
+ {
+ maxError = error;
+ }
+ }
+ }
+
+ return maxError;
+}
+
+
+//----------------------------------------------------------------------
+// Return true if the squad leader needs to wait for members to catch up, ignoring those who have broken ranks
+bool CTFBotSquad::ShouldSquadLeaderWaitForFormation( void ) const
+{
+ // skip the leader since he's what the formation forms around
+ for( int i=1; i<m_roster.Count(); ++i )
+ {
+ // the squad leader should wait if any member is out of position, but not yet broken ranks
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ {
+ if ( m_roster[i]->GetSquadFormationError() >= 1.0f &&
+ !m_roster[i]->HasBrokenFormation() &&
+ !m_roster[i]->GetLocomotionInterface()->IsStuck() &&
+ !m_roster[i]->IsPlayerClass( TF_CLASS_MEDIC ) ) // Medics do their own thing
+ {
+ // wait for me!
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------
+// Return true if the squad is in formation (everyone is in or nearly in their desired positions)
+bool CTFBotSquad::IsInFormation( void ) const
+{
+ // skip the leader since he's what the formation forms around
+ for( int i=1; i<m_roster.Count(); ++i )
+ {
+ if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() )
+ {
+ if ( m_roster[i]->HasBrokenFormation() ||
+ m_roster[i]->GetLocomotionInterface()->IsStuck() ||
+ m_roster[i]->IsPlayerClass( TF_CLASS_MEDIC ) ) // Medics do their own thing
+ {
+ // I'm not "in formation"
+ continue;
+ }
+
+ if ( m_roster[i]->GetSquadFormationError() > 0.75f )
+ {
+ // I'm not in position yet
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+//----------------------------------------------------------------------
+// Tell all members to leave the squad and then delete itself
+void CTFBotSquad::DisbandAndDeleteSquad( void )
+{
+ // Tell each member of the squad to remove this reference
+ for( int i=0; i < m_roster.Count(); ++i )
+ {
+ if ( m_roster[i].Get() != NULL )
+ {
+ m_roster[i]->DeleteSquad();
+ }
+ }
+
+ delete this;
+} \ No newline at end of file
diff --git a/game/server/tf/bot/tf_bot_squad.h b/game/server/tf/bot/tf_bot_squad.h
new file mode 100644
index 0000000..c3b145f
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_squad.h
@@ -0,0 +1,121 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_squad.h
+// Small groups of TFBot, managed as a unit
+// Michael Booth, November 2009
+
+#ifndef TF_BOT_SQUAD_H
+#define TF_BOT_SQUAD_H
+
+#include "NextBot/NextBotEventResponderInterface.h"
+
+class CTFBot;
+
+class CTFBotSquad : public INextBotEventResponder
+{
+public:
+ CTFBotSquad( void );
+ virtual ~CTFBotSquad() { }
+
+ // EventResponder ------
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const;
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const;
+ //----------------------
+
+ bool IsMember( CTFBot *bot ) const; // is the given bot in this squad?
+ bool IsLeader( CTFBot *bot ) const; // is the given bot the leader of this squad?
+
+// CTFBot *GetMember( int i );
+ int GetMemberCount( void ) const;
+
+ CTFBot *GetLeader( void ) const;
+
+ class Iterator
+ {
+ public:
+ Iterator( void )
+ {
+ m_bot = NULL;
+ m_index = -1;
+ }
+
+ Iterator( CTFBot *bot, int index )
+ {
+ m_bot = bot;
+ m_index = index;
+ }
+
+ CTFBot *operator() ( void )
+ {
+ return m_bot;
+ }
+
+ bool operator==( const Iterator &it ) const { return m_bot == it.m_bot && m_index == it.m_index; }
+ bool operator!=( const Iterator &it ) const { return m_bot != it.m_bot || m_index != it.m_index; }
+
+ CTFBot *m_bot;
+ int m_index;
+ };
+
+ Iterator GetFirstMember( void ) const;
+ Iterator GetNextMember( const Iterator &it ) const;
+ Iterator InvalidIterator() const;
+
+ void CollectMembers( CUtlVector< CTFBot * > *memberVector ) const;
+
+ #define EXCLUDE_LEADER false
+ float GetSlowestMemberSpeed( bool includeLeader = true ) const;
+ float GetSlowestMemberIdealSpeed( bool includeLeader = true ) const;
+ float GetMaxSquadFormationError( void ) const;
+
+ bool ShouldSquadLeaderWaitForFormation( void ) const; // return true if the squad leader needs to wait for members to catch up, ignoring those who have broken ranks
+ bool IsInFormation( void ) const; // return true if the squad is in formation (everyone is in or nearly in their desired positions)
+
+ float GetFormationSize( void ) const;
+ void SetFormationSize( float size );
+
+ void DisbandAndDeleteSquad( void );
+
+ void SetShouldPreserveSquad( bool bShouldPreserveSquad ) { m_bShouldPreserveSquad = bShouldPreserveSquad; }
+ bool ShouldPreserveSquad() const { return m_bShouldPreserveSquad; }
+
+private:
+ friend class CTFBot;
+
+ void Join( CTFBot *bot );
+ void Leave( CTFBot *bot );
+
+ CUtlVector< CHandle< CTFBot > > m_roster;
+ CHandle< CTFBot > m_leader;
+
+ float m_formationSize;
+ bool m_bShouldPreserveSquad;
+};
+
+inline bool CTFBotSquad::IsMember( CTFBot *bot ) const
+{
+ return m_roster.HasElement( bot );
+}
+
+inline bool CTFBotSquad::IsLeader( CTFBot *bot ) const
+{
+ return m_leader == bot;
+}
+
+inline CTFBotSquad::Iterator CTFBotSquad::InvalidIterator() const
+{
+ return Iterator( NULL, -1 );
+}
+
+inline float CTFBotSquad::GetFormationSize( void ) const
+{
+ return m_formationSize;
+}
+
+inline void CTFBotSquad::SetFormationSize( float size )
+{
+ m_formationSize = size;
+}
+
+
+#endif // TF_BOT_SQUAD_H
+
diff --git a/game/server/tf/bot/tf_bot_vision.cpp b/game/server/tf/bot/tf_bot_vision.cpp
new file mode 100644
index 0000000..6fae0ff
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_vision.cpp
@@ -0,0 +1,482 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_vision.cpp
+// Team Fortress NextBot vision interface
+// Michael Booth, May 2009
+
+#include "cbase.h"
+#include "vprof.h"
+
+#include "tf_bot.h"
+#include "tf_bot_vision.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_obj_sentrygun.h"
+
+ConVar tf_bot_choose_target_interval( "tf_bot_choose_target_interval", "0.3f", FCVAR_CHEAT, "How often, in seconds, a TFBot can reselect his target" );
+ConVar tf_bot_sniper_choose_target_interval( "tf_bot_sniper_choose_target_interval", "3.0f", FCVAR_CHEAT, "How often, in seconds, a zoomed-in Sniper can reselect his target" );
+
+
+//------------------------------------------------------------------------------------------
+// Update internal state
+void CTFBotVision::Update( void )
+{
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ // Throttle vision update rate of robots in MvM for perf at the expense of reaction times
+ if ( !m_scanTimer.IsElapsed() )
+ {
+ return;
+ }
+
+ m_scanTimer.Start( RandomFloat( 0.9f, 1.1f ) );
+ }
+
+ IVision::Update();
+
+ CTFBot *me = (CTFBot *)GetBot()->GetEntity();
+ if ( !me )
+ return;
+
+ // forget spies we have lost sight of
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS );
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( !playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) )
+ continue;
+
+ const CKnownEntity *known = GetKnown( playerVector[i] );
+
+ if ( !known || !known->IsVisibleRecently() )
+ {
+ // if a hidden spy changes disguises, we no longer recognize him
+ if ( playerVector[i]->m_Shared.InCond( TF_COND_DISGUISING ) )
+ {
+ me->ForgetSpy( playerVector[i] );
+ }
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void CTFBotVision::CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible )
+{
+ VPROF_BUDGET( "CTFBotVision::CollectPotentiallyVisibleEntities", "NextBot" );
+
+ potentiallyVisible->RemoveAll();
+
+ // include all players
+ for( int i=1; i<=gpGlobals->maxClients; ++i )
+ {
+ CBasePlayer *player = UTIL_PlayerByIndex( i );
+
+ if ( player == NULL )
+ continue;
+
+ if ( FNullEnt( player->edict() ) )
+ continue;
+
+ if ( !player->IsPlayer() )
+ continue;
+
+ if ( !player->IsConnected() )
+ continue;
+
+ if ( !player->IsAlive() )
+ continue;
+
+ potentiallyVisible->AddToTail( player );
+ }
+
+ // include sentry guns
+ UpdatePotentiallyVisibleNPCVector();
+
+ FOR_EACH_VEC( m_potentiallyVisibleNPCVector, it )
+ {
+ potentiallyVisible->AddToTail( m_potentiallyVisibleNPCVector[ it ] );
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void CTFBotVision::UpdatePotentiallyVisibleNPCVector( void )
+{
+ if ( m_potentiallyVisibleUpdateTimer.IsElapsed() )
+ {
+ m_potentiallyVisibleUpdateTimer.Start( RandomFloat( 3.0f, 4.0f ) );
+
+ // collect list of active buildings
+ m_potentiallyVisibleNPCVector.RemoveAll();
+
+ bool bShouldSeeTeleporter = !TFGameRules()->IsMannVsMachineMode() || GetBot()->GetEntity()->GetTeamNumber() != TF_TEAM_PVE_INVADERS;
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->ObjectType() == OBJ_SENTRYGUN )
+ {
+ m_potentiallyVisibleNPCVector.AddToTail( pObj );
+ }
+ else if ( pObj->ObjectType() == OBJ_DISPENSER && pObj->ClassMatches( "obj_dispenser" ) )
+ {
+ m_potentiallyVisibleNPCVector.AddToTail( pObj );
+ }
+ else if ( bShouldSeeTeleporter && pObj->ObjectType() == OBJ_TELEPORTER )
+ {
+ m_potentiallyVisibleNPCVector.AddToTail( pObj );
+ }
+ }
+
+ CUtlVector< INextBot * > botVector;
+ TheNextBots().CollectAllBots( &botVector );
+ for( int i=0; i<botVector.Count(); ++i )
+ {
+ CBaseCombatCharacter *botEntity = botVector[i]->GetEntity();
+ if ( botEntity && !botEntity->IsPlayer() )
+ {
+ // NPC
+ m_potentiallyVisibleNPCVector.AddToTail( botEntity );
+ }
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return true to completely ignore this entity.
+ * This is mostly for enemy spies. If we don't ignore them, we will look at them.
+ */
+bool CTFBotVision::IsIgnored( CBaseEntity *subject ) const
+{
+ CTFBot *me = (CTFBot *)GetBot()->GetEntity();
+
+#ifdef TF_RAID_MODE
+ if ( TFGameRules()->IsRaidMode() )
+ {
+ if ( me->IsPlayerClass( TF_CLASS_SCOUT ) )
+ {
+ // Scouts are wandering defenders, and aggro purely on proximity or damage, not vision
+ return true;
+ }
+ }
+#endif // TF_RAID_MODE
+
+ if ( me->IsAttentionFocused() )
+ {
+ // our attention is restricted to certain subjects
+ if ( !me->IsAttentionFocusedOn( subject ) )
+ {
+ return false;
+ }
+ }
+
+ if ( !me->IsEnemy( subject ) )
+ {
+ // don't ignore friends
+ return false;
+ }
+
+ if ( subject->IsEffectActive( EF_NODRAW ) )
+ {
+ return true;
+ }
+
+ if ( subject->IsPlayer() )
+ {
+ CTFPlayer *enemy = static_cast< CTFPlayer * >( subject );
+
+ // test for designer-defined ignorance
+ switch( enemy->GetPlayerClass()->GetClassIndex() )
+ {
+ case TF_CLASS_MEDIC:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_MEDICS ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_ENGINEER:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_ENGINEERS ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_SNIPER:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SNIPERS ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_SCOUT:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SCOUTS ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_SPY:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SPIES ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_DEMOMAN:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_DEMOMEN ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_SOLDIER:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SOLDIERS ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_HEAVYWEAPONS:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_HEAVIES ) )
+ {
+ return true;
+ }
+ break;
+
+ case TF_CLASS_PYRO:
+ if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_PYROS ) )
+ {
+ return true;
+ }
+ break;
+ }
+
+#ifdef STAGING_ONLY
+ if ( enemy->m_Shared.InCond( TF_COND_REPROGRAMMED ) )
+ {
+ return true;
+ }
+#endif // STAGING_ONLY
+
+ if ( me->IsKnownSpy( enemy ) )
+ {
+ // don't ignore revealed spies
+ return false;
+ }
+
+ if ( enemy->m_Shared.InCond( TF_COND_BURNING ) ||
+ enemy->m_Shared.InCond( TF_COND_URINE ) ||
+ enemy->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
+ enemy->m_Shared.InCond( TF_COND_BLEEDING ) )
+ {
+ // always notice players with these conditions
+ return false;
+ }
+
+ // An upgrade in MvM grants AE stealth where the player can fire
+ // while in stealth, and for a short period after it drops
+ if ( enemy->m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF_FADING ) )
+ {
+ return true;
+ }
+
+ if ( enemy->m_Shared.IsStealthed() )
+ {
+ if ( enemy->m_Shared.GetPercentInvisible() < 0.75f )
+ {
+ // spy is partially cloaked, and therefore attracts our attention
+ return false;
+ }
+
+ // invisible!
+ return true;
+ }
+
+ if ( enemy->IsPlacingSapper() )
+ {
+ return false;
+ }
+
+ if ( enemy->m_Shared.InCond( TF_COND_DISGUISING ) )
+ {
+ return false;
+ }
+
+ if ( enemy->m_Shared.InCond( TF_COND_DISGUISED ) && enemy->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() )
+ {
+ // spy is disguised as a member of my team
+ return true;
+ }
+ }
+ else if ( subject->IsBaseObject() ) // not a player
+ {
+ CBaseObject *object = assert_cast< CBaseObject * >( subject );
+ if ( object )
+ {
+ // ignore sapped enemy objects
+ if ( object->HasSapper() )
+ {
+ // unless we're in MvM where buildings can have really large health pools,
+ // so an engineer can die and run back in time to repair their stuff
+ if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ // ignore carried objects
+ if ( object->IsPlacing() || object->IsCarried() )
+ {
+ return true;
+ }
+
+ if ( object->GetType() == OBJ_SENTRYGUN && me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SENTRY_GUNS ) )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//------------------------------------------------------------------------------------------
+// Return true if we 'notice' the subject, even though we have LOS to it
+bool CTFBotVision::IsVisibleEntityNoticed( CBaseEntity *subject ) const
+{
+ CTFBot *me = (CTFBot *)GetBot()->GetEntity();
+
+ if ( subject->IsPlayer() && me->IsEnemy( subject ) )
+ {
+ CTFPlayer *player = static_cast< CTFPlayer * >( subject );
+
+ if ( player->m_Shared.InCond( TF_COND_BURNING ) ||
+ player->m_Shared.InCond( TF_COND_URINE ) ||
+ player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
+ player->m_Shared.InCond( TF_COND_BLEEDING ) )
+ {
+ // always notice players with these conditions
+ if ( player->m_Shared.InCond( TF_COND_STEALTHED ) )
+ {
+ me->RealizeSpy( player );
+ }
+ return true;
+ }
+
+#ifdef STAGING_ONLY
+ // Bots can be hacked/reprogrammed by spies. Ignore.
+ if ( player->m_Shared.InCond( TF_COND_REPROGRAMMED ) )
+ {
+ return false;
+ }
+#endif // STAGING_ONLY
+
+ // An upgrade in MvM grants AE stealth where the player can fire
+ // while in stealth, and for a short period after it drops
+ if ( player->m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF_FADING ) )
+ {
+ me->ForgetSpy( player );
+ return false;
+ }
+
+ if ( player->m_Shared.IsStealthed() )
+ {
+ if ( player->m_Shared.GetPercentInvisible() < 0.75f )
+ {
+ // spy is partially cloaked, and therefore attracts our attention
+ me->RealizeSpy( player );
+ return true;
+ }
+
+ // invisible!
+ me->ForgetSpy( player );
+ return false;
+ }
+
+ if ( TFGameRules()->IsMannVsMachineMode() ) // in MvM mode, forget spies as soon as they are fully disguised
+ {
+ CTFBot::SuspectedSpyInfo_t* pSuspectInfo = me->IsSuspectedSpy( player );
+ // But only if we aren't suspecting them currently. This happens when we bump into them.
+ if( !pSuspectInfo || !pSuspectInfo->IsCurrentlySuspected() )
+ {
+ if ( player->m_Shared.InCond( TF_COND_DISGUISED ) && player->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() )
+ {
+ me->ForgetSpy( player );
+ return false;
+ }
+ }
+ }
+
+ if ( me->IsKnownSpy( player ) )
+ {
+ // always notice non-invisible revealed spies
+ return true;
+ }
+
+ if ( !TFGameRules()->IsMannVsMachineMode() ) // ignore in MvM mode
+ {
+ if ( player->IsPlacingSapper() )
+ {
+ // spotted a spy!
+ me->RealizeSpy( player );
+ return true;
+ }
+ }
+
+ if ( player->m_Shared.InCond( TF_COND_DISGUISING ) )
+ {
+ // spotted a spy!
+ me->RealizeSpy( player );
+ return true;
+ }
+
+ if ( player->m_Shared.InCond( TF_COND_DISGUISED ) && player->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() )
+ {
+ // spy is disguised as a member of my team, don't notice him
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+//------------------------------------------------------------------------------------------
+// Return VISUAL reaction time
+float CTFBotVision::GetMinRecognizeTime( void ) const
+{
+ CTFBot *me = (CTFBot *)GetBot();
+
+ switch ( me->GetDifficulty() )
+ {
+ case CTFBot::EASY: return 1.0f;
+ case CTFBot::NORMAL: return 0.5f;
+ case CTFBot::HARD: return 0.3f;
+ case CTFBot::EXPERT: return 0.2f;
+ }
+
+ return 1.0f;
+}
+
+
+
+//------------------------------------------------------------------------------------------
+float CTFBotVision::GetMaxVisionRange( void ) const
+{
+ CTFBot *me = (CTFBot *)GetBot();
+
+ if ( me->GetMaxVisionRangeOverride() > 0.0f )
+ {
+ // designer specified vision range
+ return me->GetMaxVisionRangeOverride();
+ }
+
+ // long range, particularly for snipers
+ return 6000.0f;
+}
diff --git a/game/server/tf/bot/tf_bot_vision.h b/game/server/tf/bot/tf_bot_vision.h
new file mode 100644
index 0000000..7f999e5
--- /dev/null
+++ b/game/server/tf/bot/tf_bot_vision.h
@@ -0,0 +1,44 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// tf_bot_vision.h
+// Team Fortress NextBot vision interface
+// Michael Booth, May 2009
+
+#ifndef TF_BOT_VISION_H
+#define TF_BOT_VISION_H
+
+#include "NextBotVisionInterface.h"
+
+//----------------------------------------------------------------------------
+class CTFBotVision : public IVision
+{
+public:
+ CTFBotVision( INextBot *bot ) : IVision( bot )
+ {
+ }
+
+ virtual ~CTFBotVision() { }
+
+ virtual void Update( void ); // update internal state
+
+ /**
+ * Populate "potentiallyVisible" with the set of all entities we could potentially see.
+ * Entities in this set will be tested for visibility/recognition in IVision::Update()
+ */
+ virtual void CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible );
+
+ virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called)
+ virtual bool IsVisibleEntityNoticed( CBaseEntity *subject ) const; // return true if we 'notice' the subject, even though we have LOS to it
+
+ virtual float GetMaxVisionRange( void ) const; // return maximum distance vision can reach
+ virtual float GetMinRecognizeTime( void ) const; // return VISUAL reaction time
+
+private:
+ CUtlVector< CHandle< CBaseCombatCharacter > > m_potentiallyVisibleNPCVector;
+ CountdownTimer m_potentiallyVisibleUpdateTimer;
+ void UpdatePotentiallyVisibleNPCVector( void );
+
+ CountdownTimer m_scanTimer;
+};
+
+
+#endif // TF_BOT_VISION_H \ No newline at end of file