UI/PlayPage.razor

A Razor UI panel for the game's Play page. It renders lobby UI: game mode tabs, map carousel, ready/unready CTA, host-only settings (laps, bots, pickups), player slots, and a countdown overlay; it also handles navigation back, toggling settings, and UI tick/hash updates.

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

@namespace Machines.UI
@inherits Panel
@attribute [Route( "/play" )]


@{
    var flow = LobbyFlow.Current;
    var state = flow?.State ?? LobbyState.Browsing;
    var selectedMode = flow?.SelectedMode ?? GameModeType.Race;
    var modes = System.Enum.GetValues<GameModeType>();

    var countdownRemaining = state == LobbyState.Countdown && flow != null
        ? System.Math.Max( 0, (int)System.Math.Ceiling( (float)flow.CountdownEnds ) )
        : 0;

    var ctaLabel = state == LobbyState.Countdown ? "HOLD ON" : "READY UP";

    var isLocalReady = flow?.IsLocalReady ?? false;
}

<root class="play-page @(Input.UsingController ? "controller" : "mouse")">
    @if ( modes.Count() > 1 )
    {
        <div class="mode-strip-wrap">
            <div class="mode-strip">
                <InputHint Action="MenuSwitchLeft" class="mode-bumper" />
                @foreach (var m in modes)
                {
                    <div class="mode-tab @(m == selectedMode ? "active" : "")" onclick=@(() => LobbyFlow.Current?.SelectMode(m))>
                        @FormatMode(m)
                    </div>
                }
                <InputHint Action="MenuSwitchRight" class="mode-bumper" />
            </div>
        </div>
    }


    <div class="card-stage">
        <MapCarousel />

        @if ( state != LobbyState.Launching )
        {
            <div class="cta-row">
                <div class="cta-side cta-left">
                    @if ( !isLocalReady )
                    {
                        <div class="back-btn" onclick=@GoBack>
                            <InputHint Action="MenuBack" Dark=@true class="back-glyph" />
                            <span class="back-label">BACK</span>
                        </div>
                    }
                </div>
                <div class="primary-cta @(isLocalReady ? "is-ready" : "")" onclick=@(() => LobbyFlow.Current?.ToggleReady())>
                    <span class="cta-label">@(isLocalReady ? "UNREADY" : ctaLabel)</span>
                    <InputHint Action="@(isLocalReady ? "MenuBack" : "MenuSelect")" class="cta-glyph" />
                </div>
                <div class="cta-side cta-right">
                @if ( Networking.IsHost )
                {
                    <div class="settings-wrap">
                        <div class="settings-btn @(_settingsOpen ? "open" : "")" onclick=@(() => _settingsOpen = !_settingsOpen)>
                            <i class="cog">settings</i>
                        </div>
                        @if ( _settingsOpen )
                        {
                            <div class="settings-panel">
                                <div class="settings-row">
                                    <div class="settings-label">TOTAL LAPS</div>
                                    <div class="stepper">
                                        <div class="stepper-btn @(RaceMode.TotalLaps <= 1 ? "disabled" : "")" onclick=@(() => RaceMode.TotalLaps = int.Clamp( RaceMode.TotalLaps - 1, 1, MaxLaps ))>-</div>
                                        <div class="stepper-value">@RaceMode.TotalLaps</div>
                                        <div class="stepper-btn @(RaceMode.TotalLaps >= MaxLaps ? "disabled" : "")" onclick=@(() => RaceMode.TotalLaps = int.Clamp( RaceMode.TotalLaps + 1, 1, MaxLaps ))>+</div>
                                    </div>
                                </div>
                                <div class="settings-row">
                                    <div class="settings-label">PLAY WITH BOTS</div>
                                    <div class="checkbox @(BaseGameMode.FillWithBots ? "checked" : "")" onclick=@(() => BaseGameMode.FillWithBots = !BaseGameMode.FillWithBots)>
                                        @if ( BaseGameMode.FillWithBots )
                                        {
                                            <span class="check-glyph">✓</span>
                                        }
                                    </div>
                                </div>
                                <div class="settings-row">
                                    <div class="settings-label">ENABLE PICKUPS</div>
                                    <div class="checkbox @(Machines.Items.ItemBox.PickupsEnabled ? "checked" : "")" onclick=@(() => Machines.Items.ItemBox.PickupsEnabled = !Machines.Items.ItemBox.PickupsEnabled)>
                                        @if ( Machines.Items.ItemBox.PickupsEnabled )
                                        {
                                            <span class="check-glyph">✓</span>
                                        }
                                    </div>
                                </div>
                            </div>
                        }
                    </div>
                }
                </div>
            </div>
        }
    </div>

    <div class="bottombar">
        <div class="invite-hint">CLICK A SLOT TO INVITE A FRIEND</div>
        <PlayerSlots class="player-slots-bar" />
    </div>

    @if ( state == LobbyState.Countdown )
    {
        <div class="countdown-overlay">
            <CountdownNumber Value="@countdownRemaining" />
        </div>
    }
</root>

@code
{
    private const int MaxLaps = 6;

    private bool _settingsOpen;
    private bool _wasReady;

    private static string FormatMode( GameModeType m ) => m switch
    {
        _ => m.ToString()
    };

    private void GoBack()
    {
        _settingsOpen = false;

        // Explicit navigate needed; no history exists when returning from a match.
        this.Navigate( "/" );
    }

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

        // Once readied, LobbyFlow owns MenuBack (unready); don't double-handle it.
        var isLocalReady = LobbyFlow.Current?.IsLocalReady ?? false;
        var state = LobbyFlow.Current?.State ?? LobbyState.Browsing;

        // Close settings whenever ready state flips.
        if ( isLocalReady != _wasReady )
        {
            _wasReady = isLocalReady;
            _settingsOpen = false;
        }

        if ( state == LobbyState.Browsing && !isLocalReady && Input.Pressed( "MenuBack" ) )
            GoBack();
    }

    protected override int BuildHash()
    {
        var flow = LobbyFlow.Current;
        var tick = flow?.State == LobbyState.Countdown ? (int)((float)flow.CountdownEnds * 4) : 0;
        var readyHash = 0;
        if ( flow != null )
        {
            foreach ( var c in Connection.All ) readyHash = System.HashCode.Combine( readyHash, flow.Ready.TryGetValue( c.Id, out var r ) && r );
        }
        var settingsHash = System.HashCode.Combine( _settingsOpen, RaceMode.TotalLaps, BaseGameMode.FillWithBots, Machines.Items.ItemBox.PickupsEnabled );
        return System.HashCode.Combine( flow?.State, flow?.SelectedMode, flow?.SelectedMapIdent, tick, readyHash, Input.UsingController, settingsHash );
    }
}