EasterEgg/BuildPartCommands.cs

Console command helpers for the Easter egg buildable parts. Provides commands to place, set, list, clear, give, drop and tune build parts and reports sources for each part using the active configuration and resource library.

NetworkingFile Access
using System.Linq;
using Sandbox;

namespace NZombies;

/// <summary>
/// Console access to the buildable's three parts.
///
/// ⚠️ EVERY BUTTON GETS ONE. The Q-menu tool and these place the same config record, so a part
/// dropped either way saves and loads identically.
/// </summary>
public static class BuildPartCommands
{
	static NZPlayer Player
		=> PlayerCharacters.Local()
			?? NZPlayer.Local;

	/// <summary>Drop the selected part at your feet: `nz_buildpart`.</summary>
	[ConCmd( "nz_buildpart" )]
	public static void Place()
	{
		var p = Player;
		if ( !p.IsValid() ) { Log.Warning( "[nz-build] no player" ); return; }

		var cfg = ActiveConfig.Current;
		if ( cfg is null ) { Log.Warning( "[nz-build] no config" ); return; }

		var part = MapEditor.BuildPartNext;
		if ( !BuildParts.Valid( part ) )
		{
			Log.Warning( $"[nz-build] part {part} is not 1..{BuildParts.Count}" );
			return;
		}

		cfg.BuildParts.Add( new BuildPartSpot
		{
			Position = p.WorldPosition,
			Yaw = p.EyeAngles.yaw + 180f,
			Part = part,
		} );

		BuildPartManager.Ensure()?.Rebuild();
		Log.Info( $"[nz-build] {BuildParts.Name( part )} placed — {cfg.BuildParts.Count} total" );
	}

	/// <summary>
	/// Which part the tool and `nz_buildpart` place next: `nz_buildpart_set 1..3`.
	/// </summary>
	///
	/// ⚠️ BARE, IT REPORTS. A command that silently changed the selection would be the most
	/// expensive thing here to type by accident — you would lay out a whole map of the wrong piece.
	[ConCmd( "nz_buildpart_set" )]
	public static void SetCmd( int part = -1 )
	{
		if ( BuildParts.Valid( part ) ) MapEditor.BuildPartNext = part;

		Log.Info( $"[nz-build] tool places part {MapEditor.BuildPartNext}"
			+ $" — the {BuildParts.Name( MapEditor.BuildPartNext )}" );

		for ( var n = 1; n <= BuildParts.Count; n++ )
			Log.Info( $"[nz-build]   {n} = {BuildParts.Name( n )}  ({BuildParts.Model( n )})" );
	}

	/// <summary>What is placed, and what you are carrying: `nz_buildpart_list`.</summary>
	[ConCmd( "nz_buildpart_list" )]
	public static void List()
	{
		var list = ActiveConfig.Current?.BuildParts;

		if ( list is null || list.Count == 0 )
		{
			Log.Info( "[nz-build] no parts placed — nz_buildpart_set 1..3 then nz_buildpart," );
			Log.Info( "[nz-build]   or Q > Easter egg > Collection > Building piece" );
		}
		else
		{
			for ( int i = 0; i < list.Count; i++ )
			{
				var s = list[i];
				Log.Info( $"[nz-build] [{i}] {BuildParts.Name( s.Part )} at {s.Position}"
					+ (s.RequiresPower ? "  ⚡power" : "")
					+ $"  flag {DoorLinks.Display( s.Link )}" );
			}
		}

		Sources();

		Log.Info( $"[nz-build] the team holds {BuildParts.Held()}/{BuildParts.Count}"
			+ (BuildParts.HasAll()
				? " — all of them"
				: $" — missing {BuildParts.Missing()}") );
	}

