PlayerState.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

public enum PreMatchPlayerState
{
	Lobby,
	Warmup,
	Ready
}

public sealed class PlayerState : Component
{
	[Property, Sync( SyncFlags.FromHost )]
	public int TeamId { get; set; } = -1;

	[Property, Sync( SyncFlags.FromHost )]
	public bool IsAlive { get; set; } = true;

	[Property, Sync( SyncFlags.FromHost )]
	public bool IsReady { get; set; } = false;

	/// <summary>
	/// Individual state while both players are connected to the
	/// online pre-match lobby.
	///
	/// This is deliberately separate from IsReady. IsReady means
	/// the networked PlayerPrefab has initialized; PreMatchState
	/// means what that player is currently doing in the lobby.
	/// </summary>
	[Property, Sync( SyncFlags.FromHost )]
	public PreMatchPlayerState PreMatchState { get; set; } =
		PreMatchPlayerState.Lobby;

	/// <summary>
	/// Player-local warmup weapon choice, using RoundManager's
	/// SandboxWeapons catalog as the shared weapon list.
	/// </summary>
	[Property, Sync( SyncFlags.FromHost )]
	public int WarmupWeaponIndex { get; set; } = 0;

	// =========================================================
	// PLAYER IDENTITY / BOT FOUNDATION
	// =========================================================

	/// <summary>
	/// True when this player prefab is controlled by OMR bot logic instead of a
	/// human Connection. Bots remain normal player prefabs for health, inventory,
	/// locomotion, round rules and replication.
	/// </summary>
	[Property, Sync( SyncFlags.FromHost )]
	public bool IsBot { get; set; } = false;

	/// <summary>
	/// Network-visible identity used instead of the host connection name for a
	/// host-controlled bot. Human players continue to resolve their real display
	/// name from the owning connection.
	/// </summary>
	[Property, Sync( SyncFlags.FromHost )]
	public string BotDisplayName { get; set; } = "ROOK";

	public bool IsLocalHuman =>
		!IsProxy &&
		!IsBot;

	// Compatibility name retained for existing gameplay/UI callers. A host-owned
	// bot is not the local human even though the engine correctly reports it as
	// non-proxy on the host.
	public bool IsLocalPlayer =>
		IsLocalHuman;

	public string DisplayName
	{
		get
		{
			if ( IsBot )
			{
				return string.IsNullOrWhiteSpace( BotDisplayName )
					? "BOT"
					: BotDisplayName;
			}

			Connection owner =
				GameObject?.Network.Owner;

			if (
				owner is not null &&
				!string.IsNullOrWhiteSpace( owner.DisplayName )
			)
			{
				return owner.DisplayName;
			}

			return TeamId == 0
				? "PLAYER A"
				: TeamId == 1
					? "PLAYER B"
					: "PLAYER";
		}
	}

	// =========================================================
	// ROUND SPAWN STABILIZATION
	// =========================================================

	/// <summary>
	/// Short owner-side settling window after a round teleport. During this
	/// time controls are already locked by RoundManager, so OMR can safely
	/// drain stale MoveMode smoothing and clear residual rigidbody/ground
	/// velocity before the countdown releases the player.
	/// </summary>
	[Property, Group( "Round Spawn Stabilization" ), Range( 0f, 0.5f )]
	public float SpawnStabilizationDuration { get; set; } = 0.20f;

	/// <summary>
	/// Horizontal drift smaller than this is left alone so normal grounding
	/// can settle vertically without OMR fighting the PlayerController.
	/// </summary>
	[Property, Group( "Round Spawn Stabilization" ), Range( 0f, 4f )]
	public float SpawnHorizontalDriftTolerance { get; set; } = 0.5f;

	/// <summary>
	/// Temporary diagnostics for the intermittent round-spawn launch bug.
	/// Disable after the spawn path has been validated in longer sessions.
	/// </summary>
	[Property, Group( "Round Spawn Stabilization" )]
	public bool LogSpawnStabilization { get; set; } = true;

