You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3603 lines
105 KiB
3603 lines
105 KiB
//========= 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()
|
|
|