//========= 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 #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(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(GetEnemy())->GetEnemy() == this && assert_cast(GetEnemy())->FInViewCone( this ) && assert_cast(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(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( 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(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 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(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(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(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( 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( 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 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(); }