//============================================================================= // KFSM_TentacleGrappleBase //============================================================================= // Tentacle grapple //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFSM_TentacleGrappleBase extends KFSM_GrappleCombined native(SpecialMoves); /** Time from move start when skel controls are activated */ var float TentacleStartTime; /** Hit detection settings */ var float MaxRange; var float MaxClawReach; var float DetachDistance; var bool bGrabMissed; /** Skel controls */ var float CurveExponent; var float TentacleEndBoneOffset; var float TentacleBlendOutTime; var name TentacleEndBone; var name TentacleStartBone; /** Air speed when in PHYS_Flying during retract */ var float RetractAirSpeed; /** True if we've grabbed a pawn */ var bool bGrabbedPawn; var const array TentacleControlNames; var transient array TentacleControls; var const name TentacleStartCtrlName; var transient SkelControlSingleBone TentacleStartCtrl; var transient bool bTentacleCtrlStarted; /** Damage values, set by AI command */ var int TentacleDamage; var class TentacleDmgType; cpptext { virtual void TickSpecialMove(FLOAT DeltaTime); } function SpecialMoveStarted(bool bForced, Name PrevMove) { local KFPawn_MonsterBoss BossPawn; // skip default (instant attach) behavior Super.SpecialMoveStarted(bForced, PrevMove); bAlignFollowerLookSameDirAsMe = default.bAlignFollowerLookSameDirAsMe; bAlignFollowerRotation = default.bAlignFollowerRotation; Follower = KFPOwner.InteractionPawn; if( Follower != none && !Follower.IsAliveAndWell() ) { KFPOwner.EndSpecialMove(); return; } bAlignPawns = false; bTentacleCtrlStarted = FALSE; bGrabMissed = false; BossPawn = KFPawn_MonsterBoss(KFPOwner); if (BossPawn != none) { // On the server start a timer to check collision if (KFPOwner.Role == ROLE_Authority) { // Stop cloaking BossPawn.SetCloaked(false); } BossPawn.BumpFrequency = 0.f; BossPawn.PlayGrabDialog(); BossPawn.SetTimer(TentacleStartTime, false, nameof(BeginTentacleControls), Self); if (Follower != none) { DetachDistance = BossPawn.CylinderComponent.CollisionRadius + Follower.CylinderComponent.CollisionRadius + default.DetachDistance; } } } /** Activates the tentacle skel controls */ function BeginTentacleControls() { if ( TentacleControls.Length == 0 ) { InitializeSkelControls(); } SetSkelControlsActive(true); bTentacleCtrlStarted = TRUE; } /** Test grapple collision on server */ function CheckGrapple() { local vector HitLocation, HitNormal, GrabLocation, GrabDirection; local Actor HitActor; if( bGrabMissed ) { return; } if( Follower != none && Follower.IsAliveAndWell() && Follower.CanBeGrabbed(KFPOwner, true) ) { // Out of reach (MaxRange + additional claw reach) GrabLocation = Follower.Location + Follower.BaseEyeHeight * vect(0,0,0.75); GrabDirection = PawnOwner.Location - GrabLocation; if ( VSizeSq(GrabDirection) < Square(MaxRange + MaxClawReach) ) { // trace for obstructions HitActor = PawnOwner.Trace(HitLocation, HitNormal, Follower.Location, PawnOwner.Location, true); if ( HitActor == None || HitActor == Follower ) { // Do some damage DamageFollower( GrabLocation, GrabDirection ); } // Make sure our follower is still alive after taking damage if( Follower != none && Follower.IsAliveAndWell() ) { BeginGrapple(); return; } } } OnGrabMissed(); } function DamageFollower( vector GrabLocation, vector GrabDirection ) { GrabDirection = Normal(GrabDirection); Follower.TakeDamage( TentacleDamage, KFPOwner.Controller, GrabLocation, GrabDirection, TentacleDmgType,, KFPOwner ); // Do a camera shake, etc KFPawn_Monster(KFPOwner).MeleeAttackHelper.PlayMeleeHitEffects(Follower, GrabLocation, GrabDirection); } function BeginGrapple(optional KFPawn Victim) { if ( PawnOwner.Role == ROLE_Authority ) { // @todo: Server only for now because the alignment code is not // network safe on simulated proxy. Needs invstigation! bAlignPawns = default.bAlignPawns; // replicate attachment KFPOwner.SpecialMoveFlags = EGS_GrabSuccess; KFPOwner.ReplicatedSpecialMove.Flags = KFPOwner.SpecialMoveFlags; // Set our physics to flying for the attract Follower.SetPhysics( PHYS_Flying ); Follower.AirSpeed = RetractAirSpeed; KFPawn_MonsterBoss(KFPOwner).PlayGrabbedPlayerDialog( KFPawn_Human(Follower) ); } // Set up a safety net in case interaction cannot be started PawnOwner.SetTimer( InteractionStartTimeOut, FALSE, nameof(self.InteractionStartTimedOut), self ); // See if we can start interaction right now. If we can't, keep trying until we can. CheckReadyToStartInteraction(); } /** StartInteraction */ function StartInteraction() { super.StartInteraction(); bGrabbedPawn = true; } event OnGrabMissed() { bGrabMissed = true; if ( PawnOwner.Role == ROLE_Authority ) { KFPOwner.SpecialMoveFlags = EGS_GrabMiss; KFPOwner.ReplicatedSpecialMove.Flags = KFPOwner.SpecialMoveFlags; } bTentacleCtrlStarted = false; SetSkelControlsActive(false); } /** Toggle attachment */ function SpecialMoveFlagsUpdated() { if ( KFPOwner.SpecialMoveFlags == EGS_GrabSuccess ) { BeginGrapple(); } else if ( KFPOwner.SpecialMoveFlags == EGS_GrabMiss ) { OnGrabMissed(); } } function InitializeSkelControls() { local int i; local SkelControlSingleBone SkelCtrl; if ( PawnOwner.WorldInfo.NetMode == NM_DedicatedServer ) { return; } // @note: Could be done with a single (shared) control with a relative // translation, but this way we can make a nice curve to the target. for (i = 0; i < TentacleControlNames.length; ++i) { SkelCtrl = SkelControlSingleBone(PawnOwner.Mesh.FindSkelControl(TentacleControlNames[i])); SkelCtrl.BlendInTime = (GrabCheckTime - TentacleStartTime); SkelCtrl.BlendOutTime = TentacleBlendOutTime; TentacleControls.AddItem(SkelCtrl); } // move the anchor bone outside the abdomen TentacleStartCtrl = SkelControlSingleBone(PawnOwner.Mesh.FindSkelControl(TentacleStartCtrlName)); TentacleStartCtrl.BlendInTime = 0.2f; TentacleStartCtrl.BlendOutTime = 0.33f; } function SetSkelControlsActive(bool bEnabled) { local int i; if ( PawnOwner.WorldInfo.NetMode == NM_DedicatedServer ) { return; } // bEnabled==TRUE is handled in TickSpecialMove to guarantee a valid target if ( !bEnabled ) { for(i = 0; i < TentacleControls.Length; ++i) { TentacleControls[i].SetSkelControlActive(false); } } TentacleStartCtrl.SetSkelControlActive(bEnabled); } /** If the follower has been reeled in, end the special move */ event DetachGrabbedPawn() { local int i; // bEnabled==TRUE is handled in TickSpecialMove to guarantee a valid target for(i = 0; i < TentacleControls.Length; ++i) { if( TentacleControls[i] != none ) { TentacleControls[i].BlendOutTime = 0.2f; } } if( TentacleStartCtrl != none ) { TentacleStartCtrl.BlendOutTime = 0.2f; } bTentacleCtrlStarted = false; SetSkelControlsActive( false ); KFPOwner.EndSpecialMove(); if( Follower != none ) { Follower.SetPhysics( PHYS_Falling ); Follower.EndSpecialMove(); } } /** Skip super, this class does not have a looping anim */ function AnimEndNotify( AnimNodeSequence SeqNode, float PlayedTime, float ExcessTime ) { Super(KFSpecialMove).AnimEndNotify(SeqNode, PlayedTime, ExcessTime); } /** Disable grab interruption */ function NotifyOwnerTakeHit(class DamageType, vector HitLoc, vector HitDir, Controller InstigatedBy); /** Follower has left special move */ function OnFollowerLeavingSpecialMove() { super.OnFollowerLeavingSpecialMove(); ResetFollowerPhysics(); } /** Resets physics values on follower */ function ResetFollowerPhysics() { if( Follower != none ) { Follower.AirSpeed = Follower.default.AirSpeed; if( Follower.Physics == PHYS_Flying ) { Follower.SetPhysics( PHYS_Falling ); } } } /** Disable skel controls */ function SpecialMoveEnded(Name PrevMove, Name NextMove) { if( PawnOwner != none ) { PawnOwner.ClearTimer(nameof(CheckGrapple), Self); PawnOwner.ClearTimer(nameof(BeginTentacleControls), Self); KFPawn_Monster(PawnOwner).BumpFrequency = KFPawn_Monster(PawnOwner).default.BumpFrequency; } // Return follower physics to normal ResetFollowerPhysics(); SetSkelControlsActive(false); Super.SpecialMoveEnded(PrevMove, NextMove); } event vector GetSourceLocation() { return PawnOwner.Location; } /** Ignore input */ function SpecialMoveButtonRetriggered() {} function SpecialMoveButtonReleased() {} function PlayerReleasedGrapple() {} DefaultProperties { Handle=KFSM_TentacleGrappleBase FollowerSpecialMove=SM_HansGrappleVictim GrabStartAnimName=Atk_Tentical_V1 AlignDistance=108.f AlignFollowerInterpSpeed=22.f bStopAlignFollowerRotationAtGoal=false bAlignFollowerZ=true bAlignLeaderLocation=false bRetryCollisionCheck=false AlignSpeedModifier=0.2f TentacleStartTime=0.83f MaxRange=600.f MaxClawReach=50.f DetachDistance=20.f RetractAirSpeed=1000.f TentacleStartBone=FrontTentacle2 TentacleEndBone=FrontTentacle7 TentacleEndBoneOffset=-10 TentacleBlendOutTime=0.33f CurveExponent=1.25f }