// 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, 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(damageType) != none || class(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) { 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, 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 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' }