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();
}
}