PlayerSprint.cs
using Sandbox;
public enum SprintInputBehavior
{
Hold,
Toggle,
Hybrid
}
public sealed class PlayerSprint : Component
{
[Property]
public PlayerController Controller { get; set; }
[Property]
public SprintInputBehavior Behavior { get; set; } =
SprintInputBehavior.Hybrid;
[Property]
public float HybridHoldThreshold { get; set; } = 0.22f;
[Property]
public float BaseMoveSpeed { get; set; } = 250f;
[Property]
public float SprintSpeed { get; set; } = 340f;
[Property]
public float ForwardInputThreshold { get; set; } = 0.25f;
[Property]
public float StopForwardInputThreshold { get; set; } = 0.05f;
[Property]
public bool RequireGrounded { get; set; } = true;
[Property]
public bool CancelToggleWhenForwardReleased { get; set; } = true;
// =========================================================
// INTENT
// =========================================================
/// <summary>
/// The player currently wants to sprint.
///
/// This is INTENT, not physical locomotion state.
///
/// Jumping, sliding and crouching do not clear it.
/// </summary>
public bool WantsSprint { get; private set; }
public bool IsSprintButtonHeld { get; private set; }
public bool PersistentSprintToggle =>
_persistentSprintToggle;
// =========================================================
// PHYSICAL RESULT
// =========================================================
/// <summary>
/// The locomotion resolver sets this.
///
/// WantsSprint may be true while IsSprinting is false,
/// for example:
///
/// - airborne
/// - sliding
/// - blocked under a ceiling
/// </summary>
public bool IsSprinting { get; private set; }
// =========================================================
// INTERNAL HYBRID STATE
// =========================================================
private bool _persistentSprintToggle;
private bool _pressInProgress;
private float _heldTime;
private bool _holdCommitted;
private bool _pressStartedWithToggle;
protected override void OnStart()
{
Controller ??=
Components.Get<PlayerController>();
ResetSprint();
Log.Info(
$"PlayerSprint active | Proxy: {IsProxy} | Owner: {Network.OwnerId}"
);
}
/// <summary>
/// Called by PlayerLocomotion.
///
/// Nothing except raw player input changes sprint intent.
/// </summary>
public void UpdateIntent(
bool buttonDown,
bool buttonPressed,
bool buttonReleased,
float forwardInput
)
{
if ( IsProxy )
return;
IsSprintButtonHeld =
buttonDown;
switch ( Behavior )
{
case SprintInputBehavior.Hold:
{
_persistentSprintToggle =
false;
break;
}
case SprintInputBehavior.Toggle:
{
if ( buttonPressed )
{
_persistentSprintToggle =
!_persistentSprintToggle;
}
break;
}
case SprintInputBehavior.Hybrid:
{
UpdateHybridInput(
buttonDown,
buttonPressed,
buttonReleased
);
break;
}
}
//
// Releasing forward movement is itself player input,
// so it is allowed to clear a persistent sprint toggle.
//
if (
CancelToggleWhenForwardReleased &&
forwardInput <=
StopForwardInputThreshold
)
{
_persistentSprintToggle =
false;
}
bool sprintInputActive =
Behavior switch
{
SprintInputBehavior.Hold =>
buttonDown,
SprintInputBehavior.Toggle =>
_persistentSprintToggle,
SprintInputBehavior.Hybrid =>
buttonDown ||
_persistentSprintToggle,
_ =>
false
};
bool movingForward =
forwardInput >=
ForwardInputThreshold;
WantsSprint =
sprintInputActive &&
movingForward;
}
private void UpdateHybridInput(
bool buttonDown,
bool buttonPressed,
bool buttonReleased
)
{
if ( buttonPressed )
{
_pressInProgress =
true;
_heldTime =
0f;
_holdCommitted =
false;
_pressStartedWithToggle =
_persistentSprintToggle;
}
if (
buttonDown &&
_pressInProgress
)
{
_heldTime +=
Time.Delta;
if (
_heldTime >=
HybridHoldThreshold
)
{
_holdCommitted =
true;
}
}
if ( buttonReleased )
{
if (
_pressInProgress &&
!_holdCommitted
)
{
//
// Short tap toggles persistent sprint.
//
_persistentSprintToggle =
!_pressStartedWithToggle;
}
//
// Long holds do not modify the existing latch.
//
ResetPressTracking();
}
}
public void SetPhysicalSprint(
bool active
)
{
IsSprinting =
active;
}
/// <summary>
/// Hard reset for menu / round resets.
///
/// This is deliberately NOT used by movement transitions.
/// </summary>
public void ResetSprint()
{
WantsSprint =
false;
IsSprinting =
false;
IsSprintButtonHeld =
false;
_persistentSprintToggle =
false;
ResetPressTracking();
}
private void ResetPressTracking()
{
_pressInProgress =
false;
_heldTime =
0f;
_holdCommitted =
false;
_pressStartedWithToggle =
false;
}
// =========================================================
// LEGACY COMPATIBILITY
// =========================================================
//
// These remain temporarily so another old component does
// not fail compilation while we migrate.
//
// They no longer alter player intent.
//
public void CancelForSlide()
{
IsSprinting =
false;
}
public void CancelForCrouch()
{
IsSprinting =
false;
}
public void RequestSprintAfterSlide()
{
//
// Intentionally empty.
//
// There is nothing to request anymore.
// WantsSprint already represents current intent.
//
}
public void CancelSprintRequest()
{
ResetSprint();
}
public bool ResumeSprintAfterSlideRequested =>
false;
}