UI/Nameplate.razor

A Razor UI component for a player nameplate. It renders player name, optional placement medal, and a chevron, choosing color and name from either a Car component, a GhostPlayer, or fallback properties and hiding/fading logic based on local player and game state.

@using Sandbox;
@using Sandbox.UI;
@using Machines.Player;
@using Machines.Components;
@using Machines.Ghost;
@using Machines.GameModes;

@namespace Machines.UI
@inherits PanelComponent

@{
    var color = GetPlayerColor();
    var playerName = GetPlayerName();
}

<root class="nameplate @(IsFaded() ? "ghost" : "")">
    @if ( !ShouldHide() )
    {
        <div class="info" style="border-color: @color.WithAlpha( 0.9f ).Hex; background-color: @color.WithAlpha( 0.8f ).Hex">
            <div class="row">
                @if ( ShowPlacement )
                {
                    <div class="medal @MedalClass">@(PlacementIndex + 1)</div>
                }
                <span class="player-name">@playerName</span>
            </div>
        </div>
    }

    <div class="chevron" style="color: @color.Hex">▼</div>
</root>

@code
{
    private static readonly Color GhostColor = new( 0.6f, 0.6f, 0.6f );

    // Fallback values when there is no Car/Ghost parent (e.g. podium clones).
    [Property] public string PlayerName { get; set; } = "Player";
    [Property] public Color Color { get; set; } = Color.White;

    /// <summary>
    /// Finishing placement for the medal (0 = gold, 1 = silver, 2 = bronze, -1 = hidden).
    /// </summary>
    [Property] public int PlacementIndex { get; set; } = -1;

    private static readonly string[] MedalClasses = { "gold", "silver", "bronze" };
    private bool ShowPlacement => PlacementIndex >= 0 && PlacementIndex < MedalClasses.Length;
    private string MedalClass => ShowPlacement ? MedalClasses[PlacementIndex] : "";

    private Car Car => GameObject.GetComponentInParent<Car>();
    private GhostPlayer Ghost => GameObject.GetComponentInParent<GhostPlayer>();

    protected override int BuildHash()
    {
        return HashCode.Combine( Car, Ghost, ShouldHide(), PlayerName, Color, PlacementIndex );
    }

    private bool IsFaded() => Ghost.IsValid();

    private bool ShouldHide()
    {
        if ( Ghost.IsValid() )
            return false;

        var car = Car;
        if ( !car.IsValid() || !car.IsLocalPlayer )
            return false;

        // Always show during waiting/countdown.
        if ( BaseGameMode.Current.IsValid() )
        {
            var state = BaseGameMode.Current.State;
            if ( state == GameModeState.WaitingForPlayers || state == GameModeState.Countdown )
                return false;
        }

        // Hide the local player's own nameplate.
        return true;
    }

    private Color GetPlayerColor()
    {
        if ( Ghost.IsValid() )
            return GhostColor;

        var car = Car;
        if ( car.IsValid() && car.Slot >= 0 )
            return car.PlayerColor;

        return Color;
    }

    private string GetPlayerName()
    {
        var ghost = Ghost;
        if ( ghost.IsValid() )
            return string.IsNullOrEmpty( ghost.PlayerName ) ? "Player" : ghost.PlayerName;

        var car = Car;
        if ( car.IsValid() && car.Slot >= 0 )
            return car.DisplayName;

        return PlayerName;
    }
}