//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Combine Zombie... Zombie Combine... its like a... Zombine... get it? // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "ai_basenpc.h" #include "ai_default.h" #include "ai_schedule.h" #include "ai_hull.h" #include "ai_motor.h" #include "ai_memory.h" #include "ai_route.h" #include "ai_squad.h" #include "soundent.h" #include "game.h" #include "npcevent.h" #include "entitylist.h" #include "ai_task.h" #include "activitylist.h" #include "engine/IEngineSound.h" #include "npc_BaseZombie.h" #include "movevars_shared.h" #include "IEffects.h" #include "props.h" #include "physics_npc_solver.h" #include "hl2_player.h" #include "hl2_gamerules.h" #include "basecombatweapon.h" #include "basegrenade_shared.h" #include "grenade_frag.h" #include "ai_interactions.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" enum { SQUAD_SLOT_ZOMBINE_SPRINT1 = LAST_SHARED_SQUADSLOT, SQUAD_SLOT_ZOMBINE_SPRINT2, }; #define MIN_SPRINT_TIME 3.5f #define MAX_SPRINT_TIME 5.5f #define MIN_SPRINT_DISTANCE 64.0f #define MAX_SPRINT_DISTANCE 1024.0f #define SPRINT_CHANCE_VALUE 10 #define SPRINT_CHANCE_VALUE_DARKNESS 50 #define GRENADE_PULL_MAX_DISTANCE 256.0f #define ZOMBINE_MAX_GRENADES 1 int ACT_ZOMBINE_GRENADE_PULL; int ACT_ZOMBINE_GRENADE_WALK; int ACT_ZOMBINE_GRENADE_RUN; int ACT_ZOMBINE_GRENADE_IDLE; int ACT_ZOMBINE_ATTACK_FAST; int ACT_ZOMBINE_GRENADE_FLINCH_BACK; int ACT_ZOMBINE_GRENADE_FLINCH_FRONT; int ACT_ZOMBINE_GRENADE_FLINCH_WEST; int ACT_ZOMBINE_GRENADE_FLINCH_EAST; int AE_ZOMBINE_PULLPIN; extern bool IsAlyxInDarknessMode(); ConVar sk_zombie_soldier_health( "sk_zombie_soldier_health","0"); float g_flZombineGrenadeTimes = 0; class CNPC_Zombine : public CAI_BlendingHost, public CDefaultPlayerPickupVPhysics { DECLARE_DATADESC(); DECLARE_CLASS( CNPC_Zombine, CAI_BlendingHost ); public: void Spawn( void ); void Precache( void ); void SetZombieModel( void ); virtual void PrescheduleThink( void ); virtual int SelectSchedule( void ); virtual void BuildScheduleTestBits( void ); virtual void HandleAnimEvent( animevent_t *pEvent ); virtual const char *GetLegsModel( void ); virtual const char *GetTorsoModel( void ); virtual const char *GetHeadcrabClassname( void ); virtual const char *GetHeadcrabModel( void ); virtual void PainSound( const CTakeDamageInfo &info ); virtual void DeathSound( const CTakeDamageInfo &info ); virtual void AlertSound( void ); virtual void IdleSound( void ); virtual void AttackSound( void ); virtual void AttackHitSound( void ); virtual void AttackMissSound( void ); virtual void FootstepSound( bool fRightFoot ); virtual void FootscuffSound( bool fRightFoot ); virtual void MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize ); virtual void Event_Killed( const CTakeDamageInfo &info ); virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); virtual void RunTask( const Task_t *pTask ); virtual int MeleeAttack1Conditions ( float flDot, float flDist ); virtual bool ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold ); virtual void OnScheduleChange ( void ); virtual bool CanRunAScriptedNPCInteraction( bool bForced ); void GatherGrenadeConditions( void ); virtual Activity NPC_TranslateActivity( Activity baseAct ); const char *GetMoanSound( int nSound ); bool AllowedToSprint( void ); void Sprint( bool bMadSprint = false ); void StopSprint( void ); void DropGrenade( Vector vDir ); bool IsSprinting( void ) { return m_flSprintTime > gpGlobals->curtime; } bool HasGrenade( void ) { return m_hGrenade != NULL; } int TranslateSchedule( int scheduleType ); void InputStartSprint ( inputdata_t &inputdata ); void InputPullGrenade ( inputdata_t &inputdata ); virtual CBaseEntity *OnFailedPhysGunPickup ( Vector vPhysgunPos ); //Called when we want to let go of a grenade and let the physcannon pick it up. void ReleaseGrenade( Vector vPhysgunPos ); virtual bool HandleInteraction( int interactionType, void *data, CBaseCombatCharacter *sourceEnt ); enum { COND_ZOMBINE_GRENADE = LAST_BASE_ZOMBIE_CONDITION, }; enum { SCHED_ZOMBINE_PULL_GRENADE = LAST_BASE_ZOMBIE_SCHEDULE, }; public: DEFINE_CUSTOM_AI; private: float m_flSprintTime; float m_flSprintRestTime; float m_flSuperFastAttackTime; float m_flGrenadePullTime; int m_iGrenadeCount; EHANDLE m_hGrenade; protected: static const char *pMoanSounds[]; }; LINK_ENTITY_TO_CLASS( npc_zombine, CNPC_Zombine ); BEGIN_DATADESC( CNPC_Zombine ) DEFINE_FIELD( m_flSprintTime, FIELD_TIME ), DEFINE_FIELD( m_flSprintRestTime, FIELD_TIME ), DEFINE_FIELD( m_flSuperFastAttackTime, FIELD_TIME ), DEFINE_FIELD( m_hGrenade, FIELD_EHANDLE ), DEFINE_FIELD( m_flGrenadePullTime, FIELD_TIME ), DEFINE_FIELD( m_iGrenadeCount, FIELD_INTEGER ), DEFINE_INPUTFUNC( FIELD_VOID, "StartSprint", InputStartSprint ), DEFINE_INPUTFUNC( FIELD_VOID, "PullGrenade", InputPullGrenade ), END_DATADESC() //--------------------------------------------------------- //--------------------------------------------------------- const char *CNPC_Zombine::pMoanSounds[] = { "ATV_engine_null", }; void CNPC_Zombine::Spawn( void ) { Precache(); m_fIsTorso = false; m_fIsHeadless = false; #ifdef HL2_EPISODIC SetBloodColor( BLOOD_COLOR_ZOMBIE ); #else SetBloodColor( BLOOD_COLOR_GREEN ); #endif // HL2_EPISODIC m_iHealth = sk_zombie_soldier_health.GetFloat(); SetMaxHealth( m_iHealth ); m_flFieldOfView = 0.2; CapabilitiesClear(); BaseClass::Spawn(); m_flSprintTime = 0.0f; m_flSprintRestTime = 0.0f; m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 1.0, 4.0 ); g_flZombineGrenadeTimes = gpGlobals->curtime; m_flGrenadePullTime = gpGlobals->curtime; m_iGrenadeCount = ZOMBINE_MAX_GRENADES; } void CNPC_Zombine::Precache( void ) { BaseClass::Precache(); PrecacheModel( "models/zombie/zombie_soldier.mdl" ); PrecacheScriptSound( "Zombie.FootstepRight" ); PrecacheScriptSound( "Zombie.FootstepLeft" ); PrecacheScriptSound( "Zombine.ScuffRight" ); PrecacheScriptSound( "Zombine.ScuffLeft" ); PrecacheScriptSound( "Zombie.AttackHit" ); PrecacheScriptSound( "Zombie.AttackMiss" ); PrecacheScriptSound( "Zombine.Pain" ); PrecacheScriptSound( "Zombine.Die" ); PrecacheScriptSound( "Zombine.Alert" ); PrecacheScriptSound( "Zombine.Idle" ); PrecacheScriptSound( "Zombine.ReadyGrenade" ); PrecacheScriptSound( "ATV_engine_null" ); PrecacheScriptSound( "Zombine.Charge" ); PrecacheScriptSound( "Zombie.Attack" ); } void CNPC_Zombine::SetZombieModel( void ) { SetModel( "models/zombie/zombie_soldier.mdl" ); SetHullType( HULL_HUMAN ); SetBodygroup( ZOMBIE_BODYGROUP_HEADCRAB, !m_fIsHeadless ); SetHullSizeNormal( true ); SetDefaultEyeOffset(); SetActivity( ACT_IDLE ); } void CNPC_Zombine::PrescheduleThink( void ) { GatherGrenadeConditions(); if( gpGlobals->curtime > m_flNextMoanSound ) { if( CanPlayMoanSound() ) { // Classic guy idles instead of moans. IdleSound(); m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 10.0, 15.0 ); } else { m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 2.5, 5.0 ); } } if ( HasGrenade () ) { CSoundEnt::InsertSound ( SOUND_DANGER, GetAbsOrigin() + GetSmoothedVelocity() * 0.5f , 256, 0.1, this, SOUNDENT_CHANNEL_ZOMBINE_GRENADE ); if( IsSprinting() && GetEnemy() && GetEnemy()->Classify() == CLASS_PLAYER_ALLY_VITAL && HasCondition( COND_SEE_ENEMY ) ) { if( GetAbsOrigin().DistToSqr(GetEnemy()->GetAbsOrigin()) < Square( 144 ) ) { StopSprint(); } } } BaseClass::PrescheduleThink(); } void CNPC_Zombine::OnScheduleChange( void ) { if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) && IsSprinting() == true ) { m_flSuperFastAttackTime = gpGlobals->curtime + 1.0f; } BaseClass::OnScheduleChange(); } bool CNPC_Zombine::CanRunAScriptedNPCInteraction( bool bForced ) { if ( HasGrenade() == true ) return false; return BaseClass::CanRunAScriptedNPCInteraction( bForced ); } int CNPC_Zombine::SelectSchedule( void ) { if ( GetHealth() <= 0 ) return BaseClass::SelectSchedule(); if ( HasCondition( COND_ZOMBINE_GRENADE ) ) { ClearCondition( COND_ZOMBINE_GRENADE ); return SCHED_ZOMBINE_PULL_GRENADE; } return BaseClass::SelectSchedule(); } void CNPC_Zombine::BuildScheduleTestBits( void ) { BaseClass::BuildScheduleTestBits(); SetCustomInterruptCondition( COND_ZOMBINE_GRENADE ); } Activity CNPC_Zombine::NPC_TranslateActivity( Activity baseAct ) { if ( baseAct == ACT_MELEE_ATTACK1 ) { if ( m_flSuperFastAttackTime > gpGlobals->curtime || HasGrenade() ) { return (Activity)ACT_ZOMBINE_ATTACK_FAST; } } if ( baseAct == ACT_IDLE ) { if ( HasGrenade() ) { return (Activity)ACT_ZOMBINE_GRENADE_IDLE; } } return BaseClass::NPC_TranslateActivity( baseAct ); } int CNPC_Zombine::MeleeAttack1Conditions ( float flDot, float flDist ) { int iBase = BaseClass::MeleeAttack1Conditions( flDot, flDist ); if( HasGrenade() ) { //Adrian: stop spriting if we get close enough to melee and we have a grenade //this gives NPCs time to move away from you (before it was almost impossible cause of the high sprint speed) if ( iBase == COND_CAN_MELEE_ATTACK1 ) { StopSprint(); } } return iBase; } void CNPC_Zombine::GatherGrenadeConditions( void ) { if ( m_iGrenadeCount <= 0 ) return; if ( g_flZombineGrenadeTimes > gpGlobals->curtime ) return; if ( m_flGrenadePullTime > gpGlobals->curtime ) return; if ( m_flSuperFastAttackTime >= gpGlobals->curtime ) return; if ( HasGrenade() ) return; if ( GetEnemy() == NULL ) return; if ( FVisible( GetEnemy() ) == false ) return; if ( IsSprinting() ) return; if ( IsOnFire() ) return; if ( IsRunningDynamicInteraction() == true ) return; if ( m_ActBusyBehavior.IsActive() ) return; CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && pPlayer->FVisible( this ) ) { float flLengthToPlayer = (pPlayer->GetAbsOrigin() - GetAbsOrigin()).Length(); float flLengthToEnemy = flLengthToPlayer; if ( pPlayer != GetEnemy() ) { flLengthToEnemy = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin()).Length(); } if ( flLengthToPlayer <= GRENADE_PULL_MAX_DISTANCE && flLengthToEnemy <= GRENADE_PULL_MAX_DISTANCE ) { float flPullChance = 1.0f - ( flLengthToEnemy / GRENADE_PULL_MAX_DISTANCE ); m_flGrenadePullTime = gpGlobals->curtime + 0.5f; if ( flPullChance >= random->RandomFloat( 0.0f, 1.0f ) ) { g_flZombineGrenadeTimes = gpGlobals->curtime + 10.0f; SetCondition( COND_ZOMBINE_GRENADE ); } } } } int CNPC_Zombine::TranslateSchedule( int scheduleType ) { return BaseClass::TranslateSchedule( scheduleType ); } void CNPC_Zombine::DropGrenade( Vector vDir ) { if ( m_hGrenade == NULL ) return; m_hGrenade->SetParent( NULL ); m_hGrenade->SetOwnerEntity( NULL ); Vector vGunPos; QAngle angles; GetAttachment( "grenade_attachment", vGunPos, angles ); IPhysicsObject *pPhysObj = m_hGrenade->VPhysicsGetObject(); if ( pPhysObj == NULL ) { m_hGrenade->SetMoveType( MOVETYPE_VPHYSICS ); m_hGrenade->SetSolid( SOLID_VPHYSICS ); m_hGrenade->SetCollisionGroup( COLLISION_GROUP_WEAPON ); m_hGrenade->CreateVPhysics(); } if ( pPhysObj ) { pPhysObj->Wake(); pPhysObj->SetPosition( vGunPos, angles, true ); pPhysObj->ApplyForceCenter( vDir * 0.2f ); pPhysObj->RecheckCollisionFilter(); } m_hGrenade = NULL; } void CNPC_Zombine::Event_Killed( const CTakeDamageInfo &info ) { BaseClass::Event_Killed( info ); if ( HasGrenade() ) { DropGrenade( vec3_origin ); } } //----------------------------------------------------------------------------- // Purpose: This is a generic function (to be implemented by sub-classes) to // handle specific interactions between different types of characters // (For example the barnacle grabbing an NPC) // Input : Constant for the type of interaction // Output : true - if sub-class has a response for the interaction // false - if sub-class has no response //----------------------------------------------------------------------------- bool CNPC_Zombine::HandleInteraction( int interactionType, void *data, CBaseCombatCharacter *sourceEnt ) { if ( interactionType == g_interactionBarnacleVictimGrab ) { if ( HasGrenade() ) { DropGrenade( vec3_origin ); } } return BaseClass::HandleInteraction( interactionType, data, sourceEnt ); } void CNPC_Zombine::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); //Only knock grenades off their hands if it's a player doing the damage. if ( info.GetAttacker() && info.GetAttacker()->IsNPC() ) return; if ( info.GetDamageType() & ( DMG_BULLET | DMG_CLUB ) ) { if ( ptr->hitgroup == HITGROUP_LEFTARM ) { if ( HasGrenade() ) { DropGrenade( info.GetDamageForce() ); StopSprint(); } } } } void CNPC_Zombine::HandleAnimEvent( animevent_t *pEvent ) { if ( pEvent->event == AE_ZOMBINE_PULLPIN ) { Vector vecStart; QAngle angles; GetAttachment( "grenade_attachment", vecStart, angles ); CBaseGrenade *pGrenade = Fraggrenade_Create( vecStart, vec3_angle, vec3_origin, AngularImpulse( 0, 0, 0 ), this, 3.5f, true ); if ( pGrenade ) { // Move physobject to shadow IPhysicsObject *pPhysicsObject = pGrenade->VPhysicsGetObject(); if ( pPhysicsObject ) { pGrenade->VPhysicsDestroyObject(); int iAttachment = LookupAttachment( "grenade_attachment"); pGrenade->SetMoveType( MOVETYPE_NONE ); pGrenade->SetSolid( SOLID_NONE ); pGrenade->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); pGrenade->SetAbsOrigin( vecStart ); pGrenade->SetAbsAngles( angles ); pGrenade->SetParent( this, iAttachment ); pGrenade->SetDamage( 200.0f ); m_hGrenade = pGrenade; EmitSound( "Zombine.ReadyGrenade" ); // Tell player allies nearby to regard me! CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); CAI_BaseNPC *pNPC; for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) { pNPC = ppAIs[i]; if( pNPC->Classify() == CLASS_PLAYER_ALLY || ( pNPC->Classify() == CLASS_PLAYER_ALLY_VITAL && pNPC->FVisible(this) ) ) { int priority; Disposition_t disposition; priority = pNPC->IRelationPriority(this); disposition = pNPC->IRelationType(this); pNPC->AddEntityRelationship( this, disposition, priority + 1 ); } } } m_iGrenadeCount--; } return; } if ( pEvent->event == AE_NPC_ATTACK_BROADCAST ) { if ( HasGrenade() ) return; } BaseClass::HandleAnimEvent( pEvent ); } bool CNPC_Zombine::AllowedToSprint( void ) { if ( IsOnFire() ) return false; //If you're sprinting then there's no reason to sprint again. if ( IsSprinting() ) return false; int iChance = SPRINT_CHANCE_VALUE; CHL2_Player *pPlayer = dynamic_cast ( AI_GetSinglePlayer() ); if ( pPlayer ) { if ( HL2GameRules()->IsAlyxInDarknessMode() && pPlayer->FlashlightIsOn() == false ) { iChance = SPRINT_CHANCE_VALUE_DARKNESS; } //Bigger chance of this happening if the player is not looking at the zombie if ( pPlayer->FInViewCone( this ) == false ) { iChance *= 2; } } if ( HasGrenade() ) { iChance *= 4; } //Below 25% health they'll always sprint if ( ( GetHealth() > GetMaxHealth() * 0.5f ) ) { if ( IsStrategySlotRangeOccupied( SQUAD_SLOT_ZOMBINE_SPRINT1, SQUAD_SLOT_ZOMBINE_SPRINT2 ) == true ) return false; if ( random->RandomInt( 0, 100 ) > iChance ) return false; if ( m_flSprintRestTime > gpGlobals->curtime ) return false; } float flLength = ( GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter() ).Length(); if ( flLength > MAX_SPRINT_DISTANCE ) return false; return true; } void CNPC_Zombine::StopSprint( void ) { GetNavigator()->SetMovementActivity( ACT_WALK ); m_flSprintTime = gpGlobals->curtime; m_flSprintRestTime = m_flSprintTime + random->RandomFloat( 2.5f, 5.0f ); } void CNPC_Zombine::Sprint( bool bMadSprint ) { if ( IsSprinting() ) return; OccupyStrategySlotRange( SQUAD_SLOT_ZOMBINE_SPRINT1, SQUAD_SLOT_ZOMBINE_SPRINT2 ); GetNavigator()->SetMovementActivity( ACT_RUN ); float flSprintTime = random->RandomFloat( MIN_SPRINT_TIME, MAX_SPRINT_TIME ); //If holding a grenade then sprint until it blows up. if ( HasGrenade() || bMadSprint == true ) { flSprintTime = 9999; } m_flSprintTime = gpGlobals->curtime + flSprintTime; //Don't sprint for this long after I'm done with this sprint run. m_flSprintRestTime = m_flSprintTime + random->RandomFloat( 2.5f, 5.0f ); EmitSound( "Zombine.Charge" ); } void CNPC_Zombine::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_WAIT_FOR_MOVEMENT_STEP: case TASK_WAIT_FOR_MOVEMENT: { BaseClass::RunTask( pTask ); if ( IsOnFire() && IsSprinting() ) { StopSprint(); } //Only do this if I have an enemy if ( GetEnemy() ) { if ( AllowedToSprint() == true ) { Sprint( ( GetHealth() <= GetMaxHealth() * 0.5f ) ); return; } if ( HasGrenade() ) { if ( IsSprinting() ) { GetNavigator()->SetMovementActivity( (Activity)ACT_ZOMBINE_GRENADE_RUN ); } else { GetNavigator()->SetMovementActivity( (Activity)ACT_ZOMBINE_GRENADE_WALK ); } return; } if ( GetNavigator()->GetMovementActivity() != ACT_WALK ) { if ( IsSprinting() == false ) { GetNavigator()->SetMovementActivity( ACT_WALK ); } } } else { GetNavigator()->SetMovementActivity( ACT_WALK ); } break; } default: { BaseClass::RunTask( pTask ); break; } } } void CNPC_Zombine::InputStartSprint ( inputdata_t &inputdata ) { Sprint(); } void CNPC_Zombine::InputPullGrenade ( inputdata_t &inputdata ) { g_flZombineGrenadeTimes = gpGlobals->curtime + 5.0f; SetCondition( COND_ZOMBINE_GRENADE ); } //----------------------------------------------------------------------------- // Purpose: Returns a moan sound for this class of zombie. //----------------------------------------------------------------------------- const char *CNPC_Zombine::GetMoanSound( int nSound ) { return pMoanSounds[ nSound % ARRAYSIZE( pMoanSounds ) ]; } //----------------------------------------------------------------------------- // Purpose: Sound of a footstep //----------------------------------------------------------------------------- void CNPC_Zombine::FootstepSound( bool fRightFoot ) { if( fRightFoot ) { EmitSound( "Zombie.FootstepRight" ); } else { EmitSound( "Zombie.FootstepLeft" ); } } //----------------------------------------------------------------------------- // Purpose: Overloaded so that explosions don't split the zombine in twain. //----------------------------------------------------------------------------- bool CNPC_Zombine::ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold ) { return false; } //----------------------------------------------------------------------------- // Purpose: Sound of a foot sliding/scraping //----------------------------------------------------------------------------- void CNPC_Zombine::FootscuffSound( bool fRightFoot ) { if( fRightFoot ) { EmitSound( "Zombine.ScuffRight" ); } else { EmitSound( "Zombine.ScuffLeft" ); } } //----------------------------------------------------------------------------- // Purpose: Play a random attack hit sound //----------------------------------------------------------------------------- void CNPC_Zombine::AttackHitSound( void ) { EmitSound( "Zombie.AttackHit" ); } //----------------------------------------------------------------------------- // Purpose: Play a random attack miss sound //----------------------------------------------------------------------------- void CNPC_Zombine::AttackMissSound( void ) { // Play a random attack miss sound EmitSound( "Zombie.AttackMiss" ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Zombine::PainSound( const CTakeDamageInfo &info ) { // We're constantly taking damage when we are on fire. Don't make all those noises! if ( IsOnFire() ) { return; } EmitSound( "Zombine.Pain" ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Zombine::DeathSound( const CTakeDamageInfo &info ) { EmitSound( "Zombine.Die" ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Zombine::AlertSound( void ) { EmitSound( "Zombine.Alert" ); // Don't let a moan sound cut off the alert sound. m_flNextMoanSound += random->RandomFloat( 2.0, 4.0 ); } //----------------------------------------------------------------------------- // Purpose: Play a random idle sound. //----------------------------------------------------------------------------- void CNPC_Zombine::IdleSound( void ) { if( GetState() == NPC_STATE_IDLE && random->RandomFloat( 0, 1 ) == 0 ) { // Moan infrequently in IDLE state. return; } if( IsSlumped() ) { // Sleeping zombies are quiet. return; } EmitSound( "Zombine.Idle" ); MakeAISpookySound( 360.0f ); } //----------------------------------------------------------------------------- // Purpose: Play a random attack sound. //----------------------------------------------------------------------------- void CNPC_Zombine::AttackSound( void ) { } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- const char *CNPC_Zombine::GetHeadcrabModel( void ) { return "models/headcrabclassic.mdl"; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CNPC_Zombine::GetLegsModel( void ) { return "models/zombie/zombie_soldier_legs.mdl"; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- const char *CNPC_Zombine::GetTorsoModel( void ) { return "models/zombie/zombie_soldier_torso.mdl"; } //--------------------------------------------------------- // Classic zombie only uses moan sound if on fire. //--------------------------------------------------------- void CNPC_Zombine::MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize ) { if( IsOnFire() ) { BaseClass::MoanSound( pEnvelope, iEnvelopeSize ); } } //----------------------------------------------------------------------------- // Purpose: Returns the classname (ie "npc_headcrab") to spawn when our headcrab bails. //----------------------------------------------------------------------------- const char *CNPC_Zombine::GetHeadcrabClassname( void ) { return "npc_headcrab"; } void CNPC_Zombine::ReleaseGrenade( Vector vPhysgunPos ) { if ( HasGrenade() == false ) return; Vector vDir = vPhysgunPos - m_hGrenade->GetAbsOrigin(); VectorNormalize( vDir ); Activity aActivity; Vector vForward, vRight; GetVectors( &vForward, &vRight, NULL ); float flDotForward = DotProduct( vForward, vDir ); float flDotRight = DotProduct( vRight, vDir ); bool bNegativeForward = false; bool bNegativeRight = false; if ( flDotForward < 0.0f ) { bNegativeForward = true; flDotForward = flDotForward * -1; } if ( flDotRight < 0.0f ) { bNegativeRight = true; flDotRight = flDotRight * -1; } if ( flDotRight > flDotForward ) { if ( bNegativeRight == true ) aActivity = (Activity)ACT_ZOMBINE_GRENADE_FLINCH_WEST; else aActivity = (Activity)ACT_ZOMBINE_GRENADE_FLINCH_EAST; } else { if ( bNegativeForward == true ) aActivity = (Activity)ACT_ZOMBINE_GRENADE_FLINCH_BACK; else aActivity = (Activity)ACT_ZOMBINE_GRENADE_FLINCH_FRONT; } AddGesture( aActivity ); DropGrenade( vec3_origin ); if ( IsSprinting() ) { StopSprint(); } else { Sprint(); } } CBaseEntity *CNPC_Zombine::OnFailedPhysGunPickup( Vector vPhysgunPos ) { CBaseEntity *pGrenade = m_hGrenade; ReleaseGrenade( vPhysgunPos ); return pGrenade; } //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_zombine, CNPC_Zombine ) //Squad slots DECLARE_SQUADSLOT( SQUAD_SLOT_ZOMBINE_SPRINT1 ) DECLARE_SQUADSLOT( SQUAD_SLOT_ZOMBINE_SPRINT2 ) DECLARE_CONDITION( COND_ZOMBINE_GRENADE ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_PULL ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_WALK ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_RUN ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_IDLE ) DECLARE_ACTIVITY( ACT_ZOMBINE_ATTACK_FAST ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_FLINCH_BACK ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_FLINCH_FRONT ) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_FLINCH_WEST) DECLARE_ACTIVITY( ACT_ZOMBINE_GRENADE_FLINCH_EAST ) DECLARE_ANIMEVENT( AE_ZOMBINE_PULLPIN ) DEFINE_SCHEDULE ( SCHED_ZOMBINE_PULL_GRENADE, " Tasks" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ZOMBINE_GRENADE_PULL" " Interrupts" ) AI_END_CUSTOM_NPC()