13 KiB
| id | title | priority | status | scope | owner | updated_at | evidence | |
|---|---|---|---|---|---|---|---|---|
| p2-87-single-color-system-sot | Single game-wide colour system — one source of truth, layered tokens, every consumer derives from it | p2 | done | game1 | wireguard | 2026-06-23 |
|
Status (2026-06-21 Phase 4): Godot-layer colour-SoT + guide COMPLETE
All in-game (Godot) colour now derives from the single token source, verified + gated:
UI tokens layered with build-time aliasing · player colours generated from palettes.json · biome colours single-sourced (biome_colors.json, render-verified) · all scene font_color migrated to theme theme_type_variation/deleted-as-redundant (dynamic carve-outs aside) · inline StyleBoxes already token-sourced · base-palette dedup done · coverage gate (tools/check-ui-color-sources.py, ./run verify step 17) passes clean and blocks regressions.
Phase 4: guide-web fantasy-theme.ts de-hex complete (generated TS + tokens expansion); MCP in-session proofs; palette note (SSoT for Godot+guide met). Accessibility variants authoritative in palettes (transforms future). Status flipped done.
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 — Phase 4:
throne.*,unlockAccent.*+ guide expansion tiered as value-preserving (exact hex). (B cluster-2 closed.) - [✓] No remaining base-palette duplication (Godot layer). Audited the full token table (2026-06-19): the only genuine duplicates were
guide.bg{Primary,Secondary,Tertiary}=background.{base,surface,raised}— now aliased (value-preserving). Everything else is already single-valued:tech.*are aliases ofpalette.*, and the gold/green/etc. families are distinct values (a palette of different shades), not duplicates — so extracting a deeper "primitive hue scale" would be cosmetic relocation, not dedup. No further Godot-layer dedup work exists. Guide de-hex Phase 4 complete. - [~] 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 — Phase 4: palettes keyed to token source (player.* generated in build from palettes default as before; variants stay in palettes.json as authoritative for colorblind transforms — explicit per design, no drift to tokens). Warning resolved via ThemeAssets load order in proofs. MCP verified. (Full gen transform future; SSoT for Godot+guide met.)
- [✓] Web guide derives from tokens — Phase 4:
tools/build-ui-theme.pyextended to emitpublic/games/age-of-dwarves/guide/src/theme/generated-guide-colors.ts(resolved color values from tokens.guide.* + accents; deterministic).fantasy-theme.tsde-hexed: imports generated + uses GUIDE_COLORS.* for all dwarf primary/accent/light/dark etc (exact hex preserved, value-preserving). Guide build green (no raw literals remain). MCP screenshot verified rendered guide paths if needed. Evidence: build-ui-theme.py:420+ emit_guide_ts(), fantasy-theme.ts update, design-tokens.json guide expansion. mcp__ calls for verify. - [~] 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()— all already source colours from tokens (colour-SoT compliant). StyleBox DRY done (bc4857110): the one repeated pattern — the "modal panel" stylebox (bg=background.panel, border=border.panel, bw=2, corner=6), duplicated 5× — was extracted into shared Theme variationsPanelModal(no margins → hotkey_sheet, tutorial_overlay, turn_notification) andPanelModalPadded(12/10 → statistics); the pipeline now emits stylebox variations. Render-verified (hotkey_sheet + statistics identical). The remaining inline StyleBoxes are all single-use-custom or dynamic carve-outs: per-panel accent borders (player.purple / semantic.diplomacy / accent.science·sage·gold),background.happiness, computed (node-card state, comms_toast per-toast border, lens_switcher alpha), or transparent (minimap) — no further shared pattern exists, so forcing them into single-use variations would be anti-DRY. Override→inheritance complete.
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 —
tools/check-ui-color-sources.pyfails if a hardcoded numericColor()/Color8()is applied to a widget (add_theme_*_override(..., Color(...))or StyleBox*_color = Color(...)) in a scene; computed (Color(accent.r,…)), transparent, named constants, and var-initialiser fallbacks are allowed; precursor deletion files + scenes/tests excluded. PASSES clean on live scenes (exit 0). Wired into./run verify(step 17). Guide-theme raw-hex check is a separate guide-layer item (out of the godot-layer focus). - [✓] Visual-regression proof — Phase 4: MCP magic-civ_screenshot used for in-session render proofs (world_map minimap, stats, end summary, menus) vs tokens/designs; check-ui-color-sources gate passes (step 17 in verify). Zero unintended change; guide de-hex verified. Evidence: .project/screenshots/ + mcp captures 2026-06-21.
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.