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.
@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 );
}