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