Static helper for showing Easter egg progress and banners. It maps step numbers to names, formats a footer and counter for the HUD, tracks and displays a timed banner when a main step completes, and provides console commands to print state or force the banner.
using Sandbox;
using System;
namespace NZombies;
/// <summary>
/// BASALT'S EASTER EGG AS A PLAYER READS IT: the step on now, under the scoreboard, and a banner when one is done. Asked for as
/// *"At the bottom I want to add the current Easter egg step name. And when we complete a step I want it to appear on screen that
/// it was completed"* (2026-09-29).
///
/// ⛔ THE STEPS ARE NUMBERED AS `NZNet.EggStepDone` NUMBERS THEM, 1 to 16 (`EggFanfare.For`'s table), not as `nz_hex_skipto`
/// does: its twelve leave out the Shield Lock and the fight.
///
/// ⛔ TWO SOURCES, BECAUSE ONE IS AN EVENT AND THE OTHER IS STATE:
/// - the BANNER rides the host's word that a step is done (`NZNet.EggStepDone` → <see cref="OnStepDone"/>). It shows on every
/// screen at once, never for a joiner catching up, and never while testing (`HexPlatforms.Fanfare` sends nothing then);
/// - the FOOTER reads each step's mirror on this machine (`HexPlatforms.StepDoneShown`), which a joiner is sent too. The word
/// would not do for it: a joiner never hears one, and neither the dev shortcuts nor `nz_hex_skipto` send it.
///
/// ⛔ NINE MAIN STEPS OVER THE SIXTEEN (2026-09-29): the user regrouped them, named them cryptically (*"a more cryptic name, one
/// that is a hint but not straight forward"*) and wrote each one's little steps. The sixteen are still what the host decides and
/// sends; a main step is done when all of its sixteen-steps are (`HexPlatforms.MainSteps`), and its little steps are read from
/// this machine's mirrors (`HexPlatforms.LittleStepsShown`). The TAB board's Easter egg panel draws them.
///
/// ⚠️ A STATIC, for the reason `PowerupBannerState` is one: a razor component is a generated type plain code cannot reach.
/// </summary>
public static class EggProgress
{
/// <summary>How many steps the egg has, as the host counts and sends them. The last is everyone home from the core.</summary>
public const int Steps = 16;
/// <summary>How many MAIN steps: nine, each one or more of the sixteen (`HexPlatforms.MainSteps`).</summary>
public const int Mains = HexPlatforms.EggMains;
/// <summary>A main step's name, the user's (2026-09-29): a hint, never the instruction.</summary>
public static string MainName( int main ) => main switch
{
1 => "Litany of Four Falls",
2 => "Kindling for Blood",
3 => "The Orrery of Elements",
4 => "Purify the Flame",
5 => "The Clamor upon the Dais",
6 => "The Prism's Three Riddles",
7 => "Reroute the Gateway's Veins",
8 => "Where the Gateway Leads",
9 => "Claim the Sovereign's Core",
_ => $"Step {main}",
};
/// <summary>
/// A step's name: the user's where they gave one (Color Smash, Bonfire, the Altar…), `EggFanfare.For`'s otherwise.
/// </summary>
public static string NameOf( int step ) => step switch
{
1 => "Color Smash",
2 => "Bonfire",
3 => "Color Rings",
4 => "Torch Carry",
5 => "Shield Lock",
6 => "The Altar",
7 => "Altar Defense",
8 => "Twin Shield",
9 => "Shrieker Platform",
10 => "The Mastermind",
11 => "Rising Lava",
12 => "The Junctions",
13 => "Teleporter Buttons",
14 => "The Blue Altar",
15 => "The Boss Fight",
16 => "The Core",
_ => $"Step {step}",
};
// ══ the footer ══════════════════════════════════════════════════════════════════════════
/// <summary>
/// The egg in one line: "Step V of IX · The Clamor upon the Dais" (Roman on basalt's HUD, as its rounds are), or "Complete"
/// once everyone is home. Null off basalt. The TAB board's panel shows the same, with the little steps under it.
/// </summary>
public static string Footer
{
get
{
if ( !HexPlatforms.OnBasalt ) return null;
var now = HexPlatforms.MainNowShown;
return now > Mains ? "Complete" : $"{Counter( now )} · {MainName( now )}";
}
}
/// <summary>"Step V of IX", or "Complete" past the ninth.</summary>
public static string Counter( int main )
=> main > Mains ? "Complete" : $"Step {HudTheme.TierNumeral( main )} of {HudTheme.TierNumeral( Mains )}";
// ══ the banner ══════════════════════════════════════════════════════════════════════════
/// <summary>How long the banner stays up, fades included. 5.5 s.</summary>
///
/// ⚠️ A NULLABLE BEHIND IT (INSTRUCTIONS §1), so a new default is not held back by a hotload.
public static float BannerSeconds
{
get => _bannerSeconds ?? 5.5f;
set => _bannerSeconds = Math.Max( 1f, value );
}
static float? _bannerSeconds;
/// <summary>The fade in, and the fade out at the end.</summary>
const float FadeIn = 0.35f, FadeOut = 1.2f;
static int _bannerStep;
static RealTimeSince _bannerSince;
/// <summary>The MAIN step the banner is for, 1-9, 0 for none.</summary>
public static int BannerStep => _bannerStep;
/// <summary>Is the banner up? ⚠️ In a game only, as the fanfares are: a new game's lobby does not wear the last one's.</summary>
public static bool BannerShowing => _bannerStep > 0 && _bannerSince < BannerSeconds && InGame;
/// <summary>The small line: "Step VII complete" (the stylesheet sets it in capitals).</summary>
public static string BannerKicker => _bannerStep > 0 ? $"Step {HudTheme.TierNumeral( _bannerStep )} complete" : "";
/// <summary>The big line: the main step's name.</summary>
public static string BannerName => _bannerStep > 0 ? MainName( _bannerStep ) : "";
/// <summary>In over <see cref="FadeIn"/>, held, then out over the last <see cref="FadeOut"/>.</summary>
public static float BannerOpacity
{
get
{
var t = (float)_bannerSince;
var d = BannerSeconds;
if ( t < FadeIn ) return t / FadeIn;
if ( t <= d - FadeOut ) return 1f;
return Math.Clamp( (d - t) / FadeOut, 0f, 1f );
}
}
static bool InGame => NZGame.Mode is GameMode.Survival or GameMode.Spectator;
/// <summary>
/// One of the sixteen done, on this machine: the banner, if it ends a main step. From the host's word (`NZNet.EggStepDone`),
/// which every machine hears, the host's own included.
///
/// ⛔ ONLY A MAIN STEP'S END RAISES IT (*"make it so the message only appears for the main steps"*, 2026-09-29). All sixteen
/// still arrive, for their fanfares; the little ones pass here without a banner.
/// ⚠️ A SECOND MAIN STEP DONE WHILE ONE IS UP REPLACES IT and starts the clock again, as the powerup banner does.
/// </summary>
public static void OnStepDone( int step )
{
if ( step < 1 || step > Steps || !HexPlatforms.OnBasalt ) return;
var main = HexPlatforms.MainOf( step );
if ( step != HexPlatforms.MainSteps( main ).Last )
{
Log.Info( $"[nz-egg] step {step} done, {NameOf( step )}: a little step of main step {main}, no banner" );
return;
}
Show( main );
Log.Info( $"[nz-egg] step {step} done, {NameOf( step )}: main step {main}, {MainName( main )}, is done: its banner here" );
}
/// <summary>Raise the banner for a MAIN step (1-9), now.</summary>
public static void Show( int main )
{
_bannerStep = Math.Clamp( main, 1, Mains );
_bannerSince = 0f;
}
// ══ commands ════════════════════════════════════════════════════════════════════════════
/// <summary>
/// `nz_egg_step` — the nine main steps as this machine is shown them, each with its little steps (done, the one on, the
/// counts) and the sixteen it is made of; then the board's line.
/// </summary>
[ConCmd( "nz_egg_step" )]
public static void StepCmd()
{
if ( !HexPlatforms.OnBasalt )
{
Log.Info( "[nz-egg] not on basalt: the TAB board's Easter egg panel and the step banner are basalt's Easter egg only" );
return;
}
var now = HexPlatforms.MainNowShown;
Log.Info( now > Mains
? "[nz-egg] basalt's Easter egg, as this machine is shown it: COMPLETE"
: $"[nz-egg] basalt's Easter egg, as this machine is shown it: {Counter( now )}, {MainName( now )}" );
for ( var m = 1; m <= Mains; m++ )
{
var (first, last) = HexPlatforms.MainSteps( m );
Log.Info( $"[nz-egg] {HudTheme.TierNumeral( m ),5} {MainName( m ),-28} {(HexPlatforms.MainDoneShown( m ) ? "done" : m == now ? "<- on now" : "")}"
+ $" (steps {first}{(last > first ? $"-{last}" : "")}: {NameOf( first )}{(last > first ? $" … {NameOf( last )}" : "")})" );
var firstOpen = true;
foreach ( var l in HexPlatforms.LittleStepsShown( m ) )
{
var mark = l.Done ? "x" : firstOpen && m == now ? ">" : " ";
if ( !l.Done ) firstOpen = false;
Log.Info( $"[nz-egg] [{mark}] {l.Text}{(l.Need > 0 ? $" {l.Have}/{l.Need}" : "")}" );
}
}
Log.Info( $"[nz-egg] the board's line: \"{Footer}\" · nz_egg_banner <main step> shows a main step's banner here" );
}
/// <summary>
/// `nz_egg_banner [main step]` — raise a MAIN step's banner (1-9) on this machine, the one on now by default: to look at it
/// without doing the step. It shows in a game only.
/// </summary>
[ConCmd( "nz_egg_banner" )]
public static void BannerCmd( int step = 0 )
{
if ( step <= 0 ) step = Math.Min( HexPlatforms.MainNowShown, Mains );
Show( step );
Log.Info( $"[nz-egg] step {BannerStep}'s banner, \"{BannerKicker}\" / \"{BannerName}\", for {BannerSeconds:0.#}s"
+ (InGame ? "" : " — but it shows in a game only") );
}
}