aboutsummaryrefslogtreecommitdiff
path: root/sp/src/game/server/hl2/npc_playercompanion.cpp
diff options
context:
space:
mode:
authorJoe Ludwig <[email protected]>2013-06-26 15:22:04 -0700
committerJoe Ludwig <[email protected]>2013-06-26 15:22:04 -0700
commit39ed87570bdb2f86969d4be821c94b722dc71179 (patch)
treeabc53757f75f40c80278e87650ea92808274aa59 /sp/src/game/server/hl2/npc_playercompanion.cpp
downloadsource-sdk-2013-39ed87570bdb2f86969d4be821c94b722dc71179.tar.xz
source-sdk-2013-39ed87570bdb2f86969d4be821c94b722dc71179.zip
First version of the SOurce SDK 2013
Diffstat (limited to 'sp/src/game/server/hl2/npc_playercompanion.cpp')
-rw-r--r--sp/src/game/server/hl2/npc_playercompanion.cpp3997
1 files changed, 3997 insertions, 0 deletions
diff --git a/sp/src/game/server/hl2/npc_playercompanion.cpp b/sp/src/game/server/hl2/npc_playercompanion.cpp
new file mode 100644
index 00000000..58dcda2f
--- /dev/null
+++ b/sp/src/game/server/hl2/npc_playercompanion.cpp
@@ -0,0 +1,3997 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+
+#include "npc_playercompanion.h"
+
+#include "combine_mine.h"
+#include "fire.h"
+#include "func_tank.h"
+#include "globalstate.h"
+#include "npcevent.h"
+#include "props.h"
+#include "BasePropDoor.h"
+
+#include "ai_hint.h"
+#include "ai_localnavigator.h"
+#include "ai_memory.h"
+#include "ai_pathfinder.h"
+#include "ai_route.h"
+#include "ai_senses.h"
+#include "ai_squad.h"
+#include "ai_squadslot.h"
+#include "ai_tacticalservices.h"
+#include "ai_interactions.h"
+#include "filesystem.h"
+#include "collisionutils.h"
+#include "grenade_frag.h"
+#include <KeyValues.h>
+#include "physics_npc_solver.h"
+
+ConVar ai_debug_readiness("ai_debug_readiness", "0" );
+ConVar ai_use_readiness("ai_use_readiness", "1" ); // 0 = off, 1 = on, 2 = on for player squad only
+ConVar ai_readiness_decay( "ai_readiness_decay", "120" );// How many seconds it takes to relax completely
+ConVar ai_new_aiming( "ai_new_aiming", "1" );
+
+#define GetReadinessUse() ai_use_readiness.GetInt()
+
+extern ConVar g_debug_transitions;
+
+#define PLAYERCOMPANION_TRANSITION_SEARCH_DISTANCE (100*12)
+
+int AE_COMPANION_PRODUCE_FLARE;
+int AE_COMPANION_LIGHT_FLARE;
+int AE_COMPANION_RELEASE_FLARE;
+
+#define MAX_TIME_BETWEEN_BARRELS_EXPLODING 5.0f
+#define MAX_TIME_BETWEEN_CONSECUTIVE_PLAYER_KILLS 3.0f
+
+//-----------------------------------------------------------------------------
+// An aimtarget becomes invalid if it gets this close
+//-----------------------------------------------------------------------------
+#define COMPANION_AIMTARGET_NEAREST 24.0f
+#define COMPANION_AIMTARGET_NEAREST_SQR 576.0f
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+BEGIN_DATADESC( CNPC_PlayerCompanion )
+
+ DEFINE_FIELD( m_bMovingAwayFromPlayer, FIELD_BOOLEAN ),
+ DEFINE_EMBEDDED( m_SpeechWatch_PlayerLooking ),
+ DEFINE_EMBEDDED( m_FakeOutMortarTimer ),
+
+// (recomputed)
+// m_bWeightPathsInCover
+
+// These are auto-saved by AI
+// DEFINE_FIELD( m_AssaultBehavior, CAI_AssaultBehavior ),
+// DEFINE_FIELD( m_FollowBehavior, CAI_FollowBehavior ),
+// DEFINE_FIELD( m_StandoffBehavior, CAI_StandoffBehavior ),
+// DEFINE_FIELD( m_LeadBehavior, CAI_LeadBehavior ),
+// DEFINE_FIELD( m_OperatorBehavior, FIELD_EMBEDDED ),
+// m_ActBusyBehavior
+// m_PassengerBehavior
+// m_FearBehavior
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetReadinessPanic", InputSetReadinessPanic ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetReadinessStealth", InputSetReadinessStealth ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetReadinessLow", InputSetReadinessLow ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetReadinessMedium", InputSetReadinessMedium ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetReadinessHigh", InputSetReadinessHigh ),
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "LockReadiness", InputLockReadiness ),
+
+//------------------------------------------------------------------------------
+#ifdef HL2_EPISODIC
+ DEFINE_FIELD( m_hFlare, FIELD_EHANDLE ),
+
+ DEFINE_INPUTFUNC( FIELD_STRING, "EnterVehicle", InputEnterVehicle ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "EnterVehicleImmediately", InputEnterVehicleImmediately ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "ExitVehicle", InputExitVehicle ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "CancelEnterVehicle", InputCancelEnterVehicle ),
+#endif // HL2_EPISODIC
+//------------------------------------------------------------------------------
+
+ DEFINE_INPUTFUNC( FIELD_STRING, "GiveWeapon", InputGiveWeapon ),
+
+ DEFINE_FIELD( m_flReadiness, FIELD_FLOAT ),
+ DEFINE_FIELD( m_flReadinessSensitivity, FIELD_FLOAT ),
+ DEFINE_FIELD( m_bReadinessCapable, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flReadinessLockedUntil, FIELD_TIME ),
+ DEFINE_FIELD( m_fLastBarrelExploded, FIELD_TIME ),
+ DEFINE_FIELD( m_iNumConsecutiveBarrelsExploded, FIELD_INTEGER ),
+ DEFINE_FIELD( m_fLastPlayerKill, FIELD_TIME ),
+ DEFINE_FIELD( m_iNumConsecutivePlayerKills, FIELD_INTEGER ),
+
+ // m_flBoostSpeed (recomputed)
+
+ DEFINE_EMBEDDED( m_AnnounceAttackTimer ),
+
+ DEFINE_FIELD( m_hAimTarget, FIELD_EHANDLE ),
+
+ DEFINE_KEYFIELD( m_bAlwaysTransition, FIELD_BOOLEAN, "AlwaysTransition" ),
+ DEFINE_KEYFIELD( m_bDontPickupWeapons, FIELD_BOOLEAN, "DontPickupWeapons" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "EnableAlwaysTransition", InputEnableAlwaysTransition ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "DisableAlwaysTransition", InputDisableAlwaysTransition ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "EnableWeaponPickup", InputEnableWeaponPickup ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "DisableWeaponPickup", InputDisableWeaponPickup ),
+
+
+#if HL2_EPISODIC
+ DEFINE_INPUTFUNC( FIELD_VOID, "ClearAllOutputs", InputClearAllOuputs ),
+#endif
+
+ DEFINE_OUTPUT( m_OnWeaponPickup, "OnWeaponPickup" ),
+
+END_DATADESC()
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+CNPC_PlayerCompanion::eCoverType CNPC_PlayerCompanion::gm_fCoverSearchType;
+bool CNPC_PlayerCompanion::gm_bFindingCoverFromAllEnemies;
+string_t CNPC_PlayerCompanion::gm_iszMortarClassname;
+string_t CNPC_PlayerCompanion::gm_iszFloorTurretClassname;
+string_t CNPC_PlayerCompanion::gm_iszGroundTurretClassname;
+string_t CNPC_PlayerCompanion::gm_iszShotgunClassname;
+string_t CNPC_PlayerCompanion::gm_iszRollerMineClassname;
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+bool CNPC_PlayerCompanion::CreateBehaviors()
+{
+#ifdef HL2_EPISODIC
+ AddBehavior( &m_FearBehavior );
+ AddBehavior( &m_PassengerBehavior );
+#endif // HL2_EPISODIC
+
+ AddBehavior( &m_ActBusyBehavior );
+
+#ifdef HL2_EPISODIC
+ AddBehavior( &m_OperatorBehavior );
+ AddBehavior( &m_StandoffBehavior );
+ AddBehavior( &m_AssaultBehavior );
+ AddBehavior( &m_FollowBehavior );
+ AddBehavior( &m_LeadBehavior );
+#else
+ AddBehavior( &m_AssaultBehavior );
+ AddBehavior( &m_StandoffBehavior );
+ AddBehavior( &m_FollowBehavior );
+ AddBehavior( &m_LeadBehavior );
+#endif//HL2_EPISODIC
+
+ return BaseClass::CreateBehaviors();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::Precache()
+{
+ gm_iszMortarClassname = AllocPooledString( "func_tankmortar" );
+ gm_iszFloorTurretClassname = AllocPooledString( "npc_turret_floor" );
+ gm_iszGroundTurretClassname = AllocPooledString( "npc_turret_ground" );
+ gm_iszShotgunClassname = AllocPooledString( "weapon_shotgun" );
+ gm_iszRollerMineClassname = AllocPooledString( "npc_rollermine" );
+
+ PrecacheModel( STRING( GetModelName() ) );
+
+#ifdef HL2_EPISODIC
+ // The flare we're able to pull out
+ PrecacheModel( "models/props_junk/flare.mdl" );
+#endif // HL2_EPISODIC
+
+ BaseClass::Precache();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::Spawn()
+{
+ SelectModel();
+
+ Precache();
+
+ SetModel( STRING( GetModelName() ) );
+
+ SetHullType(HULL_HUMAN);
+ SetHullSizeNormal();
+
+ SetSolid( SOLID_BBOX );
+ AddSolidFlags( FSOLID_NOT_STANDABLE );
+ SetBloodColor( BLOOD_COLOR_RED );
+ m_flFieldOfView = 0.02;
+ m_NPCState = NPC_STATE_NONE;
+
+ CapabilitiesClear();
+ CapabilitiesAdd( bits_CAP_SQUAD );
+
+ if ( !HasSpawnFlags( SF_NPC_START_EFFICIENT ) )
+ {
+ CapabilitiesAdd( bits_CAP_ANIMATEDFACE | bits_CAP_TURN_HEAD );
+ CapabilitiesAdd( bits_CAP_USE_WEAPONS | bits_CAP_AIM_GUN | bits_CAP_MOVE_SHOOT );
+ CapabilitiesAdd( bits_CAP_DUCK | bits_CAP_DOORS_GROUP );
+ CapabilitiesAdd( bits_CAP_USE_SHOT_REGULATOR );
+ }
+ CapabilitiesAdd( bits_CAP_NO_HIT_PLAYER | bits_CAP_NO_HIT_SQUADMATES | bits_CAP_FRIENDLY_DMG_IMMUNE );
+ CapabilitiesAdd( bits_CAP_MOVE_GROUND );
+ SetMoveType( MOVETYPE_STEP );
+
+ m_HackedGunPos = Vector( 0, 0, 55 );
+
+ SetAimTarget(NULL);
+ m_bReadinessCapable = IsReadinessCapable();
+ SetReadinessValue( 0.0f );
+ SetReadinessSensitivity( random->RandomFloat( 0.7, 1.3 ) );
+ m_flReadinessLockedUntil = 0.0f;
+
+ m_AnnounceAttackTimer.Set( 10, 30 );
+
+#ifdef HL2_EPISODIC
+ // We strip this flag because it's been made obsolete by the StartScripting behavior
+ if ( HasSpawnFlags( SF_NPC_ALTCOLLISION ) )
+ {
+ Warning( "NPC %s using alternate collision! -- DISABLED\n", STRING( GetEntityName() ) );
+ RemoveSpawnFlags( SF_NPC_ALTCOLLISION );
+ }
+
+ m_hFlare = NULL;
+#endif // HL2_EPISODIC
+
+ BaseClass::Spawn();
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::Restore( IRestore &restore )
+{
+ int baseResult = BaseClass::Restore( restore );
+
+ if ( gpGlobals->eLoadType == MapLoad_Transition )
+ {
+ m_StandoffBehavior.SetActive( false );
+ }
+
+#ifdef HL2_EPISODIC
+ // We strip this flag because it's been made obsolete by the StartScripting behavior
+ if ( HasSpawnFlags( SF_NPC_ALTCOLLISION ) )
+ {
+ Warning( "NPC %s using alternate collision! -- DISABLED\n", STRING( GetEntityName() ) );
+ RemoveSpawnFlags( SF_NPC_ALTCOLLISION );
+ }
+#endif // HL2_EPISODIC
+
+ return baseResult;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::ObjectCaps()
+{
+ int caps = UsableNPCObjectCaps( BaseClass::ObjectCaps() );
+ return caps;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldAlwaysThink()
+{
+ return ( BaseClass::ShouldAlwaysThink() || ( GetFollowBehavior().GetFollowTarget() && GetFollowBehavior().GetFollowTarget()->IsPlayer() ) );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+Disposition_t CNPC_PlayerCompanion::IRelationType( CBaseEntity *pTarget )
+{
+ if ( !pTarget )
+ return D_NU;
+
+ Disposition_t baseRelationship = BaseClass::IRelationType( pTarget );
+
+ if ( baseRelationship != D_LI )
+ {
+ if ( IsTurret( pTarget ) )
+ {
+ // Citizens are afeared of turrets, so long as the turret
+ // is active... that is, not classifying itself as CLASS_NONE
+ if( pTarget->Classify() != CLASS_NONE )
+ {
+ if( !hl2_episodic.GetBool() && IsSafeFromFloorTurret(GetAbsOrigin(), pTarget) )
+ {
+ return D_NU;
+ }
+
+ return D_FR;
+ }
+ }
+ else if ( baseRelationship == D_HT &&
+ pTarget->IsNPC() &&
+ ((CAI_BaseNPC *)pTarget)->GetActiveWeapon() &&
+ ((CAI_BaseNPC *)pTarget)->GetActiveWeapon()->ClassMatches( gm_iszShotgunClassname ) &&
+ ( !GetActiveWeapon() || !GetActiveWeapon()->ClassMatches( gm_iszShotgunClassname ) ) )
+ {
+ if ( (pTarget->GetAbsOrigin() - GetAbsOrigin()).LengthSqr() < Square( 25 * 12 ) )
+ {
+ // Ignore enemies on the floor above us
+ if ( fabs(pTarget->GetAbsOrigin().z - GetAbsOrigin().z) < 100 )
+ return D_FR;
+ }
+ }
+ }
+
+ return baseRelationship;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsSilentSquadMember() const
+{
+ if ( (const_cast<CNPC_PlayerCompanion *>(this))->Classify() == CLASS_PLAYER_ALLY_VITAL && m_pSquad && MAKE_STRING(m_pSquad->GetName()) == GetPlayerSquadName() )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::GatherConditions()
+{
+ BaseClass::GatherConditions();
+
+ if ( AI_IsSinglePlayer() )
+ {
+ CBasePlayer *pPlayer = UTIL_GetLocalPlayer();
+
+ if ( Classify() == CLASS_PLAYER_ALLY_VITAL )
+ {
+ bool bInPlayerSquad = ( m_pSquad && MAKE_STRING(m_pSquad->GetName()) == GetPlayerSquadName() );
+ if ( bInPlayerSquad )
+ {
+ if ( GetState() == NPC_STATE_SCRIPT || ( !HasCondition( COND_SEE_PLAYER ) && (GetAbsOrigin() - pPlayer->GetAbsOrigin()).LengthSqr() > Square(50 * 12) ) )
+ {
+ RemoveFromSquad();
+ }
+ }
+ else if ( GetState() != NPC_STATE_SCRIPT )
+ {
+ if ( HasCondition( COND_SEE_PLAYER ) && (GetAbsOrigin() - pPlayer->GetAbsOrigin()).LengthSqr() < Square(25 * 12) )
+ {
+ if ( hl2_episodic.GetBool() )
+ {
+ // Don't stomp our squad if we're in one
+ if ( GetSquad() == NULL )
+ {
+ AddToSquad( GetPlayerSquadName() );
+ }
+ }
+ else
+ {
+ AddToSquad( GetPlayerSquadName() );
+ }
+ }
+ }
+ }
+
+ m_flBoostSpeed = 0;
+
+ if ( m_AnnounceAttackTimer.Expired() &&
+ ( GetLastEnemyTime() == 0.0 || gpGlobals->curtime - GetLastEnemyTime() > 20 ) )
+ {
+ // Always delay when an encounter begins
+ m_AnnounceAttackTimer.Set( 4, 8 );
+ }
+
+ if ( GetFollowBehavior().GetFollowTarget() &&
+ ( GetFollowBehavior().GetFollowTarget()->IsPlayer() || GetCommandGoal() != vec3_invalid ) &&
+ GetFollowBehavior().IsMovingToFollowTarget() &&
+ GetFollowBehavior().GetGoalRange() > 0.1 &&
+ BaseClass::GetIdealSpeed() > 0.1 )
+ {
+ Vector vPlayerToFollower = GetAbsOrigin() - pPlayer->GetAbsOrigin();
+ float dist = vPlayerToFollower.NormalizeInPlace();
+
+ bool bDoSpeedBoost = false;
+ if ( !HasCondition( COND_IN_PVS ) )
+ bDoSpeedBoost = true;
+ else if ( GetFollowBehavior().GetFollowTarget()->IsPlayer() )
+ {
+ if ( dist > GetFollowBehavior().GetGoalRange() * 2 )
+ {
+ float dot = vPlayerToFollower.Dot( pPlayer->EyeDirection3D() );
+ if ( dot < 0 )
+ {
+ bDoSpeedBoost = true;
+ }
+ }
+ }
+
+ if ( bDoSpeedBoost )
+ {
+ float lag = dist / GetFollowBehavior().GetGoalRange();
+
+ float mult;
+
+ if ( lag > 10.0 )
+ mult = 2.0;
+ else if ( lag > 5.0 )
+ mult = 1.5;
+ else if ( lag > 3.0 )
+ mult = 1.25;
+ else
+ mult = 1.1;
+
+ m_flBoostSpeed = pPlayer->GetSmoothedVelocity().Length();
+
+ if ( m_flBoostSpeed < BaseClass::GetIdealSpeed() )
+ m_flBoostSpeed = BaseClass::GetIdealSpeed();
+
+ m_flBoostSpeed *= mult;
+ }
+ }
+ }
+
+ // Update our readiness if we're
+ if ( IsReadinessCapable() )
+ {
+ UpdateReadiness();
+ }
+
+ PredictPlayerPush();
+
+ // Grovel through memories, don't forget enemies parented to func_tankmortar entities.
+ // !!!LATER - this should really call out and ask if I want to forget the enemy in question.
+ AIEnemiesIter_t iter;
+ for( AI_EnemyInfo_t *pMemory = GetEnemies()->GetFirst(&iter); pMemory != NULL; pMemory = GetEnemies()->GetNext(&iter) )
+ {
+ if ( IsMortar( pMemory->hEnemy ) || IsSniper( pMemory->hEnemy ) )
+ {
+ pMemory->bUnforgettable = ( IRelationType( pMemory->hEnemy ) < D_LI );
+ pMemory->bEludedMe = false;
+ }
+ }
+
+ if ( GetMotor()->IsDeceleratingToGoal() && IsCurTaskContinuousMove() &&
+ HasCondition( COND_PLAYER_PUSHING) && IsCurSchedule( SCHED_MOVE_AWAY ) )
+ {
+ ClearSchedule( "Being pushed by player" );
+ }
+
+ CBaseEntity *pEnemy = GetEnemy();
+ m_bWeightPathsInCover = false;
+ if ( pEnemy )
+ {
+ if ( IsMortar( pEnemy ) || IsSniper( pEnemy ) )
+ {
+ m_bWeightPathsInCover = true;
+ }
+ }
+
+ ClearCondition( COND_PC_SAFE_FROM_MORTAR );
+ if ( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND ) )
+ {
+ CSound *pSound = GetBestSound( SOUND_DANGER );
+
+ if ( pSound && (pSound->SoundType() & SOUND_CONTEXT_MORTAR) )
+ {
+ float flDistSq = (pSound->GetSoundOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( flDistSq > Square( MORTAR_BLAST_RADIUS + GetHullWidth() * 2 ) )
+ SetCondition( COND_PC_SAFE_FROM_MORTAR );
+ }
+ }
+
+ // Handle speech AI. Don't do AI speech if we're in scripts unless permitted by the EnableSpeakWhileScripting input.
+ if ( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT || m_NPCState == NPC_STATE_COMBAT ||
+ ( ( m_NPCState == NPC_STATE_SCRIPT ) && CanSpeakWhileScripting() ) )
+ {
+ DoCustomSpeechAI();
+ }
+
+ if ( AI_IsSinglePlayer() && hl2_episodic.GetBool() && !GetEnemy() && HasCondition( COND_HEAR_PLAYER ) )
+ {
+ Vector los = ( UTIL_GetLocalPlayer()->EyePosition() - EyePosition() );
+ los.z = 0;
+ VectorNormalize( los );
+
+ if ( DotProduct( los, EyeDirection2D() ) > DOT_45DEGREE )
+ {
+ ClearCondition( COND_HEAR_PLAYER );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::DoCustomSpeechAI( void )
+{
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+
+ // Don't allow this when we're getting in the car
+#ifdef HL2_EPISODIC
+ bool bPassengerInTransition = ( IsInAVehicle() && ( m_PassengerBehavior.GetPassengerState() == PASSENGER_STATE_ENTERING || m_PassengerBehavior.GetPassengerState() == PASSENGER_STATE_EXITING ) );
+#else
+ bool bPassengerInTransition = false;
+#endif
+
+ Vector vecEyePosition = EyePosition();
+ if ( bPassengerInTransition == false && pPlayer && pPlayer->FInViewCone( vecEyePosition ) && pPlayer->FVisible( vecEyePosition ) )
+ {
+ if ( m_SpeechWatch_PlayerLooking.Expired() )
+ {
+ SpeakIfAllowed( TLK_LOOK );
+ m_SpeechWatch_PlayerLooking.Stop();
+ }
+ }
+ else
+ {
+ m_SpeechWatch_PlayerLooking.Start( 1.0f );
+ }
+
+ // Mention the player is dead
+ if ( HasCondition( COND_TALKER_PLAYER_DEAD ) )
+ {
+ SpeakIfAllowed( TLK_PLDEAD );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::PredictPlayerPush()
+{
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+ if ( pPlayer && pPlayer->GetSmoothedVelocity().LengthSqr() >= Square(140))
+ {
+ Vector predictedPosition = pPlayer->WorldSpaceCenter() + pPlayer->GetSmoothedVelocity() * .4;
+ Vector delta = WorldSpaceCenter() - predictedPosition;
+ if ( delta.z < GetHullHeight() * .5 && delta.Length2DSqr() < Square(GetHullWidth() * 1.414) )
+ TestPlayerPushing( pPlayer );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Allows for modification of the interrupt mask for the current schedule.
+// In the most cases the base implementation should be called first.
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::BuildScheduleTestBits()
+{
+ BaseClass::BuildScheduleTestBits();
+
+ // Always interrupt to get into the car
+ SetCustomInterruptCondition( COND_PC_BECOMING_PASSENGER );
+
+ if ( IsCurSchedule(SCHED_RANGE_ATTACK1) )
+ {
+ SetCustomInterruptCondition( COND_PLAYER_PUSHING );
+ }
+
+ if ( ( ConditionInterruptsCurSchedule( COND_GIVE_WAY ) ||
+ IsCurSchedule(SCHED_HIDE_AND_RELOAD ) ||
+ IsCurSchedule(SCHED_RELOAD ) ||
+ IsCurSchedule(SCHED_STANDOFF ) ||
+ IsCurSchedule(SCHED_TAKE_COVER_FROM_ENEMY ) ||
+ IsCurSchedule(SCHED_COMBAT_FACE ) ||
+ IsCurSchedule(SCHED_ALERT_FACE ) ||
+ IsCurSchedule(SCHED_COMBAT_STAND ) ||
+ IsCurSchedule(SCHED_ALERT_FACE_BESTSOUND) ||
+ IsCurSchedule(SCHED_ALERT_STAND) ) )
+ {
+ SetCustomInterruptCondition( COND_HEAR_MOVE_AWAY );
+ SetCustomInterruptCondition( COND_PLAYER_PUSHING );
+ SetCustomInterruptCondition( COND_PC_HURTBYFIRE );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+CSound *CNPC_PlayerCompanion::GetBestSound( int validTypes )
+{
+ AISoundIter_t iter;
+
+ CSound *pCurrentSound = GetSenses()->GetFirstHeardSound( &iter );
+ while ( pCurrentSound )
+ {
+ // the npc cares about this sound, and it's close enough to hear.
+ if ( pCurrentSound->FIsSound() )
+ {
+ if( pCurrentSound->SoundContext() & SOUND_CONTEXT_MORTAR )
+ {
+ return pCurrentSound;
+ }
+ }
+
+ pCurrentSound = GetSenses()->GetNextHeardSound( &iter );
+ }
+
+ return BaseClass::GetBestSound( validTypes );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::QueryHearSound( CSound *pSound )
+{
+ if( !BaseClass::QueryHearSound(pSound) )
+ return false;
+
+ switch( pSound->SoundTypeNoContext() )
+ {
+ case SOUND_READINESS_LOW:
+ SetReadinessLevel( AIRL_RELAXED, false, true );
+ return false;
+
+ case SOUND_READINESS_MEDIUM:
+ SetReadinessLevel( AIRL_STIMULATED, false, true );
+ return false;
+
+ case SOUND_READINESS_HIGH:
+ SetReadinessLevel( AIRL_AGITATED, false, true );
+ return false;
+
+ default:
+ return true;
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+bool CNPC_PlayerCompanion::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC )
+{
+ CAI_BaseNPC *pOther = pEntity->MyNPCPointer();
+ if ( pOther &&
+ ( pOther->GetState() == NPC_STATE_ALERT || GetState() == NPC_STATE_ALERT || pOther->GetState() == NPC_STATE_COMBAT || GetState() == NPC_STATE_COMBAT ) &&
+ pOther->IsPlayerAlly() )
+ {
+ return true;
+ }
+
+ return BaseClass::QuerySeeEntity( pEntity, bOnlyHateOrFearIfNPC );
+}
+
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldIgnoreSound( CSound *pSound )
+{
+ if ( !BaseClass::ShouldIgnoreSound( pSound ) )
+ {
+ if ( pSound->IsSoundType( SOUND_DANGER ) && !SoundIsVisible(pSound) )
+ return true;
+
+#ifdef HL2_EPISODIC
+ // Ignore vehicle sounds when we're driving in them
+ if ( pSound->m_hOwner && pSound->m_hOwner->GetServerVehicle() != NULL )
+ {
+ if ( m_PassengerBehavior.GetPassengerState() == PASSENGER_STATE_INSIDE &&
+ m_PassengerBehavior.GetTargetVehicle() == pSound->m_hOwner->GetServerVehicle()->GetVehicleEnt() )
+ return true;
+ }
+#endif // HL2_EPISODIC
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::SelectSchedule()
+{
+ m_bMovingAwayFromPlayer = false;
+
+#ifdef HL2_EPISODIC
+ // Always defer to passenger if it's running
+ if ( ShouldDeferToPassengerBehavior() )
+ {
+ DeferSchedulingToBehavior( &m_PassengerBehavior );
+ return BaseClass::SelectSchedule();
+ }
+#endif // HL2_EPISODIC
+
+ if ( m_ActBusyBehavior.IsRunning() && m_ActBusyBehavior.NeedsToPlayExitAnim() )
+ {
+ trace_t tr;
+ Vector vUp = GetAbsOrigin();
+ vUp.z += .25;
+
+ AI_TraceHull( GetAbsOrigin(), vUp, GetHullMins(),
+ GetHullMaxs(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr );
+
+ if ( tr.startsolid )
+ {
+ if ( HasCondition( COND_HEAR_DANGER ) )
+ {
+ m_ActBusyBehavior.StopBusying();
+ }
+ DeferSchedulingToBehavior( &m_ActBusyBehavior );
+ return BaseClass::SelectSchedule();
+ }
+ }
+
+ int nSched = SelectFlinchSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+
+ int schedule = SelectScheduleDanger();
+ if ( schedule != SCHED_NONE )
+ return schedule;
+
+ schedule = SelectSchedulePriorityAction();
+ if ( schedule != SCHED_NONE )
+ return schedule;
+
+ if ( ShouldDeferToFollowBehavior() )
+ {
+ DeferSchedulingToBehavior( &(GetFollowBehavior()) );
+ }
+ else if ( !BehaviorSelectSchedule() )
+ {
+ if ( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT )
+ {
+ schedule = SelectScheduleNonCombat();
+ if ( schedule != SCHED_NONE )
+ return schedule;
+ }
+ else if ( m_NPCState == NPC_STATE_COMBAT )
+ {
+ schedule = SelectScheduleCombat();
+ if ( schedule != SCHED_NONE )
+ return schedule;
+ }
+ }
+
+ return BaseClass::SelectSchedule();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::SelectScheduleDanger()
+{
+ if( HasCondition( COND_HEAR_DANGER ) )
+ {
+ CSound *pSound;
+ pSound = GetBestSound( SOUND_DANGER );
+
+ ASSERT( pSound != NULL );
+
+ if ( pSound && (pSound->m_iType & SOUND_DANGER) )
+ {
+ if ( !(pSound->SoundContext() & (SOUND_CONTEXT_MORTAR|SOUND_CONTEXT_FROM_SNIPER)) || IsOkToCombatSpeak() )
+ SpeakIfAllowed( TLK_DANGER );
+
+ if ( HasCondition( COND_PC_SAFE_FROM_MORTAR ) )
+ {
+ // Just duck and cover if far away from the explosion, or in cover.
+ return SCHED_COWER;
+ }
+#if 1
+ else if( pSound && (pSound->m_iType & SOUND_CONTEXT_FROM_SNIPER) )
+ {
+ return SCHED_COWER;
+ }
+#endif
+
+ return SCHED_TAKE_COVER_FROM_BEST_SOUND;
+ }
+ }
+
+ if ( GetEnemy() &&
+ m_FakeOutMortarTimer.Expired() &&
+ GetFollowBehavior().GetFollowTarget() &&
+ IsMortar( GetEnemy() ) &&
+ assert_cast<CAI_BaseNPC *>(GetEnemy())->GetEnemy() == this &&
+ assert_cast<CAI_BaseNPC *>(GetEnemy())->FInViewCone( this ) &&
+ assert_cast<CAI_BaseNPC *>(GetEnemy())->FVisible( this ) )
+ {
+ m_FakeOutMortarTimer.Set( 7 );
+ return SCHED_PC_FAKEOUT_MORTAR;
+ }
+
+ if ( HasCondition( COND_HEAR_MOVE_AWAY ) )
+ return SCHED_MOVE_AWAY;
+
+ if ( HasCondition( COND_PC_HURTBYFIRE ) )
+ {
+ ClearCondition( COND_PC_HURTBYFIRE );
+ return SCHED_MOVE_AWAY;
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::SelectSchedulePriorityAction()
+{
+ if ( GetGroundEntity() && !IsInAScript() )
+ {
+ if ( GetGroundEntity()->IsPlayer() )
+ {
+ return SCHED_PC_GET_OFF_COMPANION;
+ }
+
+ if ( GetGroundEntity()->IsNPC() &&
+ IRelationType( GetGroundEntity() ) == D_LI &&
+ WorldSpaceCenter().z - GetGroundEntity()->WorldSpaceCenter().z > GetHullHeight() * .5 )
+ {
+ return SCHED_PC_GET_OFF_COMPANION;
+ }
+ }
+
+ int schedule = SelectSchedulePlayerPush();
+ if ( schedule != SCHED_NONE )
+ {
+ if ( GetFollowBehavior().IsRunning() )
+ KeepRunningBehavior();
+ return schedule;
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::SelectSchedulePlayerPush()
+{
+ if ( HasCondition( COND_PLAYER_PUSHING ) && !IsInAScript() && !IgnorePlayerPushing() )
+ {
+ // Ignore move away before gordon becomes the man
+ if ( GlobalEntity_GetState("gordon_precriminal") != GLOBAL_ON )
+ {
+ m_bMovingAwayFromPlayer = true;
+ return SCHED_MOVE_AWAY;
+ }
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IgnorePlayerPushing( void )
+{
+ if ( hl2_episodic.GetBool() )
+ {
+ // Ignore player pushes if we're leading him
+ if ( m_LeadBehavior.IsRunning() && m_LeadBehavior.HasGoal() )
+ return true;
+ if ( m_AssaultBehavior.IsRunning() && m_AssaultBehavior.OnStrictAssault() )
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::SelectScheduleCombat()
+{
+ if ( CanReload() && (HasCondition ( COND_NO_PRIMARY_AMMO ) || HasCondition(COND_LOW_PRIMARY_AMMO)) )
+ {
+ return SCHED_HIDE_AND_RELOAD;
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::CanReload( void )
+{
+ if ( IsRunningDynamicInteraction() )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldDeferToFollowBehavior()
+{
+ if ( !GetFollowBehavior().CanSelectSchedule() || !GetFollowBehavior().FarFromFollowTarget() )
+ return false;
+
+ if ( m_StandoffBehavior.CanSelectSchedule() && !m_StandoffBehavior.IsBehindBattleLines( GetFollowBehavior().GetFollowTarget()->GetAbsOrigin() ) )
+ return false;
+
+ if ( HasCondition(COND_BETTER_WEAPON_AVAILABLE) && !GetActiveWeapon() )
+ {
+ // Unarmed allies should arm themselves as soon as the opportunity presents itself.
+ return false;
+ }
+
+ // Even though assault and act busy are placed ahead of the follow behavior in precedence, the below
+ // code is necessary because we call ShouldDeferToFollowBehavior BEFORE we call the generic
+ // BehaviorSelectSchedule, which tries the behaviors in priority order.
+ if ( m_AssaultBehavior.CanSelectSchedule() && hl2_episodic.GetBool() )
+ {
+ return false;
+ }
+
+ if ( hl2_episodic.GetBool() )
+ {
+ if ( m_ActBusyBehavior.CanSelectSchedule() && m_ActBusyBehavior.IsCombatActBusy() )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// CalcReasonableFacing() is asking us if there's any reason why we wouldn't
+// want to look in this direction.
+//
+// Right now this is used to help prevent citizens aiming their guns at each other
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsValidReasonableFacing( const Vector &vecSightDir, float sightDist )
+{
+ if( !GetActiveWeapon() )
+ {
+ // If I'm not armed, it doesn't matter if I'm looking at another citizen.
+ return true;
+ }
+
+ if( ai_new_aiming.GetBool() )
+ {
+ Vector vecEyePositionCentered = GetAbsOrigin();
+ vecEyePositionCentered.z = EyePosition().z;
+
+ if( IsSquadmateInSpread(vecEyePositionCentered, vecEyePositionCentered + vecSightDir * 240.0f, VECTOR_CONE_15DEGREES.x, 12.0f * 3.0f) )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::TranslateSchedule( int scheduleType )
+{
+ switch( scheduleType )
+ {
+ case SCHED_IDLE_STAND:
+ case SCHED_ALERT_STAND:
+ if( GetActiveWeapon() )
+ {
+ // Everyone with less than half a clip takes turns reloading when not fighting.
+ CBaseCombatWeapon *pWeapon = GetActiveWeapon();
+
+ if( CanReload() && pWeapon->UsesClipsForAmmo1() && pWeapon->Clip1() < ( pWeapon->GetMaxClip1() * .5 ) && OccupyStrategySlot( SQUAD_SLOT_EXCLUSIVE_RELOAD ) )
+ {
+ if ( AI_IsSinglePlayer() )
+ {
+ CBasePlayer *pPlayer = UTIL_GetLocalPlayer();
+ pWeapon = pPlayer->GetActiveWeapon();
+ if( pWeapon && pWeapon->UsesClipsForAmmo1() &&
+ pWeapon->Clip1() < ( pWeapon->GetMaxClip1() * .75 ) &&
+ pPlayer->GetAmmoCount( pWeapon->GetPrimaryAmmoType() ) )
+ {
+ SpeakIfAllowed( TLK_PLRELOAD );
+ }
+ }
+ return SCHED_RELOAD;
+ }
+ }
+ break;
+
+ case SCHED_COWER:
+ return SCHED_PC_COWER;
+
+ case SCHED_TAKE_COVER_FROM_BEST_SOUND:
+ {
+ CSound *pSound = GetBestSound(SOUND_DANGER);
+
+ if( pSound && pSound->m_hOwner )
+ {
+ if( pSound->m_hOwner->IsNPC() && FClassnameIs( pSound->m_hOwner, "npc_zombine" ) )
+ {
+ // Run fully away from a Zombine with a grenade.
+ return SCHED_PC_TAKE_COVER_FROM_BEST_SOUND;
+ }
+ }
+
+ return SCHED_PC_MOVE_TOWARDS_COVER_FROM_BEST_SOUND;
+ }
+
+ case SCHED_FLEE_FROM_BEST_SOUND:
+ return SCHED_PC_FLEE_FROM_BEST_SOUND;
+
+ case SCHED_ESTABLISH_LINE_OF_FIRE:
+ case SCHED_MOVE_TO_WEAPON_RANGE:
+ if ( IsMortar( GetEnemy() ) )
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ break;
+
+ case SCHED_CHASE_ENEMY:
+ if ( IsMortar( GetEnemy() ) )
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ if ( GetEnemy() && FClassnameIs( GetEnemy(), "npc_combinegunship" ) )
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+ break;
+
+ case SCHED_ESTABLISH_LINE_OF_FIRE_FALLBACK:
+ // If we're fighting a gunship, try again
+ if ( GetEnemy() && FClassnameIs( GetEnemy(), "npc_combinegunship" ) )
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+ break;
+
+ case SCHED_RANGE_ATTACK1:
+ if ( IsMortar( GetEnemy() ) )
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+
+ if ( GetShotRegulator()->IsInRestInterval() )
+ return SCHED_STANDOFF;
+
+ if( !OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return SCHED_STANDOFF;
+ break;
+
+ case SCHED_FAIL_TAKE_COVER:
+ if ( IsEnemyTurret() )
+ {
+ return SCHED_PC_FAIL_TAKE_COVER_TURRET;
+ }
+ break;
+ case SCHED_RUN_FROM_ENEMY_FALLBACK:
+ {
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ return SCHED_RANGE_ATTACK1;
+ }
+ break;
+ }
+ }
+
+ return BaseClass::TranslateSchedule( scheduleType );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::StartTask( const Task_t *pTask )
+{
+ switch( pTask->iTask )
+ {
+ case TASK_SOUND_WAKE:
+ LocateEnemySound();
+ SetWait( 0.5 );
+ break;
+
+ case TASK_ANNOUNCE_ATTACK:
+ {
+ if ( GetActiveWeapon() && m_AnnounceAttackTimer.Expired() )
+ {
+ if ( SpeakIfAllowed( TLK_ATTACKING, UTIL_VarArgs("attacking_with_weapon:%s", GetActiveWeapon()->GetClassname()) ) )
+ {
+ m_AnnounceAttackTimer.Set( 10, 30 );
+ }
+ }
+
+ BaseClass::StartTask( pTask );
+ break;
+ }
+
+ case TASK_PC_WAITOUT_MORTAR:
+ if ( HasCondition( COND_NO_HEAR_DANGER ) )
+ TaskComplete();
+ break;
+
+ case TASK_MOVE_AWAY_PATH:
+ {
+ if ( m_bMovingAwayFromPlayer )
+ SpeakIfAllowed( TLK_PLPUSH );
+
+ BaseClass::StartTask( pTask );
+ }
+ break;
+
+ case TASK_PC_GET_PATH_OFF_COMPANION:
+ {
+ Assert( ( GetGroundEntity() && ( GetGroundEntity()->IsPlayer() || ( GetGroundEntity()->IsNPC() && IRelationType( GetGroundEntity() ) == D_LI ) ) ) );
+ GetNavigator()->SetAllowBigStep( GetGroundEntity() );
+ ChainStartTask( TASK_MOVE_AWAY_PATH, 48 );
+
+ /*
+ trace_t tr;
+ UTIL_TraceHull( GetAbsOrigin(), GetAbsOrigin(), GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr );
+ if ( tr.startsolid && tr.m_pEnt == GetGroundEntity() )
+ {
+ // Allow us to move through the entity for a short time
+ NPCPhysics_CreateSolver( this, GetGroundEntity(), true, 2.0f );
+ }
+ */
+ }
+ break;
+
+ default:
+ BaseClass::StartTask( pTask );
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::RunTask( const Task_t *pTask )
+{
+ switch( pTask->iTask )
+ {
+ case TASK_SOUND_WAKE:
+ if( IsWaitFinished() )
+ {
+ TaskComplete();
+ }
+ break;
+
+ case TASK_PC_WAITOUT_MORTAR:
+ {
+ if ( HasCondition( COND_NO_HEAR_DANGER ) )
+ TaskComplete();
+ }
+ break;
+
+ case TASK_MOVE_AWAY_PATH:
+ {
+ BaseClass::RunTask( pTask );
+
+ if ( GetNavigator()->IsGoalActive() && !GetEnemy() )
+ {
+ AddFacingTarget( EyePosition() + BodyDirection2D() * 240, 1.0, 2.0 );
+ }
+ }
+ break;
+
+ case TASK_PC_GET_PATH_OFF_COMPANION:
+ {
+ if ( AI_IsSinglePlayer() )
+ {
+ GetNavigator()->SetAllowBigStep( UTIL_GetLocalPlayer() );
+ }
+ ChainRunTask( TASK_MOVE_AWAY_PATH, 48 );
+ }
+ break;
+
+ default:
+ BaseClass::RunTask( pTask );
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Parses this NPC's activity remap from the actremap.txt file
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::PrepareReadinessRemap( void )
+{
+ CUtlVector< CActivityRemap > entries;
+ UTIL_LoadActivityRemapFile( "scripts/actremap.txt", "npc_playercompanion", entries );
+
+ for ( int i = 0; i < entries.Count(); i++ )
+ {
+ CCompanionActivityRemap ActRemap;
+ Q_memcpy( &ActRemap, &entries[i], sizeof( CActivityRemap ) );
+
+ KeyValues *pExtraBlock = ActRemap.GetExtraKeyValueBlock();
+
+ if ( pExtraBlock )
+ {
+ KeyValues *pKey = pExtraBlock->GetFirstSubKey();
+
+ while ( pKey )
+ {
+ const char *pKeyName = pKey->GetName();
+ const char *pKeyValue = pKey->GetString();
+
+ if ( !stricmp( pKeyName, "readiness" ) )
+ {
+ ActRemap.m_fUsageBits |= bits_REMAP_READINESS;
+
+ if ( !stricmp( pKeyValue, "AIRL_PANIC" ) )
+ {
+ ActRemap.m_readiness = AIRL_PANIC;
+ }
+ else if ( !stricmp( pKeyValue, "AIRL_STEALTH" ) )
+ {
+ ActRemap.m_readiness = AIRL_STEALTH;
+ }
+ else if ( !stricmp( pKeyValue, "AIRL_RELAXED" ) )
+ {
+ ActRemap.m_readiness = AIRL_RELAXED;
+ }
+ else if ( !stricmp( pKeyValue, "AIRL_STIMULATED" ) )
+ {
+ ActRemap.m_readiness = AIRL_STIMULATED;
+ }
+ else if ( !stricmp( pKeyValue, "AIRL_AGITATED" ) )
+ {
+ ActRemap.m_readiness = AIRL_AGITATED;
+ }
+ }
+ else if ( !stricmp( pKeyName, "aiming" ) )
+ {
+ ActRemap.m_fUsageBits |= bits_REMAP_AIMING;
+
+ if ( !stricmp( pKeyValue, "TRS_NONE" ) )
+ {
+ // This is the new way to say that we don't care, the tri-state was abandoned (jdw)
+ ActRemap.m_fUsageBits &= ~bits_REMAP_AIMING;
+ }
+ else if ( !stricmp( pKeyValue, "TRS_FALSE" ) || !stricmp( pKeyValue, "FALSE" ) )
+ {
+ ActRemap.m_bAiming = false;
+ }
+ else if ( !stricmp( pKeyValue, "TRS_TRUE" ) || !stricmp( pKeyValue, "TRUE" ) )
+ {
+ ActRemap.m_bAiming = true;
+ }
+ }
+ else if ( !stricmp( pKeyName, "weaponrequired" ) )
+ {
+ ActRemap.m_fUsageBits |= bits_REMAP_WEAPON_REQUIRED;
+
+ if ( !stricmp( pKeyValue, "TRUE" ) )
+ {
+ ActRemap.m_bWeaponRequired = true;
+ }
+ else if ( !stricmp( pKeyValue, "FALSE" ) )
+ {
+ ActRemap.m_bWeaponRequired = false;
+ }
+ }
+ else if ( !stricmp( pKeyName, "invehicle" ) )
+ {
+ ActRemap.m_fUsageBits |= bits_REMAP_IN_VEHICLE;
+
+ if ( !stricmp( pKeyValue, "TRUE" ) )
+ {
+ ActRemap.m_bInVehicle = true;
+ }
+ else if ( !stricmp( pKeyValue, "FALSE" ) )
+ {
+ ActRemap.m_bInVehicle = false;
+ }
+ }
+
+ pKey = pKey->GetNextKey();
+ }
+ }
+
+ const char *pActName = ActivityList_NameForIndex( (int)ActRemap.mappedActivity );
+
+ if ( GetActivityID( pActName ) == ACT_INVALID )
+ {
+ AddActivityToSR( pActName, (int)ActRemap.mappedActivity );
+ }
+
+ m_activityMappings.AddToTail( ActRemap );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::Activate( void )
+{
+ BaseClass::Activate();
+
+ PrepareReadinessRemap();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Translate an activity given a list of criteria
+//-----------------------------------------------------------------------------
+Activity CNPC_PlayerCompanion::TranslateActivityReadiness( Activity activity )
+{
+ // If we're in an actbusy, we don't want to mess with this
+ if ( m_ActBusyBehavior.IsActive() )
+ return activity;
+
+ if ( m_bReadinessCapable &&
+ ( GetReadinessUse() == AIRU_ALWAYS ||
+ ( GetReadinessUse() == AIRU_ONLY_PLAYER_SQUADMATES && (IsInPlayerSquad()||Classify()==CLASS_PLAYER_ALLY_VITAL) ) ) )
+ {
+ bool bShouldAim = ShouldBeAiming();
+
+ for ( int i = 0; i < m_activityMappings.Count(); i++ )
+ {
+ // Get our activity remap
+ CCompanionActivityRemap actremap = m_activityMappings[i];
+
+ // Activity must match
+ if ( activity != actremap.activity )
+ continue;
+
+ // Readiness must match
+ if ( ( actremap.m_fUsageBits & bits_REMAP_READINESS ) && GetReadinessLevel() != actremap.m_readiness )
+ continue;
+
+ // Deal with weapon state
+ if ( ( actremap.m_fUsageBits & bits_REMAP_WEAPON_REQUIRED ) && actremap.m_bWeaponRequired )
+ {
+ // Must have a weapon
+ if ( GetActiveWeapon() == NULL )
+ continue;
+
+ // Must either not care about aiming, or agree on aiming
+ if ( actremap.m_fUsageBits & bits_REMAP_AIMING )
+ {
+ if ( bShouldAim && actremap.m_bAiming == false )
+ continue;
+
+ if ( bShouldAim == false && actremap.m_bAiming )
+ continue;
+ }
+ }
+
+ // Must care about vehicle status
+ if ( actremap.m_fUsageBits & bits_REMAP_IN_VEHICLE )
+ {
+ // Deal with the two vehicle states
+ if ( actremap.m_bInVehicle && IsInAVehicle() == false )
+ continue;
+
+ if ( actremap.m_bInVehicle == false && IsInAVehicle() )
+ continue;
+ }
+
+ // We've successfully passed all criteria for remapping this
+ return actremap.mappedActivity;
+ }
+ }
+
+ return activity;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Override base class activiites
+//-----------------------------------------------------------------------------
+Activity CNPC_PlayerCompanion::NPC_TranslateActivity( Activity activity )
+{
+ if ( activity == ACT_COWER )
+ return ACT_COVER_LOW;
+
+ if ( activity == ACT_RUN && ( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND ) || IsCurSchedule( SCHED_FLEE_FROM_BEST_SOUND ) ) )
+ {
+ if ( random->RandomInt( 0, 1 ) && HaveSequenceForActivity( ACT_RUN_PROTECTED ) )
+ activity = ACT_RUN_PROTECTED;
+ }
+
+ activity = BaseClass::NPC_TranslateActivity( activity );
+
+ if ( activity == ACT_IDLE )
+ {
+ if ( (m_NPCState == NPC_STATE_COMBAT || m_NPCState == NPC_STATE_ALERT) && gpGlobals->curtime - m_flLastAttackTime < 3)
+ {
+ activity = ACT_IDLE_ANGRY;
+ }
+ }
+
+ return TranslateActivityReadiness( activity );
+}
+
+//------------------------------------------------------------------------------
+// Purpose: Handle animation events
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::HandleAnimEvent( animevent_t *pEvent )
+{
+#ifdef HL2_EPISODIC
+ // Create a flare and parent to our hand
+ if ( pEvent->event == AE_COMPANION_PRODUCE_FLARE )
+ {
+ m_hFlare = static_cast<CPhysicsProp *>(CreateEntityByName( "prop_physics" ));
+ if ( m_hFlare != NULL )
+ {
+ // Set the model
+ m_hFlare->SetModel( "models/props_junk/flare.mdl" );
+
+ // Set the parent attachment
+ m_hFlare->SetParent( this );
+ m_hFlare->SetParentAttachment( "SetParentAttachment", pEvent->options, false );
+ }
+
+ return;
+ }
+
+ // Start the flare up with proper fanfare
+ if ( pEvent->event == AE_COMPANION_LIGHT_FLARE )
+ {
+ if ( m_hFlare != NULL )
+ {
+ m_hFlare->CreateFlare( 5*60.0f );
+ }
+
+ return;
+ }
+
+ // Drop the flare to the ground
+ if ( pEvent->event == AE_COMPANION_RELEASE_FLARE )
+ {
+ // Detach
+ m_hFlare->SetParent( NULL );
+ m_hFlare->Spawn();
+ m_hFlare->RemoveInteraction( PROPINTER_PHYSGUN_CREATE_FLARE );
+
+ // Disable collisions between the NPC and the flare
+ PhysDisableEntityCollisions( this, m_hFlare );
+
+ // TODO: Find the velocity of the attachment point, at this time, in the animation cycle
+
+ // Construct a toss velocity
+ Vector vecToss;
+ AngleVectors( GetAbsAngles(), &vecToss );
+ VectorNormalize( vecToss );
+ vecToss *= random->RandomFloat( 64.0f, 72.0f );
+ vecToss[2] += 64.0f;
+
+ // Throw it
+ IPhysicsObject *pObj = m_hFlare->VPhysicsGetObject();
+ pObj->ApplyForceCenter( vecToss );
+
+ // Forget about the flare at this point
+ m_hFlare = NULL;
+
+ return;
+ }
+#endif // HL2_EPISODIC
+
+ switch( pEvent->event )
+ {
+ 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;
+
+ default:
+ BaseClass::HandleAnimEvent( pEvent );
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: This is a generic function (to be implemented by sub-classes) to
+// handle specific interactions between different types of characters
+// (For example the barnacle grabbing an NPC)
+// Input : Constant for the type of interaction
+// Output : true - if sub-class has a response for the interaction
+// false - if sub-class has no response
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt)
+{
+ if (interactionType == g_interactionHitByPlayerThrownPhysObj )
+ {
+ if ( IsOkToSpeakInResponseToPlayer() )
+ {
+ Speak( TLK_PLYR_PHYSATK );
+ }
+ return true;
+ }
+
+ return BaseClass::HandleInteraction( interactionType, data, sourceEnt );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+int CNPC_PlayerCompanion::GetSoundInterests()
+{
+ return SOUND_WORLD |
+ SOUND_COMBAT |
+ SOUND_PLAYER |
+ SOUND_DANGER |
+ SOUND_BULLET_IMPACT |
+ SOUND_MOVE_AWAY |
+ SOUND_READINESS_LOW |
+ SOUND_READINESS_MEDIUM |
+ SOUND_READINESS_HIGH;
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::Touch( CBaseEntity *pOther )
+{
+ BaseClass::Touch( pOther );
+
+ // Did the player touch me?
+ if ( pOther->IsPlayer() || ( pOther->VPhysicsGetObject() && (pOther->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) ) )
+ {
+ // Ignore if pissed at player
+ if ( m_afMemory & bits_MEMORY_PROVOKED )
+ return;
+
+ TestPlayerPushing( ( pOther->IsPlayer() ) ? pOther : AI_GetSinglePlayer() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::ModifyOrAppendCriteria( AI_CriteriaSet& set )
+{
+ BaseClass::ModifyOrAppendCriteria( set );
+ if ( GetEnemy() && IsMortar( GetEnemy() ) )
+ {
+ set.RemoveCriteria( "enemy" );
+ set.AppendCriteria( "enemy", STRING(gm_iszMortarClassname) );
+ }
+
+ if ( HasCondition( COND_PC_HURTBYFIRE ) )
+ {
+ set.AppendCriteria( "hurt_by_fire", "1" );
+ }
+
+ if ( m_bReadinessCapable )
+ {
+ switch( GetReadinessLevel() )
+ {
+ case AIRL_PANIC:
+ set.AppendCriteria( "readiness", "panic" );
+ break;
+
+ case AIRL_STEALTH:
+ set.AppendCriteria( "readiness", "stealth" );
+ break;
+
+ case AIRL_RELAXED:
+ set.AppendCriteria( "readiness", "relaxed" );
+ break;
+
+ case AIRL_STIMULATED:
+ set.AppendCriteria( "readiness", "stimulated" );
+ break;
+
+ case AIRL_AGITATED:
+ set.AppendCriteria( "readiness", "agitated" );
+ break;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsReadinessCapable()
+{
+ if ( GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON )
+ return false;
+
+#ifndef HL2_EPISODIC
+ // Allow episodic companions to use readiness even if unarmed. This allows for the panicked
+ // citizens in ep1_c17_05 (sjb)
+ if( !GetActiveWeapon() )
+ return false;
+#endif
+
+ if( GetActiveWeapon() && LookupActivity("ACT_IDLE_AIM_RIFLE_STIMULATED") == ACT_INVALID )
+ return false;
+
+ if( GetActiveWeapon() && FClassnameIs( GetActiveWeapon(), "weapon_rpg" ) )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::AddReadiness( float flAdd, bool bOverrideLock )
+{
+ if( IsReadinessLocked() && !bOverrideLock )
+ return;
+
+ SetReadinessValue( GetReadinessValue() + flAdd );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::SubtractReadiness( float flSub, bool bOverrideLock )
+{
+ if( IsReadinessLocked() && !bOverrideLock )
+ return;
+
+ // Prevent readiness from going below 0 (below 0 is only for scripted states)
+ SetReadinessValue( MAX(GetReadinessValue() - flSub, 0) );
+}
+
+//-----------------------------------------------------------------------------
+// This method returns false if the NPC is not allowed to change readiness at this point.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::AllowReadinessValueChange( void )
+{
+ if ( GetIdealActivity() == ACT_IDLE || GetIdealActivity() == ACT_WALK || GetIdealActivity() == ACT_RUN )
+ return true;
+
+ if ( HasActiveLayer() == true )
+ return false;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// NOTE: This function ignores the lock. Use the interface functions.
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::SetReadinessValue( float flSet )
+{
+ if ( AllowReadinessValueChange() == false )
+ return;
+
+ int priorReadiness = GetReadinessLevel();
+
+ flSet = MIN( 1.0f, flSet );
+ flSet = MAX( READINESS_MIN_VALUE, flSet );
+
+ m_flReadiness = flSet;
+
+ if( GetReadinessLevel() != priorReadiness )
+ {
+ // We've been bumped up into a different readiness level.
+ // Interrupt IDLE schedules (if we're playing one) so that
+ // we can pick the proper animation.
+ SetCondition( COND_IDLE_INTERRUPT );
+
+ // Force us to recalculate our animation. If we don't do this,
+ // our translated activity may change, but not our root activity,
+ // and then we won't actually visually change anims.
+ ResetActivity();
+
+ //Force the NPC to recalculate it's arrival sequence since it'll most likely be wrong now that we changed readiness level.
+ GetNavigator()->SetArrivalSequence( ACT_INVALID );
+
+ ReadinessLevelChanged( priorReadiness );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// if bOverrideLock, you'll change the readiness level even if we're within
+// a time period during which someone else has locked the level.
+//
+// if bSlam, you'll allow the readiness level to be set lower than current.
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::SetReadinessLevel( int iLevel, bool bOverrideLock, bool bSlam )
+{
+ if( IsReadinessLocked() && !bOverrideLock )
+ return;
+
+ switch( iLevel )
+ {
+ case AIRL_PANIC:
+ if( bSlam )
+ SetReadinessValue( READINESS_MODE_PANIC );
+ break;
+ case AIRL_STEALTH:
+ if( bSlam )
+ SetReadinessValue( READINESS_MODE_STEALTH );
+ break;
+ case AIRL_RELAXED:
+ if( bSlam || GetReadinessValue() < READINESS_VALUE_RELAXED )
+ SetReadinessValue( READINESS_VALUE_RELAXED );
+ break;
+ case AIRL_STIMULATED:
+ if( bSlam || GetReadinessValue() < READINESS_VALUE_STIMULATED )
+ SetReadinessValue( READINESS_VALUE_STIMULATED );
+ break;
+ case AIRL_AGITATED:
+ if( bSlam || GetReadinessValue() < READINESS_VALUE_AGITATED )
+ SetReadinessValue( READINESS_VALUE_AGITATED );
+ break;
+ default:
+ DevMsg("ERROR: Bad readiness level\n");
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::GetReadinessLevel()
+{
+ if ( m_bReadinessCapable == false )
+ return AIRL_RELAXED;
+
+ if( m_flReadiness == READINESS_MODE_PANIC )
+ {
+ return AIRL_PANIC;
+ }
+
+ if( m_flReadiness == READINESS_MODE_STEALTH )
+ {
+ return AIRL_STEALTH;
+ }
+
+ if( m_flReadiness <= READINESS_VALUE_RELAXED )
+ {
+ return AIRL_RELAXED;
+ }
+
+ if( m_flReadiness <= READINESS_VALUE_STIMULATED )
+ {
+ return AIRL_STIMULATED;
+ }
+
+ return AIRL_AGITATED;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::UpdateReadiness()
+{
+ // Only update readiness if it's not in a scripted state
+ if ( !IsInScriptedReadinessState() )
+ {
+ if( HasCondition(COND_HEAR_COMBAT) || HasCondition(COND_HEAR_BULLET_IMPACT) )
+ SetReadinessLevel( AIRL_STIMULATED, false, false );
+
+ if( HasCondition(COND_HEAR_DANGER) || HasCondition(COND_SEE_ENEMY) )
+ SetReadinessLevel( AIRL_AGITATED, false, false );
+
+ if( m_flReadiness > 0.0f && GetReadinessDecay() > 0 )
+ {
+ // Decay.
+ SubtractReadiness( ( 0.1 * (1.0f/GetReadinessDecay())) * m_flReadinessSensitivity );
+ }
+ }
+
+ if( ai_debug_readiness.GetBool() && AI_IsSinglePlayer() )
+ {
+ // Draw the readiness-o-meter
+ Vector vecSpot;
+ Vector vecOffset( 0, 0, 12 );
+ const float BARLENGTH = 12.0f;
+ const float GRADLENGTH = 4.0f;
+
+ Vector right;
+ UTIL_PlayerByIndex( 1 )->GetVectors( NULL, &right, NULL );
+
+ if ( IsInScriptedReadinessState() )
+ {
+ // Just print the name of the scripted state
+ vecSpot = EyePosition() + vecOffset;
+
+ if( GetReadinessLevel() == AIRL_STEALTH )
+ {
+ NDebugOverlay::Text( vecSpot, "Stealth", true, 0.1 );
+ }
+ else if( GetReadinessLevel() == AIRL_PANIC )
+ {
+ NDebugOverlay::Text( vecSpot, "Panic", true, 0.1 );
+ }
+ else
+ {
+ NDebugOverlay::Text( vecSpot, "Unspecified", true, 0.1 );
+ }
+ }
+ else
+ {
+ vecSpot = EyePosition() + vecOffset;
+ NDebugOverlay::Line( vecSpot, vecSpot + right * GRADLENGTH, 255, 255, 255, false, 0.1 );
+
+ vecSpot = EyePosition() + vecOffset + Vector( 0, 0, BARLENGTH * READINESS_VALUE_RELAXED );
+ NDebugOverlay::Line( vecSpot, vecSpot + right * GRADLENGTH, 0, 255, 0, false, 0.1 );
+
+ vecSpot = EyePosition() + vecOffset + Vector( 0, 0, BARLENGTH * READINESS_VALUE_STIMULATED );
+ NDebugOverlay::Line( vecSpot, vecSpot + right * GRADLENGTH, 255, 255, 0, false, 0.1 );
+
+ vecSpot = EyePosition() + vecOffset + Vector( 0, 0, BARLENGTH * READINESS_VALUE_AGITATED );
+ NDebugOverlay::Line( vecSpot, vecSpot + right * GRADLENGTH, 255, 0, 0, false, 0.1 );
+
+ vecSpot = EyePosition() + vecOffset;
+ NDebugOverlay::Line( vecSpot, vecSpot + Vector( 0, 0, BARLENGTH * GetReadinessValue() ), 255, 255, 0, false, 0.1 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+float CNPC_PlayerCompanion::GetReadinessDecay()
+{
+ return ai_readiness_decay.GetFloat();
+}
+
+//-----------------------------------------------------------------------------
+// Passing NULL to clear the aim target is acceptible.
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::SetAimTarget( CBaseEntity *pTarget )
+{
+ if( pTarget != NULL && IsAllowedToAim() )
+ {
+ m_hAimTarget = pTarget;
+ }
+ else
+ {
+ m_hAimTarget = NULL;
+ }
+
+ Activity NewActivity = NPC_TranslateActivity(GetActivity());
+
+ //Don't set the ideal activity to an activity that might not be there.
+ if ( SelectWeightedSequence( NewActivity ) == ACT_INVALID )
+ return;
+
+ if (NewActivity != GetActivity() )
+ {
+ SetIdealActivity( NewActivity );
+ }
+
+#if 0
+ if( m_hAimTarget )
+ {
+ Msg("New Aim Target: %s\n", m_hAimTarget->GetClassname() );
+ NDebugOverlay::Line(EyePosition(), m_hAimTarget->WorldSpaceCenter(), 255, 255, 0, false, 0.1 );
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::StopAiming( char *pszReason )
+{
+#if 0
+ if( pszReason )
+ {
+ Msg("Stopped aiming because %s\n", pszReason );
+ }
+#endif
+
+ SetAimTarget(NULL);
+
+ Activity NewActivity = NPC_TranslateActivity(GetActivity());
+ if (NewActivity != GetActivity())
+ {
+ SetIdealActivity( NewActivity );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+#define COMPANION_MAX_LOOK_TIME 3.0f
+#define COMPANION_MIN_LOOK_TIME 1.0f
+#define COMPANION_MAX_TACTICAL_TARGET_DIST 1800.0f // 150 feet
+
+bool CNPC_PlayerCompanion::PickTacticalLookTarget( AILookTargetArgs_t *pArgs )
+{
+ if( HasCondition( COND_SEE_ENEMY ) )
+ {
+ // Don't bother. We're dealing with our enemy.
+ return false;
+ }
+
+ float flMinLookTime;
+ float flMaxLookTime;
+
+ // Excited companions will look at each target only briefly and then find something else to look at.
+ flMinLookTime = COMPANION_MIN_LOOK_TIME + ((COMPANION_MAX_LOOK_TIME-COMPANION_MIN_LOOK_TIME) * (1.0f - GetReadinessValue()) );
+
+ switch( GetReadinessLevel() )
+ {
+ case AIRL_RELAXED:
+ // Linger on targets, look at them for quite a while.
+ flMinLookTime = COMPANION_MAX_LOOK_TIME + random->RandomFloat( 0.0f, 2.0f );
+ break;
+
+ case AIRL_STIMULATED:
+ // Look around a little quicker.
+ flMinLookTime = COMPANION_MIN_LOOK_TIME + random->RandomFloat( 0.0f, COMPANION_MAX_LOOK_TIME - 1.0f );
+ break;
+
+ case AIRL_AGITATED:
+ // Look around very quickly
+ flMinLookTime = COMPANION_MIN_LOOK_TIME;
+ break;
+ }
+
+ flMaxLookTime = flMinLookTime + random->RandomFloat( 0.0f, 0.5f );
+ pArgs->flDuration = random->RandomFloat( flMinLookTime, flMaxLookTime );
+
+ if( HasCondition(COND_SEE_PLAYER) && hl2_episodic.GetBool() )
+ {
+ // 1/3rd chance to authoritatively look at player
+ if( random->RandomInt( 0, 2 ) == 0 )
+ {
+ pArgs->hTarget = AI_GetSinglePlayer();
+ return true;
+ }
+ }
+
+ // Use hint nodes
+ CAI_Hint *pHint;
+ CHintCriteria hintCriteria;
+
+ hintCriteria.AddHintType( HINT_WORLD_VISUALLY_INTERESTING );
+ hintCriteria.AddHintType( HINT_WORLD_VISUALLY_INTERESTING_DONT_AIM );
+ hintCriteria.AddHintType( HINT_WORLD_VISUALLY_INTERESTING_STEALTH );
+ hintCriteria.SetFlag( bits_HINT_NODE_VISIBLE | bits_HINT_NODE_IN_VIEWCONE | bits_HINT_NPC_IN_NODE_FOV );
+ hintCriteria.AddIncludePosition( GetAbsOrigin(), COMPANION_MAX_TACTICAL_TARGET_DIST );
+
+ {
+ AI_PROFILE_SCOPE( CNPC_PlayerCompanion_FindHint_PickTacticalLookTarget );
+ pHint = CAI_HintManager::FindHint( this, hintCriteria );
+ }
+
+ if( pHint )
+ {
+ pArgs->hTarget = pHint;
+
+ // Turn this node off for a few seconds to stop others aiming at the same thing (except for stealth nodes)
+ if ( pHint->HintType() != HINT_WORLD_VISUALLY_INTERESTING_STEALTH )
+ {
+ pHint->DisableForSeconds( 5.0f );
+ }
+ return true;
+ }
+
+ // See what the base class thinks.
+ return BaseClass::PickTacticalLookTarget( pArgs );
+}
+
+//-----------------------------------------------------------------------------
+// Returns true if changing target.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::FindNewAimTarget()
+{
+ if( GetEnemy() )
+ {
+ // Don't bother. Aim at enemy.
+ return false;
+ }
+
+ if( !m_bReadinessCapable || GetReadinessLevel() == AIRL_RELAXED )
+ {
+ // If I'm relaxed (don't want to aim), or physically incapable,
+ // don't run this hint node searching code.
+ return false;
+ }
+
+ CAI_Hint *pHint;
+ CHintCriteria hintCriteria;
+ CBaseEntity *pPriorAimTarget = GetAimTarget();
+
+ hintCriteria.SetHintType( HINT_WORLD_VISUALLY_INTERESTING );
+ hintCriteria.SetFlag( bits_HINT_NODE_VISIBLE | bits_HINT_NODE_IN_VIEWCONE | bits_HINT_NPC_IN_NODE_FOV );
+ hintCriteria.AddIncludePosition( GetAbsOrigin(), COMPANION_MAX_TACTICAL_TARGET_DIST );
+ pHint = CAI_HintManager::FindHint( this, hintCriteria );
+
+ if( pHint )
+ {
+ if( (pHint->GetAbsOrigin() - GetAbsOrigin()).Length2D() < COMPANION_AIMTARGET_NEAREST )
+ {
+ // Too close!
+ return false;
+ }
+
+ if( !HasAimLOS(pHint) )
+ {
+ // No LOS
+ return false;
+ }
+
+ if( pHint != pPriorAimTarget )
+ {
+ // Notify of the change.
+ SetAimTarget( pHint );
+ return true;
+ }
+ }
+
+ // Didn't find an aim target, or found the same one.
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::OnNewLookTarget()
+{
+ if( ai_new_aiming.GetBool() )
+ {
+ if( GetLooktarget() )
+ {
+ // See if our looktarget is a reasonable aim target.
+ CAI_Hint *pHint = dynamic_cast<CAI_Hint*>( GetLooktarget() );
+
+ if( pHint )
+ {
+ if( pHint->HintType() == HINT_WORLD_VISUALLY_INTERESTING &&
+ (pHint->GetAbsOrigin() - GetAbsOrigin()).Length2D() > COMPANION_AIMTARGET_NEAREST &&
+ FInAimCone(pHint->GetAbsOrigin()) &&
+ HasAimLOS(pHint) )
+ {
+ SetAimTarget( pHint );
+ return;
+ }
+ }
+ }
+
+ // Search for something else.
+ FindNewAimTarget();
+ }
+ else
+ {
+ if( GetLooktarget() )
+ {
+ // Have picked a new entity to look at. Should we copy it to the aim target?
+ if( IRelationType( GetLooktarget() ) == D_LI )
+ {
+ // Don't aim at friends, just keep the old target (if any)
+ return;
+ }
+
+ if( (GetLooktarget()->GetAbsOrigin() - GetAbsOrigin()).Length2D() < COMPANION_AIMTARGET_NEAREST )
+ {
+ // Too close!
+ return;
+ }
+
+ if( !HasAimLOS( GetLooktarget() ) )
+ {
+ // No LOS
+ return;
+ }
+
+ SetAimTarget( GetLooktarget() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldBeAiming()
+{
+ if( !IsAllowedToAim() )
+ {
+ return false;
+ }
+
+ if( !GetEnemy() && !GetAimTarget() )
+ {
+ return false;
+ }
+
+ if( GetEnemy() && !HasCondition(COND_SEE_ENEMY) )
+ {
+ return false;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+#define PC_MAX_ALLOWED_AIM 2
+bool CNPC_PlayerCompanion::IsAllowedToAim()
+{
+ if( !m_pSquad )
+ return true;
+
+ if( GetReadinessLevel() == AIRL_AGITATED )
+ {
+ // Agitated companions can always aim. This makes the squad look
+ // more alert as a whole when something very serious/dangerous has happened.
+ return true;
+ }
+
+ int count = 0;
+
+ // If I'm in a squad, only a certain number of us can aim.
+ AISquadIter_t iter;
+ for ( CAI_BaseNPC *pSquadmate = m_pSquad->GetFirstMember(&iter); pSquadmate; pSquadmate = m_pSquad->GetNextMember(&iter) )
+ {
+ CNPC_PlayerCompanion *pCompanion = dynamic_cast<CNPC_PlayerCompanion*>(pSquadmate);
+ if( pCompanion && pCompanion != this && pCompanion->GetAimTarget() != NULL )
+ {
+ count++;
+ }
+ }
+
+ if( count < PC_MAX_ALLOWED_AIM )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::HasAimLOS( CBaseEntity *pAimTarget )
+{
+ trace_t tr;
+ UTIL_TraceLine( Weapon_ShootPosition(), pAimTarget->WorldSpaceCenter(), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr );
+
+ if( tr.fraction < 0.5 || (tr.m_pEnt && (tr.m_pEnt->IsNPC()||tr.m_pEnt->IsPlayer())) )
+ {
+ return false;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::AimGun()
+{
+ Vector vecAimDir;
+
+ if( !GetEnemy() )
+ {
+ if( GetAimTarget() && FInViewCone(GetAimTarget()) )
+ {
+ float flDist;
+ Vector vecAimTargetLoc = GetAimTarget()->WorldSpaceCenter();
+
+ flDist = (vecAimTargetLoc - GetAbsOrigin()).Length2DSqr();
+
+ // Throw away a looktarget if it gets too close. We don't want guys turning around as
+ // they walk through doorways which contain a looktarget.
+ if( flDist < COMPANION_AIMTARGET_NEAREST_SQR )
+ {
+ StopAiming("Target too near");
+ return;
+ }
+
+ // Aim at my target if it's in my cone
+ vecAimDir = vecAimTargetLoc - Weapon_ShootPosition();;
+ VectorNormalize( vecAimDir );
+ SetAim( vecAimDir);
+
+ if( !HasAimLOS(GetAimTarget()) )
+ {
+ // LOS is broken.
+ if( !FindNewAimTarget() )
+ {
+ // No alternative available right now. Stop aiming.
+ StopAiming("No LOS");
+ }
+ }
+
+ return;
+ }
+ else
+ {
+ if( GetAimTarget() )
+ {
+ // We're aiming at something, but we're about to stop because it's out of viewcone.
+ // Try to find something else.
+ if( FindNewAimTarget() )
+ {
+ // Found something else to aim at.
+ return;
+ }
+ else
+ {
+ // ditch the aim target, it's gone out of view.
+ StopAiming("Went out of view cone");
+ }
+ }
+
+ if( GetReadinessLevel() == AIRL_AGITATED )
+ {
+ // Aim down! Agitated animations don't have non-aiming versions, so
+ // just point the weapon down.
+ Vector vecSpot = EyePosition();
+ Vector forward, up;
+ GetVectors( &forward, NULL, &up );
+ vecSpot += forward * 128 + up * -64;
+
+ vecAimDir = vecSpot - Weapon_ShootPosition();
+ VectorNormalize( vecAimDir );
+ SetAim( vecAimDir);
+ return;
+ }
+ }
+ }
+
+ BaseClass::AimGun();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+CBaseEntity *CNPC_PlayerCompanion::GetAlternateMoveShootTarget()
+{
+ if( GetAimTarget() && !GetAimTarget()->IsNPC() && GetReadinessLevel() != AIRL_RELAXED )
+ {
+ return GetAimTarget();
+ }
+
+ return BaseClass::GetAlternateMoveShootTarget();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsValidEnemy( CBaseEntity *pEnemy )
+{
+ if ( GetFollowBehavior().GetFollowTarget() && GetFollowBehavior().GetFollowTarget()->IsPlayer() && IsSniper( pEnemy ) )
+ {
+ AI_EnemyInfo_t *pInfo = GetEnemies()->Find( pEnemy );
+ if ( pInfo )
+ {
+ if ( gpGlobals->curtime - pInfo->timeLastSeen > 10 )
+ {
+ if ( !((CAI_BaseNPC*)pEnemy)->HasCondition( COND_IN_PVS ) )
+ return false;
+ }
+ }
+ }
+
+ return BaseClass::IsValidEnemy( pEnemy );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsSafeFromFloorTurret( const Vector &vecLocation, CBaseEntity *pTurret )
+{
+ float dist = ( vecLocation - pTurret->EyePosition() ).LengthSqr();
+
+ if ( dist > Square( 4.0*12.0 ) )
+ {
+ if ( !pTurret->MyNPCPointer()->FInViewCone( vecLocation ) )
+ {
+#if 0 // Draws a green line to turrets I'm safe from
+ NDebugOverlay::Line( vecLocation, pTurret->WorldSpaceCenter(), 0, 255, 0, false, 0.1 );
+#endif
+ return true;
+ }
+ }
+
+#if 0 // Draws a red lines to ones I'm not safe from.
+ NDebugOverlay::Line( vecLocation, pTurret->WorldSpaceCenter(), 255, 0, 0, false, 0.1 );
+#endif
+ return false;
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldMoveAndShoot( void )
+{
+ return BaseClass::ShouldMoveAndShoot();
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+#define PC_LARGER_BURST_RANGE (12.0f * 10.0f) // If an enemy is this close, player companions fire larger continuous bursts.
+void CNPC_PlayerCompanion::OnUpdateShotRegulator()
+{
+ BaseClass::OnUpdateShotRegulator();
+
+ if( GetEnemy() && HasCondition(COND_CAN_RANGE_ATTACK1) )
+ {
+ if( GetAbsOrigin().DistTo( GetEnemy()->GetAbsOrigin() ) <= PC_LARGER_BURST_RANGE )
+ {
+ if( hl2_episodic.GetBool() )
+ {
+ // Longer burst
+ int longBurst = random->RandomInt( 10, 15 );
+ GetShotRegulator()->SetBurstShotsRemaining( longBurst );
+ GetShotRegulator()->SetRestInterval( 0.1, 0.2 );
+ }
+ else
+ {
+ // Longer burst
+ GetShotRegulator()->SetBurstShotsRemaining( GetShotRegulator()->GetBurstShotsRemaining() * 2 );
+
+ // Shorter Rest interval
+ float flMinInterval, flMaxInterval;
+ GetShotRegulator()->GetRestInterval( &flMinInterval, &flMaxInterval );
+ GetShotRegulator()->SetRestInterval( flMinInterval * 0.6f, flMaxInterval * 0.6f );
+ }
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::DecalTrace( trace_t *pTrace, char const *decalName )
+{
+ // Do not decal a player companion's head or face, no matter what.
+ if( pTrace->hitgroup == HITGROUP_HEAD )
+ return;
+
+ BaseClass::DecalTrace( pTrace, decalName );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::FCanCheckAttacks()
+{
+ if( GetEnemy() && ( IsSniper(GetEnemy()) || IsMortar(GetEnemy()) || IsTurret(GetEnemy()) ) )
+ {
+ // Don't attack the sniper or the mortar.
+ return false;
+ }
+
+ return BaseClass::FCanCheckAttacks();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return the actual position the NPC wants to fire at when it's trying
+// to hit it's current enemy.
+//-----------------------------------------------------------------------------
+#define CITIZEN_HEADSHOT_FREQUENCY 3 // one in this many shots at a zombie will be aimed at the zombie's head
+Vector CNPC_PlayerCompanion::GetActualShootPosition( const Vector &shootOrigin )
+{
+ if( GetEnemy() && GetEnemy()->Classify() == CLASS_ZOMBIE && random->RandomInt( 1, CITIZEN_HEADSHOT_FREQUENCY ) == 1 )
+ {
+ return GetEnemy()->HeadTarget( shootOrigin );
+ }
+
+ return BaseClass::GetActualShootPosition( shootOrigin );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+WeaponProficiency_t CNPC_PlayerCompanion::CalcWeaponProficiency( CBaseCombatWeapon *pWeapon )
+{
+ if( FClassnameIs( pWeapon, "weapon_ar2" ) )
+ {
+ return WEAPON_PROFICIENCY_VERY_GOOD;
+ }
+
+ return WEAPON_PROFICIENCY_PERFECT;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::Weapon_CanUse( CBaseCombatWeapon *pWeapon )
+{
+ if( BaseClass::Weapon_CanUse( pWeapon ) )
+ {
+ // If this weapon is a shotgun, take measures to control how many
+ // are being used in this squad. Don't allow a companion to pick up
+ // a shotgun if a squadmate already has one.
+ if( pWeapon->ClassMatches( gm_iszShotgunClassname ) )
+ {
+ return (NumWeaponsInSquad("weapon_shotgun") < 1 );
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldLookForBetterWeapon()
+{
+ if ( m_bDontPickupWeapons )
+ return false;
+
+ return BaseClass::ShouldLookForBetterWeapon();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::Weapon_Equip( CBaseCombatWeapon *pWeapon )
+{
+ BaseClass::Weapon_Equip( pWeapon );
+ m_bReadinessCapable = IsReadinessCapable();
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::PickupWeapon( CBaseCombatWeapon *pWeapon )
+{
+ BaseClass::PickupWeapon( pWeapon );
+ SpeakIfAllowed( TLK_NEWWEAPON );
+ m_OnWeaponPickup.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+const int MAX_NON_SPECIAL_MULTICOVER = 2;
+
+CUtlVector<AI_EnemyInfo_t *> g_MultiCoverSearchEnemies;
+CNPC_PlayerCompanion * g_pMultiCoverSearcher;
+
+//-------------------------------------
+
+int __cdecl MultiCoverCompare( AI_EnemyInfo_t * const *ppLeft, AI_EnemyInfo_t * const *ppRight )
+{
+ const AI_EnemyInfo_t *pLeft = *ppLeft;
+ const AI_EnemyInfo_t *pRight = *ppRight;
+
+ if ( !pLeft->hEnemy && !pRight->hEnemy)
+ return 0;
+
+ if ( !pLeft->hEnemy )
+ return 1;
+
+ if ( !pRight->hEnemy )
+ return -1;
+
+ if ( pLeft->hEnemy == g_pMultiCoverSearcher->GetEnemy() )
+ return -1;
+
+ if ( pRight->hEnemy == g_pMultiCoverSearcher->GetEnemy() )
+ return 1;
+
+ bool bLeftIsSpecial = ( CNPC_PlayerCompanion::IsMortar( pLeft->hEnemy ) || CNPC_PlayerCompanion::IsSniper( pLeft->hEnemy ) );
+ bool bRightIsSpecial = ( CNPC_PlayerCompanion::IsMortar( pLeft->hEnemy ) || CNPC_PlayerCompanion::IsSniper( pLeft->hEnemy ) );
+
+ if ( !bLeftIsSpecial && bRightIsSpecial )
+ return 1;
+
+ if ( bLeftIsSpecial && !bRightIsSpecial )
+ return -1;
+
+ float leftRelevantTime = ( pLeft->timeLastSeen == AI_INVALID_TIME || pLeft->timeLastSeen == 0 ) ? -99999 : pLeft->timeLastSeen;
+ if ( pLeft->timeLastReceivedDamageFrom != AI_INVALID_TIME && pLeft->timeLastReceivedDamageFrom > leftRelevantTime )
+ leftRelevantTime = pLeft->timeLastReceivedDamageFrom;
+
+ float rightRelevantTime = ( pRight->timeLastSeen == AI_INVALID_TIME || pRight->timeLastSeen == 0 ) ? -99999 : pRight->timeLastSeen;
+ if ( pRight->timeLastReceivedDamageFrom != AI_INVALID_TIME && pRight->timeLastReceivedDamageFrom > rightRelevantTime )
+ rightRelevantTime = pRight->timeLastReceivedDamageFrom;
+
+ if ( leftRelevantTime < rightRelevantTime )
+ return -1;
+
+ if ( leftRelevantTime > rightRelevantTime )
+ return 1;
+
+ float leftDistSq = g_pMultiCoverSearcher->GetAbsOrigin().DistToSqr( pLeft->hEnemy->GetAbsOrigin() );
+ float rightDistSq = g_pMultiCoverSearcher->GetAbsOrigin().DistToSqr( pRight->hEnemy->GetAbsOrigin() );
+
+ if ( leftDistSq < rightDistSq )
+ return -1;
+
+ if ( leftDistSq > rightDistSq )
+ return 1;
+
+ return 0;
+}
+
+//-------------------------------------
+
+void CNPC_PlayerCompanion::SetupCoverSearch( CBaseEntity *pEntity )
+{
+ if ( IsTurret( pEntity ) )
+ gm_fCoverSearchType = CT_TURRET;
+
+ gm_bFindingCoverFromAllEnemies = false;
+ g_pMultiCoverSearcher = this;
+
+ if ( Classify() == CLASS_PLAYER_ALLY_VITAL || IsInPlayerSquad() )
+ {
+ if ( GetEnemy() )
+ {
+ if ( !pEntity || GetEnemies()->NumEnemies() > 1 )
+ {
+ if ( !pEntity ) // if pEntity is NULL, test is against a point in space, so always to search against current enemy too
+ gm_bFindingCoverFromAllEnemies = true;
+
+ AIEnemiesIter_t iter;
+ for ( AI_EnemyInfo_t *pEnemyInfo = GetEnemies()->GetFirst(&iter); pEnemyInfo != NULL; pEnemyInfo = GetEnemies()->GetNext(&iter) )
+ {
+ CBaseEntity *pEnemy = pEnemyInfo->hEnemy;
+ if ( pEnemy )
+ {
+ if ( pEnemy != GetEnemy() )
+ {
+ if ( pEnemyInfo->timeAtFirstHand == AI_INVALID_TIME || gpGlobals->curtime - pEnemyInfo->timeLastSeen > 10.0 )
+ continue;
+ gm_bFindingCoverFromAllEnemies = true;
+ }
+ g_MultiCoverSearchEnemies.AddToTail( pEnemyInfo );
+ }
+ }
+
+ if ( g_MultiCoverSearchEnemies.Count() == 0 )
+ {
+ gm_bFindingCoverFromAllEnemies = false;
+ }
+ else if ( gm_bFindingCoverFromAllEnemies )
+ {
+ g_MultiCoverSearchEnemies.Sort( MultiCoverCompare );
+ Assert( g_MultiCoverSearchEnemies[0]->hEnemy == GetEnemy() );
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::CleanupCoverSearch()
+{
+ gm_fCoverSearchType = CT_NORMAL;
+ g_MultiCoverSearchEnemies.RemoveAll();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::FindCoverPos( CBaseEntity *pEntity, Vector *pResult)
+{
+ AI_PROFILE_SCOPE(CNPC_PlayerCompanion_FindCoverPos);
+
+ ASSERT_NO_REENTRY();
+
+ bool result = false;
+
+ SetupCoverSearch( pEntity );
+
+ if ( gm_bFindingCoverFromAllEnemies )
+ {
+ result = BaseClass::FindCoverPos( pEntity, pResult );
+ gm_bFindingCoverFromAllEnemies = false;
+ }
+
+ if ( !result )
+ result = BaseClass::FindCoverPos( pEntity, pResult );
+
+ CleanupCoverSearch();
+
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+bool CNPC_PlayerCompanion::FindCoverPosInRadius( CBaseEntity *pEntity, const Vector &goalPos, float coverRadius, Vector *pResult )
+{
+ AI_PROFILE_SCOPE(CNPC_PlayerCompanion_FindCoverPosInRadius);
+
+ ASSERT_NO_REENTRY();
+
+ bool result = false;
+
+ SetupCoverSearch( pEntity );
+
+ if ( gm_bFindingCoverFromAllEnemies )
+ {
+ result = BaseClass::FindCoverPosInRadius( pEntity, goalPos, coverRadius, pResult );
+ gm_bFindingCoverFromAllEnemies = false;
+ }
+
+ if ( !result )
+ {
+ result = BaseClass::FindCoverPosInRadius( pEntity, goalPos, coverRadius, pResult );
+ }
+
+ CleanupCoverSearch();
+
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+bool CNPC_PlayerCompanion::FindCoverPos( CSound *pSound, Vector *pResult )
+{
+ AI_PROFILE_SCOPE(CNPC_PlayerCompanion_FindCoverPos);
+
+ bool result = false;
+ bool bIsMortar = ( pSound->SoundContext() == SOUND_CONTEXT_MORTAR );
+
+ SetupCoverSearch( NULL );
+
+ if ( gm_bFindingCoverFromAllEnemies )
+ {
+ result = ( bIsMortar ) ? FindMortarCoverPos( pSound, pResult ) :
+ BaseClass::FindCoverPos( pSound, pResult );
+ gm_bFindingCoverFromAllEnemies = false;
+ }
+
+ if ( !result )
+ {
+ result = ( bIsMortar ) ? FindMortarCoverPos( pSound, pResult ) :
+ BaseClass::FindCoverPos( pSound, pResult );
+ }
+
+ CleanupCoverSearch();
+
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+bool CNPC_PlayerCompanion::FindMortarCoverPos( CSound *pSound, Vector *pResult )
+{
+ bool result = false;
+
+ Assert( pSound->SoundContext() == SOUND_CONTEXT_MORTAR );
+ gm_fCoverSearchType = CT_MORTAR;
+ result = GetTacticalServices()->FindLateralCover( pSound->GetSoundOrigin(), 0, pResult );
+ if ( !result )
+ {
+ result = GetTacticalServices()->FindCoverPos( pSound->GetSoundOrigin(),
+ pSound->GetSoundOrigin(),
+ 0,
+ CoverRadius(),
+ pResult );
+ }
+ gm_fCoverSearchType = CT_NORMAL;
+
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsCoverPosition( const Vector &vecThreat, const Vector &vecPosition )
+{
+ if ( gm_bFindingCoverFromAllEnemies )
+ {
+ for ( int i = 0; i < g_MultiCoverSearchEnemies.Count(); i++ )
+ {
+ // @TODO (toml 07-27-04): Should skip checking points near already checked points
+ AI_EnemyInfo_t *pEnemyInfo = g_MultiCoverSearchEnemies[i];
+ Vector testPos;
+ CBaseEntity *pEnemy = pEnemyInfo->hEnemy;
+ if ( !pEnemy )
+ continue;
+
+ if ( pEnemy == GetEnemy() || IsMortar( pEnemy ) || IsSniper( pEnemy ) || i < MAX_NON_SPECIAL_MULTICOVER )
+ {
+ testPos = pEnemyInfo->vLastKnownLocation + pEnemy->GetViewOffset();
+ }
+ else
+ break;
+
+ gm_bFindingCoverFromAllEnemies = false;
+ bool result = IsCoverPosition( testPos, vecPosition );
+ gm_bFindingCoverFromAllEnemies = true;
+
+ if ( !result )
+ return false;
+ }
+
+ if ( gm_fCoverSearchType != CT_MORTAR && GetEnemy() && vecThreat.DistToSqr( GetEnemy()->EyePosition() ) < 1 )
+ return true;
+
+ // else fall through
+ }
+
+ if ( gm_fCoverSearchType == CT_TURRET && GetEnemy() && IsSafeFromFloorTurret( vecPosition, GetEnemy() ) )
+ {
+ return true;
+ }
+
+ if ( gm_fCoverSearchType == CT_MORTAR )
+ {
+ CSound *pSound = GetBestSound( SOUND_DANGER );
+ Assert ( pSound && pSound->SoundContext() == SOUND_CONTEXT_MORTAR );
+ if( pSound )
+ {
+ // Don't get closer to the shell
+ Vector vecToSound = vecThreat - GetAbsOrigin();
+ Vector vecToPosition = vecPosition - GetAbsOrigin();
+ VectorNormalize( vecToPosition );
+ VectorNormalize( vecToSound );
+
+ if ( vecToPosition.AsVector2D().Dot( vecToSound.AsVector2D() ) > 0 )
+ return false;
+
+ // Anything outside the radius is okay
+ float flDistSqr = (vecPosition - vecThreat).Length2DSqr();
+ float radiusSq = Square( pSound->Volume() );
+ if( flDistSqr > radiusSq )
+ {
+ return true;
+ }
+ }
+ }
+
+ return BaseClass::IsCoverPosition( vecThreat, vecPosition );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsMortar( CBaseEntity *pEntity )
+{
+ if ( !pEntity )
+ return false;
+ CBaseEntity *pEntityParent = pEntity->GetParent();
+ return ( pEntityParent && pEntityParent->GetClassname() == STRING(gm_iszMortarClassname) );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsSniper( CBaseEntity *pEntity )
+{
+ if ( !pEntity )
+ return false;
+ return ( pEntity->Classify() == CLASS_PROTOSNIPER );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsTurret( CBaseEntity *pEntity )
+{
+ if ( !pEntity )
+ return false;
+ const char *pszClassname = pEntity->GetClassname();
+ return ( pszClassname == STRING(gm_iszFloorTurretClassname) || pszClassname == STRING(gm_iszGroundTurretClassname) );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsGunship( CBaseEntity *pEntity )
+{
+ if( !pEntity )
+ return false;
+ return (pEntity->Classify() == CLASS_COMBINE_GUNSHIP );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_PlayerCompanion::OnTakeDamage_Alive( const CTakeDamageInfo &info )
+{
+ if( info.GetAttacker() )
+ {
+ bool bIsEnvFire;
+ if( ( bIsEnvFire = FClassnameIs( info.GetAttacker(), "env_fire" ) ) != false || FClassnameIs( info.GetAttacker(), "entityflame" ) || FClassnameIs( info.GetAttacker(), "env_entity_igniter" ) )
+ {
+ GetMotor()->SetIdealYawToTarget( info.GetAttacker()->GetAbsOrigin() );
+ SetCondition( COND_PC_HURTBYFIRE );
+ }
+
+ // @Note (toml 07-25-04): there isn't a good solution to player companions getting injured by
+ // fires that have huge damage radii that extend outside the rendered
+ // fire. Recovery from being injured by fire will also not be done
+ // before we ship/ Here we trade one bug (guys standing around dying
+ // from flames they appear to not be near), for a lesser one
+ // this guy was standing in a fire and didn't react. Since
+ // the levels are supposed to have the centers of all the fires
+ // npc clipped, this latter case should be rare.
+ if ( bIsEnvFire )
+ {
+ if ( ( GetAbsOrigin() - info.GetAttacker()->GetAbsOrigin() ).Length2DSqr() > Square(12 + GetHullWidth() * .5 ) )
+ {
+ return 0;
+ }
+ }
+ }
+
+ return BaseClass::OnTakeDamage_Alive( info );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::OnFriendDamaged( CBaseCombatCharacter *pSquadmate, CBaseEntity *pAttackerEnt )
+{
+ AI_PROFILE_SCOPE( CNPC_PlayerCompanion_OnFriendDamaged );
+ BaseClass::OnFriendDamaged( pSquadmate, pAttackerEnt );
+
+ CAI_BaseNPC *pAttacker = pAttackerEnt->MyNPCPointer();
+ if ( pAttacker )
+ {
+ bool bDirect = ( pSquadmate->FInViewCone(pAttacker) &&
+ ( ( pSquadmate->IsPlayer() && HasCondition(COND_SEE_PLAYER) ) ||
+ ( pSquadmate->MyNPCPointer() && pSquadmate->MyNPCPointer()->IsPlayerAlly() &&
+ GetSenses()->DidSeeEntity( pSquadmate ) ) ) );
+ if ( bDirect )
+ {
+ UpdateEnemyMemory( pAttacker, pAttacker->GetAbsOrigin(), pSquadmate );
+ }
+ else
+ {
+ if ( FVisible( pSquadmate ) )
+ {
+ AI_EnemyInfo_t *pInfo = GetEnemies()->Find( pAttacker );
+ if ( !pInfo || ( gpGlobals->curtime - pInfo->timeLastSeen ) > 15.0 )
+ UpdateEnemyMemory( pAttacker, pSquadmate->GetAbsOrigin(), pSquadmate );
+ }
+ }
+
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+ if ( pPlayer && IsInPlayerSquad() && ( pPlayer->GetAbsOrigin().AsVector2D() - GetAbsOrigin().AsVector2D() ).LengthSqr() < Square( 25*12 ) && IsAllowedToSpeak( TLK_WATCHOUT ) )
+ {
+ if ( !pPlayer->FInViewCone( pAttacker ) )
+ {
+ Vector2D vPlayerDir = pPlayer->EyeDirection2D().AsVector2D();
+ Vector2D vEnemyDir = pAttacker->EyePosition().AsVector2D() - pPlayer->EyePosition().AsVector2D();
+ vEnemyDir.NormalizeInPlace();
+ float dot = vPlayerDir.Dot( vEnemyDir );
+ if ( dot < 0 )
+ Speak( TLK_WATCHOUT, "dangerloc:behind" );
+ else if ( ( pPlayer->GetAbsOrigin().AsVector2D() - pAttacker->GetAbsOrigin().AsVector2D() ).LengthSqr() > Square( 40*12 ) )
+ Speak( TLK_WATCHOUT, "dangerloc:far" );
+ }
+ else if ( pAttacker->GetAbsOrigin().z - pPlayer->GetAbsOrigin().z > 128 )
+ {
+ Speak( TLK_WATCHOUT, "dangerloc:above" );
+ }
+ else if ( pAttacker->GetHullType() <= HULL_TINY && ( pPlayer->GetAbsOrigin().AsVector2D() - pAttacker->GetAbsOrigin().AsVector2D() ).LengthSqr() > Square( 100*12 ) )
+ {
+ Speak( TLK_WATCHOUT, "dangerloc:far" );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsValidMoveAwayDest( const Vector &vecDest )
+{
+ // Don't care what the destination is unless I have an enemy and
+ // that enemy is a sniper (for now).
+ if( !GetEnemy() )
+ {
+ return true;
+ }
+
+ if( GetEnemy()->Classify() != CLASS_PROTOSNIPER )
+ {
+ return true;
+ }
+
+ if( IsCoverPosition( GetEnemy()->EyePosition(), vecDest + GetViewOffset() ) )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::FValidateHintType( CAI_Hint *pHint )
+{
+ switch( pHint->HintType() )
+ {
+ case HINT_PLAYER_SQUAD_TRANSITON_POINT:
+ case HINT_WORLD_VISUALLY_INTERESTING_DONT_AIM:
+ case HINT_PLAYER_ALLY_MOVE_AWAY_DEST:
+ case HINT_PLAYER_ALLY_FEAR_DEST:
+ return true;
+ break;
+
+ default:
+ break;
+ }
+
+ return BaseClass::FValidateHintType( pHint );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ValidateNavGoal()
+{
+ bool result;
+ if ( GetNavigator()->GetGoalType() == GOALTYPE_COVER )
+ {
+ if ( IsEnemyTurret() )
+ gm_fCoverSearchType = CT_TURRET;
+ }
+ result = BaseClass::ValidateNavGoal();
+ gm_fCoverSearchType = CT_NORMAL;
+ return result;
+}
+
+const float AVOID_TEST_DIST = 18.0f*12.0f;
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+#define COMPANION_EPISODIC_AVOID_ENTITY_FLAME_RADIUS 18.0f
+bool CNPC_PlayerCompanion::OverrideMove( float flInterval )
+{
+ bool overrode = BaseClass::OverrideMove( flInterval );
+
+ if ( !overrode && GetNavigator()->GetGoalType() != GOALTYPE_NONE )
+ {
+ string_t iszEnvFire = AllocPooledString( "env_fire" );
+ string_t iszBounceBomb = AllocPooledString( "combine_mine" );
+
+#ifdef HL2_EPISODIC
+ string_t iszNPCTurretFloor = AllocPooledString( "npc_turret_floor" );
+ string_t iszEntityFlame = AllocPooledString( "entityflame" );
+#endif // HL2_EPISODIC
+
+ if ( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND ) )
+ {
+ CSound *pSound = GetBestSound( SOUND_DANGER );
+ if( pSound && pSound->SoundContext() == SOUND_CONTEXT_MORTAR )
+ {
+ // Try not to get any closer to the center
+ GetLocalNavigator()->AddObstacle( pSound->GetSoundOrigin(), (pSound->GetSoundOrigin() - GetAbsOrigin()).Length2D() * 0.5, AIMST_AVOID_DANGER );
+ }
+ }
+
+ CBaseEntity *pEntity = NULL;
+ trace_t tr;
+
+ // For each possible entity, compare our known interesting classnames to its classname, via ID
+ while( ( pEntity = OverrideMoveCache_FindTargetsInRadius( pEntity, GetAbsOrigin(), AVOID_TEST_DIST ) ) != NULL )
+ {
+ // Handle each type
+ if ( pEntity->m_iClassname == iszEnvFire )
+ {
+ Vector vMins, vMaxs;
+ if ( FireSystem_GetFireDamageDimensions( pEntity, &vMins, &vMaxs ) )
+ {
+ UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_FIRE_SOLID, pEntity, COLLISION_GROUP_NONE, &tr );
+ if (tr.fraction == 1.0 && !tr.startsolid)
+ {
+ GetLocalNavigator()->AddObstacle( pEntity->GetAbsOrigin(), ( ( vMaxs.x - vMins.x ) * 1.414 * 0.5 ) + 6.0, AIMST_AVOID_DANGER );
+ }
+ }
+ }
+#ifdef HL2_EPISODIC
+ else if ( pEntity->m_iClassname == iszNPCTurretFloor )
+ {
+ UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_BLOCKLOS, pEntity, COLLISION_GROUP_NONE, &tr );
+ if (tr.fraction == 1.0 && !tr.startsolid)
+ {
+ float radius = 1.4 * pEntity->CollisionProp()->BoundingRadius2D();
+ GetLocalNavigator()->AddObstacle( pEntity->WorldSpaceCenter(), radius, AIMST_AVOID_OBJECT );
+ }
+ }
+ else if( pEntity->m_iClassname == iszEntityFlame && pEntity->GetParent() && !pEntity->GetParent()->IsNPC() )
+ {
+ float flDist = pEntity->WorldSpaceCenter().DistTo( WorldSpaceCenter() );
+
+ if( flDist > COMPANION_EPISODIC_AVOID_ENTITY_FLAME_RADIUS )
+ {
+ // If I'm not in the flame, prevent me from getting close to it.
+ // If I AM in the flame, avoid placing an obstacle until the flame frightens me away from itself.
+ UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_BLOCKLOS, pEntity, COLLISION_GROUP_NONE, &tr );
+ if (tr.fraction == 1.0 && !tr.startsolid)
+ {
+ GetLocalNavigator()->AddObstacle( pEntity->WorldSpaceCenter(), COMPANION_EPISODIC_AVOID_ENTITY_FLAME_RADIUS, AIMST_AVOID_OBJECT );
+ }
+ }
+ }
+#endif // HL2_EPISODIC
+ else if ( pEntity->m_iClassname == iszBounceBomb )
+ {
+ CBounceBomb *pBomb = static_cast<CBounceBomb *>(pEntity);
+ if ( pBomb && !pBomb->IsPlayerPlaced() && pBomb->IsAwake() )
+ {
+ UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_BLOCKLOS, pEntity, COLLISION_GROUP_NONE, &tr );
+ if (tr.fraction == 1.0 && !tr.startsolid)
+ {
+ GetLocalNavigator()->AddObstacle( pEntity->GetAbsOrigin(), BOUNCEBOMB_DETONATE_RADIUS * .8, AIMST_AVOID_DANGER );
+ }
+ }
+ }
+ }
+ }
+
+ return overrode;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::MovementCost( int moveType, const Vector &vecStart, const Vector &vecEnd, float *pCost )
+{
+ bool bResult = BaseClass::MovementCost( moveType, vecStart, vecEnd, pCost );
+ if ( moveType == bits_CAP_MOVE_GROUND )
+ {
+ if ( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND ) )
+ {
+ CSound *pSound = GetBestSound( SOUND_DANGER );
+ if( pSound && (pSound->SoundContext() & (SOUND_CONTEXT_MORTAR|SOUND_CONTEXT_FROM_SNIPER)) )
+ {
+ Vector vecToSound = pSound->GetSoundReactOrigin() - GetAbsOrigin();
+ Vector vecToPosition = vecEnd - GetAbsOrigin();
+ VectorNormalize( vecToPosition );
+ VectorNormalize( vecToSound );
+
+ if ( vecToPosition.AsVector2D().Dot( vecToSound.AsVector2D() ) > 0 )
+ {
+ *pCost *= 1.5;
+ bResult = true;
+ }
+ }
+ }
+
+ if ( m_bWeightPathsInCover && GetEnemy() )
+ {
+ if ( BaseClass::IsCoverPosition( GetEnemy()->EyePosition(), vecEnd ) )
+ {
+ *pCost *= 0.1;
+ bResult = true;
+ }
+ }
+ }
+ return bResult;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+float CNPC_PlayerCompanion::GetIdealSpeed() const
+{
+ float baseSpeed = BaseClass::GetIdealSpeed();
+
+ if ( baseSpeed < m_flBoostSpeed )
+ return m_flBoostSpeed;
+
+ return baseSpeed;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+float CNPC_PlayerCompanion::GetIdealAccel() const
+{
+ float multiplier = 1.0;
+ if ( AI_IsSinglePlayer() )
+ {
+ if ( m_bMovingAwayFromPlayer && (UTIL_PlayerByIndex(1)->GetAbsOrigin() - GetAbsOrigin()).Length2DSqr() < Square(3.0*12.0) )
+ multiplier = 2.0;
+ }
+ return BaseClass::GetIdealAccel() * multiplier;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::OnObstructionPreSteer( AILocalMoveGoal_t *pMoveGoal, float distClear, AIMoveResult_t *pResult )
+{
+ if ( pMoveGoal->directTrace.flTotalDist - pMoveGoal->directTrace.flDistObstructed < GetHullWidth() * 1.5 )
+ {
+ CAI_BaseNPC *pBlocker = pMoveGoal->directTrace.pObstruction->MyNPCPointer();
+ if ( pBlocker && pBlocker->IsPlayerAlly() && !pBlocker->IsMoving() && !pBlocker->IsInAScript() &&
+ ( IsCurSchedule( SCHED_NEW_WEAPON ) ||
+ IsCurSchedule( SCHED_GET_HEALTHKIT ) ||
+ pBlocker->IsCurSchedule( SCHED_FAIL ) ||
+ ( IsInPlayerSquad() && !pBlocker->IsInPlayerSquad() ) ||
+ Classify() == CLASS_PLAYER_ALLY_VITAL ||
+ IsInAScript() ) )
+
+ {
+ if ( pBlocker->ConditionInterruptsCurSchedule( COND_GIVE_WAY ) ||
+ pBlocker->ConditionInterruptsCurSchedule( COND_PLAYER_PUSHING ) )
+ {
+ // HACKHACK
+ pBlocker->GetMotor()->SetIdealYawToTarget( WorldSpaceCenter() );
+ pBlocker->SetSchedule( SCHED_MOVE_AWAY );
+ }
+
+ }
+ }
+
+ if ( pMoveGoal->directTrace.pObstruction )
+ {
+ }
+
+ return BaseClass::OnObstructionPreSteer( pMoveGoal, distClear, pResult );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Whether or not we should always transition with the player
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldAlwaysTransition( void )
+{
+ // No matter what, come through
+ if ( m_bAlwaysTransition )
+ return true;
+
+ // Squadmates always come with
+ if ( IsInPlayerSquad() )
+ return true;
+
+ // If we're following the player, then come along
+ if ( GetFollowBehavior().GetFollowTarget() && GetFollowBehavior().GetFollowTarget()->IsPlayer() )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputOutsideTransition( inputdata_t &inputdata )
+{
+ if ( !AI_IsSinglePlayer() )
+ return;
+
+ // Must want to do this
+ if ( ShouldAlwaysTransition() == false )
+ return;
+
+ // If we're in a vehicle, that vehicle will transition with us still inside (which is preferable)
+ if ( IsInAVehicle() )
+ return;
+
+ CBaseEntity *pPlayer = UTIL_GetLocalPlayer();
+ const Vector &playerPos = pPlayer->GetAbsOrigin();
+
+ // Mark us as already having succeeded if we're vital or always meant to come with the player
+ bool bAlwaysTransition = ( ( Classify() == CLASS_PLAYER_ALLY_VITAL ) || m_bAlwaysTransition );
+ bool bPathToPlayer = bAlwaysTransition;
+
+ if ( bAlwaysTransition == false )
+ {
+ AI_Waypoint_t *pPathToPlayer = GetPathfinder()->BuildRoute( GetAbsOrigin(), playerPos, pPlayer, 0 );
+
+ if ( pPathToPlayer )
+ {
+ bPathToPlayer = true;
+ CAI_Path tempPath;
+ tempPath.SetWaypoints( pPathToPlayer ); // path object will delete waypoints
+ GetPathfinder()->UnlockRouteNodes( pPathToPlayer );
+ }
+ }
+
+
+#ifdef USE_PATHING_LENGTH_REQUIREMENT_FOR_TELEPORT
+ float pathLength = tempPath.GetPathDistanceToGoal( GetAbsOrigin() );
+
+ if ( pathLength > 150 * 12 )
+ return;
+#endif
+
+ bool bMadeIt = false;
+ Vector teleportLocation;
+
+ CAI_Hint *pHint = CAI_HintManager::FindHint( this, HINT_PLAYER_SQUAD_TRANSITON_POINT, bits_HINT_NODE_NEAREST, PLAYERCOMPANION_TRANSITION_SEARCH_DISTANCE, &playerPos );
+ while ( pHint )
+ {
+ pHint->Lock(this);
+ pHint->Unlock(0.5); // prevent other squadmates and self from using during transition.
+
+ pHint->GetPosition( GetHullType(), &teleportLocation );
+ if ( GetNavigator()->CanFitAtPosition( teleportLocation, MASK_NPCSOLID ) )
+ {
+ bMadeIt = true;
+ if ( !bPathToPlayer && ( playerPos - GetAbsOrigin() ).LengthSqr() > Square(40*12) )
+ {
+ AI_Waypoint_t *pPathToTeleport = GetPathfinder()->BuildRoute( GetAbsOrigin(), teleportLocation, pPlayer, 0 );
+
+ if ( !pPathToTeleport )
+ {
+ DevMsg( 2, "NPC \"%s\" failed to teleport to transition a point because there is no path\n", STRING(GetEntityName()) );
+ bMadeIt = false;
+ }
+ else
+ {
+ CAI_Path tempPath;
+ GetPathfinder()->UnlockRouteNodes( pPathToTeleport );
+ tempPath.SetWaypoints( pPathToTeleport ); // path object will delete waypoints
+ }
+ }
+
+ if ( bMadeIt )
+ {
+ DevMsg( 2, "NPC \"%s\" teleported to transition point %d\n", STRING(GetEntityName()), pHint->GetNodeId() );
+ break;
+ }
+ }
+ else
+ {
+ if ( g_debug_transitions.GetBool() )
+ {
+ NDebugOverlay::Box( teleportLocation, GetHullMins(), GetHullMaxs(), 255,0,0, 8, 999 );
+ }
+ }
+ pHint = CAI_HintManager::FindHint( this, HINT_PLAYER_SQUAD_TRANSITON_POINT, bits_HINT_NODE_NEAREST, PLAYERCOMPANION_TRANSITION_SEARCH_DISTANCE, &playerPos );
+ }
+ if ( !bMadeIt )
+ {
+ // Force us if we didn't find a normal route
+ if ( bAlwaysTransition )
+ {
+ bMadeIt = FindSpotForNPCInRadius( &teleportLocation, pPlayer->GetAbsOrigin(), this, 32.0*1.414, true );
+ if ( !bMadeIt )
+ bMadeIt = FindSpotForNPCInRadius( &teleportLocation, pPlayer->GetAbsOrigin(), this, 32.0*1.414, false );
+ }
+ }
+
+ if ( bMadeIt )
+ {
+ Teleport( &teleportLocation, NULL, NULL );
+ }
+ else
+ {
+ DevMsg( 2, "NPC \"%s\" failed to find a suitable transition a point\n", STRING(GetEntityName()) );
+ }
+
+ BaseClass::InputOutsideTransition( inputdata );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputSetReadinessPanic( inputdata_t &inputdata )
+{
+ SetReadinessLevel( AIRL_PANIC, true, true );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputSetReadinessStealth( inputdata_t &inputdata )
+{
+ SetReadinessLevel( AIRL_STEALTH, true, true );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputSetReadinessLow( inputdata_t &inputdata )
+{
+ SetReadinessLevel( AIRL_RELAXED, true, true );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputSetReadinessMedium( inputdata_t &inputdata )
+{
+ SetReadinessLevel( AIRL_STIMULATED, true, true );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputSetReadinessHigh( inputdata_t &inputdata )
+{
+ SetReadinessLevel( AIRL_AGITATED, true, true );
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputLockReadiness( inputdata_t &inputdata )
+{
+ float value = inputdata.value.Float();
+ LockReadiness( value );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Locks the readiness state of the NCP
+// Input : time - if -1, the lock is effectively infinite
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::LockReadiness( float duration )
+{
+ if ( duration == -1.0f )
+ {
+ m_flReadinessLockedUntil = FLT_MAX;
+ }
+ else
+ {
+ m_flReadinessLockedUntil = gpGlobals->curtime + duration;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Unlocks the readiness state
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::UnlockReadiness( void )
+{
+ // Set to the past
+ m_flReadinessLockedUntil = gpGlobals->curtime - 0.1f;
+}
+
+//------------------------------------------------------------------------------
+#ifdef HL2_EPISODIC
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ShouldDeferToPassengerBehavior( void )
+{
+ if ( m_PassengerBehavior.CanSelectSchedule() )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Determines if this player companion is capable of entering a vehicle
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::CanEnterVehicle( void )
+{
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::CanExitVehicle( void )
+{
+ // See if we can exit our vehicle
+ CPropJeepEpisodic *pVehicle = dynamic_cast<CPropJeepEpisodic *>(m_PassengerBehavior.GetTargetVehicle());
+ if ( pVehicle != NULL && pVehicle->NPC_CanExitVehicle( this, true ) == false )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *lpszVehicleName -
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::EnterVehicle( CBaseEntity *pEntityVehicle, bool bImmediately )
+{
+ // Must be allowed to do this
+ if ( CanEnterVehicle() == false )
+ return;
+
+ // Find the target vehicle
+ CPropJeepEpisodic *pVehicle = dynamic_cast<CPropJeepEpisodic *>(pEntityVehicle);
+
+ // Get in the car if it's valid
+ if ( pVehicle != NULL && pVehicle->NPC_CanEnterVehicle( this, true ) )
+ {
+ // Set her into a "passenger" behavior
+ m_PassengerBehavior.Enable( pVehicle, bImmediately );
+ m_PassengerBehavior.EnterVehicle();
+
+ // Only do this if we're outside the vehicle
+ if ( m_PassengerBehavior.GetPassengerState() == PASSENGER_STATE_OUTSIDE )
+ {
+ SetCondition( COND_PC_BECOMING_PASSENGER );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Get into the requested vehicle
+// Input : &inputdata - contains the entity name of the vehicle to enter
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputEnterVehicle( inputdata_t &inputdata )
+{
+ CBaseEntity *pEntity = FindNamedEntity( inputdata.value.String() );
+ EnterVehicle( pEntity, false );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Get into the requested vehicle immediately (no animation, pop)
+// Input : &inputdata - contains the entity name of the vehicle to enter
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputEnterVehicleImmediately( inputdata_t &inputdata )
+{
+ CBaseEntity *pEntity = FindNamedEntity( inputdata.value.String() );
+ EnterVehicle( pEntity, true );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputExitVehicle( inputdata_t &inputdata )
+{
+ // See if we're allowed to exit the vehicle
+ if ( CanExitVehicle() == false )
+ return;
+
+ m_PassengerBehavior.ExitVehicle();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputCancelEnterVehicle( inputdata_t &inputdata )
+{
+ m_PassengerBehavior.CancelEnterVehicle();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Forces the NPC out of the vehicle they're riding in
+// Input : bImmediate - If we need to exit immediately, teleport to any exit location
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::ExitVehicle( void )
+{
+ // For now just get out
+ m_PassengerBehavior.ExitVehicle();
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsInAVehicle( void ) const
+{
+ // Must be active and getting in/out of vehicle
+ if ( m_PassengerBehavior.IsEnabled() && m_PassengerBehavior.GetPassengerState() != PASSENGER_STATE_OUTSIDE )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : IServerVehicle -
+//-----------------------------------------------------------------------------
+IServerVehicle *CNPC_PlayerCompanion::GetVehicle( void )
+{
+ if ( IsInAVehicle() )
+ {
+ CPropVehicleDriveable *pDriveableVehicle = m_PassengerBehavior.GetTargetVehicle();
+ if ( pDriveableVehicle != NULL )
+ return pDriveableVehicle->GetServerVehicle();
+ }
+
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : CBaseEntity
+//-----------------------------------------------------------------------------
+CBaseEntity *CNPC_PlayerCompanion::GetVehicleEntity( void )
+{
+ if ( IsInAVehicle() )
+ {
+ CPropVehicleDriveable *pDriveableVehicle = m_PassengerBehavior.GetTargetVehicle();
+ return pDriveableVehicle;
+ }
+
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Override our efficiency so that we don't jitter when we're in the middle
+// of our enter/exit animations.
+// Input : bInPVS - Whether we're in the PVS or not
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::UpdateEfficiency( bool bInPVS )
+{
+ // If we're transitioning and in the PVS, we override our efficiency
+ if ( IsInAVehicle() && bInPVS )
+ {
+ PassengerState_e nState = m_PassengerBehavior.GetPassengerState();
+ if ( nState == PASSENGER_STATE_ENTERING || nState == PASSENGER_STATE_EXITING )
+ {
+ SetEfficiency( AIE_NORMAL );
+ return;
+ }
+ }
+
+ // Do the default behavior
+ BaseClass::UpdateEfficiency( bInPVS );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Whether or not we can dynamically interact with another NPC
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::CanRunAScriptedNPCInteraction( bool bForced /*= false*/ )
+{
+ // TODO: Allow this but only for interactions who stem from being in a vehicle?
+ if ( IsInAVehicle() )
+ return false;
+
+ return BaseClass::CanRunAScriptedNPCInteraction( bForced );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsAllowedToDodge( void )
+{
+ // TODO: Allow this but only for interactions who stem from being in a vehicle?
+ if ( IsInAVehicle() )
+ return false;
+
+ return BaseClass::IsAllowedToDodge();
+}
+
+#endif //HL2_EPISODIC
+//------------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// Purpose: Always transition along with the player
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputEnableAlwaysTransition( inputdata_t &inputdata )
+{
+ m_bAlwaysTransition = true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Stop always transitioning along with the player
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputDisableAlwaysTransition( inputdata_t &inputdata )
+{
+ m_bAlwaysTransition = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Stop picking up weapons from the ground
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputEnableWeaponPickup( inputdata_t &inputdata )
+{
+ m_bDontPickupWeapons = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return to default behavior of picking up better weapons on the ground
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputDisableWeaponPickup( inputdata_t &inputdata )
+{
+ m_bDontPickupWeapons = true;
+}
+
+//------------------------------------------------------------------------------
+// Purpose: Give the NPC in question the weapon specified
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputGiveWeapon( inputdata_t &inputdata )
+{
+ // Give the NPC the specified weapon
+ string_t iszWeaponName = inputdata.value.StringID();
+ if ( iszWeaponName != NULL_STRING )
+ {
+ if( Classify() == CLASS_PLAYER_ALLY_VITAL )
+ {
+ m_iszPendingWeapon = iszWeaponName;
+ }
+ else
+ {
+ GiveWeapon( iszWeaponName );
+ }
+ }
+}
+
+#if HL2_EPISODIC
+//------------------------------------------------------------------------------
+// Purpose: Delete all outputs from this NPC.
+//------------------------------------------------------------------------------
+void CNPC_PlayerCompanion::InputClearAllOuputs( inputdata_t &inputdata )
+{
+ datamap_t *dmap = GetDataDescMap();
+ while ( dmap )
+ {
+ int fields = dmap->dataNumFields;
+ for ( int i = 0; i < fields; i++ )
+ {
+ typedescription_t *dataDesc = &dmap->dataDesc[i];
+ if ( ( dataDesc->fieldType == FIELD_CUSTOM ) && ( dataDesc->flags & FTYPEDESC_OUTPUT ) )
+ {
+ CBaseEntityOutput *pOutput = (CBaseEntityOutput *)((int)this + (int)dataDesc->fieldOffset[0]);
+ pOutput->DeleteAllElements();
+ /*
+ int nConnections = pOutput->NumberOfElements();
+ for ( int j = 0; j < nConnections; j++ )
+ {
+
+ }
+ */
+ }
+ }
+
+ dmap = dmap->baseMap;
+ }
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose: Player in our squad killed something
+// Input : *pVictim - Who he killed
+// &info - How they died
+//-----------------------------------------------------------------------------
+void CNPC_PlayerCompanion::OnPlayerKilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info )
+{
+ // filter everything that comes in here that isn't an NPC
+ CAI_BaseNPC *pCombatVictim = dynamic_cast<CAI_BaseNPC *>( pVictim );
+ if ( !pCombatVictim )
+ {
+ return;
+ }
+
+ CBaseEntity *pInflictor = info.GetInflictor();
+ int iNumBarrels = 0;
+ int iConsecutivePlayerKills = 0;
+ bool bPuntedGrenade = false;
+ bool bVictimWasEnemy = false;
+ bool bVictimWasMob = false;
+ bool bVictimWasAttacker = false;
+ bool bHeadshot = false;
+ bool bOneShot = false;
+
+ if ( dynamic_cast<CBreakableProp *>( pInflictor ) && ( info.GetDamageType() & DMG_BLAST ) )
+ {
+ // if a barrel explodes that was initiated by the player within a few seconds of the previous one,
+ // increment a counter to keep track of how many have exploded in a row.
+ if ( gpGlobals->curtime - m_fLastBarrelExploded >= MAX_TIME_BETWEEN_BARRELS_EXPLODING )
+ {
+ m_iNumConsecutiveBarrelsExploded = 0;
+ }
+ m_iNumConsecutiveBarrelsExploded++;
+ m_fLastBarrelExploded = gpGlobals->curtime;
+
+ iNumBarrels = m_iNumConsecutiveBarrelsExploded;
+ }
+ else
+ {
+ // if player kills an NPC within a few seconds of the previous kill,
+ // increment a counter to keep track of how many he's killed in a row.
+ if ( gpGlobals->curtime - m_fLastPlayerKill >= MAX_TIME_BETWEEN_CONSECUTIVE_PLAYER_KILLS )
+ {
+ m_iNumConsecutivePlayerKills = 0;
+ }
+ m_iNumConsecutivePlayerKills++;
+ m_fLastPlayerKill = gpGlobals->curtime;
+ iConsecutivePlayerKills = m_iNumConsecutivePlayerKills;
+ }
+
+ // don't comment on kills when she can't see the victim
+ if ( !FVisible( pVictim ) )
+ {
+ return;
+ }
+
+ // check if the player killed an enemy by punting a grenade
+ if ( pInflictor && Fraggrenade_WasPunted( pInflictor ) && Fraggrenade_WasCreatedByCombine( pInflictor ) )
+ {
+ bPuntedGrenade = true;
+ }
+
+ // check if the victim was Alyx's enemy
+ if ( GetEnemy() == pVictim )
+ {
+ bVictimWasEnemy = true;
+ }
+
+ AI_EnemyInfo_t *pEMemory = GetEnemies()->Find( pVictim );
+ if ( pEMemory != NULL )
+ {
+ // was Alyx being mobbed by this enemy?
+ bVictimWasMob = pEMemory->bMobbedMe;
+
+ // has Alyx recieved damage from this enemy?
+ if ( pEMemory->timeLastReceivedDamageFrom > 0 ) {
+ bVictimWasAttacker = true;
+ }
+ }
+
+ // Was it a headshot?
+ if ( ( pCombatVictim->LastHitGroup() == HITGROUP_HEAD ) && ( info.GetDamageType() & DMG_BULLET ) )
+ {
+ bHeadshot = true;
+ }
+
+ // Did the player kill the enemy with 1 shot?
+ if ( ( pCombatVictim->GetDamageCount() == 1 ) && ( info.GetDamageType() & DMG_BULLET ) )
+ {
+ bOneShot = true;
+ }
+
+ // set up the speech modifiers
+ CFmtStrN<512> modifiers( "num_barrels:%d,distancetoplayerenemy:%f,playerAmmo:%s,consecutive_player_kills:%d,"
+ "punted_grenade:%d,victim_was_enemy:%d,victim_was_mob:%d,victim_was_attacker:%d,headshot:%d,oneshot:%d",
+ iNumBarrels, EnemyDistance( pVictim ), info.GetAmmoName(), iConsecutivePlayerKills,
+ bPuntedGrenade, bVictimWasEnemy, bVictimWasMob, bVictimWasAttacker, bHeadshot, bOneShot );
+
+ SpeakIfAllowed( TLK_PLAYER_KILLED_NPC, modifiers );
+
+ BaseClass::OnPlayerKilledOther( pVictim, info );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_PlayerCompanion::IsNavigationUrgent( void )
+{
+ bool bBase = BaseClass::IsNavigationUrgent();
+
+ // Consider follow & assault behaviour urgent
+ if ( !bBase && (m_FollowBehavior.IsActive() || ( m_AssaultBehavior.IsRunning() && m_AssaultBehavior.IsUrgent() )) && Classify() == CLASS_PLAYER_ALLY_VITAL )
+ {
+ // But only if the blocker isn't the player, and isn't a physics object that's still moving
+ CBaseEntity *pBlocker = GetNavigator()->GetBlockingEntity();
+ if ( pBlocker && !pBlocker->IsPlayer() )
+ {
+ IPhysicsObject *pPhysObject = pBlocker->VPhysicsGetObject();
+ if ( pPhysObject && !pPhysObject->IsAsleep() )
+ return false;
+ if ( pBlocker->IsNPC() )
+ return false;
+ }
+
+ // If we're within the player's viewcone, then don't teleport.
+
+ // This test was made more general because previous iterations had cases where characters
+ // could not see the player but the player could in fact see them. Now the NPC's facing is
+ // irrelevant and the player's viewcone is more authorative. -- jdw
+
+ CBasePlayer *pLocalPlayer = AI_GetSinglePlayer();
+ if ( pLocalPlayer->FInViewCone( EyePosition() ) )
+ return false;
+
+ return true;
+ }
+
+ return bBase;
+}
+
+//-----------------------------------------------------------------------------
+//
+// Schedules
+//
+//-----------------------------------------------------------------------------
+
+AI_BEGIN_CUSTOM_NPC( player_companion_base, CNPC_PlayerCompanion )
+
+ // AI Interaction for being hit by a physics object
+ DECLARE_INTERACTION(g_interactionHitByPlayerThrownPhysObj)
+ DECLARE_INTERACTION(g_interactionPlayerPuntedHeavyObject)
+
+ DECLARE_CONDITION( COND_PC_HURTBYFIRE )
+ DECLARE_CONDITION( COND_PC_SAFE_FROM_MORTAR )
+ DECLARE_CONDITION( COND_PC_BECOMING_PASSENGER )
+
+ DECLARE_TASK( TASK_PC_WAITOUT_MORTAR )
+ DECLARE_TASK( TASK_PC_GET_PATH_OFF_COMPANION )
+
+ DECLARE_ANIMEVENT( AE_COMPANION_PRODUCE_FLARE )
+ DECLARE_ANIMEVENT( AE_COMPANION_LIGHT_FLARE )
+ DECLARE_ANIMEVENT( AE_COMPANION_RELEASE_FLARE )
+
+ //=========================================================
+ // > TakeCoverFromBestSound
+ //
+ // Find cover and move towards it, but only do so for a short
+ // time. This is appropriate when the dangerous item is going
+ // to detonate very soon. This way our NPC doesn't run a great
+ // distance from an object that explodes shortly after the NPC
+ // gets underway.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_MOVE_TOWARDS_COVER_FROM_BEST_SOUND,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FLEE_FROM_BEST_SOUND"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION 0"
+ " TASK_FIND_COVER_FROM_BEST_SOUND 0"
+ " TASK_RUN_PATH_TIMED 1.0"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_SAVEPOSITION 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Translated to cover
+ ""
+ " Interrupts"
+ " COND_PC_SAFE_FROM_MORTAR"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_TAKE_COVER_FROM_BEST_SOUND,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FLEE_FROM_BEST_SOUND"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION 0"
+ " TASK_FIND_COVER_FROM_BEST_SOUND 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_SAVEPOSITION 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Translated to cover
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_PC_SAFE_FROM_MORTAR"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_COWER,
+
+ " Tasks"
+ " TASK_WAIT_RANDOM 0.1"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_COWER"
+ " TASK_PC_WAITOUT_MORTAR 0"
+ " TASK_WAIT 0.1"
+ " TASK_WAIT_RANDOM 0.5"
+ ""
+ " Interrupts"
+ " "
+ )
+
+ //=========================================================
+ //
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_FLEE_FROM_BEST_SOUND,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COWER"
+ " TASK_GET_PATH_AWAY_FROM_BEST_SOUND 600"
+ " TASK_RUN_PATH_TIMED 1.5"
+ " TASK_STOP_MOVING 0"
+ " TASK_TURN_LEFT 179"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_PC_SAFE_FROM_MORTAR"
+ )
+
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_FAIL_TAKE_COVER_TURRET,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COWER"
+ " TASK_STOP_MOVING 0"
+ " TASK_MOVE_AWAY_PATH 600"
+ " TASK_RUN_PATH_FLEE 100"
+ " TASK_STOP_MOVING 0"
+ " TASK_TURN_LEFT 179"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ )
+
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_FAKEOUT_MORTAR,
+
+ " Tasks"
+ " TASK_MOVE_AWAY_PATH 300"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ ""
+ " Interrupts"
+ " COND_HEAR_DANGER"
+ )
+
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_PC_GET_OFF_COMPANION,
+
+ " Tasks"
+ " TASK_PC_GET_PATH_OFF_COMPANION 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ ""
+ " Interrupts"
+ ""
+ )
+
+AI_END_CUSTOM_NPC()
+
+
+//
+// Special movement overrides for player companions
+//
+
+#define NUM_OVERRIDE_MOVE_CLASSNAMES 4
+
+class COverrideMoveCache : public IEntityListener
+{
+public:
+
+ void LevelInitPreEntity( void )
+ {
+ CacheClassnames();
+ gEntList.AddListenerEntity( this );
+ Clear();
+ }
+ void LevelShutdownPostEntity( void )
+ {
+ gEntList.RemoveListenerEntity( this );
+ Clear();
+ }
+
+ inline void Clear( void )
+ {
+ m_Cache.Purge();
+ }
+
+ inline bool MatchesCriteria( CBaseEntity *pEntity )
+ {
+ for ( int i = 0; i < NUM_OVERRIDE_MOVE_CLASSNAMES; i++ )
+ {
+ if ( pEntity->m_iClassname == m_Classname[i] )
+ return true;
+ }
+
+ return false;
+ }
+
+ virtual void OnEntitySpawned( CBaseEntity *pEntity )
+ {
+ if ( MatchesCriteria( pEntity ) )
+ {
+ m_Cache.AddToTail( pEntity );
+ }
+ };
+
+ virtual void OnEntityDeleted( CBaseEntity *pEntity )
+ {
+ if ( !m_Cache.Count() )
+ return;
+
+ if ( MatchesCriteria( pEntity ) )
+ {
+ m_Cache.FindAndRemove( pEntity );
+ }
+ };
+
+ CBaseEntity *FindTargetsInRadius( CBaseEntity *pFirstEntity, const Vector &vecOrigin, float flRadius )
+ {
+ if ( !m_Cache.Count() )
+ return NULL;
+
+ int nIndex = m_Cache.InvalidIndex();
+
+ // If we're starting with an entity, start there and move past it
+ if ( pFirstEntity != NULL )
+ {
+ nIndex = m_Cache.Find( pFirstEntity );
+ nIndex = m_Cache.Next( nIndex );
+ if ( nIndex == m_Cache.InvalidIndex() )
+ return NULL;
+ }
+ else
+ {
+ nIndex = m_Cache.Head();
+ }
+
+ CBaseEntity *pTarget = NULL;
+ const float flRadiusSqr = Square( flRadius );
+
+ // Look through each cached target, looking for one in our range
+ while ( nIndex != m_Cache.InvalidIndex() )
+ {
+ pTarget = m_Cache[nIndex];
+ if ( pTarget && ( pTarget->GetAbsOrigin() - vecOrigin ).LengthSqr() < flRadiusSqr )
+ return pTarget;
+
+ nIndex = m_Cache.Next( nIndex );
+ }
+
+ return NULL;
+ }
+
+ void ForceRepopulateList( void )
+ {
+ Clear();
+ CacheClassnames();
+
+ CBaseEntity *pEnt = gEntList.FirstEnt();
+ while( pEnt )
+ {
+ if( MatchesCriteria( pEnt ) )
+ {
+ m_Cache.AddToTail( pEnt );
+ }
+
+ pEnt = gEntList.NextEnt( pEnt );
+ }
+ }
+
+private:
+ inline void CacheClassnames( void )
+ {
+ m_Classname[0] = AllocPooledString( "env_fire" );
+ m_Classname[1] = AllocPooledString( "combine_mine" );
+ m_Classname[2] = AllocPooledString( "npc_turret_floor" );
+ m_Classname[3] = AllocPooledString( "entityflame" );
+ }
+
+ CUtlLinkedList<EHANDLE> m_Cache;
+ string_t m_Classname[NUM_OVERRIDE_MOVE_CLASSNAMES];
+};
+
+// Singleton for access
+COverrideMoveCache g_OverrideMoveCache;
+COverrideMoveCache *OverrideMoveCache( void ) { return &g_OverrideMoveCache; }
+
+CBaseEntity *OverrideMoveCache_FindTargetsInRadius( CBaseEntity *pFirstEntity, const Vector &vecOrigin, float flRadius )
+{
+ return g_OverrideMoveCache.FindTargetsInRadius( pFirstEntity, vecOrigin, flRadius );
+}
+
+void OverrideMoveCache_ForceRepopulateList( void )
+{
+ g_OverrideMoveCache.ForceRepopulateList();
+}
+
+void OverrideMoveCache_LevelInitPreEntity( void )
+{
+ g_OverrideMoveCache.LevelInitPreEntity();
+}
+
+void OverrideMoveCache_LevelShutdownPostEntity( void )
+{
+ g_OverrideMoveCache.LevelShutdownPostEntity();
+}
+