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.
@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 );
}
}