PlayerLocomotion.cs
using Sandbox;
using System;

/// <summary>
/// OMR's player-movement coordinator.
///
/// This component owns OMR-specific sprint/crouch/slide resolution and also
/// implements IWeaponLocomotionProvider so reusable weapon code can consume
/// movement facts without depending on PlayerSprint, PlayerSlide or this class.
/// </summary>
public sealed class PlayerLocomotion : Component, IWeaponLocomotionProvider
{
	[Property]
	public PlayerController Controller { get; set; }

	[Property]
	public PlayerMovementInput MovementInput { get; set; }

	[Property]
	public PlayerSprint Sprint { get; set; }

	[Property]
	public PlayerCrouch Crouch { get; set; }

	[Property]
	public PlayerSlide Slide { get; set; }

	[Property]
	public BaseInventoryComponent Inventory { get; set; }

	private TimeSince _timeSinceJump;
	private bool _controlsWereEnabled;

	// Round-scoped movement rule. Standard rounds keep this at zero.
	private int _roundAdditionalAirJumps;
	private int _remainingRoundAirJumps;
	private bool _roundAirJumpWasGrounded;

	// Weapon-facing motion derivatives. These do not influence movement yet;
	// they simply make the locomotion contract ready for later gun-feel work.
	private bool _weaponMotionInitialized;
	private Vector3 _previousWeaponVelocity;
	private Vector3 _weaponAcceleration;
	private bool _weaponWasGrounded;
	private float _weaponLandingImpulse;

	/// <summary>
	/// Current weapon-facing locomotion snapshot. Values are built from resolved
	/// physical state, not raw input, so weapon mechanics see the same truth the
	/// movement system ultimately applied.
	/// </summary>
	public WeaponLocomotionSnapshot WeaponLocomotion =>
		BuildWeaponLocomotionSnapshot();

	protected override void OnStart()
	{
		Controller ??=
			Components.Get<PlayerController>();

		MovementInput ??=
			Components.Get<PlayerMovementInput>();

		Sprint ??=
			Components.Get<PlayerSprint>();

		Crouch ??=
			Components.Get<PlayerCrouch>();

		Slide ??=
			Components.Get<PlayerSlide>();

		Inventory ??=
			Components.Get<BaseInventoryComponent>();

		if ( Controller is null )
		{
			Log.Warning(
				"PlayerLocomotion could not find PlayerController."
			);

			return;
		}

		if ( MovementInput is null )
		{
			Log.Warning(
				"PlayerLocomotion could not find PlayerMovementInput."
			);

			return;
		}

		Controller.UseInputControls =
			false;

		_timeSinceJump =
			999f;

		InitializeWeaponMotionTracking();

		Log.Info(
			$"PlayerLocomotion active | Proxy: {IsProxy} | Owner: {Network.OwnerId}"
		);
	}

	protected override void OnFixedUpdate()
	{
		if ( Controller is not null )
		{
			UpdateWeaponMotionTracking();
		}

		if ( IsProxy )
			return;

		if (
			Controller is null ||
			MovementInput is null
		)
			return;

		Controller.UseInputControls =
			false;

		if ( !MovementInput.ControlsEnabled )
		{
			if ( _controlsWereEnabled )
			{
				ResetLocomotion();
			}

			_controlsWereEnabled =
				false;

			Controller.WishVelocity =
				Vector3.Zero;

			return;
		}

		_controlsWereEnabled =
			true;

		Vector3 moveInput =
			MovementInput.MoveInput;

		UpdateSprintIntent(
			moveInput
		);

		UpdateCrouchAndSlideIntent();

		ResolveStance(
			moveInput
		);

		ResolveSprint(
			moveInput
		);

		UpdateMovement(
			moveInput
		);

		UpdateRoundAirJumpRecharge();
		UpdateJump();
	}

	// =========================================================
	// INTENT
	// =========================================================

