UI/GameHUD.razor
@using System;
@using System.Globalization;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>

	@if ( ShowDeveloperPreview )
	{
		<div class="hud-preview-toolbar">
			<div class="hud-preview-toolbar-title">HUD PREVIEW</div>
			<div class="hud-preview-toolbar-buttons">
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.Combat ? "active" : "")" onclick=@PreviewCombat>COMBAT</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.SuddenDeath ? "active" : "")" onclick=@PreviewSuddenDeath>SUDDEN DEATH</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.WinnerDecision ? "active" : "")" onclick=@PreviewWinnerDecision>WINNER DECISION</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.ModifierDecision ? "active" : "")" onclick=@PreviewModifierDecision>MODIFIER DECISION</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.MatchEnd ? "active" : "")" onclick=@PreviewMatchEnd>MATCH END</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.Sandbox ? "active" : "")" onclick=@PreviewSandbox>SANDBOX</div>
				<div class="hud-preview-state @(DeveloperPreviewMode == OMRHUDPreviewMode.Warmup ? "active" : "")" onclick=@PreviewWarmup>WARMUP</div>
			</div>
		</div>

		<OMRHUDPreview Mode=@(DeveloperPreviewMode) />
	}
	else if ( RoundManager is not null )
	{
		@if (
			RoundManager.Mode ==
				SessionMode.Menu &&
			LocalPlayerState?.
				PreMatchState ==
					PreMatchPlayerState.Warmup
		)
		{
			<div class="combat-health sandbox-health">

				<div class="health-side local">
					<div class="health-heading">
						<label class="health-label">@PlayerDisplayName( LocalTeamId )</label>
						<label class="health-number">@LocalHealthText</label>
					</div>

					<div class="health-track">
						<div class="health-track-chrome"></div>
						<div
							class="health-fill"
							style="width: @(LocalHealthPercent * 100f)%"
						></div>
					</div>
				</div>

			</div>

			<div class="sandbox-label warmup-label">
				<div class="sandbox-label-chrome"></div>
				<label class="sandbox-title">WARMUP</label>
				<label class="sandbox-status">@LocalWeaponName</label>
				<label class="sandbox-hint">ESC - RETURN TO LOBBY</label>
			</div>
		}

		@if (
			RoundManager.Mode ==
				SessionMode.Sandbox
		)
		{
			@if ( LocalTeamId >= 0 )
			{
				<div class="combat-health sandbox-health">

					<div class="health-side local">
						<div class="health-heading">
							<label class="health-label">@PlayerDisplayName( LocalTeamId )</label>
							<label class="health-number">@LocalHealthText</label>
						</div>

						<div class="health-track">
						<div class="health-track-chrome"></div>
							<div
								class="health-fill"
								style="width: @(LocalHealthPercent * 100f)%"
							></div>
						</div>
					</div>

					<div class="health-side opponent">
						<div class="health-heading">
							<label class="health-label">@PlayerDisplayName( OpponentTeamId )</label>
							<label class="health-number">@OpponentHealthText</label>
						</div>

						<div class="health-track">
						<div class="health-track-chrome"></div>
							<div
								class="health-fill"
								style="width: @(OpponentHealthPercent * 100f)%"
							></div>
						</div>
					</div>

				</div>
			}

			<div class="sandbox-label">
				<div class="sandbox-label-chrome"></div>
				<label class="sandbox-title">SANDBOX</label>
				<label class="sandbox-status">
					TEAM @TeamName( LocalTeamId ) · @ConnectedPlayerCount PLAYER@(ConnectedPlayerCount == 1 ? "" : "S")
				</label>
				<label class="sandbox-hint">ESC - TEST MENU</label>
			</div>

			@if ( OMRClientPerformance.BotDebugOverlay && SandboxBot is not null )
			{
				<div class="bot-debug-panel">
					<div class="bot-debug-title">BOT DEBUG · @SandboxBotDisplayName</div>
					<div class="bot-debug-line">@SandboxBotDifficulty · @SandboxBotIntent</div>
					<div class="bot-debug-line">@SandboxBotInitiative</div>
					<div class="bot-debug-line">@SandboxBotPerception</div>
					<div class="bot-debug-line">@SandboxBotMemory</div>
					<div class="bot-debug-line">@SandboxBotContext</div>
					<div class="bot-debug-line">@SandboxBotModifier</div>
					<div class="bot-debug-line">@SandboxBotAdaptation</div>
					<div class="bot-debug-line">@SandboxBotSpatialMemory</div>
					<div class="bot-debug-line">@SandboxBotAwareness</div>
					<div class="bot-debug-line">@SandboxBotThreat</div>
					<div class="bot-debug-line">@SandboxBotHearing</div>
					<div class="bot-debug-line">@SandboxBotTactical</div>
					<div class="bot-debug-line">@SandboxBotAdvancedTactics</div>
					<div class="bot-debug-line">@SandboxBotUtilityBrain</div>
					<div class="bot-debug-line">@SandboxBotCombatSequence</div>
					<div class="bot-debug-line">@SandboxBotHumanization</div>
					<div class="bot-debug-line">PERF: @SandboxBot.PerformanceStatus</div>
					<div class="bot-debug-line">@SandboxBotMatchup</div>
					<div class="bot-debug-line">@SandboxBotPrediction</div>
					<div class="bot-debug-line">@SandboxBotBelief</div>
					<div class="bot-debug-line">@SandboxBotTacticalSpace</div>
					<div class="bot-debug-line">@SandboxBotNavigation</div>
					<div class="bot-debug-line">@SandboxBotMovement</div>
					<div class="bot-debug-line">@SandboxBotCombat</div>
					<div class="bot-debug-line">@SandboxBotWeapon · @SandboxBotLifeState</div>
				</div>
			}
		}

		@if (
			RoundManager.Mode ==
				SessionMode.Match
		)
		{
			@if (
				LocalTeamId >= 0
			)
			{
				<div class="combat-health">

					<div class="health-side local">

						<div class="health-heading">
							<label class="health-label">@PlayerDisplayName( LocalTeamId )</label>
							<label class="health-number">@LocalHealthText</label>
						</div>

						<div class="health-track">
						<div class="health-track-chrome"></div>
							<div
								class="health-fill"
								style="width: @(LocalHealthPercent * 100f)%"
							></div>
						</div>

					</div>

					<div class="health-side opponent">

						<div class="health-heading">
							<label class="health-label">@PlayerDisplayName( OpponentTeamId )</label>
							<label class="health-number">@OpponentHealthText</label>
						</div>

						<div class="health-track">
						<div class="health-track-chrome"></div>
							<div
								class="health-fill"
								style="width: @(OpponentHealthPercent * 100f)%"
							></div>
						</div>

					</div>

				</div>
			}

			<div class="match-header">

				<label class="round">
					ROUND @RoundManager.RoundNumber
				</label>

				@if (
					RoundManager.Phase ==
						RoundPhase.Fighting
				)
				{
					@if ( RoundManager.IsSuddenDeath )
					{
						<div class="round-timer sudden">
							<div class="round-timer-chrome"></div>
							<label class="timer-mode">SUDDEN DEATH</label>
							<label class="timer-number">@CombatSeconds</label>
							<label class="timer-note">ONE SHOT</label>
						</div>
					}
					else
					{
						<div class="round-timer">
							<div class="round-timer-chrome"></div>
							<label class="timer-number">@CombatSeconds</label>
						</div>
					}
				}

				<label class="score-label">
					BANKED · FIRST TO @RoundManager.TargetScore
				</label>

				<div class="score">

					<div class="player">

						<label class="score-player-label">
							@PlayerDisplayName( 0 )
						</label>

						<label class="score-number">
							@RoundManager.ScoreA
						</label>

					</div>

					<label class="separator">
						-
					</label>

					<div class="player">

						<label class="score-player-label">
							@PlayerDisplayName( 1 )
						</label>

						<label class="score-number">
							@RoundManager.ScoreB
						</label>

					</div>

				</div>

				@if (
					RoundManager.CurrentPot >
					0
				)
				{
					<div class="pot-display">
						<div class="pot-display-chrome"></div>

						<label class="pot-owner">
							@PlayerDisplayName( RoundManager.PotOwnerTeam )
						</label>

						<label class="pot-value">
							UNBANKED @RoundManager.CurrentPot
						</label>

					</div>
				}

				@if (
					RoundManager.ActiveModifier !=
					RoundModifier.Standard &&
					(
						RoundManager.Phase ==
							RoundPhase.Countdown ||
						RoundManager.Phase ==
							RoundPhase.Fighting ||
						RoundManager.Phase ==
							RoundPhase.RoundEnd
					)
				)
				{
					<label class="modifier-label">
						@ModifierName( RoundManager.ActiveModifier )
					</label>
				}

			</div>

			@if ( !string.IsNullOrEmpty( CenterMessage ) )
			{
				<label class="center-message">
					@CenterMessage
				</label>
			}

			@if (
				RoundManager.Phase ==
				RoundPhase.WinnerDecision
			)
			{
				<div class="decision-screen">
					<div class="decision-screen-overlay"></div>

					<div class="decision-panel">
						<div class="decision-panel-chrome"></div>

						@if (
							LocalTeamId ==
							RoundManager.PotOwnerTeam
						)
						{
							<label class="decision-eyebrow">
								YOU WON THE ROUND
							</label>

							<label class="decision-title">
								BANK OR RISK IT?
							</label>

							<div class="decision-stakes">
								<div class="decision-stat banked">
									<label class="decision-stat-label">BANKED SCORE</label>
									<label class="decision-stat-value">@RoundManager.GetBankedScore( RoundManager.PotOwnerTeam ) / @RoundManager.TargetScore</label>
									<label class="decision-stat-note">POINTS SECURED</label>
								</div>

								<div class="decision-stat pot">
									<label class="decision-stat-label">UNBANKED POT</label>
									<label class="decision-stat-value">@RoundManager.CurrentPot</label>
									<label class="decision-stat-note">@PointWord( RoundManager.CurrentPot ) AT STAKE</label>
								</div>
							</div>

							<div class="decision-clock">
								<label class="decision-timer">@DecisionSeconds</label>
								<label class="decision-timeout-hint">NO INPUT = AUTO BANK</label>
							</div>

							<div class="decision-buttons">

								<div
									class="decision-button safe"
									onclick=@Bank
								>
									<label class="decision-button-title">BANK +@RoundManager.CurrentPot</label>
									<label class="decision-button-subtitle">SECURE @RoundManager.CurrentPot @PointWord( RoundManager.CurrentPot ) NOW</label>

									<div class="decision-outcome safe-outcome">
										<label class="decision-outcome-label">RESULTING BANKED SCORE</label>
										<label class="decision-outcome-value">@RoundManager.GetBankScorePreview() / @RoundManager.TargetScore</label>
									</div>
								</div>

								<div
									class="decision-button risk"
									onclick=@OneMoreRound
								>
									<label class="decision-button-title">ONE MORE ROUND</label>
									<label class="decision-button-subtitle">RISK @RoundManager.CurrentPot @PointWord( RoundManager.CurrentPot )</label>

									<div class="decision-risk-results">
										<div class="decision-risk-result win">
											<label class="decision-outcome-label">IF YOU WIN</label>
											<label class="decision-outcome-value">POT → @RoundManager.GetNextRiskPot()</label>
										</div>

										<div class="decision-risk-result lose">
											<label class="decision-outcome-label">IF YOU LOSE</label>
											<label class="decision-outcome-value">POT → 0</label>
										</div>
									</div>

									<label class="decision-draw-note">DRAW → POT STAYS @RoundManager.CurrentPot</label>
								</div>

							</div>

							<div class="risk-preview">
								<label class="risk-preview-title">
									IF YOU RISK, YOUR OPPONENT CHOOSES ONE
								</label>

								<div class="risk-preview-options">
									<div class="risk-preview-option">
										<label class="risk-preview-name">@ModifierName( RoundManager.ModifierOptionA )</label>
										<label class="risk-preview-description">@ModifierDescription( RoundManager.ModifierOptionA )</label>
										<label class="risk-preview-weapon">WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionA )</label>
									</div>

									<div class="risk-preview-option">
										<label class="risk-preview-name">@ModifierName( RoundManager.ModifierOptionB )</label>
										<label class="risk-preview-description">@ModifierDescription( RoundManager.ModifierOptionB )</label>
										<label class="risk-preview-weapon">WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionB )</label>
									</div>
								</div>
							</div>
						}
						else
						{
							<label class="decision-eyebrow">
								@PlayerDisplayName( RoundManager.PotOwnerTeam ) WON
							</label>

							<label class="decision-title">
								@PlayerDisplayName( RoundManager.PotOwnerTeam ) IS DECIDING
							</label>

							<div class="decision-stakes">
								<div class="decision-stat banked">
									<label class="decision-stat-label">THEIR BANKED SCORE</label>
									<label class="decision-stat-value">@RoundManager.GetBankedScore( RoundManager.PotOwnerTeam ) / @RoundManager.TargetScore</label>
									<label class="decision-stat-note">POINTS SECURED</label>
								</div>

								<div class="decision-stat pot">
									<label class="decision-stat-label">THEIR UNBANKED POT</label>
									<label class="decision-stat-value">@RoundManager.CurrentPot</label>
									<label class="decision-stat-note">@PointWord( RoundManager.CurrentPot ) AT STAKE</label>
								</div>
							</div>

							<div class="decision-clock">
								<label class="decision-timer">@DecisionSeconds</label>
							</div>

							<div class="risk-preview">
								<label class="risk-preview-title">
									IF THEY RISK, YOU WILL CHOOSE ONE
								</label>

								<div class="risk-preview-options">
									<div class="risk-preview-option">
										<label class="risk-preview-name">@ModifierName( RoundManager.ModifierOptionA )</label>
										<label class="risk-preview-description">@ModifierDescription( RoundManager.ModifierOptionA )</label>
										<label class="risk-preview-weapon">WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionA )</label>
									</div>

									<div class="risk-preview-option">
										<label class="risk-preview-name">@ModifierName( RoundManager.ModifierOptionB )</label>
										<label class="risk-preview-description">@ModifierDescription( RoundManager.ModifierOptionB )</label>
										<label class="risk-preview-weapon">WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionB )</label>
									</div>
								</div>
							</div>
						}

					</div>

				</div>
			}
			else if (
				RoundManager.Phase ==
					RoundPhase.ModifierDecision
			)
			{
				<div class="decision-screen">
					<div class="decision-screen-overlay"></div>

					<div class="decision-panel">
						<div class="decision-panel-chrome"></div>

						@if (
							LocalTeamId ==
								ModifierChooserTeam
						)
						{
							<label class="decision-eyebrow">
								@PlayerDisplayName( RoundManager.PotOwnerTeam ) CHOSE ONE MORE ROUND
							</label>

							<label class="decision-title">
								YOU CHOOSE THE CONDITIONS
							</label>

							<label class="decision-pot">
								THEIR UNBANKED POT: @RoundManager.CurrentPot
							</label>

							<label class="decision-timer">
								@DecisionSeconds
							</label>

							<div class="decision-buttons">

								<div
									class="decision-button modifier"
									onclick=@ChooseModifierA
								>
									<label class="decision-button-title">
										@ModifierName( RoundManager.ModifierOptionA )
									</label>

									<label class="decision-button-description">
										@ModifierDescription( RoundManager.ModifierOptionA )
									</label>

									<label class="decision-button-meta">
										WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionA )
									</label>
								</div>

								<div
									class="decision-button modifier"
									onclick=@ChooseModifierB
								>
									<label class="decision-button-title">
										@ModifierName( RoundManager.ModifierOptionB )
									</label>

									<label class="decision-button-description">
										@ModifierDescription( RoundManager.ModifierOptionB )
									</label>

									<label class="decision-button-meta">
										WEAPON: @RoundManager.GetModifierWeaponPreview( RoundManager.ModifierOptionB )
									</label>
								</div>

							</div>
						}
						else
						{
							<label class="decision-eyebrow">
								ONE MORE ROUND
							</label>

							<label class="decision-title">
								@PlayerDisplayName( ModifierChooserTeam ) CHOOSES THE CONDITIONS
							</label>

							<label class="decision-pot">
								YOUR UNBANKED POT: @RoundManager.CurrentPot
							</label>

							<label class="decision-timer">
								@DecisionSeconds
							</label>
						}

					</div>

				</div>
			}
			else if (
				RoundManager.Phase ==
					RoundPhase.MatchEnd
			)
			{
				<div class="decision-screen">
					<div class="decision-screen-overlay"></div>

					<div class="decision-panel match-end">
						<div class="decision-panel-chrome"></div>

						<label class="decision-eyebrow">
							MATCH OVER
						</label>

						<label class="decision-title">
							@PlayerDisplayName( RoundManager.MatchWinnerTeam ) WINS
						</label>

						<label class="final-score">
							@RoundManager.ScoreA - @RoundManager.ScoreB
						</label>

						@if ( Networking.IsHost )
						{
							<div
								class="decision-button return-button"
								onclick=@ReturnToMenu
							>
								<label class="decision-button-title">
									RETURN TO MENU
								</label>
							</div>
						}
						else
						{
							<label class="decision-pot">
								WAITING FOR HOST
							</label>
						}

					</div>

				</div>
			}
		}

		@if ( ShouldShowAmmoHud )
		{
			<div class="ammo-hud">
				<div class="ammo-hud-chrome"></div>

				<label class="ammo-weapon">
					@LocalWeaponName
				</label>

				@if ( LocalWeaponIsReloading )
				{
					<label class="ammo-state">
						RELOADING
					</label>
				}

				<div class="ammo-count">
					<label class="ammo-current">@LocalClipText</label>
					<label class="ammo-divider">/</label>
					<label class="ammo-capacity">@LocalClipCapacityText</label>
				</div>

			</div>
		}

		@if ( LocalCombatFeedback is not null )
		{
			<div class="combat-damage-feedback">
				@if ( CombatTuning?.ShowDamageImpactVignette != false )
				{
					<div
						class="combat-damage-vignette"
						style="@CombatDamageVignetteInlineStyle"
					></div>
				}

				@if ( CombatTuning?.ShowDirectionalDamageArc != false && LocalCombatFeedback.HasDirectionalDamageSource )
				{
					<div
						class="combat-damage-arc-wrapper"
						style="@CombatDamageArcWrapperInlineStyle"
					>
						<div
							class="combat-damage-arc-offset"
							style="@CombatDamageArcOffsetInlineStyle"
						>
							@if ( CombatTuning?.ShowDamageIndicatorArcGlow != false )
							{
								<Image
									class="combat-damage-arc combat-damage-arc-glow"
									Texture="@CombatDamageArcTexture"
									style="@CombatDamageArcGlowInlineStyle"
								/>
							}

							<Image
								class="combat-damage-arc combat-damage-arc-core"
								Texture="@CombatDamageArcTexture"
								style="@CombatDamageArcInlineStyle"
							/>
						</div>
					</div>
				}
			</div>

			<div
				class="combat-hitmarker @CombatHitmarkerStateClass"
				style="@CombatHitmarkerInlineStyle"
			>
				<div class="combat-hitmarker-line top-left" style="@CombatHitmarkerTopLeftStyle"></div>
				<div class="combat-hitmarker-line top-right" style="@CombatHitmarkerTopRightStyle"></div>
				<div class="combat-hitmarker-line bottom-left" style="@CombatHitmarkerBottomLeftStyle"></div>
				<div class="combat-hitmarker-line bottom-right" style="@CombatHitmarkerBottomRightStyle"></div>
			</div>
		}
	}

