using ProjectM.Simulation; namespace ProjectM.Client { /// /// Pure, engine-free logic for the first-run onboarding coach-mark sequence — the testable core of /// (mirrors the project's *Math helper discipline; no UnityEngine / /// Entities types so it unit-tests as plain C#). Defines the ordered step list, a of the /// observable client state each step reads, the deterministic per-step completion test, prompt copy, the /// spatial-cue kind, and the persisted-mask helpers. /// /// Pacing (operator-locked = soft-gated): a step shows until its action is performed (no per-step timeout), /// EXCEPT two info beats — and — which also auto-advance, plus /// the timed strip. Veteran / co-op auto-suppress falls out for free: the count-based /// steps (, ) test an ABSOLUTE structure count, so a client joining /// an already-built base satisfies them on entry and skips straight past. /// public static class OnboardingStepMath { // ---- ordered steps (byte ids; bit i of GameSettings.OnboardingMask = step i complete) ---- public const byte Welcome = 0; // tiny win-condition framing strip (timed) public const byte Move = 1; public const byte Mine = 2; // attack an Ore node — mining IS combat at the base (Calm = no enemies yet) public const byte Build = 3; // open palette + place a Turret public const byte Fabricator = 4; // Ore -> Charge (soft info beat) public const byte Gate = 5; // reach the Expedition Gate public const byte Clear = 6; // clear the expedition wave (the first real enemies) public const byte Return = 7; // walk back to base to bank +1 charge public const byte Defend = 8; // survive the retaliation siege (soft info beat) public const byte Done = 9; // closing beat public const byte StepCount = 10; // ---- tunable thresholds (public so the EditMode tests pin the contract) ---- public const float WelcomeSeconds = 5f; public const float MoveThreshold = 3f; // accumulated player movement (world units) public const float FabricatorSoftSeconds = 14f; // soft beat auto-advance if no Fabricator built public const float DefendNoSiegeSeconds = 20f; // advance if no siege ever materialises public const float DoneSeconds = 6f; // closing beat lingers before going dormant // ---- spatial-cue kinds the System resolves to a live world target ---- public const byte PointerNone = 0; public const byte PointerOreNode = 1; public const byte PointerBaseGate = 2; // the base-region gate (go to the expedition) public const byte PointerExpeditionGate = 3; // the expedition-region gate (return home) /// Observable client state for one evaluation. Built by the System from ECS + input each frame. public struct Snapshot { public float StepElapsed; // seconds the current step has been shown public float MoveDistance; // accumulated player movement since the Move step began public int OreNow; // shared-ledger Ore right now public int OreBaseline; // ledger Ore captured when the Mine step began public int TurretCount; // live Turret structures (absolute) public int FabricatorCount; // live Fabricator structures (absolute) public bool OnExpedition; // local player is in the expedition region public byte ObjectiveState; // ExpeditionObjective.State (Idle/Active/Cleared) public bool SawSiege; // a Siege phase was observed while the Defend step was showing public byte Phase; // CycleState.Phase (Calm/Siege) } /// True when the step's taught action is complete (or its soft timeout has elapsed). public static bool IsSatisfied(byte step, in Snapshot s) { switch (step) { case Welcome: return s.StepElapsed >= WelcomeSeconds; case Move: return s.MoveDistance >= MoveThreshold; case Mine: return s.OreNow > s.OreBaseline; case Build: return s.TurretCount >= 1; case Fabricator: return s.FabricatorCount >= 1 || s.StepElapsed >= FabricatorSoftSeconds; case Gate: return s.OnExpedition || s.ObjectiveState == ExpeditionObjectiveState.Active; case Clear: return s.ObjectiveState == ExpeditionObjectiveState.Cleared; case Return: return !s.OnExpedition; // entered while on expedition; satisfied on crossing home case Defend: return s.SawSiege ? s.Phase == CyclePhase.Calm : s.StepElapsed >= DefendNoSiegeSeconds; case Done: return s.StepElapsed >= DoneSeconds; default: return true; } } /// Which world target (if any) the prompt should point at this step. public static byte PointerKind(byte step) { switch (step) { case Mine: return PointerOreNode; case Gate: return PointerBaseGate; case Return: return PointerExpeditionGate; default: return PointerNone; } } /// Ultra-short, verb-first prompt copy with the player's real input glyph (scheme-aware). public static string Prompt(byte step, bool gamepad) { string move = gamepad ? "Left Stick" : "WASD"; string attack = gamepad ? "RT" : "LMB"; string build = gamepad ? "Y" : "Tab"; // matches the existing HUD build-discovery chip glyph switch (step) { case Welcome: return "CLEAR EXPEDITIONS to charge the Engine — defend the Core while you do. (Esc → Pause → How to Play)"; case Move: return move + " — Move"; case Mine: return attack + " — Attack the glowing Ore to mine it"; case Build: return build + " — open Build, then place a Turret by your Core"; case Fabricator: return "Build a Fabricator — turrets need Charge (Ore → ammo)"; case Gate: return "Reach the Expedition Gate — clearing it charges the Engine"; case Clear: return "Clear the zone — defeat every enemy"; case Return: return "Return through the gate — bank your clear (+1 Engine charge)"; case Defend: return "Defend the Core! — hold the line through the siege"; case Done: return "You've got it. Clear expeditions to fill the Engine and win."; default: return ""; } } // ---- persisted-mask helpers (GameSettings.OnboardingMask) ---- /// All steps complete (the sequence is dormant). public static bool AllComplete(int mask) { int all = (1 << StepCount) - 1; return (mask & all) == all; } /// The lowest not-yet-completed step (resume point); when all are complete. public static byte FirstIncomplete(int mask) { for (byte i = 0; i < StepCount; i++) if ((mask & (1 << i)) == 0) return i; return Done; } } }