docs(specs): add M3-a shared Theme design

First of three M3 cycles (Theme -> shell 2a -> Title). Palette.gd consts
as the single source of truth; game_theme.tres for flat-bordered UI;
shaders for the three signature surfaces. EB Garamond replaces
proprietary Georgia. Proof is a human-eyeballed showcase harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:13:25 -05:00
parent 29474e5751
commit 2d7ed469bf

View File

@@ -0,0 +1,196 @@
# Shared Theme — design
**Milestone:** M3 (Visual foundation & shell), item **M3-a** — the first of three
cycles (Theme → Main Window shell 2a → Title). See `docs/roadmap.md` and
`CLAUDE.md` §16.
**§2 side:** n/a — pure presentation. Owns no game state, makes no AI/network
call. It is the visual system every later screen inherits.
**Depends on:** nothing (foundation). **Unblocks:** every M3+ screen.
---
## 1. Goal
Deliver the one shared visual system the mockups describe (`/mockups/README.md`,
"Global Design System") as reusable Godot resources, so every screen built after
this pulls the same palette, fonts, styleboxes, and signature surfaces — "the
look is one system." Nothing renders a real screen yet; this is the toolbox the
screens are built from.
Because a Theme has no behavior, its **proof is a showcase harness** a human
eyeballs in the editor — the same "live-proven / human-confirmed" bar M0M2 held.
## 2. The core architectural split
A Godot `StyleBox` **cannot run a shader** — the engine draws styleboxes itself.
So the deliverable splits in two, each half using the right tool:
| Concern | Mechanism | Covers |
|---|---|---|
| **Crisp bordered UI** | `game_theme.tres` (StyleBoxFlat + fonts) | buttons/CTA, tabs active/inactive, chips/tags, item tiles, cards, insets, dark panels; all three font families at their sizes |
| **The 3 signature surfaces** | `ShaderMaterial` on `Panel`/`ColorRect`, wrapped as drop-in mini-scenes | parchment sheet, dark diagonal-stripe bay, radial vignette overlay |
| **Code-driven color** | `palette.gd` consts | rarity bars, HP/MP/stamina gradients, disposition/turn-order colour coding |
The "procedural shaders" decision applies to the **three big backdrops only**. A
chip or button is flat-bordered and fits `StyleBoxFlat` natively; a shader on
every chip would be wasted work.
`palette.gd` is the **single source of truth** for every hex value. The
`game_theme.tres` styleboxes are authored to those same values; the shaders take
them as uniforms. **One palette, three consumers.**
## 3. Fonts (redistribution constraint)
Godot exports must **bundle** their fonts. The mock's three families become:
| Mock role | Bundled face | Licence | Notes |
|---|---|---|---|
| Georgia (prose / headings / logo) | **EB Garamond** (Regular, Italic, Bold) | OFL | Redistributable. Georgia is Microsoft-proprietary and **cannot** ship. EB Garamond is a warm old-style serif — the "aged tome" DM-prose voice. Its strong italic carries the narrative-body *italic* the mock uses. |
| Architects Daughter (hand-inked accent) | **Architects Daughter** (Regular) | OFL | Ships as-is; the mock already used the Google face. Nameplates, big stat numbers, DM-header labels. |
| Monospace (UI chrome) | **JetBrains Mono** (Regular) | OFL | Default pick (clean, wide, reads well UPPERCASE with letter-spacing); swappable for another OFL mono without design change. Labels, keybinds, numeric readouts, tags. |
## 4. File layout
```
client/
scripts/theme/
palette.gd # const Color tokens — the source of truth
theme_keys.gd # const StringNames for type-variations, so scripts never
# hardcode Theme string keys
assets/theme/
fonts/
EBGaramond-Regular.ttf EBGaramond-Italic.ttf EBGaramond-Bold.ttf
ArchitectsDaughter-Regular.ttf
JetBrainsMono-Regular.ttf
shaders/
parchment.gdshader # vertical gradient #ece2ca -> #e4d8bd
dark_bay.gdshader # 135deg repeating stripe #26221c/#221e18 + radial vignette
vignette.gdshader # standalone dim/blur-edge overlay (pause, dialogue)
game_theme.tres # the Theme resource — fonts, sizes, StyleBoxFlat variations
scenes/theme/
surfaces/
ParchmentPanel.tscn # Panel + parchment material (drop-in backdrop)
DarkBay.tscn # Panel + dark_bay material
VignetteOverlay.tscn # ColorRect + vignette material
theme_showcase.tscn # the proof: every token, stylebox, font, surface, one screen
theme_showcase.gd
tests/unit/
test_palette.gd
test_theme_resource.gd
```
Follows existing conventions: `scripts/<domain>/`, harness scenes under
`scenes/`, `tests/unit/test_*.gd`.
## 5. Contents
### 5.1 `palette.gd`
Every hex from the README's "Color tokens" section as `const Color`, grouped by
role:
- **Dark backgrounds** — near-black stage, bay stripe pair, translucent panels,
the dark-border ladder (`#6b6152``#1a1512`).
- **Parchment** — sheet gradient stops, card fills + insets, parchment borders.
- **Ink** — heading / body / muted / label / gold-accent inks.
- **Text-on-dark** — bone/cream primaries, secondaries, muted monos.
- **Accents** — blood-red set, gold/brass set, steel blue-teal set, green set.
- **Stat bars** — HP / MP / stamina gradient endpoints + track/border.
- **Rarity** — the five rarity colours as an ordered array.
Plus the small helpers the code-driven cases need:
- `rarity_color(rarity) -> Color`
- stat-bar gradient stops for HP / MP / stamina (returned as a pair, or two
named consts each).
This is the file the whole game reads from for any colour a Theme can't express.
### 5.2 `theme_keys.gd`
`const StringName`s for every type-variation name (`PrimaryCTA`, `Tab`,
`TabActive`, `Chip`, `ItemTile`, `ParchmentCard`, `DarkPanel`, …), so scripts
that set `theme_type_variation` reference a constant, never a raw string that can
typo silently.
### 5.3 `game_theme.tres`
The six reusable pieces from the README, each authored as a Godot **type
variation** (applied via `theme_type_variation` on any Control):
- **PrimaryCTA** — blood-red CTA button (gradient-look `StyleBoxFlat`, `#8f3a34`
border, cream text, brightened hover).
- **Tab** / **TabActive** — inactive (parchment `#efe6cf` + `#cbb684` border,
muted text) vs active (solid `#8f3a34`, cream text).
- **Chip** — mono-uppercase tag base; the *semantic* colour (red/gold/grey) is
set by the consuming script from `palette.gd`, since it varies per use.
- **ItemTile** — rounded square, 2px border (rarity colour applied by script),
plus a recessed dashed **empty** variant.
- **ParchmentCard** — `#f1e8d2` fill, `#bfa878` border, and a darker inset
variant.
- **DarkPanel** — translucent `rgba(18,15,11,.7)`.
Plus the three font families wired into the Theme's default / heading / mono
font roles at the README's sizes and letter-spacing.
### 5.4 The three surface scenes
Each wraps one shader as a **one-line drop-in**: later screens instance
`DarkBay.tscn` / `ParchmentPanel.tscn` / `VignetteOverlay.tscn` instead of
rebuilding a backdrop. Shader uniforms default to the `palette.gd` values (baked
into the `.tscn`/material), so the surfaces match the tokens without a runtime
dependency on the script.
- `parchment.gdshader` — vertical gradient `#ece2ca → #e4d8bd`.
- `dark_bay.gdshader` — 135° repeating stripe `#26221c` / `#221e18` (16px bands)
plus the radial vignette overlay.
- `vignette.gdshader` — standalone edge-dim overlay for the pause/dialogue dim.
## 6. The proof — `theme_showcase.tscn`
A single scrollable screen rendering, for a human to eyeball in the editor:
- the full palette as labelled swatches,
- every stylebox type-variation (CTA, both tabs, chips in each semantic colour,
filled + empty item tiles, parchment card + inset, dark panel),
- all three fonts at their real sizes (heading, prose body italic, hand-inked
accent, mono chrome),
- the rarity ladder,
- a sample HP / MP / stamina bar,
- the three shader surfaces side by side.
Pure presentation — no AI, no network, no game state. This is what confirms "the
look is one system" before any real screen is built.
## 7. Testing
- **`test_palette.gd`** — asserts the documented tokens exist and are `Color`;
`rarity_color()` maps each rarity to the right colour and handles out-of-range
safely; the stat-bar stop helpers return the documented values.
- **`test_theme_resource.gd`** — `game_theme.tres` loads without error, and every
named type-variation in `theme_keys.gd` resolves on the Theme (no dangling
key).
**Visual correctness is not GUT-testable** — it is an explicit **human-confirm**
step against `theme_showcase.tscn`, exactly as M2's live model proofs were
human-confirmed. The spec is not "done" until a human has opened the showcase and
signed off that it reads as the mockups' system.
## 8. Explicitly out of scope (this cycle)
- Any real screen (the Main Window shell 2a and Title are the next two M3
cycles).
- Real art of any kind — portraits, key art, iso/battlefield renders, item/ability
icons remain the mocks' dashed **art slots**, filled later.
- Responsive/stretch tuning beyond the project's existing 1920×1080 canvas
assumption (`canvas_items` stretch is a shell-cycle concern, not a Theme one).
- Baked-PNG texture variants — the three surfaces are shader-only this cycle.
- Animation (ember particles, pulse/`cturn` keyframes) — those belong to the
screens that use them, not the Theme.
## 9. Reconciliations touched
None. The two flagged reconciliations (damage determinism → M5, class names →
M4) do not touch presentation. This cycle introduces no charter conflict.