Establish ONE data source for biome render colour, lifting hex_renderer.gd's
authoritative 69-entry palette (value-preserving, biome_id -> [r,g,b] 0-255).
- public/games/age-of-dwarves/data/biome_colors.json — the single source.
- DataLoader: _load_biome_colors() at theme load + get_biome_color(biome_id)
with '_default' fallback then magenta sentinel.
Additive only — no consumer rerouted yet (next phases: hex_renderer, minimap,
proof scenes all read this + delete their hardcoded TERRAIN_COLORS dicts).
Verified: headless load clean, biome_colors.json parses, 0 script errors.
(data_loader.gd max-file-lines is pre-existing, tracked by p2-10k.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
magic_civ_screenshot / open_screen tools + render_client TCP transport +
render-driver-server.sh, verified end-to-end through the compiled RenderClient
(ping {ok}, screenshot -> 3420x1923 PNG of the live world map). Only the
tool-surfacing-in-a-Claude-session step remains (needs a restart; dist/ builds
locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark Phase 1 acceptance complete: MC_AUTO_START rendered boot, mcp_render_driver
TCP autoload (screenshot/open_screen/ping), plum-verified. Records the TCP-not-
stdin design pivot and the end-to-end proof (3420x1923 PNG of the live world
map). Phase 2 (TS MCP tools + npm install + .mcp.json + restart) remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cluster-1 aliased the tech fills to DIFFERENT-valued tokens (semantic.positive,
accent.gold, accent.science), which silently shifted the tech-tree colours.
That conflated two separate decisions: "tier the tokens" (intended) and "unify
to the canonical palette" (not authorised). Aliasing must be value-preserving.
- Add primitives palette.{green,greenBright,gold,goldBright,blue} holding the
EXACT original tech-state hexes (kept distinct from the brighter semantic.*/
accent.* values on purpose).
- Re-point tech.{researchedBg,researchedBorder,availableBg,availableBorder,
currentBg} aliases at those primitives. locked*/selected already matched.
Result: tech.* is fully layered (component → primitive, zero component-level
hex) AND pixel-identical to the pre-cluster-1 appearance. Verified on plum:
baked meta blob resolves every tech.* to its original hex; render matches the
original muted colours. Whether researched-green SHOULD equal semantic.positive
is now an explicit future choice, not a silent one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- p2-85: lair (+resource) sprites via the standin pipeline + hover tooltips. The
lair overlay renderer is already sprite-capable (7df76174c); this covers
generating sprites/lairs/* art and extending tile_info_panel for POI tooltips.
- p2-86: claude-player-mcp rendered-driver mode — magic_civ_screenshot /
magic_civ_open_screen so Claude can drive the UI and capture rendered screens,
complementing the headless state-only API (p2-67).
- Regenerate objectives dashboard (README.md + objectives.json), 326 objectives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the design-token system genuinely layered instead of flat single-tier.
- build-ui-theme.py: add W3C-style alias resolution. A token $value may now be
a reference `{color.x.y}` resolved (with cycle + dangling-target detection) to
the target's literal hex at build time. Literal hexes pass through unchanged,
so the resolver is transparent for existing tokens (--check stayed in sync).
- design-tokens.json: introduce a primitive `palette.*` tier (white,
neutralMuted, neutralBorder) and convert the 8 component `tech.*` tokens from
bespoke hex into ALIASES: researched→semantic.positive, available→accent.gold,
available border→accent.goldBright, current→accent.science, locked→palette
neutrals, selected→palette.white. tech.* now carries zero literal hex — a
colour lives in exactly one place, killing drift.
Rationale: the prior `tech.researchedBg = #33b333e6` was a component token with
its own hex, independent of `semantic.positive` — the duplication the token
system exists to prevent. Now component → semantic → primitive.
Verified on plum (headed render against warm import cache — SAFE, the kernel
panic is mass-import only): build --check resolves aliases into the baked meta
blob (tech.researchedBg→66e666 etc.); tech_tree_proof renders the canonical
colours, exit 0, no reimport, no panic. Screenshot reviewed in conversation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route the knowledge-tree (tech/culture) screen off 26 inline Color()
literals onto the design-token system.
- Add color.tech.* (8 node-state bg/border) + color.unlockAccent.* (7
badge accents + dim) to design-tokens.json using exact-hex equivalents
of the prior literals — zero visual change for cards/badges by
construction.
- Regenerate ui_theme.tres via tools/build-ui-theme.py (--check clean).
- Remap detail-panel/text literals to existing background.panel /
border.panel / border.divider / text.* / accent.* tokens.
- const→var refactor seeded in _resolve_theme_colors() (ThemeAssets.color
isn't const-eval safe), called before _build_layout().
- Compact the indicator-badge spec block to a data-driven loop (identical
tooltip output, fixes max-line-length).
Verified on plum: JSON valid, theme --check clean, all 26 token refs
resolve, no stale const refs (incl. subclasses), gdlint clean except the
pre-existing max-file-lines (file predates this pass; engine/scenes/ is
not gdlint-gated). Apricot visual proof pending (no godot import on plum).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace flat-black unexplored fog with an antique-cartography treatment
(Civ-VI style), themed as dwarven 'unmapped vellum/slate' — reveals no
real terrain, only a stylized stone surface.
- fog_renderer.gd: unexplored tiles painted with a procedurally generated
opaque vellum texture (FastNoiseLite domain-warped FBM → warm dark
slate→parchment gradient), generated synchronously at init so there is
no reveal-before-ready leak. No binary art asset required. Visible/seen
paths unchanged; frontier stays a clean opaque edge.
- design-tokens.json -> ui_theme.tres: fog.unexplored 000000ff -> 1a160fff
(warm 'unmapped vellum' tone) so the minimap unexplored cover matches.
Render-verified on apricot (iter_7q proof, fog enabled): undiscovered
renders as warm stone/vellum on map + minimap, no terrain bleed, clean
frontier, lit tiles unaffected. gdlint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Undiscovered (never-seen) terrain was leaking through fog on both the
main map and minimap; only the lit(visible) vs unlit(seen) distinction
worked. Root cause: unexplored overlay was sub-opaque + undersized.
- fog_renderer.gd: UNEXPLORED_COLOR alpha 0.85 -> 1.0 (opaque); edge-fade
softening now gated to the FOGGED state only — unexplored tiles stay
fully opaque even at vertices bordering a visible tile (was revealing
undiscovered terrain along the exploration frontier).
- design-tokens.json -> regenerated ui_theme.tres: fog.unexplored
alpha 0.90 -> 1.0 (fixed at token source, not hand-edited).
- minimap.gd: unexplored now drawn as a full tile-pitch opaque cover
instead of a 3x3px dot (the dot left ~5px gaps at minimap scale,
leaking terrain). fog.explored (seen dimming) left unchanged.
Verified on apricot via iter_7q_worldmap_visual_proof with fog ENABLED:
undiscovered renders solid black on map + minimap, clean hard frontier,
lit tiles unaffected. (GUT cannot prove this — render-verified.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
p2-57b gap analysis flagged dangling produces[] refs: adamantine_press /
gravity_press → dwarf_master_engineer, royal_infirmary / field_hospital_corps
→ master_surgeon, grand_chronicle → saga_writer named units that had no JSON.
Authored all three in public/resources/units/ matching the unit schema and the
shape of their nearest siblings (dwarf_grand_engineer, field_surgeon,
loremaster): Great-Person-tier support specialists with logistics blocks, real
tech gates (deep_engineering / surgical_practice / rune_archives), upgrade
chains, and GP actions consistent with their class. validate-game-data.py
PASSes all three (the 90 unrelated failures pre-exist on main).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the gap where the design system (.project/designs/design-tokens.json)
drove the React guide but not the Godot game.
- tools/build-ui-theme.py: compiles the W3C/style-dictionary token SoT into a
complete Godot Theme (7 StyleBoxFlat sub-resources, Button/Label/Panel/
PanelContainer/ItemList/RichTextLabel colors + font sizes + corner radii/
border widths per UI_DESIGN_SYSTEM.md §3/§4/§6). ui_theme.tres is now a
GENERATED artifact; tokens are the single source of truth. Deterministic
output (sorted keys, fixed float fmt, preserved uid://ui_theme_fantasy) with
a --check drift gate. Idempotent; --import does not rewrite it.
- project.godot [gui] theme/custom: applies ui_theme.tres at viewport level so
every non-overriding default Control renders the copper fantasy styling.
- ThemeAssets.color(name) -> Color: resolves dotted token names (accent.gold,
semantic.positive, text.primary, …) against the metadata/tokens JSON blob
baked into the .tres by the generator. Fully data-driven from the SoT, no
hardcoded color map. (Godot rejects dots in Theme color item names, so the
token table ships as resource metadata.) Unknown names return an explicit
fallback. This is the API p2-74 will de-hardcode 45 scripts onto.
- ui_theme_proof.{tscn,gd}: bare-widget + color()-swatch proof scene.
test_theme_assets_color.gd: GUT accessor coverage (5/5 headless).
Proof captured on apricot under weston, reviewed in conversation:
.project/screenshots/p2-73-ui-theme-proof.png. Workspace green — full unit
(16==16) and integration (18==18) suites show identical HEAD-baseline-vs-patch
failure counts, zero regressions; patch adds +5 passing tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Advances p3-10a. Moving a stack onto a wild-lair tile now opens a small
CanvasLayer mode picker (modeled on promotion_picker.tscn) before combat:
Assault (enabled), Raid (disabled — p3-10c), Siege (disabled — p3-10b). The
picker emits mode_chosen(mode) / cancelled(); world_map_combat.initiate_lair_combat
opens it and routes the Assault branch through _begin_lair_assault → the existing
p0-17 show_lair_preview → _handle_lair_clear path (per p3-10a, "the existing path
IS the assault"), so the working lair-clear flow is not regressed.
Scope note: Assault routes through the live p0-17 flow, NOT GdLair.assault()
(api-gdext/src/lair.rs) — that bridge is the 7-arg JSON marshaller and would
require building attacker/defender JSON + loading tier_NN.json + applying
loot/survivor/clear outcomes in GDScript, duplicating the working path. The
p3-10a bullet therefore stays ◐ (bridge not exercised end-to-end; no picker
proof screenshot yet).
GUT: tests/unit/test_lair_mode_picker.gd 5/5 green on apricot headless
(only-Assault-enabled, Assault emits mode_chosen("assault"), Cancel emits
cancelled() and no mode, disabled Raid/Siege never emit via the in-handler
guard, target label resolves the lair name). All-Dwarf vocab keys
(lair_picker_*, lair_mode_*) authored in vocabulary.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stats modal (statistics.gd) and end-game summary (end_game_summary.gd)
rendered raw ThemeVocabulary keys ("Statistics Tab Demographics", "Endgame
Banner Victory", "Endgame Reason Lastsurvivor", etc.) because the statistics_*,
endgame_*, trend_*, outcome_*, event_* and `close` keys were absent from
vocabulary.json — lookup() falls back to title-case on a miss.
Author all of them with Dwarf-flavoured copy: title "Records of the Hold";
tabs Census/Ledgers/Standings/Chronicle/Sagas; banners "Victory!" / "The Hold
Has Fallen" / "The Reckoning"; per-GameOverReason flavour keyed PascalCase
(endgame_reason_LastSurvivor/ConditionMet/TurnLimit/Resigned) to match the
discriminant strings the proof + GUT drive; footer "Survey the Realm / Recount
the Saga / Seal to the Vault / Inscribe to Stone / Main Menu".
statistics_proof.gd: rebuild StatsTracker.category_labels after load_vocabulary
so the demographics/graphs/rankings column + metric labels resolve to copy
("Score / Population / Military / Cities / Technology / Wonders") rather than the
title-case keys cached at autoload-init (proof-only ordering; in-game the theme
loads before StatsTracker).
Re-captured clean on apricot under weston — no title-cased placeholders remain:
.local/ui-proofs/statistics_proof_{demographics,graphs,rankings,replay,histories}.png
.local/ui-proofs/end_game_summary_proof_{LastSurvivor,ConditionMet,TurnLimit,Resigned}.png
Advances p2-47 (proof copy-caveat resolved; stays partial — snapshot-append +
bridge-parity GUT remain Rust-blocked) and p2-48a (copy caveat resolved; proof
stays [~] pending user phase-gate approval).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>