diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/hl2/npc_alyx_episodic.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/hl2/npc_alyx_episodic.cpp')
| -rw-r--r-- | game/server/hl2/npc_alyx_episodic.cpp | 3603 |
1 files changed, 3603 insertions, 0 deletions
diff --git a/game/server/hl2/npc_alyx_episodic.cpp b/game/server/hl2/npc_alyx_episodic.cpp new file mode 100644 index 0000000..e98c89f --- /dev/null +++ b/game/server/hl2/npc_alyx_episodic.cpp @@ -0,0 +1,3603 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Alyx, the female sidekick and love interest that's taking the world by storm! +// +// Try the new Alyx Brite toothpaste! +// Alyx lederhosen! +// +// FIXME: need a better comment block +// +//=============================================================================// + +#include "cbase.h" +#include "npcevent.h" +#include "ai_basenpc.h" +#include "ai_hull.h" +#include "ai_basehumanoid.h" +#include "ai_behavior_follow.h" +#include "npc_alyx_episodic.h" +#include "npc_headcrab.h" +#include "npc_BaseZombie.h" +#include "ai_senses.h" +#include "ai_memory.h" +#include "soundent.h" +#include "props.h" +#include "IEffects.h" +#include "globalstate.h" +#include "weapon_physcannon.h" +#include "info_darknessmode_lightsource.h" +#include "sceneentity.h" +#include "hl2_gamerules.h" +#include "scripted.h" +#include "hl2_player.h" +#include "env_alyxemp_shared.h" +#include "basehlcombatweapon.h" +#include "basegrenade_shared.h" +#include "ai_interactions.h" +#include "weapon_flaregun.h" +#include "env_debughistory.h" + +extern Vector PointOnLineNearestPoint(const Vector& vStartPos, const Vector& vEndPos, const Vector& vPoint); + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +bool g_HackOutland10DamageHack; + +int ACT_ALYX_DRAW_TOOL; +int ACT_ALYX_IDLE_TOOL; +int ACT_ALYX_ZAP_TOOL; +int ACT_ALYX_HOLSTER_TOOL; +int ACT_ALYX_PICKUP_RACK; + +string_t CLASSNAME_ALYXGUN; +string_t CLASSNAME_SMG1; +string_t CLASSNAME_SHOTGUN; +string_t CLASSNAME_AR2; + +bool IsInCommentaryMode( void ); + +#define ALYX_BREATHING_VOLUME_MAX 1.0 + +#define ALYX_DARKNESS_LOST_PLAYER_DIST ( 120 * 120 ) // 12 feet + +#define ALYX_MIN_MOB_DIST_SQR Square(120) // Any enemy closer than this adds to the 'mob' +#define ALYX_MIN_CONSIDER_DIST Square(1200) // Only enemies within this range are counted and considered to generate AI speech + +#define CONCEPT_ALYX_REQUEST_ITEM "TLK_ALYX_REQUEST_ITEM" +#define CONCEPT_ALYX_INTERACTION_DONE "TLK_ALYX_INTERACTION_DONE" +#define CONCEPT_ALYX_CANCEL_INTERACTION "TLK_ALYX_CANCEL_INTERACTION" + +#define ALYX_MIN_ENEMY_DIST_TO_CROUCH 360 // Minimum distance that our enemy must be for me to crouch +#define ALYX_MIN_ENEMY_HEALTH_TO_CROUCH 15 +#define ALYX_CROUCH_DELAY 5 // Time after crouching before Alyx will crouch again + +//----------------------------------------------------------------------------- +// Interactions +//----------------------------------------------------------------------------- +extern int g_interactionZombieMeleeWarning; + +LINK_ENTITY_TO_CLASS( npc_alyx, CNPC_Alyx ); + +BEGIN_DATADESC( CNPC_Alyx ) + + DEFINE_FIELD( m_hEmpTool, FIELD_EHANDLE ), + DEFINE_FIELD( m_hHackTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hStealthLookTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_bInteractionAllowed, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fTimeNextSearchForInteractTargets, FIELD_TIME ), + DEFINE_FIELD( m_bDarknessSpeechAllowed, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsEMPHolstered, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsFlashlightBlind, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fStayBlindUntil, FIELD_TIME ), + DEFINE_FIELD( m_flDontBlindUntil, FIELD_TIME ), + DEFINE_FIELD( m_bSpokeLostPlayerInDarkness, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bPlayerFlashlightState, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bHadCondSeeEnemy, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iszCurrentBlindScene, FIELD_STRING ), + DEFINE_FIELD( m_fTimeUntilNextDarknessFoundPlayer, FIELD_TIME ), + DEFINE_FIELD( m_fCombatStartTime, FIELD_TIME ), + DEFINE_FIELD( m_fCombatEndTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextCrouchTime, FIELD_TIME ), + DEFINE_FIELD( m_WeaponType, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_bShouldHaveEMP, FIELD_BOOLEAN, "ShouldHaveEMP" ), + + DEFINE_SOUNDPATCH( m_sndDarknessBreathing ), + + DEFINE_EMBEDDED( m_SpeechWatch_LostPlayer ), + DEFINE_EMBEDDED( m_SpeechTimer_HeardSound ), + DEFINE_EMBEDDED( m_SpeechWatch_SoundDelay ), + DEFINE_EMBEDDED( m_SpeechWatch_BreathingRamp ), + DEFINE_EMBEDDED( m_SpeechWatch_FoundPlayer ), + + DEFINE_EMBEDDED( m_MoveMonitor ), + + DEFINE_INPUTFUNC( FIELD_VOID, "DisallowInteraction", InputDisallowInteraction ), + DEFINE_INPUTFUNC( FIELD_VOID, "AllowInteraction", InputAllowInteraction ), + DEFINE_INPUTFUNC( FIELD_STRING, "GiveWeapon", InputGiveWeapon ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "AllowDarknessSpeech", InputAllowDarknessSpeech ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "GiveEMP", InputGiveEMP ), + DEFINE_INPUTFUNC( FIELD_VOID, "VehiclePunted", InputVehiclePunted ), + DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ), + + DEFINE_OUTPUT( m_OnFinishInteractWithObject, "OnFinishInteractWithObject" ), + DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ), + + DEFINE_USEFUNC( Use ), + +END_DATADESC() + +#define ALYX_FEAR_ZOMBIE_DIST_SQR Square(60) +#define ALYX_FEAR_ANTLION_DIST_SQR Square(360) + +//----------------------------------------------------------------------------- +// Anim events +//----------------------------------------------------------------------------- +static int AE_ALYX_EMPTOOL_ATTACHMENT; +static int AE_ALYX_EMPTOOL_SEQUENCE; +static int AE_ALYX_EMPTOOL_USE; +static int COMBINE_AE_BEGIN_ALTFIRE; +static int COMBINE_AE_ALTFIRE; + +ConVar npc_alyx_readiness( "npc_alyx_readiness", "1" ); +ConVar npc_alyx_force_stop_moving( "npc_alyx_force_stop_moving", "1" ); +ConVar npc_alyx_readiness_transitions( "npc_alyx_readiness_transitions", "1" ); +ConVar npc_alyx_crouch( "npc_alyx_crouch", "1" ); + +// global pointer to Alyx for fast lookups +CEntityClassList<CNPC_Alyx> g_AlyxList; +template <> CNPC_Alyx *CEntityClassList<CNPC_Alyx>::m_pClassList = NULL; + +//========================================================= +// initialize Alyx before keyvalues are processed +//========================================================= +CNPC_Alyx::CNPC_Alyx() +{ + g_AlyxList.Insert(this); + // defaults to having an EMP + m_bShouldHaveEMP = true; +} + +CNPC_Alyx::~CNPC_Alyx( ) +{ + g_AlyxList.Remove(this); +} + +//========================================================= +// Classify - indicates this NPC's place in the +// relationship table. +//========================================================= +Class_T CNPC_Alyx::Classify ( void ) +{ + return CLASS_PLAYER_ALLY_VITAL; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::FValidateHintType( CAI_Hint *pHint ) +{ + switch( pHint->HintType() ) + { + case HINT_WORLD_VISUALLY_INTERESTING: + return true; + break; + case HINT_WORLD_VISUALLY_INTERESTING_STEALTH: + return true; + break; + } + + return BaseClass::FValidateHintType( pHint ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::ObjectCaps() +{ + int caps = BaseClass::ObjectCaps(); + + if( m_FuncTankBehavior.IsMounted() ) + { + caps &= ~FCAP_IMPULSE_USE; + } + + return caps; +} + +//========================================================= +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Alyx::HandleAnimEvent( animevent_t *pEvent ) +{ + if (pEvent->event == AE_ALYX_EMPTOOL_ATTACHMENT) + { + if (!m_hEmpTool) + { + // Old savegame? + CreateEmpTool(); + if (!m_hEmpTool) + return; + } + + int iAttachment = LookupAttachment( pEvent->options ); + m_hEmpTool->SetParent(this, iAttachment); + m_hEmpTool->SetLocalOrigin( Vector( 0, 0, 0 ) ); + m_hEmpTool->SetLocalAngles( QAngle( 0, 0, 0 ) ); + + if( !stricmp( pEvent->options, "Emp_Holster" ) ) + { + SetEMPHolstered(true); + } + else + { + SetEMPHolstered(false); + } + + return; + } + else if (pEvent->event == AE_ALYX_EMPTOOL_SEQUENCE) + { + if (!m_hEmpTool) + return; + + CDynamicProp *pEmpTool = dynamic_cast<CDynamicProp *>(m_hEmpTool.Get()); + + if (!pEmpTool) + return; + + int iSequence = pEmpTool->LookupSequence( pEvent->options ); + if (iSequence != ACT_INVALID) + { + pEmpTool->PropSetSequence( iSequence ); + } + + return; + } + else if (pEvent->event == AE_ALYX_EMPTOOL_USE) + { + if( m_OperatorBehavior.IsGoalReady() ) + { + if( m_OperatorBehavior.m_hContextTarget.Get() != NULL ) + { + EmpZapTarget( m_OperatorBehavior.m_hContextTarget ); + } + } + return; + } + else if ( pEvent->event == COMBINE_AE_BEGIN_ALTFIRE ) + { + EmitSound( "Weapon_CombineGuard.Special1" ); + return; + } + else if ( pEvent->event == COMBINE_AE_ALTFIRE ) + { + animevent_t fakeEvent; + + fakeEvent.pSource = this; + fakeEvent.event = EVENT_WEAPON_AR2_ALTFIRE; + GetActiveWeapon()->Operator_HandleAnimEvent( &fakeEvent, this ); + //m_iNumGrenades--; + + return; + } + + switch( pEvent->event ) + { + case 1: + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// Returns a pointer to Alyx's entity +//========================================================= +CNPC_Alyx *CNPC_Alyx::GetAlyx( void ) +{ + return g_AlyxList.m_pClassList; +} + +//========================================================= +// +//========================================================= +bool CNPC_Alyx::CreateBehaviors() +{ + AddBehavior( &m_FuncTankBehavior ); + bool result = BaseClass::CreateBehaviors(); + + return result; +} + + +//========================================================= +// Spawn +//========================================================= +void CNPC_Alyx::Spawn() +{ + BaseClass::Spawn(); + + // If Alyx has a parent, she's currently inside a pod. Prevent her from moving. + if ( GetMoveParent() ) + { + SetMoveType( MOVETYPE_NONE ); + CapabilitiesClear(); + + CapabilitiesAdd( bits_CAP_ANIMATEDFACE | bits_CAP_TURN_HEAD ); + CapabilitiesAdd( bits_CAP_FRIENDLY_DMG_IMMUNE ); + } + else + { + SetupAlyxWithoutParent(); + CreateEmpTool( ); + } + + AddEFlags( EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL | EFL_NO_PHYSCANNON_INTERACTION ); + + m_iHealth = 80; + m_bloodColor = DONT_BLEED; + + NPCInit(); + + SetUse( &CNPC_Alyx::Use ); + + m_bInteractionAllowed = true; + + m_fTimeNextSearchForInteractTargets = gpGlobals->curtime; + + SetEMPHolstered(true); + + m_bDontPickupWeapons = true; + + m_bDarknessSpeechAllowed = true; + + m_fCombatStartTime = 0.0f; + m_fCombatEndTime = 0.0f; + + m_AnnounceAttackTimer.Set( 3, 5 ); +} + +//========================================================= +// Precache - precaches all resources this NPC needs +//========================================================= +void CNPC_Alyx::Precache() +{ + BaseClass::Precache(); + PrecacheScriptSound( "npc_alyx.die" ); + PrecacheModel( STRING( GetModelName() ) ); + PrecacheModel( "models/alyx_emptool_prop.mdl" ); + + // For hacking + PrecacheScriptSound( "DoSpark" ); + PrecacheScriptSound( "npc_alyx.starthacking" ); + PrecacheScriptSound( "npc_alyx.donehacking" ); + PrecacheScriptSound( "npc_alyx.readytohack" ); + PrecacheScriptSound( "npc_alyx.interruptedhacking" ); + PrecacheScriptSound( "ep_01.al_dark_breathing01" ); + PrecacheScriptSound( "Weapon_CombineGuard.Special1" ); + + UTIL_PrecacheOther( "env_alyxemp" ); + + CLASSNAME_ALYXGUN = AllocPooledString( "weapon_alyxgun" ); + CLASSNAME_SMG1 = AllocPooledString( "weapon_smg1" ); + CLASSNAME_SHOTGUN = AllocPooledString( "weapon_shotgun" ); + CLASSNAME_AR2 = AllocPooledString( "weapon_ar2" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::Activate( void ) +{ + // Alyx always kicks her health back up to full after loading a savegame. + // Avoids problems with players saving the game in places where she dies immediately afterwards. + m_iHealth = 80; + + BaseClass::Activate(); + + // Alyx always assumes she has said hello to Gordon! + SetSpokeConcept( TLK_HELLO, NULL, false ); + + // Add my personal concepts + CAI_AllySpeechManager *pSpeechManager = GetAllySpeechManager(); + + if( pSpeechManager ) + { + ConceptInfo_t conceptRequestItem = + { + CONCEPT_ALYX_REQUEST_ITEM, SPEECH_IMPORTANT, -1, -1, -1, -1, -1, -1, AICF_TARGET_PLAYER + }; + + pSpeechManager->AddCustomConcept( conceptRequestItem ); + } + + // cleanup savegames that may not have this set + if (m_hEmpTool) + { + m_hEmpTool->AddEffects( EF_PARENT_ANIMATES ); + } + + m_WeaponType = ComputeWeaponType(); + + // !!!HACKHACK for Overwatch, If we're in ep2_outland_10, do half damage to Combine + // Be advised, this will also happen in 10a, but this is not a problem. + g_HackOutland10DamageHack = false; + if( !Q_strnicmp( STRING(gpGlobals->mapname), "ep2_outland_10", 14) ) + { + g_HackOutland10DamageHack = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::StopLoopingSounds( void ) +{ + CSoundEnvelopeController::GetController().SoundDestroy( m_sndDarknessBreathing ); + m_sndDarknessBreathing = NULL; + + BaseClass::StopLoopingSounds(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::SelectModel() +{ + // Alyx is allowed to use multiple models, because she appears in the pod. + // She defaults to her normal model. + const char *szModel = STRING( GetModelName() ); + if (!szModel || !*szModel) + { + SetModelName( AllocPooledString("models/alyx.mdl") ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::SetupAlyxWithoutParent( void ) +{ + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_DOORS_GROUP | bits_CAP_TURN_HEAD | bits_CAP_DUCK | bits_CAP_SQUAD ); + CapabilitiesAdd( bits_CAP_USE_WEAPONS ); + CapabilitiesAdd( bits_CAP_ANIMATEDFACE ); + CapabilitiesAdd( bits_CAP_FRIENDLY_DMG_IMMUNE ); + CapabilitiesAdd( bits_CAP_AIM_GUN ); + CapabilitiesAdd( bits_CAP_MOVE_SHOOT ); + CapabilitiesAdd( bits_CAP_USE_SHOT_REGULATOR ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Create and initialized Alyx's EMP tool +//----------------------------------------------------------------------------- + +void CNPC_Alyx::CreateEmpTool( void ) +{ + if (!m_bShouldHaveEMP || m_hEmpTool) + return; + + m_hEmpTool = (CBaseAnimating*)CreateEntityByName( "prop_dynamic" ); + if ( m_hEmpTool ) + { + m_hEmpTool->SetModel( "models/alyx_emptool_prop.mdl" ); + m_hEmpTool->SetName( AllocPooledString("Alyx_Emptool") ); + int iAttachment = LookupAttachment( "Emp_Holster" ); + m_hEmpTool->SetParent(this, iAttachment); + m_hEmpTool->SetOwnerEntity(this); + m_hEmpTool->SetSolid( SOLID_NONE ); + m_hEmpTool->SetLocalOrigin( Vector( 0, 0, 0 ) ); + m_hEmpTool->SetLocalAngles( QAngle( 0, 0, 0 ) ); + m_hEmpTool->AddSpawnFlags(SF_DYNAMICPROP_NO_VPHYSICS); + m_hEmpTool->AddEffects( EF_PARENT_ANIMATES ); + m_hEmpTool->Spawn(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Map input to create or destroy alyx's EMP tool +//----------------------------------------------------------------------------- + +void CNPC_Alyx::InputGiveEMP( inputdata_t &inputdata ) +{ + m_bShouldHaveEMP = inputdata.value.Bool(); + if (m_bShouldHaveEMP) + { + if (!m_hEmpTool) + { + CreateEmpTool( ); + } + } + else + { + if (m_hEmpTool) + { + UTIL_Remove( m_hEmpTool ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- + +struct ReadinessTransition_t +{ + int iPreviousLevel; + int iCurrentLevel; + Activity requiredActivity; + Activity transitionActivity; +}; + + +void CNPC_Alyx::ReadinessLevelChanged( int iPriorLevel ) +{ + BaseClass::ReadinessLevelChanged( iPriorLevel ); + + // When we drop from agitated to stimulated, stand up if we were crouching. + if ( iPriorLevel == AIRL_AGITATED && GetReadinessLevel() == AIRL_STIMULATED ) + { + //Warning("CROUCH: Standing, dropping back to stimulated.\n" ); + Stand(); + } + + if ( GetActiveWeapon() == NULL ) + return; + + //If Alyx is going from Relaxed to Agitated or Stimulated, let her raise her weapon before she's able to fire. + if ( iPriorLevel == AIRL_RELAXED && GetReadinessLevel() > iPriorLevel ) + { + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + 0.5 ); + } + + // FIXME: Are there certain animations that we DO want to interrupt? + if ( HasActiveLayer() ) + return; + + if ( npc_alyx_readiness_transitions.GetBool() ) + { + // We don't have crouching readiness transitions yet + if ( IsCrouching() ) + return; + + static ReadinessTransition_t readinessTransitions[] = + { + //Previous Readiness level - Current Readiness Level - Activity NPC needs to be playing - Gesture to play + { AIRL_RELAXED, AIRL_STIMULATED, ACT_IDLE, ACT_READINESS_RELAXED_TO_STIMULATED, }, + { AIRL_RELAXED, AIRL_STIMULATED, ACT_WALK, ACT_READINESS_RELAXED_TO_STIMULATED_WALK, }, + { AIRL_AGITATED, AIRL_STIMULATED, ACT_IDLE, ACT_READINESS_AGITATED_TO_STIMULATED, }, + { AIRL_STIMULATED, AIRL_RELAXED, ACT_IDLE, ACT_READINESS_STIMULATED_TO_RELAXED, } + }; + + for ( int i = 0; i < ARRAYSIZE( readinessTransitions ); i++ ) + { + if ( GetIdealActivity() != readinessTransitions[i].requiredActivity ) + continue; + + Activity translatedTransitionActivity = Weapon_TranslateActivity( readinessTransitions[i].transitionActivity ); + + if ( translatedTransitionActivity == ACT_INVALID || translatedTransitionActivity == readinessTransitions[i].transitionActivity ) + continue; + + Activity finalActivity = TranslateActivityReadiness( translatedTransitionActivity ); + + if ( iPriorLevel == readinessTransitions[i].iPreviousLevel && GetReadinessLevel() == readinessTransitions[i].iCurrentLevel ) + { + RestartGesture( finalActivity ); + break; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::PrescheduleThink( void ) +{ + BaseClass::PrescheduleThink(); + + // Figure out if Alyx has just been removed from her parent + if ( GetMoveType() == MOVETYPE_NONE && !GetMoveParent() ) + { + // Don't confuse the passenger behavior with just removing Alyx's parent! + if ( m_PassengerBehavior.IsEnabled() == false ) + { + SetupAlyxWithoutParent(); + SetupVPhysicsHull(); + } + } + + // If Alyx is in combat, and she doesn't have her gun out, fetch it + if ( GetState() == NPC_STATE_COMBAT && IsWeaponHolstered() && !m_FuncTankBehavior.IsRunning() ) + { + SetDesiredWeaponState( DESIREDWEAPONSTATE_UNHOLSTERED ); + } + + // If we're in stealth mode, and we can still see the stealth node, keep using it + if ( GetReadinessLevel() == AIRL_STEALTH ) + { + if ( m_hStealthLookTarget && !m_hStealthLookTarget->IsDisabled() ) + { + if ( m_hStealthLookTarget->IsInNodeFOV(this) && FVisible( m_hStealthLookTarget ) ) + return; + } + + // Break out of stealth mode + SetReadinessLevel( AIRL_STIMULATED, true, true ); + ClearLookTarget( m_hStealthLookTarget ); + m_hStealthLookTarget = NULL; + } + + // If we're being blinded by the flashlight, see if we should stop + if ( m_bIsFlashlightBlind ) + { + // we used to have a bug where if we tried to remove alyx from the blind scene before it got loaded asynchronously, + // she would get stuck in the animation with m_bIsFlashlightBlind set to false. that should be fixed, but just to + // be sure, we wait a bit to prevent this from happening. + if ( m_fStayBlindUntil < gpGlobals->curtime ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + if ( pPlayer && (!CanBeBlindedByFlashlight( true ) || !pPlayer->IsIlluminatedByFlashlight(this, NULL ) || !PlayerFlashlightOnMyEyes( pPlayer )) && + !BlindedByFlare() ) + { + // Remove the actor from the flashlight scene + ADD_DEBUG_HISTORY( HISTORY_ALYX_BLIND, UTIL_VarArgs( "(%0.2f) Alyx: end blind scene '%s'\n", gpGlobals->curtime, STRING(m_iszCurrentBlindScene) ) ); + RemoveActorFromScriptedScenes( this, true, false, STRING(m_iszCurrentBlindScene) ); + + // Allow firing again, but prevent myself from firing until I'm done + GetShotRegulator()->EnableShooting(); + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + 1.0 ); + + m_bIsFlashlightBlind = false; + m_flDontBlindUntil = gpGlobals->curtime + RandomFloat( 1, 3 ); + } + } + } + else + { + CheckBlindedByFlare(); + } +} + +//----------------------------------------------------------------------------- +// Periodically look for opportunities to interact with objects in the world. +// Right now Alyx only interacts with things the player picks up with +// physcannon. +//----------------------------------------------------------------------------- +#define ALYX_INTERACT_SEARCH_FREQUENCY 1.0f // seconds +void CNPC_Alyx::SearchForInteractTargets() +{ + if( m_fTimeNextSearchForInteractTargets > gpGlobals->curtime ) + { + return; + } + + m_fTimeNextSearchForInteractTargets = gpGlobals->curtime + ALYX_INTERACT_SEARCH_FREQUENCY; + + // Ensure player can be seen. + if( !HasCondition( COND_SEE_PLAYER) ) + { + //Msg("ALYX Can't interact: can't see player\n"); + return; + } + + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + + if( !pPlayer ) + { + return; + } + + CBaseEntity *pProspect = PhysCannonGetHeldEntity(pPlayer->GetActiveWeapon()); + + if( !pProspect ) + { + //Msg("ALYX Can't interact: player not holding anything\n"); + return; + } + + if( !IsValidInteractTarget(pProspect) ) + { + //Msg("ALYX Can't interact: player holding an invalid object\n"); + return; + } + + SetInteractTarget(pProspect); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::GatherConditions() +{ + BaseClass::GatherConditions(); + + if( HasCondition( COND_HEAR_DANGER ) ) + { + // Don't let Alyx worry about combat sounds if she's panicking + // from danger sounds. This prevents her from running ALERT_FACE_BEST_SOUND + // as soon as a grenade explodes (which makes a loud combat sound). If Alyx + // is NOT panicking over a Danger sound, she'll hear the combat sounds as normal. + ClearCondition( COND_HEAR_COMBAT ); + } + + // Update flashlight state + ClearCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ); + ClearCondition( COND_ALYX_PLAYER_TURNED_ON_FLASHLIGHT ); + ClearCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ); + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + if ( pPlayer ) + { + bool bFlashlightState = pPlayer->FlashlightIsOn() != 0; + if ( bFlashlightState != m_bPlayerFlashlightState ) + { + if ( bFlashlightState ) + { + SetCondition( COND_ALYX_PLAYER_TURNED_ON_FLASHLIGHT ); + } + else + { + // If the power level is low, consider it expired, due + // to it running out or the player turning it off in anticipation. + CHL2_Player *pHLPlayer = assert_cast<CHL2_Player*>( pPlayer ); + if ( pHLPlayer->SuitPower_GetCurrentPercentage() < 15 ) + { + SetCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ); + } + else + { + SetCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ); + } + } + + m_bPlayerFlashlightState = bFlashlightState; + } + } + + + if ( m_NPCState == NPC_STATE_COMBAT ) + { + DoCustomCombatAI(); + } + + if( HasInteractTarget() ) + { + // Check that any current interact target is still valid. + if( !IsValidInteractTarget(GetInteractTarget()) ) + { + SetInteractTarget(NULL); + } + } + + // This is not an else...if because the code above could have started + // with an interact target and ended without one. + if( !HasInteractTarget() ) + { + SearchForInteractTargets(); + } + + // Set up our interact conditions. + if( HasInteractTarget() ) + { + if( CanInteractWithTarget(GetInteractTarget()) ) + { + SetCondition(COND_ALYX_CAN_INTERACT_WITH_TARGET); + ClearCondition(COND_ALYX_CAN_NOT_INTERACT_WITH_TARGET); + } + else + { + SetCondition(COND_ALYX_CAN_NOT_INTERACT_WITH_TARGET); + ClearCondition(COND_ALYX_CAN_INTERACT_WITH_TARGET); + } + + SetCondition( COND_ALYX_HAS_INTERACT_TARGET ); + ClearCondition( COND_ALYX_NO_INTERACT_TARGET ); + } + else + { + SetCondition( COND_ALYX_NO_INTERACT_TARGET ); + ClearCondition( COND_ALYX_HAS_INTERACT_TARGET ); + } + + // Check for explosions! + if( HasCondition(COND_HEAR_COMBAT) ) + { + CSound *pSound = GetBestSound(); + + if ( IsInAVehicle() == false ) // For now, don't do these animations while in the vehicle + { + if( (pSound->SoundTypeNoContext() & SOUND_COMBAT) && (pSound->SoundContext() & SOUND_CONTEXT_EXPLOSION) ) + { + if ( HasShotgun() ) + { + if ( !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST_SHOTGUN) && !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST_DAMAGED_SHOTGUN) ) + { + RestartGesture( ACT_GESTURE_FLINCH_BLAST_SHOTGUN ); + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + SequenceDuration( ACT_GESTURE_FLINCH_BLAST_SHOTGUN ) + 0.5f ); // Allow another second for Alyx to bring her weapon to bear after the flinch. + } + } + else + { + if ( !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST) && !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST_DAMAGED) ) + { + RestartGesture( ACT_GESTURE_FLINCH_BLAST ); + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + SequenceDuration( ACT_GESTURE_FLINCH_BLAST ) + 0.5f ); // Allow another second for Alyx to bring her weapon to bear after the flinch. + } + } + } + } + } + + // ROBIN: This was here to solve a problem in a playtest. We've since found what we think was the cause. + // It's a useful piece of debug to have lying there, so I've left it in. + if ( (GetFlags() & FL_FLY) && m_NPCState != NPC_STATE_SCRIPT && !m_ActBusyBehavior.IsActive() && !m_PassengerBehavior.IsEnabled() ) + { + Warning( "Removed FL_FLY from Alyx, who wasn't running a script or actbusy. Time %.2f, map %s.\n", gpGlobals->curtime, STRING(gpGlobals->mapname) ); + RemoveFlag( FL_FLY ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::ShouldPlayerAvoid( void ) +{ + if( IsCurSchedule(SCHED_ALYX_NEW_WEAPON, false) ) + return true; + +#if 1 + if( IsCurSchedule( SCHED_PC_GET_OFF_COMPANION, false) ) + { + CBaseEntity *pGroundEnt = GetGroundEntity(); + if( pGroundEnt != NULL && pGroundEnt->IsPlayer() ) + { + if( GetAbsOrigin().z < pGroundEnt->EyePosition().z ) + return true; + } + } +#endif + return BaseClass::ShouldPlayerAvoid(); +} + +//----------------------------------------------------------------------------- +// Just heard a gunfire sound. Try to figure out how much we should know +// about it. +//----------------------------------------------------------------------------- +void CNPC_Alyx::AnalyzeGunfireSound( CSound *pSound ) +{ + Assert( pSound != NULL ); + + if( GetState() != NPC_STATE_ALERT && GetState() != NPC_STATE_IDLE ) + { + // Only have code for IDLE and ALERT now. + return; + } + + // Have to verify a bunch of stuff about the sound. It must have a valid BaseCombatCharacter as the owner, + // must have a valid target, and we need a valid pointer to the player. + if( pSound->m_hOwner.Get() == NULL ) + return; + + if( pSound->m_hTarget.Get() == NULL ) + return; + + CBaseCombatCharacter *pSoundOriginBCC = pSound->m_hOwner->MyCombatCharacterPointer(); + if( pSoundOriginBCC == NULL ) + return; + + CBaseEntity *pSoundTarget = pSound->m_hTarget.Get(); + + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + + Assert( pPlayer != NULL ); + + if( pSoundTarget == this ) + { + // The shooter is firing at me. Assume if Alyx can hear the gunfire, she can deduce its origin. + UpdateEnemyMemory( pSoundOriginBCC, pSoundOriginBCC->GetAbsOrigin(), this ); + } + else if( pSoundTarget == pPlayer ) + { + // The shooter is firing at the player. Assume Alyx can deduce the origin if the player COULD see the origin, and Alyx COULD see the player. + if( pPlayer->FVisible(pSoundOriginBCC) && FVisible(pPlayer) ) + { + UpdateEnemyMemory( pSoundOriginBCC, pSoundOriginBCC->GetAbsOrigin(), this ); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsValidEnemy( CBaseEntity *pEnemy ) +{ + if ( HL2GameRules()->IsAlyxInDarknessMode() ) + { + if ( !CanSeeEntityInDarkness( pEnemy ) ) + return false; + } + + // Alyx can only take a stalker as her enemy which is angry at the player or her. + if ( pEnemy->Classify() == CLASS_STALKER ) + { + if( !pEnemy->GetEnemy() ) + { + return false; + } + + if( pEnemy->GetEnemy() != this && !pEnemy->GetEnemy()->IsPlayer() ) + { + return false; + } + } + + if ( m_AssaultBehavior.IsRunning() && IsTurret( pEnemy ) ) + { + CBaseCombatCharacter *pBCC = dynamic_cast<CBaseCombatCharacter*>(pEnemy); + + if ( pBCC != NULL && !pBCC->FInViewCone(this) ) + { + // Don't let turrets that can't shoot me distract me from my assault behavior. + // This fixes a very specific problem that appeared in Episode 2 map ep2_outland_09 + // Where Alyx wouldn't terminate an assault while standing on an assault point because + // she was afraid of a turret that was visible from the assault point, but facing the + // other direction and thus not a threat. + return false; + } + } + + return BaseClass::IsValidEnemy(pEnemy); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::Event_Killed( const CTakeDamageInfo &info ) +{ + // Destroy our EMP tool since it won't follow us onto the ragdoll anyway + if ( m_hEmpTool != NULL ) + { + UTIL_Remove( m_hEmpTool ); + } + + BaseClass::Event_Killed( info ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + // comment on killing npc's + if ( pVictim->IsNPC() ) + { + SpeakIfAllowed( TLK_ALYX_ENEMY_DEAD ); + } + + // Alyx builds a proxy for the dead enemy so she has something to shoot at for a short time after + // the enemy ragdolls. + if( !(pVictim->GetFlags() & FL_ONGROUND) || pVictim->GetMoveType() != MOVETYPE_STEP ) + { + // Don't fire up in the air, since the dead enemy will have fallen. + return; + } + + if( pVictim->GetAbsOrigin().DistTo(GetAbsOrigin()) < 96.0f ) + { + // Don't shoot at an enemy corpse that dies very near to me. This will prevent Alyx attacking + // Other nearby enemies. + return; + } + + if( !HasShotgun() ) + { + CAI_BaseNPC *pTarget = CreateCustomTarget( pVictim->GetAbsOrigin(), 2.0f ); + + AddEntityRelationship( pTarget, IRelationType(pVictim), IRelationPriority(pVictim) ); + + // Update or Create a memory entry for this target and make Alyx think she's seen this target recently. + // This prevents the baseclass from not recognizing this target and forcing Alyx into + // SCHED_WAKE_ANGRY, which wastes time and causes her to change animation sequences rapidly. + GetEnemies()->UpdateMemory( GetNavigator()->GetNetwork(), pTarget, pTarget->GetAbsOrigin(), 0.0f, true ); + AI_EnemyInfo_t *pMemory = GetEnemies()->Find( pTarget ); + + if( pMemory ) + { + // Pretend we've known about this target longer than we really have. + pMemory->timeFirstSeen = gpGlobals->curtime - 10.0f; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called by enemy NPC's when they are ignited +// Input : pVictim - entity that was ignited +//----------------------------------------------------------------------------- +void CNPC_Alyx::EnemyIgnited( CAI_BaseNPC *pVictim ) +{ + if ( FVisible( pVictim ) ) + { + SpeakIfAllowed( TLK_ENEMY_BURNING ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called by combine balls when they're socketed +// Input : pVictim - entity killed by player +//----------------------------------------------------------------------------- +void CNPC_Alyx::CombineBallSocketed( int iNumBounces ) +{ + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + + if ( !pPlayer || !FVisible(pPlayer) ) + { + return; + } + + // set up the speech modifiers + CFmtStrN<128> modifiers( "num_bounces:%d", iNumBounces ); + + // fire off a ball socketed concept + SpeakIfAllowed( TLK_BALLSOCKETED, modifiers ); +} + +//----------------------------------------------------------------------------- +// Purpose: If we're a passenger in a vehicle +//----------------------------------------------------------------------------- +bool CNPC_Alyx::RunningPassengerBehavior( void ) +{ + // Must be active and not outside the vehicle + if ( m_PassengerBehavior.IsRunning() && m_PassengerBehavior.GetPassengerState() != PASSENGER_STATE_OUTSIDE ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Handle "mobbed" combat condition when Alyx is overwhelmed by force +//----------------------------------------------------------------------------- +void CNPC_Alyx::DoMobbedCombatAI( void ) +{ + AIEnemiesIter_t iter; + + float visibleEnemiesScore = 0.0f; + float closeEnemiesScore = 0.0f; + + for ( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) + { + if ( IRelationType( pEMemory->hEnemy ) != D_NU && IRelationType( pEMemory->hEnemy ) != D_LI && pEMemory->hEnemy->GetAbsOrigin().DistToSqr(GetAbsOrigin()) <= ALYX_MIN_CONSIDER_DIST ) + { + if( pEMemory->hEnemy && pEMemory->hEnemy->IsAlive() && gpGlobals->curtime - pEMemory->timeLastSeen <= 0.5f && pEMemory->hEnemy->Classify() != CLASS_BULLSEYE ) + { + if( pEMemory->hEnemy->GetAbsOrigin().DistToSqr(GetAbsOrigin()) <= ALYX_MIN_MOB_DIST_SQR ) + { + closeEnemiesScore += 1.0f; + } + else + { + visibleEnemiesScore += 1.0f; + } + } + } + } + + if( closeEnemiesScore > 2 ) + { + SetCondition( COND_MOBBED_BY_ENEMIES ); + + // mark anyone in the mob as having mobbed me + for ( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) + { + if ( pEMemory->bMobbedMe ) + continue; + + if ( IRelationType( pEMemory->hEnemy ) != D_NU && IRelationType( pEMemory->hEnemy ) != D_LI && pEMemory->hEnemy->GetAbsOrigin().DistToSqr(GetAbsOrigin()) <= ALYX_MIN_CONSIDER_DIST ) + { + if( pEMemory->hEnemy && pEMemory->hEnemy->IsAlive() && gpGlobals->curtime - pEMemory->timeLastSeen <= 0.5f && pEMemory->hEnemy->Classify() != CLASS_BULLSEYE ) + { + if( pEMemory->hEnemy->GetAbsOrigin().DistToSqr(GetAbsOrigin()) <= ALYX_MIN_MOB_DIST_SQR ) + { + pEMemory->bMobbedMe = true; + } + } + } + } + } + else + { + ClearCondition( COND_MOBBED_BY_ENEMIES ); + } + + // Alyx's gun can never run out of ammo. Allow Alyx to ignore LOW AMMO warnings + // if she's in a close quarters fight with several enemies. She'll attempt to reload + // as soon as her combat situation is less pressing. + if( HasCondition( COND_MOBBED_BY_ENEMIES ) ) + { + ClearCondition( COND_LOW_PRIMARY_AMMO ); + } + + // Say a combat thing + if( HasCondition( COND_MOBBED_BY_ENEMIES ) ) + { + SpeakIfAllowed( TLK_MOBBED ); + } + else if( visibleEnemiesScore > 4 ) + { + SpeakIfAllowed( TLK_MANY_ENEMIES ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Custom AI for Alyx while in combat +//----------------------------------------------------------------------------- +void CNPC_Alyx::DoCustomCombatAI( void ) +{ + // Only run the following code if we're not in a vehicle + if ( RunningPassengerBehavior() == false ) + { + // Do our mobbed by enemies logic + DoMobbedCombatAI(); + } + + CBaseEntity *pEnemy = GetEnemy(); + + if( HasCondition( COND_LOW_PRIMARY_AMMO ) ) + { + if( pEnemy ) + { + if( GetAbsOrigin().DistToSqr( pEnemy->GetAbsOrigin() ) < Square( 60.0f ) ) + { + // Don't reload if an enemy is right in my face. + ClearCondition( COND_LOW_PRIMARY_AMMO ); + } + } + } + + if ( HasCondition( COND_LIGHT_DAMAGE ) ) + { + if ( pEnemy && !IsCrouching() ) + { + // If my enemy is shooting at me from a distance, crouch for protection + if ( EnemyDistance( pEnemy ) > ALYX_MIN_ENEMY_DIST_TO_CROUCH ) + { + DesireCrouch(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::DoCustomSpeechAI( void ) +{ + BaseClass::DoCustomSpeechAI(); + + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + + if ( HasCondition(COND_NEW_ENEMY) && GetEnemy() ) + { + if ( GetEnemy()->Classify() == CLASS_HEADCRAB ) + { + CBaseHeadcrab *pHC = assert_cast<CBaseHeadcrab*>(GetEnemy()); + // If we see a headcrab for the first time as he's jumping at me, freak out! + if ( ( GetEnemy()->GetEnemy() == this ) && pHC->IsJumping() && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 0.5 ) + { + SpeakIfAllowed( "TLK_SPOTTED_INCOMING_HEADCRAB" ); + } + // If we see a headcrab leaving a zombie that just died, mention it + else if ( pHC->GetOwnerEntity() && ( pHC->GetOwnerEntity()->Classify() == CLASS_ZOMBIE ) && !pHC->GetOwnerEntity()->IsAlive() ) + { + SpeakIfAllowed( "TLK_SPOTTED_HEADCRAB_LEAVING_ZOMBIE" ); + } + } + else if ( GetEnemy()->Classify() == CLASS_ZOMBIE ) + { + CNPC_BaseZombie *pZombie = assert_cast<CNPC_BaseZombie*>(GetEnemy()); + // If we see a zombie getting up, mention it + if ( pZombie->IsGettingUp() ) + { + SpeakIfAllowed( "TLK_SPOTTED_ZOMBIE_WAKEUP" ); + } + } + } + + // Darkness mode speech + ClearCondition( COND_ALYX_IN_DARK ); + if ( HL2GameRules()->IsAlyxInDarknessMode() ) + { + // Even though the darkness light system will take flares into account when Alyx + // says she's lost the player in the darkness, players still think she's silly + // when they're too far from the flare to be seen. + // So, check for lit flares or other dynamic lights, and don't do + // a bunch of the darkness speech if there's a lit flare nearby. + bool bNearbyFlare = DarknessLightSourceWithinRadius( this, 500 ); + if ( !bNearbyFlare ) + { + SetCondition( COND_ALYX_IN_DARK ); + if ( HasCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ) || HasCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ) ) + { + // Player just turned off the flashlight. Start ramping up Alyx's breathing. + if ( !m_sndDarknessBreathing ) + { + CPASAttenuationFilter filter( this ); + m_sndDarknessBreathing = CSoundEnvelopeController::GetController().SoundCreate( filter, entindex(), CHAN_STATIC, + "ep_01.al_dark_breathing01", SNDLVL_TALKING ); + CSoundEnvelopeController::GetController().Play( m_sndDarknessBreathing, 0.0f, PITCH_NORM ); + } + + if ( m_sndDarknessBreathing ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_sndDarknessBreathing, ALYX_BREATHING_VOLUME_MAX, RandomFloat(10,20) ); + m_SpeechWatch_BreathingRamp.Stop(); + } + } + } + + // If we lose an enemy due to the flashlight, comment about it + if ( !HasCondition( COND_SEE_ENEMY ) && m_bHadCondSeeEnemy && !HasCondition( COND_TALKER_PLAYER_DEAD ) ) + { + if ( m_bDarknessSpeechAllowed && HasCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ) && + GetEnemy() && ( GetEnemy()->Classify() != CLASS_BULLSEYE ) ) + { + SpeakIfAllowed( "TLK_DARKNESS_LOSTENEMY_BY_FLASHLIGHT" ); + } + else if ( m_bDarknessSpeechAllowed && HasCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ) && + GetEnemy() && ( GetEnemy()->Classify() != CLASS_BULLSEYE ) ) + { + SpeakIfAllowed( "TLK_DARKNESS_LOSTENEMY_BY_FLASHLIGHT_EXPIRED" ); + } + else if ( m_bDarknessSpeechAllowed && GetEnemy() && ( GetEnemy()->Classify() != CLASS_BULLSEYE ) && + pPlayer && pPlayer->FlashlightIsOn() && !pPlayer->IsIlluminatedByFlashlight(GetEnemy(), NULL ) && + FVisible( GetEnemy() ) ) + { + SpeakIfAllowed( TLK_DARKNESS_ENEMY_IN_DARKNESS ); + } + m_bHadCondSeeEnemy = false; + } + else if ( HasCondition( COND_SEE_ENEMY ) ) + { + m_bHadCondSeeEnemy = true; + } + else if ( ( !GetEnemy() || ( GetEnemy()->Classify() == CLASS_BULLSEYE ) ) && m_bDarknessSpeechAllowed ) + { + if ( HasCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ) ) + { + SpeakIfAllowed( TLK_DARKNESS_FLASHLIGHT_EXPIRED ); + } + else if ( HasCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ) ) + { + SpeakIfAllowed( TLK_FLASHLIGHT_OFF ); + } + else if ( HasCondition( COND_ALYX_PLAYER_TURNED_ON_FLASHLIGHT ) ) + { + SpeakIfAllowed( TLK_FLASHLIGHT_ON ); + } + } + + // If we've just seen a new enemy, and it's illuminated by the flashlight, + // tell the player to keep the flashlight on 'em. + if ( HasCondition(COND_NEW_ENEMY) && !HasCondition( COND_TALKER_PLAYER_DEAD ) ) + { + // First time we've seen this guy? + if ( gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 0.5 ) + { + if ( pPlayer && pPlayer->IsIlluminatedByFlashlight(GetEnemy(), NULL ) && m_bDarknessSpeechAllowed && + !LookerCouldSeeTargetInDarkness( this, GetEnemy() ) ) + { + SpeakIfAllowed( "TLK_DARKNESS_FOUNDENEMY_BY_FLASHLIGHT" ); + } + } + } + + // When we lose the player, start lost-player talker after some time + if ( !bNearbyFlare && m_bDarknessSpeechAllowed ) + { + if ( !HasCondition(COND_SEE_PLAYER) && !m_SpeechWatch_LostPlayer.IsRunning() ) + { + m_SpeechWatch_LostPlayer.Set( 5,8 ); + m_SpeechWatch_LostPlayer.Start(); + m_MoveMonitor.SetMark( AI_GetSinglePlayer(), 48 ); + } + else if ( m_SpeechWatch_LostPlayer.Expired() ) + { + // Can't see the player? + if ( !HasCondition(COND_SEE_PLAYER) && !HasCondition( COND_TALKER_PLAYER_DEAD ) && !HasCondition( COND_SEE_ENEMY ) && + ( !pPlayer || pPlayer->GetAbsOrigin().DistToSqr(GetAbsOrigin()) > ALYX_DARKNESS_LOST_PLAYER_DIST ) ) + { + // only speak if player hasn't moved. + if ( m_MoveMonitor.TargetMoved( AI_GetSinglePlayer() ) ) + { + SpeakIfAllowed( "TLK_DARKNESS_LOSTPLAYER" ); + m_SpeechWatch_LostPlayer.Set(10); + m_SpeechWatch_LostPlayer.Start(); + m_bSpokeLostPlayerInDarkness = true; + } + } + } + + // Speech concepts that only occur when the player's flashlight is off + if ( pPlayer && !HasCondition( COND_TALKER_PLAYER_DEAD ) && !pPlayer->FlashlightIsOn() ) + { + // When the player first turns off the light, don't talk about sounds for a bit + if ( HasCondition( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ) || HasCondition( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ) ) + { + m_SpeechTimer_HeardSound.Set(4); + } + else if ( m_SpeechWatch_SoundDelay.Expired() ) + { + // We've waited for a bit after the sound, now talk about it + SpeakIfAllowed( "TLK_DARKNESS_HEARDSOUND" ); + m_SpeechWatch_SoundDelay.Stop(); + } + else if ( HasCondition( COND_HEAR_SPOOKY ) ) + { + // If we hear anything while the player's flashlight is off, randomly mention it + if ( m_SpeechTimer_HeardSound.Expired() ) + { + m_SpeechTimer_HeardSound.Set(10); + + // Wait for the sound to play for a bit before speaking about it + m_SpeechWatch_SoundDelay.Set( 1.0,3.0 ); + m_SpeechWatch_SoundDelay.Start(); + } + } + } + } + + // Stop the heard sound response if the player turns the flashlight on + if ( bNearbyFlare || HasCondition( COND_ALYX_PLAYER_TURNED_ON_FLASHLIGHT ) ) + { + m_SpeechWatch_SoundDelay.Stop(); + + if ( m_sndDarknessBreathing ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_sndDarknessBreathing, 0.0f, 0.5 ); + m_SpeechWatch_BreathingRamp.Stop(); + } + } + } + else + { + if ( m_sndDarknessBreathing ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_sndDarknessBreathing, 0.0f, 0.5 ); + m_SpeechWatch_BreathingRamp.Stop(); + } + + if ( !HasCondition(COND_SEE_PLAYER) && !m_SpeechWatch_FoundPlayer.IsRunning() ) + { + // wait a minute before saying something when alyx sees him again + m_SpeechWatch_FoundPlayer.Set( 60, 75 ); + m_SpeechWatch_FoundPlayer.Start(); + } + else if ( HasCondition(COND_SEE_PLAYER) ) + { + if ( m_SpeechWatch_FoundPlayer.Expired() && m_bDarknessSpeechAllowed ) + { + SpeakIfAllowed( "TLK_FOUNDPLAYER" ); + } + m_SpeechWatch_FoundPlayer.Stop(); + } + } + + // If we spoke lost-player, and now we see him/her, say so + if ( m_bSpokeLostPlayerInDarkness ) + { + // If we've left darkness mode, or if the player has blinded me with + // the flashlight, don't bother speaking the found player line. + if ( !m_bIsFlashlightBlind && HL2GameRules()->IsAlyxInDarknessMode() && m_bDarknessSpeechAllowed ) + { + if ( HasCondition(COND_SEE_PLAYER) && !HasCondition( COND_TALKER_PLAYER_DEAD ) ) + { + if ( ( m_fTimeUntilNextDarknessFoundPlayer == AI_INVALID_TIME ) || ( gpGlobals->curtime < m_fTimeUntilNextDarknessFoundPlayer ) ) + { + SpeakIfAllowed( "TLK_DARKNESS_FOUNDPLAYER" ); + } + m_bSpokeLostPlayerInDarkness = false; + } + } + else + { + m_bSpokeLostPlayerInDarkness = false; + } + } + + + if ( ( !m_bDarknessSpeechAllowed || HasCondition(COND_SEE_PLAYER) ) && m_SpeechWatch_LostPlayer.IsRunning() ) + { + m_SpeechWatch_LostPlayer.Stop(); + m_MoveMonitor.ClearMark(); + } + + // Ramp the breathing back up after speaking + if ( m_SpeechWatch_BreathingRamp.IsRunning() ) + { + if ( m_SpeechWatch_BreathingRamp.Expired() ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_sndDarknessBreathing, ALYX_BREATHING_VOLUME_MAX, RandomFloat(5,10) ); + m_SpeechWatch_BreathingRamp.Stop(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::SpeakIfAllowed( AIConcept_t concept, const char *modifiers /*= NULL*/, bool bRespondingToPlayer /*= false*/, char *pszOutResponseChosen /*= NULL*/, size_t bufsize /* = 0 */ ) +{ + if ( BaseClass::SpeakIfAllowed( concept, modifiers, bRespondingToPlayer, pszOutResponseChosen, bufsize ) ) + { + // If we're breathing in the darkness, drop the volume quickly + if ( m_sndDarknessBreathing && CSoundEnvelopeController::GetController().SoundGetVolume( m_sndDarknessBreathing ) > 0.0 ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_sndDarknessBreathing, 0.0f, 0.1 ); + + // Ramp up the sound again after the response is over + float flDelay = (GetTimeSpeechComplete() - gpGlobals->curtime); + m_SpeechWatch_BreathingRamp.Set( flDelay ); + m_SpeechWatch_BreathingRamp.Start(); + } + + return true; + } + + return false; +} + +extern int ACT_ANTLION_FLIP; +extern int ACT_ANTLION_ZAP_FLIP; + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Disposition_t CNPC_Alyx::IRelationType( CBaseEntity *pTarget ) +{ + Disposition_t disposition = BaseClass::IRelationType( pTarget ); + + if ( pTarget == NULL ) + return disposition; + + if( pTarget->Classify() == CLASS_ANTLION ) + { + if( disposition == D_HT ) + { + // If Alyx hates this antlion (default relationship), make her fear it, if it is very close. + if( GetAbsOrigin().DistToSqr(pTarget->GetAbsOrigin()) < ALYX_FEAR_ANTLION_DIST_SQR ) + { + disposition = D_FR; + } + + // Fall through... + } + } + else if( pTarget->Classify() == CLASS_ZOMBIE && disposition == D_HT && GetActiveWeapon() ) + { + if( GetAbsOrigin().DistToSqr(pTarget->GetAbsOrigin()) < ALYX_FEAR_ZOMBIE_DIST_SQR ) + { + // Be afraid of a zombie that's near if I'm not allowed to dodge. This will make Alyx back away. + return D_FR; + } + } + else if ( pTarget->Classify() == CLASS_MISSILE ) + { + // Fire at missiles while in the vehicle + if ( IsInAVehicle() ) + return D_HT; + } + + return disposition; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::IRelationPriority( CBaseEntity *pTarget ) +{ + int priority = BaseClass::IRelationPriority( pTarget ); + + if( pTarget->Classify() == CLASS_ANTLION ) + { + // Make Alyx prefer Antlions that are flipped onto their backs. + // UNLESS she has a different enemy that could melee attack her while her back is turned. + CAI_BaseNPC *pNPC = pTarget->MyNPCPointer(); + if ( pNPC && ( pNPC->GetActivity() == ACT_ANTLION_FLIP || pNPC->GetActivity() == ACT_ANTLION_ZAP_FLIP ) ) + { + if( GetEnemy() && GetEnemy() != pTarget ) + { + // I have an enemy that is not this thing. If that enemy is near, I shouldn't + // become distracted. + if( GetAbsOrigin().DistToSqr(GetEnemy()->GetAbsOrigin()) < Square(180) ) + { + return priority; + } + } + + priority += 1; + } + } + + return priority; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +#define ALYX_360_VIEW_DIST_SQR 129600 // 30 feet +bool CNPC_Alyx::FInViewCone( CBaseEntity *pEntity ) +{ + // Alyx can see 360 degrees but only at limited distance. This allows her to be aware of a + // large mob of enemies (usually antlions or zombies) closing in. This situation is so obvious to the + // player that it doesn't make sense for Alyx to be unaware of the entire group simply because she + // hasn't seen all of the enemies with her own eyes. + if( ( pEntity->IsNPC() || pEntity->IsPlayer() ) && pEntity->GetAbsOrigin().DistToSqr(GetAbsOrigin()) <= ALYX_360_VIEW_DIST_SQR ) + { + // Only see players and NPC's with 360 cone + // For instance, DON'T tell the eyeball/head tracking code that you can see an object that is behind you! + return true; + } + + // Else, fall through... + if ( HL2GameRules()->IsAlyxInDarknessMode() ) + { + if ( CanSeeEntityInDarkness( pEntity ) ) + return true; + } + + return BaseClass::FInViewCone( pEntity ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +//----------------------------------------------------------------------------- +bool CNPC_Alyx::CanSeeEntityInDarkness( CBaseEntity *pEntity ) +{ + /* + // Alyx can see enemies that are right next to her + // Robin: Disabled, made her too effective, you could safely leave her alone. + if ( pEntity->IsNPC() ) + { + if ( (pEntity->WorldSpaceCenter() - EyePosition()).LengthSqr() < (80*80) ) + return true; + } + */ + + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + if ( pPlayer && pEntity != pPlayer ) + { + if ( pPlayer->IsIlluminatedByFlashlight(pEntity, NULL ) ) + return true; + } + + return LookerCouldSeeTargetInDarkness( this, pEntity ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC) +{ + if ( HL2GameRules()->IsAlyxInDarknessMode() ) + { + if ( !CanSeeEntityInDarkness( pEntity ) ) + return false; + } + + return BaseClass::QuerySeeEntity(pEntity, bOnlyHateOrFearIfNPC); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsCoverPosition( const Vector &vecThreat, const Vector &vecPosition ) +{ + return BaseClass::IsCoverPosition( vecThreat, vecPosition ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Activity CNPC_Alyx::NPC_TranslateActivity( Activity activity ) +{ + activity = BaseClass::NPC_TranslateActivity( activity ); + + if ( activity == ACT_RUN && GetEnemy() && GetEnemy()->Classify() == CLASS_COMBINE_GUNSHIP ) + { + // Always cower from gunship! + if ( HaveSequenceForActivity( ACT_RUN_PROTECTED ) ) + activity = ACT_RUN_PROTECTED; + } + + switch ( activity ) + { + // !!!HACK - Alyx doesn't have the required animations for shotguns, + // so trick her into using the rifle counterparts for now (sjb) + case ACT_RUN_AIM_SHOTGUN: return ACT_RUN_AIM_RIFLE; + case ACT_WALK_AIM_SHOTGUN: return ACT_WALK_AIM_RIFLE; + case ACT_IDLE_ANGRY_SHOTGUN: return ACT_IDLE_ANGRY_SMG1; + case ACT_RANGE_ATTACK_SHOTGUN_LOW: return ACT_RANGE_ATTACK_SMG1_LOW; + + case ACT_PICKUP_RACK: return (Activity)ACT_ALYX_PICKUP_RACK; + case ACT_DROP_WEAPON: if ( HasShotgun() ) return (Activity)ACT_DROP_WEAPON_SHOTGUN; + } + + return activity; +} + +bool CNPC_Alyx::ShouldDeferToFollowBehavior() +{ + return BaseClass::ShouldDeferToFollowBehavior(); +} + +void CNPC_Alyx::BuildScheduleTestBits() +{ + bool bIsInteracting = false; + + bIsInteracting = ( IsCurSchedule(SCHED_ALYX_PREPARE_TO_INTERACT_WITH_TARGET, false) || + IsCurSchedule(SCHED_ALYX_WAIT_TO_INTERACT_WITH_TARGET, false) || + IsCurSchedule(SCHED_ALYX_INTERACT_WITH_TARGET, false) || + IsCurSchedule(SCHED_ALYX_INTERACTION_INTERRUPTED, false) || + IsCurSchedule(SCHED_ALYX_FINISH_INTERACTING_WITH_TARGET, false) ); + + if( !bIsInteracting && IsAllowedToInteract() ) + { + switch( m_NPCState ) + { + case NPC_STATE_COMBAT: + SetCustomInterruptCondition( COND_ALYX_HAS_INTERACT_TARGET ); + SetCustomInterruptCondition( COND_ALYX_CAN_INTERACT_WITH_TARGET ); + break; + + case NPC_STATE_ALERT: + case NPC_STATE_IDLE: + SetCustomInterruptCondition( COND_ALYX_HAS_INTERACT_TARGET ); + SetCustomInterruptCondition( COND_ALYX_CAN_INTERACT_WITH_TARGET ); + break; + } + } + + // This nugget fixes a bug where Alyx will continue to attack an enemy she no longer hates in the + // case where her relationship with the enemy changes while she's running a SCHED_SCENE_GENERIC. + // Since we don't run ChooseEnemy() when we're running a schedule that doesn't interrupt on COND_NEW_ENEMY, + // we also do not re-evaluate and flush enemies we don't hate anymore. (sjb 6/9/2005) + if( IsCurSchedule(SCHED_SCENE_GENERIC) && GetEnemy() && GetEnemy()->VPhysicsGetObject() ) + { + if( GetEnemy()->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + SetCustomInterruptCondition( COND_NEW_ENEMY ); + } + } + + if( GetCurSchedule()->HasInterrupt( COND_IDLE_INTERRUPT ) ) + { + SetCustomInterruptCondition( COND_BETTER_WEAPON_AVAILABLE ); + } + + // If we're not in a script, keep an eye out for falling + if ( m_NPCState != NPC_STATE_SCRIPT && !IsInAVehicle() && !IsCurSchedule(SCHED_ALYX_FALL_TO_GROUND,false) ) + { + SetCustomInterruptCondition( COND_FLOATING_OFF_GROUND ); + } + + BaseClass::BuildScheduleTestBits(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::ShouldBehaviorSelectSchedule( CAI_BehaviorBase *pBehavior ) +{ + if( pBehavior == &m_AssaultBehavior ) + { + if( HasCondition( COND_MOBBED_BY_ENEMIES )) + return false; + } + + return BaseClass::ShouldBehaviorSelectSchedule( pBehavior ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::SelectSchedule( void ) +{ + // If we're in darkness mode, and the player has the flashlight off, and we hear a zombie footstep, + // and the player isn't nearby, deliberately turn away from the zombie to let the zombie grab me. + if ( HL2GameRules()->IsAlyxInDarknessMode() && m_NPCState == NPC_STATE_ALERT ) + { + if ( HasCondition ( COND_HEAR_COMBAT ) && !HasCondition(COND_SEE_PLAYER) ) + { + CSound *pBestSound = GetBestSound(); + if ( pBestSound && pBestSound->m_hOwner ) + { + if ( pBestSound->m_hOwner->Classify() == CLASS_ZOMBIE && pBestSound->SoundChannel() == SOUNDENT_CHANNEL_NPC_FOOTSTEP ) + return SCHED_ALYX_ALERT_FACE_AWAYFROM_BESTSOUND; + } + } + } + + if( HasCondition(COND_ALYX_CAN_INTERACT_WITH_TARGET) ) + return SCHED_ALYX_INTERACT_WITH_TARGET; + + if( HasCondition(COND_ALYX_HAS_INTERACT_TARGET) && HasCondition(COND_SEE_PLAYER) && IsAllowedToInteract() ) + { + ExpireCurrentRandomLookTarget(); + if( IsEMPHolstered() ) + { + return SCHED_ALYX_PREPARE_TO_INTERACT_WITH_TARGET; + } + + return SCHED_ALYX_WAIT_TO_INTERACT_WITH_TARGET; + } + + if( !IsEMPHolstered() && !HasInteractTarget() && !m_ActBusyBehavior.IsActive() ) + return SCHED_ALYX_HOLSTER_EMP; + + if ( HasCondition(COND_BETTER_WEAPON_AVAILABLE) ) + { + if( m_iszPendingWeapon != NULL_STRING ) + { + return SCHED_SWITCH_TO_PENDING_WEAPON; + } + else + { + CBaseHLCombatWeapon *pWeapon = dynamic_cast<CBaseHLCombatWeapon *>(Weapon_FindUsable( WEAPON_SEARCH_DELTA )); + if ( pWeapon ) + { + m_flNextWeaponSearchTime = gpGlobals->curtime + 10.0; + // Now lock the weapon for several seconds while we go to pick it up. + pWeapon->Lock( 10.0, this ); + SetTarget( pWeapon ); + return SCHED_ALYX_NEW_WEAPON; + } + } + } + + if ( HasCondition(COND_ENEMY_OCCLUDED) ) + { + //Warning("CROUCH: Standing, enemy is occluded.\n" ); + Stand(); + } + + return BaseClass::SelectSchedule(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::SelectScheduleDanger( void ) +{ + if( HasCondition( COND_HEAR_DANGER ) ) + { + CSound *pSound; + pSound = GetBestSound( SOUND_DANGER ); + + ASSERT( pSound != NULL ); + + if ( pSound && (pSound->m_iType & SOUND_DANGER) && ( pSound->SoundChannel() == SOUNDENT_CHANNEL_ZOMBINE_GRENADE ) ) + { + SpeakIfAllowed( TLK_DANGER_ZOMBINE_GRENADE ); + } + } + + return BaseClass::SelectScheduleDanger(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + case SCHED_ALERT_FACE_BESTSOUND: + return SCHED_ALYX_ALERT_FACE_BESTSOUND; + break; + + case SCHED_COMBAT_FACE: + if ( !HasCondition(COND_TASK_FAILED) && !IsCrouching() ) + return SCHED_ALYX_COMBAT_FACE; + break; + + case SCHED_WAKE_ANGRY: + return SCHED_ALYX_WAKE_ANGRY; + break; + + case SCHED_FALL_TO_GROUND: + return SCHED_ALYX_FALL_TO_GROUND; + break; + + case SCHED_ALERT_REACT_TO_COMBAT_SOUND: + return SCHED_ALYX_ALERT_REACT_TO_COMBAT_SOUND; + break; + + case SCHED_COWER: + case SCHED_PC_COWER: + // Alyx doesn't have cower animations. + return SCHED_FAIL; + + case SCHED_RANGE_ATTACK1: + { + if ( GetEnemy() ) + { + CBaseEntity *pEnemy = GetEnemy(); + if ( !IsCrouching() ) + { + // Does my enemy have enough health to warrant crouching? + if ( pEnemy->GetHealth() > ALYX_MIN_ENEMY_HEALTH_TO_CROUCH ) + { + // And are they far enough away? Expand the min dist so we don't crouch & stand immediately. + if ( EnemyDistance( pEnemy ) > (ALYX_MIN_ENEMY_DIST_TO_CROUCH * 1.5) && (pEnemy->GetFlags() & FL_ONGROUND) ) + { + //Warning("CROUCH: Desiring due to enemy far away.\n" ); + DesireCrouch(); + } + } + } + + // Are we supposed to be crouching? + if ( IsCrouching() || ( CrouchIsDesired() && !HasCondition( COND_HEAVY_DAMAGE ) ) ) + { + // See if they're a valid crouch target + if ( EnemyIsValidCrouchTarget( pEnemy ) ) + { + Crouch(); + } + else + { + //Warning("CROUCH: Standing, enemy not valid crouch target.\n" ); + Stand(); + } + } + else + { + //Warning("CROUCH: Standing, no enemy.\n" ); + Stand(); + } + } + + return SCHED_ALYX_RANGE_ATTACK1; + } + break; + + case SCHED_HIDE_AND_RELOAD: + { + if ( HL2GameRules()->IsAlyxInDarknessMode() ) + return SCHED_RELOAD; + + // If I don't have a ranged attacker as an enemy, don't try to hide + AIEnemiesIter_t iter; + for ( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) + { + CAI_BaseNPC *pEnemy = pEMemory->hEnemy.Get()->MyNPCPointer(); + if ( !pEnemy ) + continue; + + // Ignore enemies that don't hate me + if ( pEnemy->IRelationType( this ) != D_HT ) + continue; + + // Look for enemies with ranged capabilities + if ( pEnemy->CapabilitiesGet() & ( bits_CAP_WEAPON_RANGE_ATTACK1 | bits_CAP_WEAPON_RANGE_ATTACK2 | bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 ) ) + return SCHED_HIDE_AND_RELOAD; + } + + return SCHED_RELOAD; + } + break; + + case SCHED_RUN_FROM_ENEMY: + if ( HasCondition( COND_MOBBED_BY_ENEMIES ) ) + { + return SCHED_RUN_FROM_ENEMY_MOB; + } + break; + + case SCHED_IDLE_STAND: + return SCHED_ALYX_IDLE_STAND; + +#ifdef HL2_EPISODIC + case SCHED_RUN_RANDOM: + if( GetEnemy() && HasCondition(COND_SEE_ENEMY) && GetActiveWeapon() ) + { + // SCHED_RUN_RANDOM is a last ditch effort, it's the bottom of a chain of + // sequential schedule failures. Since this can cause Alyx to freeze up, + // just let her fight if she can. (sjb). + return SCHED_RANGE_ATTACK1; + } + break; +#endif// HL2_EPISODIC + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_SOUND_WAKE: + LocateEnemySound(); + // Don't do the half second wait here that the PlayerCompanion class does. (sbj) 1/4/2006 + TaskComplete(); + break; + + case TASK_ANNOUNCE_ATTACK: + { + SpeakAttacking(); + BaseClass::StartTask( pTask ); + break; + } + + case TASK_ALYX_BUILD_COMBAT_FACE_PATH: + { + if ( GetEnemy() && !FInAimCone( GetEnemyLKP() ) && FVisible( GetEnemyLKP() ) ) + { + Vector vecToEnemy = GetEnemyLKP() - GetAbsOrigin(); + VectorNormalize( vecToEnemy ); + + Vector vecMoveGoal = GetAbsOrigin() - (vecToEnemy * 24.0f); + + if ( !GetNavigator()->SetGoal( vecMoveGoal ) ) + { + TaskFail(FAIL_NO_ROUTE); + } + else + { + GetMotor()->SetIdealYawToTarget( GetEnemy()->WorldSpaceCenter() ); + GetNavigator()->SetArrivalDirection( GetEnemy() ); + TaskComplete(); + } + } + else + { + TaskFail("Defaulting To BaseClass::CombatFace"); + } + } + break; + + case TASK_ALYX_HOLSTER_AND_DESTROY_PISTOL: + { + // If we don't have the alyx gun, throw away our current, + // since the alyx gun is the only one we can tuck away. + if ( HasAlyxgun() ) + { + SetDesiredWeaponState( DESIREDWEAPONSTATE_HOLSTERED_DESTROYED ); + } + else + { + Weapon_Drop( GetActiveWeapon() ); + } + + SetWait( 1 ); // Wait while she does it. + } + break; + + case TASK_STOP_MOVING: + if ( npc_alyx_force_stop_moving.GetBool() ) + { + if ( ( GetNavigator()->IsGoalSet() && GetNavigator()->IsGoalActive() ) || GetNavType() == NAV_JUMP ) + { + DbgNavMsg( this, "Start TASK_STOP_MOVING\n" ); + DbgNavMsg( this, "Initiating stopping path\n" ); + GetNavigator()->StopMoving( false ); + + // E3 Hack + if ( HasPoseMoveYaw() ) + { + SetPoseParameter( m_poseMove_Yaw, 0 ); + } + } + else + { + if ( GetNavigator()->SetGoalFromStoppingPath() ) + { + DbgNavMsg( this, "Start TASK_STOP_MOVING\n" ); + DbgNavMsg( this, "Initiating stopping path\n" ); + } + else + { + GetNavigator()->ClearGoal(); + + if ( IsMoving() ) + { + SetIdealActivity( GetStoppedActivity() ); + } + TaskComplete(); + } + } + } + else + { + BaseClass::StartTask( pTask ); + } + break; + + case TASK_REACT_TO_COMBAT_SOUND: + { + CSound *pSound = GetBestSound(); + + if( pSound && pSound->IsSoundType(SOUND_COMBAT) && pSound->IsSoundType(SOUND_CONTEXT_GUNFIRE) ) + { + AnalyzeGunfireSound(pSound); + } + + TaskComplete(); + } + break; + + case TASK_ALYX_HOLSTER_PISTOL: + HolsterPistol(); + TaskComplete(); + break; + + case TASK_ALYX_DRAW_PISTOL: + DrawPistol(); + TaskComplete(); + break; + + case TASK_ALYX_WAIT_HACKING: + SetWait( pTask->flTaskData ); + break; + + case TASK_ALYX_GET_PATH_TO_INTERACT_TARGET: + { + if( !HasInteractTarget() ) + { + TaskFail("No interact target"); + return; + } + + AI_NavGoal_t goal; + + goal.type = GOALTYPE_LOCATION; + goal.dest = GetInteractTarget()->WorldSpaceCenter(); + goal.pTarget = GetInteractTarget(); + + GetNavigator()->SetGoal( goal ); + } + break; + + case TASK_ALYX_ANNOUNCE_HACK: + SpeakIfAllowed( CONCEPT_ALYX_REQUEST_ITEM ); + TaskComplete(); + break; + + case TASK_ALYX_BEGIN_INTERACTION: + { + INPCInteractive *pInteractive = dynamic_cast<INPCInteractive *>(GetInteractTarget()); + if ( pInteractive ) + { + EmpZapTarget( GetInteractTarget() ); + + pInteractive->AlyxStartedInteraction(); + pInteractive->NotifyInteraction(this); + pInteractive->AlyxFinishedInteraction(); + m_OnFinishInteractWithObject.FireOutput( GetInteractTarget(), this ); + } + + TaskComplete(); + } + break; + + case TASK_ALYX_COMPLETE_INTERACTION: + { + INPCInteractive *pInteractive = dynamic_cast<INPCInteractive *>(GetInteractTarget()); + + if( pInteractive ) + { + for( int i = 0 ; i < 3 ; i++ ) + { + g_pEffects->Sparks( GetInteractTarget()->WorldSpaceCenter() ); + } + + GetInteractTarget()->EmitSound("DoSpark"); + Speak( CONCEPT_ALYX_INTERACTION_DONE ); + + SetInteractTarget(NULL); + } + + TaskComplete(); + } + break; + + case TASK_ALYX_SET_IDLE_ACTIVITY: + { + Activity goalActivity = (Activity)((int)pTask->flTaskData); + if ( IsActivityFinished() ) + { + SetIdealActivity( goalActivity ); + } + } + break; + + case TASK_ALYX_FALL_TO_GROUND: + // If we wait this long without landing, we'll fall to our death + SetWait(2); + break; + + default: + BaseClass::StartTask(pTask); + break; + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_ALYX_HOLSTER_AND_DESTROY_PISTOL: + if( IsWaitFinished() ) + TaskComplete(); + break; + + case TASK_STOP_MOVING: + { + if ( npc_alyx_force_stop_moving.GetBool() ) + { + ChainRunTask( TASK_WAIT_FOR_MOVEMENT ); + if ( !TaskIsRunning() ) + { + DbgNavMsg( this, "TASK_STOP_MOVING Complete\n" ); + } + } + else + { + BaseClass::RunTask( pTask ); + } + break; + } + + case TASK_ALYX_WAIT_HACKING: + if( GetInteractTarget() && random->RandomInt(0, 3) == 0 ) + { + g_pEffects->Sparks( GetInteractTarget()->WorldSpaceCenter() ); + GetInteractTarget()->EmitSound("DoSpark"); + } + + if ( IsWaitFinished() ) + { + TaskComplete(); + } + break; + + case TASK_ALYX_SET_IDLE_ACTIVITY: + { + if ( IsActivityStarted() ) + { + TaskComplete(); + } + } + break; + + case TASK_ALYX_FALL_TO_GROUND: + if ( GetFlags() & FL_ONGROUND ) + { + TaskComplete(); + } + else if( IsWaitFinished() ) + { + // Call back to the base class & see if it can find a ground for us + // If it can't, we'll fall to our death + ChainRunTask( TASK_FALL_TO_GROUND ); + if ( TaskIsRunning() ) + { + CTakeDamageInfo info; + info.SetDamage( m_iHealth ); + info.SetAttacker( this ); + info.SetInflictor( this ); + info.SetDamageType( DMG_GENERIC ); + TakeDamage( info ); + } + } + break; + + default: + BaseClass::RunTask(pTask); + break; + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnStateChange( NPC_STATE OldState, NPC_STATE NewState ) +{ + switch( NewState ) + { + case NPC_STATE_COMBAT: + { + m_fCombatStartTime = gpGlobals->curtime; + } + break; + + default: + if( OldState == NPC_STATE_COMBAT ) + { + // coming out of combat state. + m_fCombatEndTime = gpGlobals->curtime + 2.0f; + } + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); + + // FIXME: hack until some way of removing decals after healing + m_fNoDamageDecal = true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::CanBeHitByMeleeAttack( CBaseEntity *pAttacker ) +{ + if( IsCurSchedule(SCHED_DUCK_DODGE) ) + { + return false; + } + + return BaseClass::CanBeHitByMeleeAttack( pAttacker ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Alyx::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + //!!!HACKHACK - EP1 - Stop alyx taking all physics damage to prevent her dying + // in freak accidents resembling spontaneous stress damage death (which are now impossible) + // Also stop her taking damage from flames: Fixes her being burnt to death from entity flames + // attached to random debris chunks while inside scripted sequences. + if( info.GetDamageType() & (DMG_CRUSH | DMG_BURN) ) + return 0; + + // If we're in commentary mode, prevent her taking damage from other NPCs + if ( IsInCommentaryMode() && info.GetAttacker() && info.GetAttacker()->IsNPC() ) + return 0; + + int taken = BaseClass::OnTakeDamage_Alive(info); + + if ( taken && HL2GameRules()->IsAlyxInDarknessMode() && !HasCondition( COND_TALKER_PLAYER_DEAD ) ) + { + if ( !HasCondition(COND_SEE_ENEMY) && (info.GetDamageType() & (DMG_SLASH | DMG_CLUB) ) ) + { + // I've taken melee damage. If I haven't seen the enemy for a few seconds, make some noise. + float flLastTimeSeen = GetEnemies()->LastTimeSeen( info.GetAttacker(), false ); + if ( flLastTimeSeen == AI_INVALID_TIME || gpGlobals->curtime - flLastTimeSeen > 3.0 ) + { + SpeakIfAllowed( "TLK_DARKNESS_UNKNOWN_WOUND" ); + m_fTimeUntilNextDarknessFoundPlayer = gpGlobals->curtime + RandomFloat( 3, 5 ); + } + } + } + + if( taken && (info.GetDamageType() & DMG_BLAST) ) + { + if ( HasShotgun() ) + { + if ( !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST) && !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST_DAMAGED_SHOTGUN) ) + { + RestartGesture( ACT_GESTURE_FLINCH_BLAST_DAMAGED_SHOTGUN ); + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + SequenceDuration( ACT_GESTURE_FLINCH_BLAST_DAMAGED_SHOTGUN ) + 0.5f ); + } + } + else + { + if ( !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST) && !IsPlayingGesture(ACT_GESTURE_FLINCH_BLAST_DAMAGED) ) + { + RestartGesture( ACT_GESTURE_FLINCH_BLAST_DAMAGED ); + GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + SequenceDuration( ACT_GESTURE_FLINCH_BLAST_DAMAGED ) + 0.5f ); + } + } + } + + return taken; +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +bool CNPC_Alyx::FCanCheckAttacks() +{ + if( GetEnemy() && IsGunship( GetEnemy() ) ) + { + // Don't attack gunships + return false; + } + + return BaseClass::FCanCheckAttacks(); +} + +//----------------------------------------------------------------------------- +// Purpose: Half damage against Combine Soldiers in outland_10 +//----------------------------------------------------------------------------- +float CNPC_Alyx::GetAttackDamageScale( CBaseEntity *pVictim ) +{ + if( g_HackOutland10DamageHack && pVictim->Classify() == CLASS_COMBINE ) + { + return 0.75f; + } + + return BaseClass::GetAttackDamageScale( pVictim ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) +{ + if( interactionType == g_interactionZombieMeleeWarning && IsAllowedToDodge() ) + { + // If a zombie is attacking, ditch my current schedule and duck if I'm running a schedule that will + // be interrupted if I'm hit. + if( ConditionInterruptsCurSchedule(COND_LIGHT_DAMAGE) || ConditionInterruptsCurSchedule( COND_HEAVY_DAMAGE) ) + { + //Only dodge an NPC you can see attacking. + if( sourceEnt && FInViewCone(sourceEnt) ) + { + SetSchedule(SCHED_DUCK_DODGE); + } + } + + return true; + } + + if( interactionType == g_interactionPlayerPuntedHeavyObject ) + { + // Try to get Alyx out of the way when player is punting cars around. + CBaseEntity *pProp = (CBaseEntity*)(data); + + if( pProp ) + { + float distToProp = pProp->WorldSpaceCenter().DistTo( GetAbsOrigin() ); + float distToPlayer = sourceEnt->WorldSpaceCenter().DistTo( GetAbsOrigin() ); + + // Do this if the prop is within 60 feet, and is closer to me than the player is. + if( distToProp < (60.0f * 12.0f) && (distToProp < distToPlayer) ) + { + if( fabs(pProp->WorldSpaceCenter().z - WorldSpaceCenter().z) <= 120.0f ) + { + if( sourceEnt->FInViewCone(this) ) + { + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, EarPosition(), 16, 1.0f, pProp ); + } + } + } + } + return true; + } + + return BaseClass::HandleInteraction( interactionType, data, sourceEnt ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::HolsterPistol() +{ + if( GetActiveWeapon() ) + { + GetActiveWeapon()->AddEffects(EF_NODRAW); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::DrawPistol() +{ + if( GetActiveWeapon() ) + { + GetActiveWeapon()->RemoveEffects(EF_NODRAW); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::Weapon_Drop( CBaseCombatWeapon *pWeapon, const Vector *pvecTarget, const Vector *pVelocity ) +{ + BaseClass::Weapon_Drop( pWeapon, pvecTarget, pVelocity ); + + if( pWeapon && pWeapon->ClassMatches( CLASSNAME_ALYXGUN ) ) + { + pWeapon->SUB_Remove(); + } + + m_WeaponType = WT_NONE; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsAllowedToAim() +{ + // Alyx can aim only if fully agitated + if( GetReadinessLevel() != AIRL_AGITATED ) + return false; + + return BaseClass::IsAllowedToAim(); +} + + +//----------------------------------------------------------------------------- +void CNPC_Alyx::PainSound( const CTakeDamageInfo &info ) +{ + // Alex has specific sounds for when attacked in the dark + if ( !HasCondition( COND_ALYX_IN_DARK ) ) + { + // set up the speech modifiers + CFmtStrN<128> modifiers( "damageammo:%s", info.GetAmmoName() ); + + SpeakIfAllowed( TLK_WOUND, modifiers ); + } +} + +//----------------------------------------------------------------------------- + +void CNPC_Alyx::DeathSound( const CTakeDamageInfo &info ) +{ + // Sentences don't play on dead NPCs + SentenceStop(); + + if ( !SpokeConcept( TLK_SELF_IN_BARNACLE ) ) + { + EmitSound( "npc_alyx.die" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnSeeEntity( CBaseEntity *pEntity ) +{ + BaseClass::OnSeeEntity(pEntity); + + if( pEntity->IsPlayer() && pEntity->IsEFlagSet(EFL_IS_BEING_LIFTED_BY_BARNACLE) ) + { + SpeakIfAllowed( TLK_ALLY_IN_BARNACLE ); + } +} + + +//--------------------------------------------------------- +// A sort of trivial rejection, this function tells us whether +// this object is something Alyx can interact with at all. +// (Alyx's state and the object's state are not considered +// at this stage) +//--------------------------------------------------------- +bool CNPC_Alyx::IsValidInteractTarget( CBaseEntity *pTarget ) +{ + INPCInteractive *pInteractive = dynamic_cast<INPCInteractive *>(pTarget); + + if( !pInteractive ) + { + // Not an INPCInteractive entity. + return false; + } + + if( !pInteractive->CanInteractWith(this) ) + { + return false; + } + + if( pInteractive->HasBeenInteractedWith() ) + { + // Already been interacted with. + return false; + } + + IPhysicsObject *pPhysics; + + pPhysics = pTarget->VPhysicsGetObject(); + if( pPhysics ) + { + if( !(pPhysics->GetGameFlags() & FVPHYSICS_PLAYER_HELD) ) + { + // Player isn't holding this physics object + return false; + } + } + + if( GetAbsOrigin().DistToSqr(pTarget->WorldSpaceCenter()) > (360.0f * 360.0f) ) + { + // Too far away! + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::SetInteractTarget( CBaseEntity *pTarget ) +{ + if( !pTarget ) + { + ClearCondition( COND_ALYX_HAS_INTERACT_TARGET ); + ClearCondition( COND_ALYX_CAN_INTERACT_WITH_TARGET ); + + SetCondition( COND_ALYX_NO_INTERACT_TARGET ); + SetCondition( COND_ALYX_CAN_NOT_INTERACT_WITH_TARGET ); + } + + m_hHackTarget.Set(pTarget); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::EmpZapTarget( CBaseEntity *pTarget ) +{ + g_pEffects->Sparks( pTarget->WorldSpaceCenter() ); + + CAlyxEmpEffect *pEmpEffect = (CAlyxEmpEffect*)CreateEntityByName( "env_alyxemp" ); + + if( pEmpEffect ) + { + pEmpEffect->Spawn(); + pEmpEffect->ActivateAutomatic( this, pTarget ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::CanInteractWithTarget( CBaseEntity *pTarget ) +{ + if( !IsValidInteractTarget(pTarget) ) + return false; + + float flDist; + + flDist = (WorldSpaceCenter() - pTarget->WorldSpaceCenter()).Length(); + + if( flDist > 80.0f ) + { + return false; + } + + if( !IsAllowedToInteract() ) + { + SpeakIfAllowed( TLK_CANT_INTERACT_NOW ); + return false; + } + + if( IsEMPHolstered() ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Player has illuminated this NPC with the flashlight +//----------------------------------------------------------------------------- +void CNPC_Alyx::PlayerHasIlluminatedNPC( CBasePlayer *pPlayer, float flDot ) +{ + if ( m_bIsFlashlightBlind ) + return; + + if ( !CanBeBlindedByFlashlight( true ) ) + return; + + // Ignore the flashlight if it's not shining at my eyes + if ( PlayerFlashlightOnMyEyes( pPlayer ) ) + { + char szResponse[AI_Response::MAX_RESPONSE_NAME]; + + // Only say the blinding speech if it's time to + if ( SpeakIfAllowed( "TLK_FLASHLIGHT_ILLUM", NULL, false, szResponse, AI_Response::MAX_RESPONSE_NAME ) ) + { + m_iszCurrentBlindScene = AllocPooledString( szResponse ); + ADD_DEBUG_HISTORY( HISTORY_ALYX_BLIND, UTIL_VarArgs( "(%0.2f) Alyx: start flashlight blind scene '%s'\n", gpGlobals->curtime, STRING(m_iszCurrentBlindScene) ) ); + GetShotRegulator()->DisableShooting(); + m_bIsFlashlightBlind = true; + m_fStayBlindUntil = gpGlobals->curtime + 0.1f; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Check if player has illuminated this NPC with a flare +//----------------------------------------------------------------------------- +void CNPC_Alyx::CheckBlindedByFlare( void ) +{ + if ( m_bIsFlashlightBlind ) + return; + + if ( !CanBeBlindedByFlashlight( false ) ) + return; + + // Ignore the flare if it's not too close + if ( BlindedByFlare() ) + { + char szResponse[AI_Response::MAX_RESPONSE_NAME]; + + // Only say the blinding speech if it's time to + if ( SpeakIfAllowed( "TLK_FLASHLIGHT_ILLUM", NULL, false, szResponse, AI_Response::MAX_RESPONSE_NAME ) ) + { + m_iszCurrentBlindScene = AllocPooledString( szResponse ); + ADD_DEBUG_HISTORY( HISTORY_ALYX_BLIND, UTIL_VarArgs( "(%0.2f) Alyx: start flare blind scene '%s'\n", gpGlobals->curtime, + STRING(m_iszCurrentBlindScene) ) ); + GetShotRegulator()->DisableShooting(); + m_bIsFlashlightBlind = true; + m_fStayBlindUntil = gpGlobals->curtime + 0.1f; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input: bCheckLightSources - if true, checks if any light darkness lightsources are near +//----------------------------------------------------------------------------- +bool CNPC_Alyx::CanBeBlindedByFlashlight( bool bCheckLightSources ) +{ + // Can't be blinded if we're not in alyx darkness mode + /* + if ( !HL2GameRules()->IsAlyxInDarknessMode() ) + return false; + */ + + // Can't be blinded if I'm in a script, or in combat + if ( IsInAScript() || GetState() == NPC_STATE_COMBAT || GetState() == NPC_STATE_SCRIPT ) + return false; + if ( IsSpeaking() ) + return false; + + // can't be blinded if Alyx is near a light source + if ( bCheckLightSources && DarknessLightSourceWithinRadius( this, 500 ) ) + return false; + + // Not during an actbusy + if ( m_ActBusyBehavior.IsActive() ) + return false; + if ( m_OperatorBehavior.IsRunning() ) + return false; + + // Can't be blinded if I've been in combat recently, to fix anim snaps + if ( GetLastEnemyTime() != 0.0 ) + { + if ( (gpGlobals->curtime - GetLastEnemyTime()) < 2 ) + return false; + } + + // Can't be blinded if I'm reloading + if ( IsCurSchedule(SCHED_RELOAD, false) ) + return false; + + // Can't be blinded right after being blind, to prevent oscillation + if ( gpGlobals->curtime < m_flDontBlindUntil ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_Alyx::PlayerFlashlightOnMyEyes( CBasePlayer *pPlayer ) +{ + Vector vecEyes, vecPlayerForward; + vecEyes = EyePosition(); + pPlayer->EyeVectors( &vecPlayerForward ); + + Vector vecToEyes = (vecEyes - pPlayer->EyePosition()); + float flDist = VectorNormalize( vecToEyes ); + + // We can be blinded in daylight, but only at close range + if ( HL2GameRules()->IsAlyxInDarknessMode() == false ) + { + if ( flDist > (8*12.0f) ) + return false; + } + + float flDot = DotProduct( vecPlayerForward, vecToEyes ); + if ( flDot < 0.98 ) + return false; + + // Check facing to ensure we're in front of her + Vector los = ( pPlayer->EyePosition() - vecEyes ); + los.z = 0; + VectorNormalize( los ); + Vector facingDir = EyeDirection2D(); + flDot = DotProduct( los, facingDir ); + return ( flDot > 0.3 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if Alyx is blinded by a flare +// Input : +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_Alyx::BlindedByFlare( void ) +{ + Vector vecEyes = EyePosition(); + + Vector los; + Vector vecToEyes; + Vector facingDir = EyeDirection2D(); + + // use a wider radius when she's already blind to help with edge cases + // where she flickers back and forth due to animation + float fBlindDist = ( m_bIsFlashlightBlind ) ? 35.0f : 30.0f; + + CFlare *pFlare = CFlare::GetActiveFlares(); + while( pFlare != NULL ) + { + vecToEyes = (vecEyes - pFlare->GetAbsOrigin()); + float fDist = VectorNormalize( vecToEyes ); + if ( fDist < fBlindDist ) + { + // Check facing to ensure we're in front of her + los = ( pFlare->GetAbsOrigin() - vecEyes ); + los.z = 0; + VectorNormalize( los ); + float flDot = DotProduct( los, facingDir ); + if ( ( flDot > 0.3 ) && FVisible( pFlare ) ) + { + return true; + } + } + + pFlare = pFlare->GetNextFlare(); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_Alyx::CanReload( void ) +{ + if ( m_bIsFlashlightBlind ) + return false; + + return BaseClass::CanReload(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::PickTacticalLookTarget( AILookTargetArgs_t *pArgs ) +{ + if( HasInteractTarget() ) + { + pArgs->hTarget = GetInteractTarget(); + pArgs->flInfluence = 0.8f; + pArgs->flDuration = 3.0f; + return true; + } + + if( m_ActBusyBehavior.IsActive() && m_ActBusyBehavior.IsCombatActBusy() ) + { + return false; + } + + return BaseClass::PickTacticalLookTarget( pArgs ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnSelectedLookTarget( AILookTargetArgs_t *pArgs ) +{ + if ( pArgs && pArgs->hTarget ) + { + // If it's a stealth target, we want to go into stealth mode + CAI_Hint *pHint = dynamic_cast<CAI_Hint *>(pArgs->hTarget.Get()); + if ( pHint && pHint->HintType() == HINT_WORLD_VISUALLY_INTERESTING_STEALTH ) + { + SetReadinessLevel( AIRL_STEALTH, true, true ); + pArgs->flDuration = 9999999; + m_hStealthLookTarget = pHint; + return; + } + } + + // If we're in stealth mode, break out now + if ( GetReadinessLevel() == AIRL_STEALTH ) + { + SetReadinessLevel( AIRL_STIMULATED, true, true ); + if ( m_hStealthLookTarget ) + { + ClearLookTarget( m_hStealthLookTarget ); + m_hStealthLookTarget = NULL; + } + } +} + +//----------------------------------------------------------------------------- +// Output : Behavior to use +//----------------------------------------------------------------------------- +CAI_FollowBehavior &CNPC_Alyx::GetFollowBehavior( void ) +{ + // Use the base class + return m_FollowBehavior; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::AimGun( void ) +{ + if (m_FuncTankBehavior.IsMounted()) + { + m_FuncTankBehavior.AimGun(); + return; + } + + // Always allow the passenger behavior to handle this + if ( m_PassengerBehavior.IsEnabled() ) + { + m_PassengerBehavior.AimGun(); + return; + } + + if( !GetEnemy() ) + { + if ( GetReadinessLevel() == AIRL_STEALTH && m_hStealthLookTarget != NULL ) + { + // Only aim if we're not far from the node + Vector vecAimDir = m_hStealthLookTarget->GetAbsOrigin() - Weapon_ShootPosition(); + if ( VectorNormalize( vecAimDir ) > 80 ) + { + // Ignore nodes that are behind her + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + float flDot = DotProduct( vecAimDir, vecForward ); + if ( flDot > 0 ) + { + SetAim( vecAimDir); + return; + } + } + } + } + + BaseClass::AimGun(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CNPC_Alyx::GetActualShootPosition( const Vector &shootOrigin ) +{ + if( HasShotgun() && GetEnemy() && GetEnemy()->Classify() == CLASS_ZOMBIE && random->RandomInt( 0, 1 ) == 1 ) + { + // 50-50 zombie headshots with shotgun! + return GetEnemy()->HeadTarget( shootOrigin ); + } + + return BaseClass::GetActualShootPosition( shootOrigin ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::EnemyIsValidCrouchTarget( CBaseEntity *pEnemy ) +{ + // Don't crouch to shoot flying enemies (or jumping antlions) + if ( !(pEnemy->GetFlags() & FL_ONGROUND) ) + return false; + + // Don't crouch to shoot if we couldn't see them while crouching + if ( !CouldShootIfCrouching( pEnemy ) ) + { + //Warning("CROUCH: Not valid due to crouch-no-LOS.\n" ); + return false; + } + + // Don't crouch to shoot enemies that are close to me + if ( EnemyDistance( pEnemy ) <= ALYX_MIN_ENEMY_DIST_TO_CROUCH ) + { + //Warning("CROUCH: Not valid due to enemy-too-close.\n" ); + return false; + } + + // Don't crouch to shoot enemies that are too far off my vertical plane + if ( fabs( pEnemy->GetAbsOrigin().z - GetAbsOrigin().z ) > 64 ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: degrees to turn in 0.1 seconds +//----------------------------------------------------------------------------- +float CNPC_Alyx::MaxYawSpeed( void ) +{ + if ( IsCrouching() ) + return 10; + + return BaseClass::MaxYawSpeed(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::Stand( void ) +{ + bool bWasCrouching = IsCrouching(); + if ( !BaseClass::Stand() ) + return false; + + if ( bWasCrouching ) + { + m_flNextCrouchTime = gpGlobals->curtime + ALYX_CROUCH_DELAY; + OnUpdateShotRegulator(); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::Crouch( void ) +{ + if ( !npc_alyx_crouch.GetBool() ) + return false; + + // Alyx will ignore crouch requests while she has the shotgun + if ( HasShotgun() ) + return false; + + bool bWasStanding = !IsCrouching(); + if ( !BaseClass::Crouch() ) + return false; + + if ( bWasStanding ) + { + OnUpdateShotRegulator(); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::DesireCrouch( void ) +{ + // Ignore crouch desire if we've been crouching recently to reduce oscillation + if ( m_flNextCrouchTime > gpGlobals->curtime ) + return; + + BaseClass::DesireCrouch(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Tack on extra criteria for responses +//----------------------------------------------------------------------------- +void CNPC_Alyx::ModifyOrAppendCriteria( AI_CriteriaSet &set ) +{ + AIEnemiesIter_t iter; + float fLengthOfLastCombat; + int iNumEnemies; + + if ( GetState() == NPC_STATE_COMBAT ) + { + fLengthOfLastCombat = gpGlobals->curtime - m_fCombatStartTime; + } + else + { + fLengthOfLastCombat = m_fCombatEndTime - m_fCombatStartTime; + } + + set.AppendCriteria( "combat_length", UTIL_VarArgs( "%.3f", fLengthOfLastCombat ) ); + + iNumEnemies = 0; + for ( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) + { + if ( pEMemory->hEnemy->IsAlive() && ( pEMemory->hEnemy->Classify() != CLASS_BULLSEYE ) ) + { + iNumEnemies++; + } + } + set.AppendCriteria( "num_enemies", UTIL_VarArgs( "%d", iNumEnemies ) ); + set.AppendCriteria( "darkness_mode", UTIL_VarArgs( "%d", HasCondition( COND_ALYX_IN_DARK ) ) ); + set.AppendCriteria( "water_level", UTIL_VarArgs( "%d", GetWaterLevel() ) ); + + CHL2_Player *pPlayer = assert_cast<CHL2_Player*>( UTIL_PlayerByIndex( 1 ) ); + set.AppendCriteria( "num_companions", UTIL_VarArgs( "%d", pPlayer ? pPlayer->GetNumSquadCommandables() : 0 ) ); + set.AppendCriteria( "flashlight_on", UTIL_VarArgs( "%d", pPlayer ? pPlayer->FlashlightIsOn() : 0 ) ); + + BaseClass::ModifyOrAppendCriteria( set ); +} + +//----------------------------------------------------------------------------- +// Purpose: Turn off Alyx's readiness when she's around a vehicle +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsReadinessCapable( void ) +{ + // Let the convar decide + return npc_alyx_readiness.GetBool();; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsAllowedToInteract() +{ + if ( RunningPassengerBehavior() ) + return false; + + if( IsInAScript() ) + return false; + + if( IsCurSchedule(SCHED_SCENE_GENERIC) ) + return false; + + if( GetEnemy() ) + { + if( GetEnemy()->GetAbsOrigin().DistTo( GetAbsOrigin() ) <= 240.0f ) + { + // Enemy is nearby! + return false; + } + } + + return m_bInteractionAllowed; +} + +//----------------------------------------------------------------------------- +// Purpose: Allows the NPC to react to being given a weapon +// Input : *pNewWeapon - Weapon given +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnChangeActiveWeapon( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) +{ + m_WeaponType = ComputeWeaponType(); + BaseClass::OnChangeActiveWeapon( pOldWeapon, pNewWeapon ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Allows the NPC to react to being given a weapon +// Input : *pNewWeapon - Weapon given +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnGivenWeapon( CBaseCombatWeapon *pNewWeapon ) +{ + // HACK: This causes Alyx to pull her gun from a holstered position + if ( pNewWeapon->ClassMatches( CLASSNAME_ALYXGUN ) ) + { + // Put it away so we can pull it out properly + GetActiveWeapon()->Holster(); + SetActiveWeapon( NULL ); + + // Draw the weapon when we're next able to + SetDesiredWeaponState( DESIREDWEAPONSTATE_UNHOLSTERED ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::Weapon_Equip( CBaseCombatWeapon *pWeapon ) +{ + m_WeaponType = ComputeWeaponType( pWeapon ); + BaseClass::Weapon_Equip( pWeapon ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Alyx::Weapon_CanUse( CBaseCombatWeapon *pWeapon ) +{ + if( !pWeapon->ClassMatches( CLASSNAME_SHOTGUN ) ) + return false; + + return BaseClass::Weapon_CanUse( pWeapon ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : - +//----------------------------------------------------------------------------- +void CNPC_Alyx::OnUpdateShotRegulator( ) +{ + BaseClass::OnUpdateShotRegulator(); + + if ( !HasShotgun() && IsCrouching() ) + { + // While crouching, Alyx fires longer bursts + int iMinBurst, iMaxBurst; + GetShotRegulator()->GetBurstShotCountRange( &iMinBurst, &iMaxBurst ); + GetShotRegulator()->SetBurstShotCountRange( iMinBurst * 2, iMaxBurst * 2 ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::BarnacleDeathSound( void ) +{ + Speak( TLK_SELF_IN_BARNACLE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : PassengerState_e +//----------------------------------------------------------------------------- +PassengerState_e CNPC_Alyx::GetPassengerState( void ) +{ + return m_PassengerBehavior.GetPassengerState(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Alyx::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // if I'm in the vehicle, the player is probably trying to use the vehicle + if ( GetPassengerState() == PASSENGER_STATE_INSIDE && pActivator->IsPlayer() && GetParent() ) + { + GetParent()->Use( pActivator, pCaller, useType, value ); + return; + } + m_bDontUseSemaphore = true; + SpeakIfAllowed( TLK_USE ); + m_bDontUseSemaphore = false; + + m_OnPlayerUse.FireOutput( pActivator, pCaller ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::PlayerInSpread( const Vector &sourcePos, const Vector &targetPos, float flSpread, float maxDistOffCenter, bool ignoreHatedPlayers ) +{ + // loop through all players + for (int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( pPlayer && ( !ignoreHatedPlayers || IRelationType( pPlayer ) != D_HT ) ) + { + //If the player is being lifted by a barnacle then go ahead and ignore the player and shoot. +#ifdef HL2_EPISODIC + if ( pPlayer->IsEFlagSet( EFL_IS_BEING_LIFTED_BY_BARNACLE ) ) + return false; +#endif + + if ( PointInSpread( pPlayer, sourcePos, targetPos, pPlayer->WorldSpaceCenter(), flSpread, maxDistOffCenter ) ) + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::IsCrouchedActivity( Activity activity ) +{ + Activity realActivity = TranslateActivity(activity); + + switch ( realActivity ) + { + case ACT_RELOAD_LOW: + case ACT_COVER_LOW: + case ACT_COVER_PISTOL_LOW: + case ACT_COVER_SMG1_LOW: + case ACT_RELOAD_SMG1_LOW: + + // Aren't these supposed to be a little higher than the above? + case ACT_RANGE_ATTACK1_LOW: + case ACT_RANGE_ATTACK2_LOW: + case ACT_RANGE_ATTACK_AR2_LOW: + case ACT_RANGE_ATTACK_SMG1_LOW: + case ACT_RANGE_ATTACK_SHOTGUN_LOW: + case ACT_RANGE_ATTACK_PISTOL_LOW: + case ACT_RANGE_AIM_LOW: + case ACT_RANGE_AIM_SMG1_LOW: + case ACT_RANGE_AIM_PISTOL_LOW: + case ACT_RANGE_AIM_AR2_LOW: + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_Alyx::OnBeginMoveAndShoot() +{ + if ( BaseClass::OnBeginMoveAndShoot() ) + { + SpeakAttacking(); + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Alyx::SpeakAttacking( void ) +{ + if ( GetActiveWeapon() && m_AnnounceAttackTimer.Expired() ) + { + SpeakIfAllowed( TLK_ATTACKING, UTIL_VarArgs("attacking_with_weapon:%s", GetActiveWeapon()->GetClassname()) ); + m_AnnounceAttackTimer.Set( 3, 5 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *lpszInteractionName - +// *pOther - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_Alyx::ForceVehicleInteraction( const char *lpszInteractionName, CBaseCombatCharacter *pOther ) +{ + return m_PassengerBehavior.ForceVehicleInteraction( lpszInteractionName, pOther ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CNPC_Alyx::WeaponType_t CNPC_Alyx::ComputeWeaponType( CBaseEntity *pWeapon ) +{ + if ( !pWeapon ) + { + pWeapon = GetActiveWeapon(); + } + + if ( !pWeapon ) + { + return WT_NONE; + } + + if ( pWeapon->ClassMatches( CLASSNAME_ALYXGUN ) ) + { + return WT_ALYXGUN; + } + + if ( pWeapon->ClassMatches( CLASSNAME_SMG1 ) ) + { + return WT_SMG1; + } + + if ( pWeapon->ClassMatches( CLASSNAME_SHOTGUN ) ) + { + return WT_SHOTGUN; + } + + if ( pWeapon->ClassMatches( CLASSNAME_AR2 ) ) + { + return WT_AR2; + } + + return WT_OTHER; +} + +//----------------------------------------------------------------------------- +// Purpose: Complain about being punted +//----------------------------------------------------------------------------- +void CNPC_Alyx::InputVehiclePunted( inputdata_t &inputdata ) +{ + // If we're in a vehicle, complain about being punted + if ( IsInAVehicle() && GetVehicleEntity() == inputdata.pCaller ) + { + // FIXME: Pass this up into the behavior? + SpeakIfAllowed( TLK_PASSENGER_PUNTED ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CNPC_Alyx::InputOutsideTransition( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( pPlayer && pPlayer->IsInAVehicle() ) + { + if ( ShouldAlwaysTransition() == false ) + return; + + // Enter immediately + EnterVehicle( pPlayer->GetVehicleEntity(), true ); + return; + } + + // If the player is in the vehicle and we're not, then we need to enter the vehicle immediately + BaseClass::InputOutsideTransition( inputdata ); +} + +//========================================================= +// AI Schedules Specific to this NPC +//========================================================= + +AI_BEGIN_CUSTOM_NPC( npc_alyx, CNPC_Alyx ) + + DECLARE_TASK( TASK_ALYX_BEGIN_INTERACTION ) + DECLARE_TASK( TASK_ALYX_COMPLETE_INTERACTION ) + DECLARE_TASK( TASK_ALYX_ANNOUNCE_HACK ) + DECLARE_TASK( TASK_ALYX_GET_PATH_TO_INTERACT_TARGET ) + DECLARE_TASK( TASK_ALYX_WAIT_HACKING ) + DECLARE_TASK( TASK_ALYX_DRAW_PISTOL ) + DECLARE_TASK( TASK_ALYX_HOLSTER_PISTOL ) + DECLARE_TASK( TASK_ALYX_HOLSTER_AND_DESTROY_PISTOL ) + DECLARE_TASK( TASK_ALYX_BUILD_COMBAT_FACE_PATH ) + DECLARE_TASK( TASK_ALYX_SET_IDLE_ACTIVITY ) + DECLARE_TASK( TASK_ALYX_FALL_TO_GROUND ) + + DECLARE_ANIMEVENT( AE_ALYX_EMPTOOL_ATTACHMENT ) + DECLARE_ANIMEVENT( AE_ALYX_EMPTOOL_SEQUENCE ) + DECLARE_ANIMEVENT( AE_ALYX_EMPTOOL_USE ) + DECLARE_ANIMEVENT( COMBINE_AE_BEGIN_ALTFIRE ) + DECLARE_ANIMEVENT( COMBINE_AE_ALTFIRE ) + + DECLARE_CONDITION( COND_ALYX_HAS_INTERACT_TARGET ) + DECLARE_CONDITION( COND_ALYX_NO_INTERACT_TARGET ) + DECLARE_CONDITION( COND_ALYX_CAN_INTERACT_WITH_TARGET ) + DECLARE_CONDITION( COND_ALYX_CAN_NOT_INTERACT_WITH_TARGET ) + DECLARE_CONDITION( COND_ALYX_PLAYER_TURNED_ON_FLASHLIGHT ) + DECLARE_CONDITION( COND_ALYX_PLAYER_TURNED_OFF_FLASHLIGHT ) + DECLARE_CONDITION( COND_ALYX_PLAYER_FLASHLIGHT_EXPIRED ) + DECLARE_CONDITION( COND_ALYX_IN_DARK ) + + DECLARE_ACTIVITY( ACT_ALYX_DRAW_TOOL ) + DECLARE_ACTIVITY( ACT_ALYX_IDLE_TOOL ) + DECLARE_ACTIVITY( ACT_ALYX_ZAP_TOOL ) + DECLARE_ACTIVITY( ACT_ALYX_HOLSTER_TOOL ) + DECLARE_ACTIVITY( ACT_ALYX_PICKUP_RACK ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_PREPARE_TO_INTERACT_WITH_TARGET, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ALYX_DRAW_TOOL" + " TASK_SET_ACTIVITY ACTIVITY:ACT_ALYX_IDLE_TOOL" + " TASK_FACE_PLAYER 0" + "" + " Interrupts" + "" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_WAIT_TO_INTERACT_WITH_TARGET, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_ALYX_ANNOUNCE_HACK 0" + " TASK_FACE_PLAYER 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_ALYX_IDLE_TOOL" + " TASK_WAIT 2" + "" + " Interrupts" + " COND_ALYX_CAN_INTERACT_WITH_TARGET" + " COND_ALYX_NO_INTERACT_TARGET" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_INTERACT_WITH_TARGET, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_PLAYER 0" + " TASK_ALYX_BEGIN_INTERACTION 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ALYX_ZAP_TOOL" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_ALYX_FINISH_INTERACTING_WITH_TARGET" + "" + " Interrupts" + " COND_ALYX_NO_INTERACT_TARGET" + " COND_ALYX_CAN_NOT_INTERACT_WITH_TARGET" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_FINISH_INTERACTING_WITH_TARGET, + + " Tasks" + " TASK_ALYX_COMPLETE_INTERACTION 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ALYX_HOLSTER_TOOL" + "" + " Interrupts" + "" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_HOLSTER_EMP, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ALYX_HOLSTER_TOOL" + " TASK_ALYX_DRAW_PISTOL 0" + "" + " Interrupts" + "" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_INTERACTION_INTERRUPTED, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_PLAYER 0" + " TASK_WAIT 2" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_ALERT_FACE_AWAYFROM_BESTSOUND, + " Tasks" + " TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION 0" + " TASK_STOP_MOVING 0" + " TASK_FACE_AWAY_FROM_SAVEPOSITION 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 10.0" + " TASK_FACE_REASONABLE 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_FEAR" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + ) + + //=============================================== + // > RangeAttack1 + //=============================================== + DEFINE_SCHEDULE + ( + SCHED_ALYX_RANGE_ATTACK1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack + " TASK_RANGE_ATTACK1 0" + "" + " Interrupts" + " COND_ENEMY_WENT_NULL" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + " COND_WEAPON_BLOCKED_BY_FRIEND" + " COND_WEAPON_SIGHT_OCCLUDED" + ) + + //=============================================== + // > SCHED_ALYX_ALERT_REACT_TO_COMBAT_SOUND + //=============================================== + DEFINE_SCHEDULE + ( + SCHED_ALYX_ALERT_REACT_TO_COMBAT_SOUND, + + " Tasks" + " TASK_REACT_TO_COMBAT_SOUND 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_ALERT_FACE_BESTSOUND" + "" + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_ALYX_COMBAT_FACE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ALYX_COMBAT_FACE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE" + " TASK_STOP_MOVING 0" + " TASK_ALYX_BUILD_COMBAT_FACE_PATH 0" + " TASK_RUN_PATH 0" + " TASK_FACE_IDEAL 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + ) + + //========================================================= + // > SCHED_ALYX_WAKE_ANGRY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ALYX_WAKE_ANGRY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SOUND_WAKE 0" + "" + " Interrupts" + ) + + //=============================================== + // > NewWeapon + //=============================================== + DEFINE_SCHEDULE + ( + SCHED_ALYX_NEW_WEAPON, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_TOLERANCE_DISTANCE 5" + " TASK_GET_PATH_TO_TARGET_WEAPON 0" + " TASK_WEAPON_RUN_PATH 0" + " TASK_STOP_MOVING 0" + " TASK_ALYX_HOLSTER_AND_DESTROY_PISTOL 0" + " TASK_FACE_TARGET 0" + " TASK_WEAPON_PICKUP 0" + " TASK_WAIT 1"// Don't move before done standing up + "" + " Interrupts" + ) + + //=============================================== + // > Alyx_Idle_Stand + //=============================================== + DEFINE_SCHEDULE + ( + SCHED_ALYX_IDLE_STAND, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_ALYX_SET_IDLE_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 5" + " TASK_WAIT_PVS 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_FEAR" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_SMELL" + " COND_PROVOKED" + " COND_GIVE_WAY" + " COND_HEAR_PLAYER" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_HEAR_BULLET_IMPACT" + " COND_IDLE_INTERRUPT" + ) + + //=============================================== + // Makes Alyx die if she falls too long + //=============================================== + DEFINE_SCHEDULE + ( + SCHED_ALYX_FALL_TO_GROUND, + + " Tasks" + " TASK_ALYX_FALL_TO_GROUND 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_ALYX_ALERT_FACE_BESTSOUND, + + " Tasks" + " TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION 0" + " TASK_STOP_MOVING 0" + " TASK_FACE_SAVEPOSITION 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_FEAR" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + ); + +AI_END_CUSTOM_NPC() |