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>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user