	private void UpdateSprintIntent(
		Vector3 moveInput
	)
	{
		IWeaponHandlingState activeWeapon =
			GetActiveWeaponHandling();

		if (
			Sprint is null ||
			!Sprint.Enabled
		)
		{
			activeWeapon?.SetSprintAimSuppressed(
				false
			);

			return;
		}

		bool pressed =
			MovementInput.ConsumeSprintPressed();

		bool released =
			MovementInput.ConsumeSprintReleased();

		Sprint.UpdateIntent(
			MovementInput.SprintDown,
			pressed,
			released,
			moveInput.x
		);

		// Fresh sprint input beats an already active ADS state. The weapon owns
		// how that temporary priority is represented internally.
		if (
			pressed &&
			activeWeapon is not null &&
			activeWeapon.IsAiming
		)
		{
			activeWeapon.SetSprintAimSuppressed(
				true
			);
		}

		// Once sprint intent is gone, remove the temporary ADS suppression. If
		// aim input is still held, the weapon can naturally return to ADS.
		if ( !Sprint.WantsSprint )
		{
			activeWeapon?.SetSprintAimSuppressed(
				false
			);
		}
	}

	private void UpdateCrouchAndSlideIntent()
	{
		if (
			Crouch is null ||
			!Crouch.Enabled
		)
			return;

		bool crouchPressed =
			MovementInput.ConsumeCrouchPressed();

		// Always update crouch intent. For Toggle this matters only on press;
		// for Hold it follows the button continuously.
		Crouch.UpdateIntent(
			MovementInput.CrouchDown,
			crouchPressed
		);

		if (
			Slide is null ||
			!Slide.Enabled
		)
			return;

		// Ground slide: crouch/slide pressed while expressing sprint intent.
		// Intent survives transitions, so WantsSprint is deliberately used.
		if (
			crouchPressed &&
			!Slide.IsSliding &&
			Controller.IsOnGround &&
			Sprint is not null &&
			Sprint.Enabled &&
			Sprint.WantsSprint &&
			Crouch.WantsCrouch
		)
		{
			Slide.TryStartSlide();
		}

		// Landing slide.
		if (
			crouchPressed &&
			Controller.IsAirborne
		)
		{
			if ( Crouch.WantsCrouch )
			{
				Slide.QueueLandingSlide();
			}
			else
			{
				Slide.CancelQueuedLandingSlide();
			}
		}

		// Toggle C a second time in the air and the player has withdrawn
		// crouch/landing-slide intent.
		if (
			Controller.IsAirborne &&
			!Crouch.WantsCrouch
		)
		{
			Slide.CancelQueuedLandingSlide();
		}
	}

	// =========================================================
	// STANCE RESOLUTION
	// =========================================================

	private void ResolveStance(
		Vector3 moveInput
	)
	{
		if (
			Crouch is null ||
			!Crouch.Enabled
		)
			return;

		bool sliding =
			Slide is not null &&
			Slide.Enabled &&
			Slide.IsSliding;

		IWeaponHandlingState activeWeapon =
			GetActiveWeaponHandling();

		bool weaponBlocksSprint =
			activeWeapon?.BlocksSprint == true;

		bool sprintIntent =
			Sprint is not null &&
			Sprint.Enabled &&
			Sprint.WantsSprint;

		bool movingForward =
			Sprint is not null
				? moveInput.x >=
					Sprint.ForwardInputThreshold
				: moveInput.x > 0.25f;

		// Slide always wins stance.
		bool forceCrouch =
			sliding;

		// Sprint only forces standing when the held item allows physical sprint.
		// ADS currently blocks that through IWeaponHandlingState, preserving the
		// existing crouch + ADS behavior without knowing the concrete weapon type.
		bool forceStand =
			!sliding &&
			!weaponBlocksSprint &&
			Controller.IsOnGround &&
			sprintIntent &&
			movingForward;

		if ( forceStand )
		{
			// The crouch press used for Slide should not unexpectedly return after
			// sprint eventually ends.
			Crouch.ClearCrouchIntent();
		}

		Crouch.ApplyResolvedStance(
			forceCrouch,
			forceStand
		);
	}

	// =========================================================
	// GAIT RESOLUTION
	// =========================================================

