Game component for the HexPlatforms system that implements the Shriekers (the 1911 platform) step of an Easter egg. It tracks Shriekers dying on the platform, syncs the count to clients, builds local green/blue side panels when the target is reached, and exposes a console command to manipulate or inspect the state.
using System;
using Sandbox;
namespace NZombies;
/// <summary>
/// BASALT — THE SHRIEKER PLATFORM (2026-09-26). The twin shield's column stands in the middle of a raised, stepped hexagon
/// of combine metal. Once the light blue flame has brought the shield down, three Shriekers must die on it, and then its
/// sides turn green. Asked for as *"i was thinking of having to kill 3 shriekers in the platform i put the 1911 wallbuy in
/// — this will enable the next step"*, *"add up all game, but only from the moment the combine shield is opened with the
/// blue flame"*, and *"after killing the 3 shriekers, the platform's sides become green"*. The user marked the platform
/// with a 1911 wall buy, laid flat on its west side.
///
/// ⛔ A DEATH COUNTS WHERE THE BODY FELL, as Bonfire's do (`HexPlatforms.Bonfire.cs`): `ZombieAI.Die`, the one site every
/// kill funnels through, hands over the variant and the position, whatever did the killing. A Shrieker counts if it dies
/// on the platform — anywhere on its hexagon, the outer step and where the column stood included — and only once the
/// twin shield is down. The count adds up across rounds, three finishing it; a new game starts it over, with the shield.
/// Its end begins the next step, the Mastermind (`HexPlatforms.Mastermind.cs`).
///
/// ⚠️ THE SHRIEKERS ARE BASALT'S OWN: its config sends them from round 14 (`AmbientSpecials`). The room is reached through
/// teleporter #0, and by the zombies once its debris is open — the user's word, 2026-09-26.
///
/// ⛔ THE GREEN IS COLOR SMASH'S, ON OUR OWN PANELS. The platform is map geometry, so its sides are lit the way a tile's
/// are (`HexSidePanels`): light quads laid just proud of its faces, in the tinted copy of the map's white001 that a green
/// pick is lit in (`MaterialFor`). So `nz_hex_colour green` retunes both. Tile 1's white sides were done the same way.
///
/// ⛔ WHO OWNS WHAT (INSTRUCTIONS.md, "BUILD FOR MULTIPLAYER"):
/// - the COUNT is HOST state, MIRRORED (`NZNet.HexShriekers`) on every change and to a joiner;
/// - the GREEN SIDES are LOCAL: every machine lights its own from the count it was told (<see cref="BuildShriekerSides"/>);
/// - each counted death's scrape, and the step-done clicking at the third, are the host's to send (`Cue`).
/// </summary>
public sealed partial class HexPlatforms
{
// ══ where it is ══════════════════════════════════════════════════════════════════════════
//
// Two hexagons, one on the other, round the twin column's middle (`TwinCentre`), read off the BSP on 2026-09-26:
// - the OUTER STEP, a tile's own hexagon (`HexSidePanels.Corners`: points 144u out on ±Y, flats 124u), from the room's
// floor at 1696 to its top at 1702;
// - the INNER STEP, the platform proper (brush 519): points 130u out, flats 110u, from 1696 to its top at 1706.
// Between them runs a groove 4u wide, down to the floor.
/// <summary>The outer step, as a tile: a tile's own hexagon, its top at 1702 and its sides down to the floor.</summary>
static Tile ShriekerRing => new( "1911", TwinCentre.x, TwinCentre.y, 1702f, 1696f );
/// <summary>The inner step: its top at 1706, where the 1911 lay, and its sides down to the groove's floor.</summary>
static Tile ShriekerStep => new( "1911 step", TwinCentre.x, TwinCentre.y, 1706f, 1696f );
/// <summary>
/// The inner step's corners, counter-clockwise from the north point as `HexSidePanels.Corners` runs. A property, not a
/// static table: an initialiser does not re-run on hotload.
/// </summary>
static Vector2[] ShriekerStepCorners => new Vector2[]
{
new( 0, 130 ), new( -110, 65 ), new( -110, -65 ), new( 0, -130 ), new( 110, -65 ), new( 110, 65 ),
};
/// <summary>The middle of the platform's top, the inner step's: 1706, 10u over the room's floor.</summary>
static Vector3 ShriekerPlatformTop => new( TwinCentre.x, TwinCentre.y, ShriekerStep.Top );
/// <summary>
/// How far past the outer step's edge a body may be and still count as on the platform. Tighter than tile 1's
/// (<see cref="BodyEdgeSlack"/>): the platform stands only 10u over the floor, and a Shrieker standing beside it is not
/// on it.
/// </summary>
const float ShriekerEdgeSlack = 8f;
/// <summary>How many Shriekers must die on it: three, as asked.</summary>
public const int ShriekersTarget = 3;
/// <summary>Is a body lying here on the platform: the outer step, the inner one, or where the column stood?</summary>
static bool OnShriekerPlatform( Vector3 at )
=> OutOf( at, ShriekerRing ) <= ShriekerEdgeSlack && MathF.Abs( at.z - ShriekerPlatformTop.z ) <= BodyHeightSlack;
// ══ the state ════════════════════════════════════════════════════════════════════════════
/// <summary>HOST — how many Shriekers have died on the platform since the twin shield fell.</summary>
int _shriekerKills;
/// <summary>MIRROR — the same, as this machine was told (`NZNet.HexShriekers`). The green sides are lit from it.</summary>
int _shriekerKillsShown;
/// <summary>HOST — the count, for `NZNet.PushState` to replay to a joiner.</summary>
public static int ShriekerKillsState => Instance.IsValid() ? Instance._shriekerKills : 0;
/// <summary>Is the Shrieker platform done — three died on it? As this machine was told: what the next step asks.</summary>
public bool ShriekersDoneShown => _shriekerKillsShown >= ShriekersTarget;
void SendShriekers() => NZNet.HexShriekers( _shriekerKills );
/// <summary>How many have died on it. EVERY machine — `NZNet.HexShriekers`: at three, the sides lit green.</summary>
public void ApplyShriekers( int kills )
{
_shriekerKillsShown = kills;
BuildShriekerSides();
}
/// <summary>The count back to none. HOST — with the twin shield (<see cref="ResetTwin"/>), which it follows.</summary>
void ResetShriekers()
{
// the Mastermind follows the Shrieker platform, and goes with it
ResetMastermind();
if ( _shriekerKills == 0 ) return;
_shriekerKills = 0;
SendShriekers();
}
/// <summary>Where it stands, in words. HOST.</summary>
string ShriekersStateText() => !_twinOpen
? "the Shrieker platform: waiting for the twin shield to fall"
: _shriekerKills >= ShriekersTarget
? $"the Shrieker platform DONE — {_shriekerKills} Shriekers died on it"
+ ( OnBasalt ? ( _shriekerSidesGo.IsValid() ? ", and its sides are green" : " — ⚠️ its sides are NOT lit here" ) : "" )
: $"the Shrieker platform: {_shriekerKills} of {ShriekersTarget} Shriekers killed on it";
// ══ the deaths ═══════════════════════════════════════════════════════════════════════════
/// <summary>
/// A body of this kind fell here, by the Shrieker platform's rules. HOST — `OnZombieKilled`, apart from the hook so the
/// selftest can walk it. Only a Shrieker, only on the platform, only once the twin shield is down, and never past three.
/// </summary>
void ShriekerFell( string kind, Vector3 at )
{
if ( kind != SpecialEnemies.Shrieker || !OnShriekerPlatform( at ) ) return;
if ( !_twinOpen )
{
Log.Info( "[nz-hex] a Shrieker died on the 1911 platform, but the twin shield still stands — it does not count yet" );
return;
}
if ( _shriekerKills >= ShriekersTarget ) return;
_shriekerKills++;
SendShriekers();
// a right slam's scrape at the platform for each one — and at the third, the step's clicking a moment after
Cue( SmashCue, ShriekerPlatformTop );
if ( _shriekerKills < ShriekersTarget )
{
Log.Info( $"[nz-hex] ☠ a Shrieker died on the 1911 platform — {_shriekerKills} of {ShriekersTarget}" );
return;
}
if ( !Testing ) DoneCueLater();
Fanfare( 9 );
Log.Info( $"[nz-hex] ✦ THE SHRIEKER PLATFORM DONE — {_shriekerKills} Shriekers died on the 1911 platform, and its sides turn"
+ " green" );
// …and the next step begins: the Mastermind (`HexPlatforms.Mastermind.cs`)
StartMastermind();
}
// ══ the green sides ══════════════════════════════════════════════════════════════════════
/// <summary>The colour its sides turn: a green pick's (colour 2), which `nz_hex_colour green` retunes with the tiles.</summary>
const int ShriekerSidesColour = 2;
/// <summary>LOCAL — the green sides: both steps' panels, children of one object.</summary>
GameObject _shriekerSidesGo;
/// <summary>LOCAL — were they built light blue, the junctions' energy through (`HexPlatforms.Junctions.cs`)?</summary>
bool _shriekerSidesCharged;
/// <summary>
/// How the green sides are drawn: ⚠️ BUMP IT WHEN THAT CHANGES. 1: both steps' sides, 2026-09-26. A manager that has not
/// built this layout lights them again — `OnUpdate` asks every frame — as the altar's `AltarLayout` does.
/// </summary>
const int ShriekerSidesLayout = 1;
/// <summary>LOCAL — the layout this manager last lit the sides in; 0 before it has.</summary>
int _shriekerSidesLaid;
/// <summary>
/// The platform's sides green, on basalt, once this machine was told three Shriekers died on it: every side of both
/// steps, down to the floor, so the groove between them glows too. Otherwise none. LOCAL.
/// </summary>
void BuildShriekerSides()
{
_shriekerSidesLaid = ShriekerSidesLayout;
if ( !OnBasalt || !ShriekersDoneShown || !Scene.IsValid() ) { ClearShriekerSides(); return; }
// ⚠️ LIGHT BLUE ONCE THE JUNCTIONS' ENERGY IS THROUGH: the teleporter charged (`HexPlatforms.Junctions.cs`)
var charged = JunctionsDoneShown;
if ( _shriekerSidesGo.IsValid() && _shriekerSidesCharged == charged ) return;
ClearShriekerSides();
var material = charged ? WheelMaterial( WheelLightBlue ) : MaterialFor( ShriekerSidesColour );
var root = Scene.CreateObject();
root.Name = charged ? "Shrieker platform — its sides light blue, charged" : "Shrieker platform — its sides green";
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 );
foreach ( var (name, model) in new[]
{
("the outer step", HexSidePanels.Build( new[] { ShriekerRing }, material )),
("the inner step", HexSidePanels.Build( new[] { ShriekerStep }, material, corners: ShriekerStepCorners )),
} )
{
if ( model is null ) continue;
var go = Scene.CreateObject();
go.Name = $"{root.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 );
var r = go.Components.Create<ModelRenderer>();
r.Model = model;
r.RenderType = ModelRenderer.ShadowRenderType.Off;
}
_shriekerSidesGo = root;
_shriekerSidesCharged = charged;
}
/// <summary>The platform's sides back to the map's own metal. LOCAL.</summary>
void ClearShriekerSides()
{
if ( _shriekerSidesGo.IsValid() ) _shriekerSidesGo.Destroy();
_shriekerSidesGo = null;
}
// ══ commands ════════════════════════════════════════════════════════════════════════════
/// <summary>
/// `nz_hex_shriekers [kill|done|reset] [n]` — HOST: where the Shrieker platform stands. `kill [n]` is n Shriekers dying
/// on it (one if none given), by its rules — the twin shield down first (`nz_hex_twin open`); `done` finishes it, turns
/// its sides green and begins the Mastermind, bringing the shield down first if it stood; `reset` puts the count back
/// to none, and the Mastermind with it.
/// </summary>
[ConCmd( "nz_hex_shriekers" )]
public static void ShriekersCmd( string what = "", int n = 1 )
{
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 "kill":
if ( !m._twinOpen )
{
Log.Warning( "[nz-hex] the twin shield stands, so a Shrieker on its platform counts nothing yet — nz_hex_twin open first" );
return;
}
for ( var i = 0; i < Math.Max( 1, n ); i++ ) m.ShriekerFell( SpecialEnemies.Shrieker, ShriekerPlatformTop );
break;
case "done":
if ( !m._twinOpen ) TwinCmd( "open" );
m._shriekerKills = ShriekersTarget;
m.SendShriekers();
m.StartMastermind();
break;
case "reset":
m._shriekerKills = 0;
m.SendShriekers();
m.ResetMastermind();
break;
default:
Log.Warning( "[nz-hex] nz_hex_shriekers kill [n], done or reset — or nothing, to see where it stands" );
return;
}
Log.Info( $"[nz-hex] {m.ShriekersStateText()}" );
}
}