UI/LobbyStatusOverlay.razor

A Razor UI panel for the lobby status overlay. It displays the selected map thumbnail and title, player slots, lobby state label, and ready count, updates the thumbnail each tick, positions itself above a revision card, and navigates to the play/map selector on click.

NetworkingFile Access
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Machines.Components;
@using Machines.Resources;

@namespace Machines.UI
@inherits Panel

@{
    var flow = LobbyFlow.Current;
    var map = flow?.SelectedMap;

    var total = Connection.All.Count;
    var ready = ReadyCount();
}

<root class="lobby-status @(Visible ? "" : "hidden") @(Input.UsingController ? "controller" : "mouse")" onclick=@GoToPlay>
    @if ( Visible )
    {
        <div class="ls-map">
            <div class="ls-thumb-wrap">
                <image @ref="ThumbImage" class="ls-thumb" />
                @if ( map?.Thumbnail == null )
                {
                    <div class="ls-thumb-empty">NO IMAGE</div>
                }
            </div>
            <div class="ls-map-info">
                <span class="ls-eyebrow">IN LOBBY</span>
                <span class="ls-title">@(map?.Title ?? "No map")</span>
            </div>
        </div>

        <PlayerSlots class="ls-slots" />

        <div class="ls-status">
            <span class="ls-state">@StateLabel( flow )</span>
            <span class="ls-ready">@ready / @System.Math.Max( total, 1 ) READY</span>
        </div>
    }
</root>

@code
{
    public Sandbox.UI.Image ThumbImage { get; set; }

    // Show when a lobby exists, except on /play which shows the full picker.
    private bool Visible =>
        LobbyFlow.Current is not null
        && MainMenuPanel.Instance?.CurrentUrl != "/play";

    public override void Tick()
    {
        base.Tick();

        if ( ThumbImage != null )
            ThumbImage.Texture = LobbyFlow.Current?.SelectedMap?.Thumbnail;

        StackAbovePatchNotes();
    }

    // Sit above the patch-notes card; explicit px so the CSS transition works both ways.
    private void StackAbovePatchNotes()
    {
        var revision = FindRootPanel()?.Descendants.OfType<RevisionCard>().FirstOrDefault();
        var cardHeight = revision is not null && revision.IsVisible
            ? revision.Box.Rect.Height * ScaleFromScreen + 14f
            : 0f;

        // Nudge up on the main menu home page so it clears the home layout.
        var menuOffset = MainMenuPanel.Instance?.CurrentUrl == "/home" ? 32f : 0f;

        Style.Bottom = Length.Pixels( 56f + cardHeight + menuOffset ); // $deadzone-y (+ card + gap)
    }

    // Click navigates back to the map selector.
    private void GoToPlay()
    {
        this.Navigate( "/play" );
    }

    private static string StateLabel( LobbyFlow flow )
    {
        if ( flow == null )
            return "WAITING";

        return flow.State switch
        {
            LobbyState.Countdown => $"STARTING IN {System.Math.Max( 0, (int)System.Math.Ceiling( (float)flow.CountdownEnds ) )}…",
            LobbyState.Launching => "STARTING…",
            _ => "WAITING"
        };
    }

    private static int ReadyCount()
    {
        var flow = LobbyFlow.Current;
        if ( flow == null )
            return 0;

        return Connection.All.Count( c => flow.Ready.TryGetValue( c.Id, out var r ) && r );
    }

    protected override int BuildHash()
    {
        var flow = LobbyFlow.Current;
        var countdownTick = flow?.State == LobbyState.Countdown ? (int)((float)flow.CountdownEnds * 4) : 0;

        return System.HashCode.Combine(
            Visible,
            flow?.SelectedMapIdent,
            flow?.State,
            Connection.All.Count,
            ReadyCount(),
            countdownTick,
            Input.UsingController );
    }
}