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>
9.1 KiB
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 M0–M2 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 StringNames 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,#8f3a34border, cream text, brightened hover). - Tab / TabActive — inactive (parchment
#efe6cf+#cbb684border, 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 —
#f1e8d2fill,#bfa878border, 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 areColor;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.tresloads without error, and every named type-variation intheme_keys.gdresolves 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_itemsstretch 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/
cturnkeyframes) — 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.