//========= Copyright 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 "tier0/vcrmode.h" #include "env_debughistory.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 ); 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 ); } 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 %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() : "" ); } ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): New enemy: %s\n", GetDebugName(), entindex(), GetEnemy() ? GetEnemy()->GetDebugName() : "" ) ); } } 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_( CAI_BaseNPC::GetSchedulingSymbols()->ScheduleIdToSymbol( GetCurSchedule()->GetId() ) ); 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 ); } 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_( CAI_BaseNPC::GetSchedulingSymbols()->ScheduleIdToSymbol( GetCurSchedule()->GetId() ) ); 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; } // 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 == NULL ) ? this : m_hStoredPathTarget; //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 = dynamic_cast( 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 = dynamic_cast( 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 = dynamic_cast( 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_TO_TARGET: { if (m_hTargetEnt == NULL) { TaskFail(FAIL_NO_TARGET); } else { AI_NavGoal_t goal( static_cast(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(!GetNavigator()->IsGoalActive()) { TaskComplete(); return; } if ( IsActivityFinished() ) { TaskComplete(); return; } ValidateNavGoal(); break; } case TASK_WAIT_FOR_MOVEMENT: { 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 = dynamic_cast(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 = dynamic_cast(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() ) { 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_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: { 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 = dynamic_cast( (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, MASK_NPCSOLID, &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: 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 ]; } //----------------------------------------------------------------------------- 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; }