A Blazor/Sandbox UI Panel that renders the pre-game settings UI for a card game. It enumerates GameSetting instances via GameSettings.For(Game), groups them, displays labels and descriptions, and lets the host edit settings (bool, enum, discrete steps, or numeric) which call back into the authoritative CardGame instance and call NotifySettingsChanged().
@namespace CardGames
@using Sandbox
@using Sandbox.UI
@using System
@using System.Linq
@using System.Collections.Generic
@inherits Panel
@*
Pre-game settings, shown in the Waiting lobby. Built entirely from a game's [Setting] properties via
reflection (GameSettings), so any game gets configurable rules for free. The host edits the live,
host-authoritative game instance directly; everyone else sees the chosen values read-only (they sync).
*@
<root class="game-settings">
@if ( Settings.Count > 0 )
{
<div class="head cg-eyebrow">@(CanEdit ? "Table settings" : "Table rules")</div>
@foreach ( var group in Grouped )
{
@if ( !string.IsNullOrEmpty( group.Key ) )
{
<div class="group-head">@group.Key</div>
}
@foreach ( var item in group )
{
var s = item;
<div class="row">
<div class="label">
<span class="name">@s.Label</span>
@if ( !string.IsNullOrEmpty( s.Description ) )
{
<span class="desc">@s.Description</span>
}
</div>
<div class="control">
@if ( !CanEdit )
{
<div class="cg-pill"><span class="amt">@ReadOnly( s )</span></div>
}
else if ( s.Kind == SettingKind.Bool )
{
<div class="cg-group">
<button class="cg-btn @(IsOn( s ) ? "primary" : "")" onclick=@(() => Edit( () => s.Set( Game, true ) ))>On</button>
<button class="cg-btn @(!IsOn( s ) ? "primary" : "")" onclick=@(() => Edit( () => s.Set( Game, false ) ))>Off</button>
</div>
}
else if ( s.Kind == SettingKind.Enum )
{
<div class="cg-group">
@foreach ( var name in s.EnumNames )
{
var n = name;
<button class="cg-btn @(s.GetText( Game ) == n ? "primary" : "")" onclick=@(() => Edit( () => s.Set( Game, n ) ))>@n</button>
}
</div>
}
else if ( s.HasDiscreteSteps )
{
<div class="cg-group">
@foreach ( var v in s.Steps() )
{
var val = v;
<button class="cg-btn @(IsNum( s, val ) ? "primary" : "")" onclick=@(() => Edit( () => s.Set( Game, val ) ))>@NumLabel( val )</button>
}
</div>
}
else
{
<div class="cg-group">
<button class="cg-btn" onclick=@(() => Edit( () => s.Set( Game, Stepped( s, -1 ) ) ))>−</button>
<div class="cg-btn value">@NumLabel( s.GetNumber( Game ) )</div>
<button class="cg-btn" onclick=@(() => Edit( () => s.Set( Game, Stepped( s, +1 ) ) ))>+</button>
</div>
}
</div>
</div>
}
}
}
</root>
@code
{
/// <summary>The live game whose settings we edit (the Waiting-lobby instance).</summary>
[Parameter] public CardGame Game { get; set; }
private IReadOnlyList<GameSetting> Settings => GameSettings.For( Game );
private IEnumerable<IGrouping<string, GameSetting>> Grouped => Settings.GroupBy( s => s.Group );
// Only the host edits - they own the authoritative instance; the values replicate to clients via [Sync].
private bool CanEdit => Networking.IsHost;
private bool IsOn( GameSetting s ) => s.Get( Game ) is true;
private bool IsNum( GameSetting s, float v ) => MathF.Abs( s.GetNumber( Game ) - v ) < 0.001f;
// One step in either direction, clamped to a [Range] when the setting has one.
private float Stepped( GameSetting s, int dir )
{
var v = s.GetNumber( Game ) + dir * s.Step;
if ( s.Max > s.Min ) v = Math.Clamp( v, s.Min, s.Max );
else if ( v < s.Min ) v = s.Min;
return v;
}
// Whole numbers render without a decimal point; fractional steps keep up to two places.
private string NumLabel( float v ) => v == MathF.Round( v ) ? ((int)v).ToString() : v.ToString( "0.##" );
private string ReadOnly( GameSetting s ) => s.Kind switch
{
SettingKind.Bool => s.Get( Game ) is true ? "On" : "Off",
SettingKind.Number => NumLabel( s.GetNumber( Game ) ),
_ => s.GetText( Game ),
};
// Host: apply the edit to the authoritative instance, then let the game react (e.g. re-seat bots).
private void Edit( Action change )
{
if ( !CanEdit || Game is null ) return;
change();
Game.NotifySettingsChanged();
}
protected override int BuildHash()
{
if ( Game is null ) return 0;
var hash = new HashCode();
hash.Add( Networking.IsHost );
foreach ( var s in Settings )
hash.Add( s.Get( Game ) );
return hash.ToHashCode();
}
}