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

8.2 KiB
Raw Blame History

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_names 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.