	private void ResolveSprint(
		Vector3 moveInput
	)
	{
		IWeaponHandlingState activeWeapon =
			GetActiveWeaponHandling();

		if (
			Sprint is null ||
			!Sprint.Enabled
		)
		{
			ApplyMovementSpeed(
				Controller.WalkSpeed,
				activeWeapon
			);

			return;
		}

		bool sliding =
			Slide is not null &&
			Slide.Enabled &&
			Slide.IsSliding;

		bool weaponBlocksSprint =
			activeWeapon?.BlocksSprint == true;

		bool movingForward =
			moveInput.x >=
			Sprint.ForwardInputThreshold;

		bool groundRequirementMet =
			!Sprint.RequireGrounded ||
			Controller.IsOnGround;

		bool validPhysicalMode =
			!Controller.IsSwimming &&
			!Controller.IsClimbing;

		bool physicallyAbleToSprint =
			Sprint.WantsSprint &&
			movingForward &&
			groundRequirementMet &&
			validPhysicalMode &&
			!sliding &&
			!weaponBlocksSprint &&
			!Controller.IsDucking;

		Sprint.SetPhysicalSprint(
			physicallyAbleToSprint
		);

		ApplyMovementSpeed(
			physicallyAbleToSprint
				? Sprint.SprintSpeed
				: Sprint.BaseMoveSpeed,
			activeWeapon
		);
	}

	private void ApplyMovementSpeed(
		float speed,
		IWeaponHandlingState activeWeapon
	)
	{
		float weaponScale =
			activeWeapon?.MovementSpeedScale ?? 1f;

		if ( activeWeapon?.IsAiming == true )
		{
			weaponScale *=
				activeWeapon.AimingMovementSpeedScale;
		}

		weaponScale =
			MathF.Max(
				weaponScale,
				0f
			);

		float resolvedSpeed =
			speed * weaponScale;

		Controller.WalkSpeed =
			resolvedSpeed;

		Controller.RunSpeed =
			resolvedSpeed;
	}

	// =========================================================
	// PHYSICAL MOVEMENT
	// =========================================================

	private void UpdateMovement(
		Vector3 moveInput
	)
	{
		if ( Controller.Mode is null )
		{
			Controller.WishVelocity =
				Vector3.Zero;

			return;
		}

		Rotation eyes =
			Controller.EyeAngles.ToRotation();

		Controller.WishVelocity =
			Controller.Mode.UpdateMove(
				eyes,
				moveInput
			);
	}

	// =========================================================
	// JUMP
	// =========================================================

	private void UpdateJump()
	{
		bool jumpPressed =
			MovementInput.ConsumeJumpPressed();

		if ( !jumpPressed )
			return;

		bool canGroundJump =
			Controller.TimeSinceGrounded <= 0.33f &&
			_timeSinceJump >= 0.5f;

		bool canRoundAirJump =
			!canGroundJump &&
			Controller.IsAirborne &&
			_remainingRoundAirJumps > 0 &&
			_timeSinceJump >= 0.15f;

		if ( !canGroundJump && !canRoundAirJump )
			return;

		float jumpSpeed =
			Controller.JumpSpeed;

		// Preserve OMR's reduced crouch jump only for the normal ground jump.
		// A round-granted air jump should remain consistent and readable.
		if (
			canGroundJump &&
			Crouch is not null &&
			Crouch.Enabled
		)
		{
			jumpSpeed =
				Crouch.GetJumpSpeed();
		}

		if ( jumpSpeed <= 0f )
			return;

		// Slide ends physically, but sprint INTENT remains.
		if (
			Slide is not null &&
			Slide.Enabled &&
			Slide.IsSliding
		)
		{
			Slide.EndForJump();
		}

		if ( canRoundAirJump )
		{
			_remainingRoundAirJumps =
				Math.Max(
					_remainingRoundAirJumps - 1,
					0
				);
		}

		_timeSinceJump =
			0f;

		Controller.Jump(
			Vector3.Up *
			jumpSpeed
		);

		// Once airborne we're not physically sprinting, but WantsSprint remains.
		Sprint?.SetPhysicalSprint(
			false
		);

		Controller.OnJumped();

		PlayerController.IEvents.PostToGameObject(
			GameObject,
			x => x.OnJumped()
		);
	}

	public void SetRoundAdditionalAirJumps(
		int additionalAirJumps
	)
	{
		_roundAdditionalAirJumps =
			Math.Clamp(
				additionalAirJumps,
				0,
				3
			);

		_remainingRoundAirJumps =
			_roundAdditionalAirJumps;

		_roundAirJumpWasGrounded =
			Controller?.IsOnGround == true;
	}

