//============================================================================= // KFSM_BloatKing_Gorge //============================================================================= // Bloat King gorge move. Pulls in friendly + enemy with different effects // on contact with the Bloat. //============================================================================= // Killing Floor 2 // Copyright (C) 2017 Tripwire Interactive LLC //============================================================================= class KFSM_BloatKing_Gorge extends KFSM_PlaySingleAnim; //Cached bloat reference var KFPawn_ZedBloatKing BloatPawn; //Ranges of the attack var const float GorgeAttackRangeSq; var const float GorgeHumanAttackRangeSq; //Angle range that players/zeds have to be in for this to be a valid time to start var const float GorgeMinAttackAngle; //Amount of time to take before next attempted gorge after using the ability var const Vector2D GorgeAttackCooldown[4]; //Amount of time to take between checks if starting the move was not valid var const float GorgeAttackCheckDelay; //amount of time to wait to start the pull when the special move starts var const float GorgePullDelay; //List of actors we're already pulling var array PullList; var array PulledList; //Special move to do to the person being grabbed var ESpecialMove FollowerSpecialMove; var ESpecialMove ZedFollowerSpecialMove; //Rate the pulled pawn moves towards the Bloat var float GorgePullRate; //Gorge release offset var float GorgeReleaseOffset; //Damage per-difficulty for human interaction var float GorgeBaseDamage[4]; //Track whether pull is now valid var bool bPullActive; // List of pawns that need to be killed var array GorgeHitList; // List of pawns that have to be removed as victims on next tick // SpecialMoveEnded happens in async tick work, so we can't change physics in that time var array DeferredRemovalList; /** Contains a list of particle systems spawned during the move */ var protected array AnimParticles; static function float GetGorgeCooldown(KFPawn InPawn, int Difficulty) { return Lerp(default.GorgeAttackCooldown[Difficulty].X, default.GorgeAttackCooldown[Difficulty].Y, InPawn.GetHealthPercentage()); } function SpecialMoveStarted(bool bForced, Name PrevMove) { super.SpecialMoveStarted(bForced, PrevMove); BloatPawn = KFPawn_ZedBloatKing(KFPOwner); PulledList.Length = 0; KFPOwner.SetTimer(GorgePullDelay, false, 'StartGorgePull', self); } /** Called from KFPawn::OnAnimNotifyParticleSystemSpawned() */ function OnAnimNotifyParticleSystemSpawned( const AnimNotify_PlayParticleEffect AnimNotifyData, ParticleSystemComponent PSC ) { local AnimSequence AnimSeq; if (AnimNotifyData.Outer != none) { AnimSeq = AnimSequence(AnimNotifyData.Outer); if (AnimSeq != none && string(AnimSeq.SequenceName) ~= string(AnimName)) // string conversion so we don't have case mismatches { AnimParticles.AddItem( PSC ); } } } function StartGorgePull() { if (KFPOwner.Role == ROLE_Authority) { bPullActive = true; } } function SpecialMoveEnded(Name PrevMove, Name NextMove) { local KFPawn PullPawn; local int i; bPullActive = false; for (i = PullList.Length - 1; i >= 0; --i) { PullPawn = PullList[i]; if (KFPawn_Human(PullPawn) != none) { RemoveVictim(PullPawn, false); } else { DeferredRemovalList.AddItem(PullPawn); } } PullList.Length = 0; for (i = 0; i < AnimParticles.Length; ++i) { AnimParticles[i].DeactivateSystem(); } AnimParticles.Length = 0; Super.SpecialMoveEnded(PrevMove, NextMove); } function PlayAnimation() { local float Duration; Duration = PlaySpecialMoveAnim(AnimName, AnimStance, BlendInTime, BlendOutTime, 1.f, bLoopAnim); KFPOwner.SetTimer(Duration + 2.f, false, 'Timeout', self); } function Timeout() { BloatPawn.EndSpecialMove(); } function Tick(float DeltaTime) { local KFPawn TestPawn; local KFPawn_Monster MonsterToKill; super.Tick(DeltaTime); if(KFPOwner.Role == ROLE_Authority) { if (GorgeHitList.length > 0) { foreach GorgeHitList(TestPawn) { if (TestPawn == none || !TestPawn.IsAliveAndWell()) { continue; } MonsterToKill = KFPawn_Monster(TestPawn); if (MonsterToKill != none) { MonsterToKill.KilledBy(KFPOwner); } else { TestPawn.TakeDamage(GorgeBaseDamage[KFPOwner.WorldInfo.Game.GetModifiedGameDifficulty()], KFPOwner.Controller, TestPawn.Location, vect(0, 0, 0), class'KFDT_Bludgeon_BloatKingGorge'); } } GorgeHitList.length = 0; } if (DeferredRemovalList.Length > 0) { foreach DeferredRemovalList(TestPawn) { RemoveVictim(TestPawn, false); } DeferredRemovalList.Length = 0; } if (bPullActive) { FindNewVictims(); } } UpdateVictims(DeltaTime); } function FindNewVictims() { local KFPawn PullPawn; local vector ViewDirection, ToTarget; local vector Extent, HitLocation, HitNormal; local Actor HitActor; local float DotAngle, ToTargetRange; if (BloatPawn.GorgeTrigger != None) { ViewDirection = Vector(KFPOwner.Rotation); foreach BloatPawn.GorgeTrigger.TouchingActors(class'KFPawn', PullPawn) { //Not currently being pulled, and not pulled within this cycle if (PullList.Find(PullPawn) == INDEX_NONE && PulledList.Find(PullPawn) == INDEX_NONE) { //Can target enemy and friendly, but they must be alive and can be filtered by class type //They must not already be grabbed //They must not already be in the current intended follower move if (!PullPawn.IsAliveAndWell() || !IsValidPullClass(PullPawn) || !PullPawn.CanBeGrabbed(KFPOwner, true, true) || PullPawn.IsDoingSpecialMove(FollowerSpecialMove)) { continue; } ToTarget = PullPawn.Location - KFPOwner.Location; ToTargetRange = VSizeSq(ToTarget); //Check distance. If out of range, ignore this potential target. if (PullPawn.IsHumanControlled() && ToTargetRange > default.GorgeHumanAttackRangeSq) { continue; } else if (ToTargetRange > default.GorgeAttackRangeSq) { continue; } if (!KFPOwner.IsHumanControlled()) { // Set our extent Extent.X = KFPOwner.GetCollisionRadius() * 0.5f; Extent.Y = Extent.X; Extent.Z = KFPOwner.GetCollisionHeight() * 0.5f; // trace for obstructions (already checked if IsHumanControlled()) HitActor = KFPOwner.Trace(HitLocation, HitNormal, PullPawn.Location, KFPOwner.Location, true, Extent); if (HitActor != None && HitActor != PullPawn) { continue; } } // Within field of view and not behind geometry DotAngle = ViewDirection dot Normal(ToTarget); if (DotAngle > default.GorgeMinAttackAngle && KFPOwner.FastTrace(PullPawn.Location, KFPOwner.Location)) { AddNewVictim(PullPawn); } } } } } static function bool IsValidPullClass(KFPawn PullPawn) { local KFPawn_Monster MonsterPawn; MonsterPawn = KFPawn_Monster(PullPawn); if (PullPawn.class == class'KFPawn_ZedBloatKingSubspawn' || MonsterPawn != none && MonsterPawn.IsABoss() || PullPawn.class == class'KFPawn_HRG_Warthog' || PullPawn.class == class'KFPawn_AutoTurret') { return false; } return true; } function UpdateVictims(float DeltaTime) { local KFPawn CurrentPawn; local Vector MoveVector, SocketLocation; local Rotator SocketRotation; local int i; local bool bIsHuman; KFPOwner.Mesh.GetSocketWorldLocationAndRotation('PukeSocket', SocketLocation, SocketRotation); for (i = 0; i < 11; ++i) { CurrentPawn = BloatPawn.PullVictims[i]; if (CurrentPawn != None) { bIsHuman = CurrentPawn.IsA('KFPawn_Human'); //Reached end, stop pull and do things if (VSizeSq(CurrentPawn.Location - KFPOwner.Location) < GorgeReleaseOffset * GorgeReleaseOffset) { RemoveVictim(CurrentPawn, true); } //else keep pulling else { //Scoot humans along the ground if (bIsHuman) { MoveVector = (GorgePullRate * DeltaTime) * Normal(KFPOwner.Location - CurrentPawn.Location); } //Bring zeds towards mouth else { MoveVector = (GorgePullRate * DeltaTime) * Normal(SocketLocation - CurrentPawn.Location); } CurrentPawn.SetLocation(CurrentPawn.Location + MoveVector); } } } } function AddNewVictim(KFPawn NewVictim) { //Found a target, so we're good to start the move PullList.AddItem(NewVictim); PulledList.RemoveItem(NewVictim); StartPullingPawn(NewVictim); if (BloatPawn.Role == ROLE_Authority) { BloatPawn.AddGorgeVictim(NewVictim); } } function RemoveVictim(KFPawn OldVictim, bool bReachedEnd = false) { PullList.RemoveItem(OldVictim); PulledList.AddItem(OldVictim); if (BloatPawn.Role == ROLE_Authority) { BloatPawn.RemoveGorgeVictim(OldVictim); if (bReachedEnd && KFPawn_Monster(OldVictim) != none) { BloatPawn.SpawnPoopMonster(); } } StopPullingPawn(OldVictim, bReachedEnd); } function StartPullingPawn(KFPawn NewVictim) { if (NewVictim != None) { if (KFPOwner.Role == ROLE_Authority) { //Monster types use a different special move and also turn on explosive death from // the Inflate weekly for special effects reasons. if (KFPawn_Monster(NewVictim) != none) { NewVictim.DoSpecialMove(ZedFollowerSpecialMove, true, KFPOwner); KFPawn_Monster(NewVictim).bUseExplosiveDeath = true; } else { NewVictim.DoSpecialMove(FollowerSpecialMove, true, KFPOwner); } } } } function StopPullingPawn(KFPawn OldVictim, bool bReachedEnd = true) { local KFPawn_Monster MonsterVictim; if (OldVictim != None) { MonsterVictim = KFPawn_Monster(OldVictim); OldVictim.EndSpecialMove(); if (KFPOwner.Role == ROLE_Authority) { if (MonsterVictim != None) { //Kill if this is a gorge finisher if (bReachedEnd) { GorgeHitList.AddItem(MonsterVictim); } //Otherwise turn off explosive death else { MonsterVictim.bUseExplosiveDeath = false; } } else { if(bReachedEnd) { GorgeHitList.AddItem(OldVictim); } } } } } function bool CanOverrideMoveWith(Name NewMove) { if (NewMove == 'KFSM_Knockdown') { return TRUE; } return FALSE; } defaultproperties { //Special Move Handle=KFSM_BloatKing_Gorge AnimName=Atk_Gorge AnimStance=EAS_FullBody bDisableSteering=true bDisableMovement=true bDisableTurnInPlace=true bLockPawnRotation=true bCanBeInterrupted=true CustomRotationRate=(Pitch=0, Yaw=0, Roll=0) //Gorge Specific //These are (0% health, 100% health) GorgeAttackCooldown[0]=(X=8.0,Y=11.0) //normal //x35 y45 GorgeAttackCooldown[1]=(X=8.0,Y=11.0) //hard //x25 y35 GorgeAttackCooldown[2]=(X=7.0,Y=10.0) //Suicidal //x15 y25 GorgeAttackCooldown[3]=(X=6.0,Y=9.0) //HOE //x10 y15 GorgeAttackCheckDelay=5.0 GorgePullDelay=0.01 //0.91 0.61 FollowerSpecialMove=SM_BloatKingGorgeVictim ZedFollowerSpecialMove=SM_GorgeZedVictim, GorgePullRate=300.0 //UU pur sec //1000 //375 GorgeReleaseOffset=135 //300 GorgeBaseDamage[0]=30 //normal GorgeBaseDamage[1]=40 //hard GorgeBaseDamage[2]=50 //Suicidal GorgeBaseDamage[3]=50 //HOE //1000 * 1000 - Squaring distance for performance //Also update the value in KFTrigger_BloatKingGorge or there may be some issues with first-frame spawn collision GorgeAttackRangeSq=490000 //490k = 700UU GorgeHumanAttackRangeSq=450000 //250k = 500UU 250000 GorgeMinAttackAngle=0.77f }