Console command collection for editing and inspecting DamageWall objects and related manager/editor state. It provides commands to list, set properties, clear, enable/disable, place a test box, show/hide visuals, set material/sound, apply a lava preset, query whether the player is inside a wall, and adjust depth; it updates ActiveConfig, MapEditor defaults, and triggers DamageWallManager rebuilds.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// Console access to the damage walls. Every setting in the tool panel has an
/// equivalent here, so one can be tuned and inspected without clicking.
/// </summary>
public static class DamageWallCommands
{
static MapEditor Editor
=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
static NZPlayer Player
=> NZPlayer.Local;
/// <summary>What is placed: `nz_dmgwall_list`.</summary>
[ConCmd( "nz_dmgwall_list" )]
public static void List()
{
var list = ActiveConfig.Current.DamageWalls;
if ( list.Count == 0 )
{
Log.Info( "[nz-dmgwall] none built - Q > Placeables > Damage wall, then click "
+ "the corners and once more for the height" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
var w = list[i];
var dps = w.Interval > 0f ? w.Damage / w.Interval : 0f;
// The PAIR and the rate both. 20 every 1s and 200 every 10s are the same
// 20/s and completely different to walk through, so the rate alone would
// mislead - but without it two walls cannot be compared at a glance.
Log.Info( $"[nz-dmgwall] [{i}] {w.Damage:0.#} every {w.Interval:0.##}s (~{dps:0.#}/s)"
+ $" size {w.Size.x:0}x{w.Size.y:0}x{w.Size.z:0}"
+ ( w.HasFootprint ? $" {w.Footprint.Count}-point shape" : " box" )
// SAYS WHICH OF THE TWO KINDS IT IS, and names the surface when one is on
// screen. "drawn in creative" was true of every damage wall that had ever
// existed; now it is true of only some, and a mapper looking at a wall they
// cannot see has no other way to find out which they made.
+ ( w.VisibleInGame
? $" VISIBLE IN GAME, {w.Material}"
: w.Visible ? " authoring only" : " never drawn" ) );
}
var mgr = DamageWallManager.Instance;
Log.Info( $"[nz-dmgwall] {list.Count} configured, "
+ $"{( mgr.IsValid() ? mgr.Built.ToString() : "?" )} standing"
+ $" - {( DamageWallVolume.Armed ? "ARMED" : "disabled (nz_dmgwall_enable 1)" )}" );
}
/// <summary>
/// `nz_dmgwall_set [damage] [interval] [index]` - retune every built wall AND the tool default; or, given an index
/// from `nz_dmgwall_list`, that one wall alone (2026-09-27, for the boss arena's lava: `nz_dmgwall_set 10 0.2 2`).
///
/// BOTH, DELIBERATELY. The config is what the walls already standing deal; the
/// MapEditor fields are what the NEXT one drawn is stamped with. Setting one alone
/// means the damage changes now and silently reverts later.
///
/// ⚠️ ONE WALL LEAVES THE TOOL DEFAULT ALONE: retuning one wall says nothing about the next one drawn. A standing
/// wall reads its config row every tick, so the change is live.
/// </summary>
[ConCmd( "nz_dmgwall_set" )]
public static void Set( float damage = -1f, float interval = -1f, int index = -1 )
{
var walls = ActiveConfig.Current.DamageWalls;
if ( index >= walls.Count ) { Log.Warning( $"[nz-dmgwall] no wall [{index}] - {walls.Count} configured" ); return; }
if ( index >= 0 )
{
if ( damage >= 0f ) walls[index].Damage = damage;
if ( interval >= 0f ) walls[index].Interval = interval;
}
else if ( damage >= 0f || interval >= 0f )
{
foreach ( var w in walls )
{
if ( damage >= 0f ) w.Damage = damage;
if ( interval >= 0f ) w.Interval = interval;
}
var ed = Editor;
if ( ed.IsValid() )
{
if ( damage >= 0f ) ed.DamageWallDamage = damage;
if ( interval >= 0f ) ed.DamageWallInterval = interval;
}
else
{
Log.Warning( "[nz-dmgwall] no MapEditor - the walls already built were retuned, "
+ "but the NEXT one drawn will use the old numbers" );
}
}
List();
}
/// <summary>Remove them all: `nz_dmgwall_clear`.</summary>
[ConCmd( "nz_dmgwall_clear" )]
public static void Clear()
{
var n = ActiveConfig.Current.DamageWalls.Count;
ActiveConfig.Current.DamageWalls.Clear();
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-dmgwall] removed {n}" );
}
/// <summary>
/// `nz_dmgwall_enable [0/1]` - stop every damage wall hurting anyone.
///
/// EXISTS SO A MAPPER CAN WALK THROUGH THEIR OWN KILLBOX. A damage wall has no
/// collision, so building a map with several of them means crossing them constantly
/// while placing everything else.
/// </summary>
[ConCmd( "nz_dmgwall_enable" )]
public static void Enable( int on = -1 )
{
if ( on >= 0 ) DamageWallVolume.Armed = on != 0;
Log.Info( $"[nz-dmgwall] {( DamageWallVolume.Armed ? "armed" : "disabled - nothing takes damage" )}" );
}
/// <summary>
/// `nz_dmgwall_place [width] [depth] [height]` - drop a box damage wall in front of you.
///
/// THE TOOL IS A MOUSE FLOW AND NOTHING ELSE COULD REACH IT. Every other setting here has
/// a command, but the wall itself could only be made by opening Q, picking Damage wall and
/// clicking four times - so "does a visible damage wall work" was a question nobody could
/// ask from a console, which is exactly the situation `nz_wallbuy_place` exists to fix for
/// buyables.
///
/// A BOX, NOT A FOOTPRINT. Clicking corners is the part that needs a mouse; a box covers
/// what a test needs and `DamageWallVolume.Contains` treats the two identically.
///
/// STANDS ON THE FLOOR, NOT AT EYE HEIGHT. A volume centred on the camera is one you are
/// already inside and cannot see the shape of - the first thing anyone does with this is
/// look at the thing they just made.
/// </summary>
[ConCmd( "nz_dmgwall_place" )]
public static void PlaceHere( float width = 128f, float depth = 128f, float height = 64f )
{
var player = Player;
if ( !player.IsValid() ) { Log.Warning( "[nz-dmgwall] no player" ); return; }
var cam = Game.ActiveScene?.Camera;
if ( !cam.IsValid() ) { Log.Warning( "[nz-dmgwall] no camera" ); return; }
var fwd = cam.WorldRotation.Forward.WithZ( 0f ).Normal;
var ahead = player.WorldPosition + fwd * ( depth * 0.5f + 96f );
// FINDS THE FLOOR RATHER THAN ASSUMING THE PLAYER'S. Placed ahead on a slope or over a
// drop, the player's own feet are the wrong height and the wall floats or sinks - which
// looks like the size arguments were ignored.
var down = Game.ActiveScene.Trace
.Ray( ahead + Vector3.Up * 128f, ahead + Vector3.Down * 512f )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
var floor = down.Hit ? down.HitPosition.z : player.WorldPosition.z;
var ed = Editor;
var w = new DamageWall
{
Position = ahead.WithZ( floor + height * 0.5f ),
Yaw = cam.WorldRotation.Yaw(),
Size = new Vector3( width, depth, height ),
// STAMPED FROM THE TOOL, EXACTLY LIKE `MapEditor.AddDamageWall`. A command that
// invented its own defaults would place a wall unlike anything the tool makes, so
// testing it would prove nothing about what a mapper actually builds.
Damage = ed.IsValid() ? ed.DamageWallDamage : 20f,
Interval = ed.IsValid() ? ed.DamageWallInterval : 1f,
Visible = !ed.IsValid() || ed.DamageWallVisible,
VisibleInGame = ed.IsValid() && ed.DamageWallShown,
Material = ed.IsValid() ? ed.DamageWallMaterial : "materials/nz/lava.vmat",
};
ActiveConfig.Current.DamageWalls.Add( w );
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-dmgwall] placed #{ActiveConfig.Current.DamageWalls.Count - 1} at "
+ $"{w.Position} - {width:0}x{depth:0}x{height:0}"
+ ( w.VisibleInGame ? $" - VISIBLE IN GAME, {w.Material}" : " - authoring only" ) );
}
/// <summary>
/// `nz_dmgwall_show [0/1] [index]` - leave the wall drawn once the round starts.
///
/// THE POINT OF THE WHOLE FEATURE, FROM THE CONSOLE. A damage wall has always been an
/// invisible killbox; this is what turns one into a hazard the player is meant to SEE and
/// route around - a lava pool, an electrified floor - and it is the flag `DamageWallVolume`
/// reads every frame.
///
/// REBUILDS, BECAUSE THE RENDERER MAY NOT EXIST YET. `DamageWallManager` only builds a
/// visual for a wall that asked for one, so turning this on for a wall saved with
/// `Visible: false` has nothing to toggle until the wall is built again.
///
/// INDEX IS OPTIONAL AND MEANS ALL. Every other command here edits every wall at once and
/// a mapper with one lava pool among four killboxes needs the single-wall form, so both.
/// </summary>
[ConCmd( "nz_dmgwall_show" )]
public static void Show( int on = -1, int index = -1 )
{
var list = ActiveConfig.Current.DamageWalls;
if ( on >= 0 )
{
if ( index >= 0 && index >= list.Count )
{
Log.Warning( $"[nz-dmgwall] no wall #{index} - there are {list.Count}" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
if ( index >= 0 && i != index ) continue;
list[i].VisibleInGame = on != 0;
}
var ed = Editor;
if ( ed.IsValid() && index < 0 ) ed.DamageWallShown = on != 0;
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
}
List();
}
/// <summary>
/// `nz_dmgwall_material <path> [index]` - what a drawn damage wall wears.
///
/// REBUILDS RATHER THAN REASSIGNING. For a footprint wall the material is BAKED INTO THE
/// MESH by `DebrisMesh.Build` - there is no MaterialOverride to set on a shaped wall - so
/// the mesh has to be built again for a new surface to appear at all.
/// </summary>
[ConCmd( "nz_dmgwall_material" )]
public static void SetMaterial( string path = null, int index = -1 )
{
var list = ActiveConfig.Current.DamageWalls;
if ( string.IsNullOrWhiteSpace( path ) )
{
Log.Info( "[nz-dmgwall] nz_dmgwall_material <path> [index] - "
+ "try materials/nz/lava.vmat" );
List();
return;
}
// WARNS, DOES NOT REFUSE. A path that will not load leaves the wall grey - see
// `DamageWallManager.MaterialFor` - which is survivable and obvious, whereas
// refusing a path that is merely still compiling would be wrong more often.
if ( Material.Load( path ) is null )
Log.Warning( $"[nz-dmgwall] '{path}' did not load - the wall will fall back to grey" );
if ( index >= 0 && index >= list.Count )
{
Log.Warning( $"[nz-dmgwall] no wall #{index} - there are {list.Count}" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
if ( index >= 0 && i != index ) continue;
list[i].Material = path;
}
var ed = Editor;
if ( ed.IsValid() && index < 0 ) ed.DamageWallMaterial = path;
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
List();
}
/// <summary>
/// `nz_dmgwall_sound <path> [volume] [distance]` — give the volume a looping sound.
///
/// ⚠️ `sounds/effects/fire/fire_burn_loop01.sound` IS THE LAVA ONE. The project's own 2,189
/// sound events contain no fire or lava ambience at all — every "fire" among them is a weapon
/// discharge or the Fire Sale powerup — so this comes from the engine's mounted content.
///
/// ⚠️ REBUILDS, because the emitter is created in `DamageWallManager.Build` alongside the
/// renderer. An empty path removes it.
/// </summary>
[ConCmd( "nz_dmgwall_sound" )]
public static void SetSound( string path = null, float volume = -1f, float distance = -1f,
float repeat = -1f )
{
var list = ActiveConfig.Current.DamageWalls;
if ( list.Count == 0 ) { Log.Info( "[nz-dmgwall] none built" ); return; }
if ( path is null )
{
foreach ( var w in list )
Log.Info( $"[nz-dmgwall] sound '{w.Sound}' vol {w.SoundVolume:0.##}"
+ $" dist {w.SoundDistance:0}" );
Log.Info( "[nz-dmgwall] nz_dmgwall_sound <path> [volume] [distance]"
+ " try sounds/effects/fire/fire_burn_loop01.sound" );
return;
}
// ⚠️ WARNS RATHER THAN REFUSES, the same as `nz_dmgwall_material`: rejecting a path that
// is merely still compiling is worse than accepting one that is wrong.
if ( !string.IsNullOrWhiteSpace( path )
&& ResourceLibrary.Get<SoundEvent>( path ) is null )
Log.Warning( $"[nz-dmgwall] '{path}' did not load — the wall will be silent" );
foreach ( var w in list )
{
w.Sound = path;
if ( volume >= 0f ) w.SoundVolume = volume;
if ( distance >= 0f ) w.SoundDistance = distance;
// ⚠️ A REPEAT TIME OF 0 MEANS "DO NOT REPEAT", so one argument covers both the switch
// and the interval — a separate on/off flag would let you set a time that does nothing.
if ( repeat >= 0f )
{
w.SoundRepeat = repeat > 0f;
if ( repeat > 0f ) w.SoundRepeatTime = repeat;
}
}
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( string.IsNullOrWhiteSpace( path )
? "[nz-dmgwall] sound removed"
: $"[nz-dmgwall] sound '{path}' on {list.Count} wall(s) — nz_save to keep it" );
}
/// <summary>
/// `nz_dmgwall_lava` - every damage wall becomes a visible pool of animated lava.
///
/// ONE COMMAND FOR THE WHOLE LOOK, because it is FOUR fields and getting three of them
/// right shows nothing: shown-in-game without the material is a grey slab, the material
/// without a white tint is brown, and either without `Visible` never builds a renderer to
/// begin with. Every one of those failures looks like "the lava does not work".
/// </summary>
[ConCmd( "nz_dmgwall_lava" )]
public static void Lava()
{
var list = ActiveConfig.Current.DamageWalls;
if ( list.Count == 0 )
{
Log.Info( "[nz-dmgwall] none built - Q > Placeables > Damage wall, then click "
+ "the corners and once more for the height" );
return;
}
foreach ( var w in list )
{
w.Visible = true;
w.VisibleInGame = true;
w.Material = "materials/nz/lava.vmat";
w.Tint = Color.White;
// ⚠️ AND THE SOUND, because "make it lava" means the whole thing. Only filled in when
// the wall has none, so re-running this never overwrites a sound somebody chose.
if ( string.IsNullOrWhiteSpace( w.Sound ) )
w.Sound = "sounds/effects/fire/fire_burn_loop01.sound";
}
var ed = Editor;
if ( ed.IsValid() )
{
ed.DamageWallVisible = true;
ed.DamageWallShown = true;
ed.DamageWallMaterial = "materials/nz/lava.vmat";
}
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-dmgwall] {list.Count} wall(s) are now lava - "
+ "nz_save to keep it" );
}
/// <summary>
/// `nz_dmgwall_where` - am I in one right now, and if not, is anything near?
///
/// THIS IS THE ONLY WAY TO SEE THE VOLUME. It has no collision to bump into and is
/// never drawn in a round, so "the wall does no damage" and "I am not actually standing
/// in it" are indistinguishable from inside the game. Asking Contains directly - the
/// same method the damage tick uses - settles it.
/// </summary>
[ConCmd( "nz_dmgwall_where" )]
public static void Where()
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-dmgwall] no player" ); return; }
var here = DamageWallVolume.At( p.WorldPosition );
if ( here is not null )
{
var w = here.Spot;
Log.Info( $"[nz-dmgwall] INSIDE - {w.Damage:0.#} every {w.Interval:0.##}s"
+ $" - {here.Ticks} tick(s) dealt"
+ $" - {( DamageWallVolume.Armed ? "armed" : "DISABLED, so nothing is landing" )}" );
return;
}
var walls = DamageWallVolume.All.Where( w => w.IsValid() && w.Spot is not null ).ToList();
if ( walls.Count == 0 )
{
Log.Info( "[nz-dmgwall] not inside one - and none are built" );
return;
}
// SAYS HOW FAR THE NEAREST IS. "Not inside one" alone leaves a mapper who is
// standing where they thought they drew it with nothing to go on.
var near = walls.OrderBy( w => w.Spot.Position.Distance( p.WorldPosition ) ).First();
Log.Info( $"[nz-dmgwall] not inside one - nearest of {walls.Count} is "
+ $"{near.Spot.Position.Distance( p.WorldPosition ):0}u away"
+ $" (its box is {near.Spot.Size.x:0}x{near.Spot.Size.y:0}x{near.Spot.Size.z:0})" );
}
/// <summary>
/// `nz_dmgwall_depth [index] [depth]` - how far down wall [index] reaches from its top, its top staying where it is;
/// bare, every wall's top, depth and bottom. In memory, like every edit: nz_save keeps it.
///
/// ASKED FOR AS *"take the lava on the map, and reduce how far bellow it goes to about half of what it is now"*
/// (2026-09-27): basalt's lava bed, wall 0, reached 1965u down from its surface at 1119.8, and its sides showed from under
/// the map. THE TOP STAYS because it is the surface everyone sees - and the rising lava rises from it
/// (`HexPlatforms.LavaBedTop`).
/// </summary>
[ConCmd( "nz_dmgwall_depth" )]
public static void Depth( int index = -1, float depth = 0f )
{
var list = ActiveConfig.Current.DamageWalls;
if ( index >= 0 && depth > 0f )
{
if ( index >= list.Count )
{
Log.Warning( $"[nz-dmgwall] no wall [{index}] - {list.Count} configured (nz_dmgwall_list)" );
return;
}
var w = list[index];
var top = w.Position.z + w.Size.z * 0.5f;
w.Size = new Vector3( w.Size.x, w.Size.y, depth );
w.Position = new Vector3( w.Position.x, w.Position.y, top - depth * 0.5f );
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-dmgwall] [{index}] now reaches {depth:0.#}u down from its top at {top:0.#} - nz_save to keep it" );
}
for ( int i = 0; i < list.Count; i++ )
{
var w = list[i];
var top = w.Position.z + w.Size.z * 0.5f;
Log.Info( $"[nz-dmgwall] [{i}] top {top:0.#}, {w.Size.z:0.#}u deep, bottom {top - w.Size.z:0.#}"
+ ( w.VisibleInGame ? $" {w.Material}" : " not drawn in game" ) );
}
}
}