A Razor UI component for the in-game HUD. It renders HUD child components, manages leaderboard state and periodic refreshes, tracks score flashing, and exposes properties like current entries, selected mode and filter.
@using System
@using System.Linq
@using System.Threading.Tasks
@using Sandbox
@using Sandbox.UI
@using Sandbox.Services
@namespace Breakout
@inherits PanelComponent
@if ( Game.IsValid() )
{
<root>
<HudTopbar Game=@Game Flashing=@ScoreFlashing />
<Controls Game=@Game />
<BannerOverlay Game=@Game />
<DamageFlash Game=@Game />
<MenuScreen Game=@Game
Entries=@Entries
Me=@(MyEntry())
Loading=@boardLoading
ActiveFilter=@activeFilter
FilterChanged=@( ( ScoreFilter f ) => SetFilter( f ) )
SelectedMode=@selectedMode
ModeChanged=@( ( GameMode m ) => SetMode( m ) )
BoardTitle=@BoardTitle
Version=@boardVersion />
<MessageOverlay Game=@Game
Me=@(MyEntry())
Entries=@Entries
Loading=@boardLoading
ActiveFilter=@activeFilter
FilterChanged=@( ( ScoreFilter f ) => SetFilter( f ) )
BoardTitle=@BoardTitle
Version=@boardVersion />
<div class="frame"></div>
</root>
}
@code
{
/// <summary>
/// The game the whole HUD reads its state from.
/// </summary>
[Property] public BreakoutGame Game { get; set; }
private ScoreEntry[] board;
private ScoreEntry[] meBoard;
private bool boardLoading;
private int boardVersion;
// Each fetch bumps its request id. When the async result comes back we only keep it if the id
// still matches, so a slow older fetch can't overwrite the results of a newer one.
private int boardRequest;
private int meRequest;
private ScoreFilter activeFilter = ScoreFilter.Global;
private GameMode selectedMode = GameMode.Classic;
private GameState lastState = (GameState)(-1);
private int lastScore = -1;
private TimeSince sinceScored = 1000f;
private const float ScoreFlashTime = 0.3f;
protected override void OnStart()
{
Game ??= Scene.Get<BreakoutGame>();
_ = RefreshAll();
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( !Game.IsValid() )
return;
if ( Game.Score != lastScore )
{
if ( Game.Score > lastScore && lastScore >= 0 )
sinceScored = 0f;
lastScore = Game.Score;
}
if ( Game.State != lastState )
{
lastState = Game.State;
if ( Game.State == GameState.GameOver )
activeFilter = ScoreFilter.Global;
if ( Game.State is GameState.Menu or GameState.GameOver )
_ = RefreshAll();
}
}
private async Task RefreshAll()
{
boardVersion++;
await Task.WhenAll( RefreshBoard(), RefreshMe() );
}
private async Task RefreshBoard()
{
int req = ++boardRequest;
boardLoading = true;
boardVersion++;
try
{
var result = await BreakoutLeaderboards.Fetch( activeFilter, 10, BoardMode );
// Only accept this result if no newer refresh has started in the meantime.
if ( req == boardRequest )
board = result;
}
catch ( Exception e )
{
Log.Warning( $"Leaderboard refresh failed: {e.Message}" );
}
finally
{
if ( req == boardRequest )
boardLoading = false;
boardVersion++;
}
}
private async Task RefreshMe()
{
int req = ++meRequest;
try
{
var result = await BreakoutLeaderboards.Fetch( ScoreFilter.AroundMe, 5, BoardMode );
if ( req == meRequest )
meBoard = result;
}
catch ( Exception e )
{
Log.Warning( $"Leaderboard (me) refresh failed: {e.Message}" );
}
finally
{
boardVersion++;
}
}
private void SetFilter( ScoreFilter filter )
{
if ( activeFilter == filter )
return;
// Clear the old rows right away so the previous filter's list doesn't linger on screen
// while the new one loads.
activeFilter = filter;
board = null;
boardLoading = true;
boardVersion++;
_ = RefreshBoard();
}
private void SetMode( GameMode mode )
{
if ( selectedMode == mode )
return;
selectedMode = mode;
board = null;
meBoard = null;
boardLoading = true;
boardVersion++;
_ = RefreshAll();
}
private GameMode BoardMode => (Game.IsValid() && Game.State != GameState.Menu) ? Game.Mode : selectedMode;
private string BoardTitle => BoardMode switch
{
GameMode.Arcade => "ARCADE LEADERBOARD",
GameMode.Weekly => "WEEKLY CHALLENGE",
_ => "CLASSIC LEADERBOARD"
};
private ScoreEntry[] Entries => board ?? Array.Empty<ScoreEntry>();
private long MySteamId => Sandbox.Utility.Steam.SteamId;
private ScoreEntry? MyEntry()
{
var entries = meBoard;
if ( entries == null )
return null;
long me = MySteamId;
foreach ( var e in entries )
{
if ( e.SteamId == me )
return e;
}
return null;
}
bool ScoreFlashing => sinceScored < ScoreFlashTime;
protected override int BuildHash() => System.HashCode.Combine( Game?.Score, Game?.Lives, Game?.Level, Game?.State, (int)activeFilter, (int)selectedMode, boardVersion, ScoreFlashing );
}