summaryrefslogtreecommitdiff
path: root/game/server/NextBot
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/NextBot')
-rw-r--r--game/server/NextBot/Behavior/BehaviorBackUp.h118
-rw-r--r--game/server/NextBot/Behavior/BehaviorMoveTo.h126
-rw-r--r--game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp75
-rw-r--r--game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h56
-rw-r--r--game/server/NextBot/NextBot.cpp523
-rw-r--r--game/server/NextBot/NextBot.h104
-rw-r--r--game/server/NextBot/NextBotAttentionInterface.cpp162
-rw-r--r--game/server/NextBot/NextBotAttentionInterface.h81
-rw-r--r--game/server/NextBot/NextBotBehavior.h1936
-rw-r--r--game/server/NextBot/NextBotBodyInterface.cpp55
-rw-r--r--game/server/NextBot/NextBotBodyInterface.h325
-rw-r--r--game/server/NextBot/NextBotComponentInterface.cpp24
-rw-r--r--game/server/NextBot/NextBotComponentInterface.h98
-rw-r--r--game/server/NextBot/NextBotContextualQueryInterface.h102
-rw-r--r--game/server/NextBot/NextBotDebug.h23
-rw-r--r--game/server/NextBot/NextBotEventResponderInterface.h550
-rw-r--r--game/server/NextBot/NextBotGroundLocomotion.cpp1442
-rw-r--r--game/server/NextBot/NextBotGroundLocomotion.h274
-rw-r--r--game/server/NextBot/NextBotHearingInterface.h34
-rw-r--r--game/server/NextBot/NextBotIntentionInterface.cpp91
-rw-r--r--game/server/NextBot/NextBotIntentionInterface.h211
-rw-r--r--game/server/NextBot/NextBotInterface.cpp537
-rw-r--r--game/server/NextBot/NextBotInterface.h302
-rw-r--r--game/server/NextBot/NextBotKnownEntity.h175
-rw-r--r--game/server/NextBot/NextBotLocomotionInterface.cpp520
-rw-r--r--game/server/NextBot/NextBotLocomotionInterface.h335
-rw-r--r--game/server/NextBot/NextBotManager.cpp892
-rw-r--r--game/server/NextBot/NextBotManager.h211
-rw-r--r--game/server/NextBot/NextBotUtil.h278
-rw-r--r--game/server/NextBot/NextBotVisionInterface.cpp802
-rw-r--r--game/server/NextBot/NextBotVisionInterface.h226
-rw-r--r--game/server/NextBot/Path/NextBotChasePath.cpp166
-rw-r--r--game/server/NextBot/Path/NextBotChasePath.h376
-rw-r--r--game/server/NextBot/Path/NextBotPath.cpp1094
-rw-r--r--game/server/NextBot/Path/NextBotPath.h862
-rw-r--r--game/server/NextBot/Path/NextBotPathFollow.cpp1923
-rw-r--r--game/server/NextBot/Path/NextBotPathFollow.h106
-rw-r--r--game/server/NextBot/Path/NextBotRetreatPath.h573
-rw-r--r--game/server/NextBot/Player/NextBotPlayer.cpp22
-rw-r--r--game/server/NextBot/Player/NextBotPlayer.h910
-rw-r--r--game/server/NextBot/Player/NextBotPlayerBody.cpp881
-rw-r--r--game/server/NextBot/Player/NextBotPlayerBody.h153
-rw-r--r--game/server/NextBot/Player/NextBotPlayerLocomotion.cpp826
-rw-r--r--game/server/NextBot/Player/NextBotPlayerLocomotion.h223
-rw-r--r--game/server/NextBot/simple_bot.cpp199
-rw-r--r--game/server/NextBot/simple_bot.h116
46 files changed, 19118 insertions, 0 deletions
diff --git a/game/server/NextBot/Behavior/BehaviorBackUp.h b/game/server/NextBot/Behavior/BehaviorBackUp.h
new file mode 100644
index 0000000..e3a65f0
--- /dev/null
+++ b/game/server/NextBot/Behavior/BehaviorBackUp.h
@@ -0,0 +1,118 @@
+// BehaviorBackUp.h
+// Back up for a short duration
+// Author: Michael Booth, March 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _BEHAVIOR_BACK_UP_H_
+#define _BEHAVIOR_BACK_UP_H_
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Move backwards for a short duration away from a given position.
+ * Useful to dislodge ourselves if we get stuck while following our path.
+ */
+template < typename Actor >
+class BehaviorBackUp : public Action< Actor >
+{
+public:
+ BehaviorBackUp( const Vector &avoidPos );
+
+ virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction );
+ virtual ActionResult< Actor > Update( Actor *me, float interval );
+
+ virtual EventDesiredResult< Actor > OnStuck( Actor *me );
+
+ virtual const char *GetName( void ) const { return "BehaviorBackUp"; }
+
+private:
+ CountdownTimer m_giveUpTimer;
+ CountdownTimer m_backupTimer;
+ CountdownTimer m_jumpTimer;
+ Vector m_way;
+ Vector m_avoidPos;
+};
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor >
+inline BehaviorBackUp< Actor >::BehaviorBackUp( const Vector &avoidPos )
+{
+ m_avoidPos = avoidPos;
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor >
+inline ActionResult< Actor > BehaviorBackUp< Actor >::OnStart( Actor *me, Action< Actor > *priorAction )
+{
+ ILocomotion *mover = me->GetLocomotionInterface();
+
+ // don't back off if we're on a ladder
+ if ( mover && mover->IsUsingLadder() )
+ {
+ return Done();
+ }
+
+ float backupTime = RandomFloat( 0.3f, 0.5f );
+
+ m_backupTimer.Start( backupTime );
+ m_jumpTimer.Start( 1.5f * backupTime );
+ m_giveUpTimer.Start( 2.5f * backupTime );
+
+ m_way = me->GetPosition() - m_avoidPos;
+ m_way.NormalizeInPlace();
+
+ return Continue();
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor >
+inline ActionResult< Actor > BehaviorBackUp< Actor >::Update( Actor *me, float interval )
+{
+ if ( m_giveUpTimer.IsElapsed() )
+ {
+ return Done();
+ }
+
+// if ( m_jumpTimer.HasStarted() && m_jumpTimer.IsElapsed() )
+// {
+// me->GetLocomotionInterface()->Jump();
+// m_jumpTimer.Invalidate();
+// }
+
+ ILocomotion *mover = me->GetLocomotionInterface();
+ if ( mover )
+ {
+ Vector goal;
+
+ if ( m_backupTimer.IsElapsed() )
+ {
+ // move towards bad spot
+ goal = m_avoidPos; // me->GetPosition() - 100.0f * m_way;
+ }
+ else
+ {
+ // move away from bad spot
+ goal = me->GetPosition() + 100.0f * m_way;
+ }
+
+ mover->Approach( goal );
+ }
+
+ return Continue();
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor >
+inline EventDesiredResult< Actor > BehaviorBackUp< Actor >::OnStuck( Actor *me )
+{
+ return TryToSustain( RESULT_IMPORTANT, "Stuck while trying to back up" );
+}
+
+
+
+#endif // _BEHAVIOR_BACK_UP_H_
+
diff --git a/game/server/NextBot/Behavior/BehaviorMoveTo.h b/game/server/NextBot/Behavior/BehaviorMoveTo.h
new file mode 100644
index 0000000..f3b608f
--- /dev/null
+++ b/game/server/NextBot/Behavior/BehaviorMoveTo.h
@@ -0,0 +1,126 @@
+// BehaviorMoveTo.h
+// Move to a potentially far away position
+// Author: Michael Booth, June 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _BEHAVIOR_MOVE_TO_H_
+#define _BEHAVIOR_MOVE_TO_H_
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Move to a potentially far away position, using path planning.
+ */
+template < typename Actor, typename PathCost >
+class BehaviorMoveTo : public Action< Actor >
+{
+public:
+ BehaviorMoveTo( const Vector &goal, Action< Actor > *successAction = NULL, Action< Actor > *failAction = NULL );
+
+ virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction );
+ virtual ActionResult< Actor > Update( Actor *me, float interval );
+
+ virtual EventDesiredResult< Actor > OnMoveToSuccess( Actor *me, const Path *path );
+ virtual EventDesiredResult< Actor > OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason );
+
+ virtual bool ComputePath( Actor *me, const Vector &goal, PathFollower *path );
+
+ virtual const char *GetName( void ) const { return "BehaviorMoveTo"; }
+
+private:
+ Vector m_goal;
+ PathFollower m_path;
+ Action< Actor > *m_successAction;
+ Action< Actor > *m_failAction;
+};
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline BehaviorMoveTo< Actor, PathCost >::BehaviorMoveTo( const Vector &goal, Action< Actor > *successAction, Action< Actor > *failAction )
+{
+ m_goal = goal;
+ m_path.Invalidate();
+ m_successAction = successAction;
+ m_failAction = failAction;
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline bool BehaviorMoveTo< Actor, PathCost >::ComputePath( Actor *me, const Vector &goal, PathFollower *path )
+{
+ PathCost cost( me );
+ return path->Compute( me, goal, cost );
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline ActionResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnStart( Actor *me, Action< Actor > *priorAction )
+{
+ if ( !this->ComputePath( me, m_goal, &m_path ) )
+ {
+ if ( m_failAction )
+ {
+ return this->ChangeTo( m_failAction, "No path to goal" );
+ }
+
+ return this->Done( "No path to goal" );
+ }
+
+ return this->Continue();
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline ActionResult< Actor > BehaviorMoveTo< Actor, PathCost >::Update( Actor *me, float interval )
+{
+ // if path became invalid during last tick for any reason, we're done
+ if ( !m_path.IsValid() )
+ {
+ if ( m_failAction )
+ {
+ return this->ChangeTo( m_failAction, "Path is invalid" );
+ }
+
+ return this->Done( "Path is invalid" );
+ }
+
+ // move along path - success/fail event handlers will exit behavior when goal is reached
+ m_path.Update( me );
+
+ return this->Continue();
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline EventDesiredResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnMoveToSuccess( Actor *me, const Path *path )
+{
+ if ( m_successAction )
+ {
+ return this->TryChangeTo( m_successAction, RESULT_CRITICAL, "OnMoveToSuccess" );
+ }
+
+ return this->TryDone( RESULT_CRITICAL, "OnMoveToSuccess" );
+}
+
+
+//----------------------------------------------------------------------------------------------
+template < typename Actor, typename PathCost >
+inline EventDesiredResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason )
+{
+ if ( m_failAction )
+ {
+ return this->TryChangeTo( m_failAction, RESULT_CRITICAL, "OnMoveToFailure" );
+ }
+
+ return this->TryDone( RESULT_CRITICAL, "OnMoveToFailure" );
+}
+
+
+
+#endif // _BEHAVIOR_MOVE_TO_H_
+
diff --git a/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp
new file mode 100644
index 0000000..5ac954a
--- /dev/null
+++ b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp
@@ -0,0 +1,75 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// NextBot paths that go through this entity must fulfill the given prerequisites to pass
+// Michael Booth, August 2009
+
+#include "cbase.h"
+#include "func_nav_prerequisite.h"
+#include "ndebugoverlay.h"
+#include "modelentities.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+LINK_ENTITY_TO_CLASS( func_nav_prerequisite, CFuncNavPrerequisite );
+
+BEGIN_DATADESC( CFuncNavPrerequisite )
+ DEFINE_KEYFIELD( m_task, FIELD_INTEGER, "Task" ),
+ DEFINE_KEYFIELD( m_taskEntityName, FIELD_STRING, "Entity" ),
+ DEFINE_KEYFIELD( m_taskValue, FIELD_FLOAT, "Value" ),
+ DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+END_DATADESC()
+
+IMPLEMENT_AUTO_LIST( IFuncNavPrerequisiteAutoList );
+
+
+//-----------------------------------------------------------------------------
+CFuncNavPrerequisite::CFuncNavPrerequisite()
+{
+ m_task = TASK_NONE;
+ m_hTaskEntity = NULL;
+}
+
+
+//-----------------------------------------------------------------------------
+void CFuncNavPrerequisite::Spawn( void )
+{
+ AddSpawnFlags( SF_TRIGGER_ALLOW_CLIENTS );
+
+ BaseClass::Spawn();
+ InitTrigger();
+}
+
+
+//-----------------------------------------------------------------------------
+bool CFuncNavPrerequisite::IsTask( TaskType task ) const
+{
+ return task == m_task ? true : false;
+}
+
+
+//-----------------------------------------------------------------------------
+CBaseEntity *CFuncNavPrerequisite::GetTaskEntity( void )
+{
+ if ( m_hTaskEntity == NULL )
+ {
+ m_hTaskEntity = gEntList.FindEntityByName( NULL, m_taskEntityName );
+ }
+ return m_hTaskEntity;
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CFuncNavPrerequisite::InputEnable( inputdata_t &inputdata )
+{
+ m_isDisabled = false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void CFuncNavPrerequisite::InputDisable( inputdata_t &inputdata )
+{
+ m_isDisabled = true;
+}
diff --git a/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h
new file mode 100644
index 0000000..bcc23f6
--- /dev/null
+++ b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h
@@ -0,0 +1,56 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// NextBot paths that go through this entity must fulfill the given prerequisites to pass
+// Michael Booth, August 2009
+
+#ifndef FUNC_NAV_PREREQUISITE_H
+#define FUNC_NAV_PREREQUISITE_H
+
+#include "triggers.h"
+
+/**
+ * NextBot paths that pass through this entity must fulfill the given prerequisites to pass
+ */
+DECLARE_AUTO_LIST( IFuncNavPrerequisiteAutoList );
+
+class CFuncNavPrerequisite : public CBaseTrigger, public IFuncNavPrerequisiteAutoList
+{
+ DECLARE_CLASS( CFuncNavPrerequisite, CBaseTrigger );
+
+public:
+ CFuncNavPrerequisite();
+
+ DECLARE_DATADESC();
+
+ virtual void Spawn( void );
+
+ enum TaskType
+ {
+ TASK_NONE = 0,
+ TASK_DESTROY_ENTITY = 1,
+ TASK_MOVE_TO_ENTITY = 2,
+ TASK_WAIT = 3,
+ };
+
+ bool IsTask( TaskType type ) const;
+ CBaseEntity *GetTaskEntity( void );
+ float GetTaskValue( void ) const;
+
+ void InputEnable( inputdata_t &inputdata );
+ void InputDisable( inputdata_t &inputdata );
+ bool IsEnabled( void ) const { return !m_isDisabled; }
+
+protected:
+ int m_task;
+ string_t m_taskEntityName;
+ float m_taskValue;
+ bool m_isDisabled;
+ EHANDLE m_hTaskEntity;
+};
+
+inline float CFuncNavPrerequisite::GetTaskValue( void ) const
+{
+ return m_taskValue;
+}
+
+
+#endif // FUNC_NAV_PREREQUISITE_H
diff --git a/game/server/NextBot/NextBot.cpp b/game/server/NextBot/NextBot.cpp
new file mode 100644
index 0000000..0165a03
--- /dev/null
+++ b/game/server/NextBot/NextBot.cpp
@@ -0,0 +1,523 @@
+// NextBotCombatCharacter.cpp
+// Next generation bot system
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "team.h"
+#include "CRagdollMagnet.h"
+
+#include "NextBot.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotBodyInterface.h"
+
+#ifdef TERROR
+#include "TerrorGamerules.h"
+#endif
+
+#include "vprof.h"
+#include "datacache/imdlcache.h"
+#include "EntityFlame.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+ConVar NextBotStop( "nb_stop", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "Stop all NextBots" );
+
+
+//--------------------------------------------------------------------------------------------------------
+class CSendBotCommand
+{
+public:
+ CSendBotCommand( const char *command )
+ {
+ m_command = command;
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ bot->OnCommandString( m_command );
+ return true;
+ }
+
+ const char *m_command;
+};
+
+
+CON_COMMAND_F( nb_command, "Sends a command string to all bots", FCVAR_CHEAT )
+{
+ if ( args.ArgC() <= 1 )
+ {
+ Msg( "Missing command string" );
+ return;
+ }
+
+ CSendBotCommand sendCmd( args.ArgS() );
+ TheNextBots().ForEachBot( sendCmd );
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+BEGIN_DATADESC( NextBotCombatCharacter )
+
+ DEFINE_THINKFUNC( DoThink ),
+
+END_DATADESC()
+
+
+//-----------------------------------------------------------------------------------------------------
+IMPLEMENT_SERVERCLASS_ST( NextBotCombatCharacter, DT_NextBot )
+END_SEND_TABLE()
+
+
+//-----------------------------------------------------------------------------------------------------
+NextBotDestroyer::NextBotDestroyer( int team )
+{
+ m_team = team;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool NextBotDestroyer::operator() ( INextBot *bot )
+{
+ if ( m_team == TEAM_ANY || bot->GetEntity()->GetTeamNumber() == m_team )
+ {
+ // players need to be kicked, not deleted
+ if ( bot->GetEntity()->IsPlayer() )
+ {
+ CBasePlayer *player = dynamic_cast< CBasePlayer * >( bot->GetEntity() );
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
+ }
+ else
+ {
+ UTIL_Remove( bot->GetEntity() );
+ }
+ }
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( nb_delete_all, "Delete all non-player NextBot entities.", FCVAR_CHEAT )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ CTeam *team = NULL;
+
+ if ( args.ArgC() == 2 )
+ {
+ const char *teamName = args[1];
+
+ for( int i=0; i < g_Teams.Count(); ++i )
+ {
+ if ( FStrEq( teamName, g_Teams[i]->GetName() ) )
+ {
+ // delete all bots on this team
+ team = g_Teams[i];
+ break;
+ }
+ }
+
+ if ( team == NULL )
+ {
+ Msg( "Invalid team '%s'\n", teamName );
+ return;
+ }
+ }
+
+ // delete all bots on all teams
+ NextBotDestroyer destroyer( team ? team->GetTeamNumber() : TEAM_ANY );
+ TheNextBots().ForEachBot( destroyer );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+class NextBotApproacher
+{
+public:
+ NextBotApproacher( void )
+ {
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ if ( player )
+ {
+ Vector forward;
+ player->EyeVectors( &forward );
+
+ trace_t result;
+ unsigned int mask = MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE | CONTENTS_GRATE | CONTENTS_WINDOW;
+ UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 999999.9f * forward, mask, player, COLLISION_GROUP_NONE, &result );
+ if ( result.DidHit() )
+ {
+ NDebugOverlay::Cross3D( result.endpos, 5, 0, 255, 0, true, 10.0f );
+ m_isGoalValid = true;
+ m_goal = result.endpos;
+ }
+ else
+ {
+ m_isGoalValid = false;
+ }
+ }
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ if ( TheNextBots().IsDebugFilterMatch( bot ) )
+ {
+ bot->OnCommandApproach( m_goal );
+ }
+ return true;
+ }
+
+ bool m_isGoalValid;
+ Vector m_goal;
+};
+
+CON_COMMAND_F( nb_move_to_cursor, "Tell all NextBots to move to the cursor position", FCVAR_CHEAT )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ NextBotApproacher approach;
+ TheNextBots().ForEachBot( approach );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------
+bool IgnoreActorsTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask )
+{
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+ return ( entity->MyCombatCharacterPointer() == NULL ); // includes all bots, npcs, players, and TF2 buildings
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------
+bool VisionTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask )
+{
+ // Honor BlockLOS also to allow seeing through partially-broken doors
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+ return ( entity->MyCombatCharacterPointer() == NULL && entity->BlocksLOS() );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------
+
+NextBotCombatCharacter::NextBotCombatCharacter( void )
+
+{
+ m_lastAttacker = NULL;
+ m_didModelChange = false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ // reset bot components
+ Reset();
+
+ SetSolid( SOLID_BBOX );
+ AddSolidFlags( FSOLID_NOT_STANDABLE );
+
+ SetMoveType( MOVETYPE_CUSTOM );
+
+ SetCollisionGroup( COLLISION_GROUP_PLAYER );
+
+ m_iMaxHealth = m_iHealth;
+ m_takedamage = DAMAGE_YES;
+
+ MDLCACHE_CRITICAL_SECTION();
+ InitBoneControllers( );
+
+ // set up think callback
+ SetThink( &NextBotCombatCharacter::DoThink );
+ SetNextThink( gpGlobals->curtime );
+
+ m_lastAttacker = NULL;
+}
+
+
+bool NextBotCombatCharacter::IsAreaTraversable( const CNavArea *area ) const
+{
+ if ( !area )
+ return false;
+ ILocomotion *mover = GetLocomotionInterface();
+ if ( mover && !mover->IsAreaTraversable( area ) )
+ return false;
+ return BaseClass::IsAreaTraversable( area );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::DoThink( void )
+{
+ VPROF_BUDGET( "NextBotCombatCharacter::DoThink", "NextBot" );
+
+ SetNextThink( gpGlobals->curtime );
+
+ if ( BeginUpdate() )
+ {
+ // emit model change event
+ if ( m_didModelChange )
+ {
+ m_didModelChange = false;
+
+ OnModelChanged();
+
+ // propagate model change into NextBot event responders
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnModelChanged();
+ }
+ }
+
+ UpdateLastKnownArea();
+
+ // update bot components
+ if ( !NextBotStop.GetBool() && (GetFlags() & FL_FROZEN) == 0 )
+ {
+ Update();
+ }
+
+ EndUpdate();
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::Touch( CBaseEntity *other )
+{
+ if ( ShouldTouch( other ) )
+ {
+ // propagate touch into NextBot event responders
+ trace_t result;
+ result = GetTouchTrace();
+
+ // OnContact refers to *physical* contact, not triggers or other non-physical entities
+ if ( result.DidHit() || other->MyCombatCharacterPointer() != NULL )
+ {
+ OnContact( other, &result );
+ }
+ }
+
+ BaseClass::Touch( other );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::SetModel( const char *szModelName )
+{
+ // actually change the model
+ BaseClass::SetModel( szModelName );
+
+ // need to do a lazy-check because precache system also invokes this
+ m_didModelChange = true;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner )
+{
+ BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner );
+
+ // propagate event to components
+ OnIgnite();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::Ignite( float flFlameLifetime, CBaseEntity *pAttacker )
+{
+ if ( IsOnFire() )
+ return;
+
+ // BaseClass::Ignite stuff, plus SetAttacker on the flame, so our attacker gets credit
+ CEntityFlame *pFlame = CEntityFlame::Create( this );
+ if ( pFlame )
+ {
+ pFlame->SetLifetime( flFlameLifetime );
+ AddFlag( FL_ONFIRE );
+
+ SetEffectEntity( pFlame );
+ }
+ m_OnIgnite.FireOutput( this, this );
+
+ // propagate event to components
+ OnIgnite();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+int NextBotCombatCharacter::OnTakeDamage_Alive( const CTakeDamageInfo &info )
+{
+ // track our last attacker
+ if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() )
+ {
+ m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer();
+ }
+
+ // propagate event to components
+ OnInjured( info );
+
+ return CBaseCombatCharacter::OnTakeDamage_Alive( info );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+int NextBotCombatCharacter::OnTakeDamage_Dying( const CTakeDamageInfo &info )
+{
+ // track our last attacker
+ if ( info.GetAttacker()->MyCombatCharacterPointer() )
+ {
+ m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer();
+ }
+
+ // propagate event to components
+ OnInjured( info );
+
+ return CBaseCombatCharacter::OnTakeDamage_Dying( info );
+}
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Can't use CBaseCombatCharacter's Event_Killed because it will immediately ragdoll us
+ */
+static int g_DeathStartEvent = 0;
+void NextBotCombatCharacter::Event_Killed( const CTakeDamageInfo &info )
+{
+ // track our last attacker
+ if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() )
+ {
+ m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer();
+ }
+
+ // propagate event to my components
+ OnKilled( info );
+
+ // Advance life state to dying
+ m_lifeState = LIFE_DYING;
+
+#ifdef TERROR
+ /*
+ * TODO: Make this game-generic
+ */
+ // Create the death event just like players do.
+ TerrorGameRules()->DeathNoticeForEntity( this, info );
+
+ // Infected specific event
+ TerrorGameRules()->DeathNoticeForInfected( this, info );
+#endif
+
+ if ( GetOwnerEntity() != NULL )
+ {
+ GetOwnerEntity()->DeathNotice( this );
+ }
+
+ // inform the other bots
+ TheNextBots().OnKilled( this, info );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity )
+{
+ ILocomotion *mover = GetLocomotionInterface();
+ if ( mover )
+ {
+ // hack to keep ground entity from being NULL'd when Z velocity is positive
+ SetGroundEntity( mover->GetGround() );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+bool NextBotCombatCharacter::BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector )
+{
+ // See if there's a ragdoll magnet that should influence our force.
+ Vector adjustedForceVector = forceVector;
+ CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( this );
+ if ( magnet )
+ {
+ adjustedForceVector += magnet->GetForceVector( this );
+ }
+
+ // clear the deceased's sound channels.(may have been firing or reloading when killed)
+ EmitSound( "BaseCombatCharacter.StopWeaponSounds" );
+
+ return BaseClass::BecomeRagdoll( info, adjustedForceVector );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::HandleAnimEvent( animevent_t *event )
+{
+ // propagate event to components
+ OnAnimationEvent( event );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Propagate event into NextBot event responders
+ */
+void NextBotCombatCharacter::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea )
+{
+ INextBotEventResponder::OnNavAreaChanged( enteredArea, leftArea );
+
+ BaseClass::OnNavAreaChanged( enteredArea, leftArea );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+Vector NextBotCombatCharacter::EyePosition( void )
+{
+ if ( GetBodyInterface() )
+ {
+ return GetBodyInterface()->GetEyePosition();
+ }
+
+ return BaseClass::EyePosition();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return true if this object can be +used by the bot
+ */
+bool NextBotCombatCharacter::IsUseableEntity( CBaseEntity *entity, unsigned int requiredCaps )
+{
+ if ( entity )
+ {
+ int caps = entity->ObjectCaps();
+ if ( caps & (FCAP_IMPULSE_USE|FCAP_CONTINUOUS_USE|FCAP_ONOFF_USE|FCAP_DIRECTIONAL_USE) )
+ {
+ if ( (caps & requiredCaps) == requiredCaps )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotCombatCharacter::UseEntity( CBaseEntity *entity, USE_TYPE useType )
+{
+ if ( IsUseableEntity( entity ) )
+ {
+ variant_t emptyVariant;
+ entity->AcceptInput( "Use", this, this, emptyVariant, useType );
+ }
+}
diff --git a/game/server/NextBot/NextBot.h b/game/server/NextBot/NextBot.h
new file mode 100644
index 0000000..ae89cab
--- /dev/null
+++ b/game/server/NextBot/NextBot.h
@@ -0,0 +1,104 @@
+// NextBotCombatCharacter.h
+// Next generation bot system
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_H_
+#define _NEXT_BOT_H_
+
+#include "NextBotInterface.h"
+#include "NextBotManager.h"
+
+#ifdef TERROR
+#include "player_lagcompensation.h"
+#endif
+
+class NextBotCombatCharacter;
+struct animevent_t;
+
+extern ConVar NextBotStop;
+
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * A Next Bot derived from CBaseCombatCharacter
+ */
+class NextBotCombatCharacter : public CBaseCombatCharacter, public INextBot
+{
+public:
+ DECLARE_CLASS( NextBotCombatCharacter, CBaseCombatCharacter );
+ DECLARE_SERVERCLASS();
+ DECLARE_DATADESC();
+
+ NextBotCombatCharacter( void );
+ virtual ~NextBotCombatCharacter() { }
+
+ virtual void Spawn( void );
+
+ virtual Vector EyePosition( void );
+
+ virtual INextBot *MyNextBotPointer( void ) { return this; }
+
+ // Event hooks into NextBot system ---------------------------------------
+ virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info );
+ virtual int OnTakeDamage_Dying( const CTakeDamageInfo &info );
+ virtual void Event_Killed( const CTakeDamageInfo &info );
+ virtual void HandleAnimEvent( animevent_t *event );
+ virtual void OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ); // invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL)
+ virtual void Touch( CBaseEntity *other );
+ virtual void SetModel( const char *szModelName );
+ virtual void Ignite( float flFlameLifetime, bool bNPCOnly = true, float flSize = 0.0f, bool bCalledByLevelDesigner = false );
+ virtual void Ignite( float flFlameLifetime, CBaseEntity *pAttacker );
+ //------------------------------------------------------------------------
+
+ virtual bool IsUseableEntity( CBaseEntity *entity, unsigned int requiredCaps = 0 );
+ void UseEntity( CBaseEntity *entity, USE_TYPE useType = USE_TOGGLE );
+
+ // Implement this if you use MOVETYPE_CUSTOM
+ virtual void PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity );
+
+ virtual bool BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector );
+
+ // hook to INextBot update
+ void DoThink( void );
+
+ // expose to public
+ int GetLastHitGroup( void ) const; // where on our body were we injured last
+
+ virtual bool IsAreaTraversable( const CNavArea *area ) const; // return true if we can use the given area
+
+ virtual CBaseCombatCharacter *GetLastAttacker( void ) const; // return the character who last attacked me
+
+ // begin INextBot public interface ----------------------------------------------------------------
+ virtual NextBotCombatCharacter *GetEntity( void ) const { return const_cast< NextBotCombatCharacter * >( this ); }
+ virtual NextBotCombatCharacter *GetNextBotCombatCharacter( void ) const { return const_cast< NextBotCombatCharacter * >( this ); }
+
+
+private:
+ EHANDLE m_lastAttacker;
+
+ bool m_didModelChange;
+};
+
+
+inline CBaseCombatCharacter *NextBotCombatCharacter::GetLastAttacker( void ) const
+{
+ return ( m_lastAttacker.Get() == NULL ) ? NULL : m_lastAttacker->MyCombatCharacterPointer();
+}
+
+inline int NextBotCombatCharacter::GetLastHitGroup( void ) const
+{
+ return LastHitGroup();
+}
+
+//-----------------------------------------------------------------------------------------------------
+class NextBotDestroyer
+{
+public:
+ NextBotDestroyer( int team );
+ bool operator() ( INextBot *bot );
+ int m_team; // the team to delete bots from, or TEAM_ANY for any team
+};
+
+#endif // _NEXT_BOT_H_
diff --git a/game/server/NextBot/NextBotAttentionInterface.cpp b/game/server/NextBot/NextBotAttentionInterface.cpp
new file mode 100644
index 0000000..a774551
--- /dev/null
+++ b/game/server/NextBot/NextBotAttentionInterface.cpp
@@ -0,0 +1,162 @@
+// NextBotAttentionInterface.cpp
+// Manage what this bot pays attention to
+// Author: Michael Booth, April 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBot.h"
+#include "NextBotAttentionInterface.h"
+#include "NextBotBodyInterface.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Reset to initial state
+ */
+void IAttention::Reset( void )
+{
+ m_body = GetBot()->GetBodyInterface();
+
+ m_attentionSet.RemoveAll();
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Update internal state
+ */
+void IAttention::Update( void )
+{
+}
+
+
+//------------------------------------------------------------------------------------------
+void IAttention::AttendTo( const CBaseCombatCharacter *who, const char *reason )
+{
+ if ( !IsAwareOf( who ) )
+ {
+ PointOfInterest p;
+ p.m_type = PointOfInterest::WHO;
+ p.m_who = who;
+ p.m_duration.Start();
+
+ m_attentionSet.AddToTail( p );
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void IAttention::AttendTo( const CBaseEntity *what, const char *reason )
+{
+ if ( !IsAwareOf( what ) )
+ {
+ PointOfInterest p;
+ p.m_type = PointOfInterest::WHAT;
+ p.m_what = what;
+ p.m_duration.Start();
+
+ m_attentionSet.AddToTail( p );
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void IAttention::AttendTo( const Vector &where, IAttention::SignificanceLevel significance, const char *reason )
+{
+ PointOfInterest p;
+ p.m_type = PointOfInterest::WHERE;
+ p.m_where = where;
+ p.m_duration.Start();
+
+ m_attentionSet.AddToTail( p );
+}
+
+
+//------------------------------------------------------------------------------------------
+void IAttention::Disregard( const CBaseCombatCharacter *who, const char *reason )
+{
+ FOR_EACH_VEC( m_attentionSet, it )
+ {
+ if ( m_attentionSet[ it ].m_type == PointOfInterest::WHO )
+ {
+ CBaseCombatCharacter *myWho = m_attentionSet[ it ].m_who;
+
+ if ( !myWho || myWho->entindex() == who->entindex() )
+ {
+ m_attentionSet.Remove( it );
+ return;
+ }
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void IAttention::Disregard( const CBaseEntity *what, const char *reason )
+{
+ FOR_EACH_VEC( m_attentionSet, it )
+ {
+ if ( m_attentionSet[ it ].m_type == PointOfInterest::WHAT )
+ {
+ CBaseCombatCharacter *myWhat = m_attentionSet[ it ].m_what;
+
+ if ( !myWhat || myWhat->entindex() == what->entindex() )
+ {
+ m_attentionSet.Remove( it );
+ return;
+ }
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return true if given actor is in our attending set
+ */
+bool IAttention::IsAwareOf( const CBaseCombatCharacter *who ) const
+{
+ FOR_EACH_VEC( m_attentionSet, it )
+ {
+ if ( m_attentionSet[ it ].m_type == PointOfInterest::WHO )
+ {
+ CBaseCombatCharacter *myWho = m_attentionSet[ it ].m_who;
+
+ if ( myWho && myWho->entindex() == who->entindex() )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return true if given object is in our attending set
+ */
+bool IAttention::IsAwareOf( const CBaseEntity *what ) const
+{
+ FOR_EACH_VEC( m_attentionSet, it )
+ {
+ if ( m_attentionSet[ it ].m_type == PointOfInterest::WHAT )
+ {
+ CBaseEntity *myWhat = m_attentionSet[ it ].m_what;
+
+ if ( myWhat && myWhat->entindex() == what->entindex() )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+} \ No newline at end of file
diff --git a/game/server/NextBot/NextBotAttentionInterface.h b/game/server/NextBot/NextBotAttentionInterface.h
new file mode 100644
index 0000000..26bec43
--- /dev/null
+++ b/game/server/NextBot/NextBotAttentionInterface.h
@@ -0,0 +1,81 @@
+// NextBotAttentionInterface.h
+// Manage what this bot pays attention to
+// Author: Michael Booth, April 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_ATTENTION_INTERFACE_H_
+#define _NEXT_BOT_ATTENTION_INTERFACE_H_
+
+#include "NextBotComponentInterface.h"
+
+class INextBot;
+class IBody;
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for managing what a bot pays attention to.
+ * Vision determines what see see and notice -> Attention determines which of those things we look at -> Low level head/aiming simulation actually moves our head/eyes
+ */
+class IAttention : public INextBotComponent
+{
+public:
+ IAttention( INextBot *bot ) : INextBotComponent( bot ) { }
+ virtual ~IAttention() { }
+
+ virtual void Reset( void ) { } // reset to initial state
+ virtual void Update( void ) { } // update internal state
+
+ enum SignificanceLevel
+ {
+ BORING, // background noise
+ INTERESTING, // notably interesting
+ COMPELLING, // very hard to pay attention to anything else
+ IRRESISTIBLE, // can't look away
+ };
+
+ // override these to control the significance of entities in a context-specific way
+ virtual int CompareSignificance( const CBaseEntity *a, const CBaseEntity *b ) const; // returns <0 if a < b, 0 if a==b, or >0 if a>b
+
+ // bring things to our attention
+ virtual void AttendTo( CBaseEntity *what, const char *reason = NULL );
+ virtual void AttendTo( const Vector &where, SignificanceLevel significance, const char *reason = NULL );
+
+ // remove things from our attention
+ virtual void Disregard( CBaseEntity *what, const char *reason = NULL );
+
+ virtual bool IsAwareOf( CBaseEntity *what ) const; // return true if given object is in our attending set
+ virtual float GetAwareDuration( CBaseEntity *what ) const; // return how long we've been aware of this entity
+
+ // INextBotEventResponder ------------------------------------------------------------------
+ virtual void OnInjured( const CTakeDamageInfo &info ); // when bot is damaged by something
+ virtual void OnContact( CBaseEntity *other, CGameTrace *result = NULL ); // invoked when bot touches 'other'
+ virtual void OnSight( CBaseEntity *subject ); // when subject initially enters bot's visual awareness
+ virtual void OnLostSight( CBaseEntity *subject ); // when subject leaves enters bot's visual awareness
+ virtual void OnSound( CBaseEntity *source, const CSoundParameters &params ); // when an entity emits a sound
+
+
+private:
+ IBody *m_body; // to access head aiming
+
+ struct PointOfInterest
+ {
+ enum { ENTITY, POSITION } m_type;
+ CHandle< CBaseEntity > m_entity;
+ Vector m_position;
+
+ IntervalTimer m_duration; // how long has this PoI been in our attention set
+ };
+
+ CUtlVector< PointOfInterest > m_attentionSet; // the set of things we are attending to
+
+
+};
+
+inline int IAttention::CompareSignificance( const CBaseEntity *a, const CBaseEntity *b ) const
+{
+ return 0;
+}
+
+#endif // _NEXT_BOT_ATTENTION_INTERFACE_H_
+
diff --git a/game/server/NextBot/NextBotBehavior.h b/game/server/NextBot/NextBotBehavior.h
new file mode 100644
index 0000000..2f92a84
--- /dev/null
+++ b/game/server/NextBot/NextBotBehavior.h
@@ -0,0 +1,1936 @@
+// NextBotBehaviorEngine.h
+// Behavioral system constructed from Actions
+// Author: Michael Booth, April 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _BEHAVIOR_ENGINE_H_
+#define _BEHAVIOR_ENGINE_H_
+
+#include "fmtstr.h"
+#include "NextBotEventResponderInterface.h"
+#include "NextBotContextualQueryInterface.h"
+#include "NextBotDebug.h"
+#include "tier0/vprof.h"
+
+
+//#define DEBUG_BEHAVIOR_MEMORY
+extern ConVar NextBotDebugHistory;
+
+/**
+ * Notes:
+ *
+ * By using return results to cause transitions, we ensure the atomic-ness
+ * of these transitions. For instance, it is not possible to change to a
+ * new Action and continue execution of code in the current Action.
+ *
+ * Creation and deletion of Actions during transitions allows passing of
+ * type-safe arguments between Actions via constructors.
+ *
+ * Events are propagated to each Action in the hierarchy. If an
+ * action is suspended for another action, it STILL RECEIVES EVENTS
+ * that are not handled by the events "above it" in the suspend stack.
+ * In other words, the active Action gets the first response, and if it
+ * returns CONTINUE, the Action buried beneath it can process it,
+ * and so on deeper into the stack of suspended Actions.
+ *
+ * About events:
+ * It is not possible to have event handlers instantaneously change
+ * state upon return due to out-of-order and recurrence issues, not
+ * to mention deleting the state out from under itself. Therefore,
+ * events return DESIRED results, and the highest priority result
+ * is executed at the next Update().
+ *
+ * About buried Actions causing SUSPEND_FOR results:
+ * If a buried Action reacts to an event by returning a SUSPEND_FOR,
+ * the new interrupting Action is put at the TOP of the stack, burying
+ * whatever Action was there.
+ *
+ */
+
+
+// forward declaration
+template < typename Actor > class Action;
+
+/**
+ * The possible consequences of an Action
+ */
+enum ActionResultType
+{
+ CONTINUE, // continue executing this action next frame - nothing has changed
+ CHANGE_TO, // change actions next frame
+ SUSPEND_FOR, // put the current action on hold for the new action
+ DONE, // this action has finished, resume suspended action
+ SUSTAIN, // for use with event handlers - a way to say "It's important to keep doing what I'm doing"
+};
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Actions and Event processors return results derived from this class.
+ * Do not assemble this yourself - use the Continue(), ChangeTo(), Done(), and SuspendFor()
+ * methods within Action.
+ */
+template < typename Actor >
+struct IActionResult
+{
+ IActionResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, const char *reason = NULL )
+ {
+ m_type = type;
+ m_action = action;
+ m_reason = reason;
+ }
+
+ bool IsDone( void ) const
+ {
+ return ( m_type == DONE );
+ }
+
+ bool IsContinue( void ) const
+ {
+ return ( m_type == CONTINUE );
+ }
+
+ bool IsRequestingChange( void ) const
+ {
+ return ( m_type == CHANGE_TO || m_type == SUSPEND_FOR || m_type == DONE );
+ }
+
+ const char *GetTypeName( void ) const
+ {
+ switch ( m_type )
+ {
+ case CHANGE_TO: return "CHANGE_TO";
+ case SUSPEND_FOR: return "SUSPEND_FOR";
+ case DONE: return "DONE";
+ case SUSTAIN: return "SUSTAIN";
+
+ default:
+ case CONTINUE: return "CONTINUE";
+ }
+ }
+
+ ActionResultType m_type;
+ Action< Actor > *m_action;
+ const char *m_reason;
+};
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * When an Action is executed it returns this result.
+ * Do not assemble this yourself - use the Continue(), ChangeTo(), Done(), and SuspendFor()
+ * methods within Action.
+ */
+template < typename Actor >
+struct ActionResult : public IActionResult< Actor >
+{
+ // this is derived from IActionResult to ensure that ActionResult and EventDesiredResult cannot be silently converted
+ ActionResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, const char *reason = NULL ) : IActionResult< Actor >( type, action, reason ) { }
+};
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * When an event is processed, it returns this DESIRED result,
+ * which may or MAY NOT happen, depending on other event results
+ * that occur simultaneously.
+ * Do not assemble this yourself - use the TryContinue(), TryChangeTo(), TryDone(), TrySustain(),
+ * and TrySuspendFor() methods within Action.
+ */
+enum EventResultPriorityType
+{
+ RESULT_NONE, // no result
+ RESULT_TRY, // use this result, or toss it out, either is ok
+ RESULT_IMPORTANT, // try extra-hard to use this result
+ RESULT_CRITICAL // this result must be used - emit an error if it can't be
+};
+
+template < typename Actor >
+struct EventDesiredResult : public IActionResult< Actor >
+{
+ EventDesiredResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) : IActionResult< Actor >( type, action, reason )
+ {
+ m_priority = priority;
+ }
+
+ EventResultPriorityType m_priority;
+};
+
+
+//-------------------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------------------
+/**
+ * A Behavior is the root of an Action hierarchy as well as its container/manager.
+ * Instantiate a Behavior with the root Action of your behavioral system, and
+ * call Behavior::Update() to drive it.
+ */
+template < typename Actor >
+class Behavior : public INextBotEventResponder, public IContextualQuery
+{
+public:
+ DECLARE_CLASS( Behavior, INextBotEventResponder );
+
+ Behavior( Action< Actor > *initialAction, const char *name = "" ) : m_name( "%s", name )
+ {
+ m_action = initialAction;
+ m_me = NULL;
+ }
+
+ virtual ~Behavior()
+ {
+ if ( m_me && m_action )
+ {
+ // allow all currently active Actions to end
+ m_action->InvokeOnEnd( m_me, this, NULL );
+ m_me = NULL;
+ }
+
+ // dig down to the bottom of the action stack and delete
+ // that, so we don't leak action memory since action
+ // destructors intentionally don't delete actions
+ // "buried" underneath them.
+ Action< Actor > *bottomAction;
+ for( bottomAction = m_action; bottomAction && bottomAction->m_buriedUnderMe; bottomAction = bottomAction->m_buriedUnderMe )
+ ;
+
+ if ( bottomAction )
+ {
+ delete bottomAction;
+ }
+
+ // delete any dead Actions
+ m_deadActionVector.PurgeAndDeleteElements();
+ }
+
+ /**
+ * Reset this Behavior with the given Action. If this Behavior
+ * was already running, this will delete all current Actions and
+ * restart the Behavior with the new one.
+ */
+ void Reset( Action< Actor > *action )
+ {
+ if ( m_me && m_action )
+ {
+ // allow all currently active Actions to end
+ m_action->InvokeOnEnd( m_me, this, NULL );
+ m_me = NULL;
+ }
+
+ // find "bottom" action (see comment in destructor)
+ Action< Actor > *bottomAction;
+ for( bottomAction = m_action; bottomAction && bottomAction->m_buriedUnderMe; bottomAction = bottomAction->m_buriedUnderMe )
+ ;
+
+ if ( bottomAction )
+ {
+ delete bottomAction;
+ }
+
+ // delete any dead Actions
+ m_deadActionVector.PurgeAndDeleteElements();
+
+ m_action = action;
+ }
+
+ /**
+ * Return true if this Behavior contains no actions
+ */
+ bool IsEmpty( void ) const
+ {
+ return m_action == NULL;
+ }
+
+ /**
+ * Execute this Behavior
+ */
+ void Update( Actor *me, float interval )
+ {
+ if ( me == NULL || IsEmpty() )
+ {
+ return;
+ }
+
+ m_me = me;
+
+ m_action = m_action->ApplyResult( me, this, m_action->InvokeUpdate( me, this, interval ) );
+
+ if ( m_action && me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ CFmtStr msg;
+ me->DisplayDebugText( msg.sprintf( "%s: %s", GetName(), m_action->DebugString() ) );
+ }
+
+ // delete any dead Actions
+ m_deadActionVector.PurgeAndDeleteElements();
+ }
+
+ /**
+ * If this Behavior has not been Update'd in a long time,
+ * call Resume() to let the system know its internal state may
+ * be out of date.
+ */
+ void Resume( Actor *me )
+ {
+ if ( me == NULL || IsEmpty() )
+ {
+ return;
+ }
+
+ m_action = m_action->ApplyResult( me, this, m_action->OnResume( me, NULL ) );
+
+ if ( m_action && me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ CFmtStr msg;
+ me->DisplayDebugText( msg.sprintf( "%s: %s", GetName(), m_action->DebugString() ) );
+ }
+ }
+
+ /**
+ * Use this method to destroy Actions used by this Behavior.
+ * We cannot delete Actions in-line since Action updates can potentially
+ * invoke event responders which will then use potentially deleted
+ * Action pointers, causing memory corruption.
+ * Instead, we will collect the dead Actions and delete them at the
+ * end of Update().
+ */
+ void DestroyAction( Action< Actor > *dead )
+ {
+ m_deadActionVector.AddToTail( dead );
+ }
+
+ const char *GetName( void ) const
+ {
+ return m_name;
+ }
+
+ // INextBotEventResponder propagation ----------------------------------------------------------------------
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const
+ {
+ return m_action;
+ }
+
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const
+ {
+ return NULL;
+ }
+
+ // IContextualQuery propagation ----------------------------------------------------------------------------
+ virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const // if the desired item was available right now, should we pick it up?
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->ShouldPickUp( me, item );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+ virtual QueryResultType ShouldHurry( const INextBot *me ) const // are we in a hurry?
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->ShouldHurry( me );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+ virtual QueryResultType ShouldRetreat( const INextBot *me ) const // is it time to retreat?
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->ShouldRetreat( me );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+ virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const // should we attack "them"?
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->ShouldAttack( me, them );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+ virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const // return true if we should wait for 'blocker' that is across our path somewhere up ahead.
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->IsHindrance( me, blocker );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+
+ virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const // given a subject, return the world space position we should aim at
+ {
+ Vector result = vec3_origin;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == vec3_origin )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == vec3_origin )
+ {
+ result = action->SelectTargetPoint( me, subject );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+
+ /**
+ * 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.
+ */
+ virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const
+ {
+ QueryResultType result = ANSWER_UNDEFINED;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == ANSWER_UNDEFINED )
+ {
+ result = action->IsPositionAllowed( me, pos );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+
+
+ 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, or NULL if we have no opinion
+ {
+ const CKnownEntity *result = NULL;
+
+ if ( m_action )
+ {
+ // find innermost child action
+ Action< Actor > *action;
+ for( action = m_action; action->m_child; action = action->m_child )
+ ;
+
+ // work our way through our containers
+ while( action && result == NULL )
+ {
+ Action< Actor > *containingAction = action->m_parent;
+
+ // work our way up the stack
+ while( action && result == NULL )
+ {
+ result = action->SelectMoreDangerousThreat( me, subject, threat1, threat2 );
+ action = action->GetActionBuriedUnderMe();
+ }
+
+ action = containingAction;
+ }
+ }
+
+ return result;
+ }
+
+
+private:
+ Action< Actor > *m_action;
+
+ #define MAX_NAME_LENGTH 32
+ CFmtStrN< MAX_NAME_LENGTH > m_name;
+
+ Actor *m_me;
+
+ CUtlVector< Action< Actor > * > m_deadActionVector; // completed Actions pending deletion
+};
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Something an Actor does.
+ * Actions can contain Actions, representing the precise context of the Actor's behavior.
+ * A system of Actions is contained within a Behavior, which acts as the manager
+ * of the Action system.
+ */
+template < typename Actor >
+class Action : public INextBotEventResponder, public IContextualQuery
+{
+public:
+ DECLARE_CLASS( Action, INextBotEventResponder );
+
+ Action( void );
+ virtual ~Action();
+
+ virtual const char *GetName( void ) const = 0; // return name of this action
+ virtual bool IsNamed( const char *name ) const; // return true if given name matches the name of this Action
+ virtual const char *GetFullName( void ) const; // return a temporary string showing the full lineage of this one action
+ Actor *GetActor( void ) const; // return the Actor performing this Action (valid just before OnStart() is invoked)
+
+ //-----------------------------------------------------------------------------------------
+ /**
+ * Try to start the Action. Result is immediately processed,
+ * which can cause an immediate transition, another OnStart(), etc.
+ * An Action can count on each OnStart() being followed (eventually) with an OnEnd().
+ */
+ virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction ) { return Continue(); }
+
+ /**
+ * Do the work of the Action. It is possible for Update to not be
+ * called between a given OnStart/OnEnd pair due to immediate transitions.
+ */
+ virtual ActionResult< Actor > Update( Actor *me, float interval ) { return Continue(); }
+
+ // Invoked when an Action is ended for any reason
+ virtual void OnEnd( Actor *me, Action< Actor > *nextAction ) { }
+
+ /*
+ * When an Action is suspended by a new action.
+ * Note that only CONTINUE and DONE are valid results. All other results will
+ * be considered as a CONTINUE.
+ */
+ virtual ActionResult< Actor > OnSuspend( Actor *me, Action< Actor > *interruptingAction ) { return Continue(); }
+
+ // When an Action is resumed after being suspended
+ virtual ActionResult< Actor > OnResume( Actor *me, Action< Actor > *interruptingAction ) { return Continue(); }
+
+ /**
+ * To cause a state change, use these methods to create an ActionResult to
+ * return from OnStart, Update, or OnResume.
+ */
+ ActionResult< Actor > Continue( void ) const;
+ ActionResult< Actor > ChangeTo( Action< Actor > *action, const char *reason = NULL ) const;
+ ActionResult< Actor > SuspendFor( Action< Actor > *action, const char *reason = NULL ) const;
+ ActionResult< Actor > Done( const char *reason = NULL ) const;
+
+ // create and return an Action to start as sub-action within this Action when it starts
+ virtual Action< Actor > *InitialContainedAction( Actor *me ) { return NULL; }
+
+ //-----------------------------------------------------------------------------------------
+ /**
+ * Override the event handler methods below to respond to events that occur during this Action
+ * NOTE: These are identical to the events in INextBotEventResponder with the addition
+ * of an actor argument and a return result. Their translators are located in the private area
+ * below.
+ */
+ virtual EventDesiredResult< Actor > OnLeaveGround( Actor *me, CBaseEntity *ground ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnLandOnGround( Actor *me, CBaseEntity *ground ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnContact( Actor *me, CBaseEntity *other, CGameTrace *result = NULL ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnMoveToSuccess( Actor *me, const Path *path ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnStuck( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnUnStuck( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnPostureChanged( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnAnimationActivityComplete( Actor *me, int activity ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnAnimationActivityInterrupted( Actor *me, int activity ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnAnimationEvent( Actor *me, animevent_t *event ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnIgnite( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnInjured( Actor *me, const CTakeDamageInfo &info ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnKilled( Actor *me, const CTakeDamageInfo &info ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnOtherKilled( Actor *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnSight( Actor *me, CBaseEntity *subject ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnLostSight( Actor *me, CBaseEntity *subject ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnSound( Actor *me, CBaseEntity *source, const Vector &pos, KeyValues *keys ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnSpokeConcept( Actor *me, CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnWeaponFired( Actor *me, CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnNavAreaChanged( Actor *me, CNavArea *newArea, CNavArea *oldArea ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnModelChanged( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnPickUp( Actor *me, CBaseEntity *item, CBaseCombatCharacter *giver ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnDrop( Actor *me, CBaseEntity *item ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnActorEmoted( Actor *me, CBaseCombatCharacter *emoter, int emote ) { return TryContinue(); }
+
+ virtual EventDesiredResult< Actor > OnCommandAttack( Actor *me, CBaseEntity *victim ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandApproach( Actor *me, const Vector &pos, float range ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandApproach( Actor *me, CBaseEntity *goal ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandRetreat( Actor *me, CBaseEntity *threat, float range ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandPause( Actor *me, float duration ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandResume( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandString( Actor *me, const char *command ) { return TryContinue(); }
+
+ virtual EventDesiredResult< Actor > OnShoved( Actor *me, CBaseEntity *pusher ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnBlinded( Actor *me, CBaseEntity *blinder ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnTerritoryContested( Actor *me, int territoryID ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnTerritoryCaptured( Actor *me, int territoryID ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnTerritoryLost( Actor *me, int territoryID ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnWin( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnLose( Actor *me ) { return TryContinue(); }
+
+#ifdef DOTA_SERVER_DLL
+ virtual EventDesiredResult< Actor > OnCommandMoveTo( Actor *me, const Vector &pos ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandMoveToAggressive( Actor *me, const Vector &pos ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCommandAttack( Actor *me, CBaseEntity *victim, bool bDeny ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCastAbilityNoTarget( Actor *me, CDOTABaseAbility *ability ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCastAbilityOnPosition( Actor *me, CDOTABaseAbility *ability, const Vector &pos ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCastAbilityOnTarget( Actor *me, CDOTABaseAbility *ability, CBaseEntity *target ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnDropItem( Actor *me, const Vector &pos, CBaseEntity *item ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnPickupItem( Actor *me, CBaseEntity *item ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnPickupRune( Actor *me, CBaseEntity *item ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnStop( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnFriendThreatened( Actor *me, CBaseEntity *friendly, CBaseEntity *threat ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnCancelAttack( Actor *me, CBaseEntity *pTarget ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnDominated( Actor *me ) { return TryContinue(); }
+ virtual EventDesiredResult< Actor > OnWarped( Actor *me, Vector vStartPos ) { return TryContinue(); }
+#endif
+
+ /**
+ * Event handlers must return one of these.
+ */
+ EventDesiredResult< Actor > TryContinue( EventResultPriorityType priority = RESULT_TRY ) const;
+ EventDesiredResult< Actor > TryChangeTo( Action< Actor > *action, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const;
+ EventDesiredResult< Actor > TrySuspendFor( Action< Actor > *action, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const;
+ EventDesiredResult< Actor > TryDone( EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const;
+ EventDesiredResult< Actor > TryToSustain( EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const;
+
+
+ //-----------------------------------------------------------------------------------------
+ Action< Actor > *GetActiveChildAction( void ) const;
+ Action< Actor > *GetParentAction( void ) const; // the Action that I'm running inside of
+
+ bool IsSuspended( void ) const; // return true if we are currently suspended for another Action
+
+ const char *DebugString( void ) const; // return a temporary string describing the current action stack for debugging
+
+ /**
+ * Sometimes we want to pass through other NextBots. OnContact() will always
+ * be invoked, but collision resolution can be skipped if this
+ * method returns false.
+ */
+ virtual bool IsAbleToBlockMovementOf( const INextBot *botInMotion ) const { return true; }
+
+ // INextBotEventResponder propagation ----------------------------------------------------------------------
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const;
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const;
+
+
+private:
+
+ /**
+ * These macros are used below to translate INextBotEventResponder event methods
+ * into Action event handler methods
+ */
+ #define PROCESS_EVENT( METHOD ) \
+ { \
+ if ( !m_isStarted ) \
+ return; \
+ \
+ Action< Actor > *_action = this; \
+ EventDesiredResult< Actor > _result; \
+ \
+ while( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool())) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \
+ } \
+ _result = _action->METHOD( m_actor ); \
+ if ( !_result.IsContinue() ) \
+ break; \
+ _action = _action->GetActionBuriedUnderMe(); \
+ } \
+ \
+ if ( _action ) \
+ { \
+ if ( m_actor && _result.IsRequestingChange() && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \
+ } \
+ \
+ _action->StorePendingEventResult( _result, #METHOD ); \
+ } \
+ \
+ INextBotEventResponder::METHOD(); \
+ }
+
+
+ #define PROCESS_EVENT_WITH_1_ARG( METHOD, ARG1 ) \
+ { \
+ if ( !m_isStarted ) \
+ return; \
+ \
+ Action< Actor > *_action = this; \
+ EventDesiredResult< Actor > _result; \
+ \
+ while( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \
+ } \
+ _result = _action->METHOD( m_actor, ARG1 ); \
+ if ( !_result.IsContinue() ) \
+ break; \
+ _action = _action->GetActionBuriedUnderMe(); \
+ } \
+ \
+ if ( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \
+ } \
+ \
+ _action->StorePendingEventResult( _result, #METHOD ); \
+ } \
+ \
+ INextBotEventResponder::METHOD( ARG1 ); \
+ }
+
+
+ #define PROCESS_EVENT_WITH_2_ARGS( METHOD, ARG1, ARG2 ) \
+ { \
+ if ( !m_isStarted ) \
+ return; \
+ \
+ Action< Actor > *_action = this; \
+ EventDesiredResult< Actor > _result; \
+ \
+ while( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \
+ } \
+ _result = _action->METHOD( m_actor, ARG1, ARG2 ); \
+ if ( !_result.IsContinue() ) \
+ break; \
+ _action = _action->GetActionBuriedUnderMe(); \
+ } \
+ \
+ if ( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \
+ } \
+ \
+ _action->StorePendingEventResult( _result, #METHOD ); \
+ } \
+ \
+ INextBotEventResponder::METHOD( ARG1, ARG2 ); \
+ }
+
+
+ #define PROCESS_EVENT_WITH_3_ARGS( METHOD, ARG1, ARG2, ARG3 ) \
+ { \
+ if ( !m_isStarted ) \
+ return; \
+ \
+ Action< Actor > *_action = this; \
+ EventDesiredResult< Actor > _result; \
+ \
+ while( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \
+ } \
+ _result = _action->METHOD( m_actor, ARG1, ARG2, ARG3 ); \
+ if ( !_result.IsContinue() ) \
+ break; \
+ _action = _action->GetActionBuriedUnderMe(); \
+ } \
+ \
+ if ( _action ) \
+ { \
+ if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \
+ { \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \
+ m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \
+ } \
+ \
+ _action->StorePendingEventResult( _result, #METHOD ); \
+ } \
+ \
+ INextBotEventResponder::METHOD( ARG1, ARG2, ARG3 ); \
+ }
+
+
+ /**
+ * Translate incoming events into Action events
+ * DO NOT OVERRIDE THESE METHODS
+ */
+ virtual void OnLeaveGround( CBaseEntity *ground ) { PROCESS_EVENT_WITH_1_ARG( OnLeaveGround, ground ); }
+ virtual void OnLandOnGround( CBaseEntity *ground ) { PROCESS_EVENT_WITH_1_ARG( OnLandOnGround, ground ); }
+ virtual void OnContact( CBaseEntity *other, CGameTrace *result ) { PROCESS_EVENT_WITH_2_ARGS( OnContact, other, result ); }
+ virtual void OnMoveToSuccess( const Path *path ) { PROCESS_EVENT_WITH_1_ARG( OnMoveToSuccess, path ); }
+ virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ) { PROCESS_EVENT_WITH_2_ARGS( OnMoveToFailure, path, reason ); }
+ virtual void OnStuck( void ) { PROCESS_EVENT( OnStuck ); }
+ virtual void OnUnStuck( void ) { PROCESS_EVENT( OnUnStuck ); }
+ virtual void OnPostureChanged( void ) { PROCESS_EVENT( OnPostureChanged ); }
+ virtual void OnAnimationActivityComplete( int activity ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationActivityComplete, activity ); }
+ virtual void OnAnimationActivityInterrupted( int activity ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationActivityInterrupted, activity ); }
+ virtual void OnAnimationEvent( animevent_t *event ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationEvent, event ); }
+ virtual void OnIgnite( void ) { PROCESS_EVENT( OnIgnite ); }
+ virtual void OnInjured( const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_1_ARG( OnInjured, info ); }
+ virtual void OnKilled( const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_1_ARG( OnKilled, info ); }
+ virtual void OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_2_ARGS( OnOtherKilled, victim, info ); }
+ virtual void OnSight( CBaseEntity *subject ) { PROCESS_EVENT_WITH_1_ARG( OnSight, subject ); }
+ virtual void OnLostSight( CBaseEntity *subject ) { PROCESS_EVENT_WITH_1_ARG( OnLostSight, subject ); }
+ virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ) { PROCESS_EVENT_WITH_3_ARGS( OnSound, source, pos, keys ); }
+ virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) { PROCESS_EVENT_WITH_3_ARGS( OnSpokeConcept, who, concept, response ); }
+ virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) { PROCESS_EVENT_WITH_2_ARGS( OnWeaponFired, whoFired, weapon ); }
+ virtual void OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea ) { PROCESS_EVENT_WITH_2_ARGS( OnNavAreaChanged, newArea, oldArea ); }
+ virtual void OnModelChanged( void ) { PROCESS_EVENT( OnModelChanged ); }
+ virtual void OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver ) { PROCESS_EVENT_WITH_2_ARGS( OnPickUp, item, giver ); }
+ virtual void OnDrop( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnDrop, item ); }
+ virtual void OnActorEmoted( CBaseCombatCharacter *emoter, int emote ) { PROCESS_EVENT_WITH_2_ARGS( OnActorEmoted, emoter, emote ); }
+
+ virtual void OnCommandAttack( CBaseEntity *victim ) { PROCESS_EVENT_WITH_1_ARG( OnCommandAttack, victim ); }
+ virtual void OnCommandApproach( const Vector &pos, float range ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandApproach, pos, range ); }
+ virtual void OnCommandApproach( CBaseEntity *goal ) { PROCESS_EVENT_WITH_1_ARG( OnCommandApproach, goal ); }
+ virtual void OnCommandRetreat( CBaseEntity *threat, float range ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandRetreat, threat, range ); }
+ virtual void OnCommandPause( float duration ) { PROCESS_EVENT_WITH_1_ARG( OnCommandPause, duration ); }
+ virtual void OnCommandResume( void ) { PROCESS_EVENT( OnCommandResume ); }
+ virtual void OnCommandString( const char *command ) { PROCESS_EVENT_WITH_1_ARG( OnCommandString, command ); }
+
+ virtual void OnShoved( CBaseEntity *pusher ) { PROCESS_EVENT_WITH_1_ARG( OnShoved, pusher ); }
+ virtual void OnBlinded( CBaseEntity *blinder ) { PROCESS_EVENT_WITH_1_ARG( OnBlinded, blinder ); }
+ virtual void OnTerritoryContested( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryContested, territoryID ); }
+ virtual void OnTerritoryCaptured( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryCaptured, territoryID ); }
+ virtual void OnTerritoryLost( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryLost, territoryID ); }
+ virtual void OnWin( void ) { PROCESS_EVENT( OnWin ); }
+ virtual void OnLose( void ) { PROCESS_EVENT( OnLose ); }
+
+#ifdef DOTA_SERVER_DLL
+ virtual void OnCommandMoveTo( const Vector &pos ) { PROCESS_EVENT_WITH_1_ARG( OnCommandMoveTo, pos ); }
+ virtual void OnCommandMoveToAggressive( const Vector &pos ) { PROCESS_EVENT_WITH_1_ARG( OnCommandMoveToAggressive, pos ); }
+ virtual void OnCommandAttack( CBaseEntity *victim, bool bDeny ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandAttack, victim, bDeny ); }
+ virtual void OnCastAbilityNoTarget( CDOTABaseAbility *ability ) { PROCESS_EVENT_WITH_1_ARG( OnCastAbilityNoTarget, ability ); }
+ virtual void OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos ) { PROCESS_EVENT_WITH_2_ARGS( OnCastAbilityOnPosition, ability, pos ); }
+ virtual void OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target ) { PROCESS_EVENT_WITH_2_ARGS( OnCastAbilityOnTarget, ability, target ); }
+ virtual void OnDropItem( const Vector &pos, CBaseEntity *item ) { PROCESS_EVENT_WITH_2_ARGS( OnDropItem, pos, item ); }
+ virtual void OnPickupItem( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnPickupItem, item ); }
+ virtual void OnPickupRune( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnPickupRune, item ); }
+ virtual void OnStop() { PROCESS_EVENT( OnStop ); }
+ virtual void OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat ) { PROCESS_EVENT_WITH_2_ARGS( OnFriendThreatened, friendly, threat ); }
+ virtual void OnCancelAttack( CBaseEntity *pTarget ) { PROCESS_EVENT_WITH_1_ARG( OnCancelAttack, pTarget ); }
+ virtual void OnDominated() { PROCESS_EVENT( OnDominated ); }
+ virtual void OnWarped( Vector vStartPos ) { PROCESS_EVENT_WITH_1_ARG( OnWarped, vStartPos ); }
+#endif
+
+ friend class Behavior< Actor>; // the containing Behavior class
+ Behavior< Actor > *m_behavior; // the Behavior this Action is part of
+
+ Action< Actor > *m_parent; // the Action that contains us
+ Action< Actor > *m_child; // the ACTIVE Action we contain, top of the stack. Use m_buriedUnderMe, m_coveringMe on the child to traverse to other suspended children
+
+ Action< Actor > *m_buriedUnderMe; // the Action just "under" us in the stack that we will resume to when we finish
+ Action< Actor > *m_coveringMe; // the Action just "above" us in the stack that will resume to us when it finishes
+
+ Actor *m_actor; // only valid after OnStart()
+ mutable EventDesiredResult< Actor > m_eventResult; // set by event handlers
+ bool m_isStarted; // Action doesn't start until OnStart() is invoked
+ bool m_isSuspended; // are we suspended for another Action
+
+ Action< Actor > *GetActionBuriedUnderMe( void ) const // return Action just "under" us that we will resume to when we finish
+ {
+ return m_buriedUnderMe;
+ }
+
+ Action< Actor > *GetActionCoveringMe( void ) const // return Action just "above" us that will resume to us when it finishes
+ {
+ return m_coveringMe;
+ }
+
+ /**
+ * If any Action buried underneath me has either exited
+ * or is changing to a different Action, we're "out of scope"
+ */
+ bool IsOutOfScope( void ) const
+ {
+ for( Action< Actor > *under = GetActionBuriedUnderMe(); under; under = under->GetActionBuriedUnderMe() )
+ {
+ if ( under->m_eventResult.m_type == CHANGE_TO ||
+ under->m_eventResult.m_type == DONE )
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Process any pending events with the stack. This is called
+ * by the active Action on the top of the stack, and walks
+ * through any buried Actions checking for pending event results.
+ */
+ ActionResult< Actor > ProcessPendingEvents( void ) const
+ {
+ // if an event has requested a change, honor it
+ if ( m_eventResult.IsRequestingChange() )
+ {
+ ActionResult< Actor > result( m_eventResult.m_type, m_eventResult.m_action, m_eventResult.m_reason );
+
+ // clear event result in case this change is a suspend and we later resume this action
+ m_eventResult = TryContinue( RESULT_NONE );
+
+ return result;
+ }
+
+ // check for pending event changes buried in the stack
+ Action< Actor > *under = GetActionBuriedUnderMe();
+ while( under )
+ {
+ if ( under->m_eventResult.m_type == SUSPEND_FOR )
+ {
+ // process this pending event in-place and push new Action on the top of the stack
+ ActionResult< Actor > result( under->m_eventResult.m_type, under->m_eventResult.m_action, under->m_eventResult.m_reason );
+
+ // clear event result in case this change is a suspend and we later resume this action
+ under->m_eventResult = TryContinue( RESULT_NONE );
+
+ return result;
+ }
+
+ under = under->GetActionBuriedUnderMe();
+ }
+
+ return Continue();
+ }
+
+ // given the result of this Action's work, apply the result to potentially cause a state transition
+ Action< Actor > * ApplyResult( Actor *me, Behavior< Actor > *behavior, ActionResult< Actor > result );
+
+ /**
+ * The methods below do the bookkeeping of each event, propagate the activity through the hierarchy,
+ * and invoke the virtual event for each.
+ */
+ ActionResult< Actor > InvokeOnStart( Actor *me, Behavior< Actor > *behavior, Action< Actor > *priorAction, Action< Actor > *buriedUnderMeAction );
+ ActionResult< Actor > InvokeUpdate( Actor *me, Behavior< Actor > *behavior, float interval );
+ void InvokeOnEnd( Actor *me, Behavior< Actor > *behavior, Action< Actor > *nextAction );
+ Action< Actor > * InvokeOnSuspend( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction );
+ ActionResult< Actor > InvokeOnResume( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction );
+
+ /**
+ * Store the given event result, attending to priorities
+ */
+ void StorePendingEventResult( const EventDesiredResult< Actor > &result, const char *eventName )
+ {
+ if ( result.IsContinue() )
+ {
+ return;
+ }
+
+ if ( result.m_priority >= m_eventResult.m_priority )
+ {
+ if ( m_eventResult.m_priority == RESULT_CRITICAL )
+ {
+ if ( developer.GetBool() )
+ {
+ DevMsg( "%3.2f: WARNING: %s::%s() RESULT_CRITICAL collision\n", gpGlobals->curtime, GetName(), eventName );
+ }
+ }
+
+ // new result as important or more so - destroy the replaced action
+ if ( m_eventResult.m_action )
+ {
+ delete m_eventResult.m_action;
+ }
+
+ // We keep the most recently processed event because this allows code to check history/state to
+ // do custom event collision handling. If we keep the first event at this priority and discard
+ // subsequent events (original behavior) there is no way to predict future collision resolutions (MSB).
+ m_eventResult = result;
+ }
+ else
+ {
+ // new result is lower priority than previously stored result - discard it
+ if ( result.m_action )
+ {
+ // destroy the unused action
+ delete result.m_action;
+ }
+ }
+ }
+
+ char *BuildDecoratedName( char *name, const Action< Actor > *action ) const; // recursive name outMsg for DebugString()
+
+ void PrintStateToConsole( void ) const;
+};
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+Action< Actor >::Action( void )
+{
+ m_parent = NULL;
+ m_child = NULL;
+ m_buriedUnderMe = NULL;
+ m_coveringMe = NULL;
+ m_actor = NULL;
+ m_behavior = NULL;
+
+ m_isStarted = false;
+ m_isSuspended = false;
+
+ m_eventResult = TryContinue( RESULT_NONE );
+
+#ifdef DEBUG_BEHAVIOR_MEMORY
+ ConColorMsg( Color( 255, 0, 255, 255 ), "%3.2f: NEW %0X\n", gpGlobals->curtime, this );
+#endif
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+Action< Actor >::~Action()
+{
+#ifdef DEBUG_BEHAVIOR_MEMORY
+ ConColorMsg( Color( 255, 0, 255, 255 ), "%3.2f: DELETE %0X\n", gpGlobals->curtime, this );
+#endif
+
+ if ( m_parent )
+ {
+ // if I'm my parent's active child, update parent's pointer
+ if ( m_parent->m_child == this )
+ {
+ m_parent->m_child = m_buriedUnderMe;
+ }
+ }
+
+ // delete all my children.
+ // our m_child pointer always points to the topmost
+ // child in the stack, so work our way back thru the
+ // 'buried' children and delete them.
+ Action< Actor > *child, *next = NULL;
+ for( child = m_child; child; child = next )
+ {
+ next = child->m_buriedUnderMe;
+ delete child;
+ }
+
+ if ( m_buriedUnderMe )
+ {
+ // we're going away, so my buried sibling is now on top
+ m_buriedUnderMe->m_coveringMe = NULL;
+ }
+
+ // delete any actions stacked on top of me
+ if ( m_coveringMe )
+ {
+ // recursion will march down the chain
+ delete m_coveringMe;
+ }
+
+ // delete any pending event result
+ if ( m_eventResult.m_action )
+ {
+ delete m_eventResult.m_action;
+ }
+}
+
+
+template < typename Actor >
+bool Action< Actor >::IsNamed( const char *name ) const
+{
+ return FStrEq( GetName(), name );
+}
+
+
+template < typename Actor >
+Actor *Action< Actor >::GetActor( void ) const
+{
+ return m_actor;
+}
+
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::Continue( void ) const
+{
+ return ActionResult< Actor >( CONTINUE, NULL, NULL );
+}
+
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::ChangeTo( Action< Actor > *action, const char *reason ) const
+{
+ return ActionResult< Actor >( CHANGE_TO, action, reason );
+}
+
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::SuspendFor( Action< Actor > *action, const char *reason ) const
+{
+ // clear any pending transitions requested by events, or this SuspendFor will
+ // immediately be out of scope
+ m_eventResult = TryContinue( RESULT_NONE );
+
+ return ActionResult< Actor >( SUSPEND_FOR, action, reason );
+}
+
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::Done( const char *reason ) const
+{
+ return ActionResult< Actor >( DONE, NULL, reason );
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+EventDesiredResult< Actor > Action< Actor >::TryContinue( EventResultPriorityType priority ) const
+{
+ return EventDesiredResult< Actor >( CONTINUE, NULL, priority );
+}
+
+template < typename Actor >
+EventDesiredResult< Actor > Action< Actor >::TryChangeTo( Action< Actor > *action, EventResultPriorityType priority, const char *reason ) const
+{
+ return EventDesiredResult< Actor >( CHANGE_TO, action, priority, reason );
+}
+
+template < typename Actor >
+EventDesiredResult< Actor > Action< Actor >::TrySuspendFor( Action< Actor > *action, EventResultPriorityType priority, const char *reason ) const
+{
+ return EventDesiredResult< Actor >( SUSPEND_FOR, action, priority, reason );
+}
+
+template < typename Actor >
+EventDesiredResult< Actor > Action< Actor >::TryDone( EventResultPriorityType priority, const char *reason /*= NULL*/ ) const
+{
+ return EventDesiredResult< Actor >( DONE, NULL, priority, reason );
+}
+
+template < typename Actor >
+EventDesiredResult< Actor > Action< Actor >::TryToSustain( EventResultPriorityType priority, const char *reason /*= NULL*/ ) const
+{
+ return EventDesiredResult< Actor >( SUSTAIN, NULL, priority, reason );
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+Action< Actor > *Action< Actor >::GetActiveChildAction( void ) const
+{
+ return m_child;
+}
+
+
+//-------------------------------------------------------------------------------------------
+// the Action that I'm running inside of
+template < typename Actor >
+Action< Actor > *Action< Actor >::GetParentAction( void ) const
+{
+ return m_parent;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Return true if we are currently suspended for another Action
+ */
+template < typename Actor >
+bool Action< Actor >::IsSuspended( void ) const
+{
+ return m_isSuspended;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Start this Action.
+ * The act of calling InvokeOnStart is the edge case that 'enters' a state.
+ */
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::InvokeOnStart( Actor *me, Behavior< Actor > *behavior, Action< Actor > *priorAction, Action< Actor > *buriedUnderMeAction )
+{
+ // debug display
+ if ( (me->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " STARTING " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+
+ // these value must be valid before invoking OnStart, in case an OnSuspend happens
+ m_isStarted = true;
+ m_actor = me;
+ m_behavior = behavior;
+
+ // maintain parent/child relationship during transitions
+ if ( priorAction )
+ {
+ m_parent = priorAction->m_parent;
+ }
+
+ if ( m_parent )
+ {
+ // child pointer of an Action always points to the ACTIVE child
+ // parent pointers are set when child Actions are instantiated
+ m_parent->m_child = this;
+ }
+
+ // maintain stack pointers
+ m_buriedUnderMe = buriedUnderMeAction;
+ if ( buriedUnderMeAction )
+ {
+ buriedUnderMeAction->m_coveringMe = this;
+ }
+
+ // we are always on top of the stack. if our priorAction was buried, it cleared
+ // everything covering it when it ended (which happens before we start)
+ m_coveringMe = NULL;
+
+ // start the optional child action
+ m_child = InitialContainedAction( me );
+ if ( m_child )
+ {
+ // define initial parent/child relationship
+ m_child->m_parent = this;
+
+ m_child = m_child->ApplyResult( me, behavior, ChangeTo( m_child, "Starting child Action" ) );
+ }
+
+ // start ourselves
+ ActionResult< Actor > result = OnStart( me, priorAction );
+
+ return result;
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::InvokeUpdate( Actor *me, Behavior< Actor > *behavior, float interval )
+{
+ // an explicit "out of scope" check is needed here to prevent any
+ // pending events causing an out of scope action to linger
+ if ( IsOutOfScope() )
+ {
+ // exit self to make this Action active and allow result to take effect on its next Update
+ return Done( "Out of scope" );
+ }
+
+ if ( !m_isStarted )
+ {
+ // this Action has not yet begun - start it
+ return ChangeTo( this, "Starting Action" );
+ }
+
+ // honor any pending event results
+ ActionResult< Actor > eventResult = ProcessPendingEvents();
+ if ( !eventResult.IsContinue() )
+ {
+ return eventResult;
+ }
+
+ // update our child action first, since it has the most specific behavior
+ if ( m_child )
+ {
+ m_child = m_child->ApplyResult( me, behavior, m_child->InvokeUpdate( me, behavior, interval ) );
+ }
+
+ // update ourselves
+ ActionResult< Actor > result;
+ {
+ VPROF_BUDGET( GetName(), "NextBot" );
+
+ result = Update( me, interval );
+ }
+
+ return result;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * This method calls the virtual OnEnd() method for the Action, its children, and Actions
+ * stacked on top of it.
+ * It does NOT delete resources, or disturb pointer relationships, because this Action
+ * needs to remain valid for a short while as an argument to OnStart(), OnSuspend(), etc for
+ * the next Action.
+ * The destructor for the Action frees memory for this Action, its children, etc.
+ */
+template < typename Actor >
+void Action< Actor >::InvokeOnEnd( Actor *me, Behavior< Actor > *behavior, Action< Actor > *nextAction )
+{
+ if ( !m_isStarted )
+ {
+ // we are not started (or never were)
+ return;
+ }
+
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), " ENDING " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+
+ // we are no longer started
+ m_isStarted = false;
+
+ // tell child Action(s) to leave (but don't disturb the list itself)
+ Action< Actor > *child, *next = NULL;
+ for( child = m_child; child; child = next )
+ {
+ next = child->m_buriedUnderMe;
+ child->InvokeOnEnd( me, behavior, nextAction );
+ }
+
+ // leave ourself
+ OnEnd( me, nextAction );
+
+ // leave any Actions stacked on top of me
+ if ( m_coveringMe )
+ {
+ m_coveringMe->InvokeOnEnd( me, behavior, nextAction );
+ }
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Just invoke OnSuspend - when the interrupting Action is started it will
+ * update our buried/covered pointers.
+ * OnSuspend may cause this Action to exit.
+ */
+template < typename Actor >
+Action< Actor > * Action< Actor >::InvokeOnSuspend( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction )
+{
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " SUSPENDING " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+
+ // suspend child Action
+ if ( m_child )
+ {
+ m_child = m_child->InvokeOnSuspend( me, behavior, interruptingAction );
+ }
+
+ // suspend ourselves
+ m_isSuspended = true;
+ ActionResult< Actor > result = OnSuspend( me, interruptingAction );
+
+ if ( result.IsDone() )
+ {
+ // we want to be replaced instead of suspended
+ InvokeOnEnd( me, behavior, NULL );
+
+ Action< Actor > * buried = GetActionBuriedUnderMe();
+
+ behavior->DestroyAction( this );
+
+ // new Action on top of the stack
+ return buried;
+ }
+
+ // we are still on top of the stack at this moment
+ return this;
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+ActionResult< Actor > Action< Actor >::InvokeOnResume( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction )
+{
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " RESUMING " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+
+ if ( !m_isSuspended )
+ {
+ // we were never suspended
+ return Continue();
+ }
+
+ if ( m_eventResult.IsRequestingChange() )
+ {
+ // this Action is not actually being Resumed, because a change
+ // is already pending from a prior event
+ return Continue();
+ }
+
+ // resume ourselves
+ m_isSuspended = false;
+ m_coveringMe = NULL;
+
+ if ( m_parent )
+ {
+ // we are once again our parent's active child
+ m_parent->m_child = this;
+ }
+
+ // resume child Action
+ if ( m_child )
+ {
+ m_child = m_child->ApplyResult( me, behavior, m_child->InvokeOnResume( me, behavior, interruptingAction ) );
+ }
+
+ // actually resume ourselves
+ ActionResult< Actor > result = OnResume( me, interruptingAction );
+
+ return result;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Given the result of this Action's work, apply the result to potentially create a new Action
+ */
+template < typename Actor >
+Action< Actor > *Action< Actor >::ApplyResult( Actor *me, Behavior< Actor > *behavior, ActionResult< Actor > result )
+{
+ Action< Actor > *newAction = result.m_action;
+
+ switch( result.m_type )
+ {
+ //-----------------------------------------------------------------------------------------------------
+ // transition to new Action
+ case CHANGE_TO:
+ {
+ if ( newAction == NULL )
+ {
+ DevMsg( "Error: Attempted CHANGE_TO to a NULL Action\n" );
+ AssertMsg( false, "Action: Attempted CHANGE_TO to a NULL Action" );
+ return this;
+ }
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+
+ if ( this == newAction )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "START " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() );
+ }
+ else
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), " CHANGE_TO " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() );
+ }
+
+ if ( result.m_reason )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason );
+ }
+ else
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+ }
+
+ // we are done
+ this->InvokeOnEnd( me, behavior, newAction );
+
+ // start the new Action
+ ActionResult< Actor > startResult = newAction->InvokeOnStart( me, behavior, this, this->m_buriedUnderMe );
+
+ // discard ended action
+ if ( this != newAction )
+ {
+ behavior->DestroyAction( this );
+ }
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ newAction->PrintStateToConsole();
+ }
+
+ // apply result of starting the Action
+ return newAction->ApplyResult( me, behavior, startResult );
+ }
+
+ //-----------------------------------------------------------------------------------------------------
+ // temporarily suspend ourselves for the newAction, covering it on the stack
+ case SUSPEND_FOR:
+ {
+ // interrupting Action always goes on the TOP of the stack - find it
+ Action< Actor > *topAction = this;
+ while ( topAction->m_coveringMe )
+ {
+ topAction = topAction->m_coveringMe;
+ }
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " caused " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), topAction->GetName() );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " to SUSPEND_FOR " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() );
+
+ if ( result.m_reason )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason );
+ }
+ else
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+ }
+
+ // suspend the Action we just covered up
+ topAction = topAction->InvokeOnSuspend( me, behavior, newAction );
+
+ // begin the interrupting Action.
+ ActionResult< Actor > startResult = newAction->InvokeOnStart( me, behavior, topAction, topAction );
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ newAction->PrintStateToConsole();
+ }
+
+ return newAction->ApplyResult( me, behavior, startResult );
+ }
+
+ //-----------------------------------------------------------------------------------------------------
+ case DONE:
+ {
+ // resume buried action
+ Action< Actor > *resumedAction = this->m_buriedUnderMe;
+
+ // we are finished
+ this->InvokeOnEnd( me, behavior, resumedAction );
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() );
+
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() );
+
+ if ( resumedAction )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " DONE, RESUME " );
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), resumedAction->GetName() );
+ }
+ else
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " DONE." );
+ }
+
+ if ( result.m_reason )
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason );
+ }
+ else
+ {
+ me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" );
+ }
+ }
+
+ if ( resumedAction == NULL )
+ {
+ // all Actions complete
+ behavior->DestroyAction( this );
+ return NULL;
+ }
+
+ // resume uncovered action
+ ActionResult< Actor > resumeResult = resumedAction->InvokeOnResume( me, behavior, this );
+
+ // debug display
+ if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) )
+ {
+ resumedAction->PrintStateToConsole();
+ }
+
+ // discard ended action
+ behavior->DestroyAction( this );
+
+ // apply result of OnResume()
+ return resumedAction->ApplyResult( me, behavior, resumeResult );
+ }
+
+ case CONTINUE:
+ case SUSTAIN:
+ default:
+ {
+ // no change, continue the current action next frame
+ return this;
+ }
+ }
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Propagate events to sub actions
+ */
+template < typename Actor >
+INextBotEventResponder *Action< Actor >::FirstContainedResponder( void ) const
+{
+ return GetActiveChildAction();
+}
+
+template < typename Actor >
+INextBotEventResponder *Action< Actor >::NextContainedResponder( INextBotEventResponder *current ) const
+{
+ return NULL;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Return a temporary string describing the current action stack for debugging
+ */
+template < typename Actor >
+const char *Action< Actor >::DebugString( void ) const
+{
+ static char str[ 256 ];
+
+ str[0] = '\000';
+
+ // find root
+ const Action< Actor > *root = this;
+ while ( root->m_parent )
+ {
+ root = root->m_parent;
+ }
+
+ return BuildDecoratedName( str, root );
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+char *Action< Actor >::BuildDecoratedName( char *name, const Action< Actor > *action ) const
+{
+ const int fudge = 256;
+
+ // add the name of the given action
+ Q_strcat( name, action->GetName(), fudge );
+
+ // add any contained actions
+ const Action< Actor > *child = action->GetActiveChildAction();
+ if ( child )
+ {
+ Q_strcat( name, "( ", fudge );
+ BuildDecoratedName( name, child );
+ Q_strcat( name, " )", fudge );
+ }
+
+ // append buried actions
+ const Action< Actor > *buried = action->GetActionBuriedUnderMe();
+ if ( buried )
+ {
+ Q_strcat( name, "<<", fudge );
+ BuildDecoratedName( name, buried );
+ }
+
+ return name;
+}
+
+
+//-------------------------------------------------------------------------------------------
+/**
+ * Return a temporary string showing the full lineage of this one action
+ */
+template < typename Actor >
+const char *Action< Actor >::GetFullName( void ) const
+{
+ const int fudge = 256;
+ static char str[ fudge ];
+
+ str[0] = '\000';
+
+ const int maxStack = 64;
+ const char *nameStack[ maxStack ];
+ int stackIndex = 0;
+
+ for( const Action< Actor > *action = this;
+ stackIndex < maxStack && action;
+ action = action->m_parent )
+ {
+ nameStack[ stackIndex++ ] = action->GetName();
+ }
+
+ // assemble name
+ for( int i = stackIndex-1; i > 0; --i )
+ {
+ Q_strcat( str, nameStack[ i ], fudge );
+ Q_strcat( str, "/", fudge );
+ }
+
+ Q_strcat( str, nameStack[ 0 ], fudge );
+
+ /*
+ for( int i = 0; i < stackIndex-1; ++i )
+ {
+ Q_strcat( str, " )", fudge );
+ }
+ */
+
+ return str;
+}
+
+
+//-------------------------------------------------------------------------------------------
+template < typename Actor >
+void Action< Actor >::PrintStateToConsole( void ) const
+{
+ // emit the Behavior name
+ //ConColorMsg( Color( 255, 255, 255, 255 ), "%s: ", m_behavior->GetName() );
+
+ // build the state string
+ const char *msg = DebugString();
+
+ const int colorCount = 6;
+ Color colorTable[ colorCount ];
+ colorTable[ 0 ].SetColor( 255, 150, 150, 255 );
+ colorTable[ 1 ].SetColor( 150, 255, 150, 255 );
+ colorTable[ 2 ].SetColor( 150, 150, 255, 255 );
+ colorTable[ 3 ].SetColor( 255, 255, 150, 255 );
+ colorTable[ 4 ].SetColor( 50, 255, 255, 255 );
+ colorTable[ 5 ].SetColor( 255, 150, 255, 255 );
+
+ // output the color-coded state string
+ const int maxBufferSize = 256;
+ char buffer[ maxBufferSize ];
+
+ int colorIndex = 0;
+ int buriedLevel = 0;
+
+ char *outMsg = buffer;
+ for( const char *c = msg; *c != '\000'; ++c )
+ {
+ *outMsg = *c;
+ ++outMsg;
+
+ if ( *c == '(' )
+ {
+ *outMsg = '\000';
+
+ Color color = colorTable[ colorIndex ];
+
+ if ( buriedLevel )
+ {
+ // draw buried labels darkly
+ color.SetColor( color.r() * 0.5, color.g() * 0.5, color.b() * 0.5, 255 );
+ ++buriedLevel;
+ }
+
+ //ConColorMsg( color, "%s", buffer );
+ DevMsg( "%s", buffer );
+
+ colorIndex = ( colorIndex + 1 ) % colorCount;
+
+ outMsg = buffer;
+ }
+ else if ( *c == ')' )
+ {
+ // emit the closing paren with next batch
+ --outMsg;
+ *outMsg = '\000';
+
+ Color color = colorTable[ colorIndex ];
+
+ if ( buriedLevel )
+ {
+ // draw buried labels darkly
+ color.SetColor( color.r() * 0.5, color.g() * 0.5, color.b() * 0.5, 255 );
+
+ --buriedLevel;
+ }
+
+ //ConColorMsg( color, "%s", buffer );
+ DevMsg( "%s", buffer );
+
+ --colorIndex;
+ if ( colorIndex < 0 )
+ colorIndex = colorCount-1;
+
+ outMsg = buffer;
+
+ *outMsg = ')';
+ ++outMsg;
+ }
+ else if ( *c == '<' && buriedLevel == 0 )
+ {
+ // caught a "<<" stack push
+ ++c;
+
+ *outMsg = '<';
+ ++outMsg;
+ *outMsg = '\000';
+
+ // output active substring at full brightness
+ //ConColorMsg( colorTable[ colorIndex ], "%s", buffer );
+ DevMsg( "%s", buffer );
+
+ outMsg = buffer;
+
+ // from here until end of Action, use dim colors
+ buriedLevel = 1;
+ }
+
+ }
+
+ *outMsg = '\000';
+ //ConColorMsg( colorTable[ colorIndex ], "%s", buffer );
+ DevMsg( "%s", buffer );
+
+ //ConColorMsg( colorTable[ colorIndex ], "\n\n" );
+ DevMsg( "\n\n" );
+}
+
+
+
+
+
+#endif // _BEHAVIOR_ENGINE_H_
+
+
+
+
+
diff --git a/game/server/NextBot/NextBotBodyInterface.cpp b/game/server/NextBot/NextBotBodyInterface.cpp
new file mode 100644
index 0000000..143ed4c
--- /dev/null
+++ b/game/server/NextBot/NextBotBodyInterface.cpp
@@ -0,0 +1,55 @@
+// NextBotBodyInterface.cpp
+// Control and information about the bot's body state (posture, animation state, etc)
+// Author: Michael Booth, April 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBot.h"
+#include "NextBotBodyInterface.h"
+
+
+void IBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::FAILED );
+ }
+}
+
+void IBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::FAILED );
+ }
+}
+
+bool IBody::SetPosition( const Vector &pos )
+{
+ GetBot()->GetEntity()->SetAbsOrigin( pos );
+ return true;
+}
+
+const Vector &IBody::GetEyePosition( void ) const
+{
+ static Vector eye;
+
+ eye = GetBot()->GetEntity()->WorldSpaceCenter();
+
+ return eye;
+}
+
+const Vector &IBody::GetViewVector( void ) const
+{
+ static Vector view;
+
+ AngleVectors( GetBot()->GetEntity()->EyeAngles(), &view );
+
+ return view;
+}
+
+bool IBody::IsHeadAimingOnTarget( void ) const
+{
+ return false;
+}
diff --git a/game/server/NextBot/NextBotBodyInterface.h b/game/server/NextBot/NextBotBodyInterface.h
new file mode 100644
index 0000000..8d8a59c
--- /dev/null
+++ b/game/server/NextBot/NextBotBodyInterface.h
@@ -0,0 +1,325 @@
+// NextBotBodyInterface.h
+// Control and information about the bot's body state (posture, animation state, etc)
+// Author: Michael Booth, April 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_BODY_INTERFACE_H_
+#define _NEXT_BOT_BODY_INTERFACE_H_
+
+#include "animation.h"
+#include "NextBotComponentInterface.h"
+
+class INextBot;
+struct animevent_t;
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for control and information about the bot's body state (posture, animation state, etc)
+ */
+class IBody : public INextBotComponent
+{
+public:
+ IBody( INextBot *bot ) : INextBotComponent( bot ) { }
+ virtual ~IBody() { }
+
+ virtual void Reset( void ) { INextBotComponent::Reset(); } // reset to initial state
+ virtual void Update( void ) { } // update internal state
+
+ /**
+ * Move the bot to a new position.
+ * If the body is not currently movable or if it
+ * is in a motion-controlled animation activity
+ * the position will not be changed and false will be returned.
+ */
+ virtual bool SetPosition( const Vector &pos );
+
+ virtual const Vector &GetEyePosition( void ) const; // return the eye position of the bot in world coordinates
+ virtual const Vector &GetViewVector( void ) const; // return the view unit direction vector in world coordinates
+
+ enum LookAtPriorityType
+ {
+ BORING,
+ INTERESTING, // last known enemy location, dangerous sound location
+ IMPORTANT, // a danger
+ CRITICAL, // an active threat to our safety
+ MANDATORY // nothing can interrupt this look at - two simultaneous look ats with this priority is an error
+ };
+ virtual void AimHeadTowards( const Vector &lookAtPos,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // aim the bot's head towards the given goal
+ virtual void AimHeadTowards( CBaseEntity *subject,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // continually aim the bot's head towards the given subject
+
+ virtual bool IsHeadAimingOnTarget( void ) const; // return true if the bot's head has achieved its most recent lookat target
+ virtual bool IsHeadSteady( void ) const; // return true if head is not rapidly turning to look somewhere else
+ virtual float GetHeadSteadyDuration( void ) const; // return the duration that the bot's head has not been rotating
+ virtual float GetHeadAimSubjectLeadTime( void ) const; // return how far into the future we should predict our moving subject's position to aim at when tracking subject look-ats
+ 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
+ virtual void ClearPendingAimReply( void ) { } // clear out currently pending replyWhenAimed callback
+
+ virtual float GetMaxHeadAngularVelocity( void ) const; // return max turn rate of head in degrees/second
+
+ enum ActivityType
+ {
+ MOTION_CONTROLLED_XY = 0x0001, // XY position and orientation of the bot is driven by the animation.
+ MOTION_CONTROLLED_Z = 0x0002, // Z position of the bot is driven by the animation.
+ ACTIVITY_UNINTERRUPTIBLE= 0x0004, // activity can't be changed until animation finishes
+ ACTIVITY_TRANSITORY = 0x0008, // a short animation that takes over from the underlying animation momentarily, resuming it upon completion
+ ENTINDEX_PLAYBACK_RATE = 0x0010, // played back at different rates based on entindex
+ };
+
+ /**
+ * Begin an animation activity, return false if we cant do that right now.
+ */
+ virtual bool StartActivity( Activity act, unsigned int flags = 0 );
+ virtual int SelectAnimationSequence( Activity act ) const; // given an Activity, select and return a specific animation sequence within it
+
+ virtual Activity GetActivity( void ) const; // return currently animating activity
+ virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one
+ virtual bool HasActivityType( unsigned int flags ) const; // return true if currently animating activity has any of the given flags
+
+ enum PostureType
+ {
+ STAND,
+ CROUCH,
+ SIT,
+ CRAWL,
+ LIE
+ };
+ virtual void SetDesiredPosture( PostureType posture ) { } // request a posture change
+ virtual PostureType GetDesiredPosture( void ) const; // get posture body is trying to assume
+ virtual bool IsDesiredPosture( PostureType posture ) const; // return true if body is trying to assume this posture
+ virtual bool IsInDesiredPosture( void ) const; // return true if body's actual posture matches its desired posture
+
+ virtual PostureType GetActualPosture( void ) const; // return body's current actual posture
+ virtual bool IsActualPosture( PostureType posture ) const; // return true if body is actually in the given posture
+
+ virtual bool IsPostureMobile( void ) const; // return true if body's current posture allows it to move around the world
+ virtual bool IsPostureChanging( void ) const; // return true if body's posture is in the process of changing to new posture
+
+
+ /**
+ * "Arousal" is the level of excitedness/arousal/anxiety of the body.
+ * Is changes instantaneously to avoid complex interactions with posture transitions.
+ */
+ enum ArousalType
+ {
+ NEUTRAL,
+ ALERT,
+ INTENSE
+ };
+ virtual void SetArousal( ArousalType arousal ) { } // arousal level change
+ virtual ArousalType GetArousal( void ) const; // get arousal level
+ virtual bool IsArousal( ArousalType arousal ) const; // return true if body is at this arousal level
+
+
+ virtual float GetHullWidth( void ) const; // width of bot's collision hull in XY plane
+ virtual float GetHullHeight( void ) const; // height of bot's current collision hull based on posture
+ virtual float GetStandHullHeight( void ) const; // height of bot's collision hull when standing
+ virtual float GetCrouchHullHeight( void ) const; // height of bot's collision hull when crouched
+ virtual const Vector &GetHullMins( void ) const; // return current collision hull minimums based on actual body posture
+ virtual const Vector &GetHullMaxs( void ) const; // return current collision hull maximums based on actual body posture
+
+ virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface)
+ virtual unsigned int GetCollisionGroup( void ) const;
+};
+
+
+inline bool IBody::IsHeadSteady( void ) const
+{
+ return true;
+}
+
+inline float IBody::GetHeadSteadyDuration( void ) const
+{
+ return 0.0f;
+}
+
+inline float IBody::GetHeadAimSubjectLeadTime( void ) const
+{
+ return 0.0f;
+}
+
+inline float IBody::GetHeadAimTrackingInterval( void ) const
+{
+ return 0.0f;
+}
+
+inline float IBody::GetMaxHeadAngularVelocity( void ) const
+{
+ return 1000.0f;
+}
+
+inline bool IBody::StartActivity( Activity act, unsigned int flags )
+{
+ return false;
+}
+
+inline int IBody::SelectAnimationSequence( Activity act ) const
+{
+ return 0;
+}
+
+inline Activity IBody::GetActivity( void ) const
+{
+ return ACT_INVALID;
+}
+
+inline bool IBody::IsActivity( Activity act ) const
+{
+ return false;
+}
+
+inline bool IBody::HasActivityType( unsigned int flags ) const
+{
+ return false;
+}
+
+inline IBody::PostureType IBody::GetDesiredPosture( void ) const
+{
+ return IBody::STAND;
+}
+
+inline bool IBody::IsDesiredPosture( PostureType posture ) const
+{
+ return true;
+}
+
+inline bool IBody::IsInDesiredPosture( void ) const
+{
+ return true;
+}
+
+inline IBody::PostureType IBody::GetActualPosture( void ) const
+{
+ return IBody::STAND;
+}
+
+inline bool IBody::IsActualPosture( PostureType posture ) const
+{
+ return true;
+}
+
+inline bool IBody::IsPostureMobile( void ) const
+{
+ return true;
+}
+
+inline bool IBody::IsPostureChanging( void ) const
+{
+ return false;
+}
+
+inline IBody::ArousalType IBody::GetArousal( void ) const
+{
+ return IBody::NEUTRAL;
+}
+
+inline bool IBody::IsArousal( ArousalType arousal ) const
+{
+ return true;
+}
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Width of bot's collision hull in XY plane
+ */
+inline float IBody::GetHullWidth( void ) const
+{
+ return 26.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Height of bot's current collision hull based on posture
+ */
+inline float IBody::GetHullHeight( void ) const
+{
+ switch( GetActualPosture() )
+ {
+ case LIE:
+ return 16.0f;
+
+ case SIT:
+ case CROUCH:
+ return GetCrouchHullHeight();
+
+ case STAND:
+ default:
+ return GetStandHullHeight();
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Height of bot's collision hull when standing
+ */
+inline float IBody::GetStandHullHeight( void ) const
+{
+ return 68.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Height of bot's collision hull when crouched
+ */
+inline float IBody::GetCrouchHullHeight( void ) const
+{
+ return 32.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Return current collision hull minimums based on actual body posture
+ */
+inline const Vector &IBody::GetHullMins( void ) const
+{
+ static Vector hullMins;
+
+ hullMins.x = -GetHullWidth()/2.0f;
+ hullMins.y = hullMins.x;
+ hullMins.z = 0.0f;
+
+ return hullMins;
+}
+
+
+//---------------------------------------------------------------------------------------------------------------------------
+/**
+ * Return current collision hull maximums based on actual body posture
+ */
+inline const Vector &IBody::GetHullMaxs( void ) const
+{
+ static Vector hullMaxs;
+
+ hullMaxs.x = GetHullWidth()/2.0f;
+ hullMaxs.y = hullMaxs.x;
+ hullMaxs.z = GetHullHeight();
+
+ return hullMaxs;
+}
+
+
+inline unsigned int IBody::GetSolidMask( void ) const
+{
+ return MASK_NPCSOLID;
+}
+
+inline unsigned int IBody::GetCollisionGroup( void ) const
+{
+ return COLLISION_GROUP_NONE;
+}
+
+
+
+#endif // _NEXT_BOT_BODY_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotComponentInterface.cpp b/game/server/NextBot/NextBotComponentInterface.cpp
new file mode 100644
index 0000000..0c4c172
--- /dev/null
+++ b/game/server/NextBot/NextBotComponentInterface.cpp
@@ -0,0 +1,24 @@
+// NextBotComponentInterface.cpp
+// Implentation of system methods for NextBot component interface
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBotInterface.h"
+#include "NextBotComponentInterface.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+INextBotComponent::INextBotComponent( INextBot *bot )
+{
+ m_curInterval = TICK_INTERVAL;
+ m_lastUpdateTime = 0;
+ m_bot = bot;
+
+ // register this component with the bot
+ bot->RegisterComponent( this );
+}
+
+
diff --git a/game/server/NextBot/NextBotComponentInterface.h b/game/server/NextBot/NextBotComponentInterface.h
new file mode 100644
index 0000000..344d708
--- /dev/null
+++ b/game/server/NextBot/NextBotComponentInterface.h
@@ -0,0 +1,98 @@
+// NextBotComponentInterface.h
+// Interface for all components
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_COMPONENT_INTERFACE_H_
+#define _NEXT_BOT_COMPONENT_INTERFACE_H_
+
+#include "NextBotEventResponderInterface.h"
+
+class INextBot;
+class Path;
+class CGameTrace;
+class CTakeDamageInfo;
+
+
+//--------------------------------------------------------------------------------------------------------------------------
+/**
+ * Various processes can invoke a "reply" (ie: callback) via instances of this interface
+ */
+class INextBotReply
+{
+public:
+ virtual void OnSuccess( INextBot *bot ) { } // invoked when process completed successfully
+
+ enum FailureReason
+ {
+ DENIED,
+ INTERRUPTED,
+ FAILED
+ };
+ virtual void OnFail( INextBot *bot, FailureReason reason ) { } // invoked when process failed
+};
+
+
+//--------------------------------------------------------------------------------------------------------------------------
+/**
+ * Next Bot component interface
+ */
+class INextBotComponent : public INextBotEventResponder
+{
+public:
+ INextBotComponent( INextBot *bot );
+ virtual ~INextBotComponent() { }
+
+ virtual void Reset( void ) { m_lastUpdateTime = 0; m_curInterval = TICK_INTERVAL; } // reset to initial state
+ virtual void Update( void ) = 0; // update internal state
+ virtual void Upkeep( void ) { } // lightweight update guaranteed to occur every server tick
+
+ inline bool ComputeUpdateInterval(); // return false is no time has elapsed (interval is zero)
+ inline float GetUpdateInterval();
+
+ virtual INextBot *GetBot( void ) const { return m_bot; }
+
+private:
+ float m_lastUpdateTime;
+ float m_curInterval;
+
+ friend class INextBot;
+
+ INextBot *m_bot;
+ INextBotComponent *m_nextComponent; // simple linked list of components in the bot
+};
+
+
+inline bool INextBotComponent::ComputeUpdateInterval()
+{
+ if ( m_lastUpdateTime )
+ {
+ float interval = gpGlobals->curtime - m_lastUpdateTime;
+
+ const float minInterval = 0.0001f;
+ if ( interval > minInterval )
+ {
+ m_curInterval = interval;
+ m_lastUpdateTime = gpGlobals->curtime;
+ return true;
+ }
+
+ return false;
+ }
+
+ // First update - assume a reasonable interval.
+ // We need the very first update to do work, for cases
+ // where the bot was just created and we need to propagate
+ // an event to it immediately.
+ m_curInterval = 0.033f;
+ m_lastUpdateTime = gpGlobals->curtime - m_curInterval;
+
+ return true;
+}
+
+inline float INextBotComponent::GetUpdateInterval()
+{
+ return m_curInterval;
+}
+
+#endif // _NEXT_BOT_COMPONENT_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotContextualQueryInterface.h b/game/server/NextBot/NextBotContextualQueryInterface.h
new file mode 100644
index 0000000..d117bca
--- /dev/null
+++ b/game/server/NextBot/NextBotContextualQueryInterface.h
@@ -0,0 +1,102 @@
+// NextBotContextualQueryInterface.h
+// Queries within the context of the bot's current behavior state
+// Author: Michael Booth, June 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_CONTEXTUAL_QUERY_H_
+#define _NEXT_BOT_CONTEXTUAL_QUERY_H_
+
+class INextBot;
+class CBaseEntity;
+class CBaseCombatCharacter;
+class Path;
+class CKnownEntity;
+
+/**
+ * Since behaviors can have several concurrent actions active, we ask
+ * the topmost child action first, and if it defers, its parent, and so
+ * on, until we get a definitive answer.
+ */
+enum QueryResultType
+{
+ ANSWER_NO,
+ ANSWER_YES,
+ ANSWER_UNDEFINED
+};
+
+// Can pass this into IContextualQuery::IsHindrance to see if any hindrance is ever possible
+#define IS_ANY_HINDRANCE_POSSIBLE ( (CBaseEntity*)0xFFFFFFFF )
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for queries that are dependent on the bot's current behavior state
+ */
+class IContextualQuery
+{
+public:
+ virtual ~IContextualQuery() { }
+
+ virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const; // if the desired item was available right now, should we pick it up?
+ 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 QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // return true if we should wait for 'blocker' that is across our path somewhere up ahead.
+
+ virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at
+
+ /**
+ * 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.
+ */
+ 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
+};
+
+inline QueryResultType IContextualQuery::ShouldPickUp( const INextBot *me, CBaseEntity *item ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline QueryResultType IContextualQuery::ShouldHurry( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline QueryResultType IContextualQuery::ShouldRetreat( const INextBot *me ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline QueryResultType IContextualQuery::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline QueryResultType IContextualQuery::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline Vector IContextualQuery::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const
+{
+ return vec3_origin;
+}
+
+inline QueryResultType IContextualQuery::IsPositionAllowed( const INextBot *me, const Vector &pos ) const
+{
+ return ANSWER_UNDEFINED;
+}
+
+inline const CKnownEntity *IContextualQuery::SelectMoreDangerousThreat( const INextBot *me, const CBaseCombatCharacter *subject, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const
+{
+ return NULL;
+}
+
+
+#endif // _NEXT_BOT_CONTEXTUAL_QUERY_H_
diff --git a/game/server/NextBot/NextBotDebug.h b/game/server/NextBot/NextBotDebug.h
new file mode 100644
index 0000000..88e0ba2
--- /dev/null
+++ b/game/server/NextBot/NextBotDebug.h
@@ -0,0 +1,23 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+#ifndef NEXTBOT_DEBUG_H
+#define NEXTBOT_DEBUG_H
+//------------------------------------------------------------------------------
+// Debug flags for nextbot
+
+enum NextBotDebugType
+{
+ NEXTBOT_DEBUG_NONE = 0,
+ NEXTBOT_BEHAVIOR = 0x0001,
+ NEXTBOT_LOOK_AT = 0x0002,
+ NEXTBOT_PATH = 0x0004,
+ NEXTBOT_ANIMATION = 0x0008,
+ NEXTBOT_LOCOMOTION = 0x0010,
+ NEXTBOT_VISION = 0x0020,
+ NEXTBOT_HEARING = 0x0040,
+ NEXTBOT_EVENTS = 0x0080,
+ NEXTBOT_ERRORS = 0x0100, // when things go wrong, like being stuck
+
+ NEXTBOT_DEBUG_ALL = 0xFFFF
+};
+
+#endif
diff --git a/game/server/NextBot/NextBotEventResponderInterface.h b/game/server/NextBot/NextBotEventResponderInterface.h
new file mode 100644
index 0000000..9e7ba56
--- /dev/null
+++ b/game/server/NextBot/NextBotEventResponderInterface.h
@@ -0,0 +1,550 @@
+// NextBotEventResponderInterface.h
+// Interface for propagating and responding to events
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_
+#define _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_
+
+class Path;
+class CTakeDamageInfo;
+class CBaseEntity;
+class CDOTABaseAbility;
+
+struct CSoundParameters;
+struct animevent_t;
+
+#include "ai_speech.h"
+
+
+//--------------------------------------------------------------------------------------------------------------------------
+enum MoveToFailureType
+{
+ FAIL_NO_PATH_EXISTS,
+ FAIL_STUCK,
+ FAIL_FELL_OFF,
+};
+
+//--------------------------------------------------------------------------------------------------------------------------
+/**
+ * Events propagated to/between components.
+ * To add an event, add its signature here and implement its propagation
+ * to derived classes via FirstContainedResponder() and NextContainedResponder().
+ * NOTE: Also add a translator to the Action class in NextBotBehavior.h.
+ */
+class INextBotEventResponder
+{
+public:
+ DECLARE_CLASS_NOBASE( INextBotEventResponder );
+
+ virtual ~INextBotEventResponder() { }
+
+ // these methods are used by derived classes to define how events propagate
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const { return NULL; }
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; }
+
+ //
+ // Events. All events must be 'extended' by calling the derived class explicitly to ensure propagation.
+ // Each event must implement its propagation in this interface class.
+ //
+ virtual void OnLeaveGround( CBaseEntity *ground ); // invoked when bot leaves ground for any reason
+ virtual void OnLandOnGround( CBaseEntity *ground ); // invoked when bot lands on the ground after being in the air
+
+ virtual void OnContact( CBaseEntity *other, CGameTrace *result = NULL ); // invoked when bot touches 'other'
+
+ virtual void OnMoveToSuccess( const Path *path ); // invoked when a bot reaches the end of the given Path
+ virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ); // invoked when a bot fails to reach the end of the given Path
+ virtual void OnStuck( void ); // invoked when bot becomes stuck while trying to move
+ virtual void OnUnStuck( void ); // invoked when a previously stuck bot becomes un-stuck and can again move
+
+ virtual void OnPostureChanged( void ); // when bot has assumed new posture (query IBody for posture)
+
+ virtual void OnAnimationActivityComplete( int activity ); // when animation activity has finished playing
+ virtual void OnAnimationActivityInterrupted( int activity );// when animation activity was replaced by another animation
+ virtual void OnAnimationEvent( animevent_t *event ); // when a QC-file animation event is triggered by the current animation sequence
+
+ virtual void OnIgnite( void ); // when bot starts to burn
+ virtual void OnInjured( const CTakeDamageInfo &info ); // when bot is damaged by something
+ virtual void OnKilled( const CTakeDamageInfo &info ); // when the bot's health reaches zero
+ virtual void OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); // when someone else dies
+
+ virtual void OnSight( CBaseEntity *subject ); // when subject initially enters bot's visual awareness
+ virtual void OnLostSight( CBaseEntity *subject ); // when subject leaves enters bot's visual awareness
+
+ virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ); // when an entity emits a sound. "pos" is world coordinates of sound. "keys" are from sound's GameData
+ virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ); // when an Actor speaks a concept
+ virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires a weapon
+
+ virtual void OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea ); // when bot enters a new navigation area
+
+ virtual void OnModelChanged( void ); // when the entity's model has been changed
+
+ virtual void OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver ); // when something is added to our inventory
+ virtual void OnDrop( CBaseEntity *item ); // when something is removed from our inventory
+ virtual void OnActorEmoted( CBaseCombatCharacter *emoter, int emote ); // when "emoter" does an "emote" (ie: manual voice command, etc)
+
+ virtual void OnCommandAttack( CBaseEntity *victim ); // attack the given entity
+ virtual void OnCommandApproach( const Vector &pos, float range = 0.0f ); // move to within range of the given position
+ virtual void OnCommandApproach( CBaseEntity *goal ); // follow the given leader
+ virtual void OnCommandRetreat( CBaseEntity *threat, float range = 0.0f ); // retreat from the threat at least range units away (0 == infinite)
+ virtual void OnCommandPause( float duration = 0.0f ); // pause for the given duration (0 == forever)
+ virtual void OnCommandResume( void ); // resume after a pause
+
+ virtual void OnCommandString( const char *command ); // for debugging: respond to an arbitrary string representing a generalized command
+
+ virtual void OnShoved( CBaseEntity *pusher ); // 'pusher' has shoved me
+ virtual void OnBlinded( CBaseEntity *blinder ); // 'blinder' has blinded me with a flash of light
+
+ virtual void OnTerritoryContested( int territoryID ); // territory has been invaded and is changing ownership
+ virtual void OnTerritoryCaptured( int territoryID ); // we have captured enemy territory
+ virtual void OnTerritoryLost( int territoryID ); // we have lost territory to the enemy
+
+ virtual void OnWin( void );
+ virtual void OnLose( void );
+
+#ifdef DOTA_SERVER_DLL
+ virtual void OnCommandMoveTo( const Vector &pos );
+ virtual void OnCommandMoveToAggressive( const Vector &pos );
+ virtual void OnCommandAttack( CBaseEntity *victim, bool bDeny );
+ virtual void OnCastAbilityNoTarget( CDOTABaseAbility *ability );
+ virtual void OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos );
+ virtual void OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target );
+ virtual void OnDropItem( const Vector &pos, CBaseEntity *item );
+ virtual void OnPickupItem( CBaseEntity *item );
+ virtual void OnPickupRune( CBaseEntity *item );
+ virtual void OnStop();
+ virtual void OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat );
+ virtual void OnCancelAttack( CBaseEntity *pTarget );
+ virtual void OnDominated();
+ virtual void OnWarped( Vector vStartPos );
+#endif
+};
+
+
+inline void INextBotEventResponder::OnLeaveGround( CBaseEntity *ground )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnLeaveGround( ground );
+ }
+}
+
+inline void INextBotEventResponder::OnLandOnGround( CBaseEntity *ground )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnLandOnGround( ground );
+ }
+}
+
+inline void INextBotEventResponder::OnContact( CBaseEntity *other, CGameTrace *result )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnContact( other, result );
+ }
+}
+
+inline void INextBotEventResponder::OnMoveToSuccess( const Path *path )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnMoveToSuccess( path );
+ }
+}
+
+inline void INextBotEventResponder::OnMoveToFailure( const Path *path, MoveToFailureType reason )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnMoveToFailure( path, reason );
+ }
+}
+
+inline void INextBotEventResponder::OnStuck( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnStuck();
+ }
+}
+
+inline void INextBotEventResponder::OnUnStuck( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnUnStuck();
+ }
+}
+
+inline void INextBotEventResponder::OnPostureChanged( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnPostureChanged();
+ }
+}
+
+inline void INextBotEventResponder::OnAnimationActivityComplete( int activity )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnAnimationActivityComplete( activity );
+ }
+}
+
+inline void INextBotEventResponder::OnAnimationActivityInterrupted( int activity )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnAnimationActivityInterrupted( activity );
+ }
+}
+
+inline void INextBotEventResponder::OnAnimationEvent( animevent_t *event )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnAnimationEvent( event );
+ }
+}
+
+inline void INextBotEventResponder::OnIgnite( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnIgnite();
+ }
+}
+
+inline void INextBotEventResponder::OnInjured( const CTakeDamageInfo &info )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnInjured( info );
+ }
+}
+
+inline void INextBotEventResponder::OnKilled( const CTakeDamageInfo &info )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnKilled( info );
+ }
+}
+
+inline void INextBotEventResponder::OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnOtherKilled( victim, info );
+ }
+}
+
+inline void INextBotEventResponder::OnSight( CBaseEntity *subject )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnSight( subject );
+ }
+}
+
+inline void INextBotEventResponder::OnLostSight( CBaseEntity *subject )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnLostSight( subject );
+ }
+}
+
+inline void INextBotEventResponder::OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnSound( source, pos, keys );
+ }
+}
+
+inline void INextBotEventResponder::OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnSpokeConcept( who, concept, response );
+ }
+}
+
+inline void INextBotEventResponder::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnWeaponFired( whoFired, weapon );
+ }
+}
+
+inline void INextBotEventResponder::OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnNavAreaChanged( newArea, oldArea );
+ }
+}
+
+inline void INextBotEventResponder::OnModelChanged( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnModelChanged();
+ }
+}
+
+inline void INextBotEventResponder::OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnPickUp( item, giver );
+ }
+}
+
+inline void INextBotEventResponder::OnDrop( CBaseEntity *item )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnDrop( item );
+ }
+}
+
+inline void INextBotEventResponder::OnActorEmoted( CBaseCombatCharacter *emoter, int emote )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnActorEmoted( emoter, emote );
+ }
+}
+
+inline void INextBotEventResponder::OnShoved( CBaseEntity *pusher )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnShoved( pusher );
+ }
+}
+
+inline void INextBotEventResponder::OnBlinded( CBaseEntity *blinder )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnBlinded( blinder );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandAttack( CBaseEntity *victim )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandAttack( victim );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandApproach( const Vector &pos, float range )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandApproach( pos, range );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandApproach( CBaseEntity *goal )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandApproach( goal );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandRetreat( CBaseEntity *threat, float range )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandRetreat( threat, range );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandPause( float duration )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandPause( duration );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandResume( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandResume();
+ }
+}
+
+inline void INextBotEventResponder::OnCommandString( const char *command )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandString( command );
+ }
+}
+
+inline void INextBotEventResponder::OnTerritoryContested( int territoryID )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnTerritoryContested( territoryID );
+ }
+}
+
+inline void INextBotEventResponder::OnTerritoryCaptured( int territoryID )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnTerritoryCaptured( territoryID );
+ }
+}
+
+inline void INextBotEventResponder::OnTerritoryLost( int territoryID )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnTerritoryLost( territoryID );
+ }
+}
+
+inline void INextBotEventResponder::OnWin( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnWin();
+ }
+}
+
+inline void INextBotEventResponder::OnLose( void )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnLose();
+ }
+}
+
+#ifdef DOTA_SERVER_DLL
+inline void INextBotEventResponder::OnCommandMoveTo( const Vector &pos )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandMoveTo( pos );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandMoveToAggressive( const Vector &pos )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandMoveToAggressive( pos );
+ }
+}
+
+inline void INextBotEventResponder::OnCommandAttack( CBaseEntity *victim, bool bDeny )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCommandAttack( victim, bDeny );
+ }
+}
+
+inline void INextBotEventResponder::OnCastAbilityNoTarget( CDOTABaseAbility *ability )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCastAbilityNoTarget( ability );
+ }
+}
+
+inline void INextBotEventResponder::OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCastAbilityOnPosition( ability, pos );
+ }
+}
+
+inline void INextBotEventResponder::OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCastAbilityOnTarget( ability, target );
+ }
+}
+
+inline void INextBotEventResponder::OnDropItem( const Vector &pos, CBaseEntity *item )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnDropItem( pos, item );
+ }
+}
+
+inline void INextBotEventResponder::OnPickupItem( CBaseEntity *item )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnPickupItem( item );
+ }
+}
+
+inline void INextBotEventResponder::OnPickupRune( CBaseEntity *item )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnPickupRune( item );
+ }
+}
+
+inline void INextBotEventResponder::OnStop()
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnStop();
+ }
+}
+
+inline void INextBotEventResponder::OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnFriendThreatened( friendly, threat );
+ }
+}
+
+inline void INextBotEventResponder::OnCancelAttack( CBaseEntity *pTarget )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnCancelAttack( pTarget );
+ }
+}
+
+inline void INextBotEventResponder::OnDominated()
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnDominated();
+ }
+}
+
+inline void INextBotEventResponder::OnWarped( Vector vStartPos )
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ sub->OnWarped( vStartPos );
+ }
+}
+#endif
+
+#endif // _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotGroundLocomotion.cpp b/game/server/NextBot/NextBotGroundLocomotion.cpp
new file mode 100644
index 0000000..27f6a52
--- /dev/null
+++ b/game/server/NextBot/NextBotGroundLocomotion.cpp
@@ -0,0 +1,1442 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// NextBotGroundLocomotion.cpp
+// Basic ground-based movement for NextBotCombatCharacters
+// Author: Michael Booth, February 2009
+// Note: This is a refactoring of ZombieBotLocomotion from L4D
+
+#include "cbase.h"
+
+#include "func_break.h"
+#include "func_breakablesurf.h"
+#include "activitylist.h"
+#include "BasePropDoor.h"
+
+#include "nav.h"
+#include "NextBot.h"
+#include "NextBotGroundLocomotion.h"
+#include "NextBotUtil.h"
+#include "functorutils.h"
+#include "SharedFunctorUtils.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+#pragma warning( disable : 4355 ) // warning 'this' used in base member initializer list - we're using it safely
+
+
+//----------------------------------------------------------------------------------------------------------
+NextBotGroundLocomotion::NextBotGroundLocomotion( INextBot *bot ) : ILocomotion( bot )
+{
+ m_nextBot = NULL;
+ m_ladder = NULL;
+ m_desiredLean.x = 0.0f;
+ m_desiredLean.y = 0.0f;
+ m_desiredLean.z = 0.0f;
+
+ m_bRecomputePostureOnCollision = false;
+ m_ignorePhysicsPropTimer.Invalidate();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+NextBotGroundLocomotion::~NextBotGroundLocomotion()
+{
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Reset locomotor to initial state
+ */
+void NextBotGroundLocomotion::Reset( void )
+{
+ BaseClass::Reset();
+ m_bRecomputePostureOnCollision = false;
+ m_ignorePhysicsPropTimer.Invalidate();
+
+ m_nextBot = static_cast< NextBotCombatCharacter * >( GetBot()->GetEntity() );
+
+ m_desiredSpeed = 0.0f;
+ m_velocity = vec3_origin;
+ m_acceleration = vec3_origin;
+
+ m_desiredLean.x = 0.0f;
+ m_desiredLean.y = 0.0f;
+ m_desiredLean.z = 0.0f;
+
+ m_ladder = NULL;
+
+ m_isJumping = false;
+ m_isJumpingAcrossGap = false;
+ m_ground = NULL;
+ m_groundNormal = Vector( 0, 0, 1.0f );
+ m_isClimbingUpToLedge = false;
+ m_isUsingFullFeetTrace = false;
+
+ m_moveVector = Vector( 1, 0, 0 );
+
+ m_priorPos = m_nextBot->GetPosition();
+ m_lastValidPos = m_nextBot->GetPosition();
+
+ m_inhibitObstacleAvoidanceTimer.Invalidate();
+
+ m_accumApproachVectors = vec3_origin;
+ m_accumApproachWeights = 0.0f;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move the bot along a ladder
+ */
+bool NextBotGroundLocomotion::TraverseLadder( void )
+{
+ // not climbing a ladder right now
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Update internal state
+ */
+void NextBotGroundLocomotion::Update( void )
+{
+ VPROF_BUDGET( "NextBotGroundLocomotion::Update", "NextBot" );
+
+ BaseClass::Update();
+
+ const float deltaT = GetUpdateInterval();
+
+ // apply accumulated position changes
+ ApplyAccumulatedApproach();
+
+ // need to do this first thing, because ground constraints, etc, can change it
+ Vector origPos = GetFeet();
+
+ IBody *body = GetBot()->GetBodyInterface();
+
+ if ( TraverseLadder() )
+ {
+ // bot is climbing a ladder
+ return;
+ }
+
+ if ( !body->IsPostureMobile() )
+ {
+ // sitting/lying on the ground - no slip
+ m_acceleration.x = 0.0f;
+ m_acceleration.y = 0.0f;
+ m_velocity.x = 0.0f;
+ m_velocity.y = 0.0f;
+ }
+
+ bool wasOnGround = IsOnGround();
+
+ if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) )
+ {
+ // fall if in the air
+ if ( !IsOnGround() )
+ {
+ // no ground below us - fall
+ m_acceleration.z -= GetGravity();
+ }
+
+ if ( !IsClimbingOrJumping() || m_velocity.z <= 0.0f )
+ {
+ // keep us on the ground
+ UpdateGroundConstraint();
+ }
+ }
+
+ Vector newPos = GetFeet();
+
+ //
+ // Update position physics
+ //
+ Vector right( m_moveVector.y, -m_moveVector.x, 0.0f );
+
+ if ( IsOnGround() ) // || m_isClimbingUpToLedge )
+ {
+ if ( IsAttemptingToMove() )
+ {
+ float forwardSpeed = DotProduct( m_velocity, m_moveVector );
+ Vector forwardVelocity = forwardSpeed * m_moveVector;
+ Vector sideVelocity = DotProduct( m_velocity, right ) * right;
+
+ Vector frictionAccel = vec3_origin;
+
+ // only apply friction along forward direction if we are sliding backwards
+ if ( forwardSpeed < 0.0f )
+ {
+ frictionAccel = -GetFrictionForward() * forwardVelocity;
+ }
+
+ // always apply lateral friction to counteract sideslip
+ frictionAccel += -GetFrictionSideways() * sideVelocity;
+
+ m_acceleration.x += frictionAccel.x;
+ m_acceleration.y += frictionAccel.y;
+ }
+ else
+ {
+ // come to a stop if we haven't been told to move
+ m_acceleration = vec3_origin;
+ m_velocity = vec3_origin;
+ }
+ }
+
+ // compute new position, taking into account MOTION_CONTROLLED animations in progress
+ if ( body->HasActivityType( IBody::MOTION_CONTROLLED_XY ) )
+ {
+ m_acceleration.x = 0.0f;
+ m_acceleration.y = 0.0f;
+ m_velocity.x = GetBot()->GetEntity()->GetAbsVelocity().x;
+ m_velocity.y = GetBot()->GetEntity()->GetAbsVelocity().y;
+ }
+ else
+ {
+ // euler integration
+ m_velocity.x += m_acceleration.x * deltaT;
+ m_velocity.y += m_acceleration.y * deltaT;
+
+ // euler integration
+ newPos.x += m_velocity.x * deltaT;
+ newPos.y += m_velocity.y * deltaT;
+ }
+
+ if ( body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) )
+ {
+ m_acceleration.z = 0.0f;
+ m_velocity.z = GetBot()->GetEntity()->GetAbsVelocity().z;
+ }
+ else
+ {
+ // euler integration
+ m_velocity.z += m_acceleration.z * deltaT;
+
+ // euler integration
+ newPos.z += m_velocity.z * deltaT;
+ }
+
+ // move bot to new position, resolving collisions along the way
+ UpdatePosition( newPos );
+
+
+ // set actual velocity based on position change after collision resolution step
+ Vector adjustedVelocity = ( GetFeet() - origPos ) / deltaT;
+
+ if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_XY ) )
+ {
+ m_velocity.x = adjustedVelocity.x;
+ m_velocity.y = adjustedVelocity.y;
+ }
+
+ if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) )
+ {
+ m_velocity.z = adjustedVelocity.z;
+ }
+
+
+ // collision resolution may create very high instantaneous velocities, limit it
+ Vector2D groundVel = m_velocity.AsVector2D();
+ m_actualSpeed = groundVel.NormalizeInPlace();
+
+ if ( IsOnGround() )
+ {
+ if ( m_actualSpeed > GetRunSpeed() )
+ {
+ m_actualSpeed = GetRunSpeed();
+ m_velocity.x = m_actualSpeed * groundVel.x;
+ m_velocity.y = m_actualSpeed * groundVel.y;
+ }
+
+ // remove downward velocity when landing on the ground
+ if ( !wasOnGround )
+ {
+ m_velocity.z = 0.0f;
+ m_acceleration.z = 0.0f;
+ }
+ }
+ else
+ {
+ // we're falling. if our velocity has become zero for any reason, shove it forward
+ const float epsilon = 1.0f;
+ if ( m_velocity.IsLengthLessThan( epsilon ) )
+ {
+ m_velocity = GetRunSpeed() * GetGroundMotionVector();
+ }
+ }
+
+ // update entity velocity to that of locomotor
+ m_nextBot->SetAbsVelocity( m_velocity );
+
+
+#ifdef LEANING
+ // lean sideways proportional to lateral acceleration
+ QAngle lean = GetDesiredLean();
+
+ float sideAccel = DotProduct( right, m_acceleration );
+ float slide = sideAccel / GetMaxAcceleration();
+
+ // max lean depends on how fast we're actually moving
+ float maxLeanAngle = NextBotLeanMaxAngle.GetFloat() * m_actualSpeed / GetRunSpeed();
+
+ // actual lean angle is proportional to lateral acceleration (sliding)
+ float desiredSideLean = -maxLeanAngle * slide;
+
+ lean.y += ( desiredSideLean - lean.y ) * NextBotLeanRate.GetFloat() * deltaT;
+
+ SetDesiredLean( lean );
+#endif // _DEBUG
+
+
+ // reset acceleration accumulation
+ m_acceleration = vec3_origin;
+
+ // debug display
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ // track position over time
+ if ( IsOnGround() )
+ {
+ NDebugOverlay::Cross3D( GetFeet(), 1.0f, 0, 255, 0, true, 15.0f );
+ }
+ else
+ {
+ NDebugOverlay::Cross3D( GetFeet(), 1.0f, 0, 255, 255, true, 15.0f );
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move directly towards given position.
+ * We need to do this in-air as well to land jumps.
+ */
+void NextBotGroundLocomotion::Approach( const Vector &rawPos, float goalWeight )
+{
+ BaseClass::Approach( rawPos );
+
+ m_accumApproachVectors += ( rawPos - GetFeet() ) * goalWeight;
+ m_accumApproachWeights += goalWeight;
+ m_bRecomputePostureOnCollision = true;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotGroundLocomotion::ApplyAccumulatedApproach( void )
+{
+ VPROF_BUDGET( "NextBotGroundLocomotion::ApplyAccumulatedApproach", "NextBot" );
+
+ Vector rawPos = GetFeet();
+
+ const float deltaT = GetUpdateInterval();
+
+ if ( deltaT <= 0.0f )
+ return;
+
+ if ( m_accumApproachWeights > 0.0f )
+ {
+ Vector approachDelta = m_accumApproachVectors / m_accumApproachWeights;
+
+ // limit total movement to our max speed
+ float maxMove = GetRunSpeed() * deltaT;
+
+ float desiredMove = approachDelta.NormalizeInPlace();
+ if ( desiredMove > maxMove )
+ {
+ desiredMove = maxMove;
+ }
+
+ rawPos += desiredMove * approachDelta;
+
+ m_accumApproachVectors = vec3_origin;
+ m_accumApproachWeights = 0.0f;
+ }
+
+ // can only move in 2D - geometry moves us up and down
+ Vector pos( rawPos.x, rawPos.y, GetFeet().z );
+
+ if ( !GetBot()->GetBodyInterface()->IsPostureMobile() )
+ {
+ // body is not in a movable state right now
+ return;
+ }
+
+ Vector currentPos = m_nextBot->GetPosition();
+
+ // compute unit vector to goal position
+ m_moveVector = pos - currentPos;
+ m_moveVector.z = 0.0f;
+ float change = m_moveVector.NormalizeInPlace();
+
+ const float epsilon = 0.001f;
+ if ( change < epsilon )
+ {
+ // no motion
+ m_forwardLean = 0.0f;
+ m_sideLean = 0.0f;
+ return;
+ }
+
+/*
+ // lean forward/backward based on acceleration
+ float desiredLean = m_acceleration / NextBotLeanForwardAccel.GetFloat();
+
+ QAngle lean = GetDesiredLean();
+
+ lean.x = NextBotLeanMaxAngle.GetFloat() * clamp( desiredLean, -1.0f, 1.0f );
+
+ SetDesiredLean( lean );
+*/
+
+ Vector newPos;
+
+ // if we just started a jump, don't snap to the ground - let us get in the air first
+ if ( DidJustJump() || !IsOnGround() )
+ {
+ if ( false && m_isClimbingUpToLedge ) // causes bots to hang in air stuck against edges
+ {
+ // drive towards the approach position in XY to help reach ledge
+ m_moveVector = m_ledgeJumpGoalPos - currentPos;
+ m_moveVector.z = 0.0f;
+ m_moveVector.NormalizeInPlace();
+
+ m_acceleration += GetMaxAcceleration() * m_moveVector;
+ }
+ }
+ else if ( IsOnGround() )
+ {
+ // on the ground - move towards the approach position
+ m_isClimbingUpToLedge = false;
+
+ // snap forward movement vector along floor
+ const Vector &groundNormal = GetGroundNormal();
+
+ Vector left( -m_moveVector.y, m_moveVector.x, 0.0f );
+ m_moveVector = CrossProduct( left, groundNormal );
+ m_moveVector.NormalizeInPlace();
+
+ // limit maximum forward speed from self-acceleration
+ float forwardSpeed = DotProduct( m_velocity, m_moveVector );
+
+ float maxSpeed = MIN( m_desiredSpeed, GetSpeedLimit() );
+
+ if ( forwardSpeed < maxSpeed )
+ {
+ float ratio = ( forwardSpeed <= 0.0f ) ? 0.0f : ( forwardSpeed / maxSpeed );
+ float governor = 1.0f - ( ratio * ratio * ratio * ratio );
+
+ // accelerate towards goal
+ m_acceleration += governor * GetMaxAcceleration() * m_moveVector;
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move the bot to the precise given position immediately,
+ */
+void NextBotGroundLocomotion::DriveTo( const Vector &pos )
+{
+ BaseClass::DriveTo( pos );
+ m_bRecomputePostureOnCollision = true;
+ UpdatePosition( pos );
+}
+
+
+//--------------------------------------------------------------------------------------------
+/*
+ * Trace filter solely for use with DetectCollision() below.
+ */
+class GroundLocomotionCollisionTraceFilter : public CTraceFilterSimple
+{
+public:
+ GroundLocomotionCollisionTraceFilter( INextBot *me, const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup )
+ {
+ m_me = me;
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+
+ // don't collide with ourself
+ if ( entity && m_me->IsSelf( entity ) )
+ return false;
+
+ return m_me->GetLocomotionInterface()->ShouldCollideWith( entity );
+ }
+
+ return false;
+ }
+
+ INextBot *m_me;
+};
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Check for collisions during move and attempt to resolve them
+ */
+bool NextBotGroundLocomotion::DetectCollision( trace_t *pTrace, int &recursionLimit, const Vector &from, const Vector &to, const Vector &vecMins, const Vector &vecMaxs )
+{
+ IBody *body = GetBot()->GetBodyInterface();
+
+ CBaseEntity *ignore = m_ignorePhysicsPropTimer.IsElapsed() ? NULL : m_ignorePhysicsProp;
+ GroundLocomotionCollisionTraceFilter filter( GetBot(), ignore, body->GetCollisionGroup() );
+
+ TraceHull( from, to, vecMins, vecMaxs, body->GetSolidMask(), &filter, pTrace );
+
+ if ( !pTrace->DidHit() )
+ return false;
+
+ //
+ // A collision occurred - resolve it
+ //
+
+ // bust through "flimsy" breakables and keep on going
+ if ( pTrace->DidHitNonWorldEntity() && pTrace->m_pEnt != NULL )
+ {
+ CBaseEntity *other = pTrace->m_pEnt;
+
+ if ( !other->MyCombatCharacterPointer() && IsEntityTraversable( other, IMMEDIATELY ) /*&& IsFlimsy( other )*/ )
+ {
+ if ( recursionLimit <= 0 )
+ return true;
+
+ --recursionLimit;
+
+ // break the weak breakable we collided with
+ CTakeDamageInfo damageInfo( GetBot()->GetEntity(), GetBot()->GetEntity(), 100.0f, DMG_CRUSH );
+ CalculateExplosiveDamageForce( &damageInfo, GetMotionVector(), pTrace->endpos );
+ other->TakeDamage( damageInfo );
+
+ // retry trace now that the breakable is out of the way
+ return DetectCollision( pTrace, recursionLimit, from, to, vecMins, vecMaxs );
+ }
+ }
+
+ /// @todo Only invoke OnContact() and Touch() once per collision pair
+ // inform other components of collision
+ if ( GetBot()->ShouldTouch( pTrace->m_pEnt ) )
+ {
+ GetBot()->OnContact( pTrace->m_pEnt, pTrace );
+ }
+
+ INextBot *them = dynamic_cast< INextBot * >( pTrace->m_pEnt );
+ if ( them && them->ShouldTouch( m_nextBot ) )
+ {
+ /// @todo construct mirror of trace
+ them->OnContact( m_nextBot );
+ }
+ else
+ {
+ pTrace->m_pEnt->Touch( GetBot()->GetEntity() );
+ }
+
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+Vector NextBotGroundLocomotion::ResolveCollision( const Vector &from, const Vector &to, int recursionLimit )
+{
+ VPROF_BUDGET( "NextBotGroundLocomotion::ResolveCollision", "NextBotExpensive" );
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( body == NULL || recursionLimit < 0 )
+ {
+ Assert( !m_bRecomputePostureOnCollision );
+ return to;
+ }
+
+ // Only bother to recompute posture if we're currently standing or crouching
+ if ( m_bRecomputePostureOnCollision )
+ {
+ if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) )
+ {
+ m_bRecomputePostureOnCollision = false;
+ }
+ }
+
+ // get bounding limits, ignoring step-upable height
+ bool bPerformCrouchTest = false;
+ Vector mins;
+ Vector maxs;
+ if ( m_isUsingFullFeetTrace )
+ {
+ mins = body->GetHullMins();
+ }
+ else
+ {
+ mins = body->GetHullMins() + Vector( 0, 0, GetStepHeight() );
+ }
+ if ( !m_bRecomputePostureOnCollision )
+ {
+ maxs = body->GetHullMaxs();
+ if ( mins.z >= maxs.z )
+ {
+ // if mins.z is greater than maxs.z, the engine will Assert
+ // in UTIL_TraceHull, and it won't work as advertised.
+ mins.z = maxs.z - 2.0f;
+ }
+ }
+ else
+ {
+ const float halfSize = body->GetHullWidth() / 2.0f;
+ maxs.Init( halfSize, halfSize, body->GetStandHullHeight() );
+ bPerformCrouchTest = true;
+ }
+
+ trace_t trace;
+ Vector desiredGoal = to;
+ Vector resolvedGoal;
+ IBody::PostureType nPosture = IBody::STAND;
+ while( true )
+ {
+ bool bCollided = DetectCollision( &trace, recursionLimit, from, desiredGoal, mins, maxs );
+ if ( !bCollided )
+ {
+ resolvedGoal = desiredGoal;
+ break;
+ }
+
+ // If we hit really close to our target, then stop
+ if ( !trace.startsolid && desiredGoal.DistToSqr( trace.endpos ) < 1.0f )
+ {
+ resolvedGoal = trace.endpos;
+ break;
+ }
+
+ // Check for crouch test, if it's necessary
+ // Don't bother about checking for crouch if we hit an actor
+ // Also don't bother checking for crouch if we hit a plane that pushes us upwards
+ if ( bPerformCrouchTest )
+ {
+ // Don't do this work twice
+ bPerformCrouchTest = false;
+
+ nPosture = body->GetDesiredPosture();
+
+ if ( !trace.m_pEnt->MyNextBotPointer() && !trace.m_pEnt->IsPlayer() )
+ {
+ // Here, our standing trace hit the world or something non-breakable
+ // If we're not currently crouching, then see if we could travel
+ // the entire distance if we were crouched
+ if ( nPosture != IBody::CROUCH )
+ {
+ trace_t crouchTrace;
+ NextBotTraversableTraceFilter crouchFilter( GetBot(), ILocomotion::IMMEDIATELY );
+ Vector vecCrouchMax( maxs.x, maxs.y, body->GetCrouchHullHeight() );
+ TraceHull( from, desiredGoal, mins, vecCrouchMax, body->GetSolidMask(), &crouchFilter, &crouchTrace );
+ if ( crouchTrace.fraction >= 1.0f && !crouchTrace.startsolid )
+ {
+ nPosture = IBody::CROUCH;
+ }
+ }
+ }
+ else if ( nPosture == IBody::CROUCH )
+ {
+ // Here, our standing trace hit an actor
+
+ // NOTE: This test occurs almost never, based on my tests
+ // Converts from crouch to stand in the case where the player
+ // is currently crouching, *and* his first trace (with the standing hull)
+ // hits an actor *and* if he didn't hit that actor, he could have
+ // moved standing the entire way to his desired endpoint
+ trace_t standTrace;
+ NextBotTraversableTraceFilter standFilter( GetBot(), ILocomotion::IMMEDIATELY );
+ TraceHull( from, desiredGoal, mins, maxs, body->GetSolidMask(), &standFilter, &standTrace );
+ if ( standTrace.fraction >= 1.0f && !standTrace.startsolid )
+ {
+ nPosture = IBody::STAND;
+ }
+ }
+
+ // Our first trace was based on the standing hull.
+ // If we need be crouched, the trace was bogus; we need to do another
+ if ( nPosture == IBody::CROUCH )
+ {
+ maxs.z = body->GetCrouchHullHeight();
+ continue;
+ }
+ }
+
+ if ( trace.startsolid )
+ {
+ // stuck inside solid; don't move
+
+ if ( trace.m_pEnt && !trace.m_pEnt->IsWorld() )
+ {
+ // only ignore physics props that are not doors
+ if ( dynamic_cast< CPhysicsProp * >( trace.m_pEnt ) != NULL && dynamic_cast< CBasePropDoor * >( trace.m_pEnt ) == NULL )
+ {
+ IPhysicsObject *physics = trace.m_pEnt->VPhysicsGetObject();
+ if ( physics && physics->IsMoveable() )
+ {
+ // we've intersected a (likely moving) physics prop - ignore it for awhile so we can move out of it
+ m_ignorePhysicsProp = trace.m_pEnt;
+ m_ignorePhysicsPropTimer.Start( 1.0f );
+ }
+ }
+ }
+
+ // return to last known non-interpenetrating position
+ resolvedGoal = m_lastValidPos;
+
+ break;
+ }
+
+ if ( --recursionLimit <= 0 )
+ {
+ // reached recursion limit, no more adjusting allowed
+ resolvedGoal = trace.endpos;
+ break;
+ }
+
+ // never slide downwards/concave to avoid getting stuck in the ground
+ if ( trace.plane.normal.z < 0.0f )
+ {
+ trace.plane.normal.z = 0.0f;
+ trace.plane.normal.NormalizeInPlace();
+ }
+
+ // slide off of surface we hit
+ Vector fullMove = desiredGoal - from;
+ Vector leftToMove = fullMove * ( 1.0f - trace.fraction );
+
+ // obey climbing slope limit
+ if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) &&
+ trace.plane.normal.z < GetTraversableSlopeLimit() &&
+ fullMove.z > 0.0f )
+ {
+ fullMove.z = 0.0f;
+ trace.plane.normal.z = 0.0f;
+ trace.plane.normal.NormalizeInPlace();
+ }
+
+ float blocked = DotProduct( trace.plane.normal, leftToMove );
+
+ Vector unconstrained = fullMove - blocked * trace.plane.normal;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::Line( trace.endpos,
+ trace.endpos + 20.0f * trace.plane.normal,
+ 255, 0, 150, true, 15.0f );
+ }
+
+ // check for collisions along remainder of move
+ // But don't bother if we're not going to deflect much
+ Vector remainingMove = from + unconstrained;
+ if ( remainingMove.DistToSqr( trace.endpos ) < 1.0f )
+ {
+ resolvedGoal = trace.endpos;
+ break;
+ }
+
+ desiredGoal = remainingMove;
+ }
+
+ if ( !trace.startsolid )
+ {
+ m_lastValidPos = resolvedGoal;
+ }
+
+ if ( m_bRecomputePostureOnCollision )
+ {
+ m_bRecomputePostureOnCollision = false;
+
+ if ( !body->IsActualPosture( nPosture ) )
+ {
+ body->SetDesiredPosture( nPosture );
+ }
+ }
+
+ return resolvedGoal;
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+/**
+ * Collect the closest actors
+ */
+class ClosestActorsScan
+{
+public:
+ ClosestActorsScan( const Vector &spot, int team, float maxRange = 0.0f, CBaseCombatCharacter *ignore = NULL )
+ {
+ m_spot = spot;
+ m_team = team;
+ m_close = NULL;
+
+ if ( maxRange > 0.0f )
+ {
+ m_closeRangeSq = maxRange * maxRange;
+ }
+ else
+ {
+ m_closeRangeSq = 999999999.9f;
+ }
+
+ m_ignore = ignore;
+ }
+
+ bool operator() ( CBaseCombatCharacter *actor )
+ {
+ if (actor == m_ignore)
+ return true;
+
+ if (actor->IsAlive() && (m_team == TEAM_ANY || actor->GetTeamNumber() == m_team))
+ {
+ Vector to = actor->WorldSpaceCenter() - m_spot;
+ float rangeSq = to.LengthSqr();
+ if (rangeSq < m_closeRangeSq)
+ {
+ m_closeRangeSq = rangeSq;
+ m_close = actor;
+ }
+ }
+ return true;
+ }
+
+ CBaseCombatCharacter *GetActor( void ) const
+ {
+ return m_close;
+ }
+
+ bool IsCloserThan( float range )
+ {
+ return (m_closeRangeSq < (range * range));
+ }
+
+ bool IsFartherThan( float range )
+ {
+ return (m_closeRangeSq > (range * range));
+ }
+
+ Vector m_spot;
+ int m_team;
+ CBaseCombatCharacter *m_close;
+ float m_closeRangeSq;
+ CBaseCombatCharacter *m_ignore;
+};
+
+
+#ifdef SKIPME
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Push away zombies that are interpenetrating
+ */
+Vector NextBotGroundLocomotion::ResolveZombieCollisions( const Vector &pos )
+{
+ Vector adjustedNewPos = pos;
+
+ Infected *me = m_nextBot->MyInfectedPointer();
+ const float hullWidth = me->GetBodyInterface()->GetHullWidth();
+
+ // only avoid if we're actually trying to move somewhere, and are enraged
+ if ( me != NULL && !IsUsingLadder() && !IsClimbingOrJumping() && IsOnGround() && m_nextBot->IsAlive() && IsAttemptingToMove() /*&& GetBot()->GetBodyInterface()->IsArousal( IBody::INTENSE )*/ )
+ {
+ VPROF_BUDGET( "NextBotGroundLocomotion::ResolveZombieCollisions", "NextBot" );
+
+ const CUtlVector< CHandle< Infected > > &neighbors = me->GetNeighbors();
+ Vector avoid = vec3_origin;
+ float avoidWeight = 0.0f;
+
+ FOR_EACH_VEC( neighbors, it )
+ {
+ Infected *them = neighbors[ it ];
+
+ if ( them )
+ {
+ Vector toThem = them->GetAbsOrigin() - me->GetAbsOrigin();
+ toThem.z = 0.0f;
+
+ float range = toThem.NormalizeInPlace();
+
+ if ( range < hullWidth )
+ {
+ // these two infected are in contact
+ me->Touch( them );
+
+ // move out of contact
+ float penetration = hullWidth - range;
+
+ float weight = 1.0f + ( 2.0f * penetration/hullWidth );
+ avoid += -weight * toThem;
+ avoidWeight += weight;
+ }
+ }
+ }
+
+ if ( avoidWeight > 0.0f )
+ {
+ adjustedNewPos += 3.0f * ( avoid / avoidWeight );
+ }
+ }
+
+ return adjustedNewPos;
+}
+#endif // _DEBUG
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move to newPos, resolving any collisions along the way
+ */
+void NextBotGroundLocomotion::UpdatePosition( const Vector &newPos )
+{
+ VPROF_BUDGET( "NextBotGroundLocomotion::UpdatePosition", "NextBot" );
+
+ if ( NextBotStop.GetBool() || (m_nextBot->GetFlags() & FL_FROZEN) != 0 || newPos == m_nextBot->GetPosition() )
+ {
+ return;
+ }
+
+ // avoid very nearby Actors to simulate "mushy" collisions between actors in contact with each other
+ //Vector adjustedNewPos = ResolveZombieCollisions( newPos );
+ Vector adjustedNewPos = newPos;
+
+ // check for collisions during move and resolve them
+ const int recursionLimit = 3;
+ Vector safePos = ResolveCollision( m_nextBot->GetPosition(), adjustedNewPos, recursionLimit );
+
+ // set the bot's position
+ if ( GetBot()->GetIntentionInterface()->IsPositionAllowed( GetBot(), safePos ) != ANSWER_NO )
+ {
+ m_nextBot->SetPosition( safePos );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Prevent bot from sliding through floor, and snap to the ground if we're very near it
+ */
+void NextBotGroundLocomotion::UpdateGroundConstraint( void )
+{
+ VPROF_BUDGET( "NextBotGroundLocomotion::UpdateGroundConstraint", "NextBotExpensive" );
+
+ // if we're up on the upward arc of our jump, don't interfere by snapping to ground
+ // don't do ground constraint if we're climbing a ladder
+ if ( DidJustJump() || IsAscendingOrDescendingLadder() )
+ {
+ m_isUsingFullFeetTrace = false;
+ return;
+ }
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( body == NULL )
+ {
+ return;
+ }
+
+ float halfWidth = body->GetHullWidth()/2.0f;
+
+ // since we only care about ground collisions, keep hull short to avoid issues with low ceilings
+ /// @TODO: We need to also check actual hull height to avoid interpenetrating the world
+ float hullHeight = GetStepHeight();
+
+ // always need tolerance even when jumping/falling to make sure we detect ground penetration
+ // must be at least step height to avoid 'falling' down stairs
+ const float stickToGroundTolerance = GetStepHeight() + 0.01f;
+
+ trace_t ground;
+ NextBotTraceFilterIgnoreActors filter( m_nextBot, body->GetCollisionGroup() );
+
+ TraceHull( m_nextBot->GetPosition() + Vector( 0, 0, GetStepHeight() + 0.001f ),
+ m_nextBot->GetPosition() + Vector( 0, 0, -stickToGroundTolerance ),
+ Vector( -halfWidth, -halfWidth, 0 ),
+ Vector( halfWidth, halfWidth, hullHeight ),
+ body->GetSolidMask(), &filter, &ground );
+
+ if ( ground.startsolid )
+ {
+ // we're inside the ground - bad news
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) && !( gpGlobals->framecount % 60 ) )
+ {
+ DevMsg( "%3.2f: Inside ground, ( %.0f, %.0f, %.0f )\n", gpGlobals->curtime, m_nextBot->GetPosition().x, m_nextBot->GetPosition().y, m_nextBot->GetPosition().z );
+ }
+ return;
+ }
+
+ if ( ground.fraction < 1.0f )
+ {
+ // there is ground below us
+ m_groundNormal = ground.plane.normal;
+
+ m_isUsingFullFeetTrace = false;
+
+ // zero velocity normal to the ground
+ float normalVel = DotProduct( m_groundNormal, m_velocity );
+ m_velocity -= normalVel * m_groundNormal;
+
+ // check slope limit
+ if ( ground.plane.normal.z < GetTraversableSlopeLimit() )
+ {
+ // too steep to stand here
+
+ // too steep to be ground - treat it like a wall hit
+ if ( ( m_velocity.x * ground.plane.normal.x + m_velocity.y * ground.plane.normal.y ) <= 0.0f )
+ {
+ GetBot()->OnContact( ground.m_pEnt, &ground );
+ }
+
+ // we're contacting some kind of ground
+ // zero accelerations normal to the ground
+
+ float normalAccel = DotProduct( m_groundNormal, m_acceleration );
+ m_acceleration -= normalAccel * m_groundNormal;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ DevMsg( "%3.2f: NextBotGroundLocomotion - Too steep to stand here\n", gpGlobals->curtime );
+ NDebugOverlay::Line( GetFeet(), GetFeet() + 20.0f * ground.plane.normal, 255, 150, 0, true, 5.0f );
+ }
+
+ // clear out upward velocity so we don't walk up lightpoles
+ m_velocity.z = MIN( 0, m_velocity.z );
+ m_acceleration.z = MIN( 0, m_acceleration.z );
+
+ return;
+ }
+
+ // inform other components of collision if we didn't land on the 'world'
+ if ( ground.m_pEnt && !ground.m_pEnt->IsWorld() )
+ {
+ GetBot()->OnContact( ground.m_pEnt, &ground );
+ }
+
+ // snap us to the ground
+ m_nextBot->SetPosition( ground.endpos );
+
+ if ( !IsOnGround() )
+ {
+ // just landed
+ m_nextBot->SetGroundEntity( ground.m_pEnt );
+ m_ground = ground.m_pEnt;
+
+ // landing stops any jump in progress
+ m_isJumping = false;
+ m_isJumpingAcrossGap = false;
+
+ GetBot()->OnLandOnGround( ground.m_pEnt );
+ }
+ }
+ else
+ {
+ // not on the ground
+ if ( IsOnGround() )
+ {
+ GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() );
+ if ( !IsClimbingUpToLedge() && !IsJumpingAcrossGap() )
+ {
+ m_isUsingFullFeetTrace = true; // We're in the air and there's space below us, so use the full trace
+ m_acceleration.z -= GetGravity(); // start our gravity now
+ }
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/*
+void NextBotGroundLocomotion::StandUp( void )
+{
+ // make sure there is room to stand
+ trace_t result;
+ const float halfSize = GetHullWidth()/3.0f;
+ Vector standHullMin( -halfSize, -halfSize, GetStepHeight() + 0.1f );
+ Vector standHullMax( halfSize, halfSize, GetStandHullHeight() );
+
+ TraceHull( GetFeet(), GetFeet(), standHullMin, standHullMax, MASK_NPCSOLID, m_nextBot, MASK_DEFAULTPLAYERSOLID, &result );
+
+ if ( result.fraction >= 1.0f && !result.startsolid )
+ {
+ m_isCrouching = false;
+ }
+}
+*/
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Initiate a climb to an adjacent high ledge
+ */
+bool NextBotGroundLocomotion::ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle )
+{
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Initiate a jump across an empty volume of space to far side
+ */
+void NextBotGroundLocomotion::JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward )
+{
+ // can only jump if we're on the ground
+ if ( !IsOnGround() )
+ {
+ return;
+ }
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( !body->StartActivity( ACT_JUMP ) )
+ {
+ // body can't jump right now
+ return;
+ }
+
+
+ // scale impulse to land on target
+ Vector toGoal = landingGoal - GetFeet();
+
+ // equation doesn't work if we're jumping upwards
+ float height = toGoal.z;
+ toGoal.z = 0.0f;
+
+ float range = toGoal.NormalizeInPlace();
+
+ // jump out at 45 degree angle
+ const float cos45 = 0.7071f;
+
+ // avoid division by zero
+ if ( height > 0.9f * range )
+ {
+ height = 0.9f * range;
+ }
+
+ // ballistic equation to find initial velocity assuming 45 degree inclination and landing at give range and height
+ float launchVel = ( range / cos45 ) / sqrt( ( 2.0f * ( range - height ) ) / GetGravity() );
+
+ Vector up( 0, 0, 1 );
+ Vector ahead = up + toGoal;
+ ahead.NormalizeInPlace();
+
+ //m_velocity = cos45 * launchVel * ahead;
+ m_velocity = launchVel * ahead;
+ m_acceleration = vec3_origin;
+
+ m_isJumping = true;
+ m_isJumpingAcrossGap = true;
+ m_isClimbingUpToLedge = false;
+
+ GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Initiate a simple undirected jump in the air
+ */
+void NextBotGroundLocomotion::Jump( void )
+{
+ // can only jump if we're on the ground
+ if ( !IsOnGround() )
+ {
+ return;
+ }
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( !body->StartActivity( ACT_JUMP ) )
+ {
+ // body can't jump right now
+ return;
+ }
+
+ // jump straight up
+ m_velocity.z = sqrt( 2.0f * GetGravity() * GetMaxJumpHeight() );
+
+ m_isJumping = true;
+ m_isClimbingUpToLedge = false;
+
+ GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Set movement speed to running
+ */
+void NextBotGroundLocomotion::Run( void )
+{
+ m_desiredSpeed = GetRunSpeed();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Set movement speed to walking
+ */
+void NextBotGroundLocomotion::Walk( void )
+{
+ m_desiredSpeed = GetWalkSpeed();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Set movement speed to stopeed
+ */
+void NextBotGroundLocomotion::Stop( void )
+{
+ m_desiredSpeed = 0.0f;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return true if standing on something
+ */
+bool NextBotGroundLocomotion::IsOnGround( void ) const
+{
+ return (m_nextBot->GetGroundEntity() != NULL);
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Invoked when bot leaves ground for any reason
+ */
+void NextBotGroundLocomotion::OnLeaveGround( CBaseEntity *ground )
+{
+ m_nextBot->SetGroundEntity( NULL );
+ m_ground = NULL;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ DevMsg( "%3.2f: NextBotGroundLocomotion::OnLeaveGround\n", gpGlobals->curtime );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Invoked when bot lands on the ground after being in the air
+ */
+void NextBotGroundLocomotion::OnLandOnGround( CBaseEntity *ground )
+{
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ DevMsg( "%3.2f: NextBotGroundLocomotion::GetBot()->OnLandOnGround\n", gpGlobals->curtime );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Get maximum speed bot can reach, regardless of desired speed
+ */
+float NextBotGroundLocomotion::GetSpeedLimit( void ) const
+{
+ // if we're crouched, move at reduced speed
+ if ( !GetBot()->GetBodyInterface()->IsActualPosture( IBody::STAND ) )
+ {
+ return 0.75f * GetRunSpeed();
+ }
+
+ // no limit
+ return 99999999.9f;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Climb the given ladder to the top and dismount
+ */
+void NextBotGroundLocomotion::ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal )
+{
+ // if we're already climbing this ladder, don't restart
+ if ( m_ladder == ladder && m_isGoingUpLadder )
+ {
+ return;
+ }
+
+ m_ladder = ladder;
+ m_ladderDismountGoal = dismountGoal;
+ m_isGoingUpLadder = true;
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( body )
+ {
+ // line them up to climb in XY
+ Vector mountSpot = m_ladder->m_bottom + m_ladder->GetNormal() * (0.75f * body->GetHullWidth());
+ mountSpot.z = GetBot()->GetPosition().z;
+
+ UpdatePosition( mountSpot );
+
+ body->StartActivity( ACT_CLIMB_UP, IBody::MOTION_CONTROLLED_Z );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Descend the given ladder to the bottom and dismount
+ */
+void NextBotGroundLocomotion::DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal )
+{
+ // if we're already descending this ladder, don't restart
+ if ( m_ladder == ladder && !m_isGoingUpLadder )
+ {
+ return;
+ }
+
+ m_ladder = ladder;
+ m_ladderDismountGoal = dismountGoal;
+ m_isGoingUpLadder = false;
+
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( body )
+ {
+ // line them up to climb in XY
+ Vector mountSpot = m_ladder->m_top + m_ladder->GetNormal() * (0.75f * body->GetHullWidth());
+ mountSpot.z = GetBot()->GetPosition().z;
+
+ UpdatePosition( mountSpot );
+
+ float ladderYaw = UTIL_VecToYaw( -m_ladder->GetNormal() );
+
+ QAngle angles = m_nextBot->GetLocalAngles();
+ angles.y = ladderYaw;
+
+ m_nextBot->SetLocalAngles( angles );
+
+ body->StartActivity( ACT_CLIMB_DOWN, IBody::MOTION_CONTROLLED_Z );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+bool NextBotGroundLocomotion::IsUsingLadder( void ) const
+{
+ return ( m_ladder != NULL );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * We are actually on the ladder right now, either climbing up or down
+ */
+bool NextBotGroundLocomotion::IsAscendingOrDescendingLadder( void ) const
+{
+ return IsUsingLadder();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return position of "feet" - point below centroid of bot at feet level
+ */
+const Vector &NextBotGroundLocomotion::GetFeet( void ) const
+{
+ return m_nextBot->GetPosition();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+const Vector & NextBotGroundLocomotion::GetAcceleration( void ) const
+{
+ return m_acceleration;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotGroundLocomotion::SetAcceleration( const Vector &accel )
+{
+ m_acceleration = accel;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void NextBotGroundLocomotion::SetVelocity( const Vector &vel )
+{
+ m_velocity = vel;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return current world space velocity
+ */
+const Vector &NextBotGroundLocomotion::GetVelocity( void ) const
+{
+ return m_velocity;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Invoked when an bot reaches its MoveTo goal
+ */
+void NextBotGroundLocomotion::OnMoveToSuccess( const Path *path )
+{
+ // stop
+ m_velocity = vec3_origin;
+ m_acceleration = vec3_origin;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Invoked when an bot fails to reach a MoveTo goal
+ */
+void NextBotGroundLocomotion::OnMoveToFailure( const Path *path, MoveToFailureType reason )
+{
+ // stop
+ m_velocity = vec3_origin;
+ m_acceleration = vec3_origin;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+bool NextBotGroundLocomotion::DidJustJump( void ) const
+{
+ return IsClimbingOrJumping() && (m_nextBot->GetAbsVelocity().z > 0.0f);
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Rotate body to face towards "target"
+ */
+void NextBotGroundLocomotion::FaceTowards( const Vector &target )
+{
+ const float deltaT = GetUpdateInterval();
+
+ QAngle angles = m_nextBot->GetLocalAngles();
+
+ float desiredYaw = UTIL_VecToYaw( target - GetFeet() );
+
+ float angleDiff = UTIL_AngleDiff( desiredYaw, angles.y );
+
+ float deltaYaw = GetMaxYawRate() * deltaT;
+
+ if (angleDiff < -deltaYaw)
+ {
+ angles.y -= deltaYaw;
+ }
+ else if (angleDiff > deltaYaw)
+ {
+ angles.y += deltaYaw;
+ }
+ else
+ {
+ angles.y += angleDiff;
+ }
+
+ m_nextBot->SetLocalAngles( angles );
+}
+
+
+
diff --git a/game/server/NextBot/NextBotGroundLocomotion.h b/game/server/NextBot/NextBotGroundLocomotion.h
new file mode 100644
index 0000000..ecd221a
--- /dev/null
+++ b/game/server/NextBot/NextBotGroundLocomotion.h
@@ -0,0 +1,274 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// NextBotGroundLocomotion.h
+// Basic ground-based movement for NextBotCombatCharacters
+// Author: Michael Booth, February 2009
+// Note: This is a refactoring of ZombieBotLocomotion from L4D
+
+#ifndef NEXT_BOT_GROUND_LOCOMOTION_H
+#define NEXT_BOT_GROUND_LOCOMOTION_H
+
+#include "NextBotLocomotionInterface.h"
+#include "nav_mesh.h"
+
+
+class NextBotCombatCharacter;
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Basic ground-based movement for NextBotCombatCharacters.
+ * This locomotor resolves collisions and assumes a ground-based bot under the influence of gravity.
+ */
+class NextBotGroundLocomotion : public ILocomotion
+{
+public:
+ DECLARE_CLASS( NextBotGroundLocomotion, ILocomotion );
+
+ NextBotGroundLocomotion( INextBot *bot );
+ virtual ~NextBotGroundLocomotion();
+
+ virtual void Reset( void ); // reset locomotor to initial state
+ virtual void Update( void ); // update internal state
+
+ virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position
+ virtual void DriveTo( const Vector &pos ); // Move the bot to the precise given position immediately,
+
+ virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ); // initiate a jump to an adjacent high ledge, return false if climb can't start
+ virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ); // initiate a jump across an empty volume of space to far side
+ virtual void Jump( void ); // initiate a simple undirected jump in the air
+ virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form
+ virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge
+ virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side
+
+ virtual void Run( void ); // set desired movement speed to running
+ virtual void Walk( void ); // set desired movement speed to walking
+ virtual void Stop( void ); // set desired movement speed to stopped
+ virtual bool IsRunning( void ) const;
+ virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement
+ virtual float GetDesiredSpeed( void ) const; // returns the current desired speed
+
+ virtual float GetSpeedLimit( void ) const; // get maximum speed bot can reach, regardless of desired speed
+
+ virtual bool IsOnGround( void ) const; // return true if standing on something
+ virtual void OnLeaveGround( CBaseEntity *ground ); // invoked when bot leaves ground for any reason
+ virtual void OnLandOnGround( CBaseEntity *ground ); // invoked when bot lands on the ground after being in the air
+ virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground
+ virtual const Vector &GetGroundNormal( void ) const;// surface normal of the ground we are in contact with
+
+ virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // climb the given ladder to the top and dismount
+ virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // descend the given ladder to the bottom and dismount
+ virtual bool IsUsingLadder( void ) const;
+ virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down
+
+ virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target"
+
+ virtual void SetDesiredLean( const QAngle &lean );
+ virtual const QAngle &GetDesiredLean( void ) const;
+
+ virtual const Vector &GetFeet( void ) const; // return position of "feet" - the driving point where the bot contacts the ground
+
+ virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up
+ 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 float GetWalkSpeed( void ) const; // get maximum walking speed
+
+ virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor
+ virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor
+
+ virtual const Vector &GetAcceleration( void ) const; // return current world space acceleration
+ virtual void SetAcceleration( const Vector &accel ); // set world space acceleration
+
+ virtual const Vector &GetVelocity( void ) const; // return current world space velocity
+ virtual void SetVelocity( const Vector &vel ); // set world space velocity
+
+ virtual void OnMoveToSuccess( const Path *path ); // invoked when an bot reaches its MoveTo goal
+ virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ); // invoked when an bot fails to reach a MoveTo goal
+
+private:
+ void UpdatePosition( const Vector &newPos ); // move to newPos, resolving any collisions along the way
+ void UpdateGroundConstraint( void ); // keep ground solid
+ Vector ResolveCollisionV0( Vector from, Vector to, int recursionLimit );
+
+ Vector ResolveZombieCollisions( const Vector &pos ); // push away zombies that are interpenetrating
+ Vector ResolveCollision( const Vector &from, const Vector &to, int recursionLimit ); // check for collisions along move
+ bool DetectCollision( trace_t *pTrace, int &nDestructionAllowed, const Vector &from, const Vector &to, const Vector &vecMins, const Vector &vecMaxs );
+ void ApplyAccumulatedApproach( void );
+ bool DidJustJump( void ) const; // return true if we just started a jump
+ bool TraverseLadder( void ); // return true if we are climbing a ladder
+
+ virtual float GetGravity( void ) const; // return gravity force acting on bot
+ virtual float GetFrictionForward( void ) const; // return magnitude of forward friction
+ virtual float GetFrictionSideways( void ) const; // return magnitude of lateral friction
+ virtual float GetMaxYawRate( void ) const; // return max rate of yaw rotation
+
+
+private:
+ NextBotCombatCharacter *m_nextBot;
+
+ Vector m_priorPos; // last update's position
+ Vector m_lastValidPos; // last valid position (not interpenetrating)
+
+ Vector m_acceleration;
+ Vector m_velocity;
+
+ float m_desiredSpeed; // speed bot wants to be moving
+ float m_actualSpeed; // actual speed bot is moving
+
+ float m_maxRunSpeed;
+
+ float m_forwardLean;
+ float m_sideLean;
+ QAngle m_desiredLean;
+
+ bool m_isJumping; // if true, we have jumped and have not yet hit the ground
+ bool m_isJumpingAcrossGap; // if true, we have jumped across a gap and have not yet hit the ground
+ EHANDLE m_ground; // have to manage this ourselves, since MOVETYPE_CUSTOM always NULLs out GetGroundEntity()
+ Vector m_groundNormal; // surface normal of the ground we are in contact with
+ bool m_isClimbingUpToLedge; // true if we are jumping up to an adjacent ledge
+ Vector m_ledgeJumpGoalPos;
+ bool m_isUsingFullFeetTrace; // true if we're in the air and tracing the lowest StepHeight in ResolveCollision
+
+ const CNavLadder *m_ladder; // ladder we are currently climbing/descending
+ const CNavArea *m_ladderDismountGoal; // the area we enter when finished with our ladder move
+ bool m_isGoingUpLadder; // if false, we're going down
+
+ CountdownTimer m_inhibitObstacleAvoidanceTimer; // when active, turn off path following feelers
+
+ CountdownTimer m_wiggleTimer; // for wiggling
+ NavRelativeDirType m_wiggleDirection;
+
+ mutable Vector m_eyePos; // for use with GetEyes(), etc.
+
+ Vector m_moveVector; // the direction of our motion in XY plane
+ float m_moveYaw; // global yaw of movement direction
+
+ Vector m_accumApproachVectors; // weighted sum of Approach() calls since last update
+ float m_accumApproachWeights;
+ bool m_bRecomputePostureOnCollision;
+
+ CountdownTimer m_ignorePhysicsPropTimer; // if active, don't collide with physics props (because we got stuck in one)
+ EHANDLE m_ignorePhysicsProp; // which prop to ignore
+};
+
+
+inline float NextBotGroundLocomotion::GetGravity( void ) const
+{
+ return 1000.0f;
+}
+
+inline float NextBotGroundLocomotion::GetFrictionForward( void ) const
+{
+ return 0.0f;
+}
+
+inline float NextBotGroundLocomotion::GetFrictionSideways( void ) const
+{
+ return 3.0f;
+}
+
+inline float NextBotGroundLocomotion::GetMaxYawRate( void ) const
+{
+ return 250.0f;
+}
+
+inline CBaseEntity *NextBotGroundLocomotion::GetGround( void ) const
+{
+ return m_ground;
+}
+
+
+inline const Vector &NextBotGroundLocomotion::GetGroundNormal( void ) const
+{
+ return m_groundNormal;
+}
+
+
+inline void NextBotGroundLocomotion::SetDesiredLean( const QAngle &lean )
+{
+ m_desiredLean = lean;
+}
+
+
+inline const QAngle &NextBotGroundLocomotion::GetDesiredLean( void ) const
+{
+ return m_desiredLean;
+}
+
+
+inline void NextBotGroundLocomotion::SetDesiredSpeed( float speed )
+{
+ m_desiredSpeed = speed;
+}
+
+
+inline float NextBotGroundLocomotion::GetDesiredSpeed( void ) const
+{
+ return m_desiredSpeed;
+}
+
+
+inline bool NextBotGroundLocomotion::IsClimbingOrJumping( void ) const
+{
+ return m_isJumping;
+}
+
+inline bool NextBotGroundLocomotion::IsClimbingUpToLedge( void ) const
+{
+ return m_isClimbingUpToLedge;
+}
+
+inline bool NextBotGroundLocomotion::IsJumpingAcrossGap( void ) const
+{
+ return m_isJumpingAcrossGap;
+}
+
+inline bool NextBotGroundLocomotion::IsRunning( void ) const
+{
+ /// @todo Rethink interface to distinguish actual state vs desired state (do we want to be running, or are we actually at running speed right now)
+ return m_actualSpeed > 0.9f * GetRunSpeed();
+}
+
+
+inline float NextBotGroundLocomotion::GetStepHeight( void ) const
+{
+ return 18.0f;
+}
+
+
+inline float NextBotGroundLocomotion::GetMaxJumpHeight( void ) const
+{
+ return 180.0f; // 120.0f; // 84.0f; // 58.0f;
+}
+
+
+inline float NextBotGroundLocomotion::GetDeathDropHeight( void ) const
+{
+ return 200.0f;
+}
+
+
+inline float NextBotGroundLocomotion::GetRunSpeed( void ) const
+{
+ return 150.0f;
+}
+
+
+inline float NextBotGroundLocomotion::GetWalkSpeed( void ) const
+{
+ return 75.0f;
+}
+
+inline float NextBotGroundLocomotion::GetMaxAcceleration( void ) const
+{
+ return 500.0f;
+}
+
+inline float NextBotGroundLocomotion::GetMaxDeceleration( void ) const
+{
+ return 500.0f;
+}
+
+
+#endif // NEXT_BOT_GROUND_LOCOMOTION_H
+
diff --git a/game/server/NextBot/NextBotHearingInterface.h b/game/server/NextBot/NextBotHearingInterface.h
new file mode 100644
index 0000000..76a802d
--- /dev/null
+++ b/game/server/NextBot/NextBotHearingInterface.h
@@ -0,0 +1,34 @@
+// NextBotHearingInterface.h
+// Interface for auditory queries of a bot
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_HEARING_INTERFACE_H_
+#define _NEXT_BOT_HEARING_INTERFACE_H_
+
+#include "NextBotComponentInterface.h"
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for hearing sounds
+ */
+class IHearing : public INextBotComponent
+{
+public:
+ IHearing( INextBot *bot ) : INextBotComponent( bot ) { }
+ virtual ~IHearing() { }
+
+ virtual void Reset( void ); // reset to initial state
+ virtual void Update( void ); // update internal state
+
+ virtual float GetTimeSinceHeard( int team ) const; // return time since we heard any member of the given team
+
+ virtual CBaseEntity *GetClosestRecognized( int team = TEAM_ANY ) const; // return the closest recognized entity
+ virtual int GetRecognizedCount( int team, float rangeLimit = -1.0f ) const; // return the number of actors on the given team visible to us closer than rangeLimit
+
+ virtual float GetMaxHearingRange( void ) const; // return maximum distance we can hear
+ virtual float GetMinRecognizeTime( void ) const; // return HEARING reaction time
+};
+
+
+#endif // _NEXT_BOT_HEARING_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotIntentionInterface.cpp b/game/server/NextBot/NextBotIntentionInterface.cpp
new file mode 100644
index 0000000..b775896
--- /dev/null
+++ b/game/server/NextBot/NextBotIntentionInterface.cpp
@@ -0,0 +1,91 @@
+// NextBotIntentionInterface.cpp
+// Interface for intentional thinking
+// Author: Michael Booth, November 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBotInterface.h"
+#include "NextBotIntentionInterface.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+//------------------------------------------------------------------------------------------------------------------------
+/**
+ * Given a subject, return the world space position we should aim at
+ */
+Vector IIntention::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ Vector result = query->SelectTargetPoint( me, subject );
+ if ( result != vec3_origin )
+ {
+ return result;
+ }
+ }
+ }
+
+ // no answer, use a reasonable position
+ Vector threatMins, threatMaxs;
+ subject->CollisionProp()->WorldSpaceAABB( &threatMins, &threatMaxs );
+ Vector targetPoint = subject->GetAbsOrigin();
+ targetPoint.z += 0.7f * ( threatMaxs.z - threatMins.z );
+
+ return targetPoint;
+}
+
+
+//------------------------------------------------------------------------------------------------------------------------
+/**
+ * Given two threats, decide which one is more dangerous
+ */
+const CKnownEntity *IIntention::SelectMoreDangerousThreat( const INextBot *me, const CBaseCombatCharacter *subject, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const
+{
+ if ( !threat1 || threat1->IsObsolete() )
+ {
+ if ( threat2 && !threat2->IsObsolete() )
+ return threat2;
+
+ return NULL;
+ }
+ else if ( !threat2 || threat2->IsObsolete() )
+ {
+ return threat1;
+ }
+
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ const CKnownEntity *result = query->SelectMoreDangerousThreat( me, subject, threat1, threat2 );
+ if ( result )
+ {
+ return result;
+ }
+ }
+ }
+
+ // no specific decision was made - return closest threat as most dangerous
+ float range1 = ( subject->GetAbsOrigin() - threat1->GetLastKnownPosition() ).LengthSqr();
+ float range2 = ( subject->GetAbsOrigin() - threat2->GetLastKnownPosition() ).LengthSqr();
+
+ if ( range1 < range2 )
+ {
+ return threat1;
+ }
+
+ return threat2;
+}
+
+
+
+
diff --git a/game/server/NextBot/NextBotIntentionInterface.h b/game/server/NextBot/NextBotIntentionInterface.h
new file mode 100644
index 0000000..8ff337b
--- /dev/null
+++ b/game/server/NextBot/NextBotIntentionInterface.h
@@ -0,0 +1,211 @@
+// NextBotIntentionInterface.h
+// Interface for intentional thinking
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_INTENTION_INTERFACE_H_
+#define _NEXT_BOT_INTENTION_INTERFACE_H_
+
+#include "NextBotComponentInterface.h"
+#include "NextBotContextualQueryInterface.h"
+
+class INextBot;
+
+//
+// Insert this macro in your INextBot-derived class declaration to
+// create a IIntention-derived class that handles the bookkeeping
+// of instantiating a Behavior with an initial Action and updating it.
+//
+#define DECLARE_INTENTION_INTERFACE( Actor ) \
+ \
+ class Actor##Intention : public IIntention \
+ { \
+ public: \
+ Actor##Intention( Actor *me ); \
+ virtual ~Actor##Intention(); \
+ virtual void Reset( void ); \
+ virtual void Update( void ); \
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } \
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } \
+ private: \
+ Behavior< Actor > *m_behavior; \
+ }; \
+ \
+ public: virtual IIntention *GetIntentionInterface( void ) const { return m_intention; } \
+ private: Actor##Intention *m_intention; \
+ public:
+
+
+//
+// Use this macro to create the implementation code for the IIntention-derived class
+// declared above. Since this requires InitialAction, it must occur after
+// that Action has been declared, so it can be new'd here.
+//
+#define IMPLEMENT_INTENTION_INTERFACE( Actor, InitialAction ) \
+ Actor::Actor##Intention::Actor##Intention( Actor *me ) : IIntention( me ) { m_behavior = new Behavior< Actor >( new InitialAction ); } \
+ Actor::Actor##Intention::~Actor##Intention() { delete m_behavior; } \
+ void Actor::Actor##Intention::Reset( void ) { delete m_behavior; m_behavior = new Behavior< Actor >( new InitialAction ); } \
+ void Actor::Actor##Intention::Update( void ) { m_behavior->Update( static_cast< Actor * >( GetBot() ), GetUpdateInterval() ); }
+
+
+//
+// Use this macro in the constructor of your bot to allocate the IIntention-derived class
+//
+#define ALLOCATE_INTENTION_INTERFACE( Actor ) { m_intention = new Actor##Intention( this ); }
+
+//
+// Use this macro in the destructor of your bot to deallocate the IIntention-derived class
+//
+#define DEALLOCATE_INTENTION_INTERFACE { if ( m_intention ) delete m_intention; }
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for intentional thinking.
+ * The assumption is that this is a container for one or more concurrent Behaviors.
+ * The "primary" Behavior is the FirstContainedResponder, and so on.
+ * IContextualQuery requests are prioritized in contained responder order, such that the first responder
+ * that returns a definitive answer is accepted. WITHIN a given responder (ie: a Behavior), the deepest child
+ * Behavior in the active stack is asked first, then its parent, and so on, allowing the most specific active
+ * Behavior to override the query responses of its more general parent Behaviors.
+ */
+class IIntention : public INextBotComponent, public IContextualQuery
+{
+public:
+ IIntention( INextBot *bot ) : INextBotComponent( bot ) { }
+ virtual ~IIntention() { }
+
+ virtual void Reset( void ) { INextBotComponent::Reset(); } // reset to initial state
+ virtual void Update( void ) { } // update internal state
+
+ // IContextualQuery propagation --------------------------------
+ virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const; // if the desired item was available right now, should we pick it up?
+ 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 QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // return true if we should wait for 'blocker' that is across our path somewhere up ahead.
+ 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; // is the a place we can be?
+ virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me,
+ const CBaseCombatCharacter *subject, // the subject of the danger
+ const CKnownEntity *threat1,
+ const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats, or NULL if we have no opinion
+ // NOTE: As further queries are added, update the Behavior class to propagate them
+};
+
+
+inline QueryResultType IIntention::ShouldPickUp( const INextBot *me, CBaseEntity *item ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->ShouldPickUp( me, item );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+inline QueryResultType IIntention::ShouldHurry( const INextBot *me ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->ShouldHurry( me );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+inline QueryResultType IIntention::ShouldRetreat( const INextBot *me ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->ShouldRetreat( me );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+inline QueryResultType IIntention::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->ShouldAttack( me, them );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+inline QueryResultType IIntention::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->IsHindrance( me, blocker );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+inline QueryResultType IIntention::IsPositionAllowed( const INextBot *me, const Vector &pos ) const
+{
+ for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) )
+ {
+ const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub );
+ if ( query )
+ {
+ // return the response of the first responder that gives a definitive answer
+ QueryResultType result = query->IsPositionAllowed( me, pos );
+ if ( result != ANSWER_UNDEFINED )
+ {
+ return result;
+ }
+ }
+ }
+ return ANSWER_UNDEFINED;
+}
+
+
+#endif // _NEXT_BOT_INTENTION_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotInterface.cpp b/game/server/NextBot/NextBotInterface.cpp
new file mode 100644
index 0000000..f802e10
--- /dev/null
+++ b/game/server/NextBot/NextBotInterface.cpp
@@ -0,0 +1,537 @@
+// NextBotInterface.cpp
+// Implentation of system methods for NextBot interface
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "props.h"
+#include "fmtstr.h"
+#include "team.h"
+
+#include "NextBotInterface.h"
+#include "NextBotBodyInterface.h"
+#include "NextBotManager.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+// development only, off by default for 360
+ConVar NextBotDebugHistory( "nb_debug_history", IsX360() ? "0" : "1", FCVAR_CHEAT, "If true, each bot keeps a history of debug output in memory" );
+
+//----------------------------------------------------------------------------------------------------------------
+INextBot::INextBot( void ) : m_debugHistory( MAX_NEXTBOT_DEBUG_HISTORY, 0 ) // CUtlVector: grow to max length, alloc 0 initially
+{
+ m_tickLastUpdate = -999;
+ m_id = -1;
+ m_componentList = NULL;
+ m_debugDisplayLine = 0;
+
+ m_immobileTimer.Invalidate();
+ m_immobileCheckTimer.Invalidate();
+ m_immobileAnchor = vec3_origin;
+
+ m_currentPath = NULL;
+
+ // register with the manager
+ m_id = TheNextBots().Register( this );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+INextBot::~INextBot()
+{
+ ResetDebugHistory();
+
+ // tell the manager we're gone
+ TheNextBots().UnRegister( this );
+
+ // delete Intention first, since destruction of Actions may access other components
+ if ( m_baseIntention )
+ delete m_baseIntention;
+
+ if ( m_baseLocomotion )
+ delete m_baseLocomotion;
+
+ if ( m_baseBody )
+ delete m_baseBody;
+
+ if ( m_baseVision )
+ delete m_baseVision;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void INextBot::Reset( void )
+{
+ m_tickLastUpdate = -999;
+ m_debugType = 0;
+ m_debugDisplayLine = 0;
+
+ m_immobileTimer.Invalidate();
+ m_immobileCheckTimer.Invalidate();
+ m_immobileAnchor = vec3_origin;
+
+ for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent )
+ {
+ comp->Reset();
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void INextBot::ResetDebugHistory( void )
+{
+ for ( int i=0; i<m_debugHistory.Count(); ++i )
+ {
+ delete m_debugHistory[i];
+ }
+
+ m_debugHistory.RemoveAll();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::BeginUpdate()
+{
+ if ( TheNextBots().ShouldUpdate( this ) )
+ {
+ TheNextBots().NotifyBeginUpdate( this );
+ return true;
+ }
+ return false;
+}
+
+//----------------------------------------------------------------------------------------------------------------
+void INextBot::EndUpdate( void )
+{
+ TheNextBots().NotifyEndUpdate( this );
+}
+
+//----------------------------------------------------------------------------------------------------------------
+void INextBot::Update( void )
+{
+ VPROF_BUDGET( "INextBot::Update", "NextBot" );
+
+ m_debugDisplayLine = 0;
+
+ if ( IsDebugging( NEXTBOT_DEBUG_ALL ) )
+ {
+ CFmtStr msg;
+ DisplayDebugText( msg.sprintf( "#%d", GetEntity()->entindex() ) );
+ }
+
+ UpdateImmobileStatus();
+
+ // update all components
+ for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent )
+ {
+ if ( comp->ComputeUpdateInterval() )
+ {
+ comp->Update();
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+void INextBot::Upkeep( void )
+{
+ VPROF_BUDGET( "INextBot::Upkeep", "NextBot" );
+
+ // do upkeep for all components
+ for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent )
+ {
+ comp->Upkeep();
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::SetPosition( const Vector &pos )
+{
+ IBody *body = GetBodyInterface();
+ if (body)
+ {
+ return body->SetPosition( pos );
+ }
+
+ // fall back to setting raw entity position
+ GetEntity()->SetAbsOrigin( pos );
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+const Vector &INextBot::GetPosition( void ) const
+{
+ return const_cast< INextBot * >( this )->GetEntity()->GetAbsOrigin();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if given actor is our enemy
+ */
+bool INextBot::IsEnemy( const CBaseEntity *them ) const
+{
+ if ( them == NULL )
+ return false;
+
+ // this is not strictly correct, as spectators are not enemies
+ return const_cast< INextBot * >( this )->GetEntity()->GetTeamNumber() != them->GetTeamNumber();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if given actor is our friend
+ */
+bool INextBot::IsFriend( const CBaseEntity *them ) const
+{
+ if ( them == NULL )
+ return false;
+
+ return const_cast< INextBot * >( this )->GetEntity()->GetTeamNumber() == them->GetTeamNumber();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if 'them' is actually me
+ */
+bool INextBot::IsSelf( const CBaseEntity *them ) const
+{
+ if ( them == NULL )
+ return false;
+
+ return const_cast< INextBot * >( this )->GetEntity()->entindex() == them->entindex();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Components call this to register themselves with the bot that contains them
+ */
+void INextBot::RegisterComponent( INextBotComponent *comp )
+{
+ // add to head of singly linked list
+ comp->m_nextComponent = m_componentList;
+ m_componentList = comp;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::IsRangeLessThan( CBaseEntity *subject, float range ) const
+{
+ Vector botPos;
+ CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity();
+ if ( !bot || !subject )
+ return 0.0f;
+
+ bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos );
+ float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos );
+ return computedRange < range;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::IsRangeLessThan( const Vector &pos, float range ) const
+{
+ Vector to = pos - GetPosition();
+ return to.IsLengthLessThan( range );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::IsRangeGreaterThan( CBaseEntity *subject, float range ) const
+{
+ Vector botPos;
+ CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity();
+ if ( !bot || !subject )
+ return true;
+
+ bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos );
+ float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos );
+ return computedRange > range;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::IsRangeGreaterThan( const Vector &pos, float range ) const
+{
+ Vector to = pos - GetPosition();
+ return to.IsLengthGreaterThan( range );
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+float INextBot::GetRangeTo( CBaseEntity *subject ) const
+{
+ Vector botPos;
+ CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity();
+ if ( !bot || !subject )
+ return 0.0f;
+
+ bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos );
+ float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos );
+ return computedRange;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+float INextBot::GetRangeTo( const Vector &pos ) const
+{
+ Vector to = pos - GetPosition();
+ return to.Length();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+float INextBot::GetRangeSquaredTo( CBaseEntity *subject ) const
+{
+ Vector botPos;
+ CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity();
+ if ( !bot || !subject )
+ return 0.0f;
+
+ bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos );
+ float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos );
+ return computedRange * computedRange;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+float INextBot::GetRangeSquaredTo( const Vector &pos ) const
+{
+ Vector to = pos - GetPosition();
+ return to.LengthSqr();
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+bool INextBot::IsDebugging( unsigned int type ) const
+{
+ if ( TheNextBots().IsDebugging( type ) )
+ {
+ return TheNextBots().IsDebugFilterMatch( this );
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Return the name of this bot for debugging purposes
+ */
+const char *INextBot::GetDebugIdentifier( void ) const
+{
+ const int nameSize = 256;
+ static char name[ nameSize ];
+
+ Q_snprintf( name, nameSize, "%s(#%d)", const_cast< INextBot * >( this )->GetEntity()->GetClassname(), const_cast< INextBot * >( this )->GetEntity()->entindex() );
+
+ return name;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if we match the given debug symbol
+ */
+bool INextBot::IsDebugFilterMatch( const char *name ) const
+{
+ // compare debug identifier
+ if ( !Q_strnicmp( name, GetDebugIdentifier(), Q_strlen( name ) ) )
+ {
+ return true;
+ }
+
+ // compare team name
+ CTeam *team = GetEntity()->GetTeam();
+ if ( team && !Q_strnicmp( name, team->GetName(), Q_strlen( name ) ) )
+ {
+ return true;
+ }
+
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * There are some things we never want to climb on
+ */
+bool INextBot::IsAbleToClimbOnto( const CBaseEntity *object ) const
+{
+ if ( object == NULL || !const_cast<CBaseEntity *>(object)->IsAIWalkable() )
+ {
+ return false;
+ }
+
+ // never climb onto doors
+ if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "prop_door*" ) || FClassnameIs( const_cast< CBaseEntity * >( object ), "func_door*" ) )
+ {
+ return false;
+ }
+
+ // ok to climb on this object
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * Can we break this object
+ */
+bool INextBot::IsAbleToBreak( const CBaseEntity *object ) const
+{
+ if ( object && object->m_takedamage == DAMAGE_YES )
+ {
+ if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "func_breakable" ) &&
+ object->GetHealth() )
+ {
+ return true;
+ }
+
+ if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "func_breakable_surf" ) )
+ {
+ return true;
+ }
+
+ if ( dynamic_cast< const CBreakableProp * >( object ) != NULL )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+void INextBot::DisplayDebugText( const char *text ) const
+{
+ const_cast< INextBot * >( this )->GetEntity()->EntityText( m_debugDisplayLine++, text, 0.1 );
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void INextBot::DebugConColorMsg( NextBotDebugType debugType, const Color &color, const char *fmt, ... )
+{
+ bool isDataFormatted = false;
+
+ va_list argptr;
+ char data[ MAX_NEXTBOT_DEBUG_LINE_LENGTH ];
+
+ if ( developer.GetBool() && IsDebugging( debugType ) )
+ {
+ va_start(argptr, fmt);
+ Q_vsnprintf(data, sizeof( data ), fmt, argptr);
+ va_end(argptr);
+ isDataFormatted = true;
+
+ ConColorMsg( color, "%s", data );
+ }
+
+ if ( !NextBotDebugHistory.GetBool() )
+ {
+ if ( m_debugHistory.Count() )
+ {
+ ResetDebugHistory();
+ }
+ return;
+ }
+
+ // Don't bother with event data - it's spammy enough to overshadow everything else.
+ if ( debugType == NEXTBOT_EVENTS )
+ return;
+
+ if ( !isDataFormatted )
+ {
+ va_start(argptr, fmt);
+ Q_vsnprintf(data, sizeof( data ), fmt, argptr);
+ va_end(argptr);
+ isDataFormatted = true;
+ }
+
+ int lastLine = m_debugHistory.Count() - 1;
+ if ( lastLine >= 0 )
+ {
+ NextBotDebugLineType *line = m_debugHistory[lastLine];
+ if ( line->debugType == debugType && V_strstr( line->data, "\n" ) == NULL )
+ {
+ // append onto previous line
+ V_strncat( line->data, data, MAX_NEXTBOT_DEBUG_LINE_LENGTH );
+ return;
+ }
+ }
+
+ // Prune out an old line if needed, keeping a pointer to re-use the memory
+ NextBotDebugLineType *line = NULL;
+ if ( m_debugHistory.Count() == MAX_NEXTBOT_DEBUG_HISTORY )
+ {
+ line = m_debugHistory[0];
+ m_debugHistory.Remove( 0 );
+ }
+
+ // Add to debug history
+ if ( !line )
+ {
+ line = new NextBotDebugLineType;
+ }
+ line->debugType = debugType;
+ V_strncpy( line->data, data, MAX_NEXTBOT_DEBUG_LINE_LENGTH );
+ m_debugHistory.AddToTail( line );
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+// build a vector of debug history of the given types
+void INextBot::GetDebugHistory( unsigned int type, CUtlVector< const NextBotDebugLineType * > *lines ) const
+{
+ if ( !lines )
+ return;
+
+ lines->RemoveAll();
+
+ for ( int i=0; i<m_debugHistory.Count(); ++i )
+ {
+ NextBotDebugLineType *line = m_debugHistory[i];
+ if ( line->debugType & type )
+ {
+ lines->AddToTail( line );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void INextBot::UpdateImmobileStatus( void )
+{
+ if ( m_immobileCheckTimer.IsElapsed() )
+ {
+ m_immobileCheckTimer.Start( 1.0f );
+
+ // if we haven't moved farther than this in 1 second, we're immobile
+ if ( ( GetEntity()->GetAbsOrigin() - m_immobileAnchor ).IsLengthGreaterThan( GetImmobileSpeedThreshold() ) )
+ {
+ // moved far enough, not immobile
+ m_immobileAnchor = GetEntity()->GetAbsOrigin();
+ m_immobileTimer.Invalidate();
+ }
+ else
+ {
+ // haven't escaped our anchor - we are immobile
+ if ( !m_immobileTimer.HasStarted() )
+ {
+ m_immobileTimer.Start();
+ }
+ }
+ }
+}
+
diff --git a/game/server/NextBot/NextBotInterface.h b/game/server/NextBot/NextBotInterface.h
new file mode 100644
index 0000000..a2fd67e
--- /dev/null
+++ b/game/server/NextBot/NextBotInterface.h
@@ -0,0 +1,302 @@
+// NextBotInterface.h
+// Interface for NextBot
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_INTERFACE_H_
+#define _NEXT_BOT_INTERFACE_H_
+
+#include "NextBot/NextBotKnownEntity.h"
+#include "NextBotComponentInterface.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotBodyInterface.h"
+#include "NextBotIntentionInterface.h"
+#include "NextBotVisionInterface.h"
+#include "NextBotDebug.h"
+
+class CBaseCombatCharacter;
+class PathFollower;
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * A general purpose filter interface for various bot systems
+ */
+class INextBotFilter
+{
+public:
+ virtual bool IsSelected( const CBaseEntity *candidate ) const = 0; // return true if this entity passes the filter
+};
+
+
+//----------------------------------------------------------------------------------------------------------------
+class INextBot : public INextBotEventResponder
+{
+public:
+ INextBot( void );
+ virtual ~INextBot();
+
+ int GetBotId() const;
+
+ bool BeginUpdate();
+ void EndUpdate();
+
+ virtual void Reset( void ); // (EXTEND) reset to initial state
+ virtual void Update( void ); // (EXTEND) update internal state
+ virtual void Upkeep( void ); // (EXTEND) lightweight update guaranteed to occur every server tick
+
+ void FlagForUpdate( bool b = true );
+ bool IsFlaggedForUpdate();
+ int GetTickLastUpdate() const;
+ void SetTickLastUpdate( int );
+
+ virtual bool IsRemovedOnReset( void ) const { return true; } // remove this bot when the NextBot manager calls Reset
+
+ virtual CBaseCombatCharacter *GetEntity( void ) const = 0;
+ virtual class NextBotCombatCharacter *GetNextBotCombatCharacter( void ) const { return NULL; }
+
+#ifdef TERROR
+ virtual class SurvivorBot *MySurvivorBotPointer() const { return NULL; }
+#endif
+
+ // interfaces are never NULL - return base no-op interfaces at a minimum
+ virtual ILocomotion * GetLocomotionInterface( void ) const;
+ virtual IBody * GetBodyInterface( void ) const;
+ virtual IIntention * GetIntentionInterface( void ) const;
+ virtual IVision * GetVisionInterface( void ) const;
+
+ /**
+ * Attempt to change the bot's position. Return true if successful.
+ */
+ virtual bool SetPosition( const Vector &pos );
+ virtual const Vector &GetPosition( void ) const; // get the global position of the bot
+
+ /**
+ * Friend/enemy/neutral queries
+ */
+ virtual bool IsEnemy( const CBaseEntity *them ) const; // return true if given entity is our enemy
+ virtual bool IsFriend( const CBaseEntity *them ) const; // return true if given entity is our friend
+ virtual bool IsSelf( const CBaseEntity *them ) const; // return true if 'them' is actually me
+
+ /**
+ * Can we climb onto this entity?
+ */
+ virtual bool IsAbleToClimbOnto( const CBaseEntity *object ) const;
+
+ /**
+ * Can we break this entity?
+ */
+ virtual bool IsAbleToBreak( const CBaseEntity *object ) const;
+
+ /**
+ * Sometimes we want to pass through other NextBots. OnContact() will always
+ * be invoked, but collision resolution can be skipped if this
+ * method returns false.
+ */
+ virtual bool IsAbleToBlockMovementOf( const INextBot *botInMotion ) const { return true; }
+
+ /**
+ * Should we ever care about noticing physical contact with this entity?
+ */
+ virtual bool ShouldTouch( const CBaseEntity *object ) const { return true; }
+
+ /**
+ * This immobile system is used to track the global state of "am I actually moving or not".
+ * The OnStuck() event is only emitted when following a path, and paths can be recomputed, etc.
+ */
+ virtual bool IsImmobile( void ) const; // return true if we haven't moved in awhile
+ virtual float GetImmobileDuration( void ) const; // how long have we been immobile
+ virtual void ClearImmobileStatus( void );
+ virtual float GetImmobileSpeedThreshold( void ) const; // return units/second below which this actor is considered "immobile"
+
+ /**
+ * Get the last PathFollower we followed. This method gives other interfaces a
+ * single accessor to the most recent Path being followed by the myriad of
+ * different PathFollowers used in the various behaviors the bot may be doing.
+ */
+ virtual const PathFollower *GetCurrentPath( void ) const;
+ virtual void SetCurrentPath( const PathFollower *path );
+ virtual void NotifyPathDestruction( const PathFollower *path ); // this PathFollower is going away, which may or may not be ours
+
+ // between distance utility methods
+ virtual bool IsRangeLessThan( CBaseEntity *subject, float range ) const;
+ virtual bool IsRangeLessThan( const Vector &pos, float range ) const;
+ virtual bool IsRangeGreaterThan( CBaseEntity *subject, float range ) const;
+ virtual bool IsRangeGreaterThan( const Vector &pos, float range ) const;
+ virtual float GetRangeTo( CBaseEntity *subject ) const;
+ virtual float GetRangeTo( const Vector &pos ) const;
+ virtual float GetRangeSquaredTo( CBaseEntity *subject ) const;
+ virtual float GetRangeSquaredTo( const Vector &pos ) const;
+
+ // event propagation
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const;
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const;
+
+ virtual bool IsDebugging( unsigned int type ) const; // return true if this bot is debugging any of the given types
+ virtual const char *GetDebugIdentifier( void ) const; // return the name of this bot for debugging purposes
+ virtual bool IsDebugFilterMatch( const char *name ) const; // return true if we match the given debug symbol
+ virtual void DisplayDebugText( const char *text ) const; // show a line of text on the bot in the world
+ void DebugConColorMsg( NextBotDebugType debugType, const Color &color, PRINTF_FORMAT_STRING const char *fmt, ... );
+
+ enum {
+ MAX_NEXTBOT_DEBUG_HISTORY = 100,
+ MAX_NEXTBOT_DEBUG_LINE_LENGTH = 256,
+ };
+ struct NextBotDebugLineType
+ {
+ NextBotDebugType debugType;
+ char data[ MAX_NEXTBOT_DEBUG_LINE_LENGTH ];
+ };
+ void GetDebugHistory( unsigned int type, CUtlVector< const NextBotDebugLineType * > *lines ) const; // build a vector of debug history of the given types
+ //------------------------------------------------------------------------------
+
+
+private:
+ friend class INextBotComponent;
+ void RegisterComponent( INextBotComponent *comp ); // components call this to register themselves with the bot that contains them
+ INextBotComponent *m_componentList; // the first component
+
+ const PathFollower *m_currentPath; // the path we most recently followed
+
+ int m_id;
+ bool m_bFlaggedForUpdate;
+ int m_tickLastUpdate;
+
+ unsigned int m_debugType;
+ mutable int m_debugDisplayLine;
+
+ Vector m_immobileAnchor;
+ CountdownTimer m_immobileCheckTimer;
+ IntervalTimer m_immobileTimer;
+ void UpdateImmobileStatus( void );
+
+ mutable ILocomotion *m_baseLocomotion;
+ mutable IBody *m_baseBody;
+ mutable IIntention *m_baseIntention;
+ mutable IVision *m_baseVision;
+ //mutable IAttention *m_baseAttention;
+
+ // Debugging info
+ void ResetDebugHistory( void );
+ CUtlVector< NextBotDebugLineType * > m_debugHistory;
+};
+
+
+inline const PathFollower *INextBot::GetCurrentPath( void ) const
+{
+ return m_currentPath;
+}
+
+inline void INextBot::SetCurrentPath( const PathFollower *path )
+{
+ m_currentPath = path;
+}
+
+inline void INextBot::NotifyPathDestruction( const PathFollower *path )
+{
+ if ( m_currentPath == path )
+ m_currentPath = NULL;
+}
+
+
+inline ILocomotion *INextBot::GetLocomotionInterface( void ) const
+{
+ // these base interfaces are lazy-allocated (instead of being fully instanced classes) for two reasons:
+ // 1) so the memory is only used if needed
+ // 2) so the component is registered properly
+ if ( m_baseLocomotion == NULL )
+ {
+ m_baseLocomotion = new ILocomotion( const_cast< INextBot * >( this ) );
+ }
+
+ return m_baseLocomotion;
+}
+
+inline IBody *INextBot::GetBodyInterface( void ) const
+{
+ if ( m_baseBody == NULL )
+ {
+ m_baseBody = new IBody( const_cast< INextBot * >( this ) );
+ }
+
+ return m_baseBody;
+}
+
+inline IIntention *INextBot::GetIntentionInterface( void ) const
+{
+ if ( m_baseIntention == NULL )
+ {
+ m_baseIntention = new IIntention( const_cast< INextBot * >( this ) );
+ }
+
+ return m_baseIntention;
+}
+
+inline IVision *INextBot::GetVisionInterface( void ) const
+{
+ if ( m_baseVision == NULL )
+ {
+ m_baseVision = new IVision( const_cast< INextBot * >( this ) );
+ }
+
+ return m_baseVision;
+}
+
+inline int INextBot::GetBotId() const
+{
+ return m_id;
+}
+
+inline void INextBot::FlagForUpdate( bool b )
+{
+ m_bFlaggedForUpdate = b;
+}
+
+inline bool INextBot::IsFlaggedForUpdate()
+{
+ return m_bFlaggedForUpdate;
+}
+
+inline int INextBot::GetTickLastUpdate() const
+{
+ return m_tickLastUpdate;
+}
+
+inline void INextBot::SetTickLastUpdate( int tick )
+{
+ m_tickLastUpdate = tick;
+}
+
+inline bool INextBot::IsImmobile( void ) const
+{
+ return m_immobileTimer.HasStarted();
+}
+
+inline float INextBot::GetImmobileDuration( void ) const
+{
+ return m_immobileTimer.GetElapsedTime();
+}
+
+inline void INextBot::ClearImmobileStatus( void )
+{
+ m_immobileTimer.Invalidate();
+ m_immobileAnchor = GetEntity()->GetAbsOrigin();
+}
+
+inline float INextBot::GetImmobileSpeedThreshold( void ) const
+{
+ return 30.0f;
+}
+
+inline INextBotEventResponder *INextBot::FirstContainedResponder( void ) const
+{
+ return m_componentList;
+}
+
+
+inline INextBotEventResponder *INextBot::NextContainedResponder( INextBotEventResponder *current ) const
+{
+ return static_cast< INextBotComponent * >( current )->m_nextComponent;
+}
+
+
+#endif // _NEXT_BOT_INTERFACE_H_
diff --git a/game/server/NextBot/NextBotKnownEntity.h b/game/server/NextBot/NextBotKnownEntity.h
new file mode 100644
index 0000000..7a4e9ed
--- /dev/null
+++ b/game/server/NextBot/NextBotKnownEntity.h
@@ -0,0 +1,175 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// NextBotKnownEntity.h
+// Encapsulation of being aware of an entity
+// Author: Michael Booth, June 2009
+
+#ifndef NEXT_BOT_KNOWN_ENTITY_H
+#define NEXT_BOT_KNOWN_ENTITY_H
+
+//----------------------------------------------------------------------------
+/**
+ * A "known entity" is an entity that we have seen or heard at some point
+ * and which may or may not be immediately visible to us right now but which
+ * we remember the last place we encountered it, and when.
+ *
+ * TODO: Enhance interface to allow for sets of areas where an unseen entity
+ * could potentially be, knowing his last position and his rate of movement.
+ */
+class CKnownEntity
+{
+public:
+ // constructing assumes we currently know about this entity
+ CKnownEntity( CBaseEntity *who )
+ {
+ m_who = who;
+ m_whenLastSeen = -1.0f;
+ m_whenLastBecameVisible = -1.0f;
+ m_isVisible = false;
+ m_whenBecameKnown = gpGlobals->curtime;
+ m_hasLastKnownPositionBeenSeen = false;
+ UpdatePosition();
+ }
+
+ virtual ~CKnownEntity() { }
+
+ virtual void Destroy( void )
+ {
+ m_who = NULL;
+ m_isVisible = false;
+ }
+
+ virtual void UpdatePosition( void ) // could be seen or heard, but now the entity's position is known
+ {
+ if ( m_who.Get() )
+ {
+ m_lastKnownPostion = m_who->GetAbsOrigin();
+ m_lastKnownArea = m_who->MyCombatCharacterPointer() ? m_who->MyCombatCharacterPointer()->GetLastKnownArea() : NULL;
+ m_whenLastKnown = gpGlobals->curtime;
+ }
+ }
+
+ virtual CBaseEntity *GetEntity( void ) const
+ {
+ return m_who;
+ }
+
+ virtual const Vector &GetLastKnownPosition( void ) const
+ {
+ return m_lastKnownPostion;
+ }
+
+ // Have we had a clear view of the last known position of this entity?
+ // This encapsulates the idea of "I just saw a guy right over *there* a few seconds ago, but I don't know where he is now"
+ virtual bool HasLastKnownPositionBeenSeen( void ) const
+ {
+ return m_hasLastKnownPositionBeenSeen;
+ }
+
+ virtual void MarkLastKnownPositionAsSeen( void )
+ {
+ m_hasLastKnownPositionBeenSeen = true;
+ }
+
+ virtual const CNavArea *GetLastKnownArea( void ) const
+ {
+ return m_lastKnownArea;
+ }
+
+ virtual float GetTimeSinceLastKnown( void ) const
+ {
+ return gpGlobals->curtime - m_whenLastKnown;
+ }
+
+ virtual float GetTimeSinceBecameKnown( void ) const
+ {
+ return gpGlobals->curtime - m_whenBecameKnown;
+ }
+
+ virtual void UpdateVisibilityStatus( bool visible )
+ {
+ if ( visible )
+ {
+ if ( !m_isVisible )
+ {
+ // just became visible
+ m_whenLastBecameVisible = gpGlobals->curtime;
+ }
+
+ m_whenLastSeen = gpGlobals->curtime;
+ }
+
+ m_isVisible = visible;
+ }
+
+ virtual bool IsVisibleInFOVNow( void ) const // return true if this entity is currently visible and in my field of view
+ {
+ return m_isVisible;
+ }
+
+ virtual bool IsVisibleRecently( void ) const // return true if this entity is visible or was very recently visible
+ {
+ if ( m_isVisible )
+ return true;
+
+ if ( WasEverVisible() && GetTimeSinceLastSeen() < 3.0f )
+ return true;
+
+ return false;
+ }
+
+ virtual float GetTimeSinceBecameVisible( void ) const
+ {
+ return gpGlobals->curtime - m_whenLastBecameVisible;
+ }
+
+ virtual float GetTimeWhenBecameVisible( void ) const
+ {
+ return m_whenLastBecameVisible;
+ }
+
+ virtual float GetTimeSinceLastSeen( void ) const
+ {
+ return gpGlobals->curtime - m_whenLastSeen;
+ }
+
+ virtual bool WasEverVisible( void ) const
+ {
+ return m_whenLastSeen > 0.0f;
+ }
+
+ // has our knowledge of this entity become obsolete?
+ virtual bool IsObsolete( void ) const
+ {
+ return GetEntity() == NULL || !m_who->IsAlive() || GetTimeSinceLastKnown() > 10.0f;
+ }
+
+ virtual bool operator==( const CKnownEntity &other ) const
+ {
+ if ( GetEntity() == NULL || other.GetEntity() == NULL )
+ return false;
+
+ return ( GetEntity() == other.GetEntity() );
+ }
+
+ virtual bool Is( CBaseEntity *who ) const
+ {
+ if ( GetEntity() == NULL || who == NULL )
+ return false;
+
+ return ( GetEntity() == who );
+ }
+
+private:
+ CHandle< CBaseEntity > m_who;
+ Vector m_lastKnownPostion;
+ bool m_hasLastKnownPositionBeenSeen;
+ CNavArea *m_lastKnownArea;
+ float m_whenLastSeen;
+ float m_whenLastBecameVisible;
+ float m_whenLastKnown; // last seen or heard, confirming its existance
+ float m_whenBecameKnown;
+ bool m_isVisible; // flagged by IVision update as visible or not
+};
+
+
+#endif // NEXT_BOT_KNOWN_ENTITY_H
diff --git a/game/server/NextBot/NextBotLocomotionInterface.cpp b/game/server/NextBot/NextBotLocomotionInterface.cpp
new file mode 100644
index 0000000..9acc865
--- /dev/null
+++ b/game/server/NextBot/NextBotLocomotionInterface.cpp
@@ -0,0 +1,520 @@
+// NextBotLocomotionInterface.cpp
+// Common functionality for all NextBot locomotors
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "BasePropDoor.h"
+
+#include "nav_area.h"
+#include "NextBot.h"
+#include "NextBotUtil.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotBodyInterface.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+// how far a bot must move to not be considered "stuck"
+#define STUCK_RADIUS 100.0f
+
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Reset to initial state
+ */
+ILocomotion::ILocomotion( INextBot *bot ) : INextBotComponent( bot )
+{
+ Reset();
+}
+
+ILocomotion::~ILocomotion()
+{
+}
+
+void ILocomotion::Reset( void )
+{
+ INextBotComponent::Reset();
+
+ m_motionVector = Vector( 1.0f, 0.0f, 0.0f );
+ m_speed = 0.0f;
+ m_groundMotionVector = m_motionVector;
+ m_groundSpeed = m_speed;
+
+ m_moveRequestTimer.Invalidate();
+
+ m_isStuck = false;
+ m_stuckTimer.Invalidate();
+ m_stuckPos = vec3_origin;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Update internal state
+ */
+void ILocomotion::Update( void )
+{
+ StuckMonitor();
+
+ // maintain motion vector and speed values
+ const Vector &vel = GetVelocity();
+ m_speed = vel.Length();
+ m_groundSpeed = vel.AsVector2D().Length();
+
+ const float velocityThreshold = 10.0f;
+ if ( m_speed > velocityThreshold )
+ {
+ m_motionVector = vel / m_speed;
+ }
+
+ if ( m_groundSpeed > velocityThreshold )
+ {
+ m_groundMotionVector.x = vel.x / m_groundSpeed;
+ m_groundMotionVector.y = vel.y / m_groundSpeed;
+ m_groundMotionVector.z = 0.0f;
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ // show motion vector
+ NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_groundMotionVector, 3.0f, 100, 255, 0, 255, true, 0.1f );
+ NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_motionVector, 5.0f, 255, 255, 0, 255, true, 0.1f );
+ }
+}
+
+
+//----------------------------------------------------------------------------
+void ILocomotion::AdjustPosture( const Vector &moveGoal )
+{
+ // This function has no effect if we're not standing or crouching
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) )
+ return;
+
+ //
+ // Stand or crouch as needed
+ //
+
+ // get bounding limits, ignoring step-upable height
+ const Vector &mins = body->GetHullMins() + Vector( 0, 0, GetStepHeight() );
+
+ const float halfSize = body->GetHullWidth()/2.0f;
+ Vector standMaxs( halfSize, halfSize, body->GetStandHullHeight() );
+
+ trace_t trace;
+ NextBotTraversableTraceFilter filter( GetBot(), ILocomotion::IMMEDIATELY );
+
+ // snap forward movement vector along floor
+ const Vector &groundNormal = GetGroundNormal();
+ const Vector &feet = GetFeet();
+ Vector moveDir = moveGoal - feet;
+ float moveLength = moveDir.NormalizeInPlace();
+ Vector left( -moveDir.y, moveDir.x, 0.0f );
+ Vector goal = feet + moveLength * CrossProduct( left, groundNormal ).Normalized();
+
+ TraceHull( feet, goal, mins, standMaxs, body->GetSolidMask(), &filter, &trace );
+
+ if ( trace.fraction >= 1.0f && !trace.startsolid )
+ {
+ // no collision while standing
+ if ( body->IsActualPosture( IBody::CROUCH ) )
+ {
+ body->SetDesiredPosture( IBody::STAND );
+ }
+ return;
+ }
+
+ if ( body->IsActualPosture( IBody::CROUCH ) )
+ return;
+
+ // crouch hull check
+ Vector crouchMaxs( halfSize, halfSize, body->GetCrouchHullHeight() );
+
+ TraceHull( feet, goal, mins, crouchMaxs, body->GetSolidMask(), &filter, &trace );
+
+ if ( trace.fraction >= 1.0f && !trace.startsolid )
+ {
+ // no collision while crouching
+ body->SetDesiredPosture( IBody::CROUCH );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move directly towards the given position
+ */
+void ILocomotion::Approach( const Vector &goalPos, float goalWeight )
+{
+ // there is a desire to move
+ m_moveRequestTimer.Start();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Move the bot to the precise given position immediately
+ */
+void ILocomotion::DriveTo( const Vector &pos )
+{
+ // there is a desire to move
+ m_moveRequestTimer.Start();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return true if this locomotor could potentially move along the line given.
+ * If false is returned, fraction of walkable ray is returned in 'fraction'
+ */
+bool ILocomotion::IsPotentiallyTraversable( const Vector &from, const Vector &to, TraverseWhenType when, float *fraction ) const
+{
+ VPROF_BUDGET( "Locomotion::IsPotentiallyTraversable", "NextBotExpensive" );
+
+ // if 'to' is high above us, it's not directly traversable
+ // Adding a bit of fudge room to allow for floating point roundoff errors
+ if ( ( to.z - from.z ) > GetMaxJumpHeight() + 0.1f )
+ {
+ Vector along = to - from;
+ along.NormalizeInPlace();
+ if ( along.z > GetTraversableSlopeLimit() )
+ {
+ if ( fraction )
+ {
+ *fraction = 0.0f;
+ }
+ return false;
+ }
+ }
+
+ trace_t result;
+ NextBotTraversableTraceFilter filter( GetBot(), when );
+
+ // use a small hull since we cannot simulate collision resolution and avoidance along the way
+ const float probeSize = 0.25f * GetBot()->GetBodyInterface()->GetHullWidth(); // Cant be TOO small, or open stairwells/grates/etc will cause problems
+ const float probeZ = GetStepHeight();
+
+ Vector hullMin( -probeSize, -probeSize, probeZ );
+ Vector hullMax( probeSize, probeSize, GetBot()->GetBodyInterface()->GetCrouchHullHeight() );
+ TraceHull( from, to, hullMin, hullMax, GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+/*
+ if ( result.DidHit() )
+ {
+ NDebugOverlay::SweptBox( from, result.endpos, hullMin, hullMax, vec3_angle, 255, 0, 0, 255, 9999.9f );
+ NDebugOverlay::SweptBox( result.endpos, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 9999.9f );
+ }
+ else
+ {
+ NDebugOverlay::SweptBox( from, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 0.1f );
+ }
+*/
+
+ if ( fraction )
+ {
+ *fraction = result.fraction;
+ }
+
+ return ( result.fraction >= 1.0f ) && ( !result.startsolid );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return true if there is a possible "gap" that will need to be jumped over
+ * If true is returned, fraction of ray before gap is returned in 'fraction'
+ */
+bool ILocomotion::HasPotentialGap( const Vector &from, const Vector &desiredTo, float *fraction ) const
+{
+ VPROF_BUDGET( "Locomotion::HasPotentialGap", "NextBot" );
+
+ // find section of this ray that is actually traversable
+ float traversableFraction;
+ IsPotentiallyTraversable( from, desiredTo, IMMEDIATELY, &traversableFraction );
+
+ // compute end of traversable ray
+ Vector to = from + ( desiredTo - from ) * traversableFraction;
+
+ Vector forward = to - from;
+ float length = forward.NormalizeInPlace();
+
+ IBody *body = GetBot()->GetBodyInterface();
+
+ float step = body->GetHullWidth()/2.0f;
+
+ // scan along the line checking for gaps
+ Vector pos = from;
+ Vector delta = step * forward;
+ for( float t = 0.0f; t < (length + step); t += step )
+ {
+ if ( IsGap( pos, forward ) )
+ {
+ if ( fraction )
+ {
+ *fraction = ( t - step ) / ( length + step );
+ }
+
+ return true;
+ }
+
+ pos += delta;
+ }
+
+ if ( fraction )
+ {
+ *fraction = 1.0f;
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+/**
+ * Return true if there is a "gap" here when moving in the given direction.
+ * A "gap" is a vertical dropoff that is too high to jump back up to.
+ */
+bool ILocomotion::IsGap( const Vector &pos, const Vector &forward ) const
+{
+ VPROF_BUDGET( "Locomotion::IsGap", "NextBotSpiky" );
+
+ IBody *body = GetBot()->GetBodyInterface();
+
+ //float halfWidth = ( body ) ? body->GetHullWidth()/2.0f : 1.0f;
+
+ // can't really jump effectively when crouched anyhow
+ //float hullHeight = ( body ) ? body->GetStandHullHeight() : 1.0f;
+
+ // use a small hull since we cannot simulate collision resolution and avoidance along the way
+ const float halfWidth = 1.0f;
+ const float hullHeight = 1.0f;
+
+ unsigned int mask = ( body ) ? body->GetSolidMask() : MASK_PLAYERSOLID;
+
+ trace_t ground;
+
+ NextBotTraceFilterIgnoreActors filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE );
+
+ TraceHull( pos + Vector( 0, 0, GetStepHeight() ), // start up a bit to handle rough terrain
+ pos + Vector( 0, 0, -GetMaxJumpHeight() ),
+ Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ),
+ mask, &filter, &ground );
+
+// int r,g,b;
+//
+// if ( ground.fraction >= 1.0f && !ground.startsolid )
+// {
+// r = 255, g = 0, b = 0;
+// }
+// else
+// {
+// r = 0, g = 255, b = 0;
+// }
+//
+// NDebugOverlay::SweptBox( pos,
+// pos + Vector( 0, 0, -GetStepHeight() ),
+// Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ),
+// vec3_angle,
+// r, g, b, 255, 3.0f );
+
+ // if trace hit nothing, there's a gap ahead of us
+ return ( ground.fraction >= 1.0f && !ground.startsolid );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+bool ILocomotion::IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when ) const
+{
+ if ( obstacle->IsWorld() )
+ return false;
+
+ // assume bot will open a door in its path
+ if ( FClassnameIs( obstacle, "prop_door*" ) || FClassnameIs( obstacle, "func_door*" ) )
+ {
+ CBasePropDoor *door = dynamic_cast< CBasePropDoor * >( obstacle );
+
+ if ( door && door->IsDoorOpen() )
+ {
+ // open doors are obstacles
+ return false;
+ }
+
+ return true;
+ }
+
+ // if we hit a clip brush, ignore it if it is not BRUSHSOLID_ALWAYS
+ if ( FClassnameIs( obstacle, "func_brush" ) )
+ {
+ CFuncBrush *brush = (CFuncBrush *)obstacle;
+
+ switch ( brush->m_iSolidity )
+ {
+ case CFuncBrush::BRUSHSOLID_ALWAYS:
+ return false;
+ case CFuncBrush::BRUSHSOLID_NEVER:
+ return true;
+ case CFuncBrush::BRUSHSOLID_TOGGLE:
+ return true;
+ }
+ }
+
+ if ( when == IMMEDIATELY )
+ {
+ // special rules in specific games can immediately break some breakables, etc.
+ return false;
+ }
+
+ // assume bot will EVENTUALLY break breakables in its path
+ return GetBot()->IsAbleToBreak( obstacle );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+bool ILocomotion::IsAreaTraversable( const CNavArea *baseArea ) const
+{
+ return !baseArea->IsBlocked( GetBot()->GetEntity()->GetTeamNumber() );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Reset stuck status to un-stuck
+ */
+void ILocomotion::ClearStuckStatus( const char *reason )
+{
+ if ( IsStuck() )
+ {
+ m_isStuck = false;
+
+ // tell other components we're no longer stuck
+ GetBot()->OnUnStuck();
+ }
+
+ // always reset stuck monitoring data in case we cleared preemptively are were not yet stuck
+ m_stuckPos = GetFeet();
+ m_stuckTimer.Start();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ DevMsg( "%3.2f: ClearStuckStatus: %s %s\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), reason );
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Stuck check
+ */
+void ILocomotion::StuckMonitor( void )
+{
+ // a timer is needed to smooth over a few frames of inactivity due to state changes, etc.
+ // we only want to detect idle situations when the bot really doesn't "want" to move.
+ const float idleTime = 0.25f;
+ if ( m_moveRequestTimer.IsGreaterThen( idleTime ) )
+ {
+ // we have no desire to move, and therefore cannot emit stuck events
+
+ // prepare our internal state for when the bot starts to move next
+ m_stuckPos = GetFeet();
+ m_stuckTimer.Start();
+
+ return;
+ }
+
+// if ( !IsOnGround() )
+// {
+// // can't be stuck when in-air
+// ClearStuckStatus( "Off the ground" );
+// return;
+// }
+
+// if ( IsUsingLadder() )
+// {
+// // can't be stuck when on a ladder (for now)
+// ClearStuckStatus( "On a ladder" );
+// return;
+// }
+
+ if ( IsStuck() )
+ {
+ // we are/were stuck - have we moved enough to consider ourselves "dislodged"
+ if ( GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) )
+ {
+ // we've just become un-stuck
+ ClearStuckStatus( "UN-STUCK" );
+ }
+ else
+ {
+ // still stuck - periodically resend the event
+ if ( m_stillStuckTimer.IsElapsed() )
+ {
+ m_stillStuckTimer.Start( 1.0f );
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ DevMsg( "%3.2f: %s STILL STUCK\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier() );
+ NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 1.0f );
+ }
+
+ GetBot()->OnStuck();
+ }
+ }
+ }
+ else
+ {
+ // we're not stuck - yet
+
+ if ( /*IsClimbingOrJumping() || */GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) )
+ {
+ // we have moved - reset anchor
+ m_stuckPos = GetFeet();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::Cross3D( m_stuckPos, 3.0f, 255, 0, 255, true, 3.0f );
+ }
+
+ m_stuckTimer.Start();
+ }
+ else
+ {
+ // within stuck range of anchor. if we've been here too long, we're stuck
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::Line( GetBot()->GetEntity()->WorldSpaceCenter(), m_stuckPos, 255, 0, 255, true, 0.1f );
+ }
+
+ float minMoveSpeed = 0.1f * GetDesiredSpeed() + 0.1f;
+ float escapeTime = STUCK_RADIUS / minMoveSpeed;
+ if ( m_stuckTimer.IsGreaterThen( escapeTime ) )
+ {
+ // we have taken too long - we're stuck
+ m_isStuck = true;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_ERRORS ) )
+ {
+ DevMsg( "%3.2f: %s STUCK at position( %3.2f, %3.2f, %3.2f )\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), m_stuckPos.x, m_stuckPos.y, m_stuckPos.z );
+
+ NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 15.0f ), QAngle( -90.0f, 0, 0 ), 3.0f, 255, 255, 0, 255, true, 1.0f );
+ NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 9999999.9f );
+ }
+
+ // tell other components we've become stuck
+ GetBot()->OnStuck();
+ }
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+const Vector &ILocomotion::GetFeet( void ) const
+{
+ return GetBot()->GetEntity()->GetAbsOrigin();
+}
+
diff --git a/game/server/NextBot/NextBotLocomotionInterface.h b/game/server/NextBot/NextBotLocomotionInterface.h
new file mode 100644
index 0000000..f311728
--- /dev/null
+++ b/game/server/NextBot/NextBotLocomotionInterface.h
@@ -0,0 +1,335 @@
+// NextBotLocomotionInterface.h
+// NextBot interface for movement through the environment
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_LOCOMOTION_INTERFACE_H_
+#define _NEXT_BOT_LOCOMOTION_INTERFACE_H_
+
+#include "NextBotComponentInterface.h"
+
+class Path;
+class INextBot;
+class CNavLadder;
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface encapsulating *how* a bot moves through the world (walking? flying? etc)
+ */
+class ILocomotion : public INextBotComponent
+{
+public:
+ ILocomotion( INextBot *bot );
+ virtual ~ILocomotion();
+
+ virtual void Reset( void ); // (EXTEND) reset to initial state
+ virtual void Update( void ); // (EXTEND) update internal state
+
+ //
+ // The primary locomotive method
+ // Depending on the physics of the bot's motion, it may not actually
+ // reach the given position precisely.
+ // The 'weight' can be used to combine multiple Approach() calls within
+ // a single frame into a single goal (ie: weighted average)
+ //
+ virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position
+
+ //
+ // Move the bot to the precise given position immediately,
+ // updating internal state as needed
+ // Collision resolution is done to prevent interpenetration, which may prevent
+ // the bot from reaching the given position. If no collisions occur, the
+ // bot will be at the given position when this method returns.
+ //
+ virtual void DriveTo( const Vector &pos ); // (EXTEND) Move the bot to the precise given position immediately,
+
+ //
+ // Locomotion modifiers
+ //
+ virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ) { return true; } // initiate a jump to an adjacent high ledge, return false if climb can't start
+ virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ) { } // initiate a jump across an empty volume of space to far side
+ virtual void Jump( void ) { } // initiate a simple undirected jump in the air
+ virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form
+ virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge
+ virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side
+ virtual bool IsScrambling( void ) const; // is in the middle of a complex action (climbing a ladder, climbing a ledge, jumping, etc) that shouldn't be interrupted
+
+ virtual void Run( void ) { } // set desired movement speed to running
+ virtual void Walk( void ) { } // set desired movement speed to walking
+ virtual void Stop( void ) { } // set desired movement speed to stopped
+ virtual bool IsRunning( void ) const;
+ virtual void SetDesiredSpeed( float speed ) { } // set desired speed for locomotor movement
+ virtual float GetDesiredSpeed( void ) const; // returns the current desired speed
+
+ virtual void SetSpeedLimit( float speed ) { } // set maximum speed bot can reach, regardless of desired speed
+ virtual float GetSpeedLimit( void ) const { return 1000.0f; } // get maximum speed bot can reach, regardless of desired speed
+
+ virtual bool IsOnGround( void ) const; // return true if standing on something
+ virtual void OnLeaveGround( CBaseEntity *ground ) { } // invoked when bot leaves ground for any reason
+ virtual void OnLandOnGround( CBaseEntity *ground ) { } // invoked when bot lands on the ground after being in the air
+ virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground
+ virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with
+ virtual float GetGroundSpeed( void ) const; // return current world space speed in XY plane
+ virtual const Vector &GetGroundMotionVector( void ) const; // return unit vector in XY plane describing our direction of motion - even if we are currently not moving
+
+ virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) { } // climb the given ladder to the top and dismount
+ virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) { } // descend the given ladder to the bottom and dismount
+ virtual bool IsUsingLadder( void ) const; // we are moving to get on, ascending/descending, and/or dismounting a ladder
+ virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down
+ virtual bool IsAbleToAutoCenterOnLadder( void ) const { return false; }
+
+ virtual void FaceTowards( const Vector &target ) { } // rotate body to face towards "target"
+
+ virtual void SetDesiredLean( const QAngle &lean ) { }
+ virtual const QAngle &GetDesiredLean( void ) const;
+
+
+ //
+ // Locomotion information
+ //
+ virtual bool IsAbleToJumpAcrossGaps( void ) const; // return true if this bot can jump across gaps in its path
+ virtual bool IsAbleToClimb( void ) const; // return true if this bot can climb arbitrary geometry it encounters
+
+ virtual const Vector &GetFeet( void ) const; // return position of "feet" - the driving point where the bot contacts the ground
+
+ virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up
+ 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 float GetWalkSpeed( void ) const; // get maximum walking speed
+
+ virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor
+ virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor
+
+ virtual const Vector &GetVelocity( void ) const; // return current world space velocity
+ virtual float GetSpeed( void ) const; // return current world space speed (magnitude of velocity)
+ virtual const Vector &GetMotionVector( void ) const; // return unit vector describing our direction of motion - even if we are currently not moving
+
+ virtual bool IsAreaTraversable( const CNavArea *baseArea ) const; // return true if given area can be used for navigation
+
+ virtual float GetTraversableSlopeLimit( void ) const; // return Z component of unit normal of steepest traversable slope
+
+ // return true if the given entity can be ignored during locomotion
+ enum TraverseWhenType
+ {
+ IMMEDIATELY, // the entity will not block our motion - we'll carry right through
+ EVENTUALLY // the entity will block us until we spend effort to open/destroy it
+ };
+
+ /**
+ * Return true if this locomotor could potentially move along the line given.
+ * If false is returned, fraction of walkable ray is returned in 'fraction'
+ */
+ virtual bool IsPotentiallyTraversable( const Vector &from, const Vector &to, TraverseWhenType when = EVENTUALLY, float *fraction = NULL ) const;
+
+ /**
+ * Return true if there is a possible "gap" that will need to be jumped over
+ * If true is returned, fraction of ray before gap is returned in 'fraction'
+ */
+ virtual bool HasPotentialGap( const Vector &from, const Vector &to, float *fraction = NULL ) const;
+
+ // return true if there is a "gap" here when moving in the given direction
+ virtual bool IsGap( const Vector &pos, const Vector &forward ) const;
+
+ virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const;
+
+ //
+ // Stuck state. If the locomotor cannot make progress, it becomes "stuck" and can only leave
+ // this stuck state by successfully moving and becoming un-stuck.
+ //
+ virtual bool IsStuck( void ) const; // return true if bot is stuck
+ virtual float GetStuckDuration( void ) const; // return how long we've been stuck
+ virtual void ClearStuckStatus( const char *reason = "" ); // reset stuck status to un-stuck
+
+ virtual bool IsAttemptingToMove( void ) const; // return true if we have tried to Approach() or DriveTo() very recently
+
+ void TraceHull( const Vector& start, const Vector& end, const Vector &mins, const Vector &maxs, unsigned int fMask, ITraceFilter *pFilter, trace_t *pTrace ) const;
+
+ /**
+ * Should we collide with this entity?
+ */
+ virtual bool ShouldCollideWith( const CBaseEntity *object ) const { return true; }
+
+
+protected:
+ virtual void AdjustPosture( const Vector &moveGoal );
+ virtual void StuckMonitor( void );
+
+private:
+ Vector m_motionVector;
+ Vector m_groundMotionVector;
+ float m_speed;
+ float m_groundSpeed;
+
+ // stuck monitoring
+ bool m_isStuck; // if true, we are stuck
+ IntervalTimer m_stuckTimer; // how long we've been stuck
+ CountdownTimer m_stillStuckTimer; // for resending stuck events
+ Vector m_stuckPos; // where we got stuck
+ IntervalTimer m_moveRequestTimer;
+};
+
+
+inline bool ILocomotion::IsAbleToJumpAcrossGaps( void ) const
+{
+ return true;
+}
+
+inline bool ILocomotion::IsAbleToClimb( void ) const
+{
+ return true;
+}
+
+inline bool ILocomotion::IsAttemptingToMove( void ) const
+{
+ return m_moveRequestTimer.HasStarted() && m_moveRequestTimer.GetElapsedTime() < 0.25f;
+}
+
+inline bool ILocomotion::IsScrambling( void ) const
+{
+ return !IsOnGround() || IsClimbingOrJumping() || IsAscendingOrDescendingLadder();
+}
+
+inline bool ILocomotion::IsClimbingOrJumping( void ) const
+{
+ return false;
+}
+
+inline bool ILocomotion::IsClimbingUpToLedge( void ) const
+{
+ return false;
+}
+
+inline bool ILocomotion::IsJumpingAcrossGap( void ) const
+{
+ return false;
+}
+
+inline bool ILocomotion::IsRunning( void ) const
+{
+ return false;
+}
+
+inline float ILocomotion::GetDesiredSpeed( void ) const
+{
+ return 0.0f;
+}
+
+inline bool ILocomotion::IsOnGround( void ) const
+{
+ return false;
+}
+
+inline CBaseEntity *ILocomotion::GetGround( void ) const
+{
+ return NULL;
+}
+
+inline const Vector &ILocomotion::GetGroundNormal( void ) const
+{
+ return vec3_origin;
+}
+
+inline float ILocomotion::GetGroundSpeed( void ) const
+{
+ return m_groundSpeed;
+}
+
+inline const Vector & ILocomotion::GetGroundMotionVector( void ) const
+{
+ return m_groundMotionVector;
+}
+
+inline bool ILocomotion::IsUsingLadder( void ) const
+{
+ return false;
+}
+
+inline bool ILocomotion::IsAscendingOrDescendingLadder( void ) const
+{
+ return false;
+}
+
+inline const QAngle &ILocomotion::GetDesiredLean( void ) const
+{
+ return vec3_angle;
+}
+
+inline float ILocomotion::GetStepHeight( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetMaxJumpHeight( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetDeathDropHeight( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetRunSpeed( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetWalkSpeed( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetMaxAcceleration( void ) const
+{
+ return 0.0f;
+}
+
+inline float ILocomotion::GetMaxDeceleration( void ) const
+{
+ return 0.0f;
+}
+
+inline const Vector &ILocomotion::GetVelocity( void ) const
+{
+ return vec3_origin;
+}
+
+inline float ILocomotion::GetSpeed( void ) const
+{
+ return m_speed;
+}
+
+inline const Vector & ILocomotion::GetMotionVector( void ) const
+{
+ return m_motionVector;
+}
+
+inline float ILocomotion::GetTraversableSlopeLimit( void ) const
+{
+ return 0.6;
+}
+
+inline bool ILocomotion::IsStuck( void ) const
+{
+ return m_isStuck;
+}
+
+inline float ILocomotion::GetStuckDuration( void ) const
+{
+ return ( IsStuck() ) ? m_stuckTimer.GetElapsedTime() : 0.0f;
+}
+
+inline void ILocomotion::TraceHull( const Vector& start, const Vector& end, const Vector &mins, const Vector &maxs, unsigned int fMask, ITraceFilter *pFilter, trace_t *pTrace ) const
+{
+// VPROF_BUDGET( "ILocomotion::TraceHull", "TraceHull" );
+ Ray_t ray;
+ ray.Init( start, end, mins, maxs );
+ enginetrace->TraceRay( ray, fMask, pFilter, pTrace );
+}
+
+
+
+#endif // _NEXT_BOT_LOCOMOTION_INTERFACE_H_
+
diff --git a/game/server/NextBot/NextBotManager.cpp b/game/server/NextBot/NextBotManager.cpp
new file mode 100644
index 0000000..2ce9c6c
--- /dev/null
+++ b/game/server/NextBot/NextBotManager.cpp
@@ -0,0 +1,892 @@
+// NextBotManager.cpp
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBotManager.h"
+#include "NextBotInterface.h"
+
+#ifdef TERROR
+#include "ZombieBot/Infected/Infected.h"
+#include "ZombieBot/Witch/Witch.h"
+#include "ZombieManager.h"
+#endif
+
+#include "SharedFunctorUtils.h"
+//#include "../../common/blackbox_helper.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+extern ConVar ZombieMobMaxSize;
+
+ConVar nb_update_frequency( "nb_update_frequency", ".1", FCVAR_CHEAT );
+ConVar nb_update_framelimit( "nb_update_framelimit", ( IsDebug() ) ? "30" : "15", FCVAR_CHEAT );
+ConVar nb_update_maxslide( "nb_update_maxslide", "2", FCVAR_CHEAT );
+ConVar nb_update_debug( "nb_update_debug", "0", FCVAR_CHEAT );
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+/**
+ * Singleton accessor.
+ * By returning a reference, we guarantee construction of the
+ * instance before its first use.
+ */
+NextBotManager &TheNextBots( void )
+{
+ if ( NextBotManager::GetInstance() )
+ {
+ return *NextBotManager::GetInstance();
+ }
+ else
+ {
+ static NextBotManager manager;
+ NextBotManager::SetInstance( &manager );
+ return manager;
+ }
+}
+
+NextBotManager* NextBotManager::sInstance = NULL;
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+static const char *debugTypeName[] =
+{
+ "BEHAVIOR",
+ "LOOK_AT",
+ "PATH",
+ "ANIMATION",
+ "LOCOMOTION",
+ "VISION",
+ "HEARING",
+ "EVENTS",
+ "ERRORS",
+ NULL
+};
+
+
+static void CC_SetDebug( const CCommand &args )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Msg( "Debugging stopped\n" );
+ TheNextBots().SetDebugTypes( NEXTBOT_DEBUG_NONE );
+ return;
+ }
+
+ int debugType = 0;
+
+ for( int i=1; i<args.ArgC(); ++i )
+ {
+ int type;
+ for( type = 0; debugTypeName[ type ]; ++type )
+ {
+ const char *token = args[i];
+
+ // special token that means "all"
+ if ( token[0] == '*' )
+ {
+ debugType = NEXTBOT_DEBUG_ALL;
+ break;
+ }
+
+ if ( !Q_strnicmp( args[i], debugTypeName[ type ], Q_strlen( args[1] ) ) )
+ {
+ debugType |= ( 1 << type );
+ break;
+ }
+ }
+
+ if ( !debugTypeName[ type ] )
+ {
+ Msg( "Invalid debug type '%s'\n", args[i] );
+ }
+ }
+
+ // enable debugging
+ TheNextBots().SetDebugTypes( ( NextBotDebugType ) debugType );
+}
+static ConCommand SetDebug( "nb_debug", CC_SetDebug, "Debug NextBots. Categories are: BEHAVIOR, LOOK_AT, PATH, ANIMATION, LOCOMOTION, VISION, HEARING, EVENTS, ERRORS.", FCVAR_CHEAT );
+
+//---------------------------------------------------------------------------------------------
+static void CC_SetDebugFilter( const CCommand &args )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Msg( "Debug filter cleared.\n" );
+ TheNextBots().DebugFilterClear();
+ return;
+ }
+
+ for( int i=1; i<args.ArgC(); ++i )
+ {
+ int index = Q_atoi( args[i] );
+ if ( index > 0 )
+ {
+ TheNextBots().DebugFilterAdd( index );
+ }
+ else
+ {
+ TheNextBots().DebugFilterAdd( args[i] );
+ }
+ }
+}
+static ConCommand SetDebugFilter( "nb_debug_filter", CC_SetDebugFilter, "Add items to the NextBot debug filter. Items can be entindexes or part of the indentifier of one or more bots.", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+class Selector
+{
+public:
+ Selector( CBasePlayer *player, bool useLOS )
+ {
+ m_player = player;
+ player->EyeVectors( &m_forward );
+
+ m_pick = NULL;
+ m_pickRange = 99999999999999.9f;
+ m_useLOS = useLOS;
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ CBaseCombatCharacter *botEntity = bot->GetEntity();
+ if ( botEntity->IsAlive() )
+ {
+ Vector to = botEntity->WorldSpaceCenter() - m_player->EyePosition();
+ float range = to.NormalizeInPlace();
+
+ if ( DotProduct( m_forward, to ) > 0.98f && range < m_pickRange )
+ {
+ if ( !m_useLOS || m_player->IsAbleToSee( botEntity, CBaseCombatCharacter::DISREGARD_FOV ) )
+ {
+ m_pick = bot;
+ m_pickRange = range;
+ }
+ }
+ }
+ return true;
+ }
+
+ CBasePlayer *m_player;
+ Vector m_forward;
+ INextBot *m_pick;
+ float m_pickRange;
+ bool m_useLOS;
+};
+
+static void CC_SelectBot( const CCommand &args )
+{
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ if ( player )
+ {
+ Selector select( player, false );
+ TheNextBots().ForEachBot( select );
+
+ TheNextBots().Select( select.m_pick );
+
+ if ( select.m_pick )
+ {
+ NDebugOverlay::Circle( select.m_pick->GetLocomotionInterface()->GetFeet() + Vector( 0, 0, 5 ), Vector( 1, 0, 0 ), Vector( 0, -1, 0 ), 25.0f, 0, 255, 0, 255, false, 1.0f );
+ }
+ }
+}
+static ConCommand SelectBot( "nb_select", CC_SelectBot, "Select the bot you are aiming at for further debug operations.", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+static void CC_ForceLookAt( const CCommand &args )
+{
+ CBasePlayer *player = UTIL_GetListenServerHost();
+ INextBot *pick = TheNextBots().GetSelected();
+
+ if ( player && pick )
+ {
+ pick->GetBodyInterface()->AimHeadTowards( player, IBody::CRITICAL, 9999999.9f, NULL, "Aim forced" );
+ }
+}
+static ConCommand ForceLookAt( "nb_force_look_at", CC_ForceLookAt, "Force selected bot to look at the local player's position", FCVAR_CHEAT );
+
+
+//--------------------------------------------------------------------------------------------------------
+void CC_WarpSelectedHere( const CCommand &args )
+{
+ CBasePlayer *me = dynamic_cast< CBasePlayer * >( UTIL_GetCommandClient() );
+ INextBot *pick = TheNextBots().GetSelected();
+
+ if ( me == NULL || pick == NULL )
+ {
+ return;
+ }
+
+ Vector forward;
+ me->EyeVectors( &forward );
+
+ trace_t result;
+ UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 999999.9f * forward, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, me, COLLISION_GROUP_NONE, &result );
+ if ( result.DidHit() )
+ {
+ Vector spot = result.endpos + Vector( 0, 0, 10.0f );
+ pick->GetEntity()->Teleport( &spot, &vec3_angle, &vec3_origin );
+ }
+}
+static ConCommand WarpSelectedHere( "nb_warp_selected_here", CC_WarpSelectedHere, "Teleport the selected bot to your cursor position", FCVAR_CHEAT );
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+NextBotManager::NextBotManager( void )
+{
+ m_debugType = 0;
+ m_selectedBot = NULL;
+
+ m_iUpdateTickrate = 0;
+}
+
+//---------------------------------------------------------------------------------------------
+NextBotManager::~NextBotManager()
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Reset to initial state
+ */
+void NextBotManager::Reset( void )
+{
+ // remove the NextBots that should go away during a reset (they will unregister themselves as they go)
+ int i = m_botList.Head();
+ while ( i != m_botList.InvalidIndex() )
+ {
+ int iNext = m_botList.Next( i );
+ if ( m_botList[i]->IsRemovedOnReset() )
+ {
+ UTIL_Remove( m_botList[i]->GetEntity() );
+ //Assert( !m_botList.IsInList( i ) ); // UTIL_Remove() calls UpdateOnRemove, adds EFL_KILLME, but doesn't delete until the end of the frame
+ }
+ i = iNext;
+ }
+
+ m_selectedBot = NULL;
+}
+
+
+//---------------------------------------------------------------------------------------------
+
+inline bool IsDead( INextBot *pBot )
+{
+ CBaseCombatCharacter *pEntity = pBot->GetEntity();
+ if ( pEntity )
+ {
+ if ( pEntity->IsPlayer() && pEntity->m_lifeState == LIFE_DEAD )
+ {
+ return true;
+ }
+
+ if ( pEntity->IsMarkedForDeletion() )
+ {
+ return true;
+ }
+
+ if ( pEntity->m_pfnThink == &CBaseEntity::SUB_Remove )
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+//---------------------------------------------------------------------------------------------
+
+// Debug stats for update balancing
+static int g_nRun;
+static int g_nSlid;
+static int g_nBlockedSlides;
+
+void NextBotManager::Update( void )
+{
+ // do lightweight upkeep every tick
+ for( int u=m_botList.Head(); u != m_botList.InvalidIndex(); u = m_botList.Next( u ) )
+ {
+ m_botList[ u ]->Upkeep();
+ }
+
+ // schedule full updates
+ if ( m_botList.Count() )
+ {
+ static int iCurFrame = -1;
+ if ( iCurFrame != gpGlobals->framecount )
+ {
+ iCurFrame = gpGlobals->framecount;
+ m_SumFrameTime = 0;
+ }
+ else
+ {
+ // Don't run multiple ticks in a frame
+ return;
+ }
+
+ int tickRate = TIME_TO_TICKS( nb_update_frequency.GetFloat() );
+ if ( tickRate < 0 )
+ {
+ tickRate = 0;
+ }
+
+ if ( m_iUpdateTickrate != tickRate )
+ {
+ Msg( "NextBot tickrate changed from %d (%.3fms) to %d (%.3fms)\n", m_iUpdateTickrate, TICKS_TO_TIME( m_iUpdateTickrate ), tickRate, TICKS_TO_TIME( tickRate ) );
+ m_iUpdateTickrate = tickRate;
+ }
+
+ int i = 0;
+ int nScheduled = 0;
+ int nNonResponsive = 0;
+ int nDead = 0;
+ if ( m_iUpdateTickrate > 0 )
+ {
+ INextBot *pBot;
+
+ // Count dead bots, they won't update and balancing calculations should exclude them
+ for( i = m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ if ( IsDead( m_botList[i] ) )
+ {
+ nDead++;
+ }
+ }
+
+
+ int nTargetToRun = ceilf( (float)( m_botList.Count() - nDead ) / (float)m_iUpdateTickrate );
+ int curtickcount = gpGlobals->tickcount;
+
+ for( i = m_botList.Head(); nTargetToRun && i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ pBot = m_botList[i];
+ if ( pBot->IsFlaggedForUpdate() )
+ {
+ // Was offered a run last tick but didn't take it, push it back
+ // Leave the flag set so that bot will run right away later, but be ignored
+ // until then
+ nNonResponsive++;
+ }
+ else
+ {
+ if ( curtickcount - pBot->GetTickLastUpdate() < m_iUpdateTickrate )
+ {
+ break;
+ }
+ if ( !IsDead( pBot ) )
+ {
+ pBot->FlagForUpdate();
+ nTargetToRun--;
+ nScheduled++;
+ }
+ }
+ }
+ }
+ else
+ {
+ nScheduled = m_botList.Count();
+ }
+
+ if ( nb_update_debug.GetBool() )
+ {
+ int nIntentionalSliders = 0;
+ if ( m_iUpdateTickrate > 0 )
+ {
+ for( ; i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ if ( gpGlobals->tickcount - m_botList[i]->GetTickLastUpdate() >= m_iUpdateTickrate )
+ {
+ nIntentionalSliders++;
+ }
+ }
+ }
+
+ Msg( "Frame %8d/tick %8d: %3d run of %3d, %3d sliders, %3d blocked slides, scheduled %3d for next tick, %3d intentional sliders, %d nonresponsive, %d dead\n", gpGlobals->framecount - 1, gpGlobals->tickcount - 1, g_nRun, m_botList.Count() - nDead, g_nSlid, g_nBlockedSlides, nScheduled, nIntentionalSliders, nNonResponsive, nDead );
+ g_nRun = g_nSlid = g_nBlockedSlides = 0;
+ }
+
+ }
+}
+
+//---------------------------------------------------------------------------------------------
+bool NextBotManager::ShouldUpdate( INextBot *bot )
+{
+ if ( m_iUpdateTickrate < 1 )
+ {
+ return true;
+ }
+
+ float frameLimit = nb_update_framelimit.GetFloat();
+ float sumFrameTime = 0;
+ if ( bot->IsFlaggedForUpdate() )
+ {
+ bot->FlagForUpdate( false );
+ sumFrameTime = m_SumFrameTime * 1000.0;
+ if ( frameLimit > 0.0f )
+ {
+ if ( sumFrameTime < frameLimit )
+ {
+ return true;
+ }
+ else if ( nb_update_debug.GetBool() )
+ {
+ Msg( "Frame %8d/tick %8d: frame out of budget (%.2fms > %.2fms)\n", gpGlobals->framecount, gpGlobals->tickcount, sumFrameTime, frameLimit );
+ }
+ }
+ }
+
+ int nTicksSlid = ( gpGlobals->tickcount - bot->GetTickLastUpdate() ) - m_iUpdateTickrate;
+
+ if ( nTicksSlid >= nb_update_maxslide.GetInt() )
+ {
+ if ( frameLimit == 0.0 || sumFrameTime < nb_update_framelimit.GetFloat() * 2.0 )
+ {
+ g_nBlockedSlides++;
+ return true;
+ }
+ }
+
+ if ( nb_update_debug.GetBool() )
+ {
+ if ( nTicksSlid > 0 )
+ {
+ g_nSlid++;
+ }
+ }
+
+ return false;
+}
+
+//---------------------------------------------------------------------------------------------
+void NextBotManager::NotifyBeginUpdate( INextBot *bot )
+{
+ if ( nb_update_debug.GetBool() )
+ {
+ g_nRun++;
+ }
+
+ m_botList.Unlink( bot->GetBotId() );
+ m_botList.LinkToTail( bot->GetBotId() );
+ bot->SetTickLastUpdate( gpGlobals->tickcount );
+
+ m_CurUpdateStartTime = Plat_FloatTime();
+}
+
+//---------------------------------------------------------------------------------------------
+void NextBotManager::NotifyEndUpdate( INextBot *bot )
+{
+ // This might be a good place to detect a particular bot had spiked [3/14/2008 tom]
+ m_SumFrameTime += Plat_FloatTime() - m_CurUpdateStartTime;
+}
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When the server has changed maps
+ */
+void NextBotManager::OnMapLoaded( void )
+{
+ Reset();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When the scenario restarts
+ */
+void NextBotManager::OnRoundRestart( void )
+{
+ Reset();
+}
+
+
+//---------------------------------------------------------------------------------------------
+int NextBotManager::Register( INextBot *bot )
+{
+ return m_botList.AddToHead( bot );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void NextBotManager::UnRegister( INextBot *bot )
+{
+ m_botList.Remove( bot->GetBotId() );
+
+ if ( bot == m_selectedBot)
+ {
+ // we can't access virtual methods because this is called from a destructor, so just clear it
+ m_selectedBot = NULL;
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+void NextBotManager::OnBeginChangeLevel( void )
+{
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+class NextBotKilledNotifyScan
+{
+public:
+ NextBotKilledNotifyScan( CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+ {
+ m_victim = victim;
+ m_info = info;
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ if ( bot->GetEntity()->IsAlive() && !bot->IsSelf( m_victim ) )
+ {
+ bot->OnOtherKilled( m_victim, m_info );
+ }
+ return true;
+ }
+
+ CBaseCombatCharacter *m_victim;
+ CTakeDamageInfo m_info;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When an actor is killed. Propagate to all NextBots.
+ */
+void NextBotManager::OnKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
+{
+ NextBotKilledNotifyScan notify( victim, info );
+ TheNextBots().ForEachBot( notify );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+class NextBotSoundNotifyScan
+{
+public:
+ NextBotSoundNotifyScan( CBaseEntity *source, const Vector &pos, KeyValues *keys ) : m_source( source ), m_pos( pos ), m_keys( keys )
+ {
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ if ( bot->GetEntity()->IsAlive() && !bot->IsSelf( m_source ) )
+ {
+ bot->OnSound( m_source, m_pos, m_keys );
+ }
+ return true;
+ }
+
+ CBaseEntity *m_source;
+ const Vector &m_pos;
+ KeyValues *m_keys;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When an entity emits a sound
+ */
+void NextBotManager::OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys )
+{
+ NextBotSoundNotifyScan notify( source, pos, keys );
+ TheNextBots().ForEachBot( notify );
+
+ if ( source && IsDebugging( NEXTBOT_HEARING ) )
+ {
+ int r,g,b;
+ switch( source->GetTeamNumber() )
+ {
+ case FIRST_GAME_TEAM: r = 0; g = 255; b = 0; break;
+ case (FIRST_GAME_TEAM+1): r = 255; g = 0; b = 0; break;
+ default: r = 255; g = 255; b = 0; break;
+ }
+ NDebugOverlay::Circle( pos, Vector( 1, 0, 0 ), Vector( 0, -1, 0 ), 5.0f, r, g, b, 255, true, 3.0f );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+class NextBotResponseNotifyScan
+{
+public:
+ NextBotResponseNotifyScan( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) : m_who( who ), m_concept( concept ), m_response( response )
+ {
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ if ( bot->GetEntity()->IsAlive() )
+ {
+ bot->OnSpokeConcept( m_who, m_concept, m_response );
+ }
+ return true;
+ }
+
+ CBaseCombatCharacter *m_who;
+ AIConcept_t m_concept;
+ AI_Response *m_response;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When an Actor speaks a concept
+ */
+void NextBotManager::OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response )
+{
+ NextBotResponseNotifyScan notify( who, concept, response );
+ TheNextBots().ForEachBot( notify );
+
+ if ( IsDebugging( NEXTBOT_HEARING ) )
+ {
+ // const char *who = response->GetCriteria()->GetValue( response->GetCriteria()->FindCriterionIndex( "Who" ) );
+
+ // TODO: Need concept.GetStringConcept()
+ DevMsg( "%3.2f: OnSpokeConcept( %s, %s )\n", gpGlobals->curtime, who->GetDebugName(), "concept.GetStringConcept()" );
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+class NextBotWeaponFiredNotifyScan
+{
+public:
+ NextBotWeaponFiredNotifyScan( CBaseCombatCharacter *who, CBaseCombatWeapon *weapon ) : m_who( who ), m_weapon( weapon )
+ {
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ if ( bot->GetEntity()->IsAlive() )
+ {
+ bot->OnWeaponFired( m_who, m_weapon );
+ }
+ return true;
+ }
+
+ CBaseCombatCharacter *m_who;
+ CBaseCombatWeapon *m_weapon;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * When someone fires a weapon
+ */
+void NextBotManager::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon )
+{
+ NextBotWeaponFiredNotifyScan notify( whoFired, weapon );
+ TheNextBots().ForEachBot( notify );
+
+ if ( IsDebugging( NEXTBOT_EVENTS ) )
+ {
+ DevMsg( "%3.2f: OnWeaponFired( %s, %s )\n", gpGlobals->curtime, whoFired->GetDebugName(), weapon->GetName() );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Add given entindex to the debug filter
+ */
+void NextBotManager::DebugFilterAdd( int index )
+{
+ DebugFilter filter;
+
+ filter.index = index;
+ filter.name[0] = '\000';
+
+ m_debugFilterList.AddToTail( filter );
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Add given name to the debug filter
+ */
+void NextBotManager::DebugFilterAdd( const char *name )
+{
+ DebugFilter filter;
+
+ filter.index = -1;
+ Q_strncpy( filter.name, name, DebugFilter::MAX_DEBUG_NAME_SIZE );
+
+ m_debugFilterList.AddToTail( filter );
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Remove given entindex from the debug filter
+ */
+void NextBotManager::DebugFilterRemove( int index )
+{
+ for( int i=0; i<m_debugFilterList.Count(); ++i )
+ {
+ if ( m_debugFilterList[i].index == index )
+ {
+ m_debugFilterList.Remove( i );
+ break;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Remove given name from the debug filter
+ */
+void NextBotManager::DebugFilterRemove( const char *name )
+{
+ for( int i=0; i<m_debugFilterList.Count(); ++i )
+ {
+ if ( m_debugFilterList[i].name[0] != '\000' &&
+ !Q_strnicmp( name, m_debugFilterList[i].name, MIN( Q_strlen( name ), sizeof( m_debugFilterList[i].name ) ) ) )
+ {
+ m_debugFilterList.Remove( i );
+ break;
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Clear the debug filter (remove all entries)
+ */
+void NextBotManager::DebugFilterClear( void )
+{
+ m_debugFilterList.RemoveAll();
+}
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Return true if the given bot matches the debug filter
+ */
+bool NextBotManager::IsDebugFilterMatch( const INextBot *bot ) const
+{
+ // if the filter is empty, all bots match
+ if ( m_debugFilterList.Count() == 0 )
+ {
+ return true;
+ }
+
+ for( int i=0; i<m_debugFilterList.Count(); ++i )
+ {
+ // compare entity index
+ if ( m_debugFilterList[i].index == const_cast< INextBot * >( bot )->GetEntity()->entindex() )
+ {
+ return true;
+ }
+
+ // compare debug filter
+ if ( m_debugFilterList[i].name[0] != '\000' && bot->IsDebugFilterMatch( m_debugFilterList[i].name ) )
+ {
+ return true;
+ }
+
+ // compare special keyword meaning local player is looking at them
+ if ( !Q_strnicmp( m_debugFilterList[i].name, "lookat", Q_strlen( m_debugFilterList[i].name ) ) )
+ {
+ CBasePlayer *watcher = UTIL_GetListenServerHost();
+ if ( watcher )
+ {
+ CBaseEntity *subject = watcher->GetObserverTarget();
+
+ if ( subject && bot->IsSelf( subject ) )
+ {
+ return true;
+ }
+ }
+ }
+
+ // compare special keyword meaning NextBot is selected
+ if ( !Q_strnicmp( m_debugFilterList[i].name, "selected", Q_strlen( m_debugFilterList[i].name ) ) )
+ {
+ INextBot *selected = GetSelected();
+ if ( selected && bot->IsSelf( selected->GetEntity() ) )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Get the bot under the given player's crosshair
+ */
+INextBot *NextBotManager::GetBotUnderCrosshair( CBasePlayer *picker )
+{
+ if ( !picker )
+ return NULL;
+
+ const float MaxDot = 0.7f;
+ const float MaxRange = 4000.0f;
+ TargetScan< CBaseCombatCharacter > scan( picker, TEAM_ANY, 1.0f - MaxDot, MaxRange );
+ ForEachCombatCharacter( scan );
+ CBaseCombatCharacter *target = scan.GetTarget();
+ if ( target && target->MyNextBotPointer() )
+ return target->MyNextBotPointer();
+
+ return NULL;
+}
+
+#ifdef NEED_BLACK_BOX
+//---------------------------------------------------------------------------------------------
+CON_COMMAND( nb_dump_debug_history, "Dumps debug history for the bot under the cursor to the blackbox" )
+{
+ if ( !NextBotDebugHistory.GetBool() )
+ {
+ BlackBox_Record( "bot", "nb_debug_history 0" );
+ return;
+ }
+
+ CBasePlayer *player = UTIL_GetCommandClient();
+ if ( !player )
+ {
+ player = UTIL_GetListenServerHost();
+ }
+ INextBot *bot = TheNextBots().GetBotUnderCrosshair( player );
+ if ( !bot )
+ {
+ BlackBox_Record( "bot", "no bot under crosshairs" );
+ return;
+ }
+
+ CUtlVector< const INextBot::NextBotDebugLineType * > lines;
+ bot->GetDebugHistory( (NEXTBOT_DEBUG_ALL & (~NEXTBOT_EVENTS)), &lines );
+
+ for ( int i=0; i<lines.Count(); ++i )
+ {
+ if ( IsPC() )
+ {
+ BlackBox_Record( "bot", "%s", lines[i]->data );
+ }
+ }
+}
+#endif // NEED_BLACK_BOX
+
+
+//---------------------------------------------------------------------------------------------
+void NextBotManager::CollectAllBots( CUtlVector< INextBot * > *botVector )
+{
+ if ( !botVector )
+ return;
+
+ botVector->RemoveAll();
+
+ for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ botVector->AddToTail( m_botList[i] );
+ }
+}
+
diff --git a/game/server/NextBot/NextBotManager.h b/game/server/NextBot/NextBotManager.h
new file mode 100644
index 0000000..197b2ad
--- /dev/null
+++ b/game/server/NextBot/NextBotManager.h
@@ -0,0 +1,211 @@
+// NextBotManager.h
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_MANAGER_H_
+#define _NEXT_BOT_MANAGER_H_
+
+#include "NextBotInterface.h"
+
+class CTerrorPlayer;
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The NextBotManager manager
+ */
+class NextBotManager
+{
+public:
+ NextBotManager( void );
+ virtual ~NextBotManager();
+
+ void Reset( void ); // reset to initial state
+ virtual void Update( void );
+
+ bool ShouldUpdate( INextBot *bot );
+ void NotifyBeginUpdate( INextBot *bot );
+ void NotifyEndUpdate( INextBot *bot );
+
+ int GetNextBotCount( void ) const; // How many nextbots are alive right now?
+
+
+ /**
+ * Populate given vector with all bots in the system
+ */
+ void CollectAllBots( CUtlVector< INextBot * > *botVector );
+
+
+ /**
+ * DEPRECATED: Use CollectAllBots().
+ * Execute functor for each NextBot in the system.
+ * If a functor returns false, stop iteration early
+ * and return false.
+ */
+ template < typename Functor >
+ bool ForEachBot( Functor &func )
+ {
+ for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ if ( !func( m_botList[i] ) )
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * DEPRECATED: Use CollectAllBots().
+ * Execute functor for each NextBot in the system as
+ * a CBaseCombatCharacter.
+ * If a functor returns false, stop iteration early
+ * and return false.
+ */
+ template < typename Functor >
+ bool ForEachCombatCharacter( Functor &func )
+ {
+ for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ if ( !func( m_botList[i]->GetEntity() ) )
+ {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Return closest bot to given point that passes the given filter
+ */
+ template < typename Filter >
+ INextBot *GetClosestBot( const Vector &pos, Filter &filter )
+ {
+ INextBot *close = NULL;
+ float closeRangeSq = FLT_MAX;
+
+ for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) )
+ {
+ float rangeSq = ( m_botList[i]->GetEntity()->GetAbsOrigin() - pos ).LengthSqr();
+ if ( rangeSq < closeRangeSq && filter( m_botList[i] ) )
+ {
+ closeRangeSq = rangeSq;
+ close = m_botList[i];
+ }
+ }
+
+ return close;
+ }
+
+ /**
+ * Event propagators
+ */
+ virtual void OnMapLoaded( void ); // when the server has changed maps
+ virtual void OnRoundRestart( void ); // when the scenario restarts
+ virtual void OnBeginChangeLevel( void ); // when the server is about to change maps
+ virtual void OnKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); // when an actor is killed
+ virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ); // when an entity emits a sound
+ virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ); // when an Actor speaks a concept
+ virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires a weapon
+
+ /**
+ * Debugging
+ */
+ bool IsDebugging( unsigned int type ) const; // return true if debugging system is on for the given type(s)
+ void SetDebugTypes( NextBotDebugType type ); // start displaying debug info of the given type(s)
+
+ void DebugFilterAdd( int index ); // add given entindex to the debug filter
+ void DebugFilterAdd( const char *name ); // add given name to the debug filter
+ void DebugFilterRemove( int index ); // remove given entindex from the debug filter
+ void DebugFilterRemove( const char *name ); // remove given name from the debug filter
+ void DebugFilterClear( void ); // clear the debug filter (remove all entries)
+ bool IsDebugFilterMatch( const INextBot *bot ) const; // return true if the given bot matches the debug filter
+
+ void Select( INextBot *bot ); // mark bot as selected for further operations
+ void DeselectAll( void );
+ INextBot *GetSelected( void ) const;
+
+ INextBot *GetBotUnderCrosshair( CBasePlayer *picker ); // Get the bot under the given player's crosshair
+
+ //
+ // Put these in a derived class
+ //
+ void OnSurvivorVomitedUpon( CTerrorPlayer *victim ); // when a Survivor has been hit by Boomer Vomit
+
+ static void SetInstance( NextBotManager *pInstance ) { sInstance = pInstance; };
+ static NextBotManager* GetInstance() { return sInstance; }
+
+protected:
+ static NextBotManager* sInstance;
+
+ friend class INextBot;
+
+ int Register( INextBot *bot );
+ void UnRegister( INextBot *bot );
+
+ CUtlLinkedList< INextBot * > m_botList; // list of all active NextBots
+
+ int m_iUpdateTickrate;
+ double m_CurUpdateStartTime;
+ double m_SumFrameTime;
+
+ unsigned int m_debugType; // debug flags
+
+ struct DebugFilter
+ {
+ int index; // entindex
+ enum { MAX_DEBUG_NAME_SIZE = 128 };
+ char name[ MAX_DEBUG_NAME_SIZE ];
+ };
+ CUtlVector< DebugFilter > m_debugFilterList;
+
+ INextBot *m_selectedBot; // selected bot for further debug operations
+};
+
+inline int NextBotManager::GetNextBotCount( void ) const
+{
+ return m_botList.Count();
+}
+
+inline bool NextBotManager::IsDebugging( unsigned int type ) const
+{
+ if ( type & m_debugType )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+
+inline void NextBotManager::SetDebugTypes( NextBotDebugType type )
+{
+ m_debugType = (unsigned int)type;
+}
+
+
+inline void NextBotManager::Select( INextBot *bot )
+{
+ m_selectedBot = bot;
+}
+
+inline void NextBotManager::DeselectAll( void )
+{
+ m_selectedBot = NULL;
+}
+
+inline INextBot *NextBotManager::GetSelected( void ) const
+{
+ return m_selectedBot;
+}
+
+
+
+// singleton accessor
+extern NextBotManager &TheNextBots( void );
+
+
+#endif // _NEXT_BOT_MANAGER_H_
+
diff --git a/game/server/NextBot/NextBotUtil.h b/game/server/NextBot/NextBotUtil.h
new file mode 100644
index 0000000..75a4221
--- /dev/null
+++ b/game/server/NextBot/NextBotUtil.h
@@ -0,0 +1,278 @@
+// NextBotUtil.h
+// Utilities for the NextBot system
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_UTIL_H_
+#define _NEXT_BOT_UTIL_H_
+
+#include "NextBotLocomotionInterface.h"
+#include "nav_area.h"
+#include "nav_mesh.h"
+#include "nav_pathfind.h"
+
+//--------------------------------------------------------------------------------------------
+/**
+ * A simple filter interface for various NextBot queries
+ */
+class INextBotEntityFilter
+{
+public:
+ // return true if the given entity passes this filter
+ virtual bool IsAllowed( CBaseEntity *entity ) const = 0;
+};
+
+
+// trace filter callback functions. needed for use with the querycache/optimization functionality
+bool VisionTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask );
+bool IgnoreActorsTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask );
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that skips all players and NextBots
+ */
+class NextBotTraceFilterIgnoreActors : public CTraceFilterSimple
+{
+public:
+ NextBotTraceFilterIgnoreActors( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup, IgnoreActorsTraceFilterFunction )
+ {
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that skips all players, NextBots, and non-LOS blockers
+ */
+class NextBotVisionTraceFilter : public CTraceFilterSimple
+{
+public:
+ NextBotVisionTraceFilter( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup, VisionTraceFilterFunction )
+ {
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that skips all NextBots, but includes Players
+ */
+class NextBotTraceFilterIgnoreNextBots : public CTraceFilterSimple
+{
+public:
+ NextBotTraceFilterIgnoreNextBots( const IHandleEntity *passentity, int collisionGroup )
+ : CTraceFilterSimple( passentity, collisionGroup )
+ {
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+#ifdef TERROR
+ CBasePlayer *player = ToBasePlayer( entity );
+ if ( player && player->IsGhost() )
+ return false;
+#endif // TERROR
+
+ return ( entity->MyNextBotPointer() == NULL );
+ }
+ return false;
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that obeys INextBot::IsAbleToBlockMovementOf()
+ */
+class NextBotTraceFilter : public CTraceFilterSimple
+{
+public:
+ NextBotTraceFilter( const IHandleEntity *passentity, int collisionGroup )
+ : CTraceFilterSimple( passentity, collisionGroup )
+ {
+ CBaseEntity *entity = const_cast<CBaseEntity *>(EntityFromEntityHandle( passentity ));
+ m_passBot = entity->MyNextBotPointer();
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+#ifdef TERROR
+ CBasePlayer *player = ToBasePlayer( entity );
+ if ( player && player->IsGhost() )
+ return false;
+#endif // TERROR
+
+ // Skip players on the same team - they're not solid to us, and we'll avoid them
+ if ( entity->IsPlayer() && m_passBot && m_passBot->GetEntity() &&
+ m_passBot->GetEntity()->GetTeamNumber() == entity->GetTeamNumber() )
+ return false;
+
+ INextBot *bot = entity->MyNextBotPointer();
+
+ return ( !bot || bot->IsAbleToBlockMovementOf( m_passBot ) );
+ }
+ return false;
+ }
+
+ const INextBot *m_passBot;
+};
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that only hits players and NextBots
+ */
+class NextBotTraceFilterOnlyActors : public CTraceFilterSimple
+{
+public:
+ NextBotTraceFilterOnlyActors( const IHandleEntity *passentity, int collisionGroup )
+ : CTraceFilterSimple( passentity, collisionGroup )
+ {
+ }
+
+ virtual TraceType_t GetTraceType() const
+ {
+ return TRACE_ENTITIES_ONLY;
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+
+#ifdef TERROR
+ CBasePlayer *player = ToBasePlayer( entity );
+ if ( player && player->IsGhost() )
+ return false;
+#endif // TERROR
+
+ return ( entity->MyNextBotPointer() || entity->IsPlayer() );
+ }
+ return false;
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that skips "traversable" entities. The "when" argument creates
+ * a temporal context for asking if an entity is IMMEDIATELY traversable (like thin
+ * glass that just breaks as we walk through it) or EVENTUALLY traversable (like a
+ * breakable object that will take some time to break through)
+ */
+class NextBotTraversableTraceFilter : public CTraceFilterSimple
+{
+public:
+ NextBotTraversableTraceFilter( INextBot *bot, ILocomotion::TraverseWhenType when = ILocomotion::EVENTUALLY ) : CTraceFilterSimple( bot->GetEntity(), COLLISION_GROUP_NONE )
+ {
+ m_bot = bot;
+ m_when = when;
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+
+ if ( m_bot->IsSelf( entity ) )
+ {
+ return false;
+ }
+
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ return !m_bot->GetLocomotionInterface()->IsEntityTraversable( entity, m_when );
+ }
+
+ return false;
+ }
+
+private:
+ INextBot *m_bot;
+ ILocomotion::TraverseWhenType m_when;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Given a vector of entities, a nav area, and a max travel distance, return
+ * the entity that has the shortest travel distance.
+ */
+inline CBaseEntity *SelectClosestEntityByTravelDistance( INextBot *me, const CUtlVector< CBaseEntity * > &candidateEntities, CNavArea *startArea, float travelRange )
+{
+ // collect nearby walkable areas within travelRange
+ CUtlVector< CNavArea * > nearbyAreaVector;
+ CollectSurroundingAreas( &nearbyAreaVector, startArea, travelRange, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetDeathDropHeight() );
+
+ // find closest entity in the collected area set
+ CBaseEntity *closeEntity = NULL;
+ float closeTravelRange = FLT_MAX;
+
+ for( int i=0; i<candidateEntities.Count(); ++i )
+ {
+ CBaseEntity *candidate = candidateEntities[i];
+
+ CNavArea *area = TheNavMesh->GetNearestNavArea( candidate, GETNAVAREA_CHECK_LOS, 500.0f );
+
+ if ( area && area->IsMarked() && area->GetCostSoFar() < closeTravelRange )
+ {
+ closeEntity = candidate;
+ closeTravelRange = area->GetCostSoFar();
+ }
+ }
+
+ return closeEntity;
+}
+
+
+#ifdef OBSOLETE
+//--------------------------------------------------------------------------------------------
+/**
+ * Trace filter that skips "traversable" entities, but hits other Actors.
+ * Used for obstacle avoidance.
+ */
+class NextBotMovementAvoidanceTraceFilter : public CTraceFilterSimple
+{
+public:
+ NextBotMovementAvoidanceTraceFilter( INextBot *bot ) : CTraceFilterSimple( bot->GetEntity(), COLLISION_GROUP_NONE )
+ {
+ m_bot = bot;
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ CBaseEntity *entity = EntityFromEntityHandle( pServerEntity );
+
+#ifdef TERROR
+ CBasePlayer *player = ToBasePlayer( entity );
+ if ( player && player->IsGhost() )
+ return false;
+#endif // TERROR
+
+ if ( m_bot->IsSelf( entity ) )
+ {
+ return false;
+ }
+
+ if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) )
+ {
+ return !m_bot->GetLocomotionInterface()->IsEntityTraversable( entity, ILocomotion::IMMEDIATELY );
+ }
+
+ return false;
+ }
+
+private:
+ INextBot *m_bot;
+};
+#endif
+
+
+#endif // _NEXT_BOT_UTIL_H_
diff --git a/game/server/NextBot/NextBotVisionInterface.cpp b/game/server/NextBot/NextBotVisionInterface.cpp
new file mode 100644
index 0000000..1c9d661
--- /dev/null
+++ b/game/server/NextBot/NextBotVisionInterface.cpp
@@ -0,0 +1,802 @@
+// NextBotVisionInterface.cpp
+// Implementation of common vision system
+// Author: Michael Booth, May 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "nav.h"
+#include "functorutils.h"
+
+#include "NextBot.h"
+#include "NextBotVisionInterface.h"
+#include "NextBotBodyInterface.h"
+#include "NextBotUtil.h"
+
+#ifdef TERROR
+#include "querycache.h"
+#endif
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+ConVar nb_blind( "nb_blind", "0", FCVAR_CHEAT, "Disable vision" );
+ConVar nb_debug_known_entities( "nb_debug_known_entities", "0", FCVAR_CHEAT, "Show the 'known entities' for the bot that is the current spectator target" );
+
+
+//------------------------------------------------------------------------------------------
+IVision::IVision( INextBot *bot ) : INextBotComponent( bot )
+{
+ Reset();
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Reset to initial state
+ */
+void IVision::Reset( void )
+{
+ INextBotComponent::Reset();
+
+ m_knownEntityVector.RemoveAll();
+ m_lastVisionUpdateTimestamp = 0.0f;
+ m_primaryThreat = NULL;
+
+ m_FOV = GetDefaultFieldOfView();
+ m_cosHalfFOV = cos( 0.5f * m_FOV * M_PI / 180.0f );
+
+ for( int i=0; i<MAX_TEAMS; ++i )
+ {
+ m_notVisibleTimer[i].Invalidate();
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Ask the current behavior to select the most dangerous threat from
+ * our set of currently known entities
+ * TODO: Find a semantically better place for this to live.
+ */
+const CKnownEntity *IVision::GetPrimaryKnownThreat( bool onlyVisibleThreats ) const
+{
+ if ( m_knownEntityVector.Count() == 0 )
+ return NULL;
+
+ const CKnownEntity *threat = NULL;
+ int i;
+
+ // find the first valid entity
+ for( i=0; i<m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &firstThreat = m_knownEntityVector[i];
+
+ // check in case status changes between updates
+ if ( IsAwareOf( firstThreat ) && !firstThreat.IsObsolete() && !IsIgnored( firstThreat.GetEntity() ) && GetBot()->IsEnemy( firstThreat.GetEntity() ) )
+ {
+ if ( !onlyVisibleThreats || firstThreat.IsVisibleRecently() )
+ {
+ threat = &firstThreat;
+ break;
+ }
+ }
+ }
+
+ if ( threat == NULL )
+ {
+ m_primaryThreat = NULL;
+ return NULL;
+ }
+
+ for( ++i; i<m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &newThreat = m_knownEntityVector[i];
+
+ // check in case status changes between updates
+ if ( IsAwareOf( newThreat ) && !newThreat.IsObsolete() && !IsIgnored( newThreat.GetEntity() ) && GetBot()->IsEnemy( newThreat.GetEntity() ) )
+ {
+ if ( !onlyVisibleThreats || newThreat.IsVisibleRecently() )
+ {
+ threat = GetBot()->GetIntentionInterface()->SelectMoreDangerousThreat( GetBot(), GetBot()->GetEntity(), threat, &newThreat );
+ }
+ }
+ }
+
+ // cache off threat
+ m_primaryThreat = threat ? threat->GetEntity() : NULL;
+
+ return threat;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return the closest recognized entity
+ */
+const CKnownEntity *IVision::GetClosestKnown( int team ) const
+{
+ const Vector &myPos = GetBot()->GetPosition();
+
+ const CKnownEntity *close = NULL;
+ float closeRange = 999999999.9f;
+
+ for( int i=0; i < m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &known = m_knownEntityVector[i];
+
+ if ( !known.IsObsolete() && IsAwareOf( known ) )
+ {
+ if ( team == TEAM_ANY || known.GetEntity()->GetTeamNumber() == team )
+ {
+ Vector to = known.GetLastKnownPosition() - myPos;
+ float rangeSq = to.LengthSqr();
+
+ if ( rangeSq < closeRange )
+ {
+ close = &known;
+ closeRange = rangeSq;
+ }
+ }
+ }
+ }
+
+ return close;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return the closest recognized entity that passes the given filter
+ */
+const CKnownEntity *IVision::GetClosestKnown( const INextBotEntityFilter &filter ) const
+{
+ const Vector &myPos = GetBot()->GetPosition();
+
+ const CKnownEntity *close = NULL;
+ float closeRange = 999999999.9f;
+
+ for( int i=0; i < m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &known = m_knownEntityVector[i];
+
+ if ( !known.IsObsolete() && IsAwareOf( known ) )
+ {
+ if ( filter.IsAllowed( known.GetEntity() ) )
+ {
+ Vector to = known.GetLastKnownPosition() - myPos;
+ float rangeSq = to.LengthSqr();
+
+ if ( rangeSq < closeRange )
+ {
+ close = &known;
+ closeRange = rangeSq;
+ }
+ }
+ }
+ }
+
+ return close;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Given an entity, return our known version of it (or NULL if we don't know of it)
+ */
+const CKnownEntity *IVision::GetKnown( const CBaseEntity *entity ) const
+{
+ if ( entity == NULL )
+ return NULL;
+
+ for( int i=0; i < m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &known = m_knownEntityVector[i];
+
+ if ( known.GetEntity() && known.GetEntity()->entindex() == entity->entindex() && !known.IsObsolete() )
+ {
+ return &known;
+ }
+ }
+
+ return NULL;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Introduce a known entity into the system. Its position is assumed to be known
+ * and will be updated, and it is assumed to not yet have been seen by us, allowing for learning
+ * of known entities by being told about them, hearing them, etc.
+ */
+void IVision::AddKnownEntity( CBaseEntity *entity )
+{
+ if ( entity == NULL || entity->IsWorld() )
+ {
+ // the world is not an entity we can deal with
+ return;
+ }
+
+ CKnownEntity known( entity );
+
+ // only add it if we don't already know of it
+ if ( m_knownEntityVector.Find( known ) == m_knownEntityVector.InvalidIndex() )
+ {
+ m_knownEntityVector.AddToTail( known );
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+// Remove the given entity from our awareness (whether we know if it or not)
+// Useful if we've moved to where we last saw the entity, but it's not there any longer.
+void IVision::ForgetEntity( CBaseEntity *forgetMe )
+{
+ if ( !forgetMe )
+ return;
+
+ FOR_EACH_VEC( m_knownEntityVector, it )
+ {
+ const CKnownEntity &known = m_knownEntityVector[ it ];
+
+ if ( known.GetEntity() && known.GetEntity()->entindex() == forgetMe->entindex() )
+ {
+ m_knownEntityVector.FastRemove( it );
+ return;
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+void IVision::ForgetAllKnownEntities( void )
+{
+ m_knownEntityVector.RemoveAll();
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return the number of entity on the given team known to us closer than rangeLimit
+ */
+int IVision::GetKnownCount( int team, bool onlyVisible, float rangeLimit ) const
+{
+ int count = 0;
+
+ FOR_EACH_VEC( m_knownEntityVector, it )
+ {
+ const CKnownEntity &known = m_knownEntityVector[ it ];
+
+ if ( !known.IsObsolete() && IsAwareOf( known ) )
+ {
+ if ( team == TEAM_ANY || known.GetEntity()->GetTeamNumber() == team )
+ {
+ if ( !onlyVisible || known.IsVisibleRecently() )
+ {
+ if ( rangeLimit < 0.0f || GetBot()->IsRangeLessThan( known.GetLastKnownPosition(), rangeLimit ) )
+ {
+ ++count;
+ }
+ }
+ }
+ }
+ }
+
+ return count;
+}
+
+
+//------------------------------------------------------------------------------------------
+class PopulateVisibleVector
+{
+public:
+ PopulateVisibleVector( CUtlVector< CBaseEntity * > *potentiallyVisible )
+ {
+ m_potentiallyVisible = potentiallyVisible;
+ }
+
+ bool operator() ( CBaseEntity *actor )
+ {
+ m_potentiallyVisible->AddToTail( actor );
+ return true;
+ }
+
+ CUtlVector< CBaseEntity * > *m_potentiallyVisible;
+};
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * 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()
+ */
+void IVision::CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible )
+{
+ potentiallyVisible->RemoveAll();
+
+ // by default, only consider players and other bots as potentially visible
+ PopulateVisibleVector populate( potentiallyVisible );
+ ForEachActor( populate );
+}
+
+
+//------------------------------------------------------------------------------------------
+class CollectVisible
+{
+public:
+ CollectVisible( IVision *vision )
+ {
+ m_vision = vision;
+ }
+
+ bool operator() ( CBaseEntity *entity )
+ {
+ if ( entity &&
+ !m_vision->IsIgnored( entity ) &&
+ entity->IsAlive() &&
+ entity != m_vision->GetBot()->GetEntity() &&
+ m_vision->IsAbleToSee( entity, IVision::USE_FOV ) )
+ {
+ m_recognized.AddToTail( entity );
+ }
+
+ return true;
+ }
+
+ bool Contains( CBaseEntity *entity ) const
+ {
+ for( int i=0; i < m_recognized.Count(); ++i )
+ {
+ if ( entity->entindex() == m_recognized[ i ]->entindex() )
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ IVision *m_vision;
+ CUtlVector< CBaseEntity * > m_recognized;
+};
+
+
+//------------------------------------------------------------------------------------------
+void IVision::UpdateKnownEntities( void )
+{
+ VPROF_BUDGET( "IVision::UpdateKnownEntities", "NextBot" );
+
+ // construct set of potentially visible objects
+ CUtlVector< CBaseEntity * > potentiallyVisible;
+ CollectPotentiallyVisibleEntities( &potentiallyVisible );
+
+ // collect set of visible and recognized entities at this moment
+ CollectVisible visibleNow( this );
+ FOR_EACH_VEC( potentiallyVisible, pit )
+ {
+ VPROF_BUDGET( "IVision::UpdateKnownEntities( collect visible )", "NextBot" );
+
+ if ( visibleNow( potentiallyVisible[ pit ] ) == false )
+ break;
+ }
+
+ // update known set with new data
+ { VPROF_BUDGET( "IVision::UpdateKnownEntities( update status )", "NextBot" );
+
+ int i;
+ for( i=0; i < m_knownEntityVector.Count(); ++i )
+ {
+ CKnownEntity &known = m_knownEntityVector[i];
+
+ // clear out obsolete knowledge
+ if ( known.GetEntity() == NULL || known.IsObsolete() )
+ {
+ m_knownEntityVector.Remove( i );
+ --i;
+ continue;
+ }
+
+ if ( visibleNow.Contains( known.GetEntity() ) )
+ {
+ // this visible entity was already known (but perhaps not visible until now)
+ known.UpdatePosition();
+ known.UpdateVisibilityStatus( true );
+
+ // has our reaction time just elapsed?
+ if ( gpGlobals->curtime - known.GetTimeWhenBecameVisible() >= GetMinRecognizeTime() &&
+ m_lastVisionUpdateTimestamp - known.GetTimeWhenBecameVisible() < GetMinRecognizeTime() )
+ {
+ if ( GetBot()->IsDebugging( NEXTBOT_VISION ) )
+ {
+ ConColorMsg( Color( 0, 255, 0, 255 ), "%3.2f: %s caught sight of %s(#%d)\n",
+ gpGlobals->curtime,
+ GetBot()->GetDebugIdentifier(),
+ known.GetEntity()->GetClassname(),
+ known.GetEntity()->entindex() );
+
+ NDebugOverlay::Line( GetBot()->GetBodyInterface()->GetEyePosition(), known.GetLastKnownPosition(), 255, 255, 0, false, 0.2f );
+ }
+
+ GetBot()->OnSight( known.GetEntity() );
+ }
+
+ // restart 'not seen' timer
+ m_notVisibleTimer[ known.GetEntity()->GetTeamNumber() ].Start();
+ }
+ else // known entity is not currently visible
+ {
+ if ( known.IsVisibleInFOVNow() )
+ {
+ // previously known and visible entity is now no longer visible
+ known.UpdateVisibilityStatus( false );
+
+ // lost sight of this entity
+ if ( GetBot()->IsDebugging( NEXTBOT_VISION ) )
+ {
+ ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Lost sight of %s(#%d)\n",
+ gpGlobals->curtime,
+ GetBot()->GetDebugIdentifier(),
+ known.GetEntity()->GetClassname(),
+ known.GetEntity()->entindex() );
+ }
+
+ GetBot()->OnLostSight( known.GetEntity() );
+ }
+
+ if ( !known.HasLastKnownPositionBeenSeen() )
+ {
+ // can we see the entity's last know position?
+ if ( IsAbleToSee( known.GetLastKnownPosition(), IVision::USE_FOV ) )
+ {
+ known.MarkLastKnownPositionAsSeen();
+ }
+ }
+ }
+ }
+ }
+
+ // check for new recognizes that were not in the known set
+ { VPROF_BUDGET( "IVision::UpdateKnownEntities( new recognizes )", "NextBot" );
+
+ int i, j;
+ for( i=0; i < visibleNow.m_recognized.Count(); ++i )
+ {
+ for( j=0; j < m_knownEntityVector.Count(); ++j )
+ {
+ if ( visibleNow.m_recognized[i] == m_knownEntityVector[j].GetEntity() )
+ {
+ break;
+ }
+ }
+
+ if ( j == m_knownEntityVector.Count() )
+ {
+ // recognized a previously unknown entity (emit OnSight() event after reaction time has passed)
+ CKnownEntity known( visibleNow.m_recognized[i] );
+ known.UpdatePosition();
+ known.UpdateVisibilityStatus( true );
+ m_knownEntityVector.AddToTail( known );
+ }
+ }
+ }
+
+ // debugging
+ if ( nb_debug_known_entities.GetBool() )
+ {
+ CBasePlayer *watcher = UTIL_GetListenServerHost();
+ if ( watcher )
+ {
+ CBaseEntity *subject = watcher->GetObserverTarget();
+
+ if ( subject && GetBot()->IsSelf( subject ) )
+ {
+ CUtlVector< CKnownEntity > knownVector;
+ CollectKnownEntities( &knownVector );
+
+ for( int i=0; i < knownVector.Count(); ++i )
+ {
+ CKnownEntity &known = knownVector[i];
+
+ if ( GetBot()->IsFriend( known.GetEntity() ) )
+ {
+ if ( IsAwareOf( known ) )
+ {
+ if ( known.IsVisibleInFOVNow() )
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 5.0f, 0, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ else
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 2.0f, 0, 100, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+ else
+ {
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 1.0f, 0, 100, 0, 128, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+ }
+ else
+ {
+ if ( IsAwareOf( known ) )
+ {
+ if ( known.IsVisibleInFOVNow() )
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 5.0f, 255, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ else
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 2.0f, 100, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+ else
+ {
+ NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 1.0f, 100, 0, 0, 128, true, NDEBUG_PERSIST_TILL_NEXT_SERVER );
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Update internal state
+ */
+void IVision::Update( void )
+{
+ VPROF_BUDGET( "IVision::Update", "NextBotExpensive" );
+
+/* This adds significantly to bot's reaction times
+ // throttle update rate
+ if ( !m_scanTimer.IsElapsed() )
+ {
+ return;
+ }
+
+ m_scanTimer.Start( 0.5f * GetMinRecognizeTime() );
+*/
+
+ if ( nb_blind.GetBool() )
+ {
+ m_knownEntityVector.RemoveAll();
+ return;
+ }
+
+ UpdateKnownEntities();
+
+ m_lastVisionUpdateTimestamp = gpGlobals->curtime;
+}
+
+
+//------------------------------------------------------------------------------------------
+bool IVision::IsAbleToSee( CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot ) const
+{
+ VPROF_BUDGET( "IVision::IsAbleToSee", "NextBotExpensive" );
+
+ if ( GetBot()->IsRangeGreaterThan( subject, GetMaxVisionRange() ) )
+ {
+ return false;
+ }
+
+
+ if ( GetBot()->GetEntity()->IsHiddenByFog( subject ) )
+ {
+ // lost in the fog
+ return false;
+ }
+
+ if ( checkFOV == USE_FOV && !IsInFieldOfView( subject ) )
+ {
+ return false;
+ }
+
+ CBaseCombatCharacter *combat = subject->MyCombatCharacterPointer();
+ if ( combat )
+ {
+ CNavArea *subjectArea = combat->GetLastKnownArea();
+ CNavArea *myArea = GetBot()->GetEntity()->GetLastKnownArea();
+ if ( myArea && subjectArea )
+ {
+ if ( !myArea->IsPotentiallyVisible( subjectArea ) )
+ {
+ // subject is not potentially visible, skip the expensive raycast
+ return false;
+ }
+ }
+ }
+
+ // do actual line-of-sight trace
+ if ( !IsLineOfSightClearToEntity( subject ) )
+ {
+ return false;
+ }
+
+ return IsVisibleEntityNoticed( subject );
+}
+
+
+//------------------------------------------------------------------------------------------
+bool IVision::IsAbleToSee( const Vector &pos, FieldOfViewCheckType checkFOV ) const
+{
+ VPROF_BUDGET( "IVision::IsAbleToSee", "NextBotExpensive" );
+
+
+ if ( GetBot()->IsRangeGreaterThan( pos, GetMaxVisionRange() ) )
+ {
+ return false;
+ }
+
+ if ( GetBot()->GetEntity()->IsHiddenByFog( pos ) )
+ {
+ // lost in the fog
+ return false;
+ }
+
+ if ( checkFOV == USE_FOV && !IsInFieldOfView( pos ) )
+ {
+ return false;
+ }
+
+ // do actual line-of-sight trace
+ return IsLineOfSightClear( pos );
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Angle given in degrees
+ */
+void IVision::SetFieldOfView( float horizAngle )
+{
+ m_FOV = horizAngle;
+ m_cosHalfFOV = cos( 0.5f * m_FOV * M_PI / 180.0f );
+}
+
+
+//------------------------------------------------------------------------------------------
+bool IVision::IsInFieldOfView( const Vector &pos ) const
+{
+#ifdef CHECK_OLD_CODE_AGAINST_NEW
+ bool bCheck = PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV );
+ Vector to = pos - GetBot()->GetBodyInterface()->GetEyePosition();
+ to.NormalizeInPlace();
+
+ float cosDiff = DotProduct( GetBot()->GetBodyInterface()->GetViewVector(), to );
+
+ if ( ( cosDiff > m_cosHalfFOV ) != bCheck )
+ {
+ Assert(0);
+ bool bCheck2 =
+ PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV );
+
+ }
+
+ return ( cosDiff > m_cosHalfFOV );
+#else
+ return PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV );
+#endif
+
+ return true;
+}
+
+
+//------------------------------------------------------------------------------------------
+bool IVision::IsInFieldOfView( CBaseEntity *subject ) const
+{
+ /// @todo check more points
+ if ( IsInFieldOfView( subject->WorldSpaceCenter() ) )
+ {
+ return true;
+ }
+
+ return IsInFieldOfView( subject->EyePosition() );
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Return true if the ray to the given point is unobstructed
+ */
+bool IVision::IsLineOfSightClear( const Vector &pos ) const
+{
+ VPROF_BUDGET( "IVision::IsLineOfSightClear", "NextBot" );
+ VPROF_INCREMENT_COUNTER( "IVision::IsLineOfSightClear", 1 );
+
+ trace_t result;
+ NextBotVisionTraceFilter filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), pos, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+
+ return ( result.fraction >= 1.0f && !result.startsolid );
+}
+
+
+//------------------------------------------------------------------------------------------
+bool IVision::IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *visibleSpot ) const
+{
+#ifdef TERROR
+ // TODO: Integration querycache & its dependencies
+
+ VPROF_INCREMENT_COUNTER( "IVision::IsLineOfSightClearToEntity", 1 );
+ VPROF_BUDGET( "IVision::IsLineOfSightClearToEntity", "NextBotSpiky" );
+
+ bool bClear = IsLineOfSightBetweenTwoEntitiesClear( GetBot()->GetBodyInterface()->GetEntity(), EOFFSET_MODE_EYEPOSITION,
+ subject, EOFFSET_MODE_WORLDSPACE_CENTER,
+ subject, COLLISION_GROUP_NONE,
+ MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, VisionTraceFilterFunction, 1.0 );
+
+#ifdef USE_NON_CACHE_QUERY
+ trace_t result;
+ NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ Assert( result.DidHit() != bClear );
+ if ( subject->IsPlayer() && ! bClear )
+ {
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->EyePosition(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ bClear = IsLineOfSightBetweenTwoEntitiesClear( GetBot()->GetEntity(),
+ EOFFSET_MODE_EYEPOSITION,
+ subject, EOFFSET_MODE_EYEPOSITION,
+ subject, COLLISION_GROUP_NONE,
+ MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE,
+ IgnoreActorsTraceFilterFunction, 1.0 );
+
+ // this WILL assert - the query interface happens at a different time, and has hysteresis.
+ Assert( result.DidHit() != bClear );
+ }
+#endif
+
+ return bClear;
+
+#else
+
+ // TODO: Use plain-old traces until querycache/etc gets integrated
+ VPROF_BUDGET( "IVision::IsLineOfSightClearToEntity", "NextBot" );
+
+ trace_t result;
+ NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE );
+
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ if ( result.DidHit() )
+ {
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->EyePosition(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+
+ if ( result.DidHit() )
+ {
+ UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->GetAbsOrigin(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
+ }
+ }
+
+ if ( visibleSpot )
+ {
+ *visibleSpot = result.endpos;
+ }
+
+ return ( result.fraction >= 1.0f && !result.startsolid );
+
+#endif
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Are we looking directly at the given position
+ */
+bool IVision::IsLookingAt( const Vector &pos, float cosTolerance ) const
+{
+ Vector to = pos - GetBot()->GetBodyInterface()->GetEyePosition();
+ to.NormalizeInPlace();
+
+ Vector forward;
+ AngleVectors( GetBot()->GetEntity()->EyeAngles(), &forward );
+
+ return DotProduct( to, forward ) > cosTolerance;
+}
+
+
+//------------------------------------------------------------------------------------------
+/**
+ * Are we looking directly at the given actor
+ */
+bool IVision::IsLookingAt( const CBaseCombatCharacter *actor, float cosTolerance ) const
+{
+ return IsLookingAt( actor->EyePosition(), cosTolerance );
+}
+
+
diff --git a/game/server/NextBot/NextBotVisionInterface.h b/game/server/NextBot/NextBotVisionInterface.h
new file mode 100644
index 0000000..c6361f9
--- /dev/null
+++ b/game/server/NextBot/NextBotVisionInterface.h
@@ -0,0 +1,226 @@
+// NextBotVisionInterface.h
+// Visual information query interface for bots
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_VISION_INTERFACE_H_
+#define _NEXT_BOT_VISION_INTERFACE_H_
+
+#include "NextBotComponentInterface.h"
+#include "NextBotKnownEntity.h"
+
+class IBody;
+class INextBotEntityFilter;
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for HOW the bot sees (near sighted? night vision? etc)
+ */
+class IVision : public INextBotComponent
+{
+public:
+ IVision( INextBot *bot );
+ virtual ~IVision() { }
+
+ virtual void Reset( void ); // reset to initial state
+ virtual void Update( void ); // update internal state
+
+ //-- attention/short term memory interface follows ------------------------------------------
+
+ //
+ // WARNING: Do not keep CKnownEntity pointers returned by these methods, as they can be invalidated/freed
+ //
+
+ /**
+ * Iterate each interesting entity we are aware of.
+ * If functor returns false, stop iterating and return false.
+ * NOTE: known.GetEntity() is guaranteed to be non-NULL
+ */
+ class IForEachKnownEntity
+ {
+ public:
+ virtual bool Inspect( const CKnownEntity &known ) = 0;
+ };
+ virtual bool ForEachKnownEntity( IForEachKnownEntity &func );
+
+ virtual void CollectKnownEntities( CUtlVector< CKnownEntity > *knownVector ); // populate given vector with all currently known entities
+
+ virtual const CKnownEntity *GetPrimaryKnownThreat( bool onlyVisibleThreats = false ) const; // return the biggest threat to ourselves that we are aware of
+ virtual float GetTimeSinceVisible( int team ) const; // return time since we saw any member of the given team
+
+ virtual const CKnownEntity *GetClosestKnown( int team = TEAM_ANY ) const; // return the closest known entity
+ virtual int GetKnownCount( int team, bool onlyVisible = false, float rangeLimit = -1.0f ) const; // return the number of entities on the given team known to us closer than rangeLimit
+
+ virtual const CKnownEntity *GetClosestKnown( const INextBotEntityFilter &filter ) const; // return the closest known entity that passes the given filter
+
+ virtual const CKnownEntity *GetKnown( const CBaseEntity *entity ) const; // given an entity, return our known version of it (or NULL if we don't know of it)
+
+ // Introduce a known entity into the system. Its position is assumed to be known
+ // and will be updated, and it is assumed to not yet have been seen by us, allowing for learning
+ // of known entities by being told about them, hearing them, etc.
+ virtual void AddKnownEntity( CBaseEntity *entity );
+
+ virtual void ForgetEntity( CBaseEntity *forgetMe ); // remove the given entity from our awareness (whether we know if it or not)
+ virtual void ForgetAllKnownEntities( void );
+
+ //-- physical vision interface follows ------------------------------------------------------
+
+ /**
+ * 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 float GetMaxVisionRange( void ) const; // return maximum distance vision can reach
+ virtual float GetMinRecognizeTime( void ) const; // return VISUAL reaction time
+
+ /**
+ * IsAbleToSee() returns true if the viewer can ACTUALLY SEE the subject or position,
+ * taking into account blindness, smoke effects, invisibility, etc.
+ * If 'visibleSpot' is non-NULL, the highest priority spot on the subject that is visible is returned.
+ */
+ enum FieldOfViewCheckType { USE_FOV, DISREGARD_FOV };
+ virtual bool IsAbleToSee( CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot = NULL ) const;
+ virtual bool IsAbleToSee( const Vector &pos, FieldOfViewCheckType checkFOV ) const;
+
+ 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
+
+ /**
+ * Check if 'subject' is within the viewer's field of view
+ */
+ virtual bool IsInFieldOfView( const Vector &pos ) const;
+ virtual bool IsInFieldOfView( CBaseEntity *subject ) const;
+ virtual float GetDefaultFieldOfView( void ) const; // return default FOV in degrees
+ virtual float GetFieldOfView( void ) const; // return FOV in degrees
+ virtual void SetFieldOfView( float horizAngle ); // angle given in degrees
+
+ virtual bool IsLineOfSightClear( const Vector &pos ) const; // return true if the ray to the given point is unobstructed
+
+ /**
+ * Returns true if the ray between the position and the subject is unobstructed.
+ * A visible spot on the subject is returned in 'visibleSpot'.
+ */
+ virtual bool IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *visibleSpot = NULL ) const;
+
+ /// @todo: Implement LookAt system
+ virtual bool IsLookingAt( const Vector &pos, float cosTolerance = 0.95f ) const; // are we looking at the given position
+ virtual bool IsLookingAt( const CBaseCombatCharacter *actor, float cosTolerance = 0.95f ) const; // are we looking at the given actor
+
+private:
+ CountdownTimer m_scanTimer; // for throttling update rate
+
+ float m_FOV; // current FOV in degrees
+ float m_cosHalfFOV; // the cosine of FOV/2
+
+ CUtlVector< CKnownEntity > m_knownEntityVector; // the set of enemies/friends we are aware of
+ void UpdateKnownEntities( void );
+ bool IsAwareOf( const CKnownEntity &known ) const; // return true if our reaction time has passed for this entity
+ mutable CHandle< CBaseEntity > m_primaryThreat;
+
+ float m_lastVisionUpdateTimestamp;
+ IntervalTimer m_notVisibleTimer[ MAX_TEAMS ]; // for tracking interval since last saw a member of the given team
+};
+
+inline void IVision::CollectKnownEntities( CUtlVector< CKnownEntity > *knownVector )
+{
+ if ( knownVector )
+ {
+ knownVector->RemoveAll();
+
+ for( int i=0; i<m_knownEntityVector.Count(); ++i )
+ {
+ if ( !m_knownEntityVector[i].IsObsolete() )
+ {
+ knownVector->AddToTail( m_knownEntityVector[i] );
+ }
+ }
+ }
+}
+
+inline float IVision::GetDefaultFieldOfView( void ) const
+{
+ return 90.0f;
+}
+
+inline float IVision::GetFieldOfView( void ) const
+{
+ return m_FOV;
+}
+
+
+inline float IVision::GetTimeSinceVisible( int team ) const
+{
+ if ( team == TEAM_ANY )
+ {
+ // return minimum time
+ float time = 9999999999.9f;
+ for( int i=0; i<MAX_TEAMS; ++i )
+ {
+ if ( m_notVisibleTimer[i].HasStarted() )
+ {
+ if ( time > m_notVisibleTimer[i].GetElapsedTime() )
+ {
+ team = m_notVisibleTimer[i].GetElapsedTime();
+ }
+ }
+ }
+ return time;
+ }
+
+ if ( team >= 0 && team < MAX_TEAMS )
+ {
+ return m_notVisibleTimer[ team ].GetElapsedTime();
+ }
+
+ return 0.0f;
+}
+
+
+inline bool IVision::IsAwareOf( const CKnownEntity &known ) const
+{
+ return known.GetTimeSinceBecameKnown() >= GetMinRecognizeTime();
+}
+
+
+inline bool IVision::ForEachKnownEntity( IVision::IForEachKnownEntity &func )
+{
+ for( int i=0; i<m_knownEntityVector.Count(); ++i )
+ {
+ const CKnownEntity &known = m_knownEntityVector[i];
+
+ if ( !known.IsObsolete() && IsAwareOf( known ) )
+ {
+ if ( func.Inspect( known ) == false )
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+inline bool IVision::IsVisibleEntityNoticed( CBaseEntity *subject ) const
+{
+ return true;
+}
+
+inline bool IVision::IsIgnored( CBaseEntity *subject ) const
+{
+ return false;
+}
+
+inline float IVision::GetMaxVisionRange( void ) const
+{
+ return 2000.0f;
+}
+
+inline float IVision::GetMinRecognizeTime( void ) const
+{
+ return 0.0f;
+}
+
+
+#endif // _NEXT_BOT_VISION_INTERFACE_H_
diff --git a/game/server/NextBot/Path/NextBotChasePath.cpp b/game/server/NextBot/Path/NextBotChasePath.cpp
new file mode 100644
index 0000000..1674fc3
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotChasePath.cpp
@@ -0,0 +1,166 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================
+
+#include "cbase.h"
+
+#include "NextBotChasePath.h"
+#include "tier1/fmtstr.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Try to cutoff our chase subject
+ */
+Vector ChasePath::PredictSubjectPosition( INextBot *bot, CBaseEntity *subject ) const
+{
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ const Vector &subjectPos = subject->GetAbsOrigin();
+
+ Vector to = subjectPos - bot->GetPosition();
+ to.z = 0.0f;
+ float flRangeSq = to.LengthSqr();
+
+ // don't lead if subject is very far away
+ float flLeadRadiusSq = GetLeadRadius();
+ flLeadRadiusSq *= flLeadRadiusSq;
+ if ( flRangeSq > flLeadRadiusSq )
+ return subjectPos;
+
+ // Normalize in place
+ float range = sqrt( flRangeSq );
+ to /= ( range + 0.0001f ); // avoid divide by zero
+
+ // estimate time to reach subject, assuming maximum speed
+ float leadTime = 0.5f + ( range / ( mover->GetRunSpeed() + 0.0001f ) );
+
+ // estimate amount to lead the subject
+ Vector lead = leadTime * subject->GetAbsVelocity();
+ lead.z = 0.0f;
+
+ if ( DotProduct( to, lead ) < 0.0f )
+ {
+ // the subject is moving towards us - only pay attention
+ // to his perpendicular velocity for leading
+ Vector2D to2D = to.AsVector2D();
+ to2D.NormalizeInPlace();
+
+ Vector2D perp( -to2D.y, to2D.x );
+
+ float enemyGroundSpeed = lead.x * perp.x + lead.y * perp.y;
+
+ lead.x = enemyGroundSpeed * perp.x;
+ lead.y = enemyGroundSpeed * perp.y;
+ }
+
+ // compute our desired destination
+ Vector pathTarget = subjectPos + lead;
+
+ // validate this destination
+
+ // don't lead through walls
+ if ( lead.LengthSqr() > 36.0f )
+ {
+ float fraction;
+ if ( !mover->IsPotentiallyTraversable( subjectPos, pathTarget, ILocomotion::IMMEDIATELY, &fraction ) )
+ {
+ // tried to lead through an unwalkable area - clip to walkable space
+ pathTarget = subjectPos + fraction * ( pathTarget - subjectPos );
+ }
+ }
+
+ // don't lead over cliffs
+ CNavArea *leadArea = NULL;
+
+#ifdef NEED_GPGLOBALS_SERVERCOUNT_TO_DO_THIS
+ CBaseCombatCharacter *pBCC = subject->MyCombatCharacterPointer();
+ if ( pBCC && CloseEnough( pathTarget, subjectPos, 3.0 ) )
+ {
+ pathTarget = subjectPos;
+ leadArea = pBCC->GetLastKnownArea(); // can return null?
+ }
+ else
+ {
+ struct CacheEntry_t
+ {
+ CacheEntry_t() : pArea(NULL) {}
+ Vector target;
+ CNavArea *pArea;
+ };
+
+ static int iServer;
+ static CacheEntry_t cache[4];
+ static int iNext;
+ int i;
+
+ bool bFound = false;
+ if ( iServer != gpGlobals->serverCount )
+ {
+ for ( i = 0; i < ARRAYSIZE(cache); i++ )
+ {
+ cache[i].pArea = NULL;
+ }
+ iServer = gpGlobals->serverCount;
+ }
+ else
+ {
+ for ( i = 0; i < ARRAYSIZE(cache); i++ )
+ {
+ if ( cache[i].pArea && CloseEnough( cache[i].target, pathTarget, 2.0 ) )
+ {
+ pathTarget = cache[i].target;
+ leadArea = cache[i].pArea;
+ bFound = true;
+ break;
+ }
+ }
+ }
+
+ if ( !bFound )
+ {
+ leadArea = TheNavMesh->GetNearestNavArea( pathTarget );
+ if ( leadArea )
+ {
+ cache[iNext].target = pathTarget;
+ cache[iNext].pArea = leadArea;
+ iNext = ( iNext + 1 ) % ARRAYSIZE( cache );
+ }
+ }
+ }
+#else
+ leadArea = TheNavMesh->GetNearestNavArea( pathTarget );
+#endif
+
+
+ if ( !leadArea || leadArea->GetZ( pathTarget.x, pathTarget.y ) < pathTarget.z - mover->GetMaxJumpHeight() )
+ {
+ // would fall off a cliff
+ return subjectPos;
+ }
+
+ /** This needs more thought - it is preventing bots from using dropdowns
+ if ( mover->HasPotentialGap( subjectPos, pathTarget, &fraction ) )
+ {
+ // tried to lead over a cliff - clip to safe region
+ pathTarget = subjectPos + fraction * ( pathTarget - subjectPos );
+ }
+ */
+
+ return pathTarget;
+}
+
+// if the victim is a player, poke them so they know they're being chased
+void DirectChasePath::NotifyVictim( INextBot *me, CBaseEntity *victim )
+{
+ CBaseCombatCharacter *pBCCVictim = ToBaseCombatCharacter( victim );
+ if ( !pBCCVictim )
+ return;
+
+ pBCCVictim->OnPursuedBy( me );
+} \ No newline at end of file
diff --git a/game/server/NextBot/Path/NextBotChasePath.h b/game/server/NextBot/Path/NextBotChasePath.h
new file mode 100644
index 0000000..1929648
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotChasePath.h
@@ -0,0 +1,376 @@
+// NextBotChasePath.h
+// Maintain and follow a "chase path" to a selected Actor
+// Author: Michael Booth, September 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_CHASE_PATH_
+#define _NEXT_BOT_CHASE_PATH_
+
+#include "nav.h"
+#include "NextBotInterface.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotChasePath.h"
+#include "NextBotUtil.h"
+#include "NextBotPathFollow.h"
+#include "tier0/vprof.h"
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * A ChasePath extends a PathFollower to periodically recompute a path to a chase
+ * subject, and to move along the path towards that subject.
+ */
+class ChasePath : public PathFollower
+{
+public:
+ enum SubjectChaseType
+ {
+ LEAD_SUBJECT,
+ DONT_LEAD_SUBJECT
+ };
+ ChasePath( SubjectChaseType chaseHow = DONT_LEAD_SUBJECT );
+
+ virtual ~ChasePath() { }
+
+ virtual void Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos = NULL ); // update path to chase target and move bot along path
+
+ virtual float GetLeadRadius( void ) const; // range where movement leading begins - beyond this just head right for the subject
+ virtual float GetMaxPathLength( void ) const; // return maximum path length
+ virtual Vector PredictSubjectPosition( INextBot *bot, CBaseEntity *subject ) const; // try to cutoff our chase subject, knowing our relative positions and velocities
+ virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const; // return true if situation has changed enough to warrant recomputing the current path
+
+ virtual float GetLifetime( void ) const; // Return duration this path is valid. Path will become invalid at its earliest opportunity once this duration elapses. Zero = infinite lifetime
+
+ virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid
+
+private:
+ void RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos );
+
+ CountdownTimer m_failTimer; // throttle re-pathing if last path attempt failed
+ CountdownTimer m_throttleTimer; // require a minimum time between re-paths
+ CountdownTimer m_lifetimeTimer;
+ EHANDLE m_lastPathSubject; // the subject used to compute the current/last path
+ SubjectChaseType m_chaseHow;
+};
+
+inline ChasePath::ChasePath( SubjectChaseType chaseHow )
+{
+ m_failTimer.Invalidate();
+ m_throttleTimer.Invalidate();
+ m_lifetimeTimer.Invalidate();
+ m_lastPathSubject = NULL;
+ m_chaseHow = chaseHow;
+}
+
+inline float ChasePath::GetLeadRadius( void ) const
+{
+ return 500.0f; // 1000.0f;
+}
+
+inline float ChasePath::GetMaxPathLength( void ) const
+{
+ // no limit
+ return 0.0f;
+}
+
+inline float ChasePath::GetLifetime( void ) const
+{
+ // infinite duration
+ return 0.0f;
+}
+
+inline void ChasePath::Invalidate( void )
+{
+ // path is gone, repath at earliest opportunity
+ m_throttleTimer.Invalidate();
+ m_lifetimeTimer.Invalidate();
+
+ // extend
+ PathFollower::Invalidate();
+}
+
+
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Maintain a path to our chase subject and move along that path
+ */
+inline void ChasePath::Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos )
+{
+ VPROF_BUDGET( "ChasePath::Update", "NextBot" );
+
+ // maintain the path to the subject
+ RefreshPath( bot, subject, cost, pPredictedSubjectPos );
+
+ // move along the path towards the subject
+ PathFollower::Update( bot );
+}
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Return true if situation has changed enough to warrant recomputing the current path
+ */
+inline bool ChasePath::IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const
+{
+ // the closer we get, the more accurate our path needs to be
+ Vector to = subject->GetAbsOrigin() - bot->GetPosition();
+
+ const float minTolerance = 0.0f; // 25.0f;
+ const float toleranceRate = 0.33f; // 1.0f; // 0.15f;
+
+ float tolerance = minTolerance + toleranceRate * to.Length();
+
+ return ( subject->GetAbsOrigin() - GetEndPosition() ).IsLengthGreaterThan( tolerance );
+}
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Periodically rebuild the path to our victim
+ */
+inline void ChasePath::RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos )
+{
+ VPROF_BUDGET( "ChasePath::RefreshPath", "NextBot" );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ // don't change our path if we're on a ladder
+ if ( IsValid() && mover->IsUsingLadder() )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+
+ // don't allow repath until a moment AFTER we have left the ladder
+ m_throttleTimer.Start( 1.0f );
+
+ return;
+ }
+
+ if ( subject == NULL )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No subject.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+ return;
+ }
+
+ if ( !m_failTimer.IsElapsed() )
+ {
+// if ( bot->IsDebugging( NEXTBOT_PATH ) )
+// {
+// DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Fail timer not elapsed.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+// }
+ return;
+ }
+
+ // if our path subject changed, repath immediately
+ if ( subject != m_lastPathSubject )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) Chase path subject changed (from %p to %p).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_lastPathSubject.Get(), subject );
+ }
+
+ Invalidate();
+
+ // new subject, fresh attempt
+ m_failTimer.Invalidate();
+ }
+
+ if ( IsValid() && !m_throttleTimer.IsElapsed() )
+ {
+ // require a minimum time between repaths, as long as we have a path to follow
+// if ( bot->IsDebugging( NEXTBOT_PATH ) )
+// {
+// DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+// }
+ return;
+ }
+
+ if ( IsValid() && m_lifetimeTimer.HasStarted() && m_lifetimeTimer.IsElapsed() )
+ {
+ // this path's lifetime has elapsed
+ Invalidate();
+ }
+
+ if ( !IsValid() || IsRepathNeeded( bot, subject ) )
+ {
+ // the situation has changed - try a new path
+ bool isPath;
+ Vector pathTarget = subject->GetAbsOrigin();
+
+ if ( m_chaseHow == LEAD_SUBJECT )
+ {
+ pathTarget = pPredictedSubjectPos ? *pPredictedSubjectPos : PredictSubjectPosition( bot, subject );
+ isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() );
+ }
+ else if ( subject->MyCombatCharacterPointer() && subject->MyCombatCharacterPointer()->GetLastKnownArea() )
+ {
+ isPath = Compute( bot, subject->MyCombatCharacterPointer(), cost, GetMaxPathLength() );
+ }
+ else
+ {
+ isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() );
+ }
+
+ if ( isPath )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ //const float size = 20.0f;
+ //NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, RandomInt( 0, 200 ), 255, 255, true, 30.0f );
+
+ DevMsg( "%3.2f: bot(#%d) REPATH\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+
+ m_lastPathSubject = subject;
+
+ const float minRepathInterval = 0.5f;
+ m_throttleTimer.Start( minRepathInterval );
+
+ // track the lifetime of this new path
+ float lifetime = GetLifetime();
+ if ( lifetime > 0.0f )
+ {
+ m_lifetimeTimer.Start( lifetime );
+ }
+ else
+ {
+ m_lifetimeTimer.Invalidate();
+ }
+ }
+ else
+ {
+ // can't reach subject - throttle retry based on range to subject
+ m_failTimer.Start( 0.005f * ( bot->GetRangeTo( subject ) ) );
+
+ // allow bot to react to path failure
+ bot->OnMoveToFailure( this, FAIL_NO_PATH_EXISTS );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ //const float size = 20.0f;
+ const float dT = 90.0f;
+ int c = RandomInt( 0, 100 );
+ //NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, c, c, 255, true, dT );
+ NDebugOverlay::HorzArrow( bot->GetPosition(), pathTarget, 5.0f, 255, c, c, 255, true, dT );
+
+ DevMsg( "%3.2f: bot(#%d) REPATH FAILED\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+
+ Invalidate();
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------------------------------------
+/**
+ * Directly beeline toward victim if we have a clear shot, otherwise pathfind.
+ */
+class DirectChasePath : public ChasePath
+{
+public:
+
+ DirectChasePath( ChasePath::SubjectChaseType chaseHow = ChasePath::DONT_LEAD_SUBJECT ) : ChasePath( chaseHow )
+ {
+
+ }
+
+ //-------------------------------------------------------------------------------------------------------
+ virtual void Update( INextBot *me, CBaseEntity *victim, const IPathCost &pathCost, Vector *pPredictedSubjectPos = NULL ) // update path to chase target and move bot along path
+ {
+ Assert( !pPredictedSubjectPos );
+ bool bComputedPredictedPosition;
+ Vector vecPredictedPosition;
+ if ( !DirectChase( &bComputedPredictedPosition, &vecPredictedPosition, me, victim ) )
+ {
+ // path around obstacles to reach our victim
+ ChasePath::Update( me, victim, pathCost, bComputedPredictedPosition ? &vecPredictedPosition : NULL );
+ }
+ NotifyVictim( me, victim );
+ }
+
+ //-------------------------------------------------------------------------------------------------------
+ bool DirectChase( bool *pPredictedPositionComputed, Vector *pPredictedPos, INextBot *me, CBaseEntity *victim ) // if there is nothing between us and our victim, run directly at them
+ {
+ *pPredictedPositionComputed = false;
+
+ ILocomotion *mover = me->GetLocomotionInterface();
+
+ if ( me->IsImmobile() || mover->IsScrambling() )
+ {
+ return false;
+ }
+
+ if ( IsDiscontinuityAhead( me, CLIMB_UP ) )
+ {
+ return false;
+ }
+
+ if ( IsDiscontinuityAhead( me, JUMP_OVER_GAP ) )
+ {
+ return false;
+ }
+
+ Vector leadVictimPos = PredictSubjectPosition( me, victim );
+
+ // Don't want to have to compute the predicted position twice.
+ *pPredictedPositionComputed = true;
+ *pPredictedPos = leadVictimPos;
+
+ if ( !mover->IsPotentiallyTraversable( mover->GetFeet(), leadVictimPos ) )
+ {
+ return false;
+ }
+
+ // the way is clear - move directly towards our victim
+ mover->FaceTowards( leadVictimPos );
+ mover->Approach( leadVictimPos );
+
+ me->GetBodyInterface()->AimHeadTowards( victim );
+
+ // old path is no longer useful since we've moved off of it
+ Invalidate();
+
+ return true;
+ }
+
+ //-------------------------------------------------------------------------------------------------------
+ virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const // return true if situation has changed enough to warrant recomputing the current path
+ {
+ if ( ChasePath::IsRepathNeeded( bot, subject ) )
+ {
+ return true;
+ }
+
+ return bot->GetLocomotionInterface()->IsStuck() && bot->GetLocomotionInterface()->GetStuckDuration() > 2.0f;
+ }
+
+ //-------------------------------------------------------------------------------------------------------
+ /**
+ * Determine exactly where the path goes between the given two areas
+ * on the path. Return this point in 'crossPos'.
+ */
+ virtual void ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const
+ {
+ Vector center;
+ float halfWidth;
+ from->ComputePortal( to, dir, &center, &halfWidth );
+
+ *crossPos = center;
+ }
+
+ void NotifyVictim( INextBot *me, CBaseEntity *victim );
+};
+
+
+
+
+#endif // _NEXT_BOT_CHASE_PATH_
diff --git a/game/server/NextBot/Path/NextBotPath.cpp b/game/server/NextBot/Path/NextBotPath.cpp
new file mode 100644
index 0000000..4514887
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotPath.cpp
@@ -0,0 +1,1094 @@
+// NextBotPath.cpp
+// Encapsulate and manipulate a path through the world
+// Author: Michael Booth, February 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "nav_mesh.h"
+#include "fmtstr.h"
+
+#include "NextBotPath.h"
+#include "NextBotInterface.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotBodyInterface.h"
+#include "NextBotUtil.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+ConVar NextBotPathDrawIncrement( "nb_path_draw_inc", "100", FCVAR_CHEAT );
+ConVar NextBotPathDrawSegmentCount( "nb_path_draw_segment_count", "100", FCVAR_CHEAT );
+ConVar NextBotPathSegmentInfluenceRadius( "nb_path_segment_influence_radius", "100", FCVAR_CHEAT );
+
+//--------------------------------------------------------------------------------------------------------------
+Path::Path( void )
+{
+ m_segmentCount = 0;
+
+ m_cursorPos = 0.0f;
+ m_isCursorDataDirty = true;
+ m_cursorData.segmentPrior = NULL;
+ m_ageTimer.Invalidate();
+ m_subject = NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Determine actual path positions
+ */
+bool Path::ComputePathDetails( INextBot *bot, const Vector &start )
+{
+ VPROF_BUDGET( "Path::ComputePathDetails", "NextBot" );
+
+ if (m_segmentCount == 0)
+ return false;
+
+ IBody *body = bot->GetBodyInterface();
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ const float stepHeight = ( mover ) ? mover->GetStepHeight() : 18.0f;
+
+ // inflate hull width slightly as a safety margin
+ const float hullWidth = ( body ) ? body->GetHullWidth() + 5.0f : 1.0f;
+
+ // set first path position
+ if ( m_path[0].area->Contains( start ) )
+ {
+ m_path[0].pos = start;
+ }
+ else
+ {
+ // start in first area's center
+ m_path[0].pos = m_path[0].area->GetCenter();
+ }
+ m_path[0].ladder = NULL;
+ m_path[0].how = NUM_TRAVERSE_TYPES;
+ m_path[0].type = ON_GROUND;
+
+ // set positions along the path
+ for( int i=1; i<m_segmentCount; ++i )
+ {
+ Segment *from = &m_path[ i-1 ];
+ Segment *to = &m_path[ i ];
+
+ if ( to->how <= GO_WEST ) // walk along the floor to the next area
+ {
+ to->ladder = NULL;
+
+ from->area->ComputePortal( to->area, (NavDirType)to->how, &to->m_portalCenter, &to->m_portalHalfWidth );
+
+ // compute next point
+ ComputeAreaCrossing( bot, from->area, from->pos, to->area, (NavDirType)to->how, &to->pos );
+
+ // we need to walk out of "from" area, so keep Z where we can reach it
+ to->pos.z = from->area->GetZ( to->pos );
+
+ // if this is a "jump down" connection, we must insert an additional point on the path
+ //float expectedHeightDrop = from->area->GetZ( from->pos ) - to->area->GetZ( to->pos );
+
+ // measure the drop distance relative to the actual slope of the ground
+ Vector fromPos = from->pos;
+ fromPos.z = from->area->GetZ( fromPos );
+
+ Vector toPos = to->pos;
+ toPos.z = to->area->GetZ( toPos );
+
+ Vector groundNormal;
+ from->area->ComputeNormal( &groundNormal );
+
+ Vector alongPath = toPos - fromPos;
+
+ float expectedHeightDrop = -DotProduct( alongPath, groundNormal );
+
+ if ( expectedHeightDrop > mover->GetStepHeight() )
+ {
+ // NOTE: We can't know this is a drop-down yet, because of subtle interactions
+ // between nav area links and "portals" and "area crossings"
+
+ // compute direction of path just prior to "jump down"
+ Vector2D dir;
+ DirectionToVector2D( (NavDirType)to->how, &dir );
+
+ // shift top of "jump down" out a bit to "get over the ledge"
+ const float inc = 10.0f; // 0.25f * hullWidth;
+ const float maxPushDist = 2.0f * hullWidth; // 75.0f;
+ float halfWidth = hullWidth/2.0f;
+ float hullHeight = ( body ) ? body->GetCrouchHullHeight() : 1.0f;
+
+ float pushDist;
+ for( pushDist = 0.0f; pushDist <= maxPushDist; pushDist += inc )
+ {
+ Vector pos = to->pos + Vector( pushDist * dir.x, pushDist * dir.y, 0.0f );
+ Vector lowerPos = Vector( pos.x, pos.y, toPos.z );
+
+ trace_t result;
+ NextBotTraceFilterIgnoreActors filter( bot->GetEntity(), COLLISION_GROUP_NONE );
+ UTIL_TraceHull( pos, lowerPos,
+ Vector( -halfWidth, -halfWidth, stepHeight ), Vector( halfWidth, halfWidth, hullHeight ),
+ bot->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+ if ( result.fraction >= 1.0f )
+ {
+ // found clearance to drop
+ break;
+ }
+ }
+
+ Vector startDrop( to->pos.x + pushDist * dir.x, to->pos.y + pushDist * dir.y, to->pos.z );
+ Vector endDrop( startDrop.x, startDrop.y, to->area->GetZ( to->pos ) );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Cross3D( startDrop, 5.0f, 255, 0, 255, true, 5.0f );
+ NDebugOverlay::Cross3D( endDrop, 5.0f, 255, 255, 0, true, 5.0f );
+ NDebugOverlay::VertArrow( startDrop, endDrop, 5.0f, 255, 100, 0, 255, true, 5.0f );
+ }
+
+ // verify that there is actually ground down there in case this is a far jump dropdown
+ float ground;
+ if ( TheNavMesh->GetGroundHeight( endDrop, &ground ) )
+ {
+ if ( startDrop.z > ground + stepHeight )
+ {
+ // if "ground" is lower than the next segment along the path
+ // there is a chasm between - this is not a drop down
+ // NOTE next->pos is not yet valid - this loop is computing it!
+ // const Segment *next = NextSegment( to );
+ // if ( !next || next->area->GetCenter().z < ground + stepHeight )
+ {
+ // this is a "jump down" link
+ to->pos = startDrop;
+ to->type = DROP_DOWN;
+
+ // insert a duplicate node to represent the bottom of the fall
+ if ( m_segmentCount < MAX_PATH_SEGMENTS-1 )
+ {
+ // copy nodes down
+ for( int j=m_segmentCount; j>i; --j )
+ m_path[j] = m_path[j-1];
+
+ // path is one node longer
+ ++m_segmentCount;
+
+ // move index ahead into the new node we just duplicated
+ ++i;
+
+ m_path[i].pos.x = endDrop.x;
+ m_path[i].pos.y = endDrop.y;
+ m_path[i].pos.z = ground;
+
+ m_path[i].type = ON_GROUND;
+ }
+ }
+ }
+ }
+ }
+ }
+ else if ( to->how == GO_LADDER_UP ) // to get to next area, must go up a ladder
+ {
+ // find our ladder
+ const NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_UP );
+ int it;
+ for( it=0; it<ladders->Count(); ++it )
+ {
+ CNavLadder *ladder = (*ladders)[ it ].ladder;
+
+ // can't use "behind" area when ascending...
+ if (ladder->m_topForwardArea == to->area ||
+ ladder->m_topLeftArea == to->area ||
+ ladder->m_topRightArea == to->area)
+ {
+ to->ladder = ladder;
+ to->pos = ladder->m_bottom + ladder->GetNormal() * 2.0f * HalfHumanWidth;
+ to->type = LADDER_UP;
+ break;
+ }
+ }
+
+ if (it == ladders->Count())
+ {
+ //PrintIfWatched( "ERROR: Can't find ladder in path\n" );
+ return false;
+ }
+ }
+ else if ( to->how == GO_LADDER_DOWN ) // to get to next area, must go down a ladder
+ {
+ // find our ladder
+ const NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_DOWN );
+ int it;
+ for( it=0; it<ladders->Count(); ++it )
+ {
+ CNavLadder *ladder = (*ladders)[ it ].ladder;
+
+ if (ladder->m_bottomArea == to->area)
+ {
+ to->ladder = ladder;
+ to->pos = ladder->m_top;
+ to->pos = ladder->m_top - ladder->GetNormal() * 2.0f * HalfHumanWidth;
+ to->type = LADDER_DOWN;
+ break;
+ }
+ }
+
+ if (it == ladders->Count())
+ {
+ //PrintIfWatched( "ERROR: Can't find ladder in path\n" );
+ return false;
+ }
+ }
+ else if ( to->how == GO_ELEVATOR_UP || to->how == GO_ELEVATOR_DOWN )
+ {
+ to->pos = to->area->GetCenter();
+ to->ladder = NULL;
+ }
+ }
+
+
+ //
+ // Scan for non-adjacent nav areas and add gap-jump-target nodes
+ // and jump-up target nodes for adjacent ledge mantling
+ // @todo Adjacency should be baked into the mesh data
+ //
+ for( int i=0; i<m_segmentCount-1; ++i )
+ {
+ Segment *from = &m_path[ i ];
+ Segment *to = &m_path[ i+1 ];
+
+ // first segment doesnt have a direction
+ if ( from->how != NUM_TRAVERSE_TYPES && from->how > GO_WEST )
+ continue;
+
+ if ( to->how > GO_WEST || !to->type == ON_GROUND )
+ continue;
+
+ // if areas are separated, we may need to 'gap jump' between them
+ // add a node to minimize the jump distance
+ Vector closeFrom, closeTo;
+ to->area->GetClosestPointOnArea( from->pos, &closeTo );
+ from->area->GetClosestPointOnArea( closeTo, &closeFrom );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Line( closeFrom, closeTo, 255, 0, 255, true, 5.0f );
+ }
+
+
+ const float separationTolerance = 1.9f * GenerationStepSize;
+ if ( (closeFrom - closeTo).AsVector2D().IsLengthGreaterThan( separationTolerance ) && ( closeTo - closeFrom ).AsVector2D().IsLengthGreaterThan( 0.5f * fabs( closeTo.z - closeFrom.z ) ) )
+ {
+ // areas are disjoint and mostly level - add gap jump target
+
+ // compute landing spot in 'to' area
+ Vector landingPos;
+ to->area->GetClosestPointOnArea( to->pos, &landingPos );
+
+ // compute launch spot in 'from' area
+ Vector launchPos;
+ from->area->GetClosestPointOnArea( landingPos, &launchPos );
+
+ Vector forward = landingPos - launchPos;
+ forward.NormalizeInPlace();
+
+ const float halfWidth = hullWidth/2.0f;
+
+ // adjust path position to landing spot
+ to->pos = landingPos + forward * halfWidth;
+
+ // insert launch position just before that segment to ensure bot is
+ // positioned for minimal jump distance
+ Segment newSegment = *from;
+
+ newSegment.pos = launchPos - forward * halfWidth;
+ newSegment.type = JUMP_OVER_GAP;
+
+ InsertSegment( newSegment, i+1 );
+
+ ++i;
+ }
+ else if ( (closeTo.z - closeFrom.z) > stepHeight )
+ {
+ // areas are adjacent, but need a jump-up - add a jump-to target
+
+ // adjust goal to be at top of ledge
+ //to->pos.z = to->area->GetZ( to->pos.x, to->pos.y );
+ // use center of climb-up destination area to make sure bot moves onto actual ground once they finish their climb
+ to->pos = to->area->GetCenter();
+
+ // add launch position at base of jump
+ Segment newSegment = *from;
+
+ Vector launchPos;
+ from->area->GetClosestPointOnArea( to->pos, &launchPos );
+
+ newSegment.pos = launchPos;
+ newSegment.type = CLIMB_UP;
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Cross3D( newSegment.pos, 15.0f, 255, 100, 255, true, 3.0f );
+ }
+
+ InsertSegment( newSegment, i+1 );
+
+ ++i;
+ }
+
+ /** RETHINK THIS. It doesn't work in general cases, and messes up on doorways
+ else if ( from->type == ON_GROUND && from->how <= GO_WEST )
+ {
+ // if any segment is not directly walkable, add a segment
+ // fixup corners that are being cut too tightly
+ if ( mover && !mover->IsPotentiallyTraversable( from->pos, to->pos ) )
+ {
+ Segment newSegment = *from;
+
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ NDebugOverlay::HorzArrow( from->pos, to->pos, 3.0f, 255, 0, 0, 255, true, 3.0f );
+ }
+
+ //newSegment.pos = from->area->GetCenter();
+
+ Vector2D shift;
+ DirectionToVector2D( OppositeDirection( (NavDirType)to->how ), &shift );
+
+ newSegment.pos = to->pos;
+ newSegment.pos.x += hullWidth * shift.x;
+ newSegment.pos.y += hullWidth * shift.y;
+
+ newSegment.type = ON_GROUND;
+
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ NDebugOverlay::Cross3D( newSegment.pos, 15.0f, 255, 0, 255, true, 3.0f );
+ }
+
+ InsertSegment( newSegment, i+1 );
+
+ i += 2;
+ }
+ }
+ */
+ }
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Insert new segment at index i
+ */
+void Path::InsertSegment( Segment newSegment, int i )
+{
+ if (m_segmentCount < MAX_PATH_SEGMENTS-1)
+ {
+ // shift segments to make room for new one
+ for( int j=m_segmentCount; j>i; --j )
+ m_path[j] = m_path[j-1];
+
+ // path is one node longer
+ ++m_segmentCount;
+
+ m_path[i] = newSegment;
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Build trivial path when start and goal are in the same nav area
+ */
+bool Path::BuildTrivialPath( INextBot *bot, const Vector &goal )
+{
+ const Vector &start = bot->GetPosition();
+
+ m_segmentCount = 0;
+
+ /// @todo Dangerous to use "nearset" nav area - could be far away
+ CNavArea *startArea = TheNavMesh->GetNearestNavArea( start );
+ if (startArea == NULL)
+ return false;
+
+ CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal );
+ if (goalArea == NULL)
+ return false;
+
+ m_segmentCount = 2;
+
+ m_path[0].area = startArea;
+ m_path[0].pos.x = start.x;
+ m_path[0].pos.y = start.y;
+ m_path[0].pos.z = startArea->GetZ( start );
+ m_path[0].ladder = NULL;
+ m_path[0].how = NUM_TRAVERSE_TYPES;
+ m_path[0].type = ON_GROUND;
+
+ m_path[1].area = goalArea;
+ m_path[1].pos.x = goal.x;
+ m_path[1].pos.y = goal.y;
+ m_path[1].pos.z = goalArea->GetZ( goal );
+ m_path[1].ladder = NULL;
+ m_path[1].how = NUM_TRAVERSE_TYPES;
+ m_path[1].type = ON_GROUND;
+
+ m_path[0].forward = m_path[1].pos - m_path[0].pos;
+ m_path[0].length = m_path[0].forward.NormalizeInPlace();
+ m_path[0].distanceFromStart = 0.0f;
+ m_path[0].curvature = 0.0f;
+
+ m_path[1].forward = m_path[0].forward;
+ m_path[1].length = 0.0f;
+ m_path[1].distanceFromStart = m_path[0].length;
+ m_path[1].curvature = 0.0f;
+
+ OnPathChanged( bot, COMPLETE_PATH );
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Draw the path for debugging.
+ */
+void Path::Draw( const Path::Segment *start ) const
+{
+ if ( !IsValid() )
+ return;
+
+ CFmtStr msg;
+
+ // limit length of path we draw
+ int count = NextBotPathDrawSegmentCount.GetInt();
+
+ const Segment *s = start ? start : FirstSegment();
+ int i=0;
+ while( s && count-- )
+ {
+ const Segment *next = NextSegment( s );
+ if ( next == NULL )
+ {
+ // end of the path
+ break;
+ }
+
+ Vector to = next->pos - s->pos;
+ float horiz = MAX( abs(to.x), abs(to.y) );
+ float vert = abs( to.z );
+
+ int r,g,b;
+ switch( s->type )
+ {
+ case DROP_DOWN: r = 255; g = 0; b = 255; break;
+ case CLIMB_UP: r = 0; g = 0; b = 255; break;
+ case JUMP_OVER_GAP: r = 0; g = 255; b = 255; break;
+ case LADDER_UP: r = 0; g = 255; b = 0; break;
+ case LADDER_DOWN: r = 0; g = 100; b = 0; break;
+ default: r = 255; g = 77; b = 0; break; // ON_GROUND
+ }
+
+ if ( s->ladder )
+ {
+ NDebugOverlay::VertArrow( s->ladder->m_bottom, s->ladder->m_top, 5.0f, r, g, b, 255, true, 0.1f );
+ }
+ else
+ {
+ NDebugOverlay::Line( s->pos, next->pos, r, g, b, true, 0.1f );
+ }
+
+ const float nodeLength = 25.0f;
+ if ( horiz > vert )
+ {
+ NDebugOverlay::HorzArrow( s->pos, s->pos + nodeLength * s->forward, 5.0f, r, g, b, 255, true, 0.1f );
+ }
+ else
+ {
+ NDebugOverlay::VertArrow( s->pos, s->pos + nodeLength * s->forward, 5.0f, r, g, b, 255, true, 0.1f );
+ }
+
+ NDebugOverlay::Text( s->pos, msg.sprintf( "%d", i ), true, 0.1f );
+
+ //NDebugOverlay::Text( s->pos, msg.sprintf( "%d (%3.2f)", i, s->curvature ), false, 0.1f );
+
+ s = next;
+ ++i;
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Draw the path for debugging - MODIFIES cursor position
+ */
+void Path::DrawInterpolated( float from, float to )
+{
+ if ( !IsValid() )
+ {
+ return;
+ }
+
+ float t = from;
+
+ MoveCursor( t );
+ const Data &data = GetCursorData();
+ Vector lastPos = data.pos;
+
+ do
+ {
+ t += NextBotPathDrawIncrement.GetFloat();
+
+ MoveCursor( t );
+ const Data &data = GetCursorData();
+
+ float curvePower = 3.0f * data.curvature;
+
+ int r = 255 * ( 1.0f - curvePower );
+ r = clamp( r, 0, 255 );
+
+ int g = 255 * ( 1.0f + curvePower );
+ g = clamp( g, 0, 255 );
+
+ NDebugOverlay::Line( lastPos, data.pos, r, g, 0, true, 0.1f );
+
+ /*
+ int i = 0xFF & (int)( data.pos.x + data.pos.y + data.pos.z );
+ i >>= 1;
+ i += 128;
+
+ NDebugOverlay::Line( data.pos, data.pos + 10.0f * data.forward, 0, i, i, true, 0.1f );
+ */
+
+ lastPos = data.pos;
+ }
+ while( t < to );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Check line of sight from 'anchor' node on path to subsequent nodes until
+ * we find a node that can't been seen from 'anchor'.
+ */
+int Path::FindNextOccludedNode( INextBot *bot, int anchorIndex )
+{
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ if ( mover == NULL)
+ {
+ return m_segmentCount;
+ }
+
+ Segment *anchor = &m_path[ anchorIndex ];
+
+ for( int i=anchorIndex+1; i<m_segmentCount; ++i )
+ {
+ Segment *to = &m_path[i];
+
+ // if this segment is not on the ground, or is precise, don't skip past it
+ if ( !to->type == ON_GROUND || (to->area->GetAttributes() & NAV_MESH_PRECISE) )
+ {
+ return i;
+ }
+
+ if ( !mover->IsPotentiallyTraversable( anchor->pos, to->pos, ILocomotion::IMMEDIATELY ) )
+ {
+ // cant reach this node directly from anchor node
+ return i;
+ }
+
+ if ( mover->HasPotentialGap( anchor->pos, to->pos ) )
+ {
+ // we would fall into a gap if we took this cutoff
+ return i;
+ }
+ }
+
+ return m_segmentCount;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Smooth out path, removing redundant nodes
+ */
+void Path::Optimize( INextBot *bot )
+{
+ // this is SUPER expensive - especially the IsGap() check
+ return;
+
+ VPROF_BUDGET( "Path::Optimize", "NextBot" );
+
+ if (m_segmentCount < 3)
+ return;
+
+ int anchor = 0;
+
+ while( anchor < m_segmentCount )
+ {
+ int occluded = FindNextOccludedNode( bot, anchor );
+ int nextAnchor = occluded-1;
+
+ if (nextAnchor > anchor)
+ {
+ // remove redundant nodes between anchor and nextAnchor
+ int removeCount = nextAnchor - anchor - 1;
+ if (removeCount > 0)
+ {
+ for( int i=nextAnchor; i<m_segmentCount; ++i )
+ {
+ m_path[i-removeCount] = m_path[i];
+ }
+ m_segmentCount -= removeCount;
+ }
+ }
+
+ ++anchor;
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Compute final data for completed path
+ */
+void Path::PostProcess( void )
+{
+ VPROF_BUDGET( "Path::PostProcess", "NextBot" );
+
+ m_ageTimer.Start();
+
+ if (m_segmentCount == 0)
+ return;
+
+ if (m_segmentCount == 1)
+ {
+ m_path[0].forward = vec3_origin;
+ m_path[0].length = 0.0f;
+ m_path[0].distanceFromStart = 0.0f;
+ m_path[0].curvature = 0.0f;
+ return;
+ }
+
+ float distanceSoFar = 0.0f;
+ int i;
+ for( i=0; i < m_segmentCount-1; ++i )
+ {
+ Segment *from = &m_path[ i ];
+ Segment *to = &m_path[ i+1 ];
+
+ from->forward = to->pos - from->pos;
+ from->length = from->forward.NormalizeInPlace();
+
+ from->distanceFromStart = distanceSoFar;
+
+ distanceSoFar += from->length;
+ }
+
+
+ // compute curvature in XY plane
+ Vector2D from, to;
+ for( i=1; i < m_segmentCount-1; ++i )
+ {
+ if (m_path[ i ].type != ON_GROUND)
+ {
+ m_path[ i ].curvature = 0.0f;
+ }
+ else
+ {
+ from = m_path[ i-1 ].forward.AsVector2D();
+ from.NormalizeInPlace();
+
+ to = m_path[ i ].forward.AsVector2D();
+ to.NormalizeInPlace();
+
+ m_path[ i ].curvature = 0.5f * ( 1.0f - from.Dot( to ) );
+
+ Vector2D right( -from.y, from.x );
+ if ( to.Dot( right ) < 0.0f )
+ {
+ m_path[ i ].curvature = -m_path[ i ].curvature;
+ }
+ }
+ }
+
+ // first segment has no curvature
+ m_path[ 0 ].curvature = 0.0f;
+
+ // last segment maintains direction
+ m_path[ i ].forward = m_path[ i-1 ].forward;
+ m_path[ i ].length = 0.0f;
+ m_path[ i ].distanceFromStart = distanceSoFar;
+ m_path[ i ].curvature = 0.0f;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return a position on the path at the given distance from the path start
+ */
+const Vector &Path::GetPosition( float distanceFromStart, const Segment *start ) const
+{
+ if (!IsValid())
+ {
+ return vec3_origin;
+ }
+
+ float lengthSoFar;
+ const Segment *segment;
+
+ if (start)
+ {
+ segment = start;
+ lengthSoFar = start->distanceFromStart;
+ }
+ else
+ {
+ segment = &m_path[0];
+ lengthSoFar = 0.0f;
+ }
+
+ if (segment->distanceFromStart > distanceFromStart)
+ {
+ // clamp to path start
+ return segment->pos;
+ }
+
+
+ const Segment *next = NextSegment( segment );
+
+ Vector delta;
+ float length;
+
+ while( next )
+ {
+ delta = next->pos - segment->pos;
+ length = segment->length;
+
+ if (lengthSoFar + length >= distanceFromStart)
+ {
+ // desired point is on this segment of the path
+ float overlap = distanceFromStart - lengthSoFar;
+ float t = overlap / length;
+
+ m_pathPos = segment->pos + t * delta;
+
+ return m_pathPos;
+ }
+
+ lengthSoFar += length;
+
+ segment = next;
+ next = NextSegment( next );
+ }
+
+ // clamp to path end
+ return segment->pos;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return the closest point on the path to the given position
+ */
+const Vector &Path::GetClosestPosition( const Vector &pos, const Segment *start, float alongLimit ) const
+{
+ const Segment *segment = (start) ? start : &m_path[0];
+
+ if (segment == NULL)
+ {
+ return pos;
+ }
+
+ m_closePos = pos;
+ float closeRangeSq = 99999999999.9f;
+
+ float distanceSoFar = 0.0f;
+ while( alongLimit == 0.0f || distanceSoFar <= alongLimit )
+ {
+ const Segment *nextSegment = NextSegment( segment );
+
+ if (nextSegment)
+ {
+ Vector close;
+ CalcClosestPointOnLineSegment( pos, segment->pos, nextSegment->pos, close );
+ float rangeSq = (close - pos).LengthSqr();
+ if (rangeSq < closeRangeSq)
+ {
+ m_closePos = close;
+ closeRangeSq = rangeSq;
+ }
+ }
+ else
+ {
+ // end of the path
+ break;
+ }
+
+ distanceSoFar += segment->length;
+ segment = nextSegment;
+ }
+
+ return m_closePos;
+}
+
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Replace this path with the given path's data
+ */
+void Path::Copy( INextBot *bot, const Path &path )
+{
+ VPROF_BUDGET( "Path::Copy", "NextBot" );
+
+ Invalidate();
+
+ for( int i = 0; i < path.m_segmentCount; ++i )
+ {
+ m_path[i] = path.m_path[i];
+ }
+ m_segmentCount = path.m_segmentCount;
+
+ OnPathChanged( bot, COMPLETE_PATH );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Set cursor position to closest point on path to given position
+ */
+void Path::MoveCursorToClosestPosition( const Vector &pos, SeekType type, float alongLimit ) const
+{
+ if ( !IsValid() )
+ {
+ return;
+ }
+
+ if ( type == SEEK_ENTIRE_PATH || type == SEEK_AHEAD )
+ {
+ const Segment *segment;
+
+ if ( type == SEEK_AHEAD )
+ {
+ // continue search from cursor position onward
+ if ( m_cursorData.segmentPrior )
+ {
+ segment = m_cursorData.segmentPrior;
+ }
+ else
+ {
+ // no prior segment, start from the start
+ segment = &m_path[ 0 ];
+ }
+ }
+ else
+ {
+ // search entire path from the start
+ segment = &m_path[ 0 ];
+ }
+
+ m_cursorData.pos = pos;
+ m_cursorData.segmentPrior = segment;
+ float closeRangeSq = 99999999999.9f;
+
+ float distanceSoFar = 0.0f;
+ while( alongLimit == 0.0f || distanceSoFar <= alongLimit )
+ {
+ const Segment *nextSegment = NextSegment( segment );
+
+ if ( nextSegment )
+ {
+ Vector close;
+ CalcClosestPointOnLineSegment( pos, segment->pos, nextSegment->pos, close );
+
+ float rangeSq = ( close - pos ).LengthSqr();
+ if ( rangeSq < closeRangeSq )
+ {
+ m_cursorData.pos = close;
+ m_cursorData.segmentPrior = segment;
+
+ closeRangeSq = rangeSq;
+ }
+ }
+ else
+ {
+ // end of the path
+ break;
+ }
+
+ distanceSoFar += segment->length;
+ segment = nextSegment;
+ }
+
+ //
+ // Move cursor to closest point on path
+ //
+ segment = m_cursorData.segmentPrior;
+
+ float t = ( m_cursorData.pos - segment->pos ).Length() / segment->length;
+
+ m_cursorPos = segment->distanceFromStart + t * segment->length;
+ m_isCursorDataDirty = true;
+ }
+ else
+ {
+ AssertMsg( false, "SEEK_BEHIND not implemented" );
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return path state at the current cursor position
+ */
+const Path::Data &Path::GetCursorData( void ) const
+{
+ if ( IsValid() )
+ {
+ if ( m_isCursorDataDirty )
+ {
+ const float epsilon = 0.0001f;
+ if ( m_cursorPos < epsilon || m_segmentCount < 2 )
+ {
+ // start of path
+ m_cursorData.pos = m_path[0].pos;
+ m_cursorData.forward = m_path[0].forward;
+ m_cursorData.curvature = m_path[0].curvature;
+ m_cursorData.segmentPrior = &m_path[0];
+ }
+ else if ( m_cursorPos > GetLength() - epsilon )
+ {
+ // end of path
+ m_cursorData.pos = m_path[ m_segmentCount-1 ].pos;
+ m_cursorData.forward = m_path[ m_segmentCount-1 ].forward;
+ m_cursorData.curvature = m_path[ m_segmentCount-1 ].curvature;
+ m_cursorData.segmentPrior = &m_path[ m_segmentCount-1 ];
+ }
+ else
+ {
+ // along path
+ float lengthSoFar = 0.0f;
+ const Segment *segment = &m_path[0];
+
+ const Segment *next = NextSegment( segment );
+
+ while( next )
+ {
+ float length = segment->length;
+
+ if ( lengthSoFar + length >= m_cursorPos )
+ {
+ // desired point is on this segment of the path
+ float overlap = m_cursorPos - lengthSoFar;
+ float t = 1.0f; // 0-length segments are assumed to be complete, to avoid NaNs
+ if ( length > 0.0f )
+ {
+ t = overlap / length;
+ }
+
+ // interpolate data at this point along the path
+ m_cursorData.pos = segment->pos + t * ( next->pos - segment->pos );
+ m_cursorData.forward = segment->forward + t * ( next->forward - segment->forward );
+ m_cursorData.segmentPrior = segment;
+
+ // curvature fades to zero along midpoint of long straight segments
+ // and is influenced as it nears ends of segment
+ if ( overlap < NextBotPathSegmentInfluenceRadius.GetFloat() )
+ {
+ if ( length - overlap < NextBotPathSegmentInfluenceRadius.GetFloat() )
+ {
+ // near start and end - interpolate
+ float startCurvature = segment->curvature * ( 1.0f - ( overlap / NextBotPathSegmentInfluenceRadius.GetFloat() ) );
+ float endCurvature = next->curvature * ( 1.0f - ( ( length - overlap ) / NextBotPathSegmentInfluenceRadius.GetFloat() ) );
+
+ m_cursorData.curvature = ( startCurvature + endCurvature ) / 2.0f;
+ }
+ else
+ {
+ // near start only
+ m_cursorData.curvature = segment->curvature * ( 1.0f - ( overlap / NextBotPathSegmentInfluenceRadius.GetFloat() ) );
+ }
+ }
+ else if ( length - overlap < NextBotPathSegmentInfluenceRadius.GetFloat() )
+ {
+ // near end only
+ m_cursorData.curvature = next->curvature * ( 1.0f - ( ( length - overlap ) / NextBotPathSegmentInfluenceRadius.GetFloat() ) );
+ }
+
+
+ break;
+ }
+
+ lengthSoFar += length;
+
+ segment = next;
+ next = NextSegment( next );
+ }
+ }
+
+ // data is up to date
+ m_isCursorDataDirty = false;
+ }
+ }
+ else
+ {
+ // path is not valid
+ m_cursorData.pos = vec3_origin;
+ m_cursorData.forward = Vector( 1.0f, 0, 0 );
+ m_cursorData.curvature = 0.0f;
+ m_cursorData.segmentPrior = NULL;
+ }
+
+ return m_cursorData;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Determine exactly where the path goes between the given two areas
+ * on the path. Return this point in 'crossPos'.
+ */
+void Path::ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const
+{
+ from->ComputeClosestPointInPortal( to, dir, fromPos, crossPos );
+
+ // move goal position into the goal area a bit to avoid running directly along the edge of an area against a wall, etc
+ // don't do this unless area is against a wall - and what if our hull is wider than the area?
+ // AddDirectionVector( crossPos, dir, bot->GetBodyInterface()->GetHullWidth()/2.0f );
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/game/server/NextBot/Path/NextBotPath.h b/game/server/NextBot/Path/NextBotPath.h
new file mode 100644
index 0000000..6669d92
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotPath.h
@@ -0,0 +1,862 @@
+// NextBotPath.h
+// Encapsulate and manipulate a path through the world
+// Author: Michael Booth, February 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_PATH_H_
+#define _NEXT_BOT_PATH_H_
+
+#include "NextBotInterface.h"
+
+#include "tier0/vprof.h"
+
+#define PATH_NO_LENGTH_LIMIT 0.0f // non-default argument value for Path::Compute()
+#define PATH_TRUNCATE_INCOMPLETE_PATH false // non-default argument value for Path::Compute()
+
+class INextBot;
+class CNavArea;
+class CNavLadder;
+
+
+//---------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for pathfinding costs.
+ * TODO: Replace all template cost functors with this interface, so we can virtualize and derive from them.
+ */
+class IPathCost
+{
+public:
+ virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const = 0;
+};
+
+
+//---------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for selecting a goal area during "open goal" pathfinding
+ */
+class IPathOpenGoalSelector
+{
+public:
+ // compare "newArea" to "currentGoal" and return the area that is the better goal area
+ virtual CNavArea *operator() ( CNavArea *currentGoal, CNavArea *newArea ) const = 0;
+};
+
+
+//---------------------------------------------------------------------------------------------------------------
+/**
+ * A Path through the world.
+ * Not only does this encapsulate a path to get from point A to point B,
+ * but also the selecting the decision algorithm for how to build that path.
+ */
+class Path
+{
+public:
+ Path( void );
+ virtual ~Path() { }
+
+ enum SegmentType
+ {
+ ON_GROUND,
+ DROP_DOWN,
+ CLIMB_UP,
+ JUMP_OVER_GAP,
+ LADDER_UP,
+ LADDER_DOWN,
+
+ NUM_SEGMENT_TYPES
+ };
+
+ // @todo Allow custom Segment classes for different kinds of paths
+ struct Segment
+ {
+ CNavArea *area; // the area along the path
+ NavTraverseType how; // how to enter this area from the previous one
+ Vector pos; // our movement goal position at this point in the path
+ const CNavLadder *ladder; // if "how" refers to a ladder, this is it
+
+ SegmentType type; // how to traverse this segment of the path
+ Vector forward; // unit vector along segment
+ float length; // length of this segment
+ float distanceFromStart; // distance of this node from the start of the path
+ float curvature; // how much the path 'curves' at this point in the XY plane (0 = none, 1 = 180 degree doubleback)
+
+ Vector m_portalCenter; // position of center of 'portal' between previous area and this area
+ float m_portalHalfWidth; // half width of 'portal'
+ };
+
+ virtual float GetLength( void ) const; // return length of path from start to finish
+ virtual const Vector &GetPosition( float distanceFromStart, const Segment *start = NULL ) const; // return a position on the path at the given distance from the path start
+ virtual const Vector &GetClosestPosition( const Vector &pos, const Segment *start = NULL, float alongLimit = 0.0f ) const; // return the closest point on the path to the given position
+
+ virtual const Vector &GetStartPosition( void ) const; // return the position where this path starts
+ virtual const Vector &GetEndPosition( void ) const; // return the position where this path ends
+ virtual CBaseCombatCharacter *GetSubject( void ) const; // return the actor this path leads to, or NULL if there is no subject
+
+ virtual const Path::Segment *GetCurrentGoal( void ) const; // return current goal along the path we are trying to reach
+
+ virtual float GetAge( void ) const; // return "age" of this path (time since it was built)
+
+ enum SeekType
+ {
+ SEEK_ENTIRE_PATH, // search the entire path length
+ SEEK_AHEAD, // search from current cursor position forward toward end of path
+ SEEK_BEHIND // search from current cursor position backward toward path start
+ };
+ virtual void MoveCursorToClosestPosition( const Vector &pos, SeekType type = SEEK_ENTIRE_PATH, float alongLimit = 0.0f ) const; // Set cursor position to closest point on path to given position
+
+ enum MoveCursorType
+ {
+ PATH_ABSOLUTE_DISTANCE,
+ PATH_RELATIVE_DISTANCE
+ };
+ virtual void MoveCursorToStart( void ); // set seek cursor to start of path
+ virtual void MoveCursorToEnd( void ); // set seek cursor to end of path
+ virtual void MoveCursor( float value, MoveCursorType type = PATH_ABSOLUTE_DISTANCE ); // change seek cursor position
+ virtual float GetCursorPosition( void ) const; // return position of seek cursor (distance along path)
+
+ struct Data
+ {
+ Vector pos; // the position along the path
+ Vector forward; // unit vector along path direction
+ float curvature; // how much the path 'curves' at this point in the XY plane (0 = none, 1 = 180 degree doubleback)
+ const Segment *segmentPrior; // the segment just before this position
+ };
+ virtual const Data &GetCursorData( void ) const; // return path state at the current cursor position
+
+ virtual bool IsValid( void ) const;
+ virtual void Invalidate( void ); // make path invalid (clear it)
+
+ virtual void Draw( const Path::Segment *start = NULL ) const; // draw the path for debugging
+ virtual void DrawInterpolated( float from, float to ); // draw the path for debugging - MODIFIES cursor position
+
+ virtual const Segment *FirstSegment( void ) const; // return first segment of path
+ virtual const Segment *NextSegment( const Segment *currentSegment ) const; // return next segment of path, given current one
+ virtual const Segment *PriorSegment( const Segment *currentSegment ) const; // return previous segment of path, given current one
+ virtual const Segment *LastSegment( void ) const; // return last segment of path
+
+ enum ResultType
+ {
+ COMPLETE_PATH,
+ PARTIAL_PATH,
+ NO_PATH
+ };
+ virtual void OnPathChanged( INextBot *bot, ResultType result ) { } // invoked when the path is (re)computed (path is valid at the time of this call)
+
+ virtual void Copy( INextBot *bot, const Path &path ); // Replace this path with the given path's data
+
+
+ //-----------------------------------------------------------------------------------------------------------------
+ /**
+ * Compute shortest path from bot to given actor via A* algorithm.
+ * If returns true, path was found to the subject.
+ * If returns false, path may either be invalid (use IsValid() to check), or valid but
+ * doesn't reach all the way to the subject.
+ */
+ template< typename CostFunctor >
+ bool Compute( INextBot *bot, CBaseCombatCharacter *subject, CostFunctor &costFunc, float maxPathLength = 0.0f, bool includeGoalIfPathFails = true )
+ {
+ VPROF_BUDGET( "Path::Compute(subject)", "NextBot" );
+
+ Invalidate();
+
+ m_subject = subject;
+
+ const Vector &start = bot->GetPosition();
+
+ CNavArea *startArea = bot->GetEntity()->GetLastKnownArea();
+ if ( !startArea )
+ {
+ OnPathChanged( bot, NO_PATH );
+ return false;
+ }
+
+ CNavArea *subjectArea = subject->GetLastKnownArea();
+ if ( !subjectArea )
+ {
+ OnPathChanged( bot, NO_PATH );
+ return false;
+ }
+
+ Vector subjectPos = subject->GetAbsOrigin();
+
+ // if we are already in the subject area, build trivial path
+ if ( startArea == subjectArea )
+ {
+ BuildTrivialPath( bot, subjectPos );
+ return true;
+ }
+
+ //
+ // Compute shortest path to subject
+ //
+ CNavArea *closestArea = NULL;
+ bool pathResult = NavAreaBuildPath( startArea, subjectArea, &subjectPos, costFunc, &closestArea, maxPathLength, bot->GetEntity()->GetTeamNumber() );
+
+ // Failed?
+ if ( closestArea == NULL )
+ return false;
+
+ //
+ // Build actual path by following parent links back from goal area
+ //
+
+ // get count
+ int count = 0;
+ CNavArea *area;
+ for( area = closestArea; area; area = area->GetParent() )
+ {
+ ++count;
+
+ if ( area == startArea )
+ {
+ // startArea can be re-evaluated during the pathfind and given a parent...
+ break;
+ }
+ if ( count >= MAX_PATH_SEGMENTS-1 ) // save room for endpoint
+ break;
+ }
+
+ if ( count == 1 )
+ {
+ BuildTrivialPath( bot, subjectPos );
+ return pathResult;
+ }
+
+ // assemble path
+ m_segmentCount = count;
+ for( area = closestArea; count && area; area = area->GetParent() )
+ {
+ --count;
+ m_path[ count ].area = area;
+ m_path[ count ].how = area->GetParentHow();
+ m_path[ count ].type = ON_GROUND;
+ }
+
+ if ( pathResult || includeGoalIfPathFails )
+ {
+ // append actual subject position
+ m_path[ m_segmentCount ].area = closestArea;
+ m_path[ m_segmentCount ].pos = subjectPos;
+ m_path[ m_segmentCount ].ladder = NULL;
+ m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES;
+ m_path[ m_segmentCount ].type = ON_GROUND;
+ ++m_segmentCount;
+ }
+
+ // compute path positions
+ if ( ComputePathDetails( bot, start ) == false )
+ {
+ Invalidate();
+ OnPathChanged( bot, NO_PATH );
+ return false;
+ }
+
+ // remove redundant nodes and clean up path
+ Optimize( bot );
+
+ PostProcess();
+
+ OnPathChanged( bot, pathResult ? COMPLETE_PATH : PARTIAL_PATH );
+
+ return pathResult;
+ }
+
+
+ //-----------------------------------------------------------------------------------------------------------------
+ /**
+ * Compute shortest path from bot to 'goal' via A* algorithm.
+ * If returns true, path was found to the goal position.
+ * If returns false, path may either be invalid (use IsValid() to check), or valid but
+ * doesn't reach all the way to the goal.
+ */
+ template< typename CostFunctor >
+ bool Compute( INextBot *bot, const Vector &goal, CostFunctor &costFunc, float maxPathLength = 0.0f, bool includeGoalIfPathFails = true )
+ {
+ VPROF_BUDGET( "Path::Compute(goal)", "NextBotSpiky" );
+
+ Invalidate();
+
+ const Vector &start = bot->GetPosition();
+
+ CNavArea *startArea = bot->GetEntity()->GetLastKnownArea();
+ if ( !startArea )
+ {
+ OnPathChanged( bot, NO_PATH );
+ return false;
+ }
+
+ // check line-of-sight to the goal position when finding it's nav area
+ const float maxDistanceToArea = 200.0f;
+ CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal, true, maxDistanceToArea, true );
+
+ // if we are already in the goal area, build trivial path
+ if ( startArea == goalArea )
+ {
+ BuildTrivialPath( bot, goal );
+ return true;
+ }
+
+ // make sure path end position is on the ground
+ Vector pathEndPosition = goal;
+ if ( goalArea )
+ {
+ pathEndPosition.z = goalArea->GetZ( pathEndPosition );
+ }
+ else
+ {
+ TheNavMesh->GetGroundHeight( pathEndPosition, &pathEndPosition.z );
+ }
+
+ //
+ // Compute shortest path to goal
+ //
+ CNavArea *closestArea = NULL;
+ bool pathResult = NavAreaBuildPath( startArea, goalArea, &goal, costFunc, &closestArea, maxPathLength, bot->GetEntity()->GetTeamNumber() );
+
+ // Failed?
+ if ( closestArea == NULL )
+ return false;
+
+ //
+ // Build actual path by following parent links back from goal area
+ //
+
+ // get count
+ int count = 0;
+ CNavArea *area;
+ for( area = closestArea; area; area = area->GetParent() )
+ {
+ ++count;
+
+ if ( area == startArea )
+ {
+ // startArea can be re-evaluated during the pathfind and given a parent...
+ break;
+ }
+ if ( count >= MAX_PATH_SEGMENTS-1 ) // save room for endpoint
+ break;
+ }
+
+ if ( count == 1 )
+ {
+ BuildTrivialPath( bot, goal );
+ return pathResult;
+ }
+
+ // assemble path
+ m_segmentCount = count;
+ for( area = closestArea; count && area; area = area->GetParent() )
+ {
+ --count;
+ m_path[ count ].area = area;
+ m_path[ count ].how = area->GetParentHow();
+ m_path[ count ].type = ON_GROUND;
+ }
+
+ if ( pathResult || includeGoalIfPathFails )
+ {
+ // append actual goal position
+ m_path[ m_segmentCount ].area = closestArea;
+ m_path[ m_segmentCount ].pos = pathEndPosition;
+ m_path[ m_segmentCount ].ladder = NULL;
+ m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES;
+ m_path[ m_segmentCount ].type = ON_GROUND;
+ ++m_segmentCount;
+ }
+
+ // compute path positions
+ if ( ComputePathDetails( bot, start ) == false )
+ {
+ Invalidate();
+ OnPathChanged( bot, NO_PATH );
+ return false;
+ }
+
+ // remove redundant nodes and clean up path
+ Optimize( bot );
+
+ PostProcess();
+
+ OnPathChanged( bot, pathResult ? COMPLETE_PATH : PARTIAL_PATH );
+
+ return pathResult;
+ }
+
+
+ //-----------------------------------------------------------------------------------------------------------------
+ /**
+ * Build a path from bot's current location to an undetermined goal area
+ * that minimizes the given cost along the final path and meets the
+ * goal criteria.
+ */
+ virtual bool ComputeWithOpenGoal( INextBot *bot, const IPathCost &costFunc, const IPathOpenGoalSelector &goalSelector, float maxSearchRadius = 0.0f )
+ {
+ VPROF_BUDGET( "ComputeWithOpenGoal", "NextBot" );
+
+ int teamID = bot->GetEntity()->GetTeamNumber();
+
+ CNavArea *startArea = bot->GetEntity()->GetLastKnownArea();
+
+ if ( startArea == NULL )
+ return NULL;
+
+ startArea->SetParent( NULL );
+
+ // start search
+ CNavArea::ClearSearchLists();
+
+ float initCost = costFunc( startArea, NULL, NULL, NULL, -1.0f );
+ if ( initCost < 0.0f )
+ return NULL;
+
+ startArea->SetTotalCost( initCost );
+ startArea->AddToOpenList();
+
+ // find our goal as we search
+ CNavArea *goalArea = NULL;
+
+ //
+ // Dijkstra's algorithm (since we don't know our goal).
+ //
+ while( !CNavArea::IsOpenListEmpty() )
+ {
+ // get next area to check
+ CNavArea *area = CNavArea::PopOpenList();
+
+ area->AddToClosedList();
+
+ // don't consider blocked areas
+ if ( area->IsBlocked( teamID ) )
+ continue;
+
+ // build adjacent area array
+ CollectAdjacentAreas( area );
+
+ // search adjacent areas
+ for( int i=0; i<m_adjAreaIndex; ++i )
+ {
+ CNavArea *newArea = m_adjAreaVector[ i ].area;
+
+ // only visit each area once
+ if ( newArea->IsClosed() )
+ continue;
+
+ // don't consider blocked areas
+ if ( newArea->IsBlocked( teamID ) )
+ continue;
+
+ // don't use this area if it is out of range
+ if ( maxSearchRadius > 0.0f && ( newArea->GetCenter() - bot->GetEntity()->GetAbsOrigin() ).IsLengthGreaterThan( maxSearchRadius ) )
+ continue;
+
+ // determine cost of traversing this area
+ float newCost = costFunc( newArea, area, m_adjAreaVector[ i ].ladder, NULL, -1.0f );
+
+ // don't use adjacent area if cost functor says it is a dead-end
+ if ( newCost < 0.0f )
+ continue;
+
+ if ( newArea->IsOpen() && newArea->GetTotalCost() <= newCost )
+ {
+ // we have already visited this area, and it has a better path
+ continue;
+ }
+ else
+ {
+ // whether this area has been visited or not, we now have a better path to it
+ newArea->SetParent( area, m_adjAreaVector[ i ].how );
+ newArea->SetTotalCost( newCost );
+
+ // use 'cost so far' to hold cumulative cost
+ newArea->SetCostSoFar( newCost );
+
+ // tricky bit here - relying on OpenList being sorted by cost
+ if ( newArea->IsOpen() )
+ {
+ // area already on open list, update the list order to keep costs sorted
+ newArea->UpdateOnOpenList();
+ }
+ else
+ {
+ newArea->AddToOpenList();
+ }
+
+ // keep track of best goal so far
+ goalArea = goalSelector( goalArea, newArea );
+ }
+ }
+ }
+
+ if ( goalArea )
+ {
+ // compile the path details into a usable path
+ AssemblePrecomputedPath( bot, goalArea->GetCenter(), goalArea );
+ return true;
+ }
+
+ // all adjacent areas are likely too far away
+ return false;
+ }
+
+
+ //-----------------------------------------------------------------------------------------------------------------
+ /**
+ * Given the last area in a path with valid parent pointers,
+ * construct the actual path.
+ */
+ void AssemblePrecomputedPath( INextBot *bot, const Vector &goal, CNavArea *endArea )
+ {
+ VPROF_BUDGET( "AssemblePrecomputedPath", "NextBot" );
+
+ const Vector &start = bot->GetPosition();
+
+ // get count
+ int count = 0;
+ CNavArea *area;
+ for( area = endArea; area; area = area->GetParent() )
+ {
+ ++count;
+ }
+
+ // save room for endpoint
+ if ( count > MAX_PATH_SEGMENTS-1 )
+ {
+ count = MAX_PATH_SEGMENTS-1;
+ }
+ else if ( count == 0 )
+ {
+ return;
+ }
+
+ if ( count == 1 )
+ {
+ BuildTrivialPath( bot, goal );
+ return;
+ }
+
+ // assemble path
+ m_segmentCount = count;
+ for( area = endArea; count && area; area = area->GetParent() )
+ {
+ --count;
+ m_path[ count ].area = area;
+ m_path[ count ].how = area->GetParentHow();
+ m_path[ count ].type = ON_GROUND;
+ }
+
+ // append actual goal position
+ m_path[ m_segmentCount ].area = endArea;
+ m_path[ m_segmentCount ].pos = goal;
+ m_path[ m_segmentCount ].ladder = NULL;
+ m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES;
+ m_path[ m_segmentCount ].type = ON_GROUND;
+ ++m_segmentCount;
+
+ // compute path positions
+ if ( ComputePathDetails( bot, start ) == false )
+ {
+ Invalidate();
+ OnPathChanged( bot, NO_PATH );
+ return;
+ }
+
+ // remove redundant nodes and clean up path
+ Optimize( bot );
+
+ PostProcess();
+
+ OnPathChanged( bot, COMPLETE_PATH );
+ }
+
+ /**
+ * Utility function for when start and goal are in the same area
+ */
+ bool BuildTrivialPath( INextBot *bot, const Vector &goal );
+
+ /**
+ * Determine exactly where the path goes between the given two areas
+ * on the path. Return this point in 'crossPos'.
+ */
+ virtual void ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const;
+
+
+private:
+ enum { MAX_PATH_SEGMENTS = 256 };
+ Segment m_path[ MAX_PATH_SEGMENTS ];
+ int m_segmentCount;
+
+ bool ComputePathDetails( INextBot *bot, const Vector &start ); // determine actual path positions
+
+ void Optimize( INextBot *bot );
+ void PostProcess( void );
+ int FindNextOccludedNode( INextBot *bot, int anchor ); // used by Optimize()
+
+ void InsertSegment( Segment newSegment, int i ); // insert new segment at index i
+
+ mutable Vector m_pathPos; // used by GetPosition()
+ mutable Vector m_closePos; // used by GetClosestPosition()
+
+ mutable float m_cursorPos; // current cursor position (distance along path)
+ mutable Data m_cursorData; // used by GetCursorData()
+ mutable bool m_isCursorDataDirty;
+
+ IntervalTimer m_ageTimer; // how old is this path?
+ CHandle< CBaseCombatCharacter > m_subject; // the subject this path leads to
+
+ /**
+ * Build a vector of adjacent areas reachable from the given area
+ */
+ void CollectAdjacentAreas( CNavArea *area )
+ {
+ m_adjAreaIndex = 0;
+
+ const NavConnectVector &adjNorth = *area->GetAdjacentAreas( NORTH );
+ FOR_EACH_VEC( adjNorth, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjNorth[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_NORTH;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjSouth = *area->GetAdjacentAreas( SOUTH );
+ FOR_EACH_VEC( adjSouth, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjSouth[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_SOUTH;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjWest = *area->GetAdjacentAreas( WEST );
+ FOR_EACH_VEC( adjWest, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjWest[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_WEST;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjEast = *area->GetAdjacentAreas( EAST );
+ FOR_EACH_VEC( adjEast, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjEast[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_EAST;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavLadderConnectVector &adjUpLadder = *area->GetLadders( CNavLadder::LADDER_UP );
+ FOR_EACH_VEC( adjUpLadder, it )
+ {
+ CNavLadder *ladder = adjUpLadder[ it ].ladder;
+
+ if ( ladder->m_topForwardArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topForwardArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+
+ if ( ladder->m_topLeftArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topLeftArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+
+ if ( ladder->m_topRightArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topRightArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+ }
+
+ const NavLadderConnectVector &adjDownLadder = *area->GetLadders( CNavLadder::LADDER_DOWN );
+ FOR_EACH_VEC( adjDownLadder, it )
+ {
+ CNavLadder *ladder = adjDownLadder[ it ].ladder;
+
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ if ( ladder->m_bottomArea )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_bottomArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_DOWN;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+ }
+ }
+
+ enum { MAX_ADJ_AREAS = 64 };
+
+ struct AdjInfo
+ {
+ CNavArea *area;
+ CNavLadder *ladder;
+ NavTraverseType how;
+ };
+
+ AdjInfo m_adjAreaVector[ MAX_ADJ_AREAS ];
+ int m_adjAreaIndex;
+
+};
+
+
+inline float Path::GetLength( void ) const
+{
+ if (m_segmentCount <= 0)
+ {
+ return 0.0f;
+ }
+
+ return m_path[ m_segmentCount-1 ].distanceFromStart;
+}
+
+inline bool Path::IsValid( void ) const
+{
+ return (m_segmentCount > 0);
+}
+
+inline void Path::Invalidate( void )
+{
+ m_segmentCount = 0;
+
+ m_cursorPos = 0.0f;
+
+ m_cursorData.pos = vec3_origin;
+ m_cursorData.forward = Vector( 1.0f, 0, 0 );
+ m_cursorData.curvature = 0.0f;
+ m_cursorData.segmentPrior = NULL;
+
+ m_isCursorDataDirty = true;
+
+ m_subject = NULL;
+}
+
+inline const Path::Segment *Path::FirstSegment( void ) const
+{
+ return (IsValid()) ? &m_path[0] : NULL;
+}
+
+inline const Path::Segment *Path::NextSegment( const Segment *currentSegment ) const
+{
+ if (currentSegment == NULL || !IsValid())
+ return NULL;
+
+ int i = currentSegment - m_path;
+
+ if (i < 0 || i >= m_segmentCount-1)
+ {
+ return NULL;
+ }
+
+ return &m_path[ i+1 ];
+}
+
+inline const Path::Segment *Path::PriorSegment( const Segment *currentSegment ) const
+{
+ if (currentSegment == NULL || !IsValid())
+ return NULL;
+
+ int i = currentSegment - m_path;
+
+ if (i < 1 || i >= m_segmentCount)
+ {
+ return NULL;
+ }
+
+ return &m_path[ i-1 ];
+}
+
+inline const Path::Segment *Path::LastSegment( void ) const
+{
+ return ( IsValid() ) ? &m_path[ m_segmentCount-1 ] : NULL;
+}
+
+inline const Vector &Path::GetStartPosition( void ) const
+{
+ return ( IsValid() ) ? m_path[ 0 ].pos : vec3_origin;
+}
+
+inline const Vector &Path::GetEndPosition( void ) const
+{
+ return ( IsValid() ) ? m_path[ m_segmentCount-1 ].pos : vec3_origin;
+}
+
+inline CBaseCombatCharacter *Path::GetSubject( void ) const
+{
+ return m_subject;
+}
+
+inline void Path::MoveCursorToStart( void )
+{
+ m_cursorPos = 0.0f;
+ m_isCursorDataDirty = true;
+}
+
+inline void Path::MoveCursorToEnd( void )
+{
+ m_cursorPos = GetLength();
+ m_isCursorDataDirty = true;
+}
+
+inline void Path::MoveCursor( float value, MoveCursorType type )
+{
+ if ( type == PATH_ABSOLUTE_DISTANCE )
+ {
+ m_cursorPos = value;
+ }
+ else // relative distance
+ {
+ m_cursorPos += value;
+ }
+
+ if ( m_cursorPos < 0.0f )
+ {
+ m_cursorPos = 0.0f;
+ }
+ else if ( m_cursorPos > GetLength() )
+ {
+ m_cursorPos = GetLength();
+ }
+
+ m_isCursorDataDirty = true;
+}
+
+inline float Path::GetCursorPosition( void ) const
+{
+ return m_cursorPos;
+}
+
+inline const Path::Segment *Path::GetCurrentGoal( void ) const
+{
+ return NULL;
+}
+
+inline float Path::GetAge( void ) const
+{
+ return m_ageTimer.GetElapsedTime();
+}
+
+
+#endif // _NEXT_BOT_PATH_H_
+
diff --git a/game/server/NextBot/Path/NextBotPathFollow.cpp b/game/server/NextBot/Path/NextBotPathFollow.cpp
new file mode 100644
index 0000000..58e2e85
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotPathFollow.cpp
@@ -0,0 +1,1923 @@
+// NextBotPathFollow.cpp
+// Path following
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "BasePropDoor.h"
+
+#include "nav_mesh.h"
+#include "NextBot.h"
+#include "NextBotPathFollow.h"
+#include "NextBotUtil.h"
+
+#include "NextBotLocomotionInterface.h"
+#include "NextBotBodyInterface.h"
+#include "NextBotVisionInterface.h"
+
+#include "tier0/vprof.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+ConVar NextBotSpeedLookAheadRange( "nb_speed_look_ahead_range", "150", FCVAR_CHEAT );
+ConVar NextBotGoalLookAheadRange( "nb_goal_look_ahead_range", "50", FCVAR_CHEAT );
+ConVar NextBotLadderAlignRange( "nb_ladder_align_range", "50", FCVAR_CHEAT );
+
+ConVar NextBotAllowAvoiding( "nb_allow_avoiding", "1", FCVAR_CHEAT );
+ConVar NextBotAllowClimbing( "nb_allow_climbing", "1", FCVAR_CHEAT );
+ConVar NextBotAllowGapJumping( "nb_allow_gap_jumping", "1", FCVAR_CHEAT );
+
+ConVar NextBotDebugClimbing( "nb_debug_climbing", "0", FCVAR_CHEAT );
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Constructor
+ */
+PathFollower::PathFollower( void )
+{
+ m_goal = NULL;
+ m_didAvoidCheck = false;
+
+ m_avoidTimer.Invalidate();
+ m_waitTimer.Invalidate();
+ m_hindrance = NULL;
+
+ m_minLookAheadRange = -1.0f;
+
+ // was 10.0f for L4D - need a better solution here (MSB 5/15/09)
+ m_goalTolerance = 25.0f;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+class CDetachPath
+{
+public:
+ CDetachPath( PathFollower *path )
+ {
+ m_path = path;
+ }
+
+ bool operator() ( INextBot *bot )
+ {
+ bot->NotifyPathDestruction( m_path );
+ return true;
+ }
+
+ PathFollower *m_path;
+};
+
+//--------------------------------------------------------------------------------------------------------------
+PathFollower::~PathFollower()
+{
+ // allow bots to detach pointer to me
+ CDetachPath detach( this );
+ TheNextBots().ForEachBot( detach );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * When the path is invalidated, the follower is also reset
+ */
+void PathFollower::Invalidate( void )
+{
+ // extend
+ Path::Invalidate();
+
+ m_goal = NULL;
+
+ m_avoidTimer.Invalidate();
+ m_waitTimer.Invalidate();
+ m_hindrance = NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Invoked when the path is (re)computed (path is valid at the time of this call)
+ */
+void PathFollower::OnPathChanged( INextBot *bot, Path::ResultType result )
+{
+ // start from the beginning
+ m_goal = FirstSegment();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Adjust speed based on path curvature
+ */
+void PathFollower::AdjustSpeed( INextBot *bot )
+{
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ // if we're coming up on a gap jump, or we're in the air, use maximum speed
+ if ( ( m_goal && m_goal->type == JUMP_OVER_GAP ) || !mover->IsOnGround() )
+ {
+ mover->SetDesiredSpeed( mover->GetRunSpeed() );
+ return;
+ }
+
+ MoveCursorToClosestPosition( bot->GetPosition() );
+ const Path::Data &data = GetCursorData();
+
+ // speed based on curvature
+ mover->SetDesiredSpeed( mover->GetRunSpeed() + fabs( data.curvature ) * ( mover->GetWalkSpeed() - mover->GetRunSpeed() ) );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if reached current goal along path
+ * NOTE: Ladder goals are handled elsewhere
+ */
+bool PathFollower::IsAtGoal( INextBot *bot ) const
+{
+ VPROF_BUDGET( "PathFollower::IsAtGoal", "NextBot" );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ IBody *body = bot->GetBodyInterface();
+
+ //
+ // m_goal is the node we are moving toward along the path
+ // current is the node just behind us
+ //
+ const Segment *current = PriorSegment( m_goal );
+ Vector toGoal = m_goal->pos - mover->GetFeet();
+
+// if ( m_goal->type == JUMP_OVER_GAP && !mover->IsOnGround() )
+// {
+// // jumping over a gap, don't skip ahead until we land
+// return false;
+// }
+
+ if ( current == NULL )
+ {
+ // passed goal
+ return true;
+ }
+ else if ( m_goal->type == DROP_DOWN )
+ {
+ // m_goal is the top of the drop-down, and the following segment is the landing point
+ const Segment *landing = NextSegment( m_goal );
+
+ if ( landing == NULL )
+ {
+ // passed goal or corrupt path
+ return true;
+ }
+ else
+ {
+ // did we reach the ground
+ if ( mover->GetFeet().z - landing->pos.z < mover->GetStepHeight() )
+ {
+ // reached goal
+ return true;
+ }
+ }
+
+ /// @todo: it is possible to fall into a bad place and get stuck - should move back onto the path
+
+ }
+ else if ( m_goal->type == CLIMB_UP )
+ {
+ // once jump is started, assume it is successful, since
+ // nav mesh may be substantially off from actual ground height at landing
+ const Segment *landing = NextSegment( m_goal );
+
+ if ( landing == NULL )
+ {
+ // passed goal or corrupt path
+ return true;
+ }
+ else if ( /*!mover->IsOnGround() && */ mover->GetFeet().z > m_goal->pos.z + mover->GetStepHeight() )
+ {
+ // we're off the ground, presumably climbing - assume we reached the goal
+ return true;
+ }
+ /* This breaks infected climbing up holes in the ceiling - they can get within 2D range of m_goal before finding a ledge to climb up to
+ else if ( mover->IsOnGround() )
+ {
+ // proximity check
+ // Z delta can be anything, since we may be climbing over a tall fence, a physics prop, etc.
+ const float rangeTolerance = 10.0f;
+ if ( toGoal.AsVector2D().IsLengthLessThan( rangeTolerance ) )
+ {
+ // reached goal
+ return true;
+ }
+ }
+ */
+ }
+ else
+ {
+ const Segment *next = NextSegment( m_goal );
+
+ if ( next )
+ {
+ // because mover may be off the path, check if it crossed the plane of the goal
+ // check against average of current and next forward vectors
+ Vector2D dividingPlane;
+
+ if ( current->ladder )
+ {
+ dividingPlane = m_goal->forward.AsVector2D();
+ }
+ else
+ {
+ dividingPlane = current->forward.AsVector2D() + m_goal->forward.AsVector2D();
+ }
+
+ if ( DotProduct2D( toGoal.AsVector2D(), dividingPlane ) < 0.0001f &&
+ abs( toGoal.z ) < body->GetStandHullHeight() )
+ {
+ // only skip higher Z goal if next goal is directly reachable
+ // can't use this for positions below us because we need to be able
+ // to climb over random objects along our path that we can't actually
+ // move *through*
+ if ( toGoal.z < mover->GetStepHeight() && ( mover->IsPotentiallyTraversable( mover->GetFeet(), next->pos ) && !mover->HasPotentialGap( mover->GetFeet(), next->pos ) ) )
+ {
+ // passed goal
+ return true;
+ }
+ }
+ }
+
+ // proximity check
+ // Z delta can be anything, since we may be climbing over a tall fence, a physics prop, etc.
+ if ( toGoal.AsVector2D().IsLengthLessThan( m_goalTolerance ) )
+ {
+ // reached goal
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Move bot along ladder. Return true if ladder motion is in progress, false if complete.
+ */
+bool PathFollower::LadderUpdate( INextBot *bot )
+{
+ VPROF_BUDGET( "PathFollower::LadderUpdate", "NextBot" );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ IBody *body = bot->GetBodyInterface();
+
+ if ( mover->IsUsingLadder() )
+ {
+ // wait for locomotor to finish traversing ladder
+ return true;
+ }
+
+ if ( m_goal->ladder == NULL )
+ {
+ // Check if we have somehow ended up on a ladder, if so, and its a tall down-ladder we are expecting, jump the path ahead.
+ // This happens for players, who run off ledges and the gamemovement sticks them onto ladders. We only care about
+ // tall down-ladders, because up ladders work without this, and short ladders aren't dangerous to miss and drop down
+ // instead of climbing down.
+ if ( bot->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // 'current' is the segment we are on/just passed over
+ const Segment *current = PriorSegment( m_goal );
+ if ( current == NULL )
+ {
+ return false;
+ }
+
+ // Start with current, the segment we are currently traversing. Skip the distance check for that segment, because
+ // the pos is (hopefully) behind us. And if it's a long path segment, it's already outside the climbLookAheadRange,
+ // and thus it would prevent us looking at m_goal and further for imminent planned climbs.
+ // 'current' is the segment we are on/just passed over
+ const float ladderLookAheadRange = 50.0f;
+ for( const Segment *s = current; s; s = NextSegment( s ) )
+ {
+ if ( s != current && ( s->pos - mover->GetFeet() ).AsVector2D().IsLengthGreaterThan( ladderLookAheadRange ) )
+ {
+ break;
+ }
+
+ // Only consider reasonably tall down ladders - if we don't grab onto a short ladder, it hopefully won't be a bad fall.
+ if ( s->ladder != NULL && s->how == GO_LADDER_DOWN && s->ladder->m_length > mover->GetMaxJumpHeight() )
+ {
+ float destinationHeightDelta = s->pos.z - mover->GetFeet().z;
+ if ( fabs(destinationHeightDelta) < mover->GetMaxJumpHeight() )
+ {
+ // Advance the goal, and fall through to the normal codepath.
+ m_goal = s;
+ break;
+ }
+ }
+ }
+ }
+
+ if ( m_goal->ladder == NULL )
+ {
+ // no ladder to use
+ return false;
+ }
+ }
+
+
+ // start using the ladder
+ const float mountRange = 25.0f;
+
+ if ( m_goal->how == GO_LADDER_UP )
+ {
+ // check if we're off the ladder and at the top
+ if ( !mover->IsUsingLadder() && mover->GetFeet().z > m_goal->ladder->m_top.z - mover->GetStepHeight() )
+ {
+ // we're up
+ m_goal = NextSegment( m_goal );
+ return false;
+ }
+
+ // approach the ladder
+ Vector2D to = ( m_goal->ladder->m_bottom - mover->GetFeet() ).AsVector2D();
+
+ body->AimHeadTowards( m_goal->ladder->m_top - 50.0f * m_goal->ladder->GetNormal() + Vector( 0, 0, body->GetCrouchHullHeight() ),
+ IBody::CRITICAL,
+ 2.0f,
+ NULL,
+ "Mounting upward ladder" );
+
+ float range = to.NormalizeInPlace();
+ if ( range < NextBotLadderAlignRange.GetFloat() )
+ {
+ // getting close - line up
+ Vector2D ladderNormal2D = m_goal->ladder->GetNormal().AsVector2D();
+ float dot = DotProduct2D( ladderNormal2D, to );
+
+ const float cos5 = 0.9f;
+ if ( dot < -cos5 )
+ {
+ // lined up - continue approach
+ mover->Approach( m_goal->ladder->m_bottom );
+
+ if ( range < mountRange )
+ {
+ // go up ladder
+ mover->ClimbLadder( m_goal->ladder, m_goal->area );
+ }
+ }
+ else
+ {
+ // rotate around ladder and maintain distance from it
+ Vector myPerp( -to.y, to.x, 0.0f );
+ Vector2D ladderPerp2D( -ladderNormal2D.y, ladderNormal2D.x );
+
+ Vector goal = m_goal->ladder->m_bottom;
+
+ float alignRange = NextBotLadderAlignRange.GetFloat();
+
+ if ( dot < 0.0f )
+ {
+ // we are on the correct side of the ladder
+ // align range should drop off as we reach alignment
+ alignRange = mountRange + (1.0f + dot) * (alignRange - mountRange);
+ }
+
+ goal.x -= alignRange * to.x;
+ goal.y -= alignRange * to.y;
+
+ if ( DotProduct2D( to, ladderPerp2D ) < 0.0f )
+ {
+ goal += 10.0f * myPerp;
+ }
+ else
+ {
+ goal -= 10.0f * myPerp;
+ }
+
+ mover->Approach( goal );
+ }
+ }
+ else
+ {
+ // approach the base of the ladder - use normal path following in case there are jumps/climbs on the way to the ladder
+ return false;
+ }
+ }
+ else // go down ladder
+ {
+ // check if we fell off and are now below the ladder
+ if ( mover->GetFeet().z < m_goal->ladder->m_bottom.z + mover->GetStepHeight() )
+ {
+ // we fell
+ m_goal = NextSegment( m_goal );
+ }
+ else
+ {
+ // approach the ladder
+ Vector mountPoint = m_goal->ladder->m_top + 0.5f * body->GetHullWidth() * m_goal->ladder->GetNormal();
+ Vector2D to = ( mountPoint - mover->GetFeet() ).AsVector2D();
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ const float size = 5.0f;
+ NDebugOverlay::Sphere( mountPoint, size, 255, 0, 255, true, 0.1f );
+ }
+
+ body->AimHeadTowards( m_goal->ladder->m_bottom + 50.0f * m_goal->ladder->GetNormal() + Vector( 0, 0, body->GetCrouchHullHeight() ),
+ IBody::CRITICAL,
+ 1.0f,
+ NULL,
+ "Mounting downward ladder" );
+
+ float range = to.NormalizeInPlace();
+
+ // Approach the top of the ladder. If we're already on the ladder, start descending.
+ if ( range < mountRange || bot->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // go down ladder
+ mover->DescendLadder( m_goal->ladder, m_goal->area );
+
+ // increment goal segment since locomotor will move us along the ladder
+ m_goal = NextSegment( m_goal );
+ }
+ else
+ {
+ // approach the top of the ladder - use normal path following in case there are jumps/climbs on the way to the ladder
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Check if we have reached our current path goal and
+ * iterate to next goal or finish the path
+ */
+bool PathFollower::CheckProgress( INextBot *bot )
+{
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ // skip nearby goal points that are redundant to smooth path following motion
+ const Path::Segment *pSkipToGoal = NULL;
+ if ( m_minLookAheadRange > 0.0f )
+ {
+ pSkipToGoal = m_goal;
+ const Vector &myFeet = mover->GetFeet();
+ while( pSkipToGoal && pSkipToGoal->type == ON_GROUND && mover->IsOnGround() )
+ {
+ if ( ( pSkipToGoal->pos - myFeet ).IsLengthLessThan( m_minLookAheadRange ) )
+ {
+ // goal is too close - step to next segment
+ const Path::Segment *nextSegment = NextSegment( pSkipToGoal );
+
+ if ( !nextSegment || nextSegment->type != ON_GROUND )
+ {
+ // can't skip ahead to next segment - head towards current goal
+ break;
+ }
+
+ if ( nextSegment->pos.z > myFeet.z + mover->GetStepHeight() )
+ {
+ // going uphill or up stairs tends to cause problems if we skip ahead, so don't
+ break;
+ }
+
+#ifdef DOTA_DLL
+ if ( DotProduct( mover->GetMotionVector(), nextSegment->forward ) <= 0.1f )
+ {
+ // don't skip sharp turns
+ break;
+ }
+#endif
+
+ // can we reach the next path segment directly
+ if ( mover->IsPotentiallyTraversable( myFeet, nextSegment->pos ) && !mover->HasPotentialGap( myFeet, nextSegment->pos ) )
+ {
+ pSkipToGoal = nextSegment;
+ }
+ else
+ {
+ // can't directly reach next segment - keep heading towards current goal
+ break;
+ }
+ }
+ else
+ {
+ // goal is farther than min lookahead
+ break;
+ }
+ }
+
+ // didn't find any goal to skip to
+ if ( pSkipToGoal == m_goal )
+ {
+ pSkipToGoal = NULL;
+ }
+ }
+
+ if ( IsAtGoal( bot ) )
+ {
+ // iterate to next segment of the path
+ const Path::Segment *nextSegment = pSkipToGoal ? pSkipToGoal : NextSegment( m_goal );
+
+ if ( nextSegment == NULL )
+ {
+ // must be on ground to complete the path
+ if ( mover->IsOnGround() )
+ {
+ // the end of the path has been reached
+ mover->GetBot()->OnMoveToSuccess( this );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "PathFollower: OnMoveToSuccess\n" );
+ }
+
+ // don't invalidate if OnMoveToSuccess just recomputed a new path
+ if ( GetAge() > 0.0f )
+ {
+ Invalidate();
+ }
+
+ return false;
+ }
+ }
+ else
+ {
+ // keep moving
+ m_goal = nextSegment;
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) && !mover->IsPotentiallyTraversable( mover->GetFeet(), nextSegment->pos ) )
+ {
+ Warning( "PathFollower: path to my goal is blocked by something\n" );
+ NDebugOverlay::Sphere( m_goal->pos, 5.f, 255, 0, 0, true, 3.f );
+ }
+ }
+ }
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Move mover along path
+ */
+void PathFollower::Update( INextBot *bot )
+{
+ VPROF_BUDGET( "PathFollower::Update", "NextBotSpiky" );
+
+ // track most recent path followed
+ bot->SetCurrentPath( this );
+
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ if ( !IsValid() || m_goal == NULL )
+ {
+ return;
+ }
+
+ if ( !m_waitTimer.IsElapsed() )
+ {
+ // still waiting
+ //mover->ClearStuckStatus( "Waiting for blocker to move" );
+ return;
+ }
+
+// m_didAvoidCheck = false;
+
+
+ if ( LadderUpdate( bot ) )
+ {
+ // we are traversing a ladder
+ return;
+ }
+
+
+ // adjust speed based on path curvature
+ AdjustSpeed( bot );
+
+ if ( CheckProgress( bot ) == false )
+ {
+ // goal reached
+ return;
+ }
+
+ // use the direction towards the goal as 'forward' direction
+ Vector forward = m_goal->pos - mover->GetFeet();
+
+ if ( m_goal->type == CLIMB_UP )
+ {
+ const Segment *next = NextSegment( m_goal );
+ if ( next )
+ {
+ // use landing of climb up as forward to help ledge detection
+ forward = next->pos - mover->GetFeet();
+ }
+ }
+
+ forward.z = 0.0f;
+
+ float goalRange = forward.NormalizeInPlace();
+
+ Vector left( -forward.y, forward.x, 0.0f );
+
+ if ( left.IsZero() )
+ {
+ // if left is zero, forward must also be - path follow failure
+ mover->GetBot()->OnMoveToFailure( this, FAIL_STUCK );
+
+ // don't invalidate if OnMoveToFailure just recomputed a new path
+ if ( GetAge() > 0.0f )
+ {
+ Invalidate();
+ }
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "PathFollower: OnMoveToFailure( FAIL_STUCK ) because forward and left are ZERO\n" );
+ }
+
+ return;
+ }
+
+ // unit vectors must follow floor slope
+ const Vector &normal = mover->GetGroundNormal();
+
+ // get forward vector along floor
+ forward = CrossProduct( left, normal );
+
+ // correct the sideways vector
+ left = CrossProduct( normal, forward );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ float axisSize = 25.0f;
+ NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * forward, 255, 0, 0, true, 0.1f );
+ NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * normal, 0, 255, 0, true, 0.1f );
+ NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * left, 0, 0, 255, true, 0.1f );
+ }
+
+ // climb up ledges
+ if ( !Climbing( bot, m_goal, forward, left, goalRange ) )
+ {
+ // a failed climb could mean an invalid path
+ if ( !IsValid() )
+ {
+ return;
+ }
+
+ // jump over gaps
+ JumpOverGaps( bot, m_goal, forward, left, goalRange );
+ }
+
+ // event callbacks from the above climbs and jumps may invalidate the path
+ if ( !IsValid() )
+ {
+ return;
+ }
+
+ // if our movement goal is high above us, we must have fallen
+ CNavArea *myArea = bot->GetEntity()->GetLastKnownArea();
+ bool isOnStairs = ( myArea && myArea->HasAttributes( NAV_MESH_STAIRS ) );
+
+ // limit too high distance to reasonable value for bots that can climb very high
+ float tooHighDistance = mover->GetMaxJumpHeight();
+
+ if ( !m_goal->ladder && !mover->IsClimbingOrJumping() && !isOnStairs && m_goal->pos.z > mover->GetFeet().z + tooHighDistance )
+ {
+ const float closeRange = 25.0f; // 75.0f;
+ Vector2D to( mover->GetFeet().x - m_goal->pos.x, mover->GetFeet().y - m_goal->pos.y );
+ if ( mover->IsStuck() || to.IsLengthLessThan( closeRange ) )
+ {
+ // the goal is too high to reach
+
+ // check if we can reach the next segment, in case this was a "jump down" situation
+ const Path::Segment *next = NextSegment( m_goal );
+ if ( mover->IsStuck() || !next || ( next->pos.z - mover->GetFeet().z > mover->GetMaxJumpHeight() ) || !mover->IsPotentiallyTraversable( mover->GetFeet(), next->pos ) )
+ {
+ // the next node is too high, too - we really did fall off the path
+ mover->GetBot()->OnMoveToFailure( this, FAIL_FELL_OFF );
+
+ // don't invalidate if OnMoveToFailure just recomputed a new path
+ if ( GetAge() > 0.0f )
+ {
+ Invalidate();
+ }
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "PathFollower: OnMoveToFailure( FAIL_FELL_OFF )\n" );
+ }
+
+ // reset stuck status since we're (likely) repathing anyways. otherwise, we could be stuck in a loop here and not move
+ mover->ClearStuckStatus( "Fell off path" );
+
+ return;
+ }
+ }
+ }
+
+
+ Vector goalPos = m_goal->pos;
+
+ // avoid small obstacles
+ forward = goalPos - mover->GetFeet();
+ forward.z = 0.0f;
+ float rangeToGoal = forward.NormalizeInPlace();
+
+ left.x = -forward.y;
+ left.y = forward.x;
+ left.z = 0.0f;
+
+ if ( true || m_goal != LastSegment() ) // think more about this - we often need to avoid to reach the final goal pos, too (MSB 5/15/09)
+ {
+ const float nearLedgeRange = 50.0f;
+ if ( rangeToGoal > nearLedgeRange || ( m_goal && m_goal->type != CLIMB_UP ) )
+ {
+ goalPos = Avoid( bot, goalPos, forward, left );
+ }
+ }
+
+ // face towards movement goal
+ if ( mover->IsOnGround() )
+ {
+ mover->FaceTowards( goalPos );
+ }
+
+ // move bot along path
+ mover->Approach( goalPos );
+
+ // Currently, Approach determines STAND or CROUCH.
+ // Override this if we're approaching a climb or a jump
+ if ( m_goal && ( m_goal->type == CLIMB_UP || m_goal->type == JUMP_OVER_GAP ) )
+ {
+ bot->GetBodyInterface()->SetDesiredPosture( IBody::STAND );
+ }
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ const Segment *start = GetCurrentGoal();
+ if ( start )
+ {
+ start = PriorSegment( start );
+ }
+
+ Draw( start );
+
+ /*
+ else
+ {
+ DrawInterpolated( 0.0f, GetLength() );
+ }
+ */
+
+ NDebugOverlay::Cross3D( goalPos, 5.0f, 150, 150, 255, true, 0.1f );
+ NDebugOverlay::Line( bot->GetEntity()->WorldSpaceCenter(), goalPos, 255, 255, 0, true, 0.1f );
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * If entity is returned, it is blocking us from continuing along our path
+ */
+CBaseEntity *PathFollower::FindBlocker( INextBot *bot )
+{
+ IIntention *think = bot->GetIntentionInterface();
+
+ // if we don't care about hindrances, don't do the expensive tests
+ if ( think->IsHindrance( bot, IS_ANY_HINDRANCE_POSSIBLE ) != ANSWER_YES )
+ return NULL;
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ IBody *body = bot->GetBodyInterface();
+
+ trace_t result;
+ NextBotTraceFilterOnlyActors filter( bot->GetEntity(), COLLISION_GROUP_NONE );
+
+ const float size = body->GetHullWidth()/4.0f; // keep this small to avoid lockups when groups of bots get close
+ Vector blockerMins( -size, -size, mover->GetStepHeight() );
+ Vector blockerMaxs( size, size, body->GetCrouchHullHeight() );
+
+ Vector from = mover->GetFeet();
+ float range = 0.0f;
+
+ const float maxHindranceRangeAlong = 750.0f;
+
+ // because our path goal may be far ahead of us if the way to there is unobstructed, we
+ // need to start looking from the point of the path we are actually standing on
+ MoveCursorToClosestPosition( mover->GetFeet() );
+
+ for( const Segment *s = GetCursorData().segmentPrior; s && range < maxHindranceRangeAlong; s = NextSegment( s ) )
+ {
+ // trace along direction toward goal a minimum range, in case goal and hindrance are
+ // very close, but goal is closer
+
+ Vector traceForward = s->pos - from;
+ float traceRange = traceForward.NormalizeInPlace();
+
+ const float minTraceRange = 2.0f * body->GetHullWidth();
+ if ( traceRange < minTraceRange )
+ {
+ traceRange = minTraceRange;
+ }
+
+ mover->TraceHull( from, from + traceRange * traceForward, blockerMins, blockerMaxs, body->GetSolidMask(), &filter, &result );
+
+ if ( result.DidHitNonWorldEntity() )
+ {
+ // if blocker is close, they could be behind us - check
+ Vector toBlocker = result.m_pEnt->GetAbsOrigin() - bot->GetLocomotionInterface()->GetFeet();
+
+ Vector alongPath = s->pos - from;
+ alongPath.z = 0.0f;
+
+ if ( DotProduct( toBlocker, alongPath ) > 0.0f )
+ {
+ // ask the bot if this really is a hindrance
+ if ( think->IsHindrance( bot, result.m_pEnt ) == ANSWER_YES )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Circle( bot->GetLocomotionInterface()->GetFeet(), QAngle( -90.0f, 0, 0 ), 10.0f, 255, 0, 0, 255, true, 1.0f );
+ NDebugOverlay::HorzArrow( bot->GetLocomotionInterface()->GetFeet(), result.m_pEnt->GetAbsOrigin(), 1.0f, 255, 0, 0, 255, true, 1.0f );
+ }
+
+ // we are blocked
+ return result.m_pEnt;
+ }
+ }
+ }
+
+ from = s->pos;
+ range += s->length;
+ }
+
+ return NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Do reflex avoidance movements of very nearby obstacles.
+ * Return adjusted goal.
+ */
+Vector PathFollower::Avoid( INextBot *bot, const Vector &goalPos, const Vector &forward, const Vector &left )
+{
+ VPROF_BUDGET( "PathFollower::Avoid", "NextBotExpensive" );
+
+ if ( !NextBotAllowAvoiding.GetBool() )
+ {
+ return goalPos;
+ }
+
+ if ( !m_avoidTimer.IsElapsed() )
+ {
+ return goalPos;
+ }
+
+ // low frequency check until we actually hit something we need to avoid
+ const float avoidInterval = 0.5f; // 1.0f;
+ m_avoidTimer.Start( avoidInterval );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+
+ if ( mover->IsClimbingOrJumping() || !mover->IsOnGround() )
+ {
+ return goalPos;
+ }
+
+ //
+ // Check for potential blockers along our path and wait if we're blocked
+ //
+ m_hindrance = FindBlocker( bot );
+ if ( m_hindrance != NULL )
+ {
+ // wait
+ m_waitTimer.Start( avoidInterval * RandomFloat( 1.0f, 2.0f ) );
+
+ return mover->GetFeet();
+ }
+
+
+ // if we are in a "precise" area, do not use avoid volumes
+ CNavArea *area = bot->GetEntity()->GetLastKnownArea();
+ if ( area && ( area->GetAttributes() & NAV_MESH_PRECISE ) )
+ {
+ return goalPos;
+ }
+
+ m_didAvoidCheck = true;
+
+ // we want to avoid other players, etc
+ trace_t result;
+ NextBotTraceFilterOnlyActors filter( bot->GetEntity(), COLLISION_GROUP_NONE );
+
+ IBody *body = bot->GetBodyInterface();
+ unsigned int mask = body->GetSolidMask();
+
+ const float size = body->GetHullWidth()/4.0f;
+ const float offset = size + 2.0f;
+
+ float range = mover->IsRunning() ? 50.0f : 30.0f;
+ range *= bot->GetEntity()->GetModelScale();
+
+ m_hullMin = Vector( -size, -size, mover->GetStepHeight()+0.1f );
+
+ // only use crouch-high avoid volumes, since we'll just crouch if higher obstacles are near
+ m_hullMax = Vector( size, size, body->GetCrouchHullHeight() );
+
+ Vector nextStepHullMin( -size, -size, 2.0f * mover->GetStepHeight() + 0.1f );
+
+ // avoid any open doors in our way
+ CBasePropDoor *door = NULL;
+
+ // check left side
+ m_leftFrom = mover->GetFeet() + offset * left;
+ m_leftTo = m_leftFrom + range * forward;
+
+ m_isLeftClear = true;
+ float leftAvoid = 0.0f;
+
+ NextBotTraversableTraceFilter traverseFilter( bot );
+ mover->TraceHull( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, mask, &traverseFilter, &result );
+ if ( result.fraction < 1.0f || result.startsolid )
+ {
+ // if this sensor is starting in a solid, set fraction to emulate being against a wall
+ if ( result.startsolid )
+ {
+ result.fraction = 0.0f;
+ }
+
+ leftAvoid = clamp( 1.0f - result.fraction, 0.0f, 1.0f );
+
+ m_isLeftClear = false;
+
+ // track any doors we need to avoid
+ if ( result.DidHitNonWorldEntity() )
+ {
+ door = dynamic_cast< CBasePropDoor * >( result.m_pEnt );
+ }
+
+ // check for steps
+// float firstHit = result.fraction;
+// mover->TraceHull( m_leftFrom, m_leftTo, nextStepHullMin, m_hullMax, mask, &filter, &result );
+// if ( result.fraction <= firstHit ) //+ mover->GetStepHeight()/2.0f )
+// {
+// // it's not a step - we hit something
+// m_isLeftClear = false;
+// }
+ }
+
+ // check right side
+ m_rightFrom = mover->GetFeet() - offset * left;
+ m_rightTo = m_rightFrom + range * forward;
+
+ m_isRightClear = true;
+ float rightAvoid = 0.0f;
+
+ mover->TraceHull( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, mask, &traverseFilter, &result );
+ if ( result.fraction < 1.0f || result.startsolid )
+ {
+ // if this sensor is starting in a solid, set fraction to emulate being against a wall
+ if ( result.startsolid )
+ {
+ result.fraction = 0.0f;
+ }
+
+ rightAvoid = clamp( 1.0f - result.fraction, 0.0f, 1.0f );
+
+ m_isRightClear = false;
+
+ // track any doors we need to avoid
+ if ( !door && result.DidHitNonWorldEntity() )
+ {
+ door = dynamic_cast< CBasePropDoor * >( result.m_pEnt );
+ }
+
+ // check for steps
+// float firstHit = result.fraction;
+// mover->TraceHull( m_rightFrom, m_rightTo, nextStepHullMin, m_hullMax, mask, &filter, &result );
+// if ( result.fraction <= firstHit ) // + mover->GetStepHeight()/2.0f)
+// {
+// // it's not a step - we hit something
+// m_isRightClear = false;
+// }
+ }
+
+ Vector adjustedGoal = goalPos;
+
+ // avoid doors directly in our way
+ if ( door && !m_isLeftClear && !m_isRightClear )
+ {
+ Vector forward, right, up;
+ AngleVectors( door->GetAbsAngles(), &forward, &right, &up );
+
+ const float doorWidth = 100.0f;
+ Vector doorEdge = door->GetAbsOrigin() - doorWidth * right;
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Axis( door->GetAbsOrigin(), door->GetAbsAngles(), 20.0f, true, 10.0f );
+ NDebugOverlay::Line( door->GetAbsOrigin(), doorEdge, 255, 255, 0, true, 10.0f );
+ }
+
+ // move around door
+ adjustedGoal.x = doorEdge.x;
+ adjustedGoal.y = doorEdge.y;
+
+ // do avoid check again next frame
+ m_avoidTimer.Invalidate();
+ }
+ else if ( !m_isLeftClear || !m_isRightClear )
+ {
+ // adjust goal to avoid small obstacle
+ float avoidResult = 0.0f;
+ if ( m_isLeftClear )
+ {
+ avoidResult = -rightAvoid;
+ }
+ else if (m_isRightClear)
+ {
+ avoidResult = leftAvoid;
+ }
+ else
+ {
+ // both left and right are blocked, avoid nearest
+ const float equalTolerance = 0.01f;
+ if ( fabs( rightAvoid - leftAvoid ) < equalTolerance )
+ {
+ // squarely against a wall, etc
+ return adjustedGoal;
+ }
+ else if ( rightAvoid > leftAvoid )
+ {
+ avoidResult = -rightAvoid;
+ }
+ else
+ {
+ avoidResult = leftAvoid;
+ }
+ }
+
+ // adjust goal to avoid obstacle
+ Vector avoidDir = 0.5f * forward - left * avoidResult;
+ avoidDir.NormalizeInPlace();
+
+ adjustedGoal = mover->GetFeet() + 100.0f * avoidDir;
+
+ // do avoid check again next frame
+ m_avoidTimer.Invalidate();
+ }
+
+ return adjustedGoal;
+}
+
+
+#ifdef EXPERIMENTAL_LEDGE_FINDER
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Given a hull that defines the area of space that may contain a climbable ledge,
+ * subdivide it until we find the ledge.
+ */
+bool PathFollower::FindClimbLedge( INextBot *bot, Vector startTracePos, Vector ledgeRegionMins, Vector ledgeRegionMaxs )
+{
+ float deltaZ = ledgeRegionMaxs.z - ledgeRegionMins.z;
+
+ if ( deltaZ <= bot->GetLocomotionInterface()->GetStepHeight() )
+ {
+ // reached minimum subdivision limit - stop
+ return false;
+ }
+
+ trace_t result;
+ NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY );
+
+ mover->TraceHull( startTracePos, startTracePos,
+ ledgeRegionMins, ledgeRegionMaxs,
+ bot->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+
+ if ( result.DidHit() )
+ {
+ // volume is blocked - split into upper and lower volumes and try again
+ float midZ = ( ledgeRegionMins.z + ledgeRegionMaxs.z ) / 2.0f;
+
+ Vector upperLedgeRegionMins( ledgeRegionMins.x, ledgeRegionMins.y, midZ );
+ Vector upperLedgeRegionMaxs = ledgeRegionMaxs;
+ FindClimbLedge( bot, startTracePos, upperLedgeRegionMins, upperLedgeRegionMaxs );
+
+
+ Vector lowerLedgeRegionMins = ledgeRegionMins;
+ Vector lowerLedgeRegionMaxs( ledgeRegionMaxs.x, ledgeRegionMaxs.y, midZ );
+ FindClimbLedge( bot, startTracePos, lowerLedgeRegionMins, lowerLedgeRegionMaxs );
+ }
+ else
+ {
+ // volume is clear, trace straight down to find ledge and keep lowest one we've found
+ mover->TraceHull( startTracePos,
+ startTracePos + Vector( 0, 0, -100.0f ),
+ ledgeRegionMins, ledgeRegionMaxs,
+ bot->GetBodyInterface()->GetSolidMask(), &filter, &result );
+ }
+}
+#endif // _DEBUG
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Climb up ledges
+ */
+bool PathFollower::Climbing( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &right, float goalRange )
+{
+ VPROF_BUDGET( "PathFollower::Climbing", "NextBot" );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ IBody *body = bot->GetBodyInterface();
+ CNavArea *myArea = bot->GetEntity()->GetLastKnownArea();
+
+ if ( !mover->IsAbleToClimb() || !NextBotAllowClimbing.GetBool() )
+ {
+ return false;
+ }
+
+ // use the 2D direction towards our goal
+ Vector climbDirection = forward;
+ climbDirection.z = 0.0f;
+ climbDirection.NormalizeInPlace();
+
+ // we can't have this as large as our hull width, or we'll find ledges ahead of us
+ // that we will fall from when we climb up because our hull wont actually touch at the top.
+ const float ledgeLookAheadRange = body->GetHullWidth() - 1;
+
+ if ( mover->IsClimbingOrJumping() || mover->IsAscendingOrDescendingLadder() || !mover->IsOnGround() )
+ {
+ return false;
+ }
+
+ // can be in any posture when we climb
+
+ if ( m_goal == NULL )
+ {
+ return false;
+ }
+
+ if ( TheNavMesh->IsAuthoritative() )
+ {
+ //
+ // Trust what that nav mesh tells us.
+ // No need for expensive ledge-finding for games with simpler geometry (like TF2)
+ //
+ if ( m_goal->type == CLIMB_UP )
+ {
+ const Segment *afterClimb = NextSegment( m_goal );
+ if ( afterClimb && afterClimb->area )
+ {
+ // find closest point on climb-destination area
+ Vector nearClimbGoal;
+ afterClimb->area->GetClosestPointOnArea( mover->GetFeet(), &nearClimbGoal );
+
+ climbDirection = nearClimbGoal - mover->GetFeet();
+ climbDirection.z = 0.0f;
+ climbDirection.NormalizeInPlace();
+
+ if ( mover->ClimbUpToLedge( nearClimbGoal, climbDirection, NULL ) )
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ // If we're approaching a CLIMB_UP link, save off the height delta for it, and trust the nav *just* enough
+ // to climb up to that ledge and only that ledge. We keep as large a tolerance as possible, to trust
+ // the nav as little as possible. There's no valid way to have another CLIMB_UP link within crouch height,
+ // because we can't actually fit in between the two areas, so one climb is invalid.
+ float climbUpLedgeHeightDelta = -1.0f;
+ const float climbUpLedgeTolerance = body->GetCrouchHullHeight();
+
+ if ( m_goal->type == CLIMB_UP )
+ {
+ const Segment *afterClimb = NextSegment( m_goal );
+ if ( afterClimb && afterClimb->area )
+ {
+ // find closest point on climb-destination area
+ Vector nearClimbGoal;
+ afterClimb->area->GetClosestPointOnArea( mover->GetFeet(), &nearClimbGoal );
+
+ climbDirection = nearClimbGoal - mover->GetFeet();
+ climbUpLedgeHeightDelta = climbDirection.z;
+ climbDirection.z = 0.0f;
+ climbDirection.NormalizeInPlace();
+ }
+ }
+
+ // don't try to climb up stairs
+ if ( m_goal->area->HasAttributes( NAV_MESH_STAIRS ) || ( myArea && myArea->HasAttributes( NAV_MESH_STAIRS ) ) )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Cross3D( mover->GetFeet(), 5.0f, 0, 255, 255, true, 5.0f );
+ DevMsg( "%3.2f: %s ON STAIRS\n", gpGlobals->curtime, bot->GetDebugIdentifier() );
+ }
+ return false;
+ }
+
+ // 'current' is the segment we are on/just passed over
+ const Segment *current = PriorSegment( m_goal );
+ if ( current == NULL )
+ {
+ return false;
+ }
+
+ // If path segment immediately ahead of us is not obstructed, don't try to climb.
+ // This is required to try to avoid accidentally climbing onto valid high ledges when we really want to run UNDER them to our destination.
+ // We need to check "immediate" traversability to pay attention to breakable objects in our way that we should climb over.
+ // We also need to check traversability out to 2 * ledgeLookAheadRange in case our goal is just before a tricky ledge climb and once we pass the goal it will be too late.
+ // When we're in a CLIMB_UP segment, allow us to look for ledges - we know the destination ledge height, and will only grab the correct ledge.
+ Vector toGoal = m_goal->pos - mover->GetFeet();
+ toGoal.NormalizeInPlace();
+
+ if ( toGoal.z < mover->GetTraversableSlopeLimit() &&
+ !mover->IsStuck() && m_goal->type != CLIMB_UP &&
+ mover->IsPotentiallyTraversable( mover->GetFeet(), mover->GetFeet() + 2.0f * ledgeLookAheadRange * toGoal, ILocomotion::IMMEDIATELY ) )
+ {
+ return false;
+ }
+
+
+ // can't do this - we have to find the ledge to deal with breakable railings
+#if 0
+ // If our path requires a climb, do the climb.
+ // This solves some issues where there are several possible climbable ledges at a given
+ // location, and we need to know which ledge to climb - just use the preplanned path's choice.
+ const Segment *ledge = NextSegment( m_goal );
+ if ( m_goal->type == CLIMB_UP && ledge )
+ {
+ const float startClimbRange = body->GetHullWidth();
+ if ( ( m_goal->pos - mover->GetFeet() ).IsLengthLessThan( startClimbRange ) )
+ {
+ mover->ClimbUpToLedge( ledge->pos, climbDirection );
+ return true;
+ }
+ }
+#endif
+
+
+
+ // Determine if we're approaching a planned climb.
+ // Start with current, the segment we are currently traversing. Skip the distance check for that segment, because
+ // the pos is (hopefully) behind us. And if it's a long path segment, it's already outside the climbLookAheadRange,
+ // and thus it would prevent us looking at m_goal and further for imminent planned climbs.
+ const float climbLookAheadRange = 150.0f;
+ bool isPlannedClimbImminent = false;
+ float plannedClimbZ = 0.0f;
+ for( const Segment *s = current; s; s = NextSegment( s ) )
+ {
+ if ( s != current && ( s->pos - mover->GetFeet() ).AsVector2D().IsLengthGreaterThan( climbLookAheadRange ) )
+ {
+ break;
+ }
+
+ if ( s->type == CLIMB_UP )
+ {
+ isPlannedClimbImminent = true;
+
+ const Segment *next = NextSegment( s );
+ if ( next )
+ {
+ plannedClimbZ = next->pos.z;
+ }
+ break;
+ }
+ }
+
+ unsigned int mask = body->GetSolidMask();
+ trace_t result;
+ NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY );
+
+ const float hullWidth = body->GetHullWidth();
+ const float halfSize = hullWidth / 2.0f;
+ const float minHullHeight = body->GetCrouchHullHeight();
+ const float minLedgeHeight = mover->GetStepHeight() + 0.1f;
+
+ Vector skipStepHeightHullMin( -halfSize, -halfSize, minLedgeHeight );
+
+ // need to use minimum actual hull height here to catch porous fences and railings
+ Vector skipStepHeightHullMax( halfSize, halfSize, minHullHeight + 0.1f );
+
+
+ // Find the highest height we can stand at our current location.
+ // Using the full width hull catches on small lips/ledges, so back up and try again.
+ float ceilingFraction;
+
+ // We can't use IsPotentiallyTraversable to test for ledges, because it's smaller Hull can cause the
+ // next trace (trace the ceiling height forward) to start solid.
+ // mover->IsPotentiallyTraversable( mover->GetFeet(), mover->GetFeet() + Vector( 0, 0, mover->GetMaxJumpHeight() ), ILocomotion::IMMEDIATELY, &ceilingFraction );
+
+ // Instead of IsPotentiallyTraversable, we back up the same distance and use a second upward trace
+ // to see if that one finds a higher ceiling. If so, we use that ceiling height, and use the
+ // backed-up feet position for the ledge finding traces.
+ Vector feet( mover->GetFeet() );
+ Vector ceiling( feet + Vector( 0, 0, mover->GetMaxJumpHeight() ) );
+ mover->TraceHull( feet, ceiling,
+ skipStepHeightHullMin, skipStepHeightHullMax, mask, &filter, &result );
+ ceilingFraction = result.fraction;
+ bool isBackupTraceUsed = false;
+ if ( ceilingFraction < 1.0f || result.startsolid )
+ {
+ trace_t backupTrace;
+ const float backupDistance = hullWidth * 0.25f; // The IsPotentiallyTraversable check this replaces uses a 1/4 hull width trace
+ Vector backupFeet( feet - climbDirection * backupDistance );
+ Vector backupCeiling( backupFeet + Vector( 0, 0, mover->GetMaxJumpHeight() ) );
+ mover->TraceHull( backupFeet, backupCeiling,
+ skipStepHeightHullMin, skipStepHeightHullMax, mask, &filter, &backupTrace );
+ if ( !backupTrace.startsolid && backupTrace.fraction > ceilingFraction )
+ {
+ bot->DebugConColorMsg( NEXTBOT_PATH, Color( 255, 255, 255, 255 ), "%s backing up when looking for max ledge height\n", bot->GetDebugIdentifier() );
+ result = backupTrace;
+ ceilingFraction = result.fraction;
+ feet = backupFeet;
+ ceiling = backupCeiling;
+ isBackupTraceUsed = true;
+ }
+ }
+
+ float maxLedgeHeight = ceilingFraction * mover->GetMaxJumpHeight();
+
+ if ( maxLedgeHeight <= mover->GetStepHeight() )
+ {
+ return false; // early out when we can't even climb StepHeight.
+ }
+
+ //
+ // Check for ledge climbs over things in our way.
+ // Even if we have a CLIMB_UP link in our path, we still need
+ // to find the actual ledge by tracing the local geometry.
+ //
+ Vector climbHullMax( halfSize, halfSize, maxLedgeHeight );
+
+ Vector ledgePos = feet; // to be computed below
+
+ mover->TraceHull( feet,
+ feet + climbDirection * ledgeLookAheadRange,
+ skipStepHeightHullMin, climbHullMax, mask, &filter, &result );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() )
+ {
+ // show ledge-finding hull as we move
+ NDebugOverlay::SweptBox( feet,
+ feet + climbDirection * ledgeLookAheadRange,
+ skipStepHeightHullMin, climbHullMax, vec3_angle,
+ 255, 100, 0, 255, 0.1f );
+ }
+
+ bool wasPotentialLedgeFound = result.DidHit() && !result.startsolid;
+ // To test climbing up past small lips on walls, we can force the bot to run past the overhang and use the backup trace:
+ // wasPotentialLedgeFound = wasPotentialLedgeFound && (result.fraction == 0 || isBackupTraceUsed);
+ if ( wasPotentialLedgeFound )
+ {
+ VPROF_BUDGET( "PathFollower::Climbing( Search for ledge to climb )", "NextBot" );
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() )
+ {
+ // show ledge-finding hull that found a ledge candidate
+ NDebugOverlay::SweptBox( feet,
+ feet + climbDirection * ledgeLookAheadRange,
+ skipStepHeightHullMin, climbHullMax, vec3_angle,
+ 255, 100, 0, 100, 999.9f );
+
+ // show primary climb direction
+ NDebugOverlay::HorzArrow( feet, feet + 50.0f * climbDirection, 2.0f, 0, 255, 0, 255, true, 9999.9f );
+ }
+
+ // what are we climbing over?
+ CBaseEntity *obstacle = result.m_pEnt;
+
+ if ( !result.DidHitNonWorldEntity() || bot->IsAbleToClimbOnto( obstacle ) )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: %s at potential ledge climb\n", gpGlobals->curtime, bot->GetDebugIdentifier() );
+ }
+
+ // the low hull sweep hit an obstacle - note how 'far in' this is
+ float ledgeFrontWallDepth = ledgeLookAheadRange * result.fraction;
+
+ float minLedgeDepth = body->GetHullWidth()/2.0f; // 5.0f;
+ if ( m_goal->type == CLIMB_UP )
+ {
+ // Climbing up to a narrow nav area indicates a narrow ledge. We need to reduce our minLedgeDepth
+ // here or our path will say we should climb but we'll forever fail to find a wide enough ledge.
+ const Segment *afterClimb = NextSegment( m_goal );
+ if ( afterClimb && afterClimb->area )
+ {
+ Vector depthVector = climbDirection * minLedgeDepth;
+ depthVector.z = 0;
+ if ( fabs( depthVector.x ) > afterClimb->area->GetSizeX() )
+ {
+ depthVector.x = (depthVector.x > 0) ? afterClimb->area->GetSizeX() : -afterClimb->area->GetSizeX();
+ }
+ if ( fabs( depthVector.y ) > afterClimb->area->GetSizeY() )
+ {
+ depthVector.y = (depthVector.y > 0) ? afterClimb->area->GetSizeY() : -afterClimb->area->GetSizeY();
+ }
+
+ float areaDepth = depthVector.NormalizeInPlace();
+ minLedgeDepth = MIN( minLedgeDepth, areaDepth );
+ }
+ }
+
+ //
+ // Find the ledge. Start at the lowest jump we can make
+ // and step up until we find the actual ledge.
+ //
+ // The scan is limited to maxLedgeHeight in case our max
+ // jump/climb height is so tall the highest horizontal hull
+ // trace could be on the other side of the ceiling above us
+ //
+
+ float ledgeHeight = minLedgeHeight;
+ const float ledgeHeightIncrement = 0.5f * mover->GetStepHeight();
+
+ bool foundWall = false;
+ bool foundLedge = false;
+
+ // once we have found the ledge's front wall, we must look at least minLedgeDepth farther in to verify it is a ledge
+ // NOTE: This *must* be ledgeLookAheadRange since ledges are compared against the initial trace which was ledgeLookAheadRange deep
+ float ledgeTopLookAheadRange = ledgeLookAheadRange;
+
+ // TODO: we also must look far enough ahead in case the ledge we actually find is "deeper" than the initial wall at the base
+
+ Vector climbHullMin( -halfSize, -halfSize, 0.0f );
+ Vector climbHullMax( halfSize, halfSize, minHullHeight );
+
+ Vector wallPos;
+ float wallDepth = 0.0f;
+
+ bool isLastIteration = false;
+ while( true )
+ {
+ // trace forward to find the wall in front of us, or the empty space of the ledge above us
+ mover->TraceHull( feet + Vector( 0, 0, ledgeHeight ),
+ feet + Vector( 0, 0, ledgeHeight ) + climbDirection * ledgeTopLookAheadRange,
+ climbHullMin, climbHullMax, mask, &filter, &result );
+
+ float traceDepth = ledgeTopLookAheadRange * result.fraction;
+
+ if ( !result.startsolid )
+ {
+ // if trace reached minLedgeDepth farther, this is a potential ledge
+ if ( foundWall )
+ {
+ // TODO: test that potential ledge is flat enough to stand on
+ if ( ( traceDepth - ledgeFrontWallDepth ) > minLedgeDepth )
+ {
+ bool isUsable = true;
+
+ // initialize ledgePos from result of last trace
+ ledgePos = result.endpos;
+
+ // Find the actual ground level on the potential ledge
+ // Only trace back down to the previous ledge height trace.
+ // The ledge can be no lower, or we would've found it in the last iteration.
+ mover->TraceHull( ledgePos,
+ ledgePos + Vector( 0, 0, -ledgeHeightIncrement ),
+ climbHullMin, climbHullMax, mask, &filter, &result );
+
+ ledgePos = result.endpos;
+
+ // if the whole trace is in solid, we're out of luck, but
+ // if the trace just started solid, 'ledgePos' should still be valid
+ // since the trace left the solid and then hit.
+ // if the trace hit nothing, the potential ledge is actually deeper in
+ const float MinGroundNormal = 0.7f; // players can't stand on ground steeper than 0.7
+ if ( result.allsolid || !result.DidHit() || result.plane.normal.z < MinGroundNormal )
+ {
+ // not a usable ledge, try again
+ isUsable = false;
+ }
+ else
+ {
+ if ( climbUpLedgeHeightDelta > 0.0f )
+ {
+ // if we're climbing to a specific ledge via a CLIMB_UP link, only climb to that ledge.
+ // Do this only for the world (which includes static props) so we can still opportunistically
+ // climb up onto breakable railings and physics props.
+ if ( result.DidHitWorld() )
+ {
+ float potentialLedgeHeight = result.endpos.z - feet.z;
+ if ( fabs(potentialLedgeHeight - climbUpLedgeHeightDelta) > climbUpLedgeTolerance )
+ {
+ isUsable = false;
+ }
+ }
+ }
+ }
+
+ if ( isUsable )
+ {
+ // back up until we no longer are hitting the ledge to determine the
+ // exact ledge edge position
+ Vector validLedgePos = ledgePos; // save off a valid ledge pos
+ const float edgeTolerance = 4.0f;
+ const float maxBackUp = hullWidth;
+ float backUpSoFar = edgeTolerance;
+ Vector testPos = ledgePos;
+
+ while( backUpSoFar < maxBackUp )
+ {
+ testPos -= edgeTolerance * climbDirection;
+ backUpSoFar += edgeTolerance;
+
+ mover->TraceHull( testPos,
+ testPos + Vector( 0, 0, -ledgeHeightIncrement ),
+ climbHullMin, climbHullMax, mask, &filter, &result );
+
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() )
+ {
+ // show edge-finder hulls
+ NDebugOverlay::SweptBox( testPos,
+ testPos + Vector( 0, 0, -mover->GetStepHeight() ),
+ climbHullMin, climbHullMax, vec3_angle, 255, 0, 0, 255, 999.9f );
+ }
+
+ if ( result.DidHit() && result.plane.normal.z >= MinGroundNormal )
+ {
+ // we hit, this is closer to the actual ledge edge
+ ledgePos = result.endpos;
+ }
+ else
+ {
+ // nothing but air or a steep slope below us, we have found the edge
+ break;
+ }
+ }
+
+ // we want ledgePos to be right on the edge itself, so move
+ // it ahead by half of the hull width
+ ledgePos += climbDirection * halfSize;
+
+ // Make sure this doesn't embed us in the far wall if the ledge is narrow, since we would
+ // have backed up less than halfSize.
+ Vector climbHullMinStep( climbHullMin ); // skip StepHeight for sloped ledges
+ mover->TraceHull( validLedgePos,
+ ledgePos,
+ climbHullMinStep, climbHullMax, mask, &filter, &result );
+
+ ledgePos = result.endpos;
+
+ // Now since ledgePos + StepHeight is valid, trace down to find ground on sloped ledges.
+ mover->TraceHull( ledgePos + Vector( 0, 0, StepHeight ),
+ ledgePos,
+ climbHullMin, climbHullMax, mask, &filter, &result );
+ if ( !result.startsolid )
+ {
+ ledgePos = result.endpos;
+ }
+ }
+
+
+/*** NOTE: While this saves us from climbing into a window below the window we want to get in,
+ *** it also causes us to climb in midair high over crates sitting against walls we need to climb over.
+ if ( isUsable && m_goal->type == CLIMB_UP )
+ {
+ // we can only accept ledges at least as high as our current CLIMB_UP destination
+ // NOTE: Can't use plannedClimbZ here, since that could be 2 or 3 short climbs ahead
+ const Segment *ledge = NextSegment( m_goal );
+
+ if ( !ledge || ledgeHeight < ledge->pos.z - feet.z - mover->GetStepHeight() )
+ {
+ // this ledge is below the CLIMB_UP destination - can't use it
+ isUsable = false;
+ }
+ }
+*/
+
+
+ if ( isUsable )
+ {
+ // found a usable ledge here
+ foundLedge = true;
+ break;
+ }
+ }
+ }
+ else if ( result.DidHit() )
+ {
+ // this iteration hit the wall under the ledge,
+ // meaning the next iteration that reaches far enough will be our ledge
+
+ // Since we know that our desired route is likely blocked (via the
+ // IsTraversable check above) - any ledge we hit we must climb.
+
+ // found a valid ledge wall
+ foundWall = true;
+ wallDepth = traceDepth;
+
+ // make sure the subsequent traces are at least minLedgeDepth deeper than
+ // the wall we just found, or all ledge checks will fail
+ float minTraceDepth = traceDepth + minLedgeDepth + 0.1f;
+
+ if ( ledgeTopLookAheadRange < minTraceDepth )
+ {
+ ledgeTopLookAheadRange = minTraceDepth;
+ }
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: Climbing - found wall.\n", gpGlobals->curtime );
+ if ( NextBotDebugClimbing.GetBool() )
+ {
+ NDebugOverlay::HorzArrow( result.endpos, result.endpos + 20.0f * result.plane.normal, 5.0f, 255, 100, 0, 255, true, 9999.9f );
+ }
+ wallPos = result.endpos;
+ }
+ }
+ else if ( ledgeHeight > body->GetCrouchHullHeight() && !isPlannedClimbImminent )
+ {
+ // we haven't hit anything yet, and we're already above our heads - no obstacle
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: Climbing - skipping overhead climb we can walk/crawl under.\n", gpGlobals->curtime );
+ }
+ break;
+ }
+ }
+
+ ledgeHeight += ledgeHeightIncrement;
+
+ if ( ledgeHeight >= maxLedgeHeight )
+ {
+ if ( isLastIteration )
+ {
+ // tested at max height
+ break;
+ }
+
+ // check one more time at max jump height
+ isLastIteration = true;
+ ledgeHeight = maxLedgeHeight;
+ }
+ }
+
+ if ( foundLedge )
+ {
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: STARTING LEDGE CLIMB UP\n", gpGlobals->curtime );
+
+ if ( NextBotDebugClimbing.GetBool() )
+ {
+ NDebugOverlay::Cross3D( ledgePos, 10.0f, 0, 255, 0, true, 9999.9f );
+
+ // display approximation of idealized ledge that has been found
+ Vector side( -climbDirection.y, climbDirection.x, 0.0f );
+
+ // this is an approximation, since AABB can hit at any angle
+ Vector base = feet + halfSize * climbDirection;
+
+ Vector wallBottomLeft = base + halfSize * side;
+ Vector wallBottomRight = base - halfSize * side;
+ Vector wallTopLeft = wallBottomLeft + Vector( 0, 0, ledgeHeight );
+ Vector wallTopRight = wallBottomRight + Vector( 0, 0, ledgeHeight );
+
+ NDebugOverlay::Triangle( wallBottomRight, wallBottomLeft, wallTopLeft, 255, 100, 0, 100, true, 9999.9f );
+ NDebugOverlay::Triangle( wallBottomRight, wallTopLeft, wallTopRight, 255, 100, 0, 100, true, 9999.9f );
+
+ Vector ledgeLeft = ledgePos + halfSize * side;
+ Vector ledgeRight = ledgePos - halfSize * side;
+
+ NDebugOverlay::Triangle( wallTopRight, wallTopLeft, ledgeLeft, 0, 100, 255, 100, true, 9999.9f );
+ NDebugOverlay::Triangle( wallTopRight, ledgeLeft, ledgeRight, 0, 100, 255, 100, true, 9999.9f );
+ }
+ }
+
+ if ( !mover->ClimbUpToLedge( ledgePos, climbDirection, obstacle ) )
+ {
+ // climb failed - build a new path in case we're now stuck
+ //Invalidate();
+ return false;
+ }
+
+ return true;
+ }
+ else if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ DevMsg( "%3.2f: CANT FIND LEDGE TO CLIMB\n", gpGlobals->curtime );
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Jump over gaps
+ */
+bool PathFollower::JumpOverGaps( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &right, float goalRange )
+{
+ VPROF_BUDGET( "PathFollower::JumpOverGaps", "NextBot" );
+
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ IBody *body = bot->GetBodyInterface();
+
+ if ( !mover->IsAbleToJumpAcrossGaps() || !NextBotAllowGapJumping.GetBool() )
+ {
+ return false;
+ }
+
+ if ( mover->IsClimbingOrJumping() || mover->IsAscendingOrDescendingLadder() || !mover->IsOnGround() )
+ {
+ return false;
+ }
+
+ if ( !body->IsActualPosture( IBody::STAND ) )
+ {
+ // can't jump if we're not standing
+ return false;
+ }
+
+ if ( m_goal == NULL )
+ {
+ return false;
+ }
+
+ trace_t result;
+ NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY );
+
+ const float hullWidth = ( body ) ? body->GetHullWidth() : 1.0f;
+
+ // 'current' is the segment we are on/just passed over
+ const Segment *current = PriorSegment( m_goal );
+ if ( current == NULL )
+ {
+ return false;
+ }
+
+ const float minGapJumpRange = 2.0f * hullWidth;
+
+ const Segment *gap = NULL;
+
+ if ( current->type == JUMP_OVER_GAP )
+ {
+ gap = current;
+ }
+ else
+ {
+ float searchRange = goalRange;
+ for( const Segment *s = m_goal; s; s = NextSegment( s ) )
+ {
+ if ( searchRange > minGapJumpRange )
+ {
+ break;
+ }
+
+ if ( s->type == JUMP_OVER_GAP )
+ {
+ gap = s;
+ break;
+ }
+
+ searchRange += s->length;
+ }
+ }
+
+ if ( gap )
+ {
+ VPROF_BUDGET( "PathFollower::GapJumping", "NextBot" );
+
+ float halfWidth = hullWidth/2.0f;
+
+ if ( mover->IsGap( mover->GetFeet() + halfWidth * gap->forward, gap->forward ) )
+ {
+ // there is a gap to jump over
+ const Segment *landing = NextSegment( gap );
+ if ( landing )
+ {
+ mover->JumpAcrossGap( landing->pos, landing->forward );
+
+ // if we're jumping over this gap, make sure our goal is the landing so we aim for it
+ m_goal = landing;
+
+ if ( bot->IsDebugging( NEXTBOT_PATH ) )
+ {
+ NDebugOverlay::Cross3D( m_goal->pos, 5.0f, 0, 255, 255, true, 5.0f );
+ DevMsg( "%3.2f: GAP JUMP\n", gpGlobals->curtime );
+ }
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Draw the path for debugging
+ */
+void PathFollower::Draw( const Path::Segment *start ) const
+{
+ if ( m_goal == NULL )
+ return;
+
+ // show avoid volumes
+ if ( m_didAvoidCheck )
+ {
+ QAngle angles( 0, 0, 0 );
+
+ if (m_isLeftClear)
+ NDebugOverlay::SweptBox( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, angles, 0, 255, 0, 255, 0.1f );
+ else
+ NDebugOverlay::SweptBox( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, angles, 255, 0, 0, 255, 0.1f );
+
+ if (m_isRightClear)
+ NDebugOverlay::SweptBox( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, angles, 0, 255, 0, 255, 0.1f );
+ else
+ NDebugOverlay::SweptBox( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, angles, 255, 0, 0, 255, 0.1f );
+
+ const_cast< PathFollower * >( this )->m_didAvoidCheck = false;
+ }
+
+ // highlight current goal segment
+ if ( m_goal )
+ {
+ const float size = 5.0f;
+ NDebugOverlay::Sphere( m_goal->pos, size, 255, 255, 0, true, 0.1f );
+
+ switch( m_goal->how )
+ {
+ case GO_NORTH:
+ case GO_SOUTH:
+ NDebugOverlay::Line( m_goal->m_portalCenter - Vector( m_goal->m_portalHalfWidth, 0, 0 ), m_goal->m_portalCenter + Vector( m_goal->m_portalHalfWidth, 0, 0 ), 255, 0, 255, true, 0.1f );
+ break;
+
+ default:
+ NDebugOverlay::Line( m_goal->m_portalCenter - Vector( 0, m_goal->m_portalHalfWidth, 0 ), m_goal->m_portalCenter + Vector( 0, m_goal->m_portalHalfWidth, 0 ), 255, 0, 255, true, 0.1f );
+ break;
+ }
+
+ // 'current' is the segment we are on/just passed over
+ const Segment *current = PriorSegment( m_goal );
+ if ( current )
+ {
+ NDebugOverlay::Line( current->pos, m_goal->pos, 255, 255, 0, true, 0.1f );
+ }
+ }
+
+ // extend
+ Path::Draw();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return true if there is a the given discontinuity ahead in the path within the given range (-1 = entire remaining path)
+ */
+bool PathFollower::IsDiscontinuityAhead( INextBot *bot, Path::SegmentType type, float range ) const
+{
+ if ( m_goal )
+ {
+ const Path::Segment *current = PriorSegment( m_goal );
+ if ( current && current->type == type )
+ {
+ // we're on the discontinuity now
+ return true;
+ }
+
+ float rangeSoFar = ( m_goal->pos - bot->GetLocomotionInterface()->GetFeet() ).Length();
+
+ for( const Segment *s = m_goal; s; s = NextSegment( s ) )
+ {
+ if ( rangeSoFar >= range )
+ {
+ break;
+ }
+
+ if ( s->type == type )
+ {
+ return true;
+ }
+
+ rangeSoFar += s->length;
+ }
+ }
+
+ return false;
+}
+
+
diff --git a/game/server/NextBot/Path/NextBotPathFollow.h b/game/server/NextBot/Path/NextBotPathFollow.h
new file mode 100644
index 0000000..edb27b5
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotPathFollow.h
@@ -0,0 +1,106 @@
+// NextBotPathFollow.h
+// Path following
+// Author: Michael Booth, April 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_PATH_FOLLOWER_
+#define _NEXT_BOT_PATH_FOLLOWER_
+
+#include "nav_mesh.h"
+#include "nav_pathfind.h"
+#include "NextBotPath.h"
+
+class INextBot;
+class ILocomotion;
+
+
+//--------------------------------------------------------------------------------------------------------
+/**
+ * A PathFollower extends a Path to include mechanisms to move along (follow) it
+ */
+class PathFollower : public Path
+{
+public:
+ PathFollower( void );
+ virtual ~PathFollower();
+
+ virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid
+ virtual void Draw( const Path::Segment *start = NULL ) const; // (EXTEND) draw the path for debugging
+ virtual void OnPathChanged( INextBot *bot, Path::ResultType result ); // invoked when the path is (re)computed (path is valid at the time of this call)
+
+ virtual void Update( INextBot *bot ); // move bot along path
+
+ virtual const Path::Segment *GetCurrentGoal( void ) const; // return current goal along the path we are trying to reach
+
+ virtual void SetMinLookAheadDistance( float value ); // minimum range movement goal must be along path
+
+ virtual CBaseEntity *GetHindrance( void ) const; // returns entity that is hindering our progress along the path
+
+ virtual bool IsDiscontinuityAhead( INextBot *bot, Path::SegmentType type, float range = -1.0f ) const; // return true if there is a the given discontinuity ahead in the path within the given range (-1 = entire remaining path)
+
+ void SetGoalTolerance( float range ); // set tolerance within at which we're considered to be at our goal
+
+private:
+ const Path::Segment *m_goal; // our current goal along the path
+ float m_minLookAheadRange;
+
+ bool CheckProgress( INextBot *bot );
+ bool IsAtGoal( INextBot *bot ) const; // return true if reached current path goal
+
+ //bool IsOnStairs( INextBot *bot ) const; // return true if bot is standing on a stairway
+ bool m_isOnStairs;
+
+ CountdownTimer m_avoidTimer; // do avoid check more often if we recently avoided
+
+ CountdownTimer m_waitTimer; // for waiting for a blocker to move off our path
+ CHandle< CBaseEntity > m_hindrance;
+
+ // debug display data for avoid volumes
+ bool m_didAvoidCheck;
+ Vector m_leftFrom;
+ Vector m_leftTo;
+ bool m_isLeftClear;
+ Vector m_rightFrom;
+ Vector m_rightTo;
+ bool m_isRightClear;
+ Vector m_hullMin, m_hullMax;
+
+ void AdjustSpeed( INextBot *bot ); // adjust speed based on path curvature
+
+ Vector Avoid( INextBot *bot, const Vector &goalPos, const Vector &forward, const Vector &left ); // avoidance movements for very nearby obstacles. returns modified goal position
+ bool Climbing( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &left, float goalRange ); // climb up ledges
+ bool JumpOverGaps( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &left, float goalRange ); // jump over gaps
+
+ bool LadderUpdate( INextBot *bot ); // move bot along ladder
+ CBaseEntity *FindBlocker( INextBot *bot ); // if entity is returned, it is blocking us from continuing along our path
+
+ float m_goalTolerance;
+};
+
+
+inline void PathFollower::SetGoalTolerance( float range )
+{
+ m_goalTolerance = range;
+}
+
+
+inline const Path::Segment *PathFollower::GetCurrentGoal( void ) const
+{
+ return m_goal;
+}
+
+
+inline void PathFollower::SetMinLookAheadDistance( float value )
+{
+ m_minLookAheadRange = value;
+}
+
+inline CBaseEntity *PathFollower::GetHindrance( void ) const
+{
+ return m_hindrance;
+}
+
+
+#endif // _NEXT_BOT_PATH_FOLLOWER_
+
+
diff --git a/game/server/NextBot/Path/NextBotRetreatPath.h b/game/server/NextBot/Path/NextBotRetreatPath.h
new file mode 100644
index 0000000..8d57cb4
--- /dev/null
+++ b/game/server/NextBot/Path/NextBotRetreatPath.h
@@ -0,0 +1,573 @@
+// NextBotRetreatPath.h
+// Maintain and follow a path that leads safely away from the given Actor
+// Author: Michael Booth, February 2007
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_RETREAT_PATH_
+#define _NEXT_BOT_RETREAT_PATH_
+
+#include "nav.h"
+#include "NextBotInterface.h"
+#include "NextBotLocomotionInterface.h"
+#include "NextBotRetreatPath.h"
+#include "NextBotUtil.h"
+#include "NextBotPathFollow.h"
+#include "tier0/vprof.h"
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * A RetreatPath extends a PathFollower to periodically recompute a path
+ * away from a threat, and to move along the path away from that threat.
+ */
+class RetreatPath : public PathFollower
+{
+public:
+ RetreatPath( void );
+ virtual ~RetreatPath() { }
+
+ void Update( INextBot *bot, CBaseEntity *threat ); // update path away from threat and move bot along path
+
+ virtual float GetMaxPathLength( void ) const; // return maximum path length
+
+ virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid
+
+private:
+ void RefreshPath( INextBot *bot, CBaseEntity *threat );
+
+ CountdownTimer m_throttleTimer; // require a minimum time between re-paths
+ EHANDLE m_pathThreat; // the threat of our existing path
+ Vector m_pathThreatPos; // where the threat was when the path was built
+};
+
+inline RetreatPath::RetreatPath( void )
+{
+ m_throttleTimer.Invalidate();
+ m_pathThreat = NULL;
+}
+
+inline float RetreatPath::GetMaxPathLength( void ) const
+{
+ return 1000.0f;
+}
+
+inline void RetreatPath::Invalidate( void )
+{
+ // path is gone, repath at earliest opportunity
+ m_throttleTimer.Invalidate();
+ m_pathThreat = NULL;
+
+ // extend
+ PathFollower::Invalidate();
+}
+
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Maintain a path to our chase threat and move along that path
+ */
+inline void RetreatPath::Update( INextBot *bot, CBaseEntity *threat )
+{
+ VPROF_BUDGET( "RetreatPath::Update", "NextBot" );
+
+ if ( threat == NULL )
+ {
+ return;
+ }
+
+ // if our path threat changed, repath immediately
+ if ( threat != m_pathThreat )
+ {
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) Chase path threat changed (from %X to %X).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_pathThreat.Get(), threat );
+ }
+
+ Invalidate();
+ }
+
+ // maintain the path away from the threat
+ RefreshPath( bot, threat );
+
+ // move along the path towards the threat
+ PathFollower::Update( bot );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Build a path away from retreatFromArea up to retreatRange in length.
+ */
+class RetreatPathBuilder
+{
+public:
+ RetreatPathBuilder( INextBot *me, CBaseEntity *threat, float retreatRange = 500.0f )
+ {
+ m_me = me;
+ m_mover = me->GetLocomotionInterface();
+
+ m_threat = threat;
+ m_retreatRange = retreatRange;
+ }
+
+ CNavArea *ComputePath( void )
+ {
+ VPROF_BUDGET( "NavAreaBuildRetreatPath", "NextBot" );
+
+ if ( m_mover == NULL )
+ return NULL;
+
+ CNavArea *startArea = m_me->GetEntity()->GetLastKnownArea();
+
+ if ( startArea == NULL )
+ return NULL;
+
+ CNavArea *retreatFromArea = TheNavMesh->GetNearestNavArea( m_threat->GetAbsOrigin() );
+ if ( retreatFromArea == NULL )
+ return NULL;
+
+ startArea->SetParent( NULL );
+
+ // start search
+ CNavArea::ClearSearchLists();
+
+ float initCost = Cost( startArea, NULL, NULL );
+ if ( initCost < 0.0f )
+ return NULL;
+
+ int teamID = m_me->GetEntity()->GetTeamNumber();
+
+ startArea->SetTotalCost( initCost );
+
+ startArea->AddToOpenList();
+
+ // keep track of the area farthest away from the threat
+ CNavArea *farthestArea = NULL;
+ float farthestRange = 0.0f;
+
+ //
+ // Dijkstra's algorithm (since we don't know our goal).
+ // Build a path as far away from the retreat area as possible.
+ // Minimize total path length and danger.
+ // Maximize distance to threat of end of path.
+ //
+ while( !CNavArea::IsOpenListEmpty() )
+ {
+ // get next area to check
+ CNavArea *area = CNavArea::PopOpenList();
+
+ area->AddToClosedList();
+
+ // don't consider blocked areas
+ if ( area->IsBlocked( teamID ) )
+ continue;
+
+ // build adjacent area array
+ CollectAdjacentAreas( area );
+
+ // search adjacent areas
+ for( int i=0; i<m_adjAreaIndex; ++i )
+ {
+ CNavArea *newArea = m_adjAreaVector[ i ].area;
+
+ // only visit each area once
+ if ( newArea->IsClosed() )
+ continue;
+
+ // don't consider blocked areas
+ if ( newArea->IsBlocked( teamID ) )
+ continue;
+
+ // don't use this area if it is out of range
+ if ( ( newArea->GetCenter() - m_me->GetEntity()->GetAbsOrigin() ).IsLengthGreaterThan( m_retreatRange ) )
+ continue;
+
+ // determine cost of traversing this area
+ float newCost = Cost( newArea, area, m_adjAreaVector[ i ].ladder );
+
+ // don't use adjacent area if cost functor says it is a dead-end
+ if ( newCost < 0.0f )
+ continue;
+
+ if ( newArea->IsOpen() && newArea->GetTotalCost() <= newCost )
+ {
+ // we have already visited this area, and it has a better path
+ continue;
+ }
+ else
+ {
+ // whether this area has been visited or not, we now have a better path
+ newArea->SetParent( area, m_adjAreaVector[ i ].how );
+ newArea->SetTotalCost( newCost );
+
+ // use 'cost so far' to hold cumulative cost
+ newArea->SetCostSoFar( newCost );
+
+ // tricky bit here - relying on OpenList being sorted by cost
+ if ( newArea->IsOpen() )
+ {
+ // area already on open list, update the list order to keep costs sorted
+ newArea->UpdateOnOpenList();
+ }
+ else
+ {
+ newArea->AddToOpenList();
+ }
+
+ // keep track of area farthest from threat
+ float threatRange = ( newArea->GetCenter() - m_threat->GetAbsOrigin() ).Length();
+ if ( threatRange > farthestRange )
+ {
+ farthestArea = newArea;
+ farthestRange = threatRange;
+ }
+ }
+ }
+ }
+
+ return farthestArea;
+ }
+
+
+ /**
+ * Build a vector of adjacent areas reachable from the given area
+ */
+ void CollectAdjacentAreas( CNavArea *area )
+ {
+ m_adjAreaIndex = 0;
+
+ const NavConnectVector &adjNorth = *area->GetAdjacentAreas( NORTH );
+ FOR_EACH_VEC( adjNorth, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjNorth[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_NORTH;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjSouth = *area->GetAdjacentAreas( SOUTH );
+ FOR_EACH_VEC( adjSouth, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjSouth[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_SOUTH;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjWest = *area->GetAdjacentAreas( WEST );
+ FOR_EACH_VEC( adjWest, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjWest[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_WEST;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavConnectVector &adjEast = *area->GetAdjacentAreas( EAST );
+ FOR_EACH_VEC( adjEast, it )
+ {
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ m_adjAreaVector[ m_adjAreaIndex ].area = adjEast[ it ].area;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_EAST;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
+ ++m_adjAreaIndex;
+ }
+
+ const NavLadderConnectVector &adjUpLadder = *area->GetLadders( CNavLadder::LADDER_UP );
+ FOR_EACH_VEC( adjUpLadder, it )
+ {
+ CNavLadder *ladder = adjUpLadder[ it ].ladder;
+
+ if ( ladder->m_topForwardArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topForwardArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+
+ if ( ladder->m_topLeftArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topLeftArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+
+ if ( ladder->m_topRightArea && m_adjAreaIndex < MAX_ADJ_AREAS )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topRightArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+ }
+
+ const NavLadderConnectVector &adjDownLadder = *area->GetLadders( CNavLadder::LADDER_DOWN );
+ FOR_EACH_VEC( adjDownLadder, it )
+ {
+ CNavLadder *ladder = adjDownLadder[ it ].ladder;
+
+ if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
+ break;
+
+ if ( ladder->m_bottomArea )
+ {
+ m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_bottomArea;
+ m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_DOWN;
+ m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
+ ++m_adjAreaIndex;
+ }
+ }
+ }
+
+ /**
+ * Cost minimizes path length traveled thus far and "danger" (proximity to threat(s))
+ */
+ float Cost( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder )
+ {
+ // check if we can use this area
+ if ( !m_mover->IsAreaTraversable( area ) )
+ {
+ return -1.0f;
+ }
+
+ int teamID = m_me->GetEntity()->GetTeamNumber();
+ if ( area->IsBlocked( teamID ) )
+ {
+ return -1.0f;
+ }
+
+ const float debugDeltaT = 3.0f;
+
+ float cost;
+
+ const float maxThreatRange = 500.0f;
+ const float dangerDensity = 1000.0f;
+
+ if ( fromArea == NULL )
+ {
+ cost = 0.0f;
+
+ if ( area->Contains( m_threat->GetAbsOrigin() ) )
+ {
+ // maximum danger - threat is in the area with us
+ cost += 10.0f * dangerDensity;
+
+ if ( m_me->IsDebugging( INextBot::PATH ) )
+ {
+ area->DrawFilled( 255, 0, 0, 128 );
+ }
+ }
+ else
+ {
+ // danger proportional to range to us
+ float rangeToThreat = ( m_threat->GetAbsOrigin() - m_me->GetEntity()->GetAbsOrigin() ).Length();
+
+ if ( rangeToThreat < maxThreatRange )
+ {
+ cost += dangerDensity * ( 1.0f - ( rangeToThreat / maxThreatRange ) );
+
+ if ( m_me->IsDebugging( INextBot::PATH ) )
+ {
+ NDebugOverlay::Line( m_me->GetEntity()->GetAbsOrigin(), m_threat->GetAbsOrigin(), 255, 0, 0, true, debugDeltaT );
+ }
+ }
+ }
+ }
+ else
+ {
+ // compute distance traveled along path so far
+ float dist;
+
+ if ( ladder )
+ {
+ const float ladderCostFactor = 100.0f;
+ dist = ladderCostFactor * ladder->m_length;
+ }
+ else
+ {
+ Vector to = area->GetCenter() - fromArea->GetCenter();
+
+ dist = to.Length();
+
+ // check for vertical discontinuities
+ Vector closeFrom, closeTo;
+ area->GetClosestPointOnArea( fromArea->GetCenter(), &closeTo );
+ fromArea->GetClosestPointOnArea( area->GetCenter(), &closeFrom );
+
+ float deltaZ = closeTo.z - closeFrom.z;
+
+ if ( deltaZ > m_mover->GetMaxJumpHeight() )
+ {
+ // too high to jump
+ return -1.0f;
+ }
+ else if ( -deltaZ > m_mover->GetDeathDropHeight() )
+ {
+ // too far down to drop
+ return -1.0f;
+ }
+
+ // prefer to maintain our level
+ const float climbCost = 10.0f;
+ dist += climbCost * fabs( deltaZ );
+ }
+
+ cost = dist + fromArea->GetTotalCost();
+
+
+ // Add in danger cost due to threat
+ // Assume straight line between areas and find closest point
+ // to the threat along that line segment. The distance between
+ // the threat and closest point on the line is the danger cost.
+
+ // path danger is CUMULATIVE
+ float dangerCost = fromArea->GetCostSoFar();
+
+ Vector close;
+ float t;
+ CalcClosestPointOnLineSegment( m_threat->GetAbsOrigin(), area->GetCenter(), fromArea->GetCenter(), close, &t );
+ if ( t < 0.0f )
+ {
+ close = area->GetCenter();
+ }
+ else if ( t > 1.0f )
+ {
+ close = fromArea->GetCenter();
+ }
+
+ float rangeToThreat = ( m_threat->GetAbsOrigin() - close ).Length();
+
+ if ( rangeToThreat < maxThreatRange )
+ {
+ float dangerFactor = 1.0f - ( rangeToThreat / maxThreatRange );
+ dangerCost = dangerDensity * dangerFactor;
+
+ if ( m_me->IsDebugging( INextBot::PATH ) )
+ {
+ NDebugOverlay::HorzArrow( fromArea->GetCenter(), area->GetCenter(), 5, 255 * dangerFactor, 0, 0, 255, true, debugDeltaT );
+
+ Vector to = close - m_threat->GetAbsOrigin();
+ to.NormalizeInPlace();
+
+ NDebugOverlay::Line( close, close - 50.0f * to, 255, 0, 0, true, debugDeltaT );
+ }
+ }
+
+ cost += dangerCost;
+ }
+
+ return cost;
+ }
+
+private:
+ INextBot *m_me;
+ ILocomotion *m_mover;
+
+ CBaseEntity *m_threat;
+ float m_retreatRange;
+
+ enum { MAX_ADJ_AREAS = 64 };
+
+ struct AdjInfo
+ {
+ CNavArea *area;
+ CNavLadder *ladder;
+ NavTraverseType how;
+ };
+
+ AdjInfo m_adjAreaVector[ MAX_ADJ_AREAS ];
+ int m_adjAreaIndex;
+
+};
+
+
+//----------------------------------------------------------------------------------------------
+/**
+ * Periodically rebuild the path away from our threat
+ */
+inline void RetreatPath::RefreshPath( INextBot *bot, CBaseEntity *threat )
+{
+ VPROF_BUDGET( "RetreatPath::RefreshPath", "NextBot" );
+
+ if ( threat == NULL )
+ {
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No threat.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+ return;
+ }
+
+ // don't change our path if we're on a ladder
+ ILocomotion *mover = bot->GetLocomotionInterface();
+ if ( IsValid() && mover && mover->IsUsingLadder() )
+ {
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+ return;
+ }
+
+ // the closer we get, the more accurate our path needs to be
+ Vector to = threat->GetAbsOrigin() - bot->GetPosition();
+
+ const float minTolerance = 0.0f;
+ const float toleranceRate = 0.33f;
+
+ float tolerance = minTolerance + toleranceRate * to.Length();
+
+ if ( !IsValid() || ( threat->GetAbsOrigin() - m_pathThreatPos ).IsLengthGreaterThan( tolerance ) )
+ {
+ if ( !m_throttleTimer.IsElapsed() )
+ {
+ // require a minimum time between repaths, as long as we have a path to follow
+ if ( bot->IsDebugging( INextBot::PATH ) )
+ {
+ DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
+ }
+ return;
+ }
+
+ // remember our path threat
+ m_pathThreat = threat;
+ m_pathThreatPos = threat->GetAbsOrigin();
+
+ RetreatPathBuilder retreat( bot, threat, GetMaxPathLength() );
+
+ CNavArea *goalArea = retreat.ComputePath();
+
+ if ( goalArea )
+ {
+ AssemblePrecomputedPath( bot, goalArea->GetCenter(), goalArea );
+ }
+ else
+ {
+ // all adjacent areas are too far away - just move directly away from threat
+ Vector to = threat->GetAbsOrigin() - bot->GetPosition();
+
+ BuildTrivialPath( bot, bot->GetPosition() - to );
+ }
+
+ const float minRepathInterval = 0.5f;
+ m_throttleTimer.Start( minRepathInterval );
+ }
+}
+
+
+
+#endif // _NEXT_BOT_RETREAT_PATH_
diff --git a/game/server/NextBot/Player/NextBotPlayer.cpp b/game/server/NextBot/Player/NextBotPlayer.cpp
new file mode 100644
index 0000000..a2121f7
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayer.cpp
@@ -0,0 +1,22 @@
+// NextBotPlayer.cpp
+// A CBasePlayer bot based on the NextBot technology
+// Author: Michael Booth, November 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "nav_mesh.h"
+
+#include "NextBot.h"
+#include "NextBotPlayer.h"
+
+#include "in_buttons.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+ConVar NextBotPlayerStop( "nb_player_stop", "0", FCVAR_CHEAT, "Stop all NextBotPlayers from updating" );
+ConVar NextBotPlayerWalk( "nb_player_walk", "0", FCVAR_CHEAT, "Force bots to walk" );
+ConVar NextBotPlayerCrouch( "nb_player_crouch", "0", FCVAR_CHEAT, "Force bots to crouch" );
+ConVar NextBotPlayerMove( "nb_player_move", "1", FCVAR_CHEAT, "Prevents bots from moving" );
+
diff --git a/game/server/NextBot/Player/NextBotPlayer.h b/game/server/NextBot/Player/NextBotPlayer.h
new file mode 100644
index 0000000..2032c72
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayer.h
@@ -0,0 +1,910 @@
+// NextBotPlayer.h
+// A CBasePlayer bot based on the NextBot technology
+// Author: Michael Booth, November 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_PLAYER_H_
+#define _NEXT_BOT_PLAYER_H_
+
+#include "cbase.h"
+#include "gameinterface.h"
+
+#include "NextBot.h"
+#include "Path/NextBotPathFollow.h"
+//#include "NextBotPlayerBody.h"
+#include "NextBotBehavior.h"
+
+#include "in_buttons.h"
+
+extern ConVar NextBotPlayerStop;
+extern ConVar NextBotPlayerWalk;
+extern ConVar NextBotPlayerCrouch;
+extern ConVar NextBotPlayerMove;
+
+
+
+//--------------------------------------------------------------------------------------------------
+/**
+ * Instantiate a NextBot derived from CBasePlayer and spawn it into the environment.
+ * Assumes class T is derived from CBasePlayer, and has the following method that
+ * creates a new entity of type T and returns it:
+ *
+ * static CBasePlayer *T::AllocatePlayerEntity( edict_t *pEdict, const char *playerName )
+ *
+ */
+template < typename T >
+T * NextBotCreatePlayerBot( const char *name, bool bReportFakeClient = true )
+{
+ /*
+ if ( UTIL_ClientsInGame() >= gpGlobals->maxClients )
+ {
+ Msg( "CreatePlayerBot: Failed - server is full (%d/%d clients).\n", UTIL_ClientsInGame(), gpGlobals->maxClients );
+ return NULL;
+ }
+ */
+
+ // This is a "back door" for allocating a custom player bot entity when
+ // the engine calls ClientPutInServer (from CreateFakeClient)
+ ClientPutInServerOverride( T::AllocatePlayerEntity );
+
+ // create the bot and spawn it into the environment
+ edict_t *botEdict = engine->CreateFakeClientEx( name, bReportFakeClient );
+
+ // close the "back door"
+ ClientPutInServerOverride( NULL );
+
+ if ( botEdict == NULL )
+ {
+ Msg( "CreatePlayerBot: Unable to create bot %s - CreateFakeClient() returned NULL.\n", name );
+ return NULL;
+ }
+
+ // create an instance of the bot's class and bind it to the edict
+ T *bot = dynamic_cast< T * >( CBaseEntity::Instance( botEdict ) );
+
+ if ( bot == NULL )
+ {
+ Assert( false );
+ Error( "CreatePlayerBot: Could not Instance() from the bot edict.\n" );
+ return NULL;
+ }
+
+ bot->SetPlayerName( name );
+
+ // flag this as a fakeclient (bot)
+ bot->ClearFlags();
+ bot->AddFlag( FL_CLIENT | FL_FAKECLIENT );
+
+ return bot;
+}
+
+
+//--------------------------------------------------------------------------------------------------
+/**
+ * Interface to access player input buttons.
+ * Unless a duration is given, each button is released at the start of the next frame.
+ * The release methods allow releasing a button before its duration has elapsed.
+ */
+class INextBotPlayerInput
+{
+public:
+ virtual void PressFireButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseFireButton( void ) = 0;
+
+ virtual void PressAltFireButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseAltFireButton( void ) = 0;
+
+ virtual void PressMeleeButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseMeleeButton( void ) = 0;
+
+ virtual void PressSpecialFireButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseSpecialFireButton( void ) = 0;
+
+ virtual void PressUseButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseUseButton( void ) = 0;
+
+ virtual void PressReloadButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseReloadButton( void ) = 0;
+
+ virtual void PressForwardButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseForwardButton( void ) = 0;
+
+ virtual void PressBackwardButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseBackwardButton( void ) = 0;
+
+ virtual void PressLeftButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseLeftButton( void ) = 0;
+
+ virtual void PressRightButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseRightButton( void ) = 0;
+
+ virtual void PressJumpButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseJumpButton( void ) = 0;
+
+ virtual void PressCrouchButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseCrouchButton( void ) = 0;
+
+ virtual void PressWalkButton( float duration = -1.0f ) = 0;
+ virtual void ReleaseWalkButton( void ) = 0;
+
+ virtual void SetButtonScale( float forward, float right ) = 0;
+};
+
+
+//--------------------------------------------------------------------------------------------------
+/**
+ * Drive a CBasePlayer-derived entity via NextBot logic
+ */
+template < typename PlayerType >
+class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerInput
+{
+public:
+ DECLARE_CLASS( NextBotPlayer, PlayerType );
+
+ NextBotPlayer( void );
+ virtual ~NextBotPlayer();
+
+ virtual void Spawn( void );
+
+ virtual void SetSpawnPoint( CBaseEntity *spawnPoint ); // define place in environment where bot will (re)spawn
+ virtual CBaseEntity *EntSelectSpawnPoint( void );
+
+ virtual void PhysicsSimulate( void );
+
+ virtual bool IsNetClient( void ) const { return false; } // Bots should return FALSE for this, they can't receive NET messages
+ virtual bool IsFakeClient( void ) const { return true; }
+ virtual bool IsBot( void ) const { return true; }
+ virtual INextBot *MyNextBotPointer( void ) { return this; }
+
+ // this is valid because the templatized PlayerType must be derived from CBasePlayer, which is derived from CBaseCombatCharacter
+ virtual CBaseCombatCharacter *GetEntity( void ) const { return ( PlayerType * )this; }
+
+ virtual bool IsRemovedOnReset( void ) const { return false; } // remove this bot when the NextBot manager calls Reset
+
+ virtual bool IsDormantWhenDead( void ) const { return true; } // should this player-bot continue to update itself when dead (respawn logic, etc)
+
+ // allocate a bot and bind it to the edict
+ static CBasePlayer *AllocatePlayerEntity( edict_t *edict, const char *playerName );
+
+ //------------------------------------------------------------------------
+ // utility methods
+ float GetDistanceBetween( CBaseEntity *other ) const; // return distance between us and the given entity
+ bool IsDistanceBetweenLessThan( CBaseEntity *other, float range ) const; // return true if distance between is less than the given value
+ bool IsDistanceBetweenGreaterThan( CBaseEntity *other, float range ) const; // return true if distance between is greater than the given value
+
+ float GetDistanceBetween( const Vector &target ) const; // return distance between us and the given entity
+ bool IsDistanceBetweenLessThan( const Vector &target, float range ) const; // return true if distance between is less than the given value
+ bool IsDistanceBetweenGreaterThan( const Vector &target, float range ) const; // return true if distance between is greater than the given value
+
+ //------------------------------------------------------------------------
+ // INextBotPlayerInput
+ virtual void PressFireButton( float duration = -1.0f );
+ virtual void ReleaseFireButton( void );
+
+ virtual void PressAltFireButton( float duration = -1.0f );
+ virtual void ReleaseAltFireButton( void );
+
+ virtual void PressMeleeButton( float duration = -1.0f );
+ virtual void ReleaseMeleeButton( void );
+
+ virtual void PressSpecialFireButton( float duration = -1.0f );
+ virtual void ReleaseSpecialFireButton( void );
+
+ virtual void PressUseButton( float duration = -1.0f );
+ virtual void ReleaseUseButton( void );
+
+ virtual void PressReloadButton( float duration = -1.0f );
+ virtual void ReleaseReloadButton( void );
+
+ virtual void PressForwardButton( float duration = -1.0f );
+ virtual void ReleaseForwardButton( void );
+
+ virtual void PressBackwardButton( float duration = -1.0f );
+ virtual void ReleaseBackwardButton( void );
+
+ virtual void PressLeftButton( float duration = -1.0f );
+ virtual void ReleaseLeftButton( void );
+
+ virtual void PressRightButton( float duration = -1.0f );
+ virtual void ReleaseRightButton( void );
+
+ virtual void PressJumpButton( float duration = -1.0f );
+ virtual void ReleaseJumpButton( void );
+
+ virtual void PressCrouchButton( float duration = -1.0f );
+ virtual void ReleaseCrouchButton( void );
+
+ virtual void PressWalkButton( float duration = -1.0f );
+ virtual void ReleaseWalkButton( void );
+
+ virtual void SetButtonScale( float forward, float right );
+
+ //------------------------------------------------------------------------
+ // Event hooks into NextBot system
+ virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info );
+ virtual int OnTakeDamage_Dying( const CTakeDamageInfo &info );
+ virtual void Event_Killed( const CTakeDamageInfo &info );
+ virtual void HandleAnimEvent( animevent_t *event );
+ virtual void OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ); // invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL)
+ virtual void Touch( CBaseEntity *other );
+ virtual void Weapon_Equip( CBaseCombatWeapon *weapon ); // for OnPickUp
+ virtual void Weapon_Drop( CBaseCombatWeapon *weapon, const Vector *target, const Vector *velocity ); // for OnDrop
+ virtual void OnMainActivityComplete( Activity newActivity, Activity oldActivity );
+ virtual void OnMainActivityInterrupted( Activity newActivity, Activity oldActivity );
+ //------------------------------------------------------------------------
+
+ bool IsAbleToAutoCenterOnLadders( void ) const;
+
+ virtual void AvoidPlayers( CUserCmd *pCmd ) { } // some game types allow players to pass through each other, this method pushes them apart
+
+public:
+ // begin INextBot ------------------------------------------------------------------------------------------------------------------
+ virtual void Update( void ); // (EXTEND) update internal state
+
+protected:
+ int m_inputButtons; // this is still needed to guarantee each button press is captured at least once
+ int m_prevInputButtons;
+ CountdownTimer m_fireButtonTimer;
+ CountdownTimer m_meleeButtonTimer;
+ CountdownTimer m_specialFireButtonTimer;
+ CountdownTimer m_useButtonTimer;
+ CountdownTimer m_reloadButtonTimer;
+ CountdownTimer m_forwardButtonTimer;
+ CountdownTimer m_backwardButtonTimer;
+ CountdownTimer m_leftButtonTimer;
+ CountdownTimer m_rightButtonTimer;
+ CountdownTimer m_jumpButtonTimer;
+ CountdownTimer m_crouchButtonTimer;
+ CountdownTimer m_walkButtonTimer;
+ CountdownTimer m_buttonScaleTimer;
+ IntervalTimer m_burningTimer; // how long since we were last burning
+ float m_forwardScale;
+ float m_rightScale;
+ CHandle< CBaseEntity > m_spawnPointEntity;
+};
+
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::SetSpawnPoint( CBaseEntity *spawnPoint )
+{
+ m_spawnPointEntity = spawnPoint;
+}
+
+template < typename PlayerType >
+inline CBaseEntity *NextBotPlayer< PlayerType >::EntSelectSpawnPoint( void )
+{
+ if ( m_spawnPointEntity != NULL )
+ return m_spawnPointEntity;
+
+ return BaseClass::EntSelectSpawnPoint();
+}
+
+template < typename PlayerType >
+inline float NextBotPlayer< PlayerType >::GetDistanceBetween( CBaseEntity *other ) const
+{
+ return (this->GetAbsOrigin() - other->GetAbsOrigin()).Length();
+}
+
+template < typename PlayerType >
+inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenLessThan( CBaseEntity *other, float range ) const
+{
+ return (this->GetAbsOrigin() - other->GetAbsOrigin()).IsLengthLessThan( range );
+}
+
+template < typename PlayerType >
+inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenGreaterThan( CBaseEntity *other, float range ) const
+{
+ return (this->GetAbsOrigin() - other->GetAbsOrigin()).IsLengthGreaterThan( range );
+}
+
+template < typename PlayerType >
+inline float NextBotPlayer< PlayerType >::GetDistanceBetween( const Vector &target ) const
+{
+ return (this->GetAbsOrigin() - target).Length();
+}
+
+template < typename PlayerType >
+inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenLessThan( const Vector &target, float range ) const
+{
+ return (this->GetAbsOrigin() - target).IsLengthLessThan( range );
+}
+
+template < typename PlayerType >
+inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenGreaterThan( const Vector &target, float range ) const
+{
+ return (this->GetAbsOrigin() - target).IsLengthGreaterThan( range );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressFireButton( float duration )
+{
+ m_inputButtons |= IN_ATTACK;
+ m_fireButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseFireButton( void )
+{
+ m_inputButtons &= ~IN_ATTACK;
+ m_fireButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressAltFireButton( float duration )
+{
+ PressMeleeButton( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseAltFireButton( void )
+{
+ ReleaseMeleeButton();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressMeleeButton( float duration )
+{
+ m_inputButtons |= IN_ATTACK2;
+ m_meleeButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseMeleeButton( void )
+{
+ m_inputButtons &= ~IN_ATTACK2;
+ m_meleeButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressSpecialFireButton( float duration )
+{
+ m_inputButtons |= IN_ATTACK3;
+ m_specialFireButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseSpecialFireButton( void )
+{
+ m_inputButtons &= ~IN_ATTACK3;
+ m_specialFireButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressUseButton( float duration )
+{
+ m_inputButtons |= IN_USE;
+ m_useButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseUseButton( void )
+{
+ m_inputButtons &= ~IN_USE;
+ m_useButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressReloadButton( float duration )
+{
+ m_inputButtons |= IN_RELOAD;
+ m_reloadButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseReloadButton( void )
+{
+ m_inputButtons &= ~IN_RELOAD;
+ m_reloadButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressJumpButton( float duration )
+{
+ m_inputButtons |= IN_JUMP;
+ m_jumpButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseJumpButton( void )
+{
+ m_inputButtons &= ~IN_JUMP;
+ m_jumpButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressCrouchButton( float duration )
+{
+ m_inputButtons |= IN_DUCK;
+ m_crouchButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseCrouchButton( void )
+{
+ m_inputButtons &= ~IN_DUCK;
+ m_crouchButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressWalkButton( float duration )
+{
+ m_inputButtons |= IN_SPEED;
+ m_walkButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseWalkButton( void )
+{
+ m_inputButtons &= ~IN_SPEED;
+ m_walkButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressForwardButton( float duration )
+{
+ m_inputButtons |= IN_FORWARD;
+ m_forwardButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseForwardButton( void )
+{
+ m_inputButtons &= ~IN_FORWARD;
+ m_forwardButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressBackwardButton( float duration )
+{
+ m_inputButtons |= IN_BACK;
+ m_backwardButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseBackwardButton( void )
+{
+ m_inputButtons &= ~IN_BACK;
+ m_backwardButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressLeftButton( float duration )
+{
+ m_inputButtons |= IN_MOVELEFT;
+ m_leftButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseLeftButton( void )
+{
+ m_inputButtons &= ~IN_MOVELEFT;
+ m_leftButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PressRightButton( float duration )
+{
+ m_inputButtons |= IN_MOVERIGHT;
+ m_rightButtonTimer.Start( duration );
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::ReleaseRightButton( void )
+{
+ m_inputButtons &= ~IN_MOVERIGHT;
+ m_rightButtonTimer.Invalidate();
+}
+
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::SetButtonScale( float forward, float right )
+{
+ m_forwardScale = forward;
+ m_rightScale = right;
+ m_buttonScaleTimer.Start( 0.01 );
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline NextBotPlayer< PlayerType >::NextBotPlayer( void )
+{
+ m_prevInputButtons = 0;
+ m_inputButtons = 0;
+ m_burningTimer.Invalidate();
+ m_spawnPointEntity = NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline NextBotPlayer< PlayerType >::~NextBotPlayer()
+{
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Spawn( void )
+{
+ engine->SetFakeClientConVarValue( this->edict(), "cl_autohelp", "0" );
+
+ m_prevInputButtons = m_inputButtons = 0;
+ m_fireButtonTimer.Invalidate();
+ m_meleeButtonTimer.Invalidate();
+ m_specialFireButtonTimer.Invalidate();
+ m_useButtonTimer.Invalidate();
+ m_reloadButtonTimer.Invalidate();
+ m_forwardButtonTimer.Invalidate();
+ m_backwardButtonTimer.Invalidate();
+ m_leftButtonTimer.Invalidate();
+ m_rightButtonTimer.Invalidate();
+ m_jumpButtonTimer.Invalidate();
+ m_crouchButtonTimer.Invalidate();
+ m_walkButtonTimer.Invalidate();
+ m_buttonScaleTimer.Invalidate();
+ m_forwardScale = m_rightScale = 0.04;
+ m_burningTimer.Invalidate();
+
+ // reset first, because Spawn() may access various interfaces
+ INextBot::Reset();
+
+ BaseClass::Spawn();
+}
+
+
+
+//-----------------------------------------------------------------------------------------------------
+inline void _NextBot_BuildUserCommand( CUserCmd *cmd, const QAngle &viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse )
+{
+ Q_memset( cmd, 0, sizeof( CUserCmd ) );
+
+ cmd->command_number = gpGlobals->tickcount;
+ cmd->forwardmove = forwardmove;
+ cmd->sidemove = sidemove;
+ cmd->upmove = upmove;
+ cmd->buttons = buttons;
+ cmd->impulse = impulse;
+
+ VectorCopy( viewangles, cmd->viewangles );
+
+ cmd->random_seed = random->RandomInt( 0, 0x7fffffff );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void )
+{
+ VPROF( "NextBotPlayer::PhysicsSimulate" );
+
+ // Make sure not to simulate this guy twice per frame
+ if ( PlayerType::m_nSimulationTick == gpGlobals->tickcount )
+ {
+ return;
+ }
+
+ if ( engine->IsPaused() )
+ {
+ // We're paused - don't add new commands
+ PlayerType::PhysicsSimulate();
+ return;
+ }
+
+ if ( ( IsDormantWhenDead() && PlayerType::m_lifeState == LIFE_DEAD ) || NextBotStop.GetBool() )
+ {
+ // death animation complete - nothing left to do except let PhysicsSimulate run PreThink etc
+ PlayerType::PhysicsSimulate();
+ return;
+ }
+
+ int inputButtons;
+ //
+ // Update bot behavior
+ //
+ if ( BeginUpdate() )
+ {
+ Update();
+
+ // build button bits
+ if ( !m_fireButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_ATTACK;
+
+ if ( !m_meleeButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_ATTACK2;
+
+ if ( !m_specialFireButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_ATTACK3;
+
+ if ( !m_useButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_USE;
+
+ if ( !m_reloadButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_RELOAD;
+
+ if ( !m_forwardButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_FORWARD;
+
+ if ( !m_backwardButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_BACK;
+
+ if ( !m_leftButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_MOVELEFT;
+
+ if ( !m_rightButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_MOVERIGHT;
+
+ if ( !m_jumpButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_JUMP;
+
+ if ( !m_crouchButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_DUCK;
+
+ if ( !m_walkButtonTimer.IsElapsed() )
+ m_inputButtons |= IN_SPEED;
+
+ m_prevInputButtons = m_inputButtons;
+ inputButtons = m_inputButtons;
+
+ EndUpdate();
+ }
+ else
+ {
+ // HACK: Smooth out body animations
+ GetBodyInterface()->Update();
+
+ // keep buttons pressed between Update() calls (m_prevInputButtons),
+ // and include any button presses that occurred this tick (m_inputButtons).
+ inputButtons = m_prevInputButtons | m_inputButtons;
+ }
+
+ //
+ // Convert NextBot locomotion and posture into
+ // player commands
+ //
+ IBody *body = GetBodyInterface();
+ ILocomotion *mover = GetLocomotionInterface();
+
+ if ( body->IsActualPosture( IBody::CROUCH ) )
+ {
+ inputButtons |= IN_DUCK;
+ }
+
+ float forwardSpeed = 0.0f;
+ float strafeSpeed = 0.0f;
+ float verticalSpeed = ( m_inputButtons & IN_JUMP ) ? mover->GetRunSpeed() : 0.0f;
+
+ if ( inputButtons & IN_FORWARD )
+ {
+ forwardSpeed = mover->GetRunSpeed();
+ }
+ else if ( inputButtons & IN_BACK )
+ {
+ forwardSpeed = -mover->GetRunSpeed();
+ }
+
+ if ( inputButtons & IN_MOVELEFT )
+ {
+ strafeSpeed = -mover->GetRunSpeed();
+ }
+ else if ( inputButtons & IN_MOVERIGHT )
+ {
+ strafeSpeed = mover->GetRunSpeed();
+ }
+
+ if ( NextBotPlayerWalk.GetBool() )
+ {
+ inputButtons |= IN_SPEED;
+ }
+
+ if ( NextBotPlayerCrouch.GetBool() )
+ {
+ inputButtons |= IN_DUCK;
+ }
+
+ if ( !m_buttonScaleTimer.IsElapsed() )
+ {
+ forwardSpeed = mover->GetRunSpeed() * m_forwardScale;
+ strafeSpeed = mover->GetRunSpeed() * m_rightScale;
+ }
+
+ if ( !NextBotPlayerMove.GetBool() )
+ {
+ inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP );
+ forwardSpeed = 0.0f;
+ strafeSpeed = 0.0f;
+ verticalSpeed = 0.0f;
+ }
+
+ QAngle angles = this->EyeAngles();
+
+#ifdef TERROR
+ if ( IsStunned() )
+ {
+ inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP | IN_DUCK );
+ }
+
+ // "Look" in the direction we're climbing/stumbling etc. We can't do anything anyway, and it
+ // keeps motion extraction working.
+ if ( IsRenderYawOverridden() && IsMotionControlledXY( GetMainActivity() ) )
+ {
+ angles[YAW] = GetOverriddenRenderYaw();
+ }
+#endif
+
+ // construct a "command" to move the player
+ CUserCmd userCmd;
+ _NextBot_BuildUserCommand( &userCmd, angles, forwardSpeed, strafeSpeed, verticalSpeed, inputButtons, 0 );
+
+ AvoidPlayers( &userCmd );
+
+ // allocate a new command and add it to the player's list of command to process
+ this->ProcessUsercmds( &userCmd, 1, 1, 0, false );
+
+ m_inputButtons = 0;
+
+ // actually execute player commands and do player physics
+ PlayerType::PhysicsSimulate();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea )
+{
+ // propagate into NextBot responders
+ INextBotEventResponder::OnNavAreaChanged( enteredArea, leftArea );
+
+ BaseClass::OnNavAreaChanged( enteredArea, leftArea );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Touch( CBaseEntity *other )
+{
+ if ( ShouldTouch( other ) )
+ {
+ // propagate touch into NextBot event responders
+ trace_t result;
+ result = this->GetTouchTrace();
+ OnContact( other, &result );
+ }
+
+ BaseClass::Touch( other );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Weapon_Equip( CBaseCombatWeapon *weapon )
+{
+#ifdef TERROR
+ // TODO: Reimplement GetDroppingPlayer() into GetLastOwner()
+ OnPickUp( weapon, weapon->GetDroppingPlayer() );
+#else
+ OnPickUp( weapon, NULL );
+#endif
+
+ BaseClass::Weapon_Equip( weapon );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Weapon_Drop( CBaseCombatWeapon *weapon, const Vector *target, const Vector *velocity )
+{
+ OnDrop( weapon );
+
+ BaseClass::Weapon_Drop( weapon, target, velocity );
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::OnMainActivityComplete( Activity newActivity, Activity oldActivity )
+{
+#ifdef TERROR
+ BaseClass::OnMainActivityComplete( newActivity, oldActivity );
+#endif
+ OnAnimationActivityComplete( oldActivity );
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::OnMainActivityInterrupted( Activity newActivity, Activity oldActivity )
+{
+#ifdef TERROR
+ BaseClass::OnMainActivityInterrupted( newActivity, oldActivity );
+#endif
+ OnAnimationActivityInterrupted( oldActivity );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Update( void )
+{
+ // don't spend CPU updating if this Survivor is dead
+ if ( ( this->IsAlive() || !IsDormantWhenDead() ) && !NextBotPlayerStop.GetBool() )
+ {
+ INextBot::Update();
+ }
+}
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline bool NextBotPlayer< PlayerType >::IsAbleToAutoCenterOnLadders( void ) const
+{
+ const ILocomotion *locomotion = GetLocomotionInterface();
+ return locomotion && locomotion->IsAbleToAutoCenterOnLadder();
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline int NextBotPlayer< PlayerType >::OnTakeDamage_Alive( const CTakeDamageInfo &info )
+{
+ if ( info.GetDamageType() & DMG_BURN )
+ {
+ if ( !m_burningTimer.HasStarted() || m_burningTimer.IsGreaterThen( 1.0f ) )
+ {
+ // emit ignite event periodically as long as we are burning
+ OnIgnite();
+ m_burningTimer.Start();
+ }
+ }
+
+ // propagate event to components
+ OnInjured( info );
+
+ return BaseClass::OnTakeDamage_Alive( info );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline int NextBotPlayer< PlayerType >::OnTakeDamage_Dying( const CTakeDamageInfo &info )
+{
+ if ( info.GetDamageType() & DMG_BURN )
+ {
+ if ( !m_burningTimer.HasStarted() || m_burningTimer.IsGreaterThen( 1.0f ) )
+ {
+ // emit ignite event periodically as long as we are burning
+ OnIgnite();
+ m_burningTimer.Start();
+ }
+ }
+
+ // propagate event to components
+ OnInjured( info );
+
+ return BaseClass::OnTakeDamage_Dying( info );
+}
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::Event_Killed( const CTakeDamageInfo &info )
+{
+ // propagate event to my components
+ OnKilled( info );
+
+ BaseClass::Event_Killed( info );
+}
+
+
+
+//----------------------------------------------------------------------------------------------------------
+template < typename PlayerType >
+inline void NextBotPlayer< PlayerType >::HandleAnimEvent( animevent_t *event )
+{
+ // propagate event to components
+ OnAnimationEvent( event );
+
+ BaseClass::HandleAnimEvent( event );
+}
+
+
+#endif // _NEXT_BOT_PLAYER_H_
diff --git a/game/server/NextBot/Player/NextBotPlayerBody.cpp b/game/server/NextBot/Player/NextBotPlayerBody.cpp
new file mode 100644
index 0000000..246b6fa
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayerBody.cpp
@@ -0,0 +1,881 @@
+// NextBotPlayerBody.cpp
+// Implementation of Body interface for CBasePlayer-derived classes
+// Author: Michael Booth, October 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+
+#include "NextBot.h"
+#include "NextBotPlayerBody.h"
+#include "NextBotPlayer.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+ConVar nb_saccade_time( "nb_saccade_time", "0.1", FCVAR_CHEAT );
+ConVar nb_saccade_speed( "nb_saccade_speed", "1000", FCVAR_CHEAT );
+ConVar nb_head_aim_steady_max_rate( "nb_head_aim_steady_max_rate", "100", FCVAR_CHEAT );
+ConVar nb_head_aim_settle_duration( "nb_head_aim_settle_duration", "0.3", FCVAR_CHEAT );
+ConVar nb_head_aim_resettle_angle( "nb_head_aim_resettle_angle", "100", FCVAR_CHEAT, "After rotating through this angle, the bot pauses to 'recenter' its virtual mouse on its virtual mousepad" );
+ConVar nb_head_aim_resettle_time( "nb_head_aim_resettle_time", "0.3", FCVAR_CHEAT, "How long the bot pauses to 'recenter' its virtual mouse on its virtual mousepad" );
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the fire button.
+ */
+void PressFireButtonReply::OnSuccess( INextBot *bot )
+{
+ INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() );
+ if ( playerInput )
+ {
+ playerInput->PressFireButton();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the alternate fire button.
+ */
+void PressAltFireButtonReply::OnSuccess( INextBot *bot )
+{
+ INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() );
+ if ( playerInput )
+ {
+ playerInput->PressMeleeButton();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the jump button.
+ */
+void PressJumpButtonReply::OnSuccess( INextBot *bot )
+{
+ INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() );
+ if ( playerInput )
+ {
+ playerInput->PressJumpButton();
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------
+//-----------------------------------------------------------------------------------------------
+PlayerBody::PlayerBody( INextBot *bot ) : IBody( bot )
+{
+ m_player = static_cast< CBasePlayer * >( bot->GetEntity() );
+}
+
+
+//-----------------------------------------------------------------------------------------------
+PlayerBody::~PlayerBody()
+{
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * reset to initial state
+ */
+void PlayerBody::Reset( void )
+{
+ m_posture = STAND;
+
+ m_lookAtPos = vec3_origin;
+ m_lookAtSubject = NULL;
+ m_lookAtReplyWhenAimed = NULL;
+ m_lookAtVelocity = vec3_origin;
+ m_lookAtExpireTimer.Invalidate();
+
+ m_lookAtPriority = BORING;
+ m_lookAtExpireTimer.Invalidate();
+ m_lookAtDurationTimer.Invalidate();
+ m_isSightedIn = false;
+ m_hasBeenSightedIn = false;
+ m_headSteadyTimer.Invalidate();
+ m_priorAngles = vec3_angle;
+ m_anchorRepositionTimer.Invalidate();
+ m_anchorForward = vec3_origin;
+}
+
+ConVar bot_mimic( "bot_mimic", "0", 0, "Bot uses usercmd of player by index." );
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Update internal state.
+ * Do this every tick to keep head aims smooth and accurate
+ */
+void PlayerBody::Upkeep( void )
+{
+ // If mimicking the player, don't modify the view angles.
+ static ConVarRef bot_mimic( "bot_mimic" );
+ if ( bot_mimic.IsValid() && bot_mimic.GetBool() )
+ return;
+
+ const float deltaT = gpGlobals->frametime;
+ if ( deltaT < 0.00001f )
+ {
+ return;
+ }
+
+ CBasePlayer *player = ( CBasePlayer * )GetBot()->GetEntity();
+
+ // get current view angles
+ QAngle currentAngles = player->EyeAngles() + player->GetPunchAngle();
+
+ // track when our head is "steady"
+ bool isSteady = true;
+
+ float actualPitchRate = AngleDiff( currentAngles.x, m_priorAngles.x );
+ if ( abs( actualPitchRate ) > nb_head_aim_steady_max_rate.GetFloat() * deltaT )
+ {
+ isSteady = false;
+ }
+ else
+ {
+ float actualYawRate = AngleDiff( currentAngles.y, m_priorAngles.y );
+
+ if ( abs( actualYawRate ) > nb_head_aim_steady_max_rate.GetFloat() * deltaT )
+ {
+ isSteady = false;
+ }
+ }
+
+ if ( isSteady )
+ {
+ if ( !m_headSteadyTimer.HasStarted() )
+ {
+ m_headSteadyTimer.Start();
+ }
+ }
+ else
+ {
+ m_headSteadyTimer.Invalidate();
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ if ( IsHeadSteady() )
+ {
+ const float maxTime = 3.0f;
+ float t = GetHeadSteadyDuration() / maxTime;
+ t = clamp( t, 0.f, 1.0f );
+ NDebugOverlay::Circle( player->EyePosition(), t * 10.0f, 0, 255, 0, 255, true, 2.0f * deltaT );
+ }
+ }
+
+ m_priorAngles = currentAngles;
+
+
+ // if our current look-at has expired, don't change our aim further
+ if ( m_hasBeenSightedIn && m_lookAtExpireTimer.IsElapsed() )
+ {
+ return;
+ }
+
+ // simulate limited range of mouse movements
+ // compute the angle change from "center"
+ const Vector &forward = GetViewVector();
+ float deltaAngle = RAD2DEG( acos( DotProduct( forward, m_anchorForward ) ) );
+ if ( deltaAngle > nb_head_aim_resettle_angle.GetFloat() )
+ {
+ // time to recenter our 'virtual mouse'
+ m_anchorRepositionTimer.Start( RandomFloat( 0.9f, 1.1f ) * nb_head_aim_resettle_time.GetFloat() );
+ m_anchorForward = forward;
+ return;
+ }
+
+ // if we're currently recentering our "virtual mouse", wait
+ if ( m_anchorRepositionTimer.HasStarted() && !m_anchorRepositionTimer.IsElapsed() )
+ {
+ return;
+ }
+ m_anchorRepositionTimer.Invalidate();
+
+
+ // if we have a subject, update lookat point
+ CBaseEntity *subject = m_lookAtSubject;
+ if ( subject )
+ {
+ if ( m_lookAtTrackingTimer.IsElapsed() )
+ {
+ // update subject tracking by periodically estimating linear aim velocity, allowing for "slop" between updates
+ Vector desiredLookAtPos;
+
+ if ( subject->MyCombatCharacterPointer() )
+ {
+ desiredLookAtPos = GetBot()->GetIntentionInterface()->SelectTargetPoint( GetBot(), subject->MyCombatCharacterPointer() );
+ }
+ else
+ {
+ desiredLookAtPos = subject->WorldSpaceCenter();
+ }
+
+ desiredLookAtPos += GetHeadAimSubjectLeadTime() * subject->GetAbsVelocity();
+
+ Vector errorVector = desiredLookAtPos - m_lookAtPos;
+ float error = errorVector.NormalizeInPlace();
+
+ float trackingInterval = GetHeadAimTrackingInterval();
+ if ( trackingInterval < deltaT )
+ {
+ trackingInterval = deltaT;
+ }
+
+ float errorVel = error / trackingInterval;
+
+ m_lookAtVelocity = ( errorVel * errorVector ) + subject->GetAbsVelocity();
+
+ m_lookAtTrackingTimer.Start( RandomFloat( 0.8f, 1.2f ) * trackingInterval );
+ }
+
+ m_lookAtPos += deltaT * m_lookAtVelocity;
+ }
+
+
+ // aim view towards last look at point
+ Vector to = m_lookAtPos - GetEyePosition();
+ to.NormalizeInPlace();
+
+ QAngle desiredAngles;
+ VectorAngles( to, desiredAngles );
+
+ QAngle angles;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ NDebugOverlay::Line( GetEyePosition(), GetEyePosition() + 100.0f * forward, 255, 255, 0, false, 2.0f * deltaT );
+
+ float thickness = isSteady ? 2.0f : 3.0f;
+ int r = m_isSightedIn ? 255 : 0;
+ int g = subject ? 255 : 0;
+ NDebugOverlay::HorzArrow( GetEyePosition(), m_lookAtPos, thickness, r, g, 255, 255, false, 2.0f * deltaT );
+ }
+
+
+ const float onTargetTolerance = 0.98f;
+ float dot = DotProduct( forward, to );
+ if ( dot > onTargetTolerance )
+ {
+ // on target
+ m_isSightedIn = true;
+
+ if ( !m_hasBeenSightedIn )
+ {
+ m_hasBeenSightedIn = true;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At SIGHTED IN\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName() );
+ }
+ }
+
+ if ( m_lookAtReplyWhenAimed )
+ {
+ m_lookAtReplyWhenAimed->OnSuccess( GetBot() );
+ m_lookAtReplyWhenAimed = NULL;
+ }
+ }
+ else
+ {
+ // off target
+ m_isSightedIn = false;
+ }
+
+
+ // rotate view at a rate proportional to how far we have to turn
+ // max rate if we need to turn around
+ // want first derivative continuity of rate as our aim hits to avoid pop
+ float approachRate = GetMaxHeadAngularVelocity();
+
+ const float easeOut = 0.7f;
+ if ( dot > easeOut )
+ {
+ float t = RemapVal( dot, easeOut, 1.0f, 1.0f, 0.02f );
+ const float halfPI = 1.57f;
+ approachRate *= sin( halfPI * t );
+ }
+
+ const float easeInTime = 0.25f;
+ if ( m_lookAtDurationTimer.GetElapsedTime() < easeInTime )
+ {
+ approachRate *= m_lookAtDurationTimer.GetElapsedTime() / easeInTime;
+ }
+
+ angles.y = ApproachAngle( desiredAngles.y, currentAngles.y, approachRate * deltaT );
+ angles.x = ApproachAngle( desiredAngles.x, currentAngles.x, 0.5f * approachRate * deltaT );
+ angles.z = 0.0f;
+
+ // back out "punch angle"
+ angles -= player->GetPunchAngle();
+
+ angles.x = AngleNormalize( angles.x );
+ angles.y = AngleNormalize( angles.y );
+
+ player->SnapEyeAngles( angles );
+}
+
+
+//-----------------------------------------------------------------------------------------------
+bool PlayerBody::SetPosition( const Vector &pos )
+{
+ m_player->SetAbsOrigin( pos );
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return the eye position of the bot in world coordinates
+ */
+const Vector &PlayerBody::GetEyePosition( void ) const
+{
+ m_eyePos = m_player->EyePosition();
+ return m_eyePos;
+}
+
+
+CBaseEntity *PlayerBody::GetEntity( void )
+{
+ return m_player;
+}
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return the view unit direction vector in world coordinates
+ */
+const Vector &PlayerBody::GetViewVector( void ) const
+{
+ m_player->EyeVectors( &m_viewVector );
+ return m_viewVector;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Aim the bot's head towards the given goal
+ */
+void PlayerBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ if ( duration <= 0.0f )
+ {
+ duration = 0.1f;
+ }
+
+ // don't spaz our aim around
+ if ( m_lookAtPriority == priority )
+ {
+ if ( !IsHeadSteady() || GetHeadSteadyDuration() < nb_head_aim_settle_duration.GetFloat() )
+ {
+ // we're still finishing a look-at at the same priority
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED );
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - previous aim not %s\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ reason,
+ IsHeadSteady() ? "settled long enough" : "head-steady" );
+ }
+ return;
+ }
+ }
+
+ // don't short-circuit if "sighted in" to avoid rapid view jitter
+ if ( m_lookAtPriority > priority && !m_lookAtExpireTimer.IsElapsed() )
+ {
+ // higher priority lookat still ongoing
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED );
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - higher priority aim in progress\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ reason );
+ }
+ return;
+ }
+
+ if ( m_lookAtReplyWhenAimed )
+ {
+ // in-process aim was interrupted
+ m_lookAtReplyWhenAimed->OnFail( GetBot(), INextBotReply::INTERRUPTED );
+ }
+
+ m_lookAtReplyWhenAimed = replyWhenAimed;
+ m_lookAtExpireTimer.Start( duration );
+
+ // if given the same point, just update priority
+ const float epsilon = 1.0f;
+ if ( ( m_lookAtPos - lookAtPos ).IsLengthLessThan( epsilon ) )
+ {
+ m_lookAtPriority = priority;
+ return;
+ }
+
+ // new look-at point
+
+ m_lookAtPos = lookAtPos;
+ m_lookAtSubject = NULL;
+
+ m_lookAtPriority = priority;
+ m_lookAtDurationTimer.Start();
+
+ // do NOT clear this here, or continuous calls to AimHeadTowards will keep IsHeadAimingOnTarget returning false all of the time
+ // m_isSightedIn = false;
+
+ m_hasBeenSightedIn = false;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ NDebugOverlay::Cross3D( lookAtPos, 2.0f, 255, 255, 100, true, 2.0f * duration );
+
+ const char *priName = "";
+ switch( priority )
+ {
+ case BORING: priName = "BORING"; break;
+ case INTERESTING: priName = "INTERESTING"; break;
+ case IMPORTANT: priName = "IMPORTANT"; break;
+ case CRITICAL: priName = "CRITICAL"; break;
+ }
+
+ ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At ( %g, %g, %g ) for %3.2f s, Pri = %s, Reason = %s\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ lookAtPos.x, lookAtPos.y, lookAtPos.z,
+ duration,
+ priName,
+ ( reason ) ? reason : "" );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Aim the bot's head towards the given goal
+ */
+void PlayerBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ if ( duration <= 0.0f )
+ {
+ duration = 0.1f;
+ }
+
+ if ( subject == NULL )
+ {
+ return;
+ }
+
+ // don't spaz our aim around
+ if ( m_lookAtPriority == priority )
+ {
+ if ( !IsHeadSteady() || GetHeadSteadyDuration() < nb_head_aim_settle_duration.GetFloat() )
+ {
+ // we're still finishing a look-at at the same priority
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED );
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - previous aim not %s\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ reason,
+ IsHeadSteady() ? "head-steady" : "settled long enough" );
+ }
+ return;
+ }
+ }
+
+ // don't short-circuit if "sighted in" to avoid rapid view jitter
+ if ( m_lookAtPriority > priority && !m_lookAtExpireTimer.IsElapsed() )
+ {
+ // higher priority lookat still ongoing
+ if ( replyWhenAimed )
+ {
+ replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED );
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - higher priority aim in progress\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ reason );
+ }
+ return;
+ }
+
+ if ( m_lookAtReplyWhenAimed )
+ {
+ // in-process aim was interrupted
+ m_lookAtReplyWhenAimed->OnFail( GetBot(), INextBotReply::INTERRUPTED );
+ }
+
+ m_lookAtReplyWhenAimed = replyWhenAimed;
+ m_lookAtExpireTimer.Start( duration );
+
+ // if given the same subject, just update priority
+ if ( subject == m_lookAtSubject )
+ {
+ m_lookAtPriority = priority;
+ return;
+ }
+
+ // new subject
+ m_lookAtSubject = subject;
+
+#ifdef REFACTOR_FOR_CLIENT_SIDE_EYE_TRACKING
+ CBasePlayer *pMyPlayer = static_cast< CBasePlayer * >( GetEntity() );
+ if ( subject->IsPlayer() )
+ {
+ // looking at a player, look at their eye position
+ TerrorPlayer *pMyTarget = ToTerrorPlayer( subject );
+ m_lookAtPos = subject->EyePosition();
+ if(pMyPlayer)
+ {
+ pMyPlayer->SetLookatPlayer( pMyTarget );
+ }
+ }
+ else
+ {
+ // not looking at a player
+ m_lookAtPos = subject->WorldSpaceCenter();
+ if(pMyPlayer)
+ {
+ pMyPlayer->SetLookatPlayer( NULL );
+ }
+ }
+#endif
+
+ m_lookAtPriority = priority;
+ m_lookAtDurationTimer.Start();
+
+ // do NOT clear this here, or continuous calls to AimHeadTowards will keep IsHeadAimingOnTarget returning false all of the time
+ // m_isSightedIn = false;
+
+ m_hasBeenSightedIn = false;
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) )
+ {
+ NDebugOverlay::Cross3D( m_lookAtPos, 2.0f, 100, 100, 100, true, duration );
+
+ const char *priName = "";
+ switch( priority )
+ {
+ case BORING: priName = "BORING"; break;
+ case INTERESTING: priName = "INTERESTING"; break;
+ case IMPORTANT: priName = "IMPORTANT"; break;
+ case CRITICAL: priName = "CRITICAL"; break;
+ }
+
+ ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At subject %s for %3.2f s, Pri = %s, Reason = %s\n",
+ gpGlobals->curtime,
+ m_player->GetPlayerName(),
+ subject->GetClassname(),
+ duration,
+ priName,
+ ( reason ) ? reason : "" );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if head is not rapidly turning to look somewhere else
+ */
+bool PlayerBody::IsHeadSteady( void ) const
+{
+ return m_headSteadyTimer.HasStarted();
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return the duration that the bot's head has been on-target
+ */
+float PlayerBody::GetHeadSteadyDuration( void ) const
+{
+ // return ( IsHeadAimingOnTarget() ) ? m_headSteadyTimer.GetElapsedTime() : 0.0f;
+ return m_headSteadyTimer.HasStarted() ? m_headSteadyTimer.GetElapsedTime() : 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+// Clear out currently pending replyWhenAimed callback
+void PlayerBody::ClearPendingAimReply( void )
+{
+ m_lookAtReplyWhenAimed = NULL;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+float PlayerBody::GetMaxHeadAngularVelocity( void ) const
+{
+ return nb_saccade_speed.GetFloat();
+}
+
+
+//-----------------------------------------------------------------------------------------------
+bool PlayerBody::StartActivity( Activity act, unsigned int flags )
+{
+ // player animation state is controlled on the client
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return currently animating activity
+ */
+Activity PlayerBody::GetActivity( void ) const
+{
+ return ACT_INVALID;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if currently animating activity matches the given one
+ */
+bool PlayerBody::IsActivity( Activity act ) const
+{
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if currently animating activity has any of the given flags
+ */
+bool PlayerBody::HasActivityType( unsigned int flags ) const
+{
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Request a posture change
+ */
+void PlayerBody::SetDesiredPosture( PostureType posture )
+{
+ m_posture = posture;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Get posture body is trying to assume
+ */
+IBody::PostureType PlayerBody::GetDesiredPosture( void ) const
+{
+ return m_posture;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body is trying to assume this posture
+ */
+bool PlayerBody::IsDesiredPosture( PostureType posture ) const
+{
+ return ( posture == m_posture );
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body's actual posture matches its desired posture
+ */
+bool PlayerBody::IsInDesiredPosture( void ) const
+{
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return body's current actual posture
+ */
+IBody::PostureType PlayerBody::GetActualPosture( void ) const
+{
+ return m_posture;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body is actually in the given posture
+ */
+bool PlayerBody::IsActualPosture( PostureType posture ) const
+{
+ return ( posture == m_posture );
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body's current posture allows it to move around the world
+ */
+bool PlayerBody::IsPostureMobile( void ) const
+{
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body's posture is in the process of changing to new posture
+ */
+bool PlayerBody::IsPostureChanging( void ) const
+{
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Arousal level change
+ */
+void PlayerBody::SetArousal( ArousalType arousal )
+{
+ m_arousal = arousal;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Get arousal level
+ */
+IBody::ArousalType PlayerBody::GetArousal( void ) const
+{
+ return m_arousal;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return true if body is at this arousal level
+ */
+bool PlayerBody::IsArousal( ArousalType arousal ) const
+{
+ return ( arousal == m_arousal );
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Width of bot's collision hull in XY plane
+ */
+float PlayerBody::GetHullWidth( void ) const
+{
+ return VEC_HULL_MAX_SCALED( m_player ).x - VEC_HULL_MIN_SCALED( m_player ).x;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Height of bot's current collision hull based on posture
+ */
+float PlayerBody::GetHullHeight( void ) const
+{
+ if ( m_posture == CROUCH )
+ {
+ return GetCrouchHullHeight();
+ }
+
+ return GetStandHullHeight();
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Height of bot's collision hull when standing
+ */
+float PlayerBody::GetStandHullHeight( void ) const
+{
+ return VEC_HULL_MAX_SCALED( m_player ).z - VEC_HULL_MIN_SCALED( m_player ).z;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Height of bot's collision hull when crouched
+ */
+float PlayerBody::GetCrouchHullHeight( void ) const
+{
+ return VEC_DUCK_HULL_MAX_SCALED( m_player ).z - VEC_DUCK_HULL_MIN_SCALED( m_player ).z;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return current collision hull minimums based on actual body posture
+ */
+const Vector &PlayerBody::GetHullMins( void ) const
+{
+ if ( m_posture == CROUCH )
+ {
+ m_hullMins = VEC_DUCK_HULL_MIN_SCALED( m_player );
+ }
+ else
+ {
+ m_hullMins = VEC_HULL_MIN_SCALED( m_player );
+ }
+
+ return m_hullMins;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return current collision hull maximums based on actual body posture
+ */
+const Vector &PlayerBody::GetHullMaxs( void ) const
+{
+ if ( m_posture == CROUCH )
+ {
+ m_hullMaxs = VEC_DUCK_HULL_MAX_SCALED( m_player );
+ }
+ else
+ {
+ m_hullMaxs = VEC_HULL_MAX_SCALED( m_player );
+ }
+
+ return m_hullMaxs;
+}
+
+
+//-----------------------------------------------------------------------------------------------
+/**
+ * Return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface)
+ */
+unsigned int PlayerBody::GetSolidMask( void ) const
+{
+ return ( m_player ) ? m_player->PlayerSolidMask() : MASK_PLAYERSOLID;
+}
+
+
+
+
+
diff --git a/game/server/NextBot/Player/NextBotPlayerBody.h b/game/server/NextBot/Player/NextBotPlayerBody.h
new file mode 100644
index 0000000..eda3643
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayerBody.h
@@ -0,0 +1,153 @@
+// NextBotPlayerBody.h
+// Control and information about the bot's body state (posture, animation state, etc)
+// Author: Michael Booth, October 2006
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_PLAYER_BODY_H_
+#define _NEXT_BOT_PLAYER_BODY_H_
+
+#include "NextBotBodyInterface.h"
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the fire button.
+ */
+class PressFireButtonReply : public INextBotReply
+{
+public:
+ virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully
+};
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the alt-fire button.
+ */
+class PressAltFireButtonReply : public INextBotReply
+{
+public:
+ virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully
+};
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * A useful reply for IBody::AimHeadTowards. When the
+ * head is aiming on target, press the jump button.
+ */
+class PressJumpButtonReply : public INextBotReply
+{
+public:
+ virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully
+};
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for control and information about the bot's body state (posture, animation state, etc)
+ */
+class PlayerBody : public IBody
+{
+public:
+ PlayerBody( INextBot *bot );
+ virtual ~PlayerBody();
+
+ virtual void Reset( void ); // reset to initial state
+ virtual void Upkeep( void ); // lightweight update guaranteed to occur every server tick
+
+ virtual bool SetPosition( const Vector &pos );
+
+ virtual const Vector &GetEyePosition( void ) const; // return the eye position of the bot in world coordinates
+ virtual const Vector &GetViewVector( void ) const; // return the view unit direction vector in world coordinates
+
+ virtual void AimHeadTowards( const Vector &lookAtPos,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // aim the bot's head towards the given goal
+
+ virtual void AimHeadTowards( CBaseEntity *subject,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // continually aim the bot's head towards the given subject
+
+ virtual bool IsHeadAimingOnTarget( void ) const; // return true if the bot's head has achieved its most recent lookat target
+ virtual bool IsHeadSteady( void ) const; // return true if head is not rapidly turning to look somewhere else
+ virtual float GetHeadSteadyDuration( void ) const; // return the duration that the bot's head has been on-target
+ virtual void ClearPendingAimReply( void ); // clear out currently pending replyWhenAimed callback
+
+ virtual float GetMaxHeadAngularVelocity( void ) const; // return max turn rate of head in degrees/second
+
+ virtual bool StartActivity( Activity act, unsigned int flags );
+ virtual Activity GetActivity( void ) const; // return currently animating activity
+ virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one
+ virtual bool HasActivityType( unsigned int flags ) const; // return true if currently animating activity has any of the given flags
+
+ virtual void SetDesiredPosture( PostureType posture ); // request a posture change
+ virtual PostureType GetDesiredPosture( void ) const; // get posture body is trying to assume
+ virtual bool IsDesiredPosture( PostureType posture ) const; // return true if body is trying to assume this posture
+ virtual bool IsInDesiredPosture( void ) const; // return true if body's actual posture matches its desired posture
+
+ virtual PostureType GetActualPosture( void ) const; // return body's current actual posture
+ virtual bool IsActualPosture( PostureType posture ) const; // return true if body is actually in the given posture
+
+ virtual bool IsPostureMobile( void ) const; // return true if body's current posture allows it to move around the world
+ virtual bool IsPostureChanging( void ) const; // return true if body's posture is in the process of changing to new posture
+
+ virtual void SetArousal( ArousalType arousal ); // arousal level change
+ virtual ArousalType GetArousal( void ) const; // get arousal level
+ virtual bool IsArousal( ArousalType arousal ) const; // return true if body is at this arousal level
+
+ virtual float GetHullWidth( void ) const; // width of bot's collision hull in XY plane
+ virtual float GetHullHeight( void ) const; // height of bot's current collision hull based on posture
+ virtual float GetStandHullHeight( void ) const; // height of bot's collision hull when standing
+ virtual float GetCrouchHullHeight( void ) const; // height of bot's collision hull when crouched
+ virtual const Vector &GetHullMins( void ) const; // return current collision hull minimums based on actual body posture
+ virtual const Vector &GetHullMaxs( void ) const; // return current collision hull maximums based on actual body posture
+
+ virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface)
+
+ virtual CBaseEntity *GetEntity( void ); // get the entity
+private:
+ CBasePlayer *m_player;
+
+ PostureType m_posture;
+ ArousalType m_arousal;
+
+ mutable Vector m_eyePos; // for use with GetEyePosition() ONLY
+ mutable Vector m_viewVector; // for use with GetViewVector() ONLY
+ mutable Vector m_hullMins; // for use with GetHullMins() ONLY
+ mutable Vector m_hullMaxs; // for use with GetHullMaxs() ONLY
+
+ Vector m_lookAtPos; // if m_lookAtSubject is non-NULL, it continually overwrites this position with its own
+ EHANDLE m_lookAtSubject;
+ Vector m_lookAtVelocity; // world velocity of lookat point, for tracking moving subjects
+ CountdownTimer m_lookAtTrackingTimer;
+
+ LookAtPriorityType m_lookAtPriority;
+ CountdownTimer m_lookAtExpireTimer; // how long until this lookat expired
+ IntervalTimer m_lookAtDurationTimer; // how long have we been looking at this target
+ INextBotReply *m_lookAtReplyWhenAimed;
+ bool m_isSightedIn; // true if we are looking at our last lookat target
+ bool m_hasBeenSightedIn; // true if we have hit the current lookat target
+
+ IntervalTimer m_headSteadyTimer;
+ QAngle m_priorAngles; // last update's head angles
+ QAngle m_desiredAngles;
+
+ CountdownTimer m_anchorRepositionTimer; // the time is takes us to recenter our virtual mouse
+ Vector m_anchorForward;
+};
+
+inline bool PlayerBody::IsHeadAimingOnTarget( void ) const
+{
+ // TODO: Calling this immediately after AimHeadTowards will always return false until next Upkeep() (MSB)
+ return m_isSightedIn;
+}
+
+
+#endif // _NEXT_BOT_PLAYER_BODY_H_
diff --git a/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp b/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp
new file mode 100644
index 0000000..ea30ee7
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp
@@ -0,0 +1,826 @@
+// NextBotPlayerLocomotion.cpp
+// Implementation of Locomotion interface for CBasePlayer-derived classes
+// Author: Michael Booth, November 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#include "cbase.h"
+#include "nav_mesh.h"
+#include "in_buttons.h"
+#include "NextBot.h"
+#include "NextBotUtil.h"
+#include "NextBotPlayer.h"
+#include "NextBotPlayerLocomotion.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+ConVar NextBotPlayerMoveDirect( "nb_player_move_direct", "0" );
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::PlayerLocomotion( INextBot *bot ) : ILocomotion( bot )
+{
+ m_player = NULL;
+ Reset();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Reset locomotor to initial state
+ */
+void PlayerLocomotion::Reset( void )
+{
+ m_player = static_cast< CBasePlayer * >( GetBot()->GetEntity() );
+
+ m_isJumping = false;
+ m_isClimbingUpToLedge = false;
+ m_isJumpingAcrossGap = false;
+ m_hasLeftTheGround = false;
+ m_desiredSpeed = 0.0f;
+
+
+ m_ladderState = NO_LADDER;
+ m_ladderInfo = NULL;
+ m_ladderDismountGoal = NULL;
+ m_ladderTimer.Invalidate();
+
+ m_minSpeedLimit = 0.0f;
+ m_maxSpeedLimit = 9999999.9f;
+
+ BaseClass::Reset();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::TraverseLadder( void )
+{
+ switch( m_ladderState )
+ {
+ case APPROACHING_ASCENDING_LADDER:
+ m_ladderState = ApproachAscendingLadder();
+ return true;
+
+ case APPROACHING_DESCENDING_LADDER:
+ m_ladderState = ApproachDescendingLadder();
+ return true;
+
+ case ASCENDING_LADDER:
+ m_ladderState = AscendLadder();
+ return true;
+
+ case DESCENDING_LADDER:
+ m_ladderState = DescendLadder();
+ return true;
+
+ case DISMOUNTING_LADDER_TOP:
+ m_ladderState = DismountLadderTop();
+ return true;
+
+ case DISMOUNTING_LADDER_BOTTOM:
+ m_ladderState = DismountLadderBottom();
+ return true;
+
+ case NO_LADDER:
+ default:
+ m_ladderInfo = NULL;
+
+ if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // on ladder and don't want to be
+ GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK );
+ }
+ return false;
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * We're close, but not yet on, this ladder - approach it
+ */
+PlayerLocomotion::LadderState PlayerLocomotion::ApproachAscendingLadder( void )
+{
+ if ( m_ladderInfo == NULL )
+ {
+ return NO_LADDER;
+ }
+
+ // sanity check - are we already at the end of this ladder?
+ if ( GetFeet().z >= m_ladderInfo->m_top.z - GetStepHeight() )
+ {
+ m_ladderTimer.Start( 2.0f );
+ return DISMOUNTING_LADDER_TOP;
+ }
+
+ // sanity check - are we too far below this ladder to reach it?
+ if ( GetFeet().z <= m_ladderInfo->m_bottom.z - GetMaxJumpHeight() )
+ {
+ return NO_LADDER;
+ }
+
+ FaceTowards( m_ladderInfo->m_bottom );
+
+ // it is important to approach precisely, so use a very large weight to wash out all other Approaches
+ Approach( m_ladderInfo->m_bottom, 9999999.9f );
+
+ if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // we're on the ladder
+ return ASCENDING_LADDER;
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Approach ascending ladder", 0.1f, 255, 255, 255, 255 );
+ }
+
+ return APPROACHING_ASCENDING_LADDER;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::LadderState PlayerLocomotion::ApproachDescendingLadder( void )
+{
+ if ( m_ladderInfo == NULL )
+ {
+ return NO_LADDER;
+ }
+
+ // sanity check - are we already at the end of this ladder?
+ if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetMaxJumpHeight() )
+ {
+ m_ladderTimer.Start( 2.0f );
+ return DISMOUNTING_LADDER_BOTTOM;
+ }
+
+ Vector mountPoint = m_ladderInfo->m_top + 0.25f * GetBot()->GetBodyInterface()->GetHullWidth() * m_ladderInfo->GetNormal();
+ Vector to = mountPoint - GetFeet();
+ to.z = 0.0f;
+
+ float mountRange = to.NormalizeInPlace();
+ Vector moveGoal;
+
+ const float veryClose = 10.0f;
+ if ( mountRange < veryClose )
+ {
+ // we're right at the ladder - just keep moving forward until we grab it
+ const Vector &forward = GetMotionVector();
+ moveGoal = GetFeet() + 100.0f * forward;
+ }
+ else
+ {
+ if ( DotProduct( to, m_ladderInfo->GetNormal() ) < 0.0f )
+ {
+ // approaching front of downward ladder
+ // ##
+ // ->+ ##
+ // | ##
+ // | ##
+ // | ##
+ // <-+ ##
+ // ######
+ //
+ moveGoal = m_ladderInfo->m_top - 100.0f * m_ladderInfo->GetNormal();
+ }
+ else
+ {
+ // approaching back of downward ladder
+ //
+ // ->+
+ // ##|
+ // ##|
+ // ##+-->
+ // ######
+ //
+ moveGoal = m_ladderInfo->m_top + 100.0f * m_ladderInfo->GetNormal();
+ }
+ }
+
+ FaceTowards( moveGoal );
+
+ // it is important to approach precisely, so use a very large weight to wash out all other Approaches
+ Approach( moveGoal, 9999999.9f );
+
+ if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // we're on the ladder
+ return DESCENDING_LADDER;
+ }
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Approach descending ladder", 0.1f, 255, 255, 255, 255 );
+ }
+
+ return APPROACHING_DESCENDING_LADDER;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::LadderState PlayerLocomotion::AscendLadder( void )
+{
+ if ( m_ladderInfo == NULL )
+ {
+ return NO_LADDER;
+ }
+
+ if ( GetBot()->GetEntity()->GetMoveType() != MOVETYPE_LADDER )
+ {
+ // slipped off ladder
+ m_ladderInfo = NULL;
+ return NO_LADDER;
+ }
+
+ if ( GetFeet().z >= m_ladderInfo->m_top.z )
+ {
+ // reached top of ladder
+ m_ladderTimer.Start( 2.0f );
+ return DISMOUNTING_LADDER_TOP;
+ }
+
+ // climb up this ladder - look up
+ Vector goal = GetFeet() + 100.0f * ( -m_ladderInfo->GetNormal() + Vector( 0, 0, 2 ) );
+
+ GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" );
+
+ // it is important to approach precisely, so use a very large weight to wash out all other Approaches
+ Approach( goal, 9999999.9f );
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Ascend", 0.1f, 255, 255, 255, 255 );
+ }
+
+ return ASCENDING_LADDER;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::LadderState PlayerLocomotion::DescendLadder( void )
+{
+ if ( m_ladderInfo == NULL )
+ {
+ return NO_LADDER;
+ }
+
+ if ( GetBot()->GetEntity()->GetMoveType() != MOVETYPE_LADDER )
+ {
+ // slipped off ladder
+ m_ladderInfo = NULL;
+ return NO_LADDER;
+ }
+
+ if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetBot()->GetLocomotionInterface()->GetStepHeight() )
+ {
+ // reached bottom of ladder
+ m_ladderTimer.Start( 2.0f );
+ return DISMOUNTING_LADDER_BOTTOM;
+ }
+
+ // climb down this ladder - look down
+ Vector goal = GetFeet() + 100.0f * ( m_ladderInfo->GetNormal() + Vector( 0, 0, -2 ) );
+
+ GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" );
+
+ // it is important to approach precisely, so use a very large weight to wash out all other Approaches
+ Approach( goal, 9999999.9f );
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Descend", 0.1f, 255, 255, 255, 255 );
+ }
+
+ return DESCENDING_LADDER;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::LadderState PlayerLocomotion::DismountLadderTop( void )
+{
+ if ( m_ladderInfo == NULL || m_ladderTimer.IsElapsed() )
+ {
+ m_ladderInfo = NULL;
+ return NO_LADDER;
+ }
+
+ IBody *body = GetBot()->GetBodyInterface();
+ Vector toGoal = m_ladderDismountGoal->GetCenter() - GetFeet();
+ toGoal.z = 0.0f;
+ float range = toGoal.NormalizeInPlace();
+ toGoal.z = 1.0f;
+
+ body->AimHeadTowards( body->GetEyePosition() + 100.0f * toGoal, IBody::MANDATORY, 0.1f, NULL, "Ladder dismount" );
+
+ // it is important to approach precisely, so use a very large weight to wash out all other Approaches
+ Approach( GetFeet() + 100.0f * toGoal, 9999999.9f );
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Dismount top", 0.1f, 255, 255, 255, 255 );
+ NDebugOverlay::HorzArrow( GetFeet(), m_ladderDismountGoal->GetCenter(), 5.0f, 255, 255, 0, 255, true, 0.1f );
+ }
+
+ // test 2D vector here in case nav area is under the geometry a bit
+ const float tolerance = 10.0f;
+ if ( GetBot()->GetEntity()->GetLastKnownArea() == m_ladderDismountGoal && range < tolerance )
+ {
+ // reached dismount goal
+ m_ladderInfo = NULL;
+ return NO_LADDER;
+ }
+
+ return DISMOUNTING_LADDER_TOP;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+PlayerLocomotion::LadderState PlayerLocomotion::DismountLadderBottom( void )
+{
+ if ( m_ladderInfo == NULL || m_ladderTimer.IsElapsed() )
+ {
+ m_ladderInfo = NULL;
+ return NO_LADDER;
+ }
+
+ if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER )
+ {
+ // near the bottom - just let go
+ GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK );
+ m_ladderInfo = NULL;
+ }
+
+ return NO_LADDER;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Update internal state
+ */
+void PlayerLocomotion::Update( void )
+{
+ if ( TraverseLadder() )
+ {
+ return BaseClass::Update();
+ }
+
+ if ( m_isJumpingAcrossGap || m_isClimbingUpToLedge )
+ {
+ // force a run
+ SetMinimumSpeedLimit( GetRunSpeed() );
+
+ Vector toLanding = m_landingGoal - GetFeet();
+ toLanding.z = 0.0f;
+ toLanding.NormalizeInPlace();
+
+ if ( m_hasLeftTheGround )
+ {
+ // face into the jump/climb
+ GetBot()->GetBodyInterface()->AimHeadTowards( GetBot()->GetEntity()->EyePosition() + 100.0 * toLanding, IBody::MANDATORY, 0.25f, NULL, "Facing impending jump/climb" );
+
+ if ( IsOnGround() )
+ {
+ // back on the ground - jump is complete
+ m_isClimbingUpToLedge = false;
+ m_isJumpingAcrossGap = false;
+ SetMinimumSpeedLimit( 0.0f );
+ }
+ }
+ else
+ {
+ // haven't left the ground yet - just starting the jump
+
+ if ( !IsClimbingOrJumping() )
+ {
+ Jump();
+ }
+
+ Vector vel = GetBot()->GetEntity()->GetAbsVelocity();
+
+ if ( m_isJumpingAcrossGap )
+ {
+ // cheat and max our velocity in case we were stopped at the edge of this gap
+ vel.x = GetRunSpeed() * toLanding.x;
+ vel.y = GetRunSpeed() * toLanding.y;
+ // leave vel.z unchanged
+ }
+
+ GetBot()->GetEntity()->SetAbsVelocity( vel );
+
+ if ( !IsOnGround() )
+ {
+ // jump has begun
+ m_hasLeftTheGround = true;
+ }
+ }
+
+
+ Approach( m_landingGoal );
+ }
+
+ BaseClass::Update();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void PlayerLocomotion::AdjustPosture( const Vector &moveGoal )
+{
+ // This function has no effect if we're not standing or crouching
+ IBody *body = GetBot()->GetBodyInterface();
+ if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) )
+ return;
+
+ // not all games have auto-crouch, so don't assume it here
+ BaseClass::AdjustPosture( moveGoal );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Build a user command to move this player towards the goal position
+ */
+void PlayerLocomotion::Approach( const Vector &pos, float goalWeight )
+{
+ VPROF_BUDGET( "PlayerLocomotion::Approach", "NextBot" );
+
+ BaseClass::Approach( pos );
+
+ AdjustPosture( pos );
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::Line( GetFeet(), pos, 255, 255, 0, true, 0.1f );
+ }
+
+ INextBotPlayerInput *playerButtons = dynamic_cast< INextBotPlayerInput * >( GetBot() );
+
+ if ( !playerButtons )
+ {
+ DevMsg( "PlayerLocomotion::Approach: No INextBotPlayerInput\n " );
+ return;
+ }
+
+ Vector forward3D;
+ m_player->EyeVectors( &forward3D );
+
+ Vector2D forward( forward3D.x, forward3D.y );
+ forward.NormalizeInPlace();
+
+ Vector2D right( forward.y, -forward.x );
+
+ // compute unit vector to goal position
+ Vector2D to = ( pos - GetFeet() ).AsVector2D();
+ float goalDistance = to.NormalizeInPlace();
+
+ float ahead = to.Dot( forward );
+ float side = to.Dot( right );
+
+#ifdef NEED_TO_INTEGRATE_MOTION_CONTROLLED_CODE_FROM_L4D_PLAYERS
+ // If we're climbing ledges, we need to stay crouched to prevent player movement code from messing
+ // with our origin.
+ CTerrorPlayer *player = ToTerrorPlayer(m_player);
+ if ( player && player->IsMotionControlledZ( player->GetMainActivity() ) )
+ {
+ playerButtons->PressCrouchButton();
+ return;
+ }
+#endif
+
+ if ( m_player->IsOnLadder() && IsUsingLadder() && ( m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER ) )
+ {
+ // we are on a ladder and WANT to be on a ladder.
+ playerButtons->PressForwardButton();
+
+ // Stay in center of ladder. The gamemovement will autocenter us in most cases, but this is needed in case it doesn't.
+ if ( m_ladderInfo )
+ {
+ Vector posOnLadder;
+ CalcClosestPointOnLine( GetFeet(), m_ladderInfo->m_bottom, m_ladderInfo->m_top, posOnLadder );
+
+ Vector alongLadder = m_ladderInfo->m_top - m_ladderInfo->m_bottom;
+ alongLadder.NormalizeInPlace();
+
+ Vector rightLadder = CrossProduct( alongLadder, m_ladderInfo->GetNormal() );
+
+ Vector away = GetFeet() - posOnLadder;
+
+ // we only want error in plane of ladder
+ float error = DotProduct( away, rightLadder );
+ away.NormalizeInPlace();
+
+ const float tolerance = 5.0f + 0.25f * GetBot()->GetBodyInterface()->GetHullWidth();
+ if ( error > tolerance )
+ {
+ if ( DotProduct( away, rightLadder ) > 0.0f )
+ {
+ playerButtons->PressLeftButton();
+ }
+ else
+ {
+ playerButtons->PressRightButton();
+ }
+ }
+ }
+ }
+ else
+ {
+ const float epsilon = 0.25f;
+ if ( NextBotPlayerMoveDirect.GetBool() )
+ {
+ if ( goalDistance > epsilon )
+ {
+ playerButtons->SetButtonScale( ahead, side );
+ }
+ }
+
+ if ( ahead > epsilon )
+ {
+ playerButtons->PressForwardButton();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() + 50.0f * Vector( forward.x, forward.y, 0.0f ), 15.0f, 0, 255, 0, 255, true, 0.1f );
+ }
+ }
+ else if ( ahead < -epsilon )
+ {
+ playerButtons->PressBackwardButton();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() - 50.0f * Vector( forward.x, forward.y, 0.0f ), 15.0f, 255, 0, 0, 255, true, 0.1f );
+ }
+ }
+
+ if ( side <= -epsilon )
+ {
+ playerButtons->PressLeftButton();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() - 50.0f * Vector( right.x, right.y, 0.0f ), 15.0f, 255, 0, 255, 255, true, 0.1f );
+ }
+ }
+ else if ( side >= epsilon )
+ {
+ playerButtons->PressRightButton();
+
+ if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) )
+ {
+ NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() + 50.0f * Vector( right.x, right.y, 0.0f ), 15.0f, 0, 255, 255, 255, true, 0.1f );
+ }
+ }
+ }
+
+ if ( !IsRunning() )
+ {
+ playerButtons->PressWalkButton();
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Move the bot to the precise given position immediately,
+ */
+void PlayerLocomotion::DriveTo( const Vector &pos )
+{
+ BaseClass::DriveTo( pos );
+
+ Approach( pos );
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const
+{
+ // don't jump unless we have to
+ const PathFollower *path = GetBot()->GetCurrentPath();
+ if ( path )
+ {
+ const float watchForClimbRange = 75.0f;
+ if ( !path->IsDiscontinuityAhead( GetBot(), Path::CLIMB_UP, watchForClimbRange ) )
+ {
+ // we are not planning on climbing
+
+ // always allow climbing over movable obstacles
+ if ( obstacle && !const_cast< CBaseEntity * >( obstacle )->IsWorld() )
+ {
+ IPhysicsObject *physics = obstacle->VPhysicsGetObject();
+ if ( physics && physics->IsMoveable() )
+ {
+ // movable physics object - climb over it
+ return true;
+ }
+ }
+
+ if ( !GetBot()->GetLocomotionInterface()->IsStuck() )
+ {
+ // we're not stuck - don't try to jump up yet
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle )
+{
+ if ( !IsClimbPossible( GetBot(), obstacle ) )
+ {
+ return false;
+ }
+
+ Jump();
+
+ m_isClimbingUpToLedge = true;
+ m_landingGoal = landingGoal;
+ m_hasLeftTheGround = false;
+
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+void PlayerLocomotion::JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward )
+{
+ Jump();
+
+ // face forward
+ GetBot()->GetBodyInterface()->AimHeadTowards( landingGoal, IBody::MANDATORY, 1.0f, NULL, "Looking forward while jumping a gap" );
+
+ m_isJumpingAcrossGap = true;
+ m_landingGoal = landingGoal;
+ m_hasLeftTheGround = false;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+void PlayerLocomotion::Jump( void )
+{
+ m_isJumping = true;
+ m_jumpTimer.Start( 0.5f );
+
+ INextBotPlayerInput *playerButtons = dynamic_cast< INextBotPlayerInput * >( GetBot() );
+ if ( playerButtons )
+ {
+ playerButtons->PressJumpButton();
+ }
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::IsClimbingOrJumping( void ) const
+{
+ if ( !m_isJumping )
+ return false;
+
+ if ( m_jumpTimer.IsElapsed() && IsOnGround() )
+ {
+ m_isJumping = false;
+ return false;
+ }
+
+ return true;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::IsClimbingUpToLedge( void ) const
+{
+ return m_isClimbingUpToLedge;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::IsJumpingAcrossGap( void ) const
+{
+ return m_isJumpingAcrossGap;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Return true if standing on something
+ */
+bool PlayerLocomotion::IsOnGround( void ) const
+{
+ return (m_player->GetGroundEntity() != NULL);
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Return the current ground entity or NULL if not on the ground
+ */
+CBaseEntity *PlayerLocomotion::GetGround( void ) const
+{
+ return m_player->GetGroundEntity();
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Surface normal of the ground we are in contact with
+ */
+const Vector &PlayerLocomotion::GetGroundNormal( void ) const
+{
+ static Vector up( 0, 0, 1.0f );
+ return up;
+
+ // TODO: Integrate movehelper_server for this: return m_player->GetGroundNormal();
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Climb the given ladder to the top and dismount
+ */
+void PlayerLocomotion::ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal )
+{
+ // look up and push forward
+// Vector goal = GetBot()->GetPosition() + 100.0f * ( Vector( 0, 0, 1.0f ) - ladder->GetNormal() );
+// Approach( goal );
+// FaceTowards( goal );
+
+ m_ladderState = APPROACHING_ASCENDING_LADDER;
+ m_ladderInfo = ladder;
+ m_ladderDismountGoal = dismountGoal;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Descend the given ladder to the bottom and dismount
+ */
+void PlayerLocomotion::DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal )
+{
+ // look down and push forward
+// Vector goal = GetBot()->GetPosition() + 100.0f * ( Vector( 0, 0, -1.0f ) - ladder->GetNormal() );
+// Approach( goal );
+// FaceTowards( goal );
+
+ m_ladderState = APPROACHING_DESCENDING_LADDER;
+ m_ladderInfo = ladder;
+ m_ladderDismountGoal = dismountGoal;
+}
+
+
+//----------------------------------------------------------------------------------------------------
+bool PlayerLocomotion::IsUsingLadder( void ) const
+{
+ return ( m_ladderState != NO_LADDER );
+}
+
+
+//----------------------------------------------------------------------------------------------------
+/**
+ * Rotate body to face towards "target"
+ */
+void PlayerLocomotion::FaceTowards( const Vector &target )
+{
+ // player body follows view direction
+ Vector look( target.x, target.y, GetBot()->GetEntity()->EyePosition().z );
+
+ GetBot()->GetBodyInterface()->AimHeadTowards( look, IBody::BORING, 0.1f, NULL, "Body facing" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+* Return position of "feet" - point below centroid of bot at feet level
+*/
+const Vector &PlayerLocomotion::GetFeet( void ) const
+{
+ return m_player->GetAbsOrigin();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+/**
+ * Return current world space velocity
+ */
+const Vector &PlayerLocomotion::GetVelocity( void ) const
+{
+ return m_player->GetAbsVelocity();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+float PlayerLocomotion::GetRunSpeed( void ) const
+{
+ return m_player->MaxSpeed();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+float PlayerLocomotion::GetWalkSpeed( void ) const
+{
+ return 0.5f * m_player->MaxSpeed();
+}
+
diff --git a/game/server/NextBot/Player/NextBotPlayerLocomotion.h b/game/server/NextBot/Player/NextBotPlayerLocomotion.h
new file mode 100644
index 0000000..5da7260
--- /dev/null
+++ b/game/server/NextBot/Player/NextBotPlayerLocomotion.h
@@ -0,0 +1,223 @@
+// NextBotPlayerLocomotion.h
+// Locomotor for CBasePlayer derived bots
+// Author: Michael Booth, November 2005
+//========= Copyright Valve Corporation, All rights reserved. ============//
+
+#ifndef _NEXT_BOT_PLAYER_LOCOMOTION_H_
+#define _NEXT_BOT_PLAYER_LOCOMOTION_H_
+
+#include "NextBot.h"
+#include "NextBotLocomotionInterface.h"
+#include "Path/NextBotPathFollow.h"
+
+class CBasePlayer;
+
+//--------------------------------------------------------------------------------------------------
+/**
+ * Basic player locomotion implementation
+ */
+class PlayerLocomotion : public ILocomotion
+{
+public:
+ DECLARE_CLASS( PlayerLocomotion, ILocomotion );
+
+ PlayerLocomotion( INextBot *bot );
+ virtual ~PlayerLocomotion() { }
+
+ virtual void Reset( void ); // reset to initial state
+ virtual void Update( void ); // update internal state
+
+ virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position
+ virtual void DriveTo( const Vector &pos ); // Move the bot to the precise given position immediately,
+
+ //
+ // ILocomotion modifiers
+ //
+ virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ); // initiate a jump to an adjacent high ledge, return false if climb can't start
+ virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ); // initiate a jump across an empty volume of space to far side
+ virtual void Jump( void ); // initiate a simple undirected jump in the air
+ virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form
+ virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge
+ virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side
+
+ virtual void Run( void ); // set desired movement speed to running
+ virtual void Walk( void ); // set desired movement speed to walking
+ virtual void Stop( void ); // set desired movement speed to stopped
+ virtual bool IsRunning( void ) const;
+ virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement
+ virtual float GetDesiredSpeed( void ) const; // returns the current desired speed
+ virtual void SetMinimumSpeedLimit( float limit ); // speed cannot drop below this
+ virtual void SetMaximumSpeedLimit( float limit ); // speed cannot rise above this
+
+ virtual bool IsOnGround( void ) const; // return true if standing on something
+ virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground
+ virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with
+
+ virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // climb the given ladder to the top and dismount
+ virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // descend the given ladder to the bottom and dismount
+ virtual bool IsUsingLadder( void ) const;
+ virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down
+ virtual bool IsAbleToAutoCenterOnLadder( void ) const;
+
+ virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target"
+
+ virtual void SetDesiredLean( const QAngle &lean ) { }
+ virtual const QAngle &GetDesiredLean( void ) const { static QAngle junk; return junk; }
+
+ //
+ // ILocomotion information
+ //
+ virtual const Vector &GetFeet( void ) const; // return position of "feet" - point below centroid of bot at feet level
+
+ virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up
+ 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 float GetWalkSpeed( void ) const; // get maximum walking speed
+
+ virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor
+ virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor
+
+ virtual const Vector &GetVelocity( void ) const; // return current world space velocity
+
+protected:
+ virtual void AdjustPosture( const Vector &moveGoal );
+
+private:
+ CBasePlayer *m_player; // the player we are locomoting
+
+ mutable bool m_isJumping;
+ CountdownTimer m_jumpTimer;
+
+ bool m_isClimbingUpToLedge;
+ bool m_isJumpingAcrossGap;
+ Vector m_landingGoal;
+ bool m_hasLeftTheGround;
+
+ float m_desiredSpeed;
+ float m_minSpeedLimit;
+ float m_maxSpeedLimit;
+
+ bool TraverseLadder( void ); // when climbing/descending a ladder
+
+ enum LadderState
+ {
+ NO_LADDER, // not using a ladder
+ APPROACHING_ASCENDING_LADDER,
+ APPROACHING_DESCENDING_LADDER,
+ ASCENDING_LADDER,
+ DESCENDING_LADDER,
+ DISMOUNTING_LADDER_TOP,
+ DISMOUNTING_LADDER_BOTTOM,
+ };
+
+ LadderState m_ladderState;
+ LadderState ApproachAscendingLadder( void );
+ LadderState ApproachDescendingLadder( void );
+ LadderState AscendLadder( void );
+ LadderState DescendLadder( void );
+ LadderState DismountLadderTop( void );
+ LadderState DismountLadderBottom( void );
+
+ const CNavLadder *m_ladderInfo;
+ const CNavArea *m_ladderDismountGoal;
+ CountdownTimer m_ladderTimer; // a "give up" timer if things go awry
+
+ bool IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const;
+};
+
+
+inline float PlayerLocomotion::GetStepHeight( void ) const
+{
+ return 18.0f;
+}
+
+
+inline float PlayerLocomotion::GetMaxJumpHeight( void ) const
+{
+ return 57.0f;
+}
+
+
+inline float PlayerLocomotion::GetDeathDropHeight( void ) const
+{
+ return 200.0f;
+}
+
+
+inline float PlayerLocomotion::GetMaxAcceleration( void ) const
+{
+ return 100.0f;
+}
+
+inline float PlayerLocomotion::GetMaxDeceleration( void ) const
+{
+ return 200.0f;
+}
+
+inline void PlayerLocomotion::Run( void )
+{
+ m_desiredSpeed = GetRunSpeed();
+}
+
+inline void PlayerLocomotion::Walk( void )
+{
+ m_desiredSpeed = GetWalkSpeed();
+}
+
+inline void PlayerLocomotion::Stop( void )
+{
+ m_desiredSpeed = 0.0f;
+}
+
+inline bool PlayerLocomotion::IsRunning( void ) const
+{
+ return true;
+}
+
+inline void PlayerLocomotion::SetDesiredSpeed( float speed )
+{
+ m_desiredSpeed = speed;
+}
+
+inline float PlayerLocomotion::GetDesiredSpeed( void ) const
+{
+ return clamp( m_desiredSpeed, m_minSpeedLimit, m_maxSpeedLimit );
+}
+
+inline void PlayerLocomotion::SetMinimumSpeedLimit( float limit )
+{
+ m_minSpeedLimit = limit;
+}
+
+inline void PlayerLocomotion::SetMaximumSpeedLimit( float limit )
+{
+ m_maxSpeedLimit = limit;
+}
+
+inline bool PlayerLocomotion::IsAbleToAutoCenterOnLadder( void ) const
+{
+ return IsUsingLadder() && (m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER);
+}
+
+inline bool PlayerLocomotion::IsAscendingOrDescendingLadder( void ) const
+{
+ switch( m_ladderState )
+ {
+ case ASCENDING_LADDER:
+ case DESCENDING_LADDER:
+ case DISMOUNTING_LADDER_TOP:
+ case DISMOUNTING_LADDER_BOTTOM:
+ return true;
+ default:
+ // Explicitly handle the default so that clang knows not to warn us.
+ // warning: enumeration values 'NO_LADDER', 'APPROACHING_ASCENDING_LADDER', and 'APPROACHING_DESCENDING_LADDER' not handled in switch [-Wswitch-enum]
+ break;
+ }
+
+ return false;
+}
+
+
+#endif // _NEXT_BOT_PLAYER_LOCOMOTION_H_
diff --git a/game/server/NextBot/simple_bot.cpp b/game/server/NextBot/simple_bot.cpp
new file mode 100644
index 0000000..dbae239
--- /dev/null
+++ b/game/server/NextBot/simple_bot.cpp
@@ -0,0 +1,199 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// simple_bot.cpp
+// A simple bot
+// Michael Booth, February 2009
+
+#include "cbase.h"
+#include "simple_bot.h"
+#include "nav_mesh.h"
+
+
+//-----------------------------------------------------------------------------------------------------
+// Command to add a Simple Bot where your crosshairs are aiming
+//-----------------------------------------------------------------------------------------------------
+CON_COMMAND_F( simple_bot_add, "Add a simple bot.", FCVAR_CHEAT )
+{
+ CBasePlayer *player = UTIL_GetCommandClient();
+ if ( !player )
+ {
+ return;
+ }
+
+ Vector forward;
+ player->EyeVectors( &forward );
+
+ trace_t result;
+ UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 999999.9f * forward, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, player, COLLISION_GROUP_NONE, &result );
+ if ( !result.DidHit() )
+ {
+ return;
+ }
+
+ CSimpleBot *bot = static_cast< CSimpleBot * >( CreateEntityByName( "simple_bot" ) );
+ if ( bot )
+ {
+ Vector forward = player->GetAbsOrigin() - result.endpos;
+ forward.z = 0.0f;
+ forward.NormalizeInPlace();
+
+ QAngle angles;
+ VectorAngles( forward, angles );
+
+ bot->SetAbsAngles( angles );
+ bot->SetAbsOrigin( result.endpos + Vector( 0, 0, 10.0f ) );
+
+ DispatchSpawn( bot );
+ }
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// The Simple Bot
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( simple_bot, CSimpleBot );
+
+#ifndef TF_DLL
+PRECACHE_REGISTER( simple_bot );
+#endif
+
+
+//-----------------------------------------------------------------------------------------------------
+CSimpleBot::CSimpleBot()
+{
+ ALLOCATE_INTENTION_INTERFACE( CSimpleBot );
+
+ m_locomotor = new NextBotGroundLocomotion( this );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CSimpleBot::~CSimpleBot()
+{
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CSimpleBot::Precache()
+{
+ BaseClass::Precache();
+
+#ifndef DOTA_DLL
+ PrecacheModel( "models/humans/group01/female_01.mdl" );
+#endif
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CSimpleBot::Spawn( void )
+{
+ BaseClass::Spawn();
+
+#ifndef DOTA_DLL
+ SetModel( "models/humans/group01/female_01.mdl" );
+#endif
+}
+
+
+//---------------------------------------------------------------------------------------------
+// The Simple Bot behaviors
+//---------------------------------------------------------------------------------------------
+/**
+ * For use with TheNavMesh->ForAllAreas()
+ * Find the Nth area in the sequence
+ */
+class SelectNthAreaFunctor
+{
+public:
+ SelectNthAreaFunctor( int count )
+ {
+ m_count = count;
+ m_area = NULL;
+ }
+
+ bool operator() ( CNavArea *area )
+ {
+ m_area = area;
+ return ( m_count-- > 0 );
+ }
+
+ int m_count;
+ CNavArea *m_area;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * This action causes the bot to pick a random nav area in the mesh and move to it, then
+ * pick another, etc.
+ * Actions usually each have their own .cpp/.h file and are organized into folders since there
+ * are often many of them. For this example, we're keeping everything to a single .cpp/.h file.
+ */
+class CSimpleBotRoam : public Action< CSimpleBot >
+{
+public:
+ //----------------------------------------------------------------------------------
+ // OnStart is called once when the Action first becomes active
+ virtual ActionResult< CSimpleBot > OnStart( CSimpleBot *me, Action< CSimpleBot > *priorAction )
+ {
+ // smooth out the bot's path following by moving toward a point farther down the path
+ m_path.SetMinLookAheadDistance( 300.0f );
+
+ return Continue();
+ }
+
+
+ //----------------------------------------------------------------------------------
+ // Update is called repeatedly (usually once per server frame) while the Action is active
+ virtual ActionResult< CSimpleBot > Update( CSimpleBot *me, float interval )
+ {
+ if ( m_path.IsValid() && !m_timer.IsElapsed() )
+ {
+ // PathFollower::Update() moves the bot along the path using the bot's ILocomotion and IBody interfaces
+ m_path.Update( me );
+ }
+ else
+ {
+ SelectNthAreaFunctor pick( RandomInt( 0, TheNavMesh->GetNavAreaCount() - 1 ) );
+ TheNavMesh->ForAllAreas( pick );
+
+ if ( pick.m_area )
+ {
+ CSimpleBotPathCost cost( me );
+ m_path.Compute( me, pick.m_area->GetCenter(), cost );
+ }
+
+ // follow this path for a random duration (or until we reach the end)
+ m_timer.Start( RandomFloat( 5.0f, 10.0f ) );
+ }
+
+ return Continue();
+ }
+
+
+ //----------------------------------------------------------------------------------
+ // this is an event handler - many more are available (see declaration of Action< Actor > in NextBotBehavior.h)
+ virtual EventDesiredResult< CSimpleBot > OnStuck( CSimpleBot *me )
+ {
+ // we are stuck trying to follow the current path - invalidate it so a new one is chosen
+ m_path.Invalidate();
+
+ return TryContinue();
+ }
+
+
+ virtual const char *GetName( void ) const { return "Roam"; } // return name of this action
+
+private:
+ PathFollower m_path;
+ CountdownTimer m_timer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+/**
+ * Instantiate the bot's Intention interface and start the initial Action (CSimpleBotRoam in this case)
+ */
+IMPLEMENT_INTENTION_INTERFACE( CSimpleBot, CSimpleBotRoam )
diff --git a/game/server/NextBot/simple_bot.h b/game/server/NextBot/simple_bot.h
new file mode 100644
index 0000000..b679b6c
--- /dev/null
+++ b/game/server/NextBot/simple_bot.h
@@ -0,0 +1,116 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// simple_bot.h
+// A mininal example of a NextBotCombatCharacter (ie: non-player) bot
+// Michael Booth, February 2009
+
+#ifndef SIMPLE_BOT_H
+#define SIMPLE_BOT_H
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "NextBotGroundLocomotion.h"
+#include "Path/NextBotPathFollow.h"
+
+
+//----------------------------------------------------------------------------
+/**
+ * A Simple Bot
+ */
+class CSimpleBot : public NextBotCombatCharacter
+{
+public:
+ DECLARE_CLASS( CSimpleBot, NextBotCombatCharacter );
+
+ CSimpleBot();
+ virtual ~CSimpleBot();
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ // INextBot
+ DECLARE_INTENTION_INTERFACE( CSimpleBot )
+ virtual NextBotGroundLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+
+private:
+ NextBotGroundLocomotion *m_locomotor;
+};
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Functor used with the A* algorithm of NavAreaBuildPath() to determine the "cost" of moving from one area to another.
+ * "Cost" is generally the weighted distance between the centers of the areas. If you want the bot
+ * to avoid an area/ladder/elevator, increase the cost. If you want to disallow an area/ladder/elevator, return -1.
+ */
+class CSimpleBotPathCost : public IPathCost
+{
+public:
+ CSimpleBotPathCost( CSimpleBot *me )
+ {
+ m_me = me;
+ }
+
+ // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed
+ virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const
+ {
+ if ( fromArea == NULL )
+ {
+ // first area in path, no cost
+ return 0.0f;
+ }
+ else
+ {
+ if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) )
+ {
+ // our locomotor says we can't move here
+ return -1.0f;
+ }
+
+ // compute distance traveled along path so far
+ float dist;
+
+ if ( ladder )
+ {
+ dist = ladder->m_length;
+ }
+ else if ( length > 0.0 )
+ {
+ // optimization to avoid recomputing length
+ dist = length;
+ }
+ else
+ {
+ dist = ( area->GetCenter() - fromArea->GetCenter() ).Length();
+ }
+
+ float cost = dist + fromArea->GetCostSoFar();
+
+ // check height change
+ float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area );
+ if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() )
+ {
+ if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() )
+ {
+ // too high to reach
+ return -1.0f;
+ }
+
+ // jumping is slower than flat ground
+ const float jumpPenalty = 5.0f;
+ cost += jumpPenalty * dist;
+ }
+ else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() )
+ {
+ // too far to drop
+ return -1.0f;
+ }
+
+ return cost;
+ }
+ }
+
+ CSimpleBot *m_me;
+};
+
+
+#endif // SIMPLE_BOT_H