EasterEgg/HexPlatforms.Arena.cs

Manager code that builds and controls a custom boss arena on the Basalt map. It splits the map meshes that form a floor slab into raised floor copies and remainder copies, creates an upper floor prism and a ceiling under the lava bed, triggers navmesh tile regeneration on the host, and exposes a console command to raise/lower and set the stage height locally.

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

namespace NZombies;

/// <summary>
/// BASALT — THE BOSS ARENA (2026-09-27): the great room under the lava room, walled on every side, where the teleporter is
/// to send the players at the end. The user marked it with an ASP wall buy on its floor, at (-2994, -668, -800). That floor
/// — the BSP's brush 183, 2040 × 1752u and 42u thick, its top at -800 under a pool of water 108u deep — stands RAISED 244u,
/// its top at -556, out of the water: three zombies' height, asked for as *"raise the platform its in by about 3 zombie
/// heights"*, then *"its the big floor slab im talking about"* — and 28u more since *"then make the gap 20 u"* (2026-09-27),
/// which left the upper floor's top where it stood.
///
/// ⛔ THE FLOOR IS PART OF BIGGER MESHES, SO IT IS SPLIT OUT OF THEM, as the cyan shields are (`HexPlatforms.Shields.cs`):
/// every world mesh holding a face of the slab is cloned twice — one copy without the slab's faces, in the original's place,
/// and one with only them, raised — and the original switched off. A world mesh builds its collision from its faces, so the
/// floor underfoot rises with them. The slab's faces are found by where they lie — inside its box — not by a mesh's name.
/// Its four edges run under the arena's walls, so only its top and bottom were ever drawn.
///
/// ⛔ ON IT STANDS AN UPPER FLOOR, <see cref="ArenaStageHeight"/> higher, cut to keep one zombie's height — 72u — from the
/// nearest wall all the way round: asked for as *"create another floor slightly higher than that one, but it's cut
/// perfectly to always be 1 zombie height away from the nearest wall"*. Its outline is read off the map by
/// `Tools/basalt_arena_floor.py` — every point of the arena 72u or more from a wall, straight where a wall is straight and
/// round where it wraps a wall's corner — and generated into `HexPlatforms.ArenaOutline.cs`. A prism in the floor's own
/// concrete, solid as a barrier is (`DebrisMesh`): the gap round it shows the raised floor, a drop of 20u.
///
/// ⛔ AND OVER IT ALL, A CEILING right under the lava bed — the config's first wall drawn in lava, whose bottom showed from
/// the arena as a box of lava overhead: asked for as *"add a platform right bellow the lava that's on top, so we have a
/// cieling"*. A slab of the stones' concrete, wall to wall and a few units into them, <see cref="ArenaCeilingThickness"/>
/// thick, its top <see cref="ArenaCeilingGap"/> under the lava's bottom — read off the config, so it follows the lava.
///
/// ⛔ AND THE NAVMESH OVER THE ARENA IS BUILT AGAIN, ON THE HOST, once both floors stand.
///
/// ⚠️ LOCAL, ALL GAME, ON BASALT: every machine raises its own copy of the map's floor, from these numbers, when a config is
/// shown — and tries again for a few seconds if the map's meshes were not there yet.
///
/// ⚠️ THE ARENA LIES INSIDE THE RISING LAVA'S RECTANGLE and far below its level (`HexPlatforms.Lava.cs`): anyone down here
/// while the lava is up would be taken. The steps never bring anyone here before it has sunk.
/// </summary>
public sealed partial class HexPlatforms
{
	/// <summary>The floor slab as the map has it: the BSP's brush 183.</summary>
	static Vector3 ArenaSlabMin => new( -4016f, -1364f, -842f );
	static Vector3 ArenaSlabMax => new( -1976f, 388f, -800f );

	/// <summary>A zombie's standing height, `ZombieAI.BodyHeight`'s own default.</summary>
	const float ZombieHeight = 72f;

	static float? _arenaRaise;

