Buyables/WunderfizzCommands.cs

Console command helpers for the Der Wunderfizz buyable. Provides developer/console commands to place a machine, list configured machines, clear configured machines, check the model, and inspect/retune perk-slot prices and costs.

File Access
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// Console access to Der Wunderfizz. Every button in the tool panel has an
/// equivalent here, so the machine can be placed and inspected without clicking.
/// </summary>
public static class WunderfizzCommands
{
	static MapEditor Editor
		=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();

	static NZPlayer Player
		=> NZPlayer.Local;

	/// <summary>Drop one at your feet: `nz_fizz [price]`.</summary>
	[ConCmd( "nz_fizz" )]
	public static void Place( int price = 0 )
	{
		var ed = Editor;
		var p = Player;
		if ( !ed.IsValid() || !p.IsValid() ) { Log.Warning( "[nz-fizz] no editor/player" ); return; }

		if ( price > 0 ) ed.WunderfizzPrice = 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.AddWunderfizzAt( p.WorldPosition, Vector3.Up );
	}

	/// <summary>What is placed: `nz_fizz_list`.</summary>
	[ConCmd( "nz_fizz_list" )]
	public static void List()
	{
		var list = ActiveConfig.Current.Wunderfizzes;

		if ( list.Count == 0 )
		{
			Log.Info( "[nz-fizz] none placed — Q > Placeables > Player assisting > "
				+ "Der Wunderfizz, or nz_fizz" );
			return;
		}

		for ( int i = 0; i < list.Count; i++ )
		{
			var w = list[i];
			Log.Info( $"[nz-fizz] [{i}] {w.BasePrice,6} base"
				+ (w.PriceIncrement > 0 ? $" +{w.PriceIncrement}/roll" : " (flat)")
				+ (w.StartRound > 1 ? $"  from round {w.StartRound}" : "")
				+ (w.RequiresPower ? "  ⚡power" : "")
				+ (w.PerkSlotPrice > 0
					? $"  slot {w.PerkSlotPrice}"
						+ (w.PerkSlotIncrement > 0 ? $" +{w.PerkSlotIncrement}/slot" : "")
					: "")
				+ $"  flag {DoorLinks.Display( w.Link )}" );
		}

		var mgr = WunderfizzManager.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-fizz] {list.Count} configured, "
			+ $"{(mgr.IsValid() ? mgr.Built.ToString() : "?")} standing" );
	}

	/// <summary>Remove them all: `nz_fizz_clear`.</summary>
	[ConCmd( "nz_fizz_clear" )]
	public static void Clear()
	{
		var n = ActiveConfig.Current.Wunderfizzes.Count;
		ActiveConfig.Current.Wunderfizzes.Clear();
		WunderfizzManager.Ensure( Game.ActiveScene )?.Rebuild();

		Log.Info( $"[nz-fizz] removed {n}" );
	}

	/// <summary>
	/// Is the machine's MODEL usable? `nz_fizz_model`.
	///
	/// ⚠️ Asks the asset system, not the config. A freshly ported model is the one
	/// thing most likely to be wrong here, and "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.
	/// </summary>
	[ConCmd( "nz_fizz_model" )]
	public static void ModelCheck()
	{
		var m = Model.Load( WunderfizzManager.ModelPath );

		if ( m is null )
		{
			Log.Warning( $"[nz-fizz] {WunderfizzManager.ModelPath} — NOT FOUND" );
			return;
		}

		if ( m.IsError )
		{
			Log.Warning( $"[nz-fizz] {WunderfizzManager.ModelPath} — ERROR MODEL "
				+ "(the vmdl exists but did not compile)" );
			return;
		}

		Log.Info( $"[nz-fizz] {WunderfizzManager.ModelPath} ok — "
			+ $"bounds {m.Bounds.Size}, meshes {m.MeshCount}" );
	}

	/// <summary>
	/// `nz_fizz_slotprice [base] [increment]` — retune the perk-slot price on EVERY
	/// placed machine and the tool defaults, then print the ladder.
	///
	/// ⚠️ BOTH, DELIBERATELY. The config is what standing machines charge; the MapEditor
	/// fields are what the NEXT one placed is stamped with. Setting one alone means the
	/// price changes now and silently reverts later. Negative or omitted leaves a value
	/// alone; no arguments just reports.
	/// </summary>
	[ConCmd( "nz_fizz_slotprice" )]
	public static void SlotPrice( int basePrice = -1, int increment = -1 )
	{
		var list = ActiveConfig.Current.Wunderfizzes;

		if ( basePrice >= 0 || increment >= 0 )
		{
			foreach ( var w in list )
			{
				if ( basePrice >= 0 ) w.PerkSlotPrice = basePrice;
				if ( increment >= 0 ) w.PerkSlotIncrement = increment;
			}

			var ed = Editor;

			if ( ed.IsValid() )
			{
				if ( basePrice >= 0 ) ed.WunderfizzSlotPrice = basePrice;
				if ( increment >= 0 ) ed.WunderfizzSlotIncrement = increment;
			}
			else
			{
				Log.Warning( "[nz-fizz] no MapEditor — machines already placed were retuned, "
					+ "but the NEXT one placed will use the old numbers" );
			}
		}

		if ( list.Count == 0 )
		{
			Log.Info( "[nz-fizz] none placed — nz_fizz drops one" );
			return;
		}

		SlotCost();
	}

	/// <summary>
	/// `nz_fizz_slotcost` — what the next slot costs you and the four after it.
	///
	/// ⚠️ NOT `nz_fizz_slots`, which is one character from `nz_fizz_slot` — and that one
	/// SPENDS POINTS. A read-only command a typo turns into a purchase is a trap.
	///
	/// ⛔ THE LADDER COMES FROM `SlotPriceAt`, NOT RECOMPUTED HERE. A second copy of
	/// `base + step * bought` in a diagnostic can agree with itself while disagreeing with
	/// the till — exactly the failure this is meant to catch.
	/// </summary>
	[ConCmd( "nz_fizz_slotcost" )]
	public static void SlotCost()
	{
		var p = Player;
		var machine = Game.ActiveScene?.GetAllComponents<Wunderfizz>()
			.FirstOrDefault( m => m.IsValid() );

		if ( !machine.IsValid() ) { Log.Warning( "[nz-fizz] no Wunderfizz standing — nz_fizz" ); return; }

		var spot = machine.Spot;

		Log.Info( $"[nz-fizz] perk slots — base {spot?.PerkSlotPrice ?? 0}"
			+ ( (spot?.PerkSlotIncrement ?? 0) > 0
				? $" +{spot.PerkSlotIncrement} per slot already bought"
				: " (flat — increment 0)" ) );

		if ( !p.IsValid() ) { Log.Warning( "[nz-fizz] no player" ); return; }

		Log.Info( $"[nz-fizz]   you: {p.Perks.Count}/{p.PerkSlots} used · {p.BonusPerkSlots} bought"
			+ $" · {p.Points} points" );

		var real = p.BonusPerkSlots;

		// ⛔ ASKS `SlotPriceAt` FOR EACH RUNG — it does NOT move `BonusPerkSlots` and read
		// it back. Walking the ladder by writing the player is how a diagnostic ends up
		// granting slots it only meant to price.
		for ( int i = 0; i < 5; i++ )
		{
			var price = machine.SlotPriceAt( real + i );

			Log.Info( $"[nz-fizz]   slot #{real + i + 1,-2} {price,7}"
				+ ( i == 0 ? ( p.Points >= price ? "   <- next, affordable" : "   <- next, TOO DEAR" ) : "" ) );
		}
	}
}