//=============================================================================
// 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
}