Files
code_of_conquest_dnd/docs/superpowers/specs/2026-07-11-main-window-shell-design.md
Phillip Tarrant cc365c8b73 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) <noreply@anthropic.com>
2026-07-11 08:35:02 -05:00

97 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.