Console command handlers for the power system. Provides developer commands to query prompts, show power status, place/clear/flip switches, force power on/off, mark doors or spawns as gated by power, and change the switch model.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>Console access to the whole power system — place, flip, inspect.</summary>
public static class PowerCommands
{
static NZPlayer Player
=> NZPlayer.Local;
/// <summary>Created on demand, like DebrisManager — nothing in the scene file
/// has to be wired for the power to work. NotSaved so it never gets baked
/// into a map.</summary>
static PowerManager Manager => PowerManager.Ensure( Game.ActiveScene );
/// <summary>
/// What the use prompt says right now: nz_prompt.
///
/// The prompt is the only feedback a player gets about why something will
/// not open, and it has four different wordings per door. Checking it by
/// walking up to things one at a time is how you miss the case you did not
/// think of.
/// </summary>
[ConCmd( "nz_prompt" )]
public static void Prompt()
{
var p = Player;
var text = UsePrompt.Text( p );
Log.Info( string.IsNullOrEmpty( text )
? $"[nz] no prompt — nothing usable within {UsePrompt.Range:0}u"
: $"[nz] \"{text}\"{(UsePrompt.IsRefusal( text ) ? " (refusal — drawn amber)" : "")}" );
// ⚠️ Report the ACTUAL distance whether or not a prompt showed. "No
// prompt" has two very different causes — nothing under the crosshair,
// or something under it that you are standing too far from — and the
// number is the only thing that tells them apart.
if ( !p.IsValid() ) return;
var cam = Game.ActiveScene?.Camera;
if ( !cam.IsValid() ) return;
var tr = Game.ActiveScene.Trace
.Ray( cam.WorldPosition, cam.WorldPosition + cam.WorldRotation.Forward * 512f )
.IgnoreGameObjectHierarchy( p.GameObject )
.Run();
if ( !tr.Hit ) { Log.Info( "[nz] crosshair on nothing within 512u" ); return; }
var dist = tr.EndPosition.Distance( p.WorldPosition );
Log.Info( $"[nz] crosshair on '{tr.GameObject?.Name ?? "?"}' at {dist:0}u "
+ $"(reach {UsePrompt.Range:0}u — {(dist <= UsePrompt.Range ? "in range" : "TOO FAR")})" );
}
/// <summary>
/// What a barrier's prompt would say: nz_prompt_door [index]. No index dumps
/// every one, which is the only way to see all the wordings at once.
/// </summary>
[ConCmd( "nz_prompt_door" )]
public static void PromptDoor( int index = -1 )
{
var list = ActiveConfig.Current.Debris;
if ( list.Count == 0 ) { Log.Warning( "[nz] no barriers placed" ); return; }
Log.Info( $"[nz] power {Power.Summary}, mode {NZGame.Mode}" );
for ( int i = 0; i < list.Count; i++ )
{
if ( index >= 0 && i != index ) continue;
var d = list[i];
var text = UsePrompt.ForDebris( d );
Log.Info( $"[nz] #{i} {d.Price,6}pts link {d.Link}"
+ $"{(d.RequiresPower ? " elec" : " ")}"
+ $"{(DoorLinks.IsOpen( d.Link ) ? " OPEN" : " ")}"
+ $" -> {(string.IsNullOrEmpty( text ) ? "<nothing>" : $"\"{text}\"")}" );
}
}
/// <summary>State of the power, and what depends on it: nz_power.</summary>
[ConCmd( "nz_power" )]
public static void Status()
{
var cfg = ActiveConfig.Current;
Log.Info( $"[nz] power {Power.Summary}" );
Log.Info( $"[nz] {cfg.PowerSwitches.Count} switch(es) placed" );
var doors = cfg.Debris.Where( d => d.RequiresPower ).ToList();
var free = doors.Count( d => d.Price == 0 );
var spawns = cfg.ZombieSpawns.Count( s => s.RequiresPower );
Log.Info( $"[nz] {doors.Count} door(s) need power "
+ $"({free} of them free — they open by themselves)" );
Log.Info( $"[nz] {spawns} zombie spawn(s) need power" );
if ( !Power.HasSwitch && (doors.Count > 0 || spawns > 0) )
Log.Warning( "[nz] ⚠ things are gated on power but NO SWITCH is placed — "
+ "so they are all permanently available. Place one with nz_power_place." );
}
/// <summary>Place a switch where you are looking: nz_power_place.</summary>
[ConCmd( "nz_power_place" )]
public static void Place()
{
// Aim from the CAMERA, matching nz_debris and DebrisManager.Aimed — that
// is where the crosshair is. (The camera is only the wrong anchor for
// measuring distances, which is the bug four other commands had.)
var scene = Game.ActiveScene;
var cam = scene?.Camera;
if ( !cam.IsValid() ) { Log.Warning( "[nz] no camera" ); return; }
// ⚠️ Ignore the player. The camera is at eye height inside their own
// collider, so without this the trace stops on the body it started in
// and the switch gets placed at your feet — which is exactly what
// happened the first two times.
var tr = scene.Trace
.Ray( cam.WorldPosition, cam.WorldPosition + cam.WorldRotation.Forward * 512f )
.IgnoreGameObjectHierarchy( Player?.GameObject )
.Run();
if ( !tr.Hit ) { Log.Warning( "[nz] aim at something within 512u" ); return; }
// Face the placer, same as the spawn markers and debris.
var yaw = Rotation.LookAt( (cam.WorldPosition - tr.HitPosition).WithZ( 0 ) ).Yaw();
// Sit it ON the surface rather than centred in it — half its height up.
var sw = new PowerSwitch { Yaw = yaw };
sw.Position = tr.HitPosition + Vector3.Up * (sw.Size.z * 0.5f);
var wasOn = Power.IsOn;
ActiveConfig.Current.PowerSwitches.Add( sw );
Manager?.Rebuild();
Log.Info( $"[nz] switch #{ActiveConfig.Current.PowerSwitches.Count - 1} at {sw.Position}" );
// The first switch on a map flips the power off by itself. Say so —
// otherwise it looks like placing a prop broke every door.
if ( wasOn && !Power.IsOn )
Log.Info( "[nz] ⚠ this was the FIRST switch — power is now OFF until it is used" );
}
/// <summary>Remove every switch: nz_power_clear. Power returns to ON.</summary>
[ConCmd( "nz_power_clear" )]
public static void ClearSwitches()
{
var n = ActiveConfig.Current.PowerSwitches.Count;
ActiveConfig.Current.PowerSwitches.Clear();
Manager?.Rebuild();
Log.Info( $"[nz] removed {n} switch(es) — power {Power.Summary}" );
}
/// <summary>Flip the switch you are looking at: nz_power_use.</summary>
[ConCmd( "nz_power_use" )]
public static void Use()
{
if ( Manager is null ) { Log.Warning( "[nz] no PowerManager in the scene" ); return; }
Log.Info( $"[nz] {Manager.Use( Player )}" );
}
/// <summary>Force the power on without walking to a switch: nz_power_on.
/// Testing shortcut — the real route is nz_power_use.</summary>
/// <summary>
/// `nz_power_all [0|1]` — does EVERY switch have to be flipped, or any one.
///
/// ⚠️ 1 BY DEFAULT AND BY REQUEST. 0 is the old rule, kept because a map already built around
/// "any switch" that happens to carry a second panel becomes unfinishable under the new one.
/// </summary>
[ConCmd( "nz_power_all" )]
public static void RequireAllCmd( int on = -1 )
{
if ( on >= 0 ) Power.RequireAll = on != 0;
Log.Info( Power.RequireAll
? $"[nz] power needs ALL {Power.Total} switch(es) — {Power.Flipped} flipped, {Power.Summary}"
: $"[nz] power needs ANY ONE of {Power.Total} switch(es) — {Power.Summary}" );
for ( int i = 0; i < Power.Total; i++ )
Log.Info( $"[nz] switch #{i + 1} {(Power.IsFlipped( i ) ? "ON" : "off")}"
+ $" at {ActiveConfig.Current.PowerSwitches[i].Position}" );
}
/// <summary>`nz_power_flip <n>` — flip ONE switch, 1-based, without walking to it.</summary>
[ConCmd( "nz_power_flip" )]
public static void FlipCmd( int n = 0 )
{
if ( n < 1 || n > Power.Total )
{ Log.Info( $"[nz] nz_power_flip <1..{Power.Total}>" ); return; }
Log.Info( $"[nz] switch #{n}: {Power.Flip( n - 1 )}" );
PowerManager.Instance?.Refresh();
}
[ConCmd( "nz_power_on" )]
public static void On()
{
if ( !Power.TurnOn() ) { Log.Info( $"[nz] power was already {Power.Summary}" ); return; }
Manager?.Rebuild();
}
/// <summary>Back to unpowered: nz_power_off. No-op on a map with no switch,
/// because there IsOn is always true by design.</summary>
[ConCmd( "nz_power_off" )]
public static void Off()
{
Power.Reset();
Manager?.Rebuild();
Log.Info( Power.HasSwitch
? "[nz] power OFF"
: "[nz] no switch on this map — power stays ON by design" );
}
/// <summary>
/// Mark a barrier as needing power: nz_power_door [0/1] [index].
///
/// Without an index it uses whatever you are aiming at. With one it works
/// headless, same as nz_debris_edit — which is the only way to drive this
/// from a console with no crosshair.
/// </summary>
[ConCmd( "nz_power_door" )]
public static void GateDoor( int on = 1, int index = -1 )
{
var list = ActiveConfig.Current.Debris;
if ( index < 0 )
{
var mgr = DebrisManager.Instance;
if ( mgr is null ) { Log.Warning( "[nz] no DebrisManager" ); return; }
index = mgr.Aimed( Player );
if ( index < 0 ) { Log.Warning( "[nz] not looking at a barrier — pass an index" ); return; }
}
if ( index >= list.Count ) { Log.Warning( $"[nz] no debris #{index}" ); return; }
var d = list[index];
d.RequiresPower = on != 0;
Log.Info( $"[nz] debris #{index} price {d.Price}, "
+ (d.RequiresPower
? d.Price == 0
? "needs power — and is FREE, so it opens itself when the power comes on"
: "needs power — buyable only once the power is on"
: "no longer needs power") );
}
/// <summary>Mark the nearest zombie spawn as needing power:
/// nz_power_spawn [0/1].</summary>
[ConCmd( "nz_power_spawn" )]
public static void GateSpawn( int on = 1 )
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var spawns = ActiveConfig.Current.ZombieSpawns;
if ( spawns.Count == 0 ) { Log.Warning( "[nz] no zombie spawns placed" ); return; }
var index = 0;
var best = float.MaxValue;
for ( int i = 0; i < spawns.Count; i++ )
{
var dist = spawns[i].Position.Distance( p.WorldPosition );
if ( dist < best ) { best = dist; index = i; }
}
spawns[index].RequiresPower = on != 0;
Log.Info( $"[nz] spawn #{index} ({best:0}u away) "
+ (on != 0 ? "needs power" : "no longer needs power") );
}
/// <summary>
/// Which prop the switches use: nz_power_model [path].
///
/// Bare command lists the two that are ported. ⚠️ Rebuilds immediately, so
/// the change is visible without leaving and re-entering creative — a model
/// swap you cannot see is indistinguishable from one that did not apply.
/// </summary>
[ConCmd( "nz_power_model" )]
public static void SwitchModel( string path = "" )
{
var m = Manager;
if ( m is null ) { Log.Warning( "[nz] no scene" ); return; }
if ( string.IsNullOrWhiteSpace( path ) )
{
Log.Info( $"[nz] power switch model: {m.SwitchModel}" );
Log.Info( "[nz] models/nzprops/zombies_power_lever_short.vmdl (the original's Power Switch)" );
Log.Info( "[nz] models/nzprops/zombies_power_lever.vmdl (the tall one)" );
Log.Info( "[nz] \"\" or a bad path (tinted box fallback)" );
return;
}
m.SwitchModel = path == "box" ? "" : path;
m.Rebuild();
}
}