	private bool _spawnStabilizationActive;
	private Vector3 _spawnStabilizationPosition;
	private float _spawnStabilizationRemaining;
	private int _spawnStabilizationTicks;
	private int _spawnStabilizationCorrections;
	private float _spawnMaxHorizontalDrift;
	private float _spawnMaxBodySpeed;
	private float _spawnMaxGroundSpeed;
	private float _spawnMaxSmoothedMoveSpeed;


	// =========================================================
	// MODULAR ROUND RULE LOCAL CONTROL
	// =========================================================

	private bool _roundRuleLocalActive;
	private OMRRoundRuleProfile _roundRuleLocalProfile;
	private OneMoreRoundWeapon _roundRuleWeapon;
	private readonly HashSet<string> _roundRuleWeaponModifierIds =
		new( StringComparer.OrdinalIgnoreCase );
	private float _defaultGravityScale = 1f;
	private bool _defaultGravityScaleCaptured;


	// =========================================================
	// HIGH NOON LOCAL CONTROL
	// =========================================================

	private bool _highNoonLocalActive;
	private bool _highNoonDrawSequenceActive;
	private bool _highNoonTriggerReported;
	private int _highNoonRoundNumber = -1;
	private float _highNoonDrawAt;
	private Vector3 _highNoonLookTarget;
	private Angles _highNoonLockedEyeAngles;
	private OneMoreRoundWeapon _highNoonWeapon;
	private bool _highNoonWeaponRulesConfigured;

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		if ( _roundRuleLocalActive )
		{
			ApplyRoundRuleGravity();
			EnsureRoundRuleWeaponModifiers();
		}

