Part of HexPlatforms manager for the Basalt map, implementing the blue altar Easter-egg teleporter. It builds and animates a blue-engraved altar object, controls its collider and navmesh updates, handles player placement when the pillar rises, and runs a host-authoritative send that teleports all eligible players to the boss arena with warmup, transit visuals and audio.
using System;
using System.Linq;
using Sandbox;
namespace NZombies;
/// <summary>
/// BASALT — THE BLUE ALTAR (2026-09-27). The teleporter's destination set, a second altar rises out of the floor in the
/// teleporter's middle, where the twin shield's column stood. The user marked the spot with an ASP wall buy, surveyed 2.1u
/// off that middle, on its floor at 1710. It is the Altar's own pillar (`HexPlatforms.Altar.cs`) with its engravings in
/// blue — Color Smash's. Asked for as *"when this last step is completed i want another altar to raise from the floor but
/// with blue engravings"*.
///
/// ⛔ AND IT IS THE TELEPORTER'S LAST SWITCH: E on it sends every player to the boss arena's upper floor
/// (`HexPlatforms.Arena.cs`) — *"the altar we made on the last step, interacting with it should teleport all players to
/// the platform bellow the map we just made"* (2026-09-27). As a teleporter pad goes (`Teleporter`): it charges for
/// <see cref="ArenaWarmupSeconds"/>, the warm-up heard and a portal at its foot; the passage, <see cref="ArenaTransitSeconds"/>
/// of arcs on every rider's screen; and the riders put down in a ring round the upper floor's middle, a portal where they
/// land. A rider is anyone in the round not in the arena already — the downed too, who would bleed out alone up here — so
/// a press later, by someone back in the map, takes only those still up here, never the fight's players back to the ring.
///
/// ⛔ IT RISES ONCE, AS THE LAST BUTTON TURNS LIGHT BLUE, over <see cref="BlueAltarRiseSeconds"/>, easing out of the floor,
/// the stone grinding and a thud as it settles (`nz.hex.altar.rise`, heard at it). Only then is it solid, so it never
/// shoves anyone; a body standing where it comes up is set beside it by its own machine. Seen later — joining, a hotload,
/// `nz_hex_buttons done` — it simply stands.
///
/// ⛔ WHO OWNS WHAT (INSTRUCTIONS.md, "BUILD FOR MULTIPLAYER"):
/// - whether it stands follows the teleporter's buttons, HOST state MIRRORED (`NZNet.HexButtonState`); it rises when the
/// press that set the destination arrives, and a joiner's simply stands;
/// - the PILLAR and its rise are LOCAL; so is setting this machine's own player aside, the owner moving its body;
/// - the NAVMESH round it is the host's, built again once it stands and once it goes;
/// - the SEND is the host's: it takes the press, runs the phases and moves the bodies, each by its owner
/// (`PlayerSpawner.MoveTo`); every machine is told each phase (`NZNet.HexArenaSend`) and throws its own portals, and
/// shows its own player the passage if they ride.
/// </summary>
public sealed partial class HexPlatforms
{
/// <summary>Its foot: the teleporter's middle — the twin shield's column's (<see cref="TwinCentre"/>) — on that floor, at 1710.</summary>
static Vector3 BlueAltarFoot => new( TwinCentre.x, TwinCentre.y, 1710f );
/// <summary>Where it waits for its rise: all of it under the floor, its top a little below it.</summary>
static Vector3 BlueAltarSunk => BlueAltarFoot + Vector3.Down * (AltarHeight + 2f);
static float? _blueAltarRise;
/// <summary>
/// How long it takes to rise, in seconds: 5, the grinding's length (`Tools/basalt_button_sounds.py`, `RISE_SECONDS`).
/// `nz_hex_blue_altar rise [seconds]` sets it on this machine and until a restart.
/// </summary>
public static float BlueAltarRiseSeconds { get => Math.Clamp( _blueAltarRise ?? 5f, 0.5f, 30f ); set => _blueAltarRise = value; }
/// <summary>The engravings' colour: Color Smash's blue (<see cref="MaterialFor"/> 0).</summary>
const int BlueAltarColour = 0;
/// <summary>Its rise: the stone grinding out of the floor and the thud as it settles, 3D, at it.</summary>
const string BlueAltarCue = "nz.hex.altar.rise";
/// <summary>
/// How near its axis a body's feet may be when it stands, before its machine sets it aside: the pillar's corner, 9.2u, a
/// body's half width, 16u, and a little over.
/// </summary>
const float BlueAltarClear = 28f;
/// <summary>
/// LOCAL — the pillar, its engravings and its collider, the collider kept from when it was made: switched off through the
/// rise, and a lookup does not see a switched-off component (INSTRUCTIONS.md, `GetAllComponents`).
/// </summary>
GameObject _blueAltarGo;
BoxCollider _blueAltarBox;
/// <summary>LOCAL — whether it is rising here, and since when.</summary>
bool _blueAltarRising;
TimeSince _blueAltarSince;
/// <summary>
/// How the blue altar is drawn: ⚠️ BUMP IT WHEN THAT CHANGES. 1: the Altar's pillar, blue-engraved, rising at the
/// teleporter's middle, 2026-09-27. A manager that has not built this layout builds it again — `OnUpdate` asks every frame.
/// </summary>
const int BlueAltarLayout = 1;
/// <summary>LOCAL — the layout this manager last built it in; 0 before it has.</summary>
int _blueAltarLaid;
/// <summary>Its object, by name: how a sweep knows one a manager before this one left.</summary>
const string BlueAltarName = "Basalt blue altar";
/// <summary>
/// The blue altar as this machine should have it: standing once the teleporter's destination is set, as this machine was
/// told — rising, if `rise` — and gone before. LOCAL: `ApplyButtons` as the destination is set or unset, and `OnUpdate`
/// at a new layout.
/// </summary>
void BuildBlueAltar( bool rise = false )
{
_blueAltarLaid = BlueAltarLayout;
if ( !OnBasalt || !Scene.IsValid() || !ButtonsDoneShown ) { ClearBlueAltar(); return; }
if ( _blueAltarGo.IsValid() && !rise ) return;
ClearBlueAltar();
var body = AltarModel( Material.Load( StoneMaterial ), BlueAltarFoot );
if ( body is null )
{
if ( !_altarMissing ) Log.Warning( $"[nz-hex] {StoneMaterial} did not load — the blue altar is not built" );
_altarMissing = true;
return;
}
var root = Scene.CreateObject();
root.Name = BlueAltarName;
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 = rise ? BlueAltarSunk : BlueAltarFoot;
root.WorldRotation = Rotation.Identity;
root.Components.Create<ModelRenderer>().Model = body;
// ⚠️ SOLID ONCE IT STANDS, as the Altar is — a box round the hexagon, off through the rise
var box = root.Components.Create<BoxCollider>();
box.Scale = new Vector3( AltarApothem * 2f, AltarCorner * 2f, AltarHeight );
box.Center = new Vector3( 0f, 0f, AltarHeight * 0.5f );
box.Enabled = !rise;
// the Altar's engravings, in blue: 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, BlueAltarColour, 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, BlueAltarColour, 1f ),
new Vector3( 0f, 0f, AltarHeight + EngraveProud ), Rotation.LookAt( Vector3.Up, Vector3.Forward ) );
_blueAltarGo = root;
_blueAltarBox = box;
_blueAltarRising = rise;
_blueAltarSince = 0;
if ( !rise ) BlueAltarStands();
}
/// <summary>The rise run — easing out of the floor, slow at both ends — and its end. LOCAL — `OnUpdate`.</summary>
void TickBlueAltar()
{
if ( !_blueAltarRising ) return;
if ( !_blueAltarGo.IsValid() ) { _blueAltarRising = false; return; }
var t = Math.Clamp( _blueAltarSince / BlueAltarRiseSeconds, 0f, 1f );
if ( t >= 1f )
{
_blueAltarRising = false;
_blueAltarGo.WorldPosition = BlueAltarFoot;
BlueAltarStands();
return;
}
_blueAltarGo.WorldPosition = BlueAltarSunk + Vector3.Up * ((AltarHeight + 2f) * t * t * (3f - 2f * t));
}
/// <summary>
/// It stands: solid now; this machine's own player, if it came up where they stand, set beside it, out from its middle
/// the way they were — the owner moving its body (`PlayerSpawner.PlaceLocal`); and, on the host, the navmesh round it
/// built again, so the zombies walk round it. LOCAL.
/// </summary>
void BlueAltarStands()
{
if ( _blueAltarBox.IsValid() ) _blueAltarBox.Enabled = true;
var body = PlayerPresence.Find();
if ( body.IsValid() )
{
var foot = BlueAltarFoot;
var at = body.WorldPosition;
var flat = new Vector3( at.x - foot.x, at.y - foot.y, 0f );
if ( flat.Length < BlueAltarClear && at.z > foot.z - 12f && at.z < foot.z + AltarHeight )
{
var away = flat.Length > 0.5f ? flat.Normal : Vector3.Forward;
PlayerSpawner.PlaceLocal( body, new Vector3( foot.x, foot.y, at.z ) + away * BlueAltarClear, body.WorldRotation );
Log.Info( "[nz-hex] the blue altar came up where you stood — set beside it" );
}
}
BlueAltarNav();
}
/// <summary>The blue altar gone — and any a manager before this one left, found by name; the navmesh then built again. LOCAL.</summary>
void ClearBlueAltar()
{
var had = _blueAltarGo.IsValid();
if ( had ) _blueAltarGo.Destroy();
_blueAltarGo = null;
_blueAltarBox = null;
_blueAltarRising = false;
if ( !Scene.IsValid() ) return;
foreach ( var old in Scene.GetAllObjects( false ).Where( x => x.Tags.Has( PanelTag ) && x.Name == BlueAltarName ).ToList() )
{
old.Destroy();
had = true;
}
if ( had ) BlueAltarNav();
}
/// <summary>
/// The navmesh round the blue altar built again from what stands there now. HOST: the zombies walk the host's mesh.
/// `RequestTilesGeneration`, as for a shield's column (`RebuildColumnNav`); tiles regenerate whole, so the box is padded.
/// </summary>
void BlueAltarNav()
{
if ( NZGame.IsClient || !Scene.IsValid() ) return;
var nav = Scene.NavMesh;
if ( nav is null ) return;
var extent = new Vector3( 160f, 160f, 128f );
nav.RequestTilesGeneration( new BBox( BlueAltarFoot - extent, BlueAltarFoot + extent ) );
}
// ══ the send: everyone to the boss arena ══════════════════════════════════════════════════
/// <summary>How long the altar charges before the passage, and how long the passage lasts, in seconds.</summary>
const float ArenaWarmupSeconds = 2f, ArenaTransitSeconds = 2f;
/// <summary>
/// How long a rider's arcs stay past the passage, in seconds: so the move happens under them — a client's body moves a
/// moment after the host's word — and the arena shows as they clear.
/// </summary>
const float ArenaOverlayLinger = 0.6f;
/// <summary>The send's phases, as every machine is told them: none; charging; the passage; arrived.</summary>
const int ArenaIdle = 0, ArenaCharging = 1, ArenaPassage = 2, ArenaArrived = 3;
/// <summary>HOST — the send's phase, and since when; and whether anyone has been sent this game, for the boss fight to read.</summary>
int _arenaSend;
TimeSince _arenaSendSince;
bool _arenaSent;
/// <summary>MIRROR — the send's phase as this machine was told (`NZNet.HexArenaSend`): the prompt's, and E's.</summary>
int _arenaSendShown;
/// <summary>Is the teleporter charging or in its passage, as this machine was told? The prompt says so, and E waits.</summary>
public static bool ArenaSendingShown => Instance.IsValid() && Instance._arenaSendShown is ArenaCharging or ArenaPassage;
/// <summary>
/// WHERE A PLAYER JOINING A GAME IN PROGRESS STANDS, on basalt. HOST — `NZPlayers.SpawnFor`. In the boss arena while the
/// fight is on, to its orb (*"make the new player spawn in the boss arena"*, 2026-09-27); beside a standing teammate while
/// the lava is out of its bed, since every player spawn lies under it. False otherwise — and the joiner is dealt a spawn,
/// as ever.
/// </summary>
public static bool PlaceJoiner( GameObject body )
{
var m = Instance;
if ( NZGame.IsClient || !m.IsValid() || !OnBasalt || !body.IsValid() ) return false;
if ( m._fight is FightIntro or FightPhase or FightChange or FightWon )
{
PlayerSpawner.MoveTo( body, ArenaLanding( 0, 1 ), Rotation.FromYaw( 90f ) );
Log.Info( $"[nz-hex] ⚔ {body.Name} joined in the middle of the boss fight — into the arena" );
return true;
}
if ( m._lava is LavaRising or LavaUp or LavaSinking )
{
var mate = PlayerSpawner.AllBodies( m.Scene )
.Where( b => b != body )
.Select( b => b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) )
.FirstOrDefault( p => p.IsValid() && !CannotCarry( p ) );
if ( mate.IsValid() )
{
PlayerSpawner.MoveTo( body, mate.WorldPosition + mate.WorldRotation.Backward * 40f, mate.WorldRotation );
Log.Info( $"[nz-hex] 🌋 {body.Name} joined with the lava up — beside {NameFor( mate )}" );
return true;
}
}
return false;
}
/// <summary>The host's send, for a joiner's catch-up (`NZNet.PushState`): 0 none, 1 charging, 2 the passage.</summary>
public static int ArenaSendNow => Instance.IsValid() && Instance._arenaSend is ArenaCharging or ArenaPassage ? Instance._arenaSend : 0;
/// <summary>The middle of the blue altar's top: where the send is heard.</summary>
static Vector3 BlueAltarTop => BlueAltarFoot + Vector3.Up * AltarHeight;
/// <summary>How far up its axis a look counts as at the blue altar, in units: its top, and a hand over it for a look down at it.</summary>
const float BlueAltarLookHeight = AltarHeight + 12f;
/// <summary>Where the riders land: the arena's upper floor, at its middle.</summary>
static Vector3 ArenaLandingMiddle => new( ArenaMiddle.x, ArenaMiddle.y, ArenaFloorTop + ArenaStageHeight );
/// <summary>
/// The i-th of n riders' landing, 2u over the upper floor: a ring of 96u round its middle — the middle itself for one.
/// Far inside the floor's every edge.
/// </summary>
static Vector3 ArenaLanding( int i, int n )
{
if ( n <= 1 ) return ArenaLandingMiddle + Vector3.Up * 2f;
var a = MathF.PI * 2f * i / n;
return ArenaLandingMiddle + new Vector3( MathF.Cos( a ) * 96f, MathF.Sin( a ) * 96f, 2f );
}
/// <summary>
/// Is a body down in the boss arena: over its slab, under its ceiling? Nothing else in the map lies there — the arena is
/// under the lava bed, and every floor the players walk is over it.
/// </summary>
static bool InBossArena( Vector3 feet )
{
Vector3 lo = ArenaSlabMin, hi = ArenaSlabMax;
return feet.x >= lo.x - 8f && feet.x <= hi.x + 8f && feet.y >= lo.y - 8f && feet.y <= hi.y + 8f
&& feet.z >= lo.z - 256f && feet.z <= (ArenaCeilingUnder ?? ArenaFloorTop + 640f);
}
/// <summary>Does the send take this player: in the round, the downed too, and not in the arena already?</summary>
static bool ArenaRider( NZPlayer p ) => p.IsValid() && !p.IsOutOfRound && !p.OutOfRoundNet && !InBossArena( p.WorldPosition );
/// <summary>
/// Is this player looking at the blue altar, standing, within reach? LOCAL — the prompt's test and the use key's, one
/// question for both (`UsePrompt.ForBlueAltar`, `NZPlayer.TickUse`), from this machine's camera.
/// </summary>
public static bool BlueAltarAimed( NZPlayer player )
{
var m = Instance;
if ( !m.IsValid() || !OnBasalt || !m.ButtonsDoneShown || m._blueAltarRising || !m._blueAltarGo.IsValid() || CannotCarry( player ) )
return false;
// ⚠️ AND NOTHING ONCE THE EASTER EGG IS DONE: the beast is dead, and the altar sends no one back to him
if ( m._fightShown == FightDone ) return false;
var cam = m.Scene.Camera;
return cam.IsValid() && BlueAltarAimedFrom( cam.WorldPosition, cam.WorldRotation.Forward, player.WorldPosition );
}
/// <summary>
/// Is a look from this eye, this way (a unit direction), at the blue altar, the looker's feet here within the Altar's
/// reach of its foot? At it is through a column round its axis, the Altar's look radius round, up to a hand over its top.
/// </summary>
static bool BlueAltarAimedFrom( Vector3 eye, Vector3 dir, Vector3 feet )
=> BlueAltarFoot.Distance( feet ) <= AltarReach
&& RayMeetsColumn( eye, dir, BlueAltarFoot + Vector3.Up * (BlueAltarLookHeight * 0.5f), AltarAimRadius, BlueAltarLookHeight * 0.5f );
/// <summary>E on the blue altar. LOCAL — the presser's machine asks the host, which decides (<see cref="HostUseBlueAltar"/>).</summary>
public static void UseBlueAltar( NZPlayer player )
{
if ( player.IsValid() ) NZNet.BlueAltarUse();
}
/// <summary>
/// Someone pressed E on the blue altar. HOST — `NZNet.BlueAltarUse`, with `who` the caller's connection id as the call
/// itself carries it, or "" for the host's own press.
/// </summary>
public static void HostUseBlueAltar( 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.StartArenaSend( body );
if ( refused != "" ) Log.Info( $"[nz-hex] the blue altar sends no one: {refused}" );
}
/// <summary>
/// The rules for the send. HOST — apart from the RPC, so the selftest can walk it. The teleporter's destination must be
/// set, so the altar stands; no send under way; the lava in its bed, since the arena lies under its flood; the arena's
/// upper floor built, to land on; and the presser up and within reach (`anywhere` skips that, for the test and the
/// command). It charges, and <see cref="WatchArenaSend"/> runs the rest. Returns why not, or "".
/// </summary>
string StartArenaSend( NZPlayer by, bool anywhere = false )
{
if ( (_btnFlags & BtnDone) == 0 ) return "the teleporter's destination is not set — the altar is under the floor";
if ( _fight == FightDone ) return "the Easter egg is complete — the beast is dead";
if ( _arenaSend != ArenaIdle ) return "it is sending already";
if ( _lava is LavaWarning or LavaRising or LavaUp or LavaSinking ) return "the lava is out of its bed, and the arena lies under it";
if ( !_arenaStageGo.IsValid() ) return "the arena's upper floor is not built — nz_hex_arena";
if ( !anywhere )
{
if ( CannotCarry( by ) ) return "the presser is down or gone";
if ( BlueAltarFoot.Distance( by.WorldPosition ) > AltarReach + ReachSlack ) return "they are too far from the altar";
}
_arenaSend = ArenaCharging;
_arenaSendSince = 0;
SendArenaPhase( ArenaCharging );
Cue( NZSound.TeleporterWarmup, BlueAltarTop );
Log.Info( $"[nz-hex] ⚔ THE BLUE ALTAR WAKES THE TELEPORTER — {( by.IsValid() ? NameFor( by ) : "someone" )} touched it:"
+ $" every player not in the boss arena goes there in {ArenaWarmupSeconds + ArenaTransitSeconds:0.#}s" );
return "";
}
/// <summary>
/// The send run: the charge over, the passage; the passage over, every rider put down on the arena's upper floor, each
/// body by its owner (`PlayerSpawner.MoveTo`), and heard leaving and landing. HOST — `OnUpdate`. ⚠️ While testing no body
/// moves: the selftest must not carry its tester off.
/// </summary>
void WatchArenaSend()
{
if ( _arenaSend == ArenaCharging && _arenaSendSince >= ArenaWarmupSeconds )
{
_arenaSend = ArenaPassage;
_arenaSendSince = 0;
SendArenaPhase( ArenaPassage );
// ⛔ AND NOBODY IS HIT ON THE WAY — the passage covers every rider's screen, as a pad's does (the co-op audit,
// 2026-09-27). Every rider-to-be, whoever's machine drives them (`Teleporter.InTransit`).
foreach ( var b in PlayerSpawner.AllBodies( Scene ) )
if ( ArenaRider( b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) ) )
Teleporter.GuardTransit( b, ArenaTransitSeconds + Teleporter.TransitGrace );
return;
}
if ( _arenaSend != ArenaPassage || _arenaSendSince < ArenaTransitSeconds ) return;
var riders = PlayerSpawner.AllBodies( Scene )
.Select( b => b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) )
.Where( ArenaRider )
.ToList();
if ( !Testing )
for ( var i = 0; i < riders.Count; i++ )
PlayerSpawner.MoveTo( riders[i].GameObject, ArenaLanding( i, riders.Count ), Rotation.FromYaw( 90f ) );
_arenaSend = ArenaIdle;
_arenaSent = true;
SendArenaPhase( ArenaArrived );
Cue( NZSound.TeleporterOut, BlueAltarTop );
Cue( NZSound.TeleporterIn, ArenaLandingMiddle );
Log.Info( $"[nz-hex] ⚔ {riders.Count} player(s) sent to the boss arena's upper floor"
+ $"{( riders.Count > 0 ? $" — {string.Join( ", ", riders.Select( NameFor ) )}" : "" )}" );
// …and the first send of a game begins the boss fight: every zombie gone, the round frozen (`HexPlatforms.BossFight.cs`)
if ( !Testing && _fight == FightNone )
{
// ⚠️ STEP 14 DONE: the word goes out for its banner (`EggProgress`); its fanfare plays nothing, the fight's quiet is the
// fight's.
Fanfare( 14 );
StartBossFight();
}
}
/// <summary>
/// The send's phase told to every machine — or, while the selftest walks it, to this one alone: it throws no portal and
/// no arcs anywhere, and puts the host's own back when it is done. ⚠️ THE SELFTEST ONLY, NOT ALL OF `Testing`: a skip
/// (`nz_hex_skipto`) during a send resets it for real, and every machine must hear that, or its prompt stays "charging".
/// </summary>
void SendArenaPhase( int phase )
{
if ( _armedForTest.HasValue ) ApplyArenaSend( phase );
else NZNet.HexArenaSend( phase );
}
/// <summary>
/// The send's phase as the host told it. EVERY machine — `NZNet.HexArenaSend`: a portal at the altar's foot as it
/// charges; the passage's arcs on this machine's screen, and its charge heard, if this machine's player rides; a portal
/// where the riders land.
/// </summary>
public void ApplyArenaSend( int phase )
{
_arenaSendShown = phase == ArenaArrived ? ArenaIdle : phase;
if ( Testing || !OnBasalt ) return;
switch ( phase )
{
case ArenaCharging:
TeleportPortal.Departure( BlueAltarFoot );
break;
case ArenaPassage when ArenaRider( NZPlayer.Local ):
TeleportOverlayState.Begin( ArenaTransitSeconds + ArenaOverlayLinger );
NZSound.Play( NZSound.TeleporterCharge );
break;
case ArenaArrived:
TeleportPortal.Arrival( ArenaLandingMiddle );
break;
}
}
/// <summary>No send under way, and nobody sent — and no boss fight. HOST — with the teleporter's buttons, which it follows.</summary>
void ResetArenaSend()
{
ResetBossFight();
var was = _arenaSend != ArenaIdle;
_arenaSend = ArenaIdle;
_arenaSent = false;
if ( was ) SendArenaPhase( ArenaIdle );
}
/// <summary>
/// `nz_hex_blue_altar [rise|use] [seconds]` — the blue altar: where it stands; `rise` sends it back under the floor and
/// up again on this machine alone, its sound with it, to be watched — at `seconds` a rise, on this machine and until a
/// restart, if given; `use` (HOST) is E on it from anywhere: every player not in the boss arena sent there. It stands
/// only once the destination is set (`nz_hex_buttons done`).
/// </summary>
[ConCmd( "nz_hex_blue_altar" )]
public static void BlueAltarCmd( string what = "", float seconds = 0f )
{
var m = Instance;
if ( !m.IsValid() || !OnBasalt ) { Log.Info( "[nz-hex] the blue altar is basalt's" ); return; }
if ( what.Trim().Equals( "use", StringComparison.OrdinalIgnoreCase ) )
{
if ( NZGame.IsClient ) { Log.Warning( "[nz-hex] host only" ); return; }
var refused = m.StartArenaSend( NZPlayer.Local, anywhere: true );
if ( refused != "" ) Log.Warning( $"[nz-hex] the blue altar sends no one: {refused}" );
return;
}
if ( what.Trim().Equals( "rise", StringComparison.OrdinalIgnoreCase ) )
{
if ( seconds > 0f ) BlueAltarRiseSeconds = seconds;
if ( !m.ButtonsDoneShown )
{
Log.Warning( "[nz-hex] the blue altar waits under the floor until the teleporter's destination is set — nz_hex_buttons done" );
return;
}
m.BuildBlueAltar( rise: true );
NZSound.Play( BlueAltarCue, BlueAltarFoot );
}
else if ( what.Trim() != "" )
{
Log.Warning( "[nz-hex] nz_hex_blue_altar rise [seconds] or use — or nothing, to see where it stands" );
return;
}
Log.Info( $"[nz-hex] the blue altar {( !m.ButtonsDoneShown ? "waits under the floor — the destination is not set"
: m._blueAltarRising ? "is rising" : m._blueAltarGo.IsValid() ? "stands" : "is NOT built" )} at the teleporter's middle,"
+ $" {BlueAltarFoot} — it rises in {BlueAltarRiseSeconds:0.#}s" );
}
}