rott/kf_sources/KFChar/Classes/ZombieScrake.uc
2026-07-14 20:27:09 +07:00

685 lines
20 KiB
Ucode

// Chainsaw Zombie Monster for KF Invasion gametype
// He's not quite as speedy as the other Zombies, But his attacks are TRULY damaging.
class ZombieScrake extends ZombieScrakeBase
abstract;
#exec OBJ LOAD FILE=PlayerSounds.uax
//----------------------------------------------------------------------------
// NOTE: All Variables are declared in the base class to eliminate hitching
//----------------------------------------------------------------------------
simulated function PostNetBeginPlay()
{
EnableChannelNotify ( 1,1);
AnimBlendParams(1, 1.0, 0.0,, SpineBone1);
super.PostNetBeginPlay();
}
simulated function PostBeginPlay()
{
super.PostBeginPlay();
SpawnExhaustEmitter();
}
// Make the scrakes's ambient scale higher, since there are just a few, and thier chainsaw need to be heard from a distance
simulated function CalcAmbientRelevancyScale()
{
// Make the zed only relevant by thier ambient sound out to a range of 30 meters
CustomAmbientRelevancyScale = 1500/(100 * SoundRadius);
}
simulated function PostNetReceive()
{
if (bCharging)
MovementAnims[0]='ChargeF';
else if( !(bCrispified && bBurnified) )
MovementAnims[0]=default.MovementAnims[0];
}
// This zed has been taken control of. Boost its health and speed
function SetMindControlled(bool bNewMindControlled)
{
if( bNewMindControlled )
{
NumZCDHits++;
// if we hit him a couple of times, make him rage!
if( NumZCDHits > 1 )
{
if( !IsInState('RunningToMarker') )
{
GotoState('RunningToMarker');
}
else
{
NumZCDHits = 1;
if( IsInState('RunningToMarker') )
{
GotoState('');
}
}
}
else
{
if( IsInState('RunningToMarker') )
{
GotoState('');
}
}
if( bNewMindControlled != bZedUnderControl )
{
SetGroundSpeed(OriginalGroundSpeed * 1.25);
Health *= 1.25;
HealthMax *= 1.25;
}
}
else
{
NumZCDHits=0;
}
bZedUnderControl = bNewMindControlled;
}
// Handle the zed being commanded to move to a new location
function GivenNewMarker()
{
if( bCharging && NumZCDHits > 1 )
{
GotoState('RunningToMarker');
}
else
{
GotoState('');
}
}
simulated function SetBurningBehavior()
{
// If we're burning stop charging
if( Role == Role_Authority && IsInState('RunningState') )
{
super.SetBurningBehavior();
GotoState('');
}
super.SetBurningBehavior();
}
simulated function SpawnExhaustEmitter()
{
if ( Level.NetMode != NM_DedicatedServer )
{
if ( ExhaustEffectClass != none )
{
ExhaustEffect = Spawn(ExhaustEffectClass, self);
if ( ExhaustEffect != none )
{
AttachToBone(ExhaustEffect, 'Chainsaw_lod1');
ExhaustEffect.SetRelativeLocation(vect(0, -20, 0));
}
}
}
}
simulated function UpdateExhaustEmitter()
{
local byte Throttle;
if ( Level.NetMode != NM_DedicatedServer )
{
if ( ExhaustEffect != none )
{
if ( bShotAnim )
{
Throttle = 3;
}
else
{
Throttle = 0;
}
}
else
{
if ( !bNoExhaustRespawn )
{
SpawnExhaustEmitter();
}
}
}
}
simulated function Tick(float DeltaTime)
{
super.Tick(DeltaTime);
UpdateExhaustEmitter();
}
function RangedAttack(Actor A)
{
if ( bShotAnim || Physics == PHYS_Swimming)
return;
else if ( CanAttack(A) )
{
bShotAnim = true;
SetAnimAction(MeleeAnims[Rand(2)]);
CurrentDamType = ZombieDamType[0];
//PlaySound(sound'Claw2s', SLOT_None); KFTODO: Replace this
GoToState('SawingLoop');
}
if( !bShotAnim && !bDecapitated )
{
if ( Level.Game.GameDifficulty < 5.0 )
{
if ( float(Health)/HealthMax < 0.5 )
GoToState('RunningState');
}
else
{
if ( float(Health)/HealthMax < 0.75 ) // Changed Rage Point from 0.5 to 0.75 in Balance Round 1(applied to only Suicidal and HoE in Round 7)
GoToState('RunningState');
}
}
}
state RunningState
{
// Set the zed to the zapped behavior
simulated function SetZappedBehavior()
{
Global.SetZappedBehavior();
GoToState('');
}
// Don't override speed in this state
function bool CanSpeedAdjust()
{
return false;
}
function BeginState()
{
if( bZapped )
{
GoToState('');
}
else
{
SetGroundSpeed(OriginalGroundSpeed * 3.5);
bCharging = true;
if( Level.NetMode!=NM_DedicatedServer )
PostNetReceive();
NetUpdateTime = Level.TimeSeconds - 1;
}
}
function EndState()
{
if( !bZapped )
{
SetGroundSpeed(GetOriginalGroundSpeed());
}
bCharging = False;
if( Level.NetMode!=NM_DedicatedServer )
PostNetReceive();
}
function RemoveHead()
{
GoToState('');
Global.RemoveHead();
}
function RangedAttack(Actor A)
{
if ( bShotAnim || Physics == PHYS_Swimming)
return;
else if ( CanAttack(A) )
{
bShotAnim = true;
SetAnimAction(MeleeAnims[Rand(2)]);
CurrentDamType = ZombieDamType[0];
GoToState('SawingLoop');
}
}
}
// State where the zed is charging to a marked location.
// Not sure if we need this since its just like RageCharging,
// but keeping it here for now in case we need to implement some
// custom behavior for this state
state RunningToMarker extends RunningState
{
}
State SawingLoop
{
// Don't override speed in this state
function bool CanSpeedAdjust()
{
return false;
}
function bool CanGetOutOfWay()
{
return false;
}
function BeginState()
{
local float ChargeChance, RagingChargeChance;
// Decide what chance the scrake has of charging during an attack
if( Level.Game.GameDifficulty < 2.0 )
{
ChargeChance = 0.25;
RagingChargeChance = 0.5;
}
else if( Level.Game.GameDifficulty < 4.0 )
{
ChargeChance = 0.5;
RagingChargeChance = 0.70;
}
else if( Level.Game.GameDifficulty < 5.0 )
{
ChargeChance = 0.65;
RagingChargeChance = 0.85;
}
else // Hardest difficulty
{
ChargeChance = 0.95;
RagingChargeChance = 1.0;
}
// Randomly have the scrake charge during an attack so it will be less predictable
if( (Health/HealthMax < 0.5 && FRand() <= RagingChargeChance ) || FRand() <= ChargeChance )
{
SetGroundSpeed(OriginalGroundSpeed * AttackChargeRate);
bCharging = true;
if( Level.NetMode!=NM_DedicatedServer )
PostNetReceive();
NetUpdateTime = Level.TimeSeconds - 1;
}
}
function RangedAttack(Actor A)
{
if ( bShotAnim )
return;
else if ( CanAttack(A) )
{
Acceleration = vect(0,0,0);
bShotAnim = true;
MeleeDamage = default.MeleeDamage*0.6;
SetAnimAction('SawImpaleLoop');
CurrentDamType = ZombieDamType[0];
if( AmbientSound != SawAttackLoopSound )
{
AmbientSound=SawAttackLoopSound;
}
}
else GoToState('');
}
function AnimEnd( int Channel )
{
Super.AnimEnd(Channel);
if( Controller!=None && Controller.Enemy!=None )
RangedAttack(Controller.Enemy); // Keep on attacking if possible.
}
function Tick( float Delta )
{
// Keep the scrake moving toward its target when attacking
if( Role == ROLE_Authority && bShotAnim && !bWaitForAnim )
{
if( LookTarget!=None )
{
Acceleration = AccelRate * Normal(LookTarget.Location - Location);
}
}
global.Tick(Delta);
}
function EndState()
{
AmbientSound=default.AmbientSound;
MeleeDamage = Max( DifficultyDamageModifer() * default.MeleeDamage, 1 );
SetGroundSpeed(GetOriginalGroundSpeed());
bCharging = False;
if( Level.NetMode!=NM_DedicatedServer )
PostNetReceive();
}
}
// Added in Balance Round 1 to reduce the headshot damage taken from Crossbows
function TakeDamage( int Damage, Pawn InstigatedBy, Vector Hitlocation, Vector Momentum, class<DamageType> damageType, optional int HitIndex)
{
local bool bIsHeadShot;
local PlayerController PC;
local KFSteamStatsAndAchievements Stats;
bIsHeadShot = IsHeadShot(Hitlocation, normal(Momentum), 1.0);
if ( Level.Game.GameDifficulty >= 5.0 && bIsHeadshot && (class<DamTypeCrossbow>(damageType) != none || class<DamTypeCrossbowHeadShot>(damageType) != none) )
{
Damage *= 0.5; // Was 0.5 in Balance Round 1, then 0.6 in Round 2, back to 0.5 in Round 3
}
Super.takeDamage(Damage, instigatedBy, hitLocation, momentum, damageType, HitIndex);
// Added in Balance Round 3 to make the Scrake "Rage" more reliably when his health gets low(limited to Suicidal and HoE in Round 7)
if ( Level.Game.GameDifficulty >= 5.0 && !IsInState('SawingLoop') && !IsInState('RunningState') && float(Health) / HealthMax < 0.75 )
RangedAttack(InstigatedBy);
if( damageType == class'DamTypeDBShotgun' )
{
PC = PlayerController( InstigatedBy.Controller );
if( PC != none )
{
Stats = KFSteamStatsAndAchievements( PC.SteamStatsAndAchievements );
if( Stats != none )
{
Stats.CheckAndSetAchievementComplete( Stats.KFACHIEVEMENT_PushScrakeSPJ );
}
}
}
}
function PlayTakeHit(vector HitLocation, int Damage, class<DamageType> DamageType)
{
local int StunChance;
StunChance = rand(5);
if( Level.TimeSeconds - LastPainAnim < MinTimeBetweenPainAnims )
return;
if( (Level.Game.GameDifficulty < 5.0 || StunsRemaining != 0) && (Damage>=150 || (DamageType.name=='DamTypeStunNade' && StunChance>3) || (DamageType.name=='DamTypeCrossbowHeadshot' && Damage>=200)) )
PlayDirectionalHit(HitLocation);
LastPainAnim = Level.TimeSeconds;
if( Level.TimeSeconds - LastPainSound < MinTimeBetweenPainSounds )
return;
LastPainSound = Level.TimeSeconds;
PlaySound(HitSound[0], SLOT_Pain,1.25,,400);
}
simulated function int DoAnimAction( name AnimName )
{
if( AnimName=='SawZombieAttack1' || AnimName=='SawZombieAttack2' )
{
AnimBlendParams(1, 1.0, 0.0,, FireRootBone);
PlayAnim(AnimName,, 0.1, 1);
Return 1;
}
Return Super.DoAnimAction(AnimName);
}
simulated event SetAnimAction(name NewAction)
{
local int meleeAnimIndex;
if( NewAction=='' )
Return;
if(NewAction == 'Claw')
{
meleeAnimIndex = Rand(3);
NewAction = meleeAnims[meleeAnimIndex];
CurrentDamtype = ZombieDamType[meleeAnimIndex];
}
ExpectingChannel = DoAnimAction(NewAction);
if( AnimNeedsWait(NewAction) )
{
bWaitForAnim = true;
}
if( Level.NetMode!=NM_Client )
{
AnimAction = NewAction;
bResetAnimAct = True;
ResetAnimActTime = Level.TimeSeconds+0.3;
}
}
// The animation is full body and should set the bWaitForAnim flag
simulated function bool AnimNeedsWait(name TestAnim)
{
if( TestAnim == 'SawImpaleLoop' || TestAnim == 'DoorBash' || TestAnim == 'KnockDown' )
{
return true;
}
return false;
}
function PlayDyingSound()
{
if( Level.NetMode!=NM_Client )
{
if ( bGibbed )
{
// Do nothing for now
PlaySound(GibGroupClass.static.GibSound(), SLOT_Pain,2.0,true,525);
return;
}
if( bDecapitated )
{
PlaySound(HeadlessDeathSound, SLOT_Pain,1.30,true,525);
}
else
{
PlaySound(DeathSound[0], SLOT_Pain,1.30,true,525);
}
PlaySound(ChainSawOffSound, SLOT_Misc, 2.0,,525.0);
}
}
function Died(Controller Killer, class<DamageType> damageType, vector HitLocation)
{
AmbientSound = none;
if ( ExhaustEffect != none )
{
ExhaustEffect.Destroy();
ExhaustEffect = none;
bNoExhaustRespawn = true;
}
super.Died( Killer, damageType, HitLocation );
}
simulated function ProcessHitFX()
{
local Coords boneCoords;
local class<xEmitter> HitEffects[4];
local int i,j;
local float GibPerterbation;
if( (Level.NetMode == NM_DedicatedServer) || bSkeletized || (Mesh == SkeletonMesh))
{
SimHitFxTicker = HitFxTicker;
return;
}
for ( SimHitFxTicker = SimHitFxTicker; SimHitFxTicker != HitFxTicker; SimHitFxTicker = (SimHitFxTicker + 1) % ArrayCount(HitFX) )
{
j++;
if ( j > 30 )
{
SimHitFxTicker = HitFxTicker;
return;
}
if( (HitFX[SimHitFxTicker].damtype == None) || (Level.bDropDetail && (Level.TimeSeconds - LastRenderTime > 3) && !IsHumanControlled()) )
continue;
//log("Processing effects for damtype "$HitFX[SimHitFxTicker].damtype);
if( HitFX[SimHitFxTicker].bone == 'obliterate' && !class'GameInfo'.static.UseLowGore())
{
SpawnGibs( HitFX[SimHitFxTicker].rotDir, 1);
bGibbed = true;
// Wait a tick on a listen server so the obliteration can replicate before the pawn is destroyed
if( Level.NetMode == NM_ListenServer )
{
bDestroyNextTick = true;
TimeSetDestroyNextTickTime = Level.TimeSeconds;
}
else
{
Destroy();
}
return;
}
boneCoords = GetBoneCoords( HitFX[SimHitFxTicker].bone );
if ( !Level.bDropDetail && !class'GameInfo'.static.NoBlood() && !bSkeletized && !class'GameInfo'.static.UseLowGore() )
{
//AttachEmitterEffect( BleedingEmitterClass, HitFX[SimHitFxTicker].bone, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir );
HitFX[SimHitFxTicker].damtype.static.GetHitEffects( HitEffects, Health );
if( !PhysicsVolume.bWaterVolume ) // don't attach effects under water
{
for( i = 0; i < ArrayCount(HitEffects); i++ )
{
if( HitEffects[i] == None )
continue;
AttachEffect( HitEffects[i], HitFX[SimHitFxTicker].bone, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir );
}
}
}
if ( class'GameInfo'.static.UseLowGore() )
HitFX[SimHitFxTicker].bSever = false;
if( HitFX[SimHitFxTicker].bSever )
{
GibPerterbation = HitFX[SimHitFxTicker].damtype.default.GibPerterbation;
switch( HitFX[SimHitFxTicker].bone )
{
case 'obliterate':
break;
case LeftThighBone:
if( !bLeftLegGibbed )
{
SpawnSeveredGiblet( DetachedLegClass, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, GetBoneRotation(HitFX[SimHitFxTicker].bone) );
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrainb',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
bLeftLegGibbed=true;
}
break;
case RightThighBone:
if( !bRightLegGibbed )
{
SpawnSeveredGiblet( DetachedLegClass, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, GetBoneRotation(HitFX[SimHitFxTicker].bone) );
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrainb',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
bRightLegGibbed=true;
}
break;
case LeftFArmBone:
if( !bLeftArmGibbed )
{
SpawnSeveredGiblet( DetachedArmClass, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, GetBoneRotation(HitFX[SimHitFxTicker].bone) );
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrainb',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;;
bLeftArmGibbed=true;
}
break;
case RightFArmBone:
if( !bRightArmGibbed )
{
SpawnSeveredGiblet( DetachedSpecialArmClass, boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, GetBoneRotation(HitFX[SimHitFxTicker].bone) );
KFSpawnGiblet( class 'KFMod.KFGibBrain',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
KFSpawnGiblet( class 'KFMod.KFGibBrainb',boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, GibPerterbation, 250 ) ;
bRightArmGibbed=true;
}
break;
case 'head':
if( !bHeadGibbed )
{
if ( HitFX[SimHitFxTicker].damtype == class'DamTypeDecapitation' )
{
DecapFX( boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, false);
}
else if( HitFX[SimHitFxTicker].damtype == class'DamTypeProjectileDecap' )
{
DecapFX( boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, false, true);
}
else if( HitFX[SimHitFxTicker].damtype == class'DamTypeMeleeDecapitation' )
{
DecapFX( boneCoords.Origin, HitFX[SimHitFxTicker].rotDir, true);
}
bHeadGibbed=true;
}
break;
}
if( HitFX[SimHitFXTicker].bone != 'Spine' && HitFX[SimHitFXTicker].bone != FireRootBone &&
HitFX[SimHitFXTicker].bone != 'head' && Health <=0 )
HideBone(HitFX[SimHitFxTicker].bone);
}
}
}
// Maybe spawn some chunks when the player gets obliterated
simulated function SpawnGibs(Rotator HitRotation, float ChunkPerterbation)
{
if ( ExhaustEffect != none )
{
ExhaustEffect.Destroy();
ExhaustEffect = none;
bNoExhaustRespawn = true;
}
super.SpawnGibs(HitRotation,ChunkPerterbation);
}
static simulated function PreCacheMaterials(LevelInfo myLevel)
{//should be derived and used.
myLevel.AddPrecacheMaterial(Combiner'KF_Specimens_Trip_T.scrake_env_cmb');
myLevel.AddPrecacheMaterial(Texture'KF_Specimens_Trip_T.scrake_diff');
myLevel.AddPrecacheMaterial(Texture'KF_Specimens_Trip_T.scrake_spec');
myLevel.AddPrecacheMaterial(Material'KF_Specimens_Trip_T.scrake_saw_panner');
myLevel.AddPrecacheMaterial(Material'KF_Specimens_Trip_T.scrake_FB');
myLevel.AddPrecacheMaterial(Texture'KF_Specimens_Trip_T.Chainsaw_blade_diff');
}
defaultproperties
{
//-------------------------------------------------------------------------------
// NOTE: Most Default Properties are set in the base class to eliminate hitching
//-------------------------------------------------------------------------------
EventClasses(0)="KFChar.ZombieScrake_STANDARD"
ControllerClass=Class'KFChar.SawZombieController'
}