</root>


@code
{
	[Property]
	public RoundManager RoundManager { get; set; }

	[Property, Group( "UI Foundation" ), Title( "Theme" )]
	public OMRUITheme Theme { get; set; }

	[Property, Group( "UI Foundation" ), Title( "HUD Layout" )]
	public OMRHUDLayout HudLayout { get; set; }

	[Property, Group( "Developer Preview" ), Title( "Enable HUD Preview" )]
	public bool EnableDeveloperPreview { get; set; }

	[Property, Group( "Developer Preview" ), ShowIf( nameof( EnableDeveloperPreview ), true ), Title( "Preview State" )]
	public OMRHUDPreviewMode DeveloperPreviewMode { get; set; } =
		OMRHUDPreviewMode.Combat;

	public OMRUITheme ActiveTheme =>
		Theme ?? OMRUIResources.DefaultTheme;

	public OMRHUDLayout ActiveHudLayout =>
		HudLayout ?? OMRUIResources.DefaultHudLayout;

	private OMRCombatTuning CombatTuning =>
		OMRCombatResources.DefaultTuning;

	private bool ShowDeveloperPreview =>
		EnableDeveloperPreview &&
		(
			RoundManager is null ||
			RoundManager.Mode == SessionMode.Menu
		);

	private float _nextResourceHashAt;
	private int _cachedThemeHash, _cachedHudLayoutHash, _cachedCombatTuningHash;
	private OMRUITheme _hashedTheme;
	private OMRHUDLayout _hashedLayout;
	private OMRCombatTuning _hashedTuning;
	private void RefreshResourceHashes()
	{
		OMRUITheme theme = ActiveTheme;
		OMRHUDLayout layout = ActiveHudLayout;
		OMRCombatTuning tuning = CombatTuning;
		if ( RealTime.Now < _nextResourceHashAt && theme == _hashedTheme &&
			layout == _hashedLayout && tuning == _hashedTuning ) return;
		_nextResourceHashAt = RealTime.Now + 0.20f;
		_hashedTheme = theme; _hashedLayout = layout; _hashedTuning = tuning;
		_cachedThemeHash = theme?.ContentHash() ?? 0;
		_cachedHudLayoutHash = layout?.ContentHash() ?? 0;
		_cachedCombatTuningHash = tuning?.ContentHash() ?? 0;
	}
	private int CachedThemeHash { get { RefreshResourceHashes(); return _cachedThemeHash; } }
	private int CachedHudLayoutHash { get { RefreshResourceHashes(); return _cachedHudLayoutHash; } }
	private int CachedCombatTuningHash { get { RefreshResourceHashes(); return _cachedCombatTuningHash; } }

	private int _lastRuntimeStyleHash =
		int.MinValue;

	private float _nextRuntimeStyleRefresh;

	private GameFlowManager FlowManager =>
		RoundManager?.
			GameObject?.
			Components
			.Get<GameFlowManager>();

	protected override void OnUpdate()
	{
		base.OnUpdate();

		RefreshRuntimeStyles();

		if ( RoundManager is null )
			return;

		if (
			RoundManager.Mode ==
				SessionMode.Menu &&
			LocalPlayerState?.
				PreMatchState ==
					PreMatchPlayerState.Warmup &&
			Input.EscapePressed
		)
		{
			RoundManager.RequestPreMatchWarmup();

			return;
		}

		bool winnerCanAct =
			RoundManager.Mode ==
				SessionMode.Match &&
			RoundManager.Phase ==
				RoundPhase.WinnerDecision &&
			LocalTeamId ==
				RoundManager.PotOwnerTeam;

		bool modifierCanAct =
			RoundManager.Mode ==
				SessionMode.Match &&
			RoundManager.Phase ==
				RoundPhase.ModifierDecision &&
			LocalTeamId ==
				ModifierChooserTeam;

		bool matchEndHost =
			RoundManager.Mode ==
				SessionMode.Match &&
			RoundManager.Phase ==
				RoundPhase.MatchEnd &&
			Networking.IsHost;

		bool interactiveUi =
			ShowDeveloperPreview ||
			PauseMenu.IsAnyPauseOpen ||
			winnerCanAct ||
			modifierCanAct ||
			matchEndHost;

		Mouse.Visibility =
			interactiveUi
				? MouseVisibility.Visible
				: MouseVisibility.Auto;

		Input.EnableVirtualCursor =
			interactiveUi;
	}

	private void SetDeveloperPreviewMode( OMRHUDPreviewMode mode )
	{
		DeveloperPreviewMode = mode;
		_lastRuntimeStyleHash = int.MinValue;
	}

	private void PreviewCombat() => SetDeveloperPreviewMode( OMRHUDPreviewMode.Combat );
	private void PreviewSuddenDeath() => SetDeveloperPreviewMode( OMRHUDPreviewMode.SuddenDeath );
	private void PreviewWinnerDecision() => SetDeveloperPreviewMode( OMRHUDPreviewMode.WinnerDecision );
	private void PreviewModifierDecision() => SetDeveloperPreviewMode( OMRHUDPreviewMode.ModifierDecision );
	private void PreviewMatchEnd() => SetDeveloperPreviewMode( OMRHUDPreviewMode.MatchEnd );
	private void PreviewSandbox() => SetDeveloperPreviewMode( OMRHUDPreviewMode.Sandbox );
	private void PreviewWarmup() => SetDeveloperPreviewMode( OMRHUDPreviewMode.Warmup );

	private void RefreshRuntimeStyles()
	{
		int combatFeedbackHash = HashCode.Combine(
			LocalCombatFeedback?.HitmarkerVisible ?? false,
			LocalCombatFeedback?.DamageIndicatorVisible ?? false,
			LocalCombatFeedback?.LastHitWasKill ?? false,
			LocalCombatFeedback?.LastHitWasHeadshot ?? false,
			LocalCombatFeedback?.LastDamageDirection ?? OMRDamageIndicatorDirection.Front
		);

		int styleHash = HashCode.Combine(
			CachedThemeHash,
			CachedHudLayoutHash,
			CachedCombatTuningHash,
			EnableDeveloperPreview,
			DeveloperPreviewMode,
			RoundManager?.Mode,
			RoundManager?.Phase,
			combatFeedbackHash
		);

		bool interactiveDecision =
			ShowDeveloperPreview &&
			(
				DeveloperPreviewMode == OMRHUDPreviewMode.WinnerDecision ||
				DeveloperPreviewMode == OMRHUDPreviewMode.ModifierDecision ||
				DeveloperPreviewMode == OMRHUDPreviewMode.MatchEnd
			) ||
			RoundManager is not null &&
			(
				RoundManager.Phase == RoundPhase.WinnerDecision ||
				RoundManager.Phase == RoundPhase.ModifierDecision ||
				RoundManager.Phase == RoundPhase.MatchEnd
			);

		if (
			!interactiveDecision &&
			styleHash == _lastRuntimeStyleHash &&
			RealTime.Now < _nextRuntimeStyleRefresh
		)
			return;

		OMRUIRuntimeStyle.ApplyHud(
			Panel,
			ActiveTheme,
			ActiveHudLayout
		);

		_lastRuntimeStyleHash = styleHash;
		_nextRuntimeStyleRefresh = RealTime.Now + 0.20f;
	}

	private void ReturnToMenu()
	{
		if ( RoundManager is null )
			return;

		if (
			MainMenuHost.Current is not null
		)
		{
			MainMenuHost.Current
				.ReturnToMainMenu();

			return;
		}

		if ( FlowManager is not null )
		{
			FlowManager.ReturnToMainMenu();
		}
		else
		{
			RoundManager.ReturnToMenu();
		}
	}

	// ============================================================
	// WINNER DECISION
	// ============================================================

	private void Bank()
	{
		if ( RoundManager is null )
			return;

		RoundManager.RequestBank();
	}

	private void OneMoreRound()
	{
		if ( RoundManager is null )
			return;

		RoundManager.RequestOneMoreRound();
	}

	// ============================================================
	// MODIFIER DECISION
	// ============================================================

	private void ChooseModifierA()
	{
		if ( RoundManager is null )
			return;

		RoundManager.RequestModifier(
			RoundManager.ModifierOptionA
		);
	}

	private void ChooseModifierB()
	{
		if ( RoundManager is null )
			return;

		RoundManager.RequestModifier(
			RoundManager.ModifierOptionB
		);
	}

	private readonly OMRSceneReferences _hudReferences = new();
	private OMRSceneReferences HudReferences
	{
		get { _hudReferences.Refresh( RoundManager?.Scene ); return _hudReferences; }
	}
	private OMRBotController SandboxBot => HudReferences.Bot;

	private PlayerState SandboxBotPlayer =>
		SandboxBot?.Components.Get<PlayerState>();

	private string SandboxBotDisplayName =>
		SandboxBotPlayer?.DisplayName ?? "BOT";

	private string SandboxBotDifficulty =>
		SandboxBot?.Difficulty.ToString().ToUpperInvariant() ?? "UNKNOWN";

	private string SandboxBotIntent =>
		SandboxBot?.CurrentIntent ?? "NO INTENT";

	private string SandboxBotPerception
	{
		get
		{
			if ( SandboxBot is null )
				return "SENSE: OFF";

			string target =
				string.IsNullOrWhiteSpace( SandboxBot.PerceptionTargetName )
					? "NONE"
					: SandboxBot.PerceptionTargetName;

			string reaction =
				SandboxBot.PerceptionReactionRemaining > 0.01f
					? $" · REACT {SandboxBot.PerceptionReactionRemaining:0.00}s"
					: string.Empty;

			return $"SENSE: {SandboxBot.PerceptionStatus} · {target} · {SandboxBot.PerceptionTargetDistance:0}u{reaction}";
		}
	}

	private string SandboxBotMemory
	{
		get
		{
			if ( SandboxBot is null || !SandboxBot.HasLastKnownPosition )
				return "MEMORY: NONE";

			return $"MEMORY: LAST SEEN {SandboxBot.PerceptionLastSeenSeconds:0.0}s AGO";
		}
	}

	private string SandboxBotContext
	{
		get
		{
			if ( SandboxBot is null )
				return "CTX: OFF";

			return $"CTX: {SandboxBot.TacticalContextStatus} · AGG {SandboxBot.TacticalAggression * 100f:0}%";
		}
	}

	private string SandboxBotModifier
	{
		get
		{
			if ( SandboxBot is null )
				return "MOD: OFF";

			return $"MOD: {SandboxBot.ModifierStatus}";
		}
	}

	private string SandboxBotAdaptation
	{
		get
		{
			if ( SandboxBot is null )
				return "LEARN: OFF";

			return $"LEARN: {SandboxBot.AdaptationStatus} · CONF {SandboxBot.AdaptationConfidence * 100f:0}%";
		}
	}

	private string SandboxBotSpatialMemory
	{
		get
		{
			if ( SandboxBot is null )
				return "MAP: OFF";

			return $"MAP: {SandboxBot.SpatialMemoryStatus}";
		}
	}

	private string SandboxBotAwareness
	{
		get
		{
			if ( SandboxBot is null )
				return "AWARE: OFF";

			return $"AWARE: {SandboxBot.AwarenessStatus}";
		}
	}

	private string SandboxBotThreat
	{
		get
		{
			if ( SandboxBot is null )
				return "THREAT: OFF";

			string age =
				SandboxBot.ThreatAgeSeconds >= 0f
					? $" · {SandboxBot.ThreatAgeSeconds:0.0}s"
					: string.Empty;

			return $"THREAT: {SandboxBot.ThreatStatus} · {SandboxBot.ThreatBearing}{age}";
		}
	}

	private string SandboxBotHearing
	{
		get
		{
			if ( SandboxBot is null )
				return "HEAR: OFF";

			string age =
				SandboxBot.HearingAgeSeconds >= 0f
					? $" · {SandboxBot.HearingAgeSeconds:0.0}s"
					: string.Empty;

			return $"HEAR: {SandboxBot.HearingStatus} · {SandboxBot.HearingBearing}{age}";
		}
	}

	private string SandboxBotInitiative
	{
		get
		{
			if ( SandboxBot is null )
				return "OPEN: OFF";

			return $"OPEN: {SandboxBot.InitiativeStatus}";
		}
	}

	private string SandboxBotTactical
	{
		get
		{
			if ( SandboxBot is null )
				return "TACTIC: OFF";

			return $"TACTIC: {SandboxBot.TacticalStatus} · COVER: {SandboxBot.CoverStatus}";
		}
	}

	private string SandboxBotAdvancedTactics
	{
		get
		{
			if ( SandboxBot is null )
				return "AI: OFF";

			return $"AI: SEARCH {SandboxBot.SearchStatus} · FLANK {SandboxBot.FlankStatus} · SUP {SandboxBot.SuppressionStatus}";
		}
	}

	private string SandboxBotUtilityBrain
	{
		get
		{
			if ( SandboxBot is null )
				return "BRAIN: OFF";

			return $"BRAIN: {SandboxBot.UtilityBrainStatus}";
		}
	}

	private string SandboxBotCombatSequence
	{
		get
		{
			if ( SandboxBot is null )
				return "SEQ: OFF";

			return $"SEQ: {SandboxBot.CombatSequenceStatus}";
		}
	}

	private string SandboxBotHumanization
	{
		get
		{
			if ( SandboxBot is null )
				return "HUMAN: OFF";

			return $"HUMAN: {SandboxBot.HumanizationStatus}";
		}
	}

	private string SandboxBotMatchup
	{
		get
		{
			if ( SandboxBot is null )
				return "MATCHUP: OFF";

			return $"MATCHUP: {SandboxBot.MatchupStatus}";
		}
	}

	private string SandboxBotPrediction
	{
		get
		{
			if ( SandboxBot is null )
				return "PREDICT: OFF";

			return $"PREDICT: {SandboxBot.PlayerPredictionStatus}";
		}
	}

	private string SandboxBotBelief
	{
		get
		{
			if ( SandboxBot is null )
				return "BELIEF: OFF";

			return $"BELIEF: {SandboxBot.BeliefStatus}";
		}
	}

	private string SandboxBotTacticalSpace
	{
		get
		{
			if ( SandboxBot is null )
				return "SPACE: OFF";

			return $"SPACE: {SandboxBot.TacticalMapStatus} · QUERY {SandboxBot.TacticalQueryStatus}";
		}
	}

	private string SandboxBotNavigation
	{
		get
		{
			if ( SandboxBot is null )
				return "NAV: OFF";

			string target =
				string.IsNullOrWhiteSpace( SandboxBot.NavigationTargetName )
					? "NONE"
					: SandboxBot.NavigationTargetName;

			return $"NAV: {SandboxBot.NavigationStatus} · TARGET: {target} · {SandboxBot.NavigationTargetDistance:0}u";
		}
	}

	private string SandboxBotMovement
	{
		get
		{
			if ( SandboxBot is null )
				return "MOVE: OFF";

			return $"MOVE: {SandboxBot.LocomotionStatus} · {SandboxBot.MovementStatus} · {SandboxBot.MovementSpeed:0}u/s";
		}
	}

	private string SandboxBotCombat
	{
		get
		{
			if ( SandboxBot is null )
				return "COMBAT: OFF";

			string ads = SandboxBot.CombatWantsAim ? "ADS" : "HIP";
			string ammo = SandboxBot.CombatClip >= 0
				? $" · {SandboxBot.CombatClip}/{SandboxBot.CombatReserve}"
				: string.Empty;

			return $"COMBAT: {SandboxBot.CombatStatus} · {ads} · ERR {SandboxBot.CombatAimError:0.0}°{ammo}";
		}
	}

	private string SandboxBotLifeState =>
		SandboxBotPlayer?.IsAlive == true
			? "ALIVE"
			: "RESPAWNING";

	private string SandboxBotWeapon
	{
		get
		{
			BaseInventoryComponent inventory =
				SandboxBotPlayer?
					.Components
					.Get<BaseInventoryComponent>();

			return inventory?.ActiveItem?.DisplayName?.ToUpperInvariant() ??
				"NO WEAPON";
		}
	}

	private int ConnectedPlayerCount => HudReferences.ReadyCount;
	// ============================================================
	// LOCAL PLAYER
	// ============================================================

	private PlayerState LocalPlayerState => HudReferences.LocalPlayer;

	private BaseInventoryComponent LocalInventory => HudReferences.LocalInventory;
	private PlayerCombatFeedback LocalCombatFeedback => HudReferences.LocalFeedback;

	private string CombatHitmarkerStateClass
	{
		get
		{
			if ( LocalCombatFeedback?.LastHitWasKill == true )
				return "kill";

			if ( LocalCombatFeedback?.LastHitWasHeadshot == true )
				return "headshot";

			return string.Empty;
		}
	}

	private Color CombatDamageColor =>
		CombatTuning?.OverrideDamageIndicatorColor == true
			? CombatTuning.DamageIndicatorColor
			: ActiveTheme?.DangerText ?? Color.Red;

	private Texture CombatDamageArcTexture =>
		CombatTuning?.GetDamageIndicatorArcTexture();

	private Color CombatHitmarkerColor
	{
		get
		{
			if ( CombatTuning?.OverrideHitmarkerColors == true )
			{
				if ( LocalCombatFeedback?.LastHitWasKill == true )
					return CombatTuning.KillHitmarkerColor;

				if ( LocalCombatFeedback?.LastHitWasHeadshot == true )
					return CombatTuning.HeadshotHitmarkerColor;

				return CombatTuning.HitmarkerColor;
			}

			if ( LocalCombatFeedback?.LastHitWasKill == true )
				return ActiveTheme?.DangerText ?? Color.Red;

			if ( LocalCombatFeedback?.LastHitWasHeadshot == true )
				return ActiveTheme?.AccentText ?? Color.White;

			return ActiveTheme?.PrimaryText ?? Color.White;
		}
	}

	private float CombatDamageIntensity =>
		MathX.Clamp(
			LocalCombatFeedback?.DamageIndicatorOpacity ?? 0f,
			0f,
			1f
		);

	private string CombatDamageVignetteInlineStyle
	{
		get
		{
			float peakOpacity =
				MathX.Clamp( CombatTuning?.DamageImpactVignetteOpacity ?? 0.13f, 0f, 0.7f );

			float opacity =
				CombatDamageIntensity * peakOpacity;

			return $"opacity: {Css( opacity )}; color: {CombatDamageColor.Rgba};";
		}
	}

	private string CombatDamageArcWrapperInlineStyle
	{
		get
		{
			float diameter =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcDiameter ?? 520f, 220f, 900f );

			float half =
				diameter * 0.5f;

			float fade =
				LocalCombatFeedback?.DamageIndicatorFade ?? 0f;

			float impactScale =
				MathF.Max( 1f, CombatTuning?.DamageIndicatorArcImpactScale ?? 1.06f );

			float scale =
				1f + (impactScale - 1f) * fade;

			float angle =
				(LocalCombatFeedback?.DamageAngleDegrees ?? 0f) +
				(CombatTuning?.DamageIndicatorArcRotationOffset ?? 0f);

			return $"width: {Css( diameter )}px; height: {Css( diameter )}px; margin-left: {Css( -half )}px; margin-top: {Css( -half )}px; transform: rotate( {Css( angle )}deg ) scale( {Css( scale )} );";
		}
	}

	private string CombatDamageArcOffsetInlineStyle
	{
		get
		{
			float radialOffset =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcRadialOffset ?? 0f, -180f, 320f );

			return $"transform: translateY( {Css( -radialOffset )}px );";
		}
	}

	private string CombatDamageArcInlineStyle
	{
		get
		{
			float peakOpacity =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcOpacity ?? 0.90f, 0f, 1f );

			float opacity =
				CombatDamageIntensity * peakOpacity;

			return $"opacity: {Css( opacity )}; background-image-tint: {CombatDamageColor.Rgba};";
		}
	}

	private string CombatDamageArcGlowInlineStyle
	{
		get
		{
			float arcOpacity =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcOpacity ?? 0.90f, 0f, 1f );

			float glowOpacity =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcGlowOpacity ?? 0.34f, 0f, 1f );

			float blur =
				MathX.Clamp( CombatTuning?.DamageIndicatorArcGlowBlur ?? 5f, 0f, 20f );

			float opacity =
				CombatDamageIntensity * arcOpacity * glowOpacity;

			return $"opacity: {Css( opacity )}; background-image-tint: {CombatDamageColor.Rgba}; filter-blur: {Css( blur )}px;";
		}
	}

	private string CombatHitmarkerInlineStyle
	{
		get
		{
			float size = MathX.Clamp( CombatTuning?.HitmarkerSize ?? 38f, 20f, 80f );
			float half = size * 0.5f;
			float opacity = LocalCombatFeedback?.HitmarkerOpacity ?? 0f;
			float scale = LocalCombatFeedback?.HitmarkerScale ?? 1f;

			return $"width: {Css( size )}px; height: {Css( size )}px; margin-left: {Css( -half )}px; margin-top: {Css( -half )}px; opacity: {Css( opacity )}; color: {CombatHitmarkerColor.Rgba}; transform: scale( {Css( scale )} );";
		}
	}

	private string CombatHitmarkerTopLeftStyle =>
		CombatHitmarkerLineStyle( "right", "bottom", "45deg", "100% 50%" );

	private string CombatHitmarkerTopRightStyle =>
		CombatHitmarkerLineStyle( "left", "bottom", "-45deg", "0% 50%" );

	private string CombatHitmarkerBottomLeftStyle =>
		CombatHitmarkerLineStyle( "right", "top", "-45deg", "100% 50%" );

	private string CombatHitmarkerBottomRightStyle =>
		CombatHitmarkerLineStyle( "left", "top", "45deg", "0% 50%" );

	private static string Css( float value ) =>
		value.ToString( "0.###", CultureInfo.InvariantCulture );

	private string CombatHitmarkerLineStyle(
		string horizontalProperty,
		string verticalProperty,
		string rotation,
		string transformOrigin
	)
	{
		float size = MathX.Clamp( CombatTuning?.HitmarkerSize ?? 38f, 20f, 80f );
		float length = MathX.Clamp( CombatTuning?.HitmarkerLineLength ?? 11f, 4f, 24f );
		float thickness = MathX.Clamp( CombatTuning?.HitmarkerLineThickness ?? 2f, 1f, 6f );
		float gap = MathX.Clamp( CombatTuning?.HitmarkerCenterGap ?? 4f, 0f, 14f );
		float anchor = size * 0.5f + gap;

		return $"width: {Css( length )}px; height: {Css( thickness )}px; {horizontalProperty}: {Css( anchor )}px; {verticalProperty}: {Css( anchor )}px; transform-origin: {transformOrigin}; transform: rotate( {rotation} );";
	}

	private BaseCombatWeapon LocalWeapon =>
		LocalInventory?.
			ActiveItem as BaseCombatWeapon;

	private bool IsLocalWarmup =>
		RoundManager is not null &&
		RoundManager.Mode ==
			SessionMode.Menu &&
		LocalPlayerState?.
			PreMatchState ==
				PreMatchPlayerState.Warmup;

	private bool ShouldShowAmmoHud =>
		LocalWeapon is not null &&
		(
			RoundManager.Mode ==
				SessionMode.Match ||
			RoundManager.Mode ==
				SessionMode.Sandbox ||
			IsLocalWarmup
		);

	private string LocalWeaponName
	{
		get
		{
			BaseInventoryItem item =
				LocalInventory?.
					ActiveItem;

			if ( item is null )
				return "WEAPON";

			if (
				!string.IsNullOrWhiteSpace(
					item.DisplayName
				)
			)
				return item.DisplayName.ToUpperInvariant();

			return item.GameObject.Name
				.Replace(
					"_",
					" "
				)
				.ToUpperInvariant();
		}
	}

	private string LocalClipText
	{
		get
		{
			BaseCombatWeapon weapon =
				LocalWeapon;

			if ( weapon is null )
				return "--";

			if ( !weapon.UsesPrimaryClip )
				return "∞";

			return Math.Max(
				weapon.Clip1,
				0
			)
			.ToString();
		}
	}

	private string LocalClipCapacityText
	{
		get
		{
			BaseCombatWeapon weapon =
				LocalWeapon;

			if ( weapon is null )
				return "--";

			if ( !weapon.UsesPrimaryClip )
				return "∞";

			return Math.Max(
				weapon.PrimaryClipSize,
				0
			)
			.ToString();
		}
	}

	private bool LocalWeaponIsReloading =>
		LocalWeapon?.
			IsReloading ??
		false;

	private int LocalTeamId
	{
		get
		{
			if (
				RoundManager is null ||
				RoundManager.Scene is null
			)
				return -1;

			return
				LocalPlayerState?.
					TeamId ??
				-1;
		}
	}

	private PlayerState GetPlayerForTeam( int teamId ) => HudReferences.PlayerForTeam( teamId );

	private PlayerHealth GetHealthForTeam(
		int teamId
	)
	{
		PlayerState player =
			GetPlayerForTeam(
				teamId
			);

		return player?
			.Components
			.Get<PlayerHealth>();
	}

	private int OpponentTeamId
	{
		get
		{
			return LocalTeamId switch
			{
				0 => 1,
				1 => 0,
				_ => -1
			};
		}
	}

	private float LocalHealthPercent =>
		GetHealthPercent(
			GetHealthForTeam(
				LocalTeamId
			)
		);

	private float OpponentHealthPercent =>
		GetHealthPercent(
			GetHealthForTeam(
				OpponentTeamId
			)
		);

	private string LocalHealthText =>
		GetHealthText(
			GetHealthForTeam(
				LocalTeamId
			)
		);

	private string OpponentHealthText =>
		GetHealthText(
			GetHealthForTeam(
				OpponentTeamId
			)
		);

	private float GetHealthPercent(
		PlayerHealth health
	)
	{
		if (
			health is null ||
			health.MaxHealth <= 0f
		)
			return 0f;

		return MathX.Clamp(
			health.Health /
				health.MaxHealth,
			0f,
			1f
		);
	}

	private string GetHealthText(
		PlayerHealth health
	)
	{
		if ( health is null )
			return "--";

		return $"{MathF.Ceiling( MathF.Max( health.Health, 0f ) ):0}/{MathF.Ceiling( MathF.Max( health.MaxHealth, 0f ) ):0}";
	}

	private int ModifierChooserTeam
	{
		get
		{
			if ( RoundManager is null )
				return -1;

			return RoundManager.PotOwnerTeam switch
			{
				0 => 1,
				1 => 0,
				_ => -1
			};
		}
	}

	// ============================================================
	// DISPLAY HELPERS
	// ============================================================

	private int CombatSeconds
	{
		get
		{
			if (
				RoundManager is null ||
				RoundManager.Phase !=
					RoundPhase.Fighting
			)
				return 0;

			if (
				RoundManager.ActiveUsesHighNoonFlow &&
				RoundManager.HighNoonDrawAt > 0f &&
				Time.Now <
					RoundManager.HighNoonDrawAt
			)
			{
				return (int)MathF.Ceiling(
					MathF.Max(
						0f,
						RoundManager.FightEndsAt -
							RoundManager.HighNoonDrawAt
					)
				);
			}

			return (int)MathF.Ceiling(
				MathF.Max(
					0f,
					RoundManager.FightEndsAt -
						Time.Now
				)
			);
		}
	}

	private int DecisionSeconds
	{
		get
		{
			if ( RoundManager is null )
				return 0;

			return (int)MathF.Ceiling(
				MathF.Max(
					0f,
					RoundManager.DecisionEndsAt -
						Time.Now
				)
			);
		}
	}

	private string PlayerDisplayName(
		int teamId
	)
	{
		PlayerState player =
			GetPlayerForTeam( teamId );

		if ( player is not null )
		{
			string displayName =
				player.DisplayName;

			if ( !string.IsNullOrWhiteSpace( displayName ) )
				return player.IsBot && RoundManager?.Mode == SessionMode.Match
					? $"{displayName} · {RoundManager.MatchBotDifficulty.ToString().ToUpperInvariant()}"
					: displayName;
		}

		if ( teamId == LocalTeamId )
			return "YOU";

		return teamId == 0 || teamId == 1
			? "OPPONENT"
			: "PLAYER";
	}

	private string PointWord(
		int count
	)
	{
		return count == 1
			? "POINT"
			: "POINTS";
	}

	private string TeamName(
		int team
	)
	{
		return team switch
		{
			0 => "A",
			1 => "B",
			_ => "?"
		};
	}

	private string ModifierName(
		RoundModifier modifier
	)
	{
		if ( RoundManager is not null )
			return RoundManager.GetModifierDisplayName( modifier );

		return modifier switch
		{
			RoundModifier.HighNoon => "HIGH NOON",
			RoundModifier.LowGravity => "LOW GRAVITY",
			RoundModifier.HeadshotOnly => "HEADSHOT ONLY",
			RoundModifier.LargeCaliber => "LARGE CALIBER",
			RoundModifier.DoubleJump => "DOUBLE JUMP",
			RoundModifier.HipfireOnly => "HIPFIRE ONLY",
			RoundModifier.Blitz => "BLITZ",
			_ => "STANDARD"
		};
	}

	private string ModifierDescription(
		RoundModifier modifier
	)
	{
		if ( RoundManager is not null )
			return RoundManager.GetModifierDescription( modifier );

		return modifier switch
		{
			RoundModifier.HighNoon => "Open arena. Draw on the signal.",
			RoundModifier.LowGravity => "Reduced gravity with a competitive weapon.",
			RoundModifier.HeadshotOnly => "Only headshots deal weapon damage.",
			RoundModifier.LargeCaliber => "Smaller magazine. Higher damage and recoil.",
			RoundModifier.DoubleJump => "Gain one additional jump while airborne.",
			RoundModifier.HipfireOnly => "ADS disabled. Fight from the hip.",
			RoundModifier.Blitz => "A fast 30-second round.",
			_ => "Standard arena rules."
		};
	}

	// ============================================================
	// CENTER MESSAGE
	// ============================================================

	private string CenterMessage
	{
		get
		{
			if ( RoundManager is null )
				return "";

			if (
				RoundManager.Mode !=
					SessionMode.Match
			)
				return "";

			return RoundManager.Phase switch
			{
				RoundPhase.Waiting =>
					"WAITING FOR PLAYER",

				RoundPhase.Countdown =>
					MathF.Ceiling(
						MathF.Max(
							0f,
							RoundManager.PhaseEndsAt -
								Time.Now
						)
					).ToString( "0" ),

				RoundPhase.Fighting =>
					RoundManager.ActiveUsesHighNoonFlow
							? (
								RoundManager.HighNoonState ==
									HighNoonStage.WaitingForDraw
										? "READY"
										: (
											Time.Now < RoundManager.FightMessageEndsAt
													? "DRAW!"
													: ""
										)
							)
							: (
								Time.Now <
											RoundManager.FightMessageEndsAt
												? (
											RoundManager.IsSuddenDeath
												? "SUDDEN DEATH"
												: "FIGHT!"
										)
										: ""
							),

				RoundPhase.RoundEnd =>
					RoundManager.LastRoundWinner switch
					{
						0 =>
							$"{PlayerDisplayName( 0 ).ToUpperInvariant()} WINS",

						1 =>
							$"{PlayerDisplayName( 1 ).ToUpperInvariant()} WINS",

						2 =>
							"DRAW",

						_ =>
							"ROUND OVER"
					},

				_ =>
					""
			};
		}
	}

	// ============================================================
	// RAZOR INVALIDATION
	// ============================================================

	protected override int BuildHash()
	{
		if ( ShowDeveloperPreview )
		{
			return HashCode.Combine(
				EnableDeveloperPreview,
				DeveloperPreviewMode,
				CachedThemeHash,
				CachedHudLayoutHash
			);
		}

		if ( RoundManager is null )
		{
			return HashCode.Combine(
				EnableDeveloperPreview,
				CachedThemeHash,
				CachedHudLayoutHash
			);
		}

		float phaseCountdown =
			MathF.Ceiling(
				MathF.Max(
					0f,
					RoundManager.PhaseEndsAt -
						Time.Now
				)
			);

		float decisionCountdown =
			MathF.Ceiling(
				MathF.Max(
					0f,
					RoundManager.DecisionEndsAt -
						Time.Now
				)
			);

		float combatCountdown =
			MathF.Ceiling(
				MathF.Max(
					0f,
					RoundManager.FightEndsAt -
						Time.Now
				)
			);


		HashCode hash =
			new();

		hash.Add( EnableDeveloperPreview );
		hash.Add( DeveloperPreviewMode );
		hash.Add( CachedThemeHash );
		hash.Add( CachedHudLayoutHash );
		hash.Add( CachedCombatTuningHash );

		hash.Add(
			RoundManager.Mode
		);

		hash.Add(
			RoundManager.Phase
		);

		hash.Add(
			RoundManager.RoundNumber
		);

		hash.Add(
			RoundManager.ScoreA
		);

		hash.Add(
			RoundManager.ScoreB
		);

		hash.Add(
			RoundManager.TargetScore
		);

		hash.Add(
			RoundManager.MaxPot
		);

		hash.Add(
			RoundManager.OneMoreRoundPotGain
		);

		hash.Add(
			RoundManager.LastRoundWinner
		);

		hash.Add(
			RoundManager.CurrentPot
		);

		hash.Add(
			RoundManager.PotOwnerTeam
		);

		hash.Add(
			RoundManager.ActiveModifier
		);

		hash.Add(
			RoundManager.ModifierOptionA
		);

		hash.Add(
			RoundManager.ModifierOptionB
		);

		hash.Add(
			RoundManager.ModifierOptionAWeaponPreview
		);

		hash.Add(
			RoundManager.ModifierOptionBWeaponPreview
		);

		hash.Add(
			RoundManager.MatchWinnerTeam
		);

		hash.Add(
			RoundManager.IsSuddenDeath
		);

		hash.Add(
			RoundManager.HighNoonState
		);

		hash.Add(
			RoundManager.HighNoonDrawAt
		);

		hash.Add(
			RoundManager.HighNoonFirstTriggerTeam
		);

		hash.Add(
			LocalTeamId
		);

		hash.Add(
			PlayerDisplayName( 0 )
		);

		hash.Add(
			PlayerDisplayName( 1 )
		);

		hash.Add(
			LocalHealthText
		);

		hash.Add(
			OpponentHealthText
		);

		hash.Add(
			phaseCountdown
		);

		hash.Add(
			decisionCountdown
		);

		hash.Add(
			combatCountdown
		);

		hash.Add(
			CenterMessage
		);



		hash.Add(
			RoundManager.SandboxWeaponAIndex
		);

		hash.Add(
			RoundManager.SandboxWeaponBIndex
		);

		hash.Add(
			RoundManager.SandboxSpawnALayoutIndex
		);

		hash.Add(
			RoundManager.SandboxCenterLayoutIndex
		);

		hash.Add(
			RoundManager.SandboxSpawnBLayoutIndex
		);

		hash.Add( OMRClientPerformance.BotDebugOverlay );
		if ( RoundManager.Mode == SessionMode.Sandbox && OMRClientPerformance.BotDebugOverlay )
		{
			hash.Add( SandboxBot?.Difficulty );
			hash.Add( (int)( RealTime.Now * 5f ) );
		}

		hash.Add(
			ConnectedPlayerCount
		);

		hash.Add(
			LocalPlayerState?.
				PreMatchState
		);

		hash.Add(
			LocalWeaponName
		);

		hash.Add(
			LocalClipText
		);

		hash.Add(
			LocalClipCapacityText
		);

		hash.Add(
			LocalWeaponIsReloading
		);

		// Combat feedback is intentionally short-lived. Include quantized opacity
		// in Razor invalidation so the DOM can fade and, crucially, remove the
		// indicator/hitmarker when its absolute lifetime expires.
		hash.Add(
			LocalCombatFeedback?.HitmarkerVisible ?? false
		);

		hash.Add(
			(int)MathF.Round(
				(LocalCombatFeedback?.HitmarkerOpacity ?? 0f) * 100f
			)
		);

		hash.Add(
			LocalCombatFeedback?.DamageIndicatorVisible ?? false
		);

		hash.Add(
			(int)MathF.Round(
				(LocalCombatFeedback?.DamageIndicatorOpacity ?? 0f) * 100f
			)
		);

		hash.Add(
			LocalCombatFeedback?.LastHitWasKill ?? false
		);

		hash.Add(
			LocalCombatFeedback?.LastHitWasHeadshot ?? false
		);

		hash.Add(
			LocalCombatFeedback?.HasDirectionalDamageSource ?? false
		);

		// The Revision 11 directional arc follows the source continuously around
		// the player. Quantize to two-degree steps so camera turns update the cue
		// without forcing needless sub-degree Razor rebuilds.
		hash.Add(
			(int)MathF.Round(
				(LocalCombatFeedback?.DamageAngleDegrees ?? 0f) / 2f
			)
		);

		return hash.ToHashCode();
	}
}