UI/PodiumScreen.razor

A Razor UI component for the podium/results screen in a Race game mode. It builds a list of player rows (position, name, avatar/initial, times, best lap) from RaceMode/RaceStandings and renders standings and winner text, with a debug preview toggle.

NetworkingFile Access
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox;
@using Sandbox.UI;
@using Machines.Components;
@using Machines.GameModes;
@using Machines.Player;

@namespace Machines.UI
@inherits PanelComponent

<root class="podium-screen @(ShowPodium ? "" : "hidden")">
    @if ( ShowPodium )
    {
        var rows = BuildRows();

        <div class="standings">
            <span class="standings-label">STANDINGS</span>
            @foreach ( var r in rows )
            {
                <div class="row @(r.IsLocal ? "you" : "") [email protected]">
                    <div class="pos-circle [email protected]">@r.Position</div>
                    @if ( r.IsBot )
                    {
                        <div class="initial" style="@ColorStyle( r.Color )">@r.Initial</div>
                    }
                    else
                    {
                        <div class="avatar-wrap">
                            <image class="avatar" src="avatar:@(r.SteamId)" />
                            <div class="avatar-fade"></div>
                        </div>
                    }
                    <div class="info">
                        <div class="name-line">
                            <span class="name">@r.Name</span>
                        </div>
                    </div>
                    <div class="times">
                        <span class="total">@FormatTime( r.TotalTime, r.Finished )</span>
                        @if ( r.IsLeader )
                        {
                            <span class="delta leader">leader</span>
                        }
                        else if ( r.Finished )
                        {
                            <span class="delta">+@r.DeltaToLeader.ToString( "F2" )</span>
                        }
                    </div>
                    <div class="best">
                        <span class="best-label">BEST LAP</span>
                        <span class="best-time">@FormatLap( r.BestLap )</span>
                    </div>
                </div>
            }
        </div>

        <div class="winner-section">
            <div class="race-over-pill">★ RACE OVER</div>
            <div class="winner-tape">@WinnerName WINS!</div>
        </div>
    }
</root>

@code
{
    private struct Row
    {
        public int Position;
        public Color Color;
        public string Name;
        public string Initial;
        public bool IsLocal;
        public bool IsBot;
        public ulong SteamId;
        public bool Finished;
        public float TotalTime;
        public float BestLap;
        public float DeltaToLeader;
        public bool IsLeader;
    }

    /// <summary>
    /// Force the results screen on with mock data for styling in the inspector.
    /// </summary>
    [Property]
    public bool DebugPreview { get; set; }

    private bool ShowPodium => DebugPreview || (BaseGameMode.Current is RaceMode race && race.State == GameModeState.Podium);

    private string WinnerName
    {
        get
        {
            if ( BaseGameMode.Current is RaceMode race && race.WinnerSlot >= 0 )
                return race.WinnerName;

            var rows = BuildRows();
            return rows.Count > 0 ? rows[0].Name : "";
        }
    }

    private List<Row> BuildRows()
    {
        var rows = new List<Row>();
        if ( BaseGameMode.Current is not RaceMode race )
            return rows;

        var standings = race.GetComponent<RaceStandings>();
        var ordered = standings.IsValid()
            ? standings.GetStandings().Where( s => !s.IsGhost ).OrderBy( s => s.Position ).ToList()
            : new List<RaceStandings.Standing>();

        // Leader's total time, used to compute deltas.
        var winnerTotal = 0f;
        if ( ordered.Count > 0 )
        {
            var lead = race.GetPlayerState( ordered[0].Slot );
            if ( lead.HasFinished )
                winnerTotal = lead.FinishTime - race.RaceStartTime;
        }

        foreach ( var s in ordered )
        {
            var state = race.GetPlayerState( s.Slot );
            var total = state.HasFinished ? state.FinishTime - race.RaceStartTime : 0f;

            rows.Add( new Row
            {
                Position = s.Position,
                Color = s.Color,
                Name = s.Name,
                Initial = string.IsNullOrEmpty( s.Name ) ? "?" : s.Name[0].ToString().ToUpper(),
                IsLocal = s.IsLocal,
                IsBot = s.IsBot,
                SteamId = s.SteamId != 0 ? s.SteamId : Sandbox.Game.SteamId.ValueUnsigned,
                Finished = state.HasFinished,
                TotalTime = total,
                BestLap = state.BestLapTime,
                DeltaToLeader = state.HasFinished && winnerTotal > 0f ? total - winnerTotal : 0f,
                IsLeader = s.Position == 1
            } );
        }

        return rows;
    }

    private string ColorStyle( Color c )
    {
        var r = (int)(c.r * 255);
        var g = (int)(c.g * 255);
        var b = (int)(c.b * 255);
        return $"background-color: rgba({r}, {g}, {b}, 1)";
    }

    private string FormatTime( float seconds, bool finished )
    {
        if ( !finished )
            return "DNF";

        seconds = MathF.Max( 0f, seconds );
        var mins = (int)(seconds / 60f);
        var secs = seconds % 60f;
        var whole = (int)secs;
        var hundredths = (int)((secs - whole) * 100f);
        return $"{mins:00}:{whole:00}.{hundredths:00}";
    }

    private string FormatLap( float seconds )
    {
        if ( seconds <= 0f || seconds >= float.MaxValue )
            return "--:--.--";

        return FormatTime( seconds, true );
    }

    protected override int BuildHash()
    {
        var state = (int)(BaseGameMode.Current?.State ?? GameModeState.WaitingForPlayers);
        return HashCode.Combine( state, Connection.All.Count, DebugPreview );
    }
}