377 lines
12 KiB
Ucode
377 lines
12 KiB
Ucode
//=============================================================================
|
|
// KFSM_RagdollKnockdown
|
|
//=============================================================================
|
|
// Sends pawn into a temporary ragdoll
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
// - Andrew "Strago" Ladenberger
|
|
//=============================================================================
|
|
|
|
class KFSM_RagdollKnockdown extends KFSpecialMove;
|
|
|
|
/** If falling past this distance during knockdown kill pawn */
|
|
var(Physics) float KnockdownMaxZ;
|
|
|
|
/** maximum number of pawns that can use this SM at once */
|
|
var const int MaxKnockdownPawns;
|
|
|
|
var transient vector KnockdownStartLoc;
|
|
|
|
/** Dazed particle effect */
|
|
var transient ParticleSystemComponent DazedPSC;
|
|
|
|
//failsafe
|
|
var int MaxKnockDownTests;
|
|
var int CurrentKnockdownTests;
|
|
|
|
protected function bool InternalCanDoSpecialMove()
|
|
{
|
|
local Pawn P;
|
|
local int KnockdownCount;
|
|
local bool bMayHavePartialKinematics;
|
|
|
|
// If we have a dismembered limb (partial kinematics), then we don't want to apply
|
|
// impulses via SetRBLinearVelocity. For now, just prevent the knockdown.
|
|
// @note: have to make some assumptions because gore is handled clientside
|
|
bMayHavePartialKinematics = KFPOwner.IsHeadless();
|
|
if ( bMayHavePartialKinematics )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// fast (no-cast) count of knocked down pawns
|
|
foreach PawnOwner.WorldInfo.AllPawns( class'Pawn', P )
|
|
{
|
|
if ( P.Physics == PHYS_RigidBody && !P.bPlayedDeath )
|
|
{
|
|
KnockdownCount++;
|
|
}
|
|
}
|
|
|
|
// handle MaxKnockdownPawns
|
|
if ( KnockdownCount >= MaxKnockdownPawns )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Super.InternalCanDoSpecialMove();
|
|
}
|
|
|
|
/** Notification called when Special Move starts */
|
|
function SpecialMoveStarted(bool bForced, Name PrevMove )
|
|
{
|
|
local TWDeferredWorkManager DeferredWorkManager;
|
|
|
|
DeferredWorkManager = TWDeferredWorkManager(KFPOwner.WorldInfo.DeferredWorkManager);
|
|
|
|
// KnockdownImpulse is replicated before this move (if this has problems go back to repnotify KnockdownImpulse)
|
|
DeferredWorkManager.SetTimer(0.0001, false, nameof(DeferApplyKnockdownImpulse), self);
|
|
|
|
// Spawn the dazed particle effect
|
|
if( KFPOwner.WorldInfo.NetMode != NM_DedicatedServer )
|
|
{
|
|
DazedPSC = class'KFSM_Stunned'.static.AttachDazedEffect( KFPawn_Monster(KFPOwner) );
|
|
}
|
|
}
|
|
|
|
/** Notification called when Special Move starts */
|
|
function SpecialMoveEnded(Name PrevMove, Name NextMove)
|
|
{
|
|
local TWDeferredWorkManager DeferredWorkManager;
|
|
DeferredWorkManager = TWDeferredWorkManager(KFPOwner.WorldInfo.DeferredWorkManager);
|
|
DeferredWorkManager.ClearTimer(nameof(DeferApplyKnockdownImpulse), self);
|
|
|
|
// Make sure dazed particle effect is always deactivated
|
|
if( DazedPSC != none && DazedPSC.bIsActive )
|
|
{
|
|
DazedPSC.DeactivateSystem();
|
|
}
|
|
|
|
// failsafe in case knockdown needs to be cleaned up
|
|
if ( PawnOwner.Mesh.PhysicsWeight >= 1.f && NextMove != 'KFSM_RecoverFromRagdoll' )
|
|
{
|
|
PawnOwner.ClearTimer(nameof(EndKnockdown), self);
|
|
PawnOwner.ClearTimer(nameof(KnockdownTimer), self);
|
|
|
|
TermKnockdownRagdoll(KFPOwner);
|
|
}
|
|
}
|
|
|
|
|
|
function DeferApplyKnockdownImpulse()
|
|
{
|
|
ApplyKnockdownImpulse(KFPOwner.KnockdownImpulse);
|
|
}
|
|
|
|
|
|
/** Applies the contents of KnockdownImpulse. */
|
|
function ApplyKnockdownImpulse(const out KnockdownImpulseInfo Info)
|
|
{
|
|
local vector PointImpulse;
|
|
local name PointImpulseBoneName;
|
|
|
|
// KnockdownImpulse should alwasy be non-zero
|
|
if (IsZero(Info.LinearVelocity) && IsZero(Info.AngularVelocity))
|
|
{
|
|
`warn(self@"invalid KnockdownImpulse for"@PawnOwner);
|
|
}
|
|
|
|
if (PawnOwner.Physics != PHYS_RigidBody || PawnOwner.Mesh != PawnOwner.CollisionComponent)
|
|
{
|
|
// first send them to ragdoll
|
|
PlayFallDown();
|
|
}
|
|
|
|
if (PawnOwner.Physics == PHYS_RigidBody)
|
|
{
|
|
KnockdownStartLoc = PawnOwner.Location;
|
|
|
|
// whole-body velocities
|
|
//@note: we set instead of adding for angular velocity as it's generally zero for newly created ragdolls
|
|
// and if it weren't, the client wouldn't be able to reliably derive the same value so it would just cause mismatches
|
|
PawnOwner.Mesh.SetRBAngularVelocity(Info.AngularVelocity, false);
|
|
if (!IsZero(Info.LinearVelocity))
|
|
{
|
|
PawnOwner.Mesh.SetRBLinearVelocity(Info.LinearVelocity, true);
|
|
}
|
|
|
|
if ( !IsZero(Info.ImpulseStrength) )
|
|
{
|
|
// handle radial and point impulses exclusively (network optimization)
|
|
if ( Info.bIsRadialImpulse )
|
|
{
|
|
PawnOwner.Mesh.AddRadialImpulse(Info.ImpulsePosition, Info.ImpulseStrength.X, Info.ImpulseStrength.Y, RIF_Linear, true);
|
|
}
|
|
else
|
|
{
|
|
// Apply the same scaling factor we use for dead ragdolls
|
|
PointImpulse = Info.ImpulseStrength * KFPOwner.KnockdownImpulseScale;
|
|
|
|
if ( Info.PointImpulseHitZone != 255 )
|
|
{
|
|
PointImpulseBoneName = KFPOwner.HitZones[Info.PointImpulseHitZone].BoneName;
|
|
// make sure it's a valid RB bone
|
|
PointImpulseBoneName = KFPOwner.GetRBBoneFromBoneName(PointImpulseBoneName);
|
|
}
|
|
|
|
PawnOwner.Mesh.AddImpulse(PointImpulse, Info.ImpulsePosition, PointImpulseBoneName, true);
|
|
}
|
|
}
|
|
|
|
if (KFPOwner.Role == ROLE_Authority )
|
|
{
|
|
// check for rest on an interval, but also set a maximum time
|
|
PawnOwner.SetTimer(1.5f, false, nameof(EndKnockdown), self);
|
|
PawnOwner.SetTimer(0.20f, true, nameof(KnockdownTimer), self);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`warn(self@"failed to transition to RigidBody for"@GetFuncName());
|
|
KFPOwner.EndSpecialMove();
|
|
}
|
|
}
|
|
|
|
protected function PlayFallDown()
|
|
{
|
|
//Reset phys asset for the same reason as we would if dying
|
|
if (KFPOwner.bReinitPhysAssetOnDeath && KFPOwner.CharacterArch != none && KFPOwner.CharacterArch.PhysAsset != none)
|
|
{
|
|
KFPOwner.Mesh.SetPhysicsAsset(KFPOwner.CharacterArch.PhysAsset, , true);
|
|
}
|
|
|
|
KFPOwner.PrepareRagdoll();
|
|
|
|
if( KFPOwner.InitRagdoll() )
|
|
{
|
|
// Move into post so that we are hitting physics from last frame, rather than animated from this
|
|
PawnOwner.Mesh.SetTickGroup(TG_PostAsyncWork);
|
|
PawnOwner.SetTickGroup(TG_PostAsyncWork);
|
|
|
|
if( PawnOwner.Mesh.PhysicsAssetInstance != None )
|
|
{
|
|
// Make sure all motors are off on joints.
|
|
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularPositionDrive(FALSE, FALSE, PawnOwner.Mesh, TRUE);
|
|
// Turn on velocity motors, to add some joint friction
|
|
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularDriveParams(0.f, 0.f, 0.004f, PawnOwner.Mesh, TRUE);
|
|
// Don't set motors on dangly bits.
|
|
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularVelocityDrive(TRUE, TRUE, PawnOwner.Mesh, TRUE);
|
|
}
|
|
|
|
PawnOwner.Mesh.bUpdateJointsFromAnimation = FALSE;
|
|
|
|
// we purposefully do NOT turn collide with RBBC_Pawn on
|
|
// we will innerpentrate other ragdolled pawns as we do not collide with the Pawn channel. This is acceptable because if we do
|
|
// collide with other ragdolled pawns there is a chance that we will have the physics system forcibly push us apart causing one
|
|
// or both the pawns to fly through the world
|
|
PawnOwner.Mesh.SetRBChannel(RBCC_KnockedDownPawn);
|
|
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_KnockedDownPawn, TRUE);
|
|
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_Pawn, FALSE);
|
|
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_DeadPawn, FALSE);
|
|
|
|
//ReduceConstraintLimits();
|
|
}
|
|
else
|
|
{
|
|
`Warn("Failed to init ragdoll for"@self);
|
|
}
|
|
}
|
|
|
|
protected event KnockdownFailsafe()
|
|
{
|
|
PawnOwner.Died(PawnOwner.LastHitBy, class'KFDT_Falling', PawnOwner.Location);
|
|
}
|
|
|
|
/** Timer called to check if ragdoll has come to a rest */
|
|
protected function KnockdownTimer()
|
|
{
|
|
CurrentKnockdownTests++;
|
|
if ( PawnOwner.Physics != PHYS_RigidBody || VSizeSq( PawnOwner.Velocity) < 100.f || !PawnOwner.Mesh.RigidBodyIsAwake() || MaxKnockdownTests == CurrentKnockdownTests)
|
|
{
|
|
EndKnockdown(true);
|
|
}
|
|
}
|
|
|
|
/** Initiate recover from ragdoll on the server */
|
|
protected function EndKnockdown(optional bool bForceEnd = false)
|
|
{
|
|
CurrentKnockdownTests = 0;
|
|
//don't let them recover if they shouldn't
|
|
if (!bForceEnd)
|
|
{
|
|
if (!(PawnOwner.Physics != PHYS_RigidBody || VSizeSq(PawnOwner.Velocity) < 100.f || !PawnOwner.Mesh.RigidBodyIsAwake()))
|
|
{
|
|
`log("failed to recover from knockdown " @PawnOwner.Physics @VSizeSq(PawnOwner.Velocity) < 100.f @PawnOwner.Mesh.RigidBodyIsAwake());
|
|
return;
|
|
}
|
|
}
|
|
|
|
PawnOwner.ClearTimer(nameof(EndKnockdown), self);
|
|
PawnOwner.ClearTimer(nameof(KnockdownTimer), self);
|
|
|
|
// Deactivate dazed particle effect
|
|
if( DazedPSC != none )
|
|
{
|
|
DazedPSC.DeactivateSystem();
|
|
}
|
|
|
|
// if we've fallen a significant distance then assume we're dead
|
|
if ( Abs(KnockdownStartLoc.Z - PawnOwner.Location.Z) > KnockdownMaxZ )
|
|
{
|
|
KnockdownFailsafe();
|
|
}
|
|
|
|
KFPOwner.KnockdownImpulse.LinearVelocity = vect(0,0,0);
|
|
KFPOwner.KnockdownImpulse.AngularVelocity = vect(0,0,0);
|
|
|
|
// If we have a dismembered limb (partial kinematics), then we can't get back up
|
|
// note: It should be possible to handle this using bEnableFullAnimWeightBodies/bAlwaysFullAnimWeight
|
|
// on the dismembered limb, but for now just kill the pawn.
|
|
if ( KFPOwner.HasInjuredHitZones() )
|
|
{
|
|
KnockdownFailsafe();
|
|
}
|
|
|
|
// if we're still alive at this point
|
|
if ( PawnOwner.IsAliveAndWell() && PawnOwner.Physics == PHYS_RigidBody )
|
|
{
|
|
KFPOwner.DoSpecialmove(SM_RecoverFromRagdoll, TRUE);
|
|
|
|
if ( KFPOwner.SpecialMove != SM_RecoverFromRagdoll )
|
|
{
|
|
TermKnockdownRagdoll(KFPOwner);
|
|
`log(GetFuncName() @ "Failed to find special move class for: SM_RecoverFromRagdoll" @ self);
|
|
}
|
|
}
|
|
// If something else changed owner's physics mode - should not happen!
|
|
else if ( PawnOwner.Physics != PHYS_RigidBody )
|
|
{
|
|
// if this happens the first place to look is a SetPhysics() call in AIC state Action_Idle
|
|
`Warn("Knockdown timer called outside of PHYS_RigidBody"@PawnOwner.Physics@KFPOwner.SpecialMove);
|
|
KnockdownFailsafe();
|
|
}
|
|
}
|
|
|
|
/** Restore normal pawn physics */
|
|
static function TermKnockdownRagdoll(KFPawn P)
|
|
{
|
|
local SkeletalMeshComponent Mesh;
|
|
local SkeletalMeshComponent DefaultMesh;
|
|
|
|
if ( P.bPlayedDeath )
|
|
{
|
|
return; // Skip TermKnockdown after death and use full ragdoll
|
|
}
|
|
|
|
// this code is a lot like TermRagdoll, but without turning off the PhysicsAssetInstance
|
|
P.SetPhysics(PHYS_Falling);
|
|
P.Velocity = vect(0,0,0);
|
|
P.Acceleration = vect(0,0,0);
|
|
P.CollisionComponent = P.CylinderComponent;
|
|
|
|
Mesh = P.Mesh;
|
|
DefaultMesh = P.default.Mesh;
|
|
|
|
// Restore things we turned off when we went into rag doll
|
|
if( !DefaultMesh.bHasPhysicsAssetInstance )
|
|
{
|
|
Mesh.SetHasPhysicsAssetInstance(FALSE);
|
|
}
|
|
Mesh.MinDistFactorForKinematicUpdate = DefaultMesh.MinDistFactorForKinematicUpdate;
|
|
Mesh.bUpdateKinematicBonesFromAnimation = DefaultMesh.bUpdateKinematicBonesFromAnimation;
|
|
Mesh.bUpdateJointsFromAnimation = DefaultMesh.bUpdateJointsFromAnimation;
|
|
Mesh.PhysicsWeight = DefaultMesh.PhysicsWeight;
|
|
|
|
// Restore non-default collision settings to the way it was before knockdown
|
|
RestoreRBCollision(P);
|
|
|
|
// Kick dead bodies
|
|
//Mesh.bUpdateKinematicBonesFromAnimation = true;
|
|
//Mesh.SetRBCollidesWithChannel(RBCC_DeadPawn, TRUE);
|
|
|
|
// restore danglers
|
|
if( Mesh != None && Mesh.PhysicsAssetInstance != None )
|
|
{
|
|
Mesh.PhysicsAssetInstance.SetFullAnimWeightBonesFixed(FALSE, Mesh);
|
|
}
|
|
|
|
// If Pawn was using partial kinamatics for gore (e.g. Headless), reset those settings now
|
|
if ( P.bHasBrokenConstraints && P.HasInjuredHitZones() )
|
|
{
|
|
P.InitPartialKinematics();
|
|
P.Mesh.UpdateMeshForBrokenConstraints();
|
|
}
|
|
}
|
|
|
|
/** If RB settings were modified while alive (e.g. Ragdoll knockdown) restore settings */
|
|
static function RestoreRBCollision(KFPawn P)
|
|
{
|
|
P.Mesh.SetRBCollidesWithChannel(RBCC_KnockedDownPawn, FALSE);
|
|
|
|
if( !P.Mesh.bUpdateKinematicBonesFromAnimation )
|
|
{
|
|
// No RB collision
|
|
P.Mesh.SetRBChannel(RBCC_Nothing);
|
|
}
|
|
else
|
|
{
|
|
// Go back to being a pawn, and can collide with other live pawns (for flappy bits),
|
|
// and corpses, but not knocked down alive people )
|
|
P.Mesh.SetRBChannel(RBCC_Pawn);
|
|
P.Mesh.SetRBCollidesWithChannel(RBCC_Pawn, P.default.Mesh.RBCollideWithChannels.Pawn);
|
|
}
|
|
|
|
P.UpdateMeshForFleXCollision();
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
Handle=KFSM_Knockdown
|
|
KnockdownMaxZ=750.f
|
|
MaxKnockdownPawns=5
|
|
|
|
bCanOnlyWanderAtEnd=true
|
|
bDisablesWeaponFiring=true
|
|
MaxKnockdownTests=25
|
|
} |