A Blazor-like Sandbox UI component that renders race standings as pill rows. It queries RaceStandings from the current game mode or falls back to player car components, builds a 3-position window around the local player (with a detached leader), and renders avatars, colors, ghost rows and initials.
@using Sandbox;
@using Sandbox.UI;
@using Machines.Components;
@using Machines.GameModes;
@namespace Machines.UI
@inherits Panel
<root class="standings-rail">
@foreach ( var row in GetRows() )
{
var standing = row.Standing;
var position = standing.Position;
@if ( standing.IsGhost )
{
<div class="standing-row ghost @(row.Detached ? "detached" : "")">
<div class="row-pos">@position</div>
<div class="row-pill">
<div class="color-bar" style="background-color: rgba(180, 180, 255, 0.8)"></div>
<span class="row-name">@(standing.GhostName ?? "Ghost")</span>
</div>
</div>
}
else
{
var color = standing.Color;
var r = (int)(color.r * 255);
var g = (int)(color.g * 255);
var b = (int)(color.b * 255);
var barStyle = $"background-color: rgba({r}, {g}, {b}, 1)";
<div class="standing-row @(row.IsLocal ? "local" : "") @(row.Detached ? "detached" : "")">
<div class="row-pos">@position</div>
<div class="row-pill">
<div class="color-bar" style="@barStyle"></div>
@if ( row.IsLocal )
{
@if ( standing.IsBot )
{
<div class="player-initial" style="@barStyle">@GetInitial( standing.Name )</div>
}
else
{
var steamId = standing.SteamId != 0 ? standing.SteamId : Sandbox.Game.SteamId.ValueUnsigned;
<div class="avatar-wrap">
<image class="avatar" src="avatar:@(steamId)" />
<div class="avatar-fade"></div>
</div>
}
}
<span class="row-name">@standing.Name</span>
</div>
</div>
}
}
</root>
@code
{
private struct Row
{
public RaceStandings.Standing Standing;
public bool IsLocal;
public bool Detached; // leader shown above the local window
}
/// <summary>
/// Three positions around the local player, plus detached leader when outside that window.
/// </summary>
private List<Row> GetRows()
{
var standings = GetStandings()
.Where( s => s.IsGhost || s.Car.IsValid() )
.ToList();
// Hide the ghost once real racers exist; it's just clutter then.
var hasOtherRacers = standings.Count( s => !s.IsGhost ) > 1;
standings = standings.Where( s => !s.IsGhost || !hasOtherRacers ).ToList();
if ( standings.Count == 0 )
return new List<Row>();
var maxPos = standings.Max( s => s.Position );
var local = standings.FirstOrDefault( s => !s.IsGhost && s.IsLocal );
var localPos = local.IsLocal ? local.Position : 1;
// Three-position window centred on the local player.
var lo = Math.Clamp( localPos - 1, 1, Math.Max( 1, maxPos - 2 ) );
var hi = Math.Min( maxPos, lo + 2 );
var leaderDetached = lo > 1;
return standings
.Where( s => s.Position == 1 && leaderDetached || (s.Position >= lo && s.Position <= hi) )
.OrderBy( s => s.Position )
.Select( s => new Row
{
Standing = s,
IsLocal = !s.IsGhost && s.IsLocal,
Detached = leaderDetached && s.Position == 1,
} )
.ToList();
}
private List<RaceStandings.Standing> GetStandings()
{
var standings = BaseGameMode.Current?.GetComponent<RaceStandings>();
if ( standings.IsValid() )
return standings.GetStandings();
// No race mode: fall back to slot order.
return Scene.GetAllComponents<Machines.Player.Car>()
.Where( c => c.IsValid() && c.Slot >= 0 )
.OrderBy( c => c.Slot )
.Select( ( c, i ) => new RaceStandings.Standing
{
Car = c,
Slot = c.Slot,
Name = c.DisplayName,
Color = c.PlayerColor,
IsBot = c.IsBot,
IsLocal = c.IsLocalPlayer,
Position = i + 1,
GapToAhead = -1f
} )
.ToList();
}
private string GetInitial( string name )
{
return string.IsNullOrEmpty( name ) ? "?" : name[0].ToString().ToUpper();
}
protected override int BuildHash()
{
return HashCode.Combine( Connection.All.Count, (Time.Now * 10f).FloorToInt() );
}
}