csgo-2018-source/game/server/ai_basenpc_schedule.cpp
2021-07-24 21:11:47 -07:00

4876 lines
120 KiB
C++

//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Functions and data pertaining to the NPCs' AI scheduling system.
// Implements default NPC tasks and schedules.
//
//=============================================================================//
#include "cbase.h"
#include "ai_default.h"
#include "animation.h"
#include "scripted.h"
#include "soundent.h"
#include "entitylist.h"
#include "basecombatweapon.h"
#include "bitstring.h"
#include "ai_task.h"
#include "ai_network.h"
#include "ai_schedule.h"
#include "ai_hull.h"
#include "ai_node.h"
#include "ai_motor.h"
#include "ai_hint.h"
#include "ai_memory.h"
#include "ai_navigator.h"
#include "ai_tacticalservices.h"
#include "ai_moveprobe.h"
#include "ai_squadslot.h"
#include "ai_squad.h"
#include "ai_speech.h"
#include "game.h"
#include "IEffects.h"
#include "vstdlib/random.h"
#include "ndebugoverlay.h"
#include "env_debughistory.h"
#include "ai_behavior.h"
#include "global_event_log.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
extern ConVar ai_task_pre_script;
extern ConVar ai_use_efficiency;
extern ConVar ai_use_think_optimizations;
#define ShouldUseEfficiency() ( ai_use_think_optimizations.GetBool() && ai_use_efficiency.GetBool() )
ConVar ai_simulate_task_overtime( "ai_simulate_task_overtime", "0" );
#define MAX_TASKS_RUN 10
struct TaskTimings
{
const char *pszTask;
CFastTimer selectSchedule;
CFastTimer startTimer;
CFastTimer runTimer;
};
TaskTimings g_AITaskTimings[MAX_TASKS_RUN];
int g_nAITasksRun;
void CAI_BaseNPC::DumpTaskTimings()
{
DevMsg(" Tasks timings:\n" );
for ( int i = 0; i < g_nAITasksRun; ++i )
{
DevMsg( " %32s -- select %5.2f, start %5.2f, run %5.2f\n", g_AITaskTimings[i].pszTask,
g_AITaskTimings[i].selectSchedule.GetDuration().GetMillisecondsF(),
g_AITaskTimings[i].startTimer.GetDuration().GetMillisecondsF(),
g_AITaskTimings[i].runTimer.GetDuration().GetMillisecondsF() );
}
}
//=========================================================
// FHaveSchedule - Returns true if NPC's GetCurSchedule()
// is anything other than NULL.
//=========================================================
bool CAI_BaseNPC::FHaveSchedule( void )
{
if ( GetCurSchedule() == NULL )
{
return false;
}
return true;
}
//=========================================================
// ClearSchedule - blanks out the caller's schedule pointer
// and index.
//=========================================================
void CAI_BaseNPC::ClearSchedule( const char *szReason )
{
if (szReason && m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg( this, AIMF_IGNORE_SELECTED, " Schedule cleared: %s\n", szReason );
}
if ( szReason )
{
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs( "%s(%d): Schedule cleared: %s\n", GetDebugName(), entindex(), szReason ) );
}
m_ScheduleState.timeCurTaskStarted = m_ScheduleState.timeStarted = 0;
m_ScheduleState.bScheduleWasInterrupted = true;
SetTaskStatus( TASKSTATUS_NEW );
m_IdealSchedule = SCHED_NONE;
m_pSchedule = NULL;
ResetScheduleCurTaskIndex();
m_InverseIgnoreConditions.SetAll();
}
//=========================================================
// FScheduleDone - Returns true if the caller is on the
// last task in the schedule
//=========================================================
bool CAI_BaseNPC::FScheduleDone ( void )
{
Assert( GetCurSchedule() != NULL );
if ( GetScheduleCurTaskIndex() == GetCurSchedule()->NumTasks() )
{
return true;
}
return false;
}
//=========================================================
bool CAI_BaseNPC::SetSchedule( int localScheduleID )
{
CAI_Schedule *pNewSchedule = GetScheduleOfType( localScheduleID );
if ( pNewSchedule )
{
// ken: I'm don't know of any remaining cases, but if you find one, hunt it down as to why the schedule is getting slammed while they're in the middle of script
if (m_hCine != NULL)
{
if (!(localScheduleID == SCHED_SLEEP || localScheduleID == SCHED_WAIT_FOR_SCRIPT || localScheduleID == SCHED_SCRIPTED_WALK || localScheduleID == SCHED_SCRIPTED_RUN || localScheduleID == SCHED_SCRIPTED_CUSTOM_MOVE || localScheduleID == SCHED_SCRIPTED_WAIT || localScheduleID == SCHED_SCRIPTED_FACE) )
{
Assert( 0 );
// ExitScriptedSequence();
}
}
m_IdealSchedule = GetGlobalScheduleId( localScheduleID );
SetSchedule( pNewSchedule );
return true;
}
return false;
}
//=========================================================
// SetSchedule - replaces the NPC's schedule pointer
// with the passed pointer, and sets the ScheduleIndex back
// to 0
//=========================================================
#define SCHEDULE_HISTORY_SIZE 10
void CAI_BaseNPC::SetSchedule( CAI_Schedule *pNewSchedule )
{
Assert( pNewSchedule != NULL );
OnSetSchedule();
m_ScheduleState.timeCurTaskStarted = m_ScheduleState.timeStarted = gpGlobals->curtime;
m_ScheduleState.bScheduleWasInterrupted = false;
m_pSchedule = pNewSchedule ;
ResetScheduleCurTaskIndex();
SetTaskStatus( TASKSTATUS_NEW );
m_failSchedule = SCHED_NONE;
bool bCondInPVS = HasCondition( COND_IN_PVS );
m_Conditions.ClearAll();
if ( bCondInPVS )
SetCondition( COND_IN_PVS );
m_bConditionsGathered = false;
GetNavigator()->ClearGoal();
m_InverseIgnoreConditions.SetAll();
Forget( bits_MEMORY_TURNING );
/*
#if _DEBUG
if ( !ScheduleFromName( pNewSchedule->GetName() ) )
{
DevMsg( "Schedule %s not in table!!!\n", pNewSchedule->GetName() );
}
#endif
*/
// this is very useful code if you can isolate a test case in a level with a single NPC. It will notify
// you of every schedule selection the NPC makes.
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg(this, AIMF_IGNORE_SELECTED, "Schedule: %s (time: %.2f)\n", pNewSchedule->GetName(), gpGlobals->curtime );
}
if ( m_pEvent != NULL )
{
if ( m_pScheduleEvent != NULL )
{
GlobalEventLog.RemoveEvent( m_pScheduleEvent );
}
m_pScheduleEvent = GlobalEventLog.CreateEvent( "Schedule", false, m_pEvent );
GlobalEventLog.AddKeyValue( m_pScheduleEvent, false, "Schedule", pNewSchedule->GetName() );
}
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): Schedule: %s (time: %.2f)\n", GetDebugName(), entindex(), pNewSchedule->GetName(), gpGlobals->curtime ) );
#ifdef AI_MONITOR_FOR_OSCILLATION
if( m_bSelected )
{
AIScheduleChoice_t choice;
choice.m_flTimeSelected = gpGlobals->curtime;
choice.m_pScheduleSelected = pNewSchedule;
m_ScheduleHistory.AddToHead(choice);
if( m_ScheduleHistory.Count() > SCHEDULE_HISTORY_SIZE )
{
m_ScheduleHistory.Remove( SCHEDULE_HISTORY_SIZE );
}
assert( m_ScheduleHistory.Count() <= SCHEDULE_HISTORY_SIZE );
// No analysis until the vector is full!
if( m_ScheduleHistory.Count() == SCHEDULE_HISTORY_SIZE )
{
int iNumSelections = m_ScheduleHistory.Count();
float flTimeSpan = m_ScheduleHistory.Head().m_flTimeSelected - m_ScheduleHistory.Tail().m_flTimeSelected;
float flSelectionsPerSecond = ((float)iNumSelections) / flTimeSpan;
Msg( "%d selections in %f seconds (avg. %f selections per second)\n", iNumSelections, flTimeSpan, flSelectionsPerSecond );
if( flSelectionsPerSecond >= 8.0f )
{
DevMsg("\n\n %s is thrashing schedule selection:\n", GetDebugName() );
for( int i = 0 ; i < m_ScheduleHistory.Count() ; i++ )
{
AIScheduleChoice_t choice = m_ScheduleHistory[i];
Msg("--%s %f\n", choice.m_pScheduleSelected->GetName(), choice.m_flTimeSelected );
}
Msg("\n");
CAI_BaseNPC::m_nDebugBits |= bits_debugDisableAI;
}
}
}
#endif//AI_MONITOR_FOR_OSCILLATION
}
//=========================================================
// NextScheduledTask - increments the ScheduleIndex
//=========================================================
void CAI_BaseNPC::NextScheduledTask ( void )
{
Assert( GetCurSchedule() != NULL );
SetTaskStatus( TASKSTATUS_NEW );
IncScheduleCurTaskIndex();
if ( FScheduleDone() )
{
// Reset memory of failed schedule
m_failedSchedule = NULL;
m_interuptSchedule = NULL;
// just completed last task in schedule, so make it invalid by clearing it.
SetCondition( COND_SCHEDULE_DONE );
}
}
//-----------------------------------------------------------------------------
// Purpose: This function allows NPCs to modify the interrupt mask for the
// current schedule. This enables them to use base schedules but with
// different interrupt conditions. Implement this function in your
// derived class, and Set or Clear condition bits as you please.
//
// NOTE: Always call the base class in your implementation, but be
// aware of the difference between changing the bits before vs.
// changing the bits after calling the base implementation.
//
// Input : pBitString - Receives the updated interrupt mask.
//-----------------------------------------------------------------------------
void CAI_BaseNPC::BuildScheduleTestBits( void )
{
//NOTENOTE: Always defined in the leaf classes
}
//=========================================================
// IsScheduleValid - returns true as long as the current
// schedule is still the proper schedule to be executing,
// taking into account all conditions
//=========================================================
bool CAI_BaseNPC::IsScheduleValid()
{
if ( GetCurSchedule() == NULL || GetCurSchedule()->NumTasks() == 0 )
{
return false;
}
//Start out with the base schedule's set interrupt conditions
GetCurSchedule()->GetInterruptMask( &m_CustomInterruptConditions );
// Let the leaf class modify our interrupt test bits, but:
// - Don't allow any modifications when scripted
// - Don't modify interrupts for Schedules that set the COND_NO_CUSTOM_INTERRUPTS bit.
if ( m_NPCState != NPC_STATE_SCRIPT && !IsInLockedScene() && !m_CustomInterruptConditions.IsBitSet( COND_NO_CUSTOM_INTERRUPTS ) )
{
BuildScheduleTestBits();
}
//Any conditions set here will always be forced on the interrupt conditions
SetCustomInterruptCondition( COND_NPC_FREEZE );
// This is like: m_CustomInterruptConditions &= m_Conditions;
CAI_ScheduleBits testBits;
m_CustomInterruptConditions.And( m_Conditions, &testBits );
if (!testBits.IsAllClear())
{
// If in developer mode save the interrupt text for debug output
if (g_pDeveloper->GetInt())
{
// Reset memory of failed schedule
m_failedSchedule = NULL;
m_interuptSchedule = GetCurSchedule();
// Find the first non-zero bit
for (int i=0;i<MAX_CONDITIONS;i++)
{
if (testBits.IsBitSet(i))
{
m_interruptText = ConditionName( AI_RemapToGlobal( i ) );
if (!m_interruptText)
{
m_interruptText = "(UNKNOWN CONDITION)";
/*
static const char *pError = "ERROR: Unknown condition!";
DevMsg("%s (%s)\n", pError, GetDebugName());
m_interruptText = pError;
*/
}
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg( this, AIMF_IGNORE_SELECTED, " Break condition -> %s\n", m_interruptText );
}
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): Break condition -> %s\n", GetDebugName(), entindex(), m_interruptText ) );
break;
}
}
if ( HasCondition( COND_NEW_ENEMY ) )
{
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg( this, AIMF_IGNORE_SELECTED, " New enemy: %s\n", GetEnemy() ? GetEnemy()->GetDebugName() : "<NULL>" );
}
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): New enemy: %s\n", GetDebugName(), entindex(), GetEnemy() ? GetEnemy()->GetDebugName() : "<NULL>" ) );
}
}
return false;
}
if ( HasCondition(COND_SCHEDULE_DONE) ||
HasCondition(COND_TASK_FAILED) )
{
#ifdef DEBUG
if ( HasCondition ( COND_TASK_FAILED ) && m_failSchedule == SCHED_NONE )
{
// fail! Send a visual indicator.
DevWarning( 2, "Schedule: %s Failed\n", GetCurSchedule()->GetName() );
Vector tmp;
CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &tmp );
tmp.z += 16;
g_pEffects->Sparks( tmp );
}
#endif // DEBUG
// some condition has interrupted the schedule, or the schedule is done
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Determines whether or not SelectIdealState() should be called before
// a NPC selects a new schedule.
//
// NOTE: This logic was a source of pure, distilled trouble in Half-Life.
// If you change this function, please supply good comments.
//
// Output : Returns true if yes, false if no
//-----------------------------------------------------------------------------
bool CAI_BaseNPC::ShouldSelectIdealState( void )
{
/*
HERE's the old Half-Life code that used to control this.
if ( m_IdealNPCState != NPC_STATE_DEAD &&
(m_IdealNPCState != NPC_STATE_SCRIPT || m_IdealNPCState == m_NPCState) )
{
if ( (m_afConditions && !HasConditions(bits_COND_SCHEDULE_DONE)) ||
(GetCurSchedule() && (GetCurSchedule()->iInterruptMask & bits_COND_SCHEDULE_DONE)) ||
((m_NPCState == NPC_STATE_COMBAT) && (GetEnemy() == NULL)) )
{
GetIdealState();
}
}
*/
// Don't get ideal state if you are supposed to be dead.
if ( m_IdealNPCState == NPC_STATE_DEAD )
return false;
// If I'm supposed to be in scripted state, but i'm not yet, do not allow
// SelectIdealState() to be called, because it doesn't know how to determine
// that a NPC should be in SCRIPT state and will stomp it with some other
// state. (Most likely ALERT)
if ( (m_IdealNPCState == NPC_STATE_SCRIPT) && (m_NPCState != NPC_STATE_SCRIPT) )
return false;
// If the NPC has any current conditions, and one of those conditions indicates
// that the previous schedule completed successfully, then don't run SelectIdealState().
// Paths between states only exist for interrupted schedules, or when a schedule
// contains a task that suggests that the NPC change state.
if ( !HasCondition(COND_SCHEDULE_DONE) )
return true;
// This seems like some sort of hack...
// Currently no schedule that I can see in the AI uses this feature, but if a schedule
// interrupt mask contains bits_COND_SCHEDULE_DONE, then force a call to SelectIdealState().
// If we want to keep this feature, I suggest we create a new condition with a name that
// indicates exactly what it does.
if ( GetCurSchedule() && GetCurSchedule()->HasInterrupt(COND_SCHEDULE_DONE) )
return true;
// Don't call SelectIdealState if a NPC in combat state has a valid enemy handle. Otherwise,
// we need to change state immediately because something unexpected happened to the enemy
// entity (it was blown apart by someone else, for example), and we need the NPC to change
// state. THE REST OF OUR CODE should be robust enough that this can go away!!
if ( (m_NPCState == NPC_STATE_COMBAT) && (GetEnemy() == NULL) )
return true;
if ( (m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT) && (GetEnemy() != NULL) )
return true;
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Returns a new schedule based on current condition bits.
//-----------------------------------------------------------------------------
CAI_Schedule *CAI_BaseNPC::GetNewSchedule( void )
{
int scheduleType;
//
// Schedule selection code here overrides all leaf schedule selection.
//
if (HasCondition(COND_NPC_FREEZE))
{
scheduleType = SCHED_NPC_FREEZE;
}
else
{
// I dunno how this trend got started, but we need to find the problem.
// You may not be in combat state with no enemy!!! (sjb) 11/4/03
if( m_NPCState == NPC_STATE_COMBAT && !GetEnemy() )
{
DevMsg("**ERROR: Combat State with no enemy! slamming to ALERT\n");
SetState( NPC_STATE_ALERT );
}
AI_PROFILE_SCOPE_BEGIN( CAI_BaseNPC_SelectSchedule);
if ( m_NPCState == NPC_STATE_SCRIPT || m_NPCState == NPC_STATE_DEAD || m_iInteractionState == NPCINT_MOVING_TO_MARK )
{
scheduleType = CAI_BaseNPC::SelectSchedule();
}
else
{
scheduleType = SelectSchedule();
}
m_IdealSchedule = GetGlobalScheduleId( scheduleType );
AI_PROFILE_SCOPE_END();
}
return GetScheduleOfType( scheduleType );
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
CAI_Schedule *CAI_BaseNPC::GetFailSchedule( void )
{
int prevSchedule;
int failedTask;
if ( GetCurSchedule() )
prevSchedule = GetLocalScheduleId( GetCurSchedule()->GetId() );
else
prevSchedule = SCHED_NONE;
const Task_t *pTask = GetTask();
if ( pTask )
failedTask = pTask->iTask;
else
failedTask = TASK_INVALID;
Assert( AI_IdIsLocal( prevSchedule ) );
Assert( AI_IdIsLocal( failedTask ) );
int scheduleType = SelectFailSchedule( prevSchedule, failedTask, m_ScheduleState.taskFailureCode );
return GetScheduleOfType( scheduleType );
}
//=========================================================
// MaintainSchedule - does all the per-think schedule maintenance.
// ensures that the NPC leaves this function with a valid
// schedule!
//=========================================================
static bool ShouldStopProcessingTasks( CAI_BaseNPC *pNPC, int taskTime, int timeLimit )
{
#ifdef DEBUG
if( ai_simulate_task_overtime.GetBool() )
return true;
#endif
// Always stop processing if we've queued up a navigation query on the last task
if ( pNPC->IsNavigationDeferred() )
return true;
if ( AIStrongOpt() )
{
bool bInScript = ( pNPC->GetState() == NPC_STATE_SCRIPT || pNPC->IsCurSchedule( SCHED_SCENE_GENERIC, false ) );
// We ran a costly task, don't do it again!
if ( pNPC->HasMemory( bits_MEMORY_TASK_EXPENSIVE ) && bInScript == false )
return true;
}
if ( taskTime > timeLimit )
{
if ( ShouldUseEfficiency() ||
pNPC->IsMoving() ||
( pNPC->GetIdealActivity() != ACT_RUN && pNPC->GetIdealActivity() != ACT_WALK ) )
{
return true;
}
}
return false;
}
//-------------------------------------
void CAI_BaseNPC::MaintainSchedule ( void )
{
AI_PROFILE_SCOPE(CAI_BaseNPC_RunAI_MaintainSchedule);
extern CFastTimer g_AIMaintainScheduleTimer;
CTimeScope timeScope(&g_AIMaintainScheduleTimer);
//---------------------------------
CAI_Schedule *pNewSchedule;
int i;
bool runTask = true;
#if defined( VPROF_ENABLED )
#if defined(DISABLE_DEBUG_HISTORY)
bool bDebugTaskNames = ( developer.GetBool() || ( VProfAI() && g_VProfCurrentProfile.IsEnabled() ) );
#else
bool bDebugTaskNames = true;
#endif
#else
bool bDebugTaskNames = false;
#endif
memset( g_AITaskTimings, 0, sizeof(g_AITaskTimings) );
g_nAITasksRun = 0;
const int timeLimit = ( IsDebug() ) ? 16 : 8;
int taskTime = Plat_MSTime();
// Reset this at the beginning of the frame
Forget( bits_MEMORY_TASK_EXPENSIVE );
// UNDONE: Tune/fix this MAX_TASKS_RUN... This is just here so infinite loops are impossible
bool bStopProcessing = false;
for ( i = 0; i < MAX_TASKS_RUN && !bStopProcessing; i++ )
{
if ( GetCurSchedule() != NULL && TaskIsComplete() )
{
// Schedule is valid, so advance to the next task if the current is complete.
NextScheduledTask();
// If we finished the current schedule, clear our ignored conditions so they
// aren't applied to the next schedule selection.
if ( HasCondition( COND_SCHEDULE_DONE ) )
{
// Put our conditions back the way they were after GatherConditions,
// but add in COND_SCHEDULE_DONE.
m_Conditions = m_ConditionsPreIgnore;
SetCondition( COND_SCHEDULE_DONE );
m_InverseIgnoreConditions.SetAll();
}
// --------------------------------------------------------
// If debug stepping advance when I complete a task
// --------------------------------------------------------
if (CAI_BaseNPC::m_nDebugBits & bits_debugStepAI)
{
m_nDebugCurIndex++;
return;
}
}
int curTiming = g_nAITasksRun;
g_nAITasksRun++;
// validate existing schedule
if ( !IsScheduleValid() || m_NPCState != m_IdealNPCState )
{
// Notify the NPC that his schedule is changing
m_ScheduleState.bScheduleWasInterrupted = true;
OnScheduleChange();
if ( !HasCondition(COND_NPC_FREEZE) && ( !m_bConditionsGathered || m_bSkippedChooseEnemy ) )
{
// occurs if a schedule is exhausted within one think
GatherConditions();
}
if ( ShouldSelectIdealState() )
{
NPC_STATE eIdealState = SelectIdealState();
SetIdealState( eIdealState );
}
if ( HasCondition( COND_TASK_FAILED ) && m_NPCState == m_IdealNPCState )
{
// Get a fail schedule if the previous schedule failed during execution and
// the NPC is still in its ideal state. Otherwise, the NPC would immediately
// select the same schedule again and fail again.
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg( this, AIMF_IGNORE_SELECTED, " (failed)\n" );
}
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): (failed)\n", GetDebugName(), entindex() ) );
pNewSchedule = GetFailSchedule();
m_IdealSchedule = pNewSchedule->GetId();
DevWarning( 2, "(%s) Schedule (%s) Failed at %d!\n", STRING( GetEntityName() ), GetCurSchedule() ? GetCurSchedule()->GetName() : "GetCurSchedule() == NULL", GetScheduleCurTaskIndex() );
SetSchedule( pNewSchedule );
}
else
{
// If the NPC is supposed to change state, it doesn't matter if the previous
// schedule failed or completed. Changing state means selecting an entirely new schedule.
SetState( m_IdealNPCState );
g_AITaskTimings[curTiming].selectSchedule.Start();
pNewSchedule = GetNewSchedule();
g_AITaskTimings[curTiming].selectSchedule.End();
SetSchedule( pNewSchedule );
}
}
if (!GetCurSchedule())
{
g_AITaskTimings[curTiming].selectSchedule.Start();
pNewSchedule = GetNewSchedule();
g_AITaskTimings[curTiming].selectSchedule.End();
if (pNewSchedule)
{
SetSchedule( pNewSchedule );
}
}
if ( !GetCurSchedule() || GetCurSchedule()->NumTasks() == 0 )
{
DevMsg("ERROR: Missing or invalid schedule!\n");
SetActivity ( ACT_IDLE );
return;
}
AI_PROFILE_SCOPE_BEGIN_( GetCurSchedule() ? CAI_BaseNPC::GetSchedulingSymbols()->ScheduleIdToSymbol( GetCurSchedule()->GetId() ) : "NULL SCHEDULE" );
if ( GetTaskStatus() == TASKSTATUS_NEW )
{
if ( GetScheduleCurTaskIndex() == 0 )
{
int globalId = GetCurSchedule()->GetId();
int localId = GetLocalScheduleId( globalId ); // if localId == -1, then it came from a behavior
OnStartSchedule( (localId != -1)? localId : globalId );
}
g_AITaskTimings[curTiming].startTimer.Start();
const Task_t *pTask = GetTask();
const char *pszTaskName = ( bDebugTaskNames ) ? TaskName( pTask->iTask ) : "ai_task";
Assert( pTask != NULL );
g_AITaskTimings[i].pszTask = pszTaskName;
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT)
{
DevMsg(this, AIMF_IGNORE_SELECTED, " Task: %s\n", pszTaskName );
}
if ( m_pScheduleEvent != NULL )
{
CGlobalEvent *pEvent = GlobalEventLog.CreateTempEvent( "New Task", m_pScheduleEvent );
GlobalEventLog.AddKeyValue( pEvent, false, "Task", pszTaskName );
}
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): Task: %s\n", GetDebugName(), entindex(), pszTaskName ) );
OnStartTask();
m_ScheduleState.taskFailureCode = NO_TASK_FAILURE;
m_ScheduleState.timeCurTaskStarted = gpGlobals->curtime;
AI_PROFILE_SCOPE_BEGIN_( pszTaskName );
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_StartTask);
StartTask( pTask );
AI_PROFILE_SCOPE_END();
AI_PROFILE_SCOPE_END();
if ( TaskIsRunning() && !HasCondition(COND_TASK_FAILED) )
StartTaskOverlay();
g_AITaskTimings[curTiming].startTimer.End();
// DevMsg( "%.2f StartTask( %s )\n", gpGlobals->curtime, m_pTaskSR->GetStringText( pTask->iTask ) );
}
AI_PROFILE_SCOPE_END();
// UNDONE: Twice?!!!
MaintainActivity();
AI_PROFILE_SCOPE_BEGIN_( GetCurSchedule() ? CAI_BaseNPC::GetSchedulingSymbols()->ScheduleIdToSymbol( GetCurSchedule()->GetId() ) : "NULL SCHEDULE" );
if ( !TaskIsComplete() && GetTaskStatus() != TASKSTATUS_NEW )
{
if ( TaskIsRunning() && !HasCondition(COND_TASK_FAILED) && runTask )
{
const Task_t *pTask = GetTask();
const char *pszTaskName = ( bDebugTaskNames ) ? TaskName( pTask->iTask ) : "ai_task";
Assert( pTask != NULL );
g_AITaskTimings[i].pszTask = pszTaskName;
// DevMsg( "%.2f RunTask( %s )\n", gpGlobals->curtime, m_pTaskSR->GetStringText( pTask->iTask ) );
g_AITaskTimings[curTiming].runTimer.Start();
AI_PROFILE_SCOPE_BEGIN_( pszTaskName );
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_RunTask);
int j;
for (j = 0; j < 8; j++)
{
RunTask( pTask );
if ( GetTaskInterrupt() == 0 || TaskIsComplete() || HasCondition(COND_TASK_FAILED) )
break;
if ( ShouldUseEfficiency() && ShouldStopProcessingTasks( this, Plat_MSTime() - taskTime, timeLimit ) )
{
bStopProcessing = true;
break;
}
}
AssertMsg( j < 8, "Runaway task interrupt\n" );
AI_PROFILE_SCOPE_END();
AI_PROFILE_SCOPE_END();
if ( TaskIsRunning() && !HasCondition(COND_TASK_FAILED) )
{
if ( IsCurTaskContinuousMove() )
Remember( bits_MEMORY_MOVED_FROM_SPAWN );
RunTaskOverlay();
}
g_AITaskTimings[curTiming].runTimer.End();
// don't do this again this frame
// FIXME: RunTask() should eat some of the clock, depending on what it has done
// runTask = false;
if ( !TaskIsComplete() )
{
bStopProcessing = true;
}
}
else
{
bStopProcessing = true;
}
}
AI_PROFILE_SCOPE_END();
// Decide if we should continue on this frame
if ( !bStopProcessing && ShouldStopProcessingTasks( this, Plat_MSTime() - taskTime, timeLimit ) )
bStopProcessing = true;
}
for ( i = 0; i < m_Behaviors.Count(); i++ )
{
m_Behaviors[i]->MaintainChannelSchedules();
}
// UNDONE: We have to do this so that we have an animation set to blend to if RunTask changes the animation
// RunTask() will always change animations at the end of a script!
// Don't do this twice
MaintainActivity();
// --------------------------------------------------------
// If I'm stopping to debug step, don't animate unless
// I'm in motion
// --------------------------------------------------------
if (CAI_BaseNPC::m_nDebugBits & bits_debugStepAI)
{
if (!GetNavigator()->IsGoalActive() &&
m_nDebugCurIndex >= CAI_BaseNPC::m_nDebugPauseIndex)
{
m_flPlaybackRate = 0;
}
}
}
//=========================================================
bool CAI_BaseNPC::FindCoverPos( CBaseEntity *pEntity, Vector *pResult )
{
AI_PROFILE_SCOPE(CAI_BaseNPC_FindCoverPos);
if ( !GetTacticalServices()->FindLateralCover( pEntity->EyePosition(), 0, pResult ) )
{
if ( !GetTacticalServices()->FindCoverPos( pEntity->GetAbsOrigin(), pEntity->EyePosition(), 0, CoverRadius(), pResult ) )
{
return false;
}
}
return true;
}
//=========================================================
bool CAI_BaseNPC::FindCoverPosInRadius( CBaseEntity *pEntity, const Vector &goalPos, float coverRadius, Vector *pResult )
{
AI_PROFILE_SCOPE(CAI_BaseNPC_FindCoverPosInRadius);
if ( pEntity == NULL )
{
// Find cover from self if no enemy available
pEntity = this;
}
Vector coverPos = vec3_invalid;
CAI_TacticalServices * pTacticalServices = GetTacticalServices();
const Vector & enemyPos = pEntity->GetAbsOrigin();
Vector enemyEyePos = pEntity->EyePosition();
if( ( !GetSquad() || GetSquad()->GetFirstMember() == this ) &&
IsCoverPosition( enemyEyePos, goalPos + GetViewOffset() ) &&
IsValidCover( goalPos, NULL ) )
{
coverPos = goalPos;
}
else if ( !pTacticalServices->FindCoverPos( goalPos, enemyPos, enemyEyePos, 0, coverRadius * 0.5, &coverPos ) )
{
if ( !pTacticalServices->FindLateralCover( goalPos, enemyEyePos, 0, coverRadius * 0.5, 3, &coverPos ) )
{
if ( !pTacticalServices->FindCoverPos( goalPos, enemyPos, enemyEyePos, coverRadius * 0.5 - 0.1, coverRadius, &coverPos ) )
{
pTacticalServices->FindLateralCover( goalPos, enemyEyePos, 0, coverRadius, 5, &coverPos );
}
}
}
if ( coverPos == vec3_invalid )
return false;
*pResult = coverPos;
return true;
}
//=========================================================
bool CAI_BaseNPC::FindCoverPos( CSound *pSound, Vector *pResult )
{
if ( !GetTacticalServices()->FindCoverPos( pSound->GetSoundReactOrigin(),
pSound->GetSoundReactOrigin(),
MIN( pSound->Volume(), 120.0 ),
CoverRadius(),
pResult ) )
{
return GetTacticalServices()->FindLateralCover( pSound->GetSoundReactOrigin(), MIN( pSound->Volume(), 60.0 ), pResult );
}
return true;
}
//=========================================================
// Start task - selects the correct activity and performs
// any necessary calculations to start the next task on the
// schedule.
//=========================================================
//-----------------------------------------------------------------------------
// TASK_TURN_RIGHT / TASK_TURN_LEFT
//-----------------------------------------------------------------------------
void CAI_BaseNPC::StartTurn( float flDeltaYaw )
{
float flCurrentYaw;
flCurrentYaw = UTIL_AngleMod( GetLocalAngles().y );
GetMotor()->SetIdealYaw( UTIL_AngleMod( flCurrentYaw + flDeltaYaw ) );
SetTurnActivity();
}
//-----------------------------------------------------------------------------
// TASK_CLEAR_HINTNODE
//-----------------------------------------------------------------------------
void CAI_BaseNPC::ClearHintNode( float reuseDelay )
{
if ( m_pHintNode )
{
if ( m_pHintNode->IsLockedBy(this) )
m_pHintNode->Unlock(reuseDelay);
m_pHintNode = NULL;
}
}
void CAI_BaseNPC::SetHintNode( CAI_Hint *pHintNode )
{
m_pHintNode = pHintNode;
}
//-----------------------------------------------------------------------------
bool CAI_BaseNPC::FindCoverFromEnemy( bool bNodesOnly, float flMinDistance, float flMaxDistance )
{
CBaseEntity *pEntity = GetEnemy();
// Find cover from self if no enemy available
if ( pEntity == NULL )
pEntity = this;
Vector coverPos = vec3_invalid;
ClearHintNode();
if ( bNodesOnly )
{
if ( flMaxDistance == FLT_MAX )
flMaxDistance = CoverRadius();
if ( !GetTacticalServices()->FindCoverPos( pEntity->GetAbsOrigin(), pEntity->EyePosition(), flMinDistance, flMaxDistance, &coverPos ) )
return false;
}
else
{
if ( !FindCoverPos( pEntity, &coverPos ) )
return false;
}
AI_NavGoal_t goal( GOALTYPE_COVER, coverPos, ACT_RUN, AIN_HULL_TOLERANCE );
if ( !GetNavigator()->SetGoal( goal ) )
return false;
// FIXME: add to goal
if (GetHintNode())
{
GetNavigator()->SetArrivalActivity( GetCoverActivity( GetHintNode() ) );
GetNavigator()->SetArrivalDirection( GetHintNode()->GetDirection() );
}
return true;
}
//-----------------------------------------------------------------------------
// TASK_FIND_COVER_FROM_BEST_SOUND
//-----------------------------------------------------------------------------
bool CAI_BaseNPC::FindCoverFromBestSound( Vector *pCoverPos )
{
CSound *pBestSound;
pBestSound = GetBestSound();
if (pBestSound)
{
// UNDONE: Back away if cover fails? Grunts do this.
return FindCoverPos( pBestSound, pCoverPos );
}
else
{
DevMsg( 2, "Attempting to find cover from best sound, but best sound not founc.\n" );
}
return false;
}
//-----------------------------------------------------------------------------
// TASK_FACE_REASONABLE
//-----------------------------------------------------------------------------
float CAI_BaseNPC::CalcReasonableFacing( bool bIgnoreOriginalFacing )
{
float flReasonableYaw;
if( !bIgnoreOriginalFacing && !HasMemory( bits_MEMORY_MOVED_FROM_SPAWN ) && !HasCondition( COND_SEE_ENEMY) )
{
flReasonableYaw = m_flOriginalYaw;
}
else
{
// If I'm facing a wall, change my original yaw and try to find a good direction to face.
trace_t tr;
Vector forward;
QAngle angles( 0, 0, 0 );
float idealYaw = GetMotor()->GetIdealYaw();
flReasonableYaw = idealYaw;
// Try just using the facing we have
const float MIN_DIST = GetReasonableFacingDist();
float longestTrace = 0;
// Early out if we're overriding reasonable facing
if ( !MIN_DIST )
return flReasonableYaw;
// Otherwise, scan out back and forth until something better is found
const float SLICES = 8.0f;
const float SIZE_SLICE = 360.0 / SLICES;
const int SEARCH_MAX = (int)SLICES / 2;
float zEye = GetAbsOrigin().z + m_vDefaultEyeOffset.z; // always use standing eye so as to not screw with crouch cover
for( int i = 0 ; i <= SEARCH_MAX; i++ )
{
float offset = i * SIZE_SLICE;
for ( int j = -1; j <= 1; j += 2)
{
angles.y = idealYaw + ( offset * j );
AngleVectors( angles, &forward, NULL, NULL );
float curTrace;
if( ( curTrace = LineOfSightDist( forward, zEye ) ) > longestTrace && IsValidReasonableFacing(forward, curTrace) )
{
// Take this one.
flReasonableYaw = angles.y;
longestTrace = curTrace;
}
if ( longestTrace > MIN_DIST) // found one
break;
if ( i == 0 || i == SEARCH_MAX) // if trying forwards or backwards, skip the check of the other side...
break;
}
if ( longestTrace > MIN_DIST ) // found one
break;
}
}
return flReasonableYaw;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
float CAI_BaseNPC::GetReasonableFacingDist( void )
{
if ( GetTask() && GetTask()->iTask == TASK_FACE_ENEMY )
{
const float dist = 3.5*12;
if ( GetEnemy() )
{
float distEnemy = ( GetEnemy()->GetAbsOrigin().AsVector2D() - GetAbsOrigin().AsVector2D() ).Length() - 1.0;
return MIN( distEnemy, dist );
}
return dist;
}
return 5*12;
}
//-----------------------------------------------------------------------------
// TASK_SCRIPT_RUN_TO_TARGET / TASK_SCRIPT_WALK_TO_TARGET / TASK_SCRIPT_CUSTOM_MOVE_TO_TARGET
//-----------------------------------------------------------------------------
void CAI_BaseNPC::StartScriptMoveToTargetTask( int task )
{
Activity newActivity;
if ( m_hTargetEnt == NULL)
{
TaskFail(FAIL_NO_TARGET);
}
else if ( (m_hTargetEnt->GetAbsOrigin() - GetLocalOrigin()).Length() < 1 )
{
TaskComplete();
}
else
{
//
// Select the appropriate activity.
//
if ( task == TASK_SCRIPT_WALK_TO_TARGET )
{
newActivity = ACT_WALK;
}
else if ( task == TASK_SCRIPT_RUN_TO_TARGET )
{
newActivity = ACT_RUN;
}
else
{
newActivity = GetScriptCustomMoveActivity();
}
if ( ( newActivity != ACT_SCRIPT_CUSTOM_MOVE ) && TranslateActivity( newActivity ) == ACT_INVALID )
{
// This NPC can't do this!
Assert( 0 );
}
else
{
if (m_hTargetEnt == NULL)
{
TaskFail(FAIL_NO_TARGET);
}
else
{
AI_NavGoal_t goal( GOALTYPE_TARGETENT, newActivity );
if ( GetState() == NPC_STATE_SCRIPT &&
( m_ScriptArrivalActivity != AIN_DEF_ACTIVITY ||
m_strScriptArrivalSequence != NULL_STRING ) )
{
if ( m_ScriptArrivalActivity != AIN_DEF_ACTIVITY )
{
goal.arrivalActivity = m_ScriptArrivalActivity;
}
else
{
goal.arrivalSequence = LookupSequence( m_strScriptArrivalSequence.ToCStr() );
}
}
if (!GetNavigator()->SetGoal( goal, AIN_DISCARD_IF_FAIL ))
{
if ( GetNavigator()->GetNavFailCounter() == 0 )
{
// no path was built, but OnNavFailed() did something so that next time it may work
DevWarning("%s %s failed Urgent Movement, retrying\n", GetDebugName(), TaskName( task ) );
return;
}
// FIXME: scripted sequences don't actually know how to handle failure, but we're failing. This is serious
DevWarning("%s %s failed Urgent Movement, abandoning schedule\n", GetDebugName(), TaskName( task ) );
TaskFail(FAIL_NO_ROUTE);
}
else
{
GetNavigator()->SetArrivalDirection( m_hTargetEnt->GetAbsAngles() );
}
}
}
}
m_ScriptArrivalActivity = AIN_DEF_ACTIVITY;
m_strScriptArrivalSequence = NULL_STRING;
TaskComplete();
}
//-----------------------------------------------------------------------------
// Start task!
//-----------------------------------------------------------------------------
void CAI_BaseNPC::StartTask( const Task_t *pTask )
{
int task = pTask->iTask;
switch ( pTask->iTask )
{
case TASK_RESET_ACTIVITY:
m_Activity = ACT_RESET;
TaskComplete();
break;
case TASK_CREATE_PENDING_WEAPON:
Assert( m_iszPendingWeapon != NULL_STRING );
GiveWeapon( m_iszPendingWeapon );
m_iszPendingWeapon = NULL_STRING;
TaskComplete();
break;
case TASK_RANDOMIZE_FRAMERATE:
{
float newRate = GetPlaybackRate();
float percent = pTask->flTaskData / 100.0f;
newRate += ( newRate * random->RandomFloat(-percent, percent) );
SetPlaybackRate(newRate);
TaskComplete();
}
break;
case TASK_DEFER_DODGE:
m_flNextDodgeTime = gpGlobals->curtime + pTask->flTaskData;
TaskComplete();
break;
// Default case just completes. Override in sub-classes
// to play sound / animation / or pause
case TASK_ANNOUNCE_ATTACK:
TaskComplete();
break;
case TASK_TURN_RIGHT:
StartTurn( -pTask->flTaskData );
break;
case TASK_TURN_LEFT:
StartTurn( pTask->flTaskData );
break;
case TASK_REMEMBER:
Remember ( (int)pTask->flTaskData );
TaskComplete();
break;
case TASK_FORGET:
Forget ( (int)pTask->flTaskData );
TaskComplete();
break;
case TASK_FIND_HINTNODE:
case TASK_FIND_LOCK_HINTNODE:
{
if (!GetHintNode())
{
SetHintNode( CAI_HintManager::FindHint( this, HINT_NONE, pTask->flTaskData, 2000 ) );
}
if (GetHintNode())
{
TaskComplete();
}
else
{
TaskFail(FAIL_NO_HINT_NODE);
}
if ( task == TASK_FIND_HINTNODE )
break;
}
// Fall through on TASK_FIND_LOCK_HINTNODE...
case TASK_LOCK_HINTNODE:
{
if (!GetHintNode())
{
TaskFail(FAIL_NO_HINT_NODE);
}
else if( GetHintNode()->Lock(this) )
{
TaskComplete();
}
else
{
TaskFail(FAIL_ALREADY_LOCKED);
SetHintNode( NULL );
}
break;
}
case TASK_STORE_LASTPOSITION:
m_vecLastPosition = GetLocalOrigin();
TaskComplete();
break;
case TASK_CLEAR_LASTPOSITION:
m_vecLastPosition = vec3_origin;
TaskComplete();
break;
case TASK_STORE_POSITION_IN_SAVEPOSITION:
m_vSavePosition = GetLocalOrigin();
TaskComplete();
break;
case TASK_STORE_BESTSOUND_IN_SAVEPOSITION:
{
CSound *pBestSound = GetBestSound();
if ( pBestSound )
{
m_vSavePosition = pBestSound->GetSoundOrigin();
CBaseEntity *pSoundEnt = pBestSound->m_hOwner;
if ( pSoundEnt )
{
Vector vel;
pSoundEnt->GetVelocity( &vel, NULL );
// HACKHACK: run away from cars in the right direction
m_vSavePosition += vel * 2; // add in 2 seconds of velocity
}
TaskComplete();
}
else
{
TaskFail("No Sound!");
return;
}
}
break;
case TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION:
{
CSound *pBestSound = GetBestSound();
if ( pBestSound )
{
m_vSavePosition = pBestSound->GetSoundReactOrigin();
TaskComplete();
}
else
{
TaskFail("No Sound!");
return;
}
}
break;
case TASK_STORE_ENEMY_POSITION_IN_SAVEPOSITION:
if ( GetEnemy() != NULL )
{
m_vSavePosition = GetEnemy()->GetAbsOrigin();
TaskComplete();
}
else
{
TaskFail(FAIL_NO_ENEMY);
}
break;
case TASK_CLEAR_HINTNODE:
ClearHintNode(pTask->flTaskData);
TaskComplete();
break;
case TASK_STOP_MOVING:
if ( ( GetNavigator()->IsGoalSet() && GetNavigator()->IsGoalActive() ) || GetNavType() == NAV_JUMP )
{
DbgNavMsg( this, "Start TASK_STOP_MOVING\n" );
if ( pTask->flTaskData == 1 )
{
DbgNavMsg( this, "Initiating stopping path\n" );
GetNavigator()->StopMoving( false );
}
else
{
GetNavigator()->ClearGoal();
}
// E3 Hack
if ( HasPoseMoveYaw() )
{
SetPoseParameter( m_poseMove_Yaw, 0 );
}
}
else
{
if ( pTask->flTaskData == 1 && GetNavigator()->SetGoalFromStoppingPath() )
{
DbgNavMsg( this, "Start TASK_STOP_MOVING\n" );
DbgNavMsg( this, "Initiating stopping path\n" );
}
else
{
GetNavigator()->ClearGoal();
SetIdealActivity( GetStoppedActivity() );
TaskComplete();
}
}
break;
case TASK_PLAY_PRIVATE_SEQUENCE:
case TASK_PLAY_PRIVATE_SEQUENCE_FACE_ENEMY:
case TASK_PLAY_SEQUENCE_FACE_ENEMY:
case TASK_PLAY_SEQUENCE_FACE_TARGET:
case TASK_PLAY_SEQUENCE:
SetIdealActivity( (Activity)(int)pTask->flTaskData );
break;
case TASK_ADD_GESTURE_WAIT:
{
int iLayer = AddGesture( (Activity)(int)pTask->flTaskData );
if (iLayer > 0)
{
float flDuration = GetLayerDuration( iLayer );
SetWait( flDuration );
}
else
{
TaskFail( "Unable to allocate gesture" );
}
break;
}
case TASK_ADD_GESTURE:
{
AddGesture( (Activity)(int)pTask->flTaskData );
TaskComplete();
break;
}
case TASK_PLAY_HINT_ACTIVITY:
if ( GetHintNode()->HintActivityName() != NULL_STRING )
{
Activity hintActivity = (Activity)CAI_BaseNPC::GetActivityID( STRING(GetHintNode()->HintActivityName()) );
if ( hintActivity != ACT_INVALID )
{
SetIdealActivity( GetHintActivity(GetHintNode()->HintType(), hintActivity) );
}
else
{
int iSequence = LookupSequence(STRING(GetHintNode()->HintActivityName()));
if ( iSequence > ACT_INVALID )
{
SetSequenceById( iSequence ); // ???
SetIdealActivity( ACT_DO_NOT_DISTURB );
}
else
SetIdealActivity( ACT_IDLE );
}
}
else
{
SetIdealActivity( ACT_IDLE );
}
break;
case TASK_SET_SCHEDULE:
if ( !SetSchedule( pTask->flTaskData ) )
TaskFail(FAIL_SCHEDULE_NOT_FOUND);
break;
case TASK_FIND_BACKAWAY_FROM_SAVEPOSITION:
{
if ( GetEnemy() != NULL )
{
Vector backPos;
if ( !GetTacticalServices()->FindBackAwayPos( m_vSavePosition, &backPos ) )
{
// no place to backaway
TaskFail(FAIL_NO_BACKAWAY_NODE);
}
else
{
if (GetNavigator()->SetGoal( AI_NavGoal_t( backPos, ACT_RUN ) ) )
{
TaskComplete();
}
else
{
// no place to backaway
TaskFail(FAIL_NO_ROUTE);
}
}
}
else
{
TaskFail(FAIL_NO_ENEMY);
}
}
break;
case TASK_FIND_NEAR_NODE_COVER_FROM_ENEMY:
case TASK_FIND_FAR_NODE_COVER_FROM_ENEMY:
case TASK_FIND_NODE_COVER_FROM_ENEMY:
case TASK_FIND_COVER_FROM_ENEMY:
{
bool bNodeCover = ( task != TASK_FIND_COVER_FROM_ENEMY );
float flMinDistance = ( task == TASK_FIND_FAR_NODE_COVER_FROM_ENEMY ) ? pTask->flTaskData : 0.0;
float flMaxDistance = ( task == TASK_FIND_NEAR_NODE_COVER_FROM_ENEMY ) ? pTask->flTaskData : FLT_MAX;
if ( FindCoverFromEnemy( bNodeCover, flMinDistance, flMaxDistance ) )
{
if ( task == TASK_FIND_COVER_FROM_ENEMY )
m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
TaskComplete();
}
else
TaskFail(FAIL_NO_COVER);
}
break;
case TASK_FIND_COVER_FROM_ORIGIN:
{
Vector coverPos;
if ( GetTacticalServices()->FindCoverPos( GetLocalOrigin(), EyePosition(), 0, CoverRadius(), &coverPos ) )
{
AI_NavGoal_t goal(coverPos, ACT_RUN, AIN_HULL_TOLERANCE);
GetNavigator()->SetGoal( goal );
m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
}
else
{
// no coverwhatsoever.
TaskFail(FAIL_NO_COVER);
}
}
break;
case TASK_FIND_COVER_FROM_BEST_SOUND:
{
}
break;
case TASK_FACE_HINTNODE:
// If the yaw is locked, this function will not act correctly
Assert( GetMotor()->IsYawLocked() == false );
GetMotor()->SetIdealYaw( GetHintNode()->Yaw() );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
if ( FacingIdeal() )
TaskComplete();
else
SetTurnActivity();
break;
case TASK_FACE_LASTPOSITION:
GetMotor()->SetIdealYawToTarget( m_vecLastPosition );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
SetTurnActivity();
break;
case TASK_FACE_SAVEPOSITION:
GetMotor()->SetIdealYawToTarget( m_vSavePosition );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
SetTurnActivity();
break;
case TASK_FACE_AWAY_FROM_SAVEPOSITION:
GetMotor()->SetIdealYawToTarget( m_vSavePosition, 0, 180.0 );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
SetTurnActivity();
break;
case TASK_SET_IDEAL_YAW_TO_CURRENT:
GetMotor()->SetIdealYaw( UTIL_AngleMod( GetLocalAngles().y ) );
TaskComplete();
break;
case TASK_FACE_TARGET:
if ( m_hTargetEnt != NULL )
{
GetMotor()->SetIdealYawToTarget( m_hTargetEnt->GetAbsOrigin() );
SetTurnActivity();
}
else
{
TaskFail(FAIL_NO_TARGET);
}
break;
case TASK_FACE_PLAYER:
// track head to the client for a while.
SetWait( pTask->flTaskData );
break;
case TASK_FACE_ENEMY:
{
Vector vecEnemyLKP = GetEnemyLKP();
if (!FInAimCone( vecEnemyLKP ))
{
GetMotor()->SetIdealYawToTarget( vecEnemyLKP );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
SetTurnActivity();
}
else
{
float flReasonableFacing = CalcReasonableFacing( true );
if ( fabsf( flReasonableFacing - GetMotor()->GetIdealYaw() ) < 1 )
TaskComplete();
else
{
GetMotor()->SetIdealYaw( flReasonableFacing );
SetTurnActivity();
}
}
break;
}
case TASK_FACE_IDEAL:
SetTurnActivity();
break;
case TASK_FACE_REASONABLE:
GetMotor()->SetIdealYaw( CalcReasonableFacing() );
SetTurnActivity();
break;
case TASK_FACE_PATH:
{
if (!GetNavigator()->IsGoalActive())
{
DevWarning( 2, "No route to face!\n");
TaskFail(FAIL_NO_ROUTE);
}
else
{
const float NPC_TRIVIAL_TURN = 15; // (Degrees). Turns this small or smaller, don't bother with a transition.
GetMotor()->SetIdealYawToTarget( GetNavigator()->GetCurWaypointPos());
if( fabs( GetMotor()->DeltaIdealYaw() ) <= NPC_TRIVIAL_TURN )
{
// This character is already facing the path well enough that
// moving will look fairly natural. Don't bother with a transitional
// turn animation.
TaskComplete();
break;
}
SetTurnActivity();
}
}
break;
// don't do anything.
case TASK_WAIT_PVS:
case TASK_WAIT_INDEFINITE:
break;
// set a future time that tells us when the wait is over.
case TASK_WAIT:
case TASK_WAIT_FACE_ENEMY:
SetWait( pTask->flTaskData );
break;
// set a future time that tells us when the wait is over.
case TASK_WAIT_RANDOM:
case TASK_WAIT_FACE_ENEMY_RANDOM:
SetWait( 0, pTask->flTaskData );
break;
case TASK_MOVE_TO_TARGET_RANGE:
case TASK_MOVE_TO_GOAL_RANGE:
{
// Identical tasks, except that target_range uses m_hTargetEnt,
// and Goal range uses the nav goal
CBaseEntity *pTarget = NULL;
if ( task == TASK_MOVE_TO_GOAL_RANGE )
{
pTarget = GetNavigator()->GetGoalTarget();
}
if ( !pTarget )
{
pTarget = m_hTargetEnt.Get();
}
if ( pTarget == NULL)
{
TaskFail(FAIL_NO_TARGET);
}
else if ( (pTarget->GetAbsOrigin() - GetLocalOrigin()).Length() < 1 )
{
TaskComplete();
}
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
{
TaskComplete();
GetNavigator()->ClearGoal(); // Clear residual state
}
else
{
// set that we're probably going to stop before the goal
GetNavigator()->SetArrivalDistance( pTask->flTaskData );
}
break;
}
case TASK_WAIT_UNTIL_NO_DANGER_SOUND:
if( !HasCondition( COND_HEAR_DANGER ) )
{
TaskComplete();
}
break;
case TASK_TARGET_PLAYER:
{
CBaseEntity *pPlayer = gEntList.FindEntityByName( NULL, "!player" );
if ( pPlayer )
{
SetTarget( pPlayer );
TaskComplete();
}
else
TaskFail( FAIL_NO_PLAYER );
break;
}
case TASK_SCRIPT_RUN_TO_TARGET:
case TASK_SCRIPT_WALK_TO_TARGET:
case TASK_SCRIPT_CUSTOM_MOVE_TO_TARGET:
StartScriptMoveToTargetTask( pTask->iTask );
break;
case TASK_CLEAR_MOVE_WAIT:
m_flMoveWaitFinished = gpGlobals->curtime;
TaskComplete();
break;
case TASK_MELEE_ATTACK1:
SetLastAttackTime( gpGlobals->curtime );
ResetIdealActivity( ACT_MELEE_ATTACK1 );
break;
case TASK_MELEE_ATTACK2:
SetLastAttackTime( gpGlobals->curtime );
ResetIdealActivity( ACT_MELEE_ATTACK2 );
break;
case TASK_RANGE_ATTACK1:
SetLastAttackTime( gpGlobals->curtime );
ResetIdealActivity( ACT_RANGE_ATTACK1 );
break;
case TASK_RANGE_ATTACK2:
SetLastAttackTime( gpGlobals->curtime );
ResetIdealActivity( ACT_RANGE_ATTACK2 );
break;
case TASK_RELOAD:
ResetIdealActivity( ACT_RELOAD );
break;
case TASK_SPECIAL_ATTACK1:
ResetIdealActivity( ACT_SPECIAL_ATTACK1 );
break;
case TASK_SPECIAL_ATTACK2:
ResetIdealActivity( ACT_SPECIAL_ATTACK2 );
break;
case TASK_SET_ACTIVITY:
{
Activity goalActivity = (Activity)((int)pTask->flTaskData);
if (goalActivity != ACT_RESET)
{
SetIdealActivity( goalActivity );
}
else
{
m_Activity = ACT_RESET;
}
break;
}
case TASK_GET_CHASE_PATH_TO_ENEMY:
{
CBaseEntity *pEnemy = GetEnemy();
if ( !pEnemy )
{
TaskFail(FAIL_NO_ROUTE);
return;
}
if ( ( pEnemy->GetAbsOrigin() - GetEnemyLKP() ).LengthSqr() < Square(pTask->flTaskData) )
{
ChainStartTask( TASK_GET_PATH_TO_ENEMY );
}
else
{
ChainStartTask( TASK_GET_PATH_TO_ENEMY_LKP );
}
if ( !TaskIsComplete() && !HasCondition(COND_TASK_FAILED) )
TaskFail(FAIL_NO_ROUTE);
break;
}
case TASK_GET_PATH_TO_ENEMY_LKP:
{
CBaseEntity *pEnemy = GetEnemy();
if (!pEnemy || IsUnreachable(pEnemy))
{
TaskFail(FAIL_NO_ROUTE);
return;
}
AI_NavGoal_t goal( GetEnemyLKP() );
TranslateNavGoal( pEnemy, goal.dest );
if ( GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET ) )
{
TaskComplete();
}
else
{
// no way to get there =(
DevWarning( 2, "GetPathToEnemyLKP failed!!\n" );
RememberUnreachable(GetEnemy());
TaskFail(FAIL_NO_ROUTE);
}
break;
}
case TASK_GET_PATH_TO_INTERACTION_PARTNER:
{
if ( !m_hForcedInteractionPartner || IsUnreachable(m_hForcedInteractionPartner) )
{
TaskFail(FAIL_NO_ROUTE);
return;
}
// Calculate the position we need to be at to start the interaction.
CalculateForcedInteractionPosition();
AI_NavGoal_t goal( m_vecForcedWorldPosition );
TranslateNavGoal( m_hForcedInteractionPartner, goal.dest );
if ( GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET ) )
{
TaskComplete();
}
else
{
DevWarning( 2, "GetPathToInteractionPartner failed!!\n" );
RememberUnreachable(m_hForcedInteractionPartner);
TaskFail(FAIL_NO_ROUTE);
}
break;
}
case TASK_GET_PATH_TO_RANGE_ENEMY_LKP_LOS:
{
if ( GetEnemy() )
{
// Find out which range to use (either innately or a held weapon)
float flRange = -1.0f;
if ( CapabilitiesGet() & (bits_CAP_INNATE_RANGE_ATTACK1|bits_CAP_INNATE_RANGE_ATTACK2) )
{
flRange = InnateRange1MaxRange();
}
else if ( GetActiveWeapon() )
{
flRange = MAX( GetActiveWeapon()->m_fMaxRange1, GetActiveWeapon()->m_fMaxRange2 );
}
else
{
// You can't call this task without either innate range attacks or a weapon!
Assert( 0 );
TaskFail( FAIL_NO_ROUTE );
}
// Clamp to the specified range, if supplied
if ( pTask->flTaskData != 0 && pTask->flTaskData < flRange )
flRange = pTask->flTaskData;
// For now, just try running straight at enemy
float dist = EnemyDistance( GetEnemy() );
if ( dist <= flRange || GetNavigator()->SetVectorGoalFromTarget( GetEnemy()->GetAbsOrigin(), dist - flRange ) )
{
TaskComplete();
break;
}
}
TaskFail( FAIL_NO_ROUTE );
break;
}
case TASK_GET_PATH_TO_ENEMY_LOS:
case TASK_GET_FLANK_RADIUS_PATH_TO_ENEMY_LOS:
case TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS:
case TASK_GET_PATH_TO_ENEMY_LKP_LOS:
{
if ( GetEnemy() == NULL )
{
TaskFail(FAIL_NO_ENEMY);
return;
}
AI_PROFILE_SCOPE(CAI_BaseNPC_FindLosToEnemy);
float flMaxRange = 2000;
float flMinRange = 0;
if ( GetActiveWeapon() )
{
flMaxRange = MAX( GetActiveWeapon()->m_fMaxRange1, GetActiveWeapon()->m_fMaxRange2 );
flMinRange = MIN( GetActiveWeapon()->m_fMinRange1, GetActiveWeapon()->m_fMinRange2 );
}
else if ( CapabilitiesGet() & bits_CAP_INNATE_RANGE_ATTACK1 )
{
flMaxRange = InnateRange1MaxRange();
flMinRange = InnateRange1MinRange();
}
//Check against NPC's MAX range
if ( flMaxRange > m_flDistTooFar )
{
flMaxRange = m_flDistTooFar;
}
Vector vecEnemy = ( task != TASK_GET_PATH_TO_ENEMY_LKP ) ? GetEnemy()->GetAbsOrigin() : GetEnemyLKP();
Vector vecEnemyEye = vecEnemy + GetEnemy()->GetViewOffset();
Vector posLos;
bool found = false;
if ( ( task != TASK_GET_FLANK_RADIUS_PATH_TO_ENEMY_LOS ) && ( task != TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS ) )
{
if ( GetTacticalServices()->FindLateralLos( vecEnemyEye, &posLos ) )
{
float dist = ( posLos - vecEnemyEye ).Length();
if ( dist < flMaxRange && dist > flMinRange )
found = true;
}
}
if ( !found )
{
FlankType_t eFlankType = FLANKTYPE_NONE;
Vector vecFlankRefPos = vec3_origin;
float flFlankParam = 0;
if ( task == TASK_GET_FLANK_RADIUS_PATH_TO_ENEMY_LOS )
{
eFlankType = FLANKTYPE_RADIUS;
vecFlankRefPos = m_vSavePosition;
flFlankParam = pTask->flTaskData;
}
else if ( task == TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS )
{
eFlankType = FLANKTYPE_ARC;
vecFlankRefPos = m_vSavePosition;
flFlankParam = pTask->flTaskData;
}
if ( GetTacticalServices()->FindLos( vecEnemy, vecEnemyEye, flMinRange, flMaxRange, 1.0, eFlankType, vecFlankRefPos, flFlankParam, &posLos ) )
{
found = true;
}
}
if ( !found )
{
TaskFail( FAIL_NO_SHOOT );
}
else
{
// else drop into run task to offer an interrupt
m_vInterruptSavePosition = posLos;
}
}
break;
//==================================================
// TASK_SET_GOAL
//==================================================
case TASK_SET_GOAL:
switch ( (int) pTask->flTaskData )
{
case GOAL_ENEMY: //Enemy
if ( GetEnemy() == NULL )
{
TaskFail( FAIL_NO_ENEMY );
return;
}
//Setup our stored info
m_vecStoredPathGoal = GetEnemy()->GetAbsOrigin();
m_nStoredPathType = GOALTYPE_ENEMY;
m_fStoredPathFlags = 0;
m_hStoredPathTarget = GetEnemy();
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
case GOAL_ENEMY_LKP: //Enemy's last known position
if ( GetEnemy() == NULL )
{
TaskFail( FAIL_NO_ENEMY );
return;
}
//Setup our stored info
m_vecStoredPathGoal = GetEnemyLKP();
m_nStoredPathType = GOALTYPE_LOCATION;
m_fStoredPathFlags = 0;
m_hStoredPathTarget = NULL;
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
case GOAL_TARGET: //Target entity
if ( m_hTargetEnt == NULL )
{
TaskFail( FAIL_NO_TARGET );
return;
}
//Setup our stored info
m_vecStoredPathGoal = m_hTargetEnt->GetAbsOrigin();
m_nStoredPathType = GOALTYPE_TARGETENT;
m_fStoredPathFlags = 0;
m_hStoredPathTarget = m_hTargetEnt;
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
case GOAL_SAVED_POSITION: //Saved position
//Setup our stored info
m_vecStoredPathGoal = m_vSavePosition;
m_nStoredPathType = GOALTYPE_LOCATION;
m_fStoredPathFlags = 0;
m_hStoredPathTarget = NULL;
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
}
TaskComplete();
break;
//==================================================
// TASK_GET_PATH_TO_GOAL
//==================================================
case TASK_GET_PATH_TO_GOAL:
{
AI_NavGoal_t goal( m_nStoredPathType,
AIN_DEF_ACTIVITY,
AIN_HULL_TOLERANCE,
AIN_DEF_FLAGS,
m_hStoredPathTarget );
bool foundPath = false;
//Find our path
switch ( (int) pTask->flTaskData )
{
case PATH_TRAVEL: //A land path to our goal
goal.dest = m_vecStoredPathGoal;
foundPath = GetNavigator()->SetGoal( goal );
break;
case PATH_LOS: //A path to get LOS to our goal
{
float flMaxRange = 2000.0f;
float flMinRange = 0.0f;
if ( GetActiveWeapon() )
{
flMaxRange = MAX( GetActiveWeapon()->m_fMaxRange1, GetActiveWeapon()->m_fMaxRange2 );
flMinRange = MIN( GetActiveWeapon()->m_fMinRange1, GetActiveWeapon()->m_fMinRange2 );
}
else if ( CapabilitiesGet() & bits_CAP_INNATE_RANGE_ATTACK1 )
{
flMaxRange = InnateRange1MaxRange();
flMinRange = InnateRange1MinRange();
}
// Check against NPC's MAX range
if ( flMaxRange > m_flDistTooFar )
{
flMaxRange = m_flDistTooFar;
}
Vector eyePosition = ( m_hStoredPathTarget != NULL ) ? m_hStoredPathTarget->EyePosition() : m_vecStoredPathGoal;
Vector posLos;
// See if we've found it
if ( GetTacticalServices()->FindLos( m_vecStoredPathGoal, eyePosition, flMinRange, flMaxRange, 1.0f, &posLos ) )
{
goal.dest = posLos;
foundPath = GetNavigator()->SetGoal( goal );
}
else
{
// No LOS to goal
TaskFail( FAIL_NO_SHOOT );
return;
}
}
break;
case PATH_COVER: //Get a path to cover FROM our goal
{
CBaseEntity *pEntity = ( m_hStoredPathTarget == nullptr ) ? this : m_hStoredPathTarget.Get();
//Find later cover first
Vector coverPos;
if ( GetTacticalServices()->FindLateralCover( pEntity->EyePosition(), 0, &coverPos ) )
{
AI_NavGoal_t goal( coverPos, ACT_RUN );
GetNavigator()->SetGoal( goal, AIN_CLEAR_PREVIOUS_STATE );
//FIXME: What exactly is this doing internally?
m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
TaskComplete();
return;
}
else
{
//Try any cover
if ( GetTacticalServices()->FindCoverPos( pEntity->GetAbsOrigin(), pEntity->EyePosition(), 0, CoverRadius(), &coverPos ) )
{
//If we've found it, find a safe route there
AI_NavGoal_t coverGoal( GOALTYPE_COVER,
coverPos,
ACT_RUN,
AIN_HULL_TOLERANCE,
AIN_DEF_FLAGS,
m_hStoredPathTarget );
foundPath = GetNavigator()->SetGoal( goal );
m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
}
else
{
TaskFail( FAIL_NO_COVER );
}
}
}
break;
}
//Now validate our try
if ( foundPath )
{
TaskComplete();
}
else
{
TaskFail( FAIL_NO_ROUTE );
}
}
break;
case TASK_GET_PATH_TO_ENEMY:
{
if (IsUnreachable(GetEnemy()))
{
TaskFail(FAIL_NO_ROUTE);
return;
}
CBaseEntity *pEnemy = GetEnemy();
if ( pEnemy == NULL )
{
TaskFail(FAIL_NO_ENEMY);
return;
}
if ( GetNavigator()->SetGoal( GOALTYPE_ENEMY ) )
{
TaskComplete();
}
else
{
// no way to get there =(
DevWarning( 2, "GetPathToEnemy failed!!\n" );
RememberUnreachable(GetEnemy());
TaskFail(FAIL_NO_ROUTE);
}
break;
}
case TASK_GET_PATH_TO_ENEMY_CORPSE:
{
Vector forward;
AngleVectors( GetLocalAngles(), &forward );
Vector vecEnemyLKP = GetEnemyLKP();
GetNavigator()->SetGoal( vecEnemyLKP - forward * 64, AIN_CLEAR_TARGET);
}
break;
case TASK_GET_PATH_TO_PLAYER:
{
CBaseEntity *pPlayer = gEntList.FindEntityByName( NULL, "!player" );
AI_NavGoal_t goal;
goal.type = GOALTYPE_LOCATION;
goal.dest = pPlayer->WorldSpaceCenter();
goal.pTarget = pPlayer;
GetNavigator()->SetGoal( goal );
break;
}
case TASK_GET_PATH_TO_SAVEPOSITION_LOS:
{
if ( GetEnemy() == NULL )
{
TaskFail(FAIL_NO_ENEMY);
return;
}
float flMaxRange = 2000;
float flMinRange = 0;
if ( GetActiveWeapon() )
{
flMaxRange = MAX(GetActiveWeapon()->m_fMaxRange1,GetActiveWeapon()->m_fMaxRange2);
flMinRange = MIN(GetActiveWeapon()->m_fMinRange1,GetActiveWeapon()->m_fMinRange2);
}
else if ( CapabilitiesGet() & bits_CAP_INNATE_RANGE_ATTACK1 )
{
flMaxRange = InnateRange1MaxRange();
flMinRange = InnateRange1MinRange();
}
// Check against NPC's MAX range
if (flMaxRange > m_flDistTooFar)
{
flMaxRange = m_flDistTooFar;
}
Vector posLos;
if (GetTacticalServices()->FindLos(m_vSavePosition,m_vSavePosition, flMinRange, flMaxRange, 1.0, &posLos))
{
GetNavigator()->SetGoal( AI_NavGoal_t( posLos, ACT_RUN, AIN_HULL_TOLERANCE ) );
}
else
{
// no coverwhatsoever.
TaskFail(FAIL_NO_SHOOT);
}
break;
}
case TASK_GET_PATH_TO_TARGET_WEAPON:
{
// Finds the nearest node within the leniency distances,
// whether the node can see the target or not.
const float XY_LENIENCY = 64.0;
const float Z_LENIENCY = 72.0;
if (m_hTargetEnt == NULL)
{
TaskFail(FAIL_NO_TARGET);
}
else
{
// Since this weapon MAY be on a table, we find the nearest node without verifying
// line-of-sight, since weapons on the table will not be able to see nodes very nearby.
int node = GetNavigator()->GetNetwork()->NearestNodeToPoint( this, m_hTargetEnt->GetAbsOrigin(), false );
CAI_Node *pNode = GetNavigator()->GetNetwork()->GetNode( node );
if( !pNode )
{
TaskFail( FAIL_NO_ROUTE );
break;
}
bool bHasPath = true;
Vector vecNodePos;
vecNodePos = pNode->GetPosition( GetHullType() );
float flDistZ;
flDistZ = fabs( vecNodePos.z - m_hTargetEnt->GetAbsOrigin().z );
if( flDistZ > Z_LENIENCY )
{
// The gun is too far away from its nearest node on the Z axis.
TaskFail( "Target not within Z_LENIENCY!\n");
CBaseCombatWeapon *pWeapon = ToBaseCombatWeapon( m_hTargetEnt.Get() );
if( pWeapon )
{
// Lock this weapon for a long time so no one else tries to get it.
pWeapon->Lock( 30.0f, pWeapon );
break;
}
}
if( flDistZ >= 16.0 )
{
// The gun is higher or lower, but it's within reach. (probably on a table).
float flDistXY = ( vecNodePos - m_hTargetEnt->GetAbsOrigin() ).Length2D();
// This means we have to stand on the nearest node and still be able to
// reach the gun.
if( flDistXY > XY_LENIENCY )
{
TaskFail( "Target not within XY_LENIENCY!\n" );
CBaseCombatWeapon *pWeapon = ToBaseCombatWeapon( m_hTargetEnt.Get() );
if( pWeapon )
{
// Lock this weapon for a long time so no one else tries to get it.
pWeapon->Lock( 30.0f, pWeapon );
break;
}
}
AI_NavGoal_t goal( vecNodePos );
goal.pTarget = m_hTargetEnt;
bHasPath = GetNavigator()->SetGoal( goal );
}
else
{
// The gun is likely just lying on the floor. Just pick it up.
AI_NavGoal_t goal( m_hTargetEnt->GetAbsOrigin() );
goal.pTarget = m_hTargetEnt;
bHasPath = GetNavigator()->SetGoal( goal );
}
if( !bHasPath )
{
CBaseCombatWeapon *pWeapon = ToBaseCombatWeapon( m_hTargetEnt.Get() );
if( pWeapon )
{
// Lock this weapon for a long time so no one else tries to get it.
pWeapon->Lock( 15.0f, pWeapon );
}
}
}
}
break;
case TASK_GET_PATH_OFF_OF_NPC:
{
Assert( ( GetGroundEntity() && ( GetGroundEntity()->IsPlayer() || ( GetGroundEntity()->IsNPC() && IRelationType( GetGroundEntity() ) == D_LI ) ) ) );
GetNavigator()->SetAllowBigStep( GetGroundEntity() );
ChainStartTask( TASK_MOVE_AWAY_PATH, 48 );
}
break;
case TASK_GET_PATH_TO_TARGET:
{
if (m_hTargetEnt == NULL)
{
TaskFail(FAIL_NO_TARGET);
}
else
{
AI_NavGoal_t goal( static_cast<const Vector&>(m_hTargetEnt->EyePosition()) );
goal.pTarget = m_hTargetEnt;
GetNavigator()->SetGoal( goal );
}
break;
}
case TASK_GET_PATH_TO_HINTNODE:// for active idles!
{
if (!GetHintNode())
{
TaskFail(FAIL_NO_HINT_NODE);
}
else
{
Vector vHintPos;
GetHintNode()->GetPosition(this, &vHintPos);
GetNavigator()->SetGoal( AI_NavGoal_t( vHintPos, ACT_RUN ) );
if ( pTask->flTaskData == 0 )
GetNavigator()->SetArrivalDirection( GetHintNode()->GetDirection() );
if ( GetHintNode()->HintActivityName() != NULL_STRING )
{
Activity hintActivity = (Activity)CAI_BaseNPC::GetActivityID( STRING(GetHintNode()->HintActivityName()) );
if ( hintActivity != ACT_INVALID )
{
GetNavigator()->SetArrivalActivity( GetHintActivity(GetHintNode()->HintType(), hintActivity) );
}
else
{
int iSequence = LookupSequence(STRING(GetHintNode()->HintActivityName()));;
if ( iSequence != ACT_INVALID )
GetNavigator()->SetArrivalSequence( iSequence );
}
}
}
break;
}
case TASK_GET_PATH_TO_COMMAND_GOAL:
{
if (!GetNavigator()->SetGoal( m_vecCommandGoal ))
{
OnMoveToCommandGoalFailed();
TaskFail(FAIL_NO_ROUTE);
}
break;
}
case TASK_MARK_COMMAND_GOAL_POS:
// Start watching my position to detect whether another AI process has moved me from my mark.
m_CommandMoveMonitor.SetMark( this, COMMAND_GOAL_TOLERANCE );
TaskComplete();
break;
case TASK_CLEAR_COMMAND_GOAL:
m_vecCommandGoal = vec3_invalid;
TaskComplete();
break;
case TASK_GET_PATH_TO_LASTPOSITION:
{
if (!GetNavigator()->SetGoal( m_vecLastPosition ))
{
TaskFail(FAIL_NO_ROUTE);
}
else
{
GetNavigator()->SetGoalTolerance( 48 );
}
break;
}
case TASK_GET_PATH_TO_SAVEPOSITION:
{
GetNavigator()->SetGoal( m_vSavePosition );
break;
}
case TASK_GET_PATH_TO_RANDOM_NODE: // Task argument is lenth of path to build
{
if ( GetNavigator()->SetRandomGoal( pTask->flTaskData ) )
TaskComplete();
else
TaskFail(FAIL_NO_REACHABLE_NODE);
break;
}
case TASK_GET_PATH_TO_BESTSOUND:
{
CSound *pSound = GetBestSound();
if (!pSound)
{
TaskFail(FAIL_NO_SOUND);
}
else
{
GetNavigator()->SetGoal( pSound->GetSoundReactOrigin() );
}
break;
}
case TASK_GET_PATH_TO_BESTSCENT:
{
CSound *pScent = GetBestScent();
if (!pScent)
{
TaskFail(FAIL_NO_SCENT);
}
else
{
GetNavigator()->SetGoal( pScent->GetSoundOrigin() );
}
break;
}
case TASK_GET_PATH_AWAY_FROM_BEST_SOUND:
{
CSound *pBestSound = GetBestSound();
if ( !pBestSound )
{
TaskFail("No Sound!");
break;
}
GetMotor()->SetIdealYawToTarget( pBestSound->GetSoundOrigin() );
ChainStartTask( TASK_MOVE_AWAY_PATH, pTask->flTaskData );
LockBestSound();
break;
}
case TASK_MOVE_AWAY_PATH:
{
// Drop into run task to support interrupt
DesireStand();
}
break;
case TASK_WEAPON_RUN_PATH:
case TASK_ITEM_RUN_PATH:
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
case TASK_RUN_PATH:
{
// UNDONE: This is in some default AI and some NPCs can't run? -- walk instead?
if ( TranslateActivity( ACT_RUN ) != ACT_INVALID )
{
GetNavigator()->SetMovementActivity( ACT_RUN );
}
else
{
GetNavigator()->SetMovementActivity(ACT_WALK);
}
// Cover is void once I move
Forget( bits_MEMORY_INCOVER );
TaskComplete();
break;
}
case TASK_WALK_PATH_FOR_UNITS:
{
GetNavigator()->SetMovementActivity(ACT_WALK);
break;
}
case TASK_RUN_PATH_FOR_UNITS:
{
GetNavigator()->SetMovementActivity(ACT_RUN);
break;
}
case TASK_WALK_PATH:
{
bool bIsFlying = (GetMoveType() == MOVETYPE_FLY) || (GetMoveType() == MOVETYPE_FLYGRAVITY);
if ( bIsFlying && ( TranslateActivity( ACT_FLY ) != ACT_INVALID) )
{
GetNavigator()->SetMovementActivity(ACT_FLY);
}
else if ( TranslateActivity( ACT_WALK ) != ACT_INVALID )
{
GetNavigator()->SetMovementActivity(ACT_WALK);
}
else
{
GetNavigator()->SetMovementActivity(ACT_RUN);
}
// Cover is void once I move
Forget( bits_MEMORY_INCOVER );
TaskComplete();
break;
}
case TASK_WALK_PATH_WITHIN_DIST:
{
GetNavigator()->SetMovementActivity(ACT_WALK);
// set that we're probably going to stop before the goal
GetNavigator()->SetArrivalDistance( pTask->flTaskData );
break;
}
case TASK_RUN_PATH_WITHIN_DIST:
{
GetNavigator()->SetMovementActivity(ACT_RUN);
// set that we're probably going to stop before the goal
GetNavigator()->SetArrivalDistance( pTask->flTaskData );
break;
}
case TASK_RUN_PATH_FLEE:
{
Vector vecDiff;
vecDiff = GetLocalOrigin() - GetNavigator()->GetGoalPos();
if( vecDiff.Length() <= pTask->flTaskData )
{
GetNavigator()->StopMoving();
TaskFail("Flee path shorter than task parameter");
}
else
{
GetNavigator()->SetMovementActivity(ACT_RUN);
}
break;
}
case TASK_WALK_PATH_TIMED:
{
GetNavigator()->SetMovementActivity(ACT_WALK);
SetWait( pTask->flTaskData );
break;
}
case TASK_RUN_PATH_TIMED:
{
GetNavigator()->SetMovementActivity(ACT_RUN);
SetWait( pTask->flTaskData );
break;
}
case TASK_STRAFE_PATH:
{
Vector2D vec2DirToPoint;
Vector2D vec2RightSide;
// to start strafing, we have to first figure out if the target is on the left side or right side
Vector right;
AngleVectors( GetLocalAngles(), NULL, &right, NULL );
vec2DirToPoint = ( GetNavigator()->GetCurWaypointPos() - GetLocalOrigin() ).AsVector2D();
Vector2DNormalize(vec2DirToPoint);
vec2RightSide = right.AsVector2D();
Vector2DNormalize(vec2RightSide);
if ( DotProduct2D ( vec2DirToPoint, vec2RightSide ) > 0 )
{
// strafe right
GetNavigator()->SetMovementActivity(ACT_STRAFE_RIGHT);
}
else
{
// strafe left
GetNavigator()->SetMovementActivity(ACT_STRAFE_LEFT);
}
TaskComplete();
break;
}
case TASK_WAIT_FOR_MOVEMENT_STEP:
{
if ( IsMovementFrozen() )
{
TaskFail(FAIL_FROZEN);
break;
}
if(!GetNavigator()->IsGoalActive())
{
TaskComplete();
return;
}
if ( IsActivityFinished() )
{
TaskComplete();
return;
}
ValidateNavGoal();
break;
}
case TASK_WAIT_FOR_MOVEMENT:
{
if ( IsMovementFrozen() )
{
TaskFail(FAIL_FROZEN);
break;
}
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
{
TaskComplete();
GetNavigator()->ClearGoal(); // Clear residual state
}
else if (!GetNavigator()->IsGoalActive())
{
SetIdealActivity( GetStoppedActivity() );
}
else
{
// Check validity of goal type
ValidateNavGoal();
}
break;
}
case TASK_SMALL_FLINCH:
{
Remember(bits_MEMORY_FLINCHED);
SetIdealActivity( GetFlinchActivity( false, false ) );
m_flNextFlinchTime = gpGlobals->curtime + random->RandomFloat( 3, 5 );
break;
}
case TASK_BIG_FLINCH:
{
Remember(bits_MEMORY_FLINCHED);
SetIdealActivity( GetFlinchActivity( true, false ) );
m_flNextFlinchTime = gpGlobals->curtime + random->RandomFloat( 3, 5 );
break;
}
case TASK_DIE:
{
GetNavigator()->StopMoving();
SetIdealActivity( GetDeathActivity() );
m_lifeState = LIFE_DYING;
break;
}
case TASK_SOUND_WAKE:
{
AlertSound();
TaskComplete();
break;
}
case TASK_SOUND_DIE:
{
CTakeDamageInfo info;
DeathSound( info );
TaskComplete();
break;
}
case TASK_SOUND_IDLE:
{
IdleSound();
TaskComplete();
break;
}
case TASK_SOUND_PAIN:
{
CTakeDamageInfo info;
PainSound( info );
TaskComplete();
break;
}
case TASK_SOUND_ANGRY:
{
// sounds are complete as soon as we get here, cause we've already played them.
DevMsg( 2, "SOUND\n" );
TaskComplete();
break;
}
case TASK_SPEAK_SENTENCE:
{
SpeakSentence(pTask->flTaskData);
TaskComplete();
break;
}
case TASK_WAIT_FOR_SPEAK_FINISH:
{
if ( !GetExpresser() )
TaskComplete();
else
{
// Are we waiting for our speech to end? Or for the mutex to be free?
if ( pTask->flTaskData )
{
// Waiting for our speech to end
if ( GetExpresser()->CanSpeakAfterMyself() )
{
TaskComplete();
}
}
else
{
// Waiting for the speech & the delay afterwards
if ( !GetExpresser()->IsSpeaking() )
{
TaskComplete();
}
}
break;
}
break;
}
case TASK_WAIT_FOR_SCRIPT:
{
if ( !m_hCine )
{
DevMsg( "Scripted sequence destroyed while in use\n" );
TaskFail( FAIL_SCHEDULE_NOT_FOUND );
break;
}
break;
}
case TASK_PUSH_SCRIPT_ARRIVAL_ACTIVITY:
{
if ( !m_hCine )
{
DevMsg( "Scripted sequence destroyed while in use\n" );
TaskFail( FAIL_SCHEDULE_NOT_FOUND );
break;
}
string_t iszArrivalText;
if ( m_hCine->m_iszEntry != NULL_STRING )
{
iszArrivalText = m_hCine->m_iszEntry;
}
else if ( m_hCine->m_iszPlay != NULL_STRING )
{
iszArrivalText = m_hCine->m_iszPlay;
}
else if ( m_hCine->m_iszPostIdle != NULL_STRING )
{
iszArrivalText = m_hCine->m_iszPostIdle;
}
else
iszArrivalText = NULL_STRING;
m_ScriptArrivalActivity = AIN_DEF_ACTIVITY;
m_strScriptArrivalSequence = NULL_STRING;
if ( iszArrivalText != NULL_STRING )
{
m_ScriptArrivalActivity = (Activity)GetActivityID( STRING( iszArrivalText ) );
if ( m_ScriptArrivalActivity == ACT_INVALID )
m_strScriptArrivalSequence = iszArrivalText;
}
TaskComplete();
break;
}
case TASK_PLAY_SCRIPT:
{
// Throw away any stopping paths we have saved, because we
// won't be able to resume them after the sequence.
GetNavigator()->IgnoreStoppingPath();
if ( HasMovement( GetSequence() ) || m_hCine->m_bIgnoreGravity )
{
AddFlag( FL_FLY );
SetGroundEntity( NULL );
}
if (m_hCine)
{
m_hCine->SynchronizeSequence( this );
}
//
// Start playing a scripted sequence.
//
m_scriptState = SCRIPT_PLAYING;
break;
}
case TASK_PLAY_SCRIPT_POST_IDLE:
{
//
// Start playing a scripted post idle.
//
m_scriptState = SCRIPT_POST_IDLE;
break;
}
// This is the first task of every schedule driven by a scripted_sequence.
// Delay starting the sequence until all actors have hit their marks.
case TASK_PRE_SCRIPT:
{
if ( !ai_task_pre_script.GetBool() )
{
TaskComplete();
}
else if ( !m_hCine )
{
TaskComplete();
//DevMsg( "Scripted sequence destroyed while in use\n" );
//TaskFail( FAIL_SCHEDULE_NOT_FOUND );
}
else
{
m_hCine->DelayStart( true );
TaskComplete();
}
break;
}
case TASK_ENABLE_SCRIPT:
{
//
// Start waiting to play a script. Play the script's pre idle animation if one
// is specified, otherwise just go to our default idle activity.
//
if ( m_hCine->m_iszPreIdle != NULL_STRING )
{
m_hCine->StartSequence( ( CAI_BaseNPC * )this, m_hCine->m_iszPreIdle, false );
if ( FStrEq( STRING( m_hCine->m_iszPreIdle ), STRING( m_hCine->m_iszPlay ) ) )
{
m_flPlaybackRate = 0;
}
}
else if ( m_scriptState != SCRIPT_CUSTOM_MOVE_TO_MARK )
{
// FIXME: too many ss assume its safe to leave the npc is whatever sequence they were in before, so only slam their activity
// if they're playing a recognizable movement animation
//
#ifdef HL2_EPISODIC
// dvs: Check current activity rather than ideal activity. Since scripted NPCs early out in MaintainActivity,
// they'll never reach their ideal activity if it's different from their current activity.
if ( GetActivity() == ACT_WALK ||
GetActivity() == ACT_RUN ||
GetActivity() == ACT_WALK_AIM ||
GetActivity() == ACT_RUN_AIM )
{
SetActivity( ACT_IDLE );
}
#else
if ( GetIdealActivity() == ACT_WALK ||
GetIdealActivity() == ACT_RUN ||
GetIdealActivity() == ACT_WALK_AIM ||
GetIdealActivity() == ACT_RUN_AIM )
{
SetActivity( ACT_IDLE );
}
#endif // HL2_EPISODIC
}
break;
}
case TASK_PLANT_ON_SCRIPT:
{
if ( m_hTargetEnt != NULL )
{
SetLocalOrigin( m_hTargetEnt->GetAbsOrigin() ); // Plant on target
}
TaskComplete();
break;
}
case TASK_FACE_SCRIPT:
{
if ( m_hTargetEnt != NULL )
{
GetMotor()->SetIdealYaw( UTIL_AngleMod( m_hTargetEnt->GetLocalAngles().y ) );
}
if ( m_scriptState != SCRIPT_CUSTOM_MOVE_TO_MARK )
{
SetTurnActivity();
// dvs: HACK: MaintainActivity won't do anything while scripted, so go straight there.
SetActivity( GetIdealActivity() );
}
GetNavigator()->StopMoving();
break;
}
case TASK_PLAY_SCENE:
{
// inside a scene with movement and sequence commands
break;
}
case TASK_SUGGEST_STATE:
{
SetIdealState( (NPC_STATE)(int)pTask->flTaskData );
TaskComplete();
break;
}
case TASK_SET_FAIL_SCHEDULE:
m_failSchedule = (int)pTask->flTaskData;
TaskComplete();
break;
case TASK_SET_TOLERANCE_DISTANCE:
GetNavigator()->SetGoalTolerance( (int)pTask->flTaskData );
TaskComplete();
break;
case TASK_SET_ROUTE_SEARCH_TIME:
GetNavigator()->SetMaxRouteRebuildTime( (int)pTask->flTaskData );
TaskComplete();
break;
case TASK_CLEAR_FAIL_SCHEDULE:
m_failSchedule = SCHED_NONE;
TaskComplete();
break;
case TASK_WEAPON_FIND:
{
m_hTargetEnt = Weapon_FindUsable( Vector(1000,1000,1000) );
if (m_hTargetEnt)
{
TaskComplete();
}
else
{
TaskFail(FAIL_ITEM_NO_FIND);
}
}
break;
case TASK_ITEM_PICKUP:
{
SetIdealActivity( ACT_PICKUP_GROUND );
}
break;
case TASK_WEAPON_PICKUP:
{
if( GetActiveWeapon() )
{
Weapon_Drop( GetActiveWeapon() );
}
if( GetTarget() )
{
CBaseCombatWeapon *pWeapon = ToBaseCombatWeapon(GetTarget());
if( pWeapon )
{
if( Weapon_IsOnGround( pWeapon ) )
{
// Squat down
SetIdealActivity( ACT_PICKUP_GROUND );
}
else
{
// Reach and take this weapon from rack or shelf.
SetIdealActivity( ACT_PICKUP_RACK );
}
return;
}
}
TaskFail("Weapon went away!\n");
}
break;
case TASK_WEAPON_CREATE:
{
if( !GetActiveWeapon() && GetTarget() )
{
// Create a copy of the weapon this NPC is trying to pick up.
CBaseCombatWeapon *pTargetWeapon = ToBaseCombatWeapon(GetTarget());
if( pTargetWeapon )
{
CBaseCombatWeapon *pWeapon = Weapon_Create( pTargetWeapon->GetClassname() );
if ( pWeapon )
{
Weapon_Equip( pWeapon );
}
}
}
SetTarget( NULL );
TaskComplete();
}
break;
case TASK_USE_SMALL_HULL:
{
SetHullSizeSmall();
TaskComplete();
}
break;
case TASK_FALL_TO_GROUND:
// Set a wait time to try to force a ground ent.
SetWait(4);
break;
case TASK_WANDER:
{
// This task really uses 2 parameters, so we have to extract
// them from a single integer. To send both parameters, the
// formula is MIN_DIST * 10000 + MAX_DIST
{
int iMinDist, iMaxDist, iParameter;
iParameter = pTask->flTaskData;
iMinDist = iParameter / 10000;
iMaxDist = iParameter - (iMinDist * 10000);
if ( GetNavigator()->SetWanderGoal( iMinDist, iMaxDist ) )
TaskComplete();
else
TaskFail(FAIL_NO_REACHABLE_NODE);
}
}
break;
case TASK_FREEZE:
m_flPlaybackRate = 0;
break;
case TASK_GATHER_CONDITIONS:
GatherConditions();
TaskComplete();
break;
case TASK_IGNORE_OLD_ENEMIES:
m_flAcceptableTimeSeenEnemy = gpGlobals->curtime;
if ( GetEnemy() && GetEnemyLastTimeSeen() < m_flAcceptableTimeSeenEnemy )
{
CBaseEntity *pNewEnemy = BestEnemy();
Assert( pNewEnemy != GetEnemy() );
if( pNewEnemy != NULL )
{
// New enemy! Clear the timers and set conditions.
SetEnemy( pNewEnemy );
SetState( NPC_STATE_COMBAT );
}
else
{
SetEnemy( NULL );
ClearAttackConditions();
}
}
TaskComplete();
break;
case TASK_ADD_HEALTH:
TakeHealth( (int)pTask->flTaskData, DMG_GENERIC );
TaskComplete();
break;
default:
{
DevMsg( "No StartTask entry for %s\n", TaskName( task ) );
}
break;
}
}
void CAI_BaseNPC::StartTaskOverlay()
{
if ( IsCurTaskContinuousMove() )
{
if ( ShouldMoveAndShoot() )
{
m_MoveAndShootOverlay.StartShootWhileMove();
}
else
{
m_MoveAndShootOverlay.NoShootWhileMove();
}
}
}
//-----------------------------------------------------------------------------
// TASK_DIE.
//-----------------------------------------------------------------------------
void CAI_BaseNPC::RunDieTask()
{
AutoMovement();
if ( IsActivityFinished() && GetCycle() >= 1.0f )
{
m_lifeState = LIFE_DEAD;
SetThink ( NULL );
StopAnimation();
if ( !BBoxFlat() )
{
// a bit of a hack. If a corpses' bbox is positioned such that being left solid so that it can be attacked will
// block the player on a slope or stairs, the corpse is made nonsolid.
// SetSolid( SOLID_NOT );
UTIL_SetSize ( this, Vector ( -4, -4, 0 ), Vector ( 4, 4, 1 ) );
}
else // !!!HACKHACK - put NPC in a thin, wide bounding box until we fix the solid type/bounding volume problem
UTIL_SetSize ( this, WorldAlignMins(), Vector ( WorldAlignMaxs().x, WorldAlignMaxs().y, WorldAlignMins().z + 1 ) );
}
}
//-----------------------------------------------------------------------------
// TASK_RANGE_ATTACK1 / TASK_RANGE_ATTACK2 / etc.
//-----------------------------------------------------------------------------
void CAI_BaseNPC::RunAttackTask( int task )
{
AutoMovement( );
Vector vecEnemyLKP = GetEnemyLKP();
// If our enemy was killed, but I'm not done animating, the last known position comes
// back as the origin and makes the me face the world origin if my attack schedule
// doesn't break when my enemy dies. (sjb)
if( vecEnemyLKP != vec3_origin )
{
if ( ( task == TASK_RANGE_ATTACK1 || task == TASK_RELOAD ) &&
( CapabilitiesGet() & bits_CAP_AIM_GUN ) &&
FInAimCone( vecEnemyLKP ) )
{
// Arms will aim, so leave body yaw as is
GetMotor()->SetIdealYawAndUpdate( GetMotor()->GetIdealYaw(), AI_KEEP_YAW_SPEED );
}
else
{
GetMotor()->SetIdealYawToTargetAndUpdate( vecEnemyLKP, AI_KEEP_YAW_SPEED );
}
}
if ( IsActivityFinished() )
{
if ( task == TASK_RELOAD && GetShotRegulator() )
{
GetShotRegulator()->Reset( false );
}
TaskComplete();
}
}
//=========================================================
// RunTask
//=========================================================
void CAI_BaseNPC::RunTask( const Task_t *pTask )
{
VPROF_BUDGET( "CAI_BaseNPC::RunTask", VPROF_BUDGETGROUP_NPCS );
switch ( pTask->iTask )
{
case TASK_GET_PATH_TO_RANDOM_NODE:
{
break;
}
case TASK_TURN_RIGHT:
case TASK_TURN_LEFT:
{
// If the yaw is locked, this function will not act correctly
Assert( GetMotor()->IsYawLocked() == false );
GetMotor()->UpdateYaw();
if ( FacingIdeal() )
{
TaskComplete();
}
break;
}
case TASK_PLAY_PRIVATE_SEQUENCE_FACE_ENEMY:
case TASK_PLAY_SEQUENCE_FACE_ENEMY:
case TASK_PLAY_SEQUENCE_FACE_TARGET:
{
CBaseEntity *pTarget;
if ( pTask->iTask == TASK_PLAY_SEQUENCE_FACE_TARGET )
pTarget = m_hTargetEnt;
else
pTarget = GetEnemy();
if ( pTarget )
{
GetMotor()->SetIdealYawAndUpdate( pTarget->GetAbsOrigin() - GetLocalOrigin() , AI_KEEP_YAW_SPEED );
}
if ( IsActivityFinished() )
{
TaskComplete();
}
}
break;
case TASK_PLAY_HINT_ACTIVITY:
{
if (!GetHintNode())
{
TaskFail(FAIL_NO_HINT_NODE);
}
// Put a debugging check in here
if (GetHintNode()->User() != this)
{
DevMsg("Hint node (%s) being used by non-owner!\n",GetHintNode()->GetDebugName());
}
if ( IsActivityFinished() )
{
TaskComplete();
}
break;
}
case TASK_STOP_MOVING:
{
if ( pTask->flTaskData == 1 )
{
ChainRunTask( TASK_WAIT_FOR_MOVEMENT );
if ( GetTaskStatus() == TASKSTATUS_COMPLETE )
{
DbgNavMsg( this, "TASK_STOP_MOVING Complete\n" );
}
}
else
{
// if they're jumping, wait until they land
if (GetNavType() == NAV_JUMP)
{
if (GetFlags() & FL_ONGROUND)
{
DbgNavMsg( this, "Jump landed\n" );
SetNavType( NAV_GROUND ); // this assumes that NAV_JUMP only happens with npcs that use NAV_GROUND as base movement
}
else if (GetSmoothedVelocity().Length() > 0.01) // use an EPSILON damnit!!
{
// wait until you land
break;
}
else
{
DbgNavMsg( this, "Jump stuck\n" );
// stopped and stuck!
SetNavType( NAV_GROUND );
TaskFail( FAIL_STUCK_ONTOP );
}
}
// @TODO (toml 10-30-02): this is unacceptable, but needed until navigation can handle commencing
// a navigation while in the middle of a climb
if (GetNavType() == NAV_CLIMB)
{
// wait until you reach the end
break;
}
DbgNavMsg( this, "TASK_STOP_MOVING Complete\n" );
SetIdealActivity( GetStoppedActivity() );
TaskComplete();
}
break;
}
case TASK_PLAY_SEQUENCE:
case TASK_PLAY_PRIVATE_SEQUENCE:
{
AutoMovement( );
if ( IsActivityFinished() )
{
TaskComplete();
}
break;
}
case TASK_ADD_GESTURE_WAIT:
{
if ( IsWaitFinished() )
{
TaskComplete();
}
break;
}
case TASK_SET_ACTIVITY:
{
if ( IsActivityStarted() )
{
TaskComplete();
}
}
break;
case TASK_FACE_ENEMY:
{
// If the yaw is locked, this function will not act correctly
Assert( GetMotor()->IsYawLocked() == false );
Vector vecEnemyLKP = GetEnemyLKP();
if (!FInAimCone( vecEnemyLKP ))
{
GetMotor()->SetIdealYawToTarget( vecEnemyLKP );
GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw
}
else
{
float flReasonableFacing = CalcReasonableFacing( true );
if ( fabsf( flReasonableFacing - GetMotor()->GetIdealYaw() ) > 1 )
GetMotor()->SetIdealYaw( flReasonableFacing );
}
GetMotor()->UpdateYaw();
if ( FacingIdeal( m_flFaceEnemyTolerance ) )
{
TaskComplete();
}
break;
}
case TASK_FACE_PLAYER:
{
// Get edict for one player
CBasePlayer *pPlayer = AI_GetSinglePlayer();
if ( pPlayer )
{
GetMotor()->SetIdealYawToTargetAndUpdate( pPlayer->GetAbsOrigin(), AI_KEEP_YAW_SPEED );
SetTurnActivity();
if ( IsWaitFinished() && GetMotor()->DeltaIdealYaw() < 10 )
{
TaskComplete();
}
}
else
{
TaskFail(FAIL_NO_PLAYER);
}
}
break;
case TASK_FIND_COVER_FROM_BEST_SOUND:
{
switch( GetTaskInterrupt() )
{
case 0:
{
if ( !FindCoverFromBestSound( &m_vInterruptSavePosition ) )
TaskFail(FAIL_NO_COVER);
else
{
GetNavigator()->IgnoreStoppingPath();
LockBestSound();
TaskInterrupt();
}
}
break;
case 1:
{
AI_NavGoal_t goal(m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE);
CSound *pBestSound = GetBestSound();
if ( pBestSound )
goal.maxInitialSimplificationDist = pBestSound->Volume() * 0.5;
if ( GetNavigator()->SetGoal( goal ) )
{
m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData;
}
}
break;
}
}
break;
case TASK_FACE_HINTNODE:
case TASK_FACE_LASTPOSITION:
case TASK_FACE_SAVEPOSITION:
case TASK_FACE_AWAY_FROM_SAVEPOSITION:
case TASK_FACE_TARGET:
case TASK_FACE_IDEAL:
case TASK_FACE_SCRIPT:
case TASK_FACE_PATH:
{
// If the yaw is locked, this function will not act correctly
Assert( GetMotor()->IsYawLocked() == false );
GetMotor()->UpdateYaw();
if ( FacingIdeal() )
{
TaskComplete();
}
break;
}
case TASK_FACE_REASONABLE:
{
// If the yaw is locked, this function will not act correctly
Assert( GetMotor()->IsYawLocked() == false );
GetMotor()->UpdateYaw();
if ( FacingIdeal() )
{
TaskComplete();
}
break;
}
case TASK_WAIT_PVS:
{
if ( ShouldAlwaysThink() ||
UTIL_FindClientInPVS(edict()) ||
( GetState() == NPC_STATE_COMBAT && GetEnemy() && gpGlobals->curtime - GetEnemies()->LastTimeSeen( GetEnemy() ) < 15 ) )
{
TaskComplete();
}
break;
}
case TASK_WAIT_INDEFINITE:
{
// don't do anything.
break;
}
case TASK_WAIT:
case TASK_WAIT_RANDOM:
{
if ( IsWaitFinished() )
{
TaskComplete();
}
break;
}
case TASK_WAIT_FACE_ENEMY:
case TASK_WAIT_FACE_ENEMY_RANDOM:
{
Vector vecEnemyLKP = GetEnemyLKP();
if (!FInAimCone( vecEnemyLKP ))
{
GetMotor()->SetIdealYawToTargetAndUpdate( vecEnemyLKP , AI_KEEP_YAW_SPEED );
}
if ( IsWaitFinished() )
{
TaskComplete();
}
break;
}
case TASK_WAIT_UNTIL_NO_DANGER_SOUND:
if( !HasCondition( COND_HEAR_DANGER ) )
{
TaskComplete();
}
break;
case TASK_MOVE_TO_TARGET_RANGE:
case TASK_MOVE_TO_GOAL_RANGE:
{
// Identical tasks, except that target_range uses m_hTargetEnt,
// and Goal range uses the nav goal
CBaseEntity *pTarget = NULL;
if ( pTask->iTask == TASK_MOVE_TO_GOAL_RANGE )
{
pTarget = GetNavigator()->GetGoalTarget();
}
if ( !pTarget )
{
pTarget = m_hTargetEnt.Get();
}
float distance;
if ( pTarget == NULL )
{
TaskFail(FAIL_NO_TARGET);
}
else if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
{
TaskComplete();
GetNavigator()->ClearGoal(); // Clear residual state
}
else
{
bool bForceRun = false;
// Check Z first, and only check 2d if we're within that
Vector vecGoalPos = GetNavigator()->GetGoalPos();
distance = fabs(vecGoalPos.z - GetLocalOrigin().z);
if ( distance < pTask->flTaskData )
{
distance = ( vecGoalPos - GetLocalOrigin() ).Length2D();
}
else
{
// If the target is significantly higher or lower than me, I must run.
bForceRun = true;
}
// If we're jumping, wait until we're finished to update our goal position.
if ( GetNavigator()->GetNavType() != NAV_JUMP )
{
// Re-evaluate when you think your finished, or the target has moved too far
if ( (distance < pTask->flTaskData) || (vecGoalPos - pTarget->GetAbsOrigin()).Length() > pTask->flTaskData * 0.5 )
{
distance = ( pTarget->GetAbsOrigin() - GetLocalOrigin() ).Length2D();
if ( !GetNavigator()->UpdateGoalPos( pTarget->GetAbsOrigin() ) )
{
TaskFail( FAIL_NO_ROUTE );
break;
}
}
}
// 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
if ( distance < pTask->flTaskData )
{
TaskComplete();
#ifndef HL2_DLL
// HL2 uses TASK_STOP_MOVING
GetNavigator()->StopMoving(); // Stop moving
#endif
}
else
{
// Pick the right movement activity.
Activity followActivity;
if( bForceRun )
{
followActivity = ACT_RUN;
}
else
{
followActivity = ( distance < 190 && m_NPCState != NPC_STATE_COMBAT ) ? ACT_WALK : ACT_RUN;
}
// Don't confuse move and shoot by resetting the activity every think
Activity curActivity = GetNavigator()->GetMovementActivity();
switch( curActivity )
{
case ACT_WALK_AIM: curActivity = ACT_WALK; break;
case ACT_RUN_AIM: curActivity = ACT_RUN; break;
}
if ( curActivity != followActivity )
{
GetNavigator()->SetMovementActivity(followActivity);
}
GetNavigator()->SetArrivalDirection( pTarget );
}
}
break;
}
case TASK_GET_PATH_TO_ENEMY_LOS:
case TASK_GET_FLANK_RADIUS_PATH_TO_ENEMY_LOS:
case TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS:
case TASK_GET_PATH_TO_ENEMY_LKP_LOS:
{
if ( GetEnemy() == NULL )
{
TaskFail(FAIL_NO_ENEMY);
return;
}
if ( GetTaskInterrupt() > 0 )
{
ClearTaskInterrupt();
Vector vecEnemy = ( pTask->iTask == TASK_GET_PATH_TO_ENEMY_LOS ) ? GetEnemy()->GetAbsOrigin() : GetEnemyLKP();
AI_NavGoal_t goal( m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE );
GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET );
GetNavigator()->SetArrivalDirection( vecEnemy - goal.dest );
}
else
TaskInterrupt();
}
break;
case TASK_GET_PATH_AWAY_FROM_BEST_SOUND:
{
ChainRunTask( TASK_MOVE_AWAY_PATH, pTask->flTaskData );
if ( GetNavigator()->IsGoalActive() )
{
Vector vecDest = GetNavigator()->GetGoalPos();
float flDist = ( GetAbsOrigin() - vecDest ).Length();
if( flDist < 10.0 * 12.0 )
{
TaskFail("Path away from best sound too short!\n");
}
}
break;
}
case TASK_GET_PATH_OFF_OF_NPC:
{
if ( AI_IsSinglePlayer() )
{
GetNavigator()->SetAllowBigStep( UTIL_GetLocalPlayer() );
}
ChainRunTask( TASK_MOVE_AWAY_PATH, 48 );
}
break;
case TASK_MOVE_AWAY_PATH:
{
QAngle ang = GetLocalAngles();
ang.y = GetMotor()->GetIdealYaw() + 180;
Vector move;
switch ( GetTaskInterrupt() )
{
case 0:
{
if( IsPlayerAlly() )
{
// Look for a move away hint node.
CAI_Hint *pHint;
CHintCriteria hintCriteria;
hintCriteria.AddHintType( HINT_PLAYER_ALLY_MOVE_AWAY_DEST );
hintCriteria.SetFlag( bits_HINT_NODE_NEAREST );
hintCriteria.AddIncludePosition( GetAbsOrigin(), (20.0f * 12.0f) ); // 20 feet MAX
hintCriteria.AddExcludePosition( GetAbsOrigin(), 28.0f ); // don't plant on an hint that you start on
pHint = CAI_HintManager::FindHint( this, hintCriteria );
if( pHint )
{
CBasePlayer *pPlayer = AI_GetSinglePlayer();
Vector vecGoal = pHint->GetAbsOrigin();
if( vecGoal.DistToSqr(GetAbsOrigin()) < vecGoal.DistToSqr(pPlayer->GetAbsOrigin()) )
{
if( GetNavigator()->SetGoal(vecGoal) )
{
pHint->DisableForSeconds( 0.1f ); // Force others to find their own.
TaskComplete();
break;
}
}
}
}
#ifdef HL2_EPISODIC
// See if we're moving away from a vehicle
CSound *pBestSound = GetBestSound( SOUND_MOVE_AWAY );
if ( pBestSound && pBestSound->m_hOwner && pBestSound->m_hOwner->GetServerVehicle() )
{
// Move away from the vehicle's center, regardless of our facing
move = ( GetAbsOrigin() - pBestSound->m_hOwner->WorldSpaceCenter() );
VectorNormalize( move );
}
else
{
// Use the first angles
AngleVectors( ang, &move );
}
#else
AngleVectors( ang, &move );
#endif //HL2_EPISODIC
if ( GetNavigator()->SetVectorGoal( move, (float)pTask->flTaskData, MIN(36,pTask->flTaskData), true ) && IsValidMoveAwayDest( GetNavigator()->GetGoalPos() ))
{
TaskComplete();
}
else
{
ang.y = GetMotor()->GetIdealYaw() + 91;
AngleVectors( ang, &move );
if ( GetNavigator()->SetVectorGoal( move, (float)pTask->flTaskData, MIN(24,pTask->flTaskData), true ) && IsValidMoveAwayDest( GetNavigator()->GetGoalPos() ) )
{
TaskComplete();
}
else
{
TaskInterrupt();
}
}
}
break;
case 1:
{
ang.y = GetMotor()->GetIdealYaw() + 271;
AngleVectors( ang, &move );
if ( GetNavigator()->SetVectorGoal( move, (float)pTask->flTaskData, MIN(24,pTask->flTaskData), true ) && IsValidMoveAwayDest( GetNavigator()->GetGoalPos() ) )
{
TaskComplete();
}
else
{
ang.y = GetMotor()->GetIdealYaw() + 180;
while (ang.y < 0)
ang.y += 360;
while (ang.y >= 360)
ang.y -= 360;
if ( ang.y < 45 || ang.y >= 315 )
ang.y = 0;
else if ( ang.y < 135 )
ang.y = 90;
else if ( ang.y < 225 )
ang.y = 180;
else
ang.y = 270;
AngleVectors( ang, &move );
if ( GetNavigator()->SetVectorGoal( move, (float)pTask->flTaskData, MIN(6,pTask->flTaskData), false ) && IsValidMoveAwayDest( GetNavigator()->GetGoalPos() ) )
{
TaskComplete();
}
else
{
TaskInterrupt();
}
}
}
break;
case 2:
{
ClearTaskInterrupt();
Vector coverPos;
if ( GetTacticalServices()->FindCoverPos( GetLocalOrigin(), EyePosition(), 0, CoverRadius(), &coverPos ) && IsValidMoveAwayDest( GetNavigator()->GetGoalPos() ) )
{
GetNavigator()->SetGoal( AI_NavGoal_t( coverPos, ACT_RUN ) );
m_flMoveWaitFinished = gpGlobals->curtime + 2;
}
else
{
// no coverwhatsoever.
TaskFail(FAIL_NO_ROUTE);
}
}
break;
}
}
break;
case TASK_WEAPON_RUN_PATH:
case TASK_ITEM_RUN_PATH:
{
CBaseEntity *pTarget = m_hTargetEnt;
if ( pTarget )
{
if ( pTarget->GetOwnerEntity() )
{
TaskFail(FAIL_WEAPON_OWNED);
}
else if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
{
TaskComplete();
}
}
else
{
TaskFail(FAIL_ITEM_NO_FIND);
}
}
break;
case TASK_WAIT_FOR_MOVEMENT_STEP:
case TASK_WAIT_FOR_MOVEMENT:
{
if ( IsMovementFrozen() )
{
TaskFail(FAIL_FROZEN);
break;
}
bool fTimeExpired = ( pTask->flTaskData != 0 && pTask->flTaskData < gpGlobals->curtime - GetTimeTaskStarted() );
if (fTimeExpired || GetNavigator()->GetGoalType() == GOALTYPE_NONE)
{
TaskComplete();
GetNavigator()->StopMoving(); // Stop moving
}
else if (!GetNavigator()->IsGoalActive())
{
SetIdealActivity( GetStoppedActivity() );
}
else
{
// Check validity of goal type
ValidateNavGoal();
}
break;
}
case TASK_DIE:
RunDieTask();
break;
case TASK_WAIT_FOR_SPEAK_FINISH:
Assert( GetExpresser() );
if ( GetExpresser() )
{
// Are we waiting for our speech to end? Or for the mutex to be free?
if ( pTask->flTaskData )
{
// Waiting for our speech to end
if ( GetExpresser()->CanSpeakAfterMyself() )
{
TaskComplete();
}
}
else
{
// Waiting for the speech & the delay afterwards
if ( !GetExpresser()->IsSpeaking() )
{
TaskComplete();
}
}
}
break;
case TASK_SCRIPT_RUN_TO_TARGET:
case TASK_SCRIPT_WALK_TO_TARGET:
case TASK_SCRIPT_CUSTOM_MOVE_TO_TARGET:
StartScriptMoveToTargetTask( pTask->iTask );
break;
case TASK_RANGE_ATTACK1:
case TASK_RANGE_ATTACK2:
case TASK_MELEE_ATTACK1:
case TASK_MELEE_ATTACK2:
case TASK_SPECIAL_ATTACK1:
case TASK_SPECIAL_ATTACK2:
case TASK_RELOAD:
RunAttackTask( pTask->iTask );
break;
case TASK_SMALL_FLINCH:
case TASK_BIG_FLINCH:
{
if ( IsActivityFinished() )
{
TaskComplete();
}
}
break;
case TASK_WAIT_FOR_SCRIPT:
{
//
// Waiting to play a script. If the script is ready, start playing the sequence.
//
if ( m_hCine && m_hCine->IsTimeToStart() )
{
TaskComplete();
m_hCine->OnBeginSequence();
// If we have an entry, we have to play it first
if ( m_hCine->m_iszEntry != NULL_STRING )
{
m_hCine->StartSequence( (CAI_BaseNPC *)this, m_hCine->m_iszEntry, true );
}
else
{
m_hCine->StartSequence( (CAI_BaseNPC *)this, m_hCine->m_iszPlay, true );
}
// StartSequence() can call CineCleanup(). If that happened, just exit schedule
if ( !m_hCine )
{
ClearSchedule( "Waiting for script, but lost script!" );
}
m_flPlaybackRate = 1.0;
//DevMsg( 2, "Script %s has begun for %s\n", STRING( m_hCine->m_iszPlay ), GetClassname() );
}
else if (!m_hCine)
{
DevMsg( "Cine died!\n");
TaskComplete();
}
else if ( IsRunningDynamicInteraction() )
{
// If we've lost our partner, abort
if ( !m_hInteractionPartner )
{
CineCleanup();
}
}
break;
}
case TASK_PLAY_SCRIPT:
{
//
// Playing a scripted sequence.
//
AutoMovement( );
if ( IsSequenceFinished() )
{
// Check to see if we are done with the action sequence.
if ( m_hCine->FinishedActionSequence( this ) )
{
// dvs: This is done in FixScriptNPCSchedule -- doing it here is too early because we still
// need to play our post-action idle sequence, which might also require FL_FLY.
//
// drop to ground if this guy is only marked "fly" because of the auto movement
/*if ( !(m_hCine->m_savedFlags & FL_FLY) )
{
if ( ( GetFlags() & FL_FLY ) && !m_hCine->m_bIgnoreGravity )
{
RemoveFlag( FL_FLY );
}
}*/
if (m_hCine)
{
m_hCine->SequenceDone( this );
}
TaskComplete();
}
else if ( m_hCine && m_hCine->m_bForceSynch )
{
m_hCine->SynchronizeSequence( this );
}
}
break;
}
case TASK_PLAY_SCRIPT_POST_IDLE:
{
if ( !m_hCine )
{
DevMsg( "Scripted sequence destroyed while in use\n" );
TaskFail( FAIL_SCHEDULE_NOT_FOUND );
break;
}
//
// Playing a scripted post idle sequence. Quit early if another sequence has grabbed the NPC.
//
if ( IsSequenceFinished() || ( m_hCine->m_hNextCine != NULL ) )
{
m_hCine->PostIdleDone( this );
}
break;
}
case TASK_ENABLE_SCRIPT:
{
if ( !m_hCine )
{
DevMsg( "Scripted sequence destroyed while in use\n" );
TaskFail( FAIL_SCHEDULE_NOT_FOUND );
break;
}
if (!m_hCine->IsWaitingForBegin())
{
m_hCine->DelayStart( false );
TaskComplete();
}
break;
}
case TASK_PLAY_SCENE:
{
if (!IsInLockedScene())
{
ClearSchedule( "Playing a scene, but not in a scene!" );
}
if (GetNavigator()->GetGoalType() != GOALTYPE_NONE)
{
TaskComplete();
}
break;
}
case TASK_RUN_PATH_FOR_UNITS:
case TASK_WALK_PATH_FOR_UNITS:
{
float distance;
distance = (m_vecLastPosition - GetLocalOrigin()).Length2D();
// Walk path until far enough away
if ( distance > pTask->flTaskData ||
GetNavigator()->GetGoalType() == GOALTYPE_NONE )
{
TaskComplete();
}
break;
}
case TASK_RUN_PATH_FLEE:
{
Vector vecDiff;
vecDiff = GetLocalOrigin() - GetNavigator()->GetGoalPos();
if( vecDiff.Length() <= pTask->flTaskData )
{
TaskComplete();
}
break;
}
case TASK_WALK_PATH_WITHIN_DIST:
case TASK_RUN_PATH_WITHIN_DIST:
{
Vector vecDiff;
vecDiff = GetLocalOrigin() - GetNavigator()->GetGoalPos();
if( vecDiff.Length() <= pTask->flTaskData )
{
TaskComplete();
}
break;
}
case TASK_WALK_PATH_TIMED:
case TASK_RUN_PATH_TIMED:
{
if ( IsWaitFinished() ||
GetNavigator()->GetGoalType() == GOALTYPE_NONE )
{
TaskComplete();
}
}
break;
case TASK_WEAPON_PICKUP:
{
if ( IsActivityFinished() )
{
CBaseCombatWeapon *pWeapon = ToBaseCombatWeapon( (CBaseEntity *)m_hTargetEnt);
CBaseCombatCharacter *pOwner = pWeapon->GetOwner();
if ( !pOwner )
{
TaskComplete();
}
else
{
TaskFail(FAIL_WEAPON_OWNED);
}
}
break;
}
break;
case TASK_ITEM_PICKUP:
{
if ( IsActivityFinished() )
{
TaskComplete();
}
break;
}
break;
case TASK_FALL_TO_GROUND:
if ( GetFlags() & FL_ONGROUND )
{
TaskComplete();
}
else if( GetFlags() & FL_FLY )
{
// We're never going to fall if we're FL_FLY.
RemoveFlag( FL_FLY );
}
else
{
if( IsWaitFinished() )
{
// After 4 seconds of trying to fall to ground, Assume that we're in a bad case where the NPC
// isn't actually falling, and make an attempt to slam the ground entity to whatever's under the NPC.
Vector maxs = WorldAlignMaxs() - Vector( .1, .1, .2 );
Vector mins = WorldAlignMins() + Vector( .1, .1, 0 );
Vector vecStart = GetAbsOrigin() + Vector( 0, 0, .1 );
Vector vecDown = GetAbsOrigin();
vecDown.z -= 0.2;
trace_t trace;
m_pMoveProbe->TraceHull( vecStart, vecDown, mins, maxs, GetAITraceMask(), &trace );
if( trace.m_pEnt )
{
// Found something!
SetGroundEntity( trace.m_pEnt );
TaskComplete();
}
else
{
// Try again in a few seconds.
SetWait(4);
}
}
}
break;
case TASK_WANDER:
break;
case TASK_FREEZE:
if ( m_flFrozen < 1.0f )
{
Unfreeze();
}
break;
default:
{
DevMsg( "No RunTask entry for %s\n", TaskName( pTask->iTask ) );
TaskComplete();
}
break;
}
}
void CAI_BaseNPC::RunTaskOverlay()
{
if ( IsCurTaskContinuousMove() )
{
m_MoveAndShootOverlay.RunShootWhileMove();
}
}
void CAI_BaseNPC::EndTaskOverlay()
{
m_MoveAndShootOverlay.EndShootWhileMove();
}
//=========================================================
// SetTurnActivity - measures the difference between the way
// the NPC is facing and determines whether or not to
// select one of the 180 turn animations.
//=========================================================
void CAI_BaseNPC::SetTurnActivity ( void )
{
if ( IsCrouching() )
{
SetIdealActivity( ACT_IDLE ); // failure case
return;
}
float flYD;
flYD = GetMotor()->DeltaIdealYaw();
// FIXME: unknown case, update yaw should catch these
/*
if (GetMotor()->AddTurnGesture( flYD ))
{
SetIdealActivity( ACT_IDLE );
Remember( bits_MEMORY_TURNING );
return;
}
*/
if( flYD <= -80 && flYD >= -100 && SelectWeightedSequence( ACT_90_RIGHT ) != ACTIVITY_NOT_AVAILABLE )
{
// 90 degree right.
Remember( bits_MEMORY_TURNING );
SetIdealActivity( ACT_90_RIGHT );
return;
}
if( flYD >= 80 && flYD <= 100 && SelectWeightedSequence( ACT_90_LEFT ) != ACTIVITY_NOT_AVAILABLE )
{
// 90 degree left.
Remember( bits_MEMORY_TURNING );
SetIdealActivity( ACT_90_LEFT );
return;
}
if( fabs( flYD ) >= 160 && SelectWeightedSequence ( ACT_180_LEFT ) != ACTIVITY_NOT_AVAILABLE )
{
Remember( bits_MEMORY_TURNING );
SetIdealActivity( ACT_180_LEFT );
return;
}
if ( flYD <= -45 && SelectWeightedSequence ( ACT_TURN_RIGHT ) != ACTIVITY_NOT_AVAILABLE )
{// big right turn
SetIdealActivity( ACT_TURN_RIGHT );
return;
}
if ( flYD >= 45 && SelectWeightedSequence ( ACT_TURN_LEFT ) != ACTIVITY_NOT_AVAILABLE )
{// big left turn
SetIdealActivity( ACT_TURN_LEFT );
return;
}
SetIdealActivity( ACT_IDLE ); // failure case
}
//-----------------------------------------------------------------------------
// Purpose: For a specific delta, add a turn gesture and set the yaw speed
// Input : yaw delta
//-----------------------------------------------------------------------------
bool CAI_BaseNPC::UpdateTurnGesture( void )
{
float flYD = GetMotor()->DeltaIdealYaw();
return GetMotor()->AddTurnGesture( flYD );
}
//-----------------------------------------------------------------------------
// Purpose: For non-looping animations that may be replayed sequentially (like attacks)
// Set the activity to ACT_RESET if this is a replay, otherwise just set ideal activity
// Input : newIdealActivity - desired ideal activity
//-----------------------------------------------------------------------------
void CAI_BaseNPC::ResetIdealActivity( Activity newIdealActivity )
{
if ( m_Activity == newIdealActivity )
{
m_Activity = ACT_RESET;
}
SetIdealActivity( newIdealActivity );
}
void CAI_BaseNPC::TranslateNavGoal( CBaseEntity *pEnemy, Vector &chasePosition )
{
if ( GetNavType() == NAV_FLY )
{
// UNDONE: Cache these per enemy instead?
Vector offset = pEnemy->EyePosition() - pEnemy->GetAbsOrigin();
chasePosition += offset;
}
}
//-----------------------------------------------------------------------------
// Purpose: Returns the custom movement activity for the script that this NPC
// is running.
// Output : Returns the activity, or ACT_INVALID is the sequence is unknown.
//-----------------------------------------------------------------------------
Activity CAI_BaseNPC::GetScriptCustomMoveActivity( void )
{
Activity eActivity = ACT_WALK;
if ( ( m_hCine != NULL ) && ( m_hCine->m_iszCustomMove != NULL_STRING ) )
{
// We have a valid script. Look up the custom movement activity.
eActivity = ( Activity )LookupActivity( STRING( m_hCine->m_iszCustomMove ) );
if ( eActivity == ACT_INVALID )
{
// Not an activity, at least make sure it's a valid sequence.
if ( LookupSequence( STRING( m_hCine->m_iszCustomMove ) ) != ACT_INVALID )
{
eActivity = ACT_SCRIPT_CUSTOM_MOVE;
}
else
{
eActivity = ACT_WALK;
}
}
}
else if ( m_iszSceneCustomMoveSeq != NULL_STRING )
{
eActivity = ACT_SCRIPT_CUSTOM_MOVE;
}
return eActivity;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : int
//-----------------------------------------------------------------------------
int CAI_BaseNPC::GetScriptCustomMoveSequence( void )
{
int iSequence = ACTIVITY_NOT_AVAILABLE;
// If we have a scripted sequence entity, use it's custom move
if ( m_hCine != NULL )
{
iSequence = LookupSequence( STRING( m_hCine->m_iszCustomMove ) );
if ( iSequence == ACTIVITY_NOT_AVAILABLE )
{
DevMsg( "SCRIPT_CUSTOM_MOVE: %s has no sequence:%s\n", GetClassname(), STRING(m_hCine->m_iszCustomMove) );
}
}
else if ( m_iszSceneCustomMoveSeq != NULL_STRING )
{
// Otherwise, use the .vcd custom move
iSequence = LookupSequence( STRING( m_iszSceneCustomMoveSeq ) );
if ( iSequence == ACTIVITY_NOT_AVAILABLE )
{
Warning( "SCRIPT_CUSTOM_MOVE: %s failed scripted custom move. Has no sequence called: %s\n", GetClassname(), STRING(m_iszSceneCustomMoveSeq) );
}
}
// Failed? Use walk.
if ( iSequence == ACTIVITY_NOT_AVAILABLE )
{
iSequence = SelectWeightedSequence( ACT_WALK );
}
return iSequence;
}
//=========================================================
// GetTask - returns a pointer to the current
// scheduled task. NULL if there's a problem.
//=========================================================
const Task_t *CAI_BaseNPC::GetTask( void )
{
int iScheduleIndex = GetScheduleCurTaskIndex();
if ( !GetCurSchedule() || iScheduleIndex < 0 || iScheduleIndex >= GetCurSchedule()->NumTasks() )
// iScheduleIndex is not within valid range for the NPC's current schedule.
return NULL;
return &GetCurSchedule()->GetTaskList()[ iScheduleIndex ];
}
void CAI_BaseNPC::TranslateAddOnAttachment( char *pchAttachmentName, int iCount )
{
#ifdef HL2_DLL
if( Classify() == CLASS_ZOMBIE || ClassMatches( "npc_combine*" ) )
{
if ( Q_strcmp( pchAttachmentName, "addon_rear" ) == 0 ||
Q_strcmp( pchAttachmentName, "addon_front" ) == 0 ||
Q_strcmp( pchAttachmentName, "addon_rear_or_front" ) == 0 )
{
if ( iCount == 0 )
{
Q_strcpy( pchAttachmentName, "eyes" );
}
else
{
Q_strcpy( pchAttachmentName, "" );
}
return;
}
}
#endif
if( Q_strcmp( pchAttachmentName, "addon_baseshooter" ) == 0 )
{
switch ( iCount )
{
case 0:
Q_strcpy( pchAttachmentName, "anim_attachment_lh" );
break;
case 1:
Q_strcpy( pchAttachmentName, "anim_attachment_rh" );
break;
default:
Q_strcpy( pchAttachmentName, "" );
}
return;
}
Q_strcpy( pchAttachmentName, "" );
}
//-----------------------------------------------------------------------------
bool CAI_BaseNPC::IsInterruptable()
{
if ( GetState() == NPC_STATE_SCRIPT )
{
if ( m_hCine )
{
if (!m_hCine->CanInterrupt() )
return false;
// are the in an script FL_FLY state?
if ((GetFlags() & FL_FLY ) && !(m_hCine->m_savedFlags & FL_FLY))
{
return false;
}
}
}
return IsAlive();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectInteractionSchedule( void )
{
SetTarget( m_hForcedInteractionPartner );
// If we have an interaction, we're the initiator. Move to our interaction point.
if ( m_iInteractionPlaying != NPCINT_NONE )
return SCHED_INTERACTION_MOVE_TO_PARTNER;
// Otherwise, turn towards our partner and wait for him to reach us.
//m_iInteractionState = NPCINT_MOVING_TO_MARK;
return SCHED_INTERACTION_WAIT_FOR_PARTNER;
}
//-----------------------------------------------------------------------------
// Idle schedule selection
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectIdleSchedule()
{
if ( m_hForcedInteractionPartner )
return SelectInteractionSchedule();
int nSched = SelectFlinchSchedule();
if ( nSched != SCHED_NONE )
return nSched;
if ( HasCondition ( COND_HEAR_DANGER ) ||
HasCondition ( COND_HEAR_COMBAT ) ||
HasCondition ( COND_HEAR_WORLD ) ||
HasCondition ( COND_HEAR_BULLET_IMPACT ) ||
HasCondition ( COND_HEAR_PLAYER ) )
{
return SCHED_ALERT_FACE_BESTSOUND;
}
// no valid route!
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
return SCHED_IDLE_STAND;
// valid route. Get moving
return SCHED_IDLE_WALK;
}
//-----------------------------------------------------------------------------
// Alert schedule selection
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectAlertSchedule()
{
if ( m_hForcedInteractionPartner )
return SelectInteractionSchedule();
int nSched = SelectFlinchSchedule();
if ( nSched != SCHED_NONE )
return nSched;
// Scan around for new enemies
if ( HasCondition( COND_ENEMY_DEAD ) && SelectWeightedSequence( ACT_VICTORY_DANCE ) != ACTIVITY_NOT_AVAILABLE )
return SCHED_ALERT_SCAN;
if( IsPlayerAlly() && HasCondition(COND_HEAR_COMBAT) )
{
return SCHED_ALERT_REACT_TO_COMBAT_SOUND;
}
if ( HasCondition ( COND_HEAR_DANGER ) ||
HasCondition ( COND_HEAR_PLAYER ) ||
HasCondition ( COND_HEAR_WORLD ) ||
HasCondition ( COND_HEAR_BULLET_IMPACT ) ||
HasCondition ( COND_HEAR_COMBAT ) )
{
return SCHED_ALERT_FACE_BESTSOUND;
}
if ( gpGlobals->curtime - GetEnemies()->LastTimeSeen( AI_UNKNOWN_ENEMY ) < TIME_CARE_ABOUT_DAMAGE )
return SCHED_ALERT_FACE;
return SCHED_ALERT_STAND;
}
//-----------------------------------------------------------------------------
// Combat schedule selection
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectCombatSchedule()
{
if ( m_hForcedInteractionPartner )
return SelectInteractionSchedule();
int nSched = SelectFlinchSchedule();
if ( nSched != SCHED_NONE )
return nSched;
if ( HasCondition(COND_NEW_ENEMY) && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 2.0 )
{
return SCHED_WAKE_ANGRY;
}
if ( HasCondition( COND_ENEMY_DEAD ) )
{
// clear the current (dead) enemy and try to find another.
SetEnemy( NULL );
if ( ChooseEnemy() )
{
ClearCondition( COND_ENEMY_DEAD );
return SelectSchedule();
}
SetState( NPC_STATE_ALERT );
return SelectSchedule();
}
// If I'm scared of this enemy run away
if ( IRelationType( GetEnemy() ) == D_FR )
{
if (HasCondition( COND_SEE_ENEMY ) ||
HasCondition( COND_LIGHT_DAMAGE )||
HasCondition( COND_HEAVY_DAMAGE ))
{
FearSound();
//ClearCommandGoal();
return SCHED_RUN_FROM_ENEMY;
}
// If I've seen the enemy recently, cower. Ignore the time for unforgettable enemies.
AI_EnemyInfo_t *pMemory = GetEnemies()->Find( GetEnemy() );
if ( (pMemory && pMemory->bUnforgettable) || (GetEnemyLastTimeSeen() > (gpGlobals->curtime - 5.0)) )
{
// If we're facing him, just look ready. Otherwise, face him.
if ( FInAimCone( GetEnemy()->EyePosition() ) )
return SCHED_COMBAT_STAND;
return SCHED_FEAR_FACE;
}
}
// Check if need to reload
if ( HasCondition( COND_LOW_PRIMARY_AMMO ) || HasCondition( COND_NO_PRIMARY_AMMO ) )
{
return SCHED_HIDE_AND_RELOAD;
}
// Can we see the enemy?
if ( !HasCondition(COND_SEE_ENEMY) )
{
// enemy is unseen, but not occluded!
// turn to face enemy
if ( !HasCondition(COND_ENEMY_OCCLUDED) )
return SCHED_COMBAT_FACE;
// chase!
if ( GetActiveWeapon() || (CapabilitiesGet() & (bits_CAP_INNATE_RANGE_ATTACK1|bits_CAP_INNATE_RANGE_ATTACK2)))
return SCHED_ESTABLISH_LINE_OF_FIRE;
else if ( (CapabilitiesGet() & (bits_CAP_INNATE_MELEE_ATTACK1|bits_CAP_INNATE_MELEE_ATTACK2)))
return SCHED_CHASE_ENEMY;
else
return SCHED_TAKE_COVER_FROM_ENEMY;
}
if ( HasCondition(COND_TOO_CLOSE_TO_ATTACK) )
return SCHED_BACK_AWAY_FROM_ENEMY;
if ( HasCondition( COND_WEAPON_PLAYER_IN_SPREAD ) ||
HasCondition( COND_WEAPON_BLOCKED_BY_FRIEND ) ||
HasCondition( COND_WEAPON_SIGHT_OCCLUDED ) )
{
return SCHED_ESTABLISH_LINE_OF_FIRE;
}
if ( GetShotRegulator()->IsInRestInterval() )
{
if ( HasCondition(COND_CAN_RANGE_ATTACK1) )
return SCHED_COMBAT_FACE;
}
// we can see the enemy
if ( HasCondition(COND_CAN_RANGE_ATTACK1) )
{
if ( !UseAttackSquadSlots() || OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
return SCHED_RANGE_ATTACK1;
return SCHED_COMBAT_FACE;
}
if ( HasCondition(COND_CAN_RANGE_ATTACK2) )
return SCHED_RANGE_ATTACK2;
if ( HasCondition(COND_CAN_MELEE_ATTACK1) )
return SCHED_MELEE_ATTACK1;
if ( HasCondition(COND_CAN_MELEE_ATTACK2) )
return SCHED_MELEE_ATTACK2;
if ( HasCondition(COND_NOT_FACING_ATTACK) )
return SCHED_COMBAT_FACE;
if ( !HasCondition(COND_CAN_RANGE_ATTACK1) && !HasCondition(COND_CAN_MELEE_ATTACK1) )
{
// if we can see enemy but can't use either attack type, we must need to get closer to enemy
if ( GetActiveWeapon() )
return SCHED_MOVE_TO_WEAPON_RANGE;
// If we have an innate attack and we're too far (or occluded) then get line of sight
if ( HasCondition( COND_TOO_FAR_TO_ATTACK ) && ( CapabilitiesGet() & (bits_CAP_INNATE_RANGE_ATTACK1|bits_CAP_INNATE_RANGE_ATTACK2)) )
return SCHED_MOVE_TO_WEAPON_RANGE;
// if we can see enemy but can't use either attack type, we must need to get closer to enemy
if ( CapabilitiesGet() & (bits_CAP_INNATE_MELEE_ATTACK1|bits_CAP_INNATE_MELEE_ATTACK2) )
return SCHED_CHASE_ENEMY;
else
return SCHED_TAKE_COVER_FROM_ENEMY;
}
DevWarning( 2, "No suitable combat schedule!\n" );
return SCHED_FAIL;
}
//-----------------------------------------------------------------------------
// Dead schedule selection
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectDeadSchedule()
{
if ( BecomeRagdollOnClient( vec3_origin ) )
{
CleanupOnDeath();
return SCHED_DIE_RAGDOLL;
}
// Adrian - Alread dead (by animation event maybe?)
// Is it safe to set it to SCHED_NONE?
if ( m_lifeState == LIFE_DEAD )
return SCHED_NONE;
CleanupOnDeath();
return SCHED_DIE;
}
//-----------------------------------------------------------------------------
// Script schedule selection
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectScriptSchedule()
{
Assert( m_hCine != NULL );
if ( m_hCine )
return SCHED_AISCRIPT;
DevWarning( 2, "Script failed for %s\n", GetClassname() );
CineCleanup();
return SCHED_IDLE_STAND;
}
//-----------------------------------------------------------------------------
// Purpose: Select a gesture to play in response to damage we've taken
// Output : int
//-----------------------------------------------------------------------------
void CAI_BaseNPC::PlayFlinchGesture()
{
if ( !CanFlinch() )
return;
Activity iFlinchActivity = ACT_INVALID;
float flNextFlinch = random->RandomFloat( 0.5f, 1.0f );
// If I haven't flinched for a while, play the big flinch gesture
if ( !HasMemory(bits_MEMORY_FLINCHED) )
{
iFlinchActivity = GetFlinchActivity( true, true );
if ( HaveSequenceForActivity( iFlinchActivity ) )
{
RestartGesture( iFlinchActivity );
}
Remember(bits_MEMORY_FLINCHED);
}
else
{
iFlinchActivity = GetFlinchActivity( false, true );
if ( HaveSequenceForActivity( iFlinchActivity ) )
{
RestartGesture( iFlinchActivity );
}
}
if ( iFlinchActivity != ACT_INVALID )
{
//Get the duration of the flinch and delay the next one by that (plus a bit more)
int iSequence = GetLayerSequence( FindGestureLayer( iFlinchActivity ) );
if ( iSequence != ACT_INVALID )
{
flNextFlinch += SequenceDuration( iSequence );
}
m_flNextFlinchTime = gpGlobals->curtime + flNextFlinch;
}
}
//-----------------------------------------------------------------------------
// Purpose: See if we should flinch in response to damage we've taken
// Output : int
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectFlinchSchedule()
{
if ( !HasCondition(COND_HEAVY_DAMAGE) )
return SCHED_NONE;
// If we've flinched recently, don't do it again. A gesture flinch will be played instead.
if ( HasMemory(bits_MEMORY_FLINCHED) )
return SCHED_NONE;
if ( !CanFlinch() )
return SCHED_NONE;
// Robin: This was in the original HL1 flinch code. Do we still want it?
//if ( fabs( GetMotor()->DeltaIdealYaw() ) < (1.0 - m_flFieldOfView) * 60 ) // roughly in the correct direction
// return SCHED_TAKE_COVER_FROM_ORIGIN;
// Heavy damage. Break out of my current schedule and flinch.
Activity iFlinchActivity = GetFlinchActivity( true, false );
if ( HaveSequenceForActivity( iFlinchActivity ) )
return SCHED_BIG_FLINCH;
/*
// Not used anymore, because gesture flinches are played instead for heavy damage
// taken shortly after we've already flinched full.
//
iFlinchActivity = GetFlinchActivity( false, false );
if ( HaveSequenceForActivity( iFlinchActivity ) )
return SCHED_SMALL_FLINCH;
*/
return SCHED_NONE;
}
//-----------------------------------------------------------------------------
// Purpose: Decides which type of schedule best suits the NPC's current
// state and conditions. Then calls NPC's member function to get a pointer
// to a schedule of the proper type.
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectSchedule( void )
{
if ( HasCondition( COND_FLOATING_OFF_GROUND ) )
{
SetGravity( 1.0 );
SetGroundEntity( NULL );
return SCHED_FALL_TO_GROUND;
}
switch( m_NPCState )
{
case NPC_STATE_NONE:
DevWarning( 2, "NPC_STATE IS NONE!\n" );
break;
case NPC_STATE_PRONE:
return SCHED_IDLE_STAND;
case NPC_STATE_IDLE:
AssertMsgOnce( GetEnemy() == NULL, "NPC has enemy but is not in combat state?" );
return SelectIdleSchedule();
case NPC_STATE_ALERT:
AssertMsgOnce( GetEnemy() == NULL, "NPC has enemy but is not in combat state?" );
return SelectAlertSchedule();
case NPC_STATE_COMBAT:
return SelectCombatSchedule();
case NPC_STATE_DEAD:
return SelectDeadSchedule();
case NPC_STATE_SCRIPT:
return SelectScriptSchedule();
default:
DevWarning( 2, "Invalid State for SelectSchedule!\n" );
break;
}
return SCHED_FAIL;
}
//-----------------------------------------------------------------------------
int CAI_BaseNPC::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode )
{
return ( m_failSchedule != SCHED_NONE ) ? m_failSchedule : SCHED_FAIL;
}