//============================================================================= // KFSM_GrappleCombined //============================================================================= // One grapple to rule them all. Combines GrappleAttack and GrappleStart // so we have better control over the transition. //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC //============================================================================= class KFSM_GrappleCombined extends KFSM_InteractionPawnLeader native(SpecialMoves); // Animations var array GrappleAnims; var bool bStopFullBodyWhenMoveEnds; var byte LastVariant; // The last anim index this special move played // Simple state flags for multi-stage special move enum EGrappleState { EGS_GrabAttempt, EGS_GrabSuccess, EGS_GrabMiss, }; /** Beyond this distance the grapple will fail */ var float MaxGrabDistance; /** Make sure Z distance isn't too far */ var float MaxVictimZOffset; /** Anim to play when attempting to grab attach */ var name GrabStartAnimName; /** If set, this ability can be blocked by a melee weapon */ var bool bCanBeBlocked; /** Is set, the owner can cancel/abort this move after it's been started */ var bool bCanBeInterrupted; /** The time from the start of the anim to check for a victim */ var float GrabCheckTime; /** Minimum amount of time to grab if player released button */ var float MinPlayerGrabTime; /** Should use root motion for the initial "grab attempt" animation */ var bool bUseRootMotion; /** Restrictions for doing grab animation. */ protected function bool InternalCanDoSpecialMove() { if( PawnOwner.IsHumanControlled() && PawnOwner.Physics == PHYS_Falling ) { return false; } if ( KFPOwner.IsHeadless() ) { return false; } return super.InternalCanDoSpecialMove(); } /** * Can a new special move override this one before it is finished? * This is only if CanDoSpecialMove() == TRUE && !bForce when starting it. */ function bool CanOverrideMoveWith( Name NewMove ) { if ( KFPOwner.SpecialMoveFlags == EGS_GrabAttempt ) { if ( NewMove == 'KFSM_Stunned' || NewMove == 'KFSM_Stumble' ) { return TRUE; // for NotifyAttackParried } } return FALSE; } /** Called when owning pawn has damage parried during this move */ function bool CanInterruptWithParry() { return (KFPOwner.SpecialMoveFlags == EGS_GrabAttempt); } /** Set our grab flag to EGS_GrabAttempt (we don't start the grapple until we've grabbed a pawn) */ static function byte PackFlagsBase( KFPawn P ) { return EGS_GrabAttempt; } /** Notification called when Special Move starts */ function SpecialMoveStarted(bool bForced, Name PrevMove ) { // skip default (instant attach) behavior Super(KFSpecialMove).SpecialMoveStarted(bForced, PrevMove); // Reset variables Follower = None; bAlignFollowerLookSameDirAsMe = default.bAlignFollowerLookSameDirAsMe; bAlignPawns = default.bAlignPawns; bAlignFollowerRotation = default.bAlignFollowerRotation; bPendingStopFire = false; PlayGrabAnim(); } /** Play an animation and enable the OnAnimEnd notification */ function PlayGrabAnim() { GrabCheckTime = KFSkeletalMeshComponent(PawnOwner.Mesh).GetAnimInterruptTime(GrabStartAnimName); // On the server start a timer to check collision if ( PawnOwner.Role == ROLE_Authority ) { if ( GrabCheckTime <= 0 ) { `warn("Failed to play" @ GrabStartAnimName @ "on special move" @ Self @ "on Pawn" @ PawnOwner); PawnOwner.SetTimer(0.25f, FALSE, nameof(AbortSpecialMove), Self); return; } PawnOwner.SetTimer(GrabCheckTime, FALSE, nameof(CheckGrapple), Self); } PlaySpecialMoveAnim(GrabStartAnimName, EAS_FullBody); if ( bUseRootMotion ) { EnableRootMotion(); } } /** * Check to see if we're in range to lock into a grapple * Network: Server */ function CheckGrapple() { local vector ToEnemy; local vector Extent, HitLocation, HitNormal; local Actor HitActor; local KFPawn Victim; if( KFPOwner.IsHumanControlled() ) { Victim = KFPawn(FindPlayerGrabTarget()); } else if( AIOwner != none ) { Victim = KFPawn( AIOwner.Enemy ); } if( Victim != none && Victim.IsAliveAndWell() && Victim.GetTeamNum() != KFPOwner.GetTeamNum() /*&& AIOwner.Enemy.Physics != PHYS_Falling*/ ) { if ( Victim != None && (bCanBeBlocked && Victim.MyKFWeapon != None && Victim.MyKFWeapon.IsGrappleBlocked(PawnOwner)) || (!Victim.CanBeGrabbed(KFPOwner, true)) ) { return; // blocked by weapon or pawn can't be grabbed } // Check victim Z offset if( Abs(PawnOwner.Location.Z - Victim.Location.Z) > MaxVictimZOffset ) { return; } ToEnemy = (PawnOwner.Location - Victim.Location); if( VSizeSq(ToEnemy) > Square(MaxGrabDistance) ) { return; // exceeded max distance } if( !KFPOwner.IsHumanControlled() ) { // Set our extent Extent.X = PawnOwner.GetCollisionRadius() * 0.5f; Extent.Y = Extent.X; Extent.Z = PawnOwner.GetCollisionHeight() * 0.5f; // trace for obstructions (already checked if IsHumanControlled()) HitActor = PawnOwner.Trace(HitLocation, HitNormal, Victim.Location, PawnOwner.Location, true, Extent); if( HitActor != None && HitActor != Victim ) { return; } } BeginGrapple(Victim); PlayGrappleLoopAnim(); } } /** Returns a victim to try and grab onto */ function Pawn FindPlayerGrabTarget() { local KFPawn_Monster KFPM; local vector StartLoc, EndLoc; KFPM = KFPawn_Monster(PawnOwner); if ( KFPM != None ) { StartLoc = PawnOwner.Location; EndLoc = StartLoc + Normal(Vector(PawnOwner.Rotation)) * MaxGrabDistance; return KFPM.MeleeAttackHelper.FindVictimByFOV(StartLoc, EndLoc, MaxGrabDistance); } } /** * Used for Pawn to Pawn interactions. * Return TRUE if we can perform an Interaction with this Pawn. */ function bool CanInteractWithPawn(KFPawn OtherPawn) { // Prevent interaction if potentiail victim is dead, not on our team, in Phys_Falling, or busy with another special move if (OtherPawn.CanInteractWithPawnGrapple() == false) { return false; } return( (OtherPawn.IsAliveAndWell() && !KFPOwner.IsSameTeam(OtherPawn) && OtherPawn.Physics != PHYS_Falling && !OtherPawn.IsDoingSpecialMove()) && Super.CanInteractWithPawn(OtherPawn) ); } /** Called when grapple is successful and interaction pawn is attached */ 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; KFPOwner.InteractionPawn = Victim; KFPOwner.ReplicatedSpecialMove.InteractionPawn = Victim; // replicate attachment KFPOwner.SpecialMoveFlags = EGS_GrabSuccess; KFPOwner.ReplicatedSpecialMove.Flags = KFPOwner.SpecialMoveFlags; } // See if the grab should be ended early if( PawnOwner.IsHumanControlled() && PawnOwner.IsLocallyControlled() ) { PawnOwner.SetTimer(MinPlayerGrabTime, false, nameof(CheckIfPlayerReleasedGrapple), self); } // stop root motion if ( bUseRootMotion && PawnOwner.Mesh.RootMotionMode == SMRootMotionMode ) { DisableRootMotion(); } // 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() { local KFAIDirector AIDirector; super.StartInteraction(); if( Follower != none && KFPOwner != none ) { // Prevent grenade throwing for a short time after being grabbed to prevent players blowing themselves up if( KFWeapon(Follower.Weapon) != none ) { KFWeapon(Follower.Weapon).ZedGrabGrenadeTossCooldown = Follower.WorldInfo.TimeSeconds + 0.35; } // Try to let the game's KFAIDirector know about the successful grab, so it can alert nearby Zeds // TODO: Might want to move this to a timer so other zeds aren't alerted until the grapple anim actively looping if( KFPOwner.MyKFAIC != none ) { AIDirector = KFPOwner.MyKFAIC.MyAIDirector; //Let the AI controller know the initial attack succeeded if( KFAIController_Monster(KFPOwner.MyKFAIC) != none ) { KFAIController_Monster(KFPOwner.MyKFAIC).bCompletedInitialGrabAttack = true; } } else if ( KFPOwner.WorldInfo.Game != None ) { AIDirector = KFGameInfo( KFPOwner.WorldInfo.Game ).GetAIDirector(); if ( AIDirector != None ) { // We currently don't notify if/when the player breaks away from the grab AIDirector.NotifyPawnGrabbed( Follower, KFPOwner ); } } } } /** Use timer & animlength instead of animend */ function AnimEndNotify(AnimNodeSequence SeqNode, float PlayedTime, float ExcessTime) { if( KFPOwner != none && KFPOwner.Role == ROLE_Authority ) { if ( KFPOwner.SpecialMoveFlags == EGS_GrabAttempt ) { KFPOwner.EndSpecialMove(); } else { PlayGrappleLoopAnim(); } } } /** Toggle attachment */ function SpecialMoveFlagsUpdated() { if ( KFPOwner.SpecialMoveFlags >= EGS_GrabSuccess ) { // one-time attachment if ( Follower == None ) { BeginGrapple(); } PlayGrappleLoopAnim(); } } /** Network: All */ function PlayGrappleLoopAnim() { local byte Variant; if( KFPOwner != none && KFPOwner.Role == ROLE_Authority ) { // Do not use the last variant Variant = Rand(default.GrappleAnims.Length); while(Variant == LastVariant) { Variant = Rand(default.GrappleAnims.Length); } LastVariant = Variant; KFPOwner.SpecialMoveFlags = (Variant << 4) + EGS_GrabSuccess; KFPOwner.ReplicatedSpecialMove.Flags = KFPOwner.SpecialMoveFlags; } PlaySpecialMoveAnim(GrappleAnims[KFPOwner.SpecialMoveFlags >> 4], EAS_FullBody); } /** Notification when Follower is leaving his FollowerSpecialMove */ function OnFollowerLeavingSpecialMove() { KFPOwner.EndSpecialMove(); } function SpecialMoveEnded(Name PrevMove, Name NextMove) { // Clear timers. PawnOwner.ClearTimer(nameof(CheckGrapple), Self); // stop root motion if ( bUseRootMotion && PawnOwner.Mesh.RootMotionMode == SMRootMotionMode ) { DisableRootMotion(); } if( bStopFullBodyWhenMoveEnds ) { KFPOwner.StopBodyAnim(EAS_FullBody, 0.2f); } Super.SpecialMoveEnded(PrevMove, NextMove); } /** Handle bCanBeInterrupted, also see CanOverrideMoveWith() for interrupt moves */ function NotifyOwnerTakeHit(class DamageType, vector HitLoc, vector HitDir, Controller InstigatedBy) { // Don't break grab if the damage was from someone on the same team if( InstigatedBy != none && KFPOwner != none && InstigatedBy.GetTeamNum() == KFPOwner.GetTeamNum() ) { return; } if( !KFPOwner.IsHumanControlled() ) { // End the move immediately and let ProcessAIHit force a SM_Stumble KFPOwner.EndSpecialMove(); // force stumble when damaged from a grapple if ( KFPOwner.CanDoSpecialMove(SM_Stumble) && DamageType != none && DamageType.default.StumblePower > 0 ) { KFPOwner.DoSpecialMove(SM_Stumble,,, class'KFSM_Stumble'.static.PackBodyHitSMFlags(KFPOwner, HitDir)); } } } /** Notification from the pawn that a medium (aka gun) or heavy (aka melee) affliction has been activated */ function NotifyHitReactionInterrupt() { local vector HitDir; if ( KFPOwner.SpecialMoveFlags == EGS_GrabAttempt ) { if ( bCanBeInterrupted ) { // Force stumble. Cannot simple exit this move without aborting/overriding the animation if ( KFPOwner.CanDoSpecialMove(SM_Stumble) ) { HitDir = Normal(KFPOwner.HitFxInfo.EncodedHitDirection); KFPOwner.DoSpecialMove(SM_Stumble,,, class'KFSM_Stumble'.static.PackBodyHitSMFlags(KFPOwner, HitDir)); } } } } /* Called on some player-controlled moves when a firemode input has been pressed */ function SpecialMoveButtonRetriggered() { bPendingStopFire = false; } /** Called on some player-controlled moves when a firemode input has been released */ function SpecialMoveButtonReleased() { bPendingStopFire = true; // Wait out the initial grab attempt + minimum grab duration if( Follower == none || KFPOwner.IsTimerActive(nameOf(CheckIfPlayerReleasedGrapple), self) ) { return; } KFPOwner.EndSpecialMove(); if( KFPOwner.Role < ROLE_Authority && KFPOwner.IsLocallyControlled() ) { KFPOwner.ServerDoSpecialMove( SM_None, true ); } } /** Called when aborting the move due to player input */ function CheckIfPlayerReleasedGrapple() { if( bPendingStopFire ) { KFPOwner.EndSpecialMove(); if( KFPOwner.Role < ROLE_Authority && KFPOwner.IsLocallyControlled() ) { KFPOwner.ServerDoSpecialMove( SM_None, true ); } } } defaultproperties { Handle=SM_GrappleAttack FollowerSpecialMove=SM_GrappleVictim // --------------------------------------------- // Animation GrappleAnims=(Grab_Attack_V1, Grab_Attack_V2, Grab_Attack_V3) GrabStartAnimName=Grab bStopFullBodyWhenMoveEnds=true // --------------------------------------------- // Alignment bServerOnlyPhysics=true bAlignPawns=true AlignDistance=92 // @deprecated: see ForceLookAtPawn bAlignFollowerRotation=false bAlignHumanFollowerControllerRotation=false bStopAlignFollowerRotationAtGoal=true AlignFollowerInterpSpeed=22.f // --------------------------------------------- // Movement bDisableLook=false bDisableMovement=true bLockPawnRotation=false // --------------------------------------------- // Combat MaxGrabDistance=210.f MaxVictimZOffset=128.f bCanBeBlocked=true bCanBeInterrupted=true MinPlayerGrabTime=3.f }