mirror of
https://github.com/alliedmodders/hl2sdk.git
synced 2024-12-23 01:59:43 +08:00
2839 lines
77 KiB
C++
2839 lines
77 KiB
C++
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "cbase.h"
|
|
#include "tier1/utlfixedlinkedlist.h"
|
|
#include "bitstring.h"
|
|
#include "utlvector.h"
|
|
#include "ai_navigator.h"
|
|
#include "scripted.h"
|
|
#include "ai_hint.h"
|
|
#include "ai_behavior_follow.h"
|
|
#include "ai_memory.h"
|
|
#include "ai_squad.h"
|
|
#include "ai_tacticalservices.h"
|
|
#include "ndebugoverlay.h"
|
|
#include "ai_senses.h"
|
|
|
|
#ifdef HL2_EPISODIC
|
|
#include "info_darknessmode_lightsource.h"
|
|
#endif
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
ConVar ai_debug_follow( "ai_debug_follow", "0" );
|
|
ConVar ai_follow_use_points( "ai_follow_use_points", "1" );
|
|
ConVar ai_follow_use_points_when_moving( "ai_follow_use_points_when_moving", "1" );
|
|
#define FollowMsg(s) if ( !GetOuter() || !ai_debug_follow.GetBool() ) ; else DevMsg( GetOuter(), "Follow: " s )
|
|
|
|
#define WAIT_HINT_MIN_DIST (16*16) // Was: Square(GetHullWidth())
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// Purpose: Formation management
|
|
//
|
|
// Right now, this is in a very preliminary sketch state. (toml 03-03-03)
|
|
//-----------------------------------------------------------------------------
|
|
|
|
struct AI_FollowSlot_t;
|
|
struct AI_FollowFormation_t;
|
|
struct AI_FollowGroup_t;
|
|
|
|
struct AI_Follower_t
|
|
{
|
|
AI_Follower_t()
|
|
{
|
|
slot = -1;
|
|
memset( &navInfo, 0, sizeof(navInfo) );
|
|
pGroup = NULL;
|
|
}
|
|
|
|
AIHANDLE hFollower;
|
|
int slot;
|
|
AI_FollowNavInfo_t navInfo;
|
|
AI_FollowGroup_t * pGroup; // backpointer for efficiency
|
|
};
|
|
|
|
struct AI_FollowGroup_t
|
|
{
|
|
AI_FollowFormation_t * pFormation;
|
|
EHANDLE hFollowTarget;
|
|
CUtlFixedLinkedList<AI_Follower_t> followers;
|
|
CBitString slotUsage;
|
|
};
|
|
|
|
|
|
//-------------------------------------
|
|
|
|
class CAI_FollowManager
|
|
{
|
|
public:
|
|
~CAI_FollowManager()
|
|
{
|
|
for ( int i = 0; i < m_groups.Count(); i++ )
|
|
delete m_groups[i];
|
|
}
|
|
|
|
bool AddFollower( CBaseEntity *pTarget, CAI_BaseNPC *pFollower, AI_Formations_t formation, AI_FollowManagerInfoHandle_t *pHandle );
|
|
void ChangeFormation( AI_FollowManagerInfoHandle_t &handle, AI_Formations_t formation );
|
|
void RemoveFollower( AI_FollowManagerInfoHandle_t &handle );
|
|
bool CalcFollowPosition( AI_FollowManagerInfoHandle_t &handle, AI_FollowNavInfo_t *pNavInfo );
|
|
|
|
int CountFollowersInGroup( CAI_BaseNPC *pMember )
|
|
{
|
|
AI_FollowGroup_t *pGroup = FindFollowerGroup( pMember );
|
|
|
|
if( !pGroup )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return pGroup->followers.Count();
|
|
}
|
|
|
|
int GetFollowerSlot( CAI_BaseNPC *pFollower )
|
|
{
|
|
AI_FollowGroup_t *pGroup = FindFollowerGroup( pFollower );
|
|
|
|
if( !pGroup )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int h = pGroup->followers.Head();
|
|
|
|
while( h != pGroup->followers.InvalidIndex() )
|
|
{
|
|
AI_Follower_t *it = &pGroup->followers[h];
|
|
if ( it->hFollower.Get() == pFollower )
|
|
{
|
|
return it->slot;
|
|
}
|
|
|
|
h = pGroup->followers.Next( h );
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private:
|
|
bool RedistributeSlots( AI_FollowGroup_t *pGroup );
|
|
int FindBestSlot( AI_FollowGroup_t *pGroup );
|
|
void CalculateFieldsFromSlot( AI_FollowSlot_t *pSlot, AI_FollowNavInfo_t *pFollowerInfo );
|
|
|
|
AI_FollowGroup_t *FindCreateGroup( CBaseEntity *pTarget, AI_Formations_t formation );
|
|
AI_FollowGroup_t *FindGroup( CBaseEntity *pTarget );
|
|
AI_FollowGroup_t *FindFollowerGroup( CBaseEntity *pFollower );
|
|
void RemoveGroup( AI_FollowGroup_t * );
|
|
|
|
//---------------------------------
|
|
|
|
CUtlVector<AI_FollowGroup_t *> m_groups;
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
CAI_FollowManager g_AIFollowManager;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// CAI_FollowBehavior
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
BEGIN_SIMPLE_DATADESC( AI_FollowNavInfo_t )
|
|
DEFINE_FIELD( flags, FIELD_INTEGER ),
|
|
DEFINE_FIELD( position, FIELD_VECTOR ),
|
|
DEFINE_FIELD( range, FIELD_FLOAT ),
|
|
DEFINE_FIELD( Zrange, FIELD_FLOAT ),
|
|
DEFINE_FIELD( tolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( followPointTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( targetMoveTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( repathOnRouteTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( walkTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( coverTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( enemyLOSTolerance, FIELD_FLOAT ),
|
|
DEFINE_FIELD( chaseEnemyTolerance, FIELD_FLOAT ),
|
|
END_DATADESC();
|
|
|
|
BEGIN_SIMPLE_DATADESC( AI_FollowParams_t )
|
|
DEFINE_FIELD( formation, FIELD_INTEGER ),
|
|
|
|
END_DATADESC();
|
|
|
|
BEGIN_DATADESC( CAI_FollowBehavior )
|
|
DEFINE_FIELD( m_hFollowTarget, FIELD_EHANDLE ),
|
|
DEFINE_EMBEDDED( m_FollowNavGoal ),
|
|
DEFINE_FIELD( m_flTimeUpdatedFollowPosition, FIELD_TIME ),
|
|
DEFINE_FIELD( m_bFirstFacing, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_flTimeFollowTargetVisible, FIELD_TIME ),
|
|
DEFINE_EMBEDDED( m_TargetMonitor ),
|
|
DEFINE_FIELD( m_bTargetUnreachable, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_bMovingToCover, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_flOriginalEnemyDiscardTime, FIELD_FLOAT ),
|
|
DEFINE_EMBEDDED( m_FollowDelay ),
|
|
DEFINE_CUSTOM_FIELD( m_CurrentFollowActivity, ActivityDataOps() ),
|
|
DEFINE_EMBEDDED( m_TimeBlockUseWaitPoint ),
|
|
DEFINE_EMBEDDED( m_TimeCheckForWaitPoint ),
|
|
DEFINE_FIELD( m_pInterruptWaitPoint, FIELD_CLASSPTR ),
|
|
DEFINE_EMBEDDED( m_TimeBeforeSpreadFacing ),
|
|
DEFINE_EMBEDDED( m_TimeNextSpreadFacing ),
|
|
// m_hFollowManagerInfo (reset on load)
|
|
DEFINE_EMBEDDED( m_params ),
|
|
DEFINE_FIELD( m_hFollowGoalEnt, FIELD_EHANDLE ),
|
|
DEFINE_FIELD( m_nFailedFollowAttempts, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_flTimeFailFollowStarted, FIELD_TIME ),
|
|
DEFINE_FIELD( m_vFollowMoveAnchor, FIELD_POSITION_VECTOR ),
|
|
END_DATADESC();
|
|
|
|
//-------------------------------------
|
|
|
|
CAI_FollowBehavior::CAI_FollowBehavior( const AI_FollowParams_t ¶ms )
|
|
{
|
|
memset( &m_FollowNavGoal, 0, sizeof( m_FollowNavGoal ) );
|
|
|
|
m_FollowDelay.Set( 1.0, 3.0 );
|
|
m_hFollowManagerInfo.m_pGroup = NULL;
|
|
m_hFollowManagerInfo.m_hFollower = 0;
|
|
|
|
m_TimeBlockUseWaitPoint.Set( 0.5, 1.5 );
|
|
m_TimeCheckForWaitPoint.Set( 1.0 );
|
|
m_pInterruptWaitPoint = NULL;
|
|
|
|
m_TimeBeforeSpreadFacing.Set( 2.0, 4.0 );
|
|
m_TimeNextSpreadFacing.Set( 3.0, 12.0 );
|
|
|
|
m_params = params;
|
|
|
|
NoteSuccessfulFollow();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
CAI_FollowBehavior::~CAI_FollowBehavior()
|
|
{
|
|
Assert( !m_hFollowManagerInfo.m_pGroup );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Draw any text overlays
|
|
// Input : Previous text offset from the top
|
|
// Output : Current text offset from the top
|
|
//-----------------------------------------------------------------------------
|
|
int CAI_FollowBehavior::DrawDebugTextOverlays( int text_offset )
|
|
{
|
|
char tempstr[ 512 ];
|
|
int offset;
|
|
CBaseEntity * followEnt;
|
|
|
|
offset = BaseClass::DrawDebugTextOverlays( text_offset );
|
|
if ( GetOuter()->m_debugOverlays & OVERLAY_TEXT_BIT )
|
|
{
|
|
followEnt = GetFollowTarget();
|
|
if ( followEnt != NULL )
|
|
{
|
|
Q_snprintf( tempstr, sizeof(tempstr), "Follow: (%d) %s (%s)", followEnt->entindex(), followEnt->GetDebugName(), followEnt->GetClassname() );
|
|
}
|
|
else
|
|
{
|
|
Q_snprintf( tempstr, sizeof(tempstr), "Follow: NULL" );
|
|
}
|
|
GetOuter()->EntityText( offset, tempstr, 0 );
|
|
offset++;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::SetParameters( const AI_FollowParams_t ¶ms )
|
|
{
|
|
m_params = params;
|
|
|
|
if ( m_hFollowManagerInfo.m_pGroup )
|
|
{
|
|
g_AIFollowManager.ChangeFormation( m_hFollowManagerInfo, params.formation );
|
|
m_flTimeUpdatedFollowPosition = 0;
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
CBaseEntity * CAI_FollowBehavior::GetFollowTarget()
|
|
{
|
|
return m_hFollowTarget;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
// Returns true if the NPC is actively following a target.
|
|
bool CAI_FollowBehavior::IsActive( void )
|
|
{
|
|
if ( IsRunning() && GetFollowTarget() )
|
|
{
|
|
// Only true if we're running a follow schedule
|
|
return IsCurScheduleFollowSchedule();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::SetFollowTarget( CBaseEntity *pLeader, bool fFinishCurSchedule )
|
|
{
|
|
if ( pLeader == m_hFollowTarget )
|
|
return;
|
|
|
|
m_flTimeUpdatedFollowPosition = 0;
|
|
|
|
if ( m_hFollowTarget )
|
|
{
|
|
g_AIFollowManager.RemoveFollower( m_hFollowManagerInfo );
|
|
m_hFollowTarget = NULL;
|
|
m_hFollowManagerInfo.m_pGroup = NULL;
|
|
if ( IsRunning() )
|
|
{
|
|
if ( GetNavigator()->GetGoalType() == GOALTYPE_TARGETENT )
|
|
{
|
|
GetNavigator()->StopMoving(); // Stop him from walking toward the player
|
|
}
|
|
|
|
if ( GetEnemy() != NULL )
|
|
{
|
|
GetOuter()->SetIdealState( NPC_STATE_COMBAT );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( pLeader )
|
|
{
|
|
if ( g_AIFollowManager.AddFollower( pLeader, GetOuter(), m_params.formation, &m_hFollowManagerInfo ) )
|
|
{
|
|
m_hFollowTarget = pLeader;
|
|
m_bFirstFacing = true;
|
|
m_flTimeFollowTargetVisible = 0;
|
|
SetCondition( COND_TARGET_MOVED_FROM_MARK );
|
|
m_TargetMonitor.ClearMark();
|
|
NoteSuccessfulFollow();
|
|
}
|
|
}
|
|
|
|
NotifyChangeBehaviorStatus(fFinishCurSchedule);
|
|
}
|
|
|
|
//-------------------------------------
|
|
void CAI_FollowBehavior::SetFollowGoalDirect( CAI_FollowGoal *pGoal )
|
|
{
|
|
m_hFollowGoalEnt = pGoal;
|
|
m_flTimeUpdatedFollowPosition = 0;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::SetFollowGoal( CAI_FollowGoal *pGoal, bool fFinishCurSchedule )
|
|
{
|
|
if ( GetOuter()->ShouldAcceptGoal( this, pGoal ) )
|
|
{
|
|
GetOuter()->ClearCommandGoal();
|
|
|
|
if( hl2_episodic.GetBool() )
|
|
{
|
|
// Poke the NPC to interrupt any stubborn schedules
|
|
GetOuter()->SetCondition(COND_PROVOKED);
|
|
}
|
|
|
|
SetFollowTarget( pGoal->GetGoalEntity() );
|
|
Assert( pGoal->m_iFormation == AIF_SIMPLE || pGoal->m_iFormation == AIF_WIDE || pGoal->m_iFormation == AIF_MEDIUM || pGoal->m_iFormation == AIF_SIDEKICK );
|
|
SetParameters( AI_FollowParams_t( (AI_Formations_t)pGoal->m_iFormation ) );
|
|
m_hFollowGoalEnt = pGoal;
|
|
m_flTimeUpdatedFollowPosition = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::ClearFollowGoal( CAI_FollowGoal *pGoal )
|
|
{
|
|
GetOuter()->OnClearGoal( this, pGoal );
|
|
if ( pGoal == m_hFollowGoalEnt )
|
|
{
|
|
SetFollowTarget( NULL );
|
|
m_hFollowGoalEnt = NULL;
|
|
m_flTimeUpdatedFollowPosition = 0;
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::UpdateFollowPosition()
|
|
{
|
|
AI_PROFILE_SCOPE( CAI_FollowBehavior_UpdateFollowPosition );
|
|
|
|
if ( m_flTimeUpdatedFollowPosition == gpGlobals->curtime )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (m_hFollowTarget == NULL)
|
|
return false;
|
|
|
|
if ( !g_AIFollowManager.CalcFollowPosition( m_hFollowManagerInfo, &m_FollowNavGoal ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if TODO
|
|
// @TODO (toml 07-27-03): this is too simplistic. fails when the new point is an inappropriate target
|
|
CBasePlayer *pPlayer = dynamic_cast<CBasePlayer *>(m_hFollowTarget.Get());
|
|
Vector targetVelocity = pPlayer->GetSmoothedVelocity();
|
|
m_FollowNavGoal.position += targetVelocity * 0.5;
|
|
#endif
|
|
|
|
m_flTimeUpdatedFollowPosition = gpGlobals->curtime;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsMovingToFollowTarget()
|
|
{
|
|
return ( IsRunning() && ( IsCurSchedule(SCHED_FOLLOW, false) || IsCurSchedule(SCHED_FOLLOWER_GO_TO_WAIT_POINT, false) ) );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::CanSelectSchedule()
|
|
{
|
|
if ( !GetOuter()->IsInterruptable() )
|
|
return false;
|
|
|
|
return ShouldFollow();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::PlayerIsPushing()
|
|
{
|
|
return (m_hFollowTarget && m_hFollowTarget->IsPlayer() && HasCondition( COND_PLAYER_PUSHING ) );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsFollowTargetInRange()
|
|
{
|
|
if ( !GetFollowTarget()->IsPlayer() && HasCondition( COND_RECEIVED_ORDERS ) )
|
|
return false;
|
|
|
|
if( GetNpcState() == NPC_STATE_COMBAT )
|
|
{
|
|
if( IsFollowGoalInRange( MAX( m_FollowNavGoal.coverTolerance, m_FollowNavGoal.enemyLOSTolerance ), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( IsFollowGoalInRange( MAX( m_FollowNavGoal.tolerance, GetGoalRange() ), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
if ( m_FollowNavGoal.flags & AIFF_REQUIRE_LOS_OUTSIDE_COMBAT )
|
|
{
|
|
//trace_t tr;
|
|
//AI_TraceLOS( vecStart, vecStart + vecDir * 8192, m_hFollowTarget, &tr );
|
|
//if ( AI_TraceLOS m_FollowNavGoal.position
|
|
if ( !HasCondition(COND_SEE_PLAYER) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsFollowGoalInRange( float tolerance, float zTolerance, int flags )
|
|
{
|
|
const Vector &origin = WorldSpaceCenter();
|
|
const Vector &goal = GetGoalPosition();
|
|
if ( zTolerance == -1 )
|
|
zTolerance = GetHullHeight();
|
|
float distanceSq = ( goal.AsVector2D() - origin.AsVector2D() ).LengthSqr();
|
|
tolerance += 0.1;
|
|
|
|
// Increase Z tolerance slightly as XY distance decreases
|
|
float flToleranceSq = (tolerance*tolerance);
|
|
float flIncreaseRange = flToleranceSq * 0.25;
|
|
zTolerance += zTolerance * clamp((distanceSq / flIncreaseRange), 0, 1 );
|
|
if ( fabs( origin.z - goal.z ) > zTolerance )
|
|
return false;
|
|
|
|
if ( distanceSq > flToleranceSq )
|
|
return false;
|
|
|
|
if ( flags & AIFF_REQUIRE_LOS_OUTSIDE_COMBAT && m_hFollowTarget.Get() )
|
|
{
|
|
if ( !GetOuter()->GetSenses()->DidSeeEntity( m_hFollowTarget ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsChaseGoalInRange()
|
|
{
|
|
if ( GetEnemy() && ( GetEnemy()->WorldSpaceCenter() - m_FollowNavGoal.position ).LengthSqr() > Square( m_FollowNavGoal.chaseEnemyTolerance ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::NoteFailedFollow()
|
|
{
|
|
m_nFailedFollowAttempts++;
|
|
if ( m_flTimeFailFollowStarted == FLT_MAX )
|
|
m_flTimeFailFollowStarted = gpGlobals->curtime;
|
|
|
|
if ( GetOuter() && ai_debug_follow.GetBool() )
|
|
DevMsg( GetOuter(), "Follow: NoteFailedFollow() (%d, %f)\n", m_nFailedFollowAttempts, m_flTimeFailFollowStarted );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::NoteSuccessfulFollow()
|
|
{
|
|
m_nFailedFollowAttempts = 0;
|
|
m_flTimeFailFollowStarted = FLT_MAX;
|
|
FollowMsg( "NoteSuccessfulFollow()\n" );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::BeginScheduleSelection()
|
|
{
|
|
if ( GetOuter()->m_hCine )
|
|
GetOuter()->m_hCine->CancelScript();
|
|
|
|
m_TimeBeforeSpreadFacing.Reset();
|
|
|
|
SetCondition( COND_TARGET_MOVED_FROM_MARK );
|
|
m_TargetMonitor.ClearMark();
|
|
NoteSuccessfulFollow();
|
|
|
|
// Forget about enemies that I haven't seen for >5 seconds
|
|
m_flOriginalEnemyDiscardTime = GetOuter()->GetEnemies()->GetEnemyDiscardTime();
|
|
GetOuter()->GetEnemies()->SetEnemyDiscardTime( 5.0f );
|
|
|
|
BaseClass::BeginScheduleSelection();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::EndScheduleSelection()
|
|
{
|
|
// Restore our original enemy discard time
|
|
GetOuter()->GetEnemies()->SetEnemyDiscardTime( m_flOriginalEnemyDiscardTime );
|
|
|
|
BaseClass::EndScheduleSelection();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::CleanupOnDeath( CBaseEntity *pCulprit, bool bFireDeathOutput )
|
|
{
|
|
if ( m_hFollowManagerInfo.m_pGroup )
|
|
{
|
|
g_AIFollowManager.RemoveFollower( m_hFollowManagerInfo );
|
|
m_hFollowManagerInfo.m_pGroup = NULL;
|
|
m_hFollowTarget = NULL;
|
|
}
|
|
BaseClass::CleanupOnDeath( pCulprit, bFireDeathOutput );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::Precache()
|
|
{
|
|
if ( m_hFollowTarget != NULL && m_hFollowManagerInfo.m_pGroup == NULL )
|
|
{
|
|
// Post load fixup
|
|
if ( !g_AIFollowManager.AddFollower( m_hFollowTarget, GetOuter(), m_params.formation, &m_hFollowManagerInfo ) )
|
|
{
|
|
m_hFollowTarget = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::GatherConditions( void )
|
|
{
|
|
BaseClass::GatherConditions();
|
|
|
|
if ( !GetFollowTarget() )
|
|
{
|
|
ClearCondition( COND_FOLLOW_PLAYER_IS_LIT );
|
|
ClearCondition( COND_FOLLOW_PLAYER_IS_NOT_LIT );
|
|
ClearCondition( COND_FOLLOW_TARGET_VISIBLE );
|
|
ClearCondition( COND_FOLLOW_TARGET_NOT_VISIBLE );
|
|
ClearCondition( COND_FOLLOW_DELAY_EXPIRED );
|
|
ClearCondition( COND_TARGET_MOVED_FROM_MARK );
|
|
ClearFollowPoint();
|
|
m_pInterruptWaitPoint = NULL;
|
|
m_bTargetUnreachable = false;
|
|
m_flTimeFollowTargetVisible = 0;
|
|
return;
|
|
}
|
|
|
|
if ( !m_TargetMonitor.IsMarkSet() )
|
|
{
|
|
FollowMsg( "No mark set\n" );
|
|
}
|
|
|
|
if ( m_FollowDelay.IsRunning() && m_FollowDelay.Expired())
|
|
{
|
|
SetCondition( COND_FOLLOW_DELAY_EXPIRED );
|
|
m_FollowDelay.Stop();
|
|
}
|
|
|
|
if ( m_TargetMonitor.TargetMoved2D( GetFollowTarget() ) )
|
|
{
|
|
FollowMsg( "Target moved\n" );
|
|
m_TargetMonitor.ClearMark();
|
|
SetCondition( COND_TARGET_MOVED_FROM_MARK );
|
|
m_bTargetUnreachable = false;
|
|
}
|
|
|
|
if ( !m_TargetMonitor.IsMarkSet() )
|
|
m_bTargetUnreachable = false;
|
|
|
|
m_pInterruptWaitPoint = NULL;
|
|
|
|
if ( GetHintNode() == NULL )
|
|
{
|
|
if ( ShouldUseFollowPoints() && m_TimeBlockUseWaitPoint.Expired() && m_TimeCheckForWaitPoint.Expired() )
|
|
{
|
|
m_TimeCheckForWaitPoint.Reset();
|
|
m_pInterruptWaitPoint = FindFollowPoint();
|
|
if ( m_pInterruptWaitPoint )
|
|
SetCondition( COND_FOUND_WAIT_POINT );
|
|
}
|
|
}
|
|
|
|
if ( m_flTimeUpdatedFollowPosition == 0 || gpGlobals->curtime - m_flTimeUpdatedFollowPosition > 2.0 )
|
|
UpdateFollowPosition();
|
|
|
|
if ( IsFollowTargetInRange() )
|
|
{
|
|
NoteSuccessfulFollow();
|
|
}
|
|
else if ( GetOuter()->GetTask() && !IsCurScheduleFollowSchedule() )
|
|
{
|
|
if ( !m_FollowDelay.IsRunning() || m_FollowDelay.Expired() )
|
|
{
|
|
switch ( GetOuter()->GetTask()->iTask )
|
|
{
|
|
case TASK_WAIT_RANDOM:
|
|
case TASK_WAIT_INDEFINITE:
|
|
case TASK_WAIT:
|
|
case TASK_WAIT_FACE_ENEMY:
|
|
case TASK_WAIT_FACE_ENEMY_RANDOM:
|
|
{
|
|
m_TargetMonitor.ClearMark();
|
|
if ( !HasCondition(COND_FOLLOW_PLAYER_IS_NOT_LIT) )
|
|
{
|
|
SetCondition( COND_TARGET_MOVED_FROM_MARK );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
else if ( !IsFollowPointInRange() )
|
|
{
|
|
GetHintNode()->Unlock();
|
|
SetHintNode( NULL );
|
|
}
|
|
#endif
|
|
|
|
#ifdef HL2_EPISODIC
|
|
// Let followers know if the player is lit in the darkness
|
|
if ( GetFollowTarget()->IsPlayer() && HL2GameRules()->IsAlyxInDarknessMode() )
|
|
{
|
|
if ( LookerCouldSeeTargetInDarkness( GetOuter(), GetFollowTarget() ) )
|
|
{
|
|
SetCondition( COND_FOLLOW_PLAYER_IS_LIT );
|
|
ClearCondition( COND_FOLLOW_PLAYER_IS_NOT_LIT );
|
|
}
|
|
else
|
|
{
|
|
SetCondition( COND_FOLLOW_PLAYER_IS_NOT_LIT );
|
|
ClearCondition( COND_FOLLOW_PLAYER_IS_LIT );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Set our follow target visibility state
|
|
if ( (GetFollowTarget()->IsPlayer() && HasCondition( COND_SEE_PLAYER )) || GetOuter()->FVisible( GetFollowTarget()) )
|
|
{
|
|
SetCondition( COND_FOLLOW_TARGET_VISIBLE );
|
|
ClearCondition( COND_FOLLOW_TARGET_NOT_VISIBLE );
|
|
m_flTimeFollowTargetVisible = gpGlobals->curtime;
|
|
}
|
|
else
|
|
{
|
|
ClearCondition( COND_FOLLOW_TARGET_VISIBLE );
|
|
SetCondition( COND_FOLLOW_TARGET_NOT_VISIBLE );
|
|
}
|
|
|
|
if ( HasFollowPoint() && ( m_flTimeFollowTargetVisible != 0 && gpGlobals->curtime - m_flTimeFollowTargetVisible > 5.0 ) )
|
|
SetCondition( COND_FOLLOW_WAIT_POINT_INVALID );
|
|
else
|
|
ClearCondition( COND_FOLLOW_WAIT_POINT_INVALID );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode )
|
|
{
|
|
if ( failedTask == TASK_MOVE_TO_FOLLOW_POSITION || failedTask == TASK_GET_PATH_TO_FOLLOW_POSITION )
|
|
{
|
|
if ( m_hFollowTarget )
|
|
{
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance * 0.5 );
|
|
m_FollowDelay.Start();
|
|
NoteFailedFollow();
|
|
}
|
|
}
|
|
|
|
return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ShouldFollow()
|
|
{
|
|
if ( !GetFollowTarget() )
|
|
return false;
|
|
|
|
return !( GetFollowTarget()->GetFlags() & FL_NOTARGET);
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ShouldMoveToFollowTarget()
|
|
{
|
|
if( m_bTargetUnreachable )
|
|
return false;
|
|
|
|
#ifdef HL2_EPISODIC
|
|
if ( HL2GameRules()->IsAlyxInDarknessMode() )
|
|
{
|
|
// If we're in darkness mode, the player needs to be lit by
|
|
// darkness, but we don't need line of sight to him.
|
|
if ( HasCondition(COND_FOLLOW_PLAYER_IS_NOT_LIT) )
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
if ( HasFollowPoint() )
|
|
{
|
|
if ( IsFollowPointInRange() )
|
|
return false;
|
|
}
|
|
else if ( IsFollowTargetInRange() )
|
|
return false;
|
|
|
|
if( m_FollowDelay.IsRunning() && !m_FollowDelay.Expired() && !HasCondition( COND_TARGET_MOVED_FROM_MARK ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::SelectScheduleManagePosition()
|
|
{
|
|
if ( PlayerIsPushing() )
|
|
return SCHED_MOVE_AWAY;
|
|
|
|
if ( !UpdateFollowPosition() )
|
|
return SCHED_FAIL;
|
|
|
|
return SCHED_NONE;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ShouldUseFollowPoints()
|
|
{
|
|
if ( !ai_follow_use_points.GetBool() || GetEnemy() != NULL )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::HasFollowPoint()
|
|
{
|
|
return ( GetHintNode() && GetHintNode()->HintType() == HINT_FOLLOW_WAIT_POINT );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::ClearFollowPoint()
|
|
{
|
|
if ( GetHintNode() && GetHintNode()->HintType() == HINT_FOLLOW_WAIT_POINT )
|
|
{
|
|
GetHintNode()->Unlock();
|
|
SetHintNode( NULL );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
const Vector &CAI_FollowBehavior::GetFollowPoint()
|
|
{
|
|
static Vector invalid = vec3_invalid;
|
|
if ( GetHintNode() && GetHintNode()->HintType() == HINT_FOLLOW_WAIT_POINT )
|
|
return GetHintNode()->GetAbsOrigin();
|
|
return invalid;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
CAI_Hint *CAI_FollowBehavior::FindFollowPoint()
|
|
{
|
|
if ( !m_TimeBlockUseWaitPoint.Expired() )
|
|
return NULL;
|
|
|
|
CHintCriteria hintCriteria;
|
|
hintCriteria.SetHintType( HINT_FOLLOW_WAIT_POINT );
|
|
hintCriteria.SetFlag( bits_HINT_NODE_VISIBLE | bits_HINT_NODE_NEAREST );
|
|
|
|
// Add the search position
|
|
hintCriteria.AddIncludePosition( GetGoalPosition(), MAX( m_FollowNavGoal.followPointTolerance, GetGoalRange() ) );
|
|
hintCriteria.AddExcludePosition( GetGoalPosition(), (GetFollowTarget()->WorldAlignMins().AsVector2D() - GetFollowTarget()->WorldAlignMaxs().AsVector2D()).Length());
|
|
|
|
return CAI_HintManager::FindHint( GetOuter(), hintCriteria );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsFollowPointInRange()
|
|
{
|
|
return ( GetHintNode() &&
|
|
GetHintNode()->HintType() == HINT_FOLLOW_WAIT_POINT &&
|
|
(GetHintNode()->GetAbsOrigin() - GetFollowTarget()->GetAbsOrigin()).LengthSqr() < Square(MAX(m_FollowNavGoal.followPointTolerance, GetGoalRange())) );
|
|
}
|
|
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ShouldIgnoreFollowPointFacing()
|
|
{
|
|
if ( !GetHintNode() )
|
|
return true;
|
|
|
|
HintIgnoreFacing_t hintSetting = GetHintNode()->GetIgnoreFacing();
|
|
|
|
if ( hintSetting == HIF_DEFAULT )
|
|
return ( GetHintNode()->HintActivityName() == NULL_STRING );
|
|
|
|
return ( hintSetting == HIF_YES );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::SetFollowPoint( CAI_Hint *pHintNode )
|
|
{
|
|
if ( !pHintNode )
|
|
return;
|
|
|
|
Assert( pHintNode->HintType() == HINT_FOLLOW_WAIT_POINT );
|
|
|
|
if ( GetHintNode() == pHintNode )
|
|
return;
|
|
|
|
if ( GetHintNode() )
|
|
GetHintNode()->Unlock();
|
|
|
|
if ( !pHintNode->Lock( GetOuter() ) )
|
|
{
|
|
SetHintNode( NULL );
|
|
m_TimeBlockUseWaitPoint.Reset();
|
|
}
|
|
else
|
|
SetHintNode( pHintNode );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::SelectScheduleFollowPoints()
|
|
{
|
|
bool bShouldUseFollowPoints = ( ShouldUseFollowPoints() && IsFollowGoalInRange( m_FollowNavGoal.followPointTolerance + 0.1, GetGoalZRange(), GetGoalFlags() ) );
|
|
float distSqToPoint = FLT_MAX;
|
|
bool bHasFollowPoint = HasFollowPoint();
|
|
|
|
if ( bHasFollowPoint )
|
|
{
|
|
distSqToPoint = (GetHintNode()->GetAbsOrigin() - GetAbsOrigin()).LengthSqr();
|
|
if ( !bShouldUseFollowPoints ||
|
|
distSqToPoint > Square(2.0 * GetHullWidth()) ||
|
|
HasCondition( COND_FOLLOW_WAIT_POINT_INVALID ) )
|
|
{
|
|
GetHintNode()->Unlock();
|
|
SetHintNode( NULL );
|
|
m_TimeBlockUseWaitPoint.Reset();
|
|
bShouldUseFollowPoints = false;
|
|
}
|
|
}
|
|
|
|
if ( bShouldUseFollowPoints )
|
|
{
|
|
bool bNewHint = false;
|
|
if ( GetHintNode() && !bHasFollowPoint )
|
|
{
|
|
GetHintNode()->Unlock();
|
|
SetHintNode( NULL );
|
|
}
|
|
|
|
if (!GetHintNode())
|
|
{
|
|
bNewHint = true;
|
|
SetFollowPoint( ( m_pInterruptWaitPoint ) ? m_pInterruptWaitPoint : FindFollowPoint() );
|
|
|
|
if ( GetHintNode() )
|
|
distSqToPoint = (GetHintNode()->GetAbsOrigin() - GetAbsOrigin()).LengthSqr();
|
|
}
|
|
|
|
if ( GetHintNode() )
|
|
{
|
|
if ( bNewHint || distSqToPoint > WAIT_HINT_MIN_DIST )
|
|
return SCHED_FOLLOWER_GO_TO_WAIT_POINT;
|
|
if ( !ShouldIgnoreFollowPointFacing() )
|
|
return SCHED_FOLLOWER_STAND_AT_WAIT_POINT;
|
|
}
|
|
}
|
|
else
|
|
ClearFollowPoint();
|
|
|
|
return SCHED_NONE;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::SelectScheduleMoveToFormation()
|
|
{
|
|
if( ( GetNpcState() != NPC_STATE_COMBAT && !( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ))) ||
|
|
!IsFollowGoalInRange( GetGoalRange(), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
AISquadIter_t iter;
|
|
CAI_Squad *pSquad = GetOuter()->GetSquad();
|
|
if ( pSquad )
|
|
{
|
|
for ( CAI_BaseNPC *pSquadMember = pSquad->GetFirstMember( &iter ); pSquadMember; pSquadMember = pSquad->GetNextMember( &iter ) )
|
|
{
|
|
if ( pSquadMember->HasCondition( COND_PLAYER_PUSHING ) )
|
|
{
|
|
return SCHED_NONE;
|
|
}
|
|
}
|
|
}
|
|
if ( ShouldMoveToFollowTarget() || m_bFirstFacing )
|
|
{
|
|
return SCHED_TARGET_FACE; // Code for "SCHED_MOVE_TO_FACE_FOLLOW_TARGET". Used by Talker clients to interject comment
|
|
}
|
|
}
|
|
return SCHED_NONE;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::SelectSchedule()
|
|
{
|
|
// Allow a range attack if we need to do it
|
|
if ( hl2_episodic.GetBool() )
|
|
{
|
|
// Range attack
|
|
if ( GetOuter()->ShouldMoveAndShoot() == false && HasCondition( COND_CAN_RANGE_ATTACK1 ) )
|
|
return SCHED_RANGE_ATTACK1;
|
|
}
|
|
|
|
if ( GetFollowTarget() )
|
|
{
|
|
if ( !GetFollowTarget()->IsAlive() )
|
|
{
|
|
// UNDONE: Comment about the recently dead player here?
|
|
SetFollowTarget( NULL );
|
|
}
|
|
else if ( ShouldFollow() )
|
|
{
|
|
int result = SCHED_NONE;
|
|
|
|
result = SelectScheduleManagePosition();
|
|
if ( result != SCHED_NONE )
|
|
return result;
|
|
|
|
result = SelectScheduleFollowPoints();
|
|
if ( result != SCHED_NONE )
|
|
return result;
|
|
|
|
result = SelectScheduleMoveToFormation();
|
|
if ( result != SCHED_NONE )
|
|
return result;
|
|
|
|
if ( HasCondition ( COND_NO_PRIMARY_AMMO ) && HaveSequenceForActivity( GetOuter()->TranslateActivity( ACT_RUN_AIM ) ) )
|
|
return SCHED_HIDE_AND_RELOAD;
|
|
}
|
|
|
|
if ( PlayerIsPushing() )
|
|
return SCHED_MOVE_AWAY;
|
|
}
|
|
else
|
|
{
|
|
// Should not have landed here. Follow target ent must have been destroyed
|
|
NotifyChangeBehaviorStatus();
|
|
}
|
|
|
|
if ( HasCondition( COND_TARGET_MOVED_FROM_MARK ) )
|
|
{
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance * 0.5 );
|
|
}
|
|
|
|
return BaseClass::SelectSchedule();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowBehavior::TranslateSchedule( int scheduleType )
|
|
{
|
|
switch( scheduleType )
|
|
{
|
|
case SCHED_IDLE_STAND:
|
|
{
|
|
if ( ShouldMoveToFollowTarget() && !IsFollowGoalInRange( GetGoalRange(), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
return SCHED_MOVE_TO_FACE_FOLLOW_TARGET;
|
|
}
|
|
if ( HasFollowPoint() && !ShouldIgnoreFollowPointFacing() )
|
|
return SCHED_FOLLOWER_GO_TO_WAIT_POINT;
|
|
return SCHED_FOLLOWER_IDLE_STAND;
|
|
}
|
|
|
|
case SCHED_COMBAT_STAND:
|
|
case SCHED_ALERT_STAND:
|
|
{
|
|
if ( ShouldMoveToFollowTarget() && !IsFollowGoalInRange( GetGoalRange(), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
return SCHED_MOVE_TO_FACE_FOLLOW_TARGET;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SCHED_TARGET_FACE:
|
|
{
|
|
if ( ShouldMoveToFollowTarget() && !IsFollowGoalInRange( GetGoalRange(), GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
return SCHED_MOVE_TO_FACE_FOLLOW_TARGET;
|
|
}
|
|
if ( HasFollowPoint() && !ShouldIgnoreFollowPointFacing() )
|
|
return SCHED_FOLLOWER_GO_TO_WAIT_POINT;
|
|
if ( !m_TargetMonitor.IsMarkSet() )
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance );
|
|
return SCHED_FACE_FOLLOW_TARGET; // @TODO (toml 03-03-03): should select a facing sched
|
|
}
|
|
|
|
case SCHED_TARGET_CHASE:
|
|
{
|
|
return SCHED_FOLLOW;
|
|
}
|
|
|
|
case SCHED_CHASE_ENEMY:
|
|
{
|
|
if ( IsChaseGoalInRange() == false )
|
|
return SCHED_FOLLOWER_IDLE_STAND;
|
|
break;
|
|
}
|
|
|
|
case SCHED_RANGE_ATTACK1:
|
|
{
|
|
if ( GetOuter()->GetShotRegulator()->IsInRestInterval() )
|
|
return SCHED_FOLLOWER_IDLE_STAND; // @TODO (toml 07-02-03): Should do something more tactically sensible
|
|
break;
|
|
}
|
|
|
|
case SCHED_CHASE_ENEMY_FAILED:
|
|
{
|
|
if (HasMemory(bits_MEMORY_INCOVER))
|
|
{
|
|
// Make sure I don't get too far from the player
|
|
if ( GetFollowTarget() )
|
|
{
|
|
float fDist = (GetLocalOrigin() - GetFollowTarget()->GetAbsOrigin()).Length();
|
|
if (fDist > 500)
|
|
{
|
|
return SCHED_FOLLOW;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SCHED_MOVE_AWAY_FAIL:
|
|
{
|
|
return SCHED_FOLLOWER_MOVE_AWAY_FAIL;
|
|
}
|
|
case SCHED_MOVE_AWAY_END:
|
|
{
|
|
return SCHED_FOLLOWER_MOVE_AWAY_END;
|
|
}
|
|
}
|
|
return BaseClass::TranslateSchedule( scheduleType );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::OnStartSchedule( int scheduleType )
|
|
{
|
|
if ( !IsRunning() && HasFollowPoint() )
|
|
{
|
|
ClearHintNode( 0.5 );
|
|
}
|
|
|
|
if ( !m_TargetMonitor.IsMarkSet() && !IsCurScheduleFollowSchedule() )
|
|
{
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::GetFollowTargetViewLoc( Vector *pResult )
|
|
{
|
|
if ( !dynamic_cast<CPointEntity *>(m_hFollowTarget.Get()) )
|
|
{
|
|
trace_t tr;
|
|
Vector vecStart, vecDir;
|
|
|
|
ASSERT( m_hFollowTarget != NULL );
|
|
|
|
vecStart = m_hFollowTarget->EyePosition();
|
|
|
|
CBasePlayer *pPlayer;
|
|
|
|
pPlayer = dynamic_cast<CBasePlayer *>(m_hFollowTarget.Get());
|
|
|
|
if( pPlayer )
|
|
{
|
|
// Follow target is a player.
|
|
pPlayer->EyeVectors( &vecDir, NULL, NULL );
|
|
}
|
|
else
|
|
{
|
|
// Not a player.
|
|
m_hFollowTarget->GetVectors( &vecDir, NULL, NULL );
|
|
}
|
|
|
|
AI_TraceLOS( vecStart, vecStart + vecDir * 8192, m_hFollowTarget, &tr );
|
|
|
|
*pResult = tr.endpos;
|
|
}
|
|
else
|
|
*pResult = m_hFollowTarget->GetAbsOrigin();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ValidateFaceTarget( Vector *pFaceTarget )
|
|
{
|
|
if ( *pFaceTarget == vec3_invalid )
|
|
{
|
|
if ( m_hFollowTarget != NULL )
|
|
{
|
|
*pFaceTarget = m_hFollowTarget->GetAbsOrigin();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Vector testPoint = *pFaceTarget - GetAbsOrigin();
|
|
testPoint.z = 0;
|
|
VectorNormalize( testPoint );
|
|
testPoint *= 48;
|
|
testPoint += GetOuter()->EyePosition();
|
|
|
|
trace_t tr;
|
|
AI_TraceLine( GetOuter()->EyePosition(), testPoint, MASK_OPAQUE, m_hFollowTarget, COLLISION_GROUP_NONE, &tr );
|
|
|
|
if ( tr.fraction < 1.0 )
|
|
{
|
|
*pFaceTarget = m_hFollowTarget->GetAbsOrigin();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::FindCoverFromEnemyAtFollowTarget( float coverRadius, Vector *pResult )
|
|
{
|
|
CBaseEntity *pEntity = GetEnemy();
|
|
|
|
return GetOuter()->FindCoverPosInRadius( pEntity, m_FollowNavGoal.position, coverRadius, pResult );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::StartTask( const Task_t *pTask )
|
|
{
|
|
AI_PROFILE_SCOPE( CAI_FollowBehavior_StartTask );
|
|
|
|
switch ( pTask->iTask )
|
|
{
|
|
case TASK_RANGE_ATTACK1:
|
|
BaseClass::StartTask( pTask );
|
|
break;
|
|
|
|
case TASK_GET_PATH_TO_FOLLOW_POSITION:
|
|
{
|
|
if ( !UpdateFollowPosition() )
|
|
{
|
|
TaskFail(FAIL_NO_TARGET);
|
|
}
|
|
else
|
|
{
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance );
|
|
m_bMovingToCover = false;
|
|
GetOuter()->m_vInterruptSavePosition = vec3_invalid;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case TASK_CANT_FOLLOW:
|
|
{
|
|
SetFollowTarget( NULL, true );
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
case TASK_FOLLOWER_FACE_TACTICAL:
|
|
case TASK_FACE_FOLLOW_TARGET:
|
|
{
|
|
if ( !m_TimeBeforeSpreadFacing.Expired() )
|
|
{
|
|
m_TimeNextSpreadFacing.Reset();
|
|
}
|
|
|
|
Vector faceTarget = vec3_invalid;
|
|
bool bFollowingPoint = ( dynamic_cast<CPointEntity *>(m_hFollowTarget.Get()) != NULL );
|
|
if ( GetNpcState() == NPC_STATE_COMBAT )
|
|
{
|
|
if( gpGlobals->curtime - GetOuter()->GetEnemyLastTimeSeen() < 5.0 )
|
|
{
|
|
faceTarget = GetEnemyLKP();
|
|
}
|
|
else if ( !bFollowingPoint )
|
|
{
|
|
GetFollowTargetViewLoc( &faceTarget );
|
|
}
|
|
}
|
|
else if ( m_hFollowTarget && !bFollowingPoint )
|
|
{
|
|
if ( m_bFirstFacing && m_hFollowTarget->IsPlayer() )
|
|
{
|
|
faceTarget = m_hFollowTarget->GetAbsOrigin();
|
|
}
|
|
else if ( m_TimeNextSpreadFacing.Expired() )
|
|
{
|
|
m_TimeNextSpreadFacing.Reset();
|
|
|
|
bool bIsEpisodicVitalAlly;
|
|
|
|
#ifdef HL2_DLL
|
|
bIsEpisodicVitalAlly = (hl2_episodic.GetBool() && GetOuter()->Classify() == CLASS_PLAYER_ALLY_VITAL);
|
|
#else
|
|
bIsEpisodicVitalAlly = false;
|
|
#endif//HL2_DLL
|
|
|
|
if( bIsEpisodicVitalAlly )
|
|
{
|
|
faceTarget = m_hFollowTarget->GetAbsOrigin();
|
|
}
|
|
else
|
|
{
|
|
int roll = random->RandomInt(1, 4);
|
|
if ( roll == 1 )
|
|
{
|
|
GetFollowTargetViewLoc( &faceTarget );
|
|
}
|
|
else if ( roll == 2 )
|
|
{
|
|
faceTarget = m_hFollowTarget->GetAbsOrigin();
|
|
}
|
|
else
|
|
{
|
|
// Fan out and face to cover all directions.
|
|
int count = g_AIFollowManager.CountFollowersInGroup( GetOuter() );
|
|
|
|
if( count > 0 )
|
|
{
|
|
// Slice up the directions among followers and leader. ( +1 because we count the leader!)
|
|
float flSlice = 360.0 / (count + 1);
|
|
|
|
// Add one to slots so then are 1 to N instead of 0 to N - 1.
|
|
int slot = random->RandomInt( 0, count );
|
|
|
|
QAngle angle = m_hFollowTarget->GetAbsAngles();
|
|
|
|
// split up the remaining angles among followers in my group.
|
|
angle.y = UTIL_AngleMod( angle.y + ( flSlice * slot ) );
|
|
|
|
Vector vecDir;
|
|
AngleVectors( angle, &vecDir );
|
|
|
|
faceTarget = GetOuter()->GetAbsOrigin() + vecDir * 128;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Stay where we are
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_bFirstFacing = false;
|
|
|
|
if ( ValidateFaceTarget( &faceTarget ) )
|
|
{
|
|
Assert( faceTarget != vec3_invalid );
|
|
|
|
if ( !GetOuter()->FInAimCone( faceTarget ) )
|
|
{
|
|
GetMotor()->SetIdealYawToTarget( faceTarget, 30 );
|
|
GetOuter()->SetTurnActivity();
|
|
}
|
|
else
|
|
TaskComplete();
|
|
}
|
|
else
|
|
ChainStartTask( TASK_FACE_REASONABLE );
|
|
|
|
break;
|
|
}
|
|
|
|
case TASK_MOVE_TO_FOLLOW_POSITION:
|
|
{
|
|
if ( m_hFollowTarget == NULL)
|
|
{
|
|
TaskFail(FAIL_NO_TARGET);
|
|
}
|
|
else if ( (m_hFollowTarget->GetAbsOrigin() - GetAbsOrigin()).Length() < 1 )
|
|
{
|
|
TaskComplete();
|
|
}
|
|
else if ( !GetNavigator()->IsGoalActive() )
|
|
{
|
|
TaskFail(FAIL_NO_ROUTE);
|
|
}
|
|
else
|
|
{
|
|
m_vFollowMoveAnchor = GetAbsOrigin();
|
|
m_CurrentFollowActivity = ACT_INVALID;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TASK_SET_FOLLOW_TARGET_MARK:
|
|
{
|
|
if ( m_hFollowTarget == NULL)
|
|
{
|
|
TaskFail(FAIL_NO_TARGET);
|
|
}
|
|
else
|
|
{
|
|
FollowMsg( "TASK_SET_FOLLOW_TARGET_MARK\n" );
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance );
|
|
TaskComplete();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TASK_SET_FOLLOW_DELAY:
|
|
{
|
|
m_FollowDelay.Start( pTask->flTaskData );
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
case TASK_FIND_COVER_FROM_ENEMY:
|
|
{
|
|
CBaseEntity *pLeader = GetFollowTarget();
|
|
if ( pLeader )
|
|
{
|
|
Vector coverPos = vec3_invalid;
|
|
float coverRadius = MIN( GetOuter()->CoverRadius(), m_FollowNavGoal.coverTolerance );
|
|
|
|
if ( FindCoverFromEnemyAtFollowTarget( coverRadius, &coverPos ) )
|
|
{
|
|
AI_NavGoal_t goal(GOALTYPE_COVER, coverPos, ACT_RUN, AIN_HULL_TOLERANCE, AIN_DEF_FLAGS);
|
|
GetNavigator()->SetGoal( goal );
|
|
|
|
GetOuter()->m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
|
|
TaskComplete();
|
|
}
|
|
else
|
|
TaskFail(FAIL_NO_COVER);
|
|
}
|
|
else
|
|
BaseClass::StartTask( pTask );
|
|
break;
|
|
}
|
|
|
|
case TASK_GET_PATH_TO_FOLLOW_POINT:
|
|
{
|
|
ChainStartTask( TASK_GET_PATH_TO_HINTNODE, ShouldIgnoreFollowPointFacing() );
|
|
break;
|
|
}
|
|
|
|
case TASK_ARRIVE_AT_FOLLOW_POINT:
|
|
{
|
|
if ( GetHintNode() && !ShouldIgnoreFollowPointFacing() )
|
|
ChainStartTask( TASK_FACE_HINTNODE, 0 );
|
|
else
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
case TASK_SET_FOLLOW_POINT_STAND_SCHEDULE:
|
|
{
|
|
if ( GetHintNode() && !ShouldIgnoreFollowPointFacing() )
|
|
{
|
|
float distSqToPoint = (GetHintNode()->GetAbsOrigin() - GetAbsOrigin()).LengthSqr();
|
|
if ( distSqToPoint < WAIT_HINT_MIN_DIST )
|
|
{
|
|
GetOuter()->SetSchedule( SCHED_FOLLOWER_STAND_AT_WAIT_POINT );
|
|
}
|
|
else
|
|
{
|
|
GetHintNode()->Unlock();
|
|
SetHintNode( NULL );
|
|
m_TimeBlockUseWaitPoint.Reset();
|
|
TaskFail("Couldn't get to wait node." );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GetOuter()->SetSchedule( SCHED_FACE_FOLLOW_TARGET );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TASK_BEGIN_STAND_AT_WAIT_POINT:
|
|
{
|
|
if ( !m_TargetMonitor.IsMarkSet() && IsFollowPointInRange() )
|
|
m_TargetMonitor.SetMark( m_hFollowTarget, m_FollowNavGoal.targetMoveTolerance );
|
|
if ( GetHintNode() && !ShouldIgnoreFollowPointFacing() )
|
|
ChainStartTask( TASK_FACE_HINTNODE, 0 );
|
|
else
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
default:
|
|
BaseClass::StartTask( pTask );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::RunTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_GET_PATH_TO_FOLLOW_POSITION:
|
|
{
|
|
switch( GetOuter()->GetTaskInterrupt() )
|
|
{
|
|
case 0:
|
|
{
|
|
if ( GetEnemy() )
|
|
{
|
|
Assert( GetOuter()->m_vInterruptSavePosition == vec3_invalid );
|
|
Vector coverPos = vec3_invalid;
|
|
float coverRadius = MIN( (float)12*12, m_FollowNavGoal.coverTolerance );
|
|
if ( FindCoverFromEnemyAtFollowTarget( coverRadius, &coverPos ) )
|
|
{
|
|
GetOuter()->m_vInterruptSavePosition = coverPos;
|
|
}
|
|
GetOuter()->TaskInterrupt();
|
|
break;
|
|
}
|
|
}
|
|
// Fall through...
|
|
|
|
case 1:
|
|
{
|
|
if ( GetOuter()->m_vInterruptSavePosition != vec3_invalid )
|
|
{
|
|
AI_NavGoal_t goal(GOALTYPE_COVER, GetOuter()->m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE, AIN_DEF_FLAGS);
|
|
if ( GetNavigator()->SetGoal( goal, AIN_NO_PATH_TASK_FAIL ) )
|
|
{
|
|
TaskComplete();
|
|
m_bMovingToCover = true;
|
|
}
|
|
else
|
|
{
|
|
GetOuter()->TaskInterrupt();
|
|
}
|
|
break;
|
|
}
|
|
// Fall through...
|
|
}
|
|
|
|
case 2:
|
|
{
|
|
Assert( !m_bMovingToCover );
|
|
Vector vGoalPosition;
|
|
if ( HasFollowPoint() && IsFollowPointInRange() )
|
|
vGoalPosition = GetFollowPoint();
|
|
else
|
|
vGoalPosition = GetGoalPosition();
|
|
|
|
AI_NavGoal_t goal( GetGoalPosition(), AIN_DEF_ACTIVITY, GetGoalTolerance() );
|
|
goal.pTarget = m_hFollowTarget;
|
|
if ( !GetNavigator()->SetGoal( goal ) )
|
|
{
|
|
m_FollowDelay.Start( 2.0, 5.0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case TASK_FOLLOWER_FACE_TACTICAL:
|
|
case TASK_FACE_FOLLOW_TARGET:
|
|
{
|
|
ChainRunTask( TASK_FACE_REASONABLE );
|
|
break;
|
|
}
|
|
|
|
case TASK_MOVE_TO_FOLLOW_POSITION:
|
|
{
|
|
if ( m_hFollowTarget == NULL )
|
|
{
|
|
TaskFail(FAIL_NO_TARGET);
|
|
}
|
|
else
|
|
{
|
|
if ( m_bMovingToCover )
|
|
{
|
|
ChainRunTask( TASK_WAIT_FOR_MOVEMENT );
|
|
NoteSuccessfulFollow();
|
|
return;
|
|
}
|
|
|
|
// Re-evaluate when you think your finished, or the target has moved too far
|
|
if ( !UpdateFollowPosition() )
|
|
{
|
|
TaskFail(FAIL_NO_TARGET);
|
|
break;
|
|
}
|
|
|
|
if ( ShouldUseFollowPoints() && ai_follow_use_points_when_moving.GetBool() )
|
|
{
|
|
if ( HasFollowPoint() )
|
|
{
|
|
if ( !IsFollowPointInRange() )
|
|
{
|
|
ClearFollowPoint();
|
|
GetNavigator()->SetArrivalDirection( vec3_origin );
|
|
GetNavigator()->SetArrivalActivity( ACT_INVALID );
|
|
m_TimeBlockUseWaitPoint.Reset();
|
|
m_TimeCheckForWaitPoint.Reset();
|
|
}
|
|
}
|
|
if ( GetNavigator()->GetNavType() != NAV_JUMP && !HasFollowPoint() && m_pInterruptWaitPoint )
|
|
{
|
|
SetFollowPoint( m_pInterruptWaitPoint );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearFollowPoint();
|
|
if ( GetNavigator()->IsGoalActive() )
|
|
{
|
|
GetNavigator()->SetArrivalDirection( vec3_origin );
|
|
GetNavigator()->SetArrivalActivity( ACT_INVALID );
|
|
}
|
|
}
|
|
|
|
if ( !GetNavigator()->IsGoalActive() )
|
|
{
|
|
// What this probably means is that the navigation failed but within tolerance
|
|
// So for now, just call it good and block another attempt for a bit
|
|
TaskComplete();
|
|
if ( !IsFollowPointInRange() )
|
|
ClearFollowPoint();
|
|
if ( !IsFollowGoalInRange( m_FollowNavGoal.tolerance, GetGoalZRange(), GetGoalFlags() ) )
|
|
m_FollowDelay.Start( 0.25, 0.75 );
|
|
else
|
|
{
|
|
m_TargetMonitor.SetMark( GetFollowTarget(), m_FollowNavGoal.targetMoveTolerance );
|
|
m_bTargetUnreachable = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( !HasFollowPoint() )
|
|
{
|
|
float range = GetGoalRange();
|
|
|
|
Vector vVelocity =- GetFollowTarget()->GetSmoothedVelocity();
|
|
bool bDoSlowdown = ( vVelocity.LengthSqr() < Square(4*12) );
|
|
if ( bDoSlowdown )
|
|
{
|
|
range += GetMotor()->MinStoppingDist(12) - 12;
|
|
}
|
|
|
|
if ( IsFollowGoalInRange( range, GetGoalZRange(), GetGoalFlags() ) )
|
|
{
|
|
m_TimeBeforeSpreadFacing.Reset();
|
|
TaskComplete();
|
|
GetNavigator()->StopMoving( !bDoSlowdown ); // Stop moving
|
|
m_TargetMonitor.SetMark( GetFollowTarget(), m_FollowNavGoal.targetMoveTolerance );
|
|
break;
|
|
}
|
|
|
|
// Update the nav goal if needed
|
|
if ( (GetNavigator()->GetGoalPos() - GetGoalPosition()).LengthSqr() > Square( m_FollowNavGoal.repathOnRouteTolerance ) )
|
|
{
|
|
if ( GetNavigator()->GetNavType() != NAV_JUMP )
|
|
{
|
|
if ( !GetNavigator()->UpdateGoalPos( GetGoalPosition() ) )
|
|
{
|
|
TaskFail(FAIL_NO_ROUTE);
|
|
m_bTargetUnreachable = true;
|
|
break;
|
|
}
|
|
NoteSuccessfulFollow();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const Vector &vFollowPoint = GetFollowPoint();
|
|
if ( GetNavigator()->GetGoalPos() != vFollowPoint )
|
|
{
|
|
if ( !GetNavigator()->UpdateGoalPos( vFollowPoint ) )
|
|
{
|
|
TaskFail(FAIL_NO_ROUTE);
|
|
m_bTargetUnreachable = true;
|
|
break;
|
|
}
|
|
NoteSuccessfulFollow();
|
|
|
|
if ( !ShouldIgnoreFollowPointFacing() )
|
|
GetNavigator()->SetArrivalDirection( GetHintNode()->GetDirection() );
|
|
if ( GetHintNode()->HintActivityName() != NULL_STRING )
|
|
{
|
|
Activity hintActivity = (Activity)CAI_BaseNPC::GetActivityID( STRING(GetHintNode()->HintActivityName()) );
|
|
if ( hintActivity != ACT_INVALID )
|
|
{
|
|
GetNavigator()->SetArrivalActivity( GetOuter()->GetHintActivity(GetHintNode()->HintType(), hintActivity ) );
|
|
}
|
|
else
|
|
{
|
|
int iSequence = GetOuter()->LookupSequence(STRING(GetHintNode()->HintActivityName()));
|
|
if ( iSequence != ACT_INVALID )
|
|
{
|
|
GetNavigator()->SetArrivalSequence( iSequence );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the appropriate activity based on an overlapping range
|
|
// overlap the range to prevent oscillation
|
|
// BUGBUG: this is checking linear distance (ie. through walls) and not path distance or even visibility
|
|
|
|
// Never stop running once started
|
|
if ( m_CurrentFollowActivity != ACT_RUN )
|
|
{
|
|
float distToTargetSq = ( GetNavigator()->GetGoalPos() - GetLocalOrigin() ).Length2DSqr();
|
|
|
|
// Pick the right movement activity.
|
|
Activity followActivity = ( distToTargetSq < Square(m_FollowNavGoal.walkTolerance) && GetOuter()->GetState() != NPC_STATE_COMBAT ) ? ACT_WALK : ACT_RUN;
|
|
|
|
// If we're supposed to have LOS, run to catch up
|
|
if ( m_FollowNavGoal.flags & AIFF_REQUIRE_LOS_OUTSIDE_COMBAT )
|
|
{
|
|
if ( !GetOuter()->GetSenses()->DidSeeEntity( m_hFollowTarget ) )
|
|
{
|
|
followActivity = ACT_RUN;
|
|
}
|
|
}
|
|
|
|
if ( followActivity != m_CurrentFollowActivity )
|
|
{
|
|
m_CurrentFollowActivity = followActivity;
|
|
GetNavigator()->SetMovementActivity(followActivity);
|
|
}
|
|
}
|
|
|
|
if ( ( m_vFollowMoveAnchor - GetAbsOrigin() ).LengthSqr() > Square( 15.0 * 12.0 ) )
|
|
{
|
|
m_vFollowMoveAnchor = GetAbsOrigin();
|
|
NoteSuccessfulFollow();
|
|
}
|
|
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TASK_ARRIVE_AT_FOLLOW_POINT:
|
|
{
|
|
ChainRunTask( TASK_FACE_HINTNODE, 0 );
|
|
break;
|
|
}
|
|
|
|
case TASK_BEGIN_STAND_AT_WAIT_POINT:
|
|
{
|
|
ChainRunTask( TASK_FACE_HINTNODE, 0 );
|
|
break;
|
|
}
|
|
|
|
default:
|
|
BaseClass::RunTask( pTask );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::TaskComplete( bool fIgnoreSetFailedCondition )
|
|
{
|
|
const Task_t *pTask = GetCurTask();
|
|
if ( pTask->iTask == TASK_MOVE_TO_FOLLOW_POSITION || pTask->iTask == TASK_GET_PATH_TO_FOLLOW_POSITION )
|
|
NoteSuccessfulFollow();
|
|
BaseClass::TaskComplete( fIgnoreSetFailedCondition );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::BuildScheduleTestBits()
|
|
{
|
|
BaseClass::BuildScheduleTestBits();
|
|
bool bIsTakeCover = false;
|
|
bool bIsHideAndReload = false;
|
|
bool bIsReload = false;
|
|
bool bIgnoreMovedMark = false;
|
|
|
|
if ( ( GetOuter()->ConditionInterruptsCurSchedule( COND_GIVE_WAY ) ||
|
|
GetOuter()->ConditionInterruptsCurSchedule( COND_IDLE_INTERRUPT ) ||
|
|
( bIsHideAndReload = IsCurSchedule(SCHED_HIDE_AND_RELOAD ) ) == true ||
|
|
( bIsReload = IsCurSchedule(SCHED_RELOAD ) ) == true ||
|
|
IsCurSchedule(SCHED_STANDOFF ) ||
|
|
( bIsTakeCover = IsCurSchedule(SCHED_TAKE_COVER_FROM_ENEMY ) ) == true ||
|
|
IsCurSchedule(SCHED_COMBAT_FACE ) ||
|
|
IsCurSchedule(SCHED_ALERT_FACE ) ||
|
|
IsCurSchedule(SCHED_COMBAT_STAND ) ||
|
|
IsCurSchedule(SCHED_ALERT_STAND) ) ||
|
|
IsCurSchedule(SCHED_ALERT_FACE_BESTSOUND ) )
|
|
{
|
|
#ifdef HL2_EPISODIC
|
|
if( IsCurSchedule(SCHED_RELOAD, false) && GetOuter()->Classify() == CLASS_PLAYER_ALLY_VITAL )
|
|
{
|
|
// Alyx and Barney do not stop reloading because the player has moved.
|
|
// Citizens and other regular allies do.
|
|
bIgnoreMovedMark = true;
|
|
}
|
|
#endif//HL2_EPISODIC
|
|
|
|
if( !bIgnoreMovedMark )
|
|
{
|
|
GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_TARGET_MOVED_FROM_MARK ) );
|
|
}
|
|
|
|
if ( !bIsTakeCover && !bIsHideAndReload && !bIsReload )
|
|
GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_FOLLOW_DELAY_EXPIRED) );
|
|
}
|
|
|
|
// Add logic for NPCs not able to move and shoot
|
|
if ( hl2_episodic.GetBool() )
|
|
{
|
|
if ( IsCurScheduleFollowSchedule() && GetOuter()->ShouldMoveAndShoot() == false )
|
|
{
|
|
GetOuter()->SetCustomInterruptCondition( COND_CAN_RANGE_ATTACK1 );
|
|
}
|
|
|
|
#ifdef HL2_EPISODIC
|
|
// In Alyx darkness mode, break on the player turning their flashlight off
|
|
if ( HL2GameRules()->IsAlyxInDarknessMode() )
|
|
{
|
|
if ( IsCurSchedule(SCHED_FOLLOW, false) || IsCurSchedule(SCHED_MOVE_TO_FACE_FOLLOW_TARGET, false) ||
|
|
IsCurSchedule(SCHED_FACE_FOLLOW_TARGET, false) )
|
|
{
|
|
GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_FOLLOW_PLAYER_IS_NOT_LIT ) );
|
|
}
|
|
}
|
|
#endif // HL2_EPISODIC
|
|
}
|
|
|
|
if ( GetNpcState() == NPC_STATE_COMBAT && IsCurScheduleFollowSchedule() )
|
|
{
|
|
GetOuter()->ClearCustomInterruptCondition( COND_LIGHT_DAMAGE );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
Activity CAI_FollowBehavior::NPC_TranslateActivity( Activity activity )
|
|
{
|
|
if ( activity == ACT_IDLE && HasFollowPoint() && GetHintNode()->HintActivityName() != NULL_STRING )
|
|
{
|
|
return GetOuter()->GetHintActivity(GetHintNode()->HintType(), (Activity)CAI_BaseNPC::GetActivityID( STRING(GetHintNode()->HintActivityName()) ) );
|
|
}
|
|
return BaseClass::NPC_TranslateActivity( activity );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsCurScheduleFollowSchedule()
|
|
{
|
|
int curScheduleId = ( GetOuter()->GetCurSchedule() ) ? GetOuter()->GetCurSchedule()->GetId() : SCHED_NONE;
|
|
if ( curScheduleId >= GetClassScheduleIdSpace()->ScheduleLocalToGlobal( SCHED_FOLLOWER_MOVE_AWAY_FAIL ) &&
|
|
curScheduleId <= GetClassScheduleIdSpace()->ScheduleLocalToGlobal( SCHED_FOLLOWER_STAND_AT_WAIT_POINT ) )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsCurTaskContinuousMove()
|
|
{
|
|
const Task_t *pCurTask = GetCurTask();
|
|
if ( pCurTask && pCurTask->iTask == TASK_MOVE_TO_FOLLOW_POSITION )
|
|
return true;
|
|
return BaseClass::IsCurTaskContinuousMove();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::OnMovementFailed()
|
|
{
|
|
float acceptDist = m_FollowNavGoal.range;
|
|
if ( m_FollowNavGoal.tolerance > acceptDist )
|
|
acceptDist = m_FollowNavGoal.tolerance;
|
|
|
|
if ( GetNpcState() == NPC_STATE_COMBAT )
|
|
{
|
|
if ( m_FollowNavGoal.coverTolerance > acceptDist )
|
|
acceptDist = m_FollowNavGoal.coverTolerance;
|
|
if (m_FollowNavGoal.enemyLOSTolerance > acceptDist )
|
|
acceptDist = m_FollowNavGoal.enemyLOSTolerance;
|
|
}
|
|
|
|
float flZRange = GetGoalZRange();
|
|
if ( GetGoalZRange() == -1 )
|
|
{
|
|
flZRange = GetHullHeight() * 2;
|
|
}
|
|
|
|
if ( IsFollowGoalInRange( acceptDist * 1.5, flZRange, GetGoalFlags() ) )
|
|
m_bTargetUnreachable = true;
|
|
else
|
|
m_FollowDelay.Start();
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowBehavior::OnMovementComplete()
|
|
{
|
|
if ( !IsCurSchedule(SCHED_FOLLOWER_GO_TO_WAIT_POINT) )
|
|
m_TimeBeforeSpreadFacing.Reset();
|
|
else
|
|
{
|
|
m_TimeBeforeSpreadFacing.Force();
|
|
m_TimeNextSpreadFacing.Force();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::FValidateHintType( CAI_Hint *pHint )
|
|
{
|
|
if ( pHint->HintType() == HINT_FOLLOW_WAIT_POINT )
|
|
{
|
|
if ( GetFollowTarget() && GetFollowTarget()->FVisible( pHint->GetAbsOrigin() + Vector( 0, 0, 0.1 ) ) )
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
return BaseClass::FValidateHintType( pHint );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsValidCover( const Vector &vLocation, CAI_Hint const *pHint )
|
|
{
|
|
if ( (vLocation - m_FollowNavGoal.position).LengthSqr() > Square( m_FollowNavGoal.coverTolerance + 0.1 ) )
|
|
return false;
|
|
return BaseClass::IsValidCover( vLocation, pHint );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::IsValidShootPosition( const Vector &vLocation, CAI_Node *pNode, CAI_Hint const *pHint )
|
|
{
|
|
if ( (vLocation - m_FollowNavGoal.position).LengthSqr() > Square( m_FollowNavGoal.enemyLOSTolerance + 0.1 ) )
|
|
return false;
|
|
return BaseClass::IsValidShootPosition( vLocation, pNode, pHint );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowBehavior::ShouldAlwaysThink()
|
|
{
|
|
return ( m_hFollowTarget && m_hFollowTarget->IsPlayer() );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// CAI_FollowGoal
|
|
//
|
|
// Purpose: A level tool to control the follow behavior. Use is not required
|
|
// in order to use behavior.
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
BEGIN_DATADESC( CAI_FollowGoal )
|
|
DEFINE_KEYFIELD( m_iFormation, FIELD_INTEGER, "Formation" ),
|
|
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ),
|
|
END_DATADESC()
|
|
|
|
//-------------------------------------
|
|
|
|
LINK_ENTITY_TO_CLASS( ai_goal_follow, CAI_FollowGoal );
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowGoal::EnableGoal( CAI_BaseNPC *pAI )
|
|
{
|
|
CAI_FollowBehavior *pBehavior;
|
|
if ( !pAI->GetBehavior( &pBehavior ) )
|
|
return;
|
|
|
|
CBaseEntity *pGoalEntity = GetGoalEntity();
|
|
if ( !pGoalEntity && AI_IsSinglePlayer() )
|
|
{
|
|
if ( pAI->IRelationType(UTIL_GetLocalPlayer()) == D_LI )
|
|
{
|
|
pGoalEntity = UTIL_GetLocalPlayer();
|
|
SetGoalEntity( pGoalEntity );
|
|
}
|
|
}
|
|
|
|
if ( pGoalEntity )
|
|
pBehavior->SetFollowGoal( this );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowGoal::DisableGoal( CAI_BaseNPC *pAI )
|
|
{
|
|
CAI_FollowBehavior *pBehavior;
|
|
if ( !pAI || !pAI->GetBehavior( &pBehavior ) )
|
|
return;
|
|
|
|
pBehavior->ClearFollowGoal( this );
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowGoal::InputOutsideTransition( inputdata_t &inputdata )
|
|
{
|
|
EnterDormant();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// CAI_FollowManager
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-------------------------------------
|
|
//
|
|
// Purpose: Formation definitions
|
|
//
|
|
|
|
// @TODO (toml 11-21-03): rework follow so we don't have to have class specifc formations in this file
|
|
|
|
struct AI_FollowSlot_t
|
|
{
|
|
int priority;
|
|
|
|
TableVector position;
|
|
float positionVariability;
|
|
|
|
float rangeMin;
|
|
float rangeMax;
|
|
|
|
float Zrange;
|
|
|
|
float tolerance;
|
|
|
|
// @Q (toml 02-28-03): facing?
|
|
};
|
|
|
|
struct AI_FollowFormation_t
|
|
{
|
|
const char * pszName;
|
|
unsigned flags;
|
|
int nSlots;
|
|
|
|
// Range within which can exit formation to seek a follow point
|
|
float followPointTolerance;
|
|
|
|
// Distance target must move to reset formation
|
|
float targetMoveTolerance;
|
|
|
|
// Distance from current move goal target must move to force a repathfind
|
|
float repathOnRouteTolerance;
|
|
|
|
// Distance from target within which should walk, not run to formation
|
|
float walkTolerance;
|
|
|
|
// Distance within which can exit formation to seek cover
|
|
float coverTolerance;
|
|
|
|
// Distance within which can exit formation to seek LOS to enemy
|
|
float enemyLOSTolerance;
|
|
|
|
// Distance within which can exit formation to chase enemy
|
|
float chaseEnemyTolerance;
|
|
|
|
AI_FollowSlot_t * pSlots;
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
static AI_FollowSlot_t g_SimpleFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 96, 120, -1, 128 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_SimpleFollowFormation =
|
|
{
|
|
"Simple",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_SimpleFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
36, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
190, // walkTolerance
|
|
300, // coverTolerance
|
|
300, // enemyLOSTolerance
|
|
300, // chaseEnemyTolerance
|
|
g_SimpleFollowFormationSlots,
|
|
};
|
|
|
|
|
|
//-------------------------------------
|
|
|
|
static AI_FollowSlot_t g_WideFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 240, -1, 128 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_WideFollowFormation =
|
|
{
|
|
"Wide",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_WideFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
72, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
190, // walkTolerance
|
|
600, // coverTolerance
|
|
600, // enemyLOSTolerance
|
|
600, // chaseEnemyTolerance
|
|
g_WideFollowFormationSlots,
|
|
};
|
|
|
|
//---------------------------------------------
|
|
// Antlion use very loose following criteria
|
|
|
|
static AI_FollowSlot_t g_AntlionFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 150, 250, -1, 128 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_AntlionFollowFormation =
|
|
{
|
|
"Antlion",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_AntlionFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
36, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
190, // walkTolerance
|
|
1024, // coverTolerance
|
|
1024, // enemyLOSTolerance
|
|
1024, // chaseEnemyTolerance
|
|
g_AntlionFollowFormationSlots,
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
#define COMMANDER_TOLERANCE (13.0 * 1.415)
|
|
|
|
static AI_FollowSlot_t g_CommanderFollowFormationSlots[] =
|
|
{
|
|
{ 2, { 0, 0, 0 }, 0, COMMANDER_TOLERANCE, COMMANDER_TOLERANCE, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, COMMANDER_TOLERANCE, COMMANDER_TOLERANCE, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, COMMANDER_TOLERANCE, COMMANDER_TOLERANCE, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, COMMANDER_TOLERANCE, COMMANDER_TOLERANCE, -1, 48 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_CommanderFollowFormation =
|
|
{
|
|
"Commander",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_CommanderFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
6, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
12, // walkTolerance
|
|
300, // coverTolerance
|
|
300, // enemyLOSTolerance
|
|
300, // chaseEnemyTolerance
|
|
g_CommanderFollowFormationSlots,
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
static AI_FollowSlot_t g_TightFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 0, 0, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, 0, 0, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, 0, 0, -1, 48 },
|
|
{ 1, { 0, 0, 0 }, 0, 0, 0, -1, 48 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_TightFollowFormation =
|
|
{
|
|
"Tight",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_CommanderFollowFormationSlots),
|
|
48, // followPointTolerance
|
|
6, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
12, // walkTolerance
|
|
300, // coverTolerance
|
|
32, // enemyLOSTolerance
|
|
32, // chaseEnemyTolerance
|
|
g_TightFollowFormationSlots,
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
static AI_FollowSlot_t g_MediumFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 156, 156, -1, 128 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_MediumFollowFormation =
|
|
{
|
|
"Medium",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS,
|
|
ARRAYSIZE(g_MediumFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
36, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
190, // walkTolerance
|
|
300, // coverTolerance
|
|
300, // enemyLOSTolerance
|
|
300, // chaseEnemyTolerance
|
|
g_MediumFollowFormationSlots,
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
static AI_FollowSlot_t g_SidekickFollowFormationSlots[] =
|
|
{
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
{ 1, { 0, 0, 0 }, 0, 120, 160, 256, 128 },
|
|
};
|
|
|
|
static AI_FollowFormation_t g_SidekickFollowFormation =
|
|
{
|
|
"Sidekick",
|
|
AIFF_DEFAULT | AIFF_USE_FOLLOW_POINTS | AIFF_REQUIRE_LOS_OUTSIDE_COMBAT,
|
|
ARRAYSIZE(g_SidekickFollowFormationSlots),
|
|
168, // followPointTolerance
|
|
36, // targetMoveTolerance
|
|
60, // repathOnRouteTolerance
|
|
190, // walkTolerance
|
|
300, // coverTolerance
|
|
300, // enemyLOSTolerance
|
|
300, // chaseEnemyTolerance
|
|
g_SidekickFollowFormationSlots,
|
|
};
|
|
|
|
//-------------------------------------
|
|
|
|
AI_FollowFormation_t *g_AI_Formations[] =
|
|
{
|
|
&g_SimpleFollowFormation,
|
|
&g_WideFollowFormation,
|
|
&g_AntlionFollowFormation,
|
|
&g_CommanderFollowFormation,
|
|
&g_TightFollowFormation,
|
|
&g_MediumFollowFormation,
|
|
&g_SidekickFollowFormation
|
|
};
|
|
|
|
AI_FollowFormation_t *AIGetFormation( AI_Formations_t formation )
|
|
{
|
|
if ( formation < 0 )
|
|
formation = (AI_Formations_t)0;
|
|
else if ( static_cast<size_t>(formation) >= ARRAYSIZE( g_AI_Formations ) )
|
|
formation = (AI_Formations_t)(ARRAYSIZE( g_AI_Formations ) - 1 );
|
|
|
|
return g_AI_Formations[formation];
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
|
|
bool CAI_FollowManager::AddFollower( CBaseEntity *pTarget, CAI_BaseNPC *pFollower, AI_Formations_t formation, AI_FollowManagerInfoHandle_t *pHandle )
|
|
{
|
|
AI_FollowGroup_t *pGroup = FindCreateGroup( pTarget, formation );
|
|
int slot = FindBestSlot( pGroup );
|
|
|
|
if ( slot != -1 )
|
|
{
|
|
MEM_ALLOC_CREDIT();
|
|
|
|
AI_FollowSlot_t *pSlot = &pGroup->pFormation->pSlots[slot];
|
|
|
|
int i = pGroup->followers.AddToTail( );
|
|
|
|
AI_Follower_t *iterNode = &pGroup->followers[i];
|
|
iterNode->hFollower = pFollower;
|
|
iterNode->slot = slot;
|
|
iterNode->pGroup = pGroup;
|
|
|
|
pGroup->slotUsage.SetBit( slot );
|
|
|
|
CalculateFieldsFromSlot( pSlot, &iterNode->navInfo );
|
|
|
|
pHandle->m_hFollower = i;
|
|
pHandle->m_pGroup = pGroup;
|
|
return true;
|
|
}
|
|
|
|
pHandle->m_hFollower = 0;
|
|
pHandle->m_pGroup = NULL;
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowManager::CalcFollowPosition( AI_FollowManagerInfoHandle_t& hInfo, AI_FollowNavInfo_t *pNavInfo )
|
|
{
|
|
if ( hInfo.m_pGroup && hInfo.m_hFollower )
|
|
{
|
|
AI_FollowGroup_t *pGroup = hInfo.m_pGroup;
|
|
Assert( pGroup->hFollowTarget.Get() );
|
|
CBaseEntity *pTarget = pGroup->hFollowTarget;
|
|
|
|
AI_Follower_t *iterNode = &pGroup->followers[hInfo.m_hFollower];
|
|
if ( iterNode->navInfo.position != vec3_origin )
|
|
{
|
|
QAngle angles = pTarget->GetLocalAngles();
|
|
angles.x = angles.z = 0;
|
|
|
|
matrix3x4_t fRotateMatrix;
|
|
AngleMatrix(angles, fRotateMatrix);
|
|
|
|
VectorRotate( iterNode->navInfo.position, fRotateMatrix, pNavInfo->position);
|
|
pNavInfo->position += pTarget->WorldSpaceCenter();
|
|
}
|
|
else
|
|
{
|
|
pNavInfo->position = iterNode->navInfo.position + pTarget->WorldSpaceCenter();
|
|
}
|
|
|
|
pNavInfo->tolerance = iterNode->navInfo.tolerance;
|
|
pNavInfo->range = iterNode->navInfo.range;
|
|
pNavInfo->Zrange = iterNode->navInfo.Zrange;
|
|
pNavInfo->flags = pGroup->pFormation->flags;
|
|
pNavInfo->followPointTolerance = pGroup->pFormation->followPointTolerance;
|
|
pNavInfo->targetMoveTolerance = pGroup->pFormation->targetMoveTolerance;
|
|
pNavInfo->repathOnRouteTolerance = pGroup->pFormation->repathOnRouteTolerance;
|
|
pNavInfo->walkTolerance = pGroup->pFormation->walkTolerance;
|
|
pNavInfo->coverTolerance = pGroup->pFormation->coverTolerance;
|
|
pNavInfo->enemyLOSTolerance = pGroup->pFormation->enemyLOSTolerance;
|
|
pNavInfo->chaseEnemyTolerance = pGroup->pFormation->chaseEnemyTolerance;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
bool CAI_FollowManager::RedistributeSlots( AI_FollowGroup_t *pGroup )
|
|
{
|
|
bool result = false;
|
|
|
|
CUtlRBTree<CBaseEntity *> movedFollowers;
|
|
SetDefLessFunc( movedFollowers );
|
|
|
|
const Vector &originFollowed = pGroup->hFollowTarget->GetAbsOrigin();
|
|
int bestSlot;
|
|
|
|
while ( ( bestSlot = FindBestSlot( pGroup ) ) != -1 && ((int)movedFollowers.Count() < pGroup->followers.Count()) )
|
|
{
|
|
AI_FollowSlot_t * pSlot = &pGroup->pFormation->pSlots[bestSlot];
|
|
Vector slotPos = originFollowed + pSlot->position;
|
|
int h = pGroup->followers.Head();
|
|
int hBest = pGroup->followers.InvalidIndex();
|
|
float distSqBest = FLT_MAX;
|
|
|
|
while ( h != pGroup->followers.InvalidIndex() )
|
|
{
|
|
AI_Follower_t *p = &pGroup->followers[h];
|
|
|
|
if ( movedFollowers.Find( p->hFollower ) == movedFollowers.InvalidIndex() &&
|
|
( p->slot == -1 || pSlot->priority > pGroup->pFormation->pSlots[p->slot].priority ) )
|
|
{
|
|
float distSqCur = ( p->hFollower->GetAbsOrigin() - slotPos ).LengthSqr();
|
|
if ( distSqCur < distSqBest )
|
|
{
|
|
hBest = h;
|
|
}
|
|
}
|
|
|
|
h = pGroup->followers.Next( h );
|
|
}
|
|
|
|
if ( hBest == pGroup->followers.InvalidIndex() )
|
|
break;
|
|
|
|
AI_Follower_t *pBest = &pGroup->followers[hBest];
|
|
if ( pBest->slot != -1 )
|
|
{
|
|
pGroup->slotUsage.ClearBit( pBest->slot );
|
|
}
|
|
pBest->slot = bestSlot;
|
|
CalculateFieldsFromSlot( pSlot, &pBest->navInfo );
|
|
pGroup->slotUsage.SetBit( bestSlot );
|
|
movedFollowers.Insert( pBest->hFollower );
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowManager::ChangeFormation( AI_FollowManagerInfoHandle_t& hInfo, AI_Formations_t formation )
|
|
{
|
|
if ( !hInfo.m_pGroup || !hInfo.m_hFollower )
|
|
return;
|
|
|
|
AI_FollowGroup_t *pGroup = hInfo.m_pGroup;
|
|
AI_FollowFormation_t *pNewFormation = AIGetFormation( formation );
|
|
if ( pNewFormation == pGroup->pFormation )
|
|
return;
|
|
|
|
int h = pGroup->followers.Head();
|
|
|
|
while ( h != pGroup->followers.InvalidIndex() )
|
|
{
|
|
CAI_FollowBehavior *pFollowBehavior;
|
|
|
|
AI_Follower_t *p = &pGroup->followers[h];
|
|
p->slot = -1;
|
|
p->hFollower->GetBehavior( &pFollowBehavior );
|
|
Assert( pFollowBehavior );
|
|
if ( pFollowBehavior )
|
|
{
|
|
pFollowBehavior->m_params.formation = formation;
|
|
pFollowBehavior->m_TargetMonitor.ClearMark();
|
|
pFollowBehavior->SetCondition( CAI_FollowBehavior::COND_TARGET_MOVED_FROM_MARK );
|
|
pFollowBehavior->m_bTargetUnreachable = false;
|
|
}
|
|
|
|
h = pGroup->followers.Next( h );
|
|
}
|
|
|
|
pGroup->slotUsage.ClearAllBits();
|
|
pGroup->pFormation = pNewFormation;
|
|
pGroup->slotUsage.Resize( pGroup->pFormation->nSlots );
|
|
|
|
RedistributeSlots( pGroup );
|
|
|
|
#ifdef DEBUG
|
|
h = pGroup->followers.Head();
|
|
while ( h != pGroup->followers.InvalidIndex() )
|
|
{
|
|
AI_Follower_t *p = &pGroup->followers[h];
|
|
Assert( p->slot != -1 );
|
|
h = pGroup->followers.Next( h );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowManager::RemoveFollower( AI_FollowManagerInfoHandle_t& hInfo )
|
|
{
|
|
if ( hInfo.m_pGroup && hInfo.m_hFollower )
|
|
{
|
|
AI_FollowGroup_t *pGroup = hInfo.m_pGroup;
|
|
AI_Follower_t* iterNode = &pGroup->followers[hInfo.m_hFollower];
|
|
|
|
int slot = iterNode->slot;
|
|
pGroup->slotUsage.ClearBit( slot );
|
|
pGroup->followers.Remove( hInfo.m_hFollower );
|
|
if ( pGroup->followers.Count() == 0 )
|
|
{
|
|
RemoveGroup( pGroup );
|
|
}
|
|
else
|
|
{
|
|
if ( pGroup->hFollowTarget != NULL ) // NULL on level unload
|
|
{
|
|
RedistributeSlots( pGroup );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
int CAI_FollowManager::FindBestSlot( AI_FollowGroup_t *pGroup )
|
|
{
|
|
// @TODO (toml 02-28-03): crude placeholder
|
|
int nSlots = pGroup->pFormation->nSlots;
|
|
|
|
int best = -1;
|
|
int bestPriority = -1;
|
|
|
|
for ( int i = 0; i < nSlots; i++ )
|
|
{
|
|
if ( !pGroup->slotUsage.GetBit( i ) && pGroup->pFormation->pSlots[i].priority > bestPriority )
|
|
{
|
|
bestPriority = pGroup->pFormation->pSlots[i].priority;
|
|
best = i;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowManager::CalculateFieldsFromSlot( AI_FollowSlot_t *pSlot, AI_FollowNavInfo_t *pFollowerInfo )
|
|
{
|
|
// @TODO (toml 02-28-03): placeholder. Force break if someone tries to actually use
|
|
Assert( pSlot->positionVariability == 0.0 );
|
|
//Assert( pSlot->tolerance == AIN_DEF_TOLERANCE );
|
|
|
|
pFollowerInfo->position = pSlot->position;
|
|
pFollowerInfo->range = random->RandomFloat( pSlot->rangeMin, pSlot->rangeMax );
|
|
pFollowerInfo->Zrange = pSlot->Zrange;
|
|
pFollowerInfo->tolerance = pSlot->tolerance;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
AI_FollowGroup_t *CAI_FollowManager::FindCreateGroup( CBaseEntity *pTarget, AI_Formations_t formation )
|
|
{
|
|
AI_FollowGroup_t *pGroup = FindGroup( pTarget );
|
|
|
|
if ( !pGroup )
|
|
{
|
|
{
|
|
MEM_ALLOC_CREDIT();
|
|
pGroup = new AI_FollowGroup_t;
|
|
}
|
|
|
|
pGroup->pFormation = AIGetFormation( formation );
|
|
pGroup->slotUsage.Resize( pGroup->pFormation->nSlots );
|
|
pGroup->hFollowTarget = pTarget;
|
|
|
|
m_groups.AddToHead( pGroup );
|
|
}
|
|
|
|
return pGroup;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
void CAI_FollowManager::RemoveGroup( AI_FollowGroup_t *pGroup )
|
|
{
|
|
for ( int i = 0; i < m_groups.Count(); i++ )
|
|
{
|
|
if ( m_groups[i] == pGroup )
|
|
{
|
|
delete m_groups[i];
|
|
m_groups.FastRemove(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
AI_FollowGroup_t *CAI_FollowManager::FindGroup( CBaseEntity *pTarget )
|
|
{
|
|
for ( int i = 0; i < m_groups.Count(); i++ )
|
|
{
|
|
if ( m_groups[i]->hFollowTarget == pTarget )
|
|
return m_groups[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//-------------------------------------
|
|
|
|
AI_FollowGroup_t *CAI_FollowManager::FindFollowerGroup( CBaseEntity *pFollower )
|
|
{
|
|
for ( int i = 0; i < m_groups.Count(); i++ )
|
|
{
|
|
int h = m_groups[i]->followers.Head();
|
|
while( h != m_groups[i]->followers.InvalidIndex() )
|
|
{
|
|
AI_Follower_t *p = &m_groups[i]->followers[h];
|
|
if ( p->hFollower.Get() == pFollower )
|
|
return m_groups[i];
|
|
h = m_groups[i]->followers.Next( h );
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER(CAI_FollowBehavior)
|
|
|
|
DECLARE_TASK(TASK_CANT_FOLLOW)
|
|
DECLARE_TASK(TASK_FACE_FOLLOW_TARGET)
|
|
DECLARE_TASK(TASK_MOVE_TO_FOLLOW_POSITION)
|
|
DECLARE_TASK(TASK_GET_PATH_TO_FOLLOW_POSITION)
|
|
DECLARE_TASK(TASK_SET_FOLLOW_TARGET_MARK)
|
|
DECLARE_TASK(TASK_FOLLOWER_FACE_TACTICAL)
|
|
DECLARE_TASK(TASK_SET_FOLLOW_DELAY)
|
|
DECLARE_TASK(TASK_GET_PATH_TO_FOLLOW_POINT)
|
|
DECLARE_TASK(TASK_ARRIVE_AT_FOLLOW_POINT)
|
|
DECLARE_TASK(TASK_BEGIN_STAND_AT_WAIT_POINT)
|
|
DECLARE_TASK(TASK_SET_FOLLOW_POINT_STAND_SCHEDULE)
|
|
|
|
DECLARE_CONDITION(COND_TARGET_MOVED_FROM_MARK)
|
|
DECLARE_CONDITION(COND_FOUND_WAIT_POINT)
|
|
DECLARE_CONDITION(COND_FOLLOW_DELAY_EXPIRED)
|
|
DECLARE_CONDITION(COND_FOLLOW_TARGET_VISIBLE)
|
|
DECLARE_CONDITION(COND_FOLLOW_TARGET_NOT_VISIBLE)
|
|
DECLARE_CONDITION(COND_FOLLOW_WAIT_POINT_INVALID)
|
|
DECLARE_CONDITION(COND_FOLLOW_PLAYER_IS_LIT)
|
|
DECLARE_CONDITION(COND_FOLLOW_PLAYER_IS_NOT_LIT)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOWER_MOVE_AWAY_END
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_MOVE_AWAY_END,
|
|
|
|
" Tasks"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FOLLOWER_MOVE_AWAY_FAIL "
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_FACE_FOLLOW_TARGET 0"
|
|
" TASK_SET_FOLLOW_DELAY 2"
|
|
""
|
|
" Interrupts"
|
|
" COND_PLAYER_PUSHING"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOWER_MOVE_AWAY_FAIL
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_MOVE_AWAY_FAIL,
|
|
|
|
" Tasks"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_FACE_FOLLOW_TARGET 0"
|
|
" TASK_SET_FOLLOW_DELAY 2"
|
|
""
|
|
" Interrupts"
|
|
" COND_PLAYER_PUSHING"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOW
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOW,
|
|
|
|
" Tasks"
|
|
" TASK_GET_PATH_TO_FOLLOW_POSITION 0"
|
|
" TASK_MOVE_TO_FOLLOW_POSITION 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 0"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_TARGET_FACE "
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_HEAR_DANGER"
|
|
" COND_PROVOKED"
|
|
" COND_PLAYER_PUSHING"
|
|
" COND_BETTER_WEAPON_AVAILABLE"
|
|
);
|
|
|
|
//=========================================================
|
|
// > SCHED_MOVE_TO_FACE_FOLLOW_TARGET
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_MOVE_TO_FACE_FOLLOW_TARGET,
|
|
|
|
" Tasks"
|
|
// " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
// " TASK_FACE_FOLLOW_TARGET 0"
|
|
// " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_FOLLOW"
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_HEAR_DANGER"
|
|
" COND_PROVOKED"
|
|
" COND_PLAYER_PUSHING"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FACE_FOLLOW_TARGET
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FACE_FOLLOW_TARGET,
|
|
|
|
" Tasks"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
" TASK_FACE_FOLLOW_TARGET 0"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_FOLLOWER_IDLE_STAND "
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_HEAR_DANGER"
|
|
" COND_PROVOKED"
|
|
" COND_PLAYER_PUSHING"
|
|
" COND_GIVE_WAY"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOWER_GO_TO_WAIT_POINT
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_GO_TO_WAIT_POINT,
|
|
|
|
" Tasks"
|
|
" TASK_LOCK_HINTNODE 0 " // this will fail the schedule if no hint node or not already lockable
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FOLLOWER_GO_TO_WAIT_POINT_FAIL"
|
|
" TASK_SET_TOLERANCE_DISTANCE 4"
|
|
" TASK_GET_PATH_TO_FOLLOW_POINT 0"
|
|
" TASK_SET_FOLLOW_TARGET_MARK 0"
|
|
" TASK_WALK_PATH 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 0"
|
|
" TASK_ARRIVE_AT_FOLLOW_POINT 0"
|
|
" TASK_SET_FOLLOW_POINT_STAND_SCHEDULE 0"
|
|
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_HEAR_DANGER"
|
|
" COND_PROVOKED"
|
|
" COND_PLAYER_PUSHING"
|
|
" COND_TARGET_MOVED_FROM_MARK"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOWER_GO_TO_WAIT_POINT_FAIL
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_GO_TO_WAIT_POINT_FAIL,
|
|
|
|
" Tasks"
|
|
" TASK_CLEAR_HINTNODE .5"
|
|
" TASK_SET_FOLLOW_DELAY 1"
|
|
""
|
|
" Interrupts"
|
|
)
|
|
|
|
//=========================================================
|
|
// > SCHED_FOLLOWER_STAND_AT_WAIT_POINT
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_STAND_AT_WAIT_POINT,
|
|
|
|
" Tasks"
|
|
" TASK_BEGIN_STAND_AT_WAIT_POINT 0"
|
|
" TASK_PLAY_HINT_ACTIVITY 0"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_FOLLOWER_STAND_AT_WAIT_POINT "
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_HEAR_DANGER"
|
|
" COND_PROVOKED"
|
|
" COND_PLAYER_PUSHING"
|
|
" COND_TARGET_MOVED_FROM_MARK"
|
|
" COND_GIVE_WAY"
|
|
" COND_FOLLOW_WAIT_POINT_INVALID"
|
|
// " COND_IDLE_INTERRUPT"
|
|
)
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_FOLLOWER_IDLE_STAND,
|
|
|
|
" Tasks"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
// " TASK_SET_FOLLOW_TARGET_MARK 0"
|
|
" TASK_WAIT 2.5"
|
|
" TASK_FACE_FOLLOW_TARGET 0"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
|
|
" TASK_WAIT 3"
|
|
""
|
|
" Interrupts"
|
|
" COND_NEW_ENEMY"
|
|
" COND_SEE_FEAR"
|
|
" COND_CAN_RANGE_ATTACK1"
|
|
" COND_NO_PRIMARY_AMMO"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_SMELL"
|
|
" COND_PROVOKED"
|
|
" COND_GIVE_WAY"
|
|
" COND_HEAR_DANGER"
|
|
" COND_HEAR_COMBAT"
|
|
" COND_HEAR_BULLET_IMPACT"
|
|
" COND_PLAYER_PUSHING"
|
|
" COND_TARGET_MOVED_FROM_MARK"
|
|
" COND_FOLLOW_DELAY_EXPIRED"
|
|
" COND_FOUND_WAIT_POINT"
|
|
" COND_IDLE_INTERRUPT"
|
|
" COND_BETTER_WEAPON_AVAILABLE"
|
|
)
|
|
|
|
AI_END_CUSTOM_SCHEDULE_PROVIDER()
|
|
|
|
//=============================================================================
|