Console command helpers for managing perk machines in the editor and at runtime. Provides commands to list available perks, place a machine at the player, list configured machines, clear them, buy from a nearby machine, and check model assets.
using Sandbox;
using System.Linq;
namespace NZombies;
/// <summary>
/// Console access to the perk machines. Every button in the tool panel has an equivalent here, so
/// a machine can be placed, inspected and bought from without clicking.
/// </summary>
public static class PerkMachineCommands
{
static MapEditor Editor
=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
static NZPlayer Player
=> NZPlayer.Local;
/// <summary>Which perk the tool will place, and every id you can give it: `nz_perk_machines`.</summary>
[ConCmd( "nz_perk_machines" )]
public static void Available()
{
var ed = Editor;
Log.Info( $"[nz-perk] tool is set to place: {(ed.IsValid() ? ed.PerkMachinePerk : "?")}" );
// ⚠️ NO PRICE PER PERK. Machines charge the Wunderfizz's price, the same for every perk, so
// the perk's own list price this used to print is not what anyone pays.
foreach ( var id in PerkMachineManager.Placeable )
{
var perk = PerkRegistry.Find( id );
Log.Info( $"[nz-perk] {id,-11} {perk?.Name}" );
}
var p = Player;
Log.Info( $"[nz-perk] machines charge the Wunderfizz's price: {Wunderfizz.PerkPriceRule()}"
+ (p.IsValid() ? $" — {Wunderfizz.PerkPriceFor( p ):N0} for you now, owning {p.Perks.Count}" : "") );
}
/// <summary>
/// Drop one at your feet: `nz_perk_machine [perkId] [price]`.
///
/// ⚠️ Passing a perk id also SETS the tool, so the next click places the same one. Placing
/// one thing and arming another is the sort of split that only shows up three machines later.
/// </summary>
[ConCmd( "nz_perk_machine" )]
public static void Place( string perkId = "", int price = int.MinValue )
{
var ed = Editor;
var p = Player;
if ( !ed.IsValid() || !p.IsValid() ) { Log.Warning( "[nz-perk] no editor/player" ); return; }
if ( !string.IsNullOrWhiteSpace( perkId ) )
{
if ( PerkRegistry.Find( perkId ) is null )
{
Log.Warning( $"[nz-perk] '{perkId}' is not a perk id — nz_perk_machines lists them" );
return;
}
ed.PerkMachinePerk = perkId;
}
if ( price != int.MinValue ) ed.PerkMachinePrice = price;
// ⚠️ At the PLAYER's feet with an UP normal, not a crosshair trace. The command exists so
// this can be driven headlessly, and a trace needs somewhere to be aiming.
ed.AddPerkMachineAt( p.WorldPosition, Vector3.Up );
}
/// <summary>What is placed: `nz_perk_machine_list`.</summary>
[ConCmd( "nz_perk_machine_list" )]
public static void List()
{
var list = ActiveConfig.Current.PerkMachines;
if ( list.Count == 0 )
{
Log.Info( "[nz-perk] none placed — Q > Placeables > Player assisting > "
+ "Perk machine, or nz_perk_machine <perkId>" );
return;
}
for ( int i = 0; i < list.Count; i++ )
{
var s = list[i];
var perk = PerkRegistry.Find( s.PerkId );
// ⚠️ WHAT THE LOCAL PLAYER WOULD PAY, since auto depends on how many perks they own.
var price = s.Price >= 0 ? s.Price : Wunderfizz.PerkPriceFor( Player );
Log.Info( $"[nz-perk] [{i}] {perk?.Name ?? s.PerkId,-21} {price,6}"
+ (s.Price < 0 ? " (auto)" : " ")
+ (s.StartRound > 1 ? $" from round {s.StartRound}" : "")
+ (s.RequiresPower ? " ⚡power" : "")
+ $" flag {DoorLinks.Display( s.Link )}"
+ (perk is null ? " ⚠ UNKNOWN PERK" : "") );
}
Log.Info( $"[nz-perk] auto = the Wunderfizz's price, {Wunderfizz.PerkPriceRule()}" );
var mgr = PerkMachineManager.Instance;
// ⚠️ CONFIGURED vs STANDING, said separately. A machine whose model failed to load leaves a
// spot in the config and nothing in the world, and those two numbers disagreeing is the
// only cheap way to notice.
Log.Info( $"[nz-perk] {list.Count} configured, "
+ $"{(mgr.IsValid() ? mgr.Built.ToString() : "?")} standing" );
}
/// <summary>Remove them all: `nz_perk_machine_clear`.</summary>
[ConCmd( "nz_perk_machine_clear" )]
public static void Clear()
{
var n = ActiveConfig.Current.PerkMachines.Count;
ActiveConfig.Current.PerkMachines.Clear();
PerkMachineManager.Ensure( Game.ActiveScene )?.Rebuild();
Log.Info( $"[nz-perk] removed {n}" );
}
/// <summary>
/// Buy from the machine you are standing at: `nz_perk_machine_buy`.
///
/// ⚠️ Goes through the same Buy the use key does, so a refusal here is the refusal a player
/// would get — this is not a shortcut that skips the gates.
/// </summary>
[ConCmd( "nz_perk_machine_buy" )]
public static void Buy()
{
var p = Player;
if ( !p.IsValid() ) { Log.Warning( "[nz-perk] no player" ); return; }
var m = PerkMachine.Near( p.WorldPosition );
if ( m is null ) { Log.Info( "[nz-perk] not standing at a perk machine" ); return; }
Log.Info( $"[nz-perk] {m.Buy( p )}" );
}
/// <summary>
/// Are all seventeen machine MODELS usable? `nz_perk_machine_models`.
///
/// ⚠️ Asks the asset system, not the config. "The machine did not appear" has three different
/// causes — no spot placed, a bad path, or a compiled-but-broken vmdl that loads as the
/// checkerboard error model — and only this tells them apart.
/// </summary>
[ConCmd( "nz_perk_machine_models" )]
public static void ModelCheck()
{
int ok = 0, bad = 0;
foreach ( var id in PerkMachineManager.Placeable )
{
var path = PerkMachineManager.ModelFor( id );
var name = PerkRegistry.Find( id )?.Name ?? id;
var m = Model.Load( path );
if ( m is null )
{
Log.Warning( $"[nz-perk] {name,-21} NOT FOUND {path}" ); bad++;
}
else if ( m.IsError )
{
Log.Warning( $"[nz-perk] {name,-21} ERROR MODEL (exists but did not compile) {path}" );
bad++;
}
else
{
Log.Info( $"[nz-perk] {name,-21} ok bounds {m.Bounds.Size}" ); ok++;
}
}
Log.Info( $"[nz-perk] {ok} ok, {bad} broken, of {PerkMachineManager.Placeable.Length}" );
}
}