diff options
| author | Jørgen P. Tjernø <[email protected]> | 2013-12-02 19:31:46 -0800 |
|---|---|---|
| committer | Jørgen P. Tjernø <[email protected]> | 2013-12-02 19:46:31 -0800 |
| commit | f56bb35301836e56582a575a75864392a0177875 (patch) | |
| tree | de61ddd39de3e7df52759711950b4c288592f0dc /mp/src/game/server/ai_basenpc.cpp | |
| parent | Mark some more files as text. (diff) | |
| download | source-sdk-2013-f56bb35301836e56582a575a75864392a0177875.tar.xz source-sdk-2013-f56bb35301836e56582a575a75864392a0177875.zip | |
Fix line endings. WHAMMY.
Diffstat (limited to 'mp/src/game/server/ai_basenpc.cpp')
| -rw-r--r-- | mp/src/game/server/ai_basenpc.cpp | 28260 |
1 files changed, 14130 insertions, 14130 deletions
diff --git a/mp/src/game/server/ai_basenpc.cpp b/mp/src/game/server/ai_basenpc.cpp index 54b47636..1db9eab1 100644 --- a/mp/src/game/server/ai_basenpc.cpp +++ b/mp/src/game/server/ai_basenpc.cpp @@ -1,14130 +1,14130 @@ -//========= 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()
-{
- 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);
- 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_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;
-}
+//========= 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() +{ + 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); + 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_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; +} |