	private void UpdateRoundAirJumpRecharge()
	{
		if ( Controller is null )
			return;

		bool grounded =
			Controller.IsOnGround;

		if (
			grounded &&
			!_roundAirJumpWasGrounded
		)
		{
			_remainingRoundAirJumps =
				_roundAdditionalAirJumps;
		}

		_roundAirJumpWasGrounded =
			grounded;
	}

	// =========================================================
	// WEAPON CONTRACTS
	// =========================================================

	private IWeaponHandlingState GetActiveWeaponHandling()
	{
		Inventory ??=
			Components.Get<BaseInventoryComponent>();

		return
			Inventory?.ActiveItem
			as IWeaponHandlingState;
	}

	private WeaponLocomotionSnapshot BuildWeaponLocomotionSnapshot()
	{
		if ( Controller is null )
			return WeaponLocomotionSnapshot.Invalid;

		Vector3 velocity =
			Controller.Velocity;

		float horizontalSpeed =
			velocity
				.WithZ( 0f )
				.Length;

		float referenceSpeed =
			Sprint is not null &&
			Sprint.Enabled
				? MathF.Max( Sprint.BaseMoveSpeed, 1f )
				: MathF.Max( Controller.RunSpeed, 1f );

		bool sprinting =
			Sprint is not null &&
			Sprint.Enabled &&
			(
				Sprint.IsSprinting ||
				(
					Controller.WalkSpeed > referenceSpeed + 1f &&
					horizontalSpeed > 10f
				)
			);

		return new WeaponLocomotionSnapshot(
			isValid: true,
			isGrounded: Controller.IsOnGround,
			isAirborne: Controller.IsAirborne,
			isCrouched: Controller.IsDucking,
			isSliding:
				Slide is not null &&
				Slide.Enabled &&
				Slide.IsSliding,
			wantsSprint:
				Sprint is not null &&
				Sprint.Enabled &&
				Sprint.WantsSprint,
			isSprinting: sprinting,
			velocity: velocity,
			acceleration: _weaponAcceleration,
			referenceMoveSpeed: referenceSpeed,
			landingImpulse: _weaponLandingImpulse
		);
	}

	private void InitializeWeaponMotionTracking()
	{
		if ( Controller is null )
			return;

		_weaponMotionInitialized = true;
		_previousWeaponVelocity = Controller.Velocity;
		_weaponAcceleration = Vector3.Zero;
		_weaponWasGrounded = Controller.IsOnGround;
		_weaponLandingImpulse = 0f;
	}

	private void UpdateWeaponMotionTracking()
	{
		if ( Controller is null )
			return;

		if ( !_weaponMotionInitialized )
		{
			InitializeWeaponMotionTracking();
			return;
		}

		Vector3 velocity =
			Controller.Velocity;

		float dt =
			MathX.Clamp(
				Time.Delta,
				0.001f,
				0.1f
			);

		_weaponAcceleration =
			(velocity - _previousWeaponVelocity) /
			dt;

		bool grounded =
			Controller.IsOnGround;

		if (
			!_weaponWasGrounded &&
			grounded
		)
		{
			_weaponLandingImpulse =
				MathF.Max(
					-_previousWeaponVelocity.z,
					0f
				);
		}
		else
		{
			_weaponLandingImpulse =
				MathX.ExponentialDecay(
					_weaponLandingImpulse,
					0f,
					0.08f,
					dt
				);
		}

		_previousWeaponVelocity = velocity;
		_weaponWasGrounded = grounded;
	}

	// =========================================================
	// RESET
	// =========================================================

	private void ResetLocomotion()
	{
		Sprint?.ResetSprint();

		Slide?.ResetSlide();

		Crouch?.ResetToStanding();

		GetActiveWeaponHandling()?.SetSprintAimSuppressed(
			false
		);

		Controller.WishVelocity =
			Vector3.Zero;

		_remainingRoundAirJumps =
			_roundAdditionalAirJumps;

		_roundAirJumpWasGrounded =
			Controller.IsOnGround;
	}
}