a482a9c4ed
SLICE A - rooms become places: ExpeditionBiome duplicated to sub-slot 1 (X+1500) - ground, 205 decor children, rim cliffs, ambience; collider ring + 3 landmark colliders duplicated in the subscene; NEW in-room cover at BOTH slots (4 collidable rocks each: Environment-layer box colliders in the subscene + matching Synty rock meshes in Game.unity, placed clear of the landing point, portal spot, spawn ring and boss spawn). Half of all rooms previously played on a literal void. SLICE C - owned FX wired: VFXConfig gains Portal + EnemySpawn slots (FX_Portal_Round_01 / FX_Dust_Big_01 from PolygonParticleFX); the portal beacon prefers the authored effect (procedural pillar stays as fallback); enemies get a spawn-emerge cue on the ghost add-edge (primed-scan guard skips the connect flood). SLICE D (teaching) - the two undiscoverable verbs are now taught: dash on the Move step, class ability + attack on the Rooms step (scheme-aware glyphs); How-to-Play Controls adds Class ability + Dash rows and fixes the wrong RT glyph; Loop step 1 mentions class pick + prep; the stale '50 starting Ore' now interpolates Tuning.StartingOre. 456/456 EditMode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
146 lines
9.8 KiB
C#
146 lines
9.8 KiB
C#
using ProjectM.Simulation;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// Pure, engine-free logic for the first-run onboarding coach-mark sequence — the testable core of
|
|
/// <see cref="OnboardingSystem"/> (mirrors the project's <c>*Math</c> helper discipline; no UnityEngine /
|
|
/// Entities types so it unit-tests as plain C#). Defines the ordered step list, a <see cref="Snapshot"/> 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.
|
|
///
|
|
/// RE-AUTHORED for the expedition redesign (demo polish): base nodes are gone (mining happens IN the run),
|
|
/// so the old at-base Mine step is dead — the first lap is now Build (grubstake Ore) → Fabricator →
|
|
/// READY UP → fight rooms (mine in-run) → boon → boss/return → defend. Step BITS are re-meant, never
|
|
/// renamed-in-place semantics: a veteran's full mask stays dormant; a partial first-run mask at worst
|
|
/// replays one beat.
|
|
///
|
|
/// Pacing (operator-locked = soft-gated): a step shows until its action is performed (no per-step timeout),
|
|
/// EXCEPT two info beats — <see cref="Fabricator"/> and <see cref="Defend"/> — which also auto-advance, plus
|
|
/// the timed <see cref="Welcome"/> strip. Veteran / co-op auto-suppress falls out for free: the count-based
|
|
/// steps (<see cref="Build"/>, <see cref="Fabricator"/>) test an ABSOLUTE structure count, so a client joining
|
|
/// an already-built base satisfies them on entry and skips straight past.
|
|
/// </summary>
|
|
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 Build = 2; // open palette + place a Turret (the 50-Ore grubstake covers it)
|
|
public const byte Fabricator = 3; // Ore -> Charge (soft info beat)
|
|
public const byte ReadyUp = 4; // press T / click READY — the party launches together
|
|
public const byte Rooms = 5; // fight the rooms; attack crystal nodes to haul resources
|
|
public const byte Boon = 6; // clear a room -> pick 1 of 3 boons + choose the path
|
|
public const byte Return = 7; // fell the boss — the haul + the Engine charge come home
|
|
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
|
|
public const float RoomsSeconds = 7f; // D2: keep the mine-the-crystals prompt + node pointer up a beat AFTER teleport (past the ~3s launch countdown -> ~4s in the room)
|
|
public const float ReturnMaxSeconds = 75f; // D3: soft backstop so a missed homecoming signal can NEVER stall the sequence
|
|
|
|
// ---- 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; // RETIRED target (walk-in gate died with the redesign); byte kept
|
|
public const byte PointerExpeditionGate = 3; // RETIRED target; byte kept for mask/step stability
|
|
|
|
/// <summary>Observable client state for one evaluation. Built by the System from ECS + input each frame.</summary>
|
|
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 TurretCount; // live Turret structures (absolute)
|
|
public int FabricatorCount; // live Fabricator structures (absolute)
|
|
public bool LocalReady; // the LOCAL player's replicated PlayerReady flag
|
|
public byte Lifecycle; // replicated RunInfo.Lifecycle (RunLifecycle.*)
|
|
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 bool WasOnExpedition;// D3: latched true once the player was seen on expedition during the Return step (so a start-at-base Return doesn't instantly satisfy)
|
|
|
|
public byte Phase; // CycleState.Phase (Calm/Siege)
|
|
}
|
|
|
|
/// <summary>True when the step's taught action is complete (or its soft timeout has elapsed).</summary>
|
|
public static bool IsSatisfied(byte step, in Snapshot s)
|
|
{
|
|
switch (step)
|
|
{
|
|
case Welcome: return s.StepElapsed >= WelcomeSeconds;
|
|
case Move: return s.MoveDistance >= MoveThreshold;
|
|
case Build: return s.TurretCount >= 1;
|
|
case Fabricator: return s.FabricatorCount >= 1 || s.StepElapsed >= FabricatorSoftSeconds;
|
|
case ReadyUp: return s.LocalReady || s.Lifecycle != RunLifecycle.Staging;
|
|
case Rooms: return s.OnExpedition && s.StepElapsed >= RoomsSeconds; // D2: show the mine prompt + node pointer IN the room, not the instant we teleport
|
|
case Boon: return s.ObjectiveState == ExpeditionObjectiveState.Cleared
|
|
|| s.Lifecycle == RunLifecycle.RoomReward
|
|
|| s.Lifecycle == RunLifecycle.RouteSelect;
|
|
case Return: return (s.WasOnExpedition && !s.OnExpedition) || s.StepElapsed >= ReturnMaxSeconds; // D3: home AFTER being on expedition, else a soft timeout (never gate on the 1-tick Returning edge)
|
|
case Defend: return s.SawSiege ? s.Phase == CyclePhase.Calm : s.StepElapsed >= DefendNoSiegeSeconds;
|
|
case Done: return s.StepElapsed >= DoneSeconds;
|
|
default: return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Which world target (if any) the prompt should point at this step.</summary>
|
|
public static byte PointerKind(byte step)
|
|
{
|
|
switch (step)
|
|
{
|
|
case Rooms: return PointerOreNode; // in-run crystal nodes (base nodes no longer exist)
|
|
default: return PointerNone;
|
|
}
|
|
}
|
|
|
|
/// <summary>Ultra-short, verb-first prompt copy with the player's real input glyph (scheme-aware).</summary>
|
|
public static string Prompt(byte step, bool gamepad)
|
|
{
|
|
string move = gamepad ? "Left Stick" : "WASD";
|
|
string build = gamepad ? "Y" : "Tab"; // matches the existing HUD build-discovery chip glyph
|
|
switch (step)
|
|
{
|
|
case Welcome: return "CLEAR 2 EXPEDITIONS to charge the Engine, then survive the FINAL SIEGE. (Space to continue · Esc → How to Play)";
|
|
case Move: return move + " — Move " + (gamepad ? "B" : "LShift") + " — Dash (brief invulnerability)";
|
|
case Build: return build + " — open Build, place a Turret by your Core (your " + ProjectM.Simulation.Tuning.StartingOre + " starting Ore covers it)";
|
|
case Fabricator: return "Build a Fabricator — turrets need Charge (Ore → ammo)";
|
|
case ReadyUp: return "Press T (or click READY UP) — when everyone is ready, the party launches";
|
|
case Rooms: return (gamepad ? "X attack · LT class ability" : "LMB attack · RMB class ability") + " — clear the room; shoot the glowing crystals for your haul";
|
|
case Boon: return "Clear the room — pick 1 of 3 BOONS, then choose your path on the map";
|
|
case Return: return "Fell the ALPHA HUSK, then return home — your haul + the Engine charge (+1) come with you";
|
|
case Defend: return "Defend the Core! — a completed run provokes a retaliation siege";
|
|
case Done: return "You've got it — clear 2 expeditions to fill the Engine, then hold the final siege to win.";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
// ---- persisted-mask helpers (GameSettings.OnboardingMask) ----
|
|
|
|
/// <summary>D4: the early BASE-framing steps (the coach-mark owns the prompt voice, so the HUD blanks its own
|
|
/// location line) vs the combat steps (the HUD MUST keep showing room/siege/out-of-ammo cues). Welcome..ReadyUp
|
|
/// are early. Read each frame by OnboardingSystem to set OnboardingState.SuppressLocationLine.</summary>
|
|
public static bool IsEarlyStep(byte step) => step <= ReadyUp;
|
|
|
|
/// <summary>All steps complete (the sequence is dormant).</summary>
|
|
public static bool AllComplete(int mask)
|
|
{
|
|
int all = (1 << StepCount) - 1;
|
|
return (mask & all) == all;
|
|
}
|
|
|
|
/// <summary>The lowest not-yet-completed step (resume point); <see cref="Done"/> when all are complete.</summary>
|
|
public static byte FirstIncomplete(int mask)
|
|
{
|
|
for (byte i = 0; i < StepCount; i++)
|
|
if ((mask & (1 << i)) == 0) return i;
|
|
return Done;
|
|
}
|
|
}
|
|
}
|