ui/LobbyBoard.razor

A Razor UI component for the lobby board. It shows an optional lobby message, a launch countdown when the lobby is launching, or a grid of players with avatar/bot badge, name, and win count, and computes a build hash for change detection.

Networking
@using Sandbox
@using Sandbox.UI
@using System.Linq
@inherits PanelComponent

<root>
    <div class="board">
        @if (LobbyManager.HasActiveLobbyMessage)
        {
            <div class="message-banner">@LobbyManager.LobbyMessage</div>
        }

        @if (LaunchSeconds > 0)
        {
            <div class="title">Starting in</div>
            <div class="launch-number">@LaunchSeconds</div>
        }
        else
        {
            <div class="title">Players</div>
            <div class="grid">
                @foreach (PlayerReadyState player in Scene.GetAllComponents<PlayerReadyState>())
                {
                    <div class="cell @(player.IsReady ? "ready" : "notready")">
                        @if (player.IsBot())
                        {
                            <div class="avatar bot-avatar" style="background-color: @(BotColor(player.DisplayName));">
                                @(BotInitial(player.DisplayName))
                            </div>
                        }
                        else
                        {
                            <img class="avatar" src="avatar:@(player.Network.Owner.SteamId)" />
                        }
                        <div class="name">@player.DisplayName</div>
                        <div class="wins" title="Wins">
                            <div class="trophy">🏆</div>
                            <div class="count">@player.WinCount</div>
                        </div>
                    </div>
                }
            </div>
        }
    </div>
</root>

@code {
    private int LaunchSeconds
    {
        get
        {
            LobbyManager mgr = LobbyManager.Current;
            if (mgr is null || !mgr.IsLaunching) return 0;
            return System.Math.Max(1, (int)System.MathF.Ceiling(mgr.LaunchSecondsRemaining));
        }
    }

    private static string BotInitial(string name) =>
        string.IsNullOrEmpty(name) ? "?" : name.Substring(0, 1).ToUpper();

    // Stable per-name HSL so each bot keeps the same badge color across ticks.
    private static string BotColor(string seed)
    {
        int hue = System.Math.Abs(seed.GetHashCode()) % 360;
        return $"hsl({hue}, 55%, 45%)";
    }

    protected override int BuildHash()
    {
        IEnumerable<string> parts = Scene.GetAllComponents<PlayerReadyState>()
            .Select(p => $"{p.DisplayName}:{p.IsReady}:{p.WinCount}");
        return System.HashCode.Combine(string.Join(",", parts), LaunchSeconds, LobbyManager.HasActiveLobbyMessage);
    }
}