	/// <summary>
	/// How far the floor is raised: 244u — three zombies' height and 28 more, since the gap round the upper floor went from 48u
	/// deep to 20 (*"then make the gap 20 u"*, 2026-09-27) with the upper floor's top kept where it stood, at -536. With
	/// <see cref="ArenaStageHeight"/> it makes that top: change one, change the other with it. `nz_hex_arena raise [units]` sets
	/// it on this machine and until a restart; set the default here once settled.
	/// </summary>
	public static float ArenaRaise { get => Math.Clamp( _arenaRaise ?? 3f * ZombieHeight + 28f, 0f, 1500f ); set => _arenaRaise = value; }

	/// <summary>
	/// What the split's copies carry. ⚠️ NOT `PanelTag`, WHICH `Ensure` SWEEPS AWAY — sweeping a rest-of-the-mesh copy would
	/// take the arena's walls with it, the original being off (the shields' rule).
	/// </summary>
	const string ArenaTag = "nz_arenasplit";

	/// <summary>The copies' names, after their world mesh's: how a manager after a hotload finds them again.</summary>
	const string ArenaFloorSuffix = " — the boss arena's floor", ArenaRestSuffix = " — the rest, without the boss arena's floor";

	static float? _arenaStage;

	/// <summary>
	/// How far the upper floor stands over the raised floor, in units — the gap round it that deep: 20, by the user's word:
	/// *"then make the gap 20 u"* (2026-09-27), so whoever drops into its lava can climb out. It was 48, three times the first
	/// 16 (*"make it a bit taller, like 3x taller than it currently is"*); the floor under it is raised 28u more to keep its
	/// top where it stood (<see cref="ArenaRaise"/>). `nz_hex_arena stage [height]` sets it on this machine and until a
	/// restart; 0 takes it away.
	///
	/// ⚠️ A LEDGE A PLAYER CLIMBS — AND A ZOMBIE: a player's jump — `JumpPower` 200 in basalt's config, under the engine's
	/// gravity — reaches some 24u, and a zombie's step is 32 (the navmesh's). At 48 whoever dropped in stayed there, and with
	/// the lava at 10 every 0.2s that was a death three seconds long.
	///
	/// ⚠️ AND THE GAP IS LAVA, WHICH IS THE CONFIG'S, NOT THIS CODE'S: basalt's damage wall 2, drawn in lava, from -592 to
	/// -552 — 16u under this floor's top, by the user's *"a bit bellow that"*, and 4u over the raised floor now. It does not
	/// follow these numbers: change the heights here, and move that wall to match, in Creative — `nz_dmgwall_depth` keeps a
	/// wall's top, so it cannot.
	/// </summary>
	public static float ArenaStageHeight { get => Math.Clamp( _arenaStage ?? 20f, 0f, 400f ); set => _arenaStage = value; }

	/// <summary>The upper floor's material: the arena floor's own concrete.</summary>
	const string ArenaFloorMaterial = "materials/concrete/concretefloor023a.vmat";

	/// <summary>
	/// Where the upper floor's outline is laid from: a corner of the world's 128u grid in the arena, so the prism's texture —
	/// a repeat every 128u from its own origin (`DebrisMesh`) — lines up with the map's.
	/// </summary>
	static Vector2 ArenaStageOrigin => new( -3072f, -512f );

	/// <summary>The arena's middle, near where the user's ASP lay: on the upper floor.</summary>
	static Vector2 ArenaMiddle => new( -2994f, -668f );

	/// <summary>The upper floor's object, by name.</summary>
	const string ArenaStageName = "Basalt boss arena's upper floor";

	/// <summary>LOCAL — the upper floor: its prism and its collider.</summary>
	GameObject _arenaStageGo;

	/// <summary>How thick the arena's ceiling is, and how far under the lava bed's bottom its top lies, in units.</summary>
	const float ArenaCeilingThickness = 32f, ArenaCeilingGap = 1f;

	/// <summary>
	/// The ceiling, flat: the arena's slab pushed a few units into the walls east, north and south, as the arena's lava is,
	/// so none of its sides lies on a wall's face.
	/// </summary>
	static Vector2 ArenaCeilingMin => new( -4016f, -1370f );
	static Vector2 ArenaCeilingMax => new( -1970f, 394f );

	/// <summary>The lava bed's bottom — the first config wall drawn in lava (<see cref="LavaBed"/>): its middle less half its height; null without one.</summary>
	static float? LavaBedBottom => LavaBed is DamageWall w ? w.Position.z - w.Size.z * 0.5f : null;

