UI/DifficultyPage.razor

A Razor UI panel for a Difficulty settings fullscreen page in the lobby. It renders preset tabs, section tabs, a list of settings with step controls, a right-hand description and comparison column, and handles user interactions by calling DifficultyState and playing UI sounds.

File Access
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@using NZombies;
@inherits Panel

@*
    DIFFICULTY — a fullscreen page over the lobby, laid out like Map select (2026-10-05). The user: "after selecting a map and
    gamemode, i want a new option on the lobby that allows me to customize dificulty of the game … 3 default dificulty presets,
    and then be able to change the individual things in them", then "let's design the ui for it, not wired to any of the option
    yet".

    Left: the title, the three presets as tabs, a line on the chosen one, then the four sections as tabs too ("the same way you
    did easy hard and normal", 2026-10-05) with the chosen section's settings under them, a step down and up on each, then Back and
    Reset. Right: the setting pointed at, its name, what it does, and its value in all three presets with the current one marked.
    Touching a setting makes the difficulty Custom; the preset tab it started from stays lit, and its section tab gets a gold dot.

    ⚠️ WIRED SINCE 2026-10-05, THROUGH `Difficulty`: a game started from the lobby takes these, for everyone in it. The bottom
    right says when they apply; a setting the gamemode has nothing for says so under its description.

    ⚠️ A child Panel of the lobby, as Map select is: no GameObject, no scene edit. The lobby keeps the cursor up while it is open.
*@

@if ( DifficultyState.Showing )
{
    var shown = DifficultyState.Shown;

    <div class="difpage">
        <div class="difbody">

            <div class="difleft">
                <div class="diftitle">Difficulty</div>

                <div class="diftabs">
                    @foreach ( var p in Presets )
                    {
                        var preset = p;
                        <div class="diftab @(DifficultyState.Base == preset ? "on" : "")"
                             onmouseover=@Hover
                             onclick=@( () => Choose( preset ) )>@DifficultyState.NameOf( preset )</div>
                    }

                    @if ( DifficultyState.IsCustom )
                    {
                        <div class="difcustom">Custom</div>
                    }
                </div>
                <div class="difhint">@Hint</div>

                @* ⚠️ THE SECTIONS ARE TABS TOO, THE PRESETS' OWN (2026-10-05): one section's settings at a time. A gold dot on a tab
                   whose settings moved off the preset, since its rows are out of sight. *@
                <div class="diftabs difsections">
                    @foreach ( var g in DifficultyState.Groups )
                    {
                        var group = g;
                        <div class="diftab @(DifficultyState.Section == group ? "on" : "")"
                             onmouseover=@Hover
                             onclick=@( () => PickSection( group ) )>
                            <div class="diftabname">@group</div>
                            @if ( DifficultyState.SectionChanged( group ) )
                            {
                                <div class="difdot"></div>
                            }
                        </div>
                    }
                </div>

                <div class="diflist">
                    @foreach ( var st in DifficultyState.InGroup( DifficultyState.Section ) )
                    {
                        var s = st;
                        <div class="difrow @(s.Id == shown?.Id ? "on" : "") @(DifficultyState.IsChanged( s ) ? "changed" : "")"
                             onmouseover=@( () => Point( s ) )>
                            <div class="diflabel">@s.Label</div>
                            <div class="difarrow @(AtMin( s ) ? "off" : "")" onclick=@( () => Step( s, -1 ) )>‹</div>
                            <div class="difvalue">@DifficultyState.Format( s, DifficultyState.ValueOf( s ) )</div>
                            <div class="difarrow @(AtMax( s ) ? "off" : "")" onclick=@( () => Step( s, 1 ) )>›</div>
                        </div>
                    }
                </div>

                <div class="diffoot">
                    <div class="difbtn" onmouseover=@Hover onclick=@( () => Back() )>‹  Back</div>

                    @if ( DifficultyState.IsCustom )
                    {
                        <div class="difbtn" onmouseover=@Hover onclick=@( () => Reset() )>@ResetText</div>
                    }
                </div>
            </div>

            <div class="difright">
                @if ( shown is not null )
                {
                    <div class="difname">@shown.Label</div>
                    <div class="difdesc">@shown.Description</div>

                    var note = DifficultyState.GamemodeNote( shown );
                    if ( !string.IsNullOrEmpty( note ) )
                    {
                        <div class="difwarn">@note</div>
                    }

                    <div class="difcompare">
                        @foreach ( var p in Presets )
                        {
                            var preset = p;
                            <div class="difcol @(DifficultyState.Base == preset ? "base" : "") @(Matches( shown, preset ) ? "match" : "")">
                                <div class="difcolname">@DifficultyState.NameOf( preset )</div>
                                <div class="difcolval">@DifficultyState.Format( shown, DifficultyState.PresetValue( shown, preset ) )</div>
                            </div>
                        }
                    </div>

                    <div class="difnow @(DifficultyState.IsChanged( shown ) ? "changed" : "")">@NowText( shown )</div>
                }

                @* ⚠️ SAID ON THE PAGE: when these apply, and to whom (`Difficulty.StartMatch`) *@
                <div class="difnote">Takes effect when the game starts, for everyone in it.</div>
            </div>

        </div>
    </div>
}

@code
{
    static DifficultyState.Preset[] Presets => new[]
    {
        DifficultyState.Preset.Easy,
        DifficultyState.Preset.Normal,
        DifficultyState.Preset.Hard,
    };

    /// <summary>The line under the tabs: the preset, what it is for; a custom one, what it started from.</summary>
    static string Hint => DifficultyState.IsCustom
        ? "Custom: changed from " + DifficultyState.NameOf( DifficultyState.Base ) + "."
        : DifficultyState.BlurbOf( DifficultyState.Base );

    static string ResetText => "Reset to " + DifficultyState.NameOf( DifficultyState.Base );

    /// <summary>Under the comparison: the value now, and where it stands against the preset it started from.</summary>
    static string NowText( DifficultyState.Setting s )
    {
        var now = DifficultyState.Format( s, DifficultyState.ValueOf( s ) );
        return DifficultyState.IsChanged( s ) ? "Now " + now + " · changed" : "Now " + now;
    }

    static bool Matches( DifficultyState.Setting s, DifficultyState.Preset p )
        => DifficultyState.Same( DifficultyState.ValueOf( s ), DifficultyState.PresetValue( s, p ) );

    static bool AtMin( DifficultyState.Setting s ) => DifficultyState.ValueOf( s ) <= DifficultyState.MinOf( s ) + 0.001f;

    static bool AtMax( DifficultyState.Setting s ) => DifficultyState.ValueOf( s ) >= DifficultyState.MaxOf( s ) - 0.001f;

    static void Hover() => NZSound.PlayUi( NZSound.UiHover );

    /// <summary>Pointing at a setting describes it on the right. The sound only on a change, as Map select does it.</summary>
    static void Point( DifficultyState.Setting s )
    {
        if ( DifficultyState.Hovered == s.Id ) return;

        DifficultyState.Hovered = s.Id;
        Hover();
    }

    static void Choose( DifficultyState.Preset p )
    {
        NZSound.PlayUi( NZSound.UiClick );
        DifficultyState.Choose( p );
    }

    static void PickSection( string group )
    {
        if ( DifficultyState.Section == group ) return;

        NZSound.PlayUi( NZSound.UiClick );
        DifficultyState.ChooseSection( group );
    }

    static void Step( DifficultyState.Setting s, int dir )
    {
        if ( dir < 0 ? AtMin( s ) : AtMax( s ) ) return;

        NZSound.PlayUi( NZSound.UiClick );
        DifficultyState.Step( s, dir );
    }

    static void Reset()
    {
        NZSound.PlayUi( NZSound.UiClick );
        DifficultyState.Reset();
    }

    static void Back()
    {
        NZSound.PlayUi( NZSound.UiClick );
        DifficultyState.Close();
    }

    // ⚠️ EVERYTHING THE MARKUP READS: the page open, the setting pointed at, the preset, the section, and every edit (`Version`)
    protected override int BuildHash()
        => System.HashCode.Combine( DifficultyState.Showing, DifficultyState.Hovered, DifficultyState.Base, DifficultyState.Section,
            DifficultyState.Version );
}