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.
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();
}
}