EasterEgg/HexPlatforms.Progress.cs

Partial class HexPlatforms, UI/game logic for showing egg (Easter egg) progress to a machine. It reads mirrored/internal state fields and exposes StepDoneShown, StepNowShown, grouping of 16 steps into 9 main steps, MainDoneShown/MainNowShown, and builds localized little-step entries with counts for UI display.

using Sandbox;

namespace NZombies;

/// <summary>
/// WHICH STEPS OF THE EGG THIS MACHINE IS SHOWN DONE, for the scoreboard's footer (`EggProgress.Footer`, 2026-09-29).
///
/// ⛔ THE MIRRORS, NEVER THE HOST'S OWN FIELDS. Every step is decided on the host and mirrored to each machine (`NZNet`'s
/// `HexPlatformsDone`, `HexBonfire`, `ShieldLockState`, … and `NZNet.PushState` replays them all to a joiner), so a `…Shown`
/// field reads the same on every screen, the host's included. The host-only fields (`_done`, `_bonfire`, …) would leave a
/// client's footer on step 1 for the whole game.
///
/// ⚠️ A PARTIAL OF `HexPlatforms`, so it reads the private mirrors without a public getter for each.
/// </summary>
public sealed partial class HexPlatforms
{
	/// <summary>
	/// Is step <paramref name="step"/> done, as this machine is shown it? 1-16, numbered as `NZNet.EggStepDone` numbers them
	/// (`EggFanfare.For`'s table), not as `nz_hex_skipto` does.
	///
	/// ⚠️ STEP 6, THE FLAME ON THE ALTAR, ALSO COUNTS AS DONE ONCE THE DEFENSE IS WON. Its mirror goes false again when the
	/// light-blue flame is lifted off for the twin shield (`HexPlatforms.Torch`). A failed defense takes the flame off too, and
	/// that is the one time it should read as not done: the team has to carry it back.
	/// </summary>
	public static bool StepDoneShown( int step )
	{
		var m = Instance;
		if ( !m.IsValid() ) return false;

		return step switch
		{
			1 => m._doneShown,
			2 => m._bonfireShown >= BonfireDone,
			3 => m.RingsShown.Done,
			4 => m._bonfireShown >= BonfireOut,
			5 => m._lockOpenShown,
			6 => m._flamePlacedShown || m._defenseShown == Defense.Won,
			7 => m._defenseShown == Defense.Won,
			8 => m._twinOpenShown,
			9 => m.ShriekersDoneShown,
			10 => m.MastermindDoneShown,
			11 => m.LavaDoneShown,
			12 => m.JunctionsDoneShown,
			13 => m.ButtonsDoneShown,
			14 => m._fightShown >= FightIntro,
			15 => m._fightShown >= FightWon,
			16 => m._fightShown == FightDone,
			_ => false,
		};
	}

	/// <summary>
	/// The step on now, as this machine is shown it: the first one not done, 1-16, or 17 once all sixteen are.
	///
	/// ⚠️ THE FIRST NOT DONE, NOT ONE PAST THE LAST DONE. The Shield Lock takes its code whenever the code is found, so it can
	/// open before the Torch Carry is done, and the step on is still the Torch Carry then.
	/// </summary>
	public static int StepNowShown
	{
		get
		{
			for ( var s = 1; s <= EggProgress.Steps; s++ )
				if ( !StepDoneShown( s ) ) return s;

			return EggProgress.Steps + 1;
		}
	}

	// ══ THE NINE MAIN STEPS AND THEIR LITTLE STEPS (2026-09-29) ═════════════════════════════════════════════════════════════
	//
	// The user regrouped the sixteen into nine, named them (`EggProgress.MainName`), and wrote each one's little steps: *"Ok
	// those are the main steps, now let's write the minor steps inside each one"*. The sixteen stay what the host decides and the
	// fanfares ride (`NZNet.EggStepDone`); these only read them, and a few finer mirrors this machine already has: the power,
	// the napalm's fire, the pests, each ring, the light blue flame in hand, the Shriekers, the stones.

	/// <summary>How many main steps the egg has: nine.</summary>
	public const int EggMains = 9;

	/// <summary>A little step as this machine is shown it: its line, done or not, and a count where it keeps one (`Need` 0: none).</summary>
	public readonly record struct EggLittle( string Text, bool Done, int Have = 0, int Need = 0 );

	/// <summary>The first and the last of the sixteen steps that make main step <paramref name="main"/> (1-9).</summary>
	public static (int First, int Last) MainSteps( int main ) => main switch
	{
		1 => (1, 1), 2 => (2, 2), 3 => (3, 3), 4 => (4, 7), 5 => (8, 9), 6 => (10, 11), 7 => (12, 12), 8 => (13, 14),
		_ => (15, 16),
	};

	/// <summary>The main step (1-9) one of the sixteen belongs to.</summary>
	public static int MainOf( int step ) => step switch
	{
		<= 1 => 1, 2 => 2, 3 => 3, <= 7 => 4, <= 9 => 5, <= 11 => 6, 12 => 7, <= 14 => 8, _ => 9,
	};

	/// <summary>Is main step <paramref name="main"/> done, as this machine is shown it? Every one of its sixteen-steps done.</summary>
	public static bool MainDoneShown( int main )
	{
		if ( main < 1 || main > EggMains ) return false;

		var (first, last) = MainSteps( main );
		for ( var s = first; s <= last; s++ )
			if ( !StepDoneShown( s ) ) return false;

		return true;
	}

	/// <summary>
	/// The main step on now, as this machine is shown it: the first not done, 1-9, or 10 once all nine are.
	/// ⚠️ THE FIRST NOT DONE, as `StepNowShown` is: the Shield Lock can open before the Torch Carry.
	/// </summary>
	public static int MainNowShown
	{
		get
		{
			for ( var m = 1; m <= EggMains; m++ )
				if ( !MainDoneShown( m ) ) return m;

			return EggMains + 1;
		}
	}

	/// <summary>
	/// Main step <paramref name="main"/>'s little steps, in the user's words, as this machine is shown them. Counts where the
	/// step keeps one: the pests (of `BonfireTarget`), the Shriekers on the dais (of three), the stones light blue (of six).
	/// </summary>
	public static EggLittle[] LittleStepsShown( int main )
	{
		var m = Instance;
		if ( !m.IsValid() ) return System.Array.Empty<EggLittle>();

		var rings = m.RingsShown;
		bool Ring( int colour ) => rings.Done || (rings.Active && rings.PositionOf( colour ) == rings.TargetOf( colour ));

		return main switch
		{
			// ⚠️ THE POWER COUNTS AS ON ONCE THE SLAMS ARE DONE: the slams need it, and a power that went off after is not undone
			1 => new[]
			{
				new EggLittle( "Turn on the power", Power.IsOn || m._doneShown ),
				new EggLittle( "Slam the tile slots", m._doneShown ),
			},
			2 => new[]
			{
				new EggLittle( "Kill a napalm zombie on the crucible", m._bonfireShown >= BonfireLit ),
				new EggLittle( "Kill pests in the fire", m._bonfireShown >= BonfireDone,
					m._bonfireShown >= BonfireDone ? BonfireTarget : System.Math.Clamp( m._pestsShown, 0, BonfireTarget ), BonfireTarget ),
			},
			// the user's order, blue, green, yellow, red (`ColourName`: 0 blue, 1 yellow, 2 green, 3 red)
			3 => new[]
			{
				new EggLittle( "Set the blue ring", Ring( 0 ) ),
				new EggLittle( "Set the green ring", Ring( 2 ) ),
				new EggLittle( "Set the yellow ring", Ring( 1 ) ),
				new EggLittle( "Set the red ring", Ring( 3 ) ),
			},
			4 => new[]
			{
				new EggLittle( "Kill a Shrieker in the bonfire", StepDoneShown( 4 ) ),
				new EggLittle( "Unlock the altar", StepDoneShown( 5 ) ),
				new EggLittle( "Place the flame on the altar", StepDoneShown( 6 ) ),
				new EggLittle( "Defend the altar", StepDoneShown( 7 ) ),
			},
			5 => new[]
			{
				// ⚠️ IN HAND, OR SPENT ON THE TWIN SHIELD: a carrier who loses it unticks it, since the flame is back to be taken
				new EggLittle( "Pick up the blue flame", m._twinOpenShown || (m.FlameBlueShown && m.FlameCarried) ),
				new EggLittle( "Unlock the gateway", StepDoneShown( 8 ) ),
				new EggLittle( "Kill 3 Shriekers in the gateway", StepDoneShown( 9 ),
					System.Math.Clamp( m._shriekerKillsShown, 0, ShriekersTarget ), ShriekersTarget ),
			},
			6 => new[]
			{
				new EggLittle( "Power up the magma generator", StepDoneShown( 10 ) ),
				new EggLittle( "Survive the lava", StepDoneShown( 11 ) ),
			},
			7 => new[]
			{
				new EggLittle( "Correct the junction's path", StepDoneShown( 12 ) ),
			},
			8 => new[]
			{
				new EggLittle( "Turn the stones blue in the gateway", StepDoneShown( 13 ), StonesBlueShown( m ), 6 ),
				new EggLittle( "Interact with the gateway", StepDoneShown( 14 ) ),
			},
			9 => new[]
			{
				new EggLittle( "Defeat Oberon", StepDoneShown( 15 ) ),
				new EggLittle( "Pick up the orb", StepDoneShown( 16 ) ),
			},
			_ => System.Array.Empty<EggLittle>(),
		};
	}

	/// <summary>How many of the teleporter's six stones are light blue, as this machine is shown them: 0 asleep, 6 once set.</summary>
	static int StonesBlueShown( HexPlatforms m )
	{
		if ( m.ButtonsDoneShown ) return 6;
		if ( (m._btnFlagsShown & BtnActive) == 0 ) return 0;

		var n = 0;
		for ( var b = 0; b < 6; b++ )
			if ( BtnColour( m._btnColoursShown, b ) == 2 ) n++;

		return n;
	}
}