Barricades/BarricadeManager.cs

Manager component that builds barricade entities from the active configuration into the scene and can rebuild or reset them. It creates a non-saved, non-networked GameObject per barricade, sets transform and properties, and calls Barricade.Rebuild/Reboard.

File AccessNetworking
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// Builds the config's barricades into the world, and rebuilds them on demand.
///
/// ⚠️ THE SAME SHAPE AS DebrisManager / InvisibleWallManager — created on demand
/// via Ensure, NotSaved so it never lands in the map, and Rebuild() is the single
/// entry point. Copying that shape is deliberate: a manager that is created and
/// refreshed differently from its siblings is one more thing to remember at every
/// call site that touches "put the config into the world".
/// </summary>
public sealed class BarricadeManager : Component
{
	public static BarricadeManager Instance { get; private set; }

	protected override void OnAwake() => Instance = this;
	protected override void OnDestroy() { if ( Instance == this ) Instance = null; }

	/// <summary>
	/// Get the manager, creating it if the scene has none.
	///
	/// ⚠️ Ensure, not `Instance?.` — a map whose barricades were only ever placed
	/// by the tool may have no manager yet, and a null-conditional call would
	/// silently build nothing. Same trap DebrisManager documents.
	/// </summary>
	public static BarricadeManager Ensure( Scene scene )
	{
		if ( Instance.IsValid() ) return Instance;
		if ( !scene.IsValid() ) return null;

		var go = scene.CreateObject();
		go.Name = "Barricade Manager";
		go.Flags |= GameObjectFlags.NotSaved;
		return go.Components.Create<BarricadeManager>();
	}

	/// <summary>
	/// Destroy what is standing and build the config again.
	///
	/// ⚠️ Wholesale, not diffed. Placements change rarely (a tool click) and a
	/// diff would have to decide whether an edited barricade is the same one —
	/// which is exactly the kind of identity question that goes wrong quietly.
	/// </summary>
	public void Rebuild()
	{
		foreach ( var b in Barricade.All.ToList() )
			b?.GameObject?.Destroy();
		Barricade.All.Clear();

		var list = ActiveConfig.Current?.Barricades;
		if ( list is null || list.Count == 0 ) return;

		foreach ( var spot in list )
			Build( spot );

		Log.Info( $"[nz] {list.Count} barricade(s) built" );
	}

	void Build( BarricadeSpot spot )
	{
		var go = Scene.CreateObject();
		go.Name = "Barricade";
		go.Flags |= GameObjectFlags.NotSaved;
		go.NetworkMode = NetworkMode.Never;   // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)

		// ⚠️ Sits at the midpoint of the run, at the BASE — the component grows
		// upward from its origin, so anchoring at the centre of the volume would
		// bury half of it in the floor.
		go.WorldPosition = spot.Centre.WithZ( MathF.Min( spot.A.z, spot.B.z ) );
		go.WorldRotation = Rotation.From( 0f, spot.Yaw, 0f );

		var bar = go.Components.Create<Barricade>();

		// ⛔ X = LENGTH, Y = THICKNESS. Rotation.From(0, yaw, 0) aims the object's
		// FORWARD (+x) along the yaw, so the run lives on x. The other way round
		// builds every barricade perpendicular to the two points that defined it.
		bar.Size = new Vector3( spot.Length, spot.Thickness, spot.Height );
		bar.MaterialPath = spot.Material;
		bar.Tint = spot.Tint;
		bar.RunA = spot.A;
		bar.RunB = spot.B;
		bar.SetBoards( spot.Boards );

		// ⛔ LAST, AFTER every property is assigned. The component built itself
		// once already when Create enabled it, using defaults — this is what makes
		// the wall match the two points that were actually clicked.
		bar.Rebuild();
	}

	/// <summary>Re-board every standing barricade. For a new round or a new game.</summary>
	public void ResetAll()
	{
		foreach ( var b in Barricade.All.Where( x => x.IsValid() ) )
			b.Reboard();
	}
}