diff --git a/CLAUDE.md b/CLAUDE.md index 8f7a753..91c208e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -494,7 +494,13 @@ quest log, shop, pause/settings, dialogue, combat HUD, main exploration window) plus a `README.md` handoff. **Not production code** — each screen is recreated as a **Godot 4.7 Control-node scene** pulling from a shared **`Theme`** resource (the README's palette, three fonts, and reusable styleboxes keep the look one -system). Each `.dc.html`'s `Component` class is the **behavior spec** — the +system). **Author these scenes editor-first:** the layout lives in the `.tscn` +as real Control nodes with the `Theme` and type-variations set in the editor, so +the scene *previews* and can be arranged in the Godot editor — do **not** +build the node tree in `_ready()`. Scripts hold only on-load work: `@onready` +node refs, data/DM wiring, animations, and binding state into fixed authored +nodes (see ADR [0001](docs/adr/0001-editor-first-ui-scenes.md)). Each +`.dc.html`'s `Component` class is the **behavior spec** — the combat turn manager, dialogue graph, shop economy, and point-buy creation are the highest-value logic to port; the seed-data maps (items, quests, locations, units, abilities) are starting `Resource`s. Dashed "art slots" are placeholders for real diff --git a/client/docs/README.md b/client/docs/README.md index 597233b..feb472f 100644 --- a/client/docs/README.md +++ b/client/docs/README.md @@ -12,6 +12,49 @@ Put here: Cross-cutting design (anything touching both client and api) goes in the root [`/docs`](../../docs), not here. +## UI scene conventions (shell + all screens) + +Screens are **authored editor-first** — the layout lives in the `.tscn`, not in +code. See ADR [0001](../../docs/adr/0001-editor-first-ui-scenes.md) for the why; +this is the how. Reference: `scenes/shell/{MainWindowShell,SystemDock,NarrationBook}.tscn` ++ `scripts/ui/shell/*.gd`. + +- **The `.tscn` owns the tree.** Author every node in the editor: containers, + panels, labels, buttons, art-slot placeholders. Assign the shared theme on the + scene root (`theme = game_theme.tres`) and set each node's `theme_type_variation` + (from `ThemeKeys`) in the editor, so the scene previews themed. Instance + sub-screens (`PackedScene` ext_resource) rather than rebuilding them. +- **The script does on-load work only.** `@onready var x := $Path/To/Node` refs, + signal/service wiring, animations, and binding state into the authored nodes + (label text, colours from `Palette`). Do **not** create nodes in `_ready()`. +- **Data-driven repeats = fixed authored slots, bound at runtime.** Author enough + turn-order tokens / consumable slots / etc. in the editor; the script fills the + first N from state and hides the rest. Previewable now; systems write them later. +- **Scene-root sizing.** Give the root a fixed size via offsets (e.g. 1920×1080) + for editor preview, but **no full-rect anchors on the root** — a root Control run + via F6 collapses to (0,0) under full-rect anchors. Keep `size = get_viewport_rect().size` + on `_ready` + a `size_changed` connection (the M3-a collapse lesson). +- **Button styling → Button-base variations only.** Never put a PanelContainer + variation (`DarkPanel`, `ParchmentCard`) on a `Button`; it has no `normal/hover/ + pressed` styleboxes so the button renders unskinned. Use the Button-base + variations (`PrimaryCTA`, `Tab`, `TabActive`, `Chip`, `DockButton`, `ParchmentButton`). +- **`RichTextLabel` prose** reads its dark serif ink from the theme's base + `RichTextLabel` style (it does *not* inherit `default_font`/`default_color`). + Just add the `RichTextLabel` — don't override its colour per-node. +- **Theme is generated.** All colours live in `palette.gd`; `game_theme.tres` is + built by `scripts/theme/build_game_theme.gd` (source of truth). To add/adjust a + variation, edit the builder + `theme_keys.gd`, then regenerate: + `godot --headless -s res://scripts/theme/build_game_theme.gd`. Never hand-edit + the `.tres` or put a hex literal in a script or `.tscn`. +- **Wiring the DM loop / injectable services.** Follow the shell: a `service` + var settable *between* `instantiate()` and `add_child()` lets tests inject a + `FakeTransport`-backed `DmService` so on-load calls stay hermetic; `_ready` + builds the real `HttpTransport`-backed one when none was injected. +- **Visual gate.** Headless GUT proves structure (nodes mount, sizes non-zero, + bindings run); it cannot prove rendering. Every screen needs a **human F6 + eyeball** before it's done. Note shader surfaces (`DarkBay`, vignette) render + their texture only on an F6 run, not in the editor. + ## Narrate live smoke (M2 client HTTP loop) The `narrate_harness` scene is a throwaway proof of the client → `/dm/narrate` diff --git a/docs/adr/0001-editor-first-ui-scenes.md b/docs/adr/0001-editor-first-ui-scenes.md new file mode 100644 index 0000000..6755ddc --- /dev/null +++ b/docs/adr/0001-editor-first-ui-scenes.md @@ -0,0 +1,86 @@ +# 0001 — UI screens are editor-authored scenes, not code-built + +- **Status:** accepted +- **Date:** 2026-07-11 (M3-b, Main Window shell) + +## Context + +The M3-b Main Window shell was first built by constructing every Control node in +`_ready()` (a pattern borrowed from `theme_showcase.gd`, where it was fine because +the showcase is a throwaway proof). For a real screen this was wrong: + +- **It does not preview in the Godot editor.** Opening the `.tscn` showed a bare + root Control — every widget only existed at runtime — so the human could not see + or arrange the layout in the editor. Tuning meant editing pixel offsets in code, + running, eyeballing, repeating. +- It buried layout, styling, and content in GDScript, where it is verbose and + hard to reason about, and it drifted from charter §16's own words: *"each screen + is recreated as a Godot 4.7 Control-node scene."* + +The human asked to be able to arrange the scene in the editor "as much as +possible," with code only for genuine on-load work. + +## Decision + +**UI screens (and their sub-scenes) are authored editor-first as `.tscn` node +trees.** The split: + +- **In the `.tscn` (editor):** the whole node tree — containers, panels, labels, + buttons, art-slot placeholders — with the shared `Theme` assigned on the scene + root and every `theme_type_variation` set on its node. Authored placeholder + text/values so the screen previews with representative content. Sub-screens are + **instanced** (`ExtResource` PackedScene), not rebuilt. +- **In the script (on-load only):** `@onready` references to named nodes; wiring + (signals, the DM loop, injected services); animations (e.g. the dock slide); + and **binding state into the authored nodes** (setting label text/colours from + the model). Never construct the tree in `_ready()`. +- **Data-driven repeats** (turn-order tokens, consumable slots, later inventory + grids) are a **fixed set of authored nodes** bound from state at runtime — + spare slots hide. This keeps them previewable; combat/inventory become the + writers later. (Author enough slots for the common case; if a screen ever needs + a genuinely unbounded list, revisit with a code-instanced container then.) + +Supporting conventions established alongside this decision: + +- **Scene root sizing.** Give the `.tscn` root a fixed size (e.g. 1920×1080) via + offsets so it previews, but **no full-rect anchors on the root** — a root + Control run via F6 is not sized by full-rect anchors and collapses to (0,0). + The script keeps `size = get_viewport_rect().size` on `_ready` + + `size_changed` (the M3-a lesson). +- **Button styling uses Button-base theme variations.** Never apply a + PanelContainer-base variation (`DarkPanel`, `ParchmentCard`) to a `Button` — a + Button looks up `normal/hover/pressed`, which a `panel`-only variation lacks, so + it renders unskinned. M3-b added Button-base `DockButton` and `ParchmentButton` + to the theme for exactly this. +- **RichTextLabel prose reads its style from the theme.** `RichTextLabel` does + **not** inherit `default_font`/`default_color`, so without a theme entry its + text renders near-white (unreadable on parchment). The theme now sets a base + `RichTextLabel` style (dark `INK_BODY` serif + italic face). All colours still + come from `Palette` via the theme builder — no hex in scripts or `.tscn`. + +## Consequences + +**Easier:** + +- The human arranges any screen directly in the Godot editor; layout persists in + the `.tscn`. Fine-tuning is drag-and-drop, not code-run-eyeball. +- Screens preview with themed, representative content on open. +- Scripts shrink to logic; less code, easier to review, focused files. +- The look stays one system (theme assigned per scene root). + +**Caveats / harder:** + +- **Shader surfaces render only at runtime.** `DarkBay`/`VignetteOverlay` set + their `ShaderMaterial` in `_ready()` (M3-a accepted deviation), so their + texture appears only on an F6 run — the editor shows layout but a plain panel + behind it. Layout still previews fine. +- Tests reference nodes by their authored `@onready` paths; renaming/moving a + node in the editor means updating the path (and any test that reads it). +- The visual gate stays a **human F6 eyeball** — headless tests prove structure + (nodes mount, sizes are non-zero, bindings run) but not rendering. + +## Reference implementation + +M3-b shell scenes: `client/scenes/shell/{MainWindowShell,SystemDock,NarrationBook}.tscn` +with their `client/scripts/ui/shell/*.gd` scripts. Practical how-to lives in +[`client/docs/README.md`](../../client/docs/README.md) → "UI scene conventions." diff --git a/docs/adr/README.md b/docs/adr/README.md index 2f31b60..b183a2b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -4,7 +4,7 @@ One file per decision: `NNNN-short-title.md`. Record the context, the decision, ## Index -- _none yet_ +- [0001 — UI screens are editor-authored scenes, not code-built](0001-editor-first-ui-scenes.md) (accepted)