diff options
Diffstat (limited to 'game/server/hl2/npc_playercompanion.cpp')
| -rw-r--r-- | game/server/hl2/npc_playercompanion.cpp | 3997 |
1 files changed, 3997 insertions, 0 deletions
diff --git a/game/server/hl2/npc_playercompanion.cpp b/game/server/hl2/npc_playercompanion.cpp new file mode 100644 index 0000000..a03a752 --- /dev/null +++ b/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(); +} + |