//========= Copyright © 1996-2005, 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" #if defined( HL2_EP3 ) || defined( INFESTED_DLL ) #include "physics_prop_statue.h" #endif #include "vphysics/friction.h" #include "physics_npc_solver.h" #include "death_pose.h" #include "datacache/imdlcache.h" #include "vstdlib/jobthread.h" #include "ai_addon.h" #ifdef HL2_EPISODIC #include "npc_alyx_episodic.h" #endif #ifdef PORTAL #include "portal_base2d_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" #include "physics_prop_statue.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //#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", ( IsGameConsole() ) ? "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", ( IsGameConsole() ) ? "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 // ================================================================ //------------------------------------- void CAI_BaseNPC::AddBehavior( CAI_BehaviorBase *pBehavior ) { #ifdef DEBUG Assert( m_Behaviors.Find( pBehavior ) == m_Behaviors.InvalidIndex() ); for ( int i = 0; i < m_Behaviors.Count(); i++) { Assert( typeid(*m_Behaviors[i]) != typeid(*pBehavior) ); } #endif m_Behaviors.AddToTail( pBehavior ); pBehavior->SetOuter( this ); pBehavior->SetBackBridge( this ); } //------------------------------------- void CAI_BaseNPC::RemoveAndDestroyBehavior( CAI_BehaviorBase *pBehavior ) { #ifdef DEBUG bool bFound = false; for ( int i = 0; i < m_Behaviors.Count(); i++) { if ( typeid(*m_Behaviors[i]) == typeid(*pBehavior) ) { bFound = true; break; } } Assert( bFound && !pBehavior->IsRunning() ); #endif m_Behaviors.FindAndRemove( pBehavior ); delete pBehavior; } //------------------------------------- bool CAI_BaseNPC::BehaviorSelectSchedule() { for ( int i = 0; i < m_Behaviors.Count(); i++ ) { if ( m_Behaviors[i]->CanSelectSchedule() && ShouldBehaviorSelectSchedule( m_Behaviors[i] ) ) { DeferSchedulingToBehavior( m_Behaviors[i] ); return true; } } DeferSchedulingToBehavior( NULL ); return false; } //------------------------------------ void CAI_BaseNPC::SetPrimaryBehavior( CAI_BehaviorBase *pNewBehavior ) { bool change = ( m_pPrimaryBehavior != pNewBehavior ); CAI_BehaviorBase *pOldBehavior = m_pPrimaryBehavior; m_pPrimaryBehavior = pNewBehavior; if ( change ) { if ( m_pPrimaryBehavior ) { m_pPrimaryBehavior->BeginScheduleSelection(); g_bBehaviorHost_PreventBaseClassGatherConditions = true; m_pPrimaryBehavior->GatherConditions(); g_bBehaviorHost_PreventBaseClassGatherConditions = false; } if ( pOldBehavior ) { pOldBehavior->EndScheduleSelection(); VacateStrategySlot(); } OnChangeRunningBehavior( pOldBehavior, pNewBehavior ); } } //------------------------------------- bool CAI_BaseNPC::OnBehaviorChangeStatus( CAI_BehaviorBase *pBehavior, bool fCanFinishSchedule ) { if ( pBehavior == GetPrimaryBehavior() && !pBehavior->CanSelectSchedule() && !fCanFinishSchedule ) { DeferSchedulingToBehavior( NULL ); return true; } return false; } //------------------------------------- CAI_BehaviorBase *CAI_BaseNPC::DeferSchedulingToBehavior( CAI_BehaviorBase *pNewBehavior ) { CAI_BehaviorBase *pOldBehavior = m_pPrimaryBehavior; SetPrimaryBehavior( pNewBehavior ); return pOldBehavior; } //----------------------------------------------------------------------------- // 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 && 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 ); bool bIsBelowHalfHealthBefore = ( m_iHealth <= ( m_iMaxHealth / 2 ) ); 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() && 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( 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 ( !bIsBelowHalfHealthBefore && ( 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, 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 ) { 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(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; } if ( HasCondition( iCondition ) == false ) { OnConditionSet( iCondition ); } 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; } if ( HasCondition( iCondition ) == true ) { OnConditionCleared( iCondition ); } 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((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 <> // 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 //<> // 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 <> // 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 <> // 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 // ---------------------------------------------------------- #ifndef PORTAL2 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; } #endif // !PORTAL2 // -------------------------------------------------------- // 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; } //----------------------------------------------------------------------------- extern ConVar ai_sequence_debug; 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 ) { if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) { DevMsg("RunAnimation calling ResetSequence %s\n", GetSequenceName(iSequence)); } 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 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; bool bUseThinkLimits = ( !m_bInChoreo && ShouldUseFrameThinkLimits() ); #ifdef _DEBUG const float NPC_THINK_LIMIT = 30.0 / 1000.0; #else const float NPC_THINK_LIMIT = (10.0 / 1000.0); #endif g_StartTimeCurThink = 0; if ( bUseThinkLimits ) { 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 = engine->GetTimescale(); if ( timescale < 1.0f ) { timescale = 1.0f; } 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 = Plat_FloatTime(); m_iFrameBlocked = -1; m_nLastThinkTick = TIME_TO_TICKS( m_flLastRealThinkTime ); } return true; } void CAI_BaseNPC::PostNPCThink( void ) { if ( g_StartTimeCurThink != 0.0 ) { g_NpcTimeThisFrame += Plat_FloatTime() - 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 ); } if ( IsAttackFrozen() ) { // If they are frozen, they can't attack SetCondition( COND_TOO_CLOSE_TO_ATTACK ); // Run, Forest, Run return; } 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", STRING( GetEntityName() ) ); } 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, GetAITraceMask(), &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 { DevMsg(2, "Lost enemy because we're flagged efficient\n"); 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 ); NDebugOverlay::Cross3D( GetSquad()->ComputeSquadCentroid(false,NULL), 16, 255, 255, 255, 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() ); } pWeapon->MakeWeaponNameFromEntity( this ); 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; 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.92; if ( GetActiveWeapon() ) { Vector vSpread = GetAttackSpread( GetActiveWeapon() ); if ( vSpread.x > VECTOR_CONE_15DEGREES.x ) spread = TableCos(asin(vSpread.x)); else // too much error because using point not box spread = 0.99145; // "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 * t, 0, 120 ); // 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 ); if ( NewActivity == ACT_SPECIFIC_SEQUENCE ) { translatedActivity = weaponActivity = ACT_SPECIFIC_SEQUENCE; iSequence = m_nIdealSequence; return; } 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); } if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) { DevMsg("AdvanceToIdealActivity calling SetActivityAndSequence because of change %s\n", GetSequenceName(nNextSequence)); } // 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 { /* if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) { 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 { /* if (ai_sequence_debug.GetBool() == true && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) { 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( char *szSequence ) { int iSequence = LookupSequence( szSequence ); if ( iSequence > ACTIVITY_NOT_AVAILABLE ) SetSequenceById( iSequence ); else { DevWarning( 2, "%s has no sequence to match %s 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() { // Bail if we don't need a physics shadow. if ( GetMoveType() == MOVETYPE_VPHYSICS ) 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(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 = MIN( 1.0f, GetModelHierarchyScale() ); // NOTE: Cannot scale NPC bounding box up, as pathfinding will fail (hull needs to match the traces used for the node network) 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; m_bClientSideRagdoll = false; 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 ); 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 ) { pWeapon->MakeWeaponNameFromEntity( this ); 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; for( int i = 0; i < m_Behaviors.Count(); i++ ) { m_Behaviors[i]->Spawn(); } } //----------------------------------------------------------------------------- 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 ( GetPrimaryBehavior() ) { GetPrimaryBehavior()->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), GetAITraceMask(), 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: Change faction NPC belongs to //----------------------------------------------------------------------------- void CAI_BaseNPC::ChangeFaction( int nNewFaction ) { BaseClass::ChangeFaction( nNewFaction ); // Remove anyone who is no longer an enemy AIEnemiesIter_t iter; AI_EnemyInfo_t *pNextEMemory; for( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = pNextEMemory ) { CBaseEntity *pEnemy = pEMemory->hEnemy; pNextEMemory = GetEnemies()->GetNext(&iter); if ( pEnemy && ( IRelationType( pEnemy ) == D_LI ) ) GetEnemies()->ClearMemory( pEnemy ); } } //----------------------------------------------------------------------------- // 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.Count()-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.Count()-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 * GetModelHierarchyScale(); } //----------------------------------------------------------------------------- // 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? int nEvent = pEvent->Event(); switch( nEvent ) { 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 ); AddEffects( EF_NOINTERP ); 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 = ToBaseCombatWeapon( 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 (nEvent == 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 (nEvent == 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 ( nEvent == AE_NPC_BODYDROP_HEAVY ) { if ( GetFlags() & FL_ONGROUND ) { EmitSound( "AI_BaseNPC.BodyDrop_Heavy" ); } return; } else if ( nEvent == AE_NPC_LEFTFOOT || nEvent == AE_NPC_RIGHTFOOT ) { return; } else if ( nEvent == AE_NPC_RAGDOLL ) { // Convert to ragdoll immediately BecomeRagdollOnClient( vec3_origin ); return; } else if ( nEvent == 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 ( nEvent == 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 ( nEvent == 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 ( nEvent == 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 ( nEvent == AE_NPC_SET_INTERACTION_CANTDIE ) { SetInteractionCantDie( (atoi(pEvent->options) != 0) ); return; } else if ( nEvent == 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 ( StringHasPrefixCaseSensitive( pszParam, "ME" ) ) { pTarget = this; pAttacker = m_hInteractionPartner; } else if ( StringHasPrefixCaseSensitive( pszParam, "THEM" ) ) { 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( nEvent ); DevWarning( 1, "Bad %s format. Should be: { AE_NPC_HURT_INTERACTION_PARTNER \" \" }\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 ) || ( nEvent >= EVENT_WEAPON && nEvent <= 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) { int r = 0; int g = 255; int b = 255; char tempstr[512]; // -------------- // Print Health // -------------- Q_snprintf(tempstr,sizeof(tempstr),"Health: %i (DACC:%1.2f)",m_iHealth.Get(), GetDamageAccumulator() ); EntityText(text_offset,tempstr,0,r,g,b); 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,r,g,b); text_offset++; } // ----------------- // Start Scripting? // ----------------- if( IsInAScript() ) { Q_snprintf(tempstr,sizeof(tempstr),"STARTSCRIPTING" ); EntityText(text_offset,tempstr,0,r,g,b); text_offset++; } // ----------------- // Hint Group? // ----------------- if( GetHintGroup() != NULL_STRING ) { Q_snprintf(tempstr,sizeof(tempstr),"Hint Group: %s", STRING(GetHintGroup()) ); EntityText(text_offset,tempstr,0,r,g,b); text_offset++; } // ----------------- // Print MotionType // ----------------- int navTypeIndex = (int)GetNavType() + 1; static const char *pMoveNames[] = { "None", "Ground", "Jump", "Fly", "Climb", "Crawl" }; Assert( navTypeIndex >= 0 && navTypeIndex < ARRAYSIZE(pMoveNames) ); if ( navTypeIndex < ARRAYSIZE(pMoveNames) ) { Q_snprintf(tempstr,sizeof(tempstr),"Move: %s, ", pMoveNames[navTypeIndex] ); EntityText(text_offset,tempstr,0,r,g,b); text_offset++; } // -------------- // Print Schedule // -------------- if ( GetCurSchedule() ) { CAI_BehaviorBase *pBehavior = GetPrimaryBehavior(); if ( pBehavior ) { Q_snprintf(tempstr,sizeof(tempstr),"Behv: %s, ", pBehavior->GetName() ); EntityText(text_offset,tempstr,0,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); text_offset++; bHasConditions = true; } } if (!bHasConditions) { Q_snprintf(tempstr,sizeof(tempstr),"(no conditions)"); EntityText(text_offset,tempstr,0,r,g,b); 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,r,g,b); 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,r,g,b); text_offset++; } // ------------------------------- // Print any important condtions // ------------------------------- if (HasCondition(COND_ENEMY_TOO_FAR)) { EntityText(text_offset,"Enemy too far to attack",0,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); 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,r,g,b); text_offset++; } if ( m_pSquad ) { if( m_pSquad->IsLeader(this) ) { Q_snprintf(tempstr, sizeof(tempstr),"**Squad Leader**" ); EntityText(text_offset, tempstr, 0,r,g,b); text_offset++; } Q_snprintf(tempstr, sizeof(tempstr), "SquadSlot:%s", GetSquadSlotDebugName( GetMyStrategySlot() ) ); EntityText(text_offset, tempstr, 0,r,g,b); text_offset++; } if ( m_pPrimaryBehavior ) { text_offset = m_pPrimaryBehavior->DrawDebugTextOverlays( 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 ) ) { CPortal_Base2D *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 } //----------------------------------------------------------------------------- // 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 CPortal_Base2D *pPortal = FInViewConeThroughPortal( vecEnemyLKP ); // BUG 69142: Only run this if the target is on the other side of a portal if ( pPortal && FVisibleThroughPortal( pPortal, GetEnemy() ) ) { // 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() != WL_Eyes) && (GetEnemy()->GetWaterLevel() == WL_Eyes)) { #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 ); bool bUsePerfectAccuracy = false; #ifdef HL2_DLL // Apply appropriate accuracy. if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) { CNPC_Bullseye *pBullseye = dynamic_cast(GetEnemy()); if ( pBullseye && pBullseye->UsePerfectAccuracy() ) { bUsePerfectAccuracy = true; } } #endif // HL2_DLL 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; } //----------------------------------------------------------------------------- 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( float flTolerance ) { flTolerance = MAX( flTolerance, 0.006f ); //!!!BUGBUG - no magic numbers!!! if ( fabs( GetMotor()->DeltaIdealYaw() ) <= flTolerance ) { 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 ), GetAITraceMask_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 ), GetAITraceMask_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 ), GetAITraceMask_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 ), GetAITraceMask_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 ); } OnEnemyChanged( m_hEnemy.Get(), pEnemy ); } // 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(this))->GetEnemies()->LastKnownPosition( GetEnemy() ); } float CAI_BaseNPC::GetEnemyLastTimeSeen() const { return (const_cast(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(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 ( 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_flFrozenMoveBlock, FIELD_FLOAT ), 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 ), DEFINE_INPUTFUNC( FIELD_STRING, "CreateAddon", InputCreateAddon ), // 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( flCameraDistance, FIELD_FLOAT ), DEFINE_FIELD( angCameraAngles, 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(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; } //----------------------------------------------------------------------------- // Debug function to make this NPC freeze in place (or unfreeze). //----------------------------------------------------------------------------- void CAI_BaseNPC::ToggleFreeze() { if ( GetMoveType() != MOVETYPE_NONE ) { Freeze(); } else { Unfreeze(); } } //----------------------------------------------------------------------------- // Freezes this NPC in place for a period of time. //----------------------------------------------------------------------------- void CAI_BaseNPC::Freeze( float flFreezeAmount, CBaseEntity *pFreezer, Ray_t *pFreezeRay ) { BaseClass::Freeze( flFreezeAmount, pFreezer, pFreezeRay ); if ( flFreezeAmount < 0.0f ) { SetCondition(COND_NPC_FREEZE); SetMoveType(MOVETYPE_NONE); SetGravity(0); SetLocalAngularVelocity(vec3_angle); SetAbsVelocity( vec3_origin ); } else { m_flFrozenThawRate = 0.1f; if ( ShouldBecomeStatue() ) { // Dude is frozen, so lets use a stiff server side statue if ( IsAlive() ) { CreateServerStatue( this, COLLISION_GROUP_NONE ); Event_Killed( CTakeDamageInfo( pFreezer, pFreezer, 1000.0, DMG_GENERIC | DMG_REMOVENORAGDOLL | DMG_PREVENT_PHYSICS_FORCE ) ); RemoveDeferred(); } } } } bool CAI_BaseNPC::ShouldBecomeStatue() { return ( m_flFrozen >= 1.0f ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_BaseNPC::Unfreeze() { BaseClass::Unfreeze(); // Unfreeze them. SetCondition(COND_NPC_UNFREEZE); m_Activity = ACT_RESET; SetMoveType( MOVETYPE_STEP ); // BUGBUG: this might not be the correct movetype! SetGravity(1); // Doesn't restore gravity to the original value, but who cares? } //----------------------------------------------------------------------------- // 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_pPrimaryBehavior(NULL) { 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_nAITraceMask = MASK_NPCSOLID; 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; m_flFaceEnemyTolerance = 0.006f; 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() m_pScheduleEvent = NULL; 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 CreateBehaviors(); } //----------------------------------------------------------------------------- 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(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 ); } } //----------------------------------------------------------------------------- // create an addon and attach to npc //----------------------------------------------------------------------------- void CAI_BaseNPC::InputCreateAddon( inputdata_t &inputdata ) { Vector vecSpawnOrigin = GetLocalOrigin(); const char *pszAddonName = inputdata.value.String(); // Spawn the addon CBaseEntity *pItem = (CBaseEntity *)CreateEntityByName( pszAddonName ); if ( pItem ) { pItem->SetAbsOrigin( vecSpawnOrigin ); DispatchSpawn( pItem ); // install the addon assert_cast< CAI_AddOn *>( pItem )->Install( 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) { #if defined( 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(); AddEffects( EF_NOINTERP ); // 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, GetAITraceMask(), 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 ); AddEffects( EF_NOINTERP ); } 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, bool bUseSlowHighAccuracyContacts ) { CleanupScriptsOnTeleport( false ); BaseClass::Teleport( newPosition, newAngles, newVelocity, bUseSlowHighAccuracyContacts ); CheckPVSCondition(); } //----------------------------------------------------------------------------- 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(), pNPC->GetAITraceMask(), pNPC, COLLISION_GROUP_NONE, &tr ); if( tr.fraction == 1.0 && pNPC->GetMoveProbe()->CheckStandPosition( tr.endpos, pNPC->GetAITraceMask() ) ) { *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( 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( 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 ) { int bits; bits = CapabilitiesGet() & bits_CAP_DOORS_GROUP; 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; } //----------------------------------------------------------------------------- #ifndef DBGFLAG_STRINGS_STRIP 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(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 ) ? "" : 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) ) { AIMsgGuts( pAI, flags, CFmtStr( &pszFormat ) ); } } //----------------------------------------------------------------------------- void DevMsg( CAI_BaseNPC *pAI, const char *pszFormat, ... ) { if ( (pAI->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) { AIMsgGuts( pAI, 0, CFmtStr( &pszFormat ) ); } } #endif //----------------------------------------------------------------------------- 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 = GetNavigator()->GetNetwork()->NearestNodeToPoint( vTestPoint, false ); 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, bits_BUILD_GET_CLOSE ); 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 ); KeyValues::AutoDelete autodelete_key( modelKeyValues ); 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 ( StringHasPrefixCaseSensitive( pszTrigger, "auto_in_combat" ) ) { 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 ( StringHasPrefixCaseSensitive( pszParam, "on_damage" ) ) { sInteraction.iLoopBreakTriggerMethod |= SNPCINT_LOOPBREAK_ON_DAMAGE; } if ( StringHasPrefixCaseSensitive( pszParam, "on_flashlight_illum" ) ) { 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 ( StringHasPrefixCaseSensitive( pszNeedsWeapon, "ME" ) ) { sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_ME; } else if ( StringHasPrefixCaseSensitive( pszNeedsWeapon, "THEM" ) ) { sInteraction.iFlags |= SCNPC_FLAG_NEEDS_WEAPON_THEM; } else if ( StringHasPrefixCaseSensitive( pszNeedsWeapon, "BOTH" ) ) { 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(); } } } } //----------------------------------------------------------------------------- // 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(), GetAITraceMask(), 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, GetAITraceMask(), 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 \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 \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 ( !StringHasPrefixCaseSensitive( STRING(m_ScriptedInteractions[i].iszInteractionName), 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 ); if ( m_pPrimaryBehavior ) { // Append active behavior name set.AppendCriteria( "active_behavior", GetPrimaryBehavior()->GetName() ); } // 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; }