font_color override→inheritance migration done. Remaining surface is colour-SoT compliant: misc colour keys + 2 leftover font_color are dynamic Button/RichText state colours (carve-out); the 27 inline StyleBoxes ALL source colours from tokens already (colour-compliant) — inline→Theme-inheritance is structural DRY, not a colour-source fix, reclassified as an optional follow-up (render-heavy, risky to force unattended). Loop a52b2931 stopped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
| id | title | priority | status | scope | category | owner | created | updated_at | relates_to | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| p2-87-single-color-system-sot | Single game-wide colour system — one source of truth, layered tokens, every consumer derives from it | p2 | partial | game1 | architecture | wireguard | 2026-06-18 | 2026-06-18 |
|
Goal
ONE colour system for the whole game, with .project/designs/design-tokens.json as the single source of truth. Every consumer — Godot UI, the web guide, accessibility palettes, and game-content colours — derives from it. No colour value is authored in more than one place; changing a colour is a one-line edit that propagates everywhere via the build pipeline.
Why (fragmentation audit, 2026-06-18)
Colour is currently defined in four disjoint places (verified):
- UI design tokens —
.project/designs/design-tokens.json→tools/build-ui-theme.py→public/games/age-of-dwarves/ui_theme.tres, consumed viaThemeAssets.color()+ Godot theme inheritance. (The system p2-73/p2-74 built and B is maturing.) - Accessibility palettes —
public/games/age-of-dwarves/data/palettes.json(default/deuteranopia/protanopia/tritanopia), loaded separately byThemeAssets_palettes. Not derived from the token table (hence the runtimepalette 'default' not loadedwarning seen in proofs). - Web guide theme —
public/games/age-of-dwarves/guide/src/theme/fantasy-theme.tscarries ~20 raw hex literals, independent of the tokens. The guide and the game can drift apart. - Game-content colours —
public/games/age-of-dwarves/data/terrain/*.json"color":[r,g,b]arrays,races.json, etc., PLUS thescenes/hud/minimap.gdTERRAIN_COLORSdict which diverges from the terrain JSON (documented in minimap.gd: "diverge by 10-70/channel"). Two definitions of terrain colour.
design-tokens.json's own $metadata.sources already names ui_theme.tres + fantasy-theme.ts + palettes.json as the things it should drive — but today it does not drive them; they are parallel.
Architecture (the target)
Three-tier W3C design tokens, higher tiers ALIAS lower ones (a colour lives once):
primitive palette.green = #33b333e6 ← raw hue/shade, the only literal hexes
semantic semantic.positive = {palette.green...} role-based
component tech.researchedBg = {semantic.positive} OR {palette.x} never its own hex
- Aliasing is strictly value-preserving. Tiering must never change rendered colour. Unifying two colours that were historically different (e.g. "is researched-green the same as success-green?") is a SEPARATE, explicit per-colour decision — never a silent side-effect of refactoring.
- The build pipeline (
build-ui-theme.py, extended) is the single emitter: it resolves aliases and outputs all downstream artefacts (Godotui_theme.tresmeta blob; a generated TS/CSS module for the guide; palette variants; and a reference the game-content loaders can use).
Acceptance
- [~] Alias resolution in the pipeline —
build-ui-theme.pyresolves{color.x.y}references (cycle + dangling detection), transparent for literals. Done: commit05efbebfd. - [~] Layered tiers exist — primitive
palette.*tier introduced;tech.*component tokens are aliases (value-preserving, pixel-identical). Done:a8476c395. - All component token groups tiered —
throne.*,unlockAccent.*, and any future component groups become value-preserving aliases of primitives/semantics. (B cluster-2.) - Semantic tier references primitives —
semantic.*/text.*/background.*/accent.*alias a primitive hue scale rather than each carrying its own hex (dedup the base palette). Value-preserving. - [~] Player colours single-sourced — the 12
player.*UI tokens are now GENERATED bybuild-ui-theme.pyfrompalettes.json's default variant (the runtime source that also owns the colourblind variants) and removed fromdesign-tokens.json. The same 12 colours were previously authored in BOTH files (exact-match, drift-prone); now authored once. Value-preserving: baked meta blobplayer.*== palettes default;--checkclean; headless load exit 0. Godot layer. - Accessibility palettes unified —
palettes.jsonvariants generated from / keyed to the token table, so the colourblind variants are transforms of the single source, and thepalette 'default' not loadedpath is resolved. - Web guide derives from tokens —
build-ui-theme.py(or a sibling) emits a generated TS/CSS colour module;fantasy-theme.ts's ~20 raw hexes are replaced by references to it. Guide + game can no longer drift. Guide build green. - [~] Game-content (biome) colours reconciled — biome render colour now has ONE source:
public/games/age-of-dwarves/data/biome_colors.json(69 entries, lifted value-preserving from hex_renderer), read viaDataLoader.get_biome_color(). Done: phase 1a (data + accessor,943d5e361), phase 1b (hex_rendererreroute + dict deleted,6511157ef), phase 1c (minimap reroute +terrain_id→biome_idbugfix + dict deleted,6e26f9a4e). Phase 1d (proof-scene dicts):city_proof+world_gen_lab_proofrerouted toget_biome_color(9e818f15e, both load the theme; city_proof render-verified). Remaining (3):climate_proof(also draws a colour legend off the dict — needs the biome list from elsewhere),improvement_proof+world_map_proof(don'tload_theme, so they'd need a theme-load added before routing). Test-viz only; low priority. VERIFIED 2026-06-19: the liveworld_mapminimap renders biome colours from the single source — captured via the magic-civ rendered driver (magic_civ_screenshot); the minimap shows green-forest / tan-plains terrain matching the map, confirming both the single-source colour path and theterrain_id→biome_idbugfix (the minimap terrain was previously broken). Proof:.local/ui-proofs/p2-87_worldmap_minimap_verified.png. - [~] Override → inheritance — collapse the ~188
add_theme_color_override(+ 27 inlineStyleBoxFlat.new()) onto Godot Theme type-variations so widgets inherit;ThemeAssets.color()stays only for dynamic/computed colour. Foundation done:build-ui-theme.pynow emits type variations; 9 Label variations defined (LabelTitle/Muted/Secondary/Disabled/Positive/Negative/Warning/Gold/Science) covering the high-count font_color patterns (7c8c54745). Pilot done + render-verified: knowledge_tree 4 labels →theme_type_variation(d51ec2454). Sweep in progress (loop a52b2931, ~66 overrides migrated): knowledge_tree pilot (4), statistics (15), hotkey_sheet (6+1 redundant), happiness_breakdown_panel (5), + node-type-aware batch of 19 scenes (35 Label overrides,5f6a0ccaf). Migration is Label-restricted + value-preserving (variation carries the same token colour; instance font_size overrides untouched); non-Label/dynamic colours left. font_color migration COMPLETE (~69 migrated to variations incl. top_bar conditionalLabelPositive/Negative/Secondary; 14 redundanttext.primary=Label-default overrides deleted across 12 scenes,1c7a0db0d). Onlyfont_coloroverrides left are 2 Button state-toggles (lens_switcher/overlay_panel) — dynamic carve-outs. LOOP STOPPED 2026-06-19 — colour-SoT goal met. The font_color override→inheritance migration is complete. The remaining surface is colour-SoT-compliant or genuinely dynamic: - Misc colour keys (
font_outline_color/hover/pressed/selected/default_color) + the 2 leftoverfont_coloroverrides → all Button-state / RichTextLabel / ItemList dynamic colours (toggled per UI state), the intended carve-out. - Inline
StyleBoxFlat.new()(27) — audited: all already source their colours from tokens (ThemeAssets.color(...)), so they are colour-SoT compliant. ~5 duplicate the default panel stylebox; the other ~22 are intentionally custom (per-panel accent borders: player.purple / semantic.diplomacy / accent.science·sage·gold / background.happiness), computed (node-card state, comms_toast accent param), or transparent (minimap). Converting inline→Theme-inheritance is structural DRY, not a colour-source fix — out of scope for the single-colour-system goal, and risky to force autonomously (margin/corner-sensitive, most panels have no render harness).
Optional structural follow-up (separate objective/loop, not colour-SoT): migrate the ~5 default-duplicate panels to PanelContainer/Panel inheritance (render-verify each) and, if a repeated accent-panel pattern emerges, add Panel type-variations. font-size overrides (176) are a separate typography sub-sweep.
- Coverage gate — a check (script/test) asserts no raw
Color(r,g,b,...)literals in scene scripts except sanctioned carve-outs (computed/dynamic), and no raw hex in the guide theme. Wired so regressions are caught. - Visual-regression proof — per-cluster headed render proofs on plum (warm cache — safe per feedback_no_godot_import_on_plum 2026-06-18 update) confirm zero unintended colour change.
Plan (clusters)
- cluster-1 ✅ alias pipeline + tier
tech.*(value-preserving).05efbebfd,a8476c395. - cluster-2 tier
throne.*+unlockAccent.*(value-preserving aliases). - cluster-3 dedup the semantic/base palette onto a primitive hue scale (value-preserving).
- cluster-4 web guide: emit generated colour module, de-hex
fantasy-theme.ts. - cluster-5 accessibility palettes unified to the token source.
- cluster-6 game-content colour reconciliation (terrain/minimap one definition).
- cluster-7 override → Theme type-variation inheritance migration (largest).
- cluster-8 coverage gate + final visual-regression sweep.
Rails
- Single source of truth =
design-tokens.json. Generated artefacts (ui_theme.tres, guide TS/CSS, palette variants) are NEVER hand-edited — edit tokens, rebuild. - Aliasing strictly value-preserving; colour unification is always an explicit, separately-approved decision.
- Presentation-only (Rail 3): no gameplay/logic change.
Verification host
Headed render proofs run on plum against the warm import cache (safe; the kernel-panic risk is mass image --import only — see feedback_no_godot_import_on_plum). tools/build-ui-theme.py --check, python3 JSON validation, and gdlint are all laptop-safe.
References
tools/build-ui-theme.py— the single emitter (alias resolver added)..project/designs/design-tokens.json— the source of truth..project/designs/UI_DESIGN_SYSTEM.md— design-system doc.- p2-73 (pipeline, done) · p2-74 (de-hardcode scenes, partial) — this is the strategic umbrella over both.