	/// <summary>Where the ceiling's underside is — what the arena sees overhead — or null without a lava bed to hang it under.</summary>
	static float? ArenaCeilingUnder => LavaBedBottom is float bottom ? bottom - ArenaCeilingGap - ArenaCeilingThickness : null;

	/// <summary>The ceiling's object, by name.</summary>
	const string ArenaCeilingName = "Basalt boss arena's ceiling";

	/// <summary>LOCAL — the ceiling: its slab and its collider.</summary>
	GameObject _arenaCeilingGo;

	/// <summary>
	/// LOCAL — the split: each world mesh that held the floor, switched off, with its two copies — the floor, raised, and the
	/// rest. ⚠️ MADE ON FIRST USE, NOT BY AN INITIALISER, for the hotload.
	/// </summary>
	List<(GameObject Source, GameObject Floor, GameObject Others)> _arenaSplit;

	/// <summary>
	/// How the arena is laid: ⚠️ BUMP IT WHEN THAT CHANGES. 1: its floor split out and raised, 2026-09-27; 2: and the upper
	/// floor on it, 72u from every wall; 3: the upper floor 48u tall, not 16; 4: a ceiling under the lava bed; 5: the upper
	/// floor 20u tall, the floor under it raised 28u more. A manager that has not laid this layout lays it again — `OnUpdate`
	/// asks every frame.
	/// </summary>
	const int ArenaLayout = 5;

	/// <summary>LOCAL — the layout this manager last laid the arena in; 0 before it has.</summary>
	int _arenaLaid;

	/// <summary>LOCAL — the tries at finding the floor since the config was shown, and since when the last; and whether `nz_hex_arena lower` put it back.</summary>
	int _arenaTries;
	TimeSince _arenaTriedSince;
	bool _arenaLowered;

	/// <summary>Is the floor still to be found here — on basalt, not split, not put back by hand, and a try due?</summary>
	bool ArenaWantsTry => OnBasalt && (_arenaSplit?.Count ?? 0) == 0 && !_arenaLowered && _arenaTries < 5 && _arenaTriedSince > 3f;

	/// <summary>Does a point lie inside the slab's box, a unit's slack round it — is a face centred there one of the floor's?</summary>
	static bool InArenaSlab( Vector3 p )
	{
		Vector3 lo = ArenaSlabMin, hi = ArenaSlabMax;
		return p.x >= lo.x - 1f && p.x <= hi.x + 1f && p.y >= lo.y - 1f && p.y <= hi.y + 1f && p.z >= lo.z - 1f && p.z <= hi.z + 1f;
	}

	/// <summary>This world mesh's faces that are the floor's: their middles inside the slab's box.</summary>
	static HashSet<HalfEdgeMesh.FaceHandle> ArenaFacesIn( MeshComponent mc )
	{
		var faces = new HashSet<HalfEdgeMesh.FaceHandle>();
		var mesh = mc?.Mesh;
		if ( mesh is null ) return faces;

		foreach ( var f in mesh.FaceHandles )
			if ( InArenaSlab( mc.WorldTransform.PointToWorld( mesh.GetFaceCenter( f ) ) ) )
				faces.Add( f );

		return faces;
	}

	/// <summary>
	/// The arena as it should stand here: on basalt, its floor split out and raised — a split a manager before this one made
	/// taken up again, never made twice. LOCAL: `OnConfigShown`, and `OnUpdate` at a new layout or while the floor is still to
	/// be found.
	/// </summary>
	void BuildArena()
	{
		_arenaLaid = ArenaLayout;
		if ( !OnBasalt || !Scene.IsValid() ) return;

		_arenaSplit ??= new();
		_arenaSplit.RemoveAll( s => !s.Source.IsValid() || !s.Floor.IsValid() || !s.Others.IsValid() );
		if ( _arenaSplit.Count == 0 ) TakeUpArenaSplit();

		if ( _arenaSplit.Count == 0 && !SplitArena() )
		{
			_arenaTries++;
			_arenaTriedSince = 0;
			if ( _arenaTries == 5 ) Log.Warning( "[nz-hex] the boss arena's floor was not found in the map's meshes — it stays where the map has it" );
			return;
		}

		RaiseArena();
		BuildArenaStage();
		BuildArenaCeiling();
		ArenaNav();
	}

