docs: record editor-first UI-scene convention (ADR 0001)
Capture the direction change from M3-b: UI screens are authored editor-first as .tscn node trees (previewable/arrangeable in the Godot editor), scripts do on-load work only — never build the tree in _ready(). Add ADR 0001, a "UI scene conventions" how-to in client/docs, a §16 mandate in the charter, and mark M3-a/M3-b done in the roadmap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
86
docs/adr/0001-editor-first-ui-scenes.md
Normal file
86
docs/adr/0001-editor-first-ui-scenes.md
Normal file
@@ -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."
|
||||
@@ -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)
|
||||
|
||||
<!-- Template:
|
||||
## NNNN — Title
|
||||
|
||||
@@ -55,9 +55,9 @@ M0–M2 built the **engine** (the contract, the client, the server loop, bounded
|
||||
Each screen is recreated as a Godot 4.7 Control-node scene against the mockups (§16); each `.dc.html`'s `Component` class is the behavior spec. Sequenced toward a playable vertical slice (shell → creation → combat → dialogue) first, then breadth.
|
||||
|
||||
### M3 — Visual foundation & shell ◀ *here*
|
||||
- ○ **Shared `Theme`** — palette tokens, the three font families, and reusable styleboxes (parchment card, dark panel, primary CTA, tab, item tile, tag/chip) from the mock README as a Godot `Theme` resource. Every screen pulls from it; build it first so the look is one system. *§2: n/a (presentation) · depends on nothing · goal: one consistent visual identity.*
|
||||
- ○ **Main Window shell (2a)** — the exploration HUD: isometric world-view slot + the permanent DM "narration book" (wired to the proven `/dm/narrate` loop + considering-state), the turn-order rail, minimap slot, the slide-in system dock (Inventory/Character/Quest/Map/Party toggles), and the bottom command bar (HP/MP/gold, consumables, End Turn). The frame everything else lives in. *§2: state (client owns the HUD, consumes DM text) · depends on the DM loop (M2) · goal: the game's main screen, on screen.*
|
||||
- ○ **Title screen** — new game / continue / load / settings; the entry point. *§2: n/a · depends on the Theme · goal: first impression, into the world.*
|
||||
- ✅ **Shared `Theme`** — palette tokens, the three font families, and reusable styleboxes (parchment card, dark panel, primary CTA, tab, item tile, tag/chip) from the mock README as a Godot `Theme` resource. Every screen pulls from it; build it first so the look is one system. Merged to `dev`. *§2: n/a (presentation) · depends on nothing · goal: one consistent visual identity.*
|
||||
- ✅ **Main Window shell (2a)** — the exploration HUD: isometric world-view slot + the permanent DM "narration book" (wired to the proven `/dm/narrate` loop + considering-state), the turn-order rail, minimap slot, the slide-in system dock (Inventory/Character/Quest/Map/Party/Spellbook toggles), and the bottom command bar (HP/MP/gold, consumables, End Turn). The frame everything else lives in. **Established the editor-first UI-scene convention** (ADR [0001](adr/0001-editor-first-ui-scenes.md); how-to in `client/docs/README.md`) and added Button-base `DockButton`/`ParchmentButton` + a base `RichTextLabel` prose style to the theme. *§2: state (client owns the HUD, consumes DM text) · depends on the DM loop (M2) · goal: the game's main screen, on screen.*
|
||||
- ○ **Title screen** — new game / continue / load / settings; the entry point. Author it editor-first (ADR 0001). *§2: n/a · depends on the Theme · goal: first impression, into the world.*
|
||||
|
||||
### M4 — Character creation
|
||||
- ○ **Character creation UI** — the mock's screen over our proven model: race + calling cards, six stats with **LCK hidden entirely** (§7), 3d6 floors, the **+3 additive-only spend-up** pool (§8, matches the `character-creation-stats` model), live origin blurb + nameplate. **Reconcile class names** here (§8 vs the mock's callings). Feeds new-game canon-log construction (already built). *§2: state · depends on the creation model (M0) + Theme · goal: build a character.*
|
||||
|
||||
Reference in New Issue
Block a user