UI/HudTopbar.razor

A Razor UI Panel for the top HUD in a Breakout game. It renders lives, mode, score (with easing animation), level and personal best, and reads state from a BreakoutGame instance each Tick.

Networking
@using System
@using Sandbox
@using Sandbox.UI
@namespace Breakout
@inherits Panel

@if ( Game.IsValid() && Game.State != GameState.Menu )
{
    <root>
        <div class="chip lives">
            <span class="chip-label">LIVES</span>
            <div class="dots">
                @for ( int i = 0; i < LifeSlots; i++ )
                {
                    <div class="dot @(i < Game.Lives ? "on" : "off")"></div>
                }
            </div>
        </div>

        <div class="chip mode @ModeClass">
            <span class="chip-label">@ModeName</span>
        </div>

        <div class="scorebox @(Flashing ? "flash" : "")">
            <div class="score-label">SCORE</div>
            <div class="score-value">@DisplayScore.ToString( "N0" )</div>
        </div>

        <div class="chip level">
            <span class="chip-label">LEVEL</span>
            <span class="level-num">@Game.Level</span>
        </div>

        @if ( bestScore > 0 )
        {
            <div class="chip best">
                <span class="chip-label">BEST</span>
                <span class="best-num">@bestScore.ToString( "N0" )</span>
            </div>
        }
    </root>
}

@code
{
    /// <summary>
    /// The game this bar reads score, lives, level and mode from.
    /// </summary>
    [Parameter] public BreakoutGame Game { get; set; }

    /// <summary>
    /// When true, the score box plays its "just scored" flash.
    /// </summary>
    [Parameter] public bool Flashing { get; set; }

    private double shownScore;
    private bool scoreInit;
    private long bestScore;

    private int LifeSlots => Game.IsValid() ? System.Math.Max( Game.StartingLives, Game.Lives ) : 0;

    private string ModeName => Game.IsValid() ? Game.ModeLabel : "CLASSIC";
    private string ModeClass => Game.IsValid() ? Game.ModeLabel.ToLowerInvariant() : "classic";

    /// <summary>
    /// Runs every frame to refresh the personal best and ease the displayed score up toward the real score.
    /// </summary>
    public override void Tick()
    {
        base.Tick();
        if ( !Game.IsValid() )
            return;

        bestScore = BreakoutLeaderboards.PersonalBest( Game.Mode );

        // Roll the displayed score up toward the real score so it counts up smoothly instead of
        // jumping. If the score drops (a new run resets it), snap straight to the new value.
        double target = Game.Score;
        if ( !scoreInit || target < shownScore )
        {
            shownScore = target;
            scoreInit = true;
        }
        else if ( shownScore < target )
        {
            shownScore += (target - shownScore) * System.Math.Min( 1.0, Time.Delta * 12.0 );
            if ( target - shownScore < 1.0 )
                shownScore = target;
        }
    }

    /// <summary>
    /// The score to actually display, rounded down as it eases up.
    /// </summary>
    private long DisplayScore => (long)System.Math.Floor( shownScore );

    protected override int BuildHash() => System.HashCode.Combine( DisplayScore, Game?.Lives, Game?.Level, Game?.State, Game?.Mode, Flashing, bestScore );
}