	/// <summary>Copies a manager before this one left, found by their tag and paired by their names. LOCAL.</summary>
	void TakeUpArenaSplit()
	{
		var copies = Scene.GetAllObjects( false ).Where( x => x.Tags.Has( ArenaTag ) ).ToList();
		foreach ( var floor in copies.Where( x => x.Name.EndsWith( ArenaFloorSuffix ) ) )
		{
			var name = floor.Name[..^ArenaFloorSuffix.Length];
			var rest = copies.FirstOrDefault( x => x.Parent == floor.Parent && x.Name == name + ArenaRestSuffix );
			var source = floor.Parent?.Children.FirstOrDefault( x => x.Name == name && !x.Tags.Has( ArenaTag ) );
			if ( rest.IsValid() && source.IsValid() ) _arenaSplit.Add( (source, floor, rest) );
		}
	}

	/// <summary>
	/// The floor split out of every world mesh holding a face of it: each mesh cloned twice — the rest, in its place, and the
	/// floor alone, raised — and switched off. Whether any was found. LOCAL.
	/// </summary>
	bool SplitArena()
	{
		var sources = Scene.GetAllComponents<MeshComponent>()
			.Where( mc => !mc.GameObject.Tags.Has( ArenaTag ) && !IsSplitCopy( mc.GameObject ) && ArenaFacesIn( mc ).Count > 0 )
			.Select( mc => mc.GameObject ).Distinct().ToList();
		if ( sources.Count == 0 ) return false;

		var faces = 0;
		foreach ( var source in sources )
		{
			var rest = ArenaCopy( source, keepFloor: false, source.Name + ArenaRestSuffix, out _ );
			var floor = ArenaCopy( source, keepFloor: true, source.Name + ArenaFloorSuffix, out var kept );
			if ( !rest.IsValid() || !floor.IsValid() )
			{
				if ( rest.IsValid() ) rest.Destroy();
				if ( floor.IsValid() ) floor.Destroy();
				Log.Warning( $"[nz-hex] {source.Name} would not split — its part of the boss arena's floor stays where the map has it" );
				continue;
			}

			// raised before it is switched on, so its collision is built where it stands
			floor.WorldPosition = source.WorldPosition + Vector3.Up * ArenaRaise;
			rest.Enabled = true;
			floor.Enabled = true;
			source.Enabled = false;
			_arenaSplit.Add( (source, floor, rest) );
			faces += kept;
		}

		Log.Info( $"[nz-hex] the boss arena's floor: {faces} faces split out of {_arenaSplit.Count} world meshes"
			+ $" ({string.Join( ", ", _arenaSplit.Select( s => s.Source.Name ) )})" );
		return _arenaSplit.Count > 0;
	}

	/// <summary>A copy of this world mesh, still off, keeping only the floor's faces — or only the others.</summary>
	static GameObject ArenaCopy( GameObject source, bool keepFloor, string name, out int kept )
	{
		kept = 0;
		GameObject go;
		try { go = source.Clone( new CloneConfig { Transform = source.WorldTransform, StartEnabled = false, Name = name } ); }
		catch ( Exception e ) { Log.Warning( $"[nz-hex] {source.Name} would not clone ({e.Message})" ); return null; }
		if ( !go.IsValid() ) return null;

		// ⛔ SetParent KEEPS THE WORLD TRANSFORM — and the place is set again after it all the same
		go.SetParent( source.Parent );
		go.WorldTransform = source.WorldTransform;
		go.Flags |= GameObjectFlags.NotSaved;

		// ⛔ NOT `NetworkMode.Never`, UNLIKE WHAT THIS MANAGER BUILDS (2026-09-29): a COPY OF A MAP MESH travels in a joiner's
		// snapshot, and the joiner takes it up by its tag and name (`TakeUpArenaSplit`), as a new manager after a hotload does.
		// The map's own mesh goes there too, switched off — a map object always travels — and `Scene.GetAllComponents` finds
		// only what is on, so with the copies kept home a joiner had nothing to split: eight world meshes gone from under the
		// arena. A copy is the map's own geometry and materials, which every machine draws (the strips' copies on its faces are
		// made again there, `StripLights`). See NZNetListener.
		go.Tags.Add( ArenaTag );

		var mc = go.Components.Get<MeshComponent>( FindMode.EverythingInSelf );
		if ( mc?.Mesh is null ) { go.Destroy(); return null; }

		// the faces found while the copy still stands where the original does
		var floor = ArenaFacesIn( mc );
		kept = keepFloor ? floor.Count : mc.Mesh.FaceHandles.Count() - floor.Count;
		mc.Mesh.RemoveFaces( mc.Mesh.FaceHandles.Where( f => floor.Contains( f ) != keepFloor ).ToList() );
		return go;
	}

