Effects/BasaltEngravings.cs

Component that places decorative decal carvings (engravings) on basalt pillars. It loads decal definitions, creates non-networked GameObjects with Decal components at precomputed Spots, and exposes tunables and a console command to toggle and adjust them.

File Access
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// BASALT'S ENGRAVINGS — carvings cut into its pillars: *"add some engravings on the vertical pillars and walls etc"* (2026-09-28). A
/// frieze of runes round each broad pillar side (three variants), a hexagonal medallion on the narrower, a column of script on the
/// slim posts; and on the tall columns a second band higher up. `Gameplay.Engravings` asks for them; basalt's config does.
///
/// ⛔ DECALS, NOT GEOMETRY: each is a `Decal` pressed into the face (height, normal, rough/metal/occlusion, parallax), so the map's
/// geometry, collision and navmesh are untouched and nothing new casts a shadow. The carvings are `Tools/basalt_engravings.py`'s,
/// and so is where they go — read from the map's own BSP, every exposed concrete side of a pillar, a person's height above what it
/// stands on (or above the lava it rises from): `BasaltEngravings.Spots.cs`, generated.
/// ⛔ NOT THE EASTER EGG'S GLYPHS: an alphabet of their own, so no carving reads as a clue to the shield's code.
/// ⚠️ PERMANENT, NOT TRANSIENT: `Transient` off, no lifetime, and never in `BulletDecals`' queue, which clears as rounds end.
/// ⚠️ SHALLOW AND ANGLED: a few units deep and fading off faces that turn away (`AttenuationAngle`), so a carving on one hex side
/// does not smear round the 120° corner onto the next.
/// ⚠️ LOCAL, BUILT AS A CONFIG IS SHOWN ON BASALT (`NZGame.ShowConfig`), and nothing sent — every machine lays its own.
/// ⚠️ THE TUNABLES ARE NULLABLE-BACKED (INSTRUCTIONS §1); the table of designs is a property that builds it.
/// </summary>
public sealed partial class BasaltEngravings : Component
{
	public static BasaltEngravings Instance { get; private set; }

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

	public static BasaltEngravings Ensure( Scene scene = null )
	{
		if ( Instance.IsValid() ) return Instance;

		scene ??= Game.ActiveScene;
		if ( !scene.IsValid() ) return null;

		var go = scene.CreateObject();
		go.Name = "Basalt Engravings";
		go.Flags |= GameObjectFlags.NotSaved;
		return go.Components.Create<BasaltEngravings>();
	}

	/// <summary>The carvings, index for index with the generator's designs: three friezes, the medallion, the script.</summary>
	static string[] Designs => new[]
	{
		"decals/nz/basalt/frieze_a.decal",
		"decals/nz/basalt/frieze_b.decal",
		"decals/nz/basalt/frieze_c.decal",
		"decals/nz/basalt/medallion.decal",
		"decals/nz/basalt/script.decal",
	};

	/// <summary>`nz_engravings 0` takes them all away (this session).</summary>
	public static bool On
	{
		get => _on ?? true;
		set => _on = value;
	}

	static bool? _on;

	/// <summary>How deep a carving reaches into its face, in units.</summary>
	public static float Depth
	{
		get => _depth ?? 6f;
		set => _depth = value;
	}

	static float? _depth;

	/// <summary>
	/// A carving's turn about the face, in degrees. ⚠️ 0 IS "UPRIGHT" AS FAR AS THE CODE CAN TELL: the decal looks into the face with
	/// the world's up as its own; if the friezes come out standing on end, `nz_engravings turn 90` turns every one at once.
	/// </summary>
	public static float Turn
	{
		get => _turn ?? 0f;
		set => _turn = value;
	}

	static float? _turn;

	/// <summary>How strong the carving's relief is, times the decal's own parallax.</summary>
	public static float Parallax
	{
		get => _parallax ?? 1f;
		set => _parallax = value;
	}

	static float? _parallax;

	readonly List<GameObject> _built = new();

	/// <summary>How many carvings stand.</summary>
	public int Built => _built.Count( g => g.IsValid() );

	/// <summary>How many the map's placements hold.</summary>
	public static int Planned => Spots.Length;

	/// <summary>Every carving laid again: gone off basalt, or when the map asks for none.</summary>
	public void Rebuild()
	{
		foreach ( var g in _built ) if ( g.IsValid() ) g.Destroy();
		_built.Clear();

		if ( !On || !HexPlatforms.OnBasalt || !(ActiveConfig.Current?.Gameplay?.Engravings ?? false) ) return;

		var defs = Designs.Select( p => ResourceLibrary.Get<DecalDefinition>( p ) ).ToArray();
		if ( defs.All( d => d is null ) )
		{
			Log.Warning( "[nz-engrave] none of the carvings loaded (decals/nz/basalt/*.decal) — has the editor compiled them?" );
			return;
		}

		var missing = 0;
		foreach ( var (at, normal, design) in Spots )
		{
			var def = design >= 0 && design < defs.Length ? defs[design] : null;
			if ( def is null ) { missing++; continue; }

			var go = Scene.CreateObject();
			go.Name = "Engraving";
			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.SetParent( GameObject );

			// ⚠️ A HAIR OUT OF THE FACE, LOOKING INTO IT, the world's up its own — `BulletDecals`' `LookAt( -normal )`, with an up
			go.WorldPosition = at + normal * 2f;
			go.WorldRotation = Rotation.LookAt( -normal, Vector3.Up );

			var d = go.Components.Create<Decal>();
			d.Decals = new List<DecalDefinition> { def };
			d.Transient = false;
			d.Looped = false;
			d.Depth = Depth;
			d.AttenuationAngle = 0.5f;
			d.Size = Vector2.One;
			d.Rotation = Turn;
			d.Parallax = Parallax;
			_built.Add( go );
		}

		Log.Info( $"[nz-engrave] {Built} carving(s) on basalt's pillars{(missing > 0 ? $" ({missing} of a design that did not load)" : "")}" );
	}

	/// <summary>
	/// `nz_engravings [0|1 | depth n | turn degrees | parallax n | near]` — basalt's carvings. `0` takes them away and `1` lays them
	/// again (this session); `depth`, `turn` and `parallax` retune them all and lay them again; `near` names the five nearest you.
	/// Bare: how many stand.
	/// </summary>
	[ConCmd( "nz_engravings" )]
	public static void Cmd( string what = "", float value = float.NaN )
	{
		var m = Ensure();
		if ( !m.IsValid() ) { Log.Warning( "[nz-engrave] no scene" ); return; }

		switch ( what.Trim().ToLowerInvariant() )
		{
			case "0": On = false; break;
			case "1": On = true; break;
			case "depth" when !float.IsNaN( value ): Depth = Math.Clamp( value, 0.5f, 64f ); break;
			case "turn" when !float.IsNaN( value ): Turn = value; break;
			case "parallax" when !float.IsNaN( value ): Parallax = Math.Clamp( value, 0f, 4f ); break;

			case "near":
				var me = NZPlayer.Local;
				if ( !me.IsValid() ) { Log.Warning( "[nz-engrave] no player" ); return; }
				foreach ( var s in Spots.OrderBy( s => s.At.Distance( me.WorldPosition ) ).Take( 5 ) )
					Log.Info( $"[nz-engrave]   {Designs[s.Design]} at {s.At:0} facing {s.Normal:0.##}, {s.At.Distance( me.WorldPosition ):0}u off" );
				return;

			case "":
				break;

			default:
				Log.Warning( "[nz-engrave] nz_engravings [0|1 | depth n | turn degrees | parallax n | near]" );
				return;
		}

		if ( what.Trim().Length > 0 ) m.Rebuild();
		Log.Info( $"[nz-engrave] {(On ? "on" : "OFF")} · {m.Built} of {Planned} carving(s) standing · depth {Depth:0.#}, turn {Turn:0}°,"
			+ $" parallax x{Parallax:0.##} · the map asks for them: {ActiveConfig.Current?.Gameplay?.Engravings ?? false}" );
	}
}