	/// <summary>
	/// Where each of the three parts can come from on this map, and which have nowhere.
	/// </summary>
	///
	/// ⛔ THREE SOURCES NOW, AND THE OLD CHECK ONLY KNEW ABOUT ONE. It scanned the authored spots
	/// and warned "NOT PLACED ANYWHERE" for anything missing from them — which was true when spots
	/// were the only way to get a part, and became a false alarm the moment a soul box set and a
	/// napalm kill started awarding two of the three. A warning that cries wolf about a working map
	/// is worse than no warning, because the real one stops being read.
	///
	/// ⚠️ A VARIANT THAT NEVER SPAWNS HERE IS STILL COUNTED AS A SOURCE. Whether a napalm actually
	/// turns up is the spawn curve's business and this cannot see it; what this rules out is the
	/// case nothing can fix — a part with no source anywhere in the config at all.
	static void Sources()
	{
		var spots = ActiveConfig.Current?.BuildParts;
		var boxes = ActiveConfig.Current?.SoulBoxes;
		var variants = ResourceLibrary.GetAll<ZombieVariant>().ToList();

		var orphan = 0;

		for ( var n = 1; n <= BuildParts.Count; n++ )
		{
			var from = new System.Collections.Generic.List<string>();

			var placed = spots?.Count( s => s.Part == n ) ?? 0;
			if ( placed > 0 ) from.Add( placed == 1 ? "1 spot" : $"{placed} spots" );

			foreach ( var b in boxes?.Where( b => b.FinalPart == n ) ?? Enumerable.Empty<SoulBoxSpot>() )
			{
				from.Add( $"soul box flag {DoorLinks.Display( b.Link )}" );
				break;      // ⚠️ ONE LINE PER FLAG, not one per box — five boxes are one source.
			}

			foreach ( var v in variants.Where( v => v.DropsBuildPart == n ) )
				from.Add( $"first {v.ResourceName} killed" );

			if ( from.Count == 0 )
			{
				orphan++;
				Log.Warning( $"[nz-build] {BuildParts.Name( n )} (part {n}) has NO SOURCE"
					+ " — the weapon cannot be built on this map" );
			}
			else
			{
				Log.Info( $"[nz-build] {BuildParts.Name( n )} (part {n}) ← "
					+ string.Join( ", ", from ) );
			}
		}

		if ( orphan == 0 )
			Log.Info( "[nz-build] all three have a source" );
	}

	/// <summary>Remove every placed part: `nz_buildpart_clear`.</summary>
	[ConCmd( "nz_buildpart_clear" )]
	public static void Clear()
	{
		var list = ActiveConfig.Current?.BuildParts;
		if ( list is null || list.Count == 0 ) { Log.Info( "[nz-build] none placed" ); return; }

		var n = list.Count;
		list.Clear();

		BuildPartManager.Ensure()?.Rebuild();
		Log.Info( $"[nz-build] cleared {n} part(s)" );
	}

	/// <summary>
	/// Put parts straight in your pocket: `nz_buildpart_give [1..3]`, no argument for all.
	/// </summary>
	///
	/// ⚠️ IT EXISTS BECAUSE THE TABLE IS THE HARD HALF TO TEST. Checking the hold, the prompt and
	/// the hand-over otherwise means placing three parts and walking the map every time.
	[ConCmd( "nz_buildpart_give" )]
	public static void Give( int part = -1 )
	{
		if ( BuildParts.Valid( part ) ) BuildParts.Take( part );
		else for ( var n = 1; n <= BuildParts.Count; n++ ) BuildParts.Take( n );

		Log.Info( $"[nz-build] the team holds {BuildParts.Held()}/{BuildParts.Count}" );
	}

	/// <summary>Empty your pockets: `nz_buildpart_drop`.</summary>
	[ConCmd( "nz_buildpart_drop" )]
	public static void Drop()
	{
		BuildParts.Clear();
		Log.Info( "[nz-build] the team holds nothing" );
	}

	/// <summary>
	/// The build's own knobs: `nz_build_set [hold seconds] [table reach] [part reach]`.
	/// </summary>
	[ConCmd( "nz_build_set" )]
	public static void Tune( float hold = -1f, float tableReach = -1f, float partReach = -1f )
	{
		if ( hold > 0f ) BuildTable.HoldSeconds = hold;
		if ( tableReach > 0f ) BuildTable.UseRange = tableReach;
		if ( partReach > 0f ) BuildPartManager.Reach = partReach;

		Log.Info( $"[nz-build] hold {BuildTable.HoldSeconds:0.##}s"
			+ $" · table reach {BuildTable.UseRange:0}u"
			+ $" · part reach {BuildPartManager.Reach:0}u"
			+ $" · builds {BuildParts.WeaponPrefab}" );
	}
}