EasterEgg/EggProgress.cs

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.

File Access
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") );
	}
}