MythicalWar/Assets/Invector-3rdPersonController/Basic Locomotion/Scripts/CharacterController/Actions/vLadderAction.cs

1128 lines
41 KiB
C#

using System.Collections;
using System.Collections.Generic;
#if INVECTOR_PLAYABLESANIMATOR
using InvectorCore.Playables;
using InvectorCore;
#endif
using UnityEngine;
using UnityEngine.Events;
namespace Invector.vCharacterController.vActions
{
[vClassHeader("Ladder Action", iconName = "ladderIcon")]
public class vLadderAction : vActionListener, vIAnimatorMoveReceiver
{
private const string ClimbLadderTag = "ClimbLadder";
private const string SlideDownLadderTag = "LadderSlideDown";
// PDM state tags marking which hand a ladder state leaves planted when it ends.
private const string LadderFootLeftTag = "LadderFootLeft";
private const string LadderFootRightTag = "LadderFootRight";
private const string EnterLadderTopState = "EnterLadderTop";
private const string EnterLadderBottomState = "EnterLadderBottom";
private const string ExitLadderTopState = "ExitLadderTop";
private const string ExitLadderBottomState = "ExitLadderBottom";
private const string QuickExitLadderState = "QuickExitLadder";
private const string SlideDownLadderState = "Ladder_SlideDown";
private const string IdleLadderLeftState = "IdleLadder_Left";
private const string IdleLadderRightState = "IdleLadder_Right";
private const float EnterAnimationTransitionDuration = 0.25f;
private const float ExitAnimationTransitionDuration = 0.1f;
private const float SlideDownTransitionDuration = 0.2f;
private const float ClimbInputThreshold = 0.05f;
private const float TriggerForwardDistanceThreshold = 0.8f;
private const float ExitResetNormalizedTime = 0.8f;
// The quick exit drops the character into open air, so it hands the body back to physics as soon
// as its clip stops owning Y - matched to the row's root-motion Y range and LockMovement tag
// window. The triggered exits end standing on ground and keep root motion to the end of the clip,
// so they stay on the later shared gate above.
private const float QuickExitResetNormalizedTime = 0.6f;
private const float LadderInputSmoothingDuration = 0.1f;
private const float AnimatorInputDampTime = 0.1f;
private const float StaminaRecoveryDelay = 0.25f;
#region public variables
#if INVECTOR_PLAYABLESANIMATOR
[vEditorToolbar("PDM", overrideChildOrder: true, order = 0)]
[Header("Playables Animator")]
[Tooltip("PDM that contains only the ladder states. The PDM active before entering the ladder is restored on exit.")]
public PlayablesDecisionMaker ladderPDM;
#endif
[vEditorToolbar("Settings", overrideChildOrder: true, order = 0)]
[Tooltip("Tag of the object you want to access")]
public string actionTag = "LadderTrigger";
[Tooltip("Speed multiplier for the climb ladder animations")]
public float climbSpeed = 1.5f;
[Tooltip("Speed multiplier for the climb ladder animations when the fastClimbInput is pressed")]
public float fastClimbSpeed = 3f;
[Tooltip("How much Stamina will be consumed when climbing faster")]
public float fastClimbStamina = 30f;
[Tooltip("Input to use the ladder going up or down")]
public GenericInput verticalInput = new GenericInput("Vertical", "LeftAnalogVertical", "Vertical");
[Tooltip("Input to enter the ladder")]
public GenericInput enterInput = new GenericInput("E", "A", "A");
[Tooltip("Input to exit the ladder")]
public GenericInput exitInput = new GenericInput("Space", "B", "B");
[Tooltip("Input to climb faster")]
public GenericInput fastClimbInput = new GenericInput("LeftShift", "LeftStickClick", "LeftStickClick");
[Tooltip("Input to climb faster")]
public GenericInput slideDownInput = new GenericInput("Q", "X", "X");
[vEditorToolbar("Events")]
public UnityEvent OnEnterLadder;
public UnityEvent OnExitLadder;
public UnityEvent OnEnterTriggerLadder;
public UnityEvent OnExitTriggerLadder;
[vEditorToolbar("Debug")]
public bool debugMode;
[vReadOnly(false)]
[SerializeField]
protected vTriggerLadderAction targetLadderAction;
[vReadOnly(false)]
[SerializeField]
protected vTriggerLadderAction currentLadderAction;
//[vReadOnly(false)]
//[SerializeField]
protected List<vTriggerLadderAction> actionTriggers = new List<vTriggerLadderAction>();
[vReadOnly(false)]
[SerializeField]
protected float _speed;
protected virtual float speed { get { return _speed; } set { _speed = value; } }
[vReadOnly(false)]
[SerializeField]
protected float currentClimbSpeed;
[vReadOnly(false)]
[SerializeField]
protected bool _isUsingLadder;
#if INVECTOR_PLAYABLESANIMATOR
[ExposeCondition]
#endif
public virtual bool isUsingLadder { get { return _isUsingLadder; } set { _isUsingLadder = value; } }
#if INVECTOR_PLAYABLESANIMATOR
[vReadOnly(false)]
[SerializeField]
protected float ladderInputSmooth;
/// <summary>Input used by the ladder PDM rows.</summary>
[ExposeCondition]
public virtual float LadderInputVertical { get { return ladderInputSmooth; } }
/// <summary>Clip retime factor for the UsingLadder PDM rows.</summary>
[ExposeCondition]
public virtual float LadderClimbSpeed { get { return currentClimbSpeed; } }
#endif
public enum vLadderSide { Left, Right }
[vReadOnly(false)]
[SerializeField]
protected vLadderSide ladderSide = vLadderSide.Left;
/// <summary>Which side the character is planted on; the PDM condition for the next state.</summary>
#if INVECTOR_PLAYABLESANIMATOR
[ExposeCondition]
#endif
public virtual vLadderSide LadderSide { get { return ladderSide; } }
[vReadOnly(false)]
[SerializeField]
protected bool enterLadderStarted;
[vReadOnly(false)]
[SerializeField]
protected bool inEnterLadderAnimation;
[vReadOnly(false)]
[SerializeField]
protected bool inExitingLadderAnimation;
[vReadOnly(false)]
[SerializeField]
protected bool triggerEnterOnce;
/// <summary>
/// Root XZ captured on the first frame of the enter match, so the outgoing run's root motion
/// stops deciding where the body goes while the enter curve is still near zero.
/// </summary>
protected Vector2 matchOriginXZ;
protected bool hasMatchOriginXZ;
/// <summary>When the enter match started, so the hold can be bounded to the cross-fade.</summary>
protected float matchOriginTime;
[vReadOnly(false)]
[SerializeField]
protected bool triggerExitOnce;
#endregion
protected vThirdPersonInput tpInput;
#if INVECTOR_PLAYABLESANIMATOR
protected PlayablesAnimator playablesAnimator;
protected PlayablesDecisionMaker previousPDM;
protected bool ladderPDMIsActive;
protected Coroutine restorePreviousPDMCoroutine;
protected virtual bool UsingPlayables => playablesAnimator && playablesAnimator.isActiveAndEnabled;
protected virtual bool CanUseLadderPDM => UsingPlayables && ladderPDM;
#endif
protected bool isAligningMidAir;
protected override void SetUpListener()
{
actionEnter = false;
actionStay = true;
actionExit = true;
}
protected override void Start()
{
base.Start();
#if INVECTOR_PLAYABLESANIMATOR
playablesAnimator = GetComponent<PlayablesAnimator>();
if (playablesAnimator && !playablesAnimator.isActiveAndEnabled)
{
playablesAnimator = null;
}
#endif
tpInput = GetComponent<vThirdPersonInput>();
if (tpInput)
{
tpInput.onUpdate -= UpdateLadderBehavior;
tpInput.onUpdate += UpdateLadderBehavior;
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
playablesAnimator.OnPlayablesStateEnter -= HandleLadderStateEnter;
playablesAnimator.OnPlayablesStateEnter += HandleLadderStateEnter;
}
#endif
}
}
// Detaches callbacks registered in Start, so a runtime-spawned character doesn't leak this action.
protected virtual void OnDestroy()
{
if (tpInput)
{
tpInput.onUpdate -= UpdateLadderBehavior;
}
#if INVECTOR_PLAYABLESANIMATOR
if (playablesAnimator)
{
playablesAnimator.OnPlayablesStateEnter -= HandleLadderStateEnter;
}
#endif
}
// Routed by vAnimatorMoveSender. Skipped while Playables drives so the climb doesn't tick twice.
public virtual void OnAnimatorMoveEvent()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return;
}
#endif
UsingLadder();
}
#if INVECTOR_PLAYABLESANIMATOR
// PlayablesAnimator's counterpart to OnAnimatorMoveEvent.
protected virtual void OnPlayablesMove()
{
UsingLadder();
}
#endif
protected virtual void UpdateLadderBehavior()
{
AutoEnterLadder();
EnterLadderInput();
ExitLadderInput();
}
protected virtual void EnterLadderInput()
{
if (targetLadderAction == null || tpInput.cc.customAction || tpInput.cc.isJumping || !tpInput.cc.isGrounded || tpInput.cc.isRolling)
{
return;
}
if (enterInput.GetButtonDown() && !enterLadderStarted && !isUsingLadder && !targetLadderAction.autoAction)
{
TriggerEnterLadder();
}
}
protected virtual bool IsUsingLadder()
{
return tpInput.cc.IsAnimatorTag(ClimbLadderTag);
}
#if INVECTOR_PLAYABLESANIMATOR
// Reads which hand the committed state ends on, not whatever ladderSide already held, so a
// forced idle (TriggerMidAirEnterLadder, the enter states) resyncs instead of drifting. Down
// states reuse the Up clips at negative speed, so their planted side is tagged rather than
// derived from the name.
protected virtual void HandleLadderStateEnter(in PlayablesStateContext context)
{
if (context.Layer == 0 && playablesAnimator.HasAnyTag(LadderFootLeftTag, LadderFootRightTag))
{
ladderSide = playablesAnimator.HasTag(LadderFootLeftTag) ? vLadderSide.Left : vLadderSide.Right;
}
}
#endif
// Animator-only: its climb loop is one state, so a transition means "still entering". The PDM
// alternates Left/Right every climb cycle and would report a transition almost every frame,
// blocking exit forever.
protected virtual bool IsInExitBlockingTransition()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return false;
}
#endif
return tpInput.cc.animator.IsInTransition(0);
}
protected virtual bool IsPlayingLadderState(string stateName)
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return playablesAnimator.IsPlayingState(stateName);
}
#endif
if (tpInput.cc.baseLayerInfo.IsName(stateName))
{
return true;
}
// During CrossFade the CURRENT state is still the one being left, so gating on it stays
// shut for the whole blend and the match starts late, covering the full angle in a few
// frames (the snap). The destination state is what's actually playing in.
var animator = tpInput.cc.animator;
return animator.IsInTransition(0) && animator.GetNextAnimatorStateInfo(0).IsName(stateName);
}
/// <summary>Returns the active base-layer playback progress for match-target curve evaluation.</summary>
protected virtual float GetLadderNormalizedTime()
{
float normalizedTime;
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
normalizedTime = playablesAnimator.GetNormalizedTime(0);
}
else
#endif
{
// GetNextAnimatorStateInfo(0).normalizedTime is NOT the destination clip's progress
// during a blend - it reports near 1 and snaps the match to its final pose. Scale the
// transition's own normalizedTime by blend duration over clip length instead.
var animator = tpInput.cc.animator;
if (animator.IsInTransition(0))
{
var transition = animator.GetAnimatorTransitionInfo(0);
var next = animator.GetNextAnimatorStateInfo(0);
float clipLength = next.length > 0.0001f ? next.length : 1f;
normalizedTime = Mathf.Clamp01(transition.normalizedTime) * (transition.duration / clipLength);
}
else
{
normalizedTime = tpInput.cc.baseLayerInfo.normalizedTime;
}
}
return normalizedTime;
}
protected virtual void SetLadderAnimationSpeed(float value)
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return;
}
#endif
tpInput.cc.animator.speed = value;
}
// PlayablesAnimator resolves its own transitions; Animator uses the supplied blend duration.
protected virtual void PlayAnimation(string stateName, float transitionDuration)
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
playablesAnimator.PlayState(stateName);
return;
}
#endif
tpInput.cc.animator.CrossFadeInFixedTime(stateName, transitionDuration);
}
protected virtual bool InEnterLadderAnimation()
{
return IsPlayingLadderState(EnterLadderTopState) || IsPlayingLadderState(EnterLadderBottomState);
}
// Separate from InEnterLadderAnimation: only a transition heading AWAY from the enter states
// should block the match, not the ENTER blend itself - otherwise the match starts late and snaps.
protected virtual bool InEnterLadderAnimationMatchTarget()
{
if (!InEnterLadderAnimation())
{
return false;
}
return !IsLeavingEnterLadderAnimation();
}
// True while the base layer is blending OUT of an enter-ladder state, as opposed to into one.
protected virtual bool IsLeavingEnterLadderAnimation()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return false;
}
#endif
var animator = tpInput.cc.animator;
if (!animator.IsInTransition(0))
{
return false;
}
var next = animator.GetNextAnimatorStateInfo(0);
return !next.IsName(EnterLadderTopState) && !next.IsName(EnterLadderBottomState);
}
// Animator only: Playables rows don't expose Animator tags, so it stays false for that backend.
protected virtual bool InSlideDownAnimation()
{
return tpInput.cc.IsAnimatorTag(SlideDownLadderTag);
}
protected virtual bool InExitLadderAnimation()
{
return IsPlayingLadderState(ExitLadderTopState) || IsPlayingLadderState(ExitLadderBottomState) || IsPlayingLadderState(QuickExitLadderState);
}
protected virtual void ExitLadderInput()
{
if (!isUsingLadder || InEnterLadderAnimation())
{
return;
}
if (targetLadderAction == null)
{
HandleExitWithoutTrigger();
return;
}
currentLadderAction = targetLadderAction;
var exitState = targetLadderAction.exitAnimation;
if (exitState == ExitLadderBottomState)
{
TryExitAtBottom(exitState);
}
else if (exitState == ExitLadderTopState && IsUsingLadder())
{
TryExitAtTop(exitState);
}
}
private void HandleExitWithoutTrigger()
{
if (!IsUsingLadder())
{
return;
}
if (slideDownInput.GetButtonDown() && !inExitingLadderAnimation)
{
PlayAnimation(SlideDownLadderState, SlideDownTransitionDuration);
}
if (!exitInput.GetButtonDown())
{
return;
}
LogLadderAction("Quick Exit");
SetLadderAnimationSpeed(1f);
PlayAnimation(QuickExitLadderState, ExitAnimationTransitionDuration);
}
private void TryExitAtBottom(string exitState)
{
var shouldExit = !triggerExitOnce &&
(exitInput.GetButtonDown() || speed <= -ClimbInputThreshold || InSlideDownAnimation());
if (!shouldExit)
{
return;
}
LogLadderAction("Exit Bottom");
triggerExitOnce = true;
PlayAnimation(exitState, ExitAnimationTransitionDuration);
}
private void TryExitAtTop(string exitState)
{
if (triggerExitOnce || speed < ClimbInputThreshold || IsInExitBlockingTransition())
{
return;
}
LogLadderAction("Exit Top");
triggerExitOnce = true;
PlayAnimation(exitState, ExitAnimationTransitionDuration);
}
protected virtual void AutoEnterLadder()
{
if (targetLadderAction == null || !targetLadderAction.autoAction)
{
return;
}
if (tpInput.cc.customAction || isUsingLadder || IsInExitBlockingTransition() || tpInput.cc.isRolling)
{
return;
}
if (tpInput.cc.input != Vector3.zero)
{
var activeCamera = Camera.main;
if (!activeCamera)
{
return;
}
var inputDir = activeCamera.transform.TransformDirection(new Vector3(tpInput.cc.input.x, 0f, tpInput.cc.input.z));
inputDir.y = 0f;
var dist = Vector3.Distance(inputDir.normalized, targetLadderAction.transform.forward);
if (dist < TriggerForwardDistanceThreshold)
{
TriggerEnterLadder();
}
}
}
public virtual void TriggerMidAirEnterLadder(vTriggerLadderMiddle ladderMiddle)
{
if (tpInput.cc.isGrounded || isAligningMidAir)
{
return;
}
isAligningMidAir = true;
var targetPosition = ladderMiddle.refTarget.position;
var foundClosestPoint = false;
var closestDistance = Vector3.Distance(targetPosition, transform.position);
var stepCount = (int)(ladderMiddle._collider.size.y / ladderMiddle.stepHeight);
// Walk up the rungs and keep the closest one; stop once distance starts increasing again.
for (var i = 0; i < stepCount; i++)
{
var candidatePosition = ladderMiddle.refTarget.position + ladderMiddle.transform.up * ladderMiddle.stepHeight * i;
var candidateDistance = Vector3.Distance(candidatePosition, transform.position);
if (candidateDistance <= closestDistance)
{
targetPosition = candidatePosition;
closestDistance = candidateDistance;
foundClosestPoint = true;
}
else if (foundClosestPoint)
{
break;
}
}
tpInput.cc.DisableGravityAndCollision();
tpInput.cc.isCrouching = false;
tpInput.cc.ControlCapsuleHeight();
tpInput.UpdateCameraStates();
tpInput.cc.UpdateAnimator();
triggerEnterOnce = true;
enterLadderStarted = true;
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 1); // set actionState 1 to avoid falling transitions
tpInput.cc.StopCharacter();
tpInput.SetLockAllInput(true);
tpInput.cc.ResetInputAnimatorParameters();
#if INVECTOR_PLAYABLESANIMATOR
SwitchToLadderPDM();
#endif
var localPos = ladderMiddle.transform.InverseTransformPoint(transform.position);
var idleState = localPos.x >= 0f ? IdleLadderLeftState : IdleLadderRightState;
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
// PDM has no ActionState guard against Falling, only IsUsingLadder - without this the
// idle state loses the next evaluation to Falling before AlignToMiddleLadder sets it.
isUsingLadder = true;
}
#endif
PlayAnimation(idleState, EnterAnimationTransitionDuration);
tpInput.cc.disableAnimations = true;
StartCoroutine(AlignToMiddleLadder(targetPosition, ladderMiddle.refTarget.rotation));
}
protected virtual IEnumerator AlignToMiddleLadder(Vector3 targetPosition, Quaternion targetRotation)
{
float time = 0f;
Vector3 initialPosition = transform.position;
Quaternion initialRotation = transform.rotation;
while (time <= 1f)
{
transform.position = Vector3.Lerp(initialPosition, targetPosition, time);
transform.rotation = Quaternion.Lerp(initialRotation, targetRotation, time);
time += Time.fixedDeltaTime * 8f;
yield return null;
}
// Pin the pose until the idle blend settles, so its root motion can't drag the character off
// the ladder. Uses IsInExitBlockingTransition, not IsInLadderTransition - see its summary.
do
{
transform.position = targetPosition;
transform.rotation = targetRotation;
yield return null;
}
while (IsInExitBlockingTransition());
yield return new WaitForEndOfFrame();
isUsingLadder = true;
isAligningMidAir = false;
}
protected virtual void TriggerEnterLadder()
{
if (debugMode)
{
Debug.Log("Enter Ladder", this);
}
OnExitTriggerLadder?.Invoke();
if (targetLadderAction.targetCharacterParent)
{
transform.parent = targetLadderAction.targetCharacterParent;
}
tpInput.cc.isCrouching = false;
tpInput.cc.ControlCapsuleHeight();
tpInput.UpdateCameraStates();
tpInput.cc.UpdateAnimator();
OnEnterLadder?.Invoke();
triggerEnterOnce = true;
enterLadderStarted = true;
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 1); // set actionState 1 to avoid falling transitions
tpInput.SetLockAllInput(true);
tpInput.cc.ResetInputAnimatorParameters();
targetLadderAction.OnDoAction?.Invoke();
currentLadderAction = targetLadderAction;
#if INVECTOR_PLAYABLESANIMATOR
SwitchToLadderPDM();
#endif
if (!string.IsNullOrEmpty(currentLadderAction.playAnimation))
{
if (debugMode)
{
LogLadderAction("Trigger Animation");
}
PlayAnimation(currentLadderAction.playAnimation, EnterAnimationTransitionDuration);
isUsingLadder = true;
tpInput.cc.disableAnimations = true;
tpInput.cc.StopCharacter();
}
}
protected virtual void UsingLadder()
{
if (!isUsingLadder)
{
return;
}
// update the base layer to know what animations are being played
tpInput.cc.AnimatorLayerControl();
tpInput.cc.ActionsControl();
// update camera movement
tpInput.CameraInput();
// go up or down
speed = verticalInput.GetAxis();
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
ladderInputSmooth = InEnterLadderAnimation()
? 0f
: Mathf.MoveTowards(ladderInputSmooth, speed, Time.deltaTime / LadderInputSmoothingDuration);
}
else
#endif
{
tpInput.cc.animator.SetFloat(vAnimatorParameters.InputVertical, speed, AnimatorInputDampTime, Time.deltaTime);
if (speed >= ClimbInputThreshold || speed <= -ClimbInputThreshold)
{
tpInput.cc.animator.speed = Mathf.Lerp(tpInput.cc.animator.speed, currentClimbSpeed, 2f * Time.deltaTime);
}
else
{
tpInput.cc.animator.speed = Mathf.Lerp(tpInput.cc.animator.speed, 1f, 2f * Time.deltaTime);
}
}
// increase speed by input and consume stamina
if (fastClimbInput.GetButton() && tpInput.cc.currentStamina > 0)
{
currentClimbSpeed = fastClimbSpeed;
StaminaConsumption();
}
else
{
currentClimbSpeed = climbSpeed;
}
// enter ladder behaviour
var _inEnterLadderAnimation = InEnterLadderAnimationMatchTarget();
if (_inEnterLadderAnimation)
{
inEnterLadderAnimation = true;
tpInput.cc.DisableGravityAndCollision(); // disable gravity & turn collision trigger
// disable ingame hud
if (currentLadderAction != null)
{
currentLadderAction.OnPlayerExit?.Invoke();
}
float normalizedTime = GetLadderNormalizedTime();
if (currentLadderAction.useTriggerRotation && currentLadderAction.matchTarget != null)
{
if (debugMode)
{
LogLadderAction("Rotating to target");
}
EvaluateToRotation(currentLadderAction.enterRotationCurve, currentLadderAction.matchTarget.transform.rotation, normalizedTime);
}
if (currentLadderAction.matchTarget != null)
{
if (transform.parent != currentLadderAction.targetCharacterParent)
{
transform.parent = currentLadderAction.targetCharacterParent;
}
if (debugMode)
{
LogLadderAction("Match target to enter");
}
EvaluateToPosition(currentLadderAction.enterPositionXZCurve, currentLadderAction.enterPositionYCurve, currentLadderAction.matchTarget.position, normalizedTime);
}
}
if (!_inEnterLadderAnimation && inEnterLadderAnimation)
{
enterLadderStarted = false;
inEnterLadderAnimation = false;
}
TriggerExitLadder();
}
protected virtual void TriggerExitLadder()
{
// exit ladder behaviour
inExitingLadderAnimation = InExitLadderAnimation();
if (inExitingLadderAnimation)
{
SetLadderAnimationSpeed(1f);
float normalizedTime = GetLadderNormalizedTime();
// A quick exit is taken mid-ladder, away from any trigger, so there is no
// currentLadderAction and no authored match target - the clip's own root motion is
// the whole movement. Only the trigger-driven exits match to a target.
if (currentLadderAction != null)
{
if (currentLadderAction.exitMatchTarget != null && !IsPlayingLadderState(QuickExitLadderState))
{
if (debugMode)
{
LogLadderAction("Match target to exit");
}
EvaluateToPosition(currentLadderAction.exitPositionXZCurve, currentLadderAction.exitPositionYCurve, currentLadderAction.exitMatchTarget.position, normalizedTime, true);
}
var newRot = new Vector3(0, GetLadderRootRotation().eulerAngles.y, 0);
EvaluateToRotation(currentLadderAction.exitRotationCurve, Quaternion.Euler(newRot), normalizedTime);
}
float resetTime = IsPlayingLadderState(QuickExitLadderState)
? QuickExitResetNormalizedTime
: ExitResetNormalizedTime;
if (normalizedTime >= resetTime)
{
// Hand the character back to physics exactly where the clip stops owning it: the
// LockMovement tag and the row's root-motion Y range both end at this same
// normalized time. Releasing earlier let gravity fight the clip's root motion for
// the body - the jitter - and releasing at a fixed wall-clock delay had no
// relation to where the clip actually was.
ResetPlayerSettings();
// The climb ran kinematic, so heightReached is still pinned to the top of the
// ladder. The fall that starts here is a new one, not a drop from that height -
// without this it reads as a long fall and triggers fall damage, LandHigh or the
// ragdoll.
tpInput.cc.ResetFallState();
}
}
}
protected virtual Vector3 GetLadderRootPosition()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return playablesAnimator.rootPosition;
}
#endif
return tpInput.cc.animator.rootPosition;
}
// Equivalent of Animator.rootRotation: the pending pose after this frame's root motion, applied
// by the script's own write to transform.rotation.
protected virtual Quaternion GetLadderRootRotation()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return playablesAnimator.rootRotation;
}
#endif
return tpInput.cc.animator.rootRotation;
}
protected virtual void EvaluateToPosition(AnimationCurve XZ, AnimationCurve Y, Vector3 targetPosition, float normalizedTime, bool isExit = false)
{
Vector3 rootPosition = GetLadderRootPosition();
float evaluatedXZ = XZ.Evaluate(normalizedTime);
float evaluatedY = Y.Evaluate(normalizedTime);
// Hold the XZ the root started at, but ONLY for the length of the cross-fade, and only when
// entering. During that window the state being left is still blended in and still feeding
// its own root motion - a full-speed run when the player sprints at the ladder - which the
// lerp below would otherwise carry straight into the wall the ladder is mounted on.
//
// The window is wall-clock, not curve-driven, because a curve reading ~0 means two
// opposite things here: early in EnterLadderBottom it means "the match has not taken over
// yet" (freeze is right), while in EnterLadderTop and in every exit it means "this stretch
// belongs to root motion" - those animations travel 2.45m down and off the ladder under
// their own steam, and freezing pins the character in place. Bounding the hold to the
// cross-fade covers the contaminated frames without ever reaching that authored motion.
//
// Both axes are held, unlike vGenericAction: vTriggerLadderAction has no
// useLocalX/useLocalZ, so the match always owns X and Z together.
if (!isExit)
{
if (!hasMatchOriginXZ)
{
matchOriginXZ = new Vector2(rootPosition.x, rootPosition.z);
matchOriginTime = Time.time;
hasMatchOriginXZ = true;
}
if (Time.time - matchOriginTime < EnterAnimationTransitionDuration)
{
rootPosition.x = matchOriginXZ.x;
rootPosition.z = matchOriginXZ.y;
}
}
if (evaluatedXZ < 1f)
{
rootPosition.x = Mathf.Lerp(rootPosition.x, targetPosition.x, evaluatedXZ);
rootPosition.z = Mathf.Lerp(rootPosition.z, targetPosition.z, evaluatedXZ);
}
if (evaluatedY < 1f)
{
rootPosition.y = Mathf.Lerp(rootPosition.y, targetPosition.y, evaluatedY);
}
transform.position = rootPosition;
}
protected virtual void EvaluateToRotation(AnimationCurve curve, Quaternion targetRotation, float normalizedTime)
{
Quaternion rootRotation = GetLadderRootRotation();
float evaluatedCurve = curve.Evaluate(normalizedTime);
if (evaluatedCurve < 1)
{
rootRotation = Quaternion.Lerp(rootRotation, targetRotation, evaluatedCurve);
}
transform.rotation = rootRotation;
}
protected virtual void StaminaConsumption()
{
if (tpInput.cc.currentStamina <= 0)
{
return;
}
tpInput.cc.ReduceStamina(fastClimbStamina, true); // call the ReduceStamina method from the player
tpInput.cc.currentStaminaRecoveryDelay = StaminaRecoveryDelay;
}
protected virtual void AddLadderTrigger(vTriggerLadderAction _ladderAction)
{
if (!_ladderAction)
{
return;
}
if (targetLadderAction != _ladderAction)
{
targetLadderAction = _ladderAction;
LogLadderAction("Trigger stay");
}
if (!actionTriggers.Contains(targetLadderAction))
{
actionTriggers.Add(targetLadderAction);
targetLadderAction.OnPlayerEnter?.Invoke();
}
}
protected virtual void RemoveLadderTrigger(vTriggerLadderAction _ladderAction)
{
if (_ladderAction == targetLadderAction)
{
targetLadderAction = null;
}
if (actionTriggers.Contains(_ladderAction))
{
actionTriggers.Remove(_ladderAction);
_ladderAction.OnPlayerExit?.Invoke();
}
}
protected virtual void CheckForTriggerAction(Collider other)
{
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
if (!_ladderAction)
{
return;
}
var dist = Vector3.Distance(transform.forward, _ladderAction.transform.forward);
if (isUsingLadder)
{
if (targetLadderAction != _ladderAction)
{
targetLadderAction = _ladderAction;
if (!actionTriggers.Contains(targetLadderAction))
{
actionTriggers.Add(targetLadderAction);
}
}
}
else if (!_ladderAction.activeFromForward || dist <= TriggerForwardDistanceThreshold)
{
AddLadderTrigger(_ladderAction);
OnEnterTriggerLadder?.Invoke();
}
else
{
RemoveLadderTrigger(_ladderAction);
}
}
public virtual void ResetPlayerSettings()
{
if (debugMode)
{
Debug.Log("Reset Player Settings", this);
}
speed = 0f;
#if INVECTOR_PLAYABLESANIMATOR
ladderInputSmooth = 0f;
#endif
ladderSide = vLadderSide.Left;
targetLadderAction = null;
isUsingLadder = false;
OnExitLadder?.Invoke();
triggerExitOnce = false;
triggerEnterOnce = false;
inEnterLadderAnimation = false;
enterLadderStarted = false;
hasMatchOriginXZ = false;
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 0);
tpInput.cc.EnableGravityAndCollision();
tpInput.SetLockAllInput(false);
tpInput.cc.StopCharacter();
tpInput.cc.disableAnimations = false;
#if INVECTOR_PLAYABLESANIMATOR
RestorePreviousPDMAfterGroundCheck();
#endif
if (transform.parent != null)
{
transform.parent = null;
}
}
#if INVECTOR_PLAYABLESANIMATOR
/// <summary>Temporarily assigns the optional ladder-only PDM and remembers the current assignment.</summary>
protected virtual void SwitchToLadderPDM()
{
if (!CanUseLadderPDM)
{
return;
}
if (restorePreviousPDMCoroutine != null)
{
StopCoroutine(restorePreviousPDMCoroutine);
restorePreviousPDMCoroutine = null;
}
if (ladderPDMIsActive)
{
return;
}
previousPDM = playablesAnimator.ActivePDM;
playablesAnimator.SetPDM(ladderPDM);
ladderPDMIsActive = true;
}
/// <summary>
/// Delays the base-PDM handoff until the controller has run its next fixed ground check after
/// gravity and collision are restored. A quick exit in open air therefore still selects Falling.
/// </summary>
protected virtual void RestorePreviousPDMAfterGroundCheck()
{
if (!ladderPDMIsActive || restorePreviousPDMCoroutine != null)
{
return;
}
restorePreviousPDMCoroutine = StartCoroutine(RestorePreviousPDMOnNextFixedUpdate());
}
protected virtual IEnumerator RestorePreviousPDMOnNextFixedUpdate()
{
yield return new WaitForFixedUpdate();
restorePreviousPDMCoroutine = null;
RestorePreviousPDM();
}
/// <summary>Restores the PDM that was active before this ladder interaction started.</summary>
protected virtual void RestorePreviousPDM()
{
if (!ladderPDMIsActive)
{
return;
}
// Do not overwrite a PDM selected by another system while the ladder was active.
if (UsingPlayables && playablesAnimator.ActivePDM == ladderPDM)
{
playablesAnimator.SetPDM(previousPDM);
}
previousPDM = null;
ladderPDMIsActive = false;
}
#endif
public override void OnActionStay(Collider other)
{
if (other.gameObject.CompareTag(actionTag))
{
CheckForTriggerAction(other);
}
}
public override void OnActionExit(Collider other)
{
if (other.gameObject.CompareTag(actionTag))
{
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
if (!_ladderAction)
{
return;
}
RemoveLadderTrigger(_ladderAction);
if (debugMode)
{
var parentName = other.transform.parent ? other.transform.parent.name : "<no parent>";
Debug.Log("Trigger Exit: " + other.name + "_" + parentName, other);
}
OnExitTriggerLadder?.Invoke();
}
}
private void LogLadderAction(string message)
{
if (!debugMode)
{
return;
}
var ladderAction = currentLadderAction ? currentLadderAction : targetLadderAction;
var actionName = ladderAction ? ladderAction.name : "<none>";
var parentName = ladderAction && ladderAction.transform.parent
? ladderAction.transform.parent.name
: "<no parent>";
Debug.Log(message + ": " + actionName + "_" + parentName, this);
}
}
}