//========= 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(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(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(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(pVehicleEntity) ) { pDriver = static_cast(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 \" \" \n" ); return; } pString = nexttoken( token, pString, ' ' ); if ( !token ) { Warning( "AE_ZOMBIE_POPHEADCRAB event format missing velocity parameter! Usage: event AE_ZOMBIE_POPHEADCRAB \" \" \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(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()