Code/PlayerControllerPlus/PlayerControllerPlus.DefaultControls.cs
using System;

namespace Sandbox;

public sealed partial class PlayerControllerPlus : Component
{
	/// <summary>
	/// The direction we're looking in input space.
	/// </summary>
	[Sync( SyncFlags.Interpolate )]
	public Angles EyeAngles { get; set; }

	/// <summary>
	/// The player's eye position, in first person mode
	/// </summary>
	public Vector3 EyePosition => EyeTransform.Position;

	/// <summary>
	/// The player's eye transform, in first person mode
	/// </summary>
	public Transform EyeTransform { get; private set; }

	/// <summary>
	/// True if this player is ducking
	/// </summary>
	[Sync]
	public bool IsDucking { get; set; }

	/// <summary>
	/// The distance from the top of the head to the closest ceiling.
	/// </summary>
	public float Headroom { get; set; }


	Vector3 _proxyLastPosition;

	protected override void OnUpdate()
	{
		UpdateGroundEyeRotation();

		if ( Scene.IsEditor )
			return;

		if ( IsProxy && ExtendedFeaturesEnabled )
		{
			var pos = WorldPosition;
			Velocity = (pos - _proxyLastPosition) / Time.Delta;
			_proxyLastPosition = pos;
		}

		UpdateEyeTransform();

		if ( !IsProxy )
		{
			using var scope = InputPlayerIndex >= 0 ? Input.PlayerScope( InputPlayerIndex ) : null;

			GameObject.RunEvent<IEvents>( x => x.PreInput() );

			if ( UseLookControls && !(ExtendedFeaturesEnabled && DisableLookInput) )
			{
				UpdateEyeAngles();
				UpdateLookAt();
			}

			if ( UseCameraControls )
			{
				UpdateCameraPosition();
			}

			UpdateEyeTransform();
		}

		UpdateBodyVisibility();

		if ( UseAnimatorControls && Renderer.IsValid() )
		{
			UpdateAnimation( Renderer );
		}

		if ( ExtendedFeaturesEnabled && ShowStats && !IsProxy )
		{
			DrawStats();
		}
	}

	void DrawStats()
	{
		var pos = WorldPosition + Vector3.Up * (CurrentHeight + 10);
		var vel = Velocity;

		var state = GetMoveStateName();
		var grounded = IsOnGround ? "Grounded" : "Airborne";

		DebugOverlay.Text( pos, $"Vel X: {vel.x:F1}  Y: {vel.y:F1}\n{state}  |  {grounded}", size: 14, duration: 0, overlay: true );
	}

	string GetMoveStateName()
	{
		if ( Mode is Sandbox.MovementPlus.ExternalMoveModeAdapter adapter && adapter.External.IsValid() )
			return adapter.External.GetType().Name;

		if ( Mode is Sandbox.MovementPlus.NoclipMoveModePlus ) return "Noclip";
		if ( Mode is Sandbox.MovementPlus.SitMoveModePlus ) return "Sit";
		if ( Mode is Sandbox.MovementPlus.MoveModeSwimPlus ) return "Swim";
		if ( Mode is Sandbox.MovementPlus.MoveModeLadderPlus ) return "Ladder";

		if ( Mode is Sandbox.MovementPlus.MoveModeWalkPlus )
		{
			if ( IsDucking ) return "Crouch";
			if ( EnableSlowWalk && Input.Down( SlowWalkButton ) ) return "Slow Walk";

			bool run = Input.Down( AltMoveButton );
			if ( RunByDefault ) run = !run;
			return run ? "Run" : "Walk";
		}

		return Mode?.GetType().Name ?? "None";
	}

	protected override void OnFixedUpdate()
	{
		if ( Scene.IsEditor ) return;

		UpdateModeFixedUpdates();
		UpdateHeadroom();
		UpdateFalling();

		prevPosition = WorldPosition;

		if ( IsProxy ) return;
		if ( !UseInputControls ) return;

		using var fixedScope = InputPlayerIndex >= 0 ? Input.PlayerScope( InputPlayerIndex ) : null;

		InputMove();
		UpdateDucking( Input.Down( "duck" ) );
		InputJump();

		UpdateEyeTransform();
	}

	void UpdateHeadroom()
	{
		var tr = TraceBody( WorldPosition + Vector3.Up * CurrentHeight * 0.5f, WorldPosition + Vector3.Up * (100 + CurrentHeight * 0.5f), 0.75f, 0.5f );
		Headroom = tr.Distance;
	}

	bool _wasFalling = false;
	float fallDistance = 0;
	Vector3 prevPosition;
	internal Vector3? _landingVelocity;

	void UpdateFalling()
	{
		if ( Mode is null || !Mode.AllowFalling )
		{
			_wasFalling = false;
			fallDistance = 0;
			return;
		}

		if ( !IsOnGround || _wasFalling )
		{
			var fallDelta = WorldPosition - prevPosition;
			if ( fallDelta.z < 0.0f )
			{
				_wasFalling = true;
				fallDistance -= fallDelta.z;
			}
		}

		if ( IsOnGround )
		{
			if ( _wasFalling && fallDistance > 1.0f )
			{
				if ( ExtendedFeaturesEnabled && PreserveVelocityOnLanding )
				{
					_landingVelocity = Velocity.WithZ( 0 );
				}

				IEvents.PostToGameObject( GameObject, x => x.OnLanded( fallDistance, Velocity ) );

				// play land sounds
				if ( EnableFootstepSounds )
				{
					var volume = Velocity.Length.Remap( 50, 800, 0.5f, 5 );
					var vel = Velocity.Length;

					PlayFootstepSound( WorldPosition, volume, 0 );
					PlayFootstepSound( WorldPosition, volume, 1 );
				}
			}

			_wasFalling = false;
			fallDistance = 0;
		}
	}

	Transform localGroundTransform;
	int groundHash;

	void UpdateGroundEyeRotation()
	{
		if ( GroundObject is null )
		{
			groundHash = default;
			return;
		}

		if ( !RotateWithGround )
		{
			groundHash = default;
			return;
		}

		var hash = HashCode.Combine( GroundObject );

		// Get out transform locally to the ground object
		var localTransform = GroundObject.WorldTransform.ToLocal( WorldTransform );

		// Work out the rotation delta chance since last frame
		var delta = localTransform.Rotation.Inverse * localGroundTransform.Rotation;

		// we only care about the yaw
		var deltaYaw = delta.Angles().yaw;

		//DebugDrawSystem.Current.Text( WorldPosition, $"{delta.Angles().yaw}" );

		// If we're on the same ground and we've rotated
		if ( hash == groundHash && deltaYaw != 0 )
		{
			// rotate the eye angles
			EyeAngles = EyeAngles.WithYaw( EyeAngles.yaw + deltaYaw );

			// rotate the body to avoid it animating to the new position
			if ( UseAnimatorControls && Renderer.IsValid() )
			{
				Renderer.WorldRotation *= new Angles( 0, deltaYaw, 0 );
			}
		}

		// Keep for next frame
		groundHash = hash;
		localGroundTransform = localTransform;
	}



}