//============================================================================= // KFSM_Matriarch_PlasmaCannon //============================================================================= // Flamethrower-style attack, lasts 5 seconds, interruptable with big damage. //============================================================================= // Killing Floor 2 // Copyright (C) 2016 Tripwire Interactive LLC // - Dan Weiss //============================================================================= class KFSM_Matriarch_PlasmaCannon extends KFSM_PlaySingleAnim; /** Max distance between the Matriarch and the current victim target */ var float MaxVictimDistance; /** Name of socket that shooting originates from. Used for both visual attach and damage trace */ var name ShootingSocketName; /** Size of the beam being fired */ var Vector BeamSize; /** Amount of time between attempted damage ticks */ var float DamageTickRate; /** Per-tick damage amount. Done to each target in the beam */ var float DamagePerTick; /** Damage type class */ var class CannonDamageType; /** Template for the plasma beam */ var ParticleSystem BeamPSCTemplate; /** PSC generated from the beam template. Gets recycled in and out of the emitter pool as needed. */ var ParticleSystemComponent BeamPSC; /** Template for beam hit location FX */ var const ParticleSystem BeamHitPSCTemplate; /** PSC for the hit location. On when the beam is hitting anything, off when missing everything */ var ParticleSystemComponent BeamHitPSC; var ParticleSystem MuzzleFlashPSCTemplate; var ParticleSystemComponent MuzzleFlashPSC; /** Amount of time that's passed since the last damage tick occurred */ var float SinceLastDamage; /** AK Event for start of beam charge */ var AkEvent BeamStartSFX; /** AK Event for end of special move */ var AkEvent BeamEndSFX; /** Looping sound for beam hit */ var AkEvent BeamHitSFX; var AkEvent BeamHitStopSFX; /** Wind Up animation name */ var Name WindUpAnimName; /** Wind Down animation name */ var Name WindDownAnimName; var bool bTickDamage; var KFPawn_ZedMatriarch MyMatPawn; /* Targeting */ /** Time to check before firing if an enemy is still visible */ var float VisibilityCheckTime, VisibilityCheckTimer; var float AngleCheckDot; var bool bLogTargeting; function SpecialMoveStarted( bool bForced, Name PrevMove ) { super.SpecialMoveStarted(bForced, PrevMove); PlayWindUpAnimation(); MyMatPawn = KFPawn_ZedMatriarch(KFPOwner); MyMatPawn.PlayPlasmaCannonDialog(); } /** Play the wind down if we have to */ function SpecialMoveFlagsUpdated() { switch (KFPOwner.SpecialMoveFlags) { case 1: PlayFireAnim(); break; case 2: PlayWindDownAnim(); break; } } function AnimEndNotify(AnimNodeSequence SeqNode, float PlayedTime, float ExcessTime) { switch (DeferredSeqName) { case WindUpAnimName: if (KFPOwner.Role == ROLE_Authority) { // Do the beam check on the authority and then re-do the special move with new flags, // which will cause the flags to be replicated and caught by SpecialMoveFlagsUpdated if (PrefireBeamCheck()) { KFPOwner.DoSpecialMove(SM_HoseWeaponAttack, true,, 1); } else { KFPOwner.DoSpecialMove(SM_HoseWeaponAttack, true,, 2); } } break; case AnimName: PlayWindDownAnim(); break; case WindDownAnimName: KFPOwner.EndSpecialMove(); break; } } function bool PrefireBeamCheck() { local Actor HitActor; local vector SocketLocation, HitLocation, HitNormal, ToEnemy, TraceEnd, GunTargetBoneLocation; local rotator SocketRotation; PawnOwner.Mesh.GetSocketWorldLocationAndRotation(ShootingSocketName, SocketLocation, SocketRotation); GunTargetBoneLocation = KFPOwner.Controller.Enemy.Mesh.GetBoneLocation(MyMatPawn.GunTargetBoneName); ToEnemy = Normal(GunTargetBoneLocation - SocketLocation); TraceEnd = SocketLocation + ToEnemy * MaxVictimDistance; HitActor = PawnOwner.Trace(HitLocation, HitNormal, TraceEnd, SocketLocation, true); return KFPawn(HitActor) != none; } /** Overridden to do nothing */ function PlayAnimation(); /** Plays our wind up anim, starts the barrel spin skel controller */ function PlayWindUpAnimation() { PlaySpecialMoveAnim(WindUpAnimName, EAS_FullBody, BlendInTime, BlendOutTime, 1.f); KFPOwner.ZeroMovementVariables(); } /** Plays our fire animation, starts weapon fire */ function PlayFireAnim() { MyMatPawn.SetGunTracking(true); PlaySpecialMoveAnim(AnimName, EAS_FullBody, 0.1f, 0.2f); ActivateFX(); KFPOwner.ZeroMovementVariables(); KFPOwner.SetWeaponAmbientSound(BeamStartSFX); bTickDamage = true; if (KFPOwner.Role == ROLE_Authority) { VisibilityCheckTimer = VisibilityCheckTime; } //bUseCustomRotationRate = true; PawnOwner.RotationRate = CustomRotationRate; } /** Plays our wind down animation, stops firing, disables barrel spin skel controller */ function PlayWindDownAnim() { MyMatPawn.SetGunTracking(false); PlaySpecialMoveAnim(WindDownAnimName, EAS_FullBody, BlendInTime, BlendOutTime, 1.f); KFPOwner.ZeroMovementVariables(); bTickDamage = false; DeactivateFX(); } function SpecialMoveEnded(Name PrevMove, Name NextMove) { super.SpecialMoveEnded(PrevMove, NextMove); KFPOwner.RotationRate = KFPOwner.default.RotationRate; bTickDamage = false; DeactivateFX(); } function ActivateFX() { local ParticleSysParam SourceParam; if (KFPOwner.WorldInfo.NetMode != NM_DedicatedServer) { if (BeamPSCTemplate != none) { BeamPSC = PawnOwner.WorldInfo.MyEmitterPool.SpawnEmitterMeshAttachment( BeamPSCTemplate, PawnOwner.Mesh, 'LeftHand'); if (BeamPSC != none) { SourceParam.Name = 'SourceActor'; SourceParam.ParamType = PSPT_Actor; SourceParam.Actor = PawnOwner; BeamPSC.InstanceParameters.AddItem(SourceParam); } } if (MuzzleFlashPSCTemplate != none) { MuzzleFlashPSC = PawnOwner.WorldInfo.MyEmitterPool.SpawnEmitterMeshAttachment( MuzzleFlashPSCTemplate, PawnOwner.Mesh, 'LeftHand'); if (MuzzleFlashPSC != none) { SourceParam.Name = 'SourceActor'; SourceParam.ParamType = PSPT_Actor; SourceParam.Actor = PawnOwner; MuzzleFlashPSC.InstanceParameters.AddItem(SourceParam); } } } } function DeactivateFX() { if (BeamPSC != none) { BeamPSC.DeactivateSystem(); BeamPSC = none; KFPOwner.SetWeaponAmbientSound(BeamEndSFX); } if (BeamHitPSC != none) { BeamHitPSC.DeactivateSystem(); BeamHitPSC = none; MyMatPawn.BeamHitAC.PlayEvent(BeamHitStopSFX); } if (MuzzleFlashPSC != none) { MuzzleFlashPSC.DeactivateSystem(); MuzzleFlashPSC = none; } if (MyMatPawn.BeamHitAC != none) { MyMatPawn.BeamHitAC.StopEvents(); } } function Actor GetBeamTarget(out vector HitLocation) { local Actor HitActor; local vector SocketLocation, HitNormal, TraceEnd; local rotator SocketRotation; // force the beam vfx on the client to hit the pawn that the server is hitting if (PawnOwner.Role < ROLE_Authority && MyMatPawn.BeamTarget != none) { HitLocation = MyMatPawn.BeamTarget.Mesh.GetBoneLocation(MyMatPawn.GunTargetBoneName); return MyMatPawn.BeamTarget; } PawnOwner.Mesh.GetSocketWorldLocationAndRotation(ShootingSocketName, SocketLocation, SocketRotation); TraceEnd = SocketLocation + vector(SocketRotation) * MaxVictimDistance; foreach PawnOwner.TraceActors( class'Actor', HitActor, HitLocation, HitNormal, TraceEnd, SocketLocation, BeamSize) { if (IsValidBeamTarget(HitActor)) { break; } else { HitActor = none; } } return HitActor; } function bool IsValidBeamTarget(Actor HitActor) { //If we're a pawn or we're a non-resettable static mesh actor return Pawn(HitActor) != none || (StaticMeshActor(HitActor) != none && !StaticMeshActor(HitActor).bResetCapable) || SkeletalMeshActor(HitActor) != none || StaticMeshCollectionActor(HitActor) != none; } function Tick(float DeltaTime) { local Actor HitActor; local vector SocketLocation, HitLocation; local rotator SocketRotation; super.Tick(DeltaTime); if (bTickDamage) { HitActor = GetBeamTarget(HitLocation); if (PawnOwner.Role == ROLE_Authority) { SinceLastDamage += DeltaTime; while (SinceLastDamage > DamageTickRate) { if (HitActor != none) { HitActor.TakeDamage( DamagePerTick, PawnOwner.Controller, HitActor.Location, vect(0,0,0), CannonDamageType); if (KFPawn(HitActor) != none && !KFPawn(HitActor).IsAliveAndWell()) { HitActor = none; } } MyMatPawn.BeamTarget = Pawn(HitActor); SinceLastDamage -= DamageTickRate; } if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::Tick - HitActor: "$HitActor$"; VisibilityCheckTimer: "$VisibilityCheckTimer); } if (CanHitEnemy()) { VisibilityCheckTimer = VisibilityCheckTime; } else { VisibilityCheckTimer -= DeltaTime; if (VisibilityCheckTimer <= 0) { FindNewEnemy(); VisibilityCheckTimer = VisibilityCheckTime; } } } if (PawnOwner.WorldInfo.NetMode != NM_DedicatedServer) { if (HitActor != none) { if (BeamPSC != none) { BeamPSC.SetBeamTargetPoint(0, HitLocation, 0); } if (BeamHitPSC == none) { BeamHitPSC = KFPOwner.WorldInfo.MyEmitterPool.SpawnEmitter(BeamHitPSCTemplate, HitLocation); MyMatPawn.BeamHitAC.PlayEvent(BeamHitSFX); } if (BeamHitPSC != none) { PawnOwner.Mesh.GetSocketWorldLocationAndRotation(ShootingSocketName, SocketLocation, SocketRotation); BeamHitPSC.SetAbsolute(true, true, false); BeamHitPSC.SetTranslation(HitLocation); BeamHitPSC.SetRotation(SocketRotation); } } else { if (BeamHitPSC != none && BeamHitPSC.bIsActive) { BeamHitPSC.DeactivateSystem(); BeamHitPSC = none; MyMatPawn.BeamHitAC.PlayEvent(BeamHitStopSFX); } } } } } function bool CanHitEnemy() { local Actor HitActor; local vector SocketLocation, HitLocation, HitNormal, ToEnemy, TraceEnd, GunTargetBoneLocation; local rotator SocketRotation; PawnOwner.Mesh.GetSocketWorldLocationAndRotation(ShootingSocketName, SocketLocation, SocketRotation); GunTargetBoneLocation = KFPOwner.Controller.Enemy.Mesh.GetBoneLocation(MyMatPawn.GunTargetBoneName); ToEnemy = Normal(GunTargetBoneLocation - SocketLocation); TraceEnd = SocketLocation + ToEnemy * MaxVictimDistance; HitActor = PawnOwner.Trace(HitLocation, HitNormal, TraceEnd, SocketLocation, true); return HitActor == KFPOwner.Controller.Enemy; } function FindNewEnemy() { local KFPawn_Human KFPH, BestTarget; local vector StartTrace, EndTrace, ToTarget; local rotator SocketRotation; local float TargetDot, BestDot, MaxVictimDistanceSq; local vector HitLocation, HitNormal; local actor HitActor; KFPOwner.Mesh.GetSocketWorldLocationAndRotation(ShootingSocketName, StartTrace, SocketRotation); MaxVictimDistanceSq = MaxVictimDistance * MaxVictimDistance; foreach KFPOwner.WorldInfo.AllPawns(class'KFPawn_Human', KFPH) { if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - Potential target: "$KFPH); } ToTarget = KFPH.Location - StartTrace; if (VSizeSq(ToTarget) > MaxVictimDistanceSq) { if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - too far"); } continue; } ToTarget = Normal(ToTarget); TargetDot = ToTarget dot (vector(SocketRotation)); if (TargetDot < AngleCheckDot || TargetDot < BestDot) { if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - bad angle"); } continue; } EndTrace = StartTrace + (ToTarget * MaxVictimDistance); HitActor = KFPOwner.Trace(HitLocation, HitNormal, EndTrace, StartTrace, true,,, 1); if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - HitActor: "$HitActor); } if (HitActor != KFPH) { if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - obstructed"); } continue; } if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - okay!"); } BestTarget = KFPH; BestDot = TargetDot; } if (bLogTargeting) { `log("(PLASMA BEAM TARGETING) "$self$"::FindNewEnemy - BestTarget: "$BestTarget); } if (BestTarget != none) { if (BestTarget != KFPOwner.Controller.Enemy) { KFAIController(KFPOwner.Controller).ChangeEnemy(BestTarget, false); } } else { KFPOwner.DoSpecialMove(SM_HoseWeaponAttack, true,, 2); } } defaultproperties { //Special Move Handle = KFSM_Matriarch_PlasmaCannon bDisableSteering = false bDisableMovement = true bDisableTurnInPlace=true bCanBeInterrupted=false bShouldDeferToPostTick=true bUseCustomRotationRate=false // we will manually turn to true after wind-up CustomRotationRate=(Pitch=50000, Yaw=4500, Roll=50000) //Plasma Cannon MaxVictimDistance = 2500.f BeamSize = (X=15.0,Y=15.0,Z=15.0) DamageTickRate = 0.1f ShootingSocketName = Hand_FX_End_L DamagePerTick = 7.0f CannonDamageType = class'KFDT_EMP_MatriarchPlasmaCannon' BeamPSCTemplate = ParticleSystem'ZED_Matriarch_EMIT.FX_Plasma_Cannon_Beam_01' BeamHitPSCTemplate=ParticleSystem'ZED_Matriarch_EMIT.FX_Plasma_Cannon_Impact_01' MuzzleFlashPSCTemplate=ParticleSystem'ZED_Matriarch_EMIT.FX_Plasma_Cannon_Muzzleflash_01' VisibilityCheckTime=0.8f AngleCheckDot=0.500f // 60 degrees in front, type cos(X degrees) into google, replacing "X" with a number 0-90, lower is narrower FOV bLogTargeting=false //Sounds BeamStartSFX=AkEvent'WW_ZED_Matriarch.Play_Matriarch_Plasma_Cannon_Loop_01' BeamEndSFX=AkEvent'WW_ZED_Matriarch.Play_Matriarch_Plasma_Cannon_End_01' BeamHitSFX=AkEvent'WW_ZED_Matriarch.Play_Matriarch_SFX_Attack_PulseCannon_Beam_Hit_LP' BeamHitStopSFX=AkEvent'WW_ZED_Matriarch.Stop_Matriarch_SFX_Attack_PulseCannon_Beam_Hit_LP' //Animation WindUpAnimName=Plasma_Cannon_TO_Load AnimName=Plasma_Cannon_Idle WindDownAnimName=Plasma_Cannon_TO_Idle AnimStance=EAS_UpperBody }