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 SOs — TutorialStageDef (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 driver — TutorialController 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 shapegenMapdoes, deterministically.tutorialDriver(dt)(game.js:1712) — sets action latches (sprinted,dodged,dodgedTelegraph), fires one-shot hints, advancesstepIdxpast satisfied steps (a step whosewhen()is false auto-skips), updates the#coachline. Also the stealth-spotted nudge + stealth-state pip (deferred).- Stage flow:
startRun({kind:"tutorial",stage})builds the stage; on extractendRunappliesgrants(tosave.loadoutelsesave.stash),save.tutorial.stage++, setsdoneafter stage 7, shows the tutorial result card. Stage 6→7 routes throughguidedSwapActive/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)¶
TutorialStageDeffields mirror the web entry (§2).Vector2Int/RectIntfor tiles; enemyfacingin radians (web convention up=-π/2);doorenum (S/N/E/W);kindstrings matchEnemyDatabase.TutorialStep:[TextArea] text,WhenCondition when(defaultAlways),StepCondition[] anyOf(OR semantics; the web'sbackstabbed || 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 falsewhenauto-skips the step (parked lessons vanish, reappear when their toggle flips on).TutorialLoot:kind (Gear|Coin|Blueprint),ItemDef gear,LootRarity rarity,int level. Produces aLootItem/GearItem. The 6 tutorial gear pieces areItemDefassets (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) likeBiomeSet.
3.2 Map (fixed layout + authored placement)¶
MapGenerator.BuildFromLayout(def)→MapData: border walls, fillwalls/bushesrects, carve thevaultroom + gate (door side), setPlayerSpawn, pushextractsintoPortals, setVault; thenBuildChunks(map)(reuse). No procedural zones/scatter. Size fromdef.size(must be ≤CFG.mapcap).MapPopulator.PopulateTutorial(map, def, player): place authoredenemiesviaSpawnEnemywithIsTutorial=true+ facing/hold/scan/patrol/dropsKey/boss; tutorial scale = base HP (no sponge, no power-scale);chestswith fixedTutorialLoot;groundsloot; the vault prize chest. Bypasses the zone loop. AppliesstartHurt(spawn the player wounded).EnemyControllerscripted-AI: aHoldmob stays at its authored post + facing (no wander/scan) until activated (chest-pop noise recruit, or it spots the player);Scansweeps its cone slowly;Patrolwalks a fixed waypoint loop;IsTutorialmarks it forMobsCleared/base-HP. Fixed loot onChestvia aFixedLootlist (bypasses the roller).
3.3 Runtime (state + driver + conditions)¶
TutorialState(bound per-raid when in tutorial mode):StageDef,int StageNumber,int StepIdx, latchesSprinted/Dodged/DodgedTelegraph/Backstabbed/Healed/Shielded/Inspected/Equipped,HashSet firedHints,string CoachText,int ChestsOpened.TutorialController(DI MonoBehaviour, spawned only whentutorial.done==false): each frame — set latches fromPlayerController/RaidState(Sprinting→Sprinted;Dodging+telegraph-window→DodgedTelegraph); fire one-shot hints (whentrue once → show via announce panel); advanceStepIdxwhile the current step is satisfied orwhen-skipped; writeCoachText. ExposesCanExtractfrom the stage'scanExtractcondition (gates the portal). Applies per-stagesystemstoRaidController(timer/storm) and the HUD (minimap).TutorialConditions.Eval(StepCondition, ctx)pure: each enum → a read ofTutorialState/RaidState/MapData/loadout.Eval(WhenCondition, cfg)reads theGameConfigtoggles.- Action hooks (tiny, localized): backstab crit (
PlayerController) →Backstabbed; heal-channel complete (PlayerController) →Healed;Chestopen →RaidState.ChestsOpened++.MobsCleared= noIsTutorialenemy alive.HasKey/GateOpen/BossDown/GoldCollected/Spottedread existingRaidState/MapData.
3.4 HUD (message panel + systems toggles)¶
- A
panel_tutorialcoach line added toInGameView(bottom safe-zone, above the action row, its own node — not the announce panel). Flows through MVC:TutorialState.CoachText→ controller →InGameViewModel.TutorialText/TutorialVisible→InGameViewrenders. Hidden outside tutorial. - Per-stage
systems:timer/storm=false→RaidControllerskips 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¶
RaidBootstrapbranch:!save.tutorial.done→ readTutorialSet[stage-1],BuildFromLayout+PopulateTutorial+ spawnTutorialController; else the current procedural path. Stage progression = the existing scene-reload-per-run (Continue reloads → reads the incremented stage).MetaHudtutorial 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 appliesgrants(fill an empty loadout slot, else stash) andsave.tutorial.stage++(+doneafter stage 7).- Guided swap (after stage 6, before stage 7):
guidedSwapActive= tutorial not done, stage == 7,lobbyDonefalse. 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 populator —
PopulateTutorialis a parallel path; keepPopulate(raids) untouched. - Tutorial mob HP must be base (no raid sponge/power-scale) or backstab one-shots break.
- Fixed loot needs a
Chest.FixedLootbypass of the roller; gear fromItemDefassets. - Save back-compat: existing saves must not suddenly re-enter the tutorial (migration keeps
done=true). - Parked lessons rely on
when-skip; verify a falsewhentruly 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).