Manager component that spawns and tracks Wunderfizz vending machines from configuration spots. Ensures a single Instance, can be created per Scene, rebuilds machines by creating GameObjects with model, collider and Wunderfizz component, and orients them to the local floor normal.
using Sandbox;
using System.Linq;
using System.Collections.Generic;
namespace NZombies;
/// <summary>
/// DER WUNDERFIZZ — the machine that sells a RANDOM perk.
///
/// ⚠️ Deliberately the same shape as PackAPunchManager / MysteryBoxManager /
/// DebrisManager: Ensure creates on demand, NotSaved keeps it out of the map,
/// Rebuild is the single entry point. A manager refreshed differently from its
/// siblings is one more thing to remember at every call site that puts a config
/// into the world — and this project has already been bitten by one that was.
/// </summary>
public sealed class WunderfizzManager : Component
{
public static WunderfizzManager Instance { get; private set; }
/// <summary>The BO6/Jupiter machine — the variant nZombies reaches for first
/// (`wunderfizz_machine/shared.lua:64`).</summary>
public const string ModelPath = "models/nz/wunderfizz/wunderfizz.vmdl";
protected override void OnAwake() => Instance = this;
protected override void OnDestroy() { if ( Instance == this ) Instance = null; }
public static WunderfizzManager Ensure( Scene scene )
{
if ( Instance.IsValid() ) return Instance;
if ( !scene.IsValid() ) return null;
var go = scene.CreateObject();
go.Name = "Wunderfizz Manager";
go.Flags |= GameObjectFlags.NotSaved;
return go.Components.Create<WunderfizzManager>();
}
readonly List<GameObject> _built = new();
/// <summary>How many are standing right now.</summary>
public int Built => _built.Count( g => g.IsValid() );
/// <summary>Destroy what is standing and build the config again.</summary>
public void Rebuild()
{
foreach ( var g in _built ) g?.Destroy();
_built.Clear();
var list = ActiveConfig.Current?.Wunderfizzes;
if ( list is null || list.Count == 0 ) return;
foreach ( var spot in list )
Build( spot );
// ⚠️ Says how many are STANDING, not how many are configured. A model that
// fails to load leaves a spot in the config and nothing in the world, and
// those two numbers disagreeing is the cheapest way to see it.
Log.Info( $"[nz] {Built} of {list.Count} Wunderfizz machine(s) built" );
}
void Build( WunderfizzSpot spot )
{
var go = Scene.CreateObject();
go.Name = "Der Wunderfizz";
go.Flags |= GameObjectFlags.NotSaved;
go.NetworkMode = NetworkMode.Never; // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
go.WorldPosition = spot.Position;
go.WorldRotation = OnFloor( spot );
var model = Model.Load( ModelPath );
// ⚠️ Model.Load returns null on a bad path but an ERROR MODEL on a
// compiled-but-broken one, and the error model renders happily as a
// checkerboard. Checking only for null reports success on a machine that is
// visibly wrong — the same check ZombieAI.EnsureBody documents.
if ( model is null || model.IsError )
{
Log.Warning( $"[nz] Wunderfizz model {ModelPath} "
+ $"({(model is null ? "null" : "error model")}) — recompile it" );
go.Destroy();
return;
}
var r = go.Components.Create<ModelRenderer>();
r.Model = model;
// Solid, so you cannot walk through it. Static — a vending machine has no
// reason to simulate, and one that could be shoved would end up inside a
// wall on any map with a slope.
var box = go.Components.Create<BoxCollider>();
box.Scale = model.Bounds.Size;
box.Center = model.Bounds.Center;
var fizz = go.Components.Create<Wunderfizz>();
fizz.Spot = spot;
_built.Add( go );
}
/// <summary>
/// A rotation facing `yaw` but lying flat on the spot's floor.
///
/// ⛔ The SAME maths PackAPunchManager and MysteryBoxManager document — the
/// heading is projected onto the surface plane before LookAt sees it, because
/// feeding LookAt a yaw that is not perpendicular to the up vector lets it
/// renormalise however it likes and twists the machine on a slope.
/// </summary>
static Rotation OnFloor( WunderfizzSpot spot )
{
var up = spot.Normal.IsNearlyZero() ? Vector3.Up : spot.Normal.Normal;
var heading = Rotation.FromYaw( spot.Yaw ).Forward;
var forward = (heading - up * heading.Dot( up )).Normal;
if ( forward.IsNearlyZero() )
return Rotation.From( 0f, spot.Yaw, 0f );
return Rotation.LookAt( forward, up );
}
}