UI/SoulBoxTuner.razor

A Razor UI component for a developer tuning tool that adjusts SoulBox.PowerupHeight, shows a slider, copy button, and test-drop/reset actions. It creates a host GameObject to render the panel and binds directly to the static PowerupHeight so all boxes update live.

File AccessExternal Download
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@using NZombies;
@inherits PanelComponent

@*
    SOUL BOX TUNER — a slider for where the reward powerup sits, and a Copy button.

    ⛔ A DEV TOOL, NOT A FEATURE. Same reason TradeTableTuner exists: the first height was
    `Spot.Height + 16` = 64 units, which read as "a lot" too high because the placeholder is
    48 tall and the real stone box is about 30. A number judged by eye gets dialled by eye.

    ⚠️ IT EDITS THE STATIC `SoulBox.PowerupHeight` DIRECTLY, and SoulBox.OnUpdate notices it
    changed and moves the live powerup. So every box on the map follows the slider at once and
    nothing here needs a reference to a particular one.

    ⚠️ THE VALUE DOES NOT PERSIST. It is a static, so it survives a hotload but not a restart —
    which is the point of Copy: paste the line back into SoulBox.cs to keep it.
*@

<root class="tuner @(Visible ? "" : "hidden")">
    <div class="panel">

        <div class="head">
            <div class="title">SOUL BOX — REWARD HEIGHT</div>
            <div class="close" onclick=@(() => Hide())>×</div>
        </div>

        @if ( !AnyBox )
        {
            <div class="warn">No soul box standing — nz_soul to place one, nz_soul_rebuild after a code edit.</div>
        }
        else if ( !AnyReward )
        {
            <div class="warn">Nothing to look at — press Drop test reward.</div>
        }

        <div class="row">
            <div class="label">Height</div>
            <SliderControl class="s" Value:bind=@Height Min="@(0f)" Max="@(80f)" Step="@(0.5f)"></SliderControl>
            <div class="val">@Height.ToString( "0.#" )</div>
        </div>

        <div class="out">@Snippet</div>

        <div class="buttons">
            <div class="btn copy" onclick=@(() => Copy())>@CopyLabel</div>
            <div class="btn" onclick=@(() => DropTest())>Drop test reward</div>
            <div class="btn" onclick=@(() => ResetValues())>Reset</div>
        </div>

    </div>
</root>

@code {

    /// <summary>Is the tuner on screen. Driven by `nz_soul_tune`.</summary>
    public static bool Visible { get; set; }

    // ⚠️ BOUND TO THE STATIC RATHER THAN A COPY. A local copy would drift the moment anything else
    // wrote the value, and the slider would then fight it.
    float Height
    {
        get => SoulBox.PowerupHeight;
        set => SoulBox.PowerupHeight = value;
    }

    static bool AnyBox => SoulBoxManager.All().Count > 0;
    static bool AnyReward => SoulBoxManager.All().Any( b => b.HasReward );

    /// <summary>
    /// The line to paste into SoulBox.cs.
    ///
    /// ⚠️ REAL C#, not a report — the whole point is that it goes back into the source without
    /// anyone retyping it.
    /// </summary>
    string Snippet => $"get => _powerupHeight ?? {Height:0.##}f;";

    string CopyLabel { get; set; } = "Copy";

    void Copy()
    {
        Clipboard.SetText( Snippet );
        CopyLabel = "Copied";
        Log.Info( $"[nz-soul] {Snippet}" );
    }

    /// <summary>
    /// Put a reward on the nearest box so there is something to position.
    ///
    /// ⛔ claimRemoves: false — a test drop must NOT delete the box when the powerup is taken or
    /// times out. That is the real behaviour and it would remove the thing being tuned.
    /// </summary>
    void DropTest()
    {
        var boxes = SoulBoxManager.All();
        if ( boxes.Count == 0 ) { Log.Info( "[nz-soul] no box standing" ); return; }

        var me = PlayerCharacters.Local();
        var box = me.IsValid()
            ? boxes.OrderBy( b => me.WorldPosition.Distance( b.WorldPosition ) ).First()
            : boxes[0];

        Log.Info( $"[nz-soul] test reward on box #{box.Index}: {box.SpawnReward( claimRemoves: false )}" );
    }

    /// <summary>
    /// Back to what the source ships, so a bad session can be abandoned.
    ///
    /// ⛔ NOT `Reset` — that hides `Component.Reset()`, the collision this project has already
    /// been bitten by twice.
    ///
    /// ⚠️ THIS MUST TRACK SoulBox.PowerupHeight's OWN DEFAULT. A reset that restores a value the
    /// source no longer ships is worse than no reset.
    /// </summary>
    void ResetValues()
    {
        SoulBox.PowerupHeight = 36f;
        CopyLabel = "Copy";
    }

    static void Hide()
    {
        Visible = false;
        Mouse.Visibility = MouseVisibility.Hidden;
    }

    /// <summary>
    /// `nz_soul_tune [0/1]` — show the slider.
    ///
    /// ⚠️ IT TAKES THE CURSOR, like TradeTableTuner does — a slider cannot be dragged without one,
    /// and Noclip keys off Mouse.Visibility, so leaving it hidden would have V toggling noclip
    /// under the player mid-drag.
    /// </summary>
    [ConCmd( "nz_soul_tune" )]
    public static void TuneCmd( int on = -1 )
    {
        EnsureHost();

        Visible = on < 0 ? !Visible : on != 0;
        Mouse.Visibility = Visible ? MouseVisibility.Visible : MouseVisibility.Hidden;

        Log.Info( $"[nz-soul] tuner {(Visible ? "open" : "closed")}"
            + $" — reward height {SoulBox.PowerupHeight:0.##}" );

        if ( Visible && SoulBoxManager.All().Count == 0 )
            Log.Info( "[nz-soul] no box standing — nz_soul to place one" );
    }

    static GameObject _host;

    /// <summary>
    /// Make sure something is drawing this panel.
    ///
    /// ⛔ THE PANEL HAS NO HOME IN THE SCENE, exactly like TradeTableTuner's. Without this the
    /// razor compiles and never renders: the command reports "open" and nothing appears.
    ///
    /// ⚠️ REBUILT WHENEVER THE OBJECT IS GONE, not once — a GameObject created from code does not
    /// survive a hotload.
    /// </summary>
    static void EnsureHost()
    {
        if ( _host.IsValid() ) return;

        var scene = Game.ActiveScene;
        if ( !scene.IsValid() ) return;

        _host = scene.CreateObject();
        _host.Name = "Soul Box Tuner UI";
        _host.Flags |= GameObjectFlags.NotSaved;

        var screen = _host.Components.Create<ScreenPanel>();
        screen.ZIndex = 80;

        _host.Components.Create<SoulBoxTuner>();

        Log.Info( "[nz-soul] created the tuner's screen panel" );
    }

    // ⚠️ THE VALUE IS IN THE HASH so the readout and the snippet follow the slider. Without it the
    // number freezes while the powerup moves, which reads as the panel being broken.
    protected override int BuildHash()
        => HashCode.Combine( Visible, Height, CopyLabel, AnyBox, AnyReward );
}