//========= 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 { DECLARE_CLASS( CNPC_PoisonZombie, CAI_BlendingHost ); 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()