A Razor UI panel for game settings. It displays steppers and pip meters for master, SFX, music and ambience volumes, routes clicks to GameSession adjusters, and exposes back/restore defaults actions.
@using Sandbox; @using Sandbox.UI; @namespace Coilgarden @inherits Panel @attribute [StyleSheet( "/UI/GameUi.razor.scss" )] @* Reachable from the main menu and from pause, and returns to whichever of the two it was opened from - GameUi owns that, this panel only knows how to leave. Every change here writes straight through GameSession to disk, so there is no separate "apply" step. The meter is ten pips rather than a bar because a bar needs a sized element and s&box did not apply an inline `style` to one. Pips are class-driven, which does work - and a pip is exactly one tap of the stepper, so the meter shows the size of a click. *@ <root class="scrim"> <div class="card settings-card"> <div class="heading">Settings</div> <div class="prompt">Changes apply immediately and are remembered.</div> <div class="settings-list"> <div class="setting-row"> <span class="setting-name">Master Volume</span> <div class="stepper"> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustMasterVolume, -1 ))>-</div> <div class="meter"> @foreach ( var pip in Pips( Master ) ) { <div class="@pip"></div> } </div> <span class="setting-value">@Percent( Master )</span> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustMasterVolume, 1 ))>+</div> </div> </div> <div class="setting-row"> <span class="setting-name">Effects</span> <div class="stepper"> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustSfxVolume, -1 ))>-</div> <div class="meter"> @foreach ( var pip in Pips( Sfx ) ) { <div class="@pip"></div> } </div> <span class="setting-value">@Percent( Sfx )</span> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustSfxVolume, 1 ))>+</div> </div> </div> <div class="setting-row"> <span class="setting-name">Music</span> <div class="stepper"> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustMusicVolume, -1 ))>-</div> <div class="meter"> @foreach ( var pip in Pips( Music ) ) { <div class="@pip"></div> } </div> <span class="setting-value">@Percent( Music )</span> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustMusicVolume, 1 ))>+</div> </div> </div> <div class="setting-row"> <span class="setting-name">Ambience</span> <div class="stepper"> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustAmbienceVolume, -1 ))>-</div> <div class="meter"> @foreach ( var pip in Pips( Ambience ) ) { <div class="@pip"></div> } </div> <span class="setting-value">@Percent( Ambience )</span> <div [email protected] class="step" onclick=@(() => Step( Session.AdjustAmbienceVolume, 1 ))>+</div> </div> </div> </div> <div class="menu-row"> <div [email protected] class="btn" onclick=@RestoreDefaults>Restore Defaults</div> <div [email protected] class="btn primary" onclick=@Back>Back</div> </div> </div> </root> @code { [Parameter] public GameSession Session { get; set; } /// <summary> /// Named <c>CloseRequested</c> rather than the obvious <c>OnBack</c>: <see cref="Panel"/> /// already has an <c>OnBack( PanelEvent )</c>, and a parameter of that name hides it. /// </summary> [Parameter] public Action CloseRequested { get; set; } private GameSettings S => Session?.Settings; private float Master => S?.MasterVolume ?? GameConfig.MasterVolume; private float Sfx => S?.SfxVolume ?? GameConfig.SfxVolume; private float Music => S?.MusicVolume ?? GameConfig.MusicVolume; private float Ambience => S?.AmbienceVolume ?? GameConfig.AmbienceVolume; /// <summary> /// Routes a stepper tap through its adjuster and plays the click - one place for both, so /// no button here can move a value silently. /// </summary> private void Step( Action<float> adjust, int direction ) { UiSound.Click(); adjust( direction * GameConfig.SettingsVolumeStep ); } private void RestoreDefaults() { UiSound.Click(); Session?.ResetSettings(); } private void Back() { UiSound.Click(); CloseRequested?.Invoke(); } /// <summary> /// The class for each pip of the meter, lit or unlit. Rounded rather than truncated, so a /// value sitting between two pips shows the nearer one rather than always reading low. /// </summary> private static IEnumerable<string> Pips( float value ) { var lit = (int)MathF.Round( value / GameConfig.MaxSettingsVolume * GameConfig.SettingsMeterPips ); for ( var i = 0; i < GameConfig.SettingsMeterPips; i++ ) { yield return i < lit ? "pip on" : "pip"; } } private static string Percent( float value ) => $"{(int)MathF.Round( value * 100f )}%"; protected override int BuildHash() => System.HashCode.Combine( Master, Sfx, Music, Ambience ); }