Console command helper for the NZombies map editor. Exposes ConCmds to set game mode, arm tools, place/remove objects, toggle preview, list/clear/place spawns, and helper placement offsets and random placement routines that call into the MapEditor and ActiveConfig systems.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// TOOL/COMMANDS — drive the map editor without a mouse.
///
/// Every one of these calls the SAME method the mouse buttons call, so a
/// passing test here is evidence about the real tool rather than about a
/// parallel code path built for testing.
/// </summary>
public static class ToolCommands
{
static MapEditor Editor
{
get
{
var p = NZPlayer.Local;
return p?.Components.GetOrCreate<MapEditor>();
}
}
/// <summary>Creative / Survival / Lobby. Tools only work in creative.</summary>
[ConCmd( "nz_mode" )]
public static void SetMode( string mode = "creative" )
{
var m = mode.ToLowerInvariant() switch
{
"creative" => GameMode.Creative,
"survival" => GameMode.Survival,
_ => GameMode.Lobby,
};
NZGame.SetMode( m );
}
/// <summary>Arm a tool, as clicking it in the Q menu does. Empty to holster.</summary>
[ConCmd( "nz_tool" )]
public static void Tool( string tool = "" )
{
var e = Editor;
if ( e is null ) { Log.Warning( "[nz] no player" ); return; }
e.ActiveTool = tool;
Log.Info( string.IsNullOrEmpty( tool )
? "[nz] tool holstered"
: $"[nz] tool armed: {NZTools.NameFor( tool )}" );
}
/// <summary>
/// What LMB does. Optionally cap the reach first.
///
/// ⚠️ The distance argument exists because nobody can nudge the crosshair
/// over MCP — without it, a placement always lands on whatever the camera
/// happens to be pointing at, which is usually a distant wall. Capping the
/// reach puts it on the floor in front instead, and the reach is restored
/// afterwards so the tool behaves normally for a person holding the mouse.
/// </summary>
[ConCmd( "nz_place" )]
public static void PlaceCmd( float distance = 0f )
{
var e = Editor;
if ( e is null ) { Log.Warning( "[nz] no player" ); return; }
if ( !e.HasTool ) { Log.Warning( "[nz] no tool armed — nz_tool player_spawn" ); return; }
if ( !NZGame.IsCreative ) { Log.Warning( "[nz] not in creative — nz_mode creative" ); return; }
if ( distance <= 0f ) { e.Place(); return; }
// Point along the aim ray at the requested distance. PlaceAt drops it to
// the floor, so this lands in front of you rather than in mid-air.
var cam = Game.ActiveScene?.Camera;
if ( !cam.IsValid() ) { Log.Warning( "[nz] no camera" ); return; }
e.PlaceAt( cam.WorldPosition + cam.WorldRotation.Forward * distance );
}
/// <summary>
/// Place at a forward/right offset from the camera.
///
/// ⚠️ Exists for the same reason nz_place takes a distance: without a mouse
/// there is no way to aim slightly left of where you are looking, so
/// "put one beside that" is otherwise impossible to express.
/// </summary>
/// <summary>
/// Toggle preview: nz_preview, or nz_preview 1 / nz_preview 0.
///
/// Shows the map the way a player sees it — no spawn markers, no barrier
/// outlines — without leaving Creative, so nothing is reset and the tools
/// still work.
/// </summary>
[ConCmd( "nz_preview" )]
public static void Preview( int on = -1 )
{
NZGame.PreviewMode = on < 0 ? !NZGame.PreviewMode : on != 0;
Log.Info( NZGame.PreviewMode
? "[nz] PREVIEW on — authoring visuals hidden (nz_preview to restore)"
: "[nz] preview off — markers and outlines back" );
}
[ConCmd( "nz_place_offset" )]
public static void PlaceOffset( float forward = 150f, float right = 80f )
{
var e = Editor;
if ( e is null ) { Log.Warning( "[nz] no player" ); return; }
if ( !e.HasTool ) { Log.Warning( "[nz] no tool armed" ); return; }
if ( !NZGame.IsCreative ) { Log.Warning( "[nz] not in creative" ); return; }
// ⚠️ Anchored to the PLAYER, not Scene.Camera. Scene.Camera is a separate
// object that trails the player, so a command batched right after a
// teleport measured from where the view USED to be — placements landed
// somewhere else entirely and every offset became guesswork. The player's
// own transform is authoritative the instant it moves.
var p = NZPlayer.Local;
if ( !p.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var rot = p.Components.Get<PlayerController>()?.EyeAngles.ToRotation()
?? p.WorldRotation;
// Flatten the axes — a slightly downward view would otherwise push the
// point into the floor and shorten the offset.
var fwd = rot.Forward.WithZ( 0 ).Normal;
var rgt = rot.Right.WithZ( 0 ).Normal;
e.PlaceAt( p.WorldPosition + fwd * forward + rgt * right );
}
/// <summary>What RMB does.</summary>
[ConCmd( "nz_remove" )]
public static void RemoveCmd()
{
var e = Editor;
if ( e is null ) { Log.Warning( "[nz] no player" ); return; }
e.RemoveAimed();
}
/// <summary>What is currently placed, and where.</summary>
[ConCmd( "nz_spawns" )]
public static void Spawns()
{
var c = ActiveConfig.Current;
Log.Info( $"[nz] mode {NZGame.Mode} config '{c.Name}'" );
Log.Info( $"[nz] player spawns: {c.PlayerSpawns.Count}" );
for ( int i = 0; i < c.PlayerSpawns.Count; i++ )
Log.Info( $"[nz] [{i}] {c.PlayerSpawns[i].Position} yaw {c.PlayerSpawns[i].Yaw:0}" );
Log.Info( $"[nz] zombie spawns: {c.ZombieSpawns.Count}" );
for ( int i = 0; i < c.ZombieSpawns.Count; i++ )
Log.Info( $"[nz] [{i}] {c.ZombieSpawns[i].Position} yaw {c.ZombieSpawns[i].Yaw:0}" );
}
/// <summary>
/// Place spawns at random points around the player, so the system can be
/// exercised without anyone aiming at anything.
///
/// ⚠️ Traces DOWN from above each point to find the floor, rather than
/// dropping markers at the player's own height — otherwise they float, and
/// a spawn point in mid-air is worse than useless.
/// </summary>
[ConCmd( "nz_place_random" )]
public static void PlaceRandom( int count = 4, float radius = 500f )
{
var scene = Game.ActiveScene;
var p = NZPlayer.Local;
if ( p is null ) { Log.Warning( "[nz] no player" ); return; }
int placed = 0;
for ( int i = 0; i < count; i++ )
{
var angle = (i / (float)count) * MathF.PI * 2f;
var at = p.WorldPosition + new Vector3(
MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius, 0f );
var tr = scene.Trace
.Ray( at + Vector3.Up * 500f, at - Vector3.Up * 2000f )
.Run();
if ( !tr.Hit ) continue;
// Face the player, matching what the tool does.
var yaw = Rotation.LookAt( (p.WorldPosition - tr.HitPosition).WithZ( 0 ) ).Yaw();
ActiveConfig.Current.PlayerSpawns.Add( new SpawnPoint( tr.HitPosition, yaw ) );
placed++;
}
Log.Info( $"[nz] placed {placed}/{count} player spawns "
+ $"({ActiveConfig.Current.PlayerSpawns.Count} total)" );
}
/// <summary>Wipe them, for starting a placement test over.</summary>
/// <summary>Wipe spawns. Defaults to both — pass "player" or "zombie" for one.</summary>
[ConCmd( "nz_clear_spawns" )]
public static void ClearSpawns( string which = "all" )
{
var c = ActiveConfig.Current;
var w = which.ToLowerInvariant();
if ( w is "all" or "player" )
{
Log.Info( $"[nz] cleared {c.PlayerSpawns.Count} player spawns" );
c.PlayerSpawns.Clear();
}
if ( w is "all" or "zombie" )
{
Log.Info( $"[nz] cleared {c.ZombieSpawns.Count} zombie spawns" );
c.ZombieSpawns.Clear();
}
}
}