PlayerSlide.cs
using Sandbox;
using Sandbox.Movement;
using System;
public sealed class PlayerSlide :
MoveModeWalk,
PlayerController.IEvents
{
[Property]
public float MinimumStartSpeed { get; set; } = 210f;
[Property]
public float InitialBoost { get; set; } = 30f;
[Property]
public float MinimumSlideSpeed { get; set; } = 150f;
[Property]
public float SlideDeceleration { get; set; } = 220f;
[Property]
public float MaximumDuration { get; set; } = 0.85f;
[Property]
public float SteeringStrength { get; set; } = 2.25f;
[Property]
public bool MaintainSlideMomentum { get; set; } = true;
[Property]
public float MomentumRetention { get; set; } = 0.88f;
[Property]
public float SlideGroundSnapDistance { get; set; } = 0f;
[Property]
public float AirborneEndDelay { get; set; } = 0.15f;
[Property]
public float HardStopCheckDelay { get; set; } = 0.25f;
[Property]
public float HardStopSpeed { get; set; } = 25f;
[Property]
public bool AllowLandingSlides { get; set; } = true;
[Property]
public float MinimumLandingSlideSpeed { get; set; } = 240f;
[Property]
public float LandingSlideBoost { get; set; } = 30f;
[Property, Group( "Audio" ), Description( "One-shot positional sound played when a slide begins. The owning player broadcasts it so nearby players hear the same action." )]
public SoundEvent SlideSound { get; set; }
[Property, Group( "Audio" ), Range( 0f, 2f )]
public float SlideSoundVolume { get; set; } = 0.22f;
public bool IsSliding { get; private set; }
public bool LandingSlideQueued { get; private set; }
public float CurrentSlideSpeed =>
_slideSpeed;
public override bool AllowGrounding =>
true;
public override bool AllowFalling =>
true;
private Vector3 _slideDirection;
private float _slideSpeed;
private float _slideTime;
private Vector3 _slideStartPosition;
protected override void OnStart()
{
base.OnStart();
if ( Priority <= 0 )
{
Priority =
100;
}
Log.Info(
$"PlayerSlide active | Proxy: {IsProxy} | Owner: {Network.OwnerId}"
);
}
/// <summary>
/// Locomotion decides whether Slide is contextually valid.
///
/// Slide itself only validates the physical requirements.
/// </summary>
public bool TryStartSlide()
{
if ( IsProxy )
return false;
if ( IsSliding )
return false;
var controller =
Controller;
if ( controller is null )
return false;
if ( !controller.IsOnGround )
return false;
if (
controller.IsSwimming ||
controller.IsClimbing
)
return false;
Vector3 horizontalVelocity =
controller.Velocity.WithZ(
0f
);
return BeginSlide(
horizontalVelocity,
MinimumStartSpeed,
InitialBoost,
"GROUND"
);
}
public bool QueueLandingSlide()
{
if ( IsProxy )
return false;
if ( !AllowLandingSlides )
return false;
if ( IsSliding )
return false;
var controller =
Controller;
if ( controller is null )
return false;
if ( !controller.IsAirborne )
return false;
float horizontalSpeed =
controller.Velocity
.WithZ( 0f )
.Length;
if (
horizontalSpeed <
MinimumLandingSlideSpeed
)
{
LandingSlideQueued =
false;
return false;
}
if ( !LandingSlideQueued )
{
Log.Info(
$"LANDING SLIDE QUEUED | Speed: {horizontalSpeed:0.0}"
);
}
LandingSlideQueued =
true;
return true;
}
public void CancelQueuedLandingSlide()
{
LandingSlideQueued =
false;
}
public void OnLanded(
float distance,
Vector3 impactVelocity
)
{
if ( IsProxy )
return;
if ( !LandingSlideQueued )
return;
if ( IsSliding )
return;
LandingSlideQueued =
false;
var controller =
Controller;
if ( controller is null )
return;
if (
controller.IsSwimming ||
controller.IsClimbing
)
return;
Vector3 impactHorizontal =
impactVelocity.WithZ(
0f
);
Vector3 currentHorizontal =
controller.Velocity.WithZ(
0f
);
Vector3 sourceVelocity =
impactHorizontal.Length >=
currentHorizontal.Length
? impactHorizontal
: currentHorizontal;
BeginSlide(
sourceVelocity,
MinimumLandingSlideSpeed,
LandingSlideBoost,
"LANDING"
);
}
private bool BeginSlide(
Vector3 horizontalVelocity,
float minimumSpeed,
float boost,
string source
)
{
float currentSpeed =
horizontalVelocity.Length;
if (
currentSpeed <
minimumSpeed
)
return false;
if (
horizontalVelocity.IsNearlyZero(
0.1f
)
)
return false;
_slideDirection =
horizontalVelocity.Normal;
_slideSpeed =
currentSpeed +
boost;
_slideTime =
0f;
_slideStartPosition =
WorldPosition;
IsSliding =
true;
LandingSlideQueued =
false;
//
// Slide physically requires the compact collider.
//
// This does NOT modify PlayerCrouch's user intent.
//
Controller?.UpdateDucking(
true
);
PlaySlideStartSound();
Log.Info(
$"SLIDE START ({source}) | Speed: {_slideSpeed:0.0}"
);
return true;
}
public override int Score(
PlayerController controller
)
{
return IsSliding
? Priority
: -1000;
}
public override void OnModeBegin()
{
base.OnModeBegin();
var controller =
Controller;
if ( controller is null )
return;
controller.UpdateDucking(
true
);
ApplySlideVelocity(
controller
);
}
public override Vector3 UpdateMove(
Rotation eyes,
Vector3 input
)
{
if ( !IsSliding )
return Vector3.Zero;
ApplySteering(
eyes,
input
);
return
_slideDirection *
_slideSpeed;
}
public override void UpdateRigidBody(
Rigidbody body
)
{
body.Gravity =
true;
body.LinearDamping =
0.15f;
body.AngularDamping =
1f;
}
public override void PrePhysicsStep()
{
base.PrePhysicsStep();
if ( !IsSliding )
return;
var controller =
Controller;
if ( controller is null )
{
EndSlide(
"NO CONTROLLER"
);
return;
}
if (
controller.IsSwimming ||
controller.IsClimbing
)
{
EndSlide(
"INVALID MOVEMENT MODE"
);
return;
}
_slideTime +=
Time.Delta;
_slideSpeed =
MathF.Max(
0f,
_slideSpeed -
SlideDeceleration *
Time.Delta
);
if ( MaintainSlideMomentum )
{
MaintainMomentum(
controller
);
}
if (
_slideTime >=
MaximumDuration
)
{
EndSlide(
"DURATION"
);
return;
}
if (
_slideSpeed <=
MinimumSlideSpeed
)
{
EndSlide(
"LOW SPEED"
);
return;
}
if (
controller.IsAirborne &&
controller.TimeSinceGrounded >
AirborneEndDelay
)
{
EndSlide(
"AIRBORNE"
);
return;
}
}
public override void PostPhysicsStep()
{
if ( !IsSliding )
{
base.PostPhysicsStep();
return;
}
//
// Do NOT use MoveModeWalk's full StepDownHeight
// grounding while sliding.
//
// Keeping this at zero allows ramp launches.
//
if (
SlideGroundSnapDistance >
0f
)
{
StickToGround(
SlideGroundSnapDistance
);
}
if (
_slideTime <
HardStopCheckDelay
)
return;
var controller =
Controller;
if ( controller is null )
return;
float actualSpeed =
controller.Velocity
.WithZ( 0f )
.Length;
if (
actualSpeed <
HardStopSpeed
)
{
EndSlide(
"HARD STOP"
);
}
}
public override void OnModeEnd(
MoveMode next
)
{
base.OnModeEnd(
next
);
if ( IsSliding )
{
EndSlide(
"MODE OVERRIDE"
);
}
}
public void EndForJump()
{
if ( !IsSliding )
return;
EndSlide(
"JUMP"
);
}
public void ResetSlide()
{
IsSliding =
false;
LandingSlideQueued =
false;
_slideSpeed =
0f;
_slideTime =
0f;
}
private void MaintainMomentum(
PlayerController controller
)
{
if ( controller.Body is null )
return;
Vector3 bodyVelocity =
controller.Body.Velocity;
Vector3 horizontalVelocity =
bodyVelocity.WithZ(
0f
);
float desiredMinimum =
_slideSpeed *
MomentumRetention;
if (
horizontalVelocity.Length >=
desiredMinimum
)
return;
controller.Body.Velocity =
_slideDirection *
_slideSpeed +
Vector3.Up *
bodyVelocity.z;
}
private void ApplySlideVelocity(
PlayerController controller
)
{
if ( controller.Body is null )
return;
float verticalVelocity =
controller.Body.Velocity.z;
controller.Body.Velocity =
_slideDirection *
_slideSpeed +
Vector3.Up *
verticalVelocity;
}
private void ApplySteering(
Rotation eyes,
Vector3 input
)
{
Vector3 desiredDirection =
(
eyes *
input.ClampLength(
1f
)
)
.WithZ(
0f
);
if (
desiredDirection.IsNearlyZero(
0.1f
)
)
return;
desiredDirection =
desiredDirection.Normal;
float steeringAmount =
MathX.Clamp(
SteeringStrength *
Time.Delta,
0f,
1f
);
Vector3 newDirection =
_slideDirection +
(
desiredDirection -
_slideDirection
) *
steeringAmount;
if (
!newDirection.IsNearlyZero(
0.01f
)
)
{
_slideDirection =
newDirection.Normal;
}
}
private void PlaySlideStartSound()
{
if ( SlideSound is null )
return;
if ( Networking.IsActive )
{
BroadcastSlideStartSound();
return;
}
PlaySlideStartSoundLocal();
}
[Rpc.Broadcast( NetFlags.Unreliable | NetFlags.OwnerOnly )]
private void BroadcastSlideStartSound()
{
PlaySlideStartSoundLocal();
}
private void PlaySlideStartSoundLocal()
{
if ( SlideSound is null )
return;
SoundHandle handle =
GameObject.PlaySound( SlideSound );
handle.Volume *=
MathF.Max( SlideSoundVolume, 0f );
}
private void EndSlide(
string reason
)
{
if ( !IsSliding )
return;
float distance =
_slideStartPosition.Distance(
WorldPosition
);
float actualSpeed =
Controller is not null
? Controller.Velocity
.WithZ( 0f )
.Length
: 0f;
IsSliding =
false;
Log.Info(
$"SLIDE END | {reason} | Time: {_slideTime:0.00}s | Distance: {distance:0.0} | Speed: {actualSpeed:0.0}"
);
}
}