From cc365c8b73989ec91f62c4f8274165a450d38597 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Sat, 11 Jul 2026 08:35:02 -0500 Subject: [PATCH] docs(spec): M3-b Main Window shell (2a) design The exploration HUD frame: two-panel tabletop split, DM narration book wired to the proven /dm/narrate loop (prose-only + refire), thin tested ShellState for HUD readouts, inert system dock + command bar as faithful placeholder. Pure-core/thin-shim per the house pattern. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-11-main-window-shell-design.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-11-main-window-shell-design.md diff --git a/docs/superpowers/specs/2026-07-11-main-window-shell-design.md b/docs/superpowers/specs/2026-07-11-main-window-shell-design.md new file mode 100644 index 0000000..67195f1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-main-window-shell-design.md @@ -0,0 +1,96 @@ +# Main Window shell (2a) — design + +**Milestone:** M3-b (roadmap "Main Window shell (2a)"). +**Charter:** §2 (code owns state, AI owns text), §16 (mockups are the UI bible), §13 (degraded DM, never an error), §11 (canon log). +**Mockup:** `mockups/Main Window + Wireframes.dc.html`, option **2a** (the four low-fi wireframes below it are ignored). +**Branch:** `feature/main-window-shell` off `dev`. + +## 1. Goal + +The exploration HUD — the frame every other screen lives in. A two-panel "tabletop split": an isometric world view (art slot) on the left, a permanent DM "narration book" on the right wired to the **proven** `/dm/narrate` loop. Around the world view: a turn-order rail, a minimap slot, a slide-in system dock, and a bottom command bar (vitals/gold, quick consumables, End Turn). + +This milestone builds the **frame** and its **one live seam** (the DM narration panel). Everything else is faithful placeholder until the milestones that give it meaning (combat, inventory, the individual screens). + +## 2. Scope decisions + +Settled during brainstorming: + +- **DM wire = prose only, with a refire control.** Narrate fires once on scene load to fill the book (considering-state + authored fallback + fact-harvest). One in-world refire affordance (`▸ The Master continues…`) refires it. The numbered response choices and the "describe your own action" free-text line are rendered faithfully but **inert** — there is no action system yet (combat/dialogue are later). +- **HUD readouts come from a thin, tested `ShellState` model**, not hardcoded scene values. Seed values now; later systems (combat, inventory) become its writers. This is the §2 state seam and the testable core. +- **System dock buttons are inert.** They emit `screen_requested(id)` (the routing seam) which is a no-op this milestone; the individual screens land later. +- **Dock slide animation is live** — it is the dock's visual identity and cheap. +- **Keybind navigation (I/C/J/K/M/P) is deferred** — a shortcut belongs with the screen it opens. +- **Dock has 6 items** (Inventory / Character / Quest Log / Spellbook / World Map / Party) — faithful to mock 2a. The brief listed 5 (no Spellbook); kept all 6 since the buttons are inert and it costs nothing. Flagged as a reconcile point — Spellbook is not one of the 11 mock screens. + +## 3. Architecture + +House pattern: **pure, tested cores behind thin scene shims** — the same shape as `DmService`, `ConsideringPhrases`, `MoveValidator` (pure logic unit-tested; the scene/tree wiring is the untested shim, eyeball-gated). Units, each with one purpose: + +| Unit | Kind | §2 | Purpose | +|---|---|---|---| +| `ShellState` | `RefCounted`, **tested** | state | The HUD data the shell owns: `vitals {hp, hp_max, mp, mp_max, gold}`, `turn_order[]`, `consumables[]`, `round_label`, `location_label`, `dock_open`. Getters + `toggle_dock()`. Seeded now; later systems write it. | +| `TurnEntry` | `RefCounted` (or a typed dict inside `ShellState`), **tested** | state | One turn-order combatant: `initials`, `initiative`, `is_active`, `is_downed`, `side` (`you`/`ally`/`enemy`). | +| `SystemDock` | Control scene + script, thin shim | state | The 6 toggle buttons + gold handle. Slide+opacity `Tween`, arrow flip. Emits `screen_requested(id: StringName)` (inert). Toggle drives `ShellState.dock_open`. | +| `NarrationBook` | Control scene + script, thin shim | text | Right parchment panel. DM header (round · location), prose `RichTextLabel`, placeholder response-choices, inert free-text line, one refire affordance. Consumes a `NarrateResult`; injectable service seam so `FakeTransport` drives it in tests. | +| `MainWindowShell` | Control scene root, thin shim | — | The two-panel split (1180 world / 740 book). Builds the world side inline (DarkBay iso slot + caption, turn rail from state, minimap slot, mounts `SystemDock`, command bar). Constructs `DmService` + `ConsideringIndicator` and owns the narrate loop. Handles viewport-fit. | + +### Reused, not rebuilt + +- **Theme:** `client/assets/theme/game_theme.tres`, type-variation names from `ThemeKeys`, colours from `Palette`. Surfaces `ParchmentPanel` / `DarkBay` / `VignetteOverlay` (`client/scenes/theme/surfaces/`). +- **DM loop:** `DmService.narrate(canon_log) -> NarrateResult{display_text, facts, degraded}`, via `HttpTransport(HTTPRequest)` + `FallbackLibrary`. `DmTransport` is the injectable seam; `FakeTransport` (test double) already exists. +- **Considering-state:** `ConsideringIndicator.start(label)` / `.stop()` rotates authored phrases at 3.0s. +- **Seed canon log:** hand-built like `narrate_harness.gd`'s `_build_scene_log()` (`CanonLog` + `LogPlayer` + `PartyMember` + `Quest` + facts/humiliations), until character-creation (M4) and save/load (M9) construct the real one. + +### The viewport-fit gotcha (from M3-a) + +A root `Control` run directly (F6) is **not** reliably sized by full-rect anchors — it collapses to `(0,0)` and clips everything. `MainWindowShell._ready()` must set `size = get_viewport_rect().size` explicitly (no full-rect anchors on the root) and reconnect on `get_viewport().size_changed`, exactly as `theme_showcase.gd` does. The scene is authored at 1920×1080; display is already `canvas_items` stretch @ 1920×1080, window override 1600×900. + +## 4. Data flow + +**Load (`MainWindowShell._ready()`):** +1. `theme = load(game_theme.tres)`; fit to viewport; connect `size_changed`. +2. Build seed `ShellState` and seed `CanonLog`. +3. Build the world side (from `ShellState`) and mount `NarrationBook` + `SystemDock`. +4. Construct `DmService(HttpTransport(HTTPRequest), FallbackLibrary)` and `ConsideringIndicator`. +5. Fire the initial narrate (below). + +**Narrate / refire:** +1. Disable the refire affordance. +2. `indicator.start(status_label)`. +3. `var r := await service.narrate(seed_log)`. +4. `indicator.stop()`. +5. `book.show_prose(r)` — set the `RichTextLabel`, surface `r.degraded` subtly. +6. `for f in r.facts: seed_log.add_fact(f)` (§11 — the caller applies the state write, explicit). +7. Re-enable the affordance. + +Any non-200 → `DmService` already returns the authored fallback (§13); the 35s `HttpTransport` timeout bounds a hang. **No new failure surface.** + +**Dock toggle:** handle pressed → `ShellState.toggle_dock()` → `SystemDock` Tweens slide/opacity and flips the handle arrow. + +## 5. §2 ledger + +- `ShellState`, `dock_open`, HUD numbers, turn order, consumable slots — **state**, client-owned. +- Narration prose — **text**, consumed from `/dm/narrate`, never a source of truth; only harvested `[FACT]` tags become state (§11). +- `screen_requested` routing, response choices, free-text line — inert seams; no state written, no text consumed, this milestone. + +Nothing in the shell lets an AI response set persistent state directly. + +## 6. Testing + +TDD via GUT (`cd client && ./run_tests.sh`; `.gutconfig` promotes engine errors to failures — load defensively). New `class_name`s need `godot --headless --import` before GUT sees them. + +- **`ShellState`** — seed shape; `toggle_dock()` flips `dock_open`; turn-order and consumable entries have the expected fields/values. +- **`TurnEntry`** — field round-trip; `side`/`is_active`/`is_downed` flags. +- **`SystemDock`** — emits `screen_requested` with the correct id per button; toggling updates state (signal + state assertions, no render needed). +- **`NarrationBook`** — given a `NarrateResult`, sets the prose text and surfaces `degraded`; the refire affordance invokes the injected service (drive with `FakeTransport`, assert prose updates + facts flow out). +- **`MainWindowShell`** — node-presence smoke (both panels + dock mount); viewport-fit yields non-zero `size`. + +**Visual gate (required):** a human F6 run of the shell scene. Headless tests cannot prove rendering — this catches layout/collapse regressions the unit tests can't (the M3-a lesson). + +## 7. Out of scope (this milestone) + +- Any of the individual screens the dock opens (Inventory, Character, Quest, Map, Party, Spellbook) — inert buttons only. +- A real action system behind the response choices / free-text line — combat (M5) and dialogue (M6). +- Real HUD writers (combat vitals, inventory consumables) — seed values only. +- Keybind navigation, real isometric render, real minimap, real portraits — art slots / later. +- Save/load of shell state — M9.