		if ( _highNoonLocalActive )
		{
			ApplyHighNoonLookLock();
			EnsureHighNoonWeaponRules();
		}
	}

	protected override void OnStart()
	{
		Log.Info(
			$"PlayerState started | Owner: {Network.OwnerId} | LocalHuman: {IsLocalHuman} | Bot:{IsBot}"
		);

		if ( IsProxy )
			return;

		// A player avatar belongs to exactly one connection. If that
		// connection leaves the current lobby, destroy the network object
		// instead of orphaning it locally. This prevents a stale local
		// player/viewmodel from surviving a disconnect and overlapping the
		// replacement player spawned by NetworkHelper in a new session.
		GameObject.RootNetwork.SetOrphanedMode(
			NetworkOrphaned.Destroy
		);

		CaptureDefaultGravityScale();

		ApplyMenuControls(
			true
		);

		ReportReady();
	}

	protected override void OnFixedUpdate()
	{
		if (
			IsProxy ||
			!_spawnStabilizationActive
		)
			return;

		var movementInput =
			Components.Get<PlayerMovementInput>();

		// Safety first: this stabilizer exists only inside the round lock.
		// Never keep clamping a player after gameplay controls are released.
		if (
			movementInput is not null &&
			movementInput.ControlsEnabled
		)
		{
			FinishSpawnStabilization(
				"CONTROLS ENABLED"
			);

			return;
		}

		var controller =
			Components.Get<PlayerController>();

		if ( controller is null )
		{
			FinishSpawnStabilization(
				"NO CONTROLLER"
			);

			return;
		}

		StabilizeRoundSpawnTick(
			controller
		);
	}

	[Rpc.Host]
	private void ReportReady()
	{
		if ( !Networking.IsHost )
			return;

		var health =
			Components.Get<PlayerHealth>();

		if ( health is not null )
		{
			health.ResetHealth();
		}

		IsAlive =
			true;

		IsReady =
			true;

		PreMatchState =
			PreMatchPlayerState.Lobby;

		Log.Info(
			$"PLAYER READY | Owner: {Network.OwnerId} | Team: {TeamId}"
		);
	}

	// =========================================================
	// ROUND SPAWN / OWNER-SIDE RESET
	// =========================================================

	/// <summary>
	/// Moves the owning player to the round spawn and resets
	/// owner-local transient movement/weapon state.
	///
	/// Health, reload cancellation and ammunition are reset by
	/// RoundManager on the authoritative host.
	/// </summary>
	[Rpc.Owner]
	public void MoveToRoundSpawn(
		Vector3 position,
		Vector3 lookTarget
	)
	{
		var movementInput =
			Components.Get<PlayerMovementInput>();

		movementInput?.SetControlsEnabled(
			false
		);

		var sprint =
			Components.Get<PlayerSprint>();

		var slide =
			Components.Get<PlayerSlide>();

		var crouch =
			Components.Get<PlayerCrouch>();

		sprint?.ResetSprint();
		slide?.ResetSlide();

		if (
			crouch is not null &&
			crouch.Enabled
		)
		{
			crouch.ResetToStanding();
		}

		ResetLocalWeaponState();

		Components.Get<PlayerCombatFeedback>()?.ResetTransient();

		WorldPosition =
			position;

		Network.ClearInterpolation();

		var controller =
			Components.Get<PlayerController>();

		if ( controller is null )
			return;

		Vector3 previousWishVelocity =
			controller.WishVelocity;

		Vector3 previousGroundVelocity =
			controller.GroundVelocity;

		Vector3 previousBodyVelocity =
			controller.Body is not null
				? controller.Body.Velocity
				: Vector3.Zero;

		ZeroControllerMotion(
			controller
		);

		Vector3 lookDirection =
			(
				lookTarget -
				position
			)
			.WithZ(
				0f
			);

		if (
			!lookDirection.IsNearlyZero(
				0.001f
			)
		)
		{
			Angles facingAngles =
				Rotation.LookAt(
					lookDirection.Normal,
					Vector3.Up
				)
				.Angles();

			controller.EyeAngles =
				new Angles(
					0f,
					facingAngles.yaw,
					0f
				);
		}

		BeginSpawnStabilization(
			controller,
			position,
			previousWishVelocity,
			previousGroundVelocity,
			previousBodyVelocity
		);
	}

	private void BeginSpawnStabilization(
		PlayerController controller,
		Vector3 position,
		Vector3 previousWishVelocity,
		Vector3 previousGroundVelocity,
		Vector3 previousBodyVelocity
	)
	{
		_spawnStabilizationPosition =
			position;

		_spawnStabilizationRemaining =
			MathF.Max(
				SpawnStabilizationDuration,
				0f
			);

		_spawnStabilizationTicks =
			0;

		_spawnStabilizationCorrections =
			0;

		_spawnMaxHorizontalDrift =
			0f;

		_spawnMaxBodySpeed =
			previousBodyVelocity.Length;

		_spawnMaxGroundSpeed =
			previousGroundVelocity.Length;

		_spawnMaxSmoothedMoveSpeed =
			0f;

		_spawnStabilizationActive =
			_spawnStabilizationRemaining > 0f;

		if ( LogSpawnStabilization )
		{
			Log.Info(
				$"SPAWN STABILIZE BEGIN | Team:{TeamId} | Target:{position} | Duration:{_spawnStabilizationRemaining:0.###} | WishBefore:{previousWishVelocity} | GroundBefore:{previousGroundVelocity} | BodyBefore:{previousBodyVelocity} | Mode:{controller.Mode?.GetType().Name ?? "NONE"}"
			);
		}
	}

	private void StabilizeRoundSpawnTick(
		PlayerController controller
	)
	{
		_spawnStabilizationTicks++;

		Vector3 positionBefore =
			WorldPosition;

		Vector3 drift =
			positionBefore -
			_spawnStabilizationPosition;

		Vector3 horizontalDrift =
			drift.WithZ(
				0f
			);

		float horizontalDistance =
			horizontalDrift.Length;

		float bodySpeed =
			controller.Body is not null
				? controller.Body.Velocity.Length
				: 0f;

		float groundSpeed =
			controller.GroundVelocity.Length;

		if (
			horizontalDistance >
			_spawnMaxHorizontalDrift
		)
		{
			_spawnMaxHorizontalDrift =
				horizontalDistance;
		}

		if ( bodySpeed > _spawnMaxBodySpeed )
		{
			_spawnMaxBodySpeed =
				bodySpeed;
		}

		if ( groundSpeed > _spawnMaxGroundSpeed )
		{
			_spawnMaxGroundSpeed =
				groundSpeed;
		}

		// MoveMode keeps its own private movement smoothing. OMR normally stops
		// calling UpdateMove while round controls are locked, which means the
		// last pre-reset movement vector can survive until the fight is enabled
		// again. Feed zero input during the settling window to drain that state,
		// but deliberately discard the returned velocity.
		float smoothedMoveSpeed =
			0f;

		if ( controller.Mode is not null )
		{
			Vector3 smoothedMove =
				controller.Mode.UpdateMove(
					controller.EyeAngles.ToRotation(),
					Vector3.Zero
				);

			smoothedMoveSpeed =
				smoothedMove.WithZ( 0f ).Length;

			if (
				smoothedMoveSpeed >
				_spawnMaxSmoothedMoveSpeed
			)
			{
				_spawnMaxSmoothedMoveSpeed =
					smoothedMoveSpeed;
			}
		}

		ZeroControllerMotion(
			controller
		);

		bool corrected =
			horizontalDistance >
			MathF.Max(
				SpawnHorizontalDriftTolerance,
				0f
			);

		if ( corrected )
		{
			// Preserve vertical settling. PlayerController owns grounding and may
			// legitimately move a fraction vertically while its capsule settles.
			WorldPosition =
				positionBefore -
				horizontalDrift;

			Network.ClearInterpolation();

			_spawnStabilizationCorrections++;

			if ( LogSpawnStabilization )
			{
				Log.Warning(
					$"SPAWN STABILIZE CORRECT | Team:{TeamId} | Tick:{_spawnStabilizationTicks} | HorizontalDrift:{horizontalDistance:0.###} | VerticalDrift:{drift.z:0.###} | BodyVel:{bodySpeed:0.###} | GroundVel:{groundSpeed:0.###} | SmoothedMove:{smoothedMoveSpeed:0.###} | Mode:{controller.Mode?.GetType().Name ?? "NONE"}"
				);
			}
		}

		_spawnStabilizationRemaining -=
			Time.Delta;

		if ( _spawnStabilizationRemaining <= 0f )
		{
			FinishSpawnStabilization(
				"SETTLED"
			);
		}
	}

	private static void ZeroControllerMotion(
		PlayerController controller
	)
	{
		if ( controller is null )
			return;

		controller.WishVelocity =
			Vector3.Zero;

		controller.GroundVelocity =
			Vector3.Zero;

		if ( controller.Body is null )
			return;

		controller.Body.Velocity =
			Vector3.Zero;

		controller.Body.AngularVelocity =
			Vector3.Zero;

		controller.Body.ClearForces();
	}

	private void FinishSpawnStabilization(
		string reason
	)
	{
		if ( !_spawnStabilizationActive )
			return;

		_spawnStabilizationActive =
			false;

		if ( !LogSpawnStabilization )
			return;

		var controller =
			Components.Get<PlayerController>();

		Vector3 finalDrift =
			WorldPosition -
			_spawnStabilizationPosition;

		float finalBodySpeed =
			controller is not null &&
			controller.Body is not null
				? controller.Body.Velocity.Length
				: 0f;

		Log.Info(
			$"SPAWN STABILIZE END | Team:{TeamId} | Reason:{reason} | Ticks:{_spawnStabilizationTicks} | Corrections:{_spawnStabilizationCorrections} | MaxHorizontalDrift:{_spawnMaxHorizontalDrift:0.###} | MaxBodySpeed:{_spawnMaxBodySpeed:0.###} | MaxGroundSpeed:{_spawnMaxGroundSpeed:0.###} | MaxSmoothedMove:{_spawnMaxSmoothedMoveSpeed:0.###} | FinalHorizontalDrift:{finalDrift.WithZ( 0f ).Length:0.###} | FinalVerticalDrift:{finalDrift.z:0.###} | FinalBodySpeed:{finalBodySpeed:0.###}"
		);
	}

	/// <summary>
	/// Resets only owner-local custom weapon state.
	///
	/// We deliberately do not call BaseCombatWeapon.CreateViewModel
	/// or DestroyViewModel here: those methods are not public from
	/// unrelated components in the current engine API.
	/// </summary>
	private void ResetLocalWeaponState()
	{
		BaseInventoryComponent inventory =
			Components.Get<BaseInventoryComponent>();

		if ( inventory is null )
			return;

		if (
			inventory.ActiveItem is
			OneMoreRoundWeapon weapon
		)
		{
			weapon.ResetForRoundPresentation();
		}
	}


	// =========================================================
	// MODULAR ROUND RULE RUNTIME
	// =========================================================

	/// <summary>
	/// Applies owner-simulated pieces of one authored round rule. Gravity and
	/// weapon stat modifiers must exist on the peer that actually simulates the
	/// PlayerController/weapon input; the host separately applies authoritative
	/// damage rules and its local weapon stat snapshot.
	/// </summary>
	[Rpc.Owner]
	public void ApplyRoundRule(
		RoundModifier modifier
	)
	{
		ClearRoundRuleWeaponModifiers();

		_roundRuleLocalActive = true;
		_roundRuleLocalProfile =
			FindRoundRuleDirector()?.
				ResolveRule( modifier );

		CaptureDefaultGravityScale();
		ApplyRoundRuleGravity();
		ApplyRoundRuleMovement();
		EnsureRoundRuleWeaponModifiers();
	}

	[Rpc.Owner]
	public void ClearRoundRuleState()
	{
		ClearRoundRuleLocalState();
	}

	private void CaptureDefaultGravityScale()
	{
		if ( _defaultGravityScaleCaptured )
			return;

		PlayerController controller =
			Components.Get<PlayerController>();

		if (
			controller?.Body is null
		)
			return;

		_defaultGravityScale =
			MathF.Max(
				controller.Body.GravityScale,
				0.01f
			);

		_defaultGravityScaleCaptured = true;
	}

	private void ApplyRoundRuleGravity()
	{
		PlayerController controller =
			Components.Get<PlayerController>();

		if ( controller?.Body is null )
			return;

		float requestedScale =
			_roundRuleLocalProfile is not null &&
			_roundRuleLocalProfile.IsValid
				? _roundRuleLocalProfile.GravityScale
				: _defaultGravityScale;

		requestedScale =
			MathX.Clamp(
				requestedScale,
				0.05f,
				3f
			);

		if (
			MathF.Abs(
				controller.Body.GravityScale -
				requestedScale
			) > 0.001f
		)
		{
			controller.Body.GravityScale =
				requestedScale;
		}
	}

	private void ApplyRoundRuleMovement()
	{
		PlayerLocomotion locomotion =
			Components.Get<PlayerLocomotion>();

		int additionalAirJumps =
			_roundRuleLocalProfile is not null &&
			_roundRuleLocalProfile.IsValid
				? Math.Clamp(
					_roundRuleLocalProfile.AdditionalAirJumps,
					0,
					3
				)
				: 0;

		locomotion?.SetRoundAdditionalAirJumps(
			additionalAirJumps
		);
	}

	private void EnsureRoundRuleWeaponModifiers()
	{
		BaseInventoryComponent inventory =
			Components.Get<BaseInventoryComponent>();

		OneMoreRoundWeapon activeWeapon =
			inventory?.ActiveItem as
				OneMoreRoundWeapon;

		if ( _roundRuleWeapon != activeWeapon )
		{
			ClearRoundRuleWeaponModifiers();
			_roundRuleWeapon = activeWeapon;
		}

		if ( activeWeapon is not null )
		{
			bool allowAim =
				_roundRuleLocalProfile is null ||
				!_roundRuleLocalProfile.IsValid ||
				_roundRuleLocalProfile.AllowAim;

			activeWeapon.SetRoundAimAllowed(
				allowAim
			);
		}

		if (
			activeWeapon is null ||
			_roundRuleLocalProfile is null ||
			!_roundRuleLocalProfile.IsValid ||
			_roundRuleLocalProfile.WeaponModifiers is null
		)
			return;

		foreach (
			OMRWeaponModifierDefinition modifier in
				_roundRuleLocalProfile.WeaponModifiers
		)
		{
			if (
				modifier is null ||
				!modifier.IsValid
			)
				continue;

			string id =
				modifier.ModifierId?.Trim() ??
				string.Empty;

			if (
				string.IsNullOrWhiteSpace( id ) ||
				_roundRuleWeaponModifierIds.Contains( id )
			)
				continue;

			if ( activeWeapon.SetStatModifier( modifier ) )
			{
				_roundRuleWeaponModifierIds.Add( id );
			}
		}
	}

	private void ClearRoundRuleWeaponModifiers()
	{
		if (
			_roundRuleWeapon is not null &&
			_roundRuleWeapon.IsValid
		)
		{
			_roundRuleWeapon.SetRoundAimAllowed(
				true
			);

			foreach ( string id in _roundRuleWeaponModifierIds )
			{
				_roundRuleWeapon.RemoveStatModifier( id );
			}
		}

		_roundRuleWeaponModifierIds.Clear();
		_roundRuleWeapon = null;
	}

	private void ClearRoundRuleLocalState()
	{
		ClearRoundRuleWeaponModifiers();

		_roundRuleLocalActive = false;
		_roundRuleLocalProfile = null;

		Components
			.Get<PlayerLocomotion>()?
			.SetRoundAdditionalAirJumps(
				0
			);

		CaptureDefaultGravityScale();

		PlayerController controller =
			Components.Get<PlayerController>();

		if ( controller?.Body is not null )
		{
			controller.Body.GravityScale =
				_defaultGravityScaleCaptured
					? _defaultGravityScale
					: 1f;
		}
	}

	private OMRRoundRuleDirector FindRoundRuleDirector()
	{
		return Scene?
			.GetAllComponents<OMRRoundRuleDirector>()
			.FirstOrDefault();
	}

	// =========================================================
	// HIGH NOON CONTROL STATE
	// =========================================================

	/// <summary>
	/// Locks movement, look and weapon use during the normal round countdown.
	/// The owner keeps this entirely local because its PlayerController and
	/// inventory input are owner-simulated.
	/// </summary>
	[Rpc.Owner]
	public void LockHighNoonCountdown(
		Vector3 lookTarget
	)
	{
		ApplyHighNoonLock(
			lookTarget,
			drawSequenceActive: false,
			drawAt: 0f,
			roundNumber: -1
		);
	}

	/// <summary>
	/// Begins the latency-aware High Noon draw sequence.
	///
	/// Weapon input is pumped immediately so its local gate can observe trigger
	/// release state, but CanPrimaryAttack remains blocked until the synchronized
	/// drawAt timestamp and requires a fresh post-DRAW press.
	/// </summary>
	[Rpc.Owner]
	public void BeginHighNoonDrawSequence(
		Vector3 lookTarget,
		float drawAt,
		int roundNumber
	)
	{
		ApplyHighNoonLock(
			lookTarget,
			drawSequenceActive: true,
			drawAt: drawAt,
			roundNumber: roundNumber
		);
	}

	[Rpc.Owner]
	public void ClearHighNoonRoundState()
	{
		ClearHighNoonLocalState(
			restoreLook: true
		);
	}

	private void ApplyHighNoonLock(
		Vector3 lookTarget,
		bool drawSequenceActive,
		float drawAt,
		int roundNumber
	)
	{
		_highNoonLocalActive = true;
		_highNoonDrawSequenceActive = drawSequenceActive;
		_highNoonTriggerReported = false;
		_highNoonRoundNumber = roundNumber;
		_highNoonDrawAt = drawAt;
		_highNoonLookTarget = lookTarget;
		_highNoonWeaponRulesConfigured = false;

		var controller =
			Components.Get<PlayerController>();

		if ( controller is not null )
		{
			Vector3 lookDirection =
				(
					lookTarget -
						WorldPosition
				)
				.WithZ(
					0f
				);

			if (
				!lookDirection.IsNearlyZero(
					0.001f
				)
			)
			{
				Angles facingAngles =
					Rotation.LookAt(
						lookDirection.Normal,
						Vector3.Up
					)
					.Angles();

				_highNoonLockedEyeAngles =
					new Angles(
						0f,
						facingAngles.yaw,
						0f
					);
			}
			else
			{
				_highNoonLockedEyeAngles =
					controller.EyeAngles;
			}

			controller.UseLookControls = false;
			controller.UseInputControls = false;

			ZeroControllerMotion(
				controller
			);

			controller.EyeAngles =
				_highNoonLockedEyeAngles;
		}

		var movementInput =
			Components.Get<PlayerMovementInput>();

		movementInput?.
			SetControlsEnabled(
				false
			);

		var inventory =
			Components.Get<BaseInventoryComponent>();

		if ( inventory is not null )
		{
			// During the countdown the whole weapon input path remains paused.
			// Once the draw sequence begins we pump the weapon so its temporary
			// fire gate can track trigger release and open locally at drawAt.
			inventory.ManualPumping =
				!drawSequenceActive;
		}

		EnsureHighNoonWeaponRules();
	}

	private void ApplyHighNoonLookLock()
	{
		var controller =
			Components.Get<PlayerController>();

		if ( controller is null )
			return;

		controller.UseLookControls = false;
		controller.UseInputControls = false;
		controller.WishVelocity = Vector3.Zero;

		if ( controller.Body is not null )
		{
			controller.Body.Velocity = Vector3.Zero;
			controller.Body.AngularVelocity = Vector3.Zero;
			controller.Body.ClearForces();
		}

		controller.EyeAngles =
			_highNoonLockedEyeAngles;
	}

	private void EnsureHighNoonWeaponRules()
	{
		var inventory =
			Components.Get<BaseInventoryComponent>();

		OneMoreRoundWeapon activeWeapon =
			inventory?.ActiveItem as
				OneMoreRoundWeapon;

		if (
			_highNoonWeapon is not null &&
			_highNoonWeapon != activeWeapon
		)
		{
			_highNoonWeapon.ClearTemporaryRoundRules();
			_highNoonWeapon = null;
			_highNoonWeaponRulesConfigured = false;
		}

		if ( activeWeapon is null )
			return;

		_highNoonWeapon =
			activeWeapon;

		if ( _highNoonWeaponRulesConfigured )
			return;

		activeWeapon.SetTemporarySpreadOverride(
			Vector2.Zero
		);

		if ( _highNoonDrawSequenceActive )
		{
			activeWeapon.ConfigureTemporaryPrimaryGate(
				_highNoonDrawAt,
				requireRelease: true
			);
		}

		_highNoonWeaponRulesConfigured = true;
	}

	private void ClearHighNoonLocalState(
		bool restoreLook
	)
	{
		_highNoonWeapon?.
			ClearTemporaryRoundRules();

		_highNoonWeapon = null;
		_highNoonWeaponRulesConfigured = false;
		_highNoonLocalActive = false;
		_highNoonDrawSequenceActive = false;
		_highNoonTriggerReported = false;
		_highNoonRoundNumber = -1;
		_highNoonDrawAt = 0f;
		_highNoonLookTarget = Vector3.Zero;

		if ( !restoreLook )
			return;

		var controller =
			Components.Get<PlayerController>();

		if ( controller is not null )
		{
			controller.UseLookControls = true;
		}
	}

	/// <summary>
	/// Called by OneMoreRoundWeapon only after a valid PrimaryAttack actually
	/// commits. Time.Now is synchronized across peers, so this timestamp can be
	/// compared on the host without using RPC arrival order as the reaction time.
	/// </summary>
	public void NotifyRoundPrimaryShotFired(
		float firedAt
	)
	{
		if (
			!_highNoonLocalActive ||
			!_highNoonDrawSequenceActive ||
			_highNoonTriggerReported ||
			_highNoonRoundNumber < 0
		)
			return;

		if (
			firedAt + 0.001f <
			_highNoonDrawAt
		)
			return;

		_highNoonTriggerReported = true;

		RoundManager roundManager =
			FindRoundManager();

		if ( Networking.IsHost )
		{
			roundManager?.
				HandleHighNoonTrigger(
					this,
					_highNoonRoundNumber,
					firedAt
				);

			return;
		}

		ReportHighNoonTriggerHost(
			_highNoonRoundNumber,
			firedAt
		);
	}

	[Rpc.Host]
	private void ReportHighNoonTriggerHost(
		int roundNumber,
		float firedAt
	)
	{
		if ( !Networking.IsHost )
			return;

		// The RPC lives on the owned player object. Reject a caller trying to
		// submit reaction data through somebody else's PlayerState.
		if (
			Rpc.CallerId !=
			Network.OwnerId
		)
			return;

		FindRoundManager()?.
			HandleHighNoonTrigger(
				this,
				roundNumber,
				firedAt
			);
	}

	private RoundManager FindRoundManager()
	{
		return Scene?
			.GetAllComponents<RoundManager>()
			.FirstOrDefault();
	}

	// =========================================================
	// CONTROL STATE
	// =========================================================

	[Rpc.Owner]
	public void SetRoundControls(
		bool enabled
	)
	{
		ApplyRoundControls(
			enabled
		);
	}

	[Rpc.Owner]
	public void SetMenuControls(
		bool menuOpen
	)
	{
		ApplyMenuControls(
			menuOpen
		);
	}

	private void ApplyMenuControls(
		bool menuOpen
	)
	{
		if ( menuOpen )
		{
			ClearHighNoonLocalState(
				restoreLook: false
			);

			ClearRoundRuleLocalState();
		}

		var controller =
			Components.Get<PlayerController>();

		if ( controller is not null )
		{
			controller.UseLookControls =
				!menuOpen;

			//
			// Built-in movement input stays disabled.
			//
			controller.UseInputControls =
				false;
		}

		if ( menuOpen )
		{
			ApplyRoundControls(
				false
			);
		}
	}

	private void ApplyRoundControls(
		bool enabled
	)
	{
		if ( _highNoonLocalActive )
		{
			ClearHighNoonLocalState(
				restoreLook: true
			);
		}

		if (
			enabled &&
			_spawnStabilizationActive
		)
		{
			FinishSpawnStabilization(
				"ROUND CONTROLS ENABLED"
			);
		}

		var controller =
			Components.Get<PlayerController>();

		var movementInput =
			Components.Get<PlayerMovementInput>();

		var inventory =
			Components.Get<BaseInventoryComponent>();

		if ( controller is not null )
		{
			controller.UseInputControls =
				false;
		}

		if (
			movementInput is not null
		)
		{
			movementInput.SetControlsEnabled(
				enabled
			);
		}

		if ( !enabled )
		{
			if ( controller is not null )
			{
				controller.WishVelocity =
					Vector3.Zero;

				if (
					controller.Body is not null
				)
				{
					controller.Body.Velocity =
						Vector3.Zero;

					controller.Body.AngularVelocity =
						Vector3.Zero;

					controller.Body.ClearForces();
				}
			}
		}

		if ( inventory is not null )
		{
			inventory.ManualPumping =
				!enabled;
		}
	}
}