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.
1145 lines
32 KiB
1145 lines
32 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: A hideous, putrescent, pus-filled undead carcass atop which a vile |
|
// nest of filthy poisonous headcrabs lurks. |
|
// |
|
// Anyway, this guy has two range attacks: at short range, headcrabs |
|
// will leap from the nest to attack. At long range he will wrench a |
|
// headcrab from his back to throw it at his enemy. |
|
// |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "ai_basenpc.h" |
|
#include "ai_default.h" |
|
#include "ai_schedule.h" |
|
#include "ai_hull.h" |
|
#include "ai_motor.h" |
|
#include "game.h" |
|
#include "npc_headcrab.h" |
|
#include "npcevent.h" |
|
#include "entitylist.h" |
|
#include "ai_task.h" |
|
#include "activitylist.h" |
|
#include "engine/IEngineSound.h" |
|
#include "npc_BaseZombie.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
#define BREATH_VOL_MAX 0.6 |
|
|
|
// |
|
// Controls how soon he throws the first headcrab after seeing his enemy (also when the first headcrab leaps off) |
|
// |
|
#define ZOMBIE_THROW_FIRST_MIN_DELAY 1 // min seconds before first crab throw |
|
#define ZOMBIE_THROW_FIRST_MAX_DELAY 2 // max seconds before first crab throw |
|
|
|
// |
|
// Controls how often he throws headcrabs (also how often headcrabs leap off) |
|
// |
|
#define ZOMBIE_THROW_MIN_DELAY 4 // min seconds between crab throws |
|
#define ZOMBIE_THROW_MAX_DELAY 10 // max seconds between crab throws |
|
|
|
// |
|
// Ranges for throwing headcrabs. |
|
// |
|
#define ZOMBIE_THROW_RANGE_MIN 250 |
|
#define ZOMBIE_THROW_RANGE_MAX 800 |
|
#define ZOMBIE_THROW_CONE 0.6 |
|
|
|
// |
|
// Ranges for headcrabs leaping off. |
|
// |
|
#define ZOMBIE_HC_LEAP_RANGE_MIN 12 |
|
#define ZOMBIE_HC_LEAP_RANGE_MAX 256 |
|
#define ZOMBIE_HC_LEAP_CONE 0.6 |
|
|
|
|
|
#define ZOMBIE_BODYGROUP_NEST_BASE 2 // First nest crab, +2 more |
|
#define ZOMBIE_BODYGROUP_THROW 5 // The crab in our hand for throwing |
|
|
|
#define ZOMBIE_ENEMY_BREATHE_DIST 300 // How close we must be to our enemy before we start breathing hard. |
|
|
|
|
|
envelopePoint_t envPoisonZombieMoanVolumeFast[] = |
|
{ |
|
{ 1.0f, 1.0f, |
|
0.1f, 0.1f, |
|
}, |
|
{ 0.0f, 0.0f, |
|
0.2f, 0.3f, |
|
}, |
|
}; |
|
|
|
|
|
// |
|
// Turns the breathing off for a second, then back on. |
|
// |
|
envelopePoint_t envPoisonZombieBreatheVolumeOffShort[] = |
|
{ |
|
{ 0.0f, 0.0f, |
|
0.1f, 0.1f, |
|
}, |
|
{ 0.0f, 0.0f, |
|
2.0f, 2.0f, |
|
}, |
|
{ BREATH_VOL_MAX, BREATH_VOL_MAX, |
|
1.0f, 1.0f, |
|
}, |
|
}; |
|
|
|
|
|
// |
|
// Custom schedules. |
|
// |
|
enum |
|
{ |
|
SCHED_ZOMBIE_POISON_RANGE_ATTACK2 = LAST_BASE_ZOMBIE_SCHEDULE, |
|
SCHED_ZOMBIE_POISON_RANGE_ATTACK1, |
|
}; |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// The maximum number of headcrabs we can have riding on our back. |
|
// NOTE: If you change this value you must also change the lookup table in Spawn! |
|
//----------------------------------------------------------------------------- |
|
#define MAX_CRABS 3 |
|
|
|
int AE_ZOMBIE_POISON_THROW_WARN_SOUND; |
|
int AE_ZOMBIE_POISON_PICKUP_CRAB; |
|
int AE_ZOMBIE_POISON_THROW_SOUND; |
|
int AE_ZOMBIE_POISON_THROW_CRAB; |
|
int AE_ZOMBIE_POISON_SPIT; |
|
|
|
//----------------------------------------------------------------------------- |
|
// The model we use for our legs when we get blowed up. |
|
//----------------------------------------------------------------------------- |
|
static const char *s_szLegsModel = "models/zombie/classic_legs.mdl"; |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// The classname of the headcrab that jumps off of this kind of zombie. |
|
//----------------------------------------------------------------------------- |
|
static const char *s_szHeadcrabClassname = "npc_headcrab_poison"; |
|
static const char *s_szHeadcrabModel = "models/headcrabblack.mdl"; |
|
|
|
static const char *pMoanSounds[] = |
|
{ |
|
"NPC_PoisonZombie.Moan1", |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Skill settings. |
|
//----------------------------------------------------------------------------- |
|
ConVar sk_zombie_poison_health( "sk_zombie_poison_health", "0"); |
|
ConVar sk_zombie_poison_dmg_spit( "sk_zombie_poison_dmg_spit","0"); |
|
|
|
class CNPC_PoisonZombie : public CAI_BlendingHost<CNPC_BaseZombie> |
|
{ |
|
DECLARE_CLASS( CNPC_PoisonZombie, CAI_BlendingHost<CNPC_BaseZombie> ); |
|
|
|
public: |
|
|
|
// |
|
// CBaseZombie implemenation. |
|
// |
|
virtual Vector HeadTarget( const Vector &posSrc ); |
|
bool ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold ); |
|
virtual bool IsChopped( const CTakeDamageInfo &info ) { return false; } |
|
|
|
// |
|
// CAI_BaseNPC implementation. |
|
// |
|
virtual float MaxYawSpeed( void ); |
|
|
|
virtual int RangeAttack1Conditions( float flDot, float flDist ); |
|
virtual int RangeAttack2Conditions( float flDot, float flDist ); |
|
|
|
virtual float GetClawAttackRange() const { return 70; } |
|
|
|
virtual void PrescheduleThink( void ); |
|
virtual void BuildScheduleTestBits( void ); |
|
virtual int SelectSchedule( void ); |
|
virtual int SelectFailSchedule( int nFailedSchedule, int nFailedTask, AI_TaskFailureCode_t eTaskFailCode ); |
|
virtual int TranslateSchedule( int scheduleType ); |
|
|
|
virtual bool ShouldPlayIdleSound( void ); |
|
|
|
// |
|
// CBaseAnimating implementation. |
|
// |
|
virtual void HandleAnimEvent( animevent_t *pEvent ); |
|
|
|
// |
|
// CBaseEntity implementation. |
|
// |
|
virtual void Spawn( void ); |
|
virtual void Precache( void ); |
|
virtual void SetZombieModel( void ); |
|
|
|
virtual Class_T Classify( void ); |
|
virtual void Event_Killed( const CTakeDamageInfo &info ); |
|
virtual int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); |
|
|
|
DECLARE_DATADESC(); |
|
DEFINE_CUSTOM_AI; |
|
|
|
void PainSound( const CTakeDamageInfo &info ); |
|
void AlertSound( void ); |
|
void IdleSound( void ); |
|
void AttackSound( void ); |
|
void AttackHitSound( void ); |
|
void AttackMissSound( void ); |
|
void FootstepSound( bool fRightFoot ); |
|
void FootscuffSound( bool fRightFoot ) {}; |
|
|
|
virtual void StopLoopingSounds( void ); |
|
|
|
protected: |
|
|
|
virtual void MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize ); |
|
virtual bool MustCloseToAttack( void ); |
|
|
|
virtual const char *GetMoanSound( int nSoundIndex ); |
|
virtual const char *GetLegsModel( void ); |
|
virtual const char *GetTorsoModel( void ); |
|
virtual const char *GetHeadcrabClassname( void ); |
|
virtual const char *GetHeadcrabModel( void ); |
|
|
|
private: |
|
|
|
void BreatheOffShort( void ); |
|
|
|
void EnableCrab( int nCrab, bool bEnable ); |
|
int RandomThrowCrab( void ); |
|
void EvacuateNest( bool bExplosion, float flDamage, CBaseEntity *pAttacker ); |
|
|
|
CSoundPatch *m_pFastBreathSound; |
|
CSoundPatch *m_pSlowBreathSound; |
|
|
|
int m_nCrabCount; // How many headcrabs we have on our back. |
|
bool m_bCrabs[MAX_CRABS]; // Which crabs in particular are on our back. |
|
float m_flNextCrabThrowTime; // The next time we are allowed to throw a headcrab. |
|
|
|
float m_flNextPainSoundTime; |
|
|
|
bool m_bNearEnemy; |
|
|
|
// NOT serialized: |
|
int m_nThrowCrab; // The crab we are about to throw. |
|
}; |
|
|
|
LINK_ENTITY_TO_CLASS( npc_poisonzombie, CNPC_PoisonZombie ); |
|
|
|
|
|
BEGIN_DATADESC( CNPC_PoisonZombie ) |
|
|
|
DEFINE_SOUNDPATCH( m_pFastBreathSound ), |
|
DEFINE_SOUNDPATCH( m_pSlowBreathSound ), |
|
DEFINE_KEYFIELD( m_nCrabCount, FIELD_INTEGER, "crabcount" ), |
|
DEFINE_ARRAY( m_bCrabs, FIELD_BOOLEAN, MAX_CRABS ), |
|
DEFINE_FIELD( m_flNextCrabThrowTime, FIELD_TIME ), |
|
|
|
DEFINE_FIELD( m_flNextPainSoundTime, FIELD_TIME ), |
|
|
|
DEFINE_FIELD( m_bNearEnemy, FIELD_BOOLEAN ), |
|
|
|
// NOT serialized: |
|
//DEFINE_FIELD( m_nThrowCrab, FIELD_INTEGER ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::Precache( void ) |
|
{ |
|
PrecacheModel("models/zombie/poison.mdl"); |
|
|
|
PrecacheScriptSound( "NPC_PoisonZombie.Die" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.ThrowWarn" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Throw" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Idle" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Pain" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Alert" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.FootstepRight" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.FootstepLeft" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Attack" ); |
|
|
|
PrecacheScriptSound( "NPC_PoisonZombie.FastBreath" ); |
|
PrecacheScriptSound( "NPC_PoisonZombie.Moan1" ); |
|
|
|
PrecacheScriptSound( "Zombie.AttackHit" ); |
|
PrecacheScriptSound( "Zombie.AttackMiss" ); |
|
|
|
BaseClass::Precache(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::Spawn( void ) |
|
{ |
|
Precache(); |
|
|
|
m_fIsTorso = m_fIsHeadless = false; |
|
|
|
#ifdef HL2_EPISODIC |
|
SetBloodColor( BLOOD_COLOR_ZOMBIE ); |
|
#else |
|
SetBloodColor( BLOOD_COLOR_YELLOW ); |
|
#endif // HL2_EPISODIC |
|
|
|
m_iHealth = sk_zombie_poison_health.GetFloat(); |
|
m_flFieldOfView = 0.2; |
|
|
|
CapabilitiesClear(); |
|
CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 ); |
|
|
|
BaseClass::Spawn(); |
|
|
|
CPASAttenuationFilter filter( this, ATTN_IDLE ); |
|
m_pFastBreathSound = ENVELOPE_CONTROLLER.SoundCreate( filter, entindex(), CHAN_ITEM, "NPC_PoisonZombie.FastBreath", ATTN_IDLE ); |
|
ENVELOPE_CONTROLLER.Play( m_pFastBreathSound, 0.0f, 100 ); |
|
|
|
CPASAttenuationFilter filter2( this ); |
|
m_pSlowBreathSound = ENVELOPE_CONTROLLER.SoundCreate( filter2, entindex(), CHAN_ITEM, "NPC_PoisonZombie.Moan1", ATTN_NORM ); |
|
ENVELOPE_CONTROLLER.Play( m_pSlowBreathSound, BREATH_VOL_MAX, 100 ); |
|
|
|
int nCrabs = m_nCrabCount; |
|
if ( !nCrabs ) |
|
{ |
|
nCrabs = MAX_CRABS; |
|
} |
|
m_nCrabCount = 0; |
|
|
|
// |
|
// Generate a random set of crabs based on the crab count |
|
// specified by the level designer. |
|
// |
|
int nBits[] = |
|
{ |
|
// One bit |
|
0x01, |
|
0x02, |
|
0x04, |
|
|
|
// Two bits |
|
0x03, |
|
0x05, |
|
0x06, |
|
}; |
|
|
|
int nBitMask = 7; |
|
if (nCrabs == 1) |
|
{ |
|
nBitMask = nBits[random->RandomInt( 0, 2 )]; |
|
} |
|
else if (nCrabs == 2) |
|
{ |
|
nBitMask = nBits[random->RandomInt( 3, 5 )]; |
|
} |
|
|
|
for ( int i = 0; i < MAX_CRABS; i++ ) |
|
{ |
|
EnableCrab( i, ( nBitMask & ( 1 << i ) ) != 0 ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns a moan sound for this class of zombie. |
|
//----------------------------------------------------------------------------- |
|
const char *CNPC_PoisonZombie::GetMoanSound( int nSound ) |
|
{ |
|
return pMoanSounds[nSound % ARRAYSIZE( pMoanSounds )]; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns the model to use for our legs ragdoll when we are blown in twain. |
|
//----------------------------------------------------------------------------- |
|
const char *CNPC_PoisonZombie::GetLegsModel( void ) |
|
{ |
|
return s_szLegsModel; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
const char *CNPC_PoisonZombie::GetTorsoModel( void ) |
|
{ |
|
return "models/zombie/classic_torso.mdl"; |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns the classname (ie "npc_headcrab") to spawn when our headcrab bails. |
|
//----------------------------------------------------------------------------- |
|
const char *CNPC_PoisonZombie::GetHeadcrabClassname( void ) |
|
{ |
|
return s_szHeadcrabClassname; |
|
} |
|
|
|
const char *CNPC_PoisonZombie::GetHeadcrabModel( void ) |
|
{ |
|
return s_szHeadcrabModel; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Turns the given crab on or off. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::EnableCrab( int nCrab, bool bEnable ) |
|
{ |
|
ASSERT( ( nCrab >= 0 ) && ( nCrab < MAX_CRABS ) ); |
|
|
|
if ( ( nCrab >= 0 ) && ( nCrab < MAX_CRABS ) ) |
|
{ |
|
if (m_bCrabs[nCrab] != bEnable) |
|
{ |
|
m_nCrabCount += bEnable ? 1 : -1; |
|
} |
|
|
|
m_bCrabs[nCrab] = bEnable; |
|
SetBodygroup( ZOMBIE_BODYGROUP_NEST_BASE + nCrab, bEnable ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::StopLoopingSounds( void ) |
|
{ |
|
ENVELOPE_CONTROLLER.SoundDestroy( m_pFastBreathSound ); |
|
m_pFastBreathSound = NULL; |
|
|
|
ENVELOPE_CONTROLLER.SoundDestroy( m_pSlowBreathSound ); |
|
m_pSlowBreathSound = NULL; |
|
|
|
BaseClass::StopLoopingSounds(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : info - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
if ( !( info.GetDamageType() & ( DMG_BLAST | DMG_ALWAYSGIB) ) ) |
|
{ |
|
EmitSound( "NPC_PoisonZombie.Die" ); |
|
} |
|
|
|
if ( !m_fIsTorso ) |
|
{ |
|
EvacuateNest(info.GetDamageType() == DMG_BLAST, info.GetDamage(), info.GetAttacker() ); |
|
} |
|
|
|
BaseClass::Event_Killed( info ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputInfo - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) |
|
{ |
|
// |
|
// Calculate what percentage of the creature's max health |
|
// this amount of damage represents (clips at 1.0). |
|
// |
|
float flDamagePercent = MIN( 1, inputInfo.GetDamage() / m_iMaxHealth ); |
|
|
|
// |
|
// Throw one crab for every 20% damage we take. |
|
// |
|
if ( flDamagePercent >= 0.2 ) |
|
{ |
|
m_flNextCrabThrowTime = gpGlobals->curtime; |
|
} |
|
|
|
return BaseClass::OnTakeDamage_Alive( inputInfo ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float CNPC_PoisonZombie::MaxYawSpeed( void ) |
|
{ |
|
return BaseClass::MaxYawSpeed(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
Class_T CNPC_PoisonZombie::Classify( void ) |
|
{ |
|
return CLASS_ZOMBIE; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// |
|
// NOTE: This function is still heavy with common code (found at the bottom). |
|
// we should consider moving some into the base class! (sjb) |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::SetZombieModel( void ) |
|
{ |
|
Hull_t lastHull = GetHullType(); |
|
|
|
if ( m_fIsTorso ) |
|
{ |
|
SetModel( "models/zombie/classic_torso.mdl" ); |
|
SetHullType(HULL_TINY); |
|
} |
|
else |
|
{ |
|
SetModel( "models/zombie/poison.mdl" ); |
|
SetHullType(HULL_HUMAN); |
|
} |
|
|
|
SetBodygroup( ZOMBIE_BODYGROUP_HEADCRAB, !m_fIsHeadless ); |
|
|
|
SetHullSizeNormal( true ); |
|
SetDefaultEyeOffset(); |
|
SetActivity( ACT_IDLE ); |
|
|
|
// hull changed size, notify vphysics |
|
// UNDONE: Solve this generally, systematically so other |
|
// NPCs can change size |
|
if ( lastHull != GetHullType() ) |
|
{ |
|
if ( VPhysicsGetObject() ) |
|
{ |
|
SetupVPhysicsHull(); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks conditions for letting a headcrab leap off our back at our enemy. |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::RangeAttack1Conditions( float flDot, float flDist ) |
|
{ |
|
if ( !m_nCrabCount ) |
|
{ |
|
//DevMsg("Range1: No crabs\n"); |
|
return 0; |
|
} |
|
|
|
if ( m_flNextCrabThrowTime > gpGlobals->curtime ) |
|
{ |
|
//DevMsg("Range1: Too soon\n"); |
|
return 0; |
|
} |
|
|
|
if ( flDist < ZOMBIE_HC_LEAP_RANGE_MIN ) |
|
{ |
|
//DevMsg("Range1: Too close to attack\n"); |
|
return COND_TOO_CLOSE_TO_ATTACK; |
|
} |
|
|
|
if ( flDist > ZOMBIE_HC_LEAP_RANGE_MAX ) |
|
{ |
|
//DevMsg("Range1: Too far to attack\n"); |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
|
|
if ( flDot < ZOMBIE_HC_LEAP_CONE ) |
|
{ |
|
//DevMsg("Range1: Not facing\n"); |
|
return COND_NOT_FACING_ATTACK; |
|
} |
|
|
|
m_nThrowCrab = RandomThrowCrab(); |
|
|
|
//DevMsg("*** Range1: Can range attack\n"); |
|
return COND_CAN_RANGE_ATTACK1; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks conditions for throwing a headcrab leap at our enemy. |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::RangeAttack2Conditions( float flDot, float flDist ) |
|
{ |
|
if ( !m_nCrabCount ) |
|
{ |
|
//DevMsg("Range2: No crabs\n"); |
|
return 0; |
|
} |
|
|
|
if ( m_flNextCrabThrowTime > gpGlobals->curtime ) |
|
{ |
|
//DevMsg("Range2: Too soon\n"); |
|
return 0; |
|
} |
|
|
|
if ( flDist < ZOMBIE_THROW_RANGE_MIN ) |
|
{ |
|
//DevMsg("Range2: Too close to attack\n"); |
|
return COND_TOO_CLOSE_TO_ATTACK; |
|
} |
|
|
|
if ( flDist > ZOMBIE_THROW_RANGE_MAX ) |
|
{ |
|
//DevMsg("Range2: Too far to attack\n"); |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
|
|
if ( flDot < ZOMBIE_THROW_CONE ) |
|
{ |
|
//DevMsg("Range2: Not facing\n"); |
|
return COND_NOT_FACING_ATTACK; |
|
} |
|
|
|
m_nThrowCrab = RandomThrowCrab(); |
|
|
|
//DevMsg("*** Range2: Can range attack\n"); |
|
return COND_CAN_RANGE_ATTACK2; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
Vector CNPC_PoisonZombie::HeadTarget( const Vector &posSrc ) |
|
{ |
|
int iCrabAttachment = LookupAttachment( "headcrab1" ); |
|
Assert( iCrabAttachment > 0 ); |
|
|
|
Vector vecPosition; |
|
|
|
GetAttachment( iCrabAttachment, vecPosition ); |
|
|
|
return vecPosition; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Turns off our breath so we can play another vocal sound. |
|
// TODO: pass in duration |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::BreatheOffShort( void ) |
|
{ |
|
if ( m_bNearEnemy ) |
|
{ |
|
ENVELOPE_CONTROLLER.SoundPlayEnvelope( m_pFastBreathSound, SOUNDCTRL_CHANGE_VOLUME, envPoisonZombieBreatheVolumeOffShort, ARRAYSIZE(envPoisonZombieBreatheVolumeOffShort) ); |
|
} |
|
else |
|
{ |
|
ENVELOPE_CONTROLLER.SoundPlayEnvelope( m_pSlowBreathSound, SOUNDCTRL_CHANGE_VOLUME, envPoisonZombieBreatheVolumeOffShort, ARRAYSIZE(envPoisonZombieBreatheVolumeOffShort) ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Catches the monster-specific events that occur when tagged animation |
|
// frames are played. |
|
// Input : pEvent - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::HandleAnimEvent( animevent_t *pEvent ) |
|
{ |
|
|
|
if ( pEvent->event == AE_ZOMBIE_POISON_PICKUP_CRAB ) |
|
{ |
|
EnableCrab( m_nThrowCrab, false ); |
|
SetBodygroup( ZOMBIE_BODYGROUP_THROW, 1 ); |
|
return; |
|
} |
|
|
|
if ( pEvent->event == AE_ZOMBIE_POISON_THROW_WARN_SOUND ) |
|
{ |
|
BreatheOffShort(); |
|
EmitSound( "NPC_PoisonZombie.ThrowWarn" ); |
|
return; |
|
} |
|
|
|
if ( pEvent->event == AE_ZOMBIE_POISON_THROW_SOUND ) |
|
{ |
|
BreatheOffShort(); |
|
EmitSound( "NPC_PoisonZombie.Throw" ); |
|
return; |
|
} |
|
|
|
if ( pEvent->event == AE_ZOMBIE_POISON_THROW_CRAB ) |
|
{ |
|
SetBodygroup( ZOMBIE_BODYGROUP_THROW, 0 ); |
|
|
|
CBlackHeadcrab *pCrab = (CBlackHeadcrab *)CreateNoSpawn( GetHeadcrabClassname(), EyePosition(), vec3_angle, this ); |
|
pCrab->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); |
|
|
|
// Fade if our parent is supposed to |
|
if ( HasSpawnFlags( SF_NPC_FADE_CORPSE ) ) |
|
{ |
|
pCrab->AddSpawnFlags( SF_NPC_FADE_CORPSE ); |
|
} |
|
|
|
// make me the crab's owner to avoid collision issues |
|
pCrab->SetOwnerEntity( this ); |
|
|
|
pCrab->Spawn(); |
|
|
|
pCrab->SetLocalAngles( GetLocalAngles() ); |
|
pCrab->SetActivity( ACT_RANGE_ATTACK1 ); |
|
pCrab->SetNextThink( gpGlobals->curtime ); |
|
pCrab->PhysicsSimulate(); |
|
|
|
pCrab->GetMotor()->SetIdealYaw( GetAbsAngles().y ); |
|
|
|
if ( IsOnFire() ) |
|
{ |
|
pCrab->Ignite( 100.0 ); |
|
} |
|
|
|
CBaseEntity *pEnemy = GetEnemy(); |
|
if ( pEnemy ) |
|
{ |
|
Vector vecEnemyEyePos = pEnemy->EyePosition(); |
|
pCrab->ThrowAt( vecEnemyEyePos ); |
|
} |
|
|
|
if (m_nCrabCount == 0) |
|
{ |
|
CapabilitiesRemove( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 ); |
|
} |
|
|
|
m_flNextCrabThrowTime = gpGlobals->curtime + random->RandomInt( ZOMBIE_THROW_MIN_DELAY, ZOMBIE_THROW_MAX_DELAY ); |
|
return; |
|
} |
|
|
|
if ( pEvent->event == AE_ZOMBIE_POISON_SPIT ) |
|
{ |
|
Vector forward; |
|
QAngle qaPunch( 45, random->RandomInt(-5, 5), random->RandomInt(-5, 5) ); |
|
AngleVectors( GetLocalAngles(), &forward ); |
|
forward = forward * 200; |
|
ClawAttack( GetClawAttackRange(), sk_zombie_poison_dmg_spit.GetFloat(), qaPunch, forward, ZOMBIE_BLOOD_BITE ); |
|
return; |
|
} |
|
|
|
BaseClass::HandleAnimEvent( pEvent ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns the index of a randomly chosen crab to throw. |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::RandomThrowCrab( void ) |
|
{ |
|
// FIXME: this could take a long time, theoretically |
|
int nCrab = -1; |
|
do |
|
{ |
|
int nTest = random->RandomInt( 0, 2 ); |
|
if ( m_bCrabs[nTest] ) |
|
{ |
|
nCrab = nTest; |
|
} |
|
} while ( nCrab == -1 ); |
|
|
|
return nCrab; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: The nest is dead! Evacuate the nest! |
|
// Input : bExplosion - We were evicted by an explosion so we should go a-flying. |
|
// flDamage - The damage that was done to cause the evacuation. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::EvacuateNest( bool bExplosion, float flDamage, CBaseEntity *pAttacker ) |
|
{ |
|
// HACK: if we were in mid-throw, drop the throwing crab also. |
|
if ( GetBodygroup( ZOMBIE_BODYGROUP_THROW ) ) |
|
{ |
|
SetBodygroup( ZOMBIE_BODYGROUP_THROW, 0 ); |
|
m_nCrabCount++; |
|
} |
|
|
|
for( int i = 0; i < MAX_CRABS ; i++ ) |
|
{ |
|
if( m_bCrabs[i] ) |
|
{ |
|
Vector vecPosition; |
|
QAngle vecAngles; |
|
|
|
char szAttachment[64]; |
|
|
|
switch( i ) |
|
{ |
|
case 0: |
|
strcpy( szAttachment, "headcrab2" ); |
|
break; |
|
case 1: |
|
strcpy( szAttachment, "headcrab3" ); |
|
break; |
|
case 2: |
|
strcpy( szAttachment, "headcrab4" ); |
|
break; |
|
} |
|
|
|
GetAttachment( szAttachment, vecPosition, vecAngles ); |
|
|
|
// Now slam the angles because the attachment point will have pitch and roll, which we can't use. |
|
vecAngles = QAngle( 0, random->RandomFloat( 0, 360 ), 0 ); |
|
|
|
CBlackHeadcrab *pCrab = (CBlackHeadcrab *)CreateNoSpawn( GetHeadcrabClassname(), vecPosition, vecAngles, this ); |
|
pCrab->Spawn(); |
|
|
|
if( !HeadcrabFits(pCrab) ) |
|
{ |
|
UTIL_Remove(pCrab); |
|
continue; |
|
} |
|
|
|
float flVelocityScale = 2.0f; |
|
if ( bExplosion && ( flDamage > 10 ) ) |
|
{ |
|
flVelocityScale = 0.1 * flDamage; |
|
} |
|
|
|
if (IsOnFire()) |
|
{ |
|
pCrab->Ignite( 100.0 ); |
|
} |
|
|
|
pCrab->Eject( vecAngles, flVelocityScale, pAttacker ); |
|
EnableCrab( i, false ); |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::PrescheduleThink( void ) |
|
{ |
|
if ( HasCondition( COND_NEW_ENEMY ) ) |
|
{ |
|
m_flNextCrabThrowTime = gpGlobals->curtime + random->RandomInt( ZOMBIE_THROW_FIRST_MIN_DELAY, ZOMBIE_THROW_FIRST_MAX_DELAY ); |
|
} |
|
|
|
bool bNearEnemy = false; |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
float flDist = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()).Length(); |
|
if ( flDist < ZOMBIE_ENEMY_BREATHE_DIST ) |
|
{ |
|
bNearEnemy = true; |
|
} |
|
} |
|
|
|
if ( bNearEnemy ) |
|
{ |
|
if ( !m_bNearEnemy ) |
|
{ |
|
// Our enemy is nearby. Breathe faster. |
|
float duration = random->RandomFloat( 1.0f, 2.0f ); |
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pFastBreathSound, BREATH_VOL_MAX, duration ); |
|
ENVELOPE_CONTROLLER.SoundChangePitch( m_pFastBreathSound, random->RandomInt( 100, 120 ), random->RandomFloat( 1.0f, 2.0f ) ); |
|
|
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pSlowBreathSound, 0.0f, duration ); |
|
|
|
m_bNearEnemy = true; |
|
} |
|
} |
|
else if ( m_bNearEnemy ) |
|
{ |
|
// Our enemy is far away. Slow our breathing down. |
|
float duration = random->RandomFloat( 2.0f, 4.0f ); |
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pFastBreathSound, BREATH_VOL_MAX, duration ); |
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pSlowBreathSound, 0.0f, duration ); |
|
// ENVELOPE_CONTROLLER.SoundChangePitch( m_pBreathSound, random->RandomInt( 80, 100 ), duration ); |
|
|
|
m_bNearEnemy = false; |
|
} |
|
|
|
BaseClass::PrescheduleThink(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Allows for modification of the interrupt mask for the current schedule. |
|
// In the most cases the base implementation should be called first. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::BuildScheduleTestBits( void ) |
|
{ |
|
BaseClass::BuildScheduleTestBits(); |
|
|
|
if ( IsCurSchedule( SCHED_CHASE_ENEMY ) ) |
|
{ |
|
SetCustomInterruptCondition( COND_LIGHT_DAMAGE ); |
|
SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); |
|
} |
|
else if ( IsCurSchedule( SCHED_RANGE_ATTACK1 ) || IsCurSchedule( SCHED_RANGE_ATTACK2 ) ) |
|
{ |
|
ClearCustomInterruptCondition( COND_LIGHT_DAMAGE ); |
|
ClearCustomInterruptCondition( COND_HEAVY_DAMAGE ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::SelectFailSchedule( int nFailedSchedule, int nFailedTask, AI_TaskFailureCode_t eTaskFailCode ) |
|
{ |
|
int nSchedule = BaseClass::SelectFailSchedule( nFailedSchedule, nFailedTask, eTaskFailCode ); |
|
|
|
if ( nSchedule == SCHED_CHASE_ENEMY_FAILED && m_nCrabCount > 0 ) |
|
{ |
|
return SCHED_ESTABLISH_LINE_OF_FIRE; |
|
} |
|
|
|
return nSchedule; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::SelectSchedule( void ) |
|
{ |
|
int nSchedule = BaseClass::SelectSchedule(); |
|
|
|
if ( nSchedule == SCHED_SMALL_FLINCH ) |
|
{ |
|
m_flNextFlinchTime = gpGlobals->curtime + random->RandomFloat( 1, 3 ); |
|
} |
|
|
|
return nSchedule; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : scheduleType - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CNPC_PoisonZombie::TranslateSchedule( int scheduleType ) |
|
{ |
|
if ( scheduleType == SCHED_RANGE_ATTACK2 ) |
|
{ |
|
return SCHED_ZOMBIE_POISON_RANGE_ATTACK2; |
|
} |
|
|
|
if ( scheduleType == SCHED_RANGE_ATTACK1 ) |
|
{ |
|
return SCHED_ZOMBIE_POISON_RANGE_ATTACK1; |
|
} |
|
|
|
if ( scheduleType == SCHED_COMBAT_FACE && IsUnreachable( GetEnemy() ) ) |
|
return SCHED_TAKE_COVER_FROM_ENEMY; |
|
|
|
// We'd simply like to shamble towards our enemy |
|
if ( scheduleType == SCHED_MOVE_TO_WEAPON_RANGE ) |
|
return SCHED_CHASE_ENEMY; |
|
|
|
return BaseClass::TranslateSchedule( scheduleType ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_PoisonZombie::ShouldPlayIdleSound( void ) |
|
{ |
|
return CAI_BaseNPC::ShouldPlayIdleSound(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random attack hit sound |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::AttackHitSound( void ) |
|
{ |
|
EmitSound( "Zombie.AttackHit" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random attack miss sound |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::AttackMissSound( void ) |
|
{ |
|
EmitSound( "Zombie.AttackMiss" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random attack sound. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::AttackSound( void ) |
|
{ |
|
EmitSound( "NPC_PoisonZombie.Attack" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random idle sound. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::IdleSound( void ) |
|
{ |
|
// HACK: base zombie code calls IdleSound even when not idle! |
|
if ( m_NPCState != NPC_STATE_COMBAT ) |
|
{ |
|
BreatheOffShort(); |
|
EmitSound( "NPC_PoisonZombie.Idle" ); |
|
MakeAISpookySound( 360.0f ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random pain sound. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::PainSound( const CTakeDamageInfo &info ) |
|
{ |
|
// Don't make pain sounds too often. |
|
if ( m_flNextPainSoundTime <= gpGlobals->curtime ) |
|
{ |
|
BreatheOffShort(); |
|
EmitSound( "NPC_PoisonZombie.Pain" ); |
|
m_flNextPainSoundTime = gpGlobals->curtime + random->RandomFloat( 4.0, 7.0 ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Play a random alert sound. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::AlertSound( void ) |
|
{ |
|
BreatheOffShort(); |
|
|
|
EmitSound( "NPC_PoisonZombie.Alert" ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Sound of a footstep |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::FootstepSound( bool fRightFoot ) |
|
{ |
|
if( fRightFoot ) |
|
{ |
|
EmitSound( "NPC_PoisonZombie.FootstepRight" ); |
|
} |
|
else |
|
{ |
|
EmitSound( "NPC_PoisonZombie.FootstepLeft" ); |
|
} |
|
|
|
if( ShouldPlayFootstepMoan() ) |
|
{ |
|
m_flNextMoanSound = gpGlobals->curtime; |
|
MoanSound( envPoisonZombieMoanVolumeFast, ARRAYSIZE( envPoisonZombieMoanVolumeFast ) ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: If we don't have any headcrabs to throw, we must close to attack our enemy. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_PoisonZombie::MustCloseToAttack(void) |
|
{ |
|
return (m_nCrabCount == 0); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Open a window and let a little bit of the looping moan sound |
|
// come through. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_PoisonZombie::MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize ) |
|
{ |
|
if( !m_pMoanSound ) |
|
{ |
|
// Don't set this up until the code calls for it. |
|
const char *pszSound = GetMoanSound( m_iMoanSound ); |
|
m_flMoanPitch = random->RandomInt( 98, 110 ); |
|
|
|
CPASAttenuationFilter filter( this, 1.5 ); |
|
//m_pMoanSound = ENVELOPE_CONTROLLER.SoundCreate( entindex(), CHAN_STATIC, pszSound, ATTN_NORM ); |
|
m_pMoanSound = ENVELOPE_CONTROLLER.SoundCreate( filter, entindex(), CHAN_STATIC, pszSound, 1.5 ); |
|
|
|
ENVELOPE_CONTROLLER.Play( m_pMoanSound, 0.5, m_flMoanPitch ); |
|
} |
|
|
|
envPoisonZombieMoanVolumeFast[ 1 ].durationMin = 0.1; |
|
envPoisonZombieMoanVolumeFast[ 1 ].durationMax = 0.4; |
|
|
|
if ( random->RandomInt( 1, 2 ) == 1 ) |
|
{ |
|
IdleSound(); |
|
} |
|
|
|
float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope( m_pMoanSound, SOUNDCTRL_CHANGE_VOLUME, pEnvelope, iEnvelopeSize ); |
|
|
|
float flPitchShift = random->RandomInt( -4, 4 ); |
|
ENVELOPE_CONTROLLER.SoundChangePitch( m_pMoanSound, m_flMoanPitch + flPitchShift, 0.3 ); |
|
|
|
m_flNextMoanSound = gpGlobals->curtime + duration + 9999; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Overloaded so that explosions don't split the poison zombie in twain. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_PoisonZombie::ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold ) |
|
{ |
|
return false; |
|
} |
|
|
|
|
|
int ACT_ZOMBIE_POISON_THREAT; |
|
|
|
|
|
AI_BEGIN_CUSTOM_NPC( npc_poisonzombie, CNPC_PoisonZombie ) |
|
|
|
DECLARE_ACTIVITY( ACT_ZOMBIE_POISON_THREAT ) |
|
|
|
//Adrian: events go here |
|
DECLARE_ANIMEVENT( AE_ZOMBIE_POISON_THROW_WARN_SOUND ) |
|
DECLARE_ANIMEVENT( AE_ZOMBIE_POISON_PICKUP_CRAB ) |
|
DECLARE_ANIMEVENT( AE_ZOMBIE_POISON_THROW_SOUND ) |
|
DECLARE_ANIMEVENT( AE_ZOMBIE_POISON_THROW_CRAB ) |
|
DECLARE_ANIMEVENT( AE_ZOMBIE_POISON_SPIT ) |
|
|
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_ZOMBIE_POISON_RANGE_ATTACK2, |
|
|
|
" Tasks" |
|
" TASK_STOP_MOVING 0" |
|
" TASK_FACE_IDEAL 0" |
|
" TASK_PLAY_PRIVATE_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_ZOMBIE_POISON_THREAT" |
|
" TASK_FACE_IDEAL 0" |
|
" TASK_RANGE_ATTACK2 0" |
|
|
|
" Interrupts" |
|
" COND_NO_PRIMARY_AMMO" |
|
) |
|
|
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_ZOMBIE_POISON_RANGE_ATTACK1, |
|
|
|
" Tasks" |
|
" TASK_STOP_MOVING 0" |
|
" TASK_FACE_ENEMY 0" |
|
" TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack |
|
" TASK_RANGE_ATTACK1 0" |
|
"" |
|
" Interrupts" |
|
" COND_NO_PRIMARY_AMMO" |
|
) |
|
|
|
AI_END_CUSTOM_NPC() |
|
|
|
|
|
|