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