UI/TradeTableTuner.razor

A Razor UI component for a developer tuning tool that adjusts how weapons are displayed on all trading tables. It shows sliders for height and Euler angles, copies a C# snippet to clipboard, logs actions, and creates a host ScreenPanel in the active scene so the panel can render.

File Access
@using Sandbox;
@using Sandbox.UI;
@using System;
@using NZombies;
@inherits PanelComponent

@*
    TRADING TABLE TUNER — sliders for where the stored weapon sits, and a Copy button.

    ⛔ A DEV TOOL, NOT A FEATURE. It exists because "the gun is too low and the wrong way round"
    took three round trips to get wrong twice: 24 was measured from the bbox centre, 46 centred the
    mesh in the tabletop. Numbers that are judged by eye should be dialled by eye.

    ⚠️ IT EDITS THE STATICS `TradeTable.ShowHeight` / `ShowAngles` DIRECTLY, and TradeTable.OnUpdate
    notices they changed and re-places the weapon. So every table on the map follows the sliders at
    once, and nothing here needs a reference to a particular one.

    ⚠️ THE VALUES DO NOT PERSIST. They are statics, so they survive a hotload but not a restart —
    which is the point of the Copy button: paste the line back into TradeTable.cs to keep it.
*@

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

        <div class="head">
            <div class="title">TRADING TABLE — WEAPON PLACEMENT</div>
            <div class="close" onclick=@(() => Hide())>×</div>
        </div>

        @if ( !AnyTable )
        {
            <div class="warn">No trading table placed — run nz_trade first.</div>
        }
        else if ( !AnyWeapon )
        {
            <div class="warn">No weapon on the table — run nz_trade_use to put one down.</div>
        }

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

        <div class="row">
            <div class="label">Pitch</div>
            <SliderControl class="s" Value:bind=@Pitch Min="@(-180f)" Max="@(180f)" Step="@(5f)"></SliderControl>
            <div class="val">@Pitch.ToString( "0.#" )</div>
        </div>

        <div class="row">
            <div class="label">Yaw</div>
            <SliderControl class="s" Value:bind=@Yaw Min="@(-180f)" Max="@(180f)" Step="@(5f)"></SliderControl>
            <div class="val">@Yaw.ToString( "0.#" )</div>
        </div>

        <div class="row">
            <div class="label">Roll</div>
            <SliderControl class="s" Value:bind=@Roll Min="@(-180f)" Max="@(180f)" Step="@(5f)"></SliderControl>
            <div class="val">@Roll.ToString( "0.#" )</div>
        </div>

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

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

    </div>
</root>

@code {

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

    // ⚠️ BOUND TO THE STATICS RATHER THAN COPIES. A local copy would drift the moment anything else
    // wrote the value — the console commands do — and the slider would then fight it.
    float Height
    {
        get => TradeTable.ShowHeight;
        set => TradeTable.ShowHeight = value;
    }

    float Pitch
    {
        get => TradeTable.ShowAngles.pitch;
        set => TradeTable.ShowAngles = TradeTable.ShowAngles.WithPitch( value );
    }

    float Yaw
    {
        get => TradeTable.ShowAngles.yaw;
        set => TradeTable.ShowAngles = TradeTable.ShowAngles.WithYaw( value );
    }

    float Roll
    {
        get => TradeTable.ShowAngles.roll;
        set => TradeTable.ShowAngles = TradeTable.ShowAngles.WithRoll( value );
    }

    static bool AnyTable => TradeTable.All.Exists( t => t.IsValid() );
    static bool AnyWeapon => TradeTable.All.Exists( t => t.IsValid() && t.HasWeapon );

    /// <summary>
    /// The two lines to paste into TradeTable.cs.
    ///
    /// ⚠️ REAL C#, not a report. The whole point is that it goes straight back into the source
    /// without anyone retyping four numbers and transposing two of them.
    /// </summary>
    string Snippet
        => $"ShowHeight = {Height:0.##}f;  "
         + $"ShowAngles = new( {Pitch:0.##}f, {Yaw:0.##}f, {Roll:0.##}f );";

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

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

    /// <summary>
    /// Back to what the source ships, so a bad session can be abandoned.
    ///
    /// ⛔ NOT `Reset` — that name hides `Component.Reset()`, the same collision `CamoEnabled` and
    /// `ActiveCamo` were renamed for. A member shadowing a base one compiles and then means
    /// something different depending on where it is called from.
    /// </summary>
    void ResetValues()
    {
        // ⚠️ THESE MUST TRACK TradeTable's OWN DEFAULTS. "Reset" that restores something the source
        // no longer ships is worse than no reset — it looks like the shipped value.
        TradeTable.ShowHeight = 47f;
        TradeTable.ShowAngles = new Angles( 0f, 0f, 0f );
        CopyLabel = "Copy";
    }

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

    /// <summary>
    /// `nz_trade_tune [0/1]` — show the sliders.
    ///
    /// ⚠️ IT TAKES THE CURSOR, like the Wunderfizz menu does — sliders cannot be dragged without
    /// one, and `Noclip` keys off `Mouse.Visibility` so leaving it hidden would have V toggling
    /// noclip under the player while they drag.
    /// </summary>
    [ConCmd( "nz_trade_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-trade] tuner {(Visible ? "open" : "closed")}"
            + $" — height {TradeTable.ShowHeight:0.##}, angles {TradeTable.ShowAngles}" );

        if ( Visible && !TradeTable.All.Exists( t => t.IsValid() && t.HasWeapon ) )
            Log.Info( "[nz-trade] nothing on a table yet — nz_trade then nz_trade_use" );
    }

    static GameObject _host;

    /// <summary>
    /// Make sure something is drawing this panel.
    ///
    /// ⛔ THE PANEL HAS NO HOME IN THE SCENE, exactly like WunderfizzMenu's — every HUD here is a
    /// scene object with a ScreenPanel and the scene file must not be rewritten from script, so a
    /// dev-only panel has to build its own host. 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, and a tuner that silently stops appearing after a code edit is the
    /// PowerupMusic failure again.
    ///
    /// ⚠️ ZIndex 80 puts it over the survival HUD (50) and the scoreboard (70) but under the
    /// lobby (100) — a full-screen menu should still cover a dev slider.
    /// </summary>
    static void EnsureHost()
    {
        if ( _host.IsValid() ) return;

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

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

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

        _host.Components.Create<TradeTableTuner>();

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

    // ⚠️ THE VALUES ARE IN THE HASH so the readout and the snippet follow the sliders. Without the
    // angles in here the numbers freeze while the weapon moves, which reads as the panel being
    // broken rather than stale.
    protected override int BuildHash()
        => HashCode.Combine( Visible, Height, Pitch, Yaw, Roll, CopyLabel, AnyTable, AnyWeapon );
}