First-run onboarding: contextual coach-marks + How-to-Play card + dev replay toggle
Teaches the deep, interlocking loop — especially the inverted win condition (you win by CLEARING EXPEDITIONS, not by surviving base sieges; DR-042/DR-043). - OnboardingSystem: client-only observe-only PresentationSystemGroup overlay (own UIDocument @ sortingOrder 60), soft-gated 10-beat coach-mark sequence with a world-space ▶ pointer; never mutates sim / never destroys a ghost. - OnboardingStepMath: pure, unit-tested step machine (snapshot + IsSatisfied + scheme-aware prompts + pointer kinds + persisted-mask helpers). - HowToPlayPanel: tabbed reference card (Controls / The Loop / Build / Threats / Win-Lose), reachable from the main menu and the pause overlay. - Per-client client-local state in GameSettings (TutorialHints + OnboardingMask bitmask, additive) — a Join client keeps its own; a host save-wipe never re-teaches. Settings toggle + menu "Replay Tutorial". - Dev "Force Each Launch" toggle (GameSettings.ForceOnboardingEachLaunch): SettingsService.Boot wipes the mask + forces hints on in-memory every launch so the tutorial always replays fresh. - HudSystem suppresses its own location hint while onboarding is active (single prompt voice), via OnboardingState + [UpdateAfter(OnboardingSystem)]. Validated green: 20/20 EditMode; Play smoke confirmed overlay render, clean U+25B6 pointer glyph, no system sort-cycle, and the force-wipe end-to-end. Docs: DR-043 + session log; reusable lesson archived in the build-gotchas note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: First-Run Onboarding — design decision-tree + offline build (Unity GPU-crash session)
|
||||
date: 2026-06-28
|
||||
tags: [session, onboarding, ux, hud, client-only, presentation, dots-dev]
|
||||
permalink: gamevault/07-sessions/2026/2026-06-28-first-run-onboarding
|
||||
---
|
||||
|
||||
# First-run onboarding — session
|
||||
|
||||
`/dots-dev` session on the operator's brief: *"This game needs an onboarding style type of thing, plan something that makes sense."* Full decision: [[DR-043_First_Run_Onboarding]].
|
||||
|
||||
## How it went
|
||||
1. **Ground** — 5-agent read-only fan-out (`wf_670a0cdf-832`) mapped the onboarding-relevant surfaces (HUD/UITK, controls, the macro loop, economy/build, frontend lifecycle) + an exhaustive search confirming **no onboarding/tutorial/help exists anywhere**. (2 of 5 mappers returned degenerate stubs; the 3 working ones triangulated the rest, so no re-run was needed.)
|
||||
2. **Forks** — operator asked to drive it **decision-tree style**. Two rounds of `AskUserQuestion` (3 + 4 forks) locked the 7 design decisions (table in DR-043). A genre-precedent research pass (`wf_f41c8423-68b`, NN/g + CHI-2012 + DRG/Riftbreaker/CotL/Hades/Helldivers/Remnant) backed every recommendation; the operator chose the recommended option on all 7.
|
||||
3. **Build** — mid-session the editor began **crashing randomly**. Diagnosed from `Editor.log`: empty managed stack + faulting `dxgi.dll`, GPU = **RTX 5060 Ti, driver 32.0.16.1062** → a **GPU/driver (TDR) fault, not project code** (recurring `Unity.exe.*.dmp`). Operator chose "peek at the crash, then code." Pivoted to building the whole feature **via the filesystem** (decoupled from the unstable bridge), with a single `refresh_unity force` deferred to when the editor is back.
|
||||
4. **Static review in lieu of a compiler** — 3-lens adversarial review (`wf_d804925a-f7b`) verified every symbol against source: **Lens 1 compile/API = clean PASS**. Fixed 1 major (the v1→v2 migration dropped returning players' graphics settings) + 4 minors (Esc/pause copy + self-skip, pause-freeze for timed beats, static reset on teardown, same-frame HUD suppression ordering).
|
||||
|
||||
## What shipped (code-complete, NOT yet validated)
|
||||
Contextual coach-marks (`OnboardingSystem` — client-only observe-only, own UIDocument @ sortingOrder 60) running the full first lap soft-gated, a world-space `▶` pointer, a tiny welcome strip naming the inverted win goal, a tabbed **How-to-Play** card (menu + pause), a Settings **Tutorial Hints** toggle + **Replay Tutorial**, all per-client via a client-local `GameSettings.OnboardingMask` (v1→v2 additive). Pure logic in `OnboardingStepMath` with `OnboardingStepTests`. **Zero netcode/replication/bake surface.** Files in DR-043.
|
||||
|
||||
## Validation — DONE (2026-06-29, editor stable)
|
||||
**Green.** `refresh_unity force` → console clean → **20/20 EditMode** (incl. 2 new `ForceOnboardingEachLaunch` cases) → Play smoke proved the new dev toggle (seeded a veteran `int.MaxValue` mask + `force=1` → Boot wiped it → tutorial replayed from Welcome), confirmed **no system sort-cycle** from `HudSystem`'s `[UpdateAfter(OnboardingSystem)]`, and verified the `▶` pointer renders as a clean **U+25B6 triangle** (not tofu) + HUD hint suppression. Also shipped this session: a dev **Force Each Launch** onboarding toggle (`GameSettings.ForceOnboardingEachLaunch` + `SettingsService.Boot` per-launch wipe + a Settings cycle row). The deferred CLAUDE.md gotcha was parked in `_Meta/CLAUDE_Build_Gotchas_Archive.md` (file at cap; one-time pattern doesn't earn inline space). Full detail in [[DR-043_First_Run_Onboarding]].
|
||||
|
||||
### Original plan (for the record — now executed)
|
||||
Not compiled/tested/Play-run yet. When the editor is stable: `refresh_unity scope=all mode=force` (the 5 new `.cs` have no `.meta`) → `read_console` clean → `run_tests` EditMode `ProjectM.Tests.EditMode` → Play smoke (welcome+step1 on a fresh client; dormant when hints off / mask full; per-client; no sort-cycle from the new `[UpdateAfter]`) → L3 screenshots (strip / pointer / card; confirm the `▶` glyph renders). Then add the deferred **CLAUDE.md** gotcha line (first-run flags → client-local `GameSettings`, not host `SaveData`) once green.
|
||||
|
||||
## Gotchas worth remembering
|
||||
- **Unity random/idle crashes on this machine = the RTX 5060 Ti driver (`dxgi.dll`), not code.** Fix path: DDU + NVIDIA Studio driver / disable HAGS / `-force-d3d11`. (Native memory: `gpu-crash-dxgi-driver`.)
|
||||
- **When the editor is unstable, write Assets `.cs` via the filesystem + one `refresh_unity force` later** instead of per-edit MCP `create_script` — the bridge dies with the editor; a static adversarial review can stand in for the compiler.
|
||||
- **A version bump on an additive settings/save struct re-activates the migration path for every existing file** — migrate by carrying forward the old value and seeding only new fields, never by rebuilding from `Defaults()` (else you silently reset untouched fields). Caught by the post-impl review.
|
||||
|
||||
## Next-session intent
|
||||
~~Get the editor stable (driver), run the verify ladder for DR-043~~ — **done 2026-06-29** (green). Remaining: the **operator fun-gate** — a real first-run playthrough to feel whether the welcome framing lands, the pointers read, and it stays un-naggy (and to confirm the Welcome→Move→Mine→Build→Fabricator→Gate→Clear→Return→Defend→Done cascade paces well with actual input). The dev **Force Each Launch** toggle (Settings → Onboarding) makes that repeatable.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: DR-043
|
||||
title: First-Run Onboarding — contextual coach-marks + replayable How-to-Play card
|
||||
status: accepted
|
||||
date: 2026-06-28
|
||||
tags:
|
||||
- decision
|
||||
- design
|
||||
- onboarding
|
||||
- ux
|
||||
- hud
|
||||
- client-only
|
||||
- presentation
|
||||
permalink: gamevault/07-sessions/decisions/dr-043-first-run-onboarding
|
||||
---
|
||||
|
||||
# DR-043 — First-Run Onboarding
|
||||
|
||||
> Operator (2026-06-28): *"This game needs an onboarding style type of thing, plan something that makes sense."* The game teaches NOTHING today (only a "Tab/Y — BUILD" discovery chip + a one-line expedition objective readout) yet has a deep, interlocking stack to learn: twin-stick combat → mine/economy → build palette → defend-the-Core sieges → walk-the-gate expeditions → an **inverted win condition** (you win by CLEARING EXPEDITIONS — [[DR-042_Loop_Reshape_Expedition_Driven]] — not by surviving base sieges). Designed via a 7-fork decision-tree (operator-locked) backed by a genre-precedent research pass, then built through the `/dots-dev` ladder.
|
||||
|
||||
## The problem
|
||||
A new player is dropped cold into the full loop with no scaffolding, and the win condition is **counter-intuitive** — base defense *feels* like the goal, so without explicit framing players read it as tower-defense and never discover that expeditions are the win (the "Don't Starve / Hades II — no framing" failure the research flagged). Nothing in the build addresses this.
|
||||
|
||||
## Locked design — 7 forks (decision-tree, operator-chosen)
|
||||
| Fork | Decision |
|
||||
|---|---|
|
||||
| **Style** | Contextual just-in-time coach-marks **+** a replayable How-to-Play reference card (not a guided rail, not a static-only screen) |
|
||||
| **Scope** | The **full first lap** — through one expedition clear AND the retaliation siege (stopping earlier hides the win) |
|
||||
| **Pacing** | **Soft-gated** objectives (a step shows until its action is done; never physically blocks) + auto-suppress |
|
||||
| **Guidance** | Text prompt **+ one world-space pointer** per step (off-screen edge-arrow for navigation; text for conceptual beats) |
|
||||
| **Welcome** | A **tiny non-modal welcome strip** on first spawn (names the inverted goal) → then silent coach-marks |
|
||||
| **Reference card** | **Tabbed** (Controls · The Loop · Build & Economy · Threats · Win/Lose), reachable from **menu + pause** |
|
||||
| **Opt-out** | Settings **toggle** + **auto-suppress** (a taught action already done never fires) + **replayable** from the menu + a dev **Force Each Launch** toggle (wipes the mask every boot — see below) |
|
||||
| **Co-op** | **Per-client**, keyed to each player's own first-encounter; flags in **client-local `GameSettings`**, NOT the host-only `SaveData` |
|
||||
|
||||
Research backing (3-agent genre-precedent pass, run `wf_f41c8423-68b`): NN/g pull-not-push / one-thing-at-a-time / advance-by-doing; CHI-2012 (context-sensitive > forced); shipped analogs Riftbreaker / Cult of the Lamb / DRG / Hades / Helldivers 2 / Remnant 2 all teach **one complete lap then release**, optional+replayable, per-client. The annotated loop diagram is the single highest-value asset (answers the #1 confusion "how do the pillars connect").
|
||||
|
||||
## Architecture — pure client-side presentation, ZERO netcode surface
|
||||
- **`OnboardingSystem`** — client-only observe-only `SystemBase` in `PresentationSystemGroup` (same constraints as `HudSystem`: never mutates sim, never destroys a ghost, reads replicated state once/frame). Owns its own runtime UIDocument (sortingOrder **60** — above HUD 50, below pause 100, root `pickingMode=Ignore`). A bottom-center prompt chip + a world-space `▶` pointer.
|
||||
- **`OnboardingStepMath`** — pure, engine-free step list + `Snapshot` + `IsSatisfied` + prompt copy + pointer kind + mask helpers (the unit-testable core; mirrors the `*Math` discipline).
|
||||
- **`OnboardingState`** — static `Active` flag (HudSystem reads it to blank its own location hint → single prompt voice) + `[RuntimeInitializeOnLoadMethod]` reset-on-play-enter (stale-static rule).
|
||||
- Progress persists per-client in **`GameSettings.OnboardingMask`** (a completed-step bitmask) + `TutorialHints` toggle (v1→v2 **additive**). Client-local JSON — a Join client keeps its own; a save wipe never re-teaches the host.
|
||||
- **Dev replay-each-launch** (`GameSettings.ForceOnboardingEachLaunch`, added 2026-06-29): an additive **0-default** int (no version bump — a v2 file deserializes it to 0/off). When set, `SettingsService.Boot` (a `RuntimeInitializeOnLoadMethod` that runs on **every** editor Play-enter / built-player launch) wipes the mask + forces hints on **in-memory** (not written back — the system re-persists as the player advances, and the next launch wipes again). Surfaced as a "Force Each Launch (Dev)" cycle row in Settings → Onboarding. The menu "Replay Tutorial" remains the one-shot equivalent.
|
||||
|
||||
### The lap (10 beats; soft-gated; scheme-aware glyphs; auto-suppress via absolute counts)
|
||||
Welcome(timed, names the goal) → Move → Mine (attack an Ore node — mining IS combat at the base since Calm has no enemies; pointer→nearest node) → Build a Turret → Fabricator (soft) → Reach the Gate (edge-arrow) → Clear the zone → Return (leave expedition) → Defend the siege (soft) → Done. Each step gated on the **local** player's own first-encounter; count-based steps (Build/Fabricator) test an **absolute** count so a join-client at a built base auto-skips.
|
||||
|
||||
## Wire/bake classification — **LOW blast radius**
|
||||
No `[GhostField]`, no ghost prefab/hash change, **no subscene re-bake**, no RPC, no `GhostRelevancy`, no server-system ordering. Only client-local `GameSettings` v1→v2 (additive JSON). ⇒ no pre-code netcode design review needed; relied on the verify ladder + a post-impl static review instead.
|
||||
|
||||
## Build status — shipped + validated green (built 2026-06-28, validated 2026-06-29)
|
||||
Built **2026-06-28**, written via the **filesystem** (not MCP `create_script`) because the Unity editor was crashing — a **GPU/driver fault** (`dxgi.dll`, RTX 5060 Ti, driver 32.0.16.1062; recurring `Unity.exe.*.dmp` — NOT project code; see the session log + native memory). The feature has **NOT yet been compiled / tested / Play-validated**.
|
||||
|
||||
In lieu of a compiler, a **3-lens adversarial static review** (run `wf_d804925a-f7b`) verified every symbol against the codebase: **Lens 1 (compile/API) = clean PASS** (SystemAPI-in-helpers pattern confirmed valid, the `PlayerInput`/InputSystem CS8377 gotcha respected, asmdef refs + UITK APIs all resolve). Findings fixed:
|
||||
- **[MAJOR, fixed]** the v1→v2 bump activated the lossy `SettingsService.Migrate` (rebuilt from `Defaults()`, dropping returning players' display-mode/quality/v-sync/fps/refresh) → rewrote Migrate to carry forward all old fields and seed only the new ones (Load already Clamps).
|
||||
- **[minor, fixed]** Welcome copy said "Esc: How to Play" but Esc opens Pause, and Esc (via `anyKey`) self-dismissed the framing → reworded to "Esc → Pause → How to Play" + excluded `escapeKey` from the message-beat skip.
|
||||
- **[minor, fixed]** timed beats advanced behind the pause overlay → freeze accumulation/eval on `PauseMenuController.Open`.
|
||||
- **[minor, fixed]** `OnboardingState.Active` not reset on system teardown → reset in `OnDestroy`; HudSystem now `[UpdateAfter(OnboardingSystem)]` for same-frame suppression.
|
||||
- **[L3 watch]** the `▶` pointer glyph's font coverage — verify it renders (swap to a font-independent shape if it's a missing-glyph box).
|
||||
|
||||
**Validation (2026-06-29, editor stable):** `refresh_unity scope=all mode=force` imported the 5 new `.cs` + recompiled → `read_console` clean (1 unrelated AI-assistant warning) → **20/20 EditMode pass** (`OnboardingStepTests` + `OnboardingSettingsTests`, incl. 2 new `ForceOnboardingEachLaunch` cases). Play smoke: seeded a **veteran** mask (`int.MaxValue`) + `force=1` → Boot wiped it → the overlay replayed from Welcome (live `mask` observed back at 0, then 1 as Welcome auto-completed) — proving the dev toggle AND that `HudSystem`'s new `[UpdateAfter(OnboardingSystem)]` introduces **no system sort-cycle** (world booted clean). Captured the Move + Build + Gate prompt chips and the `▶` pointer — the glyph renders as a **clean U+25B6 triangle, not a tofu box** (the flagged L3 watch), and the HUD location hint is correctly suppressed during onboarding. (The `▶` step was forced via reflection on the live `OnboardingSystem` since input can't be injected in an MCP smoke.)
|
||||
|
||||
## Files
|
||||
**New:** `Client/Onboarding/{OnboardingState, OnboardingStepMath, OnboardingSystem}.cs`, `Client/UI/HowToPlayPanel.cs`, `Tests/EditMode/OnboardingStepTests.cs`.
|
||||
**Edited:** `Client/Settings/{GameSettings, SettingsService}.cs`, `Client/UI/{MainMenuController, PauseMenuController, SettingsScreen}.cs`, `Client/Presentation/HudSystem.cs`.
|
||||
|
||||
## Consequences / open
|
||||
- **CLAUDE.md gotcha — parked in the archive, NOT inlined** (resolved 2026-06-29): the reusable lesson (*first-run / has-played / per-player UI flags → client-local `GameSettings`, never host `SaveData`* + the client-onboarding-overlay pattern + the dev force-each-launch wipe) is captured under a dated heading in `_Meta/CLAUDE_Build_Gotchas_Archive.md`. It stayed out of CLAUDE.md by design: the file is at **40884/40960 B** (76 B headroom) and this is a one-time client-overlay pattern, not a high-recurrence hazard, so by CLAUDE.md's own "hottest rules only" budget rule it doesn't earn inline space — evicting a hot rule to make room for a cold one would be a net loss.
|
||||
- Tuning knobs (live): `OnboardingStepMath.{WelcomeSeconds, MoveThreshold, FabricatorSoftSeconds, DefendNoSiegeSeconds, DoneSeconds}`.
|
||||
- Deferred (not built): contextual `?` deep-link from a coach-mark to the card page; target/HUD highlights (operator chose pointers only); a structured guided-tutorial variant.
|
||||
- See [[2026-06-28_First_Run_Onboarding]] · [[DR-042_Loop_Reshape_Expedition_Driven]] · [[DR-038_Slice1_Combat_Readability_HUD_Declutter]] (the discovery-chip/build-mode precedent) · [[DR-019_Frontend_Menu_Settings_Saves_Build]] (settings/save lifecycle).
|
||||
@@ -384,3 +384,14 @@ Added the **EB-2 felt spend ★** bullet ([[DR-033_EB2_Felt_Spend_Charge_Economy
|
||||
- **Resource-gated ability tiers / buffs ([[DR-026_Inventory_Equipment_Progression_Foundation]]):** dropped the "(replace/clear-by-SourceId → bounded buffer; folds into … both worlds)" mechanism phrasing → kept "reuse `StatModifier` … `StatRecomputeSystem`→`EffectiveAbilityStats`". (Note: this bullet's `GoalProgress.Charge` is the **goal-meter** charge, unrelated to EB-2's `ResourceId.Charge` turret ammo.)
|
||||
- **PlacedStructure ([[DR-014_M6_Build_Structures_Automation_Foundation]]):** dropped the "(turret reuses `NextTick` as fire cooldown; they're the offline-catch-up linchpin)" + "Data-driven `StructureCatalog` buffer" asides → kept the field layout + the bake-the-tick-fields rule + the DERIVED-occupancy rule.
|
||||
- **M7 Automation ([[DR-020_M7_Automation_Production_Chains]]):** dropped the "server-only `MachineInput`/`MachineOutput`" restatement and noted `Fabricator` is now LIVE on the palette via EB-2 while `Harvester`/`Conveyor` stay trimmed (code intact).
|
||||
|
||||
## 2026-06-29 — First-run onboarding validated (DR-043); CLAUDE.md line kept archive-only (file at cap)
|
||||
|
||||
DR-043's first-run onboarding shipped + Play-validated green (20/20 EditMode; live Play: a "veteran" full mask wiped by the dev toggle → tutorial replayed from Welcome, the `▶` pointer = a clean U+25B6 triangle (not tofu), no system sort-cycle from `HudSystem`'s new `[UpdateAfter(OnboardingSystem)]`). The deferred CLAUDE.md gotcha is parked HERE, not inline — the file sits at 40884/40960 B (76 B headroom) and the lesson is a one-time client-overlay pattern, not a high-recurrence hazard, so by CLAUDE.md's own "hottest rules only" rule it doesn't earn inline space. The lesson:
|
||||
|
||||
- **First-run / "has-played" / per-player UI flags belong in client-local `GameSettings` (settings.json), NEVER the host-only `SaveData`** — in co-op a Join client never sees the host save, and a host save-wipe must not re-teach. Progress = a completed-step **bitmask** in settings. Additive field, **0-default → no version bump** (a missing field deserializes to 0 = the safe off/fresh default; bumping `CurrentVersion` instead re-activates the migration path for every existing file — see the 2026-06-28 migration regression below/in DR-043).
|
||||
- **Onboarding overlay = a client-only observe-only `PresentationSystemGroup` `SystemBase`** (same constraints as `HudSystem`: never mutates sim, never destroys a ghost, reads replicated state once/frame) owning its own runtime `UIDocument` (sortingOrder 60 — above HUD 50, below pause 100, root `pickingMode=Ignore`). A static `OnboardingState.Active` (reset on `SubsystemRegistration`) lets `HudSystem` (`[UpdateAfter(OnboardingSystem)]`) blank its own location hint → a single prompt voice. Auto-suppress for veterans/co-op falls out of **ABSOLUTE count checks** (turret/fabricator count ≥1 satisfies on entry at an already-built base).
|
||||
- **Dev "Force Each Launch" toggle:** `GameSettings.ForceOnboardingEachLaunch` → `SettingsService.Boot` (a `RuntimeInitializeOnLoadMethod` that runs each editor Play-enter / built-player launch) wipes the mask + forces hints on **IN-MEMORY** (NOT written back; the system re-persists progress as the player advances, and the next launch wipes again). Validated by seeding `OnboardingMask=int.MaxValue` (veteran) + `force=1` then Play → mask observed back at 0/fresh.
|
||||
- **Forcing a specific step in a live Play smoke without input:** the step machine's `_step`/`_mask`/`_stepInit` are private — set them via reflection on `world.GetExistingSystemManaged(typeof(OnboardingSystem))`; remember an MCP `screenshot` can leave the editor **paused** (`EditorApplication.isPaused`), so the field write won't surface until you unpause. Pause + reflection-restyle the `_pointer` Label is also how to get a clean glyph capture (OnUpdate would otherwise overwrite the style next frame).
|
||||
|
||||
Net-zero: archive-only add (no CLAUDE.md bytes changed), so no inline condensation needed.
|
||||
|
||||
Reference in New Issue
Block a user