1
0
KF2-Dev-Scripts/KFGame/Classes/KFSM_InteractionPawnLeader.uc
2020-12-13 18:01:13 +03:00

492 lines
16 KiB
Ucode

//=============================================================================
// KFSM_InteractionPawnLeader
//=============================================================================
// Base class for Pawn to Pawn Interactions.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
//=============================================================================
// Based on GSM_InteractionPawnLeader_Base
// Copyright 1998-2011 Epic Games, Inc. All Rights Reserved.
//=============================================================================
class KFSM_InteractionPawnLeader extends KFSpecialMove
native(SpecialMoves)
abstract;
var transient protected CameraAnimInst ExecutionCameraAnimInst_Leader;
var transient protected CameraAnimInst ExecutionCameraAnimInst_Follower;
/** Pointer to Follower. */
var KFPawn Follower;
/** if other than SM_None, then force InteractionPawn into this special move. */
var ESpecialMove FollowerSpecialMove;
/** Max time to wait for Interaction to start. If it can't be made, special move will be aborted. */
var float InteractionStartTimeOut;
/** If TRUE, Pawns will be aligned with each other. */
var bool bAlignPawns;
/** Desired distance to align both pawns. */
var float AlignDistance;
/** Checks 2D dist between the pawns versus this threshold when determining if they are close enough */
var float AlignDistanceThreshold;
/** Should leader be aligned as well as follower */
var bool bAlignLeaderLocation;
/** If TRUE, align the Z position of follower as well */
var bool bAlignFollowerZ;
/** Should Follower look in same dir as me? */
var bool bAlignFollowerLookSameDirAsMe;
/** If Rotations should automatically be aligned */
var bool bAlignLeaderRotation;
var bool bAlignFollowerRotation;
var bool bStopAlignFollowerRotationAtGoal;
/** If human controlled controller's Rotations should automatically be aligned as well */
var bool bAlignHumanFollowerControllerRotation;
/** Used by RInterpTo, multiplied by 4096.f (see GOW3 UGearSpecialMove::MoveTo which is similar) */
var float AlignFollowerInterpSpeed;
var INT LeaderRelativeYawOffset;
/** Time it takes to pull the pawns together */
var float AlignSpeedModifier;
/** (NEW) Test interaction collision to detect when pawns get too far apart. */
var bool bRetryCollisionCheck;
// C++ functions
cpptext
{
virtual void PrePerformPhysics(FLOAT DeltaTime);
}
/**
* Trace to see if it will hit from current location to +targetrelativeoffset
* If so, it will return vector from target location to hit location
*/
native function vector GetClipSafeMeshTranslation(out vector TargetRelativeOffset);
function SpecialMoveStarted(bool bForced, Name PrevMove)
{
Super.SpecialMoveStarted(bForced, PrevMove);
// Reset variables
Follower = None;
bAlignFollowerLookSameDirAsMe = default.bAlignFollowerLookSameDirAsMe;
bAlignPawns = default.bAlignPawns;
bAlignFollowerRotation = default.bAlignFollowerRotation;
// 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();
}
function SpecialMoveEnded(Name PrevMove, Name NextMove)
{
local KFSM_InteractionPawnFollower FollowerSM;
// Clear timers
PawnOwner.ClearTimer(nameof(CheckReadyToStartInteraction), Self);
PawnOwner.ClearTimer(nameof(InteractionStartTimedOut), self);
PawnOwner.ClearTimer(nameof(RetryCollisionTimer), self);
// If the special move was ended while bAlignPawns was on, velocity may get stuck on clients
if( bAlignPawns && !KFPOwner.IsHumanControlled() )
{
PawnOwner.ZeroMovementVariables();
if( Follower != None)
{
Follower.ZeroMovementVariables();
}
}
// If the leader is leaving the special move, notify the follower
if( Follower != None && Follower.IsDoingSpecialMove(FollowerSpecialMove) )
{
FollowerSM = KFSM_InteractionPawnFollower(Follower.SpecialMoves[FollowerSpecialMove]);
FollowerSM.OnLeaderLeavingSpecialMove();
}
// Clear reference to Interaction Pawn.
Follower = None;
if (ExecutionCameraAnimInst_Leader != None)
{
if (PCOwner != None)
{
PCOwner.PlayerCamera.StopCameraAnim(ExecutionCameraAnimInst_Leader);
}
ExecutionCameraAnimInst_Leader = None;
}
if (ExecutionCameraAnimInst_Follower != None)
{
if (PCOwner != None)
{
PCOwner.PlayerCamera.StopCameraAnim(ExecutionCameraAnimInst_Follower);
}
ExecutionCameraAnimInst_Follower = None;
}
Super.SpecialMoveEnded(PrevMove, NextMove);
}
/** Safety net in case Interaction cannot be started. Abort special move. */
function InteractionStartTimedOut()
{
local KFPawn KFP;
KFP = KFPawn(PawnOwner);
if (KFP.SpecialMoves[KFP.SpecialMove] == self)
{
`Warn(KFP.WorldInfo.TimeSeconds @ KFP @ class @ GetFuncName() @ "InteractionStartTimeOut hit!! Aborting move." @ Follower @ Follower.SpecialMove );
KFP.EndSpecialMove(, true);
}
}
/** Checks if Interaction is ready to be started, and starts if it is. */
final function CheckReadyToStartInteraction()
{
local KFSM_InteractionPawnFollower FollowerSM;
// Make sure we have an InteractionPawn, this is a requirement for these types of special moves
if( KFPOwner.InteractionPawn != None )
{
// Save variable here for ease of use.
Follower = KFPOwner.InteractionPawn;
// If net owner, start Follower special move if not already done.
if( PawnOwner.WorldInfo.NetMode != NM_Client )
{
// Make sure we have a valid Pawn to work with
if( Follower != None && Follower.Health > 0 )
{
if( !Follower.IsDoingSpecialMove(FollowerSpecialMove) )
{
Follower.DoSpecialMove(FollowerSpecialMove, TRUE, PawnOwner);
}
}
else
{
// Our Pawn is never going to go in his special move... pop an error, and exit move.
`Warn(PawnOwner.WorldInfo.TimeSeconds @ PawnOwner @ class @ GetFuncName() @ "Follower not gameplay relevant, Interaction cannot be started!!!" @ Follower @ Follower.SpecialMove, KFPOwner.bLogSpecialMove);
KFPOwner.EndSpecialMove(KFPOwner.SpecialMove);
return;
}
}
}
// If not ready, then set timer to try again next tick.
if( !IsReadyToStartInteraction() )
{
if( Follower == None )
{
`log(PawnOwner.WorldInfo.TimeSeconds @ PawnOwner @ class @ GetFuncName() @ "Not ready to StartInteraction, delay... Follower:" @ Follower, KFPOwner.bLogSpecialMove,'SpecialMoves');
}
else
{
`log(PawnOwner.WorldInfo.TimeSeconds @ PawnOwner @ class @ GetFuncName() @ "Not ready to StartInteraction, delay... Follower:" @ Follower @ "Follower.SpecialMove:" @ Follower.SpecialMove, KFPOwner.bLogSpecialMove ,'SpecialMoves');
}
// Retry next frame...
PawnOwner.SetTimer( PawnOwner.WorldInfo.DeltaSeconds, FALSE, nameof(self.CheckReadyToStartInteraction), self );
}
// otherwise, start interaction now!
else
{
`log(PawnOwner.WorldInfo.TimeSeconds @ PawnOwner @ class @ GetFuncName() @ "StartInteraction. Follower:" @ Follower @ "Follower.SpecialMove:" @ Follower.SpecialMove, KFPOwner.bLogSpecialMove ,'SpecialMoves');
PawnOwner.ClearTimer(nameof(CheckReadyToStartInteraction), Self);
// Clear timeout timer
PawnOwner.ClearTimer(nameof(InteractionStartTimedOut), Self);
// Notify follower that we're about to start.
FollowerSM = KFSM_InteractionPawnFollower(Follower.SpecialMoves[FollowerSpecialMove]);
FollowerSM.InteractionStarted();
// And start interaction!
StartInteraction();
}
}
/**
* Conditions to determine if interaction is ready to be started.
* - Need an InteractionPawn
* - InteractionPawn needs to be doing his InteractionPawnSpecialMove if it is other than SM_None.
*/
function bool IsReadyToStartInteraction()
{
return (Follower != None && Follower.IsDoingSpecialMove(FollowerSpecialMove));
}
/** StartInteraction */
function StartInteraction()
{
if ( bRetryCollisionCheck && PawnOwner.Role == ROLE_Authority )
{
PawnOwner.SetTimer(0.5f, true, nameof(RetryCollisionTimer), self);
}
}
/** Called on an interval to detect when pawns get too far apart or are seperated by a door */
function RetryCollisionTimer()
{
// Test for vertical melee range (e.g. Follower fell off a ledge) & reachabilty (doors etc.)
// This started happening after PHYS_Falling handling was added to PrePerformPhysics
if ( (Abs(Follower.Location.Z - PawnOwner.Location.Z) > PawnOwner.CylinderComponent.CollisionHeight * 1.5) ||
!IsFollowerReachable() )
{
//`log("rejected outside of vertical melee range");
KFPOwner.EndSpecialMove();
}
}
/**
* @brief Checks if the "follower" can stil be reached, mostly for doors
*
* @return true if we can physically reach the "follower"
*/
function bool IsFollowerReachable()
{
// Trace from the WorldInfo, since open doors can ignore traces from zeds
return ( IsPawnPathClear(KFPOwner.WorldInfo, Follower, Follower.Location, KFPOwner.Location, vect(2,2,2), true, true)
&& IsPawnPathClear(KFPOwner.WorldInfo, Follower, Follower.Location, KFPOwner.Location,, true, true) );
}
/** Messages sent to this special move */
function bool MessageEvent(Name EventName, Object Sender)
{
if( EventName == 'FollowerLeavingSpecialMove' )
{
OnFollowerLeavingSpecialMove();
return TRUE;
}
return Super.MessageEvent(EventName, Sender);
}
/** Notification when Follower is leaving his FollowerSpecialMove */
function OnFollowerLeavingSpecialMove()
{
if( KFPOwner != none && KFPOwner.Role == ROLE_Authority )
{
KFPOwner.EndSpecialMove();
}
}
/**
* Dump relative location of a Bone to PawnOwner
* For debugging purposes
*/
function DebugSocketRelativeLocation(name InSocketName)
{
local Vector MarkerLoc;
`if(`notdefined(ShippingPC))
local Vector RelativeLoc;
`endif
local Rotator MarkerRot;
// Get position of marker
//MarkerLoc = PawnOwner.Mesh.GetBoneLocation(BoneName);
PawnOwner.Mesh.GetSocketWorldLocationAndRotation(InSocketName, MarkerLoc, MarkerRot);
// Dump a little debug info to display the location we got:
PawnOwner.DrawDebugSphere(MarkerLoc, 4, 8, 255, 0, 255, TRUE);
`if(`notdefined(ShippingPC))
// Transform marker location to be relative to Leader location and rotation.
RelativeLoc = WorldToRelativeOffset(PawnOwner.Rotation, MarkerLoc - PawnOwner.Location);
`log(GetFuncName() @ "RelativeLoc:" @ RelativeLoc);
`endif
}
/**
* Special Execution version.
* Test to play same animation on victim.
* WARNING: * ForcePitchCentering not reset for Follower
* * Follower's ViewTarget is set to the Leader.
*
* @param InCameraAnims - the list of anims to choose from
*
* @param LeaderAnimInst - a reference to the anim played on the leader
*
* @param bViewFromLeaderPOV - true to make followers see the execution from the leader's perspective, false otherwise
*
* @return True if it played an anim, false otherwise.
*/
function bool PlayExecutionCameraAnim(out const array<CameraAnim> InCameraAnims, optional out CameraAnimInst LeaderAnimInst, optional float BlendInTime=0.25f, optional float BlendOutTime=0.25f)
{
local KFPlayerController FollowerPC;
local int AnimIdx;
local CameraAnimInst FollowerAnimInst;
local ViewTargetTransitionParams TransitionParams;
FollowerPC = KFPlayerController(Follower.Controller);
if( PCOwner != None )
{
AnimIdx = FollowerPC.ChooseRandomCameraAnim(InCameraAnims);
}
if (PawnOwner.IsLocallyControlled() && AnimIdx != INDEX_NONE)
{
LeaderAnimInst = PlayCameraAnim(PawnOwner, InCameraAnims[AnimIdx],,, BlendInTime, BlendOutTime);
ExecutionCameraAnimInst_Leader = LeaderAnimInst;
}
// If Follower is human controlled, have him spectate his death from the killer's perspective
if( FollowerPC != None && Follower.IsHumanControlled() )
{
if (FollowerPC.GetViewTarget() != PawnOwner)
{
TransitionParams.BlendTime = 0;
FollowerPC.SetViewTarget(PawnOwner, TransitionParams);
}
AnimIdx = FollowerPC.ChooseRandomCameraAnim(InCameraAnims);
// Also play the death camera
if( Follower.IsLocallyControlled() )
{
FollowerAnimInst = PlayCameraAnim(Follower, InCameraAnims[AnimIdx],,, BlendInTime, BlendOutTime);
ExecutionCameraAnimInst_Follower = FollowerAnimInst;
}
}
return (FollowerAnimInst != None) || (LeaderAnimInst != None);
}
/** Attach Follower to Leader */
function AttachFollowerToLeader(Name SocketName, optional Vector AttachLocation, optional Rotator AttachRotation)
{
local SkeletalMeshSocket Socket;
// Force replication update.
Follower.bForceNetUpdate = TRUE;
Follower.SetBase(None);
Follower.SetPhysics(PHYS_None);
Follower.SetHardAttach(TRUE);
Follower.SetCollision(TRUE, FALSE);
Follower.bCollideWorld = FALSE;
// Make sure kidnapper has his attachment bone defined.
Socket = PawnOwner.Mesh.GetSocketByName(SocketName);
if( Socket != None )
{
Follower.SetBase(PawnOwner,, PawnOwner.Mesh, Socket.BoneName);
Follower.SetRelativeLocation( Socket.RelativeLocation - Follower.default.Mesh.Translation );
//Follower.SetRelativeRotation( Socket.RelativeRotation );
//Follower.UpdateMeshTranslationOffset( vect(0,0,0), true );
//Follower.Mesh.SetTranslation( vect(0,0,0) );
}
// check if bone exists with tht name
else if ( PawnOwner.Mesh.MatchRefBone(SocketName) != INDEX_NONE )
{
Follower.SetBase(PawnOwner,, PawnOwner.Mesh, SocketName);
Follower.SetRelativeLocation( AttachLocation );
Follower.SetRelativeRotation( AttachRotation );
//Follower.UpdateMeshTranslationOffset( vect(0,0,0) );
}
else
{
`Warn(PawnOwner.WorldInfo.TimeSeconds @ class @ GetFuncName() @ "Leader" @ PawnOwner.class @ "Has no attachment socket" @ SocketName @ "!!!!");
if( AttachRotation != rot(0,0,0) )
{
Follower.SetRotation(AttachRotation);
}
else
{
Follower.SetRotation(PawnOwner.Rotation);
}
// b_MF_Attach doesn't exist on all Pawns!!
if( AttachLocation != vect(0,0,0) )
{
Follower.SetLocation(AttachLocation);
}
else
{
Follower.SetLocation(PawnOwner.Location);
}
Follower.SetBase(PawnOwner);
}
// need to set PHYS_None again, because SetBase() changes physics to PHYS_Falling
Follower.SetPhysics(PHYS_None);
// This needs to be set AFTER Follower.UpdateMeshTranslationOffset( vect(0,0,0) );
//Follower.bDisableMeshTranslationChanges = TRUE;
// Log all debug information.
`log(PawnOwner.WorldInfo.TimeSeconds @ PawnOwner @ class @ GetFuncName() @ "Attached Follower:" @ Follower
@ "BaseSkelComponent:" @ Follower.BaseSkelComponent
@ "SocketName:" @ SocketName
@ "BaseBoneName:" @ Follower.BaseBoneName
@ "RelativeLocation:" @ Follower.RelativeLocation
@ "RelativeRotation:" @ Follower.RelativeRotation
@ "bHardAttach:" @ Follower.bHardAttach
@ "bIgnoreBaseRotation:" @ Follower.bIgnoreBaseRotation, KFPOwner.bLogSpecialMove,'SpecialMoves');
//REMOVED - Perf difference should be minimal and there is still an issue when the parent is not in shadow, but the child should be.
//Shadow parent, to render only one shadow for both.
//Follower.Mesh.SetShadowParent(PawnOwner.Mesh);
}
/**
* Detaches a based Pawn from the Leader.
*
* @param APawn - the Pawn to detach
*/
function DetachPawn(Pawn APawn)
{
DetachPawnHelper(APawn);
}
/**
* Detach the pawn and turn collision back on.
* Static so other classes (like GSM_Hostage) can reuse this detach code.
*
* @param APawn - the Pawn to detach
*/
static function DetachPawnHelper(Pawn APawn)
{
APawn.SetBase(None);
APawn.SetHardAttach(false);
APawn.SetPhysics(PHYS_Falling);
//APawn.bDisableMeshTranslationChanges = FALSE;
if (!APawn.bTearOff)
{
APawn.SetCollision(true, true);
}
APawn.bCollideWorld = true;
// Clear Shadow parent
APawn.Mesh.SetShadowParent(None);
}
/**
* Used for Pawn to Pawn interactions.
* Return TRUE if we can perform an Interaction with this Pawn.
*/
function bool CanInteractWithPawn(KFPawn OtherPawn)
{
return TRUE;
}
DefaultProperties
{
bAlignLeaderLocation=true
bAlignLeaderRotation=TRUE
bAlignFollowerRotation=true
bAlignFollowerZ=false
AlignFollowerInterpSpeed=8.f
AlignDistanceThreshold=0.3f
InteractionStartTimeOut=4.f
FollowerSpecialMove=SM_None
bRetryCollisionCheck=true
AlignSpeedModifier=0.5f
}