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