PlayerCrouch.cs
using Sandbox;
public enum CrouchBehavior
{
Toggle,
Hold
}
public sealed class PlayerCrouch : Component
{
[Property]
public PlayerController Controller { get; set; }
[Property]
public CrouchBehavior Behavior { get; set; } =
CrouchBehavior.Toggle;
[Property]
public float StandingJumpSpeed { get; set; } = 280f;
[Property]
public float CrouchedJumpSpeed { get; set; } = 220f;
/// <summary>
/// When false, the physical duck/stand collider state is frozen while airborne.
/// Crouch input is still tracked so a standing player can queue a landing slide
/// without shrinking the collider in mid-air. Sliding itself may still force the
/// compact collider for ramp launches and other brief airborne slide states.
/// </summary>
[Property]
public bool AllowAirborneStanceChanges { get; set; } = false;
/// <summary>
/// The crouch stance requested by the player.
///
/// Slide can force physical crouch without changing this
/// independently.
/// </summary>
public bool WantsCrouch { get; private set; }
public bool IsCrouchButtonHeld { get; private set; }
protected override void OnStart()
{
Controller ??=
Components.Get<PlayerController>();
if ( Controller is null )
{
Log.Warning(
"PlayerCrouch could not find PlayerController."
);
return;
}
WantsCrouch =
false;
IsCrouchButtonHeld =
false;
Log.Info(
$"PlayerCrouch active | Proxy: {IsProxy} | Owner: {Network.OwnerId}"
);
}
/// <summary>
/// Updates only player crouch intent.
/// </summary>
public void UpdateIntent(
bool buttonDown,
bool buttonPressed
)
{
if ( IsProxy )
return;
IsCrouchButtonHeld =
buttonDown;
switch ( Behavior )
{
case CrouchBehavior.Toggle:
{
if ( buttonPressed )
{
WantsCrouch =
!WantsCrouch;
}
break;
}
case CrouchBehavior.Hold:
{
WantsCrouch =
buttonDown;
break;
}
}
}
/// <summary>
/// Apply the stance chosen by PlayerLocomotion.
///
/// forceCrouch is used by Slide.
/// forceStand is used when active sprint intent should
/// return us to a standing gait.
///
/// PlayerController still handles actual headroom.
/// </summary>
public void ApplyResolvedStance(
bool forceCrouch,
bool forceStand
)
{
if ( IsProxy )
return;
if ( Controller is null )
return;
bool resolvedCrouch;
if ( forceCrouch )
{
resolvedCrouch =
true;
}
else if ( forceStand )
{
resolvedCrouch =
false;
}
else
{
resolvedCrouch =
WantsCrouch;
}
// Mid-air collider resizing can produce a visible vertical pop because the
// controller has to reconcile a different body height while unsupported.
// Freeze the physical stance for the duration of the jump, but keep
// WantsCrouch untouched so landing-slide intent survives until OnLanded.
if (
!AllowAirborneStanceChanges &&
!forceCrouch &&
Controller.IsAirborne
)
{
resolvedCrouch =
Controller.IsDucking;
}
Controller.UpdateDucking(
resolvedCrouch
);
}
/// <summary>
/// Once continuing sprint has won the stance decision,
/// the crouch used to enter a slide should not silently
/// return later when sprint stops.
/// </summary>
public void ClearCrouchIntent()
{
WantsCrouch =
false;
}
public float GetJumpSpeed()
{
if (
Controller is not null &&
Controller.IsDucking &&
Controller.IsOnGround
)
{
return CrouchedJumpSpeed;
}
return StandingJumpSpeed;
}
public void ResetToStanding()
{
WantsCrouch =
false;
IsCrouchButtonHeld =
false;
if (
Controller is not null &&
!IsProxy
)
{
Controller.UpdateDucking(
false
);
}
}
// =========================================================
// TEMPORARY COMPATIBILITY
// =========================================================
public void SetCrouchRequested(
bool crouched
)
{
WantsCrouch =
crouched;
}
public void RequestCrouch()
{
WantsCrouch =
true;
}
public void RequestStanding()
{
WantsCrouch =
false;
}
public void CancelCrouchForSprint()
{
WantsCrouch =
false;
}
}