magicciv/.project/objectives/p2-87-single-color-system-sot.md
Natalie 0c2d7c6d4a feat(@projects/@magic-civilization): colour-SoT coverage gate locks in the migration (p2-87)
Add tools/check-ui-color-sources.py: fails if a hardcoded numeric Color()/Color8()
is applied to a widget in a scene (add_theme_*_override / StyleBox *_color).
Allows computed Color(accent.r,…), transparent, named constants, and var-init
fallbacks; excludes scenes/tests + the 3 precursor deletion files. Passes clean
on live scenes (exit 0). Wired into ./run verify as step 17 so a hardcoded
colour can't creep back in.

Capstone for the override→inheritance / single-colour-system work: colours in
live scenes now provably come from the design-token source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:27:56 -05:00

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
p2-73-ui-theme-token-pipeline
p2-74-ui-dehardcode-to-tokens

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):

  1. UI design tokens.project/designs/design-tokens.jsontools/build-ui-theme.pypublic/games/age-of-dwarves/ui_theme.tres, consumed via ThemeAssets.color() + Godot theme inheritance. (The system p2-73/p2-74 built and B is maturing.)
  2. Accessibility palettespublic/games/age-of-dwarves/data/palettes.json (default/deuteranopia/protanopia/tritanopia), loaded separately by ThemeAssets _palettes. Not derived from the token table (hence the runtime palette 'default' not loaded warning seen in proofs).
  3. Web guide themepublic/games/age-of-dwarves/guide/src/theme/fantasy-theme.ts carries ~20 raw hex literals, independent of the tokens. The guide and the game can drift apart.
  4. Game-content colourspublic/games/age-of-dwarves/data/terrain/*.json "color":[r,g,b] arrays, races.json, etc., PLUS the scenes/hud/minimap.gd TERRAIN_COLORS dict 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 (Godot ui_theme.tres meta 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 pipelinebuild-ui-theme.py resolves {color.x.y} references (cycle + dangling detection), transparent for literals. Done: commit 05efbebfd.
  • [~] Layered tiers exist — primitive palette.* tier introduced; tech.* component tokens are aliases (value-preserving, pixel-identical). Done: a8476c395.
  • All component token groups tieredthrone.*, unlockAccent.*, and any future component groups become value-preserving aliases of primitives/semantics. (B cluster-2.)
  • Semantic tier references primitivessemantic.*/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 by build-ui-theme.py from palettes.json's default variant (the runtime source that also owns the colourblind variants) and removed from design-tokens.json. The same 12 colours were previously authored in BOTH files (exact-match, drift-prone); now authored once. Value-preserving: baked meta blob player.* == palettes default; --check clean; headless load exit 0. Godot layer.
  • Accessibility palettes unifiedpalettes.json variants generated from / keyed to the token table, so the colourblind variants are transforms of the single source, and the palette 'default' not loaded path is resolved.
  • Web guide derives from tokensbuild-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 via DataLoader.get_biome_color(). Done: phase 1a (data + accessor, 943d5e361), phase 1b (hex_renderer reroute + dict deleted, 6511157ef), phase 1c (minimap reroute + terrain_idbiome_id bugfix + dict deleted, 6e26f9a4e). Phase 1d (proof-scene dicts): city_proof + world_gen_lab_proof rerouted to get_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't load_theme, so they'd need a theme-load added before routing). Test-viz only; low priority. VERIFIED 2026-06-19: the live world_map minimap 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 the terrain_idbiome_id bugfix (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 inline StyleBoxFlat.new()) onto Godot Theme type-variations so widgets inherit; ThemeAssets.color() stays only for dynamic/computed colour. Foundation done: build-ui-theme.py now 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 conditional LabelPositive/Negative/Secondary; 14 redundant text.primary=Label-default overrides deleted across 12 scenes, 1c7a0db0d). Only font_color overrides 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 leftover font_color overrides → 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 gatetools/check-ui-color-sources.py fails if a hardcoded numeric Color()/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 — 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.