|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Implements the zombie, a horrific once-human headcrab victim.
//
// The zombie has two main states: Full and Torso.
//
// In Full state, the zombie is whole and walks upright as he did in Half-Life.
// He will try to claw the player and swat physics items at him.
//
// In Torso state, the zombie has been blasted or cut in half, and the Torso will
// drag itself along the ground with its arms. It will try to claw the player.
//
// In either state, a severely injured Zombie will release its headcrab, which
// will immediately go after the player. The Zombie will then die (ragdoll).
//
//=============================================================================//
#include "cbase.h"
#include "npc_BaseZombie.h"
#include "player.h"
#include "game.h"
#include "ai_network.h"
#include "ai_navigator.h"
#include "ai_motor.h"
#include "ai_default.h"
#include "ai_schedule.h"
#include "ai_hull.h"
#include "ai_node.h"
#include "ai_memory.h"
#include "ai_senses.h"
#include "bitstring.h"
#include "EntityFlame.h"
#include "hl2_shareddefs.h"
#include "npcevent.h"
#include "activitylist.h"
#include "entitylist.h"
#include "gib.h"
#include "soundenvelope.h"
#include "ndebugoverlay.h"
#include "rope.h"
#include "rope_shared.h"
#include "igamesystem.h"
#include "vstdlib/random.h"
#include "engine/IEngineSound.h"
#include "props.h"
#include "hl2_gamerules.h"
#include "weapon_physcannon.h"
#include "ammodef.h"
#include "vehicle_base.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
extern ConVar sk_npc_head;
#define ZOMBIE_BULLET_DAMAGE_SCALE 0.5f
int g_interactionZombieMeleeWarning;
envelopePoint_t envDefaultZombieMoanVolumeFast[] = { { 1.0f, 1.0f, 0.1f, 0.1f, }, { 0.0f, 0.0f, 0.2f, 0.3f, }, };
envelopePoint_t envDefaultZombieMoanVolume[] = { { 1.0f, 0.1f, 0.1f, 0.1f, }, { 1.0f, 1.0f, 0.2f, 0.2f, }, { 0.0f, 0.0f, 0.3f, 0.4f, }, };
// if the zombie doesn't find anything closer than this, it doesn't swat.
#define ZOMBIE_FARTHEST_PHYSICS_OBJECT 40.0*12.0
#define ZOMBIE_PHYSICS_SEARCH_DEPTH 100
// Don't swat objects unless player is closer than this.
#define ZOMBIE_PLAYER_MAX_SWAT_DIST 1000
//
// How much health a Zombie torso gets when a whole zombie is broken
// It's whole zombie's MAX Health * this value
#define ZOMBIE_TORSO_HEALTH_FACTOR 0.5
//
// When the zombie has health < m_iMaxHealth * this value, it will
// try to release its headcrab.
#define ZOMBIE_RELEASE_HEALTH_FACTOR 0.5
//
// The heaviest physics object that a zombie should try to swat. (kg)
#define ZOMBIE_MAX_PHYSOBJ_MASS 60
//
// Zombie tries to get this close to a physics object's origin to swat it
#define ZOMBIE_PHYSOBJ_SWATDIST 80
//
// Because movement code sometimes doesn't get us QUITE where we
// want to go, the zombie tries to get this close to a physics object
// Zombie will end up somewhere between PHYSOBJ_MOVE_TO_DIST & PHYSOBJ_SWATDIST
#define ZOMBIE_PHYSOBJ_MOVE_TO_DIST 48
//
// How long between physics swat attacks (in seconds).
#define ZOMBIE_SWAT_DELAY 5
//
// After taking damage, ignore further damage for n seconds. This keeps the zombie
// from being interrupted while.
//
#define ZOMBIE_FLINCH_DELAY 3
#define ZOMBIE_BURN_TIME 10 // If ignited, burn for this many seconds
#define ZOMBIE_BURN_TIME_NOISE 2 // Give or take this many seconds.
//=========================================================
// private activities
//=========================================================
int CNPC_BaseZombie::ACT_ZOM_SWATLEFTMID; int CNPC_BaseZombie::ACT_ZOM_SWATRIGHTMID; int CNPC_BaseZombie::ACT_ZOM_SWATLEFTLOW; int CNPC_BaseZombie::ACT_ZOM_SWATRIGHTLOW; int CNPC_BaseZombie::ACT_ZOM_RELEASECRAB; int CNPC_BaseZombie::ACT_ZOM_FALL;
ConVar sk_zombie_dmg_one_slash( "sk_zombie_dmg_one_slash","0"); ConVar sk_zombie_dmg_both_slash( "sk_zombie_dmg_both_slash","0");
// When a zombie spawns, he will select a 'base' pitch value
// that's somewhere between basepitchmin & basepitchmax
ConVar zombie_basemin( "zombie_basemin", "100" ); ConVar zombie_basemax( "zombie_basemax", "100" );
ConVar zombie_changemin( "zombie_changemin", "0" ); ConVar zombie_changemax( "zombie_changemax", "0" );
// play a sound once in every zombie_stepfreq steps
ConVar zombie_stepfreq( "zombie_stepfreq", "4" ); ConVar zombie_moanfreq( "zombie_moanfreq", "1" );
ConVar zombie_decaymin( "zombie_decaymin", "0.1" ); ConVar zombie_decaymax( "zombie_decaymax", "0.4" );
ConVar zombie_ambushdist( "zombie_ambushdist", "16000" );
//=========================================================
// For a couple of reasons, we keep a running count of how
// many zombies in the world are angry at any given time.
//=========================================================
static int s_iAngryZombies = 0;
//=========================================================
//=========================================================
class CAngryZombieCounter : public CAutoGameSystem { public: CAngryZombieCounter( char const *name ) : CAutoGameSystem( name ) { } // Level init, shutdown
virtual void LevelInitPreEntity() { s_iAngryZombies = 0; } };
CAngryZombieCounter AngryZombieCounter( "CAngryZombieCounter" );
int AE_ZOMBIE_ATTACK_RIGHT; int AE_ZOMBIE_ATTACK_LEFT; int AE_ZOMBIE_ATTACK_BOTH; int AE_ZOMBIE_SWATITEM; int AE_ZOMBIE_STARTSWAT; int AE_ZOMBIE_STEP_LEFT; int AE_ZOMBIE_STEP_RIGHT; int AE_ZOMBIE_SCUFF_LEFT; int AE_ZOMBIE_SCUFF_RIGHT; int AE_ZOMBIE_ATTACK_SCREAM; int AE_ZOMBIE_GET_UP; int AE_ZOMBIE_POUND; int AE_ZOMBIE_ALERTSOUND; int AE_ZOMBIE_POPHEADCRAB;
//=========================================================
//=========================================================
BEGIN_DATADESC( CNPC_BaseZombie )
DEFINE_SOUNDPATCH( m_pMoanSound ), DEFINE_FIELD( m_fIsTorso, FIELD_BOOLEAN ), DEFINE_FIELD( m_fIsHeadless, FIELD_BOOLEAN ), DEFINE_FIELD( m_flNextFlinch, FIELD_TIME ), DEFINE_FIELD( m_bHeadShot, FIELD_BOOLEAN ), DEFINE_FIELD( m_flBurnDamage, FIELD_FLOAT ), DEFINE_FIELD( m_flBurnDamageResetTime, FIELD_TIME ), DEFINE_FIELD( m_hPhysicsEnt, FIELD_EHANDLE ), DEFINE_FIELD( m_flNextMoanSound, FIELD_TIME ), DEFINE_FIELD( m_flNextSwat, FIELD_TIME ), DEFINE_FIELD( m_flNextSwatScan, FIELD_TIME ), DEFINE_FIELD( m_crabHealth, FIELD_FLOAT ), DEFINE_FIELD( m_flMoanPitch, FIELD_FLOAT ), DEFINE_FIELD( m_iMoanSound, FIELD_INTEGER ), DEFINE_FIELD( m_hObstructor, FIELD_EHANDLE ), DEFINE_FIELD( m_bIsSlumped, FIELD_BOOLEAN ),
END_DATADESC()
//LINK_ENTITY_TO_CLASS( base_zombie, CNPC_BaseZombie );
//---------------------------------------------------------
//---------------------------------------------------------
int CNPC_BaseZombie::g_numZombies = 0;
//---------------------------------------------------------
//---------------------------------------------------------
CNPC_BaseZombie::CNPC_BaseZombie() { // Gotta select which sound we're going to play, right here!
// Because everyone's constructed before they spawn.
//
// Assign moan sounds in order, over and over.
// This means if 3 or so zombies spawn near each
// other, they will definitely not pick the same
// moan loop.
m_iMoanSound = g_numZombies;
g_numZombies++; }
//---------------------------------------------------------
//---------------------------------------------------------
CNPC_BaseZombie::~CNPC_BaseZombie() { g_numZombies--; }
//---------------------------------------------------------
// The closest physics object is chosen that is:
// <= MaxMass in Mass
// Between the zombie and the enemy
// not too far from a direct line to the enemy.
//---------------------------------------------------------
bool CNPC_BaseZombie::FindNearestPhysicsObject( int iMaxMass ) { CBaseEntity *pList[ ZOMBIE_PHYSICS_SEARCH_DEPTH ]; CBaseEntity *pNearest = NULL; float flDist; IPhysicsObject *pPhysObj; int i; Vector vecDirToEnemy; Vector vecDirToObject;
if ( !CanSwatPhysicsObjects() || !GetEnemy() ) { // Can't swat, or no enemy, so no swat.
m_hPhysicsEnt = NULL; return false; }
vecDirToEnemy = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); float dist = VectorNormalize(vecDirToEnemy); vecDirToEnemy.z = 0;
if( dist > ZOMBIE_PLAYER_MAX_SWAT_DIST ) { // Player is too far away. Don't bother
// trying to swat anything at them until
// they are closer.
return false; }
float flNearestDist = MIN( dist, ZOMBIE_FARTHEST_PHYSICS_OBJECT * 0.5 ); Vector vecDelta( flNearestDist, flNearestDist, GetHullHeight() * 2.0 );
class CZombieSwatEntitiesEnum : public CFlaggedEntitiesEnum { public: CZombieSwatEntitiesEnum( CBaseEntity **pList, int listMax, int iMaxMass ) : CFlaggedEntitiesEnum( pList, listMax, 0 ), m_iMaxMass( iMaxMass ) { }
virtual IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ) { CBaseEntity *pEntity = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() ); if ( pEntity && pEntity->VPhysicsGetObject() && pEntity->VPhysicsGetObject()->GetMass() <= m_iMaxMass && pEntity->VPhysicsGetObject()->IsAsleep() && pEntity->VPhysicsGetObject()->IsMoveable() ) { return CFlaggedEntitiesEnum::EnumElement( pHandleEntity ); } return ITERATION_CONTINUE; }
int m_iMaxMass; };
CZombieSwatEntitiesEnum swatEnum( pList, ZOMBIE_PHYSICS_SEARCH_DEPTH, iMaxMass );
int count = UTIL_EntitiesInBox( GetAbsOrigin() - vecDelta, GetAbsOrigin() + vecDelta, &swatEnum );
// magically know where they are
Vector vecZombieKnees; CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.25f ), &vecZombieKnees );
for( i = 0 ; i < count ; i++ ) { pPhysObj = pList[ i ]->VPhysicsGetObject();
Assert( !( !pPhysObj || pPhysObj->GetMass() > iMaxMass || !pPhysObj->IsAsleep() ) );
Vector center = pList[ i ]->WorldSpaceCenter(); flDist = UTIL_DistApprox2D( GetAbsOrigin(), center );
if( flDist >= flNearestDist ) continue;
// This object is closer... but is it between the player and the zombie?
vecDirToObject = pList[ i ]->WorldSpaceCenter() - GetAbsOrigin(); VectorNormalize(vecDirToObject); vecDirToObject.z = 0;
if( DotProduct( vecDirToEnemy, vecDirToObject ) < 0.8 ) continue;
if( flDist >= UTIL_DistApprox2D( center, GetEnemy()->GetAbsOrigin() ) ) continue;
// don't swat things where the highest point is under my knees
// NOTE: This is a rough test; a more exact test is going to occur below
if ( (center.z + pList[i]->BoundingRadius()) < vecZombieKnees.z ) continue;
// don't swat things that are over my head.
if( center.z > EyePosition().z ) continue;
vcollide_t *pCollide = modelinfo->GetVCollide( pList[i]->GetModelIndex() ); Vector objMins, objMaxs; physcollision->CollideGetAABB( &objMins, &objMaxs, pCollide->solids[0], pList[i]->GetAbsOrigin(), pList[i]->GetAbsAngles() );
if ( objMaxs.z < vecZombieKnees.z ) continue;
if ( !FVisible( pList[i] ) ) continue;
if ( hl2_episodic.GetBool() ) { // Skip things that the enemy can't see. Do we want this as a general thing?
// The case for this feature is that zombies who are pursuing the player will
// stop along the way to swat objects at the player who is around the corner or
// otherwise not in a place that the object has a hope of hitting. This diversion
// makes the zombies very late (in a random fashion) getting where they are going. (sjb 1/2/06)
if( !GetEnemy()->FVisible( pList[i] ) ) continue; }
// Make this the last check, since it makes a string.
// Don't swat server ragdolls!
if ( FClassnameIs( pList[ i ], "physics_prop_ragdoll" ) ) continue; if ( FClassnameIs( pList[ i ], "prop_ragdoll" ) ) continue;
// The object must also be closer to the zombie than it is to the enemy
pNearest = pList[ i ]; flNearestDist = flDist; }
m_hPhysicsEnt = pNearest;
if( m_hPhysicsEnt == NULL ) { return false; } else { return true; } }
//-----------------------------------------------------------------------------
// Purpose: Returns this monster's place in the relationship table.
//-----------------------------------------------------------------------------
Class_T CNPC_BaseZombie::Classify( void ) { if ( IsSlumped() ) return CLASS_NONE;
return( CLASS_ZOMBIE ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
Disposition_t CNPC_BaseZombie::IRelationType( CBaseEntity *pTarget ) { // Slumping should not affect Zombie's opinion of others
if ( IsSlumped() ) { m_bIsSlumped = false; Disposition_t result = BaseClass::IRelationType( pTarget ); m_bIsSlumped = true; return result; }
return BaseClass::IRelationType( pTarget ); }
//-----------------------------------------------------------------------------
// Purpose: Returns the maximum yaw speed based on the monster's current activity.
//-----------------------------------------------------------------------------
float CNPC_BaseZombie::MaxYawSpeed( void ) { if( m_fIsTorso ) { return( 60 ); } else if (IsMoving() && HasPoseParameter( GetSequence(), m_poseMove_Yaw )) { return( 15 ); } else { switch( GetActivity() ) { case ACT_TURN_LEFT: case ACT_TURN_RIGHT: return 100; break; case ACT_RUN: return 15; break; case ACT_WALK: case ACT_IDLE: return 25; break; case ACT_RANGE_ATTACK1: case ACT_RANGE_ATTACK2: case ACT_MELEE_ATTACK1: case ACT_MELEE_ATTACK2: return 120; default: return 90; break; } } }
//-----------------------------------------------------------------------------
// Purpose: turn in the direction of movement
// Output :
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval ) { if (!HasPoseParameter( GetSequence(), m_poseMove_Yaw )) { return BaseClass::OverrideMoveFacing( move, flInterval ); }
// required movement direction
float flMoveYaw = UTIL_VecToYaw( move.dir ); float idealYaw = UTIL_AngleMod( flMoveYaw );
if (GetEnemy()) { float flEDist = UTIL_DistApprox2D( WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter() );
if (flEDist < 256.0) { float flEYaw = UTIL_VecToYaw( GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter() );
if (flEDist < 128.0) { idealYaw = flEYaw; } else { idealYaw = flMoveYaw + UTIL_AngleDiff( flEYaw, flMoveYaw ) * (2 - flEDist / 128.0); }
//DevMsg("was %.0f now %.0f\n", flMoveYaw, idealYaw );
} }
GetMotor()->SetIdealYawAndUpdate( idealYaw );
// find movement direction to compensate for not being turned far enough
float fSequenceMoveYaw = GetSequenceMoveYaw( GetSequence() ); float flDiff = UTIL_AngleDiff( flMoveYaw, GetLocalAngles().y + fSequenceMoveYaw ); SetPoseParameter( m_poseMove_Yaw, GetPoseParameter( m_poseMove_Yaw ) + flDiff );
return true; }
//-----------------------------------------------------------------------------
// Purpose: For innate melee attack
// Input :
// Output :
//-----------------------------------------------------------------------------
int CNPC_BaseZombie::MeleeAttack1Conditions ( float flDot, float flDist ) { float range = GetClawAttackRange();
if (flDist > range ) { // Translate a hit vehicle into its passenger if found
if ( GetEnemy() != NULL ) { #if defined(HL2_DLL) && !defined(HL2MP)
// If the player is holding an object, knock it down.
if( GetEnemy()->IsPlayer() ) { CBasePlayer *pPlayer = ToBasePlayer( GetEnemy() );
Assert( pPlayer != NULL );
// Is the player carrying something?
CBaseEntity *pObject = GetPlayerHeldEntity(pPlayer);
if( !pObject ) { pObject = PhysCannonGetHeldEntity( pPlayer->GetActiveWeapon() ); }
if( pObject ) { float flDist = pObject->WorldSpaceCenter().DistTo( WorldSpaceCenter() );
if( flDist <= GetClawAttackRange() ) return COND_CAN_MELEE_ATTACK1; } } #endif
} return COND_TOO_FAR_TO_ATTACK; }
if (flDot < 0.7) { return COND_NOT_FACING_ATTACK; }
// Build a cube-shaped hull, the same hull that ClawAttack() is going to use.
Vector vecMins = GetHullMins(); Vector vecMaxs = GetHullMaxs(); vecMins.z = vecMins.x; vecMaxs.z = vecMaxs.x;
Vector forward; GetVectors( &forward, NULL, NULL );
trace_t tr; CTraceFilterNav traceFilter( this, false, this, COLLISION_GROUP_NONE ); AI_TraceHull( WorldSpaceCenter(), WorldSpaceCenter() + forward * GetClawAttackRange(), vecMins, vecMaxs, MASK_NPCSOLID, &traceFilter, &tr );
if( tr.fraction == 1.0 || !tr.m_pEnt ) {
#ifdef HL2_EPISODIC
// If our trace was unobstructed but we were shooting
if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) return COND_CAN_MELEE_ATTACK1;
#endif // HL2_EPISODIC
// This attack would miss completely. Trick the zombie into moving around some more.
return COND_TOO_FAR_TO_ATTACK; }
if( tr.m_pEnt == GetEnemy() || tr.m_pEnt->IsNPC() || ( tr.m_pEnt->m_takedamage == DAMAGE_YES && (dynamic_cast<CBreakableProp*>(tr.m_pEnt) ) ) ) { // -Let the zombie swipe at his enemy if he's going to hit them.
// -Also let him swipe at NPC's that happen to be between the zombie and the enemy.
// This makes mobs of zombies seem more rowdy since it doesn't leave guys in the back row standing around.
// -Also let him swipe at things that takedamage, under the assumptions that they can be broken.
return COND_CAN_MELEE_ATTACK1; }
Vector vecTrace = tr.endpos - tr.startpos; float lenTraceSq = vecTrace.Length2DSqr();
if ( GetEnemy() && GetEnemy()->MyCombatCharacterPointer() && tr.m_pEnt == static_cast<CBaseCombatCharacter *>(GetEnemy())->GetVehicleEntity() ) { if ( lenTraceSq < Square( GetClawAttackRange() * 0.75f ) ) { return COND_CAN_MELEE_ATTACK1; } }
if( tr.m_pEnt->IsBSPModel() ) { // The trace hit something solid, but it's not the enemy. If this item is closer to the zombie than
// the enemy is, treat this as an obstruction.
Vector vecToEnemy = GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter();
if( lenTraceSq < vecToEnemy.Length2DSqr() ) { return COND_ZOMBIE_LOCAL_MELEE_OBSTRUCTION; } }
#ifdef HL2_EPISODIC
if ( !tr.m_pEnt->IsWorld() && GetEnemy() && GetEnemy()->GetGroundEntity() == tr.m_pEnt ) { //Try to swat whatever the player is standing on instead of acting like a dill.
return COND_CAN_MELEE_ATTACK1; }
// Bullseyes are given some grace on if they can be hit
if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) return COND_CAN_MELEE_ATTACK1;
#endif // HL2_EPISODIC
// Move around some more
return COND_TOO_FAR_TO_ATTACK; }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
#define ZOMBIE_BUCKSHOT_TRIPLE_DAMAGE_DIST 96.0f // Triple damage from buckshot at 8 feet (headshot only)
float CNPC_BaseZombie::GetHitgroupDamageMultiplier( int iHitGroup, const CTakeDamageInfo &info ) { switch( iHitGroup ) { case HITGROUP_HEAD: { if( info.GetDamageType() & DMG_BUCKSHOT ) { float flDist = FLT_MAX;
if( info.GetAttacker() ) { flDist = ( GetAbsOrigin() - info.GetAttacker()->GetAbsOrigin() ).Length(); }
if( flDist <= ZOMBIE_BUCKSHOT_TRIPLE_DAMAGE_DIST ) { return 3.0f; } } else { return 2.0f; } } }
return BaseClass::GetHitgroupDamageMultiplier( iHitGroup, info ); }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { CTakeDamageInfo infoCopy = info;
// Keep track of headshots so we can determine whether to pop off our headcrab.
if (ptr->hitgroup == HITGROUP_HEAD) { m_bHeadShot = true; }
if( infoCopy.GetDamageType() & DMG_BUCKSHOT ) { // Zombie gets across-the-board damage reduction for buckshot. This compensates for the recent changes which
// make the shotgun much more powerful, and returns the zombies to a level that has been playtested extensively.(sjb)
// This normalizes the buckshot damage to what it used to be on normal (5 dmg per pellet. Now it's 8 dmg per pellet).
infoCopy.ScaleDamage( 0.625 ); }
BaseClass::TraceAttack( infoCopy, vecDir, ptr, pAccumulator ); }
//-----------------------------------------------------------------------------
// Purpose: A zombie has taken damage. Determine whether he should split in half
// Input :
// Output : bool, true if yes.
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold ) { if ( info.GetDamageType() & DMG_REMOVENORAGDOLL ) return false;
if ( m_fIsTorso ) { // Already split.
return false; }
// Not if we're in a dss
if ( IsRunningDynamicInteraction() ) return false;
// Break in half IF:
//
// Take half or more of max health in DMG_BLAST
if( (info.GetDamageType() & DMG_BLAST) && flDamageThreshold >= 0.5 ) { return true; }
if ( hl2_episodic.GetBool() ) { // Always split after a cannon hit
if ( info.GetAmmoType() == GetAmmoDef()->Index("CombineHeavyCannon") ) return true; }
#if 0
if( info.GetDamageType() & DMG_BUCKSHOT ) { if( m_iHealth <= 0 || flDamageThreshold >= 0.5 ) { return true; } } #endif
return false; }
//-----------------------------------------------------------------------------
// Purpose: A zombie has taken damage. Determine whether he release his headcrab.
// Output : YES, IMMEDIATE, or SCHEDULED (see HeadcrabRelease_t)
//-----------------------------------------------------------------------------
HeadcrabRelease_t CNPC_BaseZombie::ShouldReleaseHeadcrab( const CTakeDamageInfo &info, float flDamageThreshold ) { if ( m_iHealth <= 0 ) { if ( info.GetDamageType() & DMG_REMOVENORAGDOLL ) return RELEASE_NO;
if ( info.GetDamageType() & DMG_SNIPER ) return RELEASE_RAGDOLL;
// If I was killed by a bullet...
if ( info.GetDamageType() & DMG_BULLET ) { if( m_bHeadShot ) { if( flDamageThreshold > 0.25 ) { // Enough force to kill the crab.
return RELEASE_RAGDOLL; } } else { // Killed by a shot to body or something. Crab is ok!
return RELEASE_IMMEDIATE; } }
// If I was killed by an explosion, release the crab.
if ( info.GetDamageType() & DMG_BLAST ) { return RELEASE_RAGDOLL; }
if ( m_fIsTorso && IsChopped( info ) ) { return RELEASE_RAGDOLL_SLICED_OFF; } }
return RELEASE_NO; }
//-----------------------------------------------------------------------------
// Purpose:
// Input : pInflictor -
// pAttacker -
// flDamage -
// bitsDamageType -
// Output : int
//-----------------------------------------------------------------------------
#define ZOMBIE_SCORCH_RATE 8
#define ZOMBIE_MIN_RENDERCOLOR 50
int CNPC_BaseZombie::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) { CTakeDamageInfo info = inputInfo;
if( inputInfo.GetDamageType() & DMG_BURN ) { // If a zombie is on fire it only takes damage from the fire that's attached to it. (DMG_DIRECT)
// This is to stop zombies from burning to death 10x faster when they're standing around
// 10 fire entities.
if( IsOnFire() && !(inputInfo.GetDamageType() & DMG_DIRECT) ) { return 0; } Scorch( ZOMBIE_SCORCH_RATE, ZOMBIE_MIN_RENDERCOLOR ); }
// Take some percentage of damage from bullets (unless hit in the crab). Always take full buckshot & sniper damage
if ( !m_bHeadShot && (info.GetDamageType() & DMG_BULLET) && !(info.GetDamageType() & (DMG_BUCKSHOT|DMG_SNIPER)) ) { info.ScaleDamage( ZOMBIE_BULLET_DAMAGE_SCALE ); }
if ( ShouldIgnite( info ) ) { Ignite( 100.0f ); }
int tookDamage = BaseClass::OnTakeDamage_Alive( info );
// flDamageThreshold is what percentage of the creature's max health
// this amount of damage represents. (clips at 1.0)
float flDamageThreshold = MIN( 1, info.GetDamage() / m_iMaxHealth ); // Being chopped up by a sharp physics object is a pretty special case
// so we handle it with some special code. Mainly for
// Ravenholm's helicopter traps right now (sjb).
bool bChopped = IsChopped(info); bool bSquashed = IsSquashed(info); bool bKilledByVehicle = ( ( info.GetDamageType() & DMG_VEHICLE ) != 0 );
if( !m_fIsTorso && (bChopped || bSquashed) && !bKilledByVehicle && !(info.GetDamageType() & DMG_REMOVENORAGDOLL) ) { if( bChopped ) { EmitSound( "E3_Phystown.Slicer" ); }
DieChopped( info ); } else { HeadcrabRelease_t release = ShouldReleaseHeadcrab( info, flDamageThreshold ); switch( release ) { case RELEASE_IMMEDIATE: ReleaseHeadcrab( EyePosition(), vec3_origin, true, true ); break;
case RELEASE_RAGDOLL: // Go a little easy on headcrab ragdoll force. They're light!
ReleaseHeadcrab( EyePosition(), inputInfo.GetDamageForce() * 0.25, true, false, true ); break;
case RELEASE_RAGDOLL_SLICED_OFF: { EmitSound( "E3_Phystown.Slicer" ); Vector vecForce = inputInfo.GetDamageForce() * 0.1; vecForce += Vector( 0, 0, 2000.0 ); ReleaseHeadcrab( EyePosition(), vecForce, true, false, true ); } break;
case RELEASE_VAPORIZE: RemoveHead(); break;
case RELEASE_SCHEDULED: SetCondition( COND_ZOMBIE_RELEASECRAB ); break; }
if( ShouldBecomeTorso( info, flDamageThreshold ) ) { bool bHitByCombineCannon = (inputInfo.GetAmmoType() == GetAmmoDef()->Index("CombineHeavyCannon"));
if ( CanBecomeLiveTorso() ) { BecomeTorso( vec3_origin, inputInfo.GetDamageForce() * 0.50 );
if ( ( info.GetDamageType() & DMG_BLAST) && random->RandomInt( 0, 1 ) == 0 ) { Ignite( 5.0 + random->RandomFloat( 0.0, 5.0 ) ); }
// For Combine cannon impacts
if ( hl2_episodic.GetBool() ) { if ( bHitByCombineCannon ) { // Catch on fire.
Ignite( 5.0f + random->RandomFloat( 0.0f, 5.0f ) ); } }
if (flDamageThreshold >= 1.0) { m_iHealth = 0; BecomeRagdollOnClient( info.GetDamageForce() ); } } else if ( random->RandomInt(1, 3) == 1 ) DieChopped( info ); } }
if( tookDamage > 0 && (info.GetDamageType() & (DMG_BURN|DMG_DIRECT)) && m_ActBusyBehavior.IsActive() ) { //!!!HACKHACK- Stuff a light_damage condition if an actbusying zombie takes direct burn damage. This will cause an
// ignited zombie to 'wake up' and rise out of its actbusy slump. (sjb)
SetCondition( COND_LIGHT_DAMAGE ); }
// IMPORTANT: always clear the headshot flag after applying damage. No early outs!
m_bHeadShot = false;
return tookDamage; }
//-----------------------------------------------------------------------------
// Purpose: make a sound Alyx can hear when in darkness mode
// Input : volume (radius) of the sound.
// Output :
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::MakeAISpookySound( float volume, float duration ) { #ifdef HL2_EPISODIC
if ( HL2GameRules()->IsAlyxInDarknessMode() ) { CSoundEnt::InsertSound( SOUND_COMBAT, EyePosition(), volume, duration, this, SOUNDENT_CHANNEL_SPOOKY_NOISE ); } #endif // HL2_EPISODIC
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::CanPlayMoanSound() { if( HasSpawnFlags( SF_NPC_GAG ) ) return false;
// Burning zombies play their moan loop at full volume for as long as they're
// burning. Don't let a moan envelope play cause it will turn the volume down when done.
if( IsOnFire() ) return false;
// Members of a small group of zombies can vocalize whenever they want
if( s_iAngryZombies <= 4 ) return true;
// This serves to limit the number of zombies that can moan at one time when there are a lot.
if( random->RandomInt( 1, zombie_moanfreq.GetInt() * (s_iAngryZombies/2) ) == 1 ) { return true; }
return false; }
//-----------------------------------------------------------------------------
// Purpose: Open a window and let a little bit of the looping moan sound
// come through.
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize ) { if( HasSpawnFlags( SF_NPC_GAG ) ) { // Not yet!
return; }
if( !m_pMoanSound ) { // Don't set this up until the code calls for it.
const char *pszSound = GetMoanSound( m_iMoanSound ); m_flMoanPitch = random->RandomInt( zombie_basemin.GetInt(), zombie_basemax.GetInt() );
//m_pMoanSound = ENVELOPE_CONTROLLER.SoundCreate( entindex(), CHAN_STATIC, pszSound, ATTN_NORM );
CPASAttenuationFilter filter( this ); m_pMoanSound = ENVELOPE_CONTROLLER.SoundCreate( filter, entindex(), CHAN_STATIC, pszSound, ATTN_NORM );
ENVELOPE_CONTROLLER.Play( m_pMoanSound, 1.0, m_flMoanPitch ); }
//HACKHACK get these from chia chin's console vars.
envDefaultZombieMoanVolumeFast[ 1 ].durationMin = zombie_decaymin.GetFloat(); envDefaultZombieMoanVolumeFast[ 1 ].durationMax = zombie_decaymax.GetFloat();
if( random->RandomInt( 1, 2 ) == 1 ) { IdleSound(); }
float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope( m_pMoanSound, SOUNDCTRL_CHANGE_VOLUME, pEnvelope, iEnvelopeSize );
float flPitch = random->RandomInt( m_flMoanPitch + zombie_changemin.GetInt(), m_flMoanPitch + zombie_changemax.GetInt() ); ENVELOPE_CONTROLLER.SoundChangePitch( m_pMoanSound, flPitch, 0.3 );
m_flNextMoanSound = gpGlobals->curtime + duration + 9999; }
//-----------------------------------------------------------------------------
// Purpose: Determine whether the zombie is chopped up by some physics item
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::IsChopped( const CTakeDamageInfo &info ) { float flDamageThreshold = MIN( 1, info.GetDamage() / m_iMaxHealth );
if ( m_iHealth > 0 || flDamageThreshold <= 0.5 ) return false;
if ( !( info.GetDamageType() & DMG_SLASH) ) return false;
if ( !( info.GetDamageType() & DMG_CRUSH) ) return false;
if ( info.GetDamageType() & DMG_REMOVENORAGDOLL ) return false;
// If you take crush and slash damage, you're hit by a sharp physics item.
return true; }
//-----------------------------------------------------------------------------
// Purpose: Return true if this gibbing zombie should ignite its gibs
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::ShouldIgniteZombieGib( void ) { #ifdef HL2_EPISODIC
// If we're in darkness mode, don't ignite giblets, because we don't want to
// pay the perf cost of multiple dynamic lights per giblet.
return ( IsOnFire() && !HL2GameRules()->IsAlyxInDarknessMode() ); #else
return IsOnFire(); #endif
}
//-----------------------------------------------------------------------------
// Purpose: Handle the special case of a zombie killed by a physics chopper.
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::DieChopped( const CTakeDamageInfo &info ) { bool bSquashed = IsSquashed(info);
Vector forceVector( vec3_origin );
forceVector += CalcDamageForceVector( info );
if( !m_fIsHeadless && !bSquashed ) { if( random->RandomInt( 0, 1 ) == 0 ) { // Drop a live crab half of the time.
ReleaseHeadcrab( EyePosition(), forceVector * 0.005, true, false, false ); } }
float flFadeTime = 0.0;
if( HasSpawnFlags( SF_NPC_FADE_CORPSE ) ) { flFadeTime = 5.0; }
SetSolid( SOLID_NONE ); AddEffects( EF_NODRAW );
Vector vecLegsForce; vecLegsForce.x = random->RandomFloat( -400, 400 ); vecLegsForce.y = random->RandomFloat( -400, 400 ); vecLegsForce.z = random->RandomFloat( 0, 250 );
if( bSquashed && vecLegsForce.z > 0 ) { // Force the broken legs down. (Give some additional force, too)
vecLegsForce.z *= -10; }
CBaseEntity *pLegGib = CreateRagGib( GetLegsModel(), GetAbsOrigin(), GetAbsAngles(), vecLegsForce, flFadeTime, ShouldIgniteZombieGib() ); if ( pLegGib ) { CopyRenderColorTo( pLegGib ); }
forceVector *= random->RandomFloat( 0.04, 0.06 ); forceVector.z = ( 100 * 12 * 5 ) * random->RandomFloat( 0.8, 1.2 );
if( bSquashed && forceVector.z > 0 ) { // Force the broken torso down.
forceVector.z *= -1.0; }
// Why do I have to fix this up?! (sjb)
QAngle TorsoAngles; TorsoAngles = GetAbsAngles(); TorsoAngles.x -= 90.0f; CBaseEntity *pTorsoGib = CreateRagGib( GetTorsoModel(), GetAbsOrigin() + Vector( 0, 0, 64 ), TorsoAngles, forceVector, flFadeTime, ShouldIgniteZombieGib() ); if ( pTorsoGib ) { CBaseAnimating *pAnimating = dynamic_cast<CBaseAnimating*>(pTorsoGib); if( pAnimating ) { pAnimating->SetBodygroup( ZOMBIE_BODYGROUP_HEADCRAB, !m_fIsHeadless ); }
pTorsoGib->SetOwnerEntity( this ); CopyRenderColorTo( pTorsoGib );
}
if ( UTIL_ShouldShowBlood( BLOOD_COLOR_YELLOW ) ) { int i; Vector vecSpot; Vector vecDir;
for ( i = 0 ; i < 4; i++ ) { vecSpot = WorldSpaceCenter();
vecSpot.x += random->RandomFloat( -12, 12 ); vecSpot.y += random->RandomFloat( -12, 12 ); vecSpot.z += random->RandomFloat( -4, 16 );
UTIL_BloodDrips( vecSpot, vec3_origin, BLOOD_COLOR_YELLOW, 50 ); }
for ( int i = 0 ; i < 4 ; i++ ) { Vector vecSpot = WorldSpaceCenter();
vecSpot.x += random->RandomFloat( -12, 12 ); vecSpot.y += random->RandomFloat( -12, 12 ); vecSpot.z += random->RandomFloat( -4, 16 );
vecDir.x = random->RandomFloat(-1, 1); vecDir.y = random->RandomFloat(-1, 1); vecDir.z = 0; VectorNormalize( vecDir );
UTIL_BloodImpact( vecSpot, vecDir, BloodColor(), 1 ); } } }
//-----------------------------------------------------------------------------
// Purpose: damage has been done. Should the zombie ignite?
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::ShouldIgnite( const CTakeDamageInfo &info ) { if ( IsOnFire() ) { // Already burning!
return false; }
if ( info.GetDamageType() & DMG_BURN ) { //
// If we take more than ten percent of our health in burn damage within a five
// second interval, we should catch on fire.
//
m_flBurnDamage += info.GetDamage(); m_flBurnDamageResetTime = gpGlobals->curtime + 5;
if ( m_flBurnDamage >= m_iMaxHealth * 0.1 ) { return true; } }
return false; }
//-----------------------------------------------------------------------------
// Purpose: Sufficient fire damage has been done. Zombie ignites!
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner ) { BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner );
#ifdef HL2_EPISODIC
if ( HL2GameRules()->IsAlyxInDarknessMode() == true && GetEffectEntity() != NULL ) { GetEffectEntity()->AddEffects( EF_DIMLIGHT ); } #endif // HL2_EPISODIC
// Set the zombie up to burn to death in about ten seconds.
SetHealth( MIN( m_iHealth, FLAME_DIRECT_DAMAGE_PER_SEC * (ZOMBIE_BURN_TIME + random->RandomFloat( -ZOMBIE_BURN_TIME_NOISE, ZOMBIE_BURN_TIME_NOISE)) ) );
// FIXME: use overlays when they come online
//AddOverlay( ACT_ZOM_WALK_ON_FIRE, false );
if( !m_ActBusyBehavior.IsActive() ) { Activity activity = GetActivity(); Activity burningActivity = activity;
if ( activity == ACT_WALK ) { burningActivity = ACT_WALK_ON_FIRE; } else if ( activity == ACT_RUN ) { burningActivity = ACT_RUN_ON_FIRE; } else if ( activity == ACT_IDLE ) { burningActivity = ACT_IDLE_ON_FIRE; }
if( HaveSequenceForActivity(burningActivity) ) { // Make sure we have a sequence for this activity (torsos don't have any, for instance)
// to prevent the baseNPC & baseAnimating code from throwing red level errors.
SetActivity( burningActivity ); } } }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::CopyRenderColorTo( CBaseEntity *pOther ) { color32 color = GetRenderColor(); pOther->SetRenderColor( color.r, color.g, color.b, color.a ); }
//-----------------------------------------------------------------------------
// Purpose: Look in front and see if the claw hit anything.
//
// Input : flDist distance to trace
// iDamage damage to do if attack hits
// vecViewPunch camera punch (if attack hits player)
// vecVelocityPunch velocity punch (if attack hits player)
//
// Output : The entity hit by claws. NULL if nothing.
//-----------------------------------------------------------------------------
CBaseEntity *CNPC_BaseZombie::ClawAttack( float flDist, int iDamage, QAngle &qaViewPunch, Vector &vecVelocityPunch, int BloodOrigin ) { // Added test because claw attack anim sometimes used when for cases other than melee
int iDriverInitialHealth = -1; CBaseEntity *pDriver = NULL; if ( GetEnemy() ) { trace_t tr; AI_TraceHull( WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), -Vector(8,8,8), Vector(8,8,8), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr );
if ( tr.fraction < 1.0f ) return NULL;
// CheckTraceHullAttack() can damage player in vehicle as side effect of melee attack damaging physics objects, which the car forwards to the player
// need to detect this to get correct damage effects
CBaseCombatCharacter *pCCEnemy = ( GetEnemy() != NULL ) ? GetEnemy()->MyCombatCharacterPointer() : NULL; CBaseEntity *pVehicleEntity; if ( pCCEnemy != NULL && ( pVehicleEntity = pCCEnemy->GetVehicleEntity() ) != NULL ) { if ( pVehicleEntity->GetServerVehicle() && dynamic_cast<CPropVehicleDriveable *>(pVehicleEntity) ) { pDriver = static_cast<CPropVehicleDriveable *>(pVehicleEntity)->GetDriver(); if ( pDriver && pDriver->IsPlayer() ) { iDriverInitialHealth = pDriver->GetHealth(); } else { pDriver = NULL; } } } }
//
// Trace out a cubic section of our hull and see what we hit.
//
Vector vecMins = GetHullMins(); Vector vecMaxs = GetHullMaxs(); vecMins.z = vecMins.x; vecMaxs.z = vecMaxs.x;
CBaseEntity *pHurt = NULL; if ( GetEnemy() && GetEnemy()->Classify() == CLASS_BULLSEYE ) { // We always hit bullseyes we're targeting
pHurt = GetEnemy(); CTakeDamageInfo info( this, this, vec3_origin, GetAbsOrigin(), iDamage, DMG_SLASH ); pHurt->TakeDamage( info ); } else { // Try to hit them with a trace
pHurt = CheckTraceHullAttack( flDist, vecMins, vecMaxs, iDamage, DMG_SLASH ); }
if ( pDriver && iDriverInitialHealth != pDriver->GetHealth() ) { pHurt = pDriver; }
if ( !pHurt && m_hPhysicsEnt != NULL && IsCurSchedule(SCHED_ZOMBIE_ATTACKITEM) ) { pHurt = m_hPhysicsEnt;
Vector vForce = pHurt->WorldSpaceCenter() - WorldSpaceCenter(); VectorNormalize( vForce );
vForce *= 5 * 24;
CTakeDamageInfo info( this, this, vForce, GetAbsOrigin(), iDamage, DMG_SLASH ); pHurt->TakeDamage( info );
pHurt = m_hPhysicsEnt; }
if ( pHurt ) { AttackHitSound();
CBasePlayer *pPlayer = ToBasePlayer( pHurt );
if ( pPlayer != NULL && !(pPlayer->GetFlags() & FL_GODMODE ) ) { pPlayer->ViewPunch( qaViewPunch ); pPlayer->VelocityPunch( vecVelocityPunch ); } else if( !pPlayer && UTIL_ShouldShowBlood(pHurt->BloodColor()) ) { // Hit an NPC. Bleed them!
Vector vecBloodPos;
switch( BloodOrigin ) { case ZOMBIE_BLOOD_LEFT_HAND: if( GetAttachment( "blood_left", vecBloodPos ) ) SpawnBlood( vecBloodPos, g_vecAttackDir, pHurt->BloodColor(), MIN( iDamage, 30 ) ); break;
case ZOMBIE_BLOOD_RIGHT_HAND: if( GetAttachment( "blood_right", vecBloodPos ) ) SpawnBlood( vecBloodPos, g_vecAttackDir, pHurt->BloodColor(), MIN( iDamage, 30 ) ); break;
case ZOMBIE_BLOOD_BOTH_HANDS: if( GetAttachment( "blood_left", vecBloodPos ) ) SpawnBlood( vecBloodPos, g_vecAttackDir, pHurt->BloodColor(), MIN( iDamage, 30 ) );
if( GetAttachment( "blood_right", vecBloodPos ) ) SpawnBlood( vecBloodPos, g_vecAttackDir, pHurt->BloodColor(), MIN( iDamage, 30 ) ); break;
case ZOMBIE_BLOOD_BITE: // No blood for these.
break; } } } else { AttackMissSound(); }
if ( pHurt == m_hPhysicsEnt && IsCurSchedule(SCHED_ZOMBIE_ATTACKITEM) ) { m_hPhysicsEnt = NULL; m_flNextSwat = gpGlobals->curtime + random->RandomFloat( 2, 4 ); }
return pHurt; }
//-----------------------------------------------------------------------------
// Purpose: The zombie is frustrated and pounding walls/doors. Make an appropriate noise
// Input :
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::PoundSound() { trace_t tr; Vector forward;
GetVectors( &forward, NULL, NULL );
AI_TraceLine( EyePosition(), EyePosition() + forward * 128, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr );
if( tr.fraction == 1.0 ) { // Didn't hit anything!
return; }
if( tr.fraction < 1.0 && tr.m_pEnt ) { const surfacedata_t *psurf = physprops->GetSurfaceData( tr.surface.surfaceProps ); if( psurf ) { EmitSound( physprops->GetString(psurf->sounds.impactHard) ); return; } }
// Otherwise fall through to the default sound.
CPASAttenuationFilter filter( this,"NPC_BaseZombie.PoundDoor" ); EmitSound( filter, entindex(),"NPC_BaseZombie.PoundDoor" ); }
//-----------------------------------------------------------------------------
// Purpose: Catches the monster-specific events that occur when tagged animation
// frames are played.
// Input : pEvent -
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::HandleAnimEvent( animevent_t *pEvent ) { if ( pEvent->event == AE_NPC_ATTACK_BROADCAST ) { if( GetEnemy() && GetEnemy()->IsNPC() ) { if( HasCondition(COND_CAN_MELEE_ATTACK1) ) { // This animation is sometimes played by code that doesn't intend to attack the enemy
// (For instance, code that makes a zombie take a frustrated swipe at an obstacle).
// Try not to trigger a reaction from our enemy unless we're really attacking.
GetEnemy()->MyNPCPointer()->DispatchInteraction( g_interactionZombieMeleeWarning, NULL, this ); } } return; }
if ( pEvent->event == AE_ZOMBIE_POUND ) { PoundSound(); return; }
if ( pEvent->event == AE_ZOMBIE_ALERTSOUND ) { AlertSound(); return; }
if ( pEvent->event == AE_ZOMBIE_STEP_LEFT ) { MakeAIFootstepSound( 180.0f ); FootstepSound( false ); return; } if ( pEvent->event == AE_ZOMBIE_STEP_RIGHT ) { MakeAIFootstepSound( 180.0f ); FootstepSound( true ); return; }
if ( pEvent->event == AE_ZOMBIE_GET_UP ) { MakeAIFootstepSound( 180.0f, 3.0f ); if( !IsOnFire() ) { // If you let this code run while a zombie is burning, it will stop wailing.
m_flNextMoanSound = gpGlobals->curtime; MoanSound( envDefaultZombieMoanVolumeFast, ARRAYSIZE( envDefaultZombieMoanVolumeFast ) ); } return; }
if ( pEvent->event == AE_ZOMBIE_SCUFF_LEFT ) { MakeAIFootstepSound( 180.0f ); FootscuffSound( false ); return; }
if ( pEvent->event == AE_ZOMBIE_SCUFF_RIGHT ) { MakeAIFootstepSound( 180.0f ); FootscuffSound( true ); return; }
// all swat animations are handled as a single case.
if ( pEvent->event == AE_ZOMBIE_STARTSWAT ) { MakeAIFootstepSound( 180.0f ); AttackSound(); return; }
if ( pEvent->event == AE_ZOMBIE_ATTACK_SCREAM ) { AttackSound(); return; }
if ( pEvent->event == AE_ZOMBIE_SWATITEM ) { CBaseEntity *pEnemy = GetEnemy(); if ( pEnemy ) { Vector v; CBaseEntity *pPhysicsEntity = m_hPhysicsEnt; if( !pPhysicsEntity ) { DevMsg( "**Zombie: Missing my physics ent!!" ); return; } IPhysicsObject *pPhysObj = pPhysicsEntity->VPhysicsGetObject();
if( !pPhysObj ) { DevMsg( "**Zombie: No Physics Object for physics Ent!" ); return; }
EmitSound( "NPC_BaseZombie.Swat" ); PhysicsImpactSound( pEnemy, pPhysObj, CHAN_BODY, pPhysObj->GetMaterialIndex(), physprops->GetSurfaceIndex("flesh"), 0.5, 800 );
Vector physicsCenter = pPhysicsEntity->WorldSpaceCenter(); v = pEnemy->WorldSpaceCenter() - physicsCenter; VectorNormalize(v);
// Send the object at 800 in/sec toward the enemy. Add 200 in/sec up velocity to keep it
// in the air for a second or so.
v = v * 800; v.z += 200;
// add some spin so the object doesn't appear to just fly in a straight line
// Also this spin will move the object slightly as it will press on whatever the object
// is resting on.
AngularImpulse angVelocity( random->RandomFloat(-180, 180), 20, random->RandomFloat(-360, 360) );
pPhysObj->AddVelocity( &v, &angVelocity );
// If we don't put the object scan time well into the future, the zombie
// will re-select the object he just hit as it is flying away from him.
// It will likely always be the nearest object because the zombie moved
// close enough to it to hit it.
m_hPhysicsEnt = NULL;
m_flNextSwatScan = gpGlobals->curtime + ZOMBIE_SWAT_DELAY;
return; } } if ( pEvent->event == AE_ZOMBIE_ATTACK_RIGHT ) { Vector right, forward; AngleVectors( GetLocalAngles(), &forward, &right, NULL ); right = right * 100; forward = forward * 200;
QAngle qa( -15, -20, -10 ); Vector vec = right + forward; ClawAttack( GetClawAttackRange(), sk_zombie_dmg_one_slash.GetFloat(), qa, vec, ZOMBIE_BLOOD_RIGHT_HAND ); return; }
if ( pEvent->event == AE_ZOMBIE_ATTACK_LEFT ) { Vector right, forward; AngleVectors( GetLocalAngles(), &forward, &right, NULL );
right = right * -100; forward = forward * 200;
QAngle qa( -15, 20, -10 ); Vector vec = right + forward; ClawAttack( GetClawAttackRange(), sk_zombie_dmg_one_slash.GetFloat(), qa, vec, ZOMBIE_BLOOD_LEFT_HAND ); return; }
if ( pEvent->event == AE_ZOMBIE_ATTACK_BOTH ) { Vector forward; QAngle qaPunch( 45, random->RandomInt(-5,5), random->RandomInt(-5,5) ); AngleVectors( GetLocalAngles(), &forward ); forward = forward * 200; ClawAttack( GetClawAttackRange(), sk_zombie_dmg_one_slash.GetFloat(), qaPunch, forward, ZOMBIE_BLOOD_BOTH_HANDS ); return; }
if ( pEvent->event == AE_ZOMBIE_POPHEADCRAB ) { if ( GetInteractionPartner() == NULL ) return;
const char *pString = pEvent->options; char token[128]; pString = nexttoken( token, pString, ' ' );
int boneIndex = GetInteractionPartner()->LookupBone( token );
if ( boneIndex == -1 ) { Warning( "AE_ZOMBIE_POPHEADCRAB event using invalid bone name! Usage: event AE_ZOMBIE_POPHEADCRAB \"<BoneName> <Speed>\" \n" ); return; }
pString = nexttoken( token, pString, ' ' );
if ( !token ) { Warning( "AE_ZOMBIE_POPHEADCRAB event format missing velocity parameter! Usage: event AE_ZOMBIE_POPHEADCRAB \"<BoneName> <Speed>\" \n" ); return; }
Vector vecBonePosition; QAngle angles; Vector vecHeadCrabPosition;
int iCrabAttachment = LookupAttachment( "headcrab" ); int iSpeed = atoi( token );
GetInteractionPartner()->GetBonePosition( boneIndex, vecBonePosition, angles ); GetAttachment( iCrabAttachment, vecHeadCrabPosition );
Vector vVelocity = vecHeadCrabPosition - vecBonePosition; VectorNormalize( vVelocity );
CTakeDamageInfo dmgInfo( this, GetInteractionPartner(), m_iHealth, DMG_DIRECT );
dmgInfo.SetDamagePosition( vecHeadCrabPosition );
ReleaseHeadcrab( EyePosition(), vVelocity * iSpeed, true, false, true );
GuessDamageForce( &dmgInfo, vVelocity, vecHeadCrabPosition, 0.5f ); TakeDamage( dmgInfo ); return; }
BaseClass::HandleAnimEvent( pEvent ); }
//-----------------------------------------------------------------------------
// Purpose: Spawn function for the base zombie.
//
// !!!IMPORTANT!!! YOUR DERIVED CLASS'S SPAWN() RESPONSIBILITIES:
//
// Call Precache();
// Set status for m_fIsTorso & m_fIsHeadless
// Set blood color
// Set health
// Set field of view
// Call CapabilitiesClear() & then set relevant capabilities
// THEN Call BaseClass::Spawn()
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::Spawn( void ) { SetSolid( SOLID_BBOX ); SetMoveType( MOVETYPE_STEP );
#ifdef _XBOX
// Always fade the corpse
AddSpawnFlags( SF_NPC_FADE_CORPSE ); #endif // _XBOX
m_NPCState = NPC_STATE_NONE;
CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_MELEE_ATTACK1 ); CapabilitiesAdd( bits_CAP_SQUAD );
m_flNextSwat = gpGlobals->curtime; m_flNextSwatScan = gpGlobals->curtime; m_pMoanSound = NULL;
m_flNextMoanSound = gpGlobals->curtime + 9999;
SetZombieModel();
NPCInit();
m_bIsSlumped = false;
// Zombies get to cheat for 6 seconds (sjb)
GetEnemies()->SetFreeKnowledgeDuration( 6.0 );
m_ActBusyBehavior.SetUseRenderBounds(true); }
//-----------------------------------------------------------------------------
// Purpose: Pecaches all resources this NPC needs.
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::Precache( void ) { UTIL_PrecacheOther( GetHeadcrabClassname() );
PrecacheScriptSound( "E3_Phystown.Slicer" ); PrecacheScriptSound( "NPC_BaseZombie.PoundDoor" ); PrecacheScriptSound( "NPC_BaseZombie.Swat" );
PrecacheModel( GetLegsModel() ); PrecacheModel( GetTorsoModel() );
PrecacheParticleSystem( "blood_impact_zombie_01" );
BaseClass::Precache(); }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::StartTouch( CBaseEntity *pOther ) { BaseClass::StartTouch( pOther );
if( IsSlumped() && hl2_episodic.GetBool() ) { if( FClassnameIs( pOther, "prop_physics" ) ) { // Get up!
m_ActBusyBehavior.StopBusying(); } } }
//---------------------------------------------------------
//---------------------------------------------------------
bool CNPC_BaseZombie::CreateBehaviors() { AddBehavior( &m_ActBusyBehavior );
return BaseClass::CreateBehaviors(); }
//---------------------------------------------------------
//---------------------------------------------------------
int CNPC_BaseZombie::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_CHASE_ENEMY: if ( HasCondition( COND_ZOMBIE_LOCAL_MELEE_OBSTRUCTION ) && !HasCondition(COND_TASK_FAILED) && IsCurSchedule( SCHED_ZOMBIE_CHASE_ENEMY, false ) ) { return SCHED_COMBAT_PATROL; } return SCHED_ZOMBIE_CHASE_ENEMY; break;
case SCHED_ZOMBIE_SWATITEM: // If the object is far away, move and swat it. If it's close, just swat it.
if( DistToPhysicsEnt() > ZOMBIE_PHYSOBJ_SWATDIST ) { return SCHED_ZOMBIE_MOVE_SWATITEM; } else { return SCHED_ZOMBIE_SWATITEM; } break;
case SCHED_STANDOFF: return SCHED_ZOMBIE_WANDER_STANDOFF;
case SCHED_MELEE_ATTACK1: return SCHED_ZOMBIE_MELEE_ATTACK1; }
return BaseClass::TranslateSchedule( scheduleType ); }
//-----------------------------------------------------------------------------
// 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_BaseZombie::BuildScheduleTestBits( void ) { // Ignore damage if we were recently damaged or we're attacking.
if ( GetActivity() == ACT_MELEE_ATTACK1 ) { ClearCustomInterruptCondition( COND_LIGHT_DAMAGE ); ClearCustomInterruptCondition( COND_HEAVY_DAMAGE ); } #ifndef HL2_EPISODIC
else if ( m_flNextFlinch >= gpGlobals->curtime ) { ClearCustomInterruptCondition( COND_LIGHT_DAMAGE ); ClearCustomInterruptCondition( COND_HEAVY_DAMAGE ); } #endif // !HL2_EPISODIC
// Everything should be interrupted if we get killed.
SetCustomInterruptCondition( COND_ZOMBIE_RELEASECRAB );
BaseClass::BuildScheduleTestBits(); }
//-----------------------------------------------------------------------------
// Purpose: Called when we change schedules.
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::OnScheduleChange( void ) { //
// If we took damage and changed schedules, ignore further damage for a few seconds.
//
if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE )) { m_flNextFlinch = gpGlobals->curtime + ZOMBIE_FLINCH_DELAY; }
BaseClass::OnScheduleChange(); }
//---------------------------------------------------------
//---------------------------------------------------------
int CNPC_BaseZombie::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) { if( failedSchedule == SCHED_ZOMBIE_WANDER_MEDIUM ) { return SCHED_ZOMBIE_WANDER_FAIL; }
// If we can swat physics objects, see if we can swat our obstructor
if ( CanSwatPhysicsObjects() ) { if ( !m_fIsTorso && IsPathTaskFailure( taskFailCode ) && m_hObstructor != NULL && m_hObstructor->VPhysicsGetObject() && m_hObstructor->VPhysicsGetObject()->GetMass() < 100 ) { m_hPhysicsEnt = m_hObstructor; m_hObstructor = NULL; return SCHED_ZOMBIE_ATTACKITEM; } }
m_hObstructor = NULL;
return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); }
//---------------------------------------------------------
//---------------------------------------------------------
int CNPC_BaseZombie::SelectSchedule ( void ) { if ( HasCondition( COND_ZOMBIE_RELEASECRAB ) ) { // Death waits for no man. Or zombie. Or something.
return SCHED_ZOMBIE_RELEASECRAB; }
if ( BehaviorSelectSchedule() ) { return BaseClass::SelectSchedule(); }
switch ( m_NPCState ) { case NPC_STATE_COMBAT: if ( HasCondition( COND_NEW_ENEMY ) && GetEnemy() ) { float flDist;
flDist = ( GetLocalOrigin() - GetEnemy()->GetLocalOrigin() ).Length();
// If this is a new enemy that's far away, ambush!!
if (flDist >= zombie_ambushdist.GetFloat() && MustCloseToAttack() ) { return SCHED_ZOMBIE_MOVE_TO_AMBUSH; } }
if ( HasCondition( COND_LOST_ENEMY ) || ( HasCondition( COND_ENEMY_UNREACHABLE ) && MustCloseToAttack() ) ) { return SCHED_ZOMBIE_WANDER_MEDIUM; }
if( HasCondition( COND_ZOMBIE_CAN_SWAT_ATTACK ) ) { return SCHED_ZOMBIE_SWATITEM; } break;
case NPC_STATE_ALERT: if ( HasCondition( COND_LOST_ENEMY ) || HasCondition( COND_ENEMY_DEAD ) || ( HasCondition( COND_ENEMY_UNREACHABLE ) && MustCloseToAttack() ) ) { ClearCondition( COND_LOST_ENEMY ); ClearCondition( COND_ENEMY_UNREACHABLE );
#ifdef DEBUG_ZOMBIES
DevMsg("Wandering\n"); #endif
// Just lost track of our enemy.
// Wander around a bit so we don't look like a dingus.
return SCHED_ZOMBIE_WANDER_MEDIUM; } break; }
return BaseClass::SelectSchedule(); }
//---------------------------------------------------------
//---------------------------------------------------------
bool CNPC_BaseZombie::IsSlumped( void ) { if( hl2_episodic.GetBool() ) { if( m_ActBusyBehavior.IsInsideActBusy() && !m_ActBusyBehavior.IsStopBusying() ) { return true; } } else { int sequence = GetSequence(); if ( sequence != -1 ) { return ( strncmp( GetSequenceName( sequence ), "slump", 5 ) == 0 ); } }
return false; }
//---------------------------------------------------------
//---------------------------------------------------------
bool CNPC_BaseZombie::IsGettingUp( void ) { if( m_ActBusyBehavior.IsActive() && m_ActBusyBehavior.IsStopBusying() ) { return true; } return false; }
//---------------------------------------------------------
//---------------------------------------------------------
int CNPC_BaseZombie::GetSwatActivity( void ) { // Hafta figure out whether to swat with left or right arm.
// Also hafta figure out whether to swat high or low. (later)
float flDot; Vector vecRight, vecDirToObj;
AngleVectors( GetLocalAngles(), NULL, &vecRight, NULL ); vecDirToObj = m_hPhysicsEnt->GetLocalOrigin() - GetLocalOrigin(); VectorNormalize(vecDirToObj);
// compare in 2D.
vecRight.z = 0.0; vecDirToObj.z = 0.0;
flDot = DotProduct( vecRight, vecDirToObj );
Vector vecMyCenter; Vector vecObjCenter;
vecMyCenter = WorldSpaceCenter(); vecObjCenter = m_hPhysicsEnt->WorldSpaceCenter(); float flZDiff;
flZDiff = vecMyCenter.z - vecObjCenter.z;
if( flDot >= 0 ) { // Right
if( flZDiff < 0 ) { return ACT_ZOM_SWATRIGHTMID; }
return ACT_ZOM_SWATRIGHTLOW; } else { // Left
if( flZDiff < 0 ) { return ACT_ZOM_SWATLEFTMID; }
return ACT_ZOM_SWATLEFTLOW; } }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::GatherConditions( void ) { ClearCondition( COND_ZOMBIE_LOCAL_MELEE_OBSTRUCTION );
BaseClass::GatherConditions();
if( m_NPCState == NPC_STATE_COMBAT && !m_fIsTorso ) { // This check for !m_pPhysicsEnt prevents a crashing bug, but also
// eliminates the zombie picking a better physics object if one happens to fall
// between him and the object he's heading for already.
if( gpGlobals->curtime >= m_flNextSwatScan && (m_hPhysicsEnt == NULL) ) { FindNearestPhysicsObject( ZOMBIE_MAX_PHYSOBJ_MASS ); m_flNextSwatScan = gpGlobals->curtime + 2.0; } }
if( (m_hPhysicsEnt != NULL) && gpGlobals->curtime >= m_flNextSwat && HasCondition( COND_SEE_ENEMY ) && !HasCondition( COND_ZOMBIE_RELEASECRAB ) ) { SetCondition( COND_ZOMBIE_CAN_SWAT_ATTACK ); } else { ClearCondition( COND_ZOMBIE_CAN_SWAT_ATTACK ); } }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::PrescheduleThink( void ) { BaseClass::PrescheduleThink(); #if 0
DevMsg(" ** %d Angry Zombies **\n", s_iAngryZombies ); #endif
#if 0
if( m_NPCState == NPC_STATE_COMBAT ) { // Zombies should make idle sounds in combat
if( random->RandomInt( 0, 30 ) == 0 ) { IdleSound(); } } #endif
//
// Cool off if we aren't burned for five seconds or so.
//
if ( ( m_flBurnDamageResetTime ) && ( gpGlobals->curtime >= m_flBurnDamageResetTime ) ) { m_flBurnDamage = 0; } }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_ZOMBIE_DIE: // Go to ragdoll
KillMe(); TaskComplete(); break;
case TASK_ZOMBIE_GET_PATH_TO_PHYSOBJ: { Vector vecGoalPos; Vector vecDir;
vecDir = GetLocalOrigin() - m_hPhysicsEnt->GetLocalOrigin(); VectorNormalize(vecDir); vecDir.z = 0;
AI_NavGoal_t goal( m_hPhysicsEnt->WorldSpaceCenter() ); goal.pTarget = m_hPhysicsEnt; GetNavigator()->SetGoal( goal );
TaskComplete(); } break;
case TASK_ZOMBIE_SWAT_ITEM: { if( m_hPhysicsEnt == NULL ) { // Physics Object is gone! Probably was an explosive
// or something else broke it.
TaskFail("Physics ent NULL"); } else if ( DistToPhysicsEnt() > ZOMBIE_PHYSOBJ_SWATDIST ) { // Physics ent is no longer in range! Probably another zombie swatted it or it moved
// for some other reason.
TaskFail( "Physics swat item has moved" ); } else { SetIdealActivity( (Activity)GetSwatActivity() ); } break; } break;
case TASK_ZOMBIE_DELAY_SWAT: m_flNextSwat = gpGlobals->curtime + pTask->flTaskData; TaskComplete(); break;
case TASK_ZOMBIE_RELEASE_HEADCRAB: { // make the crab look like it's pushing off the body
Vector vecForward; Vector vecVelocity;
AngleVectors( GetAbsAngles(), &vecForward ); vecVelocity = vecForward * 30; vecVelocity.z += 100;
ReleaseHeadcrab( EyePosition(), vecVelocity, true, true ); TaskComplete(); } break;
case TASK_ZOMBIE_WAIT_POST_MELEE: { #ifndef HL2_EPISODIC
TaskComplete(); return; #endif
// Don't wait when attacking the player
if ( GetEnemy() && GetEnemy()->IsPlayer() ) { TaskComplete(); return; }
// Wait a single think
SetWait( 0.1 ); } break;
default: BaseClass::StartTask( pTask ); } }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_ZOMBIE_SWAT_ITEM: if( IsActivityFinished() ) { TaskComplete(); } break;
case TASK_ZOMBIE_WAIT_POST_MELEE: { if ( IsWaitFinished() ) { TaskComplete(); } } break; default: BaseClass::RunTask( pTask ); break; } }
//---------------------------------------------------------
// Make the necessary changes to a zombie to make him a
// torso!
//---------------------------------------------------------
void CNPC_BaseZombie::BecomeTorso( const Vector &vecTorsoForce, const Vector &vecLegsForce ) { if( m_fIsTorso ) { DevMsg( "*** Zombie is already a torso!\n" ); return; }
if( IsOnFire() ) { Extinguish(); Ignite( 30 ); }
if ( !m_fIsHeadless ) { m_iMaxHealth = ZOMBIE_TORSO_HEALTH_FACTOR * m_iMaxHealth; m_iHealth = m_iMaxHealth;
// No more opening doors!
CapabilitiesRemove( bits_CAP_DOORS_GROUP ); ClearSchedule( "Becoming torso" ); GetNavigator()->ClearGoal(); m_hPhysicsEnt = NULL;
// Put the zombie in a TOSS / fall schedule
// Otherwise he fails and sits on the ground for a sec.
SetSchedule( SCHED_FALL_TO_GROUND );
m_fIsTorso = true;
// Put the torso up where the torso was when the zombie
// was whole.
Vector origin = GetAbsOrigin(); origin.z += 40; SetAbsOrigin( origin );
SetGroundEntity( NULL ); // assume zombie mass ~ 100 kg
ApplyAbsVelocityImpulse( vecTorsoForce * (1.0 / 100.0) ); }
float flFadeTime = 0.0;
if( HasSpawnFlags( SF_NPC_FADE_CORPSE ) ) { flFadeTime = 5.0; }
if ( m_fIsTorso == true ) { // -40 on Z to make up for the +40 on Z that we did above. This stops legs spawning above the head.
CBaseEntity *pGib = CreateRagGib( GetLegsModel(), GetAbsOrigin() - Vector(0, 0, 40), GetAbsAngles(), vecLegsForce, flFadeTime );
// don't collide with this thing ever
if ( pGib ) { pGib->SetOwnerEntity( this ); } }
SetZombieModel(); }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::Event_Killed( const CTakeDamageInfo &info ) { if ( info.GetDamageType() & DMG_VEHICLE ) { Vector vecDamageDir = info.GetDamageForce(); VectorNormalize( vecDamageDir );
// Big blood splat
UTIL_BloodSpray( WorldSpaceCenter(), vecDamageDir, BLOOD_COLOR_YELLOW, 8, FX_BLOODSPRAY_CLOUD ); }
BaseClass::Event_Killed( info ); }
//---------------------------------------------------------
//---------------------------------------------------------
bool CNPC_BaseZombie::BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector ) { bool bKilledByVehicle = ( ( info.GetDamageType() & DMG_VEHICLE ) != 0 ); if( m_fIsTorso || (!IsChopped(info) && !IsSquashed(info)) || bKilledByVehicle ) { return BaseClass::BecomeRagdoll( info, forceVector ); }
if( !(GetFlags()&FL_TRANSRAGDOLL) ) { RemoveDeferred(); }
return true; }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::StopLoopingSounds() { ENVELOPE_CONTROLLER.SoundDestroy( m_pMoanSound ); m_pMoanSound = NULL;
BaseClass::StopLoopingSounds(); }
//---------------------------------------------------------
//---------------------------------------------------------
void CNPC_BaseZombie::RemoveHead( void ) { m_fIsHeadless = true; SetZombieModel(); }
bool CNPC_BaseZombie::ShouldPlayFootstepMoan( void ) { if( random->RandomInt( 1, zombie_stepfreq.GetInt() * s_iAngryZombies ) == 1 ) { return true; }
return false; }
#define ZOMBIE_CRAB_INHERITED_SPAWNFLAGS (SF_NPC_GAG|SF_NPC_LONG_RANGE|SF_NPC_FADE_CORPSE|SF_NPC_ALWAYSTHINK)
#define CRAB_HULL_EXPAND 1.1f
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::HeadcrabFits( CBaseAnimating *pCrab ) { Vector vecSpawnLoc = pCrab->GetAbsOrigin();
CTraceFilterSimpleList traceFilter( COLLISION_GROUP_NONE ); traceFilter.AddEntityToIgnore( pCrab ); traceFilter.AddEntityToIgnore( this ); if ( GetInteractionPartner() ) { traceFilter.AddEntityToIgnore( GetInteractionPartner() ); }
trace_t tr; AI_TraceHull( vecSpawnLoc, vecSpawnLoc - Vector( 0, 0, 1 ), NAI_Hull::Mins(HULL_TINY) * CRAB_HULL_EXPAND, NAI_Hull::Maxs(HULL_TINY) * CRAB_HULL_EXPAND, MASK_NPCSOLID, &traceFilter, &tr );
if( tr.fraction != 1.0 ) { //NDebugOverlay::Box( vecSpawnLoc, NAI_Hull::Mins(HULL_TINY) * CRAB_HULL_EXPAND, NAI_Hull::Maxs(HULL_TINY) * CRAB_HULL_EXPAND, 255, 0, 0, 100, 10.0 );
return false; }
//NDebugOverlay::Box( vecSpawnLoc, NAI_Hull::Mins(HULL_TINY) * CRAB_HULL_EXPAND, NAI_Hull::Maxs(HULL_TINY) * CRAB_HULL_EXPAND, 0, 255, 0, 100, 10.0 );
return true; }
//-----------------------------------------------------------------------------
// Purpose:
// Input : &vecOrigin -
// &vecVelocity -
// fRemoveHead -
// fRagdollBody -
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::ReleaseHeadcrab( const Vector &vecOrigin, const Vector &vecVelocity, bool fRemoveHead, bool fRagdollBody, bool fRagdollCrab ) { CAI_BaseNPC *pCrab; Vector vecSpot = vecOrigin;
// Until the headcrab is a bodygroup, we have to approximate the
// location of the head with magic numbers.
if( !m_fIsTorso ) { vecSpot.z -= 16; }
if( fRagdollCrab ) { //Vector vecForce = Vector( 0, 0, random->RandomFloat( 700, 1100 ) );
CBaseEntity *pGib = CreateRagGib( GetHeadcrabModel(), vecOrigin, GetLocalAngles(), vecVelocity, 15, ShouldIgniteZombieGib() );
if ( pGib ) { CBaseAnimating *pAnimatingGib = dynamic_cast<CBaseAnimating*>(pGib);
// don't collide with this thing ever
int iCrabAttachment = LookupAttachment( "headcrab" ); if (iCrabAttachment > 0 && pAnimatingGib ) { SetHeadcrabSpawnLocation( iCrabAttachment, pAnimatingGib ); }
if( !HeadcrabFits(pAnimatingGib) ) { UTIL_Remove(pGib); return; }
pGib->SetOwnerEntity( this ); CopyRenderColorTo( pGib );
if( UTIL_ShouldShowBlood(BLOOD_COLOR_YELLOW) ) { UTIL_BloodImpact( pGib->WorldSpaceCenter(), Vector(0,0,1), BLOOD_COLOR_YELLOW, 1 );
for ( int i = 0 ; i < 3 ; i++ ) { Vector vecSpot = pGib->WorldSpaceCenter(); vecSpot.x += random->RandomFloat( -8, 8 ); vecSpot.y += random->RandomFloat( -8, 8 ); vecSpot.z += random->RandomFloat( -8, 8 );
UTIL_BloodDrips( vecSpot, vec3_origin, BLOOD_COLOR_YELLOW, 50 ); } } } } else { pCrab = (CAI_BaseNPC*)CreateEntityByName( GetHeadcrabClassname() );
if ( !pCrab ) { Warning( "**%s: Can't make %s!\n", GetClassname(), GetHeadcrabClassname() ); return; }
// Stick the crab in whatever squad the zombie was in.
pCrab->SetSquadName( m_SquadName );
// don't pop to floor, fall
pCrab->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); // add on the parent flags
pCrab->AddSpawnFlags( m_spawnflags & ZOMBIE_CRAB_INHERITED_SPAWNFLAGS ); // make me the crab's owner to avoid collision issues
pCrab->SetOwnerEntity( this );
pCrab->SetAbsOrigin( vecSpot ); pCrab->SetAbsAngles( GetAbsAngles() ); DispatchSpawn( pCrab );
pCrab->GetMotor()->SetIdealYaw( GetAbsAngles().y );
// FIXME: npc's with multiple headcrabs will need some way to query different attachments.
// NOTE: this has till after spawn is called so that the model is set up
int iCrabAttachment = LookupAttachment( "headcrab" ); if (iCrabAttachment > 0) { SetHeadcrabSpawnLocation( iCrabAttachment, pCrab ); pCrab->GetMotor()->SetIdealYaw( pCrab->GetAbsAngles().y ); // Take out any pitch
QAngle angles = pCrab->GetAbsAngles(); angles.x = 0.0; pCrab->SetAbsAngles( angles ); }
if( !HeadcrabFits(pCrab) ) { UTIL_Remove(pCrab); return; }
pCrab->SetActivity( ACT_IDLE ); pCrab->SetNextThink( gpGlobals->curtime ); pCrab->PhysicsSimulate(); pCrab->SetAbsVelocity( vecVelocity );
// if I have an enemy, stuff that to the headcrab.
CBaseEntity *pEnemy; pEnemy = GetEnemy();
pCrab->m_flNextAttack = gpGlobals->curtime + 1.0f;
if( pEnemy ) { pCrab->SetEnemy( pEnemy ); } if( ShouldIgniteZombieGib() ) { pCrab->Ignite( 30 ); }
CopyRenderColorTo( pCrab );
pCrab->Activate(); }
if( fRemoveHead ) { RemoveHead(); }
if( fRagdollBody ) { BecomeRagdollOnClient( vec3_origin ); } }
void CNPC_BaseZombie::SetHeadcrabSpawnLocation( int iCrabAttachment, CBaseAnimating *pCrab ) { Assert( iCrabAttachment > 0 );
// get world location of intended headcrab root bone
matrix3x4_t attachmentToWorld; GetAttachment( iCrabAttachment, attachmentToWorld );
// find offset of root bone from origin
pCrab->SetAbsOrigin( Vector( 0, 0, 0 ) ); pCrab->SetAbsAngles( QAngle( 0, 0, 0 ) ); pCrab->InvalidateBoneCache(); matrix3x4_t rootLocal; pCrab->GetBoneTransform( 0, rootLocal );
// invert it
matrix3x4_t rootInvLocal; MatrixInvert( rootLocal, rootInvLocal );
// find spawn location needed for rootLocal transform to match attachmentToWorld
matrix3x4_t spawnOrigin; ConcatTransforms( attachmentToWorld, rootInvLocal, spawnOrigin );
// reset location of headcrab
Vector vecOrigin; QAngle vecAngles; MatrixAngles( spawnOrigin, vecAngles, vecOrigin ); pCrab->SetAbsOrigin( vecOrigin ); // FIXME: head crabs don't like pitch or roll!
vecAngles.z = 0;
pCrab->SetAbsAngles( vecAngles ); pCrab->InvalidateBoneCache(); }
//---------------------------------------------------------
// Provides a standard way for the zombie to get the
// distance to a physics ent. Since the code to find physics
// objects uses a fast dis approx, we have to use that here
// as well.
//---------------------------------------------------------
float CNPC_BaseZombie::DistToPhysicsEnt( void ) { //return ( GetLocalOrigin() - m_hPhysicsEnt->GetLocalOrigin() ).Length();
if ( m_hPhysicsEnt != NULL ) return UTIL_DistApprox2D( GetAbsOrigin(), m_hPhysicsEnt->WorldSpaceCenter() ); return ZOMBIE_PHYSOBJ_SWATDIST + 1; }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::OnStateChange( NPC_STATE OldState, NPC_STATE NewState ) { switch( NewState ) { case NPC_STATE_COMBAT: { RemoveSpawnFlags( SF_NPC_GAG ); s_iAngryZombies++; } break;
default: if( OldState == NPC_STATE_COMBAT ) { // Only decrement if coming OUT of combat state.
s_iAngryZombies--; } break; } }
//-----------------------------------------------------------------------------
// Purpose: Refines a base activity into something more specific to our internal state.
//-----------------------------------------------------------------------------
Activity CNPC_BaseZombie::NPC_TranslateActivity( Activity baseAct ) { if ( baseAct == ACT_WALK && IsCurSchedule( SCHED_COMBAT_PATROL, false) ) baseAct = ACT_RUN;
if ( IsOnFire() ) { switch ( baseAct ) { case ACT_RUN_ON_FIRE: { return ( Activity )ACT_WALK_ON_FIRE; }
case ACT_WALK: { // I'm on fire. Put ME out.
return ( Activity )ACT_WALK_ON_FIRE; }
case ACT_IDLE: { // I'm on fire. Put ME out.
return ( Activity )ACT_IDLE_ON_FIRE; } } }
return BaseClass::NPC_TranslateActivity( baseAct ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
Vector CNPC_BaseZombie::BodyTarget( const Vector &posSrc, bool bNoisy ) { if( IsCurSchedule(SCHED_BIG_FLINCH) || m_ActBusyBehavior.IsActive() ) { // This zombie is assumed to be standing up.
// Return a position that's centered over the absorigin,
// halfway between the origin and the head.
Vector vecTarget = GetAbsOrigin(); Vector vecHead = HeadTarget( posSrc ); vecTarget.z = ((vecTarget.z + vecHead.z) * 0.5f); return vecTarget; }
return BaseClass::BodyTarget( posSrc, bNoisy ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
Vector CNPC_BaseZombie::HeadTarget( const Vector &posSrc ) { int iCrabAttachment = LookupAttachment( "headcrab" ); Assert( iCrabAttachment > 0 );
Vector vecPosition;
GetAttachment( iCrabAttachment, vecPosition );
return vecPosition; }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
float CNPC_BaseZombie::GetAutoAimRadius() { if( m_fIsTorso ) { return 12.0f; }
return BaseClass::GetAutoAimRadius(); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_BaseZombie::OnInsufficientStopDist( AILocalMoveGoal_t *pMoveGoal, float distClear, AIMoveResult_t *pResult ) { if ( pMoveGoal->directTrace.fStatus == AIMR_BLOCKED_ENTITY && gpGlobals->curtime >= m_flNextSwat ) { m_hObstructor = pMoveGoal->directTrace.pObstruction; } return false; }
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pEnemy -
// &chasePosition -
//-----------------------------------------------------------------------------
void CNPC_BaseZombie::TranslateNavGoal( CBaseEntity *pEnemy, Vector &chasePosition ) { // If our enemy is in a vehicle, we need them to tell us where to navigate to them
if ( pEnemy == NULL ) return;
CBaseCombatCharacter *pBCC = pEnemy->MyCombatCharacterPointer(); if ( pBCC && pBCC->IsInAVehicle() ) { Vector vecForward, vecRight; pBCC->GetVectors( &vecForward, &vecRight, NULL );
chasePosition = pBCC->WorldSpaceCenter() + ( vecForward * 24.0f ) + ( vecRight * 48.0f ); return; }
BaseClass::TranslateNavGoal( pEnemy, chasePosition ); }
//-----------------------------------------------------------------------------
//
// Schedules
//
//-----------------------------------------------------------------------------
AI_BEGIN_CUSTOM_NPC( base_zombie, CNPC_BaseZombie )
DECLARE_TASK( TASK_ZOMBIE_DELAY_SWAT ) DECLARE_TASK( TASK_ZOMBIE_SWAT_ITEM ) DECLARE_TASK( TASK_ZOMBIE_GET_PATH_TO_PHYSOBJ ) DECLARE_TASK( TASK_ZOMBIE_DIE ) DECLARE_TASK( TASK_ZOMBIE_RELEASE_HEADCRAB ) DECLARE_TASK( TASK_ZOMBIE_WAIT_POST_MELEE )
DECLARE_ACTIVITY( ACT_ZOM_SWATLEFTMID ) DECLARE_ACTIVITY( ACT_ZOM_SWATRIGHTMID ) DECLARE_ACTIVITY( ACT_ZOM_SWATLEFTLOW ) DECLARE_ACTIVITY( ACT_ZOM_SWATRIGHTLOW ) DECLARE_ACTIVITY( ACT_ZOM_RELEASECRAB ) DECLARE_ACTIVITY( ACT_ZOM_FALL )
DECLARE_CONDITION( COND_ZOMBIE_CAN_SWAT_ATTACK ) DECLARE_CONDITION( COND_ZOMBIE_RELEASECRAB ) DECLARE_CONDITION( COND_ZOMBIE_LOCAL_MELEE_OBSTRUCTION )
//Adrian: events go here
DECLARE_ANIMEVENT( AE_ZOMBIE_ATTACK_RIGHT ) DECLARE_ANIMEVENT( AE_ZOMBIE_ATTACK_LEFT ) DECLARE_ANIMEVENT( AE_ZOMBIE_ATTACK_BOTH ) DECLARE_ANIMEVENT( AE_ZOMBIE_SWATITEM ) DECLARE_ANIMEVENT( AE_ZOMBIE_STARTSWAT ) DECLARE_ANIMEVENT( AE_ZOMBIE_STEP_LEFT ) DECLARE_ANIMEVENT( AE_ZOMBIE_STEP_RIGHT ) DECLARE_ANIMEVENT( AE_ZOMBIE_SCUFF_LEFT ) DECLARE_ANIMEVENT( AE_ZOMBIE_SCUFF_RIGHT ) DECLARE_ANIMEVENT( AE_ZOMBIE_ATTACK_SCREAM ) DECLARE_ANIMEVENT( AE_ZOMBIE_GET_UP ) DECLARE_ANIMEVENT( AE_ZOMBIE_POUND ) DECLARE_ANIMEVENT( AE_ZOMBIE_ALERTSOUND ) DECLARE_ANIMEVENT( AE_ZOMBIE_POPHEADCRAB )
DECLARE_INTERACTION( g_interactionZombieMeleeWarning )
DEFINE_SCHEDULE ( SCHED_ZOMBIE_MOVE_SWATITEM,
" Tasks" " TASK_ZOMBIE_DELAY_SWAT 3" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" " TASK_ZOMBIE_GET_PATH_TO_PHYSOBJ 0" " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" " TASK_ZOMBIE_SWAT_ITEM 0" " " " Interrupts" " COND_ZOMBIE_RELEASECRAB" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" )
//=========================================================
// SwatItem
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_SWATITEM,
" Tasks" " TASK_ZOMBIE_DELAY_SWAT 3" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" " TASK_FACE_ENEMY 0" " TASK_ZOMBIE_SWAT_ITEM 0" " " " Interrupts" " COND_ZOMBIE_RELEASECRAB" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" )
//=========================================================
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_ATTACKITEM,
" Tasks" " TASK_FACE_ENEMY 0" " TASK_MELEE_ATTACK1 0" " " " Interrupts" " COND_ZOMBIE_RELEASECRAB" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" )
//=========================================================
// ChaseEnemy
//=========================================================
#ifdef HL2_EPISODIC
DEFINE_SCHEDULE ( SCHED_ZOMBIE_CHASE_ENEMY,
" Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED" " TASK_SET_TOLERANCE_DISTANCE 24" " TASK_GET_CHASE_PATH_TO_ENEMY 600" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" " " " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ENEMY_UNREACHABLE" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_TOO_CLOSE_TO_ATTACK" " COND_TASK_FAILED" " COND_ZOMBIE_CAN_SWAT_ATTACK" " COND_ZOMBIE_RELEASECRAB" " COND_HEAVY_DAMAGE" ) #else
DEFINE_SCHEDULE ( SCHED_ZOMBIE_CHASE_ENEMY,
" Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED" " TASK_SET_TOLERANCE_DISTANCE 24" " TASK_GET_CHASE_PATH_TO_ENEMY 600" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" " " " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ENEMY_UNREACHABLE" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_TOO_CLOSE_TO_ATTACK" " COND_TASK_FAILED" " COND_ZOMBIE_CAN_SWAT_ATTACK" " COND_ZOMBIE_RELEASECRAB" ) #endif // HL2_EPISODIC
//=========================================================
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_RELEASECRAB,
" Tasks" " TASK_PLAY_PRIVATE_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_ZOM_RELEASECRAB" " TASK_ZOMBIE_RELEASE_HEADCRAB 0" " TASK_ZOMBIE_DIE 0" " " " Interrupts" " COND_TASK_FAILED" )
//=========================================================
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_MOVE_TO_AMBUSH,
" Tasks" " TASK_WAIT 1.0" // don't react as soon as you see the player.
" TASK_FIND_COVER_FROM_ENEMY 0" " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_TURN_LEFT 180" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ZOMBIE_WAIT_AMBUSH" " " " Interrupts" " COND_TASK_FAILED" " COND_NEW_ENEMY" )
//=========================================================
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_WAIT_AMBUSH,
" Tasks" " TASK_WAIT_FACE_ENEMY 99999" " " " Interrupts" " COND_NEW_ENEMY" " COND_SEE_ENEMY" )
//=========================================================
// Wander around for a while so we don't look stupid.
// this is done if we ever lose track of our enemy.
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_WANDER_MEDIUM,
" Tasks" " TASK_STOP_MOVING 0" " TASK_WANDER 480384" // 4 feet to 32 feet
" TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_WAIT_PVS 0" // if the player left my PVS, just wait.
" TASK_SET_SCHEDULE SCHEDULE:SCHED_ZOMBIE_WANDER_MEDIUM" // keep doing it
" " " Interrupts" " COND_NEW_ENEMY" " COND_SEE_ENEMY" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" )
DEFINE_SCHEDULE ( SCHED_ZOMBIE_WANDER_STANDOFF,
" Tasks" " TASK_STOP_MOVING 0" " TASK_WANDER 480384" // 4 feet to 32 feet
" TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_WAIT_PVS 0" // if the player left my PVS, just wait.
" " " Interrupts" " COND_NEW_ENEMY" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_ENEMY_DEAD" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_ZOMBIE_RELEASECRAB" )
//=========================================================
// If you fail to wander, wait just a bit and try again.
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_WANDER_FAIL,
" Tasks" " TASK_STOP_MOVING 0" " TASK_WAIT 1" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ZOMBIE_WANDER_MEDIUM" " Interrupts" " COND_NEW_ENEMY" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_ENEMY_DEAD" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_ZOMBIE_RELEASECRAB" )
//=========================================================
// Like the base class, only don't stop in the middle of
// swinging if the enemy is killed, hides, or new enemy.
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_MELEE_ATTACK1,
" Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
" TASK_MELEE_ATTACK1 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ZOMBIE_POST_MELEE_WAIT" "" " Interrupts" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" )
//=========================================================
// Make the zombie wait a frame after a melee attack, to
// allow itself & it's enemy to test for dynamic scripted sequences.
//=========================================================
DEFINE_SCHEDULE ( SCHED_ZOMBIE_POST_MELEE_WAIT,
" Tasks" " TASK_ZOMBIE_WAIT_POST_MELEE 0" )
AI_END_CUSTOM_NPC()
|