UI/Hud.razor

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.

Networking
@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 );
}