	/// <summary>The floor's copies where the raise puts them, over their originals. LOCAL.</summary>
	void RaiseArena()
	{
		foreach ( var (source, floor, _) in _arenaSplit )
			if ( source.IsValid() && floor.IsValid() ) floor.WorldPosition = source.WorldPosition + Vector3.Up * ArenaRaise;
	}

	/// <summary>The raised floor's top, where the upper floor stands.</summary>
	static float ArenaFloorTop => ArenaSlabMax.z + ArenaRaise;

	/// <summary>
	/// The upper floor, on the raised floor: the outline extruded <see cref="ArenaStageHeight"/> up, in the floor's concrete,
	/// solid — a hull per triangle, as a drawn barrier is (`DebrisMesh`, `DebrisManager`). Built again at every call. LOCAL.
	/// </summary>
	void BuildArenaStage()
	{
		ClearArenaStage();
		if ( !OnBasalt || !Scene.IsValid() || ArenaStageHeight < 0.5f ) return;

		var material = Material.Load( ArenaFloorMaterial );
		if ( material is null ) { Log.Warning( $"[nz-hex] {ArenaFloorMaterial} did not load — the arena's upper floor is not built" ); return; }

		var origin = ArenaStageOrigin;
		var model = DebrisMesh.Build( ArenaStageOutline.Select( p => p - origin ).ToList(), ArenaStageHeight, material );
		if ( model is null ) { Log.Warning( "[nz-hex] the arena's upper floor would not extrude — its outline crosses itself?" ); return; }

		var go = Scene.CreateObject();
		go.Name = ArenaStageName;
		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.Tags.Add( PanelTag );
		go.WorldPosition = new Vector3( origin.x, origin.y, ArenaFloorTop );
		go.WorldRotation = Rotation.Identity;
		go.Components.Create<ModelRenderer>().Model = model;

		// ⚠️ ModelCollider, as a drawn barrier has: the model's hulls, one per triangle, follow the outline's every notch
		go.Components.Create<ModelCollider>().Model = model;
		_arenaStageGo = go;
	}

	/// <summary>
	/// The ceiling, right under the lava bed: a slab of the stones' concrete over the whole arena, solid — built as the upper
	/// floor is, on the world's 128u grid. None without a lava bed in the config. Built again at every call. LOCAL.
	/// </summary>
	void BuildArenaCeiling()
	{
		ClearArenaCeiling();
		if ( !OnBasalt || !Scene.IsValid() ) return;
		// ⚠️ ONLY A LOADED CONFIG CAN LACK ONE (2026-10-05). Basalt shown before its config (the lobby, "active config: NONE")
		// has no damage walls at all, and this warned then too — which, in a published game's log, read as the lava bed missing
		// from a config that has it. The arena is built again once the config is shown (`OnConfigShown`).
		if ( ArenaCeilingUnder is not float under )
		{
			if ( ActiveConfig.IsPlayable ) Log.Warning( "[nz-hex] no lava bed in the config — the arena gets no ceiling" );
			return;
		}

		var material = Material.Load( StoneMaterial );
		if ( material is null ) { Log.Warning( $"[nz-hex] {StoneMaterial} did not load — the arena's ceiling is not built" ); return; }

		Vector2 lo = ArenaCeilingMin, hi = ArenaCeilingMax, origin = ArenaStageOrigin;
		var model = DebrisMesh.Build( new List<Vector2>
		{
			new Vector2( lo.x, lo.y ) - origin, new Vector2( hi.x, lo.y ) - origin,
			new Vector2( hi.x, hi.y ) - origin, new Vector2( lo.x, hi.y ) - origin,
		}, ArenaCeilingThickness, material );
		if ( model is null ) return;

		var go = Scene.CreateObject();
		go.Name = ArenaCeilingName;
		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.Tags.Add( PanelTag );
		go.WorldPosition = new Vector3( origin.x, origin.y, under );
		go.WorldRotation = Rotation.Identity;
		go.Components.Create<ModelRenderer>().Model = model;
		go.Components.Create<ModelCollider>().Model = model;
		_arenaCeilingGo = go;
	}

	/// <summary>The ceiling gone — and any a manager before this one left, found by name. LOCAL.</summary>
	void ClearArenaCeiling()
	{
		if ( _arenaCeilingGo.IsValid() ) _arenaCeilingGo.Destroy();
		_arenaCeilingGo = null;

		if ( !Scene.IsValid() ) return;
		foreach ( var old in Scene.GetAllObjects( false ).Where( x => x.Tags.Has( PanelTag ) && x.Name == ArenaCeilingName ).ToList() )
			old.Destroy();
	}

	/// <summary>Where a look straight up from here first meets the world, or null: the selftest's check that the ceiling hangs there. LOCAL.</summary>
	float? CeilingOver( Vector3 from, float height = 2000f )
	{
		if ( !Scene.IsValid() ) return null;

		// ⚠️ THE WORLD ONLY, as `FloorUnder` looks
		var tr = Scene.Trace.Ray( from, from + Vector3.Up * height ).WithoutTags( "player", "zombie", "ragdoll", "trigger" ).Run();
		return tr.Hit ? tr.EndPosition.z : null;
	}

	/// <summary>The upper floor gone — and any a manager before this one left, found by name. LOCAL.</summary>
	void ClearArenaStage()
	{
		if ( _arenaStageGo.IsValid() ) _arenaStageGo.Destroy();
		_arenaStageGo = null;

		if ( !Scene.IsValid() ) return;
		foreach ( var old in Scene.GetAllObjects( false ).Where( x => x.Tags.Has( PanelTag ) && x.Name == ArenaStageName ).ToList() )
			old.Destroy();
	}

	/// <summary>Is a point inside a polygon — the even-odd rule, a ray to +x?</summary>
	static bool InOutline( IReadOnlyList<Vector2> poly, Vector2 p )
	{
		var inside = false;
		for ( int i = 0, j = poly.Count - 1; i < poly.Count; j = i++ )
		{
			Vector2 a = poly[i], b = poly[j];
			if ( (a.y > p.y) != (b.y > p.y) && p.x < a.x + (p.y - a.y) * (b.x - a.x) / (b.y - a.y) ) inside = !inside;
		}
		return inside;
	}

	/// <summary>The outline's area, positive for counter-clockwise.</summary>
	static float ArenaOutlineArea()
	{
		var sum = 0f;
		var p = ArenaStageOutline;
		for ( var i = 0; i < p.Length; i++ ) sum += p[i].x * p[(i + 1) % p.Length].y - p[(i + 1) % p.Length].x * p[i].y;
		return sum * 0.5f;
	}

	/// <summary>A point in the gap between the upper floor and the walls: half the gap out from the middle of the outline's longest edge.</summary>
	static Vector2 ArenaGapPoint()
	{
		var p = ArenaStageOutline;
		var best = 0;
		for ( var i = 1; i < p.Length; i++ )
			if ( (p[(i + 1) % p.Length] - p[i]).Length > (p[(best + 1) % p.Length] - p[best]).Length ) best = i;

		var a = p[best];
		var b = p[(best + 1) % p.Length];
		var along = (b - a) * (1f / (b - a).Length);
		return (a + b) * 0.5f + new Vector2( along.y, -along.x ) * (ZombieHeight * 0.5f);       // the edge turned right: out
	}

	/// <summary>The map's own floor back: its meshes switched on again, the copies gone; then the navmesh built again. LOCAL.</summary>
	void ClearArena()
	{
		ClearArenaStage();
		ClearArenaCeiling();
		if ( _arenaSplit is null || _arenaSplit.Count == 0 ) return;

		foreach ( var (source, floor, rest) in _arenaSplit )
		{
			if ( source.IsValid() ) source.Enabled = true;
			if ( floor.IsValid() ) floor.Destroy();
			if ( rest.IsValid() ) rest.Destroy();
		}
		_arenaSplit.Clear();

		if ( OnBasalt ) ArenaNav();
	}

	/// <summary>
	/// The navmesh over the arena built again from its floor as it stands. HOST: the zombies walk the host's mesh.
	/// `RequestTilesGeneration`, as for a shield's column (`RebuildColumnNav`), from under the old floor to over the new.
	/// </summary>
	void ArenaNav()
	{
		if ( NZGame.IsClient || !Scene.IsValid() ) return;

		var nav = Scene.NavMesh;
		if ( nav is null ) return;

		var pad = new Vector3( 64f, 64f, 0f );
		nav.RequestTilesGeneration( new BBox( ArenaSlabMin - pad - Vector3.Up * 64f,
			ArenaSlabMax + pad + Vector3.Up * (ArenaRaise + 256f) ) );
	}

	/// <summary>Where a look straight down from here first meets the world, or null: the selftest's check that the floor rose. LOCAL.</summary>
	float? FloorUnder( Vector3 from, float depth = 1000f )
	{
		if ( !Scene.IsValid() ) return null;

		// ⚠️ THE WORLD ONLY: a player standing under the look is not the floor — and the blue altar's send lands them in the
		// middle, where the selftest looks. Seen 2026-09-27: it met a tester's head, 72u over the upper floor
		var tr = Scene.Trace.Ray( from, from + Vector3.Down * depth ).WithoutTags( "player", "zombie", "ragdoll", "trigger" ).Run();
		return tr.Hit ? tr.EndPosition.z : null;
	}

	/// <summary>Where the arena stands, in words. LOCAL.</summary>
	string ArenaStateText()
		=> !OnBasalt ? "the boss arena is basalt's"
			: (_arenaSplit?.Count ?? 0) == 0
				? $"the boss arena's floor is the map's own, its top at {ArenaSlabMax.z:0}{( _arenaLowered ? " — put back by hand" : "" )}"
			: $"the boss arena's floor stands raised {ArenaRaise:0.#}u, its top at {ArenaFloorTop:0.#}, split out of"
				+ $" {_arenaSplit.Count} world meshes"
				+ ( _arenaStageGo.IsValid()
					? $" · the upper floor {ArenaStageHeight:0.#}u over it, its top at {ArenaFloorTop + ArenaStageHeight:0.#}, {ZombieHeight:0}u from"
						+ $" every wall ({ArenaStageOutline.Length} points)"
					: " · no upper floor" )
				+ ( _arenaCeilingGo.IsValid() ? $" · the ceiling's underside at {ArenaCeilingUnder:0.#}, under the lava bed" : " · no ceiling" );

	/// <summary>
	/// `nz_hex_arena [raise|lower|stage] [units]` — the boss arena's floors, on this machine: where they stand; `raise` raises
	/// the floor — by `units`, on this machine and until a restart, if given; `lower` puts the map's own floor back, and takes
	/// the upper floor away, until the next config or `raise`; `stage` sets how far the upper floor stands over the raised
	/// one — `stage 0` takes it away. The host's also builds the navmesh again.
	/// </summary>
	[ConCmd( "nz_hex_arena" )]
	public static void ArenaCmd( string what = "", float units = -1f )
	{
		var m = Instance;
		if ( !m.IsValid() || !OnBasalt ) { Log.Info( "[nz-hex] the boss arena is basalt's" ); return; }

		switch ( what.Trim().ToLowerInvariant() )
		{
			case "":
				break;

			case "raise":
				if ( units >= 0f ) ArenaRaise = units;
				m._arenaLowered = false;
				m._arenaTries = 0;
				m.BuildArena();
				break;

			case "lower":
				m._arenaLowered = true;
				m.ClearArena();
				break;

			case "stage":
				if ( units >= 0f ) ArenaStageHeight = units;
				if ( (m._arenaSplit?.Count ?? 0) == 0 ) { Log.Warning( "[nz-hex] the floor is not raised — nz_hex_arena raise first" ); break; }
				m.BuildArenaStage();
				m.ArenaNav();
				break;

			default:
				Log.Warning( "[nz-hex] nz_hex_arena raise [units], lower or stage [height] — or nothing, to see where it stands" );
				return;
		}

		Log.Info( $"[nz-hex] {m.ArenaStateText()}" );
	}
}