Skip to content

FTUE (Tutorial) Migration — Design

Date: 2026-06-30 · Migration item #7 (FTUE) + #6 (MapLayout loader). Ports the web prototype's scripted 7-stage tutorial (game.js TUTORIAL table, buildTutorialMap, tutorialDriver, per-stage grants, guided loadout swap, #coach prompt) to Unity, web-faithful, with pre-built tutorial map layouts as tunable ScriptableObjects (no hardcode) and a configurable, data-driven step/condition system.

1. Goal & scope

Deliver the full FTUE: a new player starts gearless and is taught one mechanic group per stage across 7 hand-authored fixed-layout stages, accumulating their first loadout, ending at a boss-gated Descent Portal that graduates them into the real procedural raid.

In scope (this build): - Pre-built tutorial layouts as SOsTutorialStageDef (one per stage) + TutorialSet, rect-based (walls/bushes as RectInt; chests/enemies/grounds/extracts/vault as tile-coord lists), no hardcoded map data. All 7 stages authored. - Configurable steps — ordered TutorialStep[] with a data-driven StepCondition (enum + param, anyOf OR-list) for the advance predicate and a WhenCondition for conditional/parked steps. - Tutorial driverTutorialController advances steps, sets action latches, fires hints, gates extraction, applies per-stage system toggles. - Separate tutorial message panel in InGameView (the web #coach prompt), distinct from the announce panel. - Grants + progression — cleared stage grants gear, advances save.tutorial.stage; stage-clear result card. - Guided loadout swap (stage 6→7) — reusable non-caging spotlight + coach line on MetaHud's loadout, "Descend" gated until the Whisper Dagger is equipped. - Parked lessons authored with when-skip — inventory inspect/equip, shield recharge, front-armor weak-point steps included but auto-skip while their config toggle is off (web-faithful).

Non-goals: the real (non-throwaway) loadout/HUD UI; new gameplay systems (in-run inventory, shield, weak-point are parked — their lessons auto-skip); replacing the procedural raid (graduation uses it as-is).

2. Web reference (what to replicate)

  • TUTORIAL[] (game.js:750) — 7 stage defs: name, size{w,h}, spawn{tx,ty}, systems{fog,timer,storm,minimap}, startHurt, walls[]/bushes[] ({x,y,w,h}), grounds[], chests[] ({tx,ty,rarity,loot()}), enemies[] ({kind,tx,ty,facing,hold,scan,scanDir,patrol[], dropsKey,boss,name}), extracts[] ({tx,ty,name,kind,bossFloor}), vault{tx,ty,w,h,door,loot()}, grants[], canExtract(G), steps[] ({text,when?(),done()}), hints[] ({id,when(),text}).
  • buildTutorialMap(def) (game.js:913) — builds the same MapData shape genMap does, deterministically.
  • tutorialDriver(dt) (game.js:1712) — sets action latches (sprinted, dodged, dodgedTelegraph), fires one-shot hints, advances stepIdx past satisfied steps (a step whose when() is false auto-skips), updates the #coach line. Also the stealth-spotted nudge + stealth-state pip (deferred).
  • Stage flow: startRun({kind:"tutorial",stage}) builds the stage; on extract endRun applies grants (to save.loadout else save.stash), save.tutorial.stage++, sets done after stage 7, shows the tutorial result card. Stage 6→7 routes through guidedSwapActive/openGuidedLoadout.
  • The 7 stages (order leads with stealth): 1 First Steps (move/sprint/extract, no enemies, no timer/minimap), 2 Loot & Explore (chest→kit, map), 3 Unseen (stealth/backstab), 4 Heard (noise), 5 Open Combat (fight/dodge/heal), 6 The Vault (key→gate→prize dagger), 7 The Gatekeeper (boss → Descent Portal → graduate).

Faithful details to keep: tutorial mobs are base-HP (a clean backstab one-shots, no raid sponge); held mobs sit at post until the chest-pop noise recruits them or they spot the player; vertical portrait maps (spawn bottom, exit top); the final step of each stage is the free "extract" instruction (Never latches; canExtract gates).

3. Architecture

Assets/Scripts/
  Config/Tutorial/
    TutorialStageDef.cs    SO: one stage's layout + systems + grants + steps + hints
    TutorialSet.cs         SO: ordered list of the 7 stage defs
    TutorialStep.cs        [Serializable]: text + WhenCondition + StepCondition[] anyOf
    StepCondition.cs       [Serializable]: { ConditionType type; string param }  + enums
    TutorialLoot.cs        [Serializable]: gear (ItemDef + rarity/level) | currency (coin/blueprint)
  Domain/
    TutorialConditions.cs  pure: Eval(StepCondition, ctx) -> bool ; Eval(WhenCondition, cfg) -> bool
  World/
    MapGenerator.cs        + BuildFromLayout(TutorialStageDef) : MapData  (reuses BuildChunks)
    MapPopulator.cs        + PopulateTutorial(MapData, TutorialStageDef, player)
    EnemyController.cs      + scripted-AI flags: IsTutorial, Hold, Scan/ScanDir, Patrol[], DropsKey
    TutorialState.cs       per-raid: stage def, StepIdx, action latches, fired hints, CoachText
    TutorialController.cs   DI driver (spawned only in tutorial mode)
    RaidController.cs       gate timer/storm by stage systems; expose ChestsOpened
    Chest.cs / PlayerController.cs : tiny action-latch hooks (chest open, backstab, heal complete)
  UI/InGame/
    InGameView.cs          + tutorial (coach) panel; InGameViewModel + TutorialText/Visible; controller feeds it
  Tester/
    RaidBootstrap.cs       tutorial branch (build stage map + spawn TutorialController) vs procedural raid
    MetaHud.cs             tutorial result card + grants/advance + guided swap (spotlight + Descend gate)
  Infrastructure/
    SaveService.cs         real tutorial save state (stage/done/lobbyDone/firstRaidShown)
Assets/Config/Tutorial/    7 TutorialStageDef assets + 1 TutorialSet + 6 tutorial ItemDef assets

Only Views touch uGUI. TutorialConditions is pure C# (unit-testable). TutorialController is the only new DI seam. Driver/state mirror the MapController/InGameViewController MVC patterns already in use.

3.1 Data layer (SOs, rect-based)

  • TutorialStageDef fields mirror the web entry (§2). Vector2Int/RectInt for tiles; enemy facing in radians (web convention up=-π/2); door enum (S/N/E/W); kind strings match EnemyDatabase.
  • TutorialStep: [TextArea] text, WhenCondition when (default Always), StepCondition[] anyOf (OR semantics; the web's backstabbed || mobsCleared).
  • StepCondition: { ConditionType type; string param }. ConditionType: GoldCollected, Sprinted, Dodged, DodgedTelegraph, Backstabbed, Healed, Shielded, MobsCleared, ChestOpened, HasStarterKit, HasItem, HasKey, GateOpen, BossDown, Inspected, Equipped, Never.
  • WhenCondition: Always, IfInRunInventory, IfWeakpointArmor, IfShieldEnabled — a false when auto-skips the step (parked lessons vanish, reappear when their toggle flips on).
  • TutorialLoot: kind (Gear|Coin|Blueprint), ItemDef gear, LootRarity rarity, int level. Produces a LootItem/GearItem. The 6 tutorial gear pieces are ItemDef assets (Worn Knife, Whisper Dagger, Oak Bow, Leaf Cuirass, Ironwood Vest, Fawn Boots) with web-exact stats/perks.
  • TutorialSet: TutorialStageDef[] stages (the 7). Bound via DI (ProjectInstaller) like BiomeSet.

3.2 Map (fixed layout + authored placement)

  • MapGenerator.BuildFromLayout(def)MapData: border walls, fill walls/bushes rects, carve the vault room + gate (door side), set PlayerSpawn, push extracts into Portals, set Vault; then BuildChunks(map) (reuse). No procedural zones/scatter. Size from def.size (must be ≤ CFG.map cap).
  • MapPopulator.PopulateTutorial(map, def, player): place authored enemies via SpawnEnemy with IsTutorial=true + facing/hold/scan/patrol/dropsKey/boss; tutorial scale = base HP (no sponge, no power-scale); chests with fixed TutorialLoot; grounds loot; the vault prize chest. Bypasses the zone loop. Applies startHurt (spawn the player wounded).
  • EnemyController scripted-AI: a Hold mob stays at its authored post + facing (no wander/scan) until activated (chest-pop noise recruit, or it spots the player); Scan sweeps its cone slowly; Patrol walks a fixed waypoint loop; IsTutorial marks it for MobsCleared/base-HP. Fixed loot on Chest via a FixedLoot list (bypasses the roller).

3.3 Runtime (state + driver + conditions)

  • TutorialState (bound per-raid when in tutorial mode): StageDef, int StageNumber, int StepIdx, latches Sprinted/Dodged/DodgedTelegraph/Backstabbed/Healed/Shielded/Inspected/Equipped, HashSet firedHints, string CoachText, int ChestsOpened.
  • TutorialController (DI MonoBehaviour, spawned only when tutorial.done==false): each frame — set latches from PlayerController/RaidState (Sprinting→Sprinted; Dodging+telegraph-window→ DodgedTelegraph); fire one-shot hints (when true once → show via announce panel); advance StepIdx while the current step is satisfied or when-skipped; write CoachText. Exposes CanExtract from the stage's canExtract condition (gates the portal). Applies per-stage systems to RaidController (timer/storm) and the HUD (minimap).
  • TutorialConditions.Eval(StepCondition, ctx) pure: each enum → a read of TutorialState/RaidState/ MapData/loadout. Eval(WhenCondition, cfg) reads the GameConfig toggles.
  • Action hooks (tiny, localized): backstab crit (PlayerController) → Backstabbed; heal-channel complete (PlayerController) → Healed; Chest open → RaidState.ChestsOpened++. MobsCleared = no IsTutorial enemy alive. HasKey/GateOpen/BossDown/GoldCollected/Spotted read existing RaidState/MapData.

3.4 HUD (message panel + systems toggles)

  • A panel_tutorial coach line added to InGameView (bottom safe-zone, above the action row, its own node — not the announce panel). Flows through MVC: TutorialState.CoachText → controller → InGameViewModel.TutorialText/TutorialVisibleInGameView renders. Hidden outside tutorial.
  • Per-stage systems: timer/storm=falseRaidController skips the countdown + storm escalation; minimap=false (stage 1) → the HUD minimap mount is suppressed. The in-raid objectives (goals) panel is hidden during tutorial (the coach drives guidance).

3.5 Lobby / flow

  • RaidBootstrap branch: !save.tutorial.done → read TutorialSet[stage-1], BuildFromLayout + PopulateTutorial + spawn TutorialController; else the current procedural path. Stage progression = the existing scene-reload-per-run (Continue reloads → reads the incremented stage).
  • MetaHud tutorial result card: on a cleared stage → "Stage N Cleared" + Continue (next stage / after stage 7 → lobby with the raid unlocked) / Retry; gear never lost. On win it applies grants (fill an empty loadout slot, else stash) and save.tutorial.stage++ (+ done after stage 7).
  • Guided swap (after stage 6, before stage 7): guidedSwapActive = tutorial not done, stage == 7, lobbyDone false. Continue routes into the loadout with a reusable non-caging spotlight (a glow on the Whisper Dagger + a coach line); the "Descend" gate blocks entering stage 7 until the dagger is equipped. Built as a small reusable spotlight component so the real loadout UI adopts it later.

3.6 Save

SaveData.tutorial becomes real: { int stage=1; bool done=false; bool lobbyDone; bool firstRaidShown }. New saves start the FTUE (done=false); the migration of existing saves keeps done=true (skip). The DI TutorialSet is the curriculum; the save only tracks the cursor.

4. Data flow

RaidBootstrap (scene load) ──!tutorial.done──▶ MapGenerator.BuildFromLayout(stageDef)
                                               MapPopulator.PopulateTutorial(...)
                                               spawn TutorialController(stageDef)
TutorialController.Update ── sets latches, advances StepIdx ──▶ TutorialState.CoachText
                                                                      │
InGameViewController.BuildSlow ── reads TutorialState ──▶ InGameViewModel.TutorialText
                                                                      │
                                                          InGameView.Render ▶ panel_tutorial
extract (CanExtract gate) ▶ RaidController.EndRaid ▶ MetaHud result card ▶ grants + stage++
stage 6 clear ▶ guided swap (spotlight + Descend gate) ▶ stage 7 ▶ done ▶ procedural raid

5. Validation

Per task: headless Unity_RunCommand — compile; BuildFromLayout produces the expected tiles/spawn/vault/portals for a stage; TutorialConditions.Eval truth-tables (each enum); the 7 stage SOs + ItemDefs load and resolve; the message panel renders a coach line. Pure TutorialConditions gets headless assertions. Play-mode smoke per stage: spawn → a step's condition advances the coach → clear → grant lands → next stage; stage 3 backstab latches; stage 6 key→gate→dagger; stage 6→7 guided swap gates Descend; stage 7 boss → Descent → done → procedural raid.

6. Risks & notes

  • Authored entity placement bypasses the procedural populatorPopulateTutorial is a parallel path; keep Populate (raids) untouched.
  • Tutorial mob HP must be base (no raid sponge/power-scale) or backstab one-shots break.
  • Fixed loot needs a Chest.FixedLoot bypass of the roller; gear from ItemDef assets.
  • Save back-compat: existing saves must not suddenly re-enter the tutorial (migration keeps done=true).
  • Parked lessons rely on when-skip; verify a false when truly auto-completes (never blocks).
  • Scope: large but cohesive — the plan phases it (framework + stage 1 end-to-end, then stages 2–7 as SO authoring, then guided swap + graduation).