Component that builds and manages pressable button game objects from configuration. It can ensure a singleton manager exists, rebuild pressable instances from ActiveConfig.Current.Pressables, create visual/model/collider components, and orient buttons to wall or floor surfaces.
using Sandbox;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// Builds the config's pressables.
///
/// ⚠️ A PLACEHOLDER BOX FOR NOW, like the misery device — a real button prop comes later.
/// Kept small and wall-mountable, because a button belongs on a wall far more often than
/// on the floor.
/// </summary>
public sealed class PressableManager : Component
{
public static PressableManager Instance { get; private set; }
protected override void OnAwake() => Instance = this;
protected override void OnDestroy() { if ( Instance == this ) Instance = null; }
public static PressableManager Ensure( Scene scene = null )
{
if ( Instance.IsValid() ) return Instance;
scene ??= Game.ActiveScene;
if ( !scene.IsValid() ) return null;
var go = scene.CreateObject();
go.Name = "Pressable Manager";
go.Flags |= GameObjectFlags.NotSaved;
return go.Components.Create<PressableManager>();
}
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?.Pressables;
if ( list is null || list.Count == 0 ) return;
foreach ( var spot in list )
{
// ⚠️ REGISTERED ON BUILD, so every flag a step names has a tie-break index before
// play starts. `EggFlags.Register` is order-dependent and idempotent — doing it
// here means the indices follow config order rather than whatever order the
// player happens to complete steps in.
spot.Step?.RegisterFlags();
Build( spot );
}
Log.Info( $"[nz] {Built} of {list.Count} pressable(s) built" );
}
void Build( PressableSpot spot )
{
var go = Scene.CreateObject();
go.Name = "Pressable";
go.Flags |= GameObjectFlags.NotSaved;
go.NetworkMode = NetworkMode.Never; // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
var n = spot.Normal.IsNearlyZero() ? Vector3.Up : spot.Normal.Normal;
var wall = n.z <= 0.7f;
go.WorldPosition = spot.Position;
go.WorldRotation = OnSurface( spot );
var size = spot.Size;
var vis = Scene.CreateObject();
vis.Name = "visual";
vis.SetParent( go );
vis.LocalRotation = Rotation.Identity;
// ⚠️ A WALL BUTTON PUSHES OUT ALONG ITS OWN FORWARD; A FLOOR ONE SITS ON ITS BASE.
// `OnSurface` has already turned the object to face out of the wall, so "out" is
// local forward there and local up on a floor — one offset expressed in the frame
// the rotation just established, rather than two special cases in world space.
vis.LocalPosition = wall
? Vector3.Forward * size.x * 0.5f
: Vector3.Up * size.z * 0.5f;
var cube = Model.Cube.Bounds.Size;
vis.LocalScale = new Vector3( size.x / cube.x, size.y / cube.y, size.z / cube.z );
var mr = vis.Components.Create<ModelRenderer>();
mr.Model = Model.Cube;
mr.Tint = new Color( 0.85f, 0.72f, 0.20f, 1f );
var solid = vis.Components.Create<BoxCollider>();
solid.Scale = cube;
var press = go.Components.Create<Pressable>();
press.Spot = spot;
// ⚠️ THE BODY IS HANDED OVER, not searched for. `Pressable` switches this whole
// child off when its step completes, and a lookup by type would find whatever a future
// prop happens to be built from rather than the thing this method actually made.
press.Visual = vis;
_built.Add( go );
}
/// <summary>Wall or floor, the buyable ending's split — see `ClueManager.OnSurface`.</summary>
static Rotation OnSurface( PressableSpot spot )
{
var n = spot.Normal.IsNearlyZero() ? Vector3.Up : spot.Normal.Normal;
if ( n.z <= 0.7f ) return Rotation.LookAt( n );
var heading = Rotation.FromYaw( spot.Yaw ).Forward;
var forward = (heading - n * heading.Dot( n )).Normal;
if ( forward.IsNearlyZero() )
return Rotation.From( 0f, spot.Yaw, 0f );
return Rotation.LookAt( forward, n );
}
}