Part of the HexPlatforms manager for the Basalt Easter egg, implements the altar pillar, its geometry and engravings, the cursed-flame placement rules and host/client command handlers. Builds a local pillar model, handles local aim/use checks, host-side placement validation, state tracking and console commands to manipulate the altar and flame parameters.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace NZombies;
/// <summary>
/// BASALT — STEP 5, THE ALTAR (named by the user, 2026-09-26). A slim hexagonal pillar stands in the middle of the cyan
/// shield's column, where the user marked the spot with an ASP wall buy — inside the shield, so it is reached only once
/// the shield lock is open — and the cursed flame is set on it. Asked for as *"the altar is a slim hexagonal pillar at
/// about 2/3 of a zombie's height that stands upright in that place and has some engravings on it, these are just for
/// ambiance, and they are white like the lights — i need to place the cursed flame there which i can only do after i
/// disable the combine wall"*.
///
/// ⛔ THE PILLAR: 48u tall — two thirds of a zombie's 72 (`ZombieAI.BodyHeight`) — and 16u across its flats, in the stepping
/// stones' own concrete, turned as the column and the tiles are, points north and south. Its engravings are strokes of the
/// map's own white light (`lights/white001`, the lights'): a double band round its foot and its head, a motif on each side
/// — a column of chevrons, a sun, a zigzag, twice round — and a hexagon on its top where the flame goes. ⚠️ NONE OF THEM
/// IS A COLOR RINGS GLYPH: they mean nothing, and a glyph would read as a clue.
///
/// ⛔ SETTING THE FLAME ON IT: the carrier, within reach and looking at the altar with the shield lock open, sees "Press E -
/// Place the cursed flame" (`UsePrompt.ForAltar`), and E sets it there (`NZPlayer.TickUse`): it leaves their hands and
/// burns over the altar's top, and the engravings shine in its colour. Placing it plays the pickup's sound at the altar,
/// and the step-done clicking after — and begins THE ALTAR DEFENSE (`HexPlatforms.Defense.cs`): a minute of a wave
/// that goes for the altar. Held, the flame burns light blue and can be taken up again; fallen, it is gone until the next
/// round brings it back over tile 1.
///
/// ⛔ WHO OWNS WHAT (INSTRUCTIONS.md, "BUILD FOR MULTIPLAYER"):
/// - the PRESS is the presser's machine's: it asks the host (`NZNet.CursedFlamePlace`), which knows who from the call;
/// - WHETHER THE FLAME IS ON THE ALTAR is HOST state, MIRRORED with its carrier (`NZNet.CursedFlameState`) and to a joiner;
/// - the ALTAR itself is LOCAL: every machine builds its own, all game, whatever the steps — it is part of the room.
/// </summary>
public sealed partial class HexPlatforms
{
// ══ where it is ══════════════════════════════════════════════════════════════════════════
/// <summary>
/// The altar's foot: the middle of the cyan shield's column (<see cref="ShieldCentre"/>, 1792 608), on its floor at 1196 —
/// where the ASP wall buy the user placed stood, 0.3u off that middle, flat on the floor (surveyed 2026-09-26).
/// </summary>
static Vector3 AltarAt => new( ShieldCentre.x, ShieldCentre.y, 1196f );
/// <summary>How tall the altar is, and how far from its middle each flat lies: 48u — two thirds of a zombie — and 8u.</summary>
const float AltarHeight = 48f, AltarApothem = 8f;
/// <summary>How far from its middle each corner lies: a regular hexagon's, 2/√3 of the flats' — and so each side's width.</summary>
static float AltarCorner => AltarApothem * 2f / MathF.Sqrt( 3f );
/// <summary>The middle of its top, where the flame is set.</summary>
static Vector3 AltarTop => AltarAt + Vector3.Up * AltarHeight;
/// <summary>
/// How far a carrier's feet may be from the altar's foot to set the flame on it: 90u, an arm's length and a step. ⚠️ THE
/// SHIELD'S FACES ARE ONLY 60U OUT, so from outside a standing shield the altar is in reach — which is why the rule asks
/// for the lock open as well, not only for reach.
/// </summary>
const float AltarReach = 90f;
/// <summary>How near its axis a look must pass to be at the altar, in units.</summary>
const float AltarAimRadius = 14f;
/// <summary>The altar's corners, round its middle, counter-clockwise from above from the north point — the tiles' order.</summary>
static Vector2[] AltarCorners()
{
float a = AltarApothem, r = AltarCorner;
return new[] { new Vector2( 0f, r ), new Vector2( -a, r * 0.5f ), new Vector2( -a, -r * 0.5f ),
new Vector2( 0f, -r ), new Vector2( a, -r * 0.5f ), new Vector2( a, r * 0.5f ) };
}
// ══ the flame on it ═════════════════════════════════════════════════════════════════════
static float? _altarFlameRadius, _altarFlameHeight, _altarFlameScale, _altarFlameLight;
static int? _altarFlames;
/// <summary>The flame on the altar: a sphere of 12u radius — between the loose one's 22 and the held one's 6 — …</summary>
public static float AltarFlameRadius { get => Math.Clamp( _altarFlameRadius ?? 12f, 2f, 80f ); set => _altarFlameRadius = value; }
/// <summary>
/// …its middle 17u over the altar's top, so its lowest flames start some 5u off the stone — lowered from 22 by request,
/// *"lower the flame placement height on the altar a little bit"*…
/// </summary>
public static float AltarFlameHeight { get => Math.Clamp( _altarFlameHeight ?? 17f, 0f, 200f ); set => _altarFlameHeight = value; }
/// <summary>…of 12 flames, each 0.8 of a burning body's, and a light of 2.</summary>
public static int AltarFlames { get => Math.Clamp( _altarFlames ?? 12, 1, 48 ); set => _altarFlames = value; }
public static float AltarFlameScale { get => Math.Clamp( _altarFlameScale ?? 0.8f, 0.05f, 10f ); set => _altarFlameScale = value; }
public static float AltarFlameLight { get => Math.Clamp( _altarFlameLight ?? 2f, 0f, 20f ); set => _altarFlameLight = value; }
/// <summary>Where the flame on the altar floats from.</summary>
static Vector3 AltarFlameHome => AltarTop + Vector3.Up * AltarFlameHeight;
/// <summary>HOST — is the cursed flame on the altar? For good, until a new game.</summary>
bool _flamePlaced;
/// <summary>MIRROR — the same, as this machine was told (`NZNet.CursedFlameState`).</summary>
bool _flamePlacedShown;
/// <summary>HOST — whether it is on the altar, for `NZNet.PushState` to replay to a joiner.</summary>
public static bool FlamePlacedState => Instance.IsValid() && Instance._flamePlaced;
/// <summary>
/// Is this player carrying the cursed flame, and looking at the altar, near enough, with the shield lock open? LOCAL —
/// the prompt's test and the use key's, one question for both (`UsePrompt.ForAltar`, `NZPlayer.TickUse`). The look is
/// the camera's ray passing near the altar's axis, from its foot up to the flame's place over it, as the altar's
/// collider is not the whole of what a look at it means.
/// </summary>
public static bool AltarAimed( NZPlayer player )
{
var m = Instance;
if ( !m.IsValid() || !OnBasalt || !m._lockOpenShown || m._flamePlacedShown || CannotCarry( player ) ) return false;
if ( !m.FlameCarried || m._flameCarrierShown != CarrierIdOf( player ) ) return false;
if ( AltarAt.Distance( player.WorldPosition ) > AltarReach ) return false;
var cam = m.Scene.Camera;
if ( !cam.IsValid() ) return false;
// spheres up the axis, 6u apart and 14u round, so together they are a column a look cannot slip between
for ( var z = 0f; z <= AltarHeight + AltarFlameHeight; z += 6f )
if ( RayMeetsSphere( cam.WorldPosition, cam.WorldRotation.Forward, AltarAt + Vector3.Up * z, AltarAimRadius ) )
return true;
return false;
}
/// <summary>E on the altar. LOCAL — the presser's machine asks the host, which decides (<see cref="HostPlaceFlame"/>).</summary>
public static void PlaceCursedFlame( NZPlayer player )
{
if ( player.IsValid() ) NZNet.CursedFlamePlace();
}
/// <summary>
/// Someone pressed E on the altar. HOST — `NZNet.CursedFlamePlace`, with `who` the caller's connection id as the call
/// itself carries it, or "" for the host's own press.
/// </summary>
public static void HostPlaceFlame( string who )
{
if ( NZGame.IsClient ) return;
var m = Instance;
if ( !m.IsValid() ) return;
var body = CarrierBodyOf( who );
if ( !body.IsValid() && (string.IsNullOrEmpty( who ) || who == Connection.Local?.Id.ToString()) ) body = NZPlayer.Local;
var refused = m.PlaceFlame( body );
if ( refused != "" ) Log.Info( $"[nz-hex] the cursed flame is not set on the altar: {refused}" );
}
/// <summary>
/// The rules for setting it on the altar. HOST — apart from the RPC, so the selftest can walk it. The fire must be out,
/// the flame not there already, the shield lock open, and the one setting it its carrier, up and within reach of the
/// altar (`anywhere` skips the reach, for the test). Returns why not, or "".
/// </summary>
string PlaceFlame( NZPlayer body, bool anywhere = false )
{
if ( _bonfire != BonfireOut ) return "the fire is not out";
if ( _flamePlaced ) return "it burns on the altar already";
if ( !_lockOpen ) return "the cyan shield stands round the altar — the lock is shut";
if ( string.IsNullOrEmpty( _flameCarrier ) || CarrierIdOf( body ) != _flameCarrier ) return "they do not carry it";
if ( CannotCarry( body ) ) return "they are down, out or gone";
if ( !anywhere && AltarAt.Distance( body.WorldPosition ) > AltarReach + ReachSlack ) return "they are too far from the altar";
_flameCarrier = "";
_flamePlaced = true;
SendFlame();
if ( !Testing ) NZSound.PlayShared( NZSound.PickupSalvage, AltarTop );
// ⚠️ THE PURPLE FLAME WAKES THE DEFENSE; the light blue one — the altar already held — only rests there again
if ( _defense == Defense.Won )
{
Log.Info( $"[nz-hex] 🗿 {NameFor( body )} set the light blue flame back on the altar" );
return "";
}
if ( !Testing ) DoneCueLater();
Log.Info( $"[nz-hex] 🗿 THE ALTAR — {NameFor( body )} set the cursed flame on the altar" );
// ⚠️ STEP 6 DONE: the word goes out for its banner (`EggProgress`); its fanfare plays nothing, the defense's music begins.
// Again each time the purple flame is set back after a lost defense, which is the step done again.
Fanfare( 6 );
StartDefense();
return "";
}
// ══ the pillar ══════════════════════════════════════════════════════════════════════════
/// <summary>LOCAL — the altar: the pillar, its collider and its engravings, children of one object.</summary>
GameObject _altarGo;
/// <summary>
/// How the altar is drawn: ⚠️ BUMP IT WHEN THAT CHANGES. 1: the pillar and its engravings, 2026-09-26. A manager that has
/// not built this layout builds the altar again — `OnUpdate` asks every frame — as the lock's `LockLayout` does.
/// </summary>
const int AltarLayout = 1;
/// <summary>LOCAL — the layout this manager last built the altar in; 0 before it has.</summary>
int _altarLaid;
/// <summary>The altar's object, by name: how a sweep knows one a manager before this one left.</summary>
const string AltarName = "Basalt altar";
/// <summary>How wide an engraving's strokes are, in units, and how far proud of the stone they lie.</summary>
const float EngraveStroke = 0.45f, EngraveProud = 0.3f;
/// <summary>Say once that the stone did not load, not at every rebuild.</summary>
static bool _altarMissing;
/// <summary>The altar, on basalt, all game: built once, and again at a new layout. LOCAL.</summary>
void BuildAltar()
{
_altarLaid = AltarLayout;
ClearAltar();
if ( !OnBasalt || !Scene.IsValid() ) return;
var body = AltarModel( Material.Load( StoneMaterial ) );
if ( body is null )
{
if ( !_altarMissing ) Log.Warning( $"[nz-hex] {StoneMaterial} did not load — the altar is not built" );
_altarMissing = true;
return;
}
var root = Scene.CreateObject();
root.Name = AltarName;
root.Flags |= GameObjectFlags.NotSaved;
root.NetworkMode = NetworkMode.Never; // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
root.Tags.Add( PanelTag );
root.WorldPosition = AltarAt;
root.WorldRotation = Rotation.Identity;
root.Components.Create<ModelRenderer>().Model = body;
// ⚠️ SOLID, as a pillar is: a box round the hexagon, its corners the box's — a sliver more than the stone, never less
var box = root.Components.Create<BoxCollider>();
box.Scale = new Vector3( AltarApothem * 2f, AltarCorner * 2f, AltarHeight );
box.Center = new Vector3( 0f, 0f, AltarHeight * 0.5f );
// the engravings: each side's, in its plane, facing out of it; and the hexagon on its top
var corners = AltarCorners();
for ( var k = 0; k < 6; k++ )
{
var a = corners[k];
var b = corners[(k + 1) % 6];
var along = (b - a) * (1f / (b - a).Length);
var n = new Vector3( along.y, -along.x, 0f ); // the edge turned right: out of the side
var mid = (a + b) * 0.5f;
AltarPart( root, $"engraving {k + 1}", IconModel( AltarSideShapes( k ), EngraveStroke, White, 1f ),
new Vector3( mid.x, mid.y, AltarHeight * 0.5f ) + n * EngraveProud, Rotation.LookAt( n, Vector3.Up ) );
}
AltarPart( root, "engraving on its top", IconModel( AltarTopShapes(), EngraveStroke, White, 1f ),
new Vector3( 0f, 0f, AltarHeight + EngraveProud ), Rotation.LookAt( Vector3.Up, Vector3.Forward ) );
_altarGo = root;
// in the flame's colour, if it burns there already (`HexPlatforms.Defense.cs`)
DressAltar();
}
/// <summary>One of the altar's engravings: a child of it, placed and turned in its own frame, drawing this model.</summary>
void AltarPart( GameObject root, string name, Model model, Vector3 at, Rotation facing )
{
if ( model is null ) return;
var go = Scene.CreateObject();
go.Name = name;
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( root, false );
go.LocalPosition = at;
go.LocalRotation = facing;
var r = go.Components.Create<ModelRenderer>();
r.Model = model;
r.RenderType = ModelRenderer.ShadowRenderType.Off;
}
/// <summary>The altar gone — and any a manager before this one left, found by name. LOCAL.</summary>
void ClearAltar()
{
if ( _altarGo.IsValid() ) _altarGo.Destroy();
_altarGo = null;
if ( !Scene.IsValid() ) return;
foreach ( var old in Scene.GetAllObjects( false ).Where( x => x.Tags.Has( PanelTag ) && x.Name == AltarName ).ToList() )
old.Destroy();
}
/// <summary>
/// The pillar as one model in the stones' concrete, from its foot at the object's origin: its six sides and its top,
/// the texture laid as the map lays it on the stones — world-aligned, u along a side and v down, one repeat every
/// 128u (`StoneUnits`) — for where its foot stands: the Altar's, unless another is given (the blue altar's,
/// `HexPlatforms.BlueAltar.cs`). Null without the material.
/// </summary>
static Model AltarModel( Material stone, Vector3? foot = null )
{
if ( stone is null ) return null;
var at = foot ?? AltarAt;
var corners = AltarCorners();
var verts = new List<Vertex>();
var idx = new List<int>();
float h = AltarHeight, uv = StoneUnits;
var place = new Vector2( at.x, at.y );
for ( var i = 0; i < 6; i++ )
{
var a = corners[i];
var b = corners[(i + 1) % 6];
var along = (b - a) * (1f / (b - a).Length);
var n = new Vector3( along.y, -along.x, 0f );
var t = new Vector3( along.x, along.y, 0f );
var wa = a + place;
var wb = b + place;
float u0 = (wa.x * along.x + wa.y * along.y) / uv, u1 = (wb.x * along.x + wb.y * along.y) / uv;
float vLo = -at.z / uv, vHi = -(at.z + h) / uv;
var s = verts.Count;
verts.Add( new Vertex( new Vector3( a.x, a.y, 0f ), n, t, new Vector4( u0, vLo, 0f, 0f ) ) );
verts.Add( new Vertex( new Vector3( b.x, b.y, 0f ), n, t, new Vector4( u1, vLo, 0f, 0f ) ) );
verts.Add( new Vertex( new Vector3( b.x, b.y, h ), n, t, new Vector4( u1, vHi, 0f, 0f ) ) );
verts.Add( new Vertex( new Vector3( a.x, a.y, h ), n, t, new Vector4( u0, vHi, 0f, 0f ) ) );
// the winding `HexSidePanels.Build` uses, which faces out
idx.Add( s ); idx.Add( s + 1 ); idx.Add( s + 2 );
idx.Add( s ); idx.Add( s + 2 ); idx.Add( s + 3 );
}
// its top: a fan from the middle, counter-clockwise from above, so it faces up
var c = verts.Count;
verts.Add( new Vertex( new Vector3( 0f, 0f, h ), Vector3.Up, Vector3.Forward, new Vector4( place.x / uv, place.y / uv, 0f, 0f ) ) );
foreach ( var p in corners )
verts.Add( new Vertex( new Vector3( p.x, p.y, h ), Vector3.Up, Vector3.Forward,
new Vector4( (place.x + p.x) / uv, (place.y + p.y) / uv, 0f, 0f ) ) );
for ( var i = 0; i < 6; i++ )
{
idx.Add( c ); idx.Add( c + 1 + i ); idx.Add( c + 1 + (i + 1) % 6 );
}
var r = AltarCorner;
var bounds = new BBox( new Vector3( -r, -r, 0f ), new Vector3( r, r, h ) );
var mesh = new Mesh( stone );
mesh.CreateVertexBuffer( verts.Count, verts );
mesh.CreateIndexBuffer( idx.Count, idx );
// ⛔ SET BY HAND, AFTER THE BUFFERS: a runtime mesh gets empty bounds otherwise, and is culled (`HexSidePanels.Build`)
mesh.Bounds = bounds;
return Model.Builder.AddMesh( mesh ).WithViewBounds( bounds ).Create();
}
/// <summary>
/// A side's engraving, in the stroke family, in units round the side's middle — u across it, the viewer's right, and v
/// up, the altar spanning -24..24 and each side 9.2u wide: a double band at its foot and its head, and between them one
/// of three motifs, side by side round the altar twice — a column of chevrons with a small sun at its head, a sun with
/// its rays, and a zigzag rising. For the room, not a clue: none is a Color Rings glyph.
/// </summary>
static (string Kind, float[] P)[] AltarSideShapes( int side )
{
const float w = 4.45f; // a band runs almost to each corner
var s = new List<(string Kind, float[] P)>
{
("line", new[] { -w, -20f, w, -20f }), ("line", new[] { -w, -18.6f, w, -18.6f }),
("line", new[] { -w, 20f, w, 20f }), ("line", new[] { -w, 18.6f, w, 18.6f }),
};
switch ( side % 3 )
{
case 0: // chevrons up a stem, a sun at its head
s.Add( ("line", new[] { 0f, -15f, 0f, 9f }) );
foreach ( var v in new[] { -11f, -5.5f, 0f, 5.5f } )
s.Add( ("line", new[] { -2.3f, v - 2.3f, 0f, v, 2.3f, v - 2.3f }) );
s.Add( ("ring", new[] { 0f, 12.6f, 2f }) );
s.Add( ("dot", new[] { 0f, 12.6f, 0.6f }) );
break;
case 1: // a sun and its rays, a dot above and below
s.Add( ("ring", new[] { 0f, 0f, 2.8f }) );
s.Add( ("dot", new[] { 0f, 0f, 0.8f }) );
// its rays: long up and down, shorter aslant, shortest across — a side is only 9.2u wide
for ( var k = 0; k < 8; k++ )
{
var a = k * MathF.PI / 4f;
float cx = MathF.Cos( a ), cy = MathF.Sin( a );
var end = k % 4 == 2 ? 6f : k % 2 == 1 ? 5.2f : 4.2f;
s.Add( ("line", new[] { cx * 3.6f, cy * 3.6f, cx * end, cy * end }) );
}
s.Add( ("dot", new[] { 0f, 11.5f, 0.7f }) );
s.Add( ("dot", new[] { 0f, -11.5f, 0.7f }) );
s.Add( ("line", new[] { 0f, 13.6f, 0f, 16f }) );
s.Add( ("line", new[] { 0f, -13.6f, 0f, -16f }) );
break;
default: // a zigzag rising to a point
s.Add( ("line", new[] { -1.9f, -15f, 1.9f, -10f, -1.9f, -5f, 1.9f, 0f, -1.9f, 5f, 1.9f, 10f, 0f, 14.5f }) );
s.Add( ("dot", new[] { -3.2f, -12.5f, 0.55f }) );
s.Add( ("dot", new[] { 3.2f, -2.5f, 0.55f }) );
s.Add( ("dot", new[] { -3.2f, 7.5f, 0.55f }) );
break;
}
return s.ToArray();
}
/// <summary>
/// The engraving on its top: a hexagon inset 2u from its edge, turned as the pillar is, and a small ring in its middle
/// under where the flame burns. In the top's own frame (`Rotation.LookAt( Up, Forward )`): u runs to world -Y, v to +X.
/// </summary>
static (string Kind, float[] P)[] AltarTopShapes()
{
var inset = (AltarApothem - 2f) / AltarApothem;
var pts = new List<float>();
foreach ( var p in AltarCorners().Append( AltarCorners()[0] ) )
{
pts.Add( -p.y * inset );
pts.Add( p.x * inset );
}
return new (string Kind, float[] P)[]
{
("line", pts.ToArray()),
("ring", new[] { 0f, 0f, 2.2f }),
};
}
// ══ commands ════════════════════════════════════════════════════════════════════════════
/// <summary>
/// `nz_hex_altar [place|off]` — HOST: the Altar as it stands. `place` sets the cursed flame on it by hand, whoever holds
/// it and whatever the lock — the fire must be out (`nz_hex_skipto torch`); `off` takes it off, back over tile 1, as
/// `nz_hex_torch drop` does. By the rules, its carrier sets it there with E, the shield lock open.
/// </summary>
[ConCmd( "nz_hex_altar" )]
public static void AltarCmd( string what = "" )
{
if ( NZGame.IsClient ) { Log.Warning( "[nz-hex] host only" ); return; }
var m = Ensure();
if ( !m.IsValid() ) { Log.Warning( "[nz-hex] no game running — start one first" ); return; }
switch ( what.Trim().ToLowerInvariant() )
{
case "":
break;
case "place":
if ( m._bonfire != BonfireOut ) { Log.Warning( "[nz-hex] the fire is not out, so there is no cursed flame — nz_hex_skipto torch first" ); return; }
m._flameCarrier = "";
m._flameLost = false;
m._flamePlaced = true;
m.SendFlame();
Log.Info( "[nz-hex] 🗿 the cursed flame set on the altar by hand" );
break;
case "off":
m.ResetFlame();
break;
default:
Log.Warning( "[nz-hex] nz_hex_altar place or off — or nothing, to see where it stands" );
return;
}
Log.Info( $"[nz-hex] {m.TorchStateText()} · the shield lock is {( m._lockOpen ? "open" : "shut" )}"
+ ( OnBasalt ? $" · the altar {( m._altarGo.IsValid() ? "stands" : "is NOT built" )} at {AltarAt}" : " · the altar is basalt's" ) );
}
/// <summary>
/// `nz_hex_altar_place` — HOST: take away any wall buy standing where the altar is — the ASP that marked the spot — so
/// the two do not stand in one another. In memory, like every edit: Save (nz_save) keeps it.
/// </summary>
[ConCmd( "nz_hex_altar_place" )]
public static void AltarPlaceCmd()
{
if ( NZGame.IsClient ) { Log.Warning( "[nz-hex] the config is the host's" ); return; }
var cfg = ActiveConfig.Current;
if ( cfg?.WallBuys is null ) { Log.Warning( "[nz-hex] no config here" ); return; }
var gone = cfg.WallBuys.Where( b => b.Position.Distance( AltarAt ) < 24f ).ToList();
foreach ( var b in gone ) cfg.WallBuys.Remove( b );
if ( gone.Count > 0 ) WallBuyManager.Ensure()?.Rebuild();
Log.Info( gone.Count == 0 ? "[nz-hex] no wall buy stands where the altar is"
: $"[nz-hex] {string.Join( ", ", gone.Select( b => b.WeaponPrefab ) )} went from where the altar is — in memory: Save (nz_save) keeps it" );
}
/// <summary>
/// `nz_hex_altar_flame [radius] [height] [flames] [scale] [light]` — the cursed flame as it burns on the altar: the radius
/// of its sphere, how high its middle floats over the top, how many flames and how big, and how bright its light —
/// redrawn at once; bare, it prints them. On this machine and until a restart, like `nz_hex_flame`.
/// </summary>
[ConCmd( "nz_hex_altar_flame" )]
public static void AltarFlameCmd( float radius = 0f, float height = -1f, int flames = 0, float scale = 0f, float light = -1f )
{
if ( radius > 0f ) AltarFlameRadius = radius;
if ( height >= 0f ) AltarFlameHeight = height;
if ( flames > 0 ) AltarFlames = flames;
if ( scale > 0f ) AltarFlameScale = scale;
if ( light >= 0f ) AltarFlameLight = light;
var m = Instance;
if ( m.IsValid() ) m.BuildRewardTile();
Log.Info( $"[nz-hex] the cursed flame on the altar: {AltarFlames} flames round a sphere of {AltarFlameRadius:0.#}u radius,"
+ $" its middle {AltarFlameHeight:0.#}u over the altar's top, each flame {AltarFlameScale:0.##}× a burning body's,"
+ $" its light {AltarFlameLight:0.##}" + ( m.IsValid() && m._flamePlacedShown ? "" : " — not on the altar now (nz_hex_altar place)" ) );
}
}