summaryrefslogtreecommitdiff
path: root/game/server/hl2/ai_behavior_actbusy.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/hl2/ai_behavior_actbusy.cpp')
-rw-r--r--game/server/hl2/ai_behavior_actbusy.cpp3151
1 files changed, 3151 insertions, 0 deletions
diff --git a/game/server/hl2/ai_behavior_actbusy.cpp b/game/server/hl2/ai_behavior_actbusy.cpp
new file mode 100644
index 0000000..9f1ac3e
--- /dev/null
+++ b/game/server/hl2/ai_behavior_actbusy.cpp
@@ -0,0 +1,3151 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+
+#include "ai_behavior_actbusy.h"
+
+#include "ai_navigator.h"
+#include "ai_hint.h"
+#include "ai_behavior_follow.h"
+#include "KeyValues.h"
+#include "filesystem.h"
+#include "eventqueue.h"
+#include "ai_playerally.h"
+#include "SoundEmitterSystem/isoundemittersystembase.h"
+#include "entityblocker.h"
+#include "npcevent.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+#define ACTBUSY_SEE_ENTITY_TIMEOUT 1.0f
+
+#define ACTBUSY_COMBAT_PLAYER_MAX_DIST 720.0f // NPCs in combat actbusy should try to stay within this distance of the player.
+
+ConVar ai_actbusy_search_time( "ai_actbusy_search_time","10.0" );
+ConVar ai_debug_actbusy( "ai_debug_actbusy", "0", FCVAR_CHEAT, "Used to debug actbusy behavior. Usage:\n\
+1: Constantly draw lines from NPCs to the actbusy nodes they've chosen to actbusy at.\n\
+2: Whenever an NPC makes a decision to use an actbusy, show which actbusy they've chosen.\n\
+3: Selected NPCs (with npc_select) will report why they're not choosing actbusy nodes.\n\
+4: Display debug output of actbusy logic.\n\
+5: Display safe zone volumes and info.\n\
+");
+
+// Anim events
+static int AE_ACTBUSY_WEAPON_FIRE_ON;
+static int AE_ACTBUSY_WEAPON_FIRE_OFF;
+
+BEGIN_DATADESC( CAI_ActBusyBehavior )
+ DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bForceActBusy, FIELD_BOOLEAN ),
+ DEFINE_CUSTOM_FIELD( m_ForcedActivity, ActivityDataOps() ),
+ DEFINE_FIELD( m_bTeleportToBusy, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bUseNearestBusy, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bLeaving, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bVisibleOnly, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bUseRenderBoundsForCollision, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flForcedMaxTime, FIELD_FLOAT ),
+ DEFINE_FIELD( m_bBusy, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bMovingToBusy, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bNeedsToPlayExitAnim, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flNextBusySearchTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flEndBusyAt, FIELD_TIME ),
+ DEFINE_FIELD( m_flBusySearchRange, FIELD_FLOAT ),
+ DEFINE_FIELD( m_bInQueue, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_iCurrentBusyAnim, FIELD_INTEGER ),
+ DEFINE_FIELD( m_hActBusyGoal, FIELD_EHANDLE ),
+ DEFINE_FIELD( m_bNeedToSetBounds, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_hSeeEntity, FIELD_EHANDLE ),
+ DEFINE_FIELD( m_fTimeLastSawSeeEntity, FIELD_TIME ),
+ DEFINE_FIELD( m_bExitedBusyToDueLostSeeEntity, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bExitedBusyToDueSeeEnemy, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_iNumConsecutivePathFailures, FIELD_INTEGER ),
+ DEFINE_FIELD( m_bAutoFireWeapon, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flDeferUntil, FIELD_TIME ),
+ DEFINE_FIELD( m_iNumEnemiesInSafeZone, FIELD_INTEGER ),
+END_DATADESC();
+
+enum
+{
+ ACTBUSY_SIGHT_METHOD_FULL = 0, // LOS and Viewcone
+ ACTBUSY_SIGHT_METHOD_LOS_ONLY,
+};
+
+//=============================================================================
+//-----------------------------------------------------------------------------
+// Purpose: Gamesystem that parses the act busy anim file
+//-----------------------------------------------------------------------------
+class CActBusyAnimData : public CAutoGameSystem
+{
+public:
+ CActBusyAnimData( void ) : CAutoGameSystem( "CActBusyAnimData" )
+ {
+ }
+
+ // Inherited from IAutoServerSystem
+ virtual void LevelInitPostEntity( void );
+ virtual void LevelShutdownPostEntity( void );
+
+ // Read in the data from the anim data file
+ void ParseAnimDataFile( void );
+
+ // Parse a keyvalues section into an act busy anim
+ bool ParseActBusyFromKV( busyanim_t *pAnim, KeyValues *pSection );
+
+ // Purpose: Returns the index of the busyanim data for the specified activity or sequence
+ int FindBusyAnim( Activity iActivity, const char *pSequence );
+
+ busyanim_t *GetBusyAnim( int iIndex ) { return &m_ActBusyAnims[iIndex]; }
+
+protected:
+ CUtlVector<busyanim_t> m_ActBusyAnims;
+};
+
+CActBusyAnimData g_ActBusyAnimDataSystem;
+
+//-----------------------------------------------------------------------------
+// Inherited from IAutoServerSystem
+//-----------------------------------------------------------------------------
+void CActBusyAnimData::LevelInitPostEntity( void )
+{
+ ParseAnimDataFile();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CActBusyAnimData::LevelShutdownPostEntity( void )
+{
+ m_ActBusyAnims.Purge();
+}
+
+//-----------------------------------------------------------------------------
+// Clear out the stats + their history
+//-----------------------------------------------------------------------------
+void CActBusyAnimData::ParseAnimDataFile( void )
+{
+ KeyValues *pKVAnimData = new KeyValues( "ActBusyAnimDatafile" );
+ if ( pKVAnimData->LoadFromFile( filesystem, "scripts/actbusy.txt" ) )
+ {
+ // Now try and parse out each act busy anim
+ KeyValues *pKVAnim = pKVAnimData->GetFirstSubKey();
+ while ( pKVAnim )
+ {
+ // Create a new anim and add it to our list
+ int index = m_ActBusyAnims.AddToTail();
+ busyanim_t *pAnim = &m_ActBusyAnims[index];
+ if ( !ParseActBusyFromKV( pAnim, pKVAnim ) )
+ {
+ m_ActBusyAnims.Remove( index );
+ }
+
+ pKVAnim = pKVAnim->GetNextKey();
+ }
+ }
+ pKVAnimData->deleteThis();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Parse a keyvalues section into the prop
+//-----------------------------------------------------------------------------
+bool CActBusyAnimData::ParseActBusyFromKV( busyanim_t *pAnim, KeyValues *pSection )
+{
+ pAnim->iszName = AllocPooledString( pSection->GetName() );
+
+ // Activities
+ pAnim->iActivities[BA_BUSY] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "busy_anim", "ACT_INVALID" ) );
+ pAnim->iActivities[BA_ENTRY] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "entry_anim", "ACT_INVALID" ) );
+ pAnim->iActivities[BA_EXIT] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "exit_anim", "ACT_INVALID" ) );
+
+ // Sequences
+ pAnim->iszSequences[BA_BUSY] = AllocPooledString( pSection->GetString( "busy_sequence", NULL ) );
+ pAnim->iszSequences[BA_ENTRY] = AllocPooledString( pSection->GetString( "entry_sequence", NULL ) );
+ pAnim->iszSequences[BA_EXIT] = AllocPooledString( pSection->GetString( "exit_sequence", NULL ) );
+
+ // Sounds
+ pAnim->iszSounds[BA_BUSY] = AllocPooledString( pSection->GetString( "busy_sound", NULL ) );
+ pAnim->iszSounds[BA_ENTRY] = AllocPooledString( pSection->GetString( "entry_sound", NULL ) );
+ pAnim->iszSounds[BA_EXIT] = AllocPooledString( pSection->GetString( "exit_sound", NULL ) );
+
+ // Times
+ pAnim->flMinTime = pSection->GetFloat( "min_time", 10.0 );
+ pAnim->flMaxTime = pSection->GetFloat( "max_time", 20.0 );
+
+ pAnim->bUseAutomovement = pSection->GetInt( "use_automovement", 0 ) != 0;
+
+ const char *sInterrupt = pSection->GetString( "interrupts", "BA_INT_DANGER" );
+ if ( !strcmp( sInterrupt, "BA_INT_PLAYER" ) )
+ {
+ pAnim->iBusyInterruptType = BA_INT_PLAYER;
+ }
+ else if ( !strcmp( sInterrupt, "BA_INT_DANGER" ) )
+ {
+ pAnim->iBusyInterruptType = BA_INT_DANGER;
+ }
+ else if ( !strcmp( sInterrupt, "BA_INT_AMBUSH" ) )
+ {
+ pAnim->iBusyInterruptType = BA_INT_AMBUSH;
+ }
+ else if ( !strcmp( sInterrupt, "BA_INT_COMBAT" ) )
+ {
+ pAnim->iBusyInterruptType = BA_INT_COMBAT;
+ }
+ else if ( !strcmp( sInterrupt, "BA_INT_ZOMBIESLUMP" ))
+ {
+ pAnim->iBusyInterruptType = BA_INT_ZOMBIESLUMP;
+ }
+ else if ( !strcmp( sInterrupt, "BA_INT_SIEGE_DEFENSE" ))
+ {
+ pAnim->iBusyInterruptType = BA_INT_SIEGE_DEFENSE;
+ }
+ else
+ {
+ pAnim->iBusyInterruptType = BA_INT_NONE;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns the busyanim data for the specified activity
+//-----------------------------------------------------------------------------
+int CActBusyAnimData::FindBusyAnim( Activity iActivity, const char *pSequence )
+{
+ int iCount = m_ActBusyAnims.Count();
+ for ( int i = 0; i < iCount; i++ )
+ {
+ busyanim_t *pBusyAnim = &m_ActBusyAnims[i];
+ Assert( pBusyAnim );
+
+ if ( pSequence && pBusyAnim->iszName != NULL_STRING && !Q_stricmp( STRING(pBusyAnim->iszName), pSequence ) )
+ return i;
+
+ if ( iActivity != ACT_INVALID && pBusyAnim->iActivities[BA_BUSY] == iActivity )
+ return i;
+ }
+
+ if ( pSequence )
+ {
+ Warning("Specified '%s' as a busy anim name, and it's not in the act busy anim list.\n", pSequence );
+ }
+ else if ( iActivity != ACT_INVALID )
+ {
+ Warning("Tried to use Activity %d as a busy anim, and it's not in the act busy anim list.\n", iActivity );
+ }
+
+ return -1;
+}
+
+//=============================================================================================================
+//=============================================================================================================
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CAI_ActBusyBehavior::CAI_ActBusyBehavior()
+{
+ // Disabled by default. Enabled by map entity.
+ m_bEnabled = false;
+ m_bUseRenderBoundsForCollision = false;
+ m_flDeferUntil = 0;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::Enable( CAI_ActBusyGoal *pGoal, float flRange, bool bVisibleOnly )
+{
+ NotifyBusyEnding();
+
+ if ( pGoal )
+ {
+ m_hActBusyGoal = pGoal;
+ }
+ m_bEnabled = true;
+ m_bBusy = false;
+ m_bMovingToBusy = false;
+ m_bNeedsToPlayExitAnim = false;
+ m_bLeaving = false;
+ m_flNextBusySearchTime = gpGlobals->curtime + ai_actbusy_search_time.GetFloat();
+ m_flEndBusyAt = 0;
+ m_bVisibleOnly = bVisibleOnly;
+ m_bInQueue = dynamic_cast<CAI_ActBusyQueueGoal*>(m_hActBusyGoal.Get()) != NULL;
+ m_ForcedActivity = ACT_INVALID;
+ m_hSeeEntity = NULL;
+ m_bExitedBusyToDueLostSeeEntity = false;
+ m_bExitedBusyToDueSeeEnemy = false;
+ m_iNumConsecutivePathFailures = 0;
+
+ SetBusySearchRange( flRange );
+
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: behavior enabled on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ }
+
+ if( IsCombatActBusy() )
+ {
+ CollectSafeZoneVolumes( pGoal );
+ }
+
+ // Robin: Due to ai goal entities delaying their EnableGoal call on each
+ // of their target Actors, NPCs that are spawned with active actbusies
+ // will have their SelectSchedule() called before their behavior has been
+ // enabled. To fix this, if we're enabled while in a schedule that can be
+ // overridden, immediately act busy.
+ if ( IsCurScheduleOverridable() )
+ {
+ // Force search time to be now.
+ m_flNextBusySearchTime = gpGlobals->curtime;
+ GetOuter()->ClearSchedule( "Enabling act busy" );
+ }
+
+ ClearCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::OnRestore()
+{
+ if ( m_bEnabled && m_hActBusyGoal != NULL && IsCombatActBusy() )
+ {
+ CollectSafeZoneVolumes( m_hActBusyGoal );
+ }
+
+ BaseClass::OnRestore();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::SetBusySearchRange( float flRange )
+{
+ m_flBusySearchRange = flRange;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::Disable( void )
+{
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: behavior disabled on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ }
+
+ if ( m_bEnabled )
+ {
+ SetCondition( COND_PROVOKED );
+ }
+
+ StopBusying();
+ m_bEnabled = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::ForceActBusy( CAI_ActBusyGoal *pGoal, CAI_Hint *pHintNode, float flMaxTime, bool bVisibleOnly, bool bTeleportToBusy, bool bUseNearestBusy, CBaseEntity *pSeeEntity, Activity activity )
+{
+ Assert( !m_bLeaving );
+
+ if ( m_bNeedsToPlayExitAnim )
+ {
+ // If we hit this, the mapmaker's told this NPC to actbusy somewhere while it's still in an actbusy.
+ // Right now, we don't support this. We could support it with a bit of work.
+ if ( HasAnimForActBusy( m_iCurrentBusyAnim, BA_EXIT ) )
+ {
+ Warning("ACTBUSY: %s(%s) was told to actbusy while inside an actbusy that needs to exit first. IGNORING.\n", GetOuter()->GetDebugName(), GetOuter()->GetClassname() );
+ return;
+ }
+ }
+
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: ForceActBusy on NPC %s (%s): ", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ if ( pHintNode )
+ {
+ Msg("Hintnode %s", pHintNode->GetDebugName());
+ }
+ else
+ {
+ Msg("No Hintnode specified");
+ }
+ Msg("\n");
+ }
+
+ Enable( pGoal, m_flBusySearchRange, bVisibleOnly );
+ m_bForceActBusy = true;
+ m_flForcedMaxTime = flMaxTime;
+ m_bTeleportToBusy = bTeleportToBusy;
+ m_bUseNearestBusy = bUseNearestBusy;
+ m_ForcedActivity = activity;
+ m_hSeeEntity = pSeeEntity;
+
+ if ( pHintNode )
+ {
+ if ( GetHintNode() && GetHintNode() != pHintNode )
+ {
+ GetHintNode()->Unlock();
+ }
+
+ if ( pHintNode->Lock( GetOuter() ) )
+ {
+ SetHintNode( pHintNode );
+ }
+ }
+
+ SetCondition( COND_PROVOKED );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Force the NPC to find an exit node and leave
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::ForceActBusyLeave( bool bVisibleOnly )
+{
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: ForceActBusyLeave on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ }
+
+ Enable( NULL, m_flBusySearchRange, bVisibleOnly );
+ m_bForceActBusy = true;
+ m_bLeaving = true;
+ m_ForcedActivity = ACT_INVALID;
+ m_hSeeEntity = NULL;
+
+ SetCondition( COND_PROVOKED );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Break the NPC out of the current busy state, but don't disable busying
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::StopBusying( void )
+{
+ if ( !GetOuter() )
+ return;
+
+ // Make sure we turn this off unconditionally!
+ m_bAutoFireWeapon = false;
+
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: StopBusying on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ }
+
+ if ( m_bBusy || m_bMovingToBusy )
+ {
+ SetCondition( COND_PROVOKED );
+ }
+
+ m_flEndBusyAt = gpGlobals->curtime;
+ m_bForceActBusy = false;
+ m_bTeleportToBusy = false;
+ m_bUseNearestBusy = false;
+ m_bLeaving = false;
+ m_bMovingToBusy = false;
+ m_ForcedActivity = ACT_INVALID;
+ m_hSeeEntity = NULL;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsStopBusying()
+{
+ return IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Find a general purpose, suitable Hint Node for me to Act Busy.
+//-----------------------------------------------------------------------------
+CAI_Hint *CAI_ActBusyBehavior::FindActBusyHintNode()
+{
+ Assert( !IsCombatActBusy() );
+
+ int iBits = bits_HINT_NODE_USE_GROUP;
+ if ( m_bVisibleOnly )
+ {
+ iBits |= bits_HINT_NODE_VISIBLE;
+ }
+
+ if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT )
+ {
+ iBits |= bits_HINT_NODE_REPORT_FAILURES;
+ }
+
+ if ( m_bUseNearestBusy )
+ {
+ iBits |= bits_HINT_NODE_NEAREST;
+ }
+ else
+ {
+ iBits |= bits_HINT_NODE_RANDOM;
+ }
+
+ CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), HINT_WORLD_WORK_POSITION, iBits, m_flBusySearchRange );
+
+ return pNode;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Find a node for me to combat act busy.
+//
+// Right now, all of this work assumes the actbusier is a player ally and
+// wants to fight near the player a'la Alyx in ep2_outland_10.
+//-----------------------------------------------------------------------------
+CAI_Hint *CAI_ActBusyBehavior::FindCombatActBusyHintNode()
+{
+ Assert( IsCombatActBusy() );
+
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+
+ if( !pPlayer )
+ return NULL;
+
+ CHintCriteria criteria;
+
+ // Ok, find a hint node THAT:
+ // -Is in my hint group
+ // -Is Visible (if specified by designer)
+ // -Is Closest to me (if specified by designer)
+ // -The player can see
+ // -Is within the accepted max dist from player
+ int iBits = bits_HINT_NODE_USE_GROUP;
+
+ if ( m_bVisibleOnly )
+ iBits |= bits_HINT_NODE_VISIBLE;
+
+ if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT )
+ iBits |= bits_HINT_NODE_REPORT_FAILURES;
+
+ if ( m_bUseNearestBusy )
+ iBits |= bits_HINT_NODE_NEAREST;
+ else
+ iBits |= bits_HINT_NODE_RANDOM;
+
+ iBits |= bits_HAS_EYEPOSITION_LOS_TO_PLAYER;
+
+ criteria.AddHintType( HINT_WORLD_WORK_POSITION );
+ criteria.SetFlag( iBits );
+ criteria.AddIncludePosition( pPlayer->GetAbsOrigin(), ACTBUSY_COMBAT_PLAYER_MAX_DIST );
+
+ CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), criteria );
+
+ return pNode;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Find a suitable combat actbusy node to teleport to. That is, find
+// one that the player is not going to see me appear at.
+//-----------------------------------------------------------------------------
+CAI_Hint *CAI_ActBusyBehavior::FindCombatActBusyTeleportHintNode()
+{
+ Assert( IsCombatActBusy() );
+
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+
+ if( !pPlayer )
+ return NULL;
+
+ CHintCriteria criteria;
+
+ // Ok, find a hint node THAT:
+ // -Is in my hint group
+ // -The player CAN NOT see so that they don't see me teleport
+ int iBits = bits_HINT_NODE_USE_GROUP;
+
+ if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT )
+ iBits |= bits_HINT_NODE_REPORT_FAILURES;
+
+ iBits |= bits_HINT_NODE_RANDOM;
+ iBits |= bits_HINT_NODE_NOT_VISIBLE_TO_PLAYER;
+
+ criteria.AddHintType( HINT_WORLD_WORK_POSITION );
+ criteria.SetFlag( iBits );
+ criteria.AddIncludePosition( pPlayer->GetAbsOrigin(), ACTBUSY_COMBAT_PLAYER_MAX_DIST * 1.1f );
+
+ CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), criteria );
+
+ return pNode;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::FValidateHintType( CAI_Hint *pHint )
+{
+ if ( pHint->HintType() != HINT_WORLD_WORK_POSITION && pHint->HintType() != HINT_NPC_EXIT_POINT )
+ return false;
+
+ // If the node doesn't want to be teleported to, we need to check for clear
+ const char *pSequenceOrActivity = STRING(pHint->HintActivityName());
+ const char *cSpace = strchr( pSequenceOrActivity, ' ' );
+ if ( cSpace )
+ {
+ if ( !Q_strncmp( cSpace+1, "teleport", 8 ) )
+ {
+ // Node is a teleport node, so it's good
+ return true;
+ }
+ }
+
+ // Check for clearance
+ trace_t tr;
+ AI_TraceHull( pHint->GetAbsOrigin(), pHint->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), MASK_SOLID, GetOuter(), COLLISION_GROUP_NONE, &tr );
+ if ( tr.fraction == 1.0 )
+ return true;
+
+ // Report failures
+ if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT )
+ {
+ NDebugOverlay::Text( pHint->GetAbsOrigin(), "Node isn't clear.", false, 60 );
+ NDebugOverlay::Box( pHint->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), 255,0,0, 8, 2.0 );
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::CanSelectSchedule( void )
+{
+ // Always active when we're busy
+ if ( m_bBusy || m_bForceActBusy || m_bNeedsToPlayExitAnim )
+ return true;
+
+ if ( !m_bEnabled )
+ return false;
+
+ if ( m_flDeferUntil > gpGlobals->curtime )
+ return false;
+
+ if ( CountEnemiesInSafeZone() > 0 )
+ {
+ // I have enemies left in the safe zone. Actbusy isn't appropriate.
+ // I should be off fighting them.
+ return false;
+ }
+
+ if ( !IsCurScheduleOverridable() )
+ return false;
+
+ // Don't select actbusy if we're not going to search for a node anyway
+ return (m_flNextBusySearchTime < gpGlobals->curtime);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if the current schedule is one that ActBusy is allowed to override
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsCurScheduleOverridable( void )
+{
+ if( IsCombatActBusy() )
+ {
+ // The whole point of a combat actbusy is that it can run in any state (including combat)
+ // the only exception is SCRIPT (sjb)
+ return (GetOuter()->GetState() != NPC_STATE_SCRIPT);
+ }
+
+ // Act busies are not valid inside of a vehicle
+ if ( GetOuter()->IsInAVehicle() )
+ return false;
+
+ // Only if we're about to idle (or SCHED_NONE to catch newly spawned guys)
+ return ( IsCurSchedule( SCHED_IDLE_STAND ) || IsCurSchedule( SCHED_NONE ) );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pSound -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::ShouldIgnoreSound( CSound *pSound )
+{
+ // If we're busy in an actbusy anim that's an ambush, stay mum as long as we can
+ if ( m_bBusy )
+ {
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+
+ if( pBusyAnim && pBusyAnim->iBusyInterruptType == BA_INT_ZOMBIESLUMP )
+ {
+ // Slumped zombies are deaf.
+ return true;
+ }
+
+ if ( pBusyAnim && ( ( pBusyAnim->iBusyInterruptType == BA_INT_AMBUSH ) || ( pBusyAnim->iBusyInterruptType == BA_INT_COMBAT ) ) )
+ {
+ /*
+ // Robin: First version ignored sounds in front of the NPC.
+ Vector vecToSound = (pSound->GetSoundReactOrigin() - GetAbsOrigin());
+ vecToSound.z = 0;
+ VectorNormalize( vecToSound );
+ Vector facingDir = GetOuter()->EyeDirection2D();
+ if ( DotProduct( vecToSound, facingDir ) > 0 )
+ return true;
+ */
+
+ // Ignore sounds that aren't visible
+ if ( !GetOuter()->FVisible( pSound->GetSoundReactOrigin() ) )
+ return true;
+ }
+ }
+
+ return BaseClass::ShouldIgnoreSound( pSound );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::OnFriendDamaged( CBaseCombatCharacter *pSquadmate, CBaseEntity *pAttacker )
+{
+ if( IsCombatActBusy() && pSquadmate->IsPlayer() && IsInSafeZone( pAttacker ) )
+ {
+ SetCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); // Break the actbusy, if we're running it.
+ m_flDeferUntil = gpGlobals->curtime + 4.0f; // Stop actbusying and go deal with that enemy!!
+ }
+
+ BaseClass::OnFriendDamaged( pSquadmate, pAttacker );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Count the number of enemies of mine that are inside my safe zone
+// volume.
+//
+// NOTE: We keep this count to prevent the NPC re-entering combat
+// actbusy whilst too many enemies are present in the safe zone.
+// This count does not automatically alert the NPC that there are
+// enemies in the safe zone.
+// You must set COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE to let
+// the NPC know.
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::CountEnemiesInSafeZone()
+{
+ if( !IsCombatActBusy() )
+ {
+ return 0;
+ }
+
+ // Grovel the AI list and count the enemies in the zone. By enemies, I mean
+ // anyone that I would fight if I saw.
+ CAI_BaseNPC ** ppAIs = g_AI_Manager.AccessAIs();
+ int nAIs = g_AI_Manager.NumAIs();
+ int count = 0;
+
+ for ( int i = 0; i < nAIs; i++ )
+ {
+ if( GetOuter()->IRelationType(ppAIs[i]) < D_LI )
+ {
+ if( IsInSafeZone(ppAIs[i]) )
+ {
+ count++;
+ }
+ }
+ }
+
+ return count;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::OnTakeDamage_Alive( const CTakeDamageInfo &info )
+{
+ if( IsCombatActBusy() && info.GetAttacker() && IsInSafeZone( info.GetAttacker() ) )
+ {
+ SetCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); // Break the actbusy, if we're running it.
+ m_flDeferUntil = gpGlobals->curtime + 4.0f; // Stop actbusying and go deal with that enemy!!
+ }
+
+ return BaseClass::OnTakeDamage_Alive( info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::GatherConditions( void )
+{
+ // Clear this condition before we call up, since the look sensing code will run and
+ // set this condition if it is relevant.
+ if( !IsCurSchedule(SCHED_ACTBUSY_BUSY, false) )
+ {
+ // Only clear this condition when we aren't busying. We want it to be sticky
+ // during that time so that schedule selection works properly (sjb)
+ ClearCondition( COND_ACTBUSY_ENEMY_TOO_CLOSE );
+ }
+
+ BaseClass::GatherConditions();
+
+ bool bCheckLOS = true;
+ bool bCheckFOV = true;
+
+ if( m_hActBusyGoal && m_hActBusyGoal->m_iSightMethod == ACTBUSY_SIGHT_METHOD_LOS_ONLY )
+ {
+ bCheckFOV = false;
+ }
+
+ // If we have a see entity, make sure we can still see it
+ if ( m_hSeeEntity && m_bBusy )
+ {
+ if ( (!bCheckFOV||GetOuter()->FInViewCone(m_hSeeEntity)) && GetOuter()->QuerySeeEntity(m_hSeeEntity) && (!bCheckLOS||GetOuter()->FVisible(m_hSeeEntity)) )
+ {
+ m_fTimeLastSawSeeEntity = gpGlobals->curtime;
+ ClearCondition( COND_ACTBUSY_LOST_SEE_ENTITY );
+ }
+ else if( m_hActBusyGoal )
+ {
+ float fDelta = gpGlobals->curtime - m_fTimeLastSawSeeEntity;
+ if( fDelta >= m_hActBusyGoal->m_flSeeEntityTimeout )
+ {
+ SetCondition( COND_ACTBUSY_LOST_SEE_ENTITY );
+ m_hActBusyGoal->NPCLostSeeEntity( GetOuter() );
+
+ if( IsCombatActBusy() && (GetOuter()->Classify() == CLASS_PLAYER_ALLY_VITAL && m_hSeeEntity->IsPlayer()) )
+ {
+ // Defer any actbusying for several seconds. This serves as a heuristic for waiting
+ // for the player to settle after moving out of the room. This helps Alyx pick a more
+ // pertinent Actbusy near the player's new location.
+ m_flDeferUntil = gpGlobals->curtime + 4.0f;
+ }
+ }
+ }
+ }
+ else
+ {
+ ClearCondition( COND_ACTBUSY_LOST_SEE_ENTITY );
+ }
+
+ // If we're busy, ignore sounds depending on our actbusy break rules
+ if ( m_bBusy )
+ {
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+ if ( pBusyAnim )
+ {
+ switch( pBusyAnim->iBusyInterruptType )
+ {
+ case BA_INT_DANGER:
+ break;
+
+ case BA_INT_AMBUSH:
+ break;
+
+ case BA_INT_ZOMBIESLUMP:
+ {
+ ClearCondition( COND_HEAR_PLAYER );
+ ClearCondition( COND_SEE_ENEMY );
+ ClearCondition( COND_NEW_ENEMY );
+
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex(1);
+
+ if( pPlayer )
+ {
+ float flDist = pPlayer->GetAbsOrigin().DistTo( GetAbsOrigin() );
+
+ if( flDist <= 60 )
+ {
+ StopBusying();
+ }
+ }
+ }
+ break;
+
+ case BA_INT_COMBAT:
+ // Ignore the player unless he shoots at us
+ ClearCondition( COND_HEAR_PLAYER );
+ ClearCondition( COND_SEE_ENEMY );
+ ClearCondition( COND_NEW_ENEMY );
+ break;
+
+ case BA_INT_PLAYER:
+ // Clear all but player.
+ ClearCondition( COND_HEAR_DANGER );
+ ClearCondition( COND_HEAR_COMBAT );
+ ClearCondition( COND_HEAR_WORLD );
+ ClearCondition( COND_HEAR_BULLET_IMPACT );
+ break;
+
+ case BA_INT_SIEGE_DEFENSE:
+ ClearCondition( COND_HEAR_PLAYER );
+ ClearCondition( COND_SEE_ENEMY );
+ ClearCondition( COND_NEW_ENEMY );
+ ClearCondition( COND_HEAR_COMBAT );
+ ClearCondition( COND_HEAR_WORLD );
+ ClearCondition( COND_HEAR_BULLET_IMPACT );
+ break;
+
+ case BA_INT_NONE:
+ // Clear all
+ ClearCondition( COND_HEAR_DANGER );
+ ClearCondition( COND_HEAR_COMBAT );
+ ClearCondition( COND_HEAR_WORLD );
+ ClearCondition( COND_HEAR_BULLET_IMPACT );
+ ClearCondition( COND_HEAR_PLAYER );
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ if( m_bAutoFireWeapon && random->RandomInt(0, 5) <= 3 )
+ {
+ CBaseCombatWeapon *pWeapon = GetOuter()->GetActiveWeapon();
+
+ if( pWeapon )
+ {
+ pWeapon->Operator_ForceNPCFire( GetOuter(), false );
+ }
+ }
+
+ if( ai_debug_actbusy.GetInt() == 5 )
+ {
+ // Visualize them there Actbusy safe volumes
+ for( int i = 0 ; i < m_SafeZones.Count() ; i++ )
+ {
+ busysafezone_t *pSafeZone = &m_SafeZones[i];
+
+ Vector vecBoxOrigin = (pSafeZone->vecMins + pSafeZone->vecMaxs) * 0.5f;
+ Vector vecBoxMins = vecBoxOrigin - pSafeZone->vecMins;
+ Vector vecBoxMaxs = vecBoxOrigin - pSafeZone->vecMaxs;
+ NDebugOverlay::Box( vecBoxOrigin, vecBoxMins, vecBoxMaxs, 255, 0, 255, 64, 0.2f );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::EndScheduleSelection( void )
+{
+ NotifyBusyEnding();
+
+ CheckAndCleanupOnExit();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : nActivity -
+//-----------------------------------------------------------------------------
+Activity CAI_ActBusyBehavior::NPC_TranslateActivity( Activity nActivity )
+{
+ // Find out what the base class wants to do with the activity
+ Activity nNewActivity = BaseClass::NPC_TranslateActivity( nActivity );
+
+ if( nActivity == ACT_RUN )
+ {
+ // FIXME: Forcing STIMULATED here is illegal if the entity doesn't support it as an activity
+ CAI_PlayerAlly *pAlly = dynamic_cast<CAI_PlayerAlly*>(GetOuter());
+ if ( pAlly )
+ return ACT_RUN_STIMULATED;
+ }
+
+ // Else stay with the base class' decision.
+ return nNewActivity;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::HandleAnimEvent( animevent_t *pEvent )
+{
+ if( pEvent->event == AE_ACTBUSY_WEAPON_FIRE_ON )
+ {
+ m_bAutoFireWeapon = true;
+ return;
+ }
+ else if( pEvent->event == AE_ACTBUSY_WEAPON_FIRE_OFF )
+ {
+ m_bAutoFireWeapon = false;
+ return;
+ }
+
+
+ return BaseClass::HandleAnimEvent( pEvent );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Actbusy's ending, ensure we haven't left NPC in broken state.
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::CheckAndCleanupOnExit( void )
+{
+ if ( m_bNeedsToPlayExitAnim && !GetOuter()->IsMarkedForDeletion() && GetOuter()->IsAlive() )
+ {
+ Warning("NPC %s(%s) left actbusy without playing exit anim.\n", GetOuter()->GetDebugName(), GetOuter()->GetClassname() );
+ m_bNeedsToPlayExitAnim = false;
+ }
+
+ GetOuter()->RemoveFlag( FL_FLY );
+
+ // If we're supposed to use render bounds while inside the busy anim, restore normal now
+ if ( m_bUseRenderBoundsForCollision )
+ {
+ GetOuter()->SetHullSizeNormal( true );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::BuildScheduleTestBits( void )
+{
+ BaseClass::BuildScheduleTestBits();
+
+ // When going to an actbusy, we can't be interrupted during the entry anim
+ if ( IsCurSchedule(SCHED_ACTBUSY_START_BUSYING) )
+ {
+ if ( GetOuter()->GetTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY )
+ return;
+
+ GetOuter()->SetCustomInterruptCondition( COND_PROVOKED );
+
+ if( IsCombatActBusy() )
+ {
+ GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_ENEMY_TOO_CLOSE) );
+ }
+ }
+
+ // If we're in a queue, or leaving, we have no extra conditions
+ if ( m_bInQueue || IsCurSchedule( SCHED_ACTBUSY_LEAVE ) )
+ return;
+
+ // If we're not busy, or we're exiting a busy, we have no extra conditions
+ if ( !m_bBusy || IsCurSchedule( SCHED_ACTBUSY_STOP_BUSYING ) )
+ return;
+
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+ if ( pBusyAnim )
+ {
+ switch( pBusyAnim->iBusyInterruptType )
+ {
+ case BA_INT_ZOMBIESLUMP:
+ {
+ GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ }
+ break;
+
+ case BA_INT_SIEGE_DEFENSE:
+ {
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER );
+ GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE) );
+ GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_ENEMY_TOO_CLOSE) );
+ }
+ break;
+
+ case BA_INT_AMBUSH:
+ case BA_INT_DANGER:
+ {
+ GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_COMBAT );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_BULLET_IMPACT );
+ GetOuter()->SetCustomInterruptCondition( COND_NEW_ENEMY );
+ GetOuter()->SetCustomInterruptCondition( COND_SEE_ENEMY );
+ GetOuter()->SetCustomInterruptCondition( COND_PLAYER_ADDED_TO_SQUAD );
+ GetOuter()->SetCustomInterruptCondition( COND_RECEIVED_ORDERS );
+ break;
+ }
+
+ case BA_INT_PLAYER:
+ {
+ GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_COMBAT );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_BULLET_IMPACT );
+ GetOuter()->SetCustomInterruptCondition( COND_NEW_ENEMY );
+ GetOuter()->SetCustomInterruptCondition( COND_PLAYER_ADDED_TO_SQUAD );
+ GetOuter()->SetCustomInterruptCondition( COND_RECEIVED_ORDERS );
+
+ // The player can interrupt us
+ GetOuter()->SetCustomInterruptCondition( COND_SEE_PLAYER );
+ break;
+ }
+
+ case BA_INT_COMBAT:
+ {
+ GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER );
+ break;
+ }
+
+ case BA_INT_NONE:
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::SelectScheduleForLeaving( void )
+{
+ // Are we already near an exit node?
+ if ( GetHintNode() )
+ {
+ if ( GetHintNode()->HintType() == HINT_NPC_EXIT_POINT )
+ {
+ // Are we near it? If so, we're done. If not, move to it.
+ if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) < 64 )
+ {
+ if ( !GetOuter()->IsMarkedForDeletion() )
+ {
+ CBaseEntity *pOwner = GetOuter()->GetOwnerEntity();
+ if ( pOwner )
+ {
+ pOwner->DeathNotice( GetOuter() );
+ GetOuter()->SetOwnerEntity( NULL );
+ }
+ GetOuter()->SetThink( &CBaseEntity::SUB_Remove); //SUB_Remove) ; //GetOuter()->SUB_Remove );
+ GetOuter()->SetNextThink( gpGlobals->curtime + 0.1 );
+
+ if ( m_hActBusyGoal )
+ {
+ m_hActBusyGoal->NPCLeft( GetOuter() );
+ }
+ }
+
+ return SCHED_IDLE_STAND;
+ }
+
+ return SCHED_ACTBUSY_LEAVE;
+ }
+ else
+ {
+ // Clear the node, it's no use to us
+ GetHintNode()->NPCStoppedUsing( GetOuter() );
+ GetHintNode()->Unlock();
+ SetHintNode( NULL );
+ }
+ }
+
+ // Find an exit node
+ CHintCriteria hintCriteria;
+ hintCriteria.SetHintType( HINT_NPC_EXIT_POINT );
+ hintCriteria.SetFlag( bits_HINT_NODE_RANDOM | bits_HINT_NODE_CLEAR | bits_HINT_NODE_USE_GROUP );
+ CAI_Hint *pNode = CAI_HintManager::FindHintRandom( GetOuter(), GetOuter()->GetAbsOrigin(), hintCriteria );
+ if ( pNode )
+ {
+ SetHintNode( pNode );
+ return SCHED_ACTBUSY_LEAVE;
+ }
+
+ // We've been told to leave, but we can't find an exit node. What to do?
+ return SCHED_IDLE_STAND;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::SelectScheduleWhileNotBusy( int iBase )
+{
+ // Randomly act busy (unless we're being forced, in which case we should search immediately)
+ if ( m_bForceActBusy || m_flNextBusySearchTime < gpGlobals->curtime )
+ {
+ // If we're being forced, think again quickly
+ if ( m_bForceActBusy || IsCombatActBusy() )
+ {
+ m_flNextBusySearchTime = gpGlobals->curtime + 2.0;
+ }
+ else
+ {
+ m_flNextBusySearchTime = gpGlobals->curtime + RandomFloat(ai_actbusy_search_time.GetFloat(), ai_actbusy_search_time.GetFloat()*2);
+ }
+
+ // We may already have a node
+ bool bForceTeleport = false;
+ CAI_Hint *pNode = GetHintNode();
+ if ( !pNode )
+ {
+ if( IsCombatActBusy() )
+ {
+ if ( m_hActBusyGoal->IsCombatActBusyTeleportAllowed() && m_iNumConsecutivePathFailures >= 2 && !AI_GetSinglePlayer()->FInViewCone(GetOuter()) )
+ {
+ // Looks like I've tried several times to find a path to a valid hint node and
+ // haven't been able to. This means I'm on a patch of node graph that simply
+ // does not connect to any hint nodes that match my criteria. So try to find
+ // a node that's safe to teleport to. (sjb) ep2_outland_10 (Alyx)
+ // (Also, I must not be in the player's viewcone)
+ pNode = FindCombatActBusyTeleportHintNode();
+ bForceTeleport = true;
+ }
+ else
+ {
+ pNode = FindCombatActBusyHintNode();
+ }
+ }
+ else
+ {
+ pNode = FindActBusyHintNode();
+ }
+ }
+ if ( pNode )
+ {
+ // Ensure we've got a sequence for the node
+ const char *pSequenceOrActivity = STRING(pNode->HintActivityName());
+ Activity iNodeActivity;
+ int iBusyAnim;
+
+ // See if the node specifies that we should teleport to it
+ const char *cSpace = strchr( pSequenceOrActivity, ' ' );
+ if ( cSpace )
+ {
+ if ( !Q_strncmp( cSpace+1, "teleport", 8 ) )
+ {
+ m_bTeleportToBusy = true;
+ }
+
+ char sActOrSeqName[512];
+ Q_strncpy( sActOrSeqName, pSequenceOrActivity, (cSpace-pSequenceOrActivity)+1 );
+ iNodeActivity = (Activity)CAI_BaseNPC::GetActivityID( sActOrSeqName );
+ iBusyAnim = g_ActBusyAnimDataSystem.FindBusyAnim( iNodeActivity, sActOrSeqName );
+ }
+ else
+ {
+ iNodeActivity = (Activity)CAI_BaseNPC::GetActivityID( pSequenceOrActivity );
+ iBusyAnim = g_ActBusyAnimDataSystem.FindBusyAnim( iNodeActivity, pSequenceOrActivity );
+ }
+
+ // Does this NPC have the activity or sequence for this node?
+ if ( HasAnimForActBusy( iBusyAnim, BA_BUSY ) )
+ {
+ if ( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) )
+ {
+ // We've lost our see entity, which means we can't continue.
+ if ( m_bForceActBusy )
+ {
+ // We were being told to act busy, which we can't do now that we've lost the see entity.
+ // Abort, and assume that the mapmaker will make us retry.
+ StopBusying();
+ }
+ return iBase;
+ }
+
+ m_iCurrentBusyAnim = iBusyAnim;
+ if ( m_iCurrentBusyAnim == -1 )
+ return iBase;
+
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: NPC %s (%s) found Actbusy node %s \n", GetOuter()->GetClassname(), GetOuter()->GetDebugName(), pNode->GetDebugName() );
+ }
+
+ if ( GetHintNode() )
+ {
+ GetHintNode()->Unlock();
+ }
+
+ SetHintNode( pNode );
+ if ( GetHintNode() && GetHintNode()->Lock( GetOuter() ) )
+ {
+ if ( ai_debug_actbusy.GetInt() == 2 )
+ {
+ // Show which actbusy we're moving towards
+ NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), pNode->GetAbsOrigin(), 0, 255, 0, true, 5.0 );
+ NDebugOverlay::Box( pNode->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), 0, 255, 0, 64, 5.0 );
+ }
+
+ // Let our act busy know we're moving to a node
+ if ( m_hActBusyGoal )
+ {
+ m_hActBusyGoal->NPCMovingToBusy( GetOuter() );
+ }
+
+ m_bMovingToBusy = true;
+
+ if( m_hActBusyGoal && m_hActBusyGoal->m_iszSeeEntityName != NULL_STRING )
+ {
+ // Set the see entity Handle if we have one.
+ m_hSeeEntity.Set( gEntList.FindEntityByName(NULL, m_hActBusyGoal->m_iszSeeEntityName) );
+ }
+
+ // At this point we know we're starting.
+ ClearCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE );
+
+ // If we're supposed to teleport, do that instead
+ if ( m_bTeleportToBusy )
+ {
+ return SCHED_ACTBUSY_TELEPORT_TO_BUSY;
+ }
+ else if( bForceTeleport )
+ {
+ // We found a place to go, so teleport there and forget that we ever had trouble.
+ m_iNumConsecutivePathFailures = 0;
+ return SCHED_ACTBUSY_TELEPORT_TO_BUSY;
+ }
+
+ return SCHED_ACTBUSY_START_BUSYING;
+ }
+ }
+ }
+ else
+ {
+ // WE DIDN'T FIND A NODE!
+ if( IsCombatActBusy() )
+ {
+ // Don't try again right away, not enough state will have changed.
+ // Just go do something useful for a few seconds.
+ m_flNextBusySearchTime = gpGlobals->curtime + 10.0;
+ }
+ }
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::SelectScheduleWhileBusy( void )
+{
+ // Are we supposed to stop on our current actbusy, but stay in the actbusy state?
+ if ( !ActBusyNodeStillActive() || (m_flEndBusyAt && gpGlobals->curtime >= m_flEndBusyAt) )
+ {
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: NPC %s (%s) ending actbusy.\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() );
+ }
+
+ StopBusying();
+ return SCHED_ACTBUSY_STOP_BUSYING;
+ }
+
+ if( IsCombatActBusy() && (HasCondition(COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE) || HasCondition(COND_ACTBUSY_ENEMY_TOO_CLOSE)) )
+ {
+ return SCHED_ACTBUSY_STOP_BUSYING;
+ }
+
+ return SCHED_ACTBUSY_BUSY;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CAI_ActBusyBehavior::SelectSchedule()
+{
+ int iBase = BaseClass::SelectSchedule();
+
+ // Only do something if the base ai doesn't want to do anything
+ if ( !IsCombatActBusy() && !m_bForceActBusy && iBase != SCHED_IDLE_STAND )
+ {
+ // If we're busy, we need to get out of it first
+ if ( m_bBusy )
+ return SCHED_ACTBUSY_STOP_BUSYING;
+
+ CheckAndCleanupOnExit();
+ return iBase;
+ }
+
+ // If we're supposed to be leaving, find a leave node and exit
+ if ( m_bLeaving )
+ return SelectScheduleForLeaving();
+
+ // NPCs should not be busy if the actbusy behaviour has been disabled, or if they've received player squad commands
+ bool bShouldNotBeBusy = (!m_bEnabled || HasCondition( COND_PLAYER_ADDED_TO_SQUAD ) || HasCondition( COND_RECEIVED_ORDERS ) );
+ if ( bShouldNotBeBusy )
+ {
+ if ( !GetOuter()->IsMarkedForDeletion() && GetOuter()->IsAlive() )
+ return SCHED_ACTBUSY_STOP_BUSYING;
+ }
+ else
+ {
+ if ( m_bBusy )
+ return SelectScheduleWhileBusy();
+
+ // I'm not busy, and I'm supposed to be
+ int schedule = SelectScheduleWhileNotBusy( iBase );
+ if ( schedule != SCHED_NONE )
+ return schedule;
+ }
+
+ CheckAndCleanupOnExit();
+ return iBase;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::ActBusyNodeStillActive( void )
+{
+ if ( !GetHintNode() )
+ return false;
+
+ return ( GetHintNode()->IsDisabled() == false );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsInterruptable( void )
+{
+ if ( IsActive() )
+ return false;
+
+ return BaseClass::IsInterruptable();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::CanFlinch( void )
+{
+ if ( m_bNeedsToPlayExitAnim )
+ return false;
+
+ return BaseClass::CanFlinch();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::CanRunAScriptedNPCInteraction( bool bForced )
+{
+ // Prevent interactions during actbusy modes
+ if ( IsActive() )
+ return false;
+
+ return BaseClass::CanRunAScriptedNPCInteraction( bForced );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::OnScheduleChange()
+{
+ if( IsCurSchedule(SCHED_ACTBUSY_BUSY, false) )
+ {
+ if( HasCondition(COND_SEE_ENEMY) )
+ {
+ m_bExitedBusyToDueSeeEnemy = true;
+ }
+
+ if( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) )
+ {
+ m_bExitedBusyToDueLostSeeEntity = true;
+ }
+ }
+
+ BaseClass::OnScheduleChange();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::QueryHearSound( CSound *pSound )
+{
+ // Ignore friendly created combat sounds while in an actbusy.
+ // Fixes friendly NPCs going in & out of actbusies when the
+ // player fires shots at their feet.
+ if ( pSound->IsSoundType( SOUND_COMBAT ) || pSound->IsSoundType( SOUND_BULLET_IMPACT ) )
+ {
+ if ( GetOuter()->IRelationType( pSound->m_hOwner ) == D_LI )
+ return false;
+ }
+
+ return BaseClass::QueryHearSound( pSound );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Because none of the startbusy schedules break on COND_NEW_ENEMY
+// we have to do this distance check against all enemy NPCs we
+// see as we're traveling to an ACTBUSY node
+//-----------------------------------------------------------------------------
+#define ACTBUSY_ENEMY_TOO_CLOSE_DIST_SQR Square(240) // 20 feet
+void CAI_ActBusyBehavior::OnSeeEntity( CBaseEntity *pEntity )
+{
+ BaseClass::OnSeeEntity( pEntity );
+
+ if( IsCombatActBusy() && GetOuter()->IRelationType(pEntity) < D_LI )
+ {
+ if( pEntity->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) <= ACTBUSY_ENEMY_TOO_CLOSE_DIST_SQR )
+ {
+ SetCondition( COND_ACTBUSY_ENEMY_TOO_CLOSE );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::ShouldPlayerAvoid( void )
+{
+ if( IsCombatActBusy() )
+ {
+ // Alyx is only allowed to push if she's getting into or out of an actbusy
+ // animation. She isn't allowed to shove you around while she's running around
+ if ( IsCurSchedule(SCHED_ACTBUSY_START_BUSYING) )
+ {
+ if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY )
+ return true;
+ }
+ else if ( IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING) )
+ {
+ if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_EXIT )
+ return true;
+ }
+ }
+ else
+ {
+ if ( IsCurSchedule ( SCHED_ACTBUSY_START_BUSYING ) )
+ {
+ if ( ( GetCurTask() && GetCurTask()->iTask == TASK_WAIT_FOR_MOVEMENT ) || GetOuter()->GetTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY )
+ return true;
+ }
+ else if ( IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING) )
+ {
+ if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_EXIT )
+ return true;
+ }
+ }
+
+ return BaseClass::ShouldPlayerAvoid();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::ComputeAndSetRenderBounds()
+{
+ Vector mins, maxs;
+ if ( GetOuter()->ComputeHitboxSurroundingBox( &mins, &maxs ) )
+ {
+ UTIL_SetSize( GetOuter(), mins - GetAbsOrigin(), maxs - GetAbsOrigin());
+ if ( GetOuter()->VPhysicsGetObject() )
+ {
+ GetOuter()->SetupVPhysicsHull();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns true if the current NPC is acting busy, or moving to an actbusy
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsActive( void )
+{
+ return ( m_bBusy || m_bForceActBusy || m_bNeedsToPlayExitAnim || m_bLeaving );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsCombatActBusy()
+{
+ if( m_hActBusyGoal != NULL )
+ return (m_hActBusyGoal->GetType() == ACTBUSY_TYPE_COMBAT);
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::CollectSafeZoneVolumes( CAI_ActBusyGoal *pActBusyGoal )
+{
+ // Reset these, so we don't use a volume from a previous actbusy goal.
+ m_SafeZones.RemoveAll();
+
+ if( pActBusyGoal->m_iszSafeZoneVolume != NULL_STRING )
+ {
+ CBaseEntity *pVolume = gEntList.FindEntityByName( NULL, pActBusyGoal->m_iszSafeZoneVolume );
+
+ while( pVolume != NULL )
+ {
+ busysafezone_t newSafeZone;
+ pVolume->CollisionProp()->WorldSpaceAABB( &newSafeZone.vecMins, &newSafeZone.vecMaxs );
+ m_SafeZones.AddToTail( newSafeZone );
+ pVolume = gEntList.FindEntityByName( pVolume, pActBusyGoal->m_iszSafeZoneVolume );
+ }
+ }
+
+ if( ai_debug_actbusy.GetInt() == 5 )
+ {
+ Msg( "Actbusy collected %d safe zones\n", m_SafeZones.Count() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::IsInSafeZone( CBaseEntity *pEntity )
+{
+ Vector vecLocation = pEntity->WorldSpaceCenter();
+
+ for( int i = 0 ; i < m_SafeZones.Count() ; i++ )
+ {
+ busysafezone_t *pSafeZone = &m_SafeZones[i];
+
+ if( vecLocation.x > pSafeZone->vecMins.x &&
+ vecLocation.y > pSafeZone->vecMins.y &&
+ vecLocation.z > pSafeZone->vecMins.z &&
+
+ vecLocation.x < pSafeZone->vecMaxs.x &&
+ vecLocation.y < pSafeZone->vecMaxs.y &&
+ vecLocation.z < pSafeZone->vecMaxs.z )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if this NPC has the anims required to use the specified actbusy hint
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::HasAnimForActBusy( int iActBusy, busyanimparts_t AnimPart )
+{
+ if ( iActBusy == -1 )
+ return false;
+
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( iActBusy );
+ if ( !pBusyAnim )
+ return false;
+
+ // Try and play the sequence first
+ if ( pBusyAnim->iszSequences[AnimPart] != NULL_STRING )
+ return (GetOuter()->LookupSequence( (char*)STRING(pBusyAnim->iszSequences[AnimPart]) ) != ACTIVITY_NOT_AVAILABLE);
+
+ // Try and play the activity second
+ if ( pBusyAnim->iActivities[AnimPart] != ACT_INVALID )
+ return GetOuter()->HaveSequenceForActivity( pBusyAnim->iActivities[AnimPart] );
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Play the sound associated with the specified part of the current actbusy, if any
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::PlaySoundForActBusy( busyanimparts_t AnimPart )
+{
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+ if ( !pBusyAnim )
+ return;
+
+ // Play the sound
+ if ( pBusyAnim->iszSounds[AnimPart] != NULL_STRING )
+ {
+ // See if we can treat it as a game sound name
+ CSoundParameters params;
+ if ( GetOuter()->GetParametersForSound( STRING(pBusyAnim->iszSounds[AnimPart]), params, STRING(GetOuter()->GetModelName()) ) )
+ {
+ CPASAttenuationFilter filter( GetOuter() );
+ GetOuter()->EmitSound( filter, GetOuter()->entindex(), params );
+ }
+ else
+ {
+ // Assume it's a response concept, and try to speak it
+ CAI_Expresser *pExpresser = GetOuter()->GetExpresser();
+ if ( pExpresser )
+ {
+ const char *concept = STRING(pBusyAnim->iszSounds[AnimPart]);
+
+ // Must be able to speak the concept
+ if ( !pExpresser->IsSpeaking() && pExpresser->CanSpeakConcept( concept ) )
+ {
+ pExpresser->Speak( concept );
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Play a sequence or activity for the current actbusy
+//-----------------------------------------------------------------------------
+bool CAI_ActBusyBehavior::PlayAnimForActBusy( busyanimparts_t AnimPart )
+{
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+ if ( !pBusyAnim )
+ return false;
+
+ // Try and play the sequence first
+ if ( pBusyAnim->iszSequences[AnimPart] != NULL_STRING )
+ {
+ GetOuter()->SetSequenceByName( (char*)STRING(pBusyAnim->iszSequences[AnimPart]) );
+ GetOuter()->SetIdealActivity( ACT_DO_NOT_DISTURB );
+ return true;
+ }
+
+ // Try and play the activity second
+ if ( pBusyAnim->iActivities[AnimPart] != ACT_INVALID )
+ {
+ GetOuter()->SetIdealActivity( pBusyAnim->iActivities[AnimPart] );
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::StartTask( const Task_t *pTask )
+{
+ switch ( pTask->iTask )
+ {
+ case TASK_ACTBUSY_PLAY_BUSY_ANIM:
+ {
+ // If we're not enabled here, it's due to the actbusy being deactivated during
+ // the NPC's entry animation. We can't abort in the middle of the entry, so we
+ // arrive here with a disabled actbusy behaviour. Exit gracefully.
+ if ( !m_bEnabled )
+ {
+ TaskComplete();
+ return;
+ }
+
+ // Set the flag to remind the code to recompute the NPC's box from render bounds.
+ // This is used to delay the process so that we don't get a box built from render bounds
+ // when a character is still interpolating to their busy pose.
+ m_bNeedToSetBounds = true;
+
+ // Get the busyanim for the specified activity
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+
+ // We start "flying" so we don't collide with the world, in case the level
+ // designer has us sitting on a chair, etc.
+ if( !pBusyAnim || !pBusyAnim->bUseAutomovement )
+ {
+ GetOuter()->AddFlag( FL_FLY );
+ }
+
+ GetOuter()->SetGroundEntity( NULL );
+
+ // Fail if we're not on the node & facing the correct way
+ // We only do this check if we're still moving to the busy. This will only
+ // be true if there was no entry animation for this busy. We do it this way
+ // because the entry code contains this same check, and so we assume we're
+ // valid even if we're off now, because some entry animations move the
+ // character off the node.
+ if ( m_bMovingToBusy )
+ {
+ if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) > 16 || !GetOuter()->FacingIdeal() )
+ {
+ TaskFail( "Not correctly on hintnode" );
+ m_flEndBusyAt = gpGlobals->curtime;
+ return;
+ }
+ }
+
+ m_bMovingToBusy = false;
+
+ if ( !ActBusyNodeStillActive() )
+ {
+ TaskFail( FAIL_NO_HINT_NODE );
+ return;
+ }
+
+ // Have we just started using this node?
+ if ( !m_bBusy )
+ {
+ m_bBusy = true;
+
+ GetHintNode()->NPCStartedUsing( GetOuter() );
+ if ( m_hActBusyGoal )
+ {
+ m_hActBusyGoal->NPCStartedBusy( GetOuter() );
+ }
+
+ if ( pBusyAnim )
+ {
+ float flMaxTime = pBusyAnim->flMaxTime;
+ float flMinTime = pBusyAnim->flMinTime;
+
+ // Mapmaker input may have specified it's own max time
+ if ( m_bForceActBusy && m_flForcedMaxTime != NO_MAX_TIME )
+ {
+ flMaxTime = m_flForcedMaxTime;
+
+ // Don't let non-unlimited time amounts be less than the mintime
+ if ( flMaxTime && flMaxTime < flMinTime )
+ {
+ flMinTime = flMaxTime;
+ }
+ }
+
+ // If we have no max time, or we're in a queue, we loop forever.
+ if ( !flMaxTime || m_bInQueue )
+ {
+ m_flEndBusyAt = 0;
+ GetOuter()->SetWait( 99999 );
+ }
+ else
+ {
+ float flTime = RandomFloat(flMinTime, flMaxTime);
+ m_flEndBusyAt = gpGlobals->curtime + flTime;
+ GetOuter()->SetWait( flTime );
+ }
+ }
+ }
+
+ // Start playing the act busy
+ PlayAnimForActBusy( BA_BUSY );
+ PlaySoundForActBusy( BA_BUSY );
+
+ // Now that we're busy, we don't need to be forced anymore
+ m_bForceActBusy = false;
+ m_bTeleportToBusy = false;
+ m_bUseNearestBusy = false;
+ m_ForcedActivity = ACT_INVALID;
+
+ // If we're supposed to use render bounds while inside the busy anim, do so
+ if ( m_bUseRenderBoundsForCollision )
+ {
+ ComputeAndSetRenderBounds();
+ }
+ }
+ break;
+
+ case TASK_ACTBUSY_PLAY_ENTRY:
+ {
+ // We start "flying" so we don't collide with the world, in case the level
+ // designer has us sitting on a chair, etc.
+
+ // Get the busyanim for the specified activity
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+
+ // We start "flying" so we don't collide with the world, in case the level
+ // designer has us sitting on a chair, etc.
+ if( !pBusyAnim || !pBusyAnim->bUseAutomovement )
+ {
+ GetOuter()->AddFlag( FL_FLY );
+ }
+
+ GetOuter()->SetGroundEntity( NULL );
+
+ m_bMovingToBusy = false;
+ m_bNeedsToPlayExitAnim = HasAnimForActBusy( m_iCurrentBusyAnim, BA_EXIT );
+
+ if ( !ActBusyNodeStillActive() )
+ {
+ TaskFail( FAIL_NO_HINT_NODE );
+ return;
+ }
+
+ // Fail if we're not on the node & facing the correct way
+ if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) > 16 || !GetOuter()->FacingIdeal() )
+ {
+ m_bBusy = false;
+ TaskFail( "Not correctly on hintnode" );
+ return;
+ }
+
+ PlaySoundForActBusy( BA_ENTRY );
+
+ // Play the entry animation. If it fails, we don't have an entry anim, so complete immediately.
+ if ( !PlayAnimForActBusy( BA_ENTRY ) )
+ {
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_ACTBUSY_VERIFY_EXIT:
+ {
+ // NPC's that changed their bounding box must ensure that they can restore their regular box
+ // before they exit their actbusy. This task is designed to delay until that time if necessary.
+ if( !m_bUseRenderBoundsForCollision )
+ {
+ // Don't bother if we didn't alter our BBox.
+ TaskComplete();
+ break;
+ }
+
+ // Set up a timer to check immediately.
+ GetOuter()->SetWait( 0 );
+ }
+ break;
+
+ case TASK_ACTBUSY_PLAY_EXIT:
+ {
+ // If we're supposed to use render bounds while inside the busy anim, restore normal now
+ if ( m_bUseRenderBoundsForCollision )
+ {
+ GetOuter()->SetHullSizeNormal( true );
+ }
+
+ if ( m_hActBusyGoal )
+ {
+ m_hActBusyGoal->NPCStartedLeavingBusy( GetOuter() );
+ }
+
+ PlaySoundForActBusy( BA_EXIT );
+
+ // Play the exit animation. If it fails, we don't have an entry anim, so complete immediately.
+ if ( !PlayAnimForActBusy( BA_EXIT ) )
+ {
+ m_bNeedsToPlayExitAnim = false;
+ GetOuter()->RemoveFlag( FL_FLY );
+ NotifyBusyEnding();
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_ACTBUSY_TELEPORT_TO_BUSY:
+ {
+ if ( !ActBusyNodeStillActive() )
+ {
+ TaskFail( FAIL_NO_HINT_NODE );
+ return;
+ }
+
+ Vector vecAbsOrigin = GetHintNode()->GetAbsOrigin();
+ QAngle vecAbsAngles = GetHintNode()->GetAbsAngles();
+ GetOuter()->Teleport( &vecAbsOrigin, &vecAbsAngles, NULL );
+ GetOuter()->GetMotor()->SetIdealYaw( vecAbsAngles.y );
+
+ TaskComplete();
+ }
+ break;
+
+ case TASK_ACTBUSY_WALK_PATH_TO_BUSY:
+ {
+ // If we have a forced activity, use that. Otherwise, walk.
+ if ( m_ForcedActivity != ACT_INVALID && m_ForcedActivity != ACT_RESET )
+ {
+ GetNavigator()->SetMovementActivity( m_ForcedActivity );
+
+ // Cover is void once I move
+ Forget( bits_MEMORY_INCOVER );
+
+ TaskComplete();
+ }
+ else
+ {
+ if( IsCombatActBusy() )
+ {
+ ChainStartTask( TASK_RUN_PATH );
+ }
+ else
+ {
+ ChainStartTask( TASK_WALK_PATH );
+ }
+ }
+ break;
+ }
+
+ case TASK_ACTBUSY_GET_PATH_TO_ACTBUSY:
+ {
+ ChainStartTask( TASK_GET_PATH_TO_HINTNODE );
+
+ if ( !HasCondition(COND_TASK_FAILED) )
+ {
+ // We successfully built a path, so stop counting consecutive failures.
+ m_iNumConsecutivePathFailures = 0;
+
+ // Set the arrival sequence for the actbusy to be the busy sequence, if we don't have an entry animation
+ busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim );
+ if ( pBusyAnim && pBusyAnim->iszSequences[BA_ENTRY] == NULL_STRING && pBusyAnim->iActivities[BA_ENTRY] == ACT_INVALID )
+ {
+ // Try and play the sequence first
+ if ( pBusyAnim->iszSequences[BA_BUSY] != NULL_STRING )
+ {
+ GetNavigator()->SetArrivalSequence( GetOuter()->LookupSequence( STRING(pBusyAnim->iszSequences[BA_BUSY]) ) );
+ }
+ else if ( pBusyAnim->iActivities[BA_BUSY] != ACT_INVALID )
+ {
+ // Try and play the activity second
+ GetNavigator()->SetArrivalActivity( pBusyAnim->iActivities[BA_BUSY] );
+ }
+ }
+ else
+ {
+ // Robin: Set the arrival sequence / activity to be the entry animation.
+ if ( pBusyAnim->iszSequences[BA_ENTRY] != NULL_STRING )
+ {
+ GetNavigator()->SetArrivalSequence( GetOuter()->LookupSequence( STRING(pBusyAnim->iszSequences[BA_ENTRY]) ) );
+ }
+ else if ( pBusyAnim->iActivities[BA_ENTRY] != ACT_INVALID )
+ {
+ // Try and play the activity second
+ GetNavigator()->SetArrivalActivity( pBusyAnim->iActivities[BA_ENTRY] );
+ }
+ }
+ }
+ else
+ {
+ m_iNumConsecutivePathFailures++;
+
+ if ( ai_debug_actbusy.GetInt() == 1 )
+ {
+ if ( GetHintNode() )
+ {
+ // Show which actbusy we're moving towards
+ NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), GetHintNode()->GetAbsOrigin(), 255, 0, 0, true, 1.0 );
+ }
+ }
+ }
+
+ break;
+ }
+
+ default:
+ BaseClass::StartTask( pTask);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::RunTask( const Task_t *pTask )
+{
+ switch ( pTask->iTask )
+ {
+ case TASK_WAIT_FOR_MOVEMENT:
+ {
+ // Ensure the hint node hasn't been disabled
+ if ( IsCurSchedule( SCHED_ACTBUSY_START_BUSYING ) )
+ {
+ if ( !ActBusyNodeStillActive() )
+ {
+ TaskFail(FAIL_NO_HINT_NODE);
+ return;
+ }
+ }
+
+ if ( ai_debug_actbusy.GetInt() == 1 )
+ {
+ if ( GetHintNode() )
+ {
+ // Show which actbusy we're moving towards
+ NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), GetHintNode()->GetAbsOrigin(), 0, 255, 0, true, 0.2 );
+ }
+ }
+
+ BaseClass::RunTask( pTask );
+ break;
+ }
+
+ case TASK_ACTBUSY_PLAY_BUSY_ANIM:
+ {
+ if( m_bUseRenderBoundsForCollision )
+ {
+ if( GetOuter()->IsSequenceFinished() && m_bNeedToSetBounds )
+ {
+ ComputeAndSetRenderBounds();
+ m_bNeedToSetBounds = false;
+ }
+ }
+
+ if( IsCombatActBusy() )
+ {
+ if( GetEnemy() != NULL && !HasCondition(COND_ENEMY_OCCLUDED) )
+ {
+ // Break a combat actbusy if an enemy gets very close.
+ // I'll probably go to hell for not doing this with conditions like I should. (sjb)
+ float flDistSqr = GetAbsOrigin().DistToSqr( GetEnemy()->GetAbsOrigin() );
+
+ if( flDistSqr < Square(12.0f * 15.0f) )
+ {
+ // End now.
+ m_flEndBusyAt = gpGlobals->curtime;
+ TaskComplete();
+ return;
+ }
+ }
+ }
+
+ GetOuter()->AutoMovement();
+ // Stop if the node's been disabled
+ if ( !ActBusyNodeStillActive() || GetOuter()->IsWaitFinished() )
+ {
+ TaskComplete();
+ }
+ else
+ {
+ CAI_PlayerAlly *pAlly = dynamic_cast<CAI_PlayerAlly*>(GetOuter());
+ if ( pAlly )
+ {
+ pAlly->SelectInterjection();
+ }
+
+ if( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) )
+ {
+ StopBusying();
+ TaskComplete();
+ }
+ }
+ break;
+ }
+
+ case TASK_ACTBUSY_PLAY_ENTRY:
+ {
+ GetOuter()->AutoMovement();
+ if ( !ActBusyNodeStillActive() || GetOuter()->IsSequenceFinished() )
+ {
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_ACTBUSY_VERIFY_EXIT:
+ {
+ if( GetOuter()->IsWaitFinished() )
+ {
+ // Trace my normal hull over this spot to see if I'm able to stand up right now.
+ trace_t tr;
+ CTraceFilterOnlyNPCsAndPlayer filter( GetOuter(), COLLISION_GROUP_NONE );
+ UTIL_TraceHull( GetOuter()->GetAbsOrigin(), GetOuter()->GetAbsOrigin(), NAI_Hull::Mins( HULL_HUMAN ), NAI_Hull::Maxs( HULL_HUMAN ), MASK_NPCSOLID, &filter, &tr );
+
+ if( tr.startsolid )
+ {
+ // Blocked. Try again later.
+ GetOuter()->SetWait( 1.0f );
+ }
+ else
+ {
+ // Put an entity blocker here for a moment until I get into my bounding box.
+ CBaseEntity *pBlocker = CEntityBlocker::Create( GetOuter()->GetAbsOrigin(), NAI_Hull::Mins( HULL_HUMAN ), NAI_Hull::Maxs( HULL_HUMAN ), GetOuter(), true );
+ g_EventQueue.AddEvent( pBlocker, "Kill", 1.0, GetOuter(), GetOuter() );
+ TaskComplete();
+ }
+ }
+ }
+ break;
+
+ case TASK_ACTBUSY_PLAY_EXIT:
+ {
+ GetOuter()->AutoMovement();
+ if ( GetOuter()->IsSequenceFinished() )
+ {
+ m_bNeedsToPlayExitAnim = false;
+ GetOuter()->RemoveFlag( FL_FLY );
+ NotifyBusyEnding();
+ TaskComplete();
+ }
+ }
+ break;
+
+ default:
+ BaseClass::RunTask( pTask);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyBehavior::NotifyBusyEnding( void )
+{
+ // Be sure to disable autofire
+ m_bAutoFireWeapon = false;
+
+ // Clear the hintnode if we're done with it
+ if ( GetHintNode() )
+ {
+ if ( m_bBusy || m_bMovingToBusy )
+ {
+ GetHintNode()->NPCStoppedUsing( GetOuter() );
+ }
+
+ GetHintNode()->Unlock();
+
+ if( IsCombatActBusy() )
+ {
+ // Don't allow anyone to use this node for a bit. This is so the tactical position
+ // doesn't get re-occupied the moment I leave it.
+ GetHintNode()->DisableForSeconds( random->RandomFloat( 10, 15) );
+ }
+
+ SetHintNode( NULL );
+ }
+
+ // Then, if we were busy, stop being busy
+ if ( m_bBusy )
+ {
+ m_bBusy = false;
+
+ if ( m_hActBusyGoal )
+ {
+ m_hActBusyGoal->NPCFinishedBusy( GetOuter() );
+
+ if ( m_bExitedBusyToDueLostSeeEntity )
+ {
+ m_hActBusyGoal->NPCLostSeeEntity( GetOuter() );
+ m_bExitedBusyToDueLostSeeEntity = false;
+ }
+
+ if ( m_bExitedBusyToDueSeeEnemy )
+ {
+ m_hActBusyGoal->NPCSeeEnemy( GetOuter() );
+ m_bExitedBusyToDueSeeEnemy = false;
+ }
+ }
+ }
+ else if ( m_bMovingToBusy && m_hActBusyGoal )
+ {
+ // Or if we were just on our way to be busy, let the goal know
+ m_hActBusyGoal->NPCAbortedMoveTo( GetOuter() );
+ }
+
+ // Don't busy again for a while
+ m_flEndBusyAt = 0;
+
+ if( IsCombatActBusy() )
+ {
+ // Actbusy again soon. Real soon.
+ m_flNextBusySearchTime = gpGlobals->curtime;
+ }
+ else
+ {
+ m_flNextBusySearchTime = gpGlobals->curtime + (RandomFloat(ai_actbusy_search_time.GetFloat(), ai_actbusy_search_time.GetFloat()*2));
+ }
+}
+
+//-------------------------------------
+
+AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_ActBusyBehavior )
+
+ DECLARE_CONDITION( COND_ACTBUSY_LOST_SEE_ENTITY )
+ DECLARE_CONDITION( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE )
+ DECLARE_CONDITION( COND_ACTBUSY_ENEMY_TOO_CLOSE )
+
+ DECLARE_TASK( TASK_ACTBUSY_PLAY_BUSY_ANIM )
+ DECLARE_TASK( TASK_ACTBUSY_PLAY_ENTRY )
+ DECLARE_TASK( TASK_ACTBUSY_PLAY_EXIT )
+ DECLARE_TASK( TASK_ACTBUSY_TELEPORT_TO_BUSY )
+ DECLARE_TASK( TASK_ACTBUSY_WALK_PATH_TO_BUSY )
+ DECLARE_TASK( TASK_ACTBUSY_GET_PATH_TO_ACTBUSY )
+ DECLARE_TASK( TASK_ACTBUSY_VERIFY_EXIT )
+
+ DECLARE_ANIMEVENT( AE_ACTBUSY_WEAPON_FIRE_ON )
+ DECLARE_ANIMEVENT( AE_ACTBUSY_WEAPON_FIRE_OFF )
+
+ //---------------------------------
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_ACTBUSY_START_BUSYING,
+
+ " Tasks"
+ " TASK_SET_TOLERANCE_DISTANCE 4"
+ " TASK_ACTBUSY_GET_PATH_TO_ACTBUSY 0"
+ " TASK_ACTBUSY_WALK_PATH_TO_BUSY 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_HINTNODE 0"
+ " TASK_ACTBUSY_PLAY_ENTRY 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_ACTBUSY_BUSY"
+ ""
+ " Interrupts"
+ " COND_ACTBUSY_LOST_SEE_ENTITY"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_ACTBUSY_BUSY,
+
+ " Tasks"
+ " TASK_ACTBUSY_PLAY_BUSY_ANIM 0"
+ ""
+ " Interrupts"
+ " COND_PROVOKED"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_ACTBUSY_STOP_BUSYING,
+
+ " Tasks"
+ " TASK_ACTBUSY_VERIFY_EXIT 0"
+ " TASK_ACTBUSY_PLAY_EXIT 0"
+ " TASK_WAIT 0.1"
+ ""
+ " Interrupts"
+ " COND_NO_CUSTOM_INTERRUPTS"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_ACTBUSY_LEAVE,
+
+ " Tasks"
+ " TASK_SET_TOLERANCE_DISTANCE 4"
+ " TASK_ACTBUSY_GET_PATH_TO_ACTBUSY 0"
+ " TASK_ACTBUSY_WALK_PATH_TO_BUSY 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ ""
+ " Interrupts"
+ " COND_PROVOKED"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_ACTBUSY_TELEPORT_TO_BUSY,
+
+ " Tasks"
+ " TASK_ACTBUSY_TELEPORT_TO_BUSY 0"
+ " TASK_ACTBUSY_PLAY_ENTRY 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_ACTBUSY_BUSY"
+ ""
+ " Interrupts"
+ " COND_PROVOKED"
+ )
+
+AI_END_CUSTOM_SCHEDULE_PROVIDER()
+
+
+//==========================================================================================================
+// ACT BUSY GOALS
+//==========================================================================================================
+//-----------------------------------------------------------------------------
+// Purpose: A level tool to control the actbusy behavior.
+//-----------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( ai_goal_actbusy, CAI_ActBusyGoal );
+
+BEGIN_DATADESC( CAI_ActBusyGoal )
+ DEFINE_KEYFIELD( m_flBusySearchRange, FIELD_FLOAT, "busysearchrange" ),
+ DEFINE_KEYFIELD( m_bVisibleOnly, FIELD_BOOLEAN, "visibleonly" ),
+ DEFINE_KEYFIELD( m_iType, FIELD_INTEGER, "type" ),
+ DEFINE_KEYFIELD( m_bAllowCombatActBusyTeleport, FIELD_BOOLEAN, "allowteleport" ),
+ DEFINE_KEYFIELD( m_iszSeeEntityName, FIELD_STRING, "SeeEntity" ),
+ DEFINE_KEYFIELD( m_flSeeEntityTimeout, FIELD_FLOAT, "SeeEntityTimeout" ),
+ DEFINE_KEYFIELD( m_iszSafeZoneVolume, FIELD_STRING, "SafeZone" ),
+ DEFINE_KEYFIELD( m_iSightMethod, FIELD_INTEGER, "sightmethod" ),
+
+ // Inputs
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBusySearchRange", InputSetBusySearchRange ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "ForceNPCToActBusy", InputForceNPCToActBusy ),
+ DEFINE_INPUTFUNC( FIELD_EHANDLE, "ForceThisNPCToActBusy", InputForceThisNPCToActBusy ),
+ DEFINE_INPUTFUNC( FIELD_EHANDLE, "ForceThisNPCToLeave", InputForceThisNPCToLeave ),
+
+ // Outputs
+ DEFINE_OUTPUT( m_OnNPCStartedBusy, "OnNPCStartedBusy" ),
+ DEFINE_OUTPUT( m_OnNPCFinishedBusy, "OnNPCFinishedBusy" ),
+ DEFINE_OUTPUT( m_OnNPCLeft, "OnNPCLeft" ),
+ DEFINE_OUTPUT( m_OnNPCLostSeeEntity, "OnNPCLostSeeEntity" ),
+ DEFINE_OUTPUT( m_OnNPCSeeEnemy, "OnNPCSeeEnemy" ),
+END_DATADESC()
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CAI_ActBusyBehavior *CAI_ActBusyGoal::GetBusyBehaviorForNPC( CBaseEntity *pEntity, const char *sInputName )
+{
+ CAI_BaseNPC *pActor = dynamic_cast<CAI_BaseNPC*>(pEntity);
+ if ( !pActor )
+ {
+ Msg("ai_goal_actbusy input %s fired targeting an entity that isn't an NPC.\n", sInputName);
+ return NULL;
+ }
+
+ // Get the NPC's behavior
+ CAI_ActBusyBehavior *pBehavior;
+ if ( !pActor->GetBehavior( &pBehavior ) )
+ {
+ Msg("ai_goal_actbusy input %s fired on an NPC that doesn't support ActBusy behavior.\n", sInputName );
+ return NULL;
+ }
+
+ return pBehavior;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CAI_ActBusyBehavior *CAI_ActBusyGoal::GetBusyBehaviorForNPC( const char *pszActorName, CBaseEntity *pActivator, CBaseEntity *pCaller, const char *sInputName )
+{
+ CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, MAKE_STRING(pszActorName), NULL, pActivator, pCaller );
+ if ( !pEntity )
+ {
+ Msg("ai_goal_actbusy input %s fired targeting a non-existant entity (%s).\n", sInputName, pszActorName );
+ return NULL;
+ }
+
+ return GetBusyBehaviorForNPC( pEntity, sInputName );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::EnableGoal( CAI_BaseNPC *pAI )
+{
+ BaseClass::EnableGoal( pAI );
+
+ // Now use this actor to lookup the Behavior
+ CAI_ActBusyBehavior *pBehavior;
+ if ( pAI->GetBehavior( &pBehavior ) )
+ {
+ // Some NPCs may already be active due to a ForceActBusy input.
+ if ( !pBehavior->IsEnabled() )
+ {
+ pBehavior->Enable( this, m_flBusySearchRange, m_bVisibleOnly );
+ }
+ }
+ else
+ {
+ DevMsg( "ActBusy goal entity activated for an NPC (%s) that doesn't have the ActBusy behavior\n", pAI->GetDebugName() );
+ return;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputActivate( inputdata_t &inputdata )
+{
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: Actbusy goal %s (%s) activated.\n", GetClassname(), GetDebugName() );
+ }
+
+ BaseClass::InputActivate( inputdata );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputDeactivate( inputdata_t &inputdata )
+{
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: Actbusy goal %s (%s) disabled.\n", GetClassname(), GetDebugName() );
+ }
+
+ BaseClass::InputDeactivate( inputdata );
+
+ for( int i = 0 ; i < NumActors() ; i++ )
+ {
+ CAI_BaseNPC *pActor = GetActor( i );
+
+ if ( pActor )
+ {
+ // Now use this actor to lookup the Behavior
+ CAI_ActBusyBehavior *pBehavior;
+ if ( pActor->GetBehavior( &pBehavior ) )
+ {
+ pBehavior->Disable();
+ }
+ else
+ {
+ DevMsg( "ActBusy goal entity deactivated for an NPC that doesn't have the ActBusy behavior\n" );
+ return;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputSetBusySearchRange( inputdata_t &inputdata )
+{
+ m_flBusySearchRange = inputdata.value.Float();
+
+ for( int i = 0 ; i < NumActors() ; i++ )
+ {
+ CAI_BaseNPC *pActor = GetActor( i );
+
+ if ( pActor )
+ {
+ // Now use this actor to lookup the Behavior
+ CAI_ActBusyBehavior *pBehavior;
+ if ( pActor->GetBehavior( &pBehavior ) )
+ {
+ pBehavior->SetBusySearchRange( m_flBusySearchRange );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputForceNPCToActBusy( inputdata_t &inputdata )
+{
+ char parseString[255];
+ Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString));
+
+ CAI_Hint *pHintNode = NULL;
+ float flMaxTime = NO_MAX_TIME;
+ bool bTeleport = false;
+ bool bUseNearestBusy = false;
+ CBaseEntity *pSeeEntity = NULL;
+
+ // Get NPC name
+ char *pszParam = strtok(parseString," ");
+ CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( pszParam, inputdata.pActivator, inputdata.pCaller, "InputForceNPCToActBusy" );
+ if ( !pBehavior )
+ return;
+
+ // Wrapped this bugfix so that it doesn't break HL2.
+ bool bEpisodicBugFix = hl2_episodic.GetBool();
+
+ // Do we have a specified node too?
+ pszParam = strtok(NULL," ");
+ if ( pszParam )
+ {
+ // Find the specified hintnode
+ CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, pszParam, NULL, inputdata.pActivator, inputdata.pCaller );
+ if ( pEntity )
+ {
+ pHintNode = dynamic_cast<CAI_Hint*>(pEntity);
+ if ( !pHintNode )
+ {
+ Msg("ai_goal_actbusy input ForceNPCToActBusy fired targeting an entity that isn't a hintnode.\n");
+ return;
+ }
+
+ if ( bEpisodicBugFix )
+ {
+ pszParam = strtok(NULL," ");
+ }
+ }
+ }
+
+ Activity activity = ACT_INVALID;
+
+ if ( !bEpisodicBugFix )
+ {
+ pszParam = strtok(NULL," ");
+ }
+
+ while ( pszParam )
+ {
+ // Teleport?
+ if ( !Q_strncmp( pszParam, "teleport", 8 ) )
+ {
+ bTeleport = true;
+ }
+ else if ( !Q_strncmp( pszParam, "nearest", 8 ) )
+ {
+ bUseNearestBusy = true;
+ }
+ else if ( !Q_strncmp( pszParam, "see:", 4 ) )
+ {
+ pSeeEntity = gEntList.FindEntityByName( NULL, pszParam+4 );
+ }
+ else if ( pszParam[0] == '$' )
+ {
+ // $ signs prepend custom movement sequences / activities
+ const char *pAnimName = pszParam+1;
+ // Try and resolve it as an activity name
+ activity = (Activity)ActivityList_IndexForName( pAnimName );
+ if ( activity == ACT_INVALID )
+ {
+ // Try it as sequence name
+ pBehavior->GetOuter()->m_iszSceneCustomMoveSeq = AllocPooledString( pAnimName );
+ activity = ACT_SCRIPT_CUSTOM_MOVE;
+ }
+ }
+ else
+ {
+ // Do we have a specified time?
+ flMaxTime = atof( pszParam );
+ }
+
+ pszParam = strtok(NULL," ");
+ }
+
+ if ( ai_debug_actbusy.GetInt() == 4 )
+ {
+ Msg("ACTBUSY: Actbusy goal %s (%s) ForceNPCToActBusy input with data: %s.\n", GetClassname(), GetDebugName(), parseString );
+ }
+
+ // Tell the NPC to immediately act busy
+ pBehavior->SetBusySearchRange( m_flBusySearchRange );
+ pBehavior->ForceActBusy( this, pHintNode, flMaxTime, m_bVisibleOnly, bTeleport, bUseNearestBusy, pSeeEntity, activity );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Force the passed in NPC to actbusy
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputForceThisNPCToActBusy( inputdata_t &inputdata )
+{
+ CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( inputdata.value.Entity(), "InputForceThisNPCToActBusy" );
+ if ( !pBehavior )
+ return;
+
+ // Tell the NPC to immediately act busy
+ pBehavior->SetBusySearchRange( m_flBusySearchRange );
+ pBehavior->ForceActBusy( this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Force the passed in NPC to walk to a point and vanish
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::InputForceThisNPCToLeave( inputdata_t &inputdata )
+{
+ CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( inputdata.value.Entity(), "InputForceThisNPCToLeave" );
+ if ( !pBehavior )
+ return;
+
+ // Tell the NPC to find a leave point and move to it
+ pBehavior->SetBusySearchRange( m_flBusySearchRange );
+ pBehavior->ForceActBusyLeave();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCMovingToBusy( CAI_BaseNPC *pNPC )
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCStartedBusy( CAI_BaseNPC *pNPC )
+{
+ m_OnNPCStartedBusy.Set( pNPC, pNPC, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCStartedLeavingBusy( CAI_BaseNPC *pNPC )
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCAbortedMoveTo( CAI_BaseNPC *pNPC )
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCFinishedBusy( CAI_BaseNPC *pNPC )
+{
+ m_OnNPCFinishedBusy.Set( pNPC, pNPC, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCLeft( CAI_BaseNPC *pNPC )
+{
+ m_OnNPCLeft.Set( pNPC, pNPC, this );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCLostSeeEntity( CAI_BaseNPC *pNPC )
+{
+ m_OnNPCLostSeeEntity.Set( pNPC, pNPC, this );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CAI_ActBusyGoal::NPCSeeEnemy( CAI_BaseNPC *pNPC )
+{
+ m_OnNPCSeeEnemy.Set( pNPC, pNPC, this );
+}
+
+//==========================================================================================================
+// ACT BUSY QUEUE
+//==========================================================================================================
+//-----------------------------------------------------------------------------
+// Purpose: A level tool to control the actbusy behavior to create NPC queues
+//-----------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( ai_goal_actbusy_queue, CAI_ActBusyQueueGoal );
+
+BEGIN_DATADESC( CAI_ActBusyQueueGoal )
+ // Keys
+ DEFINE_FIELD( m_iCurrentQueueCount, FIELD_INTEGER ),
+ DEFINE_ARRAY( m_hNodes, FIELD_EHANDLE, MAX_QUEUE_NODES ),
+ DEFINE_ARRAY( m_bPlayerBlockedNodes, FIELD_BOOLEAN, MAX_QUEUE_NODES ),
+ DEFINE_FIELD( m_hExitNode, FIELD_EHANDLE ),
+ DEFINE_FIELD( m_hExitingNPC, FIELD_EHANDLE ),
+ DEFINE_KEYFIELD( m_bForceReachFront, FIELD_BOOLEAN, "mustreachfront" ),
+ // DEFINE_ARRAY( m_iszNodes, FIELD_STRING, MAX_QUEUE_NODES ), // Silence Classcheck!
+ DEFINE_KEYFIELD( m_iszNodes[0], FIELD_STRING, "node01"),
+ DEFINE_KEYFIELD( m_iszNodes[1], FIELD_STRING, "node02"),
+ DEFINE_KEYFIELD( m_iszNodes[2], FIELD_STRING, "node03"),
+ DEFINE_KEYFIELD( m_iszNodes[3], FIELD_STRING, "node04"),
+ DEFINE_KEYFIELD( m_iszNodes[4], FIELD_STRING, "node05"),
+ DEFINE_KEYFIELD( m_iszNodes[5], FIELD_STRING, "node06"),
+ DEFINE_KEYFIELD( m_iszNodes[6], FIELD_STRING, "node07"),
+ DEFINE_KEYFIELD( m_iszNodes[7], FIELD_STRING, "node08"),
+ DEFINE_KEYFIELD( m_iszNodes[8], FIELD_STRING, "node09"),
+ DEFINE_KEYFIELD( m_iszNodes[9], FIELD_STRING, "node10"),
+ DEFINE_KEYFIELD( m_iszNodes[10], FIELD_STRING, "node11"),
+ DEFINE_KEYFIELD( m_iszNodes[11], FIELD_STRING, "node12"),
+ DEFINE_KEYFIELD( m_iszNodes[12], FIELD_STRING, "node13"),
+ DEFINE_KEYFIELD( m_iszNodes[13], FIELD_STRING, "node14"),
+ DEFINE_KEYFIELD( m_iszNodes[14], FIELD_STRING, "node15"),
+ DEFINE_KEYFIELD( m_iszNodes[15], FIELD_STRING, "node16"),
+ DEFINE_KEYFIELD( m_iszNodes[16], FIELD_STRING, "node17"),
+ DEFINE_KEYFIELD( m_iszNodes[17], FIELD_STRING, "node18"),
+ DEFINE_KEYFIELD( m_iszNodes[18], FIELD_STRING, "node19"),
+ DEFINE_KEYFIELD( m_iszNodes[19], FIELD_STRING, "node20"),
+ DEFINE_KEYFIELD( m_iszExitNode, FIELD_STRING, "node_exit"),
+
+ // Inputs
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "PlayerStartedBlocking", InputPlayerStartedBlocking ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "PlayerStoppedBlocking", InputPlayerStoppedBlocking ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "MoveQueueUp", InputMoveQueueUp ),
+
+ // Outputs
+ DEFINE_OUTPUT( m_OnQueueMoved, "OnQueueMoved" ),
+ DEFINE_OUTPUT( m_OnNPCLeftQueue, "OnNPCLeftQueue" ),
+ DEFINE_OUTPUT( m_OnNPCStartedLeavingQueue, "OnNPCStartedLeavingQueue" ),
+
+ DEFINE_THINKFUNC( QueueThink ),
+ DEFINE_THINKFUNC( MoveQueueUpThink ),
+
+END_DATADESC()
+
+#define QUEUE_THINK_CONTEXT "ActBusyQueueThinkContext"
+#define QUEUE_MOVEUP_THINK_CONTEXT "ActBusyQueueMoveUpThinkContext"
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ RegisterThinkContext( QUEUE_MOVEUP_THINK_CONTEXT );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::DrawDebugGeometryOverlays( void )
+{
+ BaseClass::DrawDebugGeometryOverlays();
+
+ // Debug for reservers
+ for ( int i = 0; i < MAX_QUEUE_NODES; i++ )
+ {
+ if ( !m_hNodes[i] )
+ continue;
+ if ( m_bPlayerBlockedNodes[i] )
+ {
+ NDebugOverlay::Box( m_hNodes[i]->GetAbsOrigin(), -Vector(5,5,5), Vector(5,5,5), 255, 0, 0, 0, 0.1 );
+ }
+ else
+ {
+ NDebugOverlay::Box( m_hNodes[i]->GetAbsOrigin(), -Vector(5,5,5), Vector(5,5,5), 255, 255, 255, 0, 0.1 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::InputActivate( inputdata_t &inputdata )
+{
+ if ( !IsActive() )
+ {
+ // Find all our nodes
+ for ( int i = 0; i < MAX_QUEUE_NODES; i++ )
+ {
+ if ( m_iszNodes[i] == NULL_STRING )
+ {
+ m_hNodes[i] = NULL;
+ continue;
+ }
+
+ CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_iszNodes[i] );
+ if ( !pEntity )
+ {
+ Warning( "Unable to find ai_goal_actbusy_queue %s's node %d: %s\n", STRING(GetEntityName()), i, STRING(m_iszNodes[i]) );
+ UTIL_Remove( this );
+ return;
+ }
+ m_hNodes[i] = dynamic_cast<CAI_Hint*>(pEntity);
+ if ( !m_hNodes[i] )
+ {
+ Warning( "ai_goal_actbusy_queue %s's node %d: '%s' is not an ai_hint.\n", STRING(GetEntityName()), i, STRING(m_iszNodes[i]) );
+ UTIL_Remove( this );
+ return;
+ }
+
+ // Disable all but the first node
+ if ( i == 0 )
+ {
+ m_hNodes[i]->SetDisabled( false );
+ }
+ else
+ {
+ m_hNodes[i]->SetDisabled( true );
+ }
+ }
+
+ // Find the exit node
+ m_hExitNode = gEntList.FindEntityByName( NULL, m_iszExitNode );
+ if ( !m_hExitNode )
+ {
+ Warning( "Unable to find ai_goal_actbusy_queue %s's exit node: %s\n", STRING(GetEntityName()), STRING(m_iszExitNode) );
+ UTIL_Remove( this );
+ return;
+ }
+
+ RecalculateQueueCount();
+
+ SetContextThink( &CAI_ActBusyQueueGoal::QueueThink, gpGlobals->curtime + 5, QUEUE_THINK_CONTEXT );
+ }
+
+ BaseClass::InputActivate( inputdata );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iCount -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::RecalculateQueueCount( void )
+{
+ // First, find the highest unused node in the queue
+ int iCount = 0;
+ for ( int i = 0; i < MAX_QUEUE_NODES; i++ )
+ {
+ if ( NodeIsOccupied(i) || m_bPlayerBlockedNodes[i] )
+ {
+ iCount = i+1;
+ }
+ }
+
+ //Msg("Count: %d (OLD %d)\n", iCount, m_iCurrentQueueCount );
+
+ // Queue hasn't changed?
+ if ( iCount == m_iCurrentQueueCount )
+ return;
+
+ for ( int i = 0; i < MAX_QUEUE_NODES; i++ )
+ {
+ if ( m_hNodes[i] )
+ {
+ // Disable nodes beyond 1 past the end of the queue
+ if ( i > iCount )
+ {
+ m_hNodes[i]->SetDisabled( true );
+ }
+ else
+ {
+ m_hNodes[i]->SetDisabled( false );
+
+ // To prevent NPCs outside the queue moving directly to nodes within the queue, only
+ // have the entry node be a valid actbusy node.
+ if ( i == iCount )
+ {
+ m_hNodes[i]->SetHintType( HINT_WORLD_WORK_POSITION );
+ }
+ else
+ {
+ m_hNodes[i]->SetHintType( HINT_NONE );
+ }
+ }
+ }
+ }
+
+ m_iCurrentQueueCount = iCount;
+ m_OnQueueMoved.Set( m_iCurrentQueueCount, this, this);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::InputPlayerStartedBlocking( inputdata_t &inputdata )
+{
+ int iNode = inputdata.value.Int() - 1;
+ Assert( iNode >= 0 && iNode < MAX_QUEUE_NODES );
+
+ m_bPlayerBlockedNodes[iNode] = true;
+
+ /*
+ // First, find all NPCs heading to points in front of the player's blocked spot
+ for ( int i = 0; i < iNode; i++ )
+ {
+ CAI_BaseNPC *pNPC = GetNPCOnNode(i);
+ if ( !pNPC )
+ continue;
+
+ CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC );
+ if ( pBehavior->IsMovingToBusy() )
+ {
+ // We may be ahead of the player in the queue, which means we can safely
+ // be left alone to reach the node. Make sure we're not closer to it than the player is
+ float flPlayerDistToNode = (inputdata.pActivator->GetAbsOrigin() - m_hNodes[i]->GetAbsOrigin()).LengthSqr();
+ if ( (pNPC->GetAbsOrigin() - m_hNodes[i]->GetAbsOrigin()).LengthSqr() < flPlayerDistToNode )
+ continue;
+
+ // We're an NPC heading to a node past the player, and yet the player's in our way.
+ pBehavior->StopBusying();
+ }
+ }
+ */
+
+ // If an NPC was heading towards this node, tell him to go elsewhere
+ CAI_BaseNPC *pNPC = GetNPCOnNode(iNode);
+ PushNPCBackInQueue( pNPC, iNode );
+
+ RecalculateQueueCount();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Find a node back in the queue to move to, and push all NPCs beyond that backwards
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::PushNPCBackInQueue( CAI_BaseNPC *pNPC, int iStartingNode )
+{
+ // Push this guy back, and tell everyone behind him to move back too, until we find a gap
+ while ( pNPC )
+ {
+ CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC );
+ pBehavior->StopBusying();
+
+ // Find any node farther back in the queue that isn't player blocked
+ for ( int iNext = iStartingNode+1; iNext < MAX_QUEUE_NODES; iNext++ )
+ {
+ if ( !m_bPlayerBlockedNodes[iNext] )
+ {
+ // Kick off any NPCs on the node we're about to steal
+ CAI_BaseNPC *pTargetNPC = GetNPCOnNode(iNext);
+ if ( pTargetNPC )
+ {
+ CAI_ActBusyBehavior *pTargetBehavior = GetQueueBehaviorForNPC( pTargetNPC );
+ pTargetBehavior->StopBusying();
+ }
+
+ // Force the NPC to move up to the empty slot
+ pBehavior->ForceActBusy( this, m_hNodes[iNext] );
+
+ // Now look for a spot for the npc who's spot we've just stolen
+ pNPC = pTargetNPC;
+ iStartingNode = iNext;
+ break;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::InputPlayerStoppedBlocking( inputdata_t &inputdata )
+{
+ int iNode = inputdata.value.Int() - 1;
+ Assert( iNode >= 0 && iNode < MAX_QUEUE_NODES );
+
+ m_bPlayerBlockedNodes[iNode] = false;
+
+ RecalculateQueueCount();
+ MoveQueueUp();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::InputMoveQueueUp( inputdata_t &inputdata )
+{
+ // Find the first NPC in the queue
+ CAI_BaseNPC *pNPC = NULL;
+ for ( int i = 0; i < MAX_QUEUE_NODES; i++ )
+ {
+ pNPC = GetNPCOnNode(i);
+ if ( pNPC )
+ {
+ CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC );
+ // If we're still en-route, we're only allowed to leave if the queue
+ // is allowed to send NPCs away that haven't reached the front.
+ if ( !pBehavior->IsMovingToBusy() || !m_bForceReachFront )
+ break;
+
+ pNPC = NULL;
+ }
+
+ // If queue members have to reach the front of the queue,
+ // break after trying the first node.
+ if ( m_bForceReachFront )
+ break;
+ }
+
+ // Did we find an NPC?
+ if ( pNPC )
+ {
+ // Make them leave the actbusy
+ CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC );
+ pBehavior->Disable();
+ m_hExitingNPC = pNPC;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::NPCMovingToBusy( CAI_BaseNPC *pNPC )
+{
+ BaseClass::NPCMovingToBusy( pNPC );
+ RecalculateQueueCount();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::NPCStartedBusy( CAI_BaseNPC *pNPC )
+{
+ BaseClass::NPCStartedBusy( pNPC );
+ MoveQueueUp();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Start a short timer that'll clean up holes in the queue
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::MoveQueueUp( void )
+{
+ // Find the node the NPC has arrived at, and tell the guy behind him to move forward
+ if ( GetNextThink( QUEUE_MOVEUP_THINK_CONTEXT ) < gpGlobals->curtime )
+ {
+ float flTime = gpGlobals->curtime + RandomFloat( 0.3, 0.5 );
+ SetContextThink( &CAI_ActBusyQueueGoal::MoveQueueUpThink, flTime, QUEUE_MOVEUP_THINK_CONTEXT );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::MoveQueueUpThink( void )
+{
+ // Find empty holes in the queue, and move NPCs past them forward
+ for ( int iEmptyNode = 0; iEmptyNode < (MAX_QUEUE_NODES-1); iEmptyNode++ )
+ {
+ if ( !NodeIsOccupied(iEmptyNode) && !m_bPlayerBlockedNodes[iEmptyNode] )
+ {
+ // Look for NPCs farther down the queue, but not on the other side of a player
+ for ( int iNext = iEmptyNode+1; iNext < MAX_QUEUE_NODES; iNext++ )
+ {
+ // Is the player blocking this node? If so, we're done
+ if ( m_bPlayerBlockedNodes[iNext] )
+ break;
+
+ CAI_BaseNPC *pNPC = GetNPCOnNode(iNext);
+ if ( pNPC )
+ {
+ CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC );
+
+ // Force the NPC to move up to the empty slot
+ pBehavior->ForceActBusy( this, m_hNodes[iEmptyNode] );
+ break;
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::NPCAbortedMoveTo( CAI_BaseNPC *pNPC )
+{
+ BaseClass::NPCAbortedMoveTo( pNPC );
+
+ RemoveNPCFromQueue( pNPC );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::NPCFinishedBusy( CAI_BaseNPC *pNPC )
+{
+ BaseClass::NPCFinishedBusy( pNPC );
+
+ // If this NPC was at the head of the line, move him to the exit node
+ if ( m_hExitingNPC == pNPC )
+ {
+ pNPC->ScheduledMoveToGoalEntity( SCHED_IDLE_WALK, m_hExitNode, ACT_WALK );
+ m_OnNPCLeftQueue.Set( pNPC, pNPC, this );
+ m_hExitingNPC = NULL;
+ }
+
+ RemoveNPCFromQueue( pNPC );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pNPC -
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::NPCStartedLeavingBusy( CAI_BaseNPC *pNPC )
+{
+ BaseClass::NPCStartedLeavingBusy( pNPC );
+
+ // If this NPC it at the head of the line, fire the output
+ if ( m_hExitingNPC == pNPC )
+ {
+ m_OnNPCStartedLeavingQueue.Set( pNPC, pNPC, this );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::RemoveNPCFromQueue( CAI_BaseNPC *pNPC )
+{
+ RecalculateQueueCount();
+
+ // Find the node the NPC was heading to, and tell the guy behind him to move forward
+ MoveQueueUp();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Move the first NPC out of the queue
+//-----------------------------------------------------------------------------
+void CAI_ActBusyQueueGoal::QueueThink( void )
+{
+ if ( !GetNPCOnNode(0) )
+ {
+ MoveQueueUp();
+ }
+
+ SetContextThink( &CAI_ActBusyQueueGoal::QueueThink, gpGlobals->curtime + 5, QUEUE_THINK_CONTEXT );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+inline bool CAI_ActBusyQueueGoal::NodeIsOccupied( int i )
+{
+ return ( m_hNodes[i] && !m_hNodes[i]->IsDisabled() && m_hNodes[i]->IsLocked() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iNode -
+// Output : CAI_BaseNPC
+//-----------------------------------------------------------------------------
+CAI_BaseNPC *CAI_ActBusyQueueGoal::GetNPCOnNode( int iNode )
+{
+ if ( !m_hNodes[iNode] )
+ return NULL;
+
+ return dynamic_cast<CAI_BaseNPC *>(m_hNodes[iNode]->User());
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iNode -
+// Output : CAI_ActBusyBehavior
+//-----------------------------------------------------------------------------
+CAI_ActBusyBehavior *CAI_ActBusyQueueGoal::GetQueueBehaviorForNPC( CAI_BaseNPC *pNPC )
+{
+ CAI_ActBusyBehavior *pBehavior;
+ pNPC->GetBehavior( &pBehavior );
+ Assert( pBehavior );
+ return pBehavior;
+}
+