Buyables/PerkMachineCommands.cs

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.

File Access
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}" );
	}
}