UI/GameHud.razor
@using Sandbox
@using Sandbox.UI
@using Sandbox.Network
@using Prototype
@using System
@using System.Collections.Generic
@using System.Threading.Tasks
@using System.Linq
@namespace Prototype
@inherits PanelComponent
<root style="pointer-events:none;" class="phase-@Game?.LocalPhase.ToString().ToLowerInvariant()">
@if ( Game is null )
{
<div class="loading">BOOTING DASHER</div>
}
else if ( Game.LocalPhase == RunPhase.Lobby )
{
<div class="main-menu" style="filter:brightness(@GameProfile.Settings.Brightness);">
<div class="browser-tabs">
<button class="browser-tab @(MenuClass(MenuPage.Play))" style="@MenuTabStyle(MenuPage.Play)" onmouseover="@(() => HoverMenuTab(MenuPage.Play))" onmouseout="@(() => HoverMenuTab(null))" onclick="@(() => OpenPage(MenuPage.Play))"><MenuGlyph Kind="home" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Play</label></button>
<button class="browser-tab @(MenuClass(MenuPage.Heroes))" style="@MenuTabStyle(MenuPage.Heroes)" onmouseover="@(() => HoverMenuTab(MenuPage.Heroes))" onmouseout="@(() => HoverMenuTab(null))" onclick="@(() => OpenPage(MenuPage.Heroes))"><MenuGlyph Kind="skills" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Skill Tree</label></button>
<button class="browser-tab @(MenuClass(MenuPage.Leaderboard))" style="@MenuTabStyle(MenuPage.Leaderboard)" onmouseover="@(() => HoverMenuTab(MenuPage.Leaderboard))" onmouseout="@(() => HoverMenuTab(null))" onclick="@(() => OpenPage(MenuPage.Leaderboard))"><MenuGlyph Kind="trophy" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Leaderboard</label></button>
<button class="browser-tab @(MenuClass(MenuPage.Wiki))" style="@MenuTabStyle(MenuPage.Wiki)" onmouseover="@(() => HoverMenuTab(MenuPage.Wiki))" onmouseout="@(() => HoverMenuTab(null))" onclick="@(() => OpenPage(MenuPage.Wiki))"><MenuGlyph Kind="news" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">News</label></button>
<button class="browser-tab @(MenuClass(MenuPage.Settings))" style="@MenuTabStyle(MenuPage.Settings)" onmouseover="@(() => HoverMenuTab(MenuPage.Settings))" onmouseout="@(() => HoverMenuTab(null))" onclick="@(() => OpenPage(MenuPage.Settings))"><MenuGlyph Kind="settings" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Settings</label></button>
<button class="browser-close" onclick="@(() => _confirmExit = true)"><MenuGlyph Kind="close" /></button>
</div>
<main class="menu-content" style="@TabMotion">
@if (_page == MenuPage.Play)
{
<div class="home-layout">
<div class="home-room">
<LobbyRoomPreview />
<div class="home-title"><label class="home-logo" style="font-size:64px; font-weight:700; color:#f5f8fa; letter-spacing:8px;">DASHER</label><label class="home-subtitle" style="font-size:20px; color:#f1dca6; letter-spacing:1px;">@(Game.LocalPartyId.Length > 0 ? CurrentParty.Name : "YOUR SOLO LOBBY")</label></div>
<div class="home-slots">
@for(var i=0;i<4;i++)
{
var members=HomeMembers; var id=i<members.Length ? members[i] : "";
<div class="home-slot">
@if(id.Length>0){<div class="home-nameplate"><img class="steam-avatar" src="@AvatarFor(id)" /><div class="home-player-text"><label class="home-player-name" style="font-size:22px; font-weight:600; letter-spacing:0px; color:#f5f8fa;">@PlayerName(id)</label>@if(Game.ReadyFor(id)){<label class="ready-state" style="font-size:14px; font-weight:700; color:#77e9b3; letter-spacing:1px;">READY</label>}</div></div>}
else {<label class="empty-slot" style="font-size:18px; color:#d2e1e9; letter-spacing:1px;">@(Game.LocalPartyId.Length>0 ? "OPEN SLOT" : "+")</label>}
</div>
}
</div>
@if(Game.LocalPartyId.Length==0){<button class="host-action" onclick="@(() => _createPrompt=true)"><MenuGlyph Kind="plus" /><label style="font-size:26px; font-weight:600; letter-spacing:0px; color:#f5f8fa;">Host Lobby</label></button>}
<div class="home-ready"><label class="countdown" style="font-size:22px; color:#f5f8fa; letter-spacing:1px;">@(Game.CountdownFor(Game.LocalPartyId)>=0 ? $"MATCH STARTS IN {Math.Ceiling(Game.CountdownFor(Game.LocalPartyId))}" : "")</label><button class="primary" disabled="@(Game.Phase != RunPhase.Lobby || Game.SessionBusy)" onclick="@Game.ToggleLobbyReady"><MenuGlyph Kind="@(Game.ReadyFor(LocalConnectionId) ? "close" : "check")" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">@(Game.ReadyFor(LocalConnectionId) ? "Cancel ready" : "Ready")</label></button></div>
@if(Game.IsOnline){<button class="home-leave" onclick="@Game.LeaveLobby"><MenuGlyph Kind="leave" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Leave lobby</label></button>}
</div>
<div class="home-browser"><label class="browser-heading" style="font-size:28px; font-weight:700; color:#f5f8fa; letter-spacing:1px;">LOBBIES</label><TextEntry class="menu-field" Placeholder="Search lobbies..." Value:bind="@_lobbySearch" />
<button class="server-refresh" disabled="@(Game.LobbySearchBusy || Game.SessionBusy)" onclick="@RefreshLobbies"><label>@(Game.LobbySearchBusy ? "Searching..." : "Refresh lobbies")</label></button><div class="home-lobby-list">@foreach(var lobby in AvailableLobbies){<div class="home-lobby-row"><div class="home-lobby-info"><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">@lobby.Name</label><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">@lobby.MemberCount / 4 @(lobby.Started ? " / In run" : "") @(lobby.Locked ? "• Password" : "")</label></div><button disabled="@(lobby.IsFull || lobby.Started || Game.SessionBusy || lobby.Id==Game.ServerPartyId)" onclick="@(() => JoinLobby(lobby))"><MenuGlyph Kind="join" /><label style="font-size:20px; font-weight:500; letter-spacing:0px; color:#edf4f8;">Join</label></button></div>}
@if(!AvailableLobbies.Any()){<label class="esc-empty">@Game.LobbySearchStatus</label>}</div></div>
</div>
}
else if (_page == MenuPage.Lobby)
{
<section class="menu-page"><div class="page-head"><div><div class="page-kicker">MULTIPLAYER</div><h1>LOBBY BROWSER</h1></div><button onclick="@RefreshLobbies">@(_lobbyBusy ? "SCANNING..." : "REFRESH")</button></div>
<div class="lobby-search"><label>SEARCH BY NAME</label><TextEntry class="menu-field" MaxLength="@(32)" Placeholder="All waiting lobbies" Value:bind="@_lobbySearch" /></div>
<div class="lobby-head"><span class="lobby-name-cell">ROOM</span><span class="lobby-players-cell">PLAYERS</span><span class="lobby-ping-cell">PING</span><span class="lobby-access-cell">ACCESS</span></div>
<div class="lobby-browser">
@foreach(var lobby in AvailableLobbies)
{
<button class="lobby-entry" onclick="@(() => JoinLobby(lobby))"><span class="lobby-name-cell"><b>@lobby.Name</b><small>#@ShortLobbyId(lobby)</small></span><strong class="lobby-players-cell">@lobby.MemberCount / @LobbyTeamCap(lobby)</strong><strong class="lobby-ping-cell">IN-GAME</strong><strong class="lobby-access-cell">@(lobby.IsFull ? "FULL" : lobby.Locked ? "LOCKED" : "OPEN")</strong></button>
}
@if(!_lobbyBusy && !AvailableLobbies.Any()){<div class="empty-list">NO WAITING LOBBIES FOUND</div>}
</div><div class="menu-status">@_menuStatus</div>
</section>
}
else if (_page == MenuPage.Settings)
{
<SettingsPanel />
}
else if (_page == MenuPage.Heroes)
{
<SkillTreePanel />
}
else if (_page == MenuPage.Leaderboard)
{
<LeaderboardPanel />
}
else
{
<section class="menu-page news-page">
<label class="news-title" style="font-size:36px; font-weight:700; letter-spacing:0px; color:#f3f7fa;">NEWS</label>
<article class="update-card">
<label class="update-date" style="font-size:13px; font-weight:600; letter-spacing:2px; color:#83d9e5; width:100%; white-space:normal; flex-shrink:0;">SEPTEMBER 12, 2026</label>
<label class="update-title" style="font-size:32px; font-weight:700; letter-spacing:0px; color:#f3f7fa; width:100%; white-space:normal; flex-shrink:0;">Headshots, Combat Feedback & Bunny Hopping</label>
<label class="update-intro" style="font-size:18px; font-weight:400; letter-spacing:0px; color:#b7cad6; width:100%; white-space:normal; flex-shrink:0;">Aim for the head, feel every hit, and build speed with chained jumps.</label>
<div class="update-section">
<label class="update-heading" style="font-size:16px; font-weight:700; letter-spacing:1px; color:#f3f7fa; width:100%; white-space:normal; flex-shrink:0;">HEADSHOTS</label>
<label class="update-text" style="font-size:18px; font-weight:400; letter-spacing:0px; color:#c4d3de; width:100%; white-space:normal; flex-shrink:0;">Headshots now deal 1.5x damage. Headshot damage numbers appear above the crosshair, while body hit numbers appear on either side.</label>
</div>
<div class="update-section">
<label class="update-heading" style="font-size:16px; font-weight:700; letter-spacing:1px; color:#f3f7fa; width:100%; white-space:normal; flex-shrink:0;">HITMARKERS & SOUND</label>
<label class="update-text" style="font-size:18px; font-weight:400; letter-spacing:0px; color:#c4d3de; width:100%; white-space:normal; flex-shrink:0;">Body hits and headshots now have distinct hitmarkers and sounds. Taking damage triggers a brief flash around the screen edges and an impact sound.</label>
</div>
<div class="update-section">
<label class="update-heading" style="font-size:16px; font-weight:700; letter-spacing:1px; color:#f3f7fa; width:100%; white-space:normal; flex-shrink:0;">CUSTOMIZE YOUR FEEDBACK</label>
<label class="update-text" style="font-size:18px; font-weight:400; letter-spacing:0px; color:#c4d3de; width:100%; white-space:normal; flex-shrink:0;">Open Settings > Damage numbers to toggle hitmarkers and choose separate colors for body hits and headshots. You can also toggle and recolor the incoming damage effect. Live previews show your choices before you apply them. These toggles only affect visuals; sounds stay enabled.</label>
</div>
<div class="update-section">
<label class="update-heading" style="font-size:16px; font-weight:700; letter-spacing:1px; color:#f3f7fa; width:100%; white-space:normal; flex-shrink:0;">BUNNY HOPPING</label>
<label class="update-text" style="font-size:18px; font-weight:400; letter-spacing:0px; color:#c4d3de; width:100%; white-space:normal; flex-shrink:0;">Hold Jump while moving to chain jumps. Each consecutive jump adds 12% movement speed, up to a 60% bonus. Stopping, running into a wall, or staying grounded for more than 0.15 seconds resets the chain.</label>
<label class="update-highlight" style="font-size:18px; font-weight:600; letter-spacing:0px; color:#f2c777; width:100%; white-space:normal; flex-shrink:0;">Dash preserves your jump chain and accumulated speed bonus. Keep hopping after a dash to continue your run!</label>
</div>
</article>
</section>
}
</main>
<footer class="menu-footer"><span>@Game.Status</span><span>DASHER · CO-OP</span></footer>
@if(_createPrompt || _confirmExit){<div class="password-backdrop"><div class="password-dialog card">
@if(_createPrompt){<label class="esc-subtitle">CREATE LOBBY</label><TextEntry class="menu-field" MaxLength="@(32)" Placeholder="Lobby name" Value:bind="@_lobbyName" /><TextEntry class="menu-field" MaxLength="@(24)" Placeholder="Password (optional)" Value:bind="@_hostPassword" /><button class="primary" onclick="@CreateLobby"><label>Create</label></button><button onclick="@(() => _createPrompt=false)"><label>Cancel</label></button>}
else {<label class="esc-subtitle">OPEN S&BOX MENU?</label><button onclick="@ShowSboxMenu"><label>Exit</label></button><button onclick="@(() => _confirmExit=false)"><label>Cancel</label></button>}
</div></div>}
@if (_passwordPromptOpen)
{
<div class="password-backdrop">
<div class="password-dialog card">
<div class="page-kicker">PROTECTED LOBBY</div>
<h2>@_selectedLobby.Name</h2>
<p>Enter the lobby password to connect.</p>
<TextEntry class="menu-field" MaxLength="@(24)" Placeholder="Password" Value:bind="@_joinPassword" />
<div class="dialog-actions"><button onclick="@CancelPassword">CANCEL</button><button class="primary" onclick="@ConfirmPassword">CONNECT →</button></div>
@if (!string.IsNullOrEmpty(_passwordError)) { <span class="password-error">@_passwordError</span> }
</div>
</div>
}
</div>
}
else
{
@if ( HasDungeonMap )
{
<div class="minimap">
<div class="map-title">MAP SEED @Game.Seed</div>
<div class="map-canvas">
@foreach ( var link in Game.Dungeon.Links )
{
<div class="map-bridge" style="left:@BridgeX(link)%; top:@BridgeY(link)%; width:@BridgeWidth(link)%; height:@BridgeHeight(link)%;"></div>
}
@foreach ( var room in Game.Dungeon.Rooms )
{
<div class="map-room @(IsCurrentRoom(room) ? "active" : "")" style="left:@RoomMapX(room)%; top:@RoomMapY(room)%; width:@RoomMapSize()%; height:@RoomMapSize()%;"></div>
}
@foreach ( var enemy in Scene.GetAll<EnemyBrain>() )
{
<div class="map-dot enemy-dot" style="left:@WorldMapX(enemy.WorldPosition)%; top:@WorldMapY(enemy.WorldPosition)%;"></div>
}
@foreach ( var obstacle in Game.Dungeon.Obstacles )
{
<div class="map-obstacle @(obstacle.IsWall ? "wall" : "block")" style="left:@ObstacleX(obstacle)%; top:@ObstacleY(obstacle)%; width:@ObstacleWidth(obstacle)%; height:@ObstacleHeight(obstacle)%;"></div>
}
@foreach ( var player in Scene.GetAll<FpsPlayer>() )
{
<div class="map-dot player-dot" style="left:@WorldMapX(player.WorldPosition)%; top:@WorldMapY(player.WorldPosition)%;"></div>
}
@if ( Game.ChestReady || Game.RewardAvailable ) { <div class="map-dot chest-dot" style="left:@WorldMapX(Game.RewardChestPosition)%; top:@WorldMapY(Game.RewardChestPosition)%;"></div> }
@if ( Game.PortalReady ) { <div class="map-dot portal-dot" style="left:@WorldMapX(Game.PortalPosition)%; top:@WorldMapY(Game.PortalPosition)%;"></div> }
</div>
</div>
}
<div class="run-score-box"><div class="run-score-label">RUN SCORE</div><div class="run-score-number">@Game.Score.ToString("N0")</div></div><div class="coin-wallet"><div class="coin-heading">COINS</div><div class="coin-total">@Game.LocalRunCoins.ToString("N0")</div></div>
<div class="enemy-overlay">
@foreach ( var enemy in VisibleEnemies )
{
var screen = EnemyScreenPosition(enemy);
<div class="screen-enemy-vitals @(enemy.Spec.Role == EnemyRole.Boss ? "boss" : "")" style="left:@(screen.x*100f)%; top:@(screen.y*100f)%;">
<div class="screen-enemy-name">@enemy.Spec.Role.ToString().ToUpperInvariant()</div>
<div class="screen-enemy-bar"><div class="fill" style="width:@EnemyHealthPercent(enemy)%"></div><span>@MathF.Ceiling(enemy.Vitality.Health) / @MathF.Ceiling(enemy.Vitality.MaxHealth)</span></div>
</div>
}
</div>
<div class="journey"><div class="journey-row"><div class="journey-level">LEVEL @Game.Level</div><div class="journey-room">ROOM @(Game.RoomNumber + 1) / 7</div></div><div class="journey-steps">@for(var step=0;step<7;step++){<div class="journey-step @(step < Game.RoomNumber ? "done" : step == Game.RoomNumber ? "current" : "")"></div>}</div><div class="journey-status">@(Game.EnemiesAlive > 0 ? $"{Game.EnemiesAlive} ENEMIES REMAIN" : "PASSAGE OPEN")</div></div>
@if ( Game.RunPlayers.Count() > 1 )
{
<div class="squad-hud">
@foreach ( var member in Game.RunPlayers.OrderBy(x => x.PlayerName) )
{
<div class="squad-member @(member.Vitality?.IsDead == true ? "fallen" : "")">
<div class="squad-name"><div class="speaker-name"><b>@member.PlayerName</b>@if (member.Components.Get<PartyVoice>() is { } voice && voice.ActiveSpeaker) { <div class="voice-mic"><div class="mic-capsule"></div><div class="mic-cradle"></div><div class="mic-stem"></div></div> }</div><span>@(member.Vitality?.IsDead == true ? "SPECTATING" : member == Player ? "YOU" : $"{MathF.Ceiling(member.Vitality?.Health ?? 0)} HP")</span></div>
<div class="hud-track"><div class="meter-fill" style="width:@(Math.Clamp((member.Vitality?.Health ?? 0) / MathF.Max(1, member.Vitality?.MaxHealth ?? 100)*100,0,100))%"></div></div>
</div>
}
</div>
}
@if ( Player is not null && !Player.IsSpectating )
{
<div class="minimal-vitals @(HealthPercent <= 25 ? "danger" : "")">
<div class="minimal-health-line"><label class="minimal-number" style="font-size:42px; font-weight:700; letter-spacing:0px; color:#f5f7f8;">@MathF.Ceiling(Player.Vitality?.Health ?? 0)</label><label class="minimal-unit" style="font-size:21px; font-weight:500; letter-spacing:0px; color:#b4c1cc;">/ @MathF.Ceiling(Player.Vitality?.MaxHealth ?? 100)</label></div>
@if(Player.Buffs?.LifeCharges > 0) { <div class="life-charge-badge"><MenuGlyph Kind="shield" /><label style="font-size:16px; font-weight:600; letter-spacing:0px; color:#e9c77d;">@(Player.Buffs.LifeCharges.ToString())</label></div> }
<div class="minimal-health-track"><div class="minimal-health-fill" style="width:@HealthPercent%"></div></div>
</div>
<div class="minimal-abilities">
<div class="minimal-ability @(Player.DashCooldownRemaining > 0 ? "cooling" : "")">
<div class="minimal-timer">@(Player.DashCooldownRemaining > 0 ? $"{MathF.Ceiling(Player.DashCooldownRemaining * 10f) / 10f:0.0}s" : "")</div>
<div class="minimal-icon dash-icon"><div class="dash-chevron first"></div><div class="dash-chevron second"></div></div><div class="minimal-key">SHIFT</div>
</div>
<div class="minimal-ability @(Player.UltimateCooldown > 0 ? "cooling" : "")">
<div class="minimal-timer">@(Player.UltimateCooldown > 0 ? $"{MathF.Ceiling(Player.UltimateCooldown)}s" : "")</div>
<div class="minimal-icon surge-icon"><div class="sun-ring"></div><div class="sun-core"></div><div class="sun-ray" style="transform:rotate(0deg);"></div><div class="sun-ray" style="transform:rotate(45deg);"></div><div class="sun-ray" style="transform:rotate(90deg);"></div><div class="sun-ray" style="transform:rotate(135deg);"></div><div class="sun-ray" style="transform:rotate(180deg);"></div><div class="sun-ray" style="transform:rotate(225deg);"></div><div class="sun-ray" style="transform:rotate(270deg);"></div><div class="sun-ray" style="transform:rotate(315deg);"></div></div><div class="minimal-key">E</div>
</div>
</div>
<div class="minimal-ammo @(Player.Weapon?.Ammo <= 3 ? "emptying" : "")">
<div class="minimal-health-line"><label class="minimal-number" style="font-size:42px; font-weight:700; letter-spacing:0px; color:#f5f7f8;">@Player.Weapon?.Ammo</label><label class="minimal-unit" style="font-size:21px; font-weight:500; letter-spacing:0px; color:#b4c1cc;">/ @Player.Weapon?.Spec.Magazine</label></div>
@if ( Player.Weapon?.Reloading == true )
{ <div class="minimal-reload-track"><div class="minimal-reload-fill" style="width:@(Player.Weapon.ReloadProgress*100)%"></div></div> }
</div>
}
@if ( Player?.IsSpectating == true )
{
<div class="spectator-banner"><b>SPECTATING � @(Player.SpectatorTarget?.PlayerName ?? "WAITING FOR TEAM")</b><span>LMB: NEXT PLAYER � RMB: PREVIOUS PLAYER</span></div>
}
else if (GameProfile.Settings.ShowCrosshair) { <div class="custom-crosshair"><CrosshairView Settings="@GameProfile.Settings" /></div> }
@if (Player is not null && !Player.IsSpectating)
{
@if (GameProfile.Settings.ShowHitmarker && Time.Now - Player.LastHitAt < .22f)
{
<HitmarkerView Headshot="@Player.LastHitHeadshot" Hue="@(Player.LastHitHeadshot ? GameProfile.Settings.HitmarkerHeadHue : GameProfile.Settings.HitmarkerBodyHue)" Visibility="@(1f-Math.Clamp((Time.Now-Player.LastHitAt)/.22f,0f,1f))" />
}
@if (GameProfile.Settings.ShowHurtEffect && Time.Now - Player.LastHurtAt < .5f)
{
<HurtEffectView Hue="@GameProfile.Settings.HurtEffectHue" Visibility="@(Player.HurtStrength*(1f-Math.Clamp((Time.Now-Player.LastHurtAt)/.5f,0f,1f)))" />
}
}
@if(Player is not null && GameProfile.Settings.ShowDamageNumbers)
{
<div class="damage-popup-layer">
@foreach(var popup in Player.DamagePopups.Where(x => Time.Now - x.Started < .7f))
{
<label class="damage-popup" style="@DamagePopupStyle(popup)">[email protected](popup.Damage)</label>
}
</div>
}
@if ( Game.Phase == RunPhase.Defeat )
{
<div class="result card result-summary">
<label class="summary-brand">DASHER</label>
<label class="summary-title" style="font-size:30px; font-weight:700; letter-spacing:0px; color:#f4f8fb; text-align:center; width:100%; margin-bottom:20px;">TEAM ELIMINATED</label>
<div class="summary-score"><label class="summary-caption">RUN SCORE</label><label style="font-size:48px; font-weight:700; letter-spacing:0px; color:#f4f8fb;">@Game.Score.ToString("N0")</label></div>
<div class="summary-stats">
<div class="summary-stat-cell"><label class="summary-caption">LEVEL</label><label class="summary-stat">@Game.Level</label></div>
<div class="summary-stat-cell"><label class="summary-caption">ROOM</label><label class="summary-stat">@(Game.RoomNumber + 1) / 7</label></div>
<div class="summary-stat-cell"><label class="summary-caption">TIME</label><label class="summary-stat">@FormatTime(Game.FinalRunSeconds)</label></div>
<div class="summary-stat-cell"><label class="summary-caption">KILLS</label><label class="summary-stat">@Game.RunKills</label></div>
</div>
<div class="summary-team"><label class="summary-caption">TEAM</label><label class="summary-names">@Game.RunTeamNames</label></div>
<div class="summary-coins">
<div class="summary-coin-cell"><label class="summary-money">@SettlementBefore.ToString("N0")</label><label class="summary-caption">BEFORE</label></div>
<label class="summary-operator">+</label>
<div class="summary-coin-cell"><label class="summary-money">@(SettlementProgress < 1f ? SettlementRemaining.ToString("N0") : _settlementEarned.ToString("N0"))</label><label class="summary-caption">EARNED</label></div>
<label class="summary-operator">=</label>
<div class="summary-coin-cell summary-wallet"><label style="font-size:32px; font-weight:700; letter-spacing:0px; color:#f0cc70;">@SettlementShown.ToString("N0")</label><label class="summary-caption">@(SettlementProgress < 1f ? "ADDING COINS..." : "YOUR WALLET")</label></div>
</div>
<div class="summary-actions"><button onclick="@ShowRunLeaderboard"><label style="font-size:16px; font-weight:600; letter-spacing:0px; color:#f4f8fb;">SHOW LEADERBOARD</label></button><button class="primary" onclick="@Game.OneMoreRound"><label style="font-size:20px; font-weight:600; letter-spacing:0px; color:#f4f8fb;">@Game.RematchButtonText</label></button><button onclick="@Game.Restart"><label style="font-size:16px; font-weight:600; letter-spacing:0px; color:#f4f8fb;">RETURN TO LOBBY</label></button></div>
</div>
}
else if ( Game.RewardAvailable )
{
<div class="reward card chest-reference">
<label class="chest-heading">BOSS CHEST</label><div class="eyebrow"> · CHOOSE ONE</div>
<label class="chest-level">LEVEL @Game.Level CLEARED</label><label class="chest-instruction">CHOOSE ONE - THIS RUN ONLY</label>
<div class="reward-options">
@foreach(var buff in Player.Buffs.Choices)
{
<button class="chest-choice [email protected]" onclick="@(() => Game.ClaimBuff(buff.Id, Game.Seed))"><div class="buff-icon">@foreach(var stroke in BuffIcons.Lines(buff.Id)){<div class="buff-stroke" style="@BuffIcons.Style(stroke)"></div>}</div><label class="chest-buff-title">@buff.Name</label><label class="chest-buff-description">@ChestBuffDescription(buff)</label><label class="chest-buff-rank">@(buff.Id == "lifeline" ? $"CHARGES: {Player.Buffs.LifeCharges} + 1" : $"RANK {Player.Buffs.Rank(buff.Id)+1}")</label><div class="chest-select"><label>SELECT</label></div></button>
}
</div>
</div>
}
else if ( Game.ChestReady ) { <div class="portal-hint chest-hint">APPROACH THE BOSS CHEST · PRESS E</div> }
else if ( Game.PortalReady ) { <div class="portal-hint">@(Game.AllRewardsChosen ? "PRESS E AT THE PORTAL" : "WAITING FOR TEAM REWARDS") - READY @Game.PortalEntered / @Game.PortalRequired ALIVE</div> }
else if ( Game.EnemiesAlive == 0 ) { <div class="room-clear">ROOM CLEAR — FOLLOW THE OPEN PASSAGE</div> }
}
@if (_showRunBoard && Game?.LocalPhase == RunPhase.Defeat)
{
<div class="run-board-backdrop"><div class="run-board-dialog">
<label style="font-size:28px; font-weight:700; letter-spacing:0px; color:#f4f8fb;">LEADERBOARD</label>
<label class="ranking-subtitle">@RunCategory.ToUpperInvariant() / LATEST TEAM RUNS</label><label class="ranking-status">@_runBoardStatus</label>
<div class="ranking-heading"><label class="ranking-place">RANK</label><label class="ranking-team">TEAM</label><label class="ranking-score">SCORE</label></div>
<div class="ranking-list">
@foreach(var row in _runBoardRows) { <div class="run-board-row @(IsCurrentRunRow(row) ? "self" : "")"><label class="ranking-place">#@row.Rank</label><div class="ranking-team"><label class="ranking-names">@TeamRowName(row)</label>@if(IsCurrentRunRow(row)){<label class="ranking-you">YOUR TEAM</label>}</div><label class="ranking-score">@row.Value.ToString("N0")</label></div> }
@if(!_runBoardBusy && _runBoardRows.Length == 0){<label class="ranking-note">No results to display yet.</label>}
</div>
<label class="ranking-note">Latest submitted result per team representative. Separate rankings for Solo, Duo, Trio and Squad.</label>
<button disabled="@_runBoardBusy" onclick="@ShowRunLeaderboard"><label style="font-size:16px; letter-spacing:0px;">@(_runBoardBusy ? "LOADING..." : "REFRESH")</label></button><button class="primary" onclick="@(() => _showRunBoard = false)"><label style="font-size:17px; letter-spacing:0px;">BACK TO RESULTS</label></button></div></div>
}
@if (Game?.IsRunMenuOpen == true && (Game.LocalPhase is RunPhase.Combat or RunPhase.Reward))
{
<div class="pause-backdrop" style="pointer-events:all;">
<div class="@(_pauseSettings && !_confirmLeave ? "run-settings-full" : "pause-dialog") @(_confirmLeave ? "esc-subpage" : "")">
@if (_confirmLeave)
{
<label class="esc-subtitle">LEAVE THE TEAM?</label>
<label class="esc-description">Your run will end. You will return to the main menu and keep the @Game.LocalRunCoins coins earned so far.</label>
<label class="esc-description">Your name stays in the team's leaderboard entry. You cannot rejoin this run.</label>
<button class="esc-leave" onclick="@ConfirmLeaveRun"><label>Leave team</label></button>
<button onclick="@(() => _confirmLeave = false)"><label>Cancel</label></button>
}
else if (_pauseSettings)
{
<SettingsPanel InRun="@true" BackRequested="@BackFromRunSettings" />
}
else
{
<div class="esc-brand"><label class="esc-logo">DASHER</label><label class="esc-caption">RUN MENU</label></div>
<div class="pause-actions"><button class="esc-resume" onclick="@CloseRunMenu"><label>Resume</label></button><button class="esc-settings" onclick="@(() => _pauseSettings = true)"><label>Settings</label></button></div>
<div class="pause-voice">
<div class="esc-voice-heading"><label>VOICE CHAT</label><label class="esc-talk">Hold T to talk</label></div>
@foreach (var member in Game.RunPlayers.Where(p => p != Player).OrderBy(p => p.PlayerName))
{
var voice = member.Components.Get<PartyVoice>();
if (voice is not null)
{
<div class="esc-member @(voice.PersonalVolume == 0 ? "muted" : voice.ActiveSpeaker ? "speaking" : "")">
<div class="esc-member-name"><div class="esc-mic"><div class="esc-mic-head"></div><div class="esc-mic-cup"></div><div class="esc-mic-stem"></div>@if(voice.PersonalVolume == 0){<div class="esc-mic-slash"></div>}</div><label>@member.PlayerName</label></div>
<div class="esc-volume-line"><SliderControl class="esc-slider" Min="@(0f)" Max="@(2f)" Step="@(.1f)" Value="@voice.PersonalVolume" ShowTextEntry="@false" OnValueChanged="@((float value) => voice.AdjustVolume(value - voice.PersonalVolume))" /><label class="esc-percent">@($"{voice.PersonalVolume*100:0}%")</label></div>
<div class="esc-volume-footer"><label>0 — 200%</label><button class="esc-reset" onclick="@(() => voice.AdjustVolume(1f - voice.PersonalVolume))"><label>Reset</label></button></div>
</div>
}
}
@if (!Game.RunPlayers.Any(p => p != Player)) { <label class="esc-empty">No other teammates in this run.</label> }
</div>
<div class="esc-footer"><button class="esc-leave" onclick="@(() => _confirmLeave = true)"><label>Leave team</label></button><label class="pause-note">Match continues while menu is open.</label></div>
}
</div>
</div>
}
@if (Game?.SessionBusy == true)
{
<div class="session-loading"><div class="session-loading-card"><label class="session-loading-title">@Game.SessionStatus</label><label>Please wait...</label></div></div>
}
@if (Game is not null && !string.IsNullOrEmpty(Game.ConnectionError))
{
<div class="session-loading"><div class="session-loading-card"><label class="session-loading-title">Connection</label><label>@Game.ConnectionError</label><button onclick="@Game.ClearConnectionError"><label>OK</label></button></div></div>
}
</root>
@code {
private string DamagePopupStyle(FpsPlayer.DamagePopup popup)
{
var age = Math.Clamp(Time.Now-popup.Started,0f,.7f);
var size = GameProfile.Settings.DamageTextSize;
var left = popup.Slot%2 == 0;
var x = left ? -310f : 70f;
var y = (popup.Slot/2-1)*(size+48f)-size/2f-age/.7f*28f;
if (popup.Headshot)
{
x = -120f;
y = -70f-size-popup.Slot*(size+10f)-age/.7f*40f;
}
var opacity = age < .15f ? 1f : 1f-(age-.15f)/.55f;
return HudAppearance.DamageStyle(GameProfile.Settings) + FormattableString.Invariant($"left:{x}px; top:{y}px; width:240px; height:{size+6}px; opacity:{opacity}; text-align:{(popup.Headshot ? "center" : left ? "right" : "left")};");
}
private void BackFromRunSettings() { _pauseSettings = false; StateHasChanged(); }
private string ChestBuffDescription(ChestBuff buff)
{
if (buff.Id != "armor") return buff.Description;
var defense = Player.TotalDefense;
var before = 100f * (1f - BuffRules.ReducedDamage(1f, defense));
var after = 100f * (1f - BuffRules.ReducedDamage(1f, defense + RunBuffs.ArmorPerRank));
return FormattableString.Invariant($"Incoming damage reduction: {before:0.#}% → {after:0.#}%");
}
int TabIndex(MenuPage page) => page switch { MenuPage.Play=>0, MenuPage.Heroes=>1, MenuPage.Leaderboard=>2, MenuPage.Wiki=>3, _=>4 };
bool _createPrompt, _confirmExit;
float _slideStart = -10f; int _slideDirection=1;
string TabMotion => $"transform:translateX({(_slideDirection*40f*MathF.Pow(1f-Math.Clamp((Time.Now-_slideStart)/.28f,0f,1f),2f)).ToString(System.Globalization.CultureInfo.InvariantCulture)}px);";
string[] HomeMembers => Game.LocalPartyId.Length>0 ? CurrentParty.Members.ToArray() : new[]{LocalConnectionId};
string AvatarFor(string id) => $"avatar:{Connection.All.FirstOrDefault(c=>c.Id.ToString()==id)?.SteamId ?? Sandbox.Game.SteamId}";
enum MenuPage { Play, Lobby, Heroes, Settings, Leaderboard, Wiki }
PrototypeGame Game => PrototypeGame.Instance;
FpsPlayer Player => Game?.LocalPlayer;
MenuPage _page = MenuPage.Play;
string _lobbyName = "Dasher Co-op";
string _hostPassword = "";
string _joinPassword = "";
string _lobbySearch = "";
string _passwordError = "";
string _boardCategory = "Solo";
string _heroId = "warden";
string _menuStatus = "NETWORK READY";
bool _lobbyBusy;
bool _pauseSettings, _confirmLeave;
void CloseRunMenu() { Game.IsRunMenuOpen = false; _pauseSettings = _confirmLeave = false; }
void ConfirmLeaveRun() { _page = MenuPage.Play; Game.LeaveRun(); }
bool _passwordPromptOpen;
PartyLobbyView _selectedLobby;
readonly string[] Categories = { "Solo", "Duo", "Trio", "Squad" };
HeroSpec ViewedHero => HeroCatalog.Get(_heroId);
IEnumerable<PartyLobbyView> AvailableLobbies => Game.ServerLobbies.Where(x => (string.IsNullOrWhiteSpace(_lobbySearch) || (x.Name ?? "").Contains(_lobbySearch.Trim(), StringComparison.OrdinalIgnoreCase)));
PartyLobbyView CurrentParty => Game.AllPartyRooms.FirstOrDefault(room => room.Id == Game.LocalPartyId);
string LocalConnectionId => Connection.Local?.Id.ToString() ?? "";
float HealthPercent => Player?.Vitality is null ? 0 : Player.Vitality.Health / Player.Vitality.MaxHealth * 100f;
IEnumerable<EnemyBrain> VisibleEnemies => Scene?.Camera is null ? Enumerable.Empty<EnemyBrain>() : Scene.GetAll<EnemyBrain>().Where(e => e.IsValid && e.Vitality is not null && !e.Vitality.IsDead && IsEnemyHudVisible(e));
bool IsEnemyHudVisible(EnemyBrain enemy)
{
var camera = Scene.Camera; var delta = enemy.WorldPosition - camera.WorldPosition;
if(Vector3.Dot(camera.WorldRotation.Forward,delta)<=20f || delta.Length>1700f)return false;
var screen = camera.PointToScreenNormal(enemy.HealthBarPosition);
if(screen.x < .055f || screen.x > .945f || screen.y < .07f || screen.y > .94f)return false;
var trace=Scene.Trace.Ray(camera.WorldPosition,enemy.WorldPosition+Vector3.Up*45f).WithoutTags("player","projectile").Run();
return !trace.Hit || trace.GameObject==enemy.GameObject || trace.GameObject?.Root==enemy.GameObject;
}
Vector2 EnemyScreenPosition(EnemyBrain enemy) => Scene.Camera.PointToScreenNormal(enemy.HealthBarPosition);
float EnemyHealthPercent(EnemyBrain enemy) => Math.Clamp(enemy.Vitality.Health / MathF.Max(1f,enemy.Vitality.MaxHealth)*100f,0f,100f);
bool HasDungeonMap => Game?.Dungeon?.Rooms is { Count: > 0 };
bool IsCurrentRoom(RoomCell room) => HasDungeonMap && Game.RoomNumber >= 0 && Game.RoomNumber < Game.Dungeon.Rooms.Count && Game.Dungeon.Rooms[Game.RoomNumber].Equals(room);
float MapWorldSpan() { if(!HasDungeonMap)return ProceduralDungeon.RoomSize; var rooms=Game.Dungeon.Rooms; var width=(rooms.Max(r=>r.X)-rooms.Min(r=>r.X))*ProceduralDungeon.RoomSpacing+ProceduralDungeon.RoomSize; var height=(rooms.Max(r=>r.Y)-rooms.Min(r=>r.Y))*ProceduralDungeon.RoomSpacing+ProceduralDungeon.RoomSize; return Math.Max(width,height); }
float MapCenterX() { if(!HasDungeonMap)return 0f; var rooms=Game.Dungeon.Rooms; return (rooms.Min(r=>r.X)+rooms.Max(r=>r.X))*ProceduralDungeon.RoomSpacing*.5f; }
float MapCenterY() { if(!HasDungeonMap)return 0f; var rooms=Game.Dungeon.Rooms; return (rooms.Min(r=>r.Y)+rooms.Max(r=>r.Y))*ProceduralDungeon.RoomSpacing*.5f; }
float RoomMapSize() => 90f*ProceduralDungeon.RoomSize/Math.Max(1f,MapWorldSpan());
float RoomMapX(RoomCell room) => WorldMapX(room.World(ProceduralDungeon.RoomSpacing))-RoomMapSize()*.5f;
float RoomMapY(RoomCell room) => WorldMapY(room.World(ProceduralDungeon.RoomSpacing))-RoomMapSize()*.5f;
float WorldMapX(Vector3 pos) => 50f+(pos.x-MapCenterX())/Math.Max(1f,MapWorldSpan())*90f;
float WorldMapY(Vector3 pos) => 50f-(pos.y-MapCenterY())/Math.Max(1f,MapWorldSpan())*90f;
float ObstacleWidth(ObstacleMapItem obstacle) => obstacle.Size.x/Math.Max(1f,MapWorldSpan())*90f;
float ObstacleHeight(ObstacleMapItem obstacle) => obstacle.Size.y/Math.Max(1f,MapWorldSpan())*90f;
float ObstacleX(ObstacleMapItem obstacle) => WorldMapX(obstacle.WorldCenter)-ObstacleWidth(obstacle)*.5f;
float ObstacleY(ObstacleMapItem obstacle) => WorldMapY(obstacle.WorldCenter)-ObstacleHeight(obstacle)*.5f;
float BridgeX(RoomLink link) => Math.Min(WorldMapX(link.From.World(ProceduralDungeon.RoomSpacing)),WorldMapX(link.To.World(ProceduralDungeon.RoomSpacing)))-(link.From.X==link.To.X?.8f:0f);
float BridgeY(RoomLink link) => Math.Min(WorldMapY(link.From.World(ProceduralDungeon.RoomSpacing)),WorldMapY(link.To.World(ProceduralDungeon.RoomSpacing)))-(link.From.Y==link.To.Y?.8f:0f);
float BridgeWidth(RoomLink link) => link.From.Y==link.To.Y ? Math.Abs(WorldMapX(link.From.World(ProceduralDungeon.RoomSpacing))-WorldMapX(link.To.World(ProceduralDungeon.RoomSpacing))) : 1.6f;
float BridgeHeight(RoomLink link) => link.From.X==link.To.X ? Math.Abs(WorldMapY(link.From.World(ProceduralDungeon.RoomSpacing))-WorldMapY(link.To.World(ProceduralDungeon.RoomSpacing))) : 1.6f;
MenuPage? _hoveredMenuTab;
void HoverMenuTab(MenuPage? page) { if (_hoveredMenuTab == page) return; _hoveredMenuTab = page; StateHasChanged(); }
bool IsMenuTabSelected(MenuPage page) => _page == page || (page == MenuPage.Play && _page == MenuPage.Lobby);
string MenuClass(MenuPage page) => IsMenuTabSelected(page) ? "selected-tab" : "";
string MenuTabStyle(MenuPage page) => IsMenuTabSelected(page)
? "background-color:#1c3544; border-bottom:3px solid #6fe5ef;"
: _hoveredMenuTab == page
? "background-color:#1c3544; border-bottom:3px solid transparent;"
: "background-color:transparent; border-bottom:3px solid transparent;";
void OpenPage(MenuPage page) { _slideDirection=TabIndex(page)>TabIndex(_page)?1:-1; _slideStart=Time.Now; _page=page; StateHasChanged(); }
void OpenLobbyPage() { _page=MenuPage.Lobby; _=RefreshLobbies(); }
void CreateLobby() { _createPrompt=false; Game.HostLobby(_lobbyName,_hostPassword); _menuStatus="CREATING LOBBY..."; }
async Task RefreshLobbies()
{
if(_lobbyBusy) return;
_lobbyBusy=true; _menuStatus="SCANNING..."; StateHasChanged();
await Game.RefreshServerLobbies();
_menuStatus = AvailableLobbies.Any() ? $"{AvailableLobbies.Count()} WAITING LOBBIES" : "NO WAITING LOBBIES";
_lobbyBusy=false; StateHasChanged();
}
void JoinLobby(PartyLobbyView lobby)
{
if(lobby.IsFull){_menuStatus="LOBBY FULL (4/4)";return;}
if(lobby.Started){_menuStatus="RUN ALREADY STARTED";return;}
if(lobby.Locked)
{
_selectedLobby=lobby; _joinPassword=""; _passwordError=""; _passwordPromptOpen=true; StateHasChanged(); return;
}
ConnectToLobby(lobby);
}
void ConfirmPassword()
{
_passwordPromptOpen=false; JoinSelectedParty(_selectedLobby);
}
void CancelPassword(){_passwordPromptOpen=false;_joinPassword="";_passwordError="";StateHasChanged();}
void ConnectToLobby(PartyLobbyView lobby)=>JoinSelectedParty(lobby);
void JoinSelectedParty(PartyLobbyView lobby){Game.JoinParty(lobby.Id,_joinPassword);_menuStatus=$"JOINING · {lobby.Name}";StateHasChanged();}
string ShortLobbyId(PartyLobbyView lobby)=>lobby.Id.Length<=6?lobby.Id:lobby.Id[^6..];
int LobbyTeamCap(PartyLobbyView lobby)=>4;
string PlayerName(string id)=>Connection.All.FirstOrDefault(connection=>connection.Id.ToString()==id)?.DisplayName ?? "PLAYER";
void ChangeSetting(string setting,float delta)
{
var s=GameProfile.Settings;
if(setting=="fov")s.FieldOfView=Math.Clamp(s.FieldOfView+delta,75f,120f);
else if(setting=="brightness")s.Brightness=Math.Clamp(s.Brightness+delta,.65f,1.5f);
else if(setting=="sound")s.SoundVolume=Math.Clamp(s.SoundVolume+delta,0f,1f);
else if(setting=="sensitivity")s.MouseSensitivity=Math.Clamp(s.MouseSensitivity+delta,.25f,3f);
GameProfile.SaveSettings(); StateHasChanged();
}
void SelectHeroView(string id){_heroId=id;StateHasChanged();}
void SelectViewedHero(){GameProfile.SelectHero(_heroId);_menuStatus=$"{ViewedHero.Name} SELECTED";StateHasChanged();}
void BuySkill(SkillSpec skill){_menuStatus=GameProfile.BuySkill(skill)?$"UPGRADED · {skill.Name}":"NOT ENOUGH COINS";StateHasChanged();}
RenderFragment SettingRow(string name,string value,Action decrease,Action increase) => @<div class="setting-row"><div><b>@name</b><span>@value</span></div><div><button onclick="@decrease">−</button><button onclick="@increase">+</button></div></div>;
string FormatTime(float seconds)=>$"{(int)seconds/60:00}:{(int)seconds%60:00}";
void SelectBoard(string category){_boardCategory=category;_=RefreshBoard();StateHasChanged();}
async Task RefreshBoard(){await GameProfile.RefreshGlobal(_boardCategory);StateHasChanged();}
void ShowSboxMenu(){_confirmExit=false;OpenSystemMenu();}
bool _allowSystemMenu;
void OpenSystemMenu(){_allowSystemMenu=true;Sandbox.Game.Overlay.ShowPauseMenu();}
protected override void OnStart(){GameProfile.Load();_heroId=HeroCatalog.Get(GameProfile.Progress.SelectedHero).Id;}
private float _settlementStart = -1f;
private int _settlementBefore, _settlementEarned;
private float SettlementProgress => Math.Clamp((Time.Now - _settlementStart - .8f) / 1.8f, 0f, 1f);
private int SettlementBefore => _settlementBefore;
private int SettlementRemaining => _settlementEarned - (int)MathF.Round(_settlementEarned * SettlementProgress);
private int SettlementShown => _settlementBefore + (int)MathF.Round(_settlementEarned * SettlementProgress);
private bool _showRunBoard;
private string _runBoardStatus = "";
private Sandbox.Services.Leaderboards.Board2.Entry[] _runBoardRows = Array.Empty<Sandbox.Services.Leaderboards.Board2.Entry>();
private string RunCategory => new[] { "Solo", "Duo", "Trio", "Squad" }[Math.Clamp(Game.RunMemberIds.Split(',', StringSplitOptions.RemoveEmptyEntries).Length,1,4)-1];
private readonly Dictionary<long, Dictionary<string, string>> _teamRowMetadata = new();
private string TeamRowData(Sandbox.Services.Leaderboards.Board2.Entry row, string key)
=> _teamRowMetadata.TryGetValue(row.SteamId, out var data) ? data.GetValueOrDefault(key, "") : "";
private async Task LoadTeamRow(Sandbox.Services.Leaderboards.Board2.Entry row)
{
if (string.IsNullOrWhiteSpace(row.DataUrl)) return;
try
{
using var timeout = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(8));
var json = await Sandbox.Http.RequestStringAsync(row.DataUrl, cancellationToken: timeout.Token);
using var doc = System.Text.Json.JsonDocument.Parse(json);
var data = new Dictionary<string,string>();
foreach(var key in new[] { "team", "run" })
if(doc.RootElement.TryGetProperty(key, out var value)) data[key] = value.ToString();
_teamRowMetadata[row.SteamId] = data;
}
catch { }
}
private string TeamRowName(Sandbox.Services.Leaderboards.Board2.Entry row)
{
var team = TeamRowData(row, "team");
return string.IsNullOrWhiteSpace(team) ? row.DisplayName : team;
}
private bool _runBoardBusy;
private bool IsCurrentRunRow(Sandbox.Services.Leaderboards.Board2.Entry row) => row.SteamId == Game.RunRankingOwner && row.Value == Game.Score && TeamRowData(row, "run") == Game.CoinRunId;
private async Task ShowRunLeaderboard()
{
if (_runBoardBusy) return;
_runBoardBusy = true;
_showRunBoard = true; _runBoardStatus = "LOADING YOUR RUN POSITION...";
try
{
var board = Sandbox.Services.Leaderboards.GetFromStat($"dasher_{RunCategory.ToLowerInvariant()}_team_last_score");
board.SetAggregationLast(); board.SetSortDescending(); board.FilterByNone(); board.MaxEntries = 9; board.CenterOnSteamId(Game.RunRankingOwner);
await board.Refresh();
_runBoardRows = board.Entries ?? Array.Empty<Sandbox.Services.Leaderboards.Board2.Entry>();
_teamRowMetadata.Clear();
foreach (var task in _runBoardRows.Select(LoadTeamRow).ToArray()) await task;
var me = _runBoardRows.FirstOrDefault(x => x.SteamId == Game.RunRankingOwner);
_runBoardStatus = me.SteamId != 0 && me.Value == Game.Score && TeamRowData(me, "run") == Game.CoinRunId ? $"YOUR RUN: #{me.Rank} / {Game.Score:N0} POINTS" : "RESULT NOT YET AVAILABLE - REFRESH IN A MOMENT";
}
catch { _runBoardRows = Array.Empty<Sandbox.Services.Leaderboards.Board2.Entry>(); _runBoardStatus = "LEADERBOARD UNAVAILABLE - TRY REFRESH"; }
_runBoardBusy = false;
StateHasChanged();
}
protected override void OnUpdate()
{
if (Game is not null && Game.LocalPhase is RunPhase.Lobby or RunPhase.Defeat) { Game.IsRunMenuOpen = false; _pauseSettings = _confirmLeave = false; }
if (Game?.LocalPhase == RunPhase.Defeat)
{
if (_settlementStart < 0f)
{
GameProfile.BankCoins(Game.CoinRunId, Game.LocalRunCoins);
_settlementEarned = Game.LocalRunCoins;
_settlementBefore = Math.Max(0, GameProfile.Progress.Coins - _settlementEarned);
_settlementStart = Time.Now;
}
}
else _settlementStart = -1f;
if(Input.EscapePressed)
{
if(_allowSystemMenu)_allowSystemMenu=false;
else
{
Input.EscapePressed=false; Sandbox.Game.Overlay.CloseAll(true);
if (Game is not null && Game.LocalPhase is RunPhase.Combat or RunPhase.Reward)
{
if (_confirmLeave) _confirmLeave = false;
else if (_pauseSettings) _pauseSettings = false;
else Game.IsRunMenuOpen = !Game.IsRunMenuOpen;
}
else if (_confirmExit || _createPrompt) { _confirmExit=false; _createPrompt=false; } else if (_page != MenuPage.Play) OpenPage(MenuPage.Play);
}
}
StateHasChanged();
}
int LobbyUiHash()
{
if(Game is null) return 0;
var hash=new HashCode();
hash.Add(Game.LocalPartyId);
hash.Add(Game.ConnectionError); hash.Add(Game.SessionBusy); hash.Add(Game.SessionStatus);
hash.Add(Game.LobbySearchBusy); hash.Add(Game.LobbySearchStatus);
foreach(var lobby in Game.ServerLobbies) { hash.Add(lobby.Id); hash.Add(lobby.Name); hash.Add(lobby.MemberCount); hash.Add(lobby.Locked); hash.Add(lobby.Started); }
foreach(var member in HomeMembers) { hash.Add(member); hash.Add(Game.ReadyFor(member)); }
hash.Add((int)Math.Ceiling(Game.CountdownFor(Game.LocalPartyId)));
return hash.ToHashCode();
}
protected override int BuildHash() => HashCode.Combine( HashCode.Combine( Game?.LocalPhase, Game?.Level, Game?.RoomNumber, Game?.EnemiesAlive, Game?.Score, Game?.Status, Player?.Vitality?.Health, Player?.Weapon?.Ammo ), _page, _boardCategory, HashCode.Combine(_menuStatus, GameProfile.Progress.Coins, LobbyUiHash()), HashCode.Combine(_hoveredMenuTab, Player?.SpectatorTarget?.PlayerName, Game?.LocalPhase != RunPhase.Lobby ? (int)(Time.Now * 20f) : 0), Game?.PortalEntered, Game?.PortalRequired );
}