You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
14135 lines
404 KiB
14135 lines
404 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
|
|
#include "ai_basenpc.h" |
|
#include "fmtstr.h" |
|
#include "activitylist.h" |
|
#include "animation.h" |
|
#include "basecombatweapon.h" |
|
#include "soundent.h" |
|
#include "decals.h" |
|
#include "entitylist.h" |
|
#include "eventqueue.h" |
|
#include "entityapi.h" |
|
#include "bitstring.h" |
|
#include "gamerules.h" // For g_pGameRules |
|
#include "scripted.h" |
|
#include "worldsize.h" |
|
#include "game.h" |
|
#include "shot_manipulator.h" |
|
|
|
#ifdef HL2_DLL |
|
#include "ai_interactions.h" |
|
#include "hl2_gamerules.h" |
|
#endif // HL2_DLL |
|
|
|
#include "ai_network.h" |
|
#include "ai_networkmanager.h" |
|
#include "ai_pathfinder.h" |
|
#include "ai_node.h" |
|
#include "ai_default.h" |
|
#include "ai_schedule.h" |
|
#include "ai_task.h" |
|
#include "ai_hull.h" |
|
#include "ai_moveprobe.h" |
|
#include "ai_hint.h" |
|
#include "ai_navigator.h" |
|
#include "ai_senses.h" |
|
#include "ai_squadslot.h" |
|
#include "ai_memory.h" |
|
#include "ai_squad.h" |
|
#include "ai_localnavigator.h" |
|
#include "ai_tacticalservices.h" |
|
#include "ai_behavior.h" |
|
#include "ai_dynamiclink.h" |
|
#include "AI_Criteria.h" |
|
#include "basegrenade_shared.h" |
|
#include "ammodef.h" |
|
#include "player.h" |
|
#include "sceneentity.h" |
|
#include "ndebugoverlay.h" |
|
#include "mathlib/mathlib.h" |
|
#include "bone_setup.h" |
|
#include "IEffects.h" |
|
#include "vstdlib/random.h" |
|
#include "engine/IEngineSound.h" |
|
#include "tier1/strtools.h" |
|
#include "doors.h" |
|
#include "BasePropDoor.h" |
|
#include "saverestore_utlvector.h" |
|
#include "npcevent.h" |
|
#include "movevars_shared.h" |
|
#include "te_effect_dispatch.h" |
|
#include "globals.h" |
|
#include "saverestore_bitstring.h" |
|
#include "checksum_crc.h" |
|
#include "iservervehicle.h" |
|
#include "filters.h" |
|
#ifdef HL2_DLL |
|
#include "npc_bullseye.h" |
|
#include "hl2_player.h" |
|
#include "weapon_physcannon.h" |
|
#endif |
|
#include "waterbullet.h" |
|
#include "in_buttons.h" |
|
#include "eventlist.h" |
|
#include "globalstate.h" |
|
#include "physics_prop_ragdoll.h" |
|
#include "vphysics/friction.h" |
|
#include "physics_npc_solver.h" |
|
#include "tier0/vcrmode.h" |
|
#include "death_pose.h" |
|
#include "datacache/imdlcache.h" |
|
#include "vstdlib/jobthread.h" |
|
|
|
#ifdef HL2_EPISODIC |
|
#include "npc_alyx_episodic.h" |
|
#endif |
|
|
|
#ifdef PORTAL |
|
#include "prop_portal_shared.h" |
|
#endif |
|
|
|
#include "env_debughistory.h" |
|
#include "collisionutils.h" |
|
|
|
extern ConVar sk_healthkit; |
|
|
|
// dvs: for opening doors -- these should probably not be here |
|
#include "ai_route.h" |
|
#include "ai_waypoint.h" |
|
|
|
#include "utlbuffer.h" |
|
#include "gamestats.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
#ifdef __clang__ |
|
// These clang 3.1 warnings don't seem very useful, and cannot easily be |
|
// avoided in this file. |
|
#pragma GCC diagnostic ignored "-Wdangling-else" // warning: add explicit braces to avoid dangling else [-Wdangling-else] |
|
#endif |
|
|
|
//#define DEBUG_LOOK |
|
|
|
bool RagdollManager_SaveImportant( CAI_BaseNPC *pNPC ); |
|
|
|
#define MIN_PHYSICS_FLINCH_DAMAGE 5.0f |
|
|
|
#define NPC_GRENADE_FEAR_DIST 200 |
|
#define MAX_GLASS_PENETRATION_DEPTH 16.0f |
|
|
|
#define FINDNAMEDENTITY_MAX_ENTITIES 32 // max number of entities to be considered for random entity selection in FindNamedEntity |
|
|
|
extern bool g_fDrawLines; |
|
extern short g_sModelIndexLaser; // holds the index for the laser beam |
|
extern short g_sModelIndexLaserDot; // holds the index for the laser beam dot |
|
|
|
// Debugging tools |
|
ConVar ai_no_select_box( "ai_no_select_box", "0" ); |
|
|
|
ConVar ai_show_think_tolerance( "ai_show_think_tolerance", "0" ); |
|
ConVar ai_debug_think_ticks( "ai_debug_think_ticks", "0" ); |
|
ConVar ai_debug_doors( "ai_debug_doors", "0" ); |
|
ConVar ai_debug_enemies( "ai_debug_enemies", "0" ); |
|
|
|
ConVar ai_rebalance_thinks( "ai_rebalance_thinks", "1" ); |
|
ConVar ai_use_efficiency( "ai_use_efficiency", "1" ); |
|
ConVar ai_use_frame_think_limits( "ai_use_frame_think_limits", "1" ); |
|
ConVar ai_default_efficient( "ai_default_efficient", ( IsX360() ) ? "1" : "0" ); |
|
ConVar ai_efficiency_override( "ai_efficiency_override", "0" ); |
|
ConVar ai_debug_efficiency( "ai_debug_efficiency", "0" ); |
|
ConVar ai_debug_dyninteractions( "ai_debug_dyninteractions", "0", FCVAR_NONE, "Debug the NPC dynamic interaction system." ); |
|
ConVar ai_frametime_limit( "ai_frametime_limit", "50", FCVAR_NONE, "frametime limit for min efficiency AIE_NORMAL (in sec's)." ); |
|
|
|
ConVar ai_use_think_optimizations( "ai_use_think_optimizations", "1" ); |
|
|
|
ConVar ai_test_moveprobe_ignoresmall( "ai_test_moveprobe_ignoresmall", "0" ); |
|
|
|
#ifdef HL2_EPISODIC |
|
extern ConVar ai_vehicle_avoidance; |
|
#endif // HL2_EPISODIC |
|
|
|
#ifndef _RETAIL |
|
#define ShouldUseEfficiency() ( ai_use_think_optimizations.GetBool() && ai_use_efficiency.GetBool() ) |
|
#define ShouldUseFrameThinkLimits() ( ai_use_think_optimizations.GetBool() && ai_use_frame_think_limits.GetBool() ) |
|
#define ShouldRebalanceThinks() ( ai_use_think_optimizations.GetBool() && ai_rebalance_thinks.GetBool() ) |
|
#define ShouldDefaultEfficient() ( ai_use_think_optimizations.GetBool() && ai_default_efficient.GetBool() ) |
|
#else |
|
#define ShouldUseEfficiency() ( true ) |
|
#define ShouldUseFrameThinkLimits() ( true ) |
|
#define ShouldRebalanceThinks() ( true ) |
|
#define ShouldDefaultEfficient() ( true ) |
|
#endif |
|
|
|
#ifndef _RETAIL |
|
#define DbgEnemyMsg if ( !ai_debug_enemies.GetBool() ) ; else DevMsg |
|
#else |
|
#define DbgEnemyMsg if ( 0 ) ; else DevMsg |
|
#endif |
|
|
|
#ifdef DEBUG_AI_FRAME_THINK_LIMITS |
|
#define DbgFrameLimitMsg DevMsg |
|
#else |
|
#define DbgFrameLimitMsg (void) |
|
#endif |
|
|
|
// NPC damage adjusters |
|
ConVar sk_npc_head( "sk_npc_head","2" ); |
|
ConVar sk_npc_chest( "sk_npc_chest","1" ); |
|
ConVar sk_npc_stomach( "sk_npc_stomach","1" ); |
|
ConVar sk_npc_arm( "sk_npc_arm","1" ); |
|
ConVar sk_npc_leg( "sk_npc_leg","1" ); |
|
ConVar showhitlocation( "showhitlocation", "0" ); |
|
|
|
// Squad debugging |
|
ConVar ai_debug_squads( "ai_debug_squads", "0" ); |
|
ConVar ai_debug_loners( "ai_debug_loners", "0" ); |
|
|
|
// Shoot trajectory |
|
ConVar ai_lead_time( "ai_lead_time","0.0" ); |
|
ConVar ai_shot_stats( "ai_shot_stats", "0" ); |
|
ConVar ai_shot_stats_term( "ai_shot_stats_term", "1000" ); |
|
ConVar ai_shot_bias( "ai_shot_bias", "1.0" ); |
|
|
|
ConVar ai_spread_defocused_cone_multiplier( "ai_spread_defocused_cone_multiplier","3.0" ); |
|
ConVar ai_spread_cone_focus_time( "ai_spread_cone_focus_time","0.6" ); |
|
ConVar ai_spread_pattern_focus_time( "ai_spread_pattern_focus_time","0.8" ); |
|
|
|
ConVar ai_reaction_delay_idle( "ai_reaction_delay_idle","0.3" ); |
|
ConVar ai_reaction_delay_alert( "ai_reaction_delay_alert", "0.1" ); |
|
|
|
ConVar ai_strong_optimizations( "ai_strong_optimizations", ( IsX360() ) ? "1" : "0" ); |
|
bool AIStrongOpt( void ) |
|
{ |
|
return ai_strong_optimizations.GetBool(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Crude frame timings |
|
// |
|
|
|
CFastTimer g_AIRunTimer; |
|
CFastTimer g_AIPostRunTimer; |
|
CFastTimer g_AIMoveTimer; |
|
|
|
CFastTimer g_AIConditionsTimer; |
|
CFastTimer g_AIPrescheduleThinkTimer; |
|
CFastTimer g_AIMaintainScheduleTimer; |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Manager g_AI_Manager; |
|
|
|
//------------------------------------- |
|
|
|
CAI_Manager::CAI_Manager() |
|
{ |
|
m_AIs.EnsureCapacity( MAX_AIS ); |
|
} |
|
|
|
//------------------------------------- |
|
|
|
CAI_BaseNPC **CAI_Manager::AccessAIs() |
|
{ |
|
if (m_AIs.Count()) |
|
return &m_AIs[0]; |
|
return NULL; |
|
} |
|
|
|
//------------------------------------- |
|
|
|
int CAI_Manager::NumAIs() |
|
{ |
|
return m_AIs.Count(); |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_Manager::AddAI( CAI_BaseNPC *pAI ) |
|
{ |
|
m_AIs.AddToTail( pAI ); |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_Manager::RemoveAI( CAI_BaseNPC *pAI ) |
|
{ |
|
int i = m_AIs.Find( pAI ); |
|
|
|
if ( i != -1 ) |
|
m_AIs.FastRemove( i ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
// ================================================================ |
|
// Init static data |
|
// ================================================================ |
|
int CAI_BaseNPC::m_nDebugBits = 0; |
|
CAI_BaseNPC* CAI_BaseNPC::m_pDebugNPC = NULL; |
|
int CAI_BaseNPC::m_nDebugPauseIndex = -1; |
|
|
|
CAI_ClassScheduleIdSpace CAI_BaseNPC::gm_ClassScheduleIdSpace( true ); |
|
CAI_GlobalScheduleNamespace CAI_BaseNPC::gm_SchedulingSymbols; |
|
CAI_LocalIdSpace CAI_BaseNPC::gm_SquadSlotIdSpace( true ); |
|
|
|
string_t CAI_BaseNPC::gm_iszPlayerSquad; |
|
|
|
int CAI_BaseNPC::gm_iNextThinkRebalanceTick; |
|
float CAI_BaseNPC::gm_flTimeLastSpawn; |
|
int CAI_BaseNPC::gm_nSpawnedThisFrame; |
|
|
|
CSimpleSimTimer CAI_BaseNPC::m_AnyUpdateEnemyPosTimer; |
|
|
|
// |
|
// Deferred Navigation calls go here |
|
// |
|
|
|
CPostFrameNavigationHook g_PostFrameNavigationHook; |
|
CPostFrameNavigationHook *PostFrameNavigationSystem( void ) |
|
{ |
|
return &g_PostFrameNavigationHook; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CPostFrameNavigationHook::Init( void ) |
|
{ |
|
m_Functors.Purge(); |
|
m_bGameFrameRunning = false; |
|
return true; |
|
} |
|
|
|
// Main query job |
|
CJob *g_pQueuedNavigationQueryJob = NULL; |
|
|
|
static void ProcessNavigationQueries( CFunctor **pData, unsigned int nCount ) |
|
{ |
|
// Run all queued navigation on a separate thread |
|
for ( int i = 0; i < nCount; i++ ) |
|
{ |
|
(*pData[i])(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPostFrameNavigationHook::FrameUpdatePreEntityThink( void ) |
|
{ |
|
// If the thread is executing, then wait for it to finish |
|
if ( g_pQueuedNavigationQueryJob ) |
|
{ |
|
g_pQueuedNavigationQueryJob->WaitForFinishAndRelease(); |
|
g_pQueuedNavigationQueryJob = NULL; |
|
m_Functors.Purge(); |
|
} |
|
|
|
if ( ai_post_frame_navigation.GetBool() == false ) |
|
return; |
|
|
|
SetGrameFrameRunning( true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Now that the game frame has collected all the navigation queries, service them |
|
//----------------------------------------------------------------------------- |
|
void CPostFrameNavigationHook::FrameUpdatePostEntityThink( void ) |
|
{ |
|
if ( ai_post_frame_navigation.GetBool() == false ) |
|
return; |
|
|
|
// The guts of the NPC will check against this to decide whether or not to queue its navigation calls |
|
SetGrameFrameRunning( false ); |
|
|
|
// Throw this off to a thread job |
|
g_pQueuedNavigationQueryJob = ThreadExecute( &ProcessNavigationQueries, m_Functors.Base(), m_Functors.Count() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Queue up our navigation call |
|
//----------------------------------------------------------------------------- |
|
void CPostFrameNavigationHook::EnqueueEntityNavigationQuery( CAI_BaseNPC *pNPC, CFunctor *pFunctor ) |
|
{ |
|
if ( ai_post_frame_navigation.GetBool() == false ) |
|
return; |
|
|
|
m_Functors.AddToTail( pFunctor ); |
|
pNPC->SetNavigationDeferred( true ); |
|
} |
|
|
|
// |
|
// Deferred Navigation calls go here |
|
// |
|
|
|
|
|
// ================================================================ |
|
// Class Methods |
|
// ================================================================ |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Static debug function to clear schedules for all NPCS |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ClearAllSchedules(void) |
|
{ |
|
CAI_BaseNPC *npc = gEntList.NextEntByClass( (CAI_BaseNPC *)NULL ); |
|
|
|
while (npc) |
|
{ |
|
npc->ClearSchedule( "CAI_BaseNPC::ClearAllSchedules" ); |
|
npc->GetNavigator()->ClearGoal(); |
|
npc = gEntList.NextEntByClass(npc); |
|
} |
|
} |
|
|
|
// ============================================================================== |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::Event_Gibbed( const CTakeDamageInfo &info ) |
|
{ |
|
bool gibbed = CorpseGib( info ); |
|
|
|
if ( gibbed ) |
|
{ |
|
// don't remove players! |
|
UTIL_Remove( this ); |
|
SetThink( NULL ); //We're going away, so don't think anymore. |
|
} |
|
else |
|
{ |
|
CorpseFade(); |
|
} |
|
|
|
return gibbed; |
|
} |
|
|
|
//========================================================= |
|
// GetFlinchActivity - determines the best type of flinch |
|
// anim to play. |
|
//========================================================= |
|
Activity CAI_BaseNPC::GetFlinchActivity( bool bHeavyDamage, bool bGesture ) |
|
{ |
|
Activity flinchActivity; |
|
|
|
switch ( LastHitGroup() ) |
|
{ |
|
// pick a region-specific flinch |
|
case HITGROUP_HEAD: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_HEAD : ACT_FLINCH_HEAD; |
|
break; |
|
case HITGROUP_STOMACH: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_STOMACH : ACT_FLINCH_STOMACH; |
|
break; |
|
case HITGROUP_LEFTARM: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_LEFTARM : ACT_FLINCH_LEFTARM; |
|
break; |
|
case HITGROUP_RIGHTARM: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_RIGHTARM : ACT_FLINCH_RIGHTARM; |
|
break; |
|
case HITGROUP_LEFTLEG: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_LEFTLEG : ACT_FLINCH_LEFTLEG; |
|
break; |
|
case HITGROUP_RIGHTLEG: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_RIGHTLEG : ACT_FLINCH_RIGHTLEG; |
|
break; |
|
case HITGROUP_CHEST: |
|
flinchActivity = bGesture ? ACT_GESTURE_FLINCH_CHEST : ACT_FLINCH_CHEST; |
|
break; |
|
case HITGROUP_GEAR: |
|
case HITGROUP_GENERIC: |
|
default: |
|
// just get a generic flinch. |
|
if ( bHeavyDamage ) |
|
{ |
|
flinchActivity = bGesture ? ACT_GESTURE_BIG_FLINCH : ACT_BIG_FLINCH; |
|
} |
|
else |
|
{ |
|
flinchActivity = bGesture ? ACT_GESTURE_SMALL_FLINCH : ACT_SMALL_FLINCH; |
|
} |
|
break; |
|
} |
|
|
|
// do we have a sequence for the ideal activity? |
|
if ( SelectWeightedSequence ( flinchActivity ) == ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
if ( bHeavyDamage ) |
|
{ |
|
flinchActivity = bGesture ? ACT_GESTURE_BIG_FLINCH : ACT_BIG_FLINCH; |
|
|
|
// If we fail at finding a big flinch, resort to a small one |
|
if ( SelectWeightedSequence ( flinchActivity ) == ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
flinchActivity = bGesture ? ACT_GESTURE_SMALL_FLINCH : ACT_SMALL_FLINCH; |
|
} |
|
} |
|
else |
|
{ |
|
flinchActivity = bGesture ? ACT_GESTURE_SMALL_FLINCH : ACT_SMALL_FLINCH; |
|
} |
|
} |
|
|
|
return flinchActivity; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CleanupOnDeath( CBaseEntity *pCulprit, bool bFireDeathOutput ) |
|
{ |
|
if ( !m_bDidDeathCleanup ) |
|
{ |
|
m_bDidDeathCleanup = true; |
|
|
|
if ( m_NPCState == NPC_STATE_SCRIPT && m_hCine ) |
|
{ |
|
// bail out of this script here |
|
m_hCine->CancelScript(); |
|
// now keep going with the death code |
|
} |
|
|
|
if ( GetHintNode() ) |
|
{ |
|
GetHintNode()->Unlock(); |
|
SetHintNode( NULL ); |
|
} |
|
|
|
if( bFireDeathOutput ) |
|
{ |
|
m_OnDeath.FireOutput( pCulprit, this ); |
|
} |
|
|
|
// Vacate any strategy slot I might have |
|
VacateStrategySlot(); |
|
|
|
// Remove from squad if in one |
|
if (m_pSquad) |
|
{ |
|
// If I'm in idle it means that I didn't see who killed me |
|
// and my squad is still in idle state. Tell squad we have |
|
// an enemy to wake them up and put the enemy position at |
|
// my death position |
|
if ( m_NPCState == NPC_STATE_IDLE && pCulprit) |
|
{ |
|
// If we already have some danger memory, don't do this cheat |
|
if ( GetEnemies()->GetDangerMemory() == NULL ) |
|
{ |
|
UpdateEnemyMemory( pCulprit, GetAbsOrigin() ); |
|
} |
|
} |
|
|
|
// Remove from squad |
|
m_pSquad->RemoveFromSquad(this, true); |
|
m_pSquad = NULL; |
|
} |
|
|
|
RemoveActorFromScriptedScenes( this, false /*all scenes*/ ); |
|
} |
|
else |
|
DevMsg( "Unexpected double-death-cleanup\n" ); |
|
} |
|
|
|
void CAI_BaseNPC::SelectDeathPose( const CTakeDamageInfo &info ) |
|
{ |
|
if ( !GetModelPtr() || (info.GetDamageType() & DMG_PREVENT_PHYSICS_FORCE) ) |
|
return; |
|
|
|
if ( ShouldPickADeathPose() == false ) |
|
return; |
|
|
|
Activity aActivity = ACT_INVALID; |
|
int iDeathFrame = 0; |
|
|
|
SelectDeathPoseActivityAndFrame( this, info, LastHitGroup(), aActivity, iDeathFrame ); |
|
if ( aActivity == ACT_INVALID ) |
|
{ |
|
SetDeathPose( ACT_INVALID ); |
|
SetDeathPoseFrame( 0 ); |
|
return; |
|
} |
|
|
|
SetDeathPose( SelectWeightedSequence( aActivity ) ); |
|
SetDeathPoseFrame( iDeathFrame ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
if (IsCurSchedule(SCHED_NPC_FREEZE)) |
|
{ |
|
// We're frozen; don't die. |
|
return; |
|
} |
|
|
|
Wake( false ); |
|
|
|
//Adrian: Select a death pose to extrapolate the ragdoll's velocity. |
|
SelectDeathPose( info ); |
|
|
|
m_lifeState = LIFE_DYING; |
|
|
|
CleanupOnDeath( info.GetAttacker() ); |
|
|
|
StopLoopingSounds(); |
|
DeathSound( info ); |
|
|
|
if ( ( GetFlags() & FL_NPC ) && ( ShouldGib( info ) == false ) ) |
|
{ |
|
SetTouch( NULL ); |
|
} |
|
|
|
BaseClass::Event_Killed( info ); |
|
|
|
if ( m_bFadeCorpse ) |
|
{ |
|
m_bImportanRagdoll = RagdollManager_SaveImportant( this ); |
|
} |
|
|
|
// Make sure this condition is fired too (OnTakeDamage breaks out before this happens on death) |
|
SetCondition( COND_LIGHT_DAMAGE ); |
|
SetIdealState( NPC_STATE_DEAD ); |
|
|
|
// Some characters rely on getting a state transition, even to death. |
|
// zombies, for instance. When a character becomes a ragdoll, their |
|
// server entity ceases to think, so we have to set the dead state here |
|
// because the AI code isn't going to pick up the change on the next think |
|
// for us. |
|
|
|
// Adrian - Only set this if we are going to become a ragdoll. We still want to |
|
// select SCHED_DIE or do something special when this NPC dies and we wont |
|
// catch the change of state if we set this to whatever the ideal state is. |
|
if ( CanBecomeRagdoll() || IsRagdoll() ) |
|
SetState( NPC_STATE_DEAD ); |
|
|
|
// If the remove-no-ragdoll flag is set in the damage type, we're being |
|
// told to remove ourselves immediately on death. This is used when something |
|
// else has some special reason for us to vanish instead of creating a ragdoll. |
|
// i.e. The barnacle does this because it's already got a ragdoll for us. |
|
if ( info.GetDamageType() & DMG_REMOVENORAGDOLL ) |
|
{ |
|
if ( !IsEFlagSet( EFL_IS_BEING_LIFTED_BY_BARNACLE ) ) |
|
{ |
|
// Go away |
|
RemoveDeferred(); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner ) |
|
{ |
|
BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); |
|
|
|
#ifdef HL2_EPISODIC |
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
if ( pPlayer->IRelationType( this ) != D_LI ) |
|
{ |
|
CNPC_Alyx *alyx = CNPC_Alyx::GetAlyx(); |
|
|
|
if ( alyx ) |
|
{ |
|
alyx->EnemyIgnited( this ); |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
ConVar ai_block_damage( "ai_block_damage","0" ); |
|
|
|
bool CAI_BaseNPC::PassesDamageFilter( const CTakeDamageInfo &info ) |
|
{ |
|
if ( ai_block_damage.GetBool() ) |
|
return false; |
|
// FIXME: hook a friendly damage filter to the npc instead? |
|
if ( (CapabilitiesGet() & bits_CAP_FRIENDLY_DMG_IMMUNE) && info.GetAttacker() && info.GetAttacker() != this ) |
|
{ |
|
// check attackers relationship with me |
|
CBaseCombatCharacter *npcEnemy = info.GetAttacker()->MyCombatCharacterPointer(); |
|
bool bHitByVehicle = false; |
|
if ( !npcEnemy ) |
|
{ |
|
if ( info.GetAttacker()->GetServerVehicle() ) |
|
{ |
|
bHitByVehicle = true; |
|
} |
|
} |
|
|
|
if ( bHitByVehicle || (npcEnemy && npcEnemy->IRelationType( this ) == D_LI) ) |
|
{ |
|
m_fNoDamageDecal = true; |
|
|
|
if ( npcEnemy && npcEnemy->IsPlayer() ) |
|
{ |
|
m_OnDamagedByPlayer.FireOutput( info.GetAttacker(), this ); |
|
// This also counts as being harmed by player's squad. |
|
m_OnDamagedByPlayerSquad.FireOutput( info.GetAttacker(), this ); |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
|
|
if ( !BaseClass::PassesDamageFilter( info ) ) |
|
{ |
|
m_fNoDamageDecal = true; |
|
return false; |
|
} |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::OnTakeDamage_Alive( const CTakeDamageInfo &info ) |
|
{ |
|
Forget( bits_MEMORY_INCOVER ); |
|
|
|
if ( !BaseClass::OnTakeDamage_Alive( info ) ) |
|
return 0; |
|
|
|
if ( GetSleepState() == AISS_WAITING_FOR_THREAT ) |
|
Wake(); |
|
|
|
// NOTE: This must happen after the base class is called; we need to reduce |
|
// health before the pain sound, since some NPCs use the final health |
|
// level as a modifier to determine which pain sound to use. |
|
|
|
// REVISIT: Combine soldiers shoot each other a lot and then talk about it |
|
// this improves that case a bunch, but it seems kind of harsh. |
|
if ( !m_pSquad || !m_pSquad->SquadIsMember( info.GetAttacker() ) ) |
|
{ |
|
PainSound( info );// "Ouch!" |
|
} |
|
|
|
// See if we're running a dynamic interaction that should break when I am damaged. |
|
if ( IsActiveDynamicInteraction() ) |
|
{ |
|
ScriptedNPCInteraction_t *pInteraction = GetRunningDynamicInteraction(); |
|
if ( pInteraction->iLoopBreakTriggerMethod & SNPCINT_LOOPBREAK_ON_DAMAGE ) |
|
{ |
|
// Can only break when we're in the action anim |
|
if ( m_hCine->IsPlayingAction() ) |
|
{ |
|
m_hCine->StopActionLoop( true ); |
|
} |
|
} |
|
} |
|
|
|
// If we're not allowed to die, refuse to die |
|
// Allow my interaction partner to kill me though |
|
if ( m_iHealth <= 0 && HasInteractionCantDie() && info.GetAttacker() != m_hInteractionPartner ) |
|
{ |
|
m_iHealth = 1; |
|
} |
|
|
|
#if 0 |
|
// HACKHACK Don't kill npcs in a script. Let them break their scripts first |
|
// THIS is a Half-Life 1 hack that's not cutting the mustard in the scripts |
|
// that have been authored for Half-Life 2 thus far. (sjb) |
|
if ( m_NPCState == NPC_STATE_SCRIPT ) |
|
{ |
|
SetCondition( COND_LIGHT_DAMAGE ); |
|
} |
|
#endif |
|
|
|
// ----------------------------------- |
|
// Fire outputs |
|
// ----------------------------------- |
|
if ( m_flLastDamageTime != gpGlobals->curtime ) |
|
{ |
|
// only fire once per frame |
|
m_OnDamaged.FireOutput( info.GetAttacker(), this); |
|
|
|
if( info.GetAttacker()->IsPlayer() ) |
|
{ |
|
m_OnDamagedByPlayer.FireOutput( info.GetAttacker(), this ); |
|
|
|
// This also counts as being harmed by player's squad. |
|
m_OnDamagedByPlayerSquad.FireOutput( info.GetAttacker(), this ); |
|
} |
|
else |
|
{ |
|
// See if the person that injured me is an NPC. |
|
CAI_BaseNPC *pAttacker = dynamic_cast<CAI_BaseNPC *>( info.GetAttacker() ); |
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
|
|
if( pAttacker && pAttacker->IsAlive() && pPlayer ) |
|
{ |
|
if( pAttacker->GetSquad() != NULL && pAttacker->IsInPlayerSquad() ) |
|
{ |
|
m_OnDamagedByPlayerSquad.FireOutput( info.GetAttacker(), this ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if( (info.GetDamageType() & DMG_CRUSH) && !(info.GetDamageType() & DMG_PHYSGUN) && info.GetDamage() >= MIN_PHYSICS_FLINCH_DAMAGE ) |
|
{ |
|
SetCondition( COND_PHYSICS_DAMAGE ); |
|
} |
|
|
|
if ( m_iHealth <= ( m_iMaxHealth / 2 ) ) |
|
{ |
|
m_OnHalfHealth.FireOutput( info.GetAttacker(), this ); |
|
} |
|
|
|
// react to the damage (get mad) |
|
if ( ( (GetFlags() & FL_NPC) == 0 ) || !info.GetAttacker() ) |
|
return 1; |
|
|
|
// If the attacker was an NPC or client update my position memory |
|
if ( info.GetAttacker()->GetFlags() & (FL_NPC | FL_CLIENT) ) |
|
{ |
|
// ------------------------------------------------------------------ |
|
// DO NOT CHANGE THIS CODE W/O CONSULTING |
|
// Only update information about my attacker I don't see my attacker |
|
// ------------------------------------------------------------------ |
|
if ( !FInViewCone( info.GetAttacker() ) || !FVisible( info.GetAttacker() ) ) |
|
{ |
|
// ------------------------------------------------------------- |
|
// If I have an inflictor (enemy / grenade) update memory with |
|
// position of inflictor, otherwise update with an position |
|
// estimate for where the attack came from |
|
// ------------------------------------------------------ |
|
Vector vAttackPos; |
|
if (info.GetInflictor()) |
|
{ |
|
vAttackPos = info.GetInflictor()->GetAbsOrigin(); |
|
} |
|
else |
|
{ |
|
vAttackPos = (GetAbsOrigin() + ( g_vecAttackDir * 64 )); |
|
} |
|
|
|
|
|
// ---------------------------------------------------------------- |
|
// If I already have an enemy, assume that the attack |
|
// came from the enemy and update my enemy's position |
|
// unless I already know about the attacker or I can see my enemy |
|
// ---------------------------------------------------------------- |
|
if ( GetEnemy() != NULL && |
|
!GetEnemies()->HasMemory( info.GetAttacker() ) && |
|
!HasCondition(COND_SEE_ENEMY) ) |
|
{ |
|
UpdateEnemyMemory(GetEnemy(), vAttackPos, GetEnemy()); |
|
} |
|
// ---------------------------------------------------------------- |
|
// If I already know about this enemy, update his position |
|
// ---------------------------------------------------------------- |
|
else if (GetEnemies()->HasMemory( info.GetAttacker() )) |
|
{ |
|
UpdateEnemyMemory(info.GetAttacker(), vAttackPos); |
|
} |
|
// ----------------------------------------------------------------- |
|
// Otherwise just note the position, but don't add enemy to my list |
|
// ----------------------------------------------------------------- |
|
else |
|
{ |
|
UpdateEnemyMemory(NULL, vAttackPos); |
|
} |
|
} |
|
|
|
// add pain to the conditions |
|
if ( IsLightDamage( info ) ) |
|
{ |
|
SetCondition( COND_LIGHT_DAMAGE ); |
|
} |
|
if ( IsHeavyDamage( info ) ) |
|
{ |
|
SetCondition( COND_HEAVY_DAMAGE ); |
|
} |
|
|
|
ForceGatherConditions(); |
|
|
|
// Keep track of how much consecutive damage I have recieved |
|
if ((gpGlobals->curtime - m_flLastDamageTime) < 1.0) |
|
{ |
|
m_flSumDamage += info.GetDamage(); |
|
} |
|
else |
|
{ |
|
m_flSumDamage = info.GetDamage(); |
|
} |
|
m_flLastDamageTime = gpGlobals->curtime; |
|
if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) |
|
m_flLastPlayerDamageTime = gpGlobals->curtime; |
|
GetEnemies()->OnTookDamageFrom( info.GetAttacker() ); |
|
|
|
if (m_flSumDamage > m_iMaxHealth*0.3) |
|
{ |
|
SetCondition(COND_REPEATED_DAMAGE); |
|
} |
|
|
|
NotifyFriendsOfDamage( info.GetAttacker() ); |
|
} |
|
|
|
// --------------------------------------------------------------- |
|
// Insert a combat sound so that nearby NPCs know I've been hit |
|
// --------------------------------------------------------------- |
|
CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), 1024, 0.5, this, SOUNDENT_CHANNEL_INJURY ); |
|
|
|
return 1; |
|
} |
|
|
|
|
|
//========================================================= |
|
// OnTakeDamage_Dying - takedamage function called when a npc's |
|
// corpse is damaged. |
|
//========================================================= |
|
int CAI_BaseNPC::OnTakeDamage_Dying( const CTakeDamageInfo &info ) |
|
{ |
|
if ( info.GetDamageType() & DMG_PLASMA ) |
|
{ |
|
if ( m_takedamage != DAMAGE_EVENTS_ONLY ) |
|
{ |
|
m_iHealth -= info.GetDamage(); |
|
|
|
if (m_iHealth < -500) |
|
{ |
|
UTIL_Remove(this); |
|
} |
|
} |
|
} |
|
return BaseClass::OnTakeDamage_Dying( info ); |
|
} |
|
|
|
//========================================================= |
|
// OnTakeDamage_Dead - takedamage function called when a npc's |
|
// corpse is damaged. |
|
//========================================================= |
|
int CAI_BaseNPC::OnTakeDamage_Dead( const CTakeDamageInfo &info ) |
|
{ |
|
Vector vecDir; |
|
|
|
// grab the vector of the incoming attack. ( pretend that the inflictor is a little lower than it really is, so the body will tend to fly upward a bit). |
|
vecDir = vec3_origin; |
|
if ( info.GetInflictor() ) |
|
{ |
|
vecDir = info.GetInflictor()->WorldSpaceCenter() - Vector ( 0, 0, 10 ) - WorldSpaceCenter(); |
|
VectorNormalize( vecDir ); |
|
g_vecAttackDir = vecDir; |
|
} |
|
|
|
#if 0// turn this back on when the bounding box issues are resolved. |
|
|
|
SetGroundEntity( NULL ); |
|
GetLocalOrigin().z += 1; |
|
|
|
// let the damage scoot the corpse around a bit. |
|
if ( info.GetInflictor() && (info.GetAttacker()->GetSolid() != SOLID_TRIGGER) ) |
|
{ |
|
ApplyAbsVelocityImpulse( vecDir * -DamageForce( flDamage ) ); |
|
} |
|
|
|
#endif |
|
|
|
// kill the corpse if enough damage was done to destroy the corpse and the damage is of a type that is allowed to destroy the corpse. |
|
if ( g_pGameRules->Damage_ShouldGibCorpse( info.GetDamageType() ) ) |
|
{ |
|
// Accumulate corpse gibbing damage, so you can gib with multiple hits |
|
if ( m_takedamage != DAMAGE_EVENTS_ONLY ) |
|
{ |
|
m_iHealth -= info.GetDamage() * 0.1; |
|
} |
|
} |
|
|
|
if ( info.GetDamageType() & DMG_PLASMA ) |
|
{ |
|
if ( m_takedamage != DAMAGE_EVENTS_ONLY ) |
|
{ |
|
m_iHealth -= info.GetDamage(); |
|
|
|
if (m_iHealth < -500) |
|
{ |
|
UTIL_Remove(this); |
|
} |
|
} |
|
} |
|
|
|
return 1; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::NotifyFriendsOfDamage( CBaseEntity *pAttackerEntity ) |
|
{ |
|
CAI_BaseNPC *pAttacker = pAttackerEntity->MyNPCPointer(); |
|
if ( pAttacker ) |
|
{ |
|
const Vector &origin = GetAbsOrigin(); |
|
for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) |
|
{ |
|
const float NEAR_Z = 10*12; |
|
const float NEAR_XY_SQ = Square( 50*12 ); |
|
CAI_BaseNPC *pNpc = g_AI_Manager.AccessAIs()[i]; |
|
if ( pNpc && pNpc != this ) |
|
{ |
|
const Vector &originNpc = pNpc->GetAbsOrigin(); |
|
if ( fabsf( originNpc.z - origin.z ) < NEAR_Z ) |
|
{ |
|
if ( (originNpc.AsVector2D() - origin.AsVector2D()).LengthSqr() < NEAR_XY_SQ ) |
|
{ |
|
if ( pNpc->GetSquad() == GetSquad() || IRelationType( pNpc ) == D_LI ) |
|
pNpc->OnFriendDamaged( this, pAttacker ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnFriendDamaged( CBaseCombatCharacter *pSquadmate, CBaseEntity *pAttacker ) |
|
{ |
|
if ( GetSleepState() != AISS_WAITING_FOR_INPUT ) |
|
{ |
|
float distSqToThreat = ( GetAbsOrigin() - pAttacker->GetAbsOrigin() ).LengthSqr(); |
|
|
|
if ( GetSleepState() != AISS_AWAKE && distSqToThreat < Square( 20 * 12 ) ) |
|
Wake(); |
|
|
|
if ( distSqToThreat < Square( 50 * 12 ) ) |
|
ForceGatherConditions(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsLightDamage( const CTakeDamageInfo &info ) |
|
{ |
|
// ALL nonzero damage is light damage! Mask off COND_LIGHT_DAMAGE if you want to ignore light damage. |
|
return ( info.GetDamage() > 0 ); |
|
} |
|
|
|
bool CAI_BaseNPC::IsHeavyDamage( const CTakeDamageInfo &info ) |
|
{ |
|
return ( info.GetDamage() > 20 ); |
|
} |
|
|
|
void CAI_BaseNPC::DoRadiusDamage( const CTakeDamageInfo &info, int iClassIgnore, CBaseEntity *pEntityIgnore ) |
|
{ |
|
RadiusDamage( info, GetAbsOrigin(), info.GetDamage() * 2.5, iClassIgnore, pEntityIgnore ); |
|
} |
|
|
|
|
|
void CAI_BaseNPC::DoRadiusDamage( const CTakeDamageInfo &info, const Vector &vecSrc, int iClassIgnore, CBaseEntity *pEntityIgnore ) |
|
{ |
|
RadiusDamage( info, vecSrc, info.GetDamage() * 2.5, iClassIgnore, pEntityIgnore ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Set the contents types that are solid by default to all NPCs |
|
//----------------------------------------------------------------------------- |
|
unsigned int CAI_BaseNPC::PhysicsSolidMaskForEntity( void ) const |
|
{ |
|
return MASK_NPCSOLID; |
|
} |
|
|
|
|
|
//========================================================= |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::DecalTrace( trace_t *pTrace, char const *decalName ) |
|
{ |
|
if ( m_fNoDamageDecal ) |
|
{ |
|
m_fNoDamageDecal = false; |
|
// @Note (toml 04-23-03): e3, don't decal face on damage if still alive |
|
return; |
|
} |
|
BaseClass::DecalTrace( pTrace, decalName ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ImpactTrace( trace_t *pTrace, int iDamageType, const char *pCustomImpactName ) |
|
{ |
|
if ( m_fNoDamageDecal ) |
|
{ |
|
m_fNoDamageDecal = false; |
|
// @Note (toml 04-23-03): e3, don't decal face on damage if still alive |
|
return; |
|
} |
|
BaseClass::ImpactTrace( pTrace, iDamageType, pCustomImpactName ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Return the number by which to multiply incoming damage |
|
// based on the hitgroup it hits. This exists mainly so |
|
// that individual NPC's can have more or less resistance |
|
// to damage done to certain hitgroups. |
|
//--------------------------------------------------------- |
|
float CAI_BaseNPC::GetHitgroupDamageMultiplier( int iHitGroup, const CTakeDamageInfo &info ) |
|
{ |
|
switch( iHitGroup ) |
|
{ |
|
case HITGROUP_GENERIC: |
|
return 1.0f; |
|
|
|
case HITGROUP_HEAD: |
|
return sk_npc_head.GetFloat(); |
|
|
|
case HITGROUP_CHEST: |
|
return sk_npc_chest.GetFloat(); |
|
|
|
case HITGROUP_STOMACH: |
|
return sk_npc_stomach.GetFloat(); |
|
|
|
case HITGROUP_LEFTARM: |
|
case HITGROUP_RIGHTARM: |
|
return sk_npc_arm.GetFloat(); |
|
|
|
case HITGROUP_LEFTLEG: |
|
case HITGROUP_RIGHTLEG: |
|
return sk_npc_leg.GetFloat(); |
|
|
|
default: |
|
return 1.0f; |
|
} |
|
} |
|
|
|
//========================================================= |
|
// TraceAttack |
|
//========================================================= |
|
void CAI_BaseNPC::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) |
|
{ |
|
m_fNoDamageDecal = false; |
|
if ( m_takedamage == DAMAGE_NO ) |
|
return; |
|
|
|
CTakeDamageInfo subInfo = info; |
|
|
|
SetLastHitGroup( ptr->hitgroup ); |
|
m_nForceBone = ptr->physicsbone; // save this bone for physics forces |
|
|
|
Assert( m_nForceBone > -255 && m_nForceBone < 256 ); |
|
|
|
bool bDebug = showhitlocation.GetBool(); |
|
|
|
switch ( ptr->hitgroup ) |
|
{ |
|
case HITGROUP_GENERIC: |
|
if( bDebug ) DevMsg("Hit Location: Generic\n"); |
|
break; |
|
|
|
// hit gear, react but don't bleed |
|
case HITGROUP_GEAR: |
|
subInfo.SetDamage( 0.01 ); |
|
ptr->hitgroup = HITGROUP_GENERIC; |
|
if( bDebug ) DevMsg("Hit Location: Gear\n"); |
|
break; |
|
|
|
case HITGROUP_HEAD: |
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) ); |
|
if( bDebug ) DevMsg("Hit Location: Head\n"); |
|
break; |
|
|
|
case HITGROUP_CHEST: |
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) ); |
|
if( bDebug ) DevMsg("Hit Location: Chest\n"); |
|
break; |
|
|
|
case HITGROUP_STOMACH: |
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) ); |
|
if( bDebug ) DevMsg("Hit Location: Stomach\n"); |
|
break; |
|
|
|
case HITGROUP_LEFTARM: |
|
case HITGROUP_RIGHTARM: |
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) ); |
|
if( bDebug ) DevMsg("Hit Location: Left/Right Arm\n"); |
|
break |
|
; |
|
case HITGROUP_LEFTLEG: |
|
case HITGROUP_RIGHTLEG: |
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) ); |
|
if( bDebug ) DevMsg("Hit Location: Left/Right Leg\n"); |
|
break; |
|
|
|
default: |
|
if( bDebug ) DevMsg("Hit Location: UNKNOWN\n"); |
|
break; |
|
} |
|
|
|
if ( subInfo.GetDamage() >= 1.0 && !(subInfo.GetDamageType() & DMG_SHOCK ) ) |
|
{ |
|
if( !IsPlayer() || ( IsPlayer() && g_pGameRules->IsMultiplayer() ) ) |
|
{ |
|
// NPC's always bleed. Players only bleed in multiplayer. |
|
SpawnBlood( ptr->endpos, vecDir, BloodColor(), subInfo.GetDamage() );// a little surface blood. |
|
} |
|
|
|
TraceBleed( subInfo.GetDamage(), vecDir, ptr, subInfo.GetDamageType() ); |
|
|
|
if ( ptr->hitgroup == HITGROUP_HEAD && m_iHealth - subInfo.GetDamage() > 0 ) |
|
{ |
|
m_fNoDamageDecal = true; |
|
} |
|
} |
|
|
|
// Airboat gun will impart major force if it's about to kill him.... |
|
if ( info.GetDamageType() & DMG_AIRBOAT ) |
|
{ |
|
if ( subInfo.GetDamage() >= GetHealth() ) |
|
{ |
|
float flMagnitude = subInfo.GetDamageForce().Length(); |
|
if ( (flMagnitude != 0.0f) && (flMagnitude < 400.0f * 65.0f) ) |
|
{ |
|
subInfo.ScaleDamageForce( 400.0f * 65.0f / flMagnitude ); |
|
} |
|
} |
|
} |
|
|
|
if( info.GetInflictor() ) |
|
{ |
|
subInfo.SetInflictor( info.GetInflictor() ); |
|
} |
|
else |
|
{ |
|
subInfo.SetInflictor( info.GetAttacker() ); |
|
} |
|
|
|
AddMultiDamage( subInfo, this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks if point is in spread angle between source and target Pos |
|
// Used to prevent friendly fire |
|
// Input : Source of attack, target position, spread angle |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::PointInSpread( CBaseCombatCharacter *pCheckEntity, const Vector &sourcePos, const Vector &targetPos, const Vector &testPoint, float flSpread, float maxDistOffCenter ) |
|
{ |
|
float distOffLine = CalcDistanceToLine2D( testPoint.AsVector2D(), sourcePos.AsVector2D(), targetPos.AsVector2D() ); |
|
if ( distOffLine < maxDistOffCenter ) |
|
{ |
|
Vector toTarget = targetPos - sourcePos; |
|
float distTarget = VectorNormalize(toTarget); |
|
|
|
Vector toTest = testPoint - sourcePos; |
|
float distTest = VectorNormalize(toTest); |
|
// Only reject if target is on other side |
|
if (distTarget > distTest) |
|
{ |
|
toTarget.z = 0.0; |
|
toTest.z = 0.0; |
|
|
|
float dotProduct = DotProduct(toTarget,toTest); |
|
if (dotProduct > flSpread) |
|
{ |
|
return true; |
|
} |
|
else if( dotProduct > 0.0f ) |
|
{ |
|
// If this guy is in front, do the hull/line test: |
|
if( pCheckEntity ) |
|
{ |
|
float flBBoxDist = NAI_Hull::Width( pCheckEntity->GetHullType() ); |
|
flBBoxDist *= 1.414f; // sqrt(2) |
|
|
|
// !!!BUGBUG - this 2d check will stop a citizen shooting at a gunship or strider |
|
// if another citizen is between them, even though the strider or gunship may be |
|
// high up in the air (sjb) |
|
distOffLine = CalcDistanceToLine( testPoint, sourcePos, targetPos ); |
|
if( distOffLine < flBBoxDist ) |
|
{ |
|
return true; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks if player is in spread angle between source and target Pos |
|
// Used to prevent friendly fire |
|
// Input : Source of attack, target position, spread angle |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::PlayerInSpread( const Vector &sourcePos, const Vector &targetPos, float flSpread, float maxDistOffCenter, bool ignoreHatedPlayers ) |
|
{ |
|
// loop through all players |
|
for (int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( pPlayer && ( !ignoreHatedPlayers || IRelationType( pPlayer ) != D_HT ) ) |
|
{ |
|
if ( PointInSpread( pPlayer, sourcePos, targetPos, pPlayer->WorldSpaceCenter(), flSpread, maxDistOffCenter ) ) |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks if player is in range of given location. Used by NPCs |
|
// to prevent friendly fire |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CBaseEntity *CAI_BaseNPC::PlayerInRange( const Vector &vecLocation, float flDist ) |
|
{ |
|
// loop through all players |
|
for (int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if (pPlayer && (vecLocation - pPlayer->WorldSpaceCenter() ).Length2D() <= flDist) |
|
{ |
|
return pPlayer; |
|
} |
|
} |
|
return NULL; |
|
} |
|
|
|
|
|
#define BULLET_WIZZDIST 80.0 |
|
#define SLOPE ( -1.0 / BULLET_WIZZDIST ) |
|
|
|
void BulletWizz( Vector vecSrc, Vector vecEndPos, edict_t *pShooter, bool isTracer ) |
|
{ |
|
CBasePlayer *pPlayer; |
|
Vector vecBulletPath; |
|
Vector vecPlayerPath; |
|
Vector vecBulletDir; |
|
Vector vecNearestPoint; |
|
float flDist; |
|
float flBulletDist; |
|
|
|
vecBulletPath = vecEndPos - vecSrc; |
|
vecBulletDir = vecBulletPath; |
|
VectorNormalize(vecBulletDir); |
|
|
|
// see how near this bullet passed by player in a single player game |
|
// for multiplayer, we need to go through the list of clients. |
|
for (int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( !pPlayer ) |
|
continue; |
|
|
|
// Don't hear one's own bullets |
|
if( pPlayer->edict() == pShooter ) |
|
continue; |
|
|
|
vecPlayerPath = pPlayer->EarPosition() - vecSrc; |
|
flDist = DotProduct( vecPlayerPath, vecBulletDir ); |
|
vecNearestPoint = vecSrc + vecBulletDir * flDist; |
|
// FIXME: minus m_vecViewOffset? |
|
flBulletDist = ( vecNearestPoint - pPlayer->EarPosition() ).Length(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Hits triggers with raycasts |
|
//----------------------------------------------------------------------------- |
|
class CTriggerTraceEnum : public IEntityEnumerator |
|
{ |
|
public: |
|
CTriggerTraceEnum( Ray_t *pRay, const CTakeDamageInfo &info, const Vector& dir, int contentsMask ) : |
|
m_info( info ), m_VecDir(dir), m_ContentsMask(contentsMask), m_pRay(pRay) |
|
{ |
|
} |
|
|
|
virtual bool EnumEntity( IHandleEntity *pHandleEntity ) |
|
{ |
|
trace_t tr; |
|
|
|
CBaseEntity *pEnt = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() ); |
|
|
|
// Done to avoid hitting an entity that's both solid & a trigger. |
|
if ( pEnt->IsSolid() ) |
|
return true; |
|
|
|
enginetrace->ClipRayToEntity( *m_pRay, m_ContentsMask, pHandleEntity, &tr ); |
|
if (tr.fraction < 1.0f) |
|
{ |
|
pEnt->DispatchTraceAttack( m_info, m_VecDir, &tr ); |
|
ApplyMultiDamage(); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
private: |
|
Vector m_VecDir; |
|
int m_ContentsMask; |
|
Ray_t *m_pRay; |
|
CTakeDamageInfo m_info; |
|
}; |
|
|
|
void CBaseEntity::TraceAttackToTriggers( const CTakeDamageInfo &info, const Vector& start, const Vector& end, const Vector& dir ) |
|
{ |
|
Ray_t ray; |
|
ray.Init( start, end ); |
|
|
|
CTriggerTraceEnum triggerTraceEnum( &ray, info, dir, MASK_SHOT ); |
|
enginetrace->EnumerateEntities( ray, true, &triggerTraceEnum ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : const char |
|
//----------------------------------------------------------------------------- |
|
const char *CAI_BaseNPC::GetTracerType( void ) |
|
{ |
|
if ( GetActiveWeapon() ) |
|
{ |
|
return GetActiveWeapon()->GetTracerType(); |
|
} |
|
|
|
return BaseClass::GetTracerType(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &vecTracerSrc - |
|
// &tr - |
|
// iTracerType - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ) |
|
{ |
|
if ( GetActiveWeapon() ) |
|
{ |
|
GetActiveWeapon()->MakeTracer( vecTracerSrc, tr, iTracerType ); |
|
return; |
|
} |
|
|
|
BaseClass::MakeTracer( vecTracerSrc, tr, iTracerType ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::FireBullets( const FireBulletsInfo_t &info ) |
|
{ |
|
#ifdef HL2_DLL |
|
// If we're shooting at a bullseye, become perfectly accurate if the bullseye demands it |
|
if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) |
|
{ |
|
CNPC_Bullseye *pBullseye = dynamic_cast<CNPC_Bullseye*>(GetEnemy()); |
|
if ( pBullseye && pBullseye->UsePerfectAccuracy() ) |
|
{ |
|
FireBulletsInfo_t accurateInfo = info; |
|
accurateInfo.m_vecSpread = vec3_origin; |
|
BaseClass::FireBullets( accurateInfo ); |
|
return; |
|
} |
|
} |
|
#endif |
|
|
|
BaseClass::FireBullets( info ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Shot statistics |
|
//----------------------------------------------------------------------------- |
|
void CBaseEntity::UpdateShotStatistics( const trace_t &tr ) |
|
{ |
|
if ( ai_shot_stats.GetBool() ) |
|
{ |
|
CAI_BaseNPC *pNpc = MyNPCPointer(); |
|
if ( pNpc ) |
|
{ |
|
pNpc->m_TotalShots++; |
|
if ( tr.m_pEnt == pNpc->GetEnemy() ) |
|
{ |
|
pNpc->m_TotalHits++; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Handle shot entering water |
|
//----------------------------------------------------------------------------- |
|
void CBaseEntity::HandleShotImpactingGlass( const FireBulletsInfo_t &info, |
|
const trace_t &tr, const Vector &vecDir, ITraceFilter *pTraceFilter ) |
|
{ |
|
// Move through the glass until we're at the other side |
|
Vector testPos = tr.endpos + ( vecDir * MAX_GLASS_PENETRATION_DEPTH ); |
|
|
|
CEffectData data; |
|
|
|
data.m_vNormal = tr.plane.normal; |
|
data.m_vOrigin = tr.endpos; |
|
|
|
DispatchEffect( "GlassImpact", data ); |
|
|
|
trace_t penetrationTrace; |
|
|
|
// Re-trace as if the bullet had passed right through |
|
UTIL_TraceLine( testPos, tr.endpos, MASK_SHOT, pTraceFilter, &penetrationTrace ); |
|
|
|
// See if we found the surface again |
|
if ( penetrationTrace.startsolid || tr.fraction == 0.0f || penetrationTrace.fraction == 1.0f ) |
|
return; |
|
|
|
//FIXME: This is technically frustrating MultiDamage, but multiple shots hitting multiple targets in one call |
|
// would do exactly the same anyway... |
|
|
|
// Impact the other side (will look like an exit effect) |
|
DoImpactEffect( penetrationTrace, GetAmmoDef()->DamageType(info.m_iAmmoType) ); |
|
|
|
data.m_vNormal = penetrationTrace.plane.normal; |
|
data.m_vOrigin = penetrationTrace.endpos; |
|
|
|
DispatchEffect( "GlassImpact", data ); |
|
|
|
// Refire the round, as if starting from behind the glass |
|
FireBulletsInfo_t behindGlassInfo; |
|
behindGlassInfo.m_iShots = 1; |
|
behindGlassInfo.m_vecSrc = penetrationTrace.endpos; |
|
behindGlassInfo.m_vecDirShooting = vecDir; |
|
behindGlassInfo.m_vecSpread = vec3_origin; |
|
behindGlassInfo.m_flDistance = info.m_flDistance*( 1.0f - tr.fraction ); |
|
behindGlassInfo.m_iAmmoType = info.m_iAmmoType; |
|
behindGlassInfo.m_iTracerFreq = info.m_iTracerFreq; |
|
behindGlassInfo.m_flDamage = info.m_flDamage; |
|
behindGlassInfo.m_pAttacker = info.m_pAttacker ? info.m_pAttacker : this; |
|
behindGlassInfo.m_nFlags = info.m_nFlags; |
|
|
|
FireBullets( behindGlassInfo ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Computes the tracer start position |
|
//----------------------------------------------------------------------------- |
|
#define SHOT_UNDERWATER_BUBBLE_DIST 400 |
|
|
|
void CBaseEntity::CreateBubbleTrailTracer( const Vector &vecShotSrc, const Vector &vecShotEnd, const Vector &vecShotDir ) |
|
{ |
|
int nBubbles; |
|
Vector vecBubbleEnd; |
|
float flLengthSqr = vecShotSrc.DistToSqr( vecShotEnd ); |
|
if ( flLengthSqr > SHOT_UNDERWATER_BUBBLE_DIST * SHOT_UNDERWATER_BUBBLE_DIST ) |
|
{ |
|
VectorMA( vecShotSrc, SHOT_UNDERWATER_BUBBLE_DIST, vecShotDir, vecBubbleEnd ); |
|
nBubbles = WATER_BULLET_BUBBLES_PER_INCH * SHOT_UNDERWATER_BUBBLE_DIST; |
|
} |
|
else |
|
{ |
|
float flLength = sqrt(flLengthSqr) - 0.1f; |
|
nBubbles = WATER_BULLET_BUBBLES_PER_INCH * flLength; |
|
VectorMA( vecShotSrc, flLength, vecShotDir, vecBubbleEnd ); |
|
} |
|
|
|
Vector vecTracerSrc; |
|
ComputeTracerStartPosition( vecShotSrc, &vecTracerSrc ); |
|
UTIL_BubbleTrail( vecTracerSrc, vecBubbleEnd, nBubbles ); |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
void CAI_BaseNPC::MakeDamageBloodDecal ( int cCount, float flNoise, trace_t *ptr, Vector vecDir ) |
|
{ |
|
// make blood decal on the wall! |
|
trace_t Bloodtr; |
|
Vector vecTraceDir; |
|
int i; |
|
|
|
if ( !IsAlive() ) |
|
{ |
|
// dealing with a dead npc. |
|
if ( m_iMaxHealth <= 0 ) |
|
{ |
|
// no blood decal for a npc that has already decalled its limit. |
|
return; |
|
} |
|
else |
|
{ |
|
m_iMaxHealth -= 1; |
|
} |
|
} |
|
|
|
for ( i = 0 ; i < cCount ; i++ ) |
|
{ |
|
vecTraceDir = vecDir; |
|
|
|
vecTraceDir.x += random->RandomFloat( -flNoise, flNoise ); |
|
vecTraceDir.y += random->RandomFloat( -flNoise, flNoise ); |
|
vecTraceDir.z += random->RandomFloat( -flNoise, flNoise ); |
|
|
|
AI_TraceLine( ptr->endpos, ptr->endpos + vecTraceDir * 172, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &Bloodtr); |
|
|
|
if ( Bloodtr.fraction != 1.0 ) |
|
{ |
|
UTIL_BloodDecalTrace( &Bloodtr, BloodColor() ); |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &tr - |
|
// nDamageType - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::DoImpactEffect( trace_t &tr, int nDamageType ) |
|
{ |
|
if ( GetActiveWeapon() != NULL ) |
|
{ |
|
GetActiveWeapon()->DoImpactEffect( tr, nDamageType ); |
|
return; |
|
} |
|
|
|
BaseClass::DoImpactEffect( tr, nDamageType ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
#define InterruptFromCondition( iCondition ) \ |
|
AI_RemapFromGlobal( ( AI_IdIsLocal( iCondition ) ? GetClassScheduleIdSpace()->ConditionLocalToGlobal( iCondition ) : iCondition ) ) |
|
|
|
void CAI_BaseNPC::SetCondition( int iCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return; |
|
} |
|
|
|
m_Conditions.Set( interrupt ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CAI_BaseNPC::HasCondition( int iCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
|
|
bool bReturn = m_Conditions.IsBitSet(interrupt); |
|
return (bReturn); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CAI_BaseNPC::HasCondition( int iCondition, bool bUseIgnoreConditions ) |
|
{ |
|
if ( bUseIgnoreConditions ) |
|
return HasCondition( iCondition ); |
|
|
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
|
|
bool bReturn = m_ConditionsPreIgnore.IsBitSet(interrupt); |
|
return (bReturn); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CAI_BaseNPC::ClearCondition( int iCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return; |
|
} |
|
|
|
m_Conditions.Clear(interrupt); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CAI_BaseNPC::ClearConditions( int *pConditions, int nConditions ) |
|
{ |
|
for ( int i = 0; i < nConditions; ++i ) |
|
{ |
|
int iCondition = pConditions[i]; |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
continue; |
|
} |
|
|
|
m_Conditions.Clear( interrupt ); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CAI_BaseNPC::SetIgnoreConditions( int *pConditions, int nConditions ) |
|
{ |
|
for ( int i = 0; i < nConditions; ++i ) |
|
{ |
|
int iCondition = pConditions[i]; |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
continue; |
|
} |
|
|
|
m_InverseIgnoreConditions.Clear( interrupt ); // clear means ignore |
|
} |
|
} |
|
|
|
void CAI_BaseNPC::ClearIgnoreConditions( int *pConditions, int nConditions ) |
|
{ |
|
for ( int i = 0; i < nConditions; ++i ) |
|
{ |
|
int iCondition = pConditions[i]; |
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
continue; |
|
} |
|
|
|
m_InverseIgnoreConditions.Set( interrupt ); // set means don't ignore |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CAI_BaseNPC::HasInterruptCondition( int iCondition ) |
|
{ |
|
if( !GetCurSchedule() ) |
|
{ |
|
return false; |
|
} |
|
|
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
return ( m_Conditions.IsBitSet( interrupt ) && GetCurSchedule()->HasInterrupt( interrupt ) ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CAI_BaseNPC::ConditionInterruptsCurSchedule( int iCondition ) |
|
{ |
|
if( !GetCurSchedule() ) |
|
{ |
|
return false; |
|
} |
|
|
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
return ( GetCurSchedule()->HasInterrupt( interrupt ) ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CAI_BaseNPC::ConditionInterruptsSchedule( int localScheduleID, int iCondition ) |
|
{ |
|
CAI_Schedule *pSchedule = GetSchedule( localScheduleID ); |
|
if ( !pSchedule ) |
|
return false; |
|
|
|
int interrupt = InterruptFromCondition( iCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
return ( pSchedule->HasInterrupt( interrupt ) ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Sets the interrupt conditions for a scripted schedule |
|
// Input : interrupt - the level of interrupt we allow |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetScriptedScheduleIgnoreConditions( Interruptability_t interrupt ) |
|
{ |
|
static int g_GeneralConditions[] = |
|
{ |
|
COND_CAN_MELEE_ATTACK1, |
|
COND_CAN_MELEE_ATTACK2, |
|
COND_CAN_RANGE_ATTACK1, |
|
COND_CAN_RANGE_ATTACK2, |
|
COND_ENEMY_DEAD, |
|
COND_HEAR_BULLET_IMPACT, |
|
COND_HEAR_COMBAT, |
|
COND_HEAR_DANGER, |
|
COND_HEAR_PHYSICS_DANGER, |
|
COND_NEW_ENEMY, |
|
COND_PROVOKED, |
|
COND_SEE_ENEMY, |
|
COND_SEE_FEAR, |
|
COND_SMELL, |
|
}; |
|
|
|
static int g_DamageConditions[] = |
|
{ |
|
COND_HEAVY_DAMAGE, |
|
COND_LIGHT_DAMAGE, |
|
COND_RECEIVED_ORDERS, |
|
}; |
|
|
|
ClearIgnoreConditions( g_GeneralConditions, ARRAYSIZE(g_GeneralConditions) ); |
|
ClearIgnoreConditions( g_DamageConditions, ARRAYSIZE(g_DamageConditions) ); |
|
|
|
if ( interrupt > GENERAL_INTERRUPTABILITY ) |
|
SetIgnoreConditions( g_GeneralConditions, ARRAYSIZE(g_GeneralConditions) ); |
|
|
|
if ( interrupt > DAMAGEORDEATH_INTERRUPTABILITY ) |
|
SetIgnoreConditions( g_DamageConditions, ARRAYSIZE(g_DamageConditions) ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Returns whether we currently have any interrupt conditions that would |
|
// interrupt the given schedule. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::HasConditionsToInterruptSchedule( int nLocalScheduleID ) |
|
{ |
|
CAI_Schedule *pSchedule = GetSchedule( nLocalScheduleID ); |
|
if ( !pSchedule ) |
|
return false; |
|
|
|
CAI_ScheduleBits bitsMask; |
|
pSchedule->GetInterruptMask( &bitsMask ); |
|
|
|
CAI_ScheduleBits bitsOut; |
|
AccessConditionBits().And( bitsMask, &bitsOut ); |
|
|
|
return !bitsOut.IsAllClear(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsCustomInterruptConditionSet( int nCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( nCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return false; |
|
} |
|
|
|
return m_CustomInterruptConditions.IsBitSet( interrupt ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Sets a flag in the custom interrupt flags, translating the condition |
|
// to the proper global space, if necessary |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetCustomInterruptCondition( int nCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( nCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return; |
|
} |
|
|
|
m_CustomInterruptConditions.Set( interrupt ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Clears a flag in the custom interrupt flags, translating the condition |
|
// to the proper global space, if necessary |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ClearCustomInterruptCondition( int nCondition ) |
|
{ |
|
int interrupt = InterruptFromCondition( nCondition ); |
|
|
|
if ( interrupt == -1 ) |
|
{ |
|
Assert(0); |
|
return; |
|
} |
|
|
|
m_CustomInterruptConditions.Clear( interrupt ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Clears all the custom interrupt flags. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ClearCustomInterruptConditions() |
|
{ |
|
m_CustomInterruptConditions.ClearAll(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::SetDistLook( float flDistLook ) |
|
{ |
|
m_pSenses->SetDistLook( flDistLook ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::QueryHearSound( CSound *pSound ) |
|
{ |
|
if ( pSound->SoundContext() & SOUND_CONTEXT_COMBINE_ONLY ) |
|
return false; |
|
|
|
if ( pSound->SoundContext() & SOUND_CONTEXT_ALLIES_ONLY ) |
|
{ |
|
if ( !IsPlayerAlly() ) |
|
return false; |
|
} |
|
|
|
if ( pSound->IsSoundType( SOUND_PLAYER ) && GetState() == NPC_STATE_IDLE && !FVisible( pSound->GetSoundReactOrigin() ) ) |
|
{ |
|
// NPC's that are IDLE should disregard player movement sounds if they can't see them. |
|
// This does not affect them hearing the player's weapon. |
|
// !!!BUGBUG - this probably makes NPC's not hear doors opening, because doors opening put SOUND_PLAYER |
|
// in the world, but the door's model will block the FVisible() trace and this code will then |
|
// deduce that the sound can not be heard |
|
return false; |
|
} |
|
|
|
// Disregard footsteps from our own class type |
|
if ( pSound->IsSoundType( SOUND_COMBAT ) && pSound->SoundChannel() == SOUNDENT_CHANNEL_NPC_FOOTSTEP ) |
|
{ |
|
if ( pSound->m_hOwner && pSound->m_hOwner->ClassMatches( m_iClassname ) ) |
|
return false; |
|
} |
|
|
|
if( ShouldIgnoreSound( pSound ) ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC ) |
|
{ |
|
if ( bOnlyHateOrFearIfNPC && pEntity->IsNPC() ) |
|
{ |
|
Disposition_t disposition = IRelationType( pEntity ); |
|
return ( disposition == D_HT || disposition == D_FR ); |
|
} |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::OnLooked( int iDistance ) |
|
{ |
|
// DON'T let visibility information from last frame sit around! |
|
static int conditionsToClear[] = |
|
{ |
|
COND_SEE_HATE, |
|
COND_SEE_DISLIKE, |
|
COND_SEE_ENEMY, |
|
COND_SEE_FEAR, |
|
COND_SEE_NEMESIS, |
|
COND_SEE_PLAYER, |
|
COND_LOST_PLAYER, |
|
COND_ENEMY_WENT_NULL, |
|
}; |
|
|
|
bool bHadSeePlayer = HasCondition(COND_SEE_PLAYER); |
|
|
|
ClearConditions( conditionsToClear, ARRAYSIZE( conditionsToClear ) ); |
|
|
|
AISightIter_t iter; |
|
CBaseEntity *pSightEnt; |
|
|
|
pSightEnt = GetSenses()->GetFirstSeenEntity( &iter ); |
|
|
|
while( pSightEnt ) |
|
{ |
|
if ( pSightEnt->IsPlayer() ) |
|
{ |
|
// if we see a client, remember that (mostly for scripted AI) |
|
SetCondition(COND_SEE_PLAYER); |
|
m_flLastSawPlayerTime = gpGlobals->curtime; |
|
} |
|
|
|
Disposition_t relation = IRelationType( pSightEnt ); |
|
|
|
// the looker will want to consider this entity |
|
// don't check anything else about an entity that can't be seen, or an entity that you don't care about. |
|
if ( relation != D_NU ) |
|
{ |
|
if ( pSightEnt == GetEnemy() ) |
|
{ |
|
// we know this ent is visible, so if it also happens to be our enemy, store that now. |
|
SetCondition(COND_SEE_ENEMY); |
|
} |
|
|
|
// don't add the Enemy's relationship to the conditions. We only want to worry about conditions when |
|
// we see npcs other than the Enemy. |
|
switch ( relation ) |
|
{ |
|
case D_HT: |
|
{ |
|
int priority = IRelationPriority( pSightEnt ); |
|
if (priority < 0) |
|
{ |
|
SetCondition(COND_SEE_DISLIKE); |
|
} |
|
else if (priority > 10) |
|
{ |
|
SetCondition(COND_SEE_NEMESIS); |
|
} |
|
else |
|
{ |
|
SetCondition(COND_SEE_HATE); |
|
} |
|
UpdateEnemyMemory(pSightEnt,pSightEnt->GetAbsOrigin()); |
|
break; |
|
|
|
} |
|
case D_FR: |
|
UpdateEnemyMemory(pSightEnt,pSightEnt->GetAbsOrigin()); |
|
SetCondition(COND_SEE_FEAR); |
|
break; |
|
case D_LI: |
|
case D_NU: |
|
break; |
|
default: |
|
DevWarning( 2, "%s can't assess %s\n", GetClassname(), pSightEnt->GetClassname() ); |
|
break; |
|
} |
|
} |
|
|
|
pSightEnt = GetSenses()->GetNextSeenEntity( &iter ); |
|
} |
|
|
|
// Did we lose the player? |
|
if ( bHadSeePlayer && !HasCondition(COND_SEE_PLAYER) ) |
|
{ |
|
SetCondition(COND_LOST_PLAYER); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::OnListened() |
|
{ |
|
AISoundIter_t iter; |
|
|
|
CSound *pCurrentSound; |
|
|
|
static int conditionsToClear[] = |
|
{ |
|
COND_HEAR_DANGER, |
|
COND_HEAR_COMBAT, |
|
COND_HEAR_WORLD, |
|
COND_HEAR_PLAYER, |
|
COND_HEAR_THUMPER, |
|
COND_HEAR_BUGBAIT, |
|
COND_HEAR_PHYSICS_DANGER, |
|
COND_HEAR_BULLET_IMPACT, |
|
COND_HEAR_MOVE_AWAY, |
|
|
|
COND_NO_HEAR_DANGER, |
|
|
|
COND_SMELL, |
|
}; |
|
|
|
ClearConditions( conditionsToClear, ARRAYSIZE( conditionsToClear ) ); |
|
|
|
pCurrentSound = GetSenses()->GetFirstHeardSound( &iter ); |
|
|
|
while ( pCurrentSound ) |
|
{ |
|
// the npc cares about this sound, and it's close enough to hear. |
|
int condition = COND_NONE; |
|
|
|
if ( pCurrentSound->FIsSound() ) |
|
{ |
|
// this is an audible sound. |
|
switch( pCurrentSound->SoundTypeNoContext() ) |
|
{ |
|
case SOUND_DANGER: |
|
if( gpGlobals->curtime > m_flIgnoreDangerSoundsUntil) |
|
condition = COND_HEAR_DANGER; |
|
break; |
|
|
|
case SOUND_THUMPER: condition = COND_HEAR_THUMPER; break; |
|
case SOUND_BUGBAIT: condition = COND_HEAR_BUGBAIT; break; |
|
case SOUND_COMBAT: |
|
if ( pCurrentSound->SoundChannel() == SOUNDENT_CHANNEL_SPOOKY_NOISE ) |
|
{ |
|
condition = COND_HEAR_SPOOKY; |
|
} |
|
else |
|
{ |
|
condition = COND_HEAR_COMBAT; |
|
} |
|
break; |
|
|
|
case SOUND_WORLD: condition = COND_HEAR_WORLD; break; |
|
case SOUND_PLAYER: condition = COND_HEAR_PLAYER; break; |
|
case SOUND_BULLET_IMPACT: condition = COND_HEAR_BULLET_IMPACT; break; |
|
case SOUND_PHYSICS_DANGER: condition = COND_HEAR_PHYSICS_DANGER; break; |
|
case SOUND_DANGER_SNIPERONLY:/* silence warning */ break; |
|
case SOUND_MOVE_AWAY: condition = COND_HEAR_MOVE_AWAY; break; |
|
case SOUND_PLAYER_VEHICLE: condition = COND_HEAR_PLAYER; break; |
|
|
|
default: |
|
DevMsg( "**ERROR: NPC %s hearing sound of unknown type %d!\n", GetClassname(), pCurrentSound->SoundType() ); |
|
break; |
|
} |
|
} |
|
else |
|
{ |
|
// if not a sound, must be a smell - determine if it's just a scent, or if it's a food scent |
|
condition = COND_SMELL; |
|
} |
|
|
|
if ( condition != COND_NONE ) |
|
{ |
|
SetCondition( condition ); |
|
} |
|
|
|
pCurrentSound = GetSenses()->GetNextHeardSound( &iter ); |
|
} |
|
|
|
if( !HasCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
SetCondition( COND_NO_HEAR_DANGER ); |
|
} |
|
|
|
// Sound outputs |
|
if ( HasCondition( COND_HEAR_WORLD ) ) |
|
{ |
|
m_OnHearWorld.FireOutput(this, this); |
|
} |
|
|
|
if ( HasCondition( COND_HEAR_PLAYER ) ) |
|
{ |
|
m_OnHearPlayer.FireOutput(this, this); |
|
} |
|
|
|
if ( HasCondition( COND_HEAR_COMBAT ) || |
|
HasCondition( COND_HEAR_BULLET_IMPACT ) || |
|
HasCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
m_OnHearCombat.FireOutput(this, this); |
|
} |
|
} |
|
|
|
//========================================================= |
|
// FValidateHintType - tells use whether or not the npc cares |
|
// about the type of Hint Node given |
|
//========================================================= |
|
bool CAI_BaseNPC::FValidateHintType ( CAI_Hint *pHint ) |
|
{ |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Override in subclasses to associate specific hint types |
|
// with activities |
|
//----------------------------------------------------------------------------- |
|
Activity CAI_BaseNPC::GetHintActivity( short sHintType, Activity HintsActivity ) |
|
{ |
|
if ( HintsActivity != ACT_INVALID ) |
|
return HintsActivity; |
|
|
|
return ACT_IDLE; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Override in subclasses to give specific hint types delays |
|
// before they can be used again |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseNPC::GetHintDelay( short sHintType ) |
|
{ |
|
return 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return incoming grenade if spotted |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CBaseGrenade* CAI_BaseNPC::IncomingGrenade(void) |
|
{ |
|
int iDist; |
|
|
|
AIEnemiesIter_t iter; |
|
for( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) |
|
{ |
|
CBaseGrenade* pBG = dynamic_cast<CBaseGrenade*>((CBaseEntity*)pEMemory->hEnemy); |
|
|
|
// Make sure this memory is for a grenade and grenade is not dead |
|
if (!pBG || pBG->m_lifeState == LIFE_DEAD) |
|
continue; |
|
|
|
// Make sure it's visible |
|
if (!FVisible(pBG)) |
|
continue; |
|
|
|
// Check if it's near me |
|
iDist = ( pBG->GetAbsOrigin() - GetAbsOrigin() ).Length(); |
|
if ( iDist <= NPC_GRENADE_FEAR_DIST ) |
|
return pBG; |
|
|
|
// Check if it's headed towards me |
|
Vector vGrenadeDir = GetAbsOrigin() - pBG->GetAbsOrigin(); |
|
Vector vGrenadeVel; |
|
pBG->GetVelocity( &vGrenadeVel, NULL ); |
|
VectorNormalize(vGrenadeDir); |
|
VectorNormalize(vGrenadeVel); |
|
float flDotPr = DotProduct(vGrenadeDir, vGrenadeVel); |
|
if (flDotPr > 0.85) |
|
return pBG; |
|
} |
|
return NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Check my physical state with the environment |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::TryRestoreHull(void) |
|
{ |
|
if ( IsUsingSmallHull() && GetCurSchedule() ) |
|
{ |
|
trace_t tr; |
|
Vector vUpBit = GetAbsOrigin(); |
|
vUpBit.z += 1; |
|
|
|
AI_TraceHull( GetAbsOrigin(), vUpBit, GetHullMins(), |
|
GetHullMaxs(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); |
|
if ( !tr.startsolid && (tr.fraction == 1.0) ) |
|
{ |
|
SetHullSizeNormal(); |
|
} |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
int CAI_BaseNPC::GetSoundInterests( void ) |
|
{ |
|
return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_PLAYER_VEHICLE | |
|
SOUND_BULLET_IMPACT; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CAI_BaseNPC::GetSoundPriority( CSound *pSound ) |
|
{ |
|
int iSoundTypeNoContext = pSound->SoundTypeNoContext(); |
|
int iSoundContext = pSound->SoundContext(); |
|
|
|
if( iSoundTypeNoContext & SOUND_DANGER ) |
|
{ |
|
return SOUND_PRIORITY_HIGHEST; |
|
} |
|
|
|
if( iSoundTypeNoContext & SOUND_COMBAT ) |
|
{ |
|
if( iSoundContext & SOUND_CONTEXT_EXPLOSION ) |
|
{ |
|
return SOUND_PRIORITY_VERY_HIGH; |
|
} |
|
|
|
return SOUND_PRIORITY_HIGH; |
|
} |
|
|
|
return SOUND_PRIORITY_NORMAL; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Return the loudest sound of this type in the sound list |
|
//--------------------------------------------------------- |
|
CSound *CAI_BaseNPC::GetLoudestSoundOfType( int iType ) |
|
{ |
|
return CSoundEnt::GetLoudestSoundOfType( iType, EarPosition() ); |
|
} |
|
|
|
//========================================================= |
|
// GetBestSound - returns a pointer to the sound the npc |
|
// should react to. Right now responds only to nearest sound. |
|
//========================================================= |
|
CSound* CAI_BaseNPC::GetBestSound( int validTypes ) |
|
{ |
|
if ( m_pLockedBestSound->m_iType != SOUND_NONE ) |
|
return m_pLockedBestSound; |
|
CSound *pResult = GetSenses()->GetClosestSound( false, validTypes ); |
|
if ( pResult == NULL) |
|
DevMsg( "Warning: NULL Return from GetBestSound\n" ); // condition previously set now no longer true. Have seen this when play too many sounds... |
|
return pResult; |
|
} |
|
|
|
//========================================================= |
|
// PBestScent - returns a pointer to the scent the npc |
|
// should react to. Right now responds only to nearest scent |
|
//========================================================= |
|
CSound* CAI_BaseNPC::GetBestScent( void ) |
|
{ |
|
CSound *pResult = GetSenses()->GetClosestSound( true ); |
|
if ( pResult == NULL) |
|
DevMsg("Warning: NULL Return from GetBestScent\n" ); |
|
return pResult; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::LockBestSound() |
|
{ |
|
UnlockBestSound(); |
|
CSound *pBestSound = GetBestSound(); |
|
if ( pBestSound ) |
|
*m_pLockedBestSound = *pBestSound; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::UnlockBestSound() |
|
{ |
|
if ( m_pLockedBestSound->m_iType != SOUND_NONE ) |
|
{ |
|
m_pLockedBestSound->m_iType = SOUND_NONE; |
|
OnListened(); // reset hearing conditions |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return true if the specified sound is visible. Handles sounds generated by entities correctly. |
|
// Input : *pSound - |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::SoundIsVisible( CSound *pSound ) |
|
{ |
|
CBaseEntity *pBlocker = NULL; |
|
if ( !FVisible( pSound->GetSoundReactOrigin(), MASK_BLOCKLOS, &pBlocker ) ) |
|
{ |
|
// Is the blocker the sound owner? |
|
if ( pBlocker && pBlocker == pSound->m_hOwner ) |
|
return true; |
|
|
|
return false; |
|
} |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true if target is in legal range of eye movements |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ValidEyeTarget(const Vector &lookTargetPos) |
|
{ |
|
Vector vHeadDir = HeadDirection3D( ); |
|
Vector lookTargetDir = lookTargetPos - EyePosition(); |
|
VectorNormalize(lookTargetDir); |
|
|
|
// Only look if it doesn't crank my eyeballs too far |
|
float dotPr = DotProduct(lookTargetDir, vHeadDir); |
|
if (dotPr > 0.7) |
|
{ |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Integrate head turn over time |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetHeadDirection( const Vector &vTargetPos, float flInterval) |
|
{ |
|
if (!(CapabilitiesGet() & bits_CAP_TURN_HEAD)) |
|
return; |
|
|
|
#ifdef DEBUG_LOOK |
|
// Draw line in body, head, and eye directions |
|
Vector vEyePos = EyePosition(); |
|
Vector vHeadDir; |
|
HeadDirection3D(&vHeadDir); |
|
Vector vBodyDir; |
|
BodyDirection2D(&vBodyDir); |
|
|
|
//UNDONE <<TODO>> |
|
// currently eye dir just returns head dir, so use vTargetPos for now |
|
//Vector vEyeDir; w |
|
//EyeDirection3D(&vEyeDir); |
|
NDebugOverlay::Line( vEyePos, vEyePos+(50*vHeadDir), 255, 0, 0, false, 0.1 ); |
|
NDebugOverlay::Line( vEyePos, vEyePos+(50*vBodyDir), 0, 255, 0, false, 0.1 ); |
|
NDebugOverlay::Line( vEyePos, vTargetPos, 0, 0, 255, false, 0.1 ); |
|
#endif |
|
|
|
//-------------------------------------- |
|
// Set head yaw |
|
//-------------------------------------- |
|
float flDesiredYaw = VecToYaw(vTargetPos - GetLocalOrigin()) - GetLocalAngles().y; |
|
if (flDesiredYaw > 180) |
|
flDesiredYaw -= 360; |
|
if (flDesiredYaw < -180) |
|
flDesiredYaw += 360; |
|
|
|
float iRate = 0.8; |
|
|
|
// Make frame rate independent |
|
float timeToUse = flInterval; |
|
while (timeToUse > 0) |
|
{ |
|
m_flHeadYaw = (iRate * m_flHeadYaw) + (1-iRate)*flDesiredYaw; |
|
timeToUse -= 0.1; |
|
} |
|
if (m_flHeadYaw > 360) m_flHeadYaw = 0; |
|
|
|
m_flHeadYaw = SetBoneController( 0, m_flHeadYaw ); |
|
|
|
|
|
//-------------------------------------- |
|
// Set head pitch |
|
//-------------------------------------- |
|
Vector vEyePosition = EyePosition(); |
|
float fTargetDist = (vTargetPos - vEyePosition).Length(); |
|
float fVertDist = vTargetPos.z - vEyePosition.z; |
|
float flDesiredPitch = -RAD2DEG(atan(fVertDist/fTargetDist)); |
|
|
|
// Make frame rate independent |
|
timeToUse = flInterval; |
|
while (timeToUse > 0) |
|
{ |
|
m_flHeadPitch = (iRate * m_flHeadPitch) + (1-iRate)*flDesiredPitch; |
|
timeToUse -= 0.1; |
|
} |
|
if (m_flHeadPitch > 360) m_flHeadPitch = 0; |
|
|
|
SetBoneController( 1, m_flHeadPitch ); |
|
|
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's eye direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseNPC::EyeDirection2D( void ) |
|
{ |
|
// UNDONE |
|
// For now just return head direction.... |
|
return HeadDirection2D( ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's eye direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseNPC::EyeDirection3D( void ) |
|
{ |
|
// UNDONE //<<TODO>> |
|
// For now just return head direction.... |
|
return HeadDirection3D( ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's head direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseNPC::HeadDirection2D( void ) |
|
{ |
|
// UNDONE <<TODO>> |
|
// This does not account for head rotations in the animation, |
|
// only those done via bone controllers |
|
|
|
// Head yaw is in local cooridnate so it must be added to the body's yaw |
|
QAngle bodyAngles = BodyAngles(); |
|
float flWorldHeadYaw = m_flHeadYaw + bodyAngles.y; |
|
|
|
// Convert head yaw into facing direction |
|
return UTIL_YawToVector( flWorldHeadYaw ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's head direction in 3D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseNPC::HeadDirection3D( void ) |
|
{ |
|
Vector vHeadDirection; |
|
|
|
// UNDONE <<TODO>> |
|
// This does not account for head rotations in the animation, |
|
// only those done via bone controllers |
|
|
|
// Head yaw is in local cooridnate so it must be added to the body's yaw |
|
QAngle bodyAngles = BodyAngles(); |
|
float flWorldHeadYaw = m_flHeadYaw + bodyAngles.y; |
|
|
|
// Convert head yaw into facing direction |
|
AngleVectors( QAngle( m_flHeadPitch, flWorldHeadYaw, 0), &vHeadDirection ); |
|
return vHeadDirection; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Look at other NPCs and clients from time to time |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CBaseEntity *CAI_BaseNPC::EyeLookTarget( void ) |
|
{ |
|
if (m_flNextEyeLookTime < gpGlobals->curtime) |
|
{ |
|
CBaseEntity* pBestEntity = NULL; |
|
float fBestDist = MAX_COORD_RANGE; |
|
float fTestDist; |
|
|
|
CBaseEntity *pEntity = NULL; |
|
|
|
for ( CEntitySphereQuery sphere( GetAbsOrigin(), 1024, 0 ); (pEntity = sphere.GetCurrentEntity()) != NULL; sphere.NextEntity() ) |
|
{ |
|
if (pEntity == this) |
|
{ |
|
continue; |
|
} |
|
CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); |
|
if (pNPC || (pEntity->GetFlags() & FL_CLIENT)) |
|
{ |
|
fTestDist = (GetAbsOrigin() - pEntity->EyePosition()).Length(); |
|
if (fTestDist < fBestDist) |
|
{ |
|
if (ValidEyeTarget(pEntity->EyePosition())) |
|
{ |
|
fBestDist = fTestDist; |
|
pBestEntity = pEntity; |
|
} |
|
} |
|
} |
|
} |
|
if (pBestEntity) |
|
{ |
|
m_flNextEyeLookTime = gpGlobals->curtime + random->RandomInt(1,5); |
|
m_hEyeLookTarget = pBestEntity; |
|
} |
|
} |
|
return m_hEyeLookTarget; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Set direction that the NPC aiming their gun |
|
// returns true is the passed Vector is in |
|
// the caller's forward aim cone. The dot product is performed |
|
// in 2d, making the view cone infinitely tall. By default, the |
|
// callers aim cone is assumed to be very narrow |
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::FInAimCone( const Vector &vecSpot ) |
|
{ |
|
Vector los = ( vecSpot - GetAbsOrigin() ); |
|
|
|
// do this in 2D |
|
los.z = 0; |
|
VectorNormalize( los ); |
|
|
|
Vector facingDir = BodyDirection2D( ); |
|
|
|
float flDot = DotProduct( los, facingDir ); |
|
|
|
if (CapabilitiesGet() & bits_CAP_AIM_GUN) |
|
{ |
|
// FIXME: query current animation for ranges |
|
return ( flDot > DOT_30DEGREE ); |
|
} |
|
|
|
if ( flDot > 0.994 )//!!!BUGBUG - magic number same as FacingIdeal(), what is this? |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Cache whatever pose parameters we intend to use |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PopulatePoseParameters( void ) |
|
{ |
|
m_poseAim_Pitch = LookupPoseParameter( "aim_pitch" ); |
|
m_poseAim_Yaw = LookupPoseParameter( "aim_yaw" ); |
|
m_poseMove_Yaw = LookupPoseParameter( "move_yaw" ); |
|
|
|
BaseClass::PopulatePoseParameters(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Set direction that the NPC aiming their gun |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::SetAim( const Vector &aimDir ) |
|
{ |
|
QAngle angDir; |
|
VectorAngles( aimDir, angDir ); |
|
float curPitch = GetPoseParameter( m_poseAim_Pitch ); |
|
float curYaw = GetPoseParameter( m_poseAim_Yaw ); |
|
|
|
float newPitch; |
|
float newYaw; |
|
|
|
if( GetEnemy() ) |
|
{ |
|
// clamp and dampen movement |
|
newPitch = curPitch + 0.8 * UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 20 ), curPitch ); |
|
|
|
float flRelativeYaw = UTIL_AngleDiff( angDir.y, GetAbsAngles().y ); |
|
// float flNewTargetYaw = UTIL_ApproachAngle( flRelativeYaw, curYaw, 20 ); |
|
// float newYaw = curYaw + 0.8 * UTIL_AngleDiff( flNewTargetYaw, curYaw ); |
|
newYaw = curYaw + UTIL_AngleDiff( flRelativeYaw, curYaw ); |
|
} |
|
else |
|
{ |
|
// Sweep your weapon more slowly if you're not fighting someone |
|
newPitch = curPitch + 0.6 * UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 20 ), curPitch ); |
|
|
|
float flRelativeYaw = UTIL_AngleDiff( angDir.y, GetAbsAngles().y ); |
|
newYaw = curYaw + 0.6 * UTIL_AngleDiff( flRelativeYaw, curYaw ); |
|
} |
|
|
|
newPitch = AngleNormalize( newPitch ); |
|
newYaw = AngleNormalize( newYaw ); |
|
|
|
SetPoseParameter( m_poseAim_Pitch, newPitch ); |
|
SetPoseParameter( m_poseAim_Yaw, newYaw ); |
|
|
|
// Msg("yaw %.0f (%.0f %.0f)\n", newYaw, angDir.y, GetAbsAngles().y ); |
|
|
|
// Calculate our interaction yaw. |
|
// If we've got a small adjustment off our abs yaw, use that. |
|
// Otherwise, use our abs yaw. |
|
if ( fabs(newYaw) < 20 ) |
|
{ |
|
m_flInteractionYaw = angDir.y; |
|
} |
|
else |
|
{ |
|
m_flInteractionYaw = GetAbsAngles().y; |
|
} |
|
} |
|
|
|
|
|
void CAI_BaseNPC::RelaxAim( ) |
|
{ |
|
float curPitch = GetPoseParameter( m_poseAim_Pitch ); |
|
float curYaw = GetPoseParameter( m_poseAim_Yaw ); |
|
|
|
// dampen existing aim |
|
float newPitch = AngleNormalize( UTIL_ApproachAngle( 0, curPitch, 3 ) ); |
|
float newYaw = AngleNormalize( UTIL_ApproachAngle( 0, curYaw, 2 ) ); |
|
|
|
SetPoseParameter( m_poseAim_Pitch, newPitch ); |
|
SetPoseParameter( m_poseAim_Yaw, newYaw ); |
|
// DevMsg("relax aim %.0f %0.f\n", newPitch, newYaw ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AimGun() |
|
{ |
|
if (GetEnemy()) |
|
{ |
|
Vector vecShootOrigin; |
|
|
|
vecShootOrigin = Weapon_ShootPosition(); |
|
Vector vecShootDir = GetShootEnemyDir( vecShootOrigin, false ); |
|
|
|
SetAim( vecShootDir ); |
|
} |
|
else |
|
{ |
|
RelaxAim( ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Set direction that the NPC is looking |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::MaintainLookTargets ( float flInterval ) |
|
{ |
|
// -------------------------------------------------------- |
|
// Try to look at enemy if I have one |
|
// -------------------------------------------------------- |
|
if ((CBaseEntity*)GetEnemy()) |
|
{ |
|
if ( ValidEyeTarget(GetEnemy()->EyePosition()) ) |
|
{ |
|
SetHeadDirection(GetEnemy()->EyePosition(),flInterval); |
|
SetViewtarget( GetEnemy()->EyePosition() ); |
|
return; |
|
} |
|
} |
|
|
|
#if 0 |
|
// -------------------------------------------------------- |
|
// First check if I've been assigned to look at an entity |
|
// -------------------------------------------------------- |
|
CBaseEntity *lookTarget = EyeLookTarget(); |
|
if (lookTarget && ValidEyeTarget(lookTarget->EyePosition())) |
|
{ |
|
SetHeadDirection(lookTarget->EyePosition(),flInterval); |
|
SetViewtarget( lookTarget->EyePosition() ); |
|
return; |
|
} |
|
#endif |
|
|
|
// -------------------------------------------------------- |
|
// If I'm moving, look at my target position |
|
// -------------------------------------------------------- |
|
if (GetNavigator()->IsGoalActive() && ValidEyeTarget(GetNavigator()->GetCurWaypointPos())) |
|
{ |
|
SetHeadDirection(GetNavigator()->GetCurWaypointPos(),flInterval); |
|
SetViewtarget( GetNavigator()->GetCurWaypointPos() ); |
|
return; |
|
} |
|
|
|
|
|
// ------------------------------------- |
|
// If I hear a combat sounds look there |
|
// ------------------------------------- |
|
if ( HasCondition(COND_HEAR_COMBAT) || HasCondition(COND_HEAR_DANGER) ) |
|
{ |
|
CSound *pSound = GetBestSound(); |
|
|
|
if ( pSound && pSound->IsSoundType(SOUND_COMBAT | SOUND_DANGER) ) |
|
{ |
|
if (ValidEyeTarget( pSound->GetSoundOrigin() )) |
|
{ |
|
SetHeadDirection(pSound->GetSoundOrigin(),flInterval); |
|
SetViewtarget( pSound->GetSoundOrigin() ); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------- |
|
// Otherwise just look around randomly |
|
// ------------------------------------- |
|
|
|
// Check that look target position is still valid |
|
if (m_flNextEyeLookTime > gpGlobals->curtime) |
|
{ |
|
if (!ValidEyeTarget(m_vEyeLookTarget)) |
|
{ |
|
// Force choosing of new look target |
|
m_flNextEyeLookTime = 0; |
|
} |
|
} |
|
|
|
if (m_flNextEyeLookTime < gpGlobals->curtime) |
|
{ |
|
Vector vBodyDir = BodyDirection2D( ); |
|
|
|
/* |
|
Vector lookSpread = Vector(0.82,0.82,0.22); |
|
float x = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); |
|
float y = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); |
|
float z = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); |
|
|
|
QAngle angles; |
|
VectorAngles( vBodyDir, angles ); |
|
Vector forward, right, up; |
|
AngleVectors( angles, &forward, &right, &up ); |
|
|
|
Vector vecDir = vBodyDir + (x * lookSpread.x * forward) + (y * lookSpread.y * right) + (z * lookSpread.z * up); |
|
float lookDist = random->RandomInt(50,2000); |
|
m_vEyeLookTarget = EyePosition() + lookDist*vecDir; |
|
*/ |
|
m_vEyeLookTarget = EyePosition() + 500*vBodyDir; |
|
m_flNextEyeLookTime = gpGlobals->curtime + 0.5; // random->RandomInt(1,5); |
|
} |
|
SetHeadDirection(m_vEyeLookTarget,flInterval); |
|
|
|
// ---------------------------------------------------- |
|
// Integrate eye turn in frame rate independent manner |
|
// ---------------------------------------------------- |
|
float timeToUse = flInterval; |
|
while (timeToUse > 0) |
|
{ |
|
m_vCurEyeTarget = ((1 - m_flEyeIntegRate) * m_vCurEyeTarget + m_flEyeIntegRate * m_vEyeLookTarget); |
|
timeToUse -= 0.1; |
|
} |
|
SetViewtarget( m_vCurEyeTarget ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Let the motor deal with turning presentation issues |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::MaintainTurnActivity( ) |
|
{ |
|
// Don't bother if we're in a vehicle |
|
if ( IsInAVehicle() ) |
|
return; |
|
|
|
AI_PROFILE_SCOPE( CAI_BaseNPC_MaintainTurnActivity ); |
|
GetMotor()->MaintainTurnActivity( ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Here's where all motion occurs |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PerformMovement() |
|
{ |
|
// don't bother to move if the npc isn't alive |
|
if (!IsAlive()) |
|
return; |
|
|
|
AI_PROFILE_SCOPE(CAI_BaseNPC_PerformMovement); |
|
g_AIMoveTimer.Start(); |
|
|
|
float flInterval = ( m_flTimeLastMovement != FLT_MAX ) ? gpGlobals->curtime - m_flTimeLastMovement : 0.1; |
|
|
|
m_pNavigator->Move( ROUND_TO_TICKS( flInterval ) ); |
|
m_flTimeLastMovement = gpGlobals->curtime; |
|
|
|
g_AIMoveTimer.End(); |
|
|
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Updates to npc after movement is completed |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::PostMovement() |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_PostMovement ); |
|
|
|
InvalidateBoneCache(); |
|
|
|
if ( GetModelPtr() && GetModelPtr()->SequencesAvailable() ) |
|
{ |
|
float flInterval = GetAnimTimeInterval(); |
|
|
|
if ( CapabilitiesGet() & bits_CAP_AIM_GUN ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_PM_AimGun ); |
|
AimGun(); |
|
} |
|
else |
|
{ |
|
// NPCs with bits_CAP_AIM_GUN update this in SetAim, called by AimGun. |
|
m_flInteractionYaw = GetAbsAngles().y; |
|
} |
|
|
|
// set look targets for npcs with animated faces |
|
if ( CapabilitiesGet() & bits_CAP_ANIMATEDFACE ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_PM_MaintainLookTargets ); |
|
MaintainLookTargets(flInterval); |
|
} |
|
} |
|
|
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_PM_MaintainTurnActivity ); |
|
// update turning as needed |
|
MaintainTurnActivity( ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
float g_AINextDisabledMessageTime; |
|
extern bool IsInCommentaryMode( void ); |
|
|
|
bool CAI_BaseNPC::PreThink( void ) |
|
{ |
|
// ---------------------------------------------------------- |
|
// Skip AI if its been disabled or networks haven't been |
|
// loaded, and put a warning message on the screen |
|
// |
|
// Don't do this if the convar wants it hidden |
|
// ---------------------------------------------------------- |
|
if ( (CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI || !g_pAINetworkManager->NetworksLoaded()) ) |
|
{ |
|
if ( gpGlobals->curtime >= g_AINextDisabledMessageTime && !IsInCommentaryMode() ) |
|
{ |
|
g_AINextDisabledMessageTime = gpGlobals->curtime + 0.5f; |
|
|
|
hudtextparms_s tTextParam; |
|
tTextParam.x = 0.7; |
|
tTextParam.y = 0.65; |
|
tTextParam.effect = 0; |
|
tTextParam.r1 = 255; |
|
tTextParam.g1 = 255; |
|
tTextParam.b1 = 255; |
|
tTextParam.a1 = 255; |
|
tTextParam.r2 = 255; |
|
tTextParam.g2 = 255; |
|
tTextParam.b2 = 255; |
|
tTextParam.a2 = 255; |
|
tTextParam.fadeinTime = 0; |
|
tTextParam.fadeoutTime = 0; |
|
tTextParam.holdTime = 0.6; |
|
tTextParam.fxTime = 0; |
|
tTextParam.channel = 1; |
|
UTIL_HudMessageAll( tTextParam, "A.I. Disabled...\n" ); |
|
} |
|
SetActivity( ACT_IDLE ); |
|
return false; |
|
} |
|
|
|
// -------------------------------------------------------- |
|
// If debug stepping |
|
// -------------------------------------------------------- |
|
if (CAI_BaseNPC::m_nDebugBits & bits_debugStepAI) |
|
{ |
|
if (m_nDebugCurIndex >= CAI_BaseNPC::m_nDebugPauseIndex) |
|
{ |
|
if (!GetNavigator()->IsGoalActive()) |
|
{ |
|
m_flPlaybackRate = 0; |
|
} |
|
return false; |
|
} |
|
else |
|
{ |
|
m_flPlaybackRate = 1; |
|
} |
|
} |
|
|
|
if ( m_hOpeningDoor.Get() && AIIsDebuggingDoors( this ) ) |
|
{ |
|
NDebugOverlay::Line( EyePosition(), m_hOpeningDoor->WorldSpaceCenter(), 255, 255, 255, false, .1 ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::RunAnimation( void ) |
|
{ |
|
VPROF_BUDGET( "CAI_BaseNPC_RunAnimation", VPROF_BUDGETGROUP_SERVER_ANIM ); |
|
|
|
if ( !GetModelPtr() ) |
|
return; |
|
|
|
float flInterval = GetAnimTimeInterval(); |
|
|
|
StudioFrameAdvance( ); // animate |
|
|
|
if ((CAI_BaseNPC::m_nDebugBits & bits_debugStepAI) && !GetNavigator()->IsGoalActive()) |
|
{ |
|
flInterval = 0; |
|
} |
|
|
|
// start or end a fidget |
|
// This needs a better home -- switching animations over time should be encapsulated on a per-activity basis |
|
// perhaps MaintainActivity() or a ShiftAnimationOverTime() or something. |
|
if ( m_NPCState != NPC_STATE_SCRIPT && m_NPCState != NPC_STATE_DEAD && m_Activity == ACT_IDLE && IsActivityFinished() ) |
|
{ |
|
int iSequence; |
|
|
|
// FIXME: this doesn't reissue a translation, so if the idle activity translation changes over time, it'll never get reset |
|
if ( SequenceLoops() ) |
|
{ |
|
// animation does loop, which means we're playing subtle idle. Might need to fidget. |
|
iSequence = SelectWeightedSequence ( m_translatedActivity ); |
|
} |
|
else |
|
{ |
|
// animation that just ended doesn't loop! That means we just finished a fidget |
|
// and should return to our heaviest weighted idle (the subtle one) |
|
iSequence = SelectHeaviestSequence ( m_translatedActivity ); |
|
} |
|
if ( iSequence != ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
ResetSequence( iSequence ); // Set to new anim (if it's there) |
|
|
|
//Adrian: Basically everywhere else in the AI code this variable gets set to whatever our sequence is. |
|
//But here it doesn't and not setting it causes any animation set through here to be stomped by the |
|
//ideal sequence before it has a chance of playing out (since there's code that reselects the ideal |
|
//sequence if it doesn't match the current one). |
|
if ( hl2_episodic.GetBool() ) |
|
{ |
|
m_nIdealSequence = iSequence; |
|
} |
|
} |
|
} |
|
|
|
DispatchAnimEvents( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::PostRun( void ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_PostRun); |
|
|
|
g_AIPostRunTimer.Start(); |
|
|
|
if ( !IsMoving() ) |
|
{ |
|
if ( GetIdealActivity() == ACT_WALK || |
|
GetIdealActivity() == ACT_RUN || |
|
GetIdealActivity() == ACT_WALK_AIM || |
|
GetIdealActivity() == ACT_RUN_AIM ) |
|
{ |
|
PostRunStopMoving(); |
|
} |
|
} |
|
|
|
RunAnimation(); |
|
|
|
// update slave items (weapons) |
|
Weapon_FrameUpdate(); |
|
|
|
g_AIPostRunTimer.End(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::PostRunStopMoving() |
|
{ |
|
DbgNavMsg1( this, "NPC %s failed to stop properly, slamming activity\n", GetDebugName() ); |
|
if ( !GetNavigator()->SetGoalFromStoppingPath() ) |
|
SetIdealActivity( GetStoppedActivity() ); // This is to prevent running in place |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::ShouldAlwaysThink() |
|
{ |
|
// @TODO (toml 07-08-03): This needs to be beefed up. |
|
// There are simple cases already seen where an AI can briefly leave |
|
// the PVS while navigating to the player. Perhaps should incorporate a heuristic taking into |
|
// account mode, enemy, last time saw player, player range etc. For example, if enemy is player, |
|
// and player is within 100 feet, and saw the player within the past 15 seconds, keep running... |
|
return HasSpawnFlags(SF_NPC_ALWAYSTHINK); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return true if the Player should be running the auto-move-out-of-way |
|
// avoidance code, which also means that the NPC shouldn't care about running into the Player. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ShouldPlayerAvoid( void ) |
|
{ |
|
if ( GetState() == NPC_STATE_SCRIPT ) |
|
return true; |
|
|
|
if ( IsInAScript() ) |
|
return true; |
|
|
|
if ( IsInLockedScene() == true ) |
|
return true; |
|
|
|
if ( HasSpawnFlags( SF_NPC_ALTCOLLISION ) ) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::UpdateEfficiency( bool bInPVS ) |
|
{ |
|
// Sleeping NPCs always dormant |
|
if ( GetSleepState() != AISS_AWAKE ) |
|
{ |
|
SetEfficiency( AIE_DORMANT ); |
|
return; |
|
} |
|
|
|
m_bInChoreo = ( GetState() == NPC_STATE_SCRIPT || IsCurSchedule( SCHED_SCENE_GENERIC, false ) ); |
|
|
|
if ( !ShouldUseEfficiency() ) |
|
{ |
|
SetEfficiency( AIE_NORMAL ); |
|
SetMoveEfficiency( AIME_NORMAL ); |
|
return; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
static Vector vPlayerEyePosition; |
|
static Vector vPlayerForward; |
|
static int iPrevFrame = -1; |
|
if ( gpGlobals->framecount != iPrevFrame ) |
|
{ |
|
iPrevFrame = gpGlobals->framecount; |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->EyePositionAndVectors( &vPlayerEyePosition, &vPlayerForward, NULL, NULL ); |
|
} |
|
} |
|
|
|
Vector vToNPC = GetAbsOrigin() - vPlayerEyePosition; |
|
float playerDist = VectorNormalize( vToNPC ); |
|
bool bPlayerFacing; |
|
|
|
bool bClientPVSExpanded = UTIL_ClientPVSIsExpanded(); |
|
|
|
if ( pPlayer ) |
|
{ |
|
bPlayerFacing = ( bClientPVSExpanded || ( bInPVS && vPlayerForward.Dot( vToNPC ) > 0 ) ); |
|
} |
|
else |
|
{ |
|
playerDist = 0; |
|
bPlayerFacing = true; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
bool bInVisibilityPVS = ( bClientPVSExpanded && UTIL_FindClientInVisibilityPVS( edict() ) != NULL ); |
|
|
|
//--------------------------------- |
|
|
|
if ( ( bInPVS && ( bPlayerFacing || playerDist < 25*12 ) ) || bClientPVSExpanded ) |
|
{ |
|
SetMoveEfficiency( AIME_NORMAL ); |
|
} |
|
else |
|
{ |
|
SetMoveEfficiency( AIME_EFFICIENT ); |
|
} |
|
|
|
//--------------------------------- |
|
|
|
if ( !IsRetail() && ai_efficiency_override.GetInt() > AIE_NORMAL && ai_efficiency_override.GetInt() <= AIE_DORMANT ) |
|
{ |
|
SetEfficiency( (AI_Efficiency_t)ai_efficiency_override.GetInt() ); |
|
return; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
// Some conditions will always force normal |
|
if ( gpGlobals->curtime - GetLastAttackTime() < .15 ) |
|
{ |
|
SetEfficiency( AIE_NORMAL ); |
|
return; |
|
} |
|
|
|
bool bFramerateOk = ( gpGlobals->frametime < ai_frametime_limit.GetFloat() ); |
|
|
|
if ( m_bForceConditionsGather || |
|
gpGlobals->curtime - GetLastAttackTime() < .2 || |
|
gpGlobals->curtime - m_flLastDamageTime < .2 || |
|
( GetState() < NPC_STATE_IDLE || GetState() > NPC_STATE_SCRIPT ) || |
|
( ( bInPVS || bInVisibilityPVS ) && |
|
( ( GetTask() && !TaskIsRunning() ) || |
|
GetTaskInterrupt() > 0 || |
|
m_bInChoreo ) ) ) |
|
{ |
|
SetEfficiency( ( bFramerateOk ) ? AIE_NORMAL : AIE_EFFICIENT ); |
|
return; |
|
} |
|
|
|
AI_Efficiency_t minEfficiency; |
|
|
|
if ( !ShouldDefaultEfficient() ) |
|
{ |
|
minEfficiency = ( bFramerateOk ) ? AIE_NORMAL : AIE_EFFICIENT; |
|
} |
|
else |
|
{ |
|
minEfficiency = ( bFramerateOk ) ? AIE_EFFICIENT : AIE_VERY_EFFICIENT; |
|
} |
|
|
|
// Stay normal if there's any chance of a relevant danger sound |
|
bool bPotentialDanger = false; |
|
|
|
if ( GetSoundInterests() & SOUND_DANGER ) |
|
{ |
|
int iSound = CSoundEnt::ActiveList(); |
|
|
|
while ( iSound != SOUNDLIST_EMPTY ) |
|
{ |
|
CSound *pCurrentSound = CSoundEnt::SoundPointerForIndex( iSound ); |
|
|
|
float hearingSensitivity = HearingSensitivity(); |
|
Vector vEarPosition = EarPosition(); |
|
|
|
if ( pCurrentSound && (SOUND_DANGER & pCurrentSound->SoundType()) ) |
|
{ |
|
float flHearDistanceSq = pCurrentSound->Volume() * hearingSensitivity; |
|
flHearDistanceSq *= flHearDistanceSq; |
|
if ( pCurrentSound->GetSoundOrigin().DistToSqr( vEarPosition ) <= flHearDistanceSq ) |
|
{ |
|
bPotentialDanger = true; |
|
break; |
|
} |
|
} |
|
|
|
iSound = pCurrentSound->NextSound(); |
|
} |
|
} |
|
|
|
if ( bPotentialDanger ) |
|
{ |
|
SetEfficiency( minEfficiency ); |
|
return; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
if ( !pPlayer ) |
|
{ |
|
// No heuristic currently for dedicated servers |
|
SetEfficiency( minEfficiency ); |
|
return; |
|
} |
|
|
|
enum |
|
{ |
|
DIST_NEAR, |
|
DIST_MID, |
|
DIST_FAR |
|
}; |
|
|
|
int range; |
|
if ( bInPVS ) |
|
{ |
|
if ( playerDist < 15*12 ) |
|
{ |
|
SetEfficiency( minEfficiency ); |
|
return; |
|
} |
|
|
|
range = ( playerDist < 50*12 ) ? DIST_NEAR : |
|
( playerDist < 200*12 ) ? DIST_MID : DIST_FAR; |
|
} |
|
else |
|
{ |
|
range = ( playerDist < 25*12 ) ? DIST_NEAR : |
|
( playerDist < 100*12 ) ? DIST_MID : DIST_FAR; |
|
} |
|
|
|
// Efficiency mappings |
|
int state = GetState(); |
|
if (state == NPC_STATE_SCRIPT ) // Treat script as alert. Already confirmed not in PVS |
|
state = NPC_STATE_ALERT; |
|
|
|
static AI_Efficiency_t mappings[] = |
|
{ |
|
// Idle |
|
// In PVS |
|
// Facing |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
AIE_EFFICIENT, |
|
// Not facing |
|
AIE_EFFICIENT, |
|
AIE_EFFICIENT, |
|
AIE_VERY_EFFICIENT, |
|
// Not in PVS |
|
AIE_VERY_EFFICIENT, |
|
AIE_SUPER_EFFICIENT, |
|
AIE_SUPER_EFFICIENT, |
|
// Alert |
|
// In PVS |
|
// Facing |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
AIE_EFFICIENT, |
|
// Not facing |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
AIE_EFFICIENT, |
|
// Not in PVS |
|
AIE_EFFICIENT, |
|
AIE_VERY_EFFICIENT, |
|
AIE_SUPER_EFFICIENT, |
|
// Combat |
|
// In PVS |
|
// Facing |
|
AIE_NORMAL, |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
// Not facing |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
AIE_EFFICIENT, |
|
// Not in PVS |
|
AIE_NORMAL, |
|
AIE_EFFICIENT, |
|
AIE_VERY_EFFICIENT, |
|
}; |
|
|
|
static const int stateBase[] = { 0, 9, 18 }; |
|
const int NOT_FACING_OFFSET = 3; |
|
const int NO_PVS_OFFSET = 6; |
|
|
|
int iStateOffset = stateBase[state - NPC_STATE_IDLE] ; |
|
int iFacingOffset = (!bInPVS || bPlayerFacing) ? 0 : NOT_FACING_OFFSET; |
|
int iPVSOffset = (bInPVS) ? 0 : NO_PVS_OFFSET; |
|
int iMapping = iStateOffset + iPVSOffset + iFacingOffset + range; |
|
|
|
Assert( iMapping < ARRAYSIZE( mappings ) ); |
|
|
|
AI_Efficiency_t efficiency = mappings[iMapping]; |
|
|
|
//--------------------------------- |
|
|
|
AI_Efficiency_t maxEfficiency = AIE_SUPER_EFFICIENT; |
|
if ( bInVisibilityPVS && state >= NPC_STATE_ALERT ) |
|
{ |
|
maxEfficiency = AIE_EFFICIENT; |
|
} |
|
else if ( bInVisibilityPVS || HasCondition( COND_SEE_PLAYER ) ) |
|
{ |
|
maxEfficiency = AIE_VERY_EFFICIENT; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
SetEfficiency( clamp( efficiency, minEfficiency, maxEfficiency ) ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::UpdateSleepState( bool bInPVS ) |
|
{ |
|
if ( GetSleepState() > AISS_AWAKE ) |
|
{ |
|
CBasePlayer *pLocalPlayer = AI_GetSinglePlayer(); |
|
if ( !pLocalPlayer ) |
|
{ |
|
if ( gpGlobals->maxClients > 1 ) |
|
{ |
|
Wake(); |
|
} |
|
else |
|
{ |
|
Warning( "CAI_BaseNPC::UpdateSleepState called with NULL pLocalPlayer\n" ); |
|
} |
|
return; |
|
} |
|
|
|
if ( m_flWakeRadius > .1 && !(pLocalPlayer->GetFlags() & FL_NOTARGET) && ( pLocalPlayer->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr() <= Square(m_flWakeRadius) ) |
|
Wake(); |
|
else if ( GetSleepState() == AISS_WAITING_FOR_PVS ) |
|
{ |
|
if ( bInPVS ) |
|
Wake(); |
|
} |
|
else if ( GetSleepState() == AISS_WAITING_FOR_THREAT ) |
|
{ |
|
if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) |
|
Wake(); |
|
else |
|
{ |
|
if ( bInPVS ) |
|
{ |
|
for (int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
if ( pPlayer && !(pPlayer->GetFlags() & FL_NOTARGET) && pPlayer->FVisible( this ) ) |
|
Wake(); |
|
} |
|
} |
|
|
|
// Should check for visible danger sounds |
|
if ( (GetSoundInterests() & SOUND_DANGER) && !(HasSpawnFlags(SF_NPC_WAIT_TILL_SEEN)) ) |
|
{ |
|
int iSound = CSoundEnt::ActiveList(); |
|
|
|
while ( iSound != SOUNDLIST_EMPTY ) |
|
{ |
|
CSound *pCurrentSound = CSoundEnt::SoundPointerForIndex( iSound ); |
|
Assert( pCurrentSound ); |
|
|
|
if ( (pCurrentSound->SoundType() & SOUND_DANGER) && |
|
GetSenses()->CanHearSound( pCurrentSound ) && |
|
SoundIsVisible( pCurrentSound )) |
|
{ |
|
Wake(); |
|
break; |
|
} |
|
|
|
iSound = pCurrentSound->NextSound(); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// NPC is awake |
|
// Don't let an NPC sleep if they're running a script! |
|
if( !IsInAScript() && m_NPCState != NPC_STATE_SCRIPT ) |
|
{ |
|
if( HasSleepFlags(AI_SLEEP_FLAG_AUTO_PVS) ) |
|
{ |
|
if( !HasCondition(COND_IN_PVS) ) |
|
{ |
|
SetSleepState( AISS_WAITING_FOR_PVS ); |
|
Sleep(); |
|
} |
|
} |
|
if( HasSleepFlags(AI_SLEEP_FLAG_AUTO_PVS_AFTER_PVS) ) |
|
{ |
|
if( HasCondition(COND_IN_PVS) ) |
|
{ |
|
// OK, we're in the player's PVS. Now switch to regular old AUTO_PVS sleep rules. |
|
AddSleepFlags(AI_SLEEP_FLAG_AUTO_PVS); |
|
RemoveSleepFlags(AI_SLEEP_FLAG_AUTO_PVS_AFTER_PVS); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
struct AIRebalanceInfo_t |
|
{ |
|
CAI_BaseNPC * pNPC; |
|
int iNextThinkTick; |
|
bool bInPVS; |
|
float dotPlayer; |
|
float distPlayer; |
|
}; |
|
|
|
int __cdecl ThinkRebalanceCompare( const AIRebalanceInfo_t *pLeft, const AIRebalanceInfo_t *pRight ) |
|
{ |
|
int base = pLeft->iNextThinkTick - pRight->iNextThinkTick; |
|
if ( base != 0 ) |
|
return base; |
|
|
|
if ( !pLeft->bInPVS && !pRight->bInPVS ) |
|
return 0; |
|
|
|
if ( !pLeft->bInPVS ) |
|
return 1; |
|
|
|
if ( !pRight->bInPVS ) |
|
return -1; |
|
|
|
if ( pLeft->dotPlayer < 0 && pRight->dotPlayer < 0 ) |
|
return 0; |
|
|
|
if ( pLeft->dotPlayer < 0 ) |
|
return 1; |
|
|
|
if ( pRight->dotPlayer < 0 ) |
|
return -1; |
|
|
|
const float NEAR_PLAYER = 50*12; |
|
|
|
if ( pLeft->distPlayer < NEAR_PLAYER && pRight->distPlayer >= NEAR_PLAYER ) |
|
return -1; |
|
|
|
if ( pRight->distPlayer < NEAR_PLAYER && pLeft->distPlayer >= NEAR_PLAYER ) |
|
return 1; |
|
|
|
if ( pLeft->dotPlayer > pRight->dotPlayer ) |
|
return -1; |
|
|
|
if ( pLeft->dotPlayer < pRight->dotPlayer ) |
|
return 1; |
|
|
|
return 0; |
|
} |
|
|
|
inline bool CAI_BaseNPC::CanThinkRebalance() |
|
{ |
|
if ( m_pfnThink != (BASEPTR)&CAI_BaseNPC::CallNPCThink ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( m_bInChoreo ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( m_NPCState == NPC_STATE_DEAD ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( GetSleepState() != AISS_AWAKE ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( !m_bUsingStandardThinkTime /*&& m_iFrameBlocked == -1 */ ) |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void CAI_BaseNPC::RebalanceThinks() |
|
{ |
|
bool bDebugThinkTicks = ai_debug_think_ticks.GetBool(); |
|
if ( bDebugThinkTicks ) |
|
{ |
|
static int iPrevTick; |
|
static int nThinksInTick; |
|
static int nRebalanceableThinksInTick; |
|
|
|
if ( gpGlobals->tickcount != iPrevTick ) |
|
{ |
|
DevMsg( "NPC per tick is %d [%d] (tick %d, frame %d)\n", nRebalanceableThinksInTick, nThinksInTick, iPrevTick, gpGlobals->framecount ); |
|
iPrevTick = gpGlobals->tickcount; |
|
nThinksInTick = 0; |
|
nRebalanceableThinksInTick = 0; |
|
} |
|
nThinksInTick++; |
|
if ( CanThinkRebalance() ) |
|
nRebalanceableThinksInTick++; |
|
} |
|
|
|
if ( ShouldRebalanceThinks() && gpGlobals->tickcount >= gm_iNextThinkRebalanceTick ) |
|
{ |
|
AI_PROFILE_SCOPE(AI_Think_Rebalance ); |
|
|
|
static CUtlVector<AIRebalanceInfo_t> rebalanceCandidates( 16, 64 ); |
|
gm_iNextThinkRebalanceTick = gpGlobals->tickcount + TIME_TO_TICKS( random->RandomFloat( 3, 5) ); |
|
|
|
int i; |
|
|
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
Vector vPlayerForward; |
|
Vector vPlayerEyePosition; |
|
|
|
vPlayerForward.Init(); |
|
vPlayerEyePosition.Init(); |
|
|
|
if ( pPlayer ) |
|
{ |
|
pPlayer->EyePositionAndVectors( &vPlayerEyePosition, &vPlayerForward, NULL, NULL ); |
|
} |
|
|
|
int iTicksPer10Hz = TIME_TO_TICKS( .1 ); |
|
int iMinTickRebalance = gpGlobals->tickcount - 1; // -1 needed for alternate ticks |
|
int iMaxTickRebalance = gpGlobals->tickcount + iTicksPer10Hz; |
|
|
|
for ( i = 0; i < g_AI_Manager.NumAIs(); i++ ) |
|
{ |
|
CAI_BaseNPC *pCandidate = g_AI_Manager.AccessAIs()[i]; |
|
if ( pCandidate->CanThinkRebalance() && |
|
( pCandidate->GetNextThinkTick() >= iMinTickRebalance && |
|
pCandidate->GetNextThinkTick() < iMaxTickRebalance ) ) |
|
{ |
|
int iInfo = rebalanceCandidates.AddToTail(); |
|
|
|
rebalanceCandidates[iInfo].pNPC = pCandidate; |
|
rebalanceCandidates[iInfo].iNextThinkTick = pCandidate->GetNextThinkTick(); |
|
|
|
if ( pCandidate->IsFlaggedEfficient() ) |
|
{ |
|
rebalanceCandidates[iInfo].bInPVS = false; |
|
} |
|
else if ( pPlayer ) |
|
{ |
|
Vector vToCandidate = pCandidate->EyePosition() - vPlayerEyePosition; |
|
rebalanceCandidates[iInfo].bInPVS = ( UTIL_FindClientInPVS( pCandidate->edict() ) != NULL ); |
|
rebalanceCandidates[iInfo].distPlayer = VectorNormalize( vToCandidate ); |
|
rebalanceCandidates[iInfo].dotPlayer = vPlayerForward.Dot( vToCandidate ); |
|
} |
|
else |
|
{ |
|
rebalanceCandidates[iInfo].bInPVS = true; |
|
rebalanceCandidates[iInfo].dotPlayer = 1; |
|
rebalanceCandidates[iInfo].distPlayer = 0; |
|
} |
|
} |
|
else if ( bDebugThinkTicks ) |
|
DevMsg( " Ignoring %d\n", pCandidate->GetNextThinkTick() ); |
|
} |
|
|
|
if ( rebalanceCandidates.Count() ) |
|
{ |
|
rebalanceCandidates.Sort( ThinkRebalanceCompare ); |
|
|
|
int iMaxThinkersPerTick = ceil( (float)( rebalanceCandidates.Count() + 1 ) / (float)iTicksPer10Hz ); // +1 to account for "this" |
|
|
|
int iCurTickDistributing = MIN( gpGlobals->tickcount, rebalanceCandidates[0].iNextThinkTick ); |
|
int iRemainingThinksToDistribute = iMaxThinkersPerTick - 1; // Start with one fewer first time because "this" is |
|
// always gets a slot in the current tick to avoid complications |
|
// in the calculation of "last think" |
|
|
|
if ( bDebugThinkTicks ) |
|
{ |
|
DevMsg( "Rebalance %d!\n", rebalanceCandidates.Count() + 1 ); |
|
DevMsg( " Distributing %d\n", iCurTickDistributing ); |
|
} |
|
|
|
for ( i = 0; i < rebalanceCandidates.Count(); i++ ) |
|
{ |
|
if ( iRemainingThinksToDistribute == 0 || rebalanceCandidates[i].iNextThinkTick > iCurTickDistributing ) |
|
{ |
|
if ( rebalanceCandidates[i].iNextThinkTick <= iCurTickDistributing ) |
|
{ |
|
iCurTickDistributing = iCurTickDistributing + 1; |
|
} |
|
else |
|
{ |
|
iCurTickDistributing = rebalanceCandidates[i].iNextThinkTick; |
|
} |
|
|
|
if ( bDebugThinkTicks ) |
|
DevMsg( " Distributing %d\n", iCurTickDistributing ); |
|
|
|
iRemainingThinksToDistribute = iMaxThinkersPerTick; |
|
} |
|
|
|
if ( rebalanceCandidates[i].pNPC->GetNextThinkTick() != iCurTickDistributing ) |
|
{ |
|
if ( bDebugThinkTicks ) |
|
DevMsg( " Bumping %d to %d\n", rebalanceCandidates[i].pNPC->GetNextThinkTick(), iCurTickDistributing ); |
|
|
|
rebalanceCandidates[i].pNPC->SetNextThink( TICKS_TO_TIME( iCurTickDistributing ) ); |
|
} |
|
else if ( bDebugThinkTicks ) |
|
{ |
|
DevMsg( " Leaving %d\n", rebalanceCandidates[i].pNPC->GetNextThinkTick() ); |
|
} |
|
|
|
iRemainingThinksToDistribute--; |
|
} |
|
} |
|
|
|
rebalanceCandidates.RemoveAll(); |
|
|
|
if ( bDebugThinkTicks ) |
|
{ |
|
DevMsg( "New distribution is:\n"); |
|
for ( i = 0; i < g_AI_Manager.NumAIs(); i++ ) |
|
{ |
|
DevMsg( " %d\n", g_AI_Manager.AccessAIs()[i]->GetNextThinkTick() ); |
|
} |
|
} |
|
|
|
Assert( GetNextThinkTick() == TICK_NEVER_THINK ); // never change this objects tick |
|
} |
|
} |
|
|
|
static float g_NpcTimeThisFrame; |
|
static float g_StartTimeCurThink; |
|
|
|
bool CAI_BaseNPC::PreNPCThink() |
|
{ |
|
static int iPrevFrame = -1; |
|
static float frameTimeLimit = FLT_MAX; |
|
static const ConVar *pHostTimescale; |
|
|
|
if ( frameTimeLimit == FLT_MAX ) |
|
{ |
|
pHostTimescale = cvar->FindVar( "host_timescale" ); |
|
} |
|
|
|
bool bUseThinkLimits = ( !m_bInChoreo && ShouldUseFrameThinkLimits() ); |
|
|
|
#ifdef _DEBUG |
|
const float NPC_THINK_LIMIT = 30.0 / 1000.0; |
|
#else |
|
const float NPC_THINK_LIMIT = ( !IsXbox() ) ? (10.0 / 1000.0) : (12.5 / 1000.0); |
|
#endif |
|
|
|
g_StartTimeCurThink = 0; |
|
|
|
if ( bUseThinkLimits && VCRGetMode() == VCR_Disabled ) |
|
{ |
|
if ( m_iFrameBlocked == gpGlobals->framecount ) |
|
{ |
|
DbgFrameLimitMsg( "Stalled %d (%d)\n", this, gpGlobals->framecount ); |
|
SetNextThink( gpGlobals->curtime ); |
|
return false; |
|
} |
|
else if ( gpGlobals->framecount != iPrevFrame ) |
|
{ |
|
DbgFrameLimitMsg( "--- FRAME: %d (%d)\n", this, gpGlobals->framecount ); |
|
float timescale = pHostTimescale->GetFloat(); |
|
if ( timescale < 1 ) |
|
timescale = 1; |
|
|
|
iPrevFrame = gpGlobals->framecount; |
|
frameTimeLimit = NPC_THINK_LIMIT * timescale; |
|
g_NpcTimeThisFrame = 0; |
|
} |
|
else |
|
{ |
|
if ( g_NpcTimeThisFrame > NPC_THINK_LIMIT ) |
|
{ |
|
float timeSinceLastRealThink = gpGlobals->curtime - m_flLastRealThinkTime; |
|
// Don't bump anyone more that a quarter second |
|
if ( timeSinceLastRealThink <= .25 ) |
|
{ |
|
DbgFrameLimitMsg( "Bumped %d (%d)\n", this, gpGlobals->framecount ); |
|
m_iFrameBlocked = gpGlobals->framecount; |
|
SetNextThink( gpGlobals->curtime ); |
|
return false; |
|
} |
|
else |
|
{ |
|
DbgFrameLimitMsg( "(Over %d )\n", this ); |
|
} |
|
} |
|
} |
|
|
|
DbgFrameLimitMsg( "Running %d (%d)\n", this, gpGlobals->framecount ); |
|
g_StartTimeCurThink = engine->Time(); |
|
|
|
m_iFrameBlocked = -1; |
|
m_nLastThinkTick = TIME_TO_TICKS( m_flLastRealThinkTime ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void CAI_BaseNPC::PostNPCThink( void ) |
|
{ |
|
if ( g_StartTimeCurThink != 0.0 && VCRGetMode() == VCR_Disabled ) |
|
{ |
|
g_NpcTimeThisFrame += engine->Time() - g_StartTimeCurThink; |
|
} |
|
} |
|
|
|
void CAI_BaseNPC::CallNPCThink( void ) |
|
{ |
|
RebalanceThinks(); |
|
|
|
//--------------------------------- |
|
|
|
m_bUsingStandardThinkTime = false; |
|
|
|
//--------------------------------- |
|
|
|
if ( !PreNPCThink() ) |
|
{ |
|
return; |
|
} |
|
|
|
// reduce cache queries by locking model in memory |
|
MDLCACHE_CRITICAL_SECTION(); |
|
|
|
this->NPCThink(); |
|
|
|
m_flLastRealThinkTime = gpGlobals->curtime; |
|
|
|
PostNPCThink(); |
|
} |
|
|
|
bool NPC_CheckBrushExclude( CBaseEntity *pEntity, CBaseEntity *pBrush ) |
|
{ |
|
CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); |
|
|
|
if ( pNPC ) |
|
{ |
|
return pNPC->GetMoveProbe()->ShouldBrushBeIgnored( pBrush ); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
class CTraceFilterPlayerAvoidance : public CTraceFilterEntitiesOnly |
|
{ |
|
public: |
|
CTraceFilterPlayerAvoidance( const CBaseEntity *pEntity ) { m_pIgnore = pEntity; } |
|
|
|
virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) |
|
{ |
|
CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity ); |
|
|
|
if ( m_pIgnore == pEntity ) |
|
return false; |
|
|
|
if ( pEntity->IsPlayer() ) |
|
return true; |
|
|
|
return false; |
|
} |
|
private: |
|
|
|
const CBaseEntity *m_pIgnore; |
|
}; |
|
|
|
void CAI_BaseNPC::GetPlayerAvoidBounds( Vector *pMins, Vector *pMaxs ) |
|
{ |
|
*pMins = WorldAlignMins(); |
|
*pMaxs = WorldAlignMaxs(); |
|
} |
|
|
|
ConVar ai_debug_avoidancebounds( "ai_debug_avoidancebounds", "0" ); |
|
|
|
void CAI_BaseNPC::SetPlayerAvoidState( void ) |
|
{ |
|
bool bShouldPlayerAvoid = false; |
|
Vector vNothing; |
|
|
|
GetSequenceLinearMotion( GetSequence(), &vNothing ); |
|
bool bIsMoving = ( IsMoving() || ( vNothing != vec3_origin ) ); |
|
|
|
//If we are coming out of a script, check if we are stuck inside the player. |
|
if ( m_bPerformAvoidance || ( ShouldPlayerAvoid() && bIsMoving ) ) |
|
{ |
|
trace_t trace; |
|
Vector vMins, vMaxs; |
|
|
|
GetPlayerAvoidBounds( &vMins, &vMaxs ); |
|
|
|
CBasePlayer *pLocalPlayer = AI_GetSinglePlayer(); |
|
|
|
if ( pLocalPlayer ) |
|
{ |
|
bShouldPlayerAvoid = IsBoxIntersectingBox( GetAbsOrigin() + vMins, GetAbsOrigin() + vMaxs, |
|
pLocalPlayer->GetAbsOrigin() + pLocalPlayer->WorldAlignMins(), pLocalPlayer->GetAbsOrigin() + pLocalPlayer->WorldAlignMaxs() ); |
|
} |
|
|
|
if ( ai_debug_avoidancebounds.GetBool() ) |
|
{ |
|
int iRed = ( bShouldPlayerAvoid == true ) ? 255 : 0; |
|
|
|
NDebugOverlay::Box( GetAbsOrigin(), vMins, vMaxs, iRed, 0, 255, 64, 0.1 ); |
|
} |
|
} |
|
|
|
m_bPlayerAvoidState = ShouldPlayerAvoid(); |
|
m_bPerformAvoidance = bShouldPlayerAvoid; |
|
|
|
if ( GetCollisionGroup() == COLLISION_GROUP_NPC || GetCollisionGroup() == COLLISION_GROUP_NPC_ACTOR ) |
|
{ |
|
if ( m_bPerformAvoidance == true ) |
|
{ |
|
SetCollisionGroup( COLLISION_GROUP_NPC_ACTOR ); |
|
} |
|
else |
|
{ |
|
SetCollisionGroup( COLLISION_GROUP_NPC ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Enables player avoidance when the player's vphysics shadow penetrates our vphysics shadow. This can |
|
// happen when the player is hit by a combine ball, which pushes them into an adjacent npc. Subclasses should |
|
// override this if it causes problems, but in general this will solve cases of the player getting stuck in |
|
// the NPC from being pushed. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PlayerPenetratingVPhysics( void ) |
|
{ |
|
m_bPerformAvoidance = true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::CheckPVSCondition() |
|
{ |
|
bool bInPVS = ( UTIL_FindClientInPVS( edict() ) != NULL ) || (UTIL_ClientPVSIsExpanded() && UTIL_FindClientInVisibilityPVS( edict() )); |
|
|
|
if ( bInPVS ) |
|
SetCondition( COND_IN_PVS ); |
|
else |
|
ClearCondition( COND_IN_PVS ); |
|
|
|
return bInPVS; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// NPC Think - calls out to core AI functions and handles this |
|
// npc's specific animation events |
|
// |
|
|
|
void CAI_BaseNPC::NPCThink( void ) |
|
{ |
|
if ( m_bCheckContacts ) |
|
{ |
|
CheckPhysicsContacts(); |
|
} |
|
|
|
Assert( !(m_NPCState == NPC_STATE_DEAD && m_lifeState == LIFE_ALIVE) ); |
|
|
|
//--------------------------------- |
|
|
|
SetNextThink( TICK_NEVER_THINK ); |
|
|
|
//--------------------------------- |
|
|
|
bool bInPVS = CheckPVSCondition(); |
|
|
|
//--------------------------------- |
|
|
|
UpdateSleepState( bInPVS ); |
|
|
|
//--------------------------------- |
|
bool bRanDecision = false; |
|
|
|
if ( GetEfficiency() < AIE_DORMANT && GetSleepState() == AISS_AWAKE ) |
|
{ |
|
static CFastTimer timer; |
|
float thinkLimit = ai_show_think_tolerance.GetFloat(); |
|
|
|
if ( thinkLimit > 0 ) |
|
timer.Start(); |
|
|
|
if ( g_pAINetworkManager && g_pAINetworkManager->IsInitialized() ) |
|
{ |
|
VPROF_BUDGET( "NPCs", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
AI_PROFILE_SCOPE_BEGIN_( GetClassScheduleIdSpace()->GetClassName() ); // need to use a string stable from map load to map load |
|
|
|
SetPlayerAvoidState(); |
|
|
|
if ( PreThink() ) |
|
{ |
|
if ( m_flNextDecisionTime <= gpGlobals->curtime ) |
|
{ |
|
bRanDecision = true; |
|
m_ScheduleState.bTaskRanAutomovement = false; |
|
m_ScheduleState.bTaskUpdatedYaw = false; |
|
RunAI(); |
|
} |
|
else |
|
{ |
|
if ( m_ScheduleState.bTaskRanAutomovement ) |
|
AutoMovement(); |
|
if ( m_ScheduleState.bTaskUpdatedYaw ) |
|
GetMotor()->UpdateYaw(); |
|
} |
|
|
|
PostRun(); |
|
|
|
PerformMovement(); |
|
|
|
m_bIsMoving = IsMoving(); |
|
|
|
PostMovement(); |
|
|
|
SetSimulationTime( gpGlobals->curtime ); |
|
} |
|
else |
|
m_flTimeLastMovement = FLT_MAX; |
|
|
|
AI_PROFILE_SCOPE_END(); |
|
} |
|
|
|
if ( thinkLimit > 0 ) |
|
{ |
|
timer.End(); |
|
|
|
float thinkTime = g_AIRunTimer.GetDuration().GetMillisecondsF(); |
|
|
|
if ( thinkTime > thinkLimit ) |
|
{ |
|
int color = (int)RemapVal( thinkTime, thinkLimit, thinkLimit * 3, 96.0, 255.0 ); |
|
if ( color > 255 ) |
|
color = 255; |
|
else if ( color < 96 ) |
|
color = 96; |
|
|
|
Vector right; |
|
Vector vecPoint; |
|
|
|
vecPoint = EyePosition() + Vector( 0, 0, 12 ); |
|
GetVectors( NULL, &right, NULL ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 64 ), color, 0, 0, false , 1.0 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 16 ) + right * 16, color, 0, 0, false , 1.0 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 16 ) - right * 16, color, 0, 0, false , 1.0 ); |
|
} |
|
} |
|
} |
|
|
|
m_bUsingStandardThinkTime = ( GetNextThinkTick() == TICK_NEVER_THINK ); |
|
|
|
UpdateEfficiency( bInPVS ); |
|
|
|
if ( m_bUsingStandardThinkTime ) |
|
{ |
|
static const char *ppszEfficiencies[] = |
|
{ |
|
"AIE_NORMAL", |
|
"AIE_EFFICIENT", |
|
"AIE_VERY_EFFICIENT", |
|
"AIE_SUPER_EFFICIENT", |
|
"AIE_DORMANT", |
|
}; |
|
|
|
static const char *ppszMoveEfficiencies[] = |
|
{ |
|
"AIME_NORMAL", |
|
"AIME_EFFICIENT", |
|
}; |
|
|
|
if ( ai_debug_efficiency.GetBool() ) |
|
DevMsg( this, "Eff: %s, Move: %s\n", ppszEfficiencies[GetEfficiency()], ppszMoveEfficiencies[GetMoveEfficiency()] ); |
|
|
|
static float g_DecisionIntervals[] = |
|
{ |
|
.1, // AIE_NORMAL |
|
.2, // AIE_EFFICIENT |
|
.4, // AIE_VERY_EFFICIENT |
|
.6, // AIE_SUPER_EFFICIENT |
|
}; |
|
|
|
if ( bRanDecision ) |
|
{ |
|
m_flNextDecisionTime = gpGlobals->curtime + g_DecisionIntervals[GetEfficiency()]; |
|
} |
|
|
|
if ( GetMoveEfficiency() == AIME_NORMAL || GetEfficiency() == AIE_NORMAL ) |
|
{ |
|
SetNextThink( gpGlobals->curtime + .1 ); |
|
} |
|
else |
|
{ |
|
SetNextThink( gpGlobals->curtime + .2 ); |
|
} |
|
} |
|
else |
|
{ |
|
m_flNextDecisionTime = 0; |
|
} |
|
} |
|
|
|
//========================================================= |
|
// CAI_BaseNPC - USE - will make a npc angry at whomever |
|
// activated it. |
|
//========================================================= |
|
void CAI_BaseNPC::NPCUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) |
|
{ |
|
return; |
|
|
|
// Can't +USE NPCs running scripts |
|
if ( GetState() == NPC_STATE_SCRIPT ) |
|
return; |
|
|
|
if ( IsInAScript() ) |
|
return; |
|
|
|
SetIdealState( NPC_STATE_ALERT ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Virtual function that allows us to have any npc ignore a set of |
|
// shared conditions. |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::RemoveIgnoredConditions( void ) |
|
{ |
|
m_ConditionsPreIgnore = m_Conditions; |
|
m_Conditions.And( m_InverseIgnoreConditions, &m_Conditions ); |
|
|
|
if ( m_NPCState == NPC_STATE_SCRIPT && m_hCine ) |
|
m_hCine->RemoveIgnoredConditions(); |
|
} |
|
|
|
//========================================================= |
|
// RangeAttack1Conditions |
|
//========================================================= |
|
int CAI_BaseNPC::RangeAttack1Conditions ( float flDot, float flDist ) |
|
{ |
|
if ( flDist < 64) |
|
{ |
|
return COND_TOO_CLOSE_TO_ATTACK; |
|
} |
|
else if (flDist > 784) |
|
{ |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
else if (flDot < 0.5) |
|
{ |
|
return COND_NOT_FACING_ATTACK; |
|
} |
|
|
|
return COND_CAN_RANGE_ATTACK1; |
|
} |
|
|
|
//========================================================= |
|
// RangeAttack2Conditions |
|
//========================================================= |
|
int CAI_BaseNPC::RangeAttack2Conditions ( float flDot, float flDist ) |
|
{ |
|
if ( flDist < 64) |
|
{ |
|
return COND_TOO_CLOSE_TO_ATTACK; |
|
} |
|
else if (flDist > 512) |
|
{ |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
else if (flDot < 0.5) |
|
{ |
|
return COND_NOT_FACING_ATTACK; |
|
} |
|
|
|
return COND_CAN_RANGE_ATTACK2; |
|
} |
|
|
|
//========================================================= |
|
// MeleeAttack1Conditions |
|
//========================================================= |
|
int CAI_BaseNPC::MeleeAttack1Conditions ( float flDot, float flDist ) |
|
{ |
|
if ( flDist > 64) |
|
{ |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
else if (flDot < 0.7) |
|
{ |
|
return 0; |
|
} |
|
else if (GetEnemy() == NULL) |
|
{ |
|
return 0; |
|
} |
|
|
|
// Decent fix to keep folks from kicking/punching hornets and snarks is to check the onground flag(sjb) |
|
if ( GetEnemy()->GetFlags() & FL_ONGROUND ) |
|
{ |
|
return COND_CAN_MELEE_ATTACK1; |
|
} |
|
return 0; |
|
} |
|
|
|
//========================================================= |
|
// MeleeAttack2Conditions |
|
//========================================================= |
|
int CAI_BaseNPC::MeleeAttack2Conditions ( float flDot, float flDist ) |
|
{ |
|
if ( flDist > 64) |
|
{ |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
else if (flDot < 0.7) |
|
{ |
|
return 0; |
|
} |
|
return COND_CAN_MELEE_ATTACK2; |
|
} |
|
|
|
// Get capability mask |
|
int CAI_BaseNPC::CapabilitiesGet( void ) const |
|
{ |
|
int capability = m_afCapability; |
|
if ( GetActiveWeapon() ) |
|
{ |
|
capability |= GetActiveWeapon()->CapabilitiesGet(); |
|
} |
|
return capability; |
|
} |
|
|
|
// Set capability mask |
|
int CAI_BaseNPC::CapabilitiesAdd( int capability ) |
|
{ |
|
m_afCapability |= capability; |
|
|
|
return m_afCapability; |
|
} |
|
|
|
// Set capability mask |
|
int CAI_BaseNPC::CapabilitiesRemove( int capability ) |
|
{ |
|
m_afCapability &= ~capability; |
|
|
|
return m_afCapability; |
|
} |
|
|
|
// Clear capability mask |
|
void CAI_BaseNPC::CapabilitiesClear( void ) |
|
{ |
|
m_afCapability = 0; |
|
} |
|
|
|
|
|
//========================================================= |
|
// ClearAttacks - clear out all attack conditions |
|
//========================================================= |
|
void CAI_BaseNPC::ClearAttackConditions( ) |
|
{ |
|
// Clear all attack conditions |
|
ClearCondition( COND_CAN_RANGE_ATTACK1 ); |
|
ClearCondition( COND_CAN_RANGE_ATTACK2 ); |
|
ClearCondition( COND_CAN_MELEE_ATTACK1 ); |
|
ClearCondition( COND_CAN_MELEE_ATTACK2 ); |
|
ClearCondition( COND_WEAPON_HAS_LOS ); |
|
ClearCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); |
|
ClearCondition( COND_WEAPON_PLAYER_IN_SPREAD ); // Player in shooting direction |
|
ClearCondition( COND_WEAPON_PLAYER_NEAR_TARGET ); // Player near shooting position |
|
ClearCondition( COND_WEAPON_SIGHT_OCCLUDED ); |
|
} |
|
|
|
//========================================================= |
|
// GatherAttackConditions - sets all of the bits for attacks that the |
|
// npc is capable of carrying out on the passed entity. |
|
//========================================================= |
|
|
|
void CAI_BaseNPC::GatherAttackConditions( CBaseEntity *pTarget, float flDist ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_GatherAttackConditions); |
|
|
|
Vector vecLOS = ( pTarget->GetAbsOrigin() - GetAbsOrigin() ); |
|
vecLOS.z = 0; |
|
VectorNormalize( vecLOS ); |
|
|
|
Vector vBodyDir = BodyDirection2D( ); |
|
float flDot = DotProduct( vecLOS, vBodyDir ); |
|
|
|
// we know the enemy is in front now. We'll find which attacks the npc is capable of by |
|
// checking for corresponding Activities in the model file, then do the simple checks to validate |
|
// those attack types. |
|
|
|
int capability; |
|
Vector targetPos; |
|
bool bWeaponHasLOS; |
|
int condition; |
|
|
|
capability = CapabilitiesGet(); |
|
|
|
// Clear all attack conditions |
|
AI_PROFILE_SCOPE_BEGIN( CAI_BaseNPC_GatherAttackConditions_PrimaryWeaponLOS ); |
|
|
|
// @TODO (toml 06-15-03): There are simple cases where |
|
// the upper torso of the enemy is visible, and the NPC is at an angle below |
|
// them, but the above test fails because BodyTarget returns the center of |
|
// the target. This needs some better handling/closer evaluation |
|
|
|
// Try the eyes first, as likely to succeed (because can see or else wouldn't be here) thus reducing |
|
// the odds of the need for a second trace |
|
ClearAttackConditions(); |
|
targetPos = pTarget->EyePosition(); |
|
bWeaponHasLOS = CurrentWeaponLOSCondition( targetPos, true ); |
|
|
|
AI_PROFILE_SCOPE_END(); |
|
|
|
if ( !bWeaponHasLOS ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_SecondaryWeaponLOS ); |
|
ClearAttackConditions( ); |
|
targetPos = pTarget->BodyTarget( GetAbsOrigin() ); |
|
bWeaponHasLOS = CurrentWeaponLOSCondition( targetPos, true ); |
|
} |
|
else |
|
{ |
|
SetCondition( COND_WEAPON_HAS_LOS ); |
|
} |
|
|
|
bool bWeaponIsReady = (GetActiveWeapon() && !IsWeaponStateChanging()); |
|
|
|
// FIXME: move this out of here |
|
if ( (capability & bits_CAP_WEAPON_RANGE_ATTACK1) && bWeaponIsReady ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_WeaponRangeAttack1Condition ); |
|
|
|
condition = GetActiveWeapon()->WeaponRangeAttack1Condition(flDot, flDist); |
|
|
|
if ( condition == COND_NOT_FACING_ATTACK && FInAimCone( targetPos ) ) |
|
DevMsg( "Warning: COND_NOT_FACING_ATTACK set but FInAimCone is true\n" ); |
|
|
|
if (condition != COND_CAN_RANGE_ATTACK1 || bWeaponHasLOS) |
|
{ |
|
SetCondition(condition); |
|
} |
|
} |
|
else if ( capability & bits_CAP_INNATE_RANGE_ATTACK1 ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_RangeAttack1Condition ); |
|
|
|
condition = RangeAttack1Conditions( flDot, flDist ); |
|
if (condition != COND_CAN_RANGE_ATTACK1 || bWeaponHasLOS) |
|
{ |
|
SetCondition(condition); |
|
} |
|
} |
|
|
|
if ( (capability & bits_CAP_WEAPON_RANGE_ATTACK2) && bWeaponIsReady && ( GetActiveWeapon()->CapabilitiesGet() & bits_CAP_WEAPON_RANGE_ATTACK2 ) ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_WeaponRangeAttack2Condition ); |
|
|
|
condition = GetActiveWeapon()->WeaponRangeAttack2Condition(flDot, flDist); |
|
if (condition != COND_CAN_RANGE_ATTACK2 || bWeaponHasLOS) |
|
{ |
|
SetCondition(condition); |
|
} |
|
} |
|
else if ( capability & bits_CAP_INNATE_RANGE_ATTACK2 ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_RangeAttack2Condition ); |
|
|
|
condition = RangeAttack2Conditions( flDot, flDist ); |
|
if (condition != COND_CAN_RANGE_ATTACK2 || bWeaponHasLOS) |
|
{ |
|
SetCondition(condition); |
|
} |
|
} |
|
|
|
if ( (capability & bits_CAP_WEAPON_MELEE_ATTACK1) && bWeaponIsReady) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_WeaponMeleeAttack1Condition ); |
|
SetCondition(GetActiveWeapon()->WeaponMeleeAttack1Condition(flDot, flDist)); |
|
} |
|
else if ( capability & bits_CAP_INNATE_MELEE_ATTACK1 ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_MeleeAttack1Condition ); |
|
SetCondition(MeleeAttack1Conditions ( flDot, flDist )); |
|
} |
|
|
|
if ( (capability & bits_CAP_WEAPON_MELEE_ATTACK2) && bWeaponIsReady) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_WeaponMeleeAttack2Condition ); |
|
SetCondition(GetActiveWeapon()->WeaponMeleeAttack2Condition(flDot, flDist)); |
|
} |
|
else if ( capability & bits_CAP_INNATE_MELEE_ATTACK2 ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_GatherAttackConditions_MeleeAttack2Condition ); |
|
SetCondition(MeleeAttack2Conditions ( flDot, flDist )); |
|
} |
|
|
|
// ----------------------------------------------------------------- |
|
// If any attacks are possible clear attack specific bits |
|
// ----------------------------------------------------------------- |
|
if (HasCondition(COND_CAN_RANGE_ATTACK2) || |
|
HasCondition(COND_CAN_RANGE_ATTACK1) || |
|
HasCondition(COND_CAN_MELEE_ATTACK2) || |
|
HasCondition(COND_CAN_MELEE_ATTACK1) ) |
|
{ |
|
ClearCondition(COND_TOO_CLOSE_TO_ATTACK); |
|
ClearCondition(COND_TOO_FAR_TO_ATTACK); |
|
ClearCondition(COND_WEAPON_BLOCKED_BY_FRIEND); |
|
} |
|
} |
|
|
|
|
|
//========================================================= |
|
// SetState |
|
//========================================================= |
|
void CAI_BaseNPC::SetState( NPC_STATE State ) |
|
{ |
|
NPC_STATE OldState; |
|
|
|
OldState = m_NPCState; |
|
|
|
if ( State != m_NPCState ) |
|
{ |
|
m_flLastStateChangeTime = gpGlobals->curtime; |
|
} |
|
|
|
switch( State ) |
|
{ |
|
// Drop enemy pointers when going to idle |
|
case NPC_STATE_IDLE: |
|
|
|
if ( GetEnemy() != NULL ) |
|
{ |
|
SetEnemy( NULL ); // not allowed to have an enemy anymore. |
|
DevMsg( 2, "Stripped\n" ); |
|
} |
|
break; |
|
} |
|
|
|
bool fNotifyChange = false; |
|
|
|
if( m_NPCState != State ) |
|
{ |
|
// Don't notify if we're changing to a state we're already in! |
|
fNotifyChange = true; |
|
} |
|
|
|
m_NPCState = State; |
|
SetIdealState( State ); |
|
|
|
// Notify the character that its state has changed. |
|
if( fNotifyChange ) |
|
{ |
|
OnStateChange( OldState, m_NPCState ); |
|
} |
|
} |
|
|
|
bool CAI_BaseNPC::WokeThisTick() const |
|
{ |
|
return m_nWakeTick == gpGlobals->tickcount ? true : false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::Wake( bool bFireOutput ) |
|
{ |
|
if ( GetSleepState() != AISS_AWAKE ) |
|
{ |
|
m_nWakeTick = gpGlobals->tickcount; |
|
SetSleepState( AISS_AWAKE ); |
|
RemoveEffects( EF_NODRAW ); |
|
if ( bFireOutput ) |
|
m_OnWake.FireOutput( this, this ); |
|
|
|
if ( m_bWakeSquad && GetSquad() ) |
|
{ |
|
AISquadIter_t iter; |
|
for ( CAI_BaseNPC *pSquadMember = GetSquad()->GetFirstMember( &iter ); pSquadMember; pSquadMember = GetSquad()->GetNextMember( &iter ) ) |
|
{ |
|
if ( pSquadMember->IsAlive() && pSquadMember != this ) |
|
{ |
|
pSquadMember->m_bWakeSquad = false; |
|
pSquadMember->Wake(); |
|
} |
|
} |
|
|
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::Sleep() |
|
{ |
|
// Don't render. |
|
AddEffects( EF_NODRAW ); |
|
|
|
if( GetState() == NPC_STATE_SCRIPT ) |
|
{ |
|
Warning( "%s put to sleep while in Scripted state!\n", GetClassname() ); |
|
} |
|
|
|
VacateStrategySlot(); |
|
|
|
// Slam my schedule. |
|
SetSchedule( SCHED_SLEEP ); |
|
|
|
m_OnSleep.FireOutput( this, this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Sets all sensing-related conditions |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PerformSensing( void ) |
|
{ |
|
GetSenses()->PerformSensing(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::ClearSenseConditions( void ) |
|
{ |
|
static int conditionsToClear[] = |
|
{ |
|
COND_SEE_HATE, |
|
COND_SEE_DISLIKE, |
|
COND_SEE_ENEMY, |
|
COND_SEE_FEAR, |
|
COND_SEE_NEMESIS, |
|
COND_SEE_PLAYER, |
|
COND_HEAR_DANGER, |
|
COND_HEAR_COMBAT, |
|
COND_HEAR_WORLD, |
|
COND_HEAR_PLAYER, |
|
COND_HEAR_THUMPER, |
|
COND_HEAR_BUGBAIT, |
|
COND_HEAR_PHYSICS_DANGER, |
|
COND_HEAR_MOVE_AWAY, |
|
COND_SMELL, |
|
}; |
|
|
|
ClearConditions( conditionsToClear, ARRAYSIZE( conditionsToClear ) ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::CheckOnGround( void ) |
|
{ |
|
bool bScriptedWait = ( IsCurSchedule( SCHED_WAIT_FOR_SCRIPT ) || ( m_hCine && m_scriptState == CAI_BaseNPC::SCRIPT_WAIT ) ); |
|
if ( !bScriptedWait && !HasCondition( COND_FLOATING_OFF_GROUND ) ) |
|
{ |
|
// parented objects are never floating |
|
if (GetMoveParent() != NULL) |
|
return; |
|
|
|
// NPCs in scripts with the fly flag shouldn't fall. |
|
// FIXME: should NPCS with FL_FLY ever fall? Doesn't seem like they should. |
|
if ( ( GetState() == NPC_STATE_SCRIPT ) && ( GetFlags() & FL_FLY ) ) |
|
return; |
|
|
|
if ( ( GetNavType() == NAV_GROUND ) && ( GetMoveType() != MOVETYPE_VPHYSICS ) && ( GetMoveType() != MOVETYPE_NONE ) ) |
|
{ |
|
if ( m_CheckOnGroundTimer.Expired() ) |
|
{ |
|
m_CheckOnGroundTimer.Set(0.5); |
|
|
|
// check a shrunk box centered around the foot |
|
Vector maxs = WorldAlignMaxs(); |
|
Vector mins = WorldAlignMins(); |
|
|
|
if ( mins != maxs ) // some NPCs have no hull, so mins == maxs == vec3_origin |
|
{ |
|
maxs -= Vector( 0.0f, 0.0f, 0.2f ); |
|
|
|
Vector vecStart = GetAbsOrigin() + Vector( 0, 0, .1f ); |
|
Vector vecDown = GetAbsOrigin(); |
|
vecDown.z -= 4.0; |
|
|
|
trace_t trace; |
|
m_pMoveProbe->TraceHull( vecStart, vecDown, mins, maxs, MASK_NPCSOLID, &trace ); |
|
|
|
if (trace.fraction == 1.0) |
|
{ |
|
SetCondition( COND_FLOATING_OFF_GROUND ); |
|
SetGroundEntity( NULL ); |
|
} |
|
else |
|
{ |
|
if ( trace.startsolid && trace.m_pEnt->GetMoveType() == MOVETYPE_VPHYSICS && |
|
trace.m_pEnt->VPhysicsGetObject() && trace.m_pEnt->VPhysicsGetObject()->GetMass() < VPHYSICS_LARGE_OBJECT_MASS ) |
|
{ |
|
// stuck inside a small physics object? |
|
m_CheckOnGroundTimer.Set(0.1f); |
|
NPCPhysics_CreateSolver( this, trace.m_pEnt, true, 0.25f ); |
|
if ( VPhysicsGetObject() ) |
|
{ |
|
VPhysicsGetObject()->RecheckContactPoints(); |
|
} |
|
} |
|
// Check to see if someone changed the ground on us... |
|
if ( trace.m_pEnt && trace.m_pEnt != GetGroundEntity() ) |
|
{ |
|
SetGroundEntity( trace.m_pEnt ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// parented objects are never floating |
|
if ( bScriptedWait || GetMoveParent() != NULL || (GetFlags() & FL_ONGROUND ) || GetNavType() != NAV_GROUND ) |
|
{ |
|
ClearCondition( COND_FLOATING_OFF_GROUND ); |
|
} |
|
} |
|
|
|
} |
|
|
|
void CAI_BaseNPC::NotifyPushMove() |
|
{ |
|
// don't recheck ground when I'm being push-moved |
|
m_CheckOnGroundTimer.Set( 0.5f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::CanFlinch( void ) |
|
{ |
|
if ( IsCurSchedule( SCHED_BIG_FLINCH ) ) |
|
return false; |
|
|
|
if ( m_flNextFlinchTime >= gpGlobals->curtime ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CheckFlinches( void ) |
|
{ |
|
// If we're currently flinching, don't allow gesture flinches to be overlaid |
|
if ( IsCurSchedule( SCHED_BIG_FLINCH ) ) |
|
{ |
|
ClearCondition( COND_LIGHT_DAMAGE ); |
|
ClearCondition( COND_HEAVY_DAMAGE ); |
|
} |
|
|
|
// If we've taken heavy damage, try to do a full schedule flinch |
|
if ( HasCondition(COND_HEAVY_DAMAGE) ) |
|
{ |
|
// If we've already flinched recently, gesture flinch instead. |
|
if ( HasMemory(bits_MEMORY_FLINCHED) ) |
|
{ |
|
// Clear the heavy damage condition so we don't interrupt schedules |
|
// when we play a gesture flinch because we recently did a full flinch. |
|
// Prevents the player from stun-locking enemies, even though they don't full flinch. |
|
ClearCondition( COND_HEAVY_DAMAGE ); |
|
} |
|
else if ( !HasInterruptCondition(COND_HEAVY_DAMAGE) ) |
|
{ |
|
// If we have taken heavy damage, but the current schedule doesn't |
|
// break on that, resort to just playing a gesture flinch. |
|
PlayFlinchGesture(); |
|
} |
|
|
|
// Otherwise, do nothing. The heavy damage will interrupt our schedule and we'll flinch. |
|
} |
|
else if ( HasCondition( COND_LIGHT_DAMAGE ) ) |
|
{ |
|
// If we have taken light damage play gesture flinches |
|
PlayFlinchGesture(); |
|
} |
|
|
|
// If it's been a while since we did a full flinch, forget that we flinched so we'll flinch fully again |
|
if ( HasMemory(bits_MEMORY_FLINCHED) && gpGlobals->curtime > m_flNextFlinchTime ) |
|
{ |
|
Forget(bits_MEMORY_FLINCHED); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::GatherConditions( void ) |
|
{ |
|
m_bConditionsGathered = true; |
|
g_AIConditionsTimer.Start(); |
|
|
|
if( gpGlobals->curtime > m_flTimePingEffect && m_flTimePingEffect > 0.0f ) |
|
{ |
|
// Turn off the pinging. |
|
DispatchUpdateTransmitState(); |
|
m_flTimePingEffect = 0.0f; |
|
} |
|
|
|
if ( m_NPCState != NPC_STATE_NONE && m_NPCState != NPC_STATE_DEAD ) |
|
{ |
|
if ( FacingIdeal() ) |
|
Forget( bits_MEMORY_TURNING ); |
|
|
|
bool bForcedGather = m_bForceConditionsGather; |
|
m_bForceConditionsGather = false; |
|
|
|
if ( m_pfnThink != (BASEPTR)&CAI_BaseNPC::CallNPCThink ) |
|
{ |
|
if ( UTIL_FindClientInPVS( edict() ) != NULL ) |
|
SetCondition( COND_IN_PVS ); |
|
else |
|
ClearCondition( COND_IN_PVS ); |
|
} |
|
|
|
// Sample the environment. Do this unconditionally if there is a player in this |
|
// npc's PVS. NPCs in COMBAT state are allowed to simulate when there is no player in |
|
// the same PVS. This is so that any fights in progress will continue even if the player leaves the PVS. |
|
if ( !IsFlaggedEfficient() && |
|
( bForcedGather || |
|
HasCondition( COND_IN_PVS ) || |
|
ShouldAlwaysThink() || |
|
m_NPCState == NPC_STATE_COMBAT ) ) |
|
{ |
|
CheckOnGround(); |
|
|
|
if ( ShouldPlayIdleSound() ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_IdleSound); |
|
IdleSound(); |
|
} |
|
|
|
PerformSensing(); |
|
|
|
GetEnemies()->RefreshMemories(); |
|
ChooseEnemy(); |
|
|
|
// Check to see if there is a better weapon available |
|
if (Weapon_IsBetterAvailable()) |
|
{ |
|
SetCondition(COND_BETTER_WEAPON_AVAILABLE); |
|
} |
|
|
|
if ( GetCurSchedule() && |
|
( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT) && |
|
GetEnemy() && |
|
!HasCondition( COND_NEW_ENEMY ) && |
|
GetCurSchedule()->HasInterrupt( COND_NEW_ENEMY ) ) |
|
{ |
|
// @Note (toml 05-05-04): There seems to be a case where an NPC can not respond |
|
// to COND_NEW_ENEMY. Only evidence right now is save |
|
// games after the fact, so for now, just patching it up |
|
DevMsg( 2, "Had to force COND_NEW_ENEMY\n" ); |
|
SetCondition(COND_NEW_ENEMY); |
|
} |
|
} |
|
else |
|
{ |
|
// if not done, can have problems if leave PVS in same frame heard/saw things, |
|
// since only PerformSensing clears conditions |
|
ClearSenseConditions(); |
|
} |
|
|
|
// do these calculations if npc has an enemy. |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
if ( !IsFlaggedEfficient() ) |
|
{ |
|
GatherEnemyConditions( GetEnemy() ); |
|
m_flLastEnemyTime = gpGlobals->curtime; |
|
} |
|
else |
|
{ |
|
SetEnemy( NULL ); |
|
} |
|
} |
|
|
|
// do these calculations if npc has a target |
|
if ( GetTarget() != NULL ) |
|
{ |
|
CheckTarget( GetTarget() ); |
|
} |
|
|
|
CheckAmmo(); |
|
|
|
CheckFlinches(); |
|
|
|
CheckSquad(); |
|
} |
|
else |
|
ClearCondition( COND_IN_PVS ); |
|
|
|
g_AIConditionsTimer.End(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PrescheduleThink( void ) |
|
{ |
|
#ifdef HL2_EPISODIC |
|
CheckForScriptedNPCInteractions(); |
|
#endif |
|
|
|
// If we use weapons, and our desired weapon state is not the current, fix it |
|
if( (CapabilitiesGet() & bits_CAP_USE_WEAPONS) && (m_iDesiredWeaponState == DESIREDWEAPONSTATE_HOLSTERED || m_iDesiredWeaponState == DESIREDWEAPONSTATE_UNHOLSTERED || m_iDesiredWeaponState == DESIREDWEAPONSTATE_HOLSTERED_DESTROYED ) ) |
|
{ |
|
if ( IsAlive() && !IsInAScript() ) |
|
{ |
|
if ( !IsCurSchedule( SCHED_MELEE_ATTACK1, false ) && !IsCurSchedule( SCHED_MELEE_ATTACK2, false ) && |
|
!IsCurSchedule( SCHED_RANGE_ATTACK1, false ) && !IsCurSchedule( SCHED_RANGE_ATTACK2, false ) ) |
|
{ |
|
if ( m_iDesiredWeaponState == DESIREDWEAPONSTATE_HOLSTERED || m_iDesiredWeaponState == DESIREDWEAPONSTATE_HOLSTERED_DESTROYED ) |
|
{ |
|
HolsterWeapon(); |
|
} |
|
else if ( m_iDesiredWeaponState == DESIREDWEAPONSTATE_UNHOLSTERED ) |
|
{ |
|
UnholsterWeapon(); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// Throw away the request |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_IGNORE; |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Main entry point for processing AI |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::RunAI( void ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_RunAI); |
|
g_AIRunTimer.Start(); |
|
|
|
if( ai_debug_squads.GetBool() ) |
|
{ |
|
if( IsInSquad() && GetSquad() && !CAI_Squad::IsSilentMember(this ) && ( GetSquad()->IsLeader( this ) || GetSquad()->NumMembers() == 1 ) ) |
|
{ |
|
AISquadIter_t iter; |
|
CAI_Squad *pSquad = GetSquad(); |
|
|
|
Vector right; |
|
Vector vecPoint; |
|
|
|
vecPoint = EyePosition() + Vector( 0, 0, 12 ); |
|
GetVectors( NULL, &right, NULL ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 64 ), 0, 255, 0, false , 0.1 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 32 ) + right * 32, 0, 255, 0, false , 0.1 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 32 ) - right * 32, 0, 255, 0, false , 0.1 ); |
|
|
|
for ( CAI_BaseNPC *pSquadMember = pSquad->GetFirstMember( &iter, false ); pSquadMember; pSquadMember = pSquad->GetNextMember( &iter, false ) ) |
|
{ |
|
if ( pSquadMember != this ) |
|
NDebugOverlay::Line( EyePosition(), pSquadMember->EyePosition(), 0, |
|
CAI_Squad::IsSilentMember(pSquadMember) ? 127 : 255, 0, false , 0.1 ); |
|
} |
|
} |
|
} |
|
|
|
if( ai_debug_loners.GetBool() && !IsInSquad() && AI_IsSinglePlayer() ) |
|
{ |
|
Vector right; |
|
Vector vecPoint; |
|
|
|
vecPoint = EyePosition() + Vector( 0, 0, 12 ); |
|
|
|
UTIL_GetLocalPlayer()->GetVectors( NULL, &right, NULL ); |
|
|
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 64 ), 255, 0, 0, false , 0.1 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 32 ) + right * 32, 255, 0, 0, false , 0.1 ); |
|
NDebugOverlay::Line( vecPoint, vecPoint + Vector( 0, 0, 32 ) - right * 32, 255, 0, 0, false , 0.1 ); |
|
} |
|
|
|
#ifdef _DEBUG |
|
m_bSelected = ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) != 0 ); |
|
#endif |
|
|
|
m_bConditionsGathered = false; |
|
m_bSkippedChooseEnemy = false; |
|
|
|
if ( g_pDeveloper->GetInt() && !GetNavigator()->IsOnNetwork() ) |
|
{ |
|
AddTimedOverlay( "NPC w/no reachable nodes!", 5 ); |
|
} |
|
|
|
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_RunAI_GatherConditions); |
|
GatherConditions(); |
|
RemoveIgnoredConditions(); |
|
AI_PROFILE_SCOPE_END(); |
|
|
|
if ( !m_bConditionsGathered ) |
|
m_bConditionsGathered = true; // derived class didn't call to base |
|
|
|
TryRestoreHull(); |
|
|
|
g_AIPrescheduleThinkTimer.Start(); |
|
|
|
AI_PROFILE_SCOPE_BEGIN(CAI_RunAI_PrescheduleThink); |
|
PrescheduleThink(); |
|
AI_PROFILE_SCOPE_END(); |
|
|
|
g_AIPrescheduleThinkTimer.End(); |
|
|
|
MaintainSchedule(); |
|
|
|
PostscheduleThink(); |
|
|
|
ClearTransientConditions(); |
|
|
|
g_AIRunTimer.End(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ClearTransientConditions() |
|
{ |
|
// if the npc didn't use these conditions during the above call to MaintainSchedule() |
|
// we throw them out cause we don't want them sitting around through the lifespan of a schedule |
|
// that doesn't use them. |
|
ClearCondition( COND_LIGHT_DAMAGE ); |
|
ClearCondition( COND_HEAVY_DAMAGE ); |
|
ClearCondition( COND_PHYSICS_DAMAGE ); |
|
ClearCondition( COND_PLAYER_PUSHING ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Selecting the idle ideal state |
|
//----------------------------------------------------------------------------- |
|
NPC_STATE CAI_BaseNPC::SelectIdleIdealState() |
|
{ |
|
// IDLE goes to ALERT upon hearing a sound |
|
// IDLE goes to ALERT upon being injured |
|
// IDLE goes to ALERT upon seeing food |
|
// IDLE goes to COMBAT upon sighting an enemy |
|
if ( HasCondition(COND_NEW_ENEMY) || |
|
HasCondition(COND_SEE_ENEMY) ) |
|
{ |
|
// new enemy! This means an idle npc has seen someone it dislikes, or |
|
// that a npc in combat has found a more suitable target to attack |
|
return NPC_STATE_COMBAT; |
|
} |
|
|
|
// Set our ideal yaw if we've taken damage |
|
if ( HasCondition(COND_LIGHT_DAMAGE) || |
|
HasCondition(COND_HEAVY_DAMAGE) || |
|
(!GetEnemy() && gpGlobals->curtime - GetEnemies()->LastTimeSeen( AI_UNKNOWN_ENEMY ) < TIME_CARE_ABOUT_DAMAGE ) ) |
|
{ |
|
Vector vecEnemyLKP; |
|
|
|
// Fill in where we're trying to look |
|
if ( GetEnemy() ) |
|
{ |
|
vecEnemyLKP = GetEnemyLKP(); |
|
} |
|
else |
|
{ |
|
if ( GetEnemies()->Find( AI_UNKNOWN_ENEMY ) ) |
|
{ |
|
vecEnemyLKP = GetEnemies()->LastKnownPosition( AI_UNKNOWN_ENEMY ); |
|
} |
|
else |
|
{ |
|
// Don't have an enemy, so face the direction the last attack came from (don't face north) |
|
vecEnemyLKP = WorldSpaceCenter() + ( g_vecAttackDir * 128 ); |
|
} |
|
} |
|
|
|
// Set the ideal |
|
GetMotor()->SetIdealYawToTarget( vecEnemyLKP ); |
|
|
|
return NPC_STATE_ALERT; |
|
} |
|
|
|
if ( HasCondition(COND_HEAR_DANGER) || |
|
HasCondition(COND_HEAR_COMBAT) || |
|
HasCondition(COND_HEAR_WORLD) || |
|
HasCondition(COND_HEAR_PLAYER) || |
|
HasCondition(COND_HEAR_THUMPER) || |
|
HasCondition(COND_HEAR_BULLET_IMPACT) ) |
|
{ |
|
// Interrupted by a sound. So make our ideal yaw the |
|
// source of the sound! |
|
CSound *pSound; |
|
|
|
pSound = GetBestSound(); |
|
Assert( pSound != NULL ); |
|
if ( pSound ) |
|
{ |
|
// BRJ 1/7/04: This code used to set the ideal yaw. |
|
// It's really side-effecty to set the yaw here. |
|
// That is now done by the FACE_BESTSOUND schedule. |
|
// Revert this change if it causes problems. |
|
GetMotor()->SetIdealYawToTarget( pSound->GetSoundReactOrigin() ); |
|
if ( pSound->IsSoundType( SOUND_COMBAT | SOUND_DANGER | SOUND_BULLET_IMPACT ) ) |
|
{ |
|
return NPC_STATE_ALERT; |
|
} |
|
} |
|
} |
|
|
|
if ( HasInterruptCondition(COND_SMELL) ) |
|
{ |
|
return NPC_STATE_ALERT; |
|
} |
|
|
|
return NPC_STATE_INVALID; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Selecting the alert ideal state |
|
//----------------------------------------------------------------------------- |
|
NPC_STATE CAI_BaseNPC::SelectAlertIdealState() |
|
{ |
|
// ALERT goes to IDLE upon becoming bored |
|
// ALERT goes to COMBAT upon sighting an enemy |
|
if ( HasCondition(COND_NEW_ENEMY) || |
|
HasCondition(COND_SEE_ENEMY) || |
|
GetEnemy() != NULL ) |
|
{ |
|
return NPC_STATE_COMBAT; |
|
} |
|
|
|
// Set our ideal yaw if we've taken damage |
|
if ( HasCondition(COND_LIGHT_DAMAGE) || |
|
HasCondition(COND_HEAVY_DAMAGE) || |
|
(!GetEnemy() && gpGlobals->curtime - GetEnemies()->LastTimeSeen( AI_UNKNOWN_ENEMY ) < TIME_CARE_ABOUT_DAMAGE ) ) |
|
{ |
|
Vector vecEnemyLKP; |
|
|
|
// Fill in where we're trying to look |
|
if ( GetEnemy() ) |
|
{ |
|
vecEnemyLKP = GetEnemyLKP(); |
|
} |
|
else |
|
{ |
|
if ( GetEnemies()->Find( AI_UNKNOWN_ENEMY ) ) |
|
{ |
|
vecEnemyLKP = GetEnemies()->LastKnownPosition( AI_UNKNOWN_ENEMY ); |
|
} |
|
else |
|
{ |
|
// Don't have an enemy, so face the direction the last attack came from (don't face north) |
|
vecEnemyLKP = WorldSpaceCenter() + ( g_vecAttackDir * 128 ); |
|
} |
|
} |
|
|
|
// Set the ideal |
|
GetMotor()->SetIdealYawToTarget( vecEnemyLKP ); |
|
|
|
return NPC_STATE_ALERT; |
|
} |
|
|
|
if ( HasCondition(COND_HEAR_DANGER) || |
|
HasCondition(COND_HEAR_COMBAT) ) |
|
{ |
|
CSound *pSound = GetBestSound(); |
|
AssertOnce( pSound != NULL ); |
|
|
|
if ( pSound ) |
|
{ |
|
GetMotor()->SetIdealYawToTarget( pSound->GetSoundReactOrigin() ); |
|
} |
|
|
|
return NPC_STATE_ALERT; |
|
} |
|
|
|
if ( ShouldGoToIdleState() ) |
|
{ |
|
return NPC_STATE_IDLE; |
|
} |
|
|
|
return NPC_STATE_INVALID; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Selecting the alert ideal state |
|
//----------------------------------------------------------------------------- |
|
NPC_STATE CAI_BaseNPC::SelectScriptIdealState() |
|
{ |
|
if ( HasCondition(COND_TASK_FAILED) || |
|
HasCondition(COND_LIGHT_DAMAGE) || |
|
HasCondition(COND_HEAVY_DAMAGE) ) |
|
{ |
|
ExitScriptedSequence(); // This will set the ideal state |
|
} |
|
|
|
if ( m_IdealNPCState == NPC_STATE_IDLE ) |
|
{ |
|
// Exiting a script. Select the ideal state assuming we were idle now. |
|
m_NPCState = NPC_STATE_IDLE; |
|
NPC_STATE eIdealState = SelectIdealState(); |
|
m_NPCState = NPC_STATE_SCRIPT; |
|
return eIdealState; |
|
} |
|
|
|
return NPC_STATE_INVALID; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Surveys the Conditions information available and finds the best |
|
// new state for a npc. |
|
// |
|
// NOTICE the CAI_BaseNPC implementation of this function does not care about |
|
// private conditions! |
|
// |
|
// Output : NPC_STATE - the suggested ideal state based on current conditions. |
|
//----------------------------------------------------------------------------- |
|
NPC_STATE CAI_BaseNPC::SelectIdealState( void ) |
|
{ |
|
// dvs: FIXME: lots of side effecty code in here!! this function should ONLY return an ideal state! |
|
|
|
// --------------------------- |
|
// Do some squad stuff first |
|
// --------------------------- |
|
if (m_pSquad) |
|
{ |
|
switch( m_NPCState ) |
|
{ |
|
case NPC_STATE_IDLE: |
|
case NPC_STATE_ALERT: |
|
if ( HasCondition ( COND_NEW_ENEMY ) ) |
|
{ |
|
m_pSquad->SquadNewEnemy( GetEnemy() ); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
// --------------------------- |
|
// Set ideal state |
|
// --------------------------- |
|
switch ( m_NPCState ) |
|
{ |
|
case NPC_STATE_IDLE: |
|
{ |
|
NPC_STATE nState = SelectIdleIdealState(); |
|
if ( nState != NPC_STATE_INVALID ) |
|
return nState; |
|
} |
|
break; |
|
|
|
case NPC_STATE_ALERT: |
|
{ |
|
NPC_STATE nState = SelectAlertIdealState(); |
|
if ( nState != NPC_STATE_INVALID ) |
|
return nState; |
|
} |
|
break; |
|
|
|
case NPC_STATE_COMBAT: |
|
// COMBAT goes to ALERT upon death of enemy |
|
{ |
|
if ( GetEnemy() == NULL ) |
|
{ |
|
DevWarning( 2, "***Combat state with no enemy!\n" ); |
|
return NPC_STATE_ALERT; |
|
} |
|
break; |
|
} |
|
case NPC_STATE_SCRIPT: |
|
{ |
|
NPC_STATE nState = SelectScriptIdealState(); |
|
if ( nState != NPC_STATE_INVALID ) |
|
return nState; |
|
} |
|
break; |
|
|
|
case NPC_STATE_DEAD: |
|
return NPC_STATE_DEAD; |
|
} |
|
|
|
// The best ideal state is the current ideal state. |
|
return m_IdealNPCState; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
//------------------------------------------------------------------------------ |
|
void CAI_BaseNPC::GiveWeapon( string_t iszWeaponName ) |
|
{ |
|
CBaseCombatWeapon *pWeapon = Weapon_Create( STRING(iszWeaponName) ); |
|
if ( !pWeapon ) |
|
{ |
|
Warning( "Couldn't create weapon %s to give NPC %s.\n", STRING(iszWeaponName), STRING(GetEntityName()) ); |
|
return; |
|
} |
|
|
|
// If I have a weapon already, drop it |
|
if ( GetActiveWeapon() ) |
|
{ |
|
Weapon_Drop( GetActiveWeapon() ); |
|
} |
|
|
|
// If I have a name, make my weapon match it with "_weapon" appended |
|
if ( GetEntityName() != NULL_STRING ) |
|
{ |
|
pWeapon->SetName( AllocPooledString(UTIL_VarArgs("%s_weapon", STRING(GetEntityName()) )) ); |
|
} |
|
|
|
Weapon_Equip( pWeapon ); |
|
|
|
// Handle this case |
|
OnGivenWeapon( pWeapon ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Rather specific function that tells us if an NPC is in the process of |
|
// moving to a weapon with the intent to pick it up. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsMovingToPickupWeapon() |
|
{ |
|
return IsCurSchedule( SCHED_NEW_WEAPON ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ShouldLookForBetterWeapon() |
|
{ |
|
if( m_flNextWeaponSearchTime > gpGlobals->curtime ) |
|
return false; |
|
|
|
if( !(CapabilitiesGet() & bits_CAP_USE_WEAPONS) ) |
|
return false; |
|
|
|
// Already armed and currently fighting. Don't try to upgrade. |
|
if( GetActiveWeapon() && m_NPCState == NPC_STATE_COMBAT ) |
|
return false; |
|
|
|
if( IsMovingToPickupWeapon() ) |
|
return false; |
|
|
|
if( !IsPlayerAlly() && GetActiveWeapon() ) |
|
return false; |
|
|
|
if( IsInAScript() ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Check if a better weapon is available. |
|
// For now check if there is a weapon and I don't have one. In |
|
// the future |
|
// UNDONE: actually rate the weapons based on there strength |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::Weapon_IsBetterAvailable() |
|
{ |
|
if( m_iszPendingWeapon != NULL_STRING ) |
|
{ |
|
// Some weapon is reserved for us. |
|
return true; |
|
} |
|
|
|
if( ShouldLookForBetterWeapon() ) |
|
{ |
|
if( GetActiveWeapon() ) |
|
{ |
|
m_flNextWeaponSearchTime = gpGlobals->curtime + 2; |
|
} |
|
else |
|
{ |
|
if( IsInPlayerSquad() ) |
|
{ |
|
// Look for a weapon frequently. |
|
m_flNextWeaponSearchTime = gpGlobals->curtime + 1; |
|
} |
|
else |
|
{ |
|
m_flNextWeaponSearchTime = gpGlobals->curtime + 2; |
|
} |
|
} |
|
|
|
if ( Weapon_FindUsable( WEAPON_SEARCH_DELTA ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true is weapon has a line of sight. If bSetConditions is |
|
// true, also sets LOC conditions |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions ) |
|
{ |
|
#if 0 |
|
// @TODO (toml 03-07-04): this code might be better (not tested) |
|
Vector vecLocalRelativePosition; |
|
VectorITransform( npcOwner->Weapon_ShootPosition(), npcOwner->EntityToWorldTransform(), vecLocalRelativePosition ); |
|
|
|
// Compute desired test transform |
|
|
|
// Compute desired x axis |
|
Vector xaxis; |
|
VectorSubtract( targetPos, ownerPos, xaxis ); |
|
|
|
// FIXME: Insert angle test here? |
|
float flAngle = acos( xaxis.z / xaxis.Length() ); |
|
|
|
xaxis.z = 0.0f; |
|
float flLength = VectorNormalize( xaxis ); |
|
if ( flLength < 1e-3 ) |
|
return false; |
|
|
|
Vector yaxis( -xaxis.y, xaxis.x, 0.0f ); |
|
|
|
matrix3x4_t losTestToWorld; |
|
MatrixInitialize( losTestToWorld, ownerPos, xaxis, yaxis, zaxis ); |
|
|
|
Vector barrelPos; |
|
VectorTransform( vecLocalRelativePosition, losTestToWorld, barrelPos ); |
|
|
|
#endif |
|
|
|
bool bHaveLOS = true; |
|
|
|
if (GetActiveWeapon()) |
|
{ |
|
bHaveLOS = GetActiveWeapon()->WeaponLOSCondition(ownerPos, targetPos, bSetConditions); |
|
} |
|
else if (CapabilitiesGet() & bits_CAP_INNATE_RANGE_ATTACK1) |
|
{ |
|
bHaveLOS = InnateWeaponLOSCondition(ownerPos, targetPos, bSetConditions); |
|
} |
|
else |
|
{ |
|
if (bSetConditions) |
|
{ |
|
SetCondition( COND_NO_WEAPON ); |
|
} |
|
bHaveLOS = false; |
|
} |
|
// ------------------------------------------- |
|
// Check for friendly fire with the player |
|
// ------------------------------------------- |
|
if ( CapabilitiesGet() & ( bits_CAP_NO_HIT_PLAYER | bits_CAP_NO_HIT_SQUADMATES ) ) |
|
{ |
|
float spread = 0.92f; |
|
if ( GetActiveWeapon() ) |
|
{ |
|
Vector vSpread = GetAttackSpread( GetActiveWeapon() ); |
|
if ( vSpread.x > VECTOR_CONE_15DEGREES.x ) |
|
spread = FastCos( asin(vSpread.x) ); |
|
else // too much error because using point not box |
|
spread = 0.99145f; // "15 degrees" |
|
} |
|
if ( CapabilitiesGet() & bits_CAP_NO_HIT_PLAYER) |
|
{ |
|
// Check shoot direction relative to player |
|
if (PlayerInSpread( ownerPos, targetPos, spread, 8*12 )) |
|
{ |
|
if (bSetConditions) |
|
{ |
|
SetCondition( COND_WEAPON_PLAYER_IN_SPREAD ); |
|
} |
|
bHaveLOS = false; |
|
} |
|
/* For grenades etc. check that player is clear? |
|
// Check player position also |
|
if (PlayerInRange( targetPos, 100 )) |
|
{ |
|
SetCondition( COND_WEAPON_PLAYER_NEAR_TARGET ); |
|
} |
|
*/ |
|
} |
|
|
|
if ( bHaveLOS ) |
|
{ |
|
if ( ( CapabilitiesGet() & bits_CAP_NO_HIT_SQUADMATES) && m_pSquad && GetEnemy() ) |
|
{ |
|
if ( IsSquadmateInSpread( ownerPos, targetPos, spread, 8*12 ) ) |
|
{ |
|
SetCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); |
|
bHaveLOS = false; |
|
} |
|
} |
|
} |
|
} |
|
return bHaveLOS; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Check the innate weapon LOS for an owner at an arbitrary position |
|
// If bSetConditions is true, LOS related conditions will also be set |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::InnateWeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions ) |
|
{ |
|
// -------------------- |
|
// Check for occlusion |
|
// -------------------- |
|
// Base class version assumes innate weapon position is at eye level |
|
Vector barrelPos = ownerPos + GetViewOffset(); |
|
trace_t tr; |
|
AI_TraceLine( barrelPos, targetPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); |
|
|
|
if ( tr.fraction == 1.0 ) |
|
{ |
|
return true; |
|
} |
|
|
|
CBaseEntity *pHitEntity = tr.m_pEnt; |
|
|
|
// Translate a hit vehicle into its passenger if found |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
CBaseCombatCharacter *pCCEnemy = GetEnemy()->MyCombatCharacterPointer(); |
|
if ( pCCEnemy != NULL && pCCEnemy->IsInAVehicle() ) |
|
{ |
|
// Ok, player in vehicle, check if vehicle is target we're looking at, fire if it is |
|
// Also, check to see if the owner of the entity is the vehicle, in which case it's valid too. |
|
// This catches vehicles that use bone followers. |
|
CBaseEntity *pVehicleEnt = pCCEnemy->GetVehicleEntity(); |
|
if ( pHitEntity == pVehicleEnt || pHitEntity->GetOwnerEntity() == pVehicleEnt ) |
|
return true; |
|
} |
|
} |
|
|
|
if ( pHitEntity == GetEnemy() ) |
|
{ |
|
return true; |
|
} |
|
else if ( pHitEntity && pHitEntity->MyCombatCharacterPointer() ) |
|
{ |
|
if (IRelationType( pHitEntity ) == D_HT) |
|
{ |
|
return true; |
|
} |
|
else if (bSetConditions) |
|
{ |
|
SetCondition(COND_WEAPON_BLOCKED_BY_FRIEND); |
|
} |
|
} |
|
else if (bSetConditions) |
|
{ |
|
SetCondition(COND_WEAPON_SIGHT_OCCLUDED); |
|
SetEnemyOccluder(tr.m_pEnt); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//========================================================= |
|
// CanCheckAttacks - prequalifies a npc to do more fine |
|
// checking of potential attacks. |
|
//========================================================= |
|
bool CAI_BaseNPC::FCanCheckAttacks( void ) |
|
{ |
|
// Not allowed to check attacks while climbing or jumping |
|
// Otherwise schedule is interrupted while on ladder/etc |
|
// which is NOT a legal place to attack from |
|
if ( GetNavType() == NAV_CLIMB || GetNavType() == NAV_JUMP ) |
|
return false; |
|
|
|
if ( HasCondition(COND_SEE_ENEMY) && !HasCondition( COND_ENEMY_TOO_FAR)) |
|
{ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return dist. to enemy (closest of origin/head/feet) |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseNPC::EnemyDistance( CBaseEntity *pEnemy ) |
|
{ |
|
Vector enemyDelta = pEnemy->WorldSpaceCenter() - WorldSpaceCenter(); |
|
|
|
// NOTE: We ignore rotation for computing height. Assume it isn't an effect |
|
// we care about, so we simply use OBBSize().z for height. |
|
// Otherwise you'd do this: |
|
// pEnemy->CollisionProp()->WorldSpaceSurroundingBounds( &enemyMins, &enemyMaxs ); |
|
// float enemyHeight = enemyMaxs.z - enemyMins.z; |
|
|
|
float enemyHeight = pEnemy->CollisionProp()->OBBSize().z; |
|
float myHeight = CollisionProp()->OBBSize().z; |
|
|
|
// max distance our centers can be apart with the boxes still overlapping |
|
float flMaxZDist = ( enemyHeight + myHeight ) * 0.5f; |
|
|
|
// see if the enemy is closer to my head, feet or in between |
|
if ( enemyDelta.z > flMaxZDist ) |
|
{ |
|
// enemy feet above my head, compute distance from my head to his feet |
|
enemyDelta.z -= flMaxZDist; |
|
} |
|
else if ( enemyDelta.z < -flMaxZDist ) |
|
{ |
|
// enemy head below my feet, return distance between my feet and his head |
|
enemyDelta.z += flMaxZDist; |
|
} |
|
else |
|
{ |
|
// boxes overlap in Z, no delta |
|
enemyDelta.z = 0; |
|
} |
|
|
|
return enemyDelta.Length(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
float CAI_BaseNPC::GetReactionDelay( CBaseEntity *pEnemy ) |
|
{ |
|
return ( m_NPCState == NPC_STATE_ALERT || m_NPCState == NPC_STATE_COMBAT ) ? |
|
ai_reaction_delay_alert.GetFloat() : |
|
ai_reaction_delay_idle.GetFloat(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Update information on my enemy |
|
// Input : |
|
// Output : Returns true is new enemy, false is known enemy |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::UpdateEnemyMemory( CBaseEntity *pEnemy, const Vector &position, CBaseEntity *pInformer ) |
|
{ |
|
bool firstHand = ( pInformer == NULL || pInformer == this ); |
|
|
|
AI_PROFILE_SCOPE(CAI_BaseNPC_UpdateEnemyMemory); |
|
|
|
if ( GetEnemies() ) |
|
{ |
|
// If the was eluding me and allow the NPC to play a sound |
|
if (GetEnemies()->HasEludedMe(pEnemy)) |
|
{ |
|
FoundEnemySound(); |
|
} |
|
float reactionDelay = ( !pInformer || pInformer == this ) ? GetReactionDelay( pEnemy ) : 0.0; |
|
bool result = GetEnemies()->UpdateMemory(GetNavigator()->GetNetwork(), pEnemy, position, reactionDelay, firstHand); |
|
|
|
if ( !firstHand && pEnemy && result && GetState() == NPC_STATE_IDLE ) // if it's a new potential enemy |
|
ForceDecisionThink(); |
|
|
|
if ( firstHand && pEnemy && m_pSquad ) |
|
{ |
|
m_pSquad->UpdateEnemyMemory( this, pEnemy, position ); |
|
} |
|
return result; |
|
} |
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Remembers the thing my enemy is hiding behind. Called when either |
|
// COND_ENEMY_OCCLUDED or COND_WEAPON_SIGHT_OCCLUDED is set. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetEnemyOccluder(CBaseEntity *pBlocker) |
|
{ |
|
m_hEnemyOccluder = pBlocker; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Gets the thing my enemy is hiding behind (assuming they are hiding). |
|
//----------------------------------------------------------------------------- |
|
CBaseEntity *CAI_BaseNPC::GetEnemyOccluder(void) |
|
{ |
|
return m_hEnemyOccluder.Get(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: part of the Condition collection process |
|
// gets and stores data and conditions pertaining to a npc's |
|
// enemy. |
|
// @TODO (toml 07-27-03): this should become subservient to the senses. right |
|
// now, it yields different result |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::GatherEnemyConditions( CBaseEntity *pEnemy ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_GatherEnemyConditions); |
|
|
|
ClearCondition( COND_ENEMY_FACING_ME ); |
|
ClearCondition( COND_BEHIND_ENEMY ); |
|
|
|
// --------------------------- |
|
// Set visibility conditions |
|
// --------------------------- |
|
if ( HasCondition( COND_NEW_ENEMY ) || GetSenses()->GetTimeLastUpdate( GetEnemy() ) == gpGlobals->curtime ) |
|
{ |
|
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_GatherEnemyConditions_Visibility); |
|
|
|
ClearCondition( COND_HAVE_ENEMY_LOS ); |
|
ClearCondition( COND_ENEMY_OCCLUDED ); |
|
|
|
CBaseEntity *pBlocker = NULL; |
|
SetEnemyOccluder(NULL); |
|
|
|
bool bSensesDidSee = GetSenses()->DidSeeEntity( pEnemy ); |
|
|
|
if ( !bSensesDidSee && ( ( EnemyDistance( pEnemy ) >= GetSenses()->GetDistLook() ) || !FVisible( pEnemy, MASK_BLOCKLOS, &pBlocker ) ) ) |
|
{ |
|
// No LOS to enemy |
|
SetEnemyOccluder(pBlocker); |
|
SetCondition( COND_ENEMY_OCCLUDED ); |
|
ClearCondition( COND_SEE_ENEMY ); |
|
|
|
if (HasMemory( bits_MEMORY_HAD_LOS )) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_GatherEnemyConditions_Outputs); |
|
// Send output event |
|
if (GetEnemy()->IsPlayer()) |
|
{ |
|
m_OnLostPlayerLOS.FireOutput( GetEnemy(), this ); |
|
} |
|
m_OnLostEnemyLOS.FireOutput( GetEnemy(), this ); |
|
} |
|
Forget( bits_MEMORY_HAD_LOS ); |
|
} |
|
else |
|
{ |
|
// Have LOS but may not be in view cone |
|
SetCondition( COND_HAVE_ENEMY_LOS ); |
|
|
|
if ( bSensesDidSee ) |
|
{ |
|
// Have LOS and in view cone |
|
SetCondition( COND_SEE_ENEMY ); |
|
} |
|
else |
|
{ |
|
ClearCondition( COND_SEE_ENEMY ); |
|
} |
|
|
|
if (!HasMemory( bits_MEMORY_HAD_LOS )) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_BaseNPC_GatherEnemyConditions_Outputs); |
|
// Send output event |
|
EHANDLE hEnemy; |
|
hEnemy.Set( GetEnemy() ); |
|
|
|
if (GetEnemy()->IsPlayer()) |
|
{ |
|
m_OnFoundPlayer.Set(hEnemy, this, this); |
|
m_OnFoundEnemy.Set(hEnemy, this, this); |
|
} |
|
else |
|
{ |
|
m_OnFoundEnemy.Set(hEnemy, this, this); |
|
} |
|
} |
|
Remember( bits_MEMORY_HAD_LOS ); |
|
} |
|
|
|
AI_PROFILE_SCOPE_END(); |
|
} |
|
|
|
// ------------------- |
|
// If enemy is dead |
|
// ------------------- |
|
if ( !pEnemy->IsAlive() ) |
|
{ |
|
SetCondition( COND_ENEMY_DEAD ); |
|
ClearCondition( COND_SEE_ENEMY ); |
|
ClearCondition( COND_ENEMY_OCCLUDED ); |
|
return; |
|
} |
|
|
|
float flDistToEnemy = EnemyDistance(pEnemy); |
|
|
|
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_GatherEnemyConditions_SeeEnemy); |
|
|
|
if ( HasCondition( COND_SEE_ENEMY ) ) |
|
{ |
|
// Trail the enemy a bit if he's moving |
|
if (pEnemy->GetSmoothedVelocity() != vec3_origin) |
|
{ |
|
Vector vTrailPos = pEnemy->GetAbsOrigin() - pEnemy->GetSmoothedVelocity() * random->RandomFloat( -0.05, 0 ); |
|
UpdateEnemyMemory(pEnemy,vTrailPos); |
|
} |
|
else |
|
{ |
|
UpdateEnemyMemory(pEnemy,pEnemy->GetAbsOrigin()); |
|
} |
|
|
|
// If it's not an NPC, assume it can't see me |
|
if ( pEnemy->MyCombatCharacterPointer() && pEnemy->MyCombatCharacterPointer()->FInViewCone ( this ) ) |
|
{ |
|
SetCondition ( COND_ENEMY_FACING_ME ); |
|
ClearCondition ( COND_BEHIND_ENEMY ); |
|
} |
|
else |
|
{ |
|
ClearCondition( COND_ENEMY_FACING_ME ); |
|
SetCondition ( COND_BEHIND_ENEMY ); |
|
} |
|
} |
|
else if ( (!HasCondition(COND_ENEMY_OCCLUDED) && !HasCondition(COND_SEE_ENEMY)) && ( flDistToEnemy <= 256 ) ) |
|
{ |
|
// if the enemy is not occluded, and unseen, that means it is behind or beside the npc. |
|
// if the enemy is near enough the npc, we go ahead and let the npc know where the |
|
// enemy is. Send the enemy in as the informer so this knowledge will be regarded as |
|
// secondhand so that the NPC doesn't |
|
UpdateEnemyMemory( pEnemy, pEnemy->GetAbsOrigin(), pEnemy ); |
|
} |
|
|
|
AI_PROFILE_SCOPE_END(); |
|
|
|
float tooFar = m_flDistTooFar; |
|
if ( GetActiveWeapon() && HasCondition(COND_SEE_ENEMY) ) |
|
{ |
|
tooFar = MAX( m_flDistTooFar, GetActiveWeapon()->m_fMaxRange1 ); |
|
} |
|
|
|
if ( flDistToEnemy >= tooFar ) |
|
{ |
|
// enemy is very far away from npc |
|
SetCondition( COND_ENEMY_TOO_FAR ); |
|
} |
|
else |
|
{ |
|
ClearCondition( COND_ENEMY_TOO_FAR ); |
|
} |
|
|
|
if ( FCanCheckAttacks() ) |
|
{ |
|
// This may also call SetEnemyOccluder! |
|
GatherAttackConditions( GetEnemy(), flDistToEnemy ); |
|
} |
|
else |
|
{ |
|
ClearAttackConditions(); |
|
} |
|
|
|
// If my enemy has moved significantly, or if the enemy has changed update my path |
|
UpdateEnemyPos(); |
|
|
|
// If my target entity has moved significantly, update my path |
|
// This is an odd place to put this, but where else should it go? |
|
UpdateTargetPos(); |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Check if enemy is reachable via the node graph unless I'm not on a network |
|
// ---------------------------------------------------------------------------- |
|
if (GetNavigator()->IsOnNetwork()) |
|
{ |
|
// Note that unreachablity times out |
|
if (IsUnreachable(GetEnemy())) |
|
{ |
|
SetCondition(COND_ENEMY_UNREACHABLE); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
// If I haven't seen the enemy in a while he may have eluded me |
|
//----------------------------------------------------------------------- |
|
if (gpGlobals->curtime - GetEnemyLastTimeSeen() > 8) |
|
{ |
|
//----------------------------------------------------------------------- |
|
// I'm at last known position at enemy isn't in sight then has eluded me |
|
// ---------------------------------------------------------------------- |
|
Vector flEnemyLKP = GetEnemyLKP(); |
|
if (((flEnemyLKP - GetAbsOrigin()).Length2D() < 48) && |
|
!HasCondition(COND_SEE_ENEMY)) |
|
{ |
|
MarkEnemyAsEluded(); |
|
} |
|
//------------------------------------------------------------------- |
|
// If enemy isn't reachable, I can see last known position and enemy |
|
// isn't there, then he has eluded me |
|
// ------------------------------------------------------------------ |
|
if (!HasCondition(COND_SEE_ENEMY) && HasCondition(COND_ENEMY_UNREACHABLE)) |
|
{ |
|
if ( !FVisible( flEnemyLKP ) ) |
|
{ |
|
MarkEnemyAsEluded(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// In the case of goaltype enemy, update the goal position |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseNPC::GetGoalRepathTolerance( CBaseEntity *pGoalEnt, GoalType_t type, const Vector &curGoal, const Vector &curTargetPos ) |
|
{ |
|
float distToGoal = ( GetAbsOrigin() - curTargetPos ).Length() - GetNavigator()->GetArrivalDistance(); |
|
float distMoved1Sec = GetSmoothedVelocity().Length(); |
|
float result = 120; // FIXME: why 120? |
|
|
|
if (distMoved1Sec > 0.0) |
|
{ |
|
float t = distToGoal / distMoved1Sec; |
|
|
|
result = clamp( 120.f * t, 0.f, 120.f ); |
|
// Msg("t %.2f : d %.0f (%.0f)\n", t, result, distMoved1Sec ); |
|
} |
|
|
|
if ( !pGoalEnt->IsPlayer() ) |
|
result *= 1.20; |
|
|
|
return result; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// In the case of goaltype enemy, update the goal position |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::UpdateEnemyPos() |
|
{ |
|
// Don't perform path recomputations during a climb or a jump |
|
if ( !GetNavigator()->IsInterruptable() ) |
|
return; |
|
|
|
if ( m_AnyUpdateEnemyPosTimer.Expired() && m_UpdateEnemyPosTimer.Expired() ) |
|
{ |
|
// FIXME: does GetGoalRepathTolerance() limit re-routing enough to remove this? |
|
// m_UpdateEnemyPosTimer.Set( 0.5, 1.0 ); |
|
|
|
// If my enemy has moved significantly, or if the enemy has changed update my path |
|
if ( GetNavigator()->GetGoalType() == GOALTYPE_ENEMY ) |
|
{ |
|
if (m_hEnemy != GetNavigator()->GetGoalTarget()) |
|
{ |
|
GetNavigator()->SetGoalTarget( m_hEnemy, vec3_origin ); |
|
} |
|
else |
|
{ |
|
Vector vEnemyLKP = GetEnemyLKP(); |
|
TranslateNavGoal( GetEnemy(), vEnemyLKP ); |
|
float tolerance = GetGoalRepathTolerance( GetEnemy(), GOALTYPE_ENEMY, GetNavigator()->GetGoalPos(), vEnemyLKP); |
|
if ( (GetNavigator()->GetGoalPos() - vEnemyLKP).Length() > tolerance ) |
|
{ |
|
// FIXME: when fleeing crowds, won't this severely limit the effectiveness of each individual? Shouldn't this be a mutex that's held for some period so that at least one attacker is effective? |
|
m_AnyUpdateEnemyPosTimer.Set( 0.1 ); // FIXME: what's a reasonable interval? |
|
if ( !GetNavigator()->RefindPathToGoal( false ) ) |
|
{ |
|
TaskFail( FAIL_NO_ROUTE ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// In the case of goaltype targetent, update the goal position |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::UpdateTargetPos() |
|
{ |
|
// BRJ 10/7/02 |
|
// FIXME: make this check time based instead of distance based! |
|
|
|
// Don't perform path recomputations during a climb or a jump |
|
if ( !GetNavigator()->IsInterruptable() ) |
|
return; |
|
|
|
// If my target entity has moved significantly, or has changed, update my path |
|
// This is an odd place to put this, but where else should it go? |
|
if ( GetNavigator()->GetGoalType() == GOALTYPE_TARGETENT ) |
|
{ |
|
if (m_hTargetEnt != GetNavigator()->GetGoalTarget()) |
|
{ |
|
GetNavigator()->SetGoalTarget( m_hTargetEnt, vec3_origin ); |
|
} |
|
else if ( GetNavigator()->GetGoalFlags() & AIN_UPDATE_TARGET_POS ) |
|
{ |
|
if ( GetTarget() == NULL || (GetNavigator()->GetGoalPos() - GetTarget()->GetAbsOrigin()).Length() > GetGoalRepathTolerance( GetTarget(), GOALTYPE_TARGETENT, GetNavigator()->GetGoalPos(), GetTarget()->GetAbsOrigin()) ) |
|
{ |
|
if ( !GetNavigator()->RefindPathToGoal( false ) ) |
|
{ |
|
TaskFail( FAIL_NO_ROUTE ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: part of the Condition collection process |
|
// gets and stores data and conditions pertaining to a npc's |
|
// enemy. |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CheckTarget( CBaseEntity *pTarget ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_Enemies_CheckTarget); |
|
|
|
ClearCondition ( COND_HAVE_TARGET_LOS ); |
|
ClearCondition ( COND_TARGET_OCCLUDED ); |
|
|
|
// --------------------------- |
|
// Set visibility conditions |
|
// --------------------------- |
|
if ( ( EnemyDistance( pTarget ) >= GetSenses()->GetDistLook() ) || !FVisible( pTarget ) ) |
|
{ |
|
// No LOS to target |
|
SetCondition( COND_TARGET_OCCLUDED ); |
|
} |
|
else |
|
{ |
|
// Have LOS (may not be in view cone) |
|
SetCondition( COND_HAVE_TARGET_LOS ); |
|
} |
|
|
|
UpdateTargetPos(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Creates a bullseye of limited lifespan at the provided position |
|
// Input : vecOrigin - Where to create the bullseye |
|
// duration - The lifespan of the bullseye |
|
// Output : A BaseNPC pointer to the bullseye |
|
// |
|
// NOTES : It is the caller's responsibility to set up relationships with |
|
// this bullseye! |
|
//----------------------------------------------------------------------------- |
|
CAI_BaseNPC *CAI_BaseNPC::CreateCustomTarget( const Vector &vecOrigin, float duration ) |
|
{ |
|
#ifdef HL2_DLL |
|
CNPC_Bullseye *pTarget = (CNPC_Bullseye*)CreateEntityByName( "npc_bullseye" ); |
|
|
|
ASSERT( pTarget != NULL ); |
|
|
|
// Build a nonsolid bullseye and place it in the desired location |
|
// The bullseye must take damage or the SetHealth 0 call will not be able |
|
pTarget->AddSpawnFlags( SF_BULLSEYE_NONSOLID ); |
|
pTarget->SetAbsOrigin( vecOrigin ); |
|
pTarget->Spawn(); |
|
|
|
// Set it up to remove itself, unless told to be infinite (-1) |
|
if( duration > -1 ) |
|
{ |
|
variant_t value; |
|
value.SetFloat(0); |
|
g_EventQueue.AddEvent( pTarget, "SetHealth", value, duration, this, this ); |
|
} |
|
|
|
return pTarget; |
|
#else |
|
return NULL; |
|
#endif// HL2_DLL |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : eNewActivity - |
|
// Output : Activity |
|
//----------------------------------------------------------------------------- |
|
Activity CAI_BaseNPC::NPC_TranslateActivity( Activity eNewActivity ) |
|
{ |
|
Assert( eNewActivity != ACT_INVALID ); |
|
|
|
if (eNewActivity == ACT_RANGE_ATTACK1) |
|
{ |
|
if ( IsCrouching() ) |
|
{ |
|
eNewActivity = ACT_RANGE_ATTACK1_LOW; |
|
} |
|
} |
|
else if (eNewActivity == ACT_RELOAD) |
|
{ |
|
if (IsCrouching()) |
|
{ |
|
eNewActivity = ACT_RELOAD_LOW; |
|
} |
|
} |
|
else if ( eNewActivity == ACT_IDLE ) |
|
{ |
|
if ( IsCrouching() ) |
|
{ |
|
eNewActivity = ACT_CROUCHIDLE; |
|
} |
|
} |
|
// ==== |
|
// HACK : LEIPZIG 06 - The underlying problem is that the AR2 and SMG1 cannot map IDLE_ANGRY to a crouched equivalent automatically |
|
// which causes the character to pop up and down in their idle state of firing while crouched. -- jdw |
|
else if ( eNewActivity == ACT_IDLE_ANGRY_SMG1 ) |
|
{ |
|
if ( IsCrouching() ) |
|
{ |
|
eNewActivity = ACT_RANGE_AIM_LOW; |
|
} |
|
} |
|
// ==== |
|
|
|
if (CapabilitiesGet() & bits_CAP_DUCK) |
|
{ |
|
if (eNewActivity == ACT_RELOAD) |
|
{ |
|
return GetReloadActivity(GetHintNode()); |
|
} |
|
else if ((eNewActivity == ACT_COVER ) || |
|
(eNewActivity == ACT_IDLE && HasMemory(bits_MEMORY_INCOVER))) |
|
{ |
|
Activity nCoverActivity = GetCoverActivity(GetHintNode()); |
|
// --------------------------------------------------------------- |
|
// Some NPCs don't have a cover activity defined so just use idle |
|
// --------------------------------------------------------------- |
|
if (SelectWeightedSequence( nCoverActivity ) == ACTIVITY_NOT_AVAILABLE) |
|
{ |
|
nCoverActivity = ACT_IDLE; |
|
} |
|
|
|
return nCoverActivity; |
|
} |
|
} |
|
return eNewActivity; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
Activity CAI_BaseNPC::TranslateActivity( Activity idealActivity, Activity *pIdealWeaponActivity ) |
|
{ |
|
const int MAX_TRIES = 5; |
|
int count = 0; |
|
|
|
bool bIdealWeaponRequired = false; |
|
Activity idealWeaponActivity; |
|
Activity baseTranslation; |
|
bool bWeaponRequired = false; |
|
Activity weaponTranslation; |
|
Activity last; |
|
Activity current; |
|
|
|
idealWeaponActivity = Weapon_TranslateActivity( idealActivity, &bIdealWeaponRequired ); |
|
if ( pIdealWeaponActivity ) |
|
*pIdealWeaponActivity = idealWeaponActivity; |
|
|
|
baseTranslation = idealActivity; |
|
weaponTranslation = idealActivity; |
|
last = idealActivity; |
|
while ( count++ < MAX_TRIES ) |
|
{ |
|
current = NPC_TranslateActivity( last ); |
|
if ( current != last ) |
|
baseTranslation = current; |
|
|
|
weaponTranslation = Weapon_TranslateActivity( current, &bWeaponRequired ); |
|
|
|
if ( weaponTranslation == last ) |
|
break; |
|
|
|
last = weaponTranslation; |
|
} |
|
AssertMsg( count < MAX_TRIES, "Circular activity translation!" ); |
|
|
|
if ( last == ACT_SCRIPT_CUSTOM_MOVE ) |
|
return ACT_SCRIPT_CUSTOM_MOVE; |
|
|
|
if ( HaveSequenceForActivity( weaponTranslation ) ) |
|
return weaponTranslation; |
|
|
|
if ( bWeaponRequired ) |
|
{ |
|
// only complain about an activity once |
|
static CUtlVector< Activity > sUniqueActivities; |
|
|
|
if (!sUniqueActivities.Find( weaponTranslation)) |
|
{ |
|
// FIXME: warning |
|
DevWarning( "%s missing activity \"%s\" needed by weapon\"%s\"\n", |
|
GetClassname(), GetActivityName( weaponTranslation ), GetActiveWeapon()->GetClassname() ); |
|
|
|
sUniqueActivities.AddToTail( weaponTranslation ); |
|
} |
|
} |
|
|
|
if ( baseTranslation != weaponTranslation && HaveSequenceForActivity( baseTranslation ) ) |
|
return baseTranslation; |
|
|
|
if ( idealWeaponActivity != baseTranslation && HaveSequenceForActivity( idealWeaponActivity ) ) |
|
return idealActivity; |
|
|
|
if ( idealActivity != idealWeaponActivity && HaveSequenceForActivity( idealActivity ) ) |
|
return idealActivity; |
|
|
|
Assert( !HaveSequenceForActivity( idealActivity ) ); |
|
if ( idealActivity == ACT_RUN ) |
|
{ |
|
idealActivity = ACT_WALK; |
|
} |
|
else if ( idealActivity == ACT_WALK ) |
|
{ |
|
idealActivity = ACT_RUN; |
|
} |
|
|
|
return idealActivity; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : NewActivity - |
|
// iSequence - |
|
// translatedActivity - |
|
// weaponActivity - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ResolveActivityToSequence(Activity NewActivity, int &iSequence, Activity &translatedActivity, Activity &weaponActivity) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_ResolveActivityToSequence ); |
|
|
|
iSequence = ACTIVITY_NOT_AVAILABLE; |
|
|
|
translatedActivity = TranslateActivity( NewActivity, &weaponActivity ); |
|
|
|
if ( NewActivity == ACT_SCRIPT_CUSTOM_MOVE ) |
|
{ |
|
iSequence = GetScriptCustomMoveSequence(); |
|
} |
|
else |
|
{ |
|
iSequence = SelectWeightedSequence( translatedActivity ); |
|
|
|
if ( iSequence == ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
static CAI_BaseNPC *pLastWarn; |
|
static Activity lastWarnActivity; |
|
static float timeLastWarn; |
|
|
|
if ( ( pLastWarn != this && lastWarnActivity != translatedActivity ) || gpGlobals->curtime - timeLastWarn > 5.0 ) |
|
{ |
|
DevWarning( "%s:%s:%s has no sequence for act:%s\n", GetClassname(), GetDebugName(), STRING( GetModelName() ), ActivityList_NameForIndex(translatedActivity) ); |
|
pLastWarn = this; |
|
lastWarnActivity = translatedActivity; |
|
timeLastWarn = gpGlobals->curtime; |
|
} |
|
|
|
if ( translatedActivity == ACT_RUN ) |
|
{ |
|
translatedActivity = ACT_WALK; |
|
iSequence = SelectWeightedSequence( translatedActivity ); |
|
} |
|
} |
|
} |
|
|
|
if ( iSequence == ACT_INVALID ) |
|
{ |
|
// Abject failure. Use sequence zero. |
|
iSequence = 0; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : NewActivity - |
|
// iSequence - |
|
// translatedActivity - |
|
// weaponActivity - |
|
//----------------------------------------------------------------------------- |
|
extern ConVar ai_sequence_debug; |
|
|
|
void CAI_BaseNPC::SetActivityAndSequence(Activity NewActivity, int iSequence, Activity translatedActivity, Activity weaponActivity) |
|
{ |
|
m_translatedActivity = translatedActivity; |
|
|
|
if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) |
|
{ |
|
DevMsg("SetActivityAndSequence : %s: %s:%s -> %s:%s / %s:%s\n", GetClassname(), |
|
GetActivityName(GetActivity()), GetSequenceName(GetSequence()), |
|
GetActivityName(NewActivity), GetSequenceName(iSequence), |
|
GetActivityName(translatedActivity), GetActivityName(weaponActivity) ); |
|
|
|
} |
|
|
|
// Set to the desired anim, or default anim if the desired is not present |
|
if ( iSequence > ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
if ( GetSequence() != iSequence || !SequenceLoops() ) |
|
{ |
|
// |
|
// Don't reset frame between movement phased animations |
|
if (!IsActivityMovementPhased( m_Activity ) || |
|
!IsActivityMovementPhased( NewActivity )) |
|
{ |
|
SetCycle( 0 ); |
|
} |
|
} |
|
|
|
ResetSequence( iSequence ); |
|
Weapon_SetActivity( weaponActivity, SequenceDuration( iSequence ) ); |
|
} |
|
else |
|
{ |
|
// Not available try to get default anim |
|
ResetSequence( 0 ); |
|
} |
|
|
|
// Set the view position based on the current activity |
|
SetViewOffset( EyeOffset(m_translatedActivity) ); |
|
|
|
if (m_Activity != NewActivity) |
|
{ |
|
OnChangeActivity(NewActivity); |
|
} |
|
|
|
// NOTE: We DO NOT write the translated activity here. |
|
// This is to abstract the activity translation from the AI code. |
|
// As far as the code is concerned, a translation is merely a new set of sequences |
|
// that should be regarded as the activity in question. |
|
|
|
// Go ahead and set this so it doesn't keep trying when the anim is not present |
|
m_Activity = NewActivity; |
|
|
|
// this cannot be called until m_Activity stores NewActivity! |
|
GetMotor()->RecalculateYawSpeed(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Sets the activity to the desired activity immediately, skipping any |
|
// transition sequences. |
|
// Input : NewActivity - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetActivity( Activity NewActivity ) |
|
{ |
|
// If I'm already doing the NewActivity I can bail. |
|
// FIXME: Should this be based on the current translated activity and ideal translated activity (calculated below)? |
|
// The old code only cared about the logical activity, not translated. |
|
|
|
if (m_Activity == NewActivity) |
|
{ |
|
return; |
|
} |
|
|
|
// Don't do this if I'm playing a transition, unless it's ACT_RESET. |
|
if ( NewActivity != ACT_RESET && m_Activity == ACT_TRANSITION && m_IdealActivity != ACT_DO_NOT_DISTURB ) |
|
{ |
|
return; |
|
} |
|
|
|
if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) |
|
{ |
|
DevMsg("SetActivity : %s: %s -> %s\n", GetClassname(), GetActivityName(GetActivity()), GetActivityName(NewActivity)); |
|
} |
|
|
|
if ( !GetModelPtr() ) |
|
return; |
|
|
|
// In case someone calls this with something other than the ideal activity. |
|
m_IdealActivity = NewActivity; |
|
|
|
// Resolve to ideals and apply directly, skipping transitions. |
|
ResolveActivityToSequence(m_IdealActivity, m_nIdealSequence, m_IdealTranslatedActivity, m_IdealWeaponActivity); |
|
|
|
//DevMsg("%s: SLAM %s -> %s\n", GetClassname(), GetSequenceName(GetSequence()), GetSequenceName(m_nIdealSequence)); |
|
|
|
SetActivityAndSequence(m_IdealActivity, m_nIdealSequence, m_IdealTranslatedActivity, m_IdealWeaponActivity); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Sets the activity that we would like to transition toward. |
|
// Input : NewActivity - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetIdealActivity( Activity NewActivity ) |
|
{ |
|
// ignore if it's an ACT_TRANSITION, it means somewhere we're setting IdealActivity with a bogus intermediate value |
|
if (NewActivity == ACT_TRANSITION) |
|
{ |
|
Assert( 0 ); |
|
return; |
|
} |
|
|
|
if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) |
|
{ |
|
DevMsg("SetIdealActivity : %s: %s -> %s\n", GetClassname(), GetActivityName(GetActivity()), GetActivityName(NewActivity)); |
|
} |
|
|
|
|
|
if (NewActivity == ACT_RESET) |
|
{ |
|
// They probably meant to call SetActivity(ACT_RESET)... we'll fix it for them. |
|
SetActivity(ACT_RESET); |
|
return; |
|
} |
|
|
|
m_IdealActivity = NewActivity; |
|
|
|
if( NewActivity == ACT_DO_NOT_DISTURB ) |
|
{ |
|
// Don't resolve anything! Leave it the way the user has it right now. |
|
return; |
|
} |
|
|
|
if ( !GetModelPtr() ) |
|
return; |
|
|
|
// Perform translation in case we need to change sequences within a single activity, |
|
// such as between a standing idle and a crouching idle. |
|
ResolveActivityToSequence(m_IdealActivity, m_nIdealSequence, m_IdealTranslatedActivity, m_IdealWeaponActivity); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Moves toward the ideal activity through any transition sequences. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AdvanceToIdealActivity(void) |
|
{ |
|
// If there is a transition sequence between the current sequence and the ideal sequence... |
|
int nNextSequence = FindTransitionSequence(GetSequence(), m_nIdealSequence, NULL); |
|
if (nNextSequence != -1) |
|
{ |
|
// We found a transition sequence or possibly went straight to |
|
// the ideal sequence. |
|
if (nNextSequence != m_nIdealSequence) |
|
{ |
|
// DevMsg("%s: TRANSITION %s -> %s -> %s\n", GetClassname(), GetSequenceName(GetSequence()), GetSequenceName(nNextSequence), GetSequenceName(m_nIdealSequence)); |
|
|
|
Activity eWeaponActivity = ACT_TRANSITION; |
|
Activity eTranslatedActivity = ACT_TRANSITION; |
|
|
|
// Figure out if the transition sequence has an associated activity that |
|
// we can use for our weapon. Do activity translation also. |
|
Activity eTransitionActivity = GetSequenceActivity(nNextSequence); |
|
if (eTransitionActivity != ACT_INVALID) |
|
{ |
|
int nDiscard; |
|
ResolveActivityToSequence(eTransitionActivity, nDiscard, eTranslatedActivity, eWeaponActivity); |
|
} |
|
|
|
// Set activity and sequence to the transition stuff. Set the activity to ACT_TRANSITION |
|
// so we know we're in a transition. |
|
SetActivityAndSequence(ACT_TRANSITION, nNextSequence, eTranslatedActivity, eWeaponActivity); |
|
} |
|
else |
|
{ |
|
//DevMsg("%s: IDEAL %s -> %s\n", GetClassname(), GetSequenceName(GetSequence()), GetSequenceName(m_nIdealSequence)); |
|
|
|
// Set activity and sequence to the ideal stuff that was set up in MaintainActivity. |
|
SetActivityAndSequence(m_IdealActivity, m_nIdealSequence, m_IdealTranslatedActivity, m_IdealWeaponActivity); |
|
} |
|
} |
|
// Else go straight there to the ideal activity. |
|
else |
|
{ |
|
//DevMsg("%s: Unable to get from sequence %s to %s!\n", GetClassname(), GetSequenceName(GetSequence()), GetSequenceName(m_nIdealSequence)); |
|
SetActivity(m_IdealActivity); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Tries to achieve our ideal animation state, playing any transition |
|
// sequences that we need to play to get there. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::MaintainActivity(void) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_MaintainActivity ); |
|
|
|
if ( m_lifeState == LIFE_DEAD ) |
|
{ |
|
// Don't maintain activities if we're daid. |
|
// Blame Speyrer |
|
return; |
|
} |
|
|
|
if ((GetState() == NPC_STATE_SCRIPT)) |
|
{ |
|
// HACK: finish any transitions we might be playing before we yield control to the script |
|
if (GetActivity() != ACT_TRANSITION) |
|
{ |
|
// Our animation state is being controlled by a script. |
|
return; |
|
} |
|
} |
|
|
|
if( m_IdealActivity == ACT_DO_NOT_DISTURB || !GetModelPtr() ) |
|
{ |
|
return; |
|
} |
|
|
|
// We may have work to do if we aren't playing our ideal activity OR if we |
|
// aren't playing our ideal sequence. |
|
if ((GetActivity() != m_IdealActivity) || (GetSequence() != m_nIdealSequence)) |
|
{ |
|
if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) |
|
{ |
|
DevMsg("MaintainActivity %s : %s:%s -> %s:%s\n", GetClassname(), |
|
GetActivityName(GetActivity()), GetSequenceName(GetSequence()), |
|
GetActivityName(m_IdealActivity), GetSequenceName(m_nIdealSequence)); |
|
} |
|
|
|
bool bAdvance = false; |
|
|
|
// If we're in a transition activity, see if we are done with the transition. |
|
if (GetActivity() == ACT_TRANSITION) |
|
{ |
|
// If the current sequence is finished, try to go to the next one |
|
// closer to our ideal sequence. |
|
if (IsSequenceFinished()) |
|
{ |
|
bAdvance = true; |
|
} |
|
// Else a transition sequence is in progress, do nothing. |
|
} |
|
// Else get a specific sequence for the activity and try to transition to that. |
|
else |
|
{ |
|
// Save off a target sequence and translated activities to apply when we finish |
|
// playing all the transitions and finally arrive at our ideal activity. |
|
ResolveActivityToSequence(m_IdealActivity, m_nIdealSequence, m_IdealTranslatedActivity, m_IdealWeaponActivity); |
|
bAdvance = true; |
|
} |
|
|
|
if (bAdvance) |
|
{ |
|
// Try to go to the next sequence closer to our ideal sequence. |
|
AdvanceToIdealActivity(); |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true if our ideal activity has finished playing. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsActivityFinished( void ) |
|
{ |
|
return (IsSequenceFinished() && (GetSequence() == m_nIdealSequence)); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks to see if the activity is one of the standard phase-matched movement activities |
|
// Input : activity |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsActivityMovementPhased( Activity activity ) |
|
{ |
|
switch( activity ) |
|
{ |
|
case ACT_WALK: |
|
case ACT_WALK_AIM: |
|
case ACT_WALK_CROUCH: |
|
case ACT_WALK_CROUCH_AIM: |
|
case ACT_RUN: |
|
case ACT_RUN_AIM: |
|
case ACT_RUN_CROUCH: |
|
case ACT_RUN_CROUCH_AIM: |
|
case ACT_RUN_PROTECTED: |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnChangeActivity( Activity eNewActivity ) |
|
{ |
|
if ( eNewActivity == ACT_RUN || |
|
eNewActivity == ACT_RUN_AIM || |
|
eNewActivity == ACT_WALK ) |
|
{ |
|
Stand(); |
|
} |
|
} |
|
|
|
//========================================================= |
|
// SetSequenceByName |
|
//========================================================= |
|
void CAI_BaseNPC::SetSequenceByName( const char *szSequence ) |
|
{ |
|
int iSequence = LookupSequence( szSequence ); |
|
|
|
if ( iSequence > ACTIVITY_NOT_AVAILABLE ) |
|
SetSequenceById( iSequence ); |
|
else |
|
{ |
|
DevWarning( 2, "%s has no sequence %s to match request\n", GetClassname(), szSequence ); |
|
SetSequence( 0 ); // Set to the reset anim (if it's there) |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::SetSequenceById( int iSequence ) |
|
{ |
|
// Set to the desired anim, or default anim if the desired is not present |
|
if ( iSequence > ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
if ( GetSequence() != iSequence || !SequenceLoops() ) |
|
{ |
|
SetCycle( 0 ); |
|
} |
|
|
|
ResetSequence( iSequence ); // Set to the reset anim (if it's there) |
|
GetMotor()->RecalculateYawSpeed(); |
|
} |
|
else |
|
{ |
|
// Not available try to get default anim |
|
DevWarning( 2, "%s invalid sequence requested\n", GetClassname() ); |
|
SetSequence( 0 ); // Set to the reset anim (if it's there) |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns the target entity |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CBaseEntity *CAI_BaseNPC::GetNavTargetEntity(void) |
|
{ |
|
if ( GetNavigator()->GetGoalType() == GOALTYPE_ENEMY ) |
|
return m_hEnemy; |
|
else if ( GetNavigator()->GetGoalType() == GOALTYPE_TARGETENT ) |
|
return m_hTargetEnt; |
|
return NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: returns zero if the caller can jump from |
|
// vecStart to vecEnd ignoring collisions with pTarget |
|
// |
|
// if the throw fails, returns the distance |
|
// that can be travelled before an obstacle is hit |
|
//----------------------------------------------------------------------------- |
|
#include "ai_initutils.h" |
|
//#define _THROWDEBUG |
|
float CAI_BaseNPC::ThrowLimit( const Vector &vecStart, |
|
const Vector &vecEnd, |
|
float fGravity, |
|
float fArcSize, |
|
const Vector &mins, |
|
const Vector &maxs, |
|
CBaseEntity *pTarget, |
|
Vector *jumpVel, |
|
CBaseEntity **pBlocker) |
|
{ |
|
// Get my jump velocity |
|
Vector rawJumpVel = CalcThrowVelocity(vecStart, vecEnd, fGravity, fArcSize); |
|
*jumpVel = rawJumpVel; |
|
Vector vecFrom = vecStart; |
|
|
|
// Calculate the total time of the jump minus a tiny fraction |
|
float jumpTime = (vecStart - vecEnd).Length2D()/rawJumpVel.Length2D(); |
|
float timeStep = jumpTime / 10.0; |
|
|
|
Vector gravity = Vector(0,0,fGravity); |
|
|
|
// this loop takes single steps to the goal. |
|
for (float flTime = 0 ; flTime < jumpTime-0.1 ; flTime += timeStep ) |
|
{ |
|
// Calculate my position after the time step (average velocity over this time step) |
|
Vector nextPos = vecFrom + (rawJumpVel - 0.5 * gravity * timeStep) * timeStep; |
|
|
|
// If last time step make next position the target position |
|
if ((flTime + timeStep) > jumpTime) |
|
{ |
|
nextPos = vecEnd; |
|
} |
|
|
|
trace_t tr; |
|
AI_TraceHull( vecFrom, nextPos, mins, maxs, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if (tr.startsolid || tr.fraction < 1.0) |
|
{ |
|
CBaseEntity *pEntity = tr.m_pEnt; |
|
|
|
// If we hit the target we are good to go! |
|
if (pEntity == pTarget) |
|
{ |
|
return 0; |
|
} |
|
|
|
#ifdef _THROWDEBUG |
|
NDebugOverlay::Line( vecFrom, nextPos, 255, 0, 0, true, 1.0 ); |
|
#endif |
|
// ---------------------------------------------------------- |
|
// If blocked by an npc remember |
|
// ---------------------------------------------------------- |
|
*pBlocker = pEntity; |
|
|
|
// Return distance sucessfully traveled before block encountered |
|
return ((tr.endpos - vecStart).Length()); |
|
} |
|
#ifdef _THROWDEBUG |
|
else |
|
{ |
|
NDebugOverlay::Line( vecFrom, nextPos, 255, 255, 255, true, 1.0 ); |
|
} |
|
#endif |
|
|
|
|
|
rawJumpVel = rawJumpVel - gravity * timeStep; |
|
vecFrom = nextPos; |
|
} |
|
return 0; |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called to initialize or re-initialize the vphysics hull when the size |
|
// of the NPC changes |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetupVPhysicsHull() |
|
{ |
|
if ( GetMoveType() == MOVETYPE_VPHYSICS || GetMoveType() == MOVETYPE_NONE ) |
|
return; |
|
|
|
if ( VPhysicsGetObject() ) |
|
{ |
|
// Disable collisions to get |
|
VPhysicsGetObject()->EnableCollisions(false); |
|
VPhysicsDestroyObject(); |
|
} |
|
VPhysicsInitShadow( true, false ); |
|
IPhysicsObject *pPhysObj = VPhysicsGetObject(); |
|
if ( pPhysObj ) |
|
{ |
|
float mass = Studio_GetMass(GetModelPtr()); |
|
if ( mass > 0 ) |
|
{ |
|
pPhysObj->SetMass( mass ); |
|
} |
|
#if _DEBUG |
|
else |
|
{ |
|
DevMsg("Warning: %s has no physical mass\n", STRING(GetModelName())); |
|
} |
|
#endif |
|
IPhysicsShadowController *pController = pPhysObj->GetShadowController(); |
|
float avgsize = (WorldAlignSize().x + WorldAlignSize().y) * 0.5; |
|
//pController->SetTeleportDistance( avgsize * 0.5 ); |
|
m_bCheckContacts = true; |
|
} |
|
} |
|
|
|
|
|
// Check for problematic physics objects resting on this NPC. |
|
// They can screw up his navigation, so attach a controller to |
|
// help separate the NPC & physics when you encounter these. |
|
ConVar ai_auto_contact_solver( "ai_auto_contact_solver", "1" ); |
|
void CAI_BaseNPC::CheckPhysicsContacts() |
|
{ |
|
return; |
|
|
|
if ( gpGlobals->frametime <= 0.0f || !ai_auto_contact_solver.GetBool() ) |
|
return; |
|
|
|
m_bCheckContacts = false; |
|
if ( GetMoveType() == MOVETYPE_STEP && VPhysicsGetObject()) |
|
{ |
|
IPhysicsObject *pPhysics = VPhysicsGetObject(); |
|
IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); |
|
CBaseEntity *pGroundEntity = GetGroundEntity(); |
|
float heightCheck = GetAbsOrigin().z + GetHullMaxs().z; |
|
Vector npcVel; |
|
pPhysics->GetVelocity( &npcVel, NULL ); |
|
CBaseEntity *pOtherEntity = NULL; |
|
bool createSolver = false; |
|
float solverTime = 0.0f; |
|
while ( pSnapshot->IsValid() ) |
|
{ |
|
IPhysicsObject *pOther = pSnapshot->GetObject(1); |
|
if( !pOther ) |
|
continue; |
|
pOtherEntity = static_cast<CBaseEntity *>(pOther->GetGameData()); |
|
|
|
if ( pOtherEntity && pGroundEntity != pOtherEntity ) |
|
{ |
|
float otherMass = PhysGetEntityMass(pOtherEntity); |
|
|
|
if ( pOtherEntity->GetMoveType() == MOVETYPE_VPHYSICS && pOther->IsMoveable() && |
|
otherMass < VPHYSICS_LARGE_OBJECT_MASS && !pOtherEntity->GetServerVehicle() ) |
|
{ |
|
m_bCheckContacts = true; |
|
Vector vel, point; |
|
pOther->GetVelocity( &vel, NULL ); |
|
pSnapshot->GetContactPoint( point ); |
|
|
|
// compare the relative velocity |
|
vel -= npcVel; |
|
|
|
// slow moving object probably won't clear itself. |
|
// Either set ignore, or disable collisions entirely |
|
if ( vel.LengthSqr() < 5.0f*5.0f ) |
|
{ |
|
float topdist = fabs(point.z-heightCheck); |
|
// 4 seconds to ignore this for nav |
|
solverTime = 4.0f; |
|
if ( topdist < 2.0f ) |
|
{ |
|
// Resting on my head so disable collisions for a bit |
|
solverTime = 0.5f; // UNDONE: Tune |
|
if ( pOther->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) |
|
{ |
|
// player is being a monkey |
|
solverTime = 0.25f; |
|
} |
|
|
|
//Msg("Dropping %s from %s\n", pOtherEntity->GetClassname(), GetClassname() ); |
|
Assert( !NPCPhysics_SolverExists(this, pOtherEntity) ); |
|
createSolver = true; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
pSnapshot->NextFrictionData(); |
|
} |
|
pPhysics->DestroyFrictionSnapshot( pSnapshot ); |
|
if ( createSolver ) |
|
{ |
|
// turn collisions back on once we've been separated for enough time |
|
NPCPhysics_CreateSolver( this, pOtherEntity, true, solverTime ); |
|
pPhysics->RecheckContactPoints(); |
|
} |
|
} |
|
} |
|
|
|
void CAI_BaseNPC::StartTouch( CBaseEntity *pOther ) |
|
{ |
|
BaseClass::StartTouch(pOther); |
|
|
|
if ( pOther->GetMoveType() == MOVETYPE_VPHYSICS ) |
|
{ |
|
m_bCheckContacts = true; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: To be called instead of UTIL_SetSize, so pathfinding hull |
|
// and actual hull agree |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetHullSizeNormal( bool force ) |
|
{ |
|
if ( m_fIsUsingSmallHull || force ) |
|
{ |
|
// Find out what the height difference will be between the versions and adjust our bbox accordingly to keep us level |
|
const float flScale = GetModelScale(); |
|
Vector vecMins = ( GetHullMins() * flScale ); |
|
Vector vecMaxs = ( GetHullMaxs() * flScale ); |
|
|
|
UTIL_SetSize( this, vecMins, vecMaxs ); |
|
|
|
m_fIsUsingSmallHull = false; |
|
if ( VPhysicsGetObject() ) |
|
{ |
|
SetupVPhysicsHull(); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: To be called instead of UTIL_SetSize, so pathfinding hull |
|
// and actual hull agree |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::SetHullSizeSmall( bool force ) |
|
{ |
|
if ( !m_fIsUsingSmallHull || force ) |
|
{ |
|
UTIL_SetSize(this, NAI_Hull::SmallMins(GetHullType()),NAI_Hull::SmallMaxs(GetHullType())); |
|
m_fIsUsingSmallHull = true; |
|
if ( VPhysicsGetObject() ) |
|
{ |
|
SetupVPhysicsHull(); |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Checks to see that the nav hull is valid for the NPC |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsNavHullValid() const |
|
{ |
|
Assert( GetSolid() != SOLID_BSP ); |
|
|
|
Vector hullMin = GetHullMins(); |
|
Vector hullMax = GetHullMaxs(); |
|
Vector vecMins, vecMaxs; |
|
if ( GetSolid() == SOLID_BBOX ) |
|
{ |
|
vecMins = WorldAlignMins(); |
|
vecMaxs = WorldAlignMaxs(); |
|
} |
|
else if ( GetSolid() == SOLID_VPHYSICS ) |
|
{ |
|
Assert( VPhysicsGetObject() ); |
|
const CPhysCollide *pPhysCollide = VPhysicsGetObject()->GetCollide(); |
|
physcollision->CollideGetAABB( &vecMins, &vecMaxs, pPhysCollide, GetAbsOrigin(), GetAbsAngles() ); |
|
vecMins -= GetAbsOrigin(); |
|
vecMaxs -= GetAbsOrigin(); |
|
} |
|
else |
|
{ |
|
vecMins = hullMin; |
|
vecMaxs = hullMax; |
|
} |
|
|
|
if ( (hullMin.x > vecMins.x) || (hullMax.x < vecMaxs.x) || |
|
(hullMin.y > vecMins.y) || (hullMax.y < vecMaxs.y) || |
|
(hullMin.z > vecMins.z) || (hullMax.z < vecMaxs.z) ) |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//========================================================= |
|
// NPCInit - after a npc is spawned, it needs to |
|
// be dropped into the world, checked for mobility problems, |
|
// and put on the proper path, if any. This function does |
|
// all of those things after the npc spawns. Any |
|
// initialization that should take place for all npcs |
|
// goes here. |
|
//========================================================= |
|
void CAI_BaseNPC::NPCInit ( void ) |
|
{ |
|
if (!g_pGameRules->FAllowNPCs()) |
|
{ |
|
UTIL_Remove( this ); |
|
return; |
|
} |
|
|
|
if( IsWaitingToRappel() ) |
|
{ |
|
// If this guy's supposed to rappel, keep him from |
|
// falling to the ground when he spawns. |
|
AddFlag( FL_FLY ); |
|
} |
|
|
|
#ifdef _DEBUG |
|
// Make sure that the bounding box is appropriate for the hull size... |
|
// FIXME: We can't test vphysics objects because NPCInit occurs before VPhysics is set up |
|
if ( GetSolid() != SOLID_VPHYSICS && !IsSolidFlagSet(FSOLID_NOT_SOLID) ) |
|
{ |
|
if ( !IsNavHullValid() ) |
|
{ |
|
Warning("NPC Entity %s (%d) has a bounding box which extends outside its nav box!\n", |
|
STRING(m_iClassname), entindex() ); |
|
} |
|
} |
|
#endif |
|
|
|
// Set fields common to all npcs |
|
AddFlag( FL_AIMTARGET | FL_NPC ); |
|
AddSolidFlags( FSOLID_NOT_STANDABLE ); |
|
|
|
m_flOriginalYaw = GetAbsAngles().y; |
|
|
|
SetBlocksLOS( false ); |
|
|
|
SetGravity(1.0); // Don't change |
|
m_takedamage = DAMAGE_YES; |
|
GetMotor()->SetIdealYaw( GetLocalAngles().y ); |
|
m_iMaxHealth = m_iHealth; |
|
m_lifeState = LIFE_ALIVE; |
|
SetIdealState( NPC_STATE_IDLE );// Assume npc will be idle, until proven otherwise |
|
SetIdealActivity( ACT_IDLE ); |
|
SetActivity( ACT_IDLE ); |
|
|
|
#ifdef HL1_DLL |
|
SetDeathPose( ACT_INVALID ); |
|
#endif |
|
|
|
ClearCommandGoal(); |
|
|
|
ClearSchedule( "Initializing NPC" ); |
|
GetNavigator()->ClearGoal(); |
|
InitBoneControllers( ); // FIX: should be done in Spawn |
|
if ( GetModelPtr() ) |
|
{ |
|
ResetActivityIndexes(); |
|
ResetEventIndexes(); |
|
} |
|
|
|
SetHintNode( NULL ); |
|
|
|
m_afMemory = MEMORY_CLEAR; |
|
|
|
SetEnemy( NULL ); |
|
|
|
m_flDistTooFar = 1024.0; |
|
SetDistLook( 2048.0 ); |
|
|
|
if ( HasSpawnFlags( SF_NPC_LONG_RANGE ) ) |
|
{ |
|
m_flDistTooFar = 1e9f; |
|
SetDistLook( 6000.0 ); |
|
} |
|
|
|
// Clear conditions |
|
m_Conditions.ClearAll(); |
|
|
|
// set eye position |
|
SetDefaultEyeOffset(); |
|
|
|
// Only give weapon of allowed to have one |
|
if (CapabilitiesGet() & bits_CAP_USE_WEAPONS) |
|
{ // Does this npc spawn with a weapon |
|
if ( m_spawnEquipment != NULL_STRING && strcmp(STRING(m_spawnEquipment), "0")) |
|
{ |
|
CBaseCombatWeapon *pWeapon = Weapon_Create( STRING(m_spawnEquipment) ); |
|
if ( pWeapon ) |
|
{ |
|
// If I have a name, make my weapon match it with "_weapon" appended |
|
if ( GetEntityName() != NULL_STRING ) |
|
{ |
|
pWeapon->SetName( AllocPooledString(UTIL_VarArgs("%s_weapon", STRING(GetEntityName()))) ); |
|
} |
|
|
|
if ( GetEffects() & EF_NOSHADOW ) |
|
{ |
|
// BUGBUG: if this NPC drops this weapon it will forevermore have no shadow |
|
pWeapon->AddEffects( EF_NOSHADOW ); |
|
} |
|
|
|
Weapon_Equip( pWeapon ); |
|
} |
|
} |
|
} |
|
|
|
// Robin: Removed this, since it stomps the weapon's settings, and it's stomped |
|
// by OnUpdateShotRegulator() as soon as they finish firing the first time. |
|
//GetShotRegulator()->SetParameters( 2, 6, 0.3f, 0.8f ); |
|
|
|
SetUse ( &CAI_BaseNPC::NPCUse ); |
|
|
|
// NOTE: Can't call NPC Init Think directly... logic changed about |
|
// what time it is when worldspawn happens.. |
|
|
|
// We must put off the rest of our initialization |
|
// until we're sure everything else has had a chance to spawn. Otherwise |
|
// we may try to reference entities that haven't spawned yet.(sjb) |
|
SetThink( &CAI_BaseNPC::NPCInitThink ); |
|
SetNextThink( gpGlobals->curtime + 0.01f ); |
|
|
|
ForceGatherConditions(); |
|
|
|
// HACKHACK: set up a pre idle animation |
|
// NOTE: Must do this before CreateVPhysics() so bone followers have the correct initial positions. |
|
if ( HasSpawnFlags( SF_NPC_WAIT_FOR_SCRIPT ) ) |
|
{ |
|
const char *pStartSequence = CAI_ScriptedSequence::GetSpawnPreIdleSequenceForScript( this ); |
|
if ( pStartSequence ) |
|
{ |
|
SetSequence( LookupSequence( pStartSequence ) ); |
|
} |
|
} |
|
|
|
CreateVPhysics(); |
|
|
|
if ( HasSpawnFlags( SF_NPC_START_EFFICIENT ) ) |
|
{ |
|
SetEfficiency( AIE_EFFICIENT ); |
|
} |
|
|
|
m_bFadeCorpse = ShouldFadeOnDeath(); |
|
|
|
m_GiveUpOnDeadEnemyTimer.Set( 0.75, 2.0 ); |
|
|
|
m_flTimeLastMovement = FLT_MAX; |
|
|
|
m_flIgnoreDangerSoundsUntil = 0; |
|
|
|
SetDeathPose( ACT_INVALID ); |
|
SetDeathPoseFrame( 0 ); |
|
|
|
m_EnemiesSerialNumber = -1; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::CreateVPhysics() |
|
{ |
|
if ( IsAlive() && !VPhysicsGetObject() ) |
|
{ |
|
SetupVPhysicsHull(); |
|
} |
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Set up the shot regulator based on the equipped weapon |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnUpdateShotRegulator( ) |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
if ( !pWeapon ) |
|
return; |
|
|
|
// Default values |
|
m_ShotRegulator.SetBurstInterval( pWeapon->GetFireRate(), pWeapon->GetFireRate() ); |
|
m_ShotRegulator.SetBurstShotCountRange( pWeapon->GetMinBurst(), pWeapon->GetMaxBurst() ); |
|
m_ShotRegulator.SetRestInterval( pWeapon->GetMinRestTime(), pWeapon->GetMaxRestTime() ); |
|
|
|
// Let the behavior have a whack at it. |
|
if ( GetRunningBehavior() ) |
|
{ |
|
GetRunningBehavior()->OnUpdateShotRegulator(); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Set up the shot regulator based on the equipped weapon |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnChangeActiveWeapon( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) |
|
{ |
|
BaseClass::OnChangeActiveWeapon( pOldWeapon, pNewWeapon ); |
|
|
|
// Shot regulator code |
|
if ( pNewWeapon ) |
|
{ |
|
OnUpdateShotRegulator(); |
|
m_ShotRegulator.Reset( true ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Tests to see if NPC can holster their weapon (if animation exists to holster weapon) |
|
// Output : true if holster weapon animation exists |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::CanHolsterWeapon( void ) |
|
{ |
|
int seq = SelectWeightedSequence( ACT_DISARM ); |
|
return (seq >= 0); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::HolsterWeapon( void ) |
|
{ |
|
if ( IsWeaponHolstered() ) |
|
return -1; |
|
|
|
int iHolsterGesture = FindGestureLayer( ACT_DISARM ); |
|
if ( iHolsterGesture != -1 ) |
|
return iHolsterGesture; |
|
|
|
int iLayer = AddGesture( ACT_DISARM, true ); |
|
//iLayer = AddGesture( ACT_GESTURE_DISARM, true ); |
|
|
|
if (iLayer != -1) |
|
{ |
|
// Prevent firing during the holster / unholster |
|
float flDuration = GetLayerDuration( iLayer ); |
|
m_ShotRegulator.FireNoEarlierThan( gpGlobals->curtime + flDuration + 0.5 ); |
|
|
|
if( m_iDesiredWeaponState == DESIREDWEAPONSTATE_HOLSTERED_DESTROYED ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_CHANGING_DESTROY; |
|
} |
|
else |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_CHANGING; |
|
} |
|
|
|
// Make sure we don't try to reload while we're holstering |
|
ClearCondition(COND_LOW_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_SECONDARY_AMMO); |
|
} |
|
|
|
return iLayer; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::UnholsterWeapon( void ) |
|
{ |
|
if ( !IsWeaponHolstered() ) |
|
return -1; |
|
|
|
int iHolsterGesture = FindGestureLayer( ACT_ARM ); |
|
if ( iHolsterGesture != -1 ) |
|
return iHolsterGesture; |
|
|
|
// Deploy the first weapon you can find |
|
for (int i = 0; i < WeaponCount(); i++) |
|
{ |
|
if ( GetWeapon( i )) |
|
{ |
|
SetActiveWeapon( GetWeapon(i) ); |
|
|
|
int iLayer = AddGesture( ACT_ARM, true ); |
|
//iLayer = AddGesture( ACT_GESTURE_ARM, true ); |
|
|
|
if (iLayer != -1) |
|
{ |
|
// Prevent firing during the holster / unholster |
|
float flDuration = GetLayerDuration( iLayer ); |
|
m_ShotRegulator.FireNoEarlierThan( gpGlobals->curtime + flDuration + 0.5 ); |
|
|
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_CHANGING; |
|
} |
|
|
|
// Refill the clip |
|
if ( GetActiveWeapon()->UsesClipsForAmmo1() ) |
|
{ |
|
GetActiveWeapon()->m_iClip1 = GetActiveWeapon()->GetMaxClip1(); |
|
} |
|
|
|
// Make sure we don't try to reload while we're unholstering |
|
ClearCondition(COND_LOW_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_SECONDARY_AMMO); |
|
|
|
return iLayer; |
|
} |
|
} |
|
|
|
return -1; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputHolsterWeapon( inputdata_t &inputdata ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_HOLSTERED; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputHolsterAndDestroyWeapon( inputdata_t &inputdata ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_HOLSTERED_DESTROYED; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputUnholsterWeapon( inputdata_t &inputdata ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_UNHOLSTERED; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsWeaponHolstered( void ) |
|
{ |
|
if( !GetActiveWeapon() ) |
|
return true; |
|
|
|
if( GetActiveWeapon()->IsEffectActive(EF_NODRAW) ) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsWeaponStateChanging( void ) |
|
{ |
|
return ( m_iDesiredWeaponState == DESIREDWEAPONSTATE_CHANGING || m_iDesiredWeaponState == DESIREDWEAPONSTATE_CHANGING_DESTROY ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Set up the shot regulator based on the equipped weapon |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnRangeAttack1() |
|
{ |
|
SetLastAttackTime( gpGlobals->curtime ); |
|
|
|
// Houston, there is a problem! |
|
AssertOnce( GetShotRegulator()->ShouldShoot() ); |
|
|
|
m_ShotRegulator.OnFiredWeapon(); |
|
if ( m_ShotRegulator.IsInRestInterval() ) |
|
{ |
|
OnUpdateShotRegulator(); |
|
} |
|
|
|
SetNextAttack( m_ShotRegulator.NextShotTime() ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Initialze the relationship table from the keyvalues |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InitRelationshipTable(void) |
|
{ |
|
AddRelationship( STRING( m_RelationshipString ), NULL ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AddRelationship( const char *pszRelationship, CBaseEntity *pActivator ) |
|
{ |
|
// Parse the keyvalue data |
|
char parseString[1000]; |
|
Q_strncpy(parseString, pszRelationship, sizeof(parseString)); |
|
|
|
// Look for an entity string |
|
char *entityString = strtok(parseString," "); |
|
while (entityString) |
|
{ |
|
// Get the disposition |
|
char *dispositionString = strtok(NULL," "); |
|
Disposition_t disposition = D_NU; |
|
if ( dispositionString ) |
|
{ |
|
if (!stricmp(dispositionString,"D_HT")) |
|
{ |
|
disposition = D_HT; |
|
} |
|
else if (!stricmp(dispositionString,"D_FR")) |
|
{ |
|
disposition = D_FR; |
|
} |
|
else if (!stricmp(dispositionString,"D_LI")) |
|
{ |
|
disposition = D_LI; |
|
} |
|
else if (!stricmp(dispositionString,"D_NU")) |
|
{ |
|
disposition = D_NU; |
|
} |
|
else |
|
{ |
|
disposition = D_NU; |
|
Warning( "***ERROR***\nBad relationship type (%s) to unknown entity (%s)!\n", dispositionString,entityString ); |
|
Assert( 0 ); |
|
return; |
|
} |
|
} |
|
else |
|
{ |
|
Warning("Can't parse relationship info (%s) - Expecting 'name [D_HT, D_FR, D_LI, D_NU] [1-99]'\n", pszRelationship ); |
|
Assert(0); |
|
return; |
|
} |
|
|
|
// Get the priority |
|
char *priorityString = strtok(NULL," "); |
|
int priority = ( priorityString ) ? atoi(priorityString) : DEF_RELATIONSHIP_PRIORITY; |
|
|
|
bool bFoundEntity = false; |
|
|
|
// Try to get pointer to an entity of this name |
|
CBaseEntity *entity = gEntList.FindEntityByName( NULL, entityString ); |
|
while( entity ) |
|
{ |
|
// make sure you catch all entities of this name. |
|
bFoundEntity = true; |
|
AddEntityRelationship(entity, disposition, priority ); |
|
entity = gEntList.FindEntityByName( entity, entityString ); |
|
} |
|
|
|
if( !bFoundEntity ) |
|
{ |
|
// Need special condition for player as we can only have one |
|
if (!stricmp("player", entityString) || !stricmp("!player", entityString)) |
|
{ |
|
AddClassRelationship( CLASS_PLAYER, disposition, priority ); |
|
} |
|
// Otherwise try to create one too see if a valid classname and get class type |
|
else |
|
{ |
|
// HACKHACK: |
|
CBaseEntity *pEntity = CanCreateEntityClass( entityString ) ? CreateEntityByName( entityString ) : NULL; |
|
if (pEntity) |
|
{ |
|
AddClassRelationship( pEntity->Classify(), disposition, priority ); |
|
UTIL_RemoveImmediate(pEntity); |
|
} |
|
else |
|
{ |
|
DevWarning( "Couldn't set relationship to unknown entity or class (%s)!\n", entityString ); |
|
} |
|
} |
|
} |
|
// Check for another entity in the list |
|
entityString = strtok(NULL," "); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AddEntityRelationship( CBaseEntity *pEntity, Disposition_t nDisposition, int nPriority ) |
|
{ |
|
#if 0 |
|
ForceGatherConditions(); |
|
#endif |
|
BaseClass::AddEntityRelationship( pEntity, nDisposition, nPriority ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AddClassRelationship( Class_T nClass, Disposition_t nDisposition, int nPriority ) |
|
{ |
|
#if 0 |
|
ForceGatherConditions(); |
|
#endif |
|
BaseClass::AddClassRelationship( nClass, nDisposition, nPriority ); |
|
} |
|
|
|
//========================================================= |
|
// NPCInitThink - Calls StartNPC. Startnpc is |
|
// virtual, but this function cannot be |
|
//========================================================= |
|
void CAI_BaseNPC::NPCInitThink ( void ) |
|
{ |
|
// Initialize the relationship table |
|
InitRelationshipTable(); |
|
|
|
StartNPC(); |
|
|
|
PostNPCInit(); |
|
|
|
if( GetSleepState() == AISS_AUTO_PVS ) |
|
{ |
|
// This code is a bit wonky, but it makes it easier for level designers to |
|
// select this option in Hammer. So we set a sleep flag to indicate the choice, |
|
// and then set the sleep state to awake (normal) |
|
AddSleepFlags( AI_SLEEP_FLAG_AUTO_PVS ); |
|
SetSleepState( AISS_AWAKE ); |
|
} |
|
|
|
if( GetSleepState() == AISS_AUTO_PVS_AFTER_PVS ) |
|
{ |
|
AddSleepFlags( AI_SLEEP_FLAG_AUTO_PVS_AFTER_PVS ); |
|
SetSleepState( AISS_AWAKE ); |
|
} |
|
|
|
if ( GetSleepState() > AISS_AWAKE ) |
|
{ |
|
Sleep(); |
|
} |
|
|
|
m_flLastRealThinkTime = gpGlobals->curtime; |
|
} |
|
|
|
//========================================================= |
|
// StartNPC - final bit of initization before a npc |
|
// is turned over to the AI. |
|
//========================================================= |
|
void CAI_BaseNPC::StartNPC( void ) |
|
{ |
|
// Raise npc off the floor one unit, then drop to floor |
|
if ( (GetMoveType() != MOVETYPE_FLY) && (GetMoveType() != MOVETYPE_FLYGRAVITY) && |
|
!(CapabilitiesGet() & bits_CAP_MOVE_FLY) && |
|
!HasSpawnFlags( SF_NPC_FALL_TO_GROUND ) && !IsWaitingToRappel() && !GetMoveParent() ) |
|
{ |
|
Vector origin = GetLocalOrigin(); |
|
|
|
if (!GetMoveProbe()->FloorPoint( origin + Vector(0, 0, 0.1), MASK_NPCSOLID, 0, -2048, &origin )) |
|
{ |
|
Warning( "NPC %s stuck in wall--level design error at (%.2f %.2f %.2f)\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); |
|
if ( g_pDeveloper->GetInt() > 1 ) |
|
{ |
|
m_debugOverlays |= OVERLAY_BBOX_BIT; |
|
} |
|
} |
|
|
|
SetLocalOrigin( origin ); |
|
} |
|
else |
|
{ |
|
SetGroundEntity( NULL ); |
|
} |
|
|
|
if ( m_target != NULL_STRING )// this npc has a target |
|
{ |
|
// Find the npc's initial target entity, stash it |
|
SetGoalEnt( gEntList.FindEntityByName( NULL, m_target ) ); |
|
|
|
if ( !GetGoalEnt() ) |
|
{ |
|
Warning( "ReadyNPC()--%s couldn't find target %s\n", GetClassname(), STRING(m_target)); |
|
} |
|
else |
|
{ |
|
StartTargetHandling( GetGoalEnt() ); |
|
} |
|
} |
|
|
|
//SetState ( m_IdealNPCState ); |
|
//SetActivity ( m_IdealActivity ); |
|
|
|
InitSquad(); |
|
|
|
//--------------------------------- |
|
// |
|
// Spread think times of simultaneously spawned NPCs so that they don't all happen at the same time |
|
// |
|
// Think distribution based on spawn order is: |
|
// |
|
// Tick offset Think time Spawn order |
|
// 0 0 1 |
|
// 1 0.015 13 |
|
// 2 0.03 5 |
|
// 3 0.045 9 |
|
// 4 0.06 18 |
|
// 5 0.075 3 |
|
// 6 0.09 15 |
|
// 7 0.105 11 |
|
// 8 0.12 7 |
|
// 9 0.135 17 |
|
// 10 0.15 2 |
|
// 11 0.165 14 |
|
// 12 0.18 6 |
|
// 13 0.195 19 |
|
// 14 0.21 10 |
|
// 15 0.225 4 |
|
// 16 0.24 16 |
|
// 17 0.255 12 |
|
// 18 0.27 8 |
|
// 19 0.285 20 |
|
|
|
|
|
// If this NPC is spawning late in the game, just push through the rest of the initialization |
|
// start thinking right now. Some spread is added to handle triggered spawns that bring |
|
// a bunch of NPCs into the level |
|
SetThink ( &CAI_BaseNPC::CallNPCThink ); |
|
|
|
if ( gm_flTimeLastSpawn != gpGlobals->curtime ) |
|
{ |
|
gm_nSpawnedThisFrame = 0; |
|
gm_flTimeLastSpawn = gpGlobals->curtime; |
|
} |
|
|
|
static const float nextThinkTimes[20] = |
|
{ |
|
.0, .150, .075, .225, .030, .180, .120, .270, .045, .210, .105, .255, .015, .165, .090, .240, .135, .060, .195, .285 |
|
}; |
|
|
|
SetNextThink( gpGlobals->curtime + nextThinkTimes[gm_nSpawnedThisFrame % 20] ); |
|
|
|
gm_nSpawnedThisFrame++; |
|
|
|
//--------------------------------- |
|
|
|
m_ScriptArrivalActivity = AIN_DEF_ACTIVITY; |
|
m_strScriptArrivalSequence = NULL_STRING; |
|
|
|
if ( HasSpawnFlags(SF_NPC_WAIT_FOR_SCRIPT) ) |
|
{ |
|
SetState( NPC_STATE_IDLE ); |
|
m_Activity = m_IdealActivity; |
|
m_nIdealSequence = GetSequence(); |
|
SetSchedule( SCHED_WAIT_FOR_SCRIPT ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::StartTargetHandling( CBaseEntity *pTargetEnt ) |
|
{ |
|
// set the npc up to walk a path corner path. |
|
// !!!BUGBUG - this is a minor bit of a hack. |
|
// JAYJAY |
|
|
|
// NPC will start turning towards his destination |
|
bool bIsFlying = (GetMoveType() == MOVETYPE_FLY) || (GetMoveType() == MOVETYPE_FLYGRAVITY); |
|
AI_NavGoal_t goal( GOALTYPE_PATHCORNER, pTargetEnt->GetAbsOrigin(), |
|
bIsFlying ? ACT_FLY : ACT_WALK, |
|
AIN_DEF_TOLERANCE, AIN_YAW_TO_DEST); |
|
|
|
SetState( NPC_STATE_IDLE ); |
|
SetSchedule( SCHED_IDLE_WALK ); |
|
|
|
if ( !GetNavigator()->SetGoal( goal ) ) |
|
{ |
|
DevWarning( 2, "Can't Create Route!\n" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Connect my memory to the squad's |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::InitSquad( void ) |
|
{ |
|
// ------------------------------------------------------- |
|
// If I form squads add me to a squad |
|
// ------------------------------------------------------- |
|
if (!m_pSquad && ( CapabilitiesGet() & bits_CAP_SQUAD )) |
|
{ |
|
if ( !m_SquadName ) |
|
{ |
|
DevMsg(2, "Found %s that isn't in a squad\n",GetClassname()); |
|
} |
|
else |
|
{ |
|
m_pSquad = g_AI_SquadManager.FindCreateSquad(this, m_SquadName); |
|
} |
|
} |
|
|
|
return ( m_pSquad != NULL ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Get the memory for this NPC |
|
//----------------------------------------------------------------------------- |
|
CAI_Enemies *CAI_BaseNPC::GetEnemies( void ) |
|
{ |
|
return m_pEnemies; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Remove this NPC's memory |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::RemoveMemory( void ) |
|
{ |
|
delete m_pEnemies; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::TaskComplete( bool fIgnoreSetFailedCondition ) |
|
{ |
|
EndTaskOverlay(); |
|
|
|
// Handy thing to use for debugging |
|
//if (IsCurSchedule(SCHED_PUT_HERE) && |
|
// GetTask()->iTask == TASK_PUT_HERE) |
|
//{ |
|
// int put_breakpoint_here = 5; |
|
//} |
|
|
|
if ( fIgnoreSetFailedCondition || !HasCondition(COND_TASK_FAILED) ) |
|
{ |
|
SetTaskStatus( TASKSTATUS_COMPLETE ); |
|
} |
|
} |
|
|
|
void CAI_BaseNPC::TaskMovementComplete( void ) |
|
{ |
|
switch( GetTaskStatus() ) |
|
{ |
|
case TASKSTATUS_NEW: |
|
case TASKSTATUS_RUN_MOVE_AND_TASK: |
|
SetTaskStatus( TASKSTATUS_RUN_TASK ); |
|
break; |
|
|
|
case TASKSTATUS_RUN_MOVE: |
|
TaskComplete(); |
|
break; |
|
|
|
case TASKSTATUS_RUN_TASK: |
|
// FIXME: find out how to safely restart movement |
|
//Warning( "Movement completed twice!\n" ); |
|
//Assert( 0 ); |
|
break; |
|
|
|
case TASKSTATUS_COMPLETE: |
|
break; |
|
} |
|
|
|
// JAY: Put this back in. |
|
// UNDONE: Figure out how much of the timestep was consumed by movement |
|
// this frame and restart the movement/schedule engine if necessary |
|
if ( m_scriptState != SCRIPT_CUSTOM_MOVE_TO_MARK ) |
|
{ |
|
SetIdealActivity( GetStoppedActivity() ); |
|
} |
|
|
|
// Advance past the last node (in case there is some event at this node) |
|
if ( GetNavigator()->IsGoalActive() ) |
|
{ |
|
GetNavigator()->AdvancePath(); |
|
} |
|
|
|
// Now clear the path, it's done. |
|
GetNavigator()->ClearGoal(); |
|
|
|
OnMovementComplete(); |
|
} |
|
|
|
|
|
int CAI_BaseNPC::TaskIsRunning( void ) |
|
{ |
|
if ( GetTaskStatus() != TASKSTATUS_COMPLETE && |
|
GetTaskStatus() != TASKSTATUS_RUN_MOVE ) |
|
return 1; |
|
|
|
return 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::TaskFail( AI_TaskFailureCode_t code ) |
|
{ |
|
EndTaskOverlay(); |
|
|
|
// Handy tool for debugging |
|
//if (IsCurSchedule(SCHED_PUT_NAME_HERE)) |
|
//{ |
|
// int put_breakpoint_here = 5; |
|
//} |
|
|
|
// If in developer mode save the fail text for debug output |
|
if (g_pDeveloper->GetInt()) |
|
{ |
|
m_failText = TaskFailureToString( code ); |
|
|
|
m_interuptSchedule = NULL; |
|
m_failedSchedule = GetCurSchedule(); |
|
|
|
if (m_debugOverlays & OVERLAY_TASK_TEXT_BIT) |
|
{ |
|
DevMsg(this, AIMF_IGNORE_SELECTED, " TaskFail -> %s\n", m_failText ); |
|
} |
|
|
|
ADD_DEBUG_HISTORY( HISTORY_AI_DECISIONS, UTIL_VarArgs("%s(%d): TaskFail -> %s\n", GetDebugName(), entindex(), m_failText ) ); |
|
|
|
//AddTimedOverlay( fail_text, 5); |
|
} |
|
|
|
m_ScheduleState.taskFailureCode = code; |
|
SetCondition(COND_TASK_FAILED); |
|
Forget( bits_MEMORY_TURNING ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Remember that this entity wasn't reachable |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
void CAI_BaseNPC::RememberUnreachable(CBaseEntity *pEntity, float duration ) |
|
{ |
|
if ( pEntity == GetEnemy() ) |
|
{ |
|
ForceChooseNewEnemy(); |
|
} |
|
|
|
const float NPC_UNREACHABLE_TIMEOUT = ( duration > 0.0 ) ? duration : 3; |
|
// Only add to list if not already on it |
|
for (int i=m_UnreachableEnts.Size()-1;i>=0;i--) |
|
{ |
|
// If record already exists just update mark time |
|
if (pEntity == m_UnreachableEnts[i].hUnreachableEnt) |
|
{ |
|
m_UnreachableEnts[i].fExpireTime = gpGlobals->curtime + NPC_UNREACHABLE_TIMEOUT; |
|
m_UnreachableEnts[i].vLocationWhenUnreachable = pEntity->GetAbsOrigin(); |
|
return; |
|
} |
|
} |
|
|
|
// Add new unreachabe entity to list |
|
int nNewIndex = m_UnreachableEnts.AddToTail(); |
|
m_UnreachableEnts[nNewIndex].hUnreachableEnt = pEntity; |
|
m_UnreachableEnts[nNewIndex].fExpireTime = gpGlobals->curtime + NPC_UNREACHABLE_TIMEOUT; |
|
m_UnreachableEnts[nNewIndex].vLocationWhenUnreachable = pEntity->GetAbsOrigin(); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Returns true is entity was remembered as unreachable. |
|
// After a time delay reachability is checked |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
bool CAI_BaseNPC::IsUnreachable(CBaseEntity *pEntity) |
|
{ |
|
float UNREACHABLE_DIST_TOLERANCE_SQ = (120*120); |
|
|
|
// Note that it's ok to remove elements while I'm iterating |
|
// as long as I iterate backwards and remove them using FastRemove |
|
for (int i=m_UnreachableEnts.Size()-1;i>=0;i--) |
|
{ |
|
// Remove any dead elements |
|
if (m_UnreachableEnts[i].hUnreachableEnt == NULL) |
|
{ |
|
m_UnreachableEnts.FastRemove(i); |
|
} |
|
else if (pEntity == m_UnreachableEnts[i].hUnreachableEnt) |
|
{ |
|
// Test for reachablility on any elements that have timed out |
|
if ( gpGlobals->curtime > m_UnreachableEnts[i].fExpireTime || |
|
pEntity->GetAbsOrigin().DistToSqr(m_UnreachableEnts[i].vLocationWhenUnreachable) > UNREACHABLE_DIST_TOLERANCE_SQ) |
|
{ |
|
m_UnreachableEnts.FastRemove(i); |
|
return false; |
|
} |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
bool CAI_BaseNPC::IsValidEnemy( CBaseEntity *pEnemy ) |
|
{ |
|
CAI_BaseNPC *pEnemyNPC = pEnemy->MyNPCPointer(); |
|
if ( pEnemyNPC && pEnemyNPC->CanBeAnEnemyOf( this ) == false ) |
|
return false; |
|
|
|
// Test our enemy filter |
|
if ( m_hEnemyFilter.Get()!= NULL && m_hEnemyFilter->PassesFilter( this, pEnemy ) == false ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
bool CAI_BaseNPC::CanBeAnEnemyOf( CBaseEntity *pEnemy ) |
|
{ |
|
if ( GetSleepState() > AISS_WAITING_FOR_THREAT ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Picks best enemy from my list of enemies |
|
// Prefers reachable enemies over enemies that are unreachable, |
|
// regardless of priority. For enemies that are both reachable or |
|
// unreachable picks by priority. If priority is the same, picks |
|
// by distance. |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
|
|
CBaseEntity *CAI_BaseNPC::BestEnemy( void ) |
|
{ |
|
AI_PROFILE_SCOPE( CAI_BaseNPC_BestEnemy ); |
|
// TODO - may want to consider distance, attack types, back turned, etc. |
|
|
|
CBaseEntity* pBestEnemy = NULL; |
|
int iBestDistSq = MAX_COORD_RANGE * MAX_COORD_RANGE;// so first visible entity will become the closest. |
|
int iBestPriority = -1000; |
|
bool bBestUnreachable = true; // Forces initial check |
|
ThreeState_t fBestSeen = TRS_NONE; |
|
ThreeState_t fBestVisible = TRS_NONE; |
|
int iDistSq; |
|
bool bUnreachable = false; |
|
|
|
AIEnemiesIter_t iter; |
|
|
|
DbgEnemyMsg( this, "BestEnemy() {\n" ); |
|
|
|
for( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) |
|
{ |
|
CBaseEntity *pEnemy = pEMemory->hEnemy; |
|
|
|
if (!pEnemy || !pEnemy->IsAlive()) |
|
{ |
|
if ( pEnemy ) |
|
DbgEnemyMsg( this, " %s rejected: dead\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
if ( (pEnemy->GetFlags() & FL_NOTARGET) ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: no target\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
if ( m_bIgnoreUnseenEnemies ) |
|
{ |
|
const float TIME_CONSIDER_ENEMY_UNSEEN = .4; |
|
if ( pEMemory->timeLastSeen < gpGlobals->curtime - TIME_CONSIDER_ENEMY_UNSEEN ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: not seen and set to ignore unseen enemies\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
} |
|
|
|
// UNDONE: Move relationship checks into IsValidEnemy? |
|
Disposition_t relation = IRelationType( pEnemy ); |
|
if ( (relation != D_HT && relation != D_FR) ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: no hate/fear\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
if ( m_flAcceptableTimeSeenEnemy > 0.0 && pEMemory->timeLastSeen < m_flAcceptableTimeSeenEnemy ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: old\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
if ( pEMemory->timeValidEnemy > gpGlobals->curtime ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: not yet valid\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
// Skip enemies that have eluded me to prevent infinite loops |
|
if ( pEMemory->bEludedMe ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: eluded\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
// Skip enemies I fear that I've never seen. (usually seen through an enemy finder) |
|
if ( relation == D_FR && !pEMemory->bUnforgettable && pEMemory->timeFirstSeen == AI_INVALID_TIME ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: feared, but never seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
if ( !IsValidEnemy( pEnemy ) ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: not valid\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
// establish the reachability of this enemy |
|
bUnreachable = IsUnreachable(pEnemy); |
|
|
|
// If best is reachable and current is unreachable, skip the unreachable enemy regardless of priority |
|
if (!bBestUnreachable && bUnreachable) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: unreachable\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
|
|
// If best is unreachable and current is reachable, always pick the current regardless of priority |
|
if (bBestUnreachable && !bUnreachable) |
|
{ |
|
DbgEnemyMsg( this, " %s accepted (1)\n", pEnemy->GetDebugName() ); |
|
if ( pBestEnemy ) |
|
DbgEnemyMsg( this, " (%s displaced)\n", pBestEnemy->GetDebugName() ); |
|
|
|
iBestPriority = IRelationPriority ( pEnemy ); |
|
iBestDistSq = (pEnemy->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); |
|
pBestEnemy = pEnemy; |
|
bBestUnreachable = bUnreachable; |
|
fBestSeen = TRS_NONE; |
|
fBestVisible = TRS_NONE; |
|
} |
|
// If both are unreachable or both are reachable, choose enemy based on priority and distance |
|
else if ( IRelationPriority( pEnemy ) > iBestPriority ) |
|
{ |
|
DbgEnemyMsg( this, " %s accepted\n", pEnemy->GetDebugName() ); |
|
if ( pBestEnemy ) |
|
DbgEnemyMsg( this, " (%s displaced due to priority, %d > %d )\n", pBestEnemy->GetDebugName(), IRelationPriority( pEnemy ), iBestPriority ); |
|
// this entity is disliked MORE than the entity that we |
|
// currently think is the best visible enemy. No need to do |
|
// a distance check, just get mad at this one for now. |
|
iBestPriority = IRelationPriority ( pEnemy ); |
|
iBestDistSq = ( pEnemy->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); |
|
pBestEnemy = pEnemy; |
|
bBestUnreachable = bUnreachable; |
|
fBestSeen = TRS_NONE; |
|
fBestVisible = TRS_NONE; |
|
} |
|
else if ( IRelationPriority( pEnemy ) == iBestPriority ) |
|
{ |
|
// this entity is disliked just as much as the entity that |
|
// we currently think is the best visible enemy, so we only |
|
// get mad at it if it is closer. |
|
iDistSq = ( pEnemy->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); |
|
|
|
bool bAcceptCurrent = false; |
|
bool bCloser = ( ( iBestDistSq - iDistSq ) > EnemyDistTolerance() ); |
|
ThreeState_t fCurSeen = TRS_NONE; |
|
ThreeState_t fCurVisible = TRS_NONE; |
|
|
|
// The following code is constructed in such a verbose manner to |
|
// ensure the expensive calls only occur if absolutely needed |
|
|
|
// If current is farther, and best has previously been confirmed as seen or visible, move on |
|
if ( !bCloser) |
|
{ |
|
if ( fBestSeen == TRS_TRUE || fBestVisible == TRS_TRUE ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: current is closer and seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
} |
|
|
|
// If current is closer, and best has previously been confirmed as not seen and not visible, take it |
|
if ( bCloser) |
|
{ |
|
if ( fBestSeen == TRS_FALSE && fBestVisible == TRS_FALSE ) |
|
{ |
|
bAcceptCurrent = true; |
|
} |
|
} |
|
|
|
if ( !bAcceptCurrent ) |
|
{ |
|
// If current is closer, and seen, take it |
|
if ( bCloser ) |
|
{ |
|
fCurSeen = ( GetSenses()->DidSeeEntity( pEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
|
|
bAcceptCurrent = ( fCurSeen == TRS_TRUE ); |
|
} |
|
} |
|
|
|
if ( !bAcceptCurrent ) |
|
{ |
|
// If current is farther, and best is seen, move on |
|
if ( !bCloser ) |
|
{ |
|
if ( fBestSeen == TRS_NONE ) |
|
{ |
|
fBestSeen = ( GetSenses()->DidSeeEntity( pBestEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
} |
|
|
|
if ( fBestSeen == TRS_TRUE ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: current is closer and seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
} |
|
|
|
// At this point, need to start performing expensive tests |
|
if ( bCloser && fBestVisible == TRS_NONE ) |
|
{ |
|
// Perform shortest FVisible |
|
fCurVisible = ( ( EnemyDistance( pEnemy ) < GetSenses()->GetDistLook() ) && FVisible( pEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
|
|
bAcceptCurrent = ( fCurVisible == TRS_TRUE ); |
|
} |
|
|
|
// Alas, must do the most expensive comparison |
|
if ( !bAcceptCurrent ) |
|
{ |
|
if ( fBestSeen == TRS_NONE ) |
|
{ |
|
fBestSeen = ( GetSenses()->DidSeeEntity( pBestEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
} |
|
|
|
if ( fBestVisible == TRS_NONE ) |
|
{ |
|
fBestVisible = ( ( EnemyDistance( pBestEnemy ) < GetSenses()->GetDistLook() ) && FVisible( pBestEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
} |
|
|
|
if ( fCurSeen == TRS_NONE ) |
|
{ |
|
fCurSeen = ( GetSenses()->DidSeeEntity( pEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
} |
|
|
|
if ( fCurVisible == TRS_NONE ) |
|
{ |
|
fCurVisible = ( ( EnemyDistance( pEnemy ) < GetSenses()->GetDistLook() ) && FVisible( pEnemy ) ) ? TRS_TRUE : TRS_FALSE; |
|
} |
|
|
|
bool bBestSeenOrVisible = ( fBestSeen == TRS_TRUE || fBestVisible == TRS_TRUE ); |
|
bool bCurSeenOrVisible = ( fCurSeen == TRS_TRUE || fCurVisible == TRS_TRUE ); |
|
|
|
if ( !bCloser) |
|
{ |
|
if ( bBestSeenOrVisible ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: current is closer and seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
else if ( !bCurSeenOrVisible ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: current is closer and neither is seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
} |
|
else // Closer |
|
{ |
|
if ( !bCurSeenOrVisible && bBestSeenOrVisible ) |
|
{ |
|
DbgEnemyMsg( this, " %s rejected: current is father but seen\n", pEnemy->GetDebugName() ); |
|
continue; |
|
} |
|
} |
|
} |
|
} |
|
|
|
DbgEnemyMsg( this, " %s accepted\n", pEnemy->GetDebugName() ); |
|
if ( pBestEnemy ) |
|
DbgEnemyMsg( this, " (%s displaced due to distance/visibility)\n", pBestEnemy->GetDebugName() ); |
|
fBestSeen = fCurSeen; |
|
fBestVisible = fCurVisible; |
|
iBestDistSq = iDistSq; |
|
iBestPriority = IRelationPriority ( pEnemy ); |
|
pBestEnemy = pEnemy; |
|
bBestUnreachable = bUnreachable; |
|
} |
|
else |
|
DbgEnemyMsg( this, " %s rejected: lower priority\n", pEnemy->GetDebugName() ); |
|
} |
|
|
|
DbgEnemyMsg( this, "} == %s\n", pBestEnemy->GetDebugName() ); |
|
|
|
return pBestEnemy; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Given a node returns the appropriate reload activity |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
Activity CAI_BaseNPC::GetReloadActivity( CAI_Hint* pHint ) |
|
{ |
|
Activity nReloadActivity = ACT_RELOAD; |
|
|
|
if (pHint && GetEnemy()!=NULL) |
|
{ |
|
switch (pHint->HintType()) |
|
{ |
|
case HINT_TACTICAL_COVER_LOW: |
|
case HINT_TACTICAL_COVER_MED: |
|
{ |
|
if (SelectWeightedSequence( ACT_RELOAD_LOW ) != ACTIVITY_NOT_AVAILABLE) |
|
{ |
|
Vector vEyePos = GetAbsOrigin() + EyeOffset(ACT_RELOAD_LOW); |
|
// Check if this location will block the threat's line of sight to me |
|
trace_t tr; |
|
AI_TraceLOS( vEyePos, GetEnemy()->EyePosition(), this, &tr ); |
|
if (tr.fraction != 1.0) |
|
{ |
|
nReloadActivity = ACT_RELOAD_LOW; |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
return nReloadActivity; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Given a node returns the appropriate cover activity |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
Activity CAI_BaseNPC::GetCoverActivity( CAI_Hint *pHint ) |
|
{ |
|
Activity nCoverActivity = ACT_INVALID; |
|
|
|
// --------------------------------------------------------------- |
|
// Check if hint node specifies different cover type |
|
// --------------------------------------------------------------- |
|
if (pHint) |
|
{ |
|
switch (pHint->HintType()) |
|
{ |
|
case HINT_TACTICAL_COVER_MED: |
|
{ |
|
nCoverActivity = ACT_COVER_MED; |
|
break; |
|
} |
|
case HINT_TACTICAL_COVER_LOW: |
|
{ |
|
nCoverActivity = ACT_COVER_LOW; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if ( nCoverActivity == ACT_INVALID ) |
|
nCoverActivity = ACT_COVER; |
|
|
|
return nCoverActivity; |
|
} |
|
|
|
//========================================================= |
|
// CalcIdealYaw - gets a yaw value for the caller that would |
|
// face the supplied vector. Value is stuffed into the npc's |
|
// ideal_yaw |
|
//========================================================= |
|
float CAI_BaseNPC::CalcIdealYaw( const Vector &vecTarget ) |
|
{ |
|
Vector vecProjection; |
|
|
|
// strafing npc needs to face 90 degrees away from its goal |
|
if ( GetNavigator()->GetMovementActivity() == ACT_STRAFE_LEFT ) |
|
{ |
|
vecProjection.x = -vecTarget.y; |
|
vecProjection.y = vecTarget.x; |
|
|
|
return UTIL_VecToYaw( vecProjection - GetLocalOrigin() ); |
|
} |
|
else if ( GetNavigator()->GetMovementActivity() == ACT_STRAFE_RIGHT ) |
|
{ |
|
vecProjection.x = vecTarget.y; |
|
vecProjection.y = vecTarget.x; |
|
|
|
return UTIL_VecToYaw( vecProjection - GetLocalOrigin() ); |
|
} |
|
else |
|
{ |
|
return UTIL_VecToYaw ( vecTarget - GetLocalOrigin() ); |
|
} |
|
} |
|
|
|
//========================================================= |
|
// SetEyePosition |
|
// |
|
// queries the npc's model for $eyeposition and copies |
|
// that vector to the npc's m_vDefaultEyeOffset and m_vecViewOffset |
|
// |
|
//========================================================= |
|
void CAI_BaseNPC::SetDefaultEyeOffset ( void ) |
|
{ |
|
if ( GetModelPtr() ) |
|
{ |
|
GetEyePosition( GetModelPtr(), m_vDefaultEyeOffset ); |
|
|
|
if ( m_vDefaultEyeOffset == vec3_origin ) |
|
{ |
|
if ( Classify() != CLASS_NONE ) |
|
{ |
|
DevMsg( "WARNING: %s(%s) has no eye offset in .qc!\n", GetClassname(), STRING(GetModelName()) ); |
|
} |
|
VectorAdd( WorldAlignMins(), WorldAlignMaxs(), m_vDefaultEyeOffset ); |
|
m_vDefaultEyeOffset *= 0.75; |
|
} |
|
} |
|
else |
|
m_vDefaultEyeOffset = vec3_origin; |
|
|
|
SetViewOffset( m_vDefaultEyeOffset ); |
|
|
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Returns eye offset for an NPC for the given activity |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseNPC::EyeOffset( Activity nActivity ) |
|
{ |
|
if ( CapabilitiesGet() & bits_CAP_DUCK ) |
|
{ |
|
if ( IsCrouchedActivity( nActivity ) ) |
|
return GetCrouchEyeOffset(); |
|
} |
|
|
|
// if the hint doesn't tell anything, assume current state |
|
if ( IsCrouching() ) |
|
return GetCrouchEyeOffset(); |
|
|
|
return m_vDefaultEyeOffset * GetModelScale(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Vector |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::EyePosition( void ) |
|
{ |
|
if ( IsCrouching() ) |
|
return GetAbsOrigin() + GetCrouchEyeOffset(); |
|
|
|
return BaseClass::EyePosition(); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
void CAI_BaseNPC::HandleAnimEvent( animevent_t *pEvent ) |
|
{ |
|
// UNDONE: Share this code into CBaseAnimating as appropriate? |
|
switch( pEvent->event ) |
|
{ |
|
case SCRIPT_EVENT_DEAD: |
|
if ( m_NPCState == NPC_STATE_SCRIPT ) |
|
{ |
|
m_lifeState = LIFE_DYING; |
|
// Kill me now! (and fade out when CineCleanup() is called) |
|
#if _DEBUG |
|
DevMsg( 2, "Death event: %s\n", GetClassname() ); |
|
#endif |
|
m_iHealth = 0; |
|
} |
|
#if _DEBUG |
|
else |
|
DevWarning( 2, "INVALID death event:%s\n", GetClassname() ); |
|
#endif |
|
break; |
|
case SCRIPT_EVENT_NOT_DEAD: |
|
if ( m_NPCState == NPC_STATE_SCRIPT ) |
|
{ |
|
m_lifeState = LIFE_ALIVE; |
|
// This is for life/death sequences where the player can determine whether a character is dead or alive after the script |
|
m_iHealth = m_iMaxHealth; |
|
} |
|
break; |
|
|
|
case SCRIPT_EVENT_SOUND: // Play a named wave file |
|
{ |
|
EmitSound( pEvent->options ); |
|
} |
|
break; |
|
|
|
case SCRIPT_EVENT_SOUND_VOICE: |
|
{ |
|
EmitSound( pEvent->options ); |
|
} |
|
break; |
|
|
|
case SCRIPT_EVENT_SENTENCE_RND1: // Play a named sentence group 33% of the time |
|
if (random->RandomInt(0,2) == 0) |
|
break; |
|
// fall through... |
|
case SCRIPT_EVENT_SENTENCE: // Play a named sentence group |
|
SENTENCEG_PlayRndSz( edict(), pEvent->options, 1.0, SNDLVL_TALKING, 0, 100 ); |
|
break; |
|
|
|
case SCRIPT_EVENT_FIREEVENT: |
|
{ |
|
// |
|
// Fire a script event. The number of the script event to fire is in the options string. |
|
// |
|
if ( m_hCine != NULL ) |
|
{ |
|
m_hCine->FireScriptEvent( atoi( pEvent->options ) ); |
|
} |
|
else |
|
{ |
|
// FIXME: look so see if it's playing a vcd and fire those instead |
|
// AssertOnce( 0 ); |
|
} |
|
break; |
|
} |
|
case SCRIPT_EVENT_FIRE_INPUT: |
|
{ |
|
variant_t emptyVariant; |
|
this->AcceptInput( pEvent->options, this, this, emptyVariant, 0 ); |
|
break; |
|
} |
|
|
|
case SCRIPT_EVENT_NOINTERRUPT: // Can't be interrupted from now on |
|
if ( m_hCine ) |
|
m_hCine->AllowInterrupt( false ); |
|
break; |
|
|
|
case SCRIPT_EVENT_CANINTERRUPT: // OK to interrupt now |
|
if ( m_hCine ) |
|
m_hCine->AllowInterrupt( true ); |
|
break; |
|
|
|
#if 0 |
|
case SCRIPT_EVENT_INAIR: // Don't engine->DropToFloor() |
|
case SCRIPT_EVENT_ENDANIMATION: // Set ending animation sequence to |
|
break; |
|
#endif |
|
case SCRIPT_EVENT_BODYGROUPON: |
|
case SCRIPT_EVENT_BODYGROUPOFF: |
|
case SCRIPT_EVENT_BODYGROUPTEMP: |
|
DevMsg( "Bodygroup!\n" ); |
|
break; |
|
|
|
case AE_NPC_ATTACK_BROADCAST: |
|
break; |
|
|
|
case NPC_EVENT_BODYDROP_HEAVY: |
|
if ( GetFlags() & FL_ONGROUND ) |
|
{ |
|
EmitSound( "AI_BaseNPC.BodyDrop_Heavy" ); |
|
} |
|
break; |
|
|
|
case NPC_EVENT_BODYDROP_LIGHT: |
|
if ( GetFlags() & FL_ONGROUND ) |
|
{ |
|
EmitSound( "AI_BaseNPC.BodyDrop_Light" ); |
|
} |
|
break; |
|
|
|
case NPC_EVENT_SWISHSOUND: |
|
{ |
|
// NO NPC may use this anim event unless that npc's precache precaches this sound!!! |
|
EmitSound( "AI_BaseNPC.SwishSound" ); |
|
break; |
|
} |
|
|
|
|
|
case NPC_EVENT_180TURN: |
|
{ |
|
//DevMsg( "Turned!\n" ); |
|
SetIdealActivity( ACT_IDLE ); |
|
Forget( bits_MEMORY_TURNING ); |
|
SetBoneController( 0, GetLocalAngles().y ); |
|
IncrementInterpolationFrame(); |
|
break; |
|
} |
|
|
|
case NPC_EVENT_ITEM_PICKUP: |
|
{ |
|
CBaseEntity *pPickup = NULL; |
|
|
|
// |
|
// Figure out what we're supposed to pick up. |
|
// |
|
if ( pEvent->options && strlen( pEvent->options ) > 0 ) |
|
{ |
|
// Pick up the weapon or item that was specified in the anim event. |
|
pPickup = gEntList.FindEntityGenericNearest( pEvent->options, GetAbsOrigin(), 256, this ); |
|
} |
|
else |
|
{ |
|
// Pick up the weapon or item that was found earlier and cached in our target pointer. |
|
pPickup = GetTarget(); |
|
} |
|
|
|
// Make sure we found something to pick up. |
|
if ( !pPickup ) |
|
{ |
|
TaskFail("Item no longer available!\n"); |
|
break; |
|
} |
|
|
|
// Make sure the item hasn't moved. |
|
float flDist = ( pPickup->WorldSpaceCenter() - GetAbsOrigin() ).Length2D(); |
|
if ( flDist > ITEM_PICKUP_TOLERANCE ) |
|
{ |
|
TaskFail("Item has moved!\n"); |
|
break; |
|
} |
|
|
|
CBaseCombatWeapon *pWeapon = dynamic_cast<CBaseCombatWeapon *>( pPickup ); |
|
if ( pWeapon ) |
|
{ |
|
// Picking up a weapon. |
|
CBaseCombatCharacter *pOwner = pWeapon->GetOwner(); |
|
if ( pOwner ) |
|
{ |
|
TaskFail( "Weapon in use by someone else" ); |
|
} |
|
else if ( !pWeapon ) |
|
{ |
|
TaskFail( "Weapon doesn't exist" ); |
|
} |
|
else if (!Weapon_CanUse( pWeapon )) |
|
{ |
|
TaskFail( "Can't use this weapon type" ); |
|
} |
|
else |
|
{ |
|
PickupWeapon( pWeapon ); |
|
TaskComplete(); |
|
break; |
|
} |
|
} |
|
else |
|
{ |
|
// Picking up an item. |
|
PickupItem( pPickup ); |
|
TaskComplete(); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case NPC_EVENT_WEAPON_SET_SEQUENCE_NUMBER: |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
if ((pWeapon) && (pEvent->options)) |
|
{ |
|
int nSequence = atoi(pEvent->options); |
|
if (nSequence != -1) |
|
{ |
|
pWeapon->ResetSequence(nSequence); |
|
} |
|
} |
|
break; |
|
} |
|
|
|
case NPC_EVENT_WEAPON_SET_SEQUENCE_NAME: |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
if ((pWeapon) && (pEvent->options)) |
|
{ |
|
int nSequence = pWeapon->LookupSequence(pEvent->options); |
|
if (nSequence != -1) |
|
{ |
|
pWeapon->ResetSequence(nSequence); |
|
} |
|
} |
|
break; |
|
} |
|
|
|
case NPC_EVENT_WEAPON_SET_ACTIVITY: |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
if ((pWeapon) && (pEvent->options)) |
|
{ |
|
Activity act = (Activity)pWeapon->LookupActivity(pEvent->options); |
|
if (act != ACT_INVALID) |
|
{ |
|
// FIXME: where should the duration come from? normally it would come from the current sequence |
|
Weapon_SetActivity(act, 0); |
|
} |
|
} |
|
break; |
|
} |
|
|
|
case NPC_EVENT_WEAPON_DROP: |
|
{ |
|
// |
|
// Drop our active weapon (or throw it at the specified target entity). |
|
// |
|
CBaseEntity *pTarget = NULL; |
|
if (pEvent->options) |
|
{ |
|
pTarget = gEntList.FindEntityGeneric(NULL, pEvent->options, this); |
|
} |
|
|
|
if (pTarget) |
|
{ |
|
Vector vecTargetPos = pTarget->WorldSpaceCenter(); |
|
Weapon_Drop(GetActiveWeapon(), &vecTargetPos); |
|
} |
|
else |
|
{ |
|
Weapon_Drop(GetActiveWeapon()); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case EVENT_WEAPON_RELOAD: |
|
{ |
|
if ( GetActiveWeapon() ) |
|
{ |
|
GetActiveWeapon()->WeaponSound( RELOAD_NPC ); |
|
GetActiveWeapon()->m_iClip1 = GetActiveWeapon()->GetMaxClip1(); |
|
ClearCondition(COND_LOW_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_SECONDARY_AMMO); |
|
} |
|
break; |
|
} |
|
|
|
case EVENT_WEAPON_RELOAD_SOUND: |
|
{ |
|
if ( GetActiveWeapon() ) |
|
{ |
|
GetActiveWeapon()->WeaponSound( RELOAD_NPC ); |
|
} |
|
break; |
|
} |
|
|
|
case EVENT_WEAPON_RELOAD_FILL_CLIP: |
|
{ |
|
if ( GetActiveWeapon() ) |
|
{ |
|
GetActiveWeapon()->m_iClip1 = GetActiveWeapon()->GetMaxClip1(); |
|
ClearCondition(COND_LOW_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_PRIMARY_AMMO); |
|
ClearCondition(COND_NO_SECONDARY_AMMO); |
|
} |
|
break; |
|
} |
|
|
|
case NPC_EVENT_LEFTFOOT: |
|
case NPC_EVENT_RIGHTFOOT: |
|
// For right now, do nothing. All functionality for this lives in individual npcs. |
|
break; |
|
|
|
case NPC_EVENT_OPEN_DOOR: |
|
{ |
|
CBasePropDoor *pDoor = (CBasePropDoor *)(CBaseEntity *)GetNavigator()->GetPath()->GetCurWaypoint()->GetEHandleData(); |
|
if (pDoor != NULL) |
|
{ |
|
OpenPropDoorNow( pDoor ); |
|
} |
|
|
|
break; |
|
} |
|
|
|
default: |
|
if ((pEvent->type & AE_TYPE_NEWEVENTSYSTEM) && (pEvent->type & AE_TYPE_SERVER)) |
|
{ |
|
if (pEvent->event == AE_NPC_HOLSTER) |
|
{ |
|
// Cache off the weapon. |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
|
|
Assert( pWeapon != NULL ); |
|
|
|
GetActiveWeapon()->Holster(); |
|
SetActiveWeapon( NULL ); |
|
|
|
//Force the NPC to recalculate it's arrival activity since it'll most likely be wrong now that we don't have a weapon out. |
|
GetNavigator()->SetArrivalSequence( ACT_INVALID ); |
|
|
|
if ( m_iDesiredWeaponState == DESIREDWEAPONSTATE_CHANGING_DESTROY ) |
|
{ |
|
// Get rid of it! |
|
UTIL_Remove( pWeapon ); |
|
} |
|
|
|
if ( m_iDesiredWeaponState != DESIREDWEAPONSTATE_IGNORE ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_IGNORE; |
|
m_Activity = ACT_RESET; |
|
} |
|
|
|
return; |
|
} |
|
else if (pEvent->event == AE_NPC_DRAW) |
|
{ |
|
if (GetActiveWeapon()) |
|
{ |
|
GetActiveWeapon()->Deploy(); |
|
|
|
//Force the NPC to recalculate it's arrival activity since it'll most likely be wrong now. |
|
GetNavigator()->SetArrivalSequence( ACT_INVALID ); |
|
|
|
if ( m_iDesiredWeaponState != DESIREDWEAPONSTATE_IGNORE ) |
|
{ |
|
m_iDesiredWeaponState = DESIREDWEAPONSTATE_IGNORE; |
|
m_Activity = ACT_RESET; |
|
} |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_BODYDROP_HEAVY ) |
|
{ |
|
if ( GetFlags() & FL_ONGROUND ) |
|
{ |
|
EmitSound( "AI_BaseNPC.BodyDrop_Heavy" ); |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_LEFTFOOT || pEvent->event == AE_NPC_RIGHTFOOT ) |
|
{ |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_RAGDOLL ) |
|
{ |
|
// Convert to ragdoll immediately |
|
BecomeRagdollOnClient( vec3_origin ); |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_ADDGESTURE ) |
|
{ |
|
Activity act = ( Activity )LookupActivity( pEvent->options ); |
|
if (act != ACT_INVALID) |
|
{ |
|
act = TranslateActivity( act ); |
|
if (act != ACT_INVALID) |
|
{ |
|
AddGesture( act ); |
|
} |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_RESTARTGESTURE ) |
|
{ |
|
Activity act = ( Activity )LookupActivity( pEvent->options ); |
|
if (act != ACT_INVALID) |
|
{ |
|
act = TranslateActivity( act ); |
|
if (act != ACT_INVALID) |
|
{ |
|
RestartGesture( act ); |
|
} |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_WEAPON_DROP ) |
|
{ |
|
// Drop our active weapon (or throw it at the specified target entity). |
|
CBaseEntity *pTarget = NULL; |
|
if (pEvent->options) |
|
{ |
|
pTarget = gEntList.FindEntityGeneric(NULL, pEvent->options, this); |
|
} |
|
|
|
if (pTarget) |
|
{ |
|
Vector vecTargetPos = pTarget->WorldSpaceCenter(); |
|
Weapon_Drop(GetActiveWeapon(), &vecTargetPos); |
|
} |
|
else |
|
{ |
|
Weapon_Drop(GetActiveWeapon()); |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_WEAPON_SET_ACTIVITY ) |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon(); |
|
if ((pWeapon) && (pEvent->options)) |
|
{ |
|
Activity act = (Activity)pWeapon->LookupActivity(pEvent->options); |
|
if (act == ACT_INVALID) |
|
{ |
|
// Try and translate it |
|
act = Weapon_TranslateActivity( (Activity)CAI_BaseNPC::GetActivityID(pEvent->options), NULL ); |
|
} |
|
|
|
if (act != ACT_INVALID) |
|
{ |
|
// FIXME: where should the duration come from? normally it would come from the current sequence |
|
Weapon_SetActivity(act, 0); |
|
} |
|
} |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_SET_INTERACTION_CANTDIE ) |
|
{ |
|
SetInteractionCantDie( (atoi(pEvent->options) != 0) ); |
|
return; |
|
} |
|
else if ( pEvent->event == AE_NPC_HURT_INTERACTION_PARTNER ) |
|
{ |
|
// If we're currently interacting with an enemy, hurt them/me |
|
if ( m_hInteractionPartner ) |
|
{ |
|
CAI_BaseNPC *pTarget = NULL; |
|
CAI_BaseNPC *pAttacker = NULL; |
|
if ( pEvent->options ) |
|
{ |
|
char szEventOptions[128]; |
|
Q_strncpy( szEventOptions, pEvent->options, sizeof(szEventOptions) ); |
|
char *pszParam = strtok( szEventOptions, " " ); |
|
if ( pszParam ) |
|
{ |
|
if ( !Q_strncmp( pszParam, "ME", 2 ) ) |
|
{ |
|
pTarget = this; |
|
pAttacker = m_hInteractionPartner; |
|
} |
|
else if ( !Q_strncmp( pszParam, "THEM", 4 ) ) |
|
{ |
|
pAttacker = this; |
|
pTarget = m_hInteractionPartner; |
|
} |
|
|
|
pszParam = strtok(NULL," "); |
|
if ( pAttacker && pTarget && pszParam ) |
|
{ |
|
int iDamage = atoi( pszParam ); |
|
if ( iDamage ) |
|
{ |
|
// We've got a target, and damage. Now hurt them. |
|
CTakeDamageInfo info; |
|
info.SetDamage( iDamage ); |
|
info.SetAttacker( pAttacker ); |
|
info.SetInflictor( pAttacker ); |
|
info.SetDamageType( DMG_GENERIC | DMG_PREVENT_PHYSICS_FORCE ); |
|
pTarget->TakeDamage( info ); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Bad data. Explain how to use this anim event. |
|
const char *pName = EventList_NameForIndex( pEvent->event ); |
|
DevWarning( 1, "Bad %s format. Should be: { AE_NPC_HURT_INTERACTION_PARTNER <frame number> \"<ME/THEM> <Amount of damage done>\" }\n", pName ); |
|
return; |
|
} |
|
|
|
DevWarning( "%s received AE_NPC_HURT_INTERACTION_PARTNER anim event, but it's not interacting with anything.\n", GetDebugName() ); |
|
return; |
|
} |
|
} |
|
|
|
// FIXME: why doesn't this code pass unhandled events down to its parent? |
|
// Came from my weapon? |
|
//Adrian I'll clean this up once the old event system is phased out. |
|
if ( pEvent->pSource != this || ( pEvent->type & AE_TYPE_NEWEVENTSYSTEM && pEvent->type & AE_TYPE_WEAPON ) || (pEvent->event >= EVENT_WEAPON && pEvent->event <= EVENT_WEAPON_LAST) ) |
|
{ |
|
Weapon_HandleAnimEvent( pEvent ); |
|
} |
|
else |
|
{ |
|
BaseClass::HandleAnimEvent( pEvent ); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Override base class to add display of routes |
|
// Input : |
|
// Output : Current text offset from the top |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::DrawDebugGeometryOverlays(void) |
|
{ |
|
// Handy for debug |
|
//NDebugOverlay::Cross3D(EyePosition(),Vector(-2,-2,-2),Vector(2,2,2),0,255,0,true); |
|
|
|
// ------------------------------ |
|
// Remove me if requested |
|
// ------------------------------ |
|
if (m_debugOverlays & OVERLAY_NPC_ZAP_BIT) |
|
{ |
|
VacateStrategySlot(); |
|
Weapon_Drop( GetActiveWeapon() ); |
|
m_iHealth = 0; |
|
SetThink( &CAI_BaseNPC::SUB_Remove ); |
|
} |
|
|
|
// ------------------------------ |
|
// properly kill an NPC. |
|
// ------------------------------ |
|
if (m_debugOverlays & OVERLAY_NPC_KILL_BIT) |
|
{ |
|
CTakeDamageInfo info; |
|
|
|
info.SetDamage( m_iHealth ); |
|
info.SetAttacker( this ); |
|
info.SetInflictor( ( AI_IsSinglePlayer() ) ? (CBaseEntity *)AI_GetSinglePlayer() : (CBaseEntity *)this ); |
|
info.SetDamageType( DMG_GENERIC ); |
|
|
|
m_debugOverlays &= ~OVERLAY_NPC_KILL_BIT; |
|
TakeDamage( info ); |
|
return; |
|
} |
|
|
|
|
|
// ------------------------------ |
|
// Draw route if requested |
|
// ------------------------------ |
|
if ((m_debugOverlays & OVERLAY_NPC_ROUTE_BIT)) |
|
{ |
|
GetNavigator()->DrawDebugRouteOverlay(); |
|
if ( IsMoving() ) |
|
{ |
|
float yaw = GetMotor()->GetIdealYaw(); |
|
Vector vecYaw = UTIL_YawToVector(yaw); |
|
NDebugOverlay::Line(WorldSpaceCenter(),WorldSpaceCenter() + vecYaw * GetHullWidth() * .5,255,255,255,true,0.0); |
|
} |
|
} |
|
|
|
if (!(CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI) && (IsCurSchedule(SCHED_FORCED_GO) || IsCurSchedule(SCHED_FORCED_GO_RUN))) |
|
{ |
|
NDebugOverlay::Box(m_vecLastPosition, Vector(-5,-5,-5),Vector(5,5,5), 255, 0, 255, 0, 0); |
|
NDebugOverlay::HorzArrow( GetAbsOrigin(), m_vecLastPosition, 16, 255, 0, 255, 64, true, 0 ); |
|
} |
|
|
|
// ------------------------------ |
|
// Draw red box around if selected |
|
// ------------------------------ |
|
if ((m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) && !ai_no_select_box.GetBool()) |
|
{ |
|
NDebugOverlay::EntityBounds(this, 255, 0, 0, 20, 0); |
|
} |
|
|
|
// ------------------------------ |
|
// Draw nearest node if selected |
|
// ------------------------------ |
|
if ((m_debugOverlays & OVERLAY_NPC_NEAREST_BIT)) |
|
{ |
|
int iNodeID = GetPathfinder()->NearestNodeToNPC(); |
|
if (iNodeID != NO_NODE) |
|
{ |
|
NDebugOverlay::Box(GetNavigator()->GetNetwork()->AccessNodes()[iNodeID]->GetPosition(GetHullType()), Vector(-10,-10,-10),Vector(10,10,10), 255, 255, 255, 0, 0); |
|
} |
|
} |
|
|
|
// ------------------------------ |
|
// Draw viewcone if selected |
|
// ------------------------------ |
|
if ((m_debugOverlays & OVERLAY_NPC_VIEWCONE_BIT)) |
|
{ |
|
float flViewRange = acos(m_flFieldOfView); |
|
Vector vEyeDir = EyeDirection2D( ); |
|
Vector vLeftDir, vRightDir; |
|
float fSin, fCos; |
|
SinCos( flViewRange, &fSin, &fCos ); |
|
|
|
vLeftDir.x = vEyeDir.x * fCos - vEyeDir.y * fSin; |
|
vLeftDir.y = vEyeDir.x * fSin + vEyeDir.y * fCos; |
|
vLeftDir.z = vEyeDir.z; |
|
fSin = sin(-flViewRange); |
|
fCos = cos(-flViewRange); |
|
vRightDir.x = vEyeDir.x * fCos - vEyeDir.y * fSin; |
|
vRightDir.y = vEyeDir.x * fSin + vEyeDir.y * fCos; |
|
vRightDir.z = vEyeDir.z; |
|
|
|
// Visualize it |
|
NDebugOverlay::VertArrow( EyePosition(), EyePosition() + ( vLeftDir * 200 ), 64, 255, 0, 0, 50, false, 0 ); |
|
NDebugOverlay::VertArrow( EyePosition(), EyePosition() + ( vRightDir * 200 ), 64, 255, 0, 0, 50, false, 0 ); |
|
NDebugOverlay::VertArrow( EyePosition(), EyePosition() + ( vEyeDir * 100 ), 8, 0, 255, 0, 50, false, 0 ); |
|
NDebugOverlay::Box(EyePosition(), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, 128, 0 ); |
|
} |
|
|
|
// ---------------------------------------------- |
|
// Draw the relationships for this NPC to others |
|
// ---------------------------------------------- |
|
if ( m_debugOverlays & OVERLAY_NPC_RELATION_BIT ) |
|
{ |
|
// Show the relationships to entities around us |
|
int r = 0; |
|
int g = 0; |
|
int b = 0; |
|
|
|
int nRelationship; |
|
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); |
|
|
|
// Rate all NPCs |
|
for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) |
|
{ |
|
if ( ppAIs[i] == NULL || ppAIs[i] == this ) |
|
continue; |
|
|
|
// Get our relation to the target |
|
nRelationship = IRelationType( ppAIs[i] ); |
|
|
|
// Get the color for the arrow |
|
UTIL_GetDebugColorForRelationship( nRelationship, r, g, b ); |
|
|
|
// Draw an arrow |
|
NDebugOverlay::HorzArrow( GetAbsOrigin(), ppAIs[i]->GetAbsOrigin(), 16, r, g, b, 64, true, 0.0f ); |
|
} |
|
|
|
// Also include all players |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
if ( pPlayer == NULL ) |
|
continue; |
|
|
|
// Get our relation to the target |
|
nRelationship = IRelationType( pPlayer ); |
|
|
|
// Get the color for the arrow |
|
UTIL_GetDebugColorForRelationship( nRelationship, r, g, b ); |
|
|
|
// Draw an arrow |
|
NDebugOverlay::HorzArrow( GetAbsOrigin(), pPlayer->GetAbsOrigin(), 16, r, g, b, 64, true, 0.0f ); |
|
} |
|
} |
|
|
|
// ------------------------------ |
|
// Draw enemies if selected |
|
// ------------------------------ |
|
if ((m_debugOverlays & OVERLAY_NPC_ENEMIES_BIT)) |
|
{ |
|
AIEnemiesIter_t iter; |
|
for( AI_EnemyInfo_t *eMemory = GetEnemies()->GetFirst(&iter); eMemory != NULL; eMemory = GetEnemies()->GetNext(&iter) ) |
|
{ |
|
if (eMemory->hEnemy) |
|
{ |
|
CBaseCombatCharacter *npcEnemy = (eMemory->hEnemy)->MyCombatCharacterPointer(); |
|
if (npcEnemy) |
|
{ |
|
float r,g,b; |
|
char debugText[255]; |
|
debugText[0] = NULL; |
|
|
|
if (npcEnemy == GetEnemy()) |
|
{ |
|
Q_strncat(debugText,"Current Enemy", sizeof( debugText ), COPY_ALL_CHARACTERS ); |
|
} |
|
else if (npcEnemy == GetTarget()) |
|
{ |
|
Q_strncat(debugText,"Current Target", sizeof( debugText ), COPY_ALL_CHARACTERS ); |
|
} |
|
else |
|
{ |
|
Q_strncat(debugText,"Other Memory", sizeof( debugText ), COPY_ALL_CHARACTERS ); |
|
} |
|
if (IsUnreachable(npcEnemy)) |
|
{ |
|
Q_strncat(debugText," (Unreachable)", sizeof( debugText ), COPY_ALL_CHARACTERS ); |
|
} |
|
if (eMemory->bEludedMe) |
|
{ |
|
Q_strncat(debugText," (Eluded)", sizeof( debugText ), COPY_ALL_CHARACTERS ); |
|
} |
|
// Unreachable enemy drawn in green |
|
if (IsUnreachable(npcEnemy)) |
|
{ |
|
r = 0; |
|
g = 255; |
|
b = 0; |
|
} |
|
// Eluded enemy drawn in blue |
|
else if (eMemory->bEludedMe) |
|
{ |
|
r = 0; |
|
g = 0; |
|
b = 255; |
|
} |
|
// Current enemy drawn in red |
|
else if (npcEnemy == GetEnemy()) |
|
{ |
|
r = 255; |
|
g = 0; |
|
b = 0; |
|
} |
|
// Current traget drawn in magenta |
|
else if (npcEnemy == GetTarget()) |
|
{ |
|
r = 255; |
|
g = 0; |
|
b = 255; |
|
} |
|
// All other enemies drawn in pink |
|
else |
|
{ |
|
r = 255; |
|
g = 100; |
|
b = 100; |
|
} |
|
|
|
|
|
Vector drawPos = eMemory->vLastKnownLocation; |
|
NDebugOverlay::Text( drawPos, debugText, false, 0.0 ); |
|
|
|
// If has a line on the player draw cross slightly in front so player can see |
|
if (npcEnemy->IsPlayer() && |
|
(eMemory->vLastKnownLocation - npcEnemy->GetAbsOrigin()).Length()<10 ) |
|
{ |
|
Vector vEnemyFacing = npcEnemy->BodyDirection2D( ); |
|
Vector eyePos = npcEnemy->EyePosition() + vEnemyFacing*10.0; |
|
Vector upVec = Vector(0,0,2); |
|
Vector sideVec; |
|
CrossProduct( vEnemyFacing, upVec, sideVec); |
|
NDebugOverlay::Line(eyePos+sideVec+upVec, eyePos-sideVec-upVec, r,g,b, false,0); |
|
NDebugOverlay::Line(eyePos+sideVec-upVec, eyePos-sideVec+upVec, r,g,b, false,0); |
|
|
|
NDebugOverlay::Text( eyePos, debugText, false, 0.0 ); |
|
} |
|
else |
|
{ |
|
NDebugOverlay::Cross3D(drawPos,NAI_Hull::Mins(npcEnemy->GetHullType()),NAI_Hull::Maxs(npcEnemy->GetHullType()),r,g,b,false,0); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
// ---------------------------------------------- |
|
// Draw line to target and enemy entity if exist |
|
// ---------------------------------------------- |
|
if ((m_debugOverlays & OVERLAY_NPC_FOCUS_BIT)) |
|
{ |
|
if (GetEnemy() != NULL) |
|
{ |
|
NDebugOverlay::Line(EyePosition(),GetEnemy()->EyePosition(),255,0,0,true,0.0); |
|
} |
|
if (GetTarget() != NULL) |
|
{ |
|
NDebugOverlay::Line(EyePosition(),GetTarget()->EyePosition(),0,0,255,true,0.0); |
|
} |
|
} |
|
|
|
|
|
GetPathfinder()->DrawDebugGeometryOverlays(m_debugOverlays); |
|
|
|
CBaseEntity::DrawDebugGeometryOverlays(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Draw any debug text overlays |
|
// Input : |
|
// Output : Current text offset from the top |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::DrawDebugTextOverlays(void) |
|
{ |
|
int text_offset = 0; |
|
|
|
// --------------------- |
|
// Print Baseclass text |
|
// --------------------- |
|
text_offset = BaseClass::DrawDebugTextOverlays(); |
|
|
|
if (m_debugOverlays & OVERLAY_NPC_SQUAD_BIT) |
|
{ |
|
// Print health |
|
char tempstr[512]; |
|
Q_snprintf(tempstr,sizeof(tempstr),"Health: %i",m_iHealth.Get()); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
// Print squad name |
|
Q_strncpy(tempstr,"Squad: ",sizeof(tempstr)); |
|
if (m_pSquad) |
|
{ |
|
Q_strncat(tempstr,m_pSquad->GetName(),sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
|
|
if( m_pSquad->GetLeader() == this ) |
|
{ |
|
Q_strncat(tempstr," (LEADER)",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
|
|
Q_strncat(tempstr,"\n",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
else |
|
{ |
|
Q_strncat(tempstr," - \n",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
// Print enemy name |
|
Q_strncpy(tempstr,"Enemy: ",sizeof(tempstr)); |
|
if (GetEnemy()) |
|
{ |
|
if (GetEnemy()->GetEntityName() != NULL_STRING) |
|
{ |
|
Q_strncat(tempstr,STRING(GetEnemy()->GetEntityName()),sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
Q_strncat(tempstr,"\n",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
else |
|
{ |
|
Q_strncat(tempstr,STRING(GetEnemy()->m_iClassname),sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
Q_strncat(tempstr,"\n",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
} |
|
else |
|
{ |
|
Q_strncat(tempstr," - \n",sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
// Print slot |
|
Q_snprintf(tempstr,sizeof(tempstr),"Slot: %s (%d)\n", |
|
SquadSlotName(m_iMySquadSlot), m_iMySquadSlot); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
} |
|
|
|
if (m_debugOverlays & OVERLAY_TEXT_BIT) |
|
{ |
|
char tempstr[512]; |
|
// -------------- |
|
// Print Health |
|
// -------------- |
|
Q_snprintf(tempstr,sizeof(tempstr),"Health: %i (DACC:%1.2f)",m_iHealth.Get(), GetDamageAccumulator() ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
// -------------- |
|
// Print State |
|
// -------------- |
|
static const char *pStateNames[] = { "None", "Idle", "Alert", "Combat", "Scripted", "PlayDead", "Dead" }; |
|
if ( (int)m_NPCState < ARRAYSIZE(pStateNames) ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"Stat: %s, ", pStateNames[m_NPCState] ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
// ----------------- |
|
// Start Scripting? |
|
// ----------------- |
|
if( IsInAScript() ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"STARTSCRIPTING" ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
// ----------------- |
|
// Hint Group? |
|
// ----------------- |
|
if( GetHintGroup() != NULL_STRING ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"Hint Group: %s", STRING(GetHintGroup()) ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
// ----------------- |
|
// Print MotionType |
|
// ----------------- |
|
int navTypeIndex = (int)GetNavType() + 1; |
|
static const char *pMoveNames[] = { "None", "Ground", "Jump", "Fly", "Climb" }; |
|
Assert( navTypeIndex >= 0 && navTypeIndex < ARRAYSIZE(pMoveNames) ); |
|
if ( navTypeIndex < ARRAYSIZE(pMoveNames) ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"Move: %s, ", pMoveNames[navTypeIndex] ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
// -------------- |
|
// Print Schedule |
|
// -------------- |
|
if ( GetCurSchedule() ) |
|
{ |
|
CAI_BehaviorBase *pBehavior = GetRunningBehavior(); |
|
if ( pBehavior ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"Behv: %s, ", pBehavior->GetName() ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
const char *pName = NULL; |
|
pName = GetCurSchedule()->GetName(); |
|
if ( !pName ) |
|
{ |
|
pName = "Unknown"; |
|
} |
|
Q_snprintf(tempstr,sizeof(tempstr),"Schd: %s, ", pName ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
if (m_debugOverlays & OVERLAY_NPC_TASK_BIT) |
|
{ |
|
for (int i = 0 ; i < GetCurSchedule()->NumTasks(); i++) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"%s%s%s%s", |
|
((i==0) ? "Task:":" "), |
|
((i==GetScheduleCurTaskIndex()) ? "->" :" "), |
|
TaskName(GetCurSchedule()->GetTaskList()[i].iTask), |
|
((i==GetScheduleCurTaskIndex()) ? "<-" :"")); |
|
|
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
} |
|
else |
|
{ |
|
const Task_t *pTask = GetTask(); |
|
if ( pTask ) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"Task: %s (#%d), ", TaskName(pTask->iTask), GetScheduleCurTaskIndex() ); |
|
} |
|
else |
|
{ |
|
Q_strncpy(tempstr,"Task: None",sizeof(tempstr)); |
|
} |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
} |
|
|
|
// -------------- |
|
// Print Acitivity |
|
// -------------- |
|
if( m_Activity != ACT_INVALID && m_IdealActivity != ACT_INVALID && m_Activity != ACT_RESET) |
|
{ |
|
Activity iActivity = TranslateActivity( m_Activity ); |
|
|
|
Activity iIdealActivity = Weapon_TranslateActivity( m_IdealActivity ); |
|
iIdealActivity = NPC_TranslateActivity( iIdealActivity ); |
|
|
|
const char *pszActivity = GetActivityName( iActivity ); |
|
const char *pszIdealActivity = GetActivityName( iIdealActivity ); |
|
const char *pszRootActivity = GetActivityName( m_Activity ); |
|
|
|
Q_snprintf(tempstr,sizeof(tempstr),"Actv: %s (%s) [%s]\n", pszActivity, pszIdealActivity, pszRootActivity ); |
|
} |
|
else if (m_Activity == ACT_RESET) |
|
{ |
|
Q_strncpy(tempstr,"Actv: RESET",sizeof(tempstr) ); |
|
} |
|
else |
|
{ |
|
Q_strncpy(tempstr,"Actv: INVALID", sizeof(tempstr) ); |
|
} |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
|
|
// |
|
// Print all the current conditions. |
|
// |
|
if (m_debugOverlays & OVERLAY_NPC_CONDITIONS_BIT) |
|
{ |
|
bool bHasConditions = false; |
|
for (int i = 0; i < MAX_CONDITIONS; i++) |
|
{ |
|
if (m_Conditions.IsBitSet(i)) |
|
{ |
|
Q_snprintf(tempstr, sizeof(tempstr), "Cond: %s\n", ConditionName(AI_RemapToGlobal(i))); |
|
EntityText(text_offset, tempstr, 0); |
|
text_offset++; |
|
bHasConditions = true; |
|
} |
|
} |
|
if (!bHasConditions) |
|
{ |
|
Q_snprintf(tempstr,sizeof(tempstr),"(no conditions)"); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
} |
|
|
|
if ( GetFlags() & FL_FLY ) |
|
{ |
|
EntityText(text_offset,"HAS FL_FLY",0); |
|
text_offset++; |
|
} |
|
|
|
// -------------- |
|
// Print Interrupte |
|
// -------------- |
|
if (m_interuptSchedule) |
|
{ |
|
const char *pName = NULL; |
|
pName = m_interuptSchedule->GetName(); |
|
if ( !pName ) |
|
{ |
|
pName = "Unknown"; |
|
} |
|
|
|
Q_snprintf(tempstr,sizeof(tempstr),"Intr: %s (%s)\n", pName, m_interruptText ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
// -------------- |
|
// Print Failure |
|
// -------------- |
|
if (m_failedSchedule) |
|
{ |
|
const char *pName = NULL; |
|
pName = m_failedSchedule->GetName(); |
|
if ( !pName ) |
|
{ |
|
pName = "Unknown"; |
|
} |
|
Q_snprintf(tempstr,sizeof(tempstr),"Fail: %s (%s)\n", pName,m_failText ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
|
|
|
|
// ------------------------------- |
|
// Print any important condtions |
|
// ------------------------------- |
|
if (HasCondition(COND_ENEMY_TOO_FAR)) |
|
{ |
|
EntityText(text_offset,"Enemy too far to attack",0); |
|
text_offset++; |
|
} |
|
if ( GetAbsVelocity() != vec3_origin || GetLocalAngularVelocity() != vec3_angle ) |
|
{ |
|
char tmp[512]; |
|
Q_snprintf( tmp, sizeof(tmp), "Vel %.1f %.1f %.1f Ang: %.1f %.1f %.1f\n", |
|
GetAbsVelocity().x, GetAbsVelocity().y, GetAbsVelocity().z, |
|
GetLocalAngularVelocity().x, GetLocalAngularVelocity().y, GetLocalAngularVelocity().z ); |
|
EntityText(text_offset,tmp,0); |
|
text_offset++; |
|
} |
|
|
|
// ------------------------------- |
|
// Print shot accuracy |
|
// ------------------------------- |
|
if ( m_LastShootAccuracy != -1 && ai_shot_stats.GetBool() ) |
|
{ |
|
CFmtStr msg; |
|
EntityText(text_offset,msg.sprintf("Cur Accuracy: %.1f", m_LastShootAccuracy),0); |
|
text_offset++; |
|
if ( m_TotalShots ) |
|
{ |
|
EntityText(text_offset,msg.sprintf("Act Accuracy: %.1f", ((float)m_TotalHits/(float)m_TotalShots)*100.0),0); |
|
text_offset++; |
|
} |
|
|
|
if ( GetActiveWeapon() && GetEnemy() ) |
|
{ |
|
Vector curSpread = GetAttackSpread(GetActiveWeapon(), GetEnemy()); |
|
float curCone = RAD2DEG(asin(curSpread.x)) * 2; |
|
float bias = GetSpreadBias( GetActiveWeapon(), GetEnemy()); |
|
EntityText(text_offset,msg.sprintf("Cone %.1f, Bias %.2f", curCone, bias),0); |
|
text_offset++; |
|
} |
|
} |
|
|
|
if ( GetGoalEnt() && GetNavigator()->GetGoalType() == GOALTYPE_PATHCORNER ) |
|
{ |
|
Q_strncpy(tempstr,"Pathcorner/goal ent: ",sizeof(tempstr)); |
|
if (GetGoalEnt()->GetEntityName() != NULL_STRING) |
|
{ |
|
Q_strncat(tempstr,STRING(GetGoalEnt()->GetEntityName()),sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
else |
|
{ |
|
Q_strncat(tempstr,STRING(GetGoalEnt()->m_iClassname),sizeof(tempstr), COPY_ALL_CHARACTERS); |
|
} |
|
EntityText(text_offset, tempstr, 0); |
|
text_offset++; |
|
} |
|
|
|
if ( VPhysicsGetObject() ) |
|
{ |
|
vphysics_objectstress_t stressOut; |
|
CalculateObjectStress( VPhysicsGetObject(), this, &stressOut ); |
|
Q_snprintf(tempstr, sizeof(tempstr),"Stress: %.2f", stressOut.receivedStress ); |
|
EntityText(text_offset, tempstr, 0); |
|
text_offset++; |
|
} |
|
if ( m_pSquad ) |
|
{ |
|
if( m_pSquad->IsLeader(this) ) |
|
{ |
|
Q_snprintf(tempstr, sizeof(tempstr),"**Squad Leader**" ); |
|
EntityText(text_offset, tempstr, 0); |
|
text_offset++; |
|
} |
|
|
|
Q_snprintf(tempstr, sizeof(tempstr), "SquadSlot:%s", GetSquadSlotDebugName( GetMyStrategySlot() ) ); |
|
EntityText(text_offset, tempstr, 0); |
|
text_offset++; |
|
} |
|
} |
|
return text_offset; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Displays information in the console about the state of this npc. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ReportAIState( void ) |
|
{ |
|
static const char *pStateNames[] = { "None", "Idle", "Alert", "Combat", "Scripted", "PlayDead", "Dead" }; |
|
|
|
DevMsg( "%s: ", GetClassname() ); |
|
if ( (int)m_NPCState < ARRAYSIZE(pStateNames) ) |
|
DevMsg( "State: %s, ", pStateNames[m_NPCState] ); |
|
|
|
if( m_Activity != ACT_INVALID && m_IdealActivity != ACT_INVALID ) |
|
{ |
|
const char *pszActivity = GetActivityName(m_Activity); |
|
const char *pszIdealActivity = GetActivityName(m_IdealActivity); |
|
|
|
DevMsg( "Activity: %s - Ideal Activity: %s\n", pszActivity, pszIdealActivity ); |
|
} |
|
|
|
if ( GetCurSchedule() ) |
|
{ |
|
const char *pName = NULL; |
|
pName = GetCurSchedule()->GetName(); |
|
if ( !pName ) |
|
pName = "Unknown"; |
|
DevMsg( "Schedule %s, ", pName ); |
|
const Task_t *pTask = GetTask(); |
|
if ( pTask ) |
|
DevMsg( "Task %d (#%d), ", pTask->iTask, GetScheduleCurTaskIndex() ); |
|
} |
|
else |
|
DevMsg( "No Schedule, " ); |
|
|
|
if ( GetEnemy() != NULL ) |
|
{ |
|
g_pEffects->Sparks( GetEnemy()->GetAbsOrigin() + Vector( 0, 0, 64 ) ); |
|
DevMsg( "\nEnemy is %s", GetEnemy()->GetClassname() ); |
|
} |
|
else |
|
DevMsg( "No enemy " ); |
|
|
|
if ( IsMoving() ) |
|
{ |
|
DevMsg( " Moving " ); |
|
if ( m_flMoveWaitFinished > gpGlobals->curtime ) |
|
DevMsg( ": Stopped for %.2f. ", m_flMoveWaitFinished - gpGlobals->curtime ); |
|
else if ( m_IdealActivity == GetStoppedActivity() ) |
|
DevMsg( ": In stopped anim. " ); |
|
} |
|
|
|
DevMsg( "Leader." ); |
|
|
|
DevMsg( "\n" ); |
|
DevMsg( "Yaw speed:%3.1f,Health: %3d\n", GetMotor()->GetYawSpeed(), m_iHealth.Get() ); |
|
|
|
if ( GetGroundEntity() ) |
|
{ |
|
DevMsg( "Groundent:%s\n\n", GetGroundEntity()->GetClassname() ); |
|
} |
|
else |
|
{ |
|
DevMsg( "Groundent: NULL\n\n" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
ConVar ai_report_task_timings_on_limit( "ai_report_task_timings_on_limit", "0", FCVAR_ARCHIVE ); |
|
ConVar ai_think_limit_label( "ai_think_limit_label", "0", FCVAR_ARCHIVE ); |
|
|
|
void CAI_BaseNPC::ReportOverThinkLimit( float time ) |
|
{ |
|
DevMsg( "%s thinking for %.02fms!!! (%s); r%.2f (c%.2f, pst%.2f, ms%.2f), p-r%.2f, m%.2f\n", |
|
GetDebugName(), time, GetCurSchedule()->GetName(), |
|
g_AIRunTimer.GetDuration().GetMillisecondsF(), |
|
g_AIConditionsTimer.GetDuration().GetMillisecondsF(), |
|
g_AIPrescheduleThinkTimer.GetDuration().GetMillisecondsF(), |
|
g_AIMaintainScheduleTimer.GetDuration().GetMillisecondsF(), |
|
g_AIPostRunTimer.GetDuration().GetMillisecondsF(), |
|
g_AIMoveTimer.GetDuration().GetMillisecondsF() ); |
|
|
|
if (ai_think_limit_label.GetBool()) |
|
{ |
|
Vector tmp; |
|
CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &tmp ); |
|
tmp.z += 16; |
|
|
|
float max = -1; |
|
const char *pszMax = "unknown"; |
|
|
|
if ( g_AIConditionsTimer.GetDuration().GetMillisecondsF() > max ) |
|
{ |
|
max = g_AIConditionsTimer.GetDuration().GetMillisecondsF(); |
|
pszMax = "Conditions"; |
|
} |
|
if ( g_AIPrescheduleThinkTimer.GetDuration().GetMillisecondsF() > max ) |
|
{ |
|
max = g_AIPrescheduleThinkTimer.GetDuration().GetMillisecondsF(); |
|
pszMax = "Pre-think"; |
|
} |
|
if ( g_AIMaintainScheduleTimer.GetDuration().GetMillisecondsF() > max ) |
|
{ |
|
max = g_AIMaintainScheduleTimer.GetDuration().GetMillisecondsF(); |
|
pszMax = "Schedule"; |
|
} |
|
if ( g_AIPostRunTimer.GetDuration().GetMillisecondsF() > max ) |
|
{ |
|
max = g_AIPostRunTimer.GetDuration().GetMillisecondsF(); |
|
pszMax = "Post-run"; |
|
} |
|
if ( g_AIMoveTimer.GetDuration().GetMillisecondsF() > max ) |
|
{ |
|
max = g_AIMoveTimer.GetDuration().GetMillisecondsF(); |
|
pszMax = "Move"; |
|
} |
|
NDebugOverlay::Text( tmp, (char*)(const char *)CFmtStr( "Slow %.1f, %s %.1f ", time, pszMax, max ), false, 1 ); |
|
} |
|
|
|
if ( ai_report_task_timings_on_limit.GetBool() ) |
|
DumpTaskTimings(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns whether or not this npc can play the scripted sequence or AI |
|
// sequence that is trying to possess it. If DisregardState is set, the npc |
|
// will be sucked into the script no matter what state it is in. ONLY |
|
// Scripted AI ents should allow this. |
|
// Input : fDisregardNPCState - |
|
// interruptLevel - |
|
// eMode - If the function returns true, eMode will be one of the following values: |
|
// CAN_PLAY_NOW |
|
// CAN_PLAY_ENQUEUED |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CanPlaySequence_t CAI_BaseNPC::CanPlaySequence( bool fDisregardNPCState, int interruptLevel ) |
|
{ |
|
CanPlaySequence_t eReturn = CAN_PLAY_NOW; |
|
|
|
if ( m_hCine ) |
|
{ |
|
if ( !m_hCine->CanEnqueueAfter() ) |
|
{ |
|
// npc is already running one scripted sequence and has an important script to play next |
|
return CANNOT_PLAY; |
|
} |
|
|
|
eReturn = CAN_PLAY_ENQUEUED; |
|
} |
|
|
|
if ( !IsAlive() ) |
|
{ |
|
// npc is dead! |
|
return CANNOT_PLAY; |
|
} |
|
|
|
// An NPC in a vehicle cannot play a scripted sequence |
|
if ( IsInAVehicle() ) |
|
return CANNOT_PLAY; |
|
|
|
if ( fDisregardNPCState ) |
|
{ |
|
// ok to go, no matter what the npc state. (scripted AI) |
|
|
|
// No clue as to how to proced if they're climbing or jumping |
|
// Assert( GetNavType() != NAV_JUMP && GetNavType() != NAV_CLIMB ); |
|
|
|
return eReturn; |
|
} |
|
|
|
if ( m_NPCState == NPC_STATE_NONE || m_NPCState == NPC_STATE_IDLE || m_IdealNPCState == NPC_STATE_IDLE ) |
|
{ |
|
// ok to go, but only in these states |
|
return eReturn; |
|
} |
|
|
|
if ( m_NPCState == NPC_STATE_ALERT && interruptLevel >= SS_INTERRUPT_BY_NAME ) |
|
{ |
|
return eReturn; |
|
} |
|
|
|
// unknown situation |
|
return CANNOT_PLAY; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::SetHintGroup( string_t newGroup, bool bHintGroupNavLimiting ) |
|
{ |
|
string_t oldGroup = m_strHintGroup; |
|
m_strHintGroup = newGroup; |
|
m_bHintGroupNavLimiting = bHintGroupNavLimiting; |
|
|
|
if ( oldGroup != newGroup ) |
|
OnChangeHintGroup( oldGroup, newGroup ); |
|
|
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::GetShootEnemyDir( const Vector &shootOrigin, bool bNoisy ) |
|
{ |
|
CBaseEntity *pEnemy = GetEnemy(); |
|
|
|
if ( pEnemy ) |
|
{ |
|
Vector vecEnemyLKP = GetEnemyLKP(); |
|
|
|
Vector vecEnemyOffset = pEnemy->BodyTarget( shootOrigin, bNoisy ) - pEnemy->GetAbsOrigin(); |
|
|
|
#ifdef PORTAL |
|
// Translate the enemy's position across the portals if it's only seen in the portal view cone |
|
if ( !FInViewCone( vecEnemyLKP ) || !FVisible( vecEnemyLKP ) ) |
|
{ |
|
CProp_Portal *pPortal = FInViewConeThroughPortal( vecEnemyLKP ); |
|
if ( pPortal ) |
|
{ |
|
UTIL_Portal_VectorTransform( pPortal->m_hLinkedPortal->MatrixThisToLinked(), vecEnemyOffset, vecEnemyOffset ); |
|
UTIL_Portal_PointTransform( pPortal->m_hLinkedPortal->MatrixThisToLinked(), vecEnemyLKP, vecEnemyLKP ); |
|
} |
|
} |
|
#endif |
|
|
|
Vector retval = vecEnemyOffset + vecEnemyLKP - shootOrigin; |
|
VectorNormalize( retval ); |
|
return retval; |
|
} |
|
else |
|
{ |
|
Vector forward; |
|
AngleVectors( GetLocalAngles(), &forward ); |
|
return forward; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Simulates many times and reports statistical accuracy. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CollectShotStats( const Vector &vecShootOrigin, const Vector &vecShootDir ) |
|
{ |
|
#ifdef HL2_DLL |
|
if( ai_shot_stats.GetBool() != 0 && GetEnemy()->IsPlayer() ) |
|
{ |
|
int iterations = ai_shot_stats_term.GetInt(); |
|
int iHits = 0; |
|
Vector testDir = vecShootDir; |
|
|
|
CShotManipulator manipulator( testDir ); |
|
|
|
for( int i = 0 ; i < iterations ; i++ ) |
|
{ |
|
// Apply appropriate accuracy. |
|
manipulator.ApplySpread( GetAttackSpread( GetActiveWeapon(), GetEnemy() ), GetSpreadBias( GetActiveWeapon(), GetEnemy() ) ); |
|
Vector shotDir = manipulator.GetResult(); |
|
|
|
Vector vecEnd = vecShootOrigin + shotDir * 8192; |
|
|
|
trace_t tr; |
|
AI_TraceLine( vecShootOrigin, vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); |
|
|
|
if( tr.m_pEnt && tr.m_pEnt == GetEnemy() ) |
|
{ |
|
iHits++; |
|
} |
|
Vector vecProjectedPosition = GetActualShootPosition( vecShootOrigin ); |
|
Vector testDir = vecProjectedPosition - vecShootOrigin; |
|
VectorNormalize( testDir ); |
|
manipulator.SetShootDir( testDir ); |
|
} |
|
|
|
float flHitPercent = ((float)iHits / (float)iterations) * 100.0; |
|
m_LastShootAccuracy = flHitPercent; |
|
//DevMsg("Shots:%d Hits:%d Percentage:%.1f\n", iterations, iHits, flHitPercent); |
|
} |
|
else |
|
{ |
|
m_LastShootAccuracy = -1; |
|
} |
|
#endif |
|
} |
|
|
|
#ifdef HL2_DLL |
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return the actual position the NPC wants to fire at when it's trying |
|
// to hit it's current enemy. |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::GetActualShootPosition( const Vector &shootOrigin ) |
|
{ |
|
// Project the target's location into the future. |
|
Vector vecEnemyLKP = GetEnemyLKP(); |
|
Vector vecEnemyOffset = GetEnemy()->BodyTarget( shootOrigin ) - GetEnemy()->GetAbsOrigin(); |
|
Vector vecTargetPosition = vecEnemyOffset + vecEnemyLKP; |
|
|
|
#ifdef PORTAL |
|
// Check if it's also visible through portals |
|
CProp_Portal *pPortal = FInViewConeThroughPortal( vecEnemyLKP ); |
|
if ( pPortal ) |
|
{ |
|
// Get the target's position through portals |
|
Vector vecEnemyOffsetTransformed; |
|
Vector vecEnemyLKPTransformed; |
|
UTIL_Portal_VectorTransform( pPortal->m_hLinkedPortal->MatrixThisToLinked(), vecEnemyOffset, vecEnemyOffsetTransformed ); |
|
UTIL_Portal_PointTransform( pPortal->m_hLinkedPortal->MatrixThisToLinked(), vecEnemyLKP, vecEnemyLKPTransformed ); |
|
Vector vecTargetPositionTransformed = vecEnemyOffsetTransformed + vecEnemyLKPTransformed; |
|
|
|
// Get the distance to the target with and without portals |
|
float fDistanceToEnemyThroughPortalSqr = GetAbsOrigin().DistToSqr( vecTargetPositionTransformed ); |
|
float fDistanceToEnemySqr = GetAbsOrigin().DistToSqr( vecTargetPosition ); |
|
|
|
if ( fDistanceToEnemyThroughPortalSqr < fDistanceToEnemySqr || !FInViewCone( vecEnemyLKP ) || !FVisible( vecEnemyLKP ) ) |
|
{ |
|
// We're better off shooting through the portals |
|
vecTargetPosition = vecTargetPositionTransformed; |
|
} |
|
} |
|
#endif |
|
|
|
// lead for some fraction of a second. |
|
return (vecTargetPosition + ( GetEnemy()->GetSmoothedVelocity() * ai_lead_time.GetFloat() )); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseNPC::GetSpreadBias( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ) |
|
{ |
|
float bias = BaseClass::GetSpreadBias( pWeapon, pTarget ); |
|
AI_EnemyInfo_t *pEnemyInfo = m_pEnemies->Find( pTarget ); |
|
if ( ai_shot_bias.GetFloat() != 1.0 ) |
|
bias = ai_shot_bias.GetFloat(); |
|
if ( pEnemyInfo ) |
|
{ |
|
float timeToFocus = ai_spread_pattern_focus_time.GetFloat(); |
|
if ( timeToFocus > 0.0 ) |
|
{ |
|
float timeSinceValidEnemy = gpGlobals->curtime - pEnemyInfo->timeValidEnemy; |
|
if ( timeSinceValidEnemy < 0.0f ) |
|
{ |
|
timeSinceValidEnemy = 0.0f; |
|
} |
|
float timeSinceReacquire = gpGlobals->curtime - pEnemyInfo->timeLastReacquired; |
|
if ( timeSinceValidEnemy < timeToFocus ) |
|
{ |
|
float scale = timeSinceValidEnemy / timeToFocus; |
|
Assert( scale >= 0.0 && scale <= 1.0 ); |
|
bias *= scale; |
|
} |
|
else if ( timeSinceReacquire < timeToFocus ) // handled seperately as might be tuning seperately |
|
{ |
|
float scale = timeSinceReacquire / timeToFocus; |
|
Assert( scale >= 0.0 && scale <= 1.0 ); |
|
bias *= scale; |
|
} |
|
|
|
} |
|
} |
|
return bias; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::GetAttackSpread( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ) |
|
{ |
|
Vector baseResult = BaseClass::GetAttackSpread( pWeapon, pTarget ); |
|
|
|
AI_EnemyInfo_t *pEnemyInfo = m_pEnemies->Find( pTarget ); |
|
if ( pEnemyInfo ) |
|
{ |
|
float timeToFocus = ai_spread_cone_focus_time.GetFloat(); |
|
if ( timeToFocus > 0.0 ) |
|
{ |
|
float timeSinceValidEnemy = gpGlobals->curtime - pEnemyInfo->timeValidEnemy; |
|
if ( timeSinceValidEnemy < 0 ) |
|
timeSinceValidEnemy = 0; |
|
if ( timeSinceValidEnemy < timeToFocus ) |
|
{ |
|
float coneMultiplier = ai_spread_defocused_cone_multiplier.GetFloat(); |
|
if ( coneMultiplier > 1.0 ) |
|
{ |
|
float scale = 1.0 - timeSinceValidEnemy / timeToFocus; |
|
Assert( scale >= 0.0 && scale <= 1.0 ); |
|
float multiplier = ( (coneMultiplier - 1.0) * scale ) + 1.0; |
|
baseResult *= multiplier; |
|
} |
|
} |
|
} |
|
} |
|
return baseResult; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Similar to calling GetShootEnemyDir, but returns the exact trajectory to |
|
// fire the bullet along, after calculating for target speed, location, |
|
// concealment, etc. |
|
// |
|
// Ultimately, this code results in the shooter aiming his weapon somewhere ahead of |
|
// a moving target. Since bullet traces are instant, aiming ahead of a target |
|
// will result in more misses than hits. This is designed to provide targets with |
|
// a bonus when moving perpendicular to the shooter's line of sight. |
|
// |
|
// Do not confuse this with leading a target in an effort to more easily hit it. |
|
// |
|
// This code PENALIZES a target for moving directly along the shooter's line of sight. |
|
// |
|
// This code REWARDS a target for moving perpendicular to the shooter's line of sight. |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::GetActualShootTrajectory( const Vector &shootOrigin ) |
|
{ |
|
if( !GetEnemy() ) |
|
return GetShootEnemyDir( shootOrigin ); |
|
|
|
// If we're above water shooting at a player underwater, bias some of the shots forward of |
|
// the player so that they see the cool bubble trails in the water ahead of them. |
|
if (GetEnemy()->IsPlayer() && (GetWaterLevel() != 3) && (GetEnemy()->GetWaterLevel() == 3)) |
|
{ |
|
#if 1 |
|
if (random->RandomInt(0, 4) < 3) |
|
{ |
|
Vector vecEnemyForward; |
|
GetEnemy()->GetVectors( &vecEnemyForward, NULL, NULL ); |
|
vecEnemyForward.z = 0; |
|
|
|
// Lead up to a second ahead of them unless they are moving backwards. |
|
Vector vecEnemyVelocity = GetEnemy()->GetSmoothedVelocity(); |
|
VectorNormalize( vecEnemyVelocity ); |
|
float flVelocityScale = vecEnemyForward.Dot( vecEnemyVelocity ); |
|
if ( flVelocityScale < 0.0f ) |
|
{ |
|
flVelocityScale = 0.0f; |
|
} |
|
|
|
Vector vecAimPos = GetEnemy()->EyePosition() + ( 48.0f * vecEnemyForward ) + (flVelocityScale * GetEnemy()->GetSmoothedVelocity() ); |
|
//NDebugOverlay::Cross3D(vecAimPos, Vector(-16,-16,-16), Vector(16,16,16), 255, 255, 0, true, 1.0f ); |
|
|
|
//vecAimPos.z = UTIL_WaterLevel( vecAimPos, vecAimPos.z, vecAimPos.z + 400.0f ); |
|
//NDebugOverlay::Cross3D(vecAimPos, Vector(-32,-32,-32), Vector(32,32,32), 255, 0, 0, true, 1.0f ); |
|
|
|
Vector vecShotDir = vecAimPos - shootOrigin; |
|
VectorNormalize( vecShotDir ); |
|
return vecShotDir; |
|
} |
|
#else |
|
if (random->RandomInt(0, 4) < 3) |
|
{ |
|
// Aim at a point a few feet in front of the player's eyes |
|
Vector vecEnemyForward; |
|
GetEnemy()->GetVectors( &vecEnemyForward, NULL, NULL ); |
|
|
|
Vector vecAimPos = GetEnemy()->EyePosition() + (120.0f * vecEnemyForward ); |
|
|
|
Vector vecShotDir = vecAimPos - shootOrigin; |
|
VectorNormalize( vecShotDir ); |
|
|
|
CShotManipulator manipulator( vecShotDir ); |
|
manipulator.ApplySpread( VECTOR_CONE_10DEGREES, 1 ); |
|
vecShotDir = manipulator.GetResult(); |
|
|
|
return vecShotDir; |
|
} |
|
#endif |
|
} |
|
|
|
Vector vecProjectedPosition = GetActualShootPosition( shootOrigin ); |
|
|
|
Vector shotDir = vecProjectedPosition - shootOrigin; |
|
VectorNormalize( shotDir ); |
|
|
|
CollectShotStats( shootOrigin, shotDir ); |
|
|
|
// NOW we have a shoot direction. Where a 100% accurate bullet should go. |
|
// Modify it by weapon proficiency. |
|
// construct a manipulator |
|
CShotManipulator manipulator( shotDir ); |
|
|
|
// Apply appropriate accuracy. |
|
bool bUsePerfectAccuracy = false; |
|
if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) |
|
{ |
|
CNPC_Bullseye *pBullseye = dynamic_cast<CNPC_Bullseye*>(GetEnemy()); |
|
if ( pBullseye && pBullseye->UsePerfectAccuracy() ) |
|
{ |
|
bUsePerfectAccuracy = true; |
|
} |
|
} |
|
|
|
if ( !bUsePerfectAccuracy ) |
|
{ |
|
manipulator.ApplySpread( GetAttackSpread( GetActiveWeapon(), GetEnemy() ), GetSpreadBias( GetActiveWeapon(), GetEnemy() ) ); |
|
shotDir = manipulator.GetResult(); |
|
} |
|
|
|
// Look for an opportunity to make misses hit interesting things. |
|
CBaseCombatCharacter *pEnemy; |
|
|
|
pEnemy = GetEnemy()->MyCombatCharacterPointer(); |
|
|
|
if( pEnemy && pEnemy->ShouldShootMissTarget( this ) ) |
|
{ |
|
Vector vecEnd = shootOrigin + shotDir * 8192; |
|
trace_t tr; |
|
|
|
AI_TraceLine(shootOrigin, vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); |
|
|
|
if( tr.fraction != 1.0 && tr.m_pEnt && tr.m_pEnt->m_takedamage != DAMAGE_NO ) |
|
{ |
|
// Hit something we can harm. Just shoot it. |
|
return manipulator.GetResult(); |
|
} |
|
|
|
// Find something interesting around the enemy to shoot instead of just missing. |
|
CBaseEntity *pMissTarget = pEnemy->FindMissTarget(); |
|
|
|
if( pMissTarget ) |
|
{ |
|
shotDir = pMissTarget->WorldSpaceCenter() - shootOrigin; |
|
VectorNormalize( shotDir ); |
|
} |
|
} |
|
|
|
return shotDir; |
|
} |
|
#endif // HL2_DLL |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
Vector CAI_BaseNPC::BodyTarget( const Vector &posSrc, bool bNoisy ) |
|
{ |
|
Vector low = WorldSpaceCenter() - ( WorldSpaceCenter() - GetAbsOrigin() ) * .25; |
|
Vector high = EyePosition(); |
|
Vector delta = high - low; |
|
Vector result; |
|
if ( bNoisy ) |
|
{ |
|
// bell curve |
|
float rand1 = random->RandomFloat( 0.0, 0.5 ); |
|
float rand2 = random->RandomFloat( 0.0, 0.5 ); |
|
result = low + delta * rand1 + delta * rand2; |
|
} |
|
else |
|
result = low + delta * 0.5; |
|
|
|
return result; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::ShouldMoveAndShoot() |
|
{ |
|
return ( ( CapabilitiesGet() & bits_CAP_MOVE_SHOOT ) != 0 ); |
|
} |
|
|
|
|
|
//========================================================= |
|
// FacingIdeal - tells us if a npc is facing its ideal |
|
// yaw. Created this function because many spots in the |
|
// code were checking the yawdiff against this magic |
|
// number. Nicer to have it in one place if we're gonna |
|
// be stuck with it. |
|
//========================================================= |
|
bool CAI_BaseNPC::FacingIdeal( void ) |
|
{ |
|
if ( fabs( GetMotor()->DeltaIdealYaw() ) <= 0.006 )//!!!BUGBUG - no magic numbers!!! |
|
{ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//--------------------------------- |
|
|
|
void CAI_BaseNPC::AddFacingTarget( CBaseEntity *pTarget, float flImportance, float flDuration, float flRamp ) |
|
{ |
|
GetMotor()->AddFacingTarget( pTarget, flImportance, flDuration, flRamp ); |
|
} |
|
|
|
void CAI_BaseNPC::AddFacingTarget( const Vector &vecPosition, float flImportance, float flDuration, float flRamp ) |
|
{ |
|
GetMotor()->AddFacingTarget( vecPosition, flImportance, flDuration, flRamp ); |
|
} |
|
|
|
void CAI_BaseNPC::AddFacingTarget( CBaseEntity *pTarget, const Vector &vecPosition, float flImportance, float flDuration, float flRamp ) |
|
{ |
|
GetMotor()->AddFacingTarget( pTarget, vecPosition, flImportance, flDuration, flRamp ); |
|
} |
|
|
|
float CAI_BaseNPC::GetFacingDirection( Vector &vecDir ) |
|
{ |
|
return (GetMotor()->GetFacingDirection( vecDir )); |
|
} |
|
|
|
|
|
//--------------------------------- |
|
|
|
|
|
int CAI_BaseNPC::PlaySentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, CBaseEntity *pListener ) |
|
{ |
|
int sentenceIndex = -1; |
|
|
|
if ( pszSentence && IsAlive() ) |
|
{ |
|
|
|
if ( pszSentence[0] == '!' ) |
|
{ |
|
sentenceIndex = SENTENCEG_Lookup( pszSentence ); |
|
CPASAttenuationFilter filter( this, soundlevel ); |
|
CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, volume, soundlevel, 0, PITCH_NORM ); |
|
} |
|
else |
|
{ |
|
sentenceIndex = SENTENCEG_PlayRndSz( edict(), pszSentence, volume, soundlevel, 0, PITCH_NORM ); |
|
} |
|
} |
|
|
|
return sentenceIndex; |
|
} |
|
|
|
|
|
int CAI_BaseNPC::PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ) |
|
{ |
|
return PlaySentence( pszSentence, delay, volume, soundlevel, pListener ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CBaseEntity *CAI_BaseNPC::FindNamedEntity( const char *name, IEntityFindFilter *pFilter ) |
|
{ |
|
if ( !stricmp( name, "!player" )) |
|
{ |
|
return ( CBaseEntity * )AI_GetSinglePlayer(); |
|
} |
|
else if ( !stricmp( name, "!enemy" ) ) |
|
{ |
|
if (GetEnemy() != NULL) |
|
return GetEnemy(); |
|
} |
|
else if ( !stricmp( name, "!self" ) || !stricmp( name, "!target1" ) ) |
|
{ |
|
return this; |
|
} |
|
else if ( !stricmp( name, "!nearestfriend" ) || !stricmp( name, "!friend" ) ) |
|
{ |
|
// FIXME: look at CBaseEntity *CNPCSimpleTalker::FindNearestFriend(bool fPlayer) |
|
// punt for now |
|
return ( CBaseEntity * )AI_GetSinglePlayer(); |
|
} |
|
else if (!stricmp( name, "self" )) |
|
{ |
|
static int selfwarningcount = 0; |
|
|
|
// fix the vcd, the reserved names have changed |
|
if ( ++selfwarningcount < 5 ) |
|
{ |
|
DevMsg( "ERROR: \"self\" is no longer used, use \"!self\" in vcd instead!\n" ); |
|
} |
|
return this; |
|
} |
|
else if ( !stricmp( name, "Player" )) |
|
{ |
|
static int playerwarningcount = 0; |
|
if ( ++playerwarningcount < 5 ) |
|
{ |
|
DevMsg( "ERROR: \"player\" is no longer used, use \"!player\" in vcd instead!\n" ); |
|
} |
|
return ( CBaseEntity * )AI_GetSinglePlayer(); |
|
} |
|
else |
|
{ |
|
// search for up to 32 entities with the same name and choose one randomly |
|
CBaseEntity *entityList[ FINDNAMEDENTITY_MAX_ENTITIES ]; |
|
CBaseEntity *entity; |
|
int iCount; |
|
|
|
entity = NULL; |
|
for( iCount = 0; iCount < FINDNAMEDENTITY_MAX_ENTITIES; iCount++ ) |
|
{ |
|
entity = gEntList.FindEntityByName( entity, name, NULL, NULL, NULL, pFilter ); |
|
if ( !entity ) |
|
{ |
|
break; |
|
} |
|
entityList[ iCount ] = entity; |
|
} |
|
|
|
if ( iCount > 0 ) |
|
{ |
|
int index = RandomInt( 0, iCount - 1 ); |
|
entity = entityList[ index ]; |
|
return entity; |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
|
|
void CAI_BaseNPC::CorpseFallThink( void ) |
|
{ |
|
if ( GetFlags() & FL_ONGROUND ) |
|
{ |
|
SetThink ( NULL ); |
|
|
|
SetSequenceBox( ); |
|
} |
|
else |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
} |
|
} |
|
|
|
// Call after animation/pose is set up |
|
void CAI_BaseNPC::NPCInitDead( void ) |
|
{ |
|
InitBoneControllers(); |
|
|
|
RemoveSolidFlags( FSOLID_NOT_SOLID ); |
|
|
|
// so he'll fall to ground |
|
SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); |
|
|
|
SetCycle( 0 ); |
|
ResetSequenceInfo( ); |
|
m_flPlaybackRate = 0; |
|
|
|
// Copy health |
|
m_iMaxHealth = m_iHealth; |
|
m_lifeState = LIFE_DEAD; |
|
|
|
UTIL_SetSize(this, vec3_origin, vec3_origin ); |
|
|
|
// Setup health counters, etc. |
|
SetThink( &CAI_BaseNPC::CorpseFallThink ); |
|
|
|
SetNextThink( gpGlobals->curtime + 0.5f ); |
|
} |
|
|
|
//========================================================= |
|
// BBoxIsFlat - check to see if the npc's bounding box |
|
// is lying flat on a surface (traces from all four corners |
|
// are same length.) |
|
//========================================================= |
|
bool CAI_BaseNPC::BBoxFlat ( void ) |
|
{ |
|
trace_t tr; |
|
Vector vecPoint; |
|
float flXSize, flYSize; |
|
float flLength; |
|
float flLength2; |
|
|
|
flXSize = WorldAlignSize().x / 2; |
|
flYSize = WorldAlignSize().y / 2; |
|
|
|
vecPoint.x = GetAbsOrigin().x + flXSize; |
|
vecPoint.y = GetAbsOrigin().y + flYSize; |
|
vecPoint.z = GetAbsOrigin().z; |
|
|
|
AI_TraceLine ( vecPoint, vecPoint - Vector ( 0, 0, 100 ), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
flLength = (vecPoint - tr.endpos).Length(); |
|
|
|
vecPoint.x = GetAbsOrigin().x - flXSize; |
|
vecPoint.y = GetAbsOrigin().y - flYSize; |
|
|
|
AI_TraceLine ( vecPoint, vecPoint - Vector ( 0, 0, 100 ), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
flLength2 = (vecPoint - tr.endpos).Length(); |
|
if ( flLength2 > flLength ) |
|
{ |
|
return false; |
|
} |
|
flLength = flLength2; |
|
|
|
vecPoint.x = GetAbsOrigin().x - flXSize; |
|
vecPoint.y = GetAbsOrigin().y + flYSize; |
|
AI_TraceLine ( vecPoint, vecPoint - Vector ( 0, 0, 100 ), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
flLength2 = (vecPoint - tr.endpos).Length(); |
|
if ( flLength2 > flLength ) |
|
{ |
|
return false; |
|
} |
|
flLength = flLength2; |
|
|
|
vecPoint.x = GetAbsOrigin().x + flXSize; |
|
vecPoint.y = GetAbsOrigin().y - flYSize; |
|
AI_TraceLine ( vecPoint, vecPoint - Vector ( 0, 0, 100 ), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
flLength2 = (vecPoint - tr.endpos).Length(); |
|
if ( flLength2 > flLength ) |
|
{ |
|
return false; |
|
} |
|
flLength = flLength2; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pEnemy - |
|
// bSetCondNewEnemy - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::SetEnemy( CBaseEntity *pEnemy, bool bSetCondNewEnemy ) |
|
{ |
|
if (m_hEnemy != pEnemy) |
|
{ |
|
ClearAttackConditions( ); |
|
VacateStrategySlot(); |
|
m_GiveUpOnDeadEnemyTimer.Stop(); |
|
|
|
// If we've just found a new enemy, set the condition |
|
if ( pEnemy && bSetCondNewEnemy ) |
|
{ |
|
SetCondition( COND_NEW_ENEMY ); |
|
} |
|
} |
|
|
|
// Assert( (pEnemy == NULL) || (m_NPCState == NPC_STATE_COMBAT) ); |
|
|
|
m_hEnemy = pEnemy; |
|
m_flTimeEnemyAcquired = gpGlobals->curtime; |
|
|
|
m_LastShootAccuracy = -1; |
|
m_TotalShots = 0; |
|
m_TotalHits = 0; |
|
|
|
if ( !pEnemy ) |
|
ClearCondition( COND_NEW_ENEMY ); |
|
} |
|
|
|
const Vector &CAI_BaseNPC::GetEnemyLKP() const |
|
{ |
|
return (const_cast<CAI_BaseNPC *>(this))->GetEnemies()->LastKnownPosition( GetEnemy() ); |
|
} |
|
|
|
float CAI_BaseNPC::GetEnemyLastTimeSeen() const |
|
{ |
|
return (const_cast<CAI_BaseNPC *>(this))->GetEnemies()->LastTimeSeen( GetEnemy() ); |
|
} |
|
|
|
void CAI_BaseNPC::MarkEnemyAsEluded() |
|
{ |
|
GetEnemies()->MarkAsEluded( GetEnemy() ); |
|
} |
|
|
|
void CAI_BaseNPC::ClearEnemyMemory() |
|
{ |
|
GetEnemies()->ClearMemory( GetEnemy() ); |
|
} |
|
|
|
bool CAI_BaseNPC::EnemyHasEludedMe() const |
|
{ |
|
return (const_cast<CAI_BaseNPC *>(this))->GetEnemies()->HasEludedMe( GetEnemy() ); |
|
} |
|
|
|
void CAI_BaseNPC::SetTarget( CBaseEntity *pTarget ) |
|
{ |
|
m_hTargetEnt = pTarget; |
|
} |
|
|
|
|
|
//========================================================= |
|
// Choose Enemy - tries to find the best suitable enemy for the npc. |
|
//========================================================= |
|
|
|
bool CAI_BaseNPC::ShouldChooseNewEnemy() |
|
{ |
|
CBaseEntity *pEnemy = GetEnemy(); |
|
if ( pEnemy ) |
|
{ |
|
if ( GetEnemies()->GetSerialNumber() != m_EnemiesSerialNumber ) |
|
{ |
|
return true; |
|
} |
|
|
|
m_EnemiesSerialNumber = GetEnemies()->GetSerialNumber(); |
|
|
|
if ( EnemyHasEludedMe() || (IRelationType( pEnemy ) != D_HT && IRelationType( pEnemy ) != D_FR) || !IsValidEnemy( pEnemy ) ) |
|
{ |
|
DbgEnemyMsg( this, "ShouldChooseNewEnemy() --> true (1)\n" ); |
|
return true; |
|
} |
|
if ( HasCondition(COND_SEE_HATE) || HasCondition(COND_SEE_DISLIKE) || HasCondition(COND_SEE_NEMESIS) || HasCondition(COND_SEE_FEAR) ) |
|
{ |
|
DbgEnemyMsg( this, "ShouldChooseNewEnemy() --> true (2)\n" ); |
|
return true; |
|
} |
|
if ( !pEnemy->IsAlive() ) |
|
{ |
|
if ( m_GiveUpOnDeadEnemyTimer.IsRunning() ) |
|
{ |
|
if ( m_GiveUpOnDeadEnemyTimer.Expired() ) |
|
{ |
|
DbgEnemyMsg( this, "ShouldChooseNewEnemy() --> true (3)\n" ); |
|
return true; |
|
} |
|
} |
|
else |
|
m_GiveUpOnDeadEnemyTimer.Start(); |
|
} |
|
|
|
AI_EnemyInfo_t *pInfo = GetEnemies()->Find( pEnemy ); |
|
|
|
if ( m_FailChooseEnemyTimer.Expired() ) |
|
{ |
|
m_FailChooseEnemyTimer.Set( 1.5 ); |
|
if ( HasCondition( COND_TASK_FAILED ) || |
|
( pInfo && ( pInfo->timeAtFirstHand == AI_INVALID_TIME || gpGlobals->curtime - pInfo->timeLastSeen > 10 ) ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
if ( pInfo && pInfo->timeValidEnemy < gpGlobals->curtime ) |
|
{ |
|
DbgEnemyMsg( this, "ShouldChooseNewEnemy() --> false\n" ); |
|
return false; |
|
} |
|
} |
|
|
|
DbgEnemyMsg( this, "ShouldChooseNewEnemy() --> true (4)\n" ); |
|
m_EnemiesSerialNumber = GetEnemies()->GetSerialNumber(); |
|
|
|
return true; |
|
} |
|
|
|
//------------------------------------- |
|
|
|
bool CAI_BaseNPC::ChooseEnemy( void ) |
|
{ |
|
AI_PROFILE_SCOPE(CAI_Enemies_ChooseEnemy); |
|
|
|
DbgEnemyMsg( this, "ChooseEnemy() {\n" ); |
|
|
|
//--------------------------------- |
|
// |
|
// Gather initial conditions |
|
// |
|
|
|
CBaseEntity *pInitialEnemy = GetEnemy(); |
|
CBaseEntity *pChosenEnemy = pInitialEnemy; |
|
|
|
// Use memory bits in case enemy pointer altered outside this function, (e.g., ehandle goes NULL) |
|
bool fHadEnemy = ( HasMemory( bits_MEMORY_HAD_ENEMY | bits_MEMORY_HAD_PLAYER ) ); |
|
bool fEnemyWasPlayer = HasMemory( bits_MEMORY_HAD_PLAYER ); |
|
bool fEnemyWentNull = ( fHadEnemy && !pInitialEnemy ); |
|
bool fEnemyEluded = ( fEnemyWentNull || ( pInitialEnemy && GetEnemies()->HasEludedMe( pInitialEnemy ) ) ); |
|
|
|
//--------------------------------- |
|
// |
|
// Establish suitability of choosing a new enemy |
|
// |
|
|
|
bool fHaveCondNewEnemy; |
|
bool fHaveCondLostEnemy; |
|
|
|
if ( !m_ScheduleState.bScheduleWasInterrupted && GetCurSchedule() && !FScheduleDone() ) |
|
{ |
|
Assert( InterruptFromCondition( COND_NEW_ENEMY ) == COND_NEW_ENEMY && InterruptFromCondition( COND_LOST_ENEMY ) == COND_LOST_ENEMY ); |
|
fHaveCondNewEnemy = GetCurSchedule()->HasInterrupt( COND_NEW_ENEMY ); |
|
fHaveCondLostEnemy = GetCurSchedule()->HasInterrupt( COND_LOST_ENEMY ); |
|
|
|
// See if they've been added as a custom interrupt |
|
if ( !fHaveCondNewEnemy ) |
|
{ |
|
fHaveCondNewEnemy = IsCustomInterruptConditionSet( COND_NEW_ENEMY ); |
|
} |
|
if ( !fHaveCondLostEnemy ) |
|
{ |
|
fHaveCondLostEnemy = IsCustomInterruptConditionSet( COND_LOST_ENEMY ); |
|
} |
|
} |
|
else |
|
{ |
|
fHaveCondNewEnemy = true; // not having a schedule is the same as being interruptable by any condition |
|
fHaveCondLostEnemy = true; |
|
} |
|
|
|
if ( !fEnemyWentNull ) |
|
{ |
|
if ( !fHaveCondNewEnemy && !( fHaveCondLostEnemy && fEnemyEluded ) ) |
|
{ |
|
// DO NOT mess with the npc's enemy pointer unless the schedule the npc is currently |
|
// running will be interrupted by COND_NEW_ENEMY or COND_LOST_ENEMY. This will |
|
// eliminate the problem of npcs getting a new enemy while they are in a schedule |
|
// that doesn't care, and then not realizing it by the time they get to a schedule |
|
// that does. I don't feel this is a good permanent fix. |
|
m_bSkippedChooseEnemy = true; |
|
|
|
DbgEnemyMsg( this, "Skipped enemy selection due to schedule restriction\n" ); |
|
DbgEnemyMsg( this, "}\n" ); |
|
return ( pChosenEnemy != NULL ); |
|
} |
|
} |
|
else if ( !fHaveCondNewEnemy && !fHaveCondLostEnemy && GetCurSchedule() ) |
|
{ |
|
DevMsg( 2, "WARNING: AI enemy went NULL but schedule (%s) is not interested\n", GetCurSchedule()->GetName() ); |
|
} |
|
|
|
m_bSkippedChooseEnemy = false; |
|
|
|
//--------------------------------- |
|
// |
|
// Select a target |
|
// |
|
|
|
if ( ShouldChooseNewEnemy() ) |
|
{ |
|
pChosenEnemy = BestEnemy(); |
|
} |
|
|
|
//--------------------------------- |
|
// |
|
// React to result of selection |
|
// |
|
|
|
bool fChangingEnemy = ( pChosenEnemy != pInitialEnemy ); |
|
|
|
if ( fChangingEnemy || fEnemyWentNull ) |
|
{ |
|
DbgEnemyMsg( this, "Enemy changed from %s to %s\n", pInitialEnemy->GetDebugName(), pChosenEnemy->GetDebugName() ); |
|
Forget( bits_MEMORY_HAD_ENEMY | bits_MEMORY_HAD_PLAYER ); |
|
|
|
// Did our old enemy snuff it? |
|
if ( pInitialEnemy && !pInitialEnemy->IsAlive() ) |
|
{ |
|
SetCondition( COND_ENEMY_DEAD ); |
|
} |
|
|
|
SetEnemy( pChosenEnemy ); |
|
|
|
if ( fHadEnemy ) |
|
{ |
|
// Vacate any strategy slot on old enemy |
|
VacateStrategySlot(); |
|
|
|
// Force output event for establishing LOS |
|
Forget( bits_MEMORY_HAD_LOS ); |
|
// m_flLastAttackTime = 0; |
|
} |
|
|
|
if ( !pChosenEnemy ) |
|
{ |
|
// Don't break on enemies going null if they've been killed |
|
if ( !HasCondition(COND_ENEMY_DEAD) ) |
|
{ |
|
SetCondition( COND_ENEMY_WENT_NULL ); |
|
} |
|
|
|
if ( fEnemyEluded ) |
|
{ |
|
SetCondition( COND_LOST_ENEMY ); |
|
LostEnemySound(); |
|
} |
|
|
|
if ( fEnemyWasPlayer ) |
|
{ |
|
m_OnLostPlayer.FireOutput( pInitialEnemy, this ); |
|
} |
|
m_OnLostEnemy.FireOutput( pInitialEnemy, this); |
|
} |
|
else |
|
{ |
|
Remember( ( pChosenEnemy->IsPlayer() ) ? bits_MEMORY_HAD_PLAYER : bits_MEMORY_HAD_ENEMY ); |
|
} |
|
} |
|
|
|
//--------------------------------- |
|
|
|
return ( pChosenEnemy != NULL ); |
|
} |
|
|
|
|
|
//========================================================= |
|
void CAI_BaseNPC::PickupWeapon( CBaseCombatWeapon *pWeapon ) |
|
{ |
|
pWeapon->OnPickedUp( this ); |
|
Weapon_Equip( pWeapon ); |
|
m_iszPendingWeapon = NULL_STRING; |
|
} |
|
|
|
//========================================================= |
|
// DropItem - dead npc drops named item |
|
//========================================================= |
|
CBaseEntity *CAI_BaseNPC::DropItem ( const char *pszItemName, Vector vecPos, QAngle vecAng ) |
|
{ |
|
if ( !pszItemName ) |
|
{ |
|
DevMsg( "DropItem() - No item name!\n" ); |
|
return NULL; |
|
} |
|
|
|
CBaseEntity *pItem = CBaseEntity::Create( pszItemName, vecPos, vecAng, this ); |
|
|
|
if ( pItem ) |
|
{ |
|
if ( g_pGameRules->IsAllowedToSpawn( pItem ) == false ) |
|
{ |
|
UTIL_Remove( pItem ); |
|
return NULL; |
|
} |
|
|
|
IPhysicsObject *pPhys = pItem->VPhysicsGetObject(); |
|
|
|
if ( pPhys ) |
|
{ |
|
// Add an extra push in a random direction |
|
Vector vel = RandomVector( -64.0f, 64.0f ); |
|
AngularImpulse angImp = RandomAngularImpulse( -300.0f, 300.0f ); |
|
|
|
vel[2] = 0.0f; |
|
pPhys->AddVelocity( &vel, &angImp ); |
|
} |
|
else |
|
{ |
|
// do we want this behavior to be default?! (sjb) |
|
pItem->ApplyAbsVelocityImpulse( GetAbsVelocity() ); |
|
pItem->ApplyLocalAngularVelocityImpulse( AngularImpulse( 0, random->RandomFloat( 0, 100 ), 0 ) ); |
|
} |
|
|
|
return pItem; |
|
} |
|
else |
|
{ |
|
DevMsg( "DropItem() - Didn't create!\n" ); |
|
return NULL; |
|
} |
|
|
|
} |
|
|
|
bool CAI_BaseNPC::ShouldFadeOnDeath( void ) |
|
{ |
|
if ( g_RagdollLVManager.IsLowViolence() ) |
|
{ |
|
return true; |
|
} |
|
else |
|
{ |
|
// if flagged to fade out |
|
return HasSpawnFlags(SF_NPC_FADE_CORPSE); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Indicates whether or not this npc should play an idle sound now. |
|
// |
|
// |
|
// Output : Returns true if yes, false if no. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ShouldPlayIdleSound( void ) |
|
{ |
|
if ( ( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT ) && |
|
random->RandomInt(0,99) == 0 && !HasSpawnFlags(SF_NPC_GAG) ) |
|
{ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Make a sound that other AI's can hear, to broadcast our presence |
|
// Input : volume (radius) of the sound. |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::MakeAIFootstepSound( float volume, float duration ) |
|
{ |
|
CSoundEnt::InsertSound( SOUND_COMBAT, EyePosition(), volume, duration, this, SOUNDENT_CHANNEL_NPC_FOOTSTEP ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::FOkToMakeSound( int soundPriority ) |
|
{ |
|
// ask the squad to filter sounds if I'm in one |
|
if ( m_pSquad ) |
|
{ |
|
if ( !m_pSquad->FOkToMakeSound( soundPriority ) ) |
|
return false; |
|
} |
|
else |
|
{ |
|
// otherwise, check my own sound timer |
|
// Am I making uninterruptable sound? |
|
if (gpGlobals->curtime <= m_flSoundWaitTime) |
|
{ |
|
if ( soundPriority <= m_nSoundPriority ) |
|
return false; |
|
} |
|
} |
|
|
|
// no talking outside of combat if gagged. |
|
if ( HasSpawnFlags(SF_NPC_GAG) && ( m_NPCState != NPC_STATE_COMBAT ) ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::JustMadeSound( int soundPriority, float flSoundLength ) |
|
{ |
|
m_flSoundWaitTime = gpGlobals->curtime + flSoundLength + random->RandomFloat(1.5, 2.0); |
|
m_nSoundPriority = soundPriority; |
|
|
|
if (m_pSquad) |
|
{ |
|
m_pSquad->JustMadeSound( soundPriority, gpGlobals->curtime + flSoundLength + random->RandomFloat(1.5, 2.0) ); |
|
} |
|
} |
|
|
|
Activity CAI_BaseNPC::GetStoppedActivity( void ) |
|
{ |
|
if (GetNavigator()->IsGoalActive()) |
|
{ |
|
Activity activity = GetNavigator()->GetArrivalActivity(); |
|
|
|
if (activity > ACT_RESET) |
|
{ |
|
return activity; |
|
} |
|
} |
|
|
|
return ACT_IDLE; |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
void CAI_BaseNPC::OnScheduleChange ( void ) |
|
{ |
|
EndTaskOverlay(); |
|
|
|
m_pNavigator->OnScheduleChange(); |
|
|
|
m_flMoveWaitFinished = 0; |
|
|
|
VacateStrategySlot(); |
|
|
|
// If I still have have a route, clear it |
|
// FIXME: Routes should only be cleared inside of tasks (kenb) |
|
GetNavigator()->ClearGoal(); |
|
|
|
UnlockBestSound(); |
|
|
|
// If I locked a hint node clear it |
|
if ( HasMemory(bits_MEMORY_LOCKED_HINT) && GetHintNode() != NULL) |
|
{ |
|
float hintDelay = GetHintDelay(GetHintNode()->HintType()); |
|
GetHintNode()->Unlock(hintDelay); |
|
SetHintNode( NULL ); |
|
} |
|
} |
|
|
|
|
|
|
|
CBaseCombatCharacter* CAI_BaseNPC::GetEnemyCombatCharacterPointer() |
|
{ |
|
if ( GetEnemy() == NULL ) |
|
return NULL; |
|
|
|
return GetEnemy()->MyCombatCharacterPointer(); |
|
} |
|
|
|
|
|
// Global Savedata for npc |
|
// |
|
// This should be an exact copy of the var's in the header. Fields |
|
// that aren't save/restored are commented out |
|
|
|
BEGIN_DATADESC( CAI_BaseNPC ) |
|
|
|
// m_pSchedule (reacquired on restore) |
|
DEFINE_EMBEDDED( m_ScheduleState ), |
|
DEFINE_FIELD( m_IdealSchedule, FIELD_INTEGER ), // handled specially but left in for "virtual" schedules |
|
DEFINE_FIELD( m_failSchedule, FIELD_INTEGER ), // handled specially but left in for "virtual" schedules |
|
DEFINE_FIELD( m_bUsingStandardThinkTime, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_flLastRealThinkTime, FIELD_TIME ), |
|
// m_iFrameBlocked (not saved) |
|
// m_bInChoreo (not saved) |
|
// m_bDoPostRestoreRefindPath (not saved) |
|
// gm_flTimeLastSpawn (static) |
|
// gm_nSpawnedThisFrame (static) |
|
// m_Conditions (custom save) |
|
// m_CustomInterruptConditions (custom save) |
|
// m_ConditionsPreIgnore (custom save) |
|
// m_InverseIgnoreConditions (custom save) |
|
// m_poseAim_Pitch (not saved; recomputed on restore) |
|
// m_poseAim_Yaw (not saved; recomputed on restore) |
|
// m_poseMove_Yaw (not saved; recomputed on restore) |
|
DEFINE_FIELD( m_flTimePingEffect, FIELD_TIME ), |
|
DEFINE_FIELD( m_bForceConditionsGather, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bConditionsGathered, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bSkippedChooseEnemy, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_NPCState, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_IdealNPCState, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flLastStateChangeTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_Efficiency, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_MoveEfficiency, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flNextDecisionTime, FIELD_TIME ), |
|
DEFINE_KEYFIELD( m_SleepState, FIELD_INTEGER, "sleepstate" ), |
|
DEFINE_FIELD( m_SleepFlags, FIELD_INTEGER ), |
|
DEFINE_KEYFIELD( m_flWakeRadius, FIELD_FLOAT, "wakeradius" ), |
|
DEFINE_KEYFIELD( m_bWakeSquad, FIELD_BOOLEAN, "wakesquad" ), |
|
DEFINE_FIELD( m_nWakeTick, FIELD_TICK ), |
|
|
|
DEFINE_CUSTOM_FIELD( m_Activity, ActivityDataOps() ), |
|
DEFINE_CUSTOM_FIELD( m_translatedActivity, ActivityDataOps() ), |
|
DEFINE_CUSTOM_FIELD( m_IdealActivity, ActivityDataOps() ), |
|
DEFINE_CUSTOM_FIELD( m_IdealTranslatedActivity, ActivityDataOps() ), |
|
DEFINE_CUSTOM_FIELD( m_IdealWeaponActivity, ActivityDataOps() ), |
|
|
|
DEFINE_FIELD( m_nIdealSequence, FIELD_INTEGER ), |
|
DEFINE_EMBEDDEDBYREF( m_pSenses ), |
|
DEFINE_EMBEDDEDBYREF( m_pLockedBestSound ), |
|
DEFINE_FIELD( m_hEnemy, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flTimeEnemyAcquired, FIELD_TIME ), |
|
DEFINE_FIELD( m_hTargetEnt, FIELD_EHANDLE ), |
|
DEFINE_EMBEDDED( m_GiveUpOnDeadEnemyTimer ), |
|
DEFINE_EMBEDDED( m_FailChooseEnemyTimer ), |
|
DEFINE_FIELD( m_EnemiesSerialNumber, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flAcceptableTimeSeenEnemy, FIELD_TIME ), |
|
DEFINE_EMBEDDED( m_UpdateEnemyPosTimer ), |
|
// m_flTimeAnyUpdateEnemyPos (static) |
|
DEFINE_FIELD( m_vecCommandGoal, FIELD_VECTOR ), |
|
DEFINE_EMBEDDED( m_CommandMoveMonitor ), |
|
DEFINE_FIELD( m_flSoundWaitTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_nSoundPriority, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flIgnoreDangerSoundsUntil, FIELD_TIME ), |
|
DEFINE_FIELD( m_afCapability, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flMoveWaitFinished, FIELD_TIME ), |
|
DEFINE_FIELD( m_hOpeningDoor, FIELD_EHANDLE ), |
|
DEFINE_EMBEDDEDBYREF( m_pNavigator ), |
|
DEFINE_EMBEDDEDBYREF( m_pLocalNavigator ), |
|
DEFINE_EMBEDDEDBYREF( m_pPathfinder ), |
|
DEFINE_EMBEDDEDBYREF( m_pMoveProbe ), |
|
DEFINE_EMBEDDEDBYREF( m_pMotor ), |
|
DEFINE_UTLVECTOR(m_UnreachableEnts, FIELD_EMBEDDED), |
|
DEFINE_FIELD( m_hInteractionPartner, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_hLastInteractionTestTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_hForcedInteractionPartner, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flForcedInteractionTimeout, FIELD_TIME ), |
|
DEFINE_FIELD( m_vecForcedWorldPosition, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_bCannotDieDuringInteraction, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_iInteractionState, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iInteractionPlaying, FIELD_INTEGER ), |
|
DEFINE_UTLVECTOR(m_ScriptedInteractions,FIELD_EMBEDDED), |
|
DEFINE_FIELD( m_flInteractionYaw, FIELD_FLOAT ), |
|
DEFINE_EMBEDDED( m_CheckOnGroundTimer ), |
|
DEFINE_FIELD( m_vDefaultEyeOffset, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flNextEyeLookTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flEyeIntegRate, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_vEyeLookTarget, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_vCurEyeTarget, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_hEyeLookTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flHeadYaw, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flHeadPitch, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flOriginalYaw, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_bInAScript, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_scriptState, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hCine, FIELD_EHANDLE ), |
|
DEFINE_CUSTOM_FIELD( m_ScriptArrivalActivity, ActivityDataOps() ), |
|
DEFINE_FIELD( m_strScriptArrivalSequence, FIELD_STRING ), |
|
DEFINE_FIELD( m_flSceneTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_iszSceneCustomMoveSeq, FIELD_STRING ), |
|
// m_pEnemies Saved specially in ai_saverestore.cpp |
|
DEFINE_FIELD( m_afMemory, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hEnemyOccluder, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flSumDamage, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flLastDamageTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flLastPlayerDamageTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flLastSawPlayerTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flLastAttackTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flLastEnemyTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flNextWeaponSearchTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_iszPendingWeapon, FIELD_STRING ), |
|
DEFINE_KEYFIELD( m_bIgnoreUnseenEnemies, FIELD_BOOLEAN , "ignoreunseenenemies"), |
|
DEFINE_EMBEDDED( m_ShotRegulator ), |
|
DEFINE_FIELD( m_iDesiredWeaponState, FIELD_INTEGER ), |
|
// m_pSquad Saved specially in ai_saverestore.cpp |
|
DEFINE_KEYFIELD(m_SquadName, FIELD_STRING, "squadname" ), |
|
DEFINE_FIELD( m_iMySquadSlot, FIELD_INTEGER ), |
|
DEFINE_KEYFIELD( m_strHintGroup, FIELD_STRING, "hintgroup" ), |
|
DEFINE_KEYFIELD( m_bHintGroupNavLimiting, FIELD_BOOLEAN, "hintlimiting" ), |
|
DEFINE_EMBEDDEDBYREF( m_pTacticalServices ), |
|
DEFINE_FIELD( m_flWaitFinished, FIELD_TIME ), |
|
DEFINE_FIELD( m_flNextFlinchTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flNextDodgeTime, FIELD_TIME ), |
|
DEFINE_EMBEDDED( m_MoveAndShootOverlay ), |
|
DEFINE_FIELD( m_vecLastPosition, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_vSavePosition, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_vInterruptSavePosition, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_pHintNode, FIELD_EHANDLE), |
|
DEFINE_FIELD( m_cAmmoLoaded, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flDistTooFar, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_hGoalEnt, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flTimeLastMovement, FIELD_TIME ), |
|
DEFINE_KEYFIELD(m_spawnEquipment, FIELD_STRING, "additionalequipment" ), |
|
DEFINE_FIELD( m_fNoDamageDecal, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_hStoredPathTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_vecStoredPathGoal, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_nStoredPathType, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_fStoredPathFlags, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_bDidDeathCleanup, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bCrouchDesired, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bForceCrouch, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bIsCrouching, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bPerformAvoidance, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bIsMoving, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bFadeCorpse, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_iDeathPose, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iDeathFrame, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_bCheckContacts, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bSpeedModActive, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_iSpeedModRadius, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iSpeedModSpeed, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hEnemyFilter, FIELD_EHANDLE ), |
|
DEFINE_KEYFIELD( m_iszEnemyFilterName, FIELD_STRING, "enemyfilter" ), |
|
DEFINE_FIELD( m_bImportanRagdoll, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bPlayerAvoidState, FIELD_BOOLEAN ), |
|
|
|
// Satisfy classcheck |
|
// DEFINE_FIELD( m_ScheduleHistory, CUtlVector < AIScheduleChoice_t > ), |
|
|
|
// m_fIsUsingSmallHull TODO -- This needs more consideration than simple save/load |
|
// m_failText DEBUG |
|
// m_interruptText DEBUG |
|
// m_failedSchedule DEBUG |
|
// m_interuptSchedule DEBUG |
|
// m_nDebugCurIndex DEBUG |
|
|
|
// m_LastShootAccuracy DEBUG |
|
// m_RecentShotAccuracy DEBUG |
|
// m_TotalShots DEBUG |
|
// m_TotalHits DEBUG |
|
// m_bSelected DEBUG |
|
// m_TimeLastShotMark DEBUG |
|
// m_bDeferredNavigation |
|
|
|
|
|
// Outputs |
|
DEFINE_OUTPUT( m_OnDamaged, "OnDamaged" ), |
|
DEFINE_OUTPUT( m_OnDeath, "OnDeath" ), |
|
DEFINE_OUTPUT( m_OnHalfHealth, "OnHalfHealth" ), |
|
DEFINE_OUTPUT( m_OnFoundEnemy, "OnFoundEnemy" ), |
|
DEFINE_OUTPUT( m_OnLostEnemyLOS, "OnLostEnemyLOS" ), |
|
DEFINE_OUTPUT( m_OnLostEnemy, "OnLostEnemy" ), |
|
DEFINE_OUTPUT( m_OnFoundPlayer, "OnFoundPlayer" ), |
|
DEFINE_OUTPUT( m_OnLostPlayerLOS, "OnLostPlayerLOS" ), |
|
DEFINE_OUTPUT( m_OnLostPlayer, "OnLostPlayer" ), |
|
DEFINE_OUTPUT( m_OnHearWorld, "OnHearWorld" ), |
|
DEFINE_OUTPUT( m_OnHearPlayer, "OnHearPlayer" ), |
|
DEFINE_OUTPUT( m_OnHearCombat, "OnHearCombat" ), |
|
DEFINE_OUTPUT( m_OnDamagedByPlayer, "OnDamagedByPlayer" ), |
|
DEFINE_OUTPUT( m_OnDamagedByPlayerSquad, "OnDamagedByPlayerSquad" ), |
|
DEFINE_OUTPUT( m_OnDenyCommanderUse, "OnDenyCommanderUse" ), |
|
DEFINE_OUTPUT( m_OnRappelTouchdown, "OnRappelTouchdown" ), |
|
DEFINE_OUTPUT( m_OnWake, "OnWake" ), |
|
DEFINE_OUTPUT( m_OnSleep, "OnSleep" ), |
|
DEFINE_OUTPUT( m_OnForcedInteractionStarted, "OnForcedInteractionStarted" ), |
|
DEFINE_OUTPUT( m_OnForcedInteractionAborted, "OnForcedInteractionAborted" ), |
|
DEFINE_OUTPUT( m_OnForcedInteractionFinished, "OnForcedInteractionFinished" ), |
|
|
|
// Inputs |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SetRelationship", InputSetRelationship ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SetEnemyFilter", InputSetEnemyFilter ), |
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "BeginRappel", InputBeginRappel ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SetSquad", InputSetSquad ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "Wake", InputWake ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "ForgetEntity", InputForgetEntity ), |
|
DEFINE_INPUTFUNC( FIELD_FLOAT, "IgnoreDangerSounds", InputIgnoreDangerSounds ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "StartScripting", InputStartScripting ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "StopScripting", InputStopScripting ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "GagEnable", InputGagEnable ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "GagDisable", InputGagDisable ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "InsideTransition", InputInsideTransition ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "ActivateSpeedModifier", InputActivateSpeedModifier ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "DisableSpeedModifier", InputDisableSpeedModifier ), |
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSpeedModRadius", InputSetSpeedModifierRadius ), |
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSpeedModSpeed", InputSetSpeedModifierSpeed ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "HolsterWeapon", InputHolsterWeapon ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "HolsterAndDestroyWeapon", InputHolsterAndDestroyWeapon ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "UnholsterWeapon", InputUnholsterWeapon ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "ForceInteractionWithNPC", InputForceInteractionWithNPC ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "UpdateEnemyMemory", InputUpdateEnemyMemory ), |
|
|
|
// Function pointers |
|
DEFINE_USEFUNC( NPCUse ), |
|
DEFINE_THINKFUNC( CallNPCThink ), |
|
DEFINE_THINKFUNC( CorpseFallThink ), |
|
DEFINE_THINKFUNC( NPCInitThink ), |
|
|
|
END_DATADESC() |
|
|
|
BEGIN_SIMPLE_DATADESC( AIScheduleState_t ) |
|
DEFINE_FIELD( iCurTask, FIELD_INTEGER ), |
|
DEFINE_FIELD( fTaskStatus, FIELD_INTEGER ), |
|
DEFINE_FIELD( timeStarted, FIELD_TIME ), |
|
DEFINE_FIELD( timeCurTaskStarted, FIELD_TIME ), |
|
DEFINE_FIELD( taskFailureCode, FIELD_INTEGER ), |
|
DEFINE_FIELD( iTaskInterrupt, FIELD_INTEGER ), |
|
DEFINE_FIELD( bTaskRanAutomovement, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( bTaskUpdatedYaw, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( bScheduleWasInterrupted, FIELD_BOOLEAN ), |
|
END_DATADESC() |
|
|
|
|
|
IMPLEMENT_SERVERCLASS_ST( CAI_BaseNPC, DT_AI_BaseNPC ) |
|
SendPropInt( SENDINFO( m_lifeState ), 3, SPROP_UNSIGNED ), |
|
SendPropBool( SENDINFO( m_bPerformAvoidance ) ), |
|
SendPropBool( SENDINFO( m_bIsMoving ) ), |
|
SendPropBool( SENDINFO( m_bFadeCorpse ) ), |
|
SendPropInt( SENDINFO( m_iDeathPose ), ANIMATION_SEQUENCE_BITS ), |
|
SendPropInt( SENDINFO( m_iDeathFrame ), 5 ), |
|
SendPropBool( SENDINFO( m_bSpeedModActive ) ), |
|
SendPropInt( SENDINFO( m_iSpeedModRadius ) ), |
|
SendPropInt( SENDINFO( m_iSpeedModSpeed ) ), |
|
SendPropBool( SENDINFO( m_bImportanRagdoll ) ), |
|
SendPropFloat( SENDINFO( m_flTimePingEffect ) ), |
|
END_SEND_TABLE() |
|
|
|
//------------------------------------- |
|
|
|
BEGIN_SIMPLE_DATADESC( UnreachableEnt_t ) |
|
|
|
DEFINE_FIELD( hUnreachableEnt, FIELD_EHANDLE ), |
|
DEFINE_FIELD( fExpireTime, FIELD_TIME ), |
|
DEFINE_FIELD( vLocationWhenUnreachable, FIELD_POSITION_VECTOR ), |
|
|
|
END_DATADESC() |
|
|
|
//------------------------------------- |
|
|
|
BEGIN_SIMPLE_DATADESC( ScriptedNPCInteraction_Phases_t ) |
|
DEFINE_FIELD( iszSequence, FIELD_STRING ), |
|
DEFINE_FIELD( iActivity, FIELD_INTEGER ), |
|
END_DATADESC() |
|
|
|
//------------------------------------- |
|
|
|
BEGIN_SIMPLE_DATADESC( ScriptedNPCInteraction_t ) |
|
DEFINE_FIELD( iszInteractionName, FIELD_STRING ), |
|
DEFINE_FIELD( iFlags, FIELD_INTEGER ), |
|
DEFINE_FIELD( iTriggerMethod, FIELD_INTEGER ), |
|
DEFINE_FIELD( iLoopBreakTriggerMethod, FIELD_INTEGER ), |
|
DEFINE_FIELD( vecRelativeOrigin, FIELD_VECTOR ), |
|
DEFINE_FIELD( angRelativeAngles, FIELD_VECTOR ), |
|
DEFINE_FIELD( vecRelativeVelocity, FIELD_VECTOR ), |
|
DEFINE_FIELD( flDelay, FIELD_FLOAT ), |
|
DEFINE_FIELD( flDistSqr, FIELD_FLOAT ), |
|
DEFINE_FIELD( iszMyWeapon, FIELD_STRING ), |
|
DEFINE_FIELD( iszTheirWeapon, FIELD_STRING ), |
|
DEFINE_EMBEDDED_ARRAY( sPhases, SNPCINT_NUM_PHASES ), |
|
DEFINE_FIELD( matDesiredLocalToWorld, FIELD_VMATRIX ), |
|
DEFINE_FIELD( bValidOnCurrentEnemy, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( flNextAttemptTime, FIELD_TIME ), |
|
END_DATADESC() |
|
|
|
//------------------------------------- |
|
|
|
void CAI_BaseNPC::PostConstructor( const char *szClassname ) |
|
{ |
|
BaseClass::PostConstructor( szClassname ); |
|
CreateComponents(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::Activate( void ) |
|
{ |
|
BaseClass::Activate(); |
|
|
|
if ( GetModelPtr() ) |
|
{ |
|
ParseScriptedNPCInteractions(); |
|
} |
|
|
|
// Get a handle to my enemy filter entity if there is one. |
|
if ( m_iszEnemyFilterName != NULL_STRING ) |
|
{ |
|
CBaseEntity *pFilter = gEntList.FindEntityByName( NULL, m_iszEnemyFilterName ); |
|
if ( pFilter != NULL ) |
|
{ |
|
m_hEnemyFilter = dynamic_cast<CBaseFilter*>(pFilter); |
|
} |
|
} |
|
|
|
#ifdef AI_MONITOR_FOR_OSCILLATION |
|
m_ScheduleHistory.RemoveAll(); |
|
#endif//AI_MONITOR_FOR_OSCILLATION |
|
|
|
} |
|
|
|
void CAI_BaseNPC::Precache( void ) |
|
{ |
|
gm_iszPlayerSquad = AllocPooledString( PLAYER_SQUADNAME ); // cache for fast IsPlayerSquad calls |
|
|
|
if ( m_spawnEquipment != NULL_STRING && strcmp(STRING(m_spawnEquipment), "0") ) |
|
{ |
|
UTIL_PrecacheOther( STRING(m_spawnEquipment) ); |
|
} |
|
|
|
// Make sure schedules are loaded for this NPC type |
|
if (!LoadedSchedules()) |
|
{ |
|
DevMsg("ERROR: Rejecting spawn of %s as error in NPC's schedules.\n",GetDebugName()); |
|
UTIL_Remove(this); |
|
return; |
|
} |
|
|
|
PrecacheScriptSound( "AI_BaseNPC.SwishSound" ); |
|
PrecacheScriptSound( "AI_BaseNPC.BodyDrop_Heavy" ); |
|
PrecacheScriptSound( "AI_BaseNPC.BodyDrop_Light" ); |
|
PrecacheScriptSound( "AI_BaseNPC.SentenceStop" ); |
|
|
|
BaseClass::Precache(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
const short AI_EXTENDED_SAVE_HEADER_VERSION = 5; |
|
const short AI_EXTENDED_SAVE_HEADER_RESET_VERSION = 3; |
|
|
|
const short AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_CONDITIONS = 2; |
|
const short AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_SCHEDULE_ID_FIXUP = 3; |
|
const short AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_SEQUENCE = 4; |
|
const short AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_NAVIGATOR_SAVE = 5; |
|
|
|
struct AIExtendedSaveHeader_t |
|
{ |
|
AIExtendedSaveHeader_t() |
|
: version(AI_EXTENDED_SAVE_HEADER_VERSION), |
|
flags(0), |
|
scheduleCrc(0) |
|
{ |
|
szSchedule[0] = 0; |
|
szIdealSchedule[0] = 0; |
|
szFailSchedule[0] = 0; |
|
szSequence[0] = 0; |
|
} |
|
|
|
short version; |
|
unsigned flags; |
|
char szSchedule[128]; |
|
CRC32_t scheduleCrc; |
|
char szIdealSchedule[128]; |
|
char szFailSchedule[128]; |
|
char szSequence[128]; |
|
|
|
DECLARE_SIMPLE_DATADESC(); |
|
}; |
|
|
|
enum AIExtendedSaveHeaderFlags_t |
|
{ |
|
AIESH_HAD_ENEMY = 0x01, |
|
AIESH_HAD_TARGET = 0x02, |
|
AIESH_HAD_NAVGOAL = 0x04, |
|
}; |
|
|
|
//------------------------------------- |
|
|
|
BEGIN_SIMPLE_DATADESC( AIExtendedSaveHeader_t ) |
|
DEFINE_FIELD( version, FIELD_SHORT ), |
|
DEFINE_FIELD( flags, FIELD_INTEGER ), |
|
DEFINE_AUTO_ARRAY( szSchedule, FIELD_CHARACTER ), |
|
DEFINE_FIELD( scheduleCrc, FIELD_INTEGER ), |
|
DEFINE_AUTO_ARRAY( szIdealSchedule, FIELD_CHARACTER ), |
|
DEFINE_AUTO_ARRAY( szFailSchedule, FIELD_CHARACTER ), |
|
DEFINE_AUTO_ARRAY( szSequence, FIELD_CHARACTER ), |
|
END_DATADESC() |
|
|
|
//------------------------------------- |
|
|
|
int CAI_BaseNPC::Save( ISave &save ) |
|
{ |
|
AIExtendedSaveHeader_t saveHeader; |
|
|
|
if ( GetEnemy() ) |
|
saveHeader.flags |= AIESH_HAD_ENEMY; |
|
if ( GetTarget() ) |
|
saveHeader.flags |= AIESH_HAD_TARGET; |
|
if ( GetNavigator()->IsGoalActive() ) |
|
saveHeader.flags |= AIESH_HAD_NAVGOAL; |
|
|
|
if ( m_pSchedule ) |
|
{ |
|
const char *pszSchedule = m_pSchedule->GetName(); |
|
|
|
Assert( Q_strlen( pszSchedule ) < sizeof( saveHeader.szSchedule ) - 1 ); |
|
Q_strncpy( saveHeader.szSchedule, pszSchedule, sizeof( saveHeader.szSchedule ) ); |
|
|
|
CRC32_Init( &saveHeader.scheduleCrc ); |
|
CRC32_ProcessBuffer( &saveHeader.scheduleCrc, (void *)m_pSchedule->GetTaskList(), m_pSchedule->NumTasks() * sizeof(Task_t) ); |
|
CRC32_Final( &saveHeader.scheduleCrc ); |
|
} |
|
else |
|
{ |
|
saveHeader.szSchedule[0] = 0; |
|
saveHeader.scheduleCrc = 0; |
|
} |
|
|
|
int idealSchedule = GetGlobalScheduleId( m_IdealSchedule ); |
|
|
|
if ( idealSchedule != -1 && idealSchedule != AI_RemapToGlobal( SCHED_NONE ) && idealSchedule != AI_RemapToGlobal( SCHED_AISCRIPT ) ) |
|
{ |
|
CAI_Schedule *pIdealSchedule = GetSchedule( m_IdealSchedule ); |
|
if ( pIdealSchedule ) |
|
{ |
|
const char *pszIdealSchedule = pIdealSchedule->GetName(); |
|
Assert( Q_strlen( pszIdealSchedule ) < sizeof( saveHeader.szIdealSchedule ) - 1 ); |
|
Q_strncpy( saveHeader.szIdealSchedule, pszIdealSchedule, sizeof( saveHeader.szIdealSchedule ) ); |
|
} |
|
} |
|
|
|
int failSchedule = GetGlobalScheduleId( m_failSchedule ); |
|
if ( failSchedule != -1 && failSchedule != AI_RemapToGlobal( SCHED_NONE ) && failSchedule != AI_RemapToGlobal( SCHED_AISCRIPT ) ) |
|
{ |
|
CAI_Schedule *pFailSchedule = GetSchedule( m_failSchedule ); |
|
if ( pFailSchedule ) |
|
{ |
|
const char *pszFailSchedule = pFailSchedule->GetName(); |
|
Assert( Q_strlen( pszFailSchedule ) < sizeof( saveHeader.szFailSchedule ) - 1 ); |
|
Q_strncpy( saveHeader.szFailSchedule, pszFailSchedule, sizeof( saveHeader.szFailSchedule ) ); |
|
} |
|
} |
|
|
|
if ( GetSequence() != ACT_INVALID && GetModelPtr() ) |
|
{ |
|
const char *pszSequenceName = GetSequenceName( GetSequence() ); |
|
if ( pszSequenceName && *pszSequenceName ) |
|
{ |
|
Assert( Q_strlen( pszSequenceName ) < sizeof( saveHeader.szSequence ) - 1 ); |
|
Q_strncpy( saveHeader.szSequence, pszSequenceName, sizeof(saveHeader.szSequence) ); |
|
} |
|
} |
|
|
|
save.WriteAll( &saveHeader ); |
|
|
|
save.StartBlock(); |
|
SaveConditions( save, m_Conditions ); |
|
SaveConditions( save, m_CustomInterruptConditions ); |
|
SaveConditions( save, m_ConditionsPreIgnore ); |
|
CAI_ScheduleBits ignoreConditions; |
|
m_InverseIgnoreConditions.Not( &ignoreConditions ); |
|
SaveConditions( save, ignoreConditions ); |
|
save.EndBlock(); |
|
|
|
save.StartBlock(); |
|
GetNavigator()->Save( save ); |
|
save.EndBlock(); |
|
|
|
return BaseClass::Save(save); |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_BaseNPC::DiscardScheduleState() |
|
{ |
|
// We don't save/restore routes yet |
|
GetNavigator()->ClearGoal(); |
|
|
|
// We don't save/restore schedules yet |
|
ClearSchedule( "Restoring NPC" ); |
|
|
|
// Reset animation |
|
m_Activity = ACT_RESET; |
|
|
|
// If we don't have an enemy, clear conditions like see enemy, etc. |
|
if ( GetEnemy() == NULL ) |
|
{ |
|
m_Conditions.ClearAll(); |
|
} |
|
|
|
// went across a transition and lost my m_hCine |
|
bool bLostScript = ( m_NPCState == NPC_STATE_SCRIPT && m_hCine == NULL ); |
|
if ( bLostScript ) |
|
{ |
|
// UNDONE: Do something better here? |
|
// for now, just go back to idle and let the AI figure out what to do. |
|
SetState( NPC_STATE_IDLE ); |
|
SetIdealState( NPC_STATE_IDLE ); |
|
DevMsg(1, "Scripted Sequence stripped on level transition for %s\n", GetDebugName() ); |
|
} |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_BaseNPC::OnRestore() |
|
{ |
|
gm_iszPlayerSquad = AllocPooledString( PLAYER_SQUADNAME ); // cache for fast IsPlayerSquad calls |
|
|
|
if ( m_bDoPostRestoreRefindPath && CAI_NetworkManager::NetworksLoaded() ) |
|
{ |
|
CAI_DynamicLink::InitDynamicLinks(); |
|
if ( !GetNavigator()->RefindPathToGoal( false ) ) |
|
DiscardScheduleState(); |
|
} |
|
else |
|
{ |
|
GetNavigator()->ClearGoal(); |
|
} |
|
BaseClass::OnRestore(); |
|
m_bCheckContacts = true; |
|
} |
|
|
|
|
|
//------------------------------------- |
|
|
|
int CAI_BaseNPC::Restore( IRestore &restore ) |
|
{ |
|
AIExtendedSaveHeader_t saveHeader; |
|
restore.ReadAll( &saveHeader ); |
|
|
|
if ( saveHeader.version >= AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_CONDITIONS ) |
|
{ |
|
restore.StartBlock(); |
|
RestoreConditions( restore, &m_Conditions ); |
|
RestoreConditions( restore, &m_CustomInterruptConditions ); |
|
RestoreConditions( restore, &m_ConditionsPreIgnore ); |
|
CAI_ScheduleBits ignoreConditions; |
|
RestoreConditions( restore, &ignoreConditions ); |
|
ignoreConditions.Not( &m_InverseIgnoreConditions ); |
|
restore.EndBlock(); |
|
} |
|
|
|
if ( saveHeader.version >= AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_NAVIGATOR_SAVE ) |
|
{ |
|
restore.StartBlock(); |
|
GetNavigator()->Restore( restore ); |
|
restore.EndBlock(); |
|
} |
|
|
|
// do a normal restore |
|
int status = BaseClass::Restore(restore); |
|
if ( !status ) |
|
return 0; |
|
|
|
// Do schedule fix-up |
|
if ( saveHeader.version >= AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_SCHEDULE_ID_FIXUP ) |
|
{ |
|
if ( saveHeader.szIdealSchedule[0] ) |
|
{ |
|
CAI_Schedule *pIdealSchedule = g_AI_SchedulesManager.GetScheduleByName( saveHeader.szIdealSchedule ); |
|
m_IdealSchedule = ( pIdealSchedule ) ? pIdealSchedule->GetId() : SCHED_NONE; |
|
} |
|
|
|
if ( saveHeader.szFailSchedule[0] ) |
|
{ |
|
CAI_Schedule *pFailSchedule = g_AI_SchedulesManager.GetScheduleByName( saveHeader.szFailSchedule ); |
|
m_failSchedule = ( pFailSchedule ) ? pFailSchedule->GetId() : SCHED_NONE; |
|
} |
|
} |
|
|
|
bool bLostSequence = false; |
|
if ( saveHeader.version >= AI_EXTENDED_SAVE_HEADER_FIRST_VERSION_WITH_SEQUENCE && saveHeader.szSequence[0] && GetModelPtr() ) |
|
{ |
|
SetSequence( LookupSequence( saveHeader.szSequence ) ); |
|
if ( GetSequence() == ACT_INVALID ) |
|
{ |
|
DevMsg( this, AIMF_IGNORE_SELECTED, "Discarding missing sequence %s on load.\n", saveHeader.szSequence ); |
|
SetSequence( 0 ); |
|
bLostSequence = true; |
|
} |
|
|
|
Assert( IsValidSequence( GetSequence() ) ); |
|
} |
|
|
|
bool bLostScript = ( m_NPCState == NPC_STATE_SCRIPT && m_hCine == NULL ); |
|
bool bDiscardScheduleState = ( bLostScript || |
|
bLostSequence || |
|
saveHeader.szSchedule[0] == 0 || |
|
saveHeader.version < AI_EXTENDED_SAVE_HEADER_RESET_VERSION || |
|
( (saveHeader.flags & AIESH_HAD_ENEMY) && !GetEnemy() ) || |
|
( (saveHeader.flags & AIESH_HAD_TARGET) && !GetTarget() ) ); |
|
|
|
if ( m_ScheduleState.taskFailureCode >= NUM_FAIL_CODES ) |
|
m_ScheduleState.taskFailureCode = FAIL_NO_TARGET; // must have been a string, gotta punt |
|
|
|
if ( !bDiscardScheduleState ) |
|
{ |
|
m_pSchedule = g_AI_SchedulesManager.GetScheduleByName( saveHeader.szSchedule ); |
|
if ( m_pSchedule ) |
|
{ |
|
CRC32_t scheduleCrc; |
|
CRC32_Init( &scheduleCrc ); |
|
CRC32_ProcessBuffer( &scheduleCrc, (void *)m_pSchedule->GetTaskList(), m_pSchedule->NumTasks() * sizeof(Task_t) ); |
|
CRC32_Final( &scheduleCrc ); |
|
|
|
if ( scheduleCrc != saveHeader.scheduleCrc ) |
|
{ |
|
m_pSchedule = NULL; |
|
} |
|
} |
|
} |
|
|
|
if ( !m_pSchedule ) |
|
bDiscardScheduleState = true; |
|
|
|
if ( !bDiscardScheduleState ) |
|
m_bDoPostRestoreRefindPath = ( ( saveHeader.flags & AIESH_HAD_NAVGOAL) != 0 ); |
|
else |
|
{ |
|
m_bDoPostRestoreRefindPath = false; |
|
DiscardScheduleState(); |
|
} |
|
|
|
return status; |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_BaseNPC::SaveConditions( ISave &save, const CAI_ScheduleBits &conditions ) |
|
{ |
|
for (int i = 0; i < MAX_CONDITIONS; i++) |
|
{ |
|
if (conditions.IsBitSet(i)) |
|
{ |
|
const char *pszConditionName = ConditionName(AI_RemapToGlobal(i)); |
|
if ( !pszConditionName ) |
|
break; |
|
save.WriteString( pszConditionName ); |
|
} |
|
} |
|
save.WriteString( "" ); |
|
} |
|
|
|
//------------------------------------- |
|
|
|
void CAI_BaseNPC::RestoreConditions( IRestore &restore, CAI_ScheduleBits *pConditions ) |
|
{ |
|
pConditions->ClearAll(); |
|
char szCondition[256]; |
|
for (;;) |
|
{ |
|
restore.ReadString( szCondition, sizeof(szCondition), 0 ); |
|
if ( !szCondition[0] ) |
|
break; |
|
int iCondition = GetSchedulingSymbols()->ConditionSymbolToId( szCondition ); |
|
if ( iCondition != -1 ) |
|
pConditions->Set( AI_RemapFromGlobal( iCondition ) ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::KeyValue( const char *szKeyName, const char *szValue ) |
|
{ |
|
bool bResult = BaseClass::KeyValue( szKeyName, szValue ); |
|
|
|
if( !bResult ) |
|
{ |
|
// Defer unhandled Keys to behaviors |
|
CAI_BehaviorBase **ppBehaviors = AccessBehaviors(); |
|
|
|
for ( int i = 0; i < NumBehaviors(); i++ ) |
|
{ |
|
if( ppBehaviors[ i ]->KeyValue( szKeyName, szValue ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return bResult; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Debug function to make this NPC freeze in place (or unfreeze). |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ToggleFreeze(void) |
|
{ |
|
if (!IsCurSchedule(SCHED_NPC_FREEZE)) |
|
{ |
|
// Freeze them. |
|
SetCondition(COND_NPC_FREEZE); |
|
SetMoveType(MOVETYPE_NONE); |
|
SetGravity(0); |
|
SetLocalAngularVelocity(vec3_angle); |
|
SetAbsVelocity( vec3_origin ); |
|
} |
|
else |
|
{ |
|
// Unfreeze them. |
|
SetCondition(COND_NPC_UNFREEZE); |
|
m_Activity = ACT_RESET; |
|
|
|
// BUGBUG: this might not be the correct movetype! |
|
SetMoveType( MOVETYPE_STEP ); |
|
|
|
// Doesn't restore gravity to the original value, but who cares? |
|
SetGravity(1); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Written by subclasses macro to load schedules |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::LoadSchedules(void) |
|
{ |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::LoadedSchedules(void) |
|
{ |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Constructor |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CAI_BaseNPC::CAI_BaseNPC(void) |
|
: m_UnreachableEnts( 0, 4 ), |
|
m_bDeferredNavigation( false ) |
|
{ |
|
m_pMotor = NULL; |
|
m_pMoveProbe = NULL; |
|
m_pNavigator = NULL; |
|
m_pSenses = NULL; |
|
m_pPathfinder = NULL; |
|
m_pLocalNavigator = NULL; |
|
|
|
m_pSchedule = NULL; |
|
m_IdealSchedule = SCHED_NONE; |
|
|
|
#ifdef _DEBUG |
|
// necessary since in debug, we initialize vectors to NAN for debugging |
|
m_vecLastPosition.Init(); |
|
m_vSavePosition.Init(); |
|
m_vEyeLookTarget.Init(); |
|
m_vCurEyeTarget.Init(); |
|
m_vDefaultEyeOffset.Init(); |
|
|
|
#endif |
|
m_bDidDeathCleanup = false; |
|
|
|
m_afCapability = 0; // Make sure this is cleared in the base class |
|
|
|
SetHullType(HULL_HUMAN); // Give human hull by default, subclasses should override |
|
|
|
m_iMySquadSlot = SQUAD_SLOT_NONE; |
|
m_flSumDamage = 0; |
|
m_flLastDamageTime = 0; |
|
m_flLastAttackTime = 0; |
|
m_flSoundWaitTime = 0; |
|
m_flNextEyeLookTime = 0; |
|
m_flHeadYaw = 0; |
|
m_flHeadPitch = 0; |
|
m_spawnEquipment = NULL_STRING; |
|
m_SquadName = NULL_STRING; |
|
m_pEnemies = new CAI_Enemies; |
|
m_bIgnoreUnseenEnemies = false; |
|
m_flEyeIntegRate = 0.95; |
|
SetTarget( NULL ); |
|
|
|
m_pSquad = NULL; |
|
|
|
m_flMoveWaitFinished = 0; |
|
|
|
m_fIsUsingSmallHull = true; |
|
|
|
m_bHintGroupNavLimiting = false; |
|
|
|
m_fNoDamageDecal = false; |
|
|
|
SetInAScript( false ); |
|
|
|
m_pLockedBestSound = new CSound; |
|
m_pLockedBestSound->m_iType = SOUND_NONE; |
|
|
|
// ---------------------------- |
|
// Debugging fields |
|
// ---------------------------- |
|
m_interruptText = NULL; |
|
m_failText = NULL; |
|
m_failedSchedule = NULL; |
|
m_interuptSchedule = NULL; |
|
m_nDebugPauseIndex = 0; |
|
|
|
g_AI_Manager.AddAI( this ); |
|
|
|
if ( g_AI_Manager.NumAIs() == 1 ) |
|
{ |
|
m_AnyUpdateEnemyPosTimer.Force(); |
|
gm_flTimeLastSpawn = -1; |
|
gm_nSpawnedThisFrame = 0; |
|
gm_iNextThinkRebalanceTick = 0; |
|
} |
|
|
|
m_iFrameBlocked = -1; |
|
m_bInChoreo = true; // assume so until call to UpdateEfficiency() |
|
|
|
SetCollisionGroup( COLLISION_GROUP_NPC ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Destructor |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
CAI_BaseNPC::~CAI_BaseNPC(void) |
|
{ |
|
g_AI_Manager.RemoveAI( this ); |
|
|
|
delete m_pLockedBestSound; |
|
|
|
RemoveMemory(); |
|
|
|
delete m_pPathfinder; |
|
delete m_pNavigator; |
|
delete m_pMotor; |
|
delete m_pLocalNavigator; |
|
delete m_pMoveProbe; |
|
delete m_pSenses; |
|
delete m_pTacticalServices; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::UpdateOnRemove(void) |
|
{ |
|
if ( !m_bDidDeathCleanup ) |
|
{ |
|
if ( m_NPCState == NPC_STATE_DEAD ) |
|
DevMsg( "May not have cleaned up on NPC death\n"); |
|
|
|
CleanupOnDeath( NULL, false ); |
|
} |
|
|
|
// Chain at end to mimic destructor unwind order |
|
BaseClass::UpdateOnRemove(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::UpdateTransmitState() |
|
{ |
|
if( gpGlobals->curtime < m_flTimePingEffect ) |
|
{ |
|
return SetTransmitState( FL_EDICT_ALWAYS ); |
|
} |
|
|
|
return BaseClass::UpdateTransmitState(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::CreateComponents() |
|
{ |
|
m_pSenses = CreateSenses(); |
|
if ( !m_pSenses ) |
|
return false; |
|
|
|
m_pMotor = CreateMotor(); |
|
if ( !m_pMotor ) |
|
return false; |
|
|
|
m_pLocalNavigator = CreateLocalNavigator(); |
|
if ( !m_pLocalNavigator ) |
|
return false; |
|
|
|
m_pMoveProbe = CreateMoveProbe(); |
|
if ( !m_pMoveProbe ) |
|
return false; |
|
|
|
m_pNavigator = CreateNavigator(); |
|
if ( !m_pNavigator ) |
|
return false; |
|
|
|
m_pPathfinder = CreatePathfinder(); |
|
if ( !m_pPathfinder ) |
|
return false; |
|
|
|
m_pTacticalServices = CreateTacticalServices(); |
|
if ( !m_pTacticalServices ) |
|
return false; |
|
|
|
m_MoveAndShootOverlay.SetOuter( this ); |
|
|
|
m_pMotor->Init( m_pLocalNavigator ); |
|
m_pLocalNavigator->Init( m_pNavigator ); |
|
m_pNavigator->Init( g_pBigAINet ); |
|
m_pPathfinder->Init( g_pBigAINet ); |
|
m_pTacticalServices->Init( g_pBigAINet ); |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Senses *CAI_BaseNPC::CreateSenses() |
|
{ |
|
CAI_Senses *pSenses = new CAI_Senses; |
|
pSenses->SetOuter( this ); |
|
return pSenses; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Motor *CAI_BaseNPC::CreateMotor() |
|
{ |
|
return new CAI_Motor( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_MoveProbe *CAI_BaseNPC::CreateMoveProbe() |
|
{ |
|
return new CAI_MoveProbe( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_LocalNavigator *CAI_BaseNPC::CreateLocalNavigator() |
|
{ |
|
return new CAI_LocalNavigator( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_TacticalServices *CAI_BaseNPC::CreateTacticalServices() |
|
{ |
|
return new CAI_TacticalServices( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Navigator *CAI_BaseNPC::CreateNavigator() |
|
{ |
|
return new CAI_Navigator( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Pathfinder *CAI_BaseNPC::CreatePathfinder() |
|
{ |
|
return new CAI_Pathfinder( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputSetRelationship( inputdata_t &inputdata ) |
|
{ |
|
AddRelationship( inputdata.value.String(), inputdata.pActivator ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Won't affect the current enemy, only future enemy acquisitions. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputSetEnemyFilter( inputdata_t &inputdata ) |
|
{ |
|
// Get a handle to my enemy filter entity if there is one. |
|
CBaseEntity *pFilter = gEntList.FindEntityByName( NULL, inputdata.value.StringID() ); |
|
m_hEnemyFilter = dynamic_cast<CBaseFilter*>(pFilter); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputSetHealth( inputdata_t &inputdata ) |
|
{ |
|
int iNewHealth = inputdata.value.Int(); |
|
int iDelta = abs(GetHealth() - iNewHealth); |
|
if ( iNewHealth > GetHealth() ) |
|
{ |
|
TakeHealth( iDelta, DMG_GENERIC ); |
|
} |
|
else if ( iNewHealth < GetHealth() ) |
|
{ |
|
TakeDamage( CTakeDamageInfo( this, this, iDelta, DMG_GENERIC ) ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputBeginRappel( inputdata_t &inputdata ) |
|
{ |
|
BeginRappel(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputSetSquad( inputdata_t &inputdata ) |
|
{ |
|
if ( !( CapabilitiesGet() & bits_CAP_SQUAD ) ) |
|
{ |
|
Warning("SetSquad Input received for NPC %s, but that NPC can't use squads.\n", GetDebugName() ); |
|
return; |
|
} |
|
|
|
m_SquadName = inputdata.value.StringID(); |
|
|
|
// Removing from squad? |
|
if ( m_SquadName == NULL_STRING ) |
|
{ |
|
if ( m_pSquad ) |
|
{ |
|
m_pSquad->RemoveFromSquad(this, true); |
|
m_pSquad = NULL; |
|
} |
|
} |
|
else |
|
{ |
|
m_pSquad = g_AI_SquadManager.FindCreateSquad(this, m_SquadName); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputWake( inputdata_t &inputdata ) |
|
{ |
|
Wake(); |
|
|
|
// Check if we have a path to follow. This is normally done in StartNPC, |
|
// but putting the NPC to sleep will cancel it, so we have to do it again. |
|
if ( m_target != NULL_STRING )// this npc has a target |
|
{ |
|
// Find the npc's initial target entity, stash it |
|
SetGoalEnt( gEntList.FindEntityByName( NULL, m_target ) ); |
|
|
|
if ( !GetGoalEnt() ) |
|
{ |
|
Warning( "ReadyNPC()--%s couldn't find target %s\n", GetClassname(), STRING(m_target)); |
|
} |
|
else |
|
{ |
|
StartTargetHandling( GetGoalEnt() ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputForgetEntity( inputdata_t &inputdata ) |
|
{ |
|
const char *pszEntityToForget = inputdata.value.String(); |
|
|
|
if ( g_pDeveloper->GetInt() && pszEntityToForget[strlen( pszEntityToForget ) - 1] == '*' ) |
|
DevMsg( "InputForgetEntity does not support wildcards\n" ); |
|
|
|
CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, pszEntityToForget ); |
|
if ( pEntity ) |
|
{ |
|
if ( GetEnemy() == pEntity ) |
|
{ |
|
SetEnemy( NULL ); |
|
SetIdealState( NPC_STATE_ALERT ); |
|
} |
|
GetEnemies()->ClearMemory( pEntity ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputIgnoreDangerSounds( inputdata_t &inputdata ) |
|
{ |
|
// Default is 10 seconds. |
|
float flDelay = 10.0f; |
|
|
|
if( inputdata.value.Float() > 0.0f ) |
|
{ |
|
flDelay = inputdata.value.Float(); |
|
} |
|
|
|
m_flIgnoreDangerSoundsUntil = gpGlobals->curtime + flDelay; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputUpdateEnemyMemory( inputdata_t &inputdata ) |
|
{ |
|
const char *pszEnemy = inputdata.value.String(); |
|
CBaseEntity *pEnemy = gEntList.FindEntityByName( NULL, pszEnemy ); |
|
|
|
if( pEnemy ) |
|
{ |
|
UpdateEnemyMemory( pEnemy, pEnemy->GetAbsOrigin(), this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputOutsideTransition( inputdata_t &inputdata ) |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when this NPC transitions to another level with the player |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputInsideTransition( inputdata_t &inputdata ) |
|
{ |
|
CleanupScriptsOnTeleport( true ); |
|
|
|
// If we're inside a vcd, tell it to stop |
|
if ( IsCurSchedule( SCHED_SCENE_GENERIC, false ) ) |
|
{ |
|
RemoveActorFromScriptedScenes( this, false ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CleanupScriptsOnTeleport( bool bEnrouteAsWell ) |
|
{ |
|
// If I'm running a scripted sequence, I need to clean up |
|
if ( m_NPCState == NPC_STATE_SCRIPT && m_hCine ) |
|
{ |
|
if ( !bEnrouteAsWell ) |
|
{ |
|
// |
|
// Don't cancel scripts when they're teleporting an NPC |
|
// to the script for the purposes of movement. |
|
// |
|
if ( ( m_scriptState == CAI_BaseNPC::SCRIPT_WALK_TO_MARK ) || |
|
( m_scriptState == CAI_BaseNPC::SCRIPT_RUN_TO_MARK ) || |
|
( m_scriptState == CAI_BaseNPC::SCRIPT_CUSTOM_MOVE_TO_MARK ) || |
|
m_hCine->IsTeleportingDueToMoveTo() ) |
|
{ |
|
return; |
|
} |
|
} |
|
|
|
m_hCine->ScriptEntityCancel( m_hCine, true ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) |
|
{ |
|
#ifdef HL2_DLL |
|
if ( interactionType == g_interactionBarnacleVictimGrab ) |
|
{ |
|
// Make the victim stop thinking so they're as good as dead without |
|
// shocking the system by destroying the entity. |
|
StopLoopingSounds(); |
|
BarnacleDeathSound(); |
|
SetThink( NULL ); |
|
|
|
// Gag the NPC so they won't talk anymore |
|
AddSpawnFlags( SF_NPC_GAG ); |
|
|
|
// Drop any weapon they're holding |
|
if ( GetActiveWeapon() ) |
|
{ |
|
Weapon_Drop( GetActiveWeapon() ); |
|
} |
|
|
|
return true; |
|
} |
|
#endif // HL2_DLL |
|
|
|
return BaseClass::HandleInteraction( interactionType, data, sourceEnt ); |
|
} |
|
|
|
CAI_BaseNPC *CAI_BaseNPC::GetInteractionPartner( void ) |
|
{ |
|
if ( m_hInteractionPartner == NULL ) |
|
return NULL; |
|
|
|
return m_hInteractionPartner->MyNPCPointer(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when exiting a scripted sequence. |
|
// Output : Returns true if alive, false if dead. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ExitScriptedSequence( ) |
|
{ |
|
if ( m_lifeState == LIFE_DYING ) |
|
{ |
|
// is this legal? |
|
// BUGBUG -- This doesn't call Killed() |
|
SetIdealState( NPC_STATE_DEAD ); |
|
return false; |
|
} |
|
|
|
if (m_hCine) |
|
{ |
|
m_hCine->CancelScript( ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
ConVar sv_test_scripted_sequences( "sv_test_scripted_sequences", "0", FCVAR_NONE, "Tests for scripted sequences that are embedded in the world. Run through your map with this set to check for NPCs falling through the world." ); |
|
|
|
bool CAI_BaseNPC::CineCleanup() |
|
{ |
|
CAI_ScriptedSequence *pOldCine = m_hCine; |
|
int nSavedFlags = ( m_hCine ? m_hCine->m_savedFlags : GetFlags() ); |
|
|
|
bool bDestroyCine = false; |
|
if ( IsRunningDynamicInteraction() ) |
|
{ |
|
bDestroyCine = true; |
|
|
|
// Re-enable physics collisions between me & the other NPC |
|
if ( m_hInteractionPartner ) |
|
{ |
|
PhysEnableEntityCollisions( this, m_hInteractionPartner ); |
|
//Msg("%s(%s) enabled collisions with %s(%s) at %0.2f\n", GetClassname(), GetDebugName(), m_hInteractionPartner->GetClassName(), m_hInteractionPartner->GetDebugName(), gpGlobals->curtime ); |
|
} |
|
|
|
if ( m_hForcedInteractionPartner ) |
|
{ |
|
// We've finished a forced interaction. Let the mapmaker know. |
|
m_OnForcedInteractionFinished.FireOutput( this, this ); |
|
} |
|
|
|
// Clear interaction partner, because we're not running a scripted sequence anymore |
|
m_hInteractionPartner = NULL; |
|
CleanupForcedInteraction(); |
|
} |
|
|
|
// am I linked to a cinematic? |
|
if (m_hCine) |
|
{ |
|
// okay, reset me to what it thought I was before |
|
m_hCine->SetTarget( NULL ); |
|
// NOTE that this will have had EF_NODRAW removed in script.dll when it's cached off |
|
SetEffects( m_hCine->m_saved_effects ); |
|
|
|
SetCollisionGroup( m_hCine->m_savedCollisionGroup ); |
|
} |
|
else |
|
{ |
|
// arg, punt |
|
AddSolidFlags( FSOLID_NOT_STANDABLE ); |
|
} |
|
|
|
m_hCine = NULL; |
|
SetTarget( NULL ); |
|
SetGoalEnt( NULL ); |
|
if (m_lifeState == LIFE_DYING) |
|
{ |
|
// last frame of death animation? |
|
if ( m_iHealth > 0 ) |
|
{ |
|
m_iHealth = 0; |
|
} |
|
|
|
AddSolidFlags( FSOLID_NOT_SOLID ); |
|
SetState( NPC_STATE_DEAD ); |
|
m_lifeState = LIFE_DEAD; |
|
UTIL_SetSize( this, WorldAlignMins(), Vector(WorldAlignMaxs().x, WorldAlignMaxs().y, WorldAlignMins().z + 2) ); |
|
|
|
if ( pOldCine && pOldCine->HasSpawnFlags( SF_SCRIPT_LEAVECORPSE ) ) |
|
{ |
|
SetUse( NULL ); // BUGBUG -- This doesn't call Killed() |
|
SetThink( NULL ); // This will probably break some stuff |
|
SetTouch( NULL ); |
|
} |
|
else |
|
SUB_StartFadeOut(); // SetThink( SUB_DoNothing ); |
|
|
|
|
|
//Not becoming a ragdoll, so set the NOINTERP flag on. |
|
if ( CanBecomeRagdoll() == false ) |
|
{ |
|
StopAnimation(); |
|
IncrementInterpolationFrame(); // Don't interpolate either, assume the corpse is positioned in its final resting place |
|
} |
|
|
|
SetMoveType( MOVETYPE_NONE ); |
|
return false; |
|
} |
|
|
|
// If we actually played a sequence |
|
if ( pOldCine && pOldCine->m_iszPlay != NULL_STRING && pOldCine->PlayedSequence() ) |
|
{ |
|
if ( !pOldCine->HasSpawnFlags(SF_SCRIPT_DONT_TELEPORT_AT_END) ) |
|
{ |
|
// reset position |
|
Vector new_origin; |
|
QAngle new_angle; |
|
GetBonePosition( 0, new_origin, new_angle ); |
|
|
|
// Figure out how far they have moved |
|
// We can't really solve this problem because we can't query the movement of the origin relative |
|
// to the sequence. We can get the root bone's position as we do here, but there are |
|
// cases where the root bone is in a different relative position to the entity's origin |
|
// before/after the sequence plays. So we are stuck doing this: |
|
|
|
// !!!HACKHACK: Float the origin up and drop to floor because some sequences have |
|
// irregular motion that can't be properly accounted for. |
|
|
|
// UNDONE: THIS SHOULD ONLY HAPPEN IF WE ACTUALLY PLAYED THE SEQUENCE. |
|
Vector oldOrigin = GetLocalOrigin(); |
|
|
|
// UNDONE: ugly hack. Don't move NPC if they don't "seem" to move |
|
// this really needs to be done with the AX,AY,etc. flags, but that aren't consistantly |
|
// being set, so animations that really do move won't be caught. |
|
if ((oldOrigin - new_origin).Length2D() < 8.0) |
|
new_origin = oldOrigin; |
|
|
|
Vector origin = GetLocalOrigin(); |
|
|
|
origin.x = new_origin.x; |
|
origin.y = new_origin.y; |
|
origin.z += 1; |
|
|
|
if ( nSavedFlags & FL_FLY ) |
|
{ |
|
origin.z = new_origin.z; |
|
SetLocalOrigin( origin ); |
|
} |
|
else |
|
{ |
|
SetLocalOrigin( origin ); |
|
|
|
int drop = UTIL_DropToFloor( this, MASK_NPCSOLID, UTIL_GetLocalPlayer() ); |
|
|
|
// Origin in solid? Set to org at the end of the sequence |
|
if ( ( drop < 0 ) || sv_test_scripted_sequences.GetBool() ) |
|
{ |
|
SetLocalOrigin( oldOrigin ); |
|
} |
|
else if ( drop == 0 ) // Hanging in air? |
|
{ |
|
Vector origin = GetLocalOrigin(); |
|
origin.z = new_origin.z; |
|
SetLocalOrigin( origin ); |
|
SetGroundEntity( NULL ); |
|
} |
|
} |
|
|
|
origin = GetLocalOrigin(); |
|
|
|
// teleport if it's a non-trivial distance |
|
if ((oldOrigin - origin).Length() > 8.0) |
|
{ |
|
// Call teleport to notify |
|
Teleport( &origin, NULL, NULL ); |
|
SetLocalOrigin( origin ); |
|
IncrementInterpolationFrame(); |
|
} |
|
|
|
if ( m_iHealth <= 0 ) |
|
{ |
|
// Dropping out because he got killed |
|
SetIdealState( NPC_STATE_DEAD ); |
|
SetCondition( COND_LIGHT_DAMAGE ); |
|
m_lifeState = LIFE_DYING; |
|
} |
|
} |
|
|
|
// We should have some animation to put these guys in, but for now it's idle. |
|
// Due to NOINTERP above, there won't be any blending between this anim & the sequence |
|
m_Activity = ACT_RESET; |
|
} |
|
|
|
// set them back into a normal state |
|
if ( m_iHealth > 0 ) |
|
{ |
|
SetIdealState( NPC_STATE_IDLE ); |
|
} |
|
else |
|
{ |
|
// Dropping out because he got killed |
|
SetIdealState( NPC_STATE_DEAD ); |
|
SetCondition( COND_LIGHT_DAMAGE ); |
|
} |
|
|
|
// SetAnimation( m_NPCState ); |
|
CLEARBITS(m_spawnflags, SF_NPC_WAIT_FOR_SCRIPT ); |
|
|
|
if ( bDestroyCine ) |
|
{ |
|
UTIL_Remove( pOldCine ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) |
|
{ |
|
CleanupScriptsOnTeleport( false ); |
|
|
|
BaseClass::Teleport( newPosition, newAngles, newVelocity ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::FindSpotForNPCInRadius( Vector *pResult, const Vector &vStartPos, CAI_BaseNPC *pNPC, float radius, bool bOutOfPlayerViewcone ) |
|
{ |
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
QAngle fan; |
|
|
|
fan.x = 0; |
|
fan.z = 0; |
|
|
|
for( fan.y = 0 ; fan.y < 360 ; fan.y += 18.0 ) |
|
{ |
|
Vector vecTest; |
|
Vector vecDir; |
|
|
|
AngleVectors( fan, &vecDir ); |
|
|
|
vecTest = vStartPos + vecDir * radius; |
|
|
|
if ( bOutOfPlayerViewcone && pPlayer && !pPlayer->FInViewCone( vecTest ) ) |
|
continue; |
|
|
|
trace_t tr; |
|
|
|
UTIL_TraceLine( vecTest, vecTest - Vector( 0, 0, 8192 ), MASK_SHOT, pNPC, COLLISION_GROUP_NONE, &tr ); |
|
if( tr.fraction == 1.0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
UTIL_TraceHull( tr.endpos, |
|
tr.endpos + Vector( 0, 0, 10 ), |
|
pNPC->GetHullMins(), |
|
pNPC->GetHullMaxs(), |
|
MASK_NPCSOLID, |
|
pNPC, |
|
COLLISION_GROUP_NONE, |
|
&tr ); |
|
|
|
if( tr.fraction == 1.0 && pNPC->GetMoveProbe()->CheckStandPosition( tr.endpos, MASK_NPCSOLID ) ) |
|
{ |
|
*pResult = tr.endpos; |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsNavigationUrgent() |
|
{ |
|
// return true if the navigation is for something that can't react well to failure |
|
if ( IsCurSchedule( SCHED_SCRIPTED_WALK, false ) || |
|
IsCurSchedule( SCHED_SCRIPTED_RUN, false ) || |
|
IsCurSchedule( SCHED_SCRIPTED_CUSTOM_MOVE, false ) || |
|
( IsCurSchedule( SCHED_SCENE_GENERIC, false ) && IsInLockedScene() ) ) |
|
{ |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::ShouldFailNav( bool bMovementFailed ) |
|
{ |
|
#ifdef HL2_EPISODIC |
|
|
|
if ( ai_vehicle_avoidance.GetBool() ) |
|
{ |
|
// Never be blocked this way by a vehicle (creates too many headaches around the levels) |
|
CBaseEntity *pEntity = GetNavigator()->GetBlockingEntity(); |
|
if ( pEntity && pEntity->GetServerVehicle() ) |
|
{ |
|
// Vital allies never get stuck, and urgent moves cannot be blocked by a vehicle |
|
if ( Classify() == CLASS_PLAYER_ALLY_VITAL || IsNavigationUrgent() ) |
|
return false; |
|
} |
|
} |
|
|
|
#endif // HL2_EPISODIC |
|
|
|
// It's up to the schedule that requested movement to deal with failed movement. Currently, only a handfull of |
|
// schedules are considered Urgent, and they need to deal with what to do when there's no route, which by inspection |
|
// they'd don't. |
|
|
|
if ( IsNavigationUrgent()) |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
Navigation_t CAI_BaseNPC::GetNavType() const |
|
{ |
|
return m_pNavigator->GetNavType(); |
|
} |
|
|
|
void CAI_BaseNPC::SetNavType( Navigation_t navType ) |
|
{ |
|
m_pNavigator->SetNavType( navType ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// NPCs can override this to tweak with how costly particular movements are |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::MovementCost( int moveType, const Vector &vecStart, const Vector &vecEnd, float *pCost ) |
|
{ |
|
// We have nothing to say on the matter, but derived classes might |
|
return false; |
|
} |
|
|
|
bool CAI_BaseNPC::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval ) |
|
{ |
|
return false; |
|
} |
|
|
|
bool CAI_BaseNPC::OverrideMove( float flInterval ) |
|
{ |
|
return false; |
|
} |
|
|
|
|
|
//========================================================= |
|
// VecToYaw - turns a directional vector into a yaw value |
|
// that points down that vector. |
|
//========================================================= |
|
float CAI_BaseNPC::VecToYaw( const Vector &vecDir ) |
|
{ |
|
if (vecDir.x == 0 && vecDir.y == 0 && vecDir.z == 0) |
|
return GetLocalAngles().y; |
|
|
|
return UTIL_VecToYaw( vecDir ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Inherited from IAI_MotorMovementServices |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseNPC::CalcYawSpeed( void ) |
|
{ |
|
// Negative values are invalud |
|
return -1.0f; |
|
} |
|
|
|
bool CAI_BaseNPC::OnCalcBaseMove( AILocalMoveGoal_t *pMoveGoal, |
|
float distClear, |
|
AIMoveResult_t *pResult ) |
|
{ |
|
if ( pMoveGoal->directTrace.pObstruction ) |
|
{ |
|
CBasePropDoor *pPropDoor = dynamic_cast<CBasePropDoor *>( pMoveGoal->directTrace.pObstruction ); |
|
if ( pPropDoor && OnUpcomingPropDoor( pMoveGoal, pPropDoor, distClear, pResult ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
bool CAI_BaseNPC::OnObstructionPreSteer( AILocalMoveGoal_t *pMoveGoal, |
|
float distClear, |
|
AIMoveResult_t *pResult ) |
|
{ |
|
if ( pMoveGoal->directTrace.pObstruction ) |
|
{ |
|
CBaseDoor *pDoor = dynamic_cast<CBaseDoor *>( pMoveGoal->directTrace.pObstruction ); |
|
if ( pDoor && OnObstructingDoor( pMoveGoal, pDoor, distClear, pResult ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
bool CAI_BaseNPC::OnObstructingDoor( AILocalMoveGoal_t *pMoveGoal, |
|
CBaseDoor *pDoor, |
|
float distClear, |
|
AIMoveResult_t *pResult ) |
|
{ |
|
if ( pMoveGoal->maxDist < distClear ) |
|
return false; |
|
|
|
// By default, NPCs don't know how to open doors |
|
if ( pDoor->m_toggle_state == TS_AT_BOTTOM || pDoor->m_toggle_state == TS_GOING_DOWN ) |
|
{ |
|
if ( distClear < 0.1 ) |
|
{ |
|
*pResult = AIMR_BLOCKED_ENTITY; |
|
} |
|
else |
|
{ |
|
pMoveGoal->maxDist = distClear; |
|
*pResult = AIMR_OK; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : pMoveGoal - |
|
// pDoor - |
|
// distClear - |
|
// default - |
|
// spawn - |
|
// oldorg - |
|
// pfPosition - |
|
// neworg - |
|
// Output : Returns true if movement is solved, false otherwise. |
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::OnUpcomingPropDoor( AILocalMoveGoal_t *pMoveGoal, |
|
CBasePropDoor *pDoor, |
|
float distClear, |
|
AIMoveResult_t *pResult ) |
|
{ |
|
if ( (pMoveGoal->flags & AILMG_TARGET_IS_GOAL) && pMoveGoal->maxDist < distClear ) |
|
return false; |
|
|
|
if ( pMoveGoal->maxDist + GetHullWidth() * .25 < distClear ) |
|
return false; |
|
|
|
if (pDoor == m_hOpeningDoor) |
|
{ |
|
if ( pDoor->IsNPCOpening( this ) ) |
|
{ |
|
// We're in the process of opening the door, don't be blocked by it. |
|
pMoveGoal->maxDist = distClear; |
|
*pResult = AIMR_OK; |
|
return true; |
|
} |
|
m_hOpeningDoor = NULL; |
|
} |
|
|
|
if ((CapabilitiesGet() & bits_CAP_DOORS_GROUP) && !pDoor->IsDoorLocked() && (pDoor->IsDoorClosed() || pDoor->IsDoorClosing())) |
|
{ |
|
AI_Waypoint_t *pOpenDoorRoute = NULL; |
|
|
|
opendata_t opendata; |
|
pDoor->GetNPCOpenData(this, opendata); |
|
|
|
// dvs: FIXME: local route might not be sufficient |
|
pOpenDoorRoute = GetPathfinder()->BuildLocalRoute( |
|
GetLocalOrigin(), |
|
opendata.vecStandPos, |
|
NULL, |
|
bits_WP_TO_DOOR | bits_WP_DONT_SIMPLIFY, |
|
NO_NODE, |
|
bits_BUILD_GROUND | bits_BUILD_IGNORE_NPCS, |
|
0.0); |
|
|
|
if ( pOpenDoorRoute ) |
|
{ |
|
if ( AIIsDebuggingDoors(this) ) |
|
{ |
|
NDebugOverlay::Cross3D(opendata.vecStandPos + Vector(0,0,1), 32, 255, 255, 255, false, 1.0 ); |
|
Msg( "Opening door!\n" ); |
|
} |
|
|
|
// Attach the door to the waypoint so we open it when we get there. |
|
// dvs: FIXME: this is kind of bullshit, I need to find the exact waypoint to open the door |
|
// should I just walk the path until I find it? |
|
pOpenDoorRoute->m_hData = pDoor; |
|
|
|
GetNavigator()->GetPath()->PrependWaypoints( pOpenDoorRoute ); |
|
|
|
m_hOpeningDoor = pDoor; |
|
pMoveGoal->maxDist = distClear; |
|
*pResult = AIMR_CHANGE_TYPE; |
|
|
|
return true; |
|
} |
|
else |
|
AIDoorDebugMsg( this, "Failed create door route!\n" ); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called by the navigator to initiate the opening of a prop_door |
|
// that is in our way. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OpenPropDoorBegin( CBasePropDoor *pDoor ) |
|
{ |
|
// dvs: not quite working, disabled for now. |
|
//opendata_t opendata; |
|
//pDoor->GetNPCOpenData(this, opendata); |
|
// |
|
//if (HaveSequenceForActivity(opendata.eActivity)) |
|
//{ |
|
// SetIdealActivity(opendata.eActivity); |
|
//} |
|
//else |
|
{ |
|
// We don't have an appropriate sequence, just open the door magically. |
|
OpenPropDoorNow( pDoor ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when we are trying to open a prop_door and it's time to start |
|
// the door moving. This is called either in response to an anim event |
|
// or as a fallback when we don't have an appropriate open activity. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OpenPropDoorNow( CBasePropDoor *pDoor ) |
|
{ |
|
// Start the door moving. |
|
pDoor->NPCOpenDoor(this); |
|
|
|
// Wait for the door to finish opening before trying to move through the doorway. |
|
m_flMoveWaitFinished = gpGlobals->curtime + pDoor->GetOpenInterval(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when the door we were trying to open becomes fully open. |
|
// Input : pDoor - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnDoorFullyOpen(CBasePropDoor *pDoor) |
|
{ |
|
// We're done with the door. |
|
m_hOpeningDoor = NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when the door we were trying to open becomes blocked before opening. |
|
// Input : pDoor - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::OnDoorBlocked(CBasePropDoor *pDoor) |
|
{ |
|
// dvs: FIXME: do something so that we don't loop forever trying to open this door |
|
// not clearing out the door handle will cause the NPC to invalidate the connection |
|
// We're done with the door. |
|
//m_hOpeningDoor = NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Template NPCs are marked as templates by the level designer. They |
|
// do not spawn, but their keyvalues are saved for use by a template |
|
// spawner. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsTemplate( void ) |
|
{ |
|
return HasSpawnFlags( SF_NPC_TEMPLATE ); |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Movement code for walking + flying |
|
// |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::FlyMove( const Vector& pfPosition, unsigned int mask ) |
|
{ |
|
Vector oldorg, neworg; |
|
trace_t trace; |
|
|
|
// try the move |
|
VectorCopy( GetAbsOrigin(), oldorg ); |
|
VectorAdd( oldorg, pfPosition, neworg ); |
|
UTIL_TraceEntity( this, oldorg, neworg, mask, &trace ); |
|
if (trace.fraction == 1) |
|
{ |
|
if ( (GetFlags() & FL_SWIM) && enginetrace->GetPointContents(trace.endpos) == CONTENTS_EMPTY ) |
|
return false; // swim monster left water |
|
|
|
SetAbsOrigin( trace.endpos ); |
|
PhysicsTouchTriggers(); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : ent - |
|
// Dir - Normalized direction vector for movement. |
|
// dist - Distance along 'Dir' to move. |
|
// iMode - |
|
// Output : Returns nonzero on success, zero on failure. |
|
//----------------------------------------------------------------------------- |
|
int CAI_BaseNPC::WalkMove( const Vector& vecPosition, unsigned int mask ) |
|
{ |
|
if ( GetFlags() & (FL_FLY | FL_SWIM) ) |
|
{ |
|
return FlyMove( vecPosition, mask ); |
|
} |
|
|
|
if ( (GetFlags() & FL_ONGROUND) == 0 ) |
|
{ |
|
return 0; |
|
} |
|
|
|
trace_t trace; |
|
Vector oldorg, neworg, end; |
|
Vector move( vecPosition[0], vecPosition[1], 0.0f ); |
|
VectorCopy( GetAbsOrigin(), oldorg ); |
|
VectorAdd( oldorg, move, neworg ); |
|
|
|
// push down from a step height above the wished position |
|
float flStepSize = sv_stepsize.GetFloat(); |
|
neworg[2] += flStepSize; |
|
VectorCopy(neworg, end); |
|
end[2] -= flStepSize*2; |
|
|
|
UTIL_TraceEntity( this, neworg, end, mask, &trace ); |
|
if ( trace.allsolid ) |
|
return false; |
|
|
|
if (trace.startsolid) |
|
{ |
|
neworg[2] -= flStepSize; |
|
UTIL_TraceEntity( this, neworg, end, mask, &trace ); |
|
if ( trace.allsolid || trace.startsolid ) |
|
return false; |
|
} |
|
|
|
if (trace.fraction == 1) |
|
{ |
|
// if monster had the ground pulled out, go ahead and fall |
|
if ( GetFlags() & FL_PARTIALGROUND ) |
|
{ |
|
SetAbsOrigin( oldorg + move ); |
|
PhysicsTouchTriggers(); |
|
SetGroundEntity( NULL ); |
|
return true; |
|
} |
|
|
|
return false; // walked off an edge |
|
} |
|
|
|
// check point traces down for dangling corners |
|
SetAbsOrigin( trace.endpos ); |
|
|
|
if (UTIL_CheckBottom( this, NULL, flStepSize ) == 0) |
|
{ |
|
if ( GetFlags() & FL_PARTIALGROUND ) |
|
{ |
|
// entity had floor mostly pulled out from underneath it |
|
// and is trying to correct |
|
PhysicsTouchTriggers(); |
|
return true; |
|
} |
|
|
|
// Reset to original position |
|
SetAbsOrigin( oldorg ); |
|
return false; |
|
} |
|
|
|
if ( GetFlags() & FL_PARTIALGROUND ) |
|
{ |
|
// Con_Printf ("back on ground\n"); |
|
RemoveFlag( FL_PARTIALGROUND ); |
|
} |
|
|
|
// the move is ok |
|
SetGroundEntity( trace.m_pEnt ); |
|
PhysicsTouchTriggers(); |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
static void AIMsgGuts( CAI_BaseNPC *pAI, unsigned flags, const char *pszMsg ) |
|
{ |
|
int len = strlen( pszMsg ); |
|
const char *pszFmt2 = NULL; |
|
|
|
if ( len && pszMsg[len-1] == '\n' ) |
|
{ |
|
(const_cast<char *>(pszMsg))[len-1] = 0; |
|
pszFmt2 = "%s (%s: %d/%s) [%d]\n"; |
|
} |
|
else |
|
pszFmt2 = "%s (%s: %d/%s) [%d]"; |
|
|
|
DevMsg( pszFmt2, |
|
pszMsg, |
|
pAI->GetClassname(), |
|
pAI->entindex(), |
|
( pAI->GetEntityName() == NULL_STRING ) ? "<unnamed>" : STRING(pAI->GetEntityName()), |
|
gpGlobals->tickcount ); |
|
} |
|
|
|
void DevMsg( CAI_BaseNPC *pAI, unsigned flags, const char *pszFormat, ... ) |
|
{ |
|
if ( (flags & AIMF_IGNORE_SELECTED) || (pAI->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
va_list ap; |
|
va_start(ap, pszFormat); |
|
char szTempMsgBuf[512]; |
|
V_vsprintf_safe( szTempMsgBuf, pszFormat, ap ); |
|
|
|
AIMsgGuts( pAI, flags, szTempMsgBuf ); |
|
va_end(ap); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void DevMsg( CAI_BaseNPC *pAI, const char *pszFormat, ... ) |
|
{ |
|
if ( (pAI->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
va_list ap; |
|
va_start(ap, pszFormat); |
|
char szTempMsgBuf[512]; |
|
V_vsprintf_safe( szTempMsgBuf, pszFormat, ap ); |
|
|
|
AIMsgGuts( pAI, 0, szTempMsgBuf ); |
|
va_end(ap); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsPlayerAlly( CBasePlayer *pPlayer ) |
|
{ |
|
if ( pPlayer == NULL ) |
|
{ |
|
// in multiplayer mode we need a valid pPlayer |
|
// or override this virtual function |
|
if ( !AI_IsSinglePlayer() ) |
|
return false; |
|
|
|
// NULL means single player mode |
|
pPlayer = UTIL_GetLocalPlayer(); |
|
} |
|
|
|
return ( !pPlayer || IRelationType( pPlayer ) == D_LI ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::SetCommandGoal( const Vector &vecGoal ) |
|
{ |
|
m_vecCommandGoal = vecGoal; |
|
m_CommandMoveMonitor.ClearMark(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::ClearCommandGoal() |
|
{ |
|
m_vecCommandGoal = vec3_invalid; |
|
m_CommandMoveMonitor.ClearMark(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsInPlayerSquad() const |
|
{ |
|
return ( m_pSquad && MAKE_STRING(m_pSquad->GetName()) == GetPlayerSquadName() && !CAI_Squad::IsSilentMember(this) ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::CanBeUsedAsAFriend( void ) |
|
{ |
|
if ( IsCurSchedule(SCHED_FORCED_GO) || IsCurSchedule(SCHED_FORCED_GO_RUN) ) |
|
return false; |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
Vector CAI_BaseNPC::GetSmoothedVelocity( void ) |
|
{ |
|
if( GetNavType() == NAV_GROUND || GetNavType() == NAV_FLY ) |
|
{ |
|
return m_pMotor->GetCurVel(); |
|
} |
|
|
|
return BaseClass::GetSmoothedVelocity(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsCoverPosition( const Vector &vecThreat, const Vector &vecPosition ) |
|
{ |
|
trace_t tr; |
|
|
|
// By default, we ignore the viewer (me) when determining cover positions |
|
CTraceFilterLOS filter( NULL, COLLISION_GROUP_NONE, this ); |
|
|
|
// If I'm trying to find cover from the player, and the player is in a vehicle, |
|
// ignore the vehicle for the purpose of determining line of sight. |
|
CBaseEntity *pEnemy = GetEnemy(); |
|
if ( pEnemy ) |
|
{ |
|
// Hack to see if our threat position is our enemy |
|
bool bThreatPosIsEnemy = ( (vecThreat - GetEnemy()->EyePosition()).LengthSqr() < 0.1f ); |
|
if ( bThreatPosIsEnemy ) |
|
{ |
|
CBaseCombatCharacter *pCCEnemy = GetEnemy()->MyCombatCharacterPointer(); |
|
if ( pCCEnemy != NULL && pCCEnemy->IsInAVehicle() ) |
|
{ |
|
// Ignore the vehicle |
|
filter.SetPassEntity( pCCEnemy->GetVehicleEntity() ); |
|
} |
|
|
|
if ( !filter.GetPassEntity() ) |
|
{ |
|
filter.SetPassEntity( pEnemy ); |
|
} |
|
} |
|
} |
|
|
|
AI_TraceLOS( vecThreat, vecPosition, this, &tr, &filter ); |
|
|
|
if( tr.fraction != 1.0 && hl2_episodic.GetBool() ) |
|
{ |
|
if( tr.m_pEnt->m_iClassname == m_iClassname ) |
|
{ |
|
// Don't hide behind buddies! |
|
return false; |
|
} |
|
} |
|
|
|
return (tr.fraction != 1.0); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
float CAI_BaseNPC::SetWait( float minWait, float maxWait ) |
|
{ |
|
int minThinks = Ceil2Int( minWait * 10 ); |
|
|
|
if ( maxWait == 0.0 ) |
|
{ |
|
m_flWaitFinished = gpGlobals->curtime + ( 0.1 * minThinks ); |
|
} |
|
else |
|
{ |
|
if ( minThinks == 0 ) // random 0..n is almost certain to not return 0 |
|
minThinks = 1; |
|
int maxThinks = Ceil2Int( maxWait * 10 ); |
|
|
|
m_flWaitFinished = gpGlobals->curtime + ( 0.1 * random->RandomInt( minThinks, maxThinks ) ); |
|
} |
|
return m_flWaitFinished; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseNPC::ClearWait() |
|
{ |
|
m_flWaitFinished = FLT_MAX; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsWaitFinished() |
|
{ |
|
return ( gpGlobals->curtime >= m_flWaitFinished ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::IsWaitSet() |
|
{ |
|
return ( m_flWaitFinished != FLT_MAX ); |
|
} |
|
|
|
void CAI_BaseNPC::TestPlayerPushing( CBaseEntity *pEntity ) |
|
{ |
|
if ( HasSpawnFlags( SF_NPC_NO_PLAYER_PUSHAWAY ) ) |
|
return; |
|
|
|
// Heuristic for determining if the player is pushing me away |
|
CBasePlayer *pPlayer = ToBasePlayer( pEntity ); |
|
if ( pPlayer && !( pPlayer->GetFlags() & FL_NOTARGET ) ) |
|
{ |
|
if ( (pPlayer->m_nButtons & (IN_FORWARD|IN_BACK|IN_MOVELEFT|IN_MOVERIGHT)) || |
|
pPlayer->GetAbsVelocity().AsVector2D().LengthSqr() > 50*50 ) |
|
{ |
|
SetCondition( COND_PLAYER_PUSHING ); |
|
Vector vecPush = GetAbsOrigin() - pPlayer->GetAbsOrigin(); |
|
VectorNormalize( vecPush ); |
|
CascadePlayerPush( vecPush, pPlayer->WorldSpaceCenter() ); |
|
} |
|
} |
|
} |
|
|
|
void CAI_BaseNPC::CascadePlayerPush( const Vector &push, const Vector &pushOrigin ) |
|
{ |
|
// |
|
// Try to push any friends that are in the way. |
|
// |
|
float hullWidth = GetHullWidth(); |
|
const Vector & origin = GetAbsOrigin(); |
|
const Vector2D &origin2D = origin.AsVector2D(); |
|
|
|
const float MIN_Z_TO_TRANSMIT = GetHullHeight() * 0.5 + 0.1; |
|
const float DIST_REQD_TO_TRANSMIT_PUSH_SQ = Square( hullWidth * 5 + 0.1 ); |
|
const float DIST_FROM_PUSH_VECTOR_REQD_SQ = Square( hullWidth + 0.1 ); |
|
|
|
Vector2D pushTestPoint = vec2_invalid; |
|
|
|
for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) |
|
{ |
|
CAI_BaseNPC *pOther = g_AI_Manager.AccessAIs()[i]; |
|
if ( pOther != this && pOther->IRelationType(this) == D_LI && !pOther->HasCondition( COND_PLAYER_PUSHING ) ) |
|
{ |
|
const Vector &friendOrigin = pOther->GetAbsOrigin(); |
|
if ( fabsf( friendOrigin.z - origin.z ) < MIN_Z_TO_TRANSMIT && |
|
( friendOrigin.AsVector2D() - origin.AsVector2D() ).LengthSqr() < DIST_REQD_TO_TRANSMIT_PUSH_SQ ) |
|
{ |
|
if ( pushTestPoint == vec2_invalid ) |
|
{ |
|
pushTestPoint = origin2D - pushOrigin.AsVector2D(); |
|
// No normalize, since it wants to just be a big number and we can't be less that a hull away |
|
pushTestPoint *= 2000; |
|
pushTestPoint += origin2D; |
|
|
|
} |
|
float t; |
|
float distSq = CalcDistanceSqrToLine2D( friendOrigin.AsVector2D(), origin2D, pushTestPoint, &t ); |
|
if ( t > 0 && distSq < DIST_FROM_PUSH_VECTOR_REQD_SQ ) |
|
{ |
|
pOther->SetCondition( COND_PLAYER_PUSHING ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Break into pieces! |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::Break( CBaseEntity *pBreaker ) |
|
{ |
|
m_takedamage = DAMAGE_NO; |
|
|
|
Vector velocity; |
|
AngularImpulse angVelocity; |
|
IPhysicsObject *pPhysics = VPhysicsGetObject(); |
|
Vector origin; |
|
QAngle angles; |
|
AddSolidFlags( FSOLID_NOT_SOLID ); |
|
if ( pPhysics ) |
|
{ |
|
pPhysics->GetVelocity( &velocity, &angVelocity ); |
|
pPhysics->GetPosition( &origin, &angles ); |
|
pPhysics->RecheckCollisionFilter(); |
|
} |
|
else |
|
{ |
|
velocity = GetAbsVelocity(); |
|
QAngleToAngularImpulse( GetLocalAngularVelocity(), angVelocity ); |
|
origin = GetAbsOrigin(); |
|
angles = GetAbsAngles(); |
|
} |
|
|
|
breakablepropparams_t params( GetAbsOrigin(), GetAbsAngles(), velocity, angVelocity ); |
|
params.impactEnergyScale = m_impactEnergyScale; |
|
params.defCollisionGroup = GetCollisionGroup(); |
|
if ( params.defCollisionGroup == COLLISION_GROUP_NONE ) |
|
{ |
|
// don't automatically make anything COLLISION_GROUP_NONE or it will |
|
// collide with debris being ejected by breaking |
|
params.defCollisionGroup = COLLISION_GROUP_INTERACTIVE; |
|
} |
|
|
|
// no damage/damage force? set a burst of 100 for some movement |
|
params.defBurstScale = 100;//pDamageInfo ? 0 : 100; |
|
PropBreakableCreateAll( GetModelIndex(), pPhysics, params, this, -1, false ); |
|
|
|
UTIL_Remove(this); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Input handler for breaking the breakable immediately. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputBreak( inputdata_t &inputdata ) |
|
{ |
|
Break( inputdata.pActivator ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseNPC::FindNearestValidGoalPos( const Vector &vTestPoint, Vector *pResult ) |
|
{ |
|
AIMoveTrace_t moveTrace; |
|
Vector vCandidate = vec3_invalid; |
|
if ( GetNavigator()->CanFitAtPosition( vTestPoint, MASK_SOLID_BRUSHONLY ) ) |
|
{ |
|
if ( GetMoveProbe()->CheckStandPosition( vTestPoint, MASK_SOLID_BRUSHONLY ) ) |
|
{ |
|
vCandidate = vTestPoint; |
|
} |
|
} |
|
|
|
if ( vCandidate == vec3_invalid ) |
|
{ |
|
int iNearestNode = GetPathfinder()->NearestNodeToPoint( vTestPoint ); |
|
if ( iNearestNode != NO_NODE ) |
|
{ |
|
GetMoveProbe()->MoveLimit( NAV_GROUND, |
|
g_pBigAINet->GetNodePosition(GetHullType(), iNearestNode ), |
|
vTestPoint, |
|
MASK_SOLID_BRUSHONLY, |
|
NULL, |
|
0, |
|
&moveTrace ); |
|
if ( ( moveTrace.vEndPosition - vTestPoint ).Length2DSqr() < Square( GetHullWidth() * 3.0 ) && |
|
GetMoveProbe()->CheckStandPosition( moveTrace.vEndPosition, MASK_SOLID_BRUSHONLY ) ) |
|
{ |
|
vCandidate = moveTrace.vEndPosition; |
|
} |
|
} |
|
} |
|
|
|
if ( vCandidate != vec3_invalid ) |
|
{ |
|
AI_Waypoint_t *pPathToPoint = GetPathfinder()->BuildRoute( GetAbsOrigin(), vCandidate, AI_GetSinglePlayer(), 5*12, NAV_NONE, true ); |
|
if ( pPathToPoint ) |
|
{ |
|
GetPathfinder()->UnlockRouteNodes( pPathToPoint ); |
|
CAI_Path tempPath; |
|
tempPath.SetWaypoints( pPathToPoint ); // path object will delete waypoints |
|
} |
|
else |
|
vCandidate = vec3_invalid; |
|
} |
|
|
|
if ( vCandidate == vec3_invalid ) |
|
{ |
|
GetMoveProbe()->MoveLimit( NAV_GROUND, |
|
GetAbsOrigin(), |
|
vTestPoint, |
|
MASK_SOLID_BRUSHONLY, |
|
NULL, |
|
0, |
|
&moveTrace ); |
|
vCandidate = moveTrace.vEndPosition; |
|
} |
|
|
|
if ( vCandidate == vec3_invalid ) |
|
return false; |
|
|
|
if ( pResult != NULL ) |
|
{ |
|
*pResult = vCandidate; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Pass a direction to get how far an NPC would see if facing |
|
// that direction. Pass nothing to get the length of the NPC's |
|
// current line of sight. |
|
//--------------------------------------------------------- |
|
float CAI_BaseNPC::LineOfSightDist( const Vector &vecDir, float zEye ) |
|
{ |
|
Vector testDir; |
|
if( vecDir == vec3_invalid ) |
|
{ |
|
testDir = EyeDirection3D(); |
|
} |
|
else |
|
{ |
|
testDir = vecDir; |
|
} |
|
|
|
if ( zEye == FLT_MAX ) |
|
zEye = EyePosition().z; |
|
|
|
trace_t tr; |
|
// Need to center trace so don't get erratic results based on orientation |
|
Vector testPos( GetAbsOrigin().x, GetAbsOrigin().y, zEye ); |
|
AI_TraceLOS( testPos, testPos + testDir * MAX_COORD_RANGE, this, &tr ); |
|
return (tr.startpos - tr.endpos ).Length(); |
|
} |
|
|
|
ConVar ai_LOS_mode( "ai_LOS_mode", "0", FCVAR_REPLICATED ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Use this to perform AI tracelines that are trying to determine LOS between points. |
|
// LOS checks between entities should use FVisible. |
|
//----------------------------------------------------------------------------- |
|
void AI_TraceLOS( const Vector& vecAbsStart, const Vector& vecAbsEnd, CBaseEntity *pLooker, trace_t *ptr, ITraceFilter *pFilter ) |
|
{ |
|
AI_PROFILE_SCOPE( AI_TraceLOS ); |
|
|
|
if ( ai_LOS_mode.GetBool() ) |
|
{ |
|
// Don't use LOS tracefilter |
|
UTIL_TraceLine( vecAbsStart, vecAbsEnd, MASK_BLOCKLOS, pLooker, COLLISION_GROUP_NONE, ptr ); |
|
return; |
|
} |
|
|
|
// Use the custom LOS trace filter |
|
CTraceFilterLOS traceFilter( pLooker, COLLISION_GROUP_NONE ); |
|
if ( !pFilter ) |
|
pFilter = &traceFilter; |
|
AI_TraceLine( vecAbsStart, vecAbsEnd, MASK_BLOCKLOS_AND_NPCS, pFilter, ptr ); |
|
} |
|
|
|
void CAI_BaseNPC::InputSetSpeedModifierRadius( inputdata_t &inputdata ) |
|
{ |
|
m_iSpeedModRadius = inputdata.value.Int(); |
|
m_iSpeedModRadius *= m_iSpeedModRadius; |
|
} |
|
void CAI_BaseNPC::InputSetSpeedModifierSpeed( inputdata_t &inputdata ) |
|
{ |
|
m_iSpeedModSpeed = inputdata.value.Int(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsAllowedToDodge( void ) |
|
{ |
|
// Can't do it if I'm not available |
|
if ( m_NPCState != NPC_STATE_IDLE && m_NPCState != NPC_STATE_ALERT && m_NPCState != NPC_STATE_COMBAT ) |
|
return false; |
|
|
|
return ( m_flNextDodgeTime <= gpGlobals->curtime ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ParseScriptedNPCInteractions( void ) |
|
{ |
|
// Already parsed them? |
|
if ( m_ScriptedInteractions.Count() ) |
|
return; |
|
|
|
// Parse the model's key values and find any dynamic interactions |
|
KeyValues *modelKeyValues = new KeyValues(""); |
|
CUtlBuffer buf( 1024, 0, CUtlBuffer::TEXT_BUFFER ); |
|
|
|
if (! modelinfo->GetModelKeyValue( GetModel(), buf )) |
|
return; |
|
|
|
if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), buf ) ) |
|
{ |
|
// Do we have a dynamic interactions section? |
|
KeyValues *pkvInteractions = modelKeyValues->FindKey("dynamic_interactions"); |
|
if ( pkvInteractions ) |
|
{ |
|
KeyValues *pkvNode = pkvInteractions->GetFirstSubKey(); |
|
while ( pkvNode ) |
|
{ |
|
ScriptedNPCInteraction_t sInteraction; |
|
sInteraction.iszInteractionName = AllocPooledString( pkvNode->GetName() ); |
|
|
|
// Trigger method |
|
const char *pszTrigger = pkvNode->GetString( "trigger", NULL ); |
|
if ( pszTrigger ) |
|
{ |
|
if ( !Q_strncmp( pszTrigger, "auto_in_combat", 14) ) |
|
{ |
|
sInteraction.iTriggerMethod = SNPCINT_AUTOMATIC_IN_COMBAT; |
|
} |
|
} |
|
|
|
// Loop Break trigger method |
|
pszTrigger = pkvNode->GetString( "loop_break_trigger", NULL ); |
|
if ( pszTrigger ) |
|
{ |
|
char szTrigger[256]; |
|
Q_strncpy( szTrigger, pszTrigger, sizeof(szTrigger) ); |
|
char *pszParam = strtok( szTrigger, " " ); |
|
while (pszParam) |
|
{ |
|
if ( !Q_strncmp( pszParam, "on_damage", 9) ) |
|
{ |
|
sInteraction.iLoopBreakTriggerMethod |= SNPCINT_LOOPBREAK_ON_DAMAGE; |
|
} |
|
if ( !Q_strncmp( pszParam, "on_flashlight_illum", 19) ) |
|
{ |
|
sInteraction.iLoopBreakTriggerMethod |= SNPCINT_LOOPBREAK_ON_FLASHLIGHT_ILLUM; |
|
} |
|
|
|
pszParam = strtok(NULL," "); |
|
} |
|
} |
|
|
|
// Origin |
|
const char *pszOrigin = pkvNode->GetString( "origin_relative", "0 0 0" ); |
|
UTIL_StringToVector( sInteraction.vecRelativeOrigin.Base(), pszOrigin ); |
|
|
|
// Angles |
|
const char *pszAngles = pkvNode->GetString( "angles_relative", NULL ); |
|
if ( pszAngles ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_TEST_OTHER_ANGLES; |
|
UTIL_StringToVector( sInteraction.angRelativeAngles.Base(), pszAngles ); |
|
} |
|
|
|
// Velocity |
|
const char *pszVelocity = pkvNode->GetString( "velocity_relative", NULL ); |
|
if ( pszVelocity ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_TEST_OTHER_VELOCITY; |
|
UTIL_StringToVector( sInteraction.vecRelativeVelocity.Base(), pszVelocity ); |
|
} |
|
|
|
// Entry Sequence |
|
const char *pszSequence = pkvNode->GetString( "entry_sequence", NULL ); |
|
if ( pszSequence ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_ENTRY].iszSequence = AllocPooledString( pszSequence ); |
|
} |
|
// Entry Activity |
|
const char *pszActivity = pkvNode->GetString( "entry_activity", NULL ); |
|
if ( pszActivity ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_ENTRY].iActivity = GetActivityID( pszActivity ); |
|
} |
|
|
|
// Sequence |
|
pszSequence = pkvNode->GetString( "sequence", NULL ); |
|
if ( pszSequence ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_SEQUENCE].iszSequence = AllocPooledString( pszSequence ); |
|
} |
|
// Activity |
|
pszActivity = pkvNode->GetString( "activity", NULL ); |
|
if ( pszActivity ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_SEQUENCE].iActivity = GetActivityID( pszActivity ); |
|
} |
|
|
|
// Exit Sequence |
|
pszSequence = pkvNode->GetString( "exit_sequence", NULL ); |
|
if ( pszSequence ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_EXIT].iszSequence = AllocPooledString( pszSequence ); |
|
} |
|
// Exit Activity |
|
pszActivity = pkvNode->GetString( "exit_activity", NULL ); |
|
if ( pszActivity ) |
|
{ |
|
sInteraction.sPhases[SNPCINT_EXIT].iActivity = GetActivityID( pszActivity ); |
|
} |
|
|
|
// Delay |
|
sInteraction.flDelay = pkvNode->GetFloat( "delay", 10.0 ); |
|
|
|
// Delta |
|
sInteraction.flDistSqr = pkvNode->GetFloat( "origin_max_delta", (DSS_MAX_DIST * DSS_MAX_DIST) ); |
|
|
|
// Loop? |
|
if ( pkvNode->GetFloat( "loop_in_action", 0 ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_LOOP_IN_ACTION; |
|
} |
|
|
|
// Fixup position? |
|
const char *pszDontFixup = pkvNode->GetString( "dont_teleport_at_end", NULL ); |
|
if ( pszDontFixup ) |
|
{ |
|
if ( !Q_stricmp( pszDontFixup, "me" ) || !Q_stricmp( pszDontFixup, "both" ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_ME; |
|
} |
|
else if ( !Q_stricmp( pszDontFixup, "them" ) || !Q_stricmp( pszDontFixup, "both" ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_THEM; |
|
} |
|
} |
|
|
|
// Needs a weapon? |
|
const char *pszNeedsWeapon = pkvNode->GetString( "needs_weapon", NULL ); |
|
if ( pszNeedsWeapon ) |
|
{ |
|
if ( !Q_strncmp( pszNeedsWeapon, "ME", 2 ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_ME; |
|
} |
|
else if ( !Q_strncmp( pszNeedsWeapon, "THEM", 4 ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_THEM; |
|
} |
|
else if ( !Q_strncmp( pszNeedsWeapon, "BOTH", 4 ) ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_ME; |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_THEM; |
|
} |
|
} |
|
|
|
// Specific weapon types |
|
const char *pszWeaponName = pkvNode->GetString( "weapon_mine", NULL ); |
|
if ( pszWeaponName ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_ME; |
|
sInteraction.iszMyWeapon = AllocPooledString( pszWeaponName ); |
|
} |
|
pszWeaponName = pkvNode->GetString( "weapon_theirs", NULL ); |
|
if ( pszWeaponName ) |
|
{ |
|
sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_THEM; |
|
sInteraction.iszTheirWeapon = AllocPooledString( pszWeaponName ); |
|
} |
|
|
|
// Add it to the list |
|
AddScriptedNPCInteraction( &sInteraction ); |
|
|
|
// Move to next interaction |
|
pkvNode = pkvNode->GetNextKey(); |
|
} |
|
} |
|
} |
|
|
|
modelKeyValues->deleteThis(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::AddScriptedNPCInteraction( ScriptedNPCInteraction_t *pInteraction ) |
|
{ |
|
int nNewIndex = m_ScriptedInteractions.AddToTail(); |
|
|
|
if ( ai_debug_dyninteractions.GetBool() ) |
|
{ |
|
Msg("%s(%s): Added dynamic interaction: %s\n", GetClassname(), GetDebugName(), STRING(pInteraction->iszInteractionName) ); |
|
} |
|
|
|
// Copy the interaction over |
|
ScriptedNPCInteraction_t *pNewInt = &(m_ScriptedInteractions[nNewIndex]); |
|
memcpy( pNewInt, pInteraction, sizeof(ScriptedNPCInteraction_t) ); |
|
|
|
// Calculate the local to world matrix |
|
m_ScriptedInteractions[nNewIndex].matDesiredLocalToWorld.SetupMatrixOrgAngles( pInteraction->vecRelativeOrigin, pInteraction->angRelativeAngles ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *CAI_BaseNPC::GetScriptedNPCInteractionSequence( ScriptedNPCInteraction_t *pInteraction, int iPhase ) |
|
{ |
|
if ( pInteraction->sPhases[iPhase].iActivity != ACT_INVALID ) |
|
{ |
|
int iSequence = SelectWeightedSequence( (Activity)pInteraction->sPhases[iPhase].iActivity ); |
|
return GetSequenceName( iSequence ); |
|
} |
|
|
|
if ( pInteraction->sPhases[iPhase].iszSequence != NULL_STRING ) |
|
return STRING(pInteraction->sPhases[iPhase].iszSequence); |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::StartRunningInteraction( CAI_BaseNPC *pOtherNPC, bool bActive ) |
|
{ |
|
m_hInteractionPartner = pOtherNPC; |
|
if ( bActive ) |
|
{ |
|
m_iInteractionState = NPCINT_RUNNING_ACTIVE; |
|
} |
|
else |
|
{ |
|
m_iInteractionState = NPCINT_RUNNING_PARTNER; |
|
} |
|
m_bCannotDieDuringInteraction = true; |
|
|
|
// Force the NPC into an idle schedule so they don't move. |
|
// NOTE: We must set SCHED_IDLE_STAND directly, to prevent derived NPC |
|
// classes from translating the idle stand schedule away to do something bad. |
|
SetSchedule( GetSchedule(SCHED_IDLE_STAND) ); |
|
|
|
// Prepare the NPC for the script. Setting this allows the scripted sequences |
|
// that we're about to create to immediately grab & use this NPC right away. |
|
// This prevents the NPC from being able to make any schedule decisions |
|
// before the DSS gets underway. |
|
m_scriptState = SCRIPT_PLAYING; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::StartScriptedNPCInteraction( CAI_BaseNPC *pOtherNPC, ScriptedNPCInteraction_t *pInteraction, Vector vecOtherOrigin, QAngle angOtherAngles ) |
|
{ |
|
variant_t emptyVariant; |
|
|
|
StartRunningInteraction( pOtherNPC, true ); |
|
if ( pOtherNPC ) |
|
{ |
|
pOtherNPC->StartRunningInteraction( this, false ); |
|
|
|
//Msg("%s(%s) disabled collisions with %s(%s) at %0.2f\n", GetClassname(), GetDebugName(), pOtherNPC->GetClassName(), pOtherNPC->GetDebugName(), gpGlobals->curtime ); |
|
PhysDisableEntityCollisions( this, pOtherNPC ); |
|
} |
|
|
|
// Determine which sequences we're going to use |
|
const char *pszEntrySequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_ENTRY ); |
|
const char *pszSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_SEQUENCE ); |
|
const char *pszExitSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_EXIT ); |
|
|
|
// Debug |
|
if ( ai_debug_dyninteractions.GetBool() ) |
|
{ |
|
if ( pOtherNPC ) |
|
{ |
|
Msg("%s(%s) starting dynamic interaction \"%s\" with %s(%s).\n", GetClassname(), GetDebugName(), STRING(pInteraction->iszInteractionName), pOtherNPC->GetClassname(), pOtherNPC->GetDebugName() ); |
|
if ( pszEntrySequence ) |
|
{ |
|
Msg( " - Entry sequence: %s\n", pszEntrySequence ); |
|
} |
|
Msg( " - Core sequence: %s\n", pszSequence ); |
|
if ( pszExitSequence ) |
|
{ |
|
Msg( " - Exit sequence: %s\n", pszExitSequence ); |
|
} |
|
} |
|
} |
|
|
|
// Create a scripted sequence name that's guaranteed to be unique |
|
char szSSName[256]; |
|
if ( pOtherNPC ) |
|
{ |
|
Q_snprintf( szSSName, sizeof(szSSName), "dss_%s%d%s%d", GetDebugName(), entindex(), pOtherNPC->GetDebugName(), pOtherNPC->entindex() ); |
|
} |
|
else |
|
{ |
|
Q_snprintf( szSSName, sizeof(szSSName), "dss_%s%d", GetDebugName(), entindex() ); |
|
} |
|
string_t iszSSName = AllocPooledString(szSSName); |
|
|
|
// Setup next attempt |
|
pInteraction->flNextAttemptTime = gpGlobals->curtime + pInteraction->flDelay + RandomFloat(-2,2); |
|
|
|
// Spawn a scripted sequence for this NPC to play the interaction anim |
|
CAI_ScriptedSequence *pMySequence = (CAI_ScriptedSequence*)CreateEntityByName( "scripted_sequence" ); |
|
pMySequence->KeyValue( "m_iszEntry", pszEntrySequence ); |
|
pMySequence->KeyValue( "m_iszPlay", pszSequence ); |
|
pMySequence->KeyValue( "m_iszPostIdle", pszExitSequence ); |
|
pMySequence->KeyValue( "m_fMoveTo", "5" ); |
|
pMySequence->SetAbsOrigin( GetAbsOrigin() ); |
|
|
|
QAngle angDesired = GetAbsAngles(); |
|
angDesired[YAW] = m_flInteractionYaw; |
|
|
|
pMySequence->SetAbsAngles( angDesired ); |
|
pMySequence->ForceSetTargetEntity( this, true ); |
|
pMySequence->SetName( iszSSName ); |
|
pMySequence->AddSpawnFlags( SF_SCRIPT_NOINTERRUPT | SF_SCRIPT_HIGH_PRIORITY | SF_SCRIPT_OVERRIDESTATE ); |
|
if ((pInteraction->iFlags & SCNPC_FLAG_DONT_TELEPORT_AT_END_ME) != 0) |
|
{ |
|
pMySequence->AddSpawnFlags( SF_SCRIPT_DONT_TELEPORT_AT_END ); |
|
} |
|
pMySequence->SetLoopActionSequence( (pInteraction->iFlags & SCNPC_FLAG_LOOP_IN_ACTION) != 0 ); |
|
pMySequence->SetSynchPostIdles( true ); |
|
if ( ai_debug_dyninteractions.GetBool() ) |
|
{ |
|
pMySequence->m_debugOverlays |= OVERLAY_TEXT_BIT | OVERLAY_PIVOT_BIT; |
|
} |
|
|
|
// Spawn the matching scripted sequence for the other NPC |
|
CAI_ScriptedSequence *pTheirSequence = NULL; |
|
if ( pOtherNPC ) |
|
{ |
|
pTheirSequence = (CAI_ScriptedSequence*)CreateEntityByName( "scripted_sequence" ); |
|
pTheirSequence->KeyValue( "m_iszEntry", pszEntrySequence ); |
|
pTheirSequence->KeyValue( "m_iszPlay", pszSequence ); |
|
pTheirSequence->KeyValue( "m_iszPostIdle", pszExitSequence ); |
|
pTheirSequence->KeyValue( "m_fMoveTo", "5" ); |
|
pTheirSequence->SetAbsOrigin( vecOtherOrigin ); |
|
pTheirSequence->SetAbsAngles( angOtherAngles ); |
|
pTheirSequence->ForceSetTargetEntity( pOtherNPC, true ); |
|
pTheirSequence->SetName( iszSSName ); |
|
pTheirSequence->AddSpawnFlags( SF_SCRIPT_NOINTERRUPT | SF_SCRIPT_HIGH_PRIORITY | SF_SCRIPT_OVERRIDESTATE ); |
|
if ((pInteraction->iFlags & SCNPC_FLAG_DONT_TELEPORT_AT_END_THEM) != 0) |
|
{ |
|
pTheirSequence->AddSpawnFlags( SF_SCRIPT_DONT_TELEPORT_AT_END ); |
|
} |
|
pTheirSequence->SetLoopActionSequence( (pInteraction->iFlags & SCNPC_FLAG_LOOP_IN_ACTION) != 0 ); |
|
pTheirSequence->SetSynchPostIdles( true ); |
|
if ( ai_debug_dyninteractions.GetBool() ) |
|
{ |
|
pTheirSequence->m_debugOverlays |= OVERLAY_TEXT_BIT | OVERLAY_PIVOT_BIT; |
|
} |
|
|
|
// Tell their sequence to keep their position relative to me |
|
pTheirSequence->SetupInteractionPosition( this, pInteraction->matDesiredLocalToWorld ); |
|
} |
|
|
|
// Spawn both sequences at once |
|
pMySequence->Spawn(); |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->Spawn(); |
|
} |
|
|
|
// Call activate on both sequences at once |
|
pMySequence->Activate(); |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->Activate(); |
|
} |
|
|
|
// Setup the outputs for both sequences. The first kills them both when it finishes |
|
pMySequence->KeyValue( "OnCancelFailedSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
if ( pszExitSequence ) |
|
{ |
|
pMySequence->KeyValue( "OnPostIdleEndSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->KeyValue( "OnPostIdleEndSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
} |
|
} |
|
else |
|
{ |
|
pMySequence->KeyValue( "OnEndSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->KeyValue( "OnEndSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
} |
|
} |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->KeyValue( "OnCancelFailedSequence", UTIL_VarArgs("%s,Kill,,0,-1", szSSName ) ); |
|
} |
|
|
|
// Tell both sequences to start |
|
pMySequence->AcceptInput( "BeginSequence", this, this, emptyVariant, 0 ); |
|
if ( pTheirSequence ) |
|
{ |
|
pTheirSequence->AcceptInput( "BeginSequence", this, this, emptyVariant, 0 ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::CanRunAScriptedNPCInteraction( bool bForced ) |
|
{ |
|
if ( m_NPCState != NPC_STATE_IDLE && m_NPCState != NPC_STATE_ALERT && m_NPCState != NPC_STATE_COMBAT ) |
|
return false; |
|
|
|
if ( !IsAlive() ) |
|
return false; |
|
|
|
if ( IsOnFire() ) |
|
return false; |
|
|
|
if ( IsCrouching() ) |
|
return false; |
|
|
|
// Not while running scripted sequences |
|
if ( m_hCine ) |
|
return false; |
|
|
|
if ( bForced ) |
|
{ |
|
if ( !m_hForcedInteractionPartner ) |
|
return false; |
|
} |
|
else |
|
{ |
|
if ( m_hForcedInteractionPartner || m_hInteractionPartner ) |
|
return false; |
|
if ( IsInAScript() || !HasCondition(COND_IN_PVS) ) |
|
return false; |
|
if ( HasCondition(COND_HEAR_DANGER) || HasCondition(COND_HEAR_MOVE_AWAY) ) |
|
return false; |
|
|
|
// Default AI prevents interactions while melee attacking, but not ranged attacking |
|
if ( IsCurSchedule( SCHED_MELEE_ATTACK1 ) || IsCurSchedule( SCHED_MELEE_ATTACK2 ) ) |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CheckForScriptedNPCInteractions( void ) |
|
{ |
|
// Are we being forced to interact with another NPC? If so, do that |
|
if ( m_hForcedInteractionPartner ) |
|
{ |
|
CheckForcedNPCInteractions(); |
|
return; |
|
} |
|
|
|
// Otherwise, see if we can interaction with our enemy |
|
if ( !m_ScriptedInteractions.Count() || !GetEnemy() ) |
|
return; |
|
|
|
CAI_BaseNPC *pNPC = GetEnemy()->MyNPCPointer(); |
|
|
|
if( !pNPC ) |
|
return; |
|
|
|
// Recalculate interaction capability whenever we switch enemies |
|
if ( m_hLastInteractionTestTarget != GetEnemy() ) |
|
{ |
|
m_hLastInteractionTestTarget = GetEnemy(); |
|
|
|
CalculateValidEnemyInteractions(); |
|
} |
|
|
|
// First, make sure I'm in a state where I can do this |
|
if ( !CanRunAScriptedNPCInteraction() ) |
|
return; |
|
if ( pNPC && !pNPC->CanRunAScriptedNPCInteraction() ) |
|
return; |
|
|
|
for ( int i = 0; i < m_ScriptedInteractions.Count(); i++ ) |
|
{ |
|
ScriptedNPCInteraction_t *pInteraction = &m_ScriptedInteractions[i]; |
|
|
|
if ( !pInteraction->bValidOnCurrentEnemy ) |
|
continue; |
|
|
|
if ( pInteraction->flNextAttemptTime > gpGlobals->curtime ) |
|
continue; |
|
|
|
Vector vecOrigin; |
|
QAngle angAngles; |
|
if ( InteractionCouldStart( pNPC, pInteraction, vecOrigin, angAngles ) ) |
|
{ |
|
m_iInteractionPlaying = i; |
|
StartScriptedNPCInteraction( pNPC, pInteraction, vecOrigin, angAngles ); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Calculate all the valid dynamic interactions we can perform with our current enemy |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CalculateValidEnemyInteractions( void ) |
|
{ |
|
CAI_BaseNPC *pNPC = GetEnemy()->MyNPCPointer(); |
|
if ( !pNPC ) |
|
return; |
|
|
|
bool bDebug = (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT && ai_debug_dyninteractions.GetBool()); |
|
if ( bDebug ) |
|
{ |
|
Msg("%s(%s): Computing valid interactions with %s(%s)\n", GetClassname(), GetDebugName(), pNPC->GetClassname(), pNPC->GetDebugName() ); |
|
} |
|
|
|
bool bFound = false; |
|
for ( int i = 0; i < m_ScriptedInteractions.Count(); i++ ) |
|
{ |
|
ScriptedNPCInteraction_t *pInteraction = &m_ScriptedInteractions[i]; |
|
pInteraction->bValidOnCurrentEnemy = false; |
|
|
|
// If the trigger method of the interaction isn't the one we're after, we're done |
|
if ( pInteraction->iTriggerMethod != SNPCINT_AUTOMATIC_IN_COMBAT ) |
|
continue; |
|
|
|
if ( !pNPC->GetModelPtr() ) |
|
continue; |
|
|
|
// If we have a damage filter that prevents us hurting the enemy, |
|
// don't interact with him, since most interactions kill the enemy. |
|
// Create a fake damage info to test it with. |
|
CTakeDamageInfo tempinfo( this, this, vec3_origin, vec3_origin, 1.0, DMG_BULLET ); |
|
if ( !pNPC->PassesDamageFilter( tempinfo ) ) |
|
continue; |
|
|
|
// Check the weapon requirements for the interaction |
|
if ( pInteraction->iFlags & SCNPC_FLAG_NEEDS_WEAPON_ME ) |
|
{ |
|
if ( !GetActiveWeapon()) |
|
continue; |
|
|
|
// Check the specific weapon type |
|
if ( pInteraction->iszMyWeapon != NULL_STRING && GetActiveWeapon()->m_iClassname != pInteraction->iszMyWeapon ) |
|
continue; |
|
} |
|
if ( pInteraction->iFlags & SCNPC_FLAG_NEEDS_WEAPON_THEM ) |
|
{ |
|
if ( !pNPC->GetActiveWeapon() ) |
|
continue; |
|
|
|
// Check the specific weapon type |
|
if ( pInteraction->iszTheirWeapon != NULL_STRING && pNPC->GetActiveWeapon()->m_iClassname != pInteraction->iszTheirWeapon ) |
|
continue; |
|
} |
|
|
|
// Script needs the other NPC, so make sure they're not dead |
|
if ( !pNPC->IsAlive() ) |
|
continue; |
|
|
|
// Use sequence? or activity? |
|
if ( pInteraction->sPhases[SNPCINT_SEQUENCE].iActivity != ACT_INVALID ) |
|
{ |
|
// Resolve the activity to a sequence, and make sure our enemy has it |
|
const char *pszSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_SEQUENCE ); |
|
if ( !pszSequence ) |
|
continue; |
|
if ( pNPC->LookupSequence( pszSequence ) == -1 ) |
|
continue; |
|
} |
|
else |
|
{ |
|
if ( pNPC->LookupSequence( STRING(pInteraction->sPhases[SNPCINT_SEQUENCE].iszSequence) ) == -1 ) |
|
continue; |
|
} |
|
|
|
pInteraction->bValidOnCurrentEnemy = true; |
|
bFound = true; |
|
|
|
if ( bDebug ) |
|
{ |
|
Msg(" Found: %s\n", STRING(pInteraction->iszInteractionName) ); |
|
} |
|
} |
|
|
|
if ( bDebug && !bFound ) |
|
{ |
|
Msg(" No valid interactions found.\n"); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CheckForcedNPCInteractions( void ) |
|
{ |
|
// If we don't have an interaction, we're waiting for our partner to start it. Do nothing. |
|
if ( m_iInteractionPlaying == NPCINT_NONE ) |
|
return; |
|
|
|
CAI_BaseNPC *pNPC = m_hForcedInteractionPartner->MyNPCPointer(); |
|
bool bAbort = false; |
|
|
|
// First, make sure both NPCs are able to do this |
|
if ( !CanRunAScriptedNPCInteraction( true ) || !pNPC->CanRunAScriptedNPCInteraction( true ) ) |
|
{ |
|
// If we were still moving to our target, abort. |
|
if ( m_iInteractionState == NPCINT_MOVING_TO_MARK ) |
|
{ |
|
bAbort = true; |
|
} |
|
else |
|
{ |
|
return; |
|
} |
|
} |
|
|
|
// Check to see if we can start our interaction. If we can, dance. |
|
Vector vecOrigin; |
|
QAngle angAngles; |
|
|
|
ScriptedNPCInteraction_t *pInteraction = &m_ScriptedInteractions[m_iInteractionPlaying]; |
|
|
|
if ( !bAbort ) |
|
{ |
|
if ( !InteractionCouldStart( pNPC, pInteraction, vecOrigin, angAngles ) ) |
|
{ |
|
if ( ( gpGlobals->curtime > m_flForcedInteractionTimeout ) && ( m_iInteractionState == NPCINT_MOVING_TO_MARK ) ) |
|
{ |
|
bAbort = true; |
|
} |
|
else |
|
{ |
|
return; |
|
} |
|
} |
|
} |
|
|
|
if ( bAbort ) |
|
{ |
|
if ( m_hForcedInteractionPartner ) |
|
{ |
|
// We've aborted a forced interaction. Let the mapmaker know. |
|
m_OnForcedInteractionAborted.FireOutput( this, this ); |
|
} |
|
|
|
CleanupForcedInteraction(); |
|
pNPC->CleanupForcedInteraction(); |
|
|
|
return; |
|
} |
|
|
|
StartScriptedNPCInteraction( pNPC, pInteraction, vecOrigin, angAngles ); |
|
m_OnForcedInteractionStarted.FireOutput( this, this ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Returns whether two NPCs can fit at each other's origin. |
|
// Kinda like that movie with Eddie Murphy and Dan Akroyd. |
|
//----------------------------------------------------------------------------- |
|
bool CanNPCsTradePlaces( CAI_BaseNPC *pNPC1, CAI_BaseNPC *pNPC2, bool bDebug ) |
|
{ |
|
bool bTest1At2 = true; |
|
bool bTest2At1 = true; |
|
|
|
if ( ( pNPC1->GetHullMins().x <= pNPC2->GetHullMins().x ) && |
|
( pNPC1->GetHullMins().y <= pNPC2->GetHullMins().y ) && |
|
( pNPC1->GetHullMins().z <= pNPC2->GetHullMins().z ) && |
|
( pNPC1->GetHullMaxs().x >= pNPC2->GetHullMaxs().x ) && |
|
( pNPC1->GetHullMaxs().y >= pNPC2->GetHullMaxs().y ) && |
|
( pNPC1->GetHullMaxs().z >= pNPC2->GetHullMaxs().z ) ) |
|
{ |
|
// 1 bigger than 2 in all axes, skip 2 in 1 test |
|
bTest2At1 = false; |
|
} |
|
else if ( ( pNPC2->GetHullMins().x <= pNPC1->GetHullMins().x ) && |
|
( pNPC2->GetHullMins().y <= pNPC1->GetHullMins().y ) && |
|
( pNPC2->GetHullMins().z <= pNPC1->GetHullMins().z ) && |
|
( pNPC2->GetHullMaxs().x >= pNPC1->GetHullMaxs().x ) && |
|
( pNPC2->GetHullMaxs().y >= pNPC1->GetHullMaxs().y ) && |
|
( pNPC2->GetHullMaxs().z >= pNPC1->GetHullMaxs().z ) ) |
|
{ |
|
// 2 bigger than 1 in all axes, skip 1 in 2 test |
|
bTest1At2 = false; |
|
} |
|
|
|
trace_t tr; |
|
CTraceFilterSkipTwoEntities traceFilter( pNPC1, pNPC2, COLLISION_GROUP_NONE ); |
|
|
|
if ( bTest1At2 ) |
|
{ |
|
AI_TraceHull( pNPC2->GetAbsOrigin(), pNPC2->GetAbsOrigin(), pNPC1->GetHullMins(), pNPC1->GetHullMaxs(), MASK_SOLID, &traceFilter, &tr ); |
|
if ( tr.startsolid ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Box( pNPC2->GetAbsOrigin(), pNPC1->GetHullMins(), pNPC1->GetHullMaxs(), 255,0,0, true, 1.0 ); |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
if ( bTest2At1 ) |
|
{ |
|
AI_TraceHull( pNPC1->GetAbsOrigin(), pNPC1->GetAbsOrigin(), pNPC2->GetHullMins(), pNPC2->GetHullMaxs(), MASK_SOLID, &traceFilter, &tr ); |
|
if ( tr.startsolid ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Box( pNPC1->GetAbsOrigin(), pNPC2->GetHullMins(), pNPC2->GetHullMaxs(), 255,0,0, true, 1.0 ); |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInteraction_t *pInteraction, Vector &vecOrigin, QAngle &angAngles ) |
|
{ |
|
// Get a matrix that'll convert from my local interaction space to world space |
|
VMatrix matMeToWorld, matLocalToWorld; |
|
QAngle angMyCurrent = GetAbsAngles(); |
|
angMyCurrent[YAW] = m_flInteractionYaw; |
|
matMeToWorld.SetupMatrixOrgAngles( GetAbsOrigin(), angMyCurrent ); |
|
MatrixMultiply( matMeToWorld, pInteraction->matDesiredLocalToWorld, matLocalToWorld ); |
|
|
|
// Get the desired NPC position in worldspace |
|
vecOrigin = matLocalToWorld.GetTranslation(); |
|
MatrixToAngles( matLocalToWorld, angAngles ); |
|
|
|
bool bDebug = ai_debug_dyninteractions.GetBool(); |
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Axis( vecOrigin, angAngles, 20, true, 0.1 ); |
|
} |
|
|
|
// Determine whether or not the enemy is on the target |
|
float flDistSqr = (vecOrigin - pOtherNPC->GetAbsOrigin()).LengthSqr(); |
|
if ( flDistSqr > pInteraction->flDistSqr ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
if ( m_debugOverlays & OVERLAY_NPC_SELECTED_BIT || pOtherNPC->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) |
|
{ |
|
if ( ai_debug_dyninteractions.GetFloat() == 2 ) |
|
{ |
|
Msg(" %s distsqr: %0.2f (%0.2f %0.2f %0.2f), desired: <%0.2f (%0.2f %0.2f %0.2f)\n", GetDebugName(), flDistSqr, |
|
pOtherNPC->GetAbsOrigin().x, pOtherNPC->GetAbsOrigin().y, pOtherNPC->GetAbsOrigin().z, pInteraction->flDistSqr, vecOrigin.x, vecOrigin.y, vecOrigin.z ); |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
if ( bDebug ) |
|
{ |
|
Msg("DYNINT: (%s) testing interaction \"%s\"\n", GetDebugName(), STRING(pInteraction->iszInteractionName) ); |
|
Msg(" %s is at: %0.2f %0.2f %0.2f\n", GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); |
|
Msg(" %s distsqr: %0.2f (%0.2f %0.2f %0.2f), desired: (%0.2f %0.2f %0.2f)\n", GetDebugName(), flDistSqr, |
|
pOtherNPC->GetAbsOrigin().x, pOtherNPC->GetAbsOrigin().y, pOtherNPC->GetAbsOrigin().z, vecOrigin.x, vecOrigin.y, vecOrigin.z ); |
|
|
|
if ( pOtherNPC ) |
|
{ |
|
float flOtherSpeed = pOtherNPC->GetSequenceGroundSpeed( pOtherNPC->GetSequence() ); |
|
Msg(" %s Speed: %.2f\n", pOtherNPC->GetSequenceName( pOtherNPC->GetSequence() ), flOtherSpeed); |
|
} |
|
} |
|
|
|
// Angle check, if we're supposed to |
|
if ( pInteraction->iFlags & SCNPC_FLAG_TEST_OTHER_ANGLES ) |
|
{ |
|
QAngle angEnemyAngles = pOtherNPC->GetAbsAngles(); |
|
bool bMatches = true; |
|
for ( int ang = 0; ang < 3; ang++ ) |
|
{ |
|
float flAngDiff = AngleDiff( angEnemyAngles[ang], angAngles[ang] ); |
|
if ( fabs(flAngDiff) > DSS_MAX_ANGLE_DIFF ) |
|
{ |
|
bMatches = false; |
|
break; |
|
} |
|
} |
|
if ( !bMatches ) |
|
return false; |
|
|
|
if ( bDebug ) |
|
{ |
|
Msg(" %s angle matched: (%0.2f %0.2f %0.2f), desired (%0.2f, %0.2f, %0.2f)\n", GetDebugName(), |
|
anglemod(angEnemyAngles.x), anglemod(angEnemyAngles.y), anglemod(angEnemyAngles.z), anglemod(angAngles.x), anglemod(angAngles.y), anglemod(angAngles.z) ); |
|
} |
|
} |
|
|
|
// TODO: Velocity check, if we're supposed to |
|
if ( pInteraction->iFlags & SCNPC_FLAG_TEST_OTHER_VELOCITY ) |
|
{ |
|
|
|
} |
|
|
|
// Valid so far. Now check to make sure there's nothing in the way. |
|
// This isn't a very good method of checking, but it's cheap and rules out the problems we're seeing so far. |
|
// If we start getting interactions that start a fair distance apart, we're going to need to do more work here. |
|
trace_t tr; |
|
AI_TraceLine( EyePosition(), pOtherNPC->EyePosition(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr); |
|
if ( tr.fraction != 1.0 && tr.m_pEnt != pOtherNPC ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
Msg( " %s Interaction was blocked.\n", GetDebugName() ); |
|
NDebugOverlay::Line( tr.startpos, tr.endpos, 0,255,0, true, 1.0 ); |
|
NDebugOverlay::Line( pOtherNPC->EyePosition(), tr.endpos, 255,0,0, true, 1.0 ); |
|
} |
|
return false; |
|
} |
|
|
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Line( tr.startpos, tr.endpos, 0,255,0, true, 1.0 ); |
|
} |
|
|
|
// Do a knee-level trace to find low physics objects |
|
Vector vecMyKnee, vecOtherKnee; |
|
CollisionProp()->NormalizedToWorldSpace( Vector(0,0,0.25f), &vecMyKnee ); |
|
pOtherNPC->CollisionProp()->NormalizedToWorldSpace( Vector(0,0,0.25f), &vecOtherKnee ); |
|
AI_TraceLine( vecMyKnee, vecOtherKnee, MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr); |
|
if ( tr.fraction != 1.0 && tr.m_pEnt != pOtherNPC ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
Msg( " %s Interaction was blocked.\n", GetDebugName() ); |
|
NDebugOverlay::Line( tr.startpos, tr.endpos, 0,255,0, true, 1.0 ); |
|
NDebugOverlay::Line( vecOtherKnee, tr.endpos, 255,0,0, true, 1.0 ); |
|
} |
|
return false; |
|
} |
|
|
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Line( tr.startpos, tr.endpos, 0,255,0, true, 1.0 ); |
|
} |
|
|
|
// Finally, make sure the NPC can actually fit at the interaction position |
|
// This solves problems with NPCs who are a few units or so above the |
|
// interaction point, and would sink into the ground when playing the anim. |
|
CTraceFilterSkipTwoEntities traceFilter( pOtherNPC, this, COLLISION_GROUP_NONE ); |
|
AI_TraceHull( vecOrigin, vecOrigin, pOtherNPC->GetHullMins(), pOtherNPC->GetHullMaxs(), MASK_SOLID, &traceFilter, &tr ); |
|
if ( tr.startsolid ) |
|
{ |
|
if ( bDebug ) |
|
{ |
|
NDebugOverlay::Box( vecOrigin, pOtherNPC->GetHullMins(), pOtherNPC->GetHullMaxs(), 255,0,0, true, 1.0 ); |
|
} |
|
return false; |
|
} |
|
|
|
// If the NPCs are swapping places during this interaction, make sure they can fit at each |
|
// others' origins before allowing the interaction. |
|
if ( !CanNPCsTradePlaces( this, pOtherNPC, bDebug ) ) |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return true if this NPC cannot die because it's in an interaction |
|
// and the flag has been set by the animation. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::HasInteractionCantDie( void ) |
|
{ |
|
return ( m_bCannotDieDuringInteraction && IsRunningDynamicInteraction() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::InputForceInteractionWithNPC( inputdata_t &inputdata ) |
|
{ |
|
// Get the interaction name & target |
|
char parseString[255]; |
|
Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); |
|
|
|
// First, the target's name |
|
char *pszParam = strtok(parseString," "); |
|
if ( !pszParam || !pszParam[0] ) |
|
{ |
|
Warning("%s(%s) received ForceInteractionWithNPC input with bad parameters: %s\nFormat should be: ForceInteractionWithNPC <target NPC> <interaction name>\n", GetClassname(), GetDebugName(), inputdata.value.String() ); |
|
return; |
|
} |
|
// Find the target |
|
CBaseEntity *pTarget = FindNamedEntity( pszParam ); |
|
if ( !pTarget ) |
|
{ |
|
Warning("%s(%s) received ForceInteractionWithNPC input, but couldn't find entity named: %s\n", GetClassname(), GetDebugName(), pszParam ); |
|
return; |
|
} |
|
CAI_BaseNPC *pNPC = pTarget->MyNPCPointer(); |
|
if ( !pNPC || !pNPC->GetModelPtr() ) |
|
{ |
|
Warning("%s(%s) received ForceInteractionWithNPC input, but entity named %s cannot run dynamic interactions.\n", GetClassname(), GetDebugName(), pszParam ); |
|
return; |
|
} |
|
|
|
// Second, the interaction name |
|
pszParam = strtok(NULL," "); |
|
if ( !pszParam || !pszParam[0] ) |
|
{ |
|
Warning("%s(%s) received ForceInteractionWithNPC input with bad parameters: %s\nFormat should be: ForceInteractionWithNPC <target NPC> <interaction name>\n", GetClassname(), GetDebugName(), inputdata.value.String() ); |
|
return; |
|
} |
|
|
|
// Find the interaction from the name, and ensure it's one that the target NPC can play |
|
int iInteraction = -1; |
|
for ( int i = 0; i < m_ScriptedInteractions.Count(); i++ ) |
|
{ |
|
if ( Q_strncmp( pszParam, STRING(m_ScriptedInteractions[i].iszInteractionName), strlen(pszParam) ) ) |
|
continue; |
|
|
|
// Use sequence? or activity? |
|
if ( m_ScriptedInteractions[i].sPhases[SNPCINT_SEQUENCE].iActivity != ACT_INVALID ) |
|
{ |
|
if ( !pNPC->HaveSequenceForActivity( (Activity)m_ScriptedInteractions[i].sPhases[SNPCINT_SEQUENCE].iActivity ) ) |
|
{ |
|
// Other NPC may have all the matching sequences, but just without the activity specified. |
|
// Lets find a single sequence for us, and ensure they have a matching one. |
|
int iMySeq = SelectWeightedSequence( (Activity)m_ScriptedInteractions[i].sPhases[SNPCINT_SEQUENCE].iActivity ); |
|
if ( pNPC->LookupSequence( GetSequenceName(iMySeq) ) == -1 ) |
|
continue; |
|
} |
|
} |
|
else |
|
{ |
|
if ( pNPC->LookupSequence( STRING(m_ScriptedInteractions[i].sPhases[SNPCINT_SEQUENCE].iszSequence) ) == -1 ) |
|
continue; |
|
} |
|
|
|
iInteraction = i; |
|
break; |
|
} |
|
|
|
if ( iInteraction == -1 ) |
|
{ |
|
Warning("%s(%s) received ForceInteractionWithNPC input, but couldn't find an interaction named %s that entity named %s could run.\n", GetClassname(), GetDebugName(), pszParam, pNPC->GetDebugName() ); |
|
return; |
|
} |
|
|
|
// Found both pieces of data, lets dance. |
|
StartForcedInteraction( pNPC, iInteraction ); |
|
pNPC->StartForcedInteraction( this, NPCINT_NONE ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::StartForcedInteraction( CAI_BaseNPC *pNPC, int iInteraction ) |
|
{ |
|
m_hForcedInteractionPartner = pNPC; |
|
ClearSchedule( "Starting a forced interaction" ); |
|
|
|
m_flForcedInteractionTimeout = gpGlobals->curtime + 8.0f; |
|
m_iInteractionPlaying = iInteraction; |
|
m_iInteractionState = NPCINT_MOVING_TO_MARK; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CleanupForcedInteraction( void ) |
|
{ |
|
m_hForcedInteractionPartner = NULL; |
|
m_iInteractionPlaying = NPCINT_NONE; |
|
m_iInteractionState = NPCINT_NOT_RUNNING; |
|
m_flForcedInteractionTimeout = 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Calculate a position to move to so that I can interact with my |
|
// target NPC. |
|
// |
|
// FIXME: THIS ONLY WORKS FOR INTERACTIONS THAT REQUIRE THE TARGET |
|
// NPC TO BE SOME DISTANCE DIRECTLY IN FRONT OF ME. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::CalculateForcedInteractionPosition( void ) |
|
{ |
|
if ( m_iInteractionPlaying == NPCINT_NONE ) |
|
return; |
|
|
|
ScriptedNPCInteraction_t *pInteraction = GetRunningDynamicInteraction(); |
|
|
|
// Pretend I was facing the target, and extrapolate from that the position I should be at |
|
Vector vecToTarget = m_hForcedInteractionPartner->GetAbsOrigin() - GetAbsOrigin(); |
|
VectorNormalize( vecToTarget ); |
|
QAngle angToTarget; |
|
VectorAngles( vecToTarget, angToTarget ); |
|
|
|
// Get the desired position in worldspace, relative to the target |
|
VMatrix matMeToWorld, matLocalToWorld; |
|
matMeToWorld.SetupMatrixOrgAngles( GetAbsOrigin(), angToTarget ); |
|
MatrixMultiply( matMeToWorld, pInteraction->matDesiredLocalToWorld, matLocalToWorld ); |
|
|
|
Vector vecOrigin = GetAbsOrigin() - matLocalToWorld.GetTranslation(); |
|
m_vecForcedWorldPosition = m_hForcedInteractionPartner->GetAbsOrigin() + vecOrigin; |
|
|
|
//NDebugOverlay::Axis( m_vecForcedWorldPosition, angToTarget, 20, true, 3.0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pPlayer - |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::PlayerHasIlluminatedNPC( CBasePlayer *pPlayer, float flDot ) |
|
{ |
|
#ifdef HL2_EPISODIC |
|
if ( IsActiveDynamicInteraction() ) |
|
{ |
|
ScriptedNPCInteraction_t *pInteraction = GetRunningDynamicInteraction(); |
|
if ( pInteraction->iLoopBreakTriggerMethod & SNPCINT_LOOPBREAK_ON_FLASHLIGHT_ILLUM ) |
|
{ |
|
// Only do this in alyx darkness mode |
|
if ( HL2GameRules()->IsAlyxInDarknessMode() ) |
|
{ |
|
// Can only break when we're in the action anim |
|
if ( m_hCine->IsPlayingAction() ) |
|
{ |
|
m_hCine->StopActionLoop( true ); |
|
} |
|
} |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::ModifyOrAppendCriteria( AI_CriteriaSet& set ) |
|
{ |
|
BaseClass::ModifyOrAppendCriteria( set ); |
|
|
|
// Append time since seen player |
|
if ( m_flLastSawPlayerTime ) |
|
{ |
|
set.AppendCriteria( "timesinceseenplayer", UTIL_VarArgs( "%f", gpGlobals->curtime - m_flLastSawPlayerTime ) ); |
|
} |
|
else |
|
{ |
|
set.AppendCriteria( "timesinceseenplayer", "-1" ); |
|
} |
|
|
|
// Append distance to my enemy |
|
if ( GetEnemy() ) |
|
{ |
|
set.AppendCriteria( "distancetoenemy", UTIL_VarArgs( "%f", EnemyDistance(GetEnemy()) ) ); |
|
} |
|
else |
|
{ |
|
set.AppendCriteria( "distancetoenemy", "-1" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// If I were crouching at my current location, could I shoot this target? |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::CouldShootIfCrouching( CBaseEntity *pTarget ) |
|
{ |
|
bool bWasStanding = !IsCrouching(); |
|
Crouch(); |
|
|
|
Vector vecTarget; |
|
if (GetActiveWeapon()) |
|
{ |
|
vecTarget = pTarget->BodyTarget( GetActiveWeapon()->GetLocalOrigin() ); |
|
} |
|
else |
|
{ |
|
vecTarget = pTarget->BodyTarget( GetLocalOrigin() ); |
|
} |
|
|
|
bool bResult = WeaponLOSCondition( GetLocalOrigin(), vecTarget, false ); |
|
|
|
if ( bWasStanding ) |
|
{ |
|
Stand(); |
|
} |
|
|
|
return bResult; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsCrouchedActivity( Activity activity ) |
|
{ |
|
Activity realActivity = TranslateActivity(activity); |
|
|
|
switch ( realActivity ) |
|
{ |
|
case ACT_RELOAD_LOW: |
|
case ACT_COVER_LOW: |
|
case ACT_COVER_PISTOL_LOW: |
|
case ACT_COVER_SMG1_LOW: |
|
case ACT_RELOAD_SMG1_LOW: |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Get shoot position of BCC at an arbitrary position |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseNPC::Weapon_ShootPosition( void ) |
|
{ |
|
Vector right; |
|
GetVectors( NULL, &right, NULL ); |
|
|
|
bool bStanding = !IsCrouching(); |
|
if ( bStanding && (CapabilitiesGet() & bits_CAP_DUCK) ) |
|
{ |
|
if ( IsCrouchedActivity( GetActivity() ) ) |
|
{ |
|
bStanding = false; |
|
} |
|
} |
|
|
|
if ( !bStanding ) |
|
return (GetAbsOrigin() + GetCrouchGunOffset() + right * 8); |
|
|
|
return BaseClass::Weapon_ShootPosition(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::ShouldProbeCollideAgainstEntity( CBaseEntity *pEntity ) |
|
{ |
|
if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) |
|
{ |
|
if ( ai_test_moveprobe_ignoresmall.GetBool() && IsNavigationUrgent() ) |
|
{ |
|
IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); |
|
|
|
if ( pPhysics->IsMoveable() && pPhysics->GetMass() < 40.0 ) |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::Crouch( void ) |
|
{ |
|
m_bIsCrouching = true; |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::IsCrouching( void ) |
|
{ |
|
return ( (CapabilitiesGet() & bits_CAP_DUCK) && m_bIsCrouching ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseNPC::Stand( void ) |
|
{ |
|
if ( m_bForceCrouch ) |
|
return false; |
|
|
|
m_bIsCrouching = false; |
|
DesireStand(); |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseNPC::DesireCrouch( void ) |
|
{ |
|
m_bCrouchDesired = true; |
|
} |
|
|
|
bool CAI_BaseNPC::IsInChoreo() const |
|
{ |
|
return m_bInChoreo; |
|
}
|
|
|