Game opening becomes a moddable JSON script driven by mc_worldsim::StartScriptRunner
and exposed to Godot via GdStartScript. Start scripts + dwarf tribe/wanderer units
live in public/resources/start_scripts; START_SCRIPTS.md documents the contract.
Adds tools/validate-start-scripts.py + wires it into CI (stage 3b) and verify.sh
(step 0b). Marks p3-14 done and regenerates the objectives dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the static .tscn label/button text in 5 scenes (end_game_summary section
titles + footer, merge_panel, city_screen merge button, game_setup section
headers, past_games) through ThemeVocabulary.lookup() in their controllers'
_ready, and remove the hardcoded .tscn text. Add the corresponding vocab keys;
drop 3 redundant endgame_* keys (footer buttons already use endgame_footer_*).
validate-i18n now passes: 145 scenes scanned, 0 hardcoded UI strings.
This clears the i18n verify gate with no bypass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace hardcoded user-visible strings in 12 scene scripts (great_person_modal,
merge_panel, specialists_drag_panel, throne_room_great_works,
intelligence_log_panel, knowledge_tree tier badge, credits, game_setup,
past_games, replay_viewer, lens_switcher/ransom_offers tooltips) with
ThemeVocabulary.lookup() + add the corresponding keys (incl. fmt_* for format
strings). Part of clearing validate-i18n with no bypass. (48 → 23 remaining.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
replay_viewer.tscn hardcoded its title/placeholder/turn/play/step/speed-label
text (pre-existing since May). Route the word labels through ThemeVocabulary
(replay_back/title/placeholder/step keys) set in _ready; runtime-set labels
(turn/play-pause/speed) lose their static .tscn text. Symbolic speed buttons
(0.5×/1×/2×) left as-is (not flagged). Clears validate-i18n for this scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark override→inheritance done: all font_color migrated to theme_type_variation
/ redundant-deleted / dynamic carve-outs, and the one repeated modal-panel
StyleBox extracted into shared PanelModal/PanelModalPadded variations
(ac019c060). Remaining inline StyleBoxes are single-use-custom or dynamic
carve-outs (no shared pattern left). Coverage gate guards regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record audit finding (no remaining base-palette dedup; guide bg tokens aliased)
and add a status header: all in-game colour derives from the single token
source, verified + gated. Remaining items are non-Godot (guide de-hex,
accessibility variants) or non-colour (StyleBox geometry, font-size).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
guide.bg{Primary,Secondary,Tertiary} held the same hexes as
background.{base,surface,raised} — the only genuine colour duplication left in
the token table. Aliased them so each base colour lives in exactly one place.
Value-preserving (resolved meta blob unchanged; ui_theme.tres byte-identical).
Audit finding: no other dedup exists in the Godot-layer tokens — tech.* are
already aliases of palette.*, and the gold/green families are distinct values,
not duplicates. The single-colour-source goal has no remaining godot-layer work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Tools register after restart (confirmed). First in-session call timed out (slow
throttled MCP-spawned windowed Godot); fixed via connect-first + 150s grace +
respawn (6310433cc), re-verified through the built client (NODE_EXIT=0, real
3.85MB PNG). Running MCP needs to reload the post-fix dist (one restart) to use
it in-session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stale May 31–Jun 8 agent worktrees, all behind main with no unique work
(every change landed or superseded). Removed checkouts, branches, and the
accidentally-committed gitlink entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first in-session magic_civ_screenshot timed out: windowed Godot spawned by
the MCP (a throttled background GUI process under Claude Code) inits far slower
than a terminal launch, exceeding the 45s connect timeout — though the driver
DID bind the port (verified 8787 open). Fixes:
- connect-first: if a rendered driver is already listening (e.g. a foreground
`MC_MCP_RENDER=1 ./run play`), connect to it directly and skip the slow,
throttled background spawn entirely.
- start timeout 45s -> 150s for the spawn fallback.
- a failed start now clears its cached promise + reaps the child so the next
tool call respawns instead of replaying the rejection until an MCP restart.
Re-verified end-to-end through the built RenderClient (spawn path): ping {ok},
screenshot -> a real 3420x1923 PNG of the live world map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
world_map lair POI overlay, tile_info_panel tooltip wiring, lair standin
sprites + build_demo_lairs.py, tooltip unit test, lair proof scenes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
These Label nodes set font_color to text.primary — which is already the Label
theme default in ui_theme.tres — so the override was a no-op duplicate. Deleted
across 12 scenes; the Labels now inherit the default. Value-preserving by
construction (default == deleted value); all targets confirmed Label-typed.
gdlint: no new issues (pre-existing class-order/const-name in some files
untouched). knowledge_tree render-verified via tech_tree_proof (exit 0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the %HappinessLabel state-conditional font_color (positive/negative/
neutral) to theme_type_variation (LabelPositive/Negative/Secondary). It's a
Label (top_bar.tscn:86), so the variation applies cleanly and is value-preserving.
This completes the static Label font_color override→inheritance migration. The
only remaining add_theme_color_override("font_color", ...) are 2 Button
state-toggles (lens_switcher active-lens, overlay_panel) — genuinely dynamic
(toggled per UI state, would need a Button-based variation), left as the
dynamic carve-out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node-type-aware batch: replace add_theme_color_override("font_color",
ThemeAssets.color("<token>")) with theme_type_variation on declared Label vars
only (35 overrides across 19 HUD/menu/notification/world-map scenes). Non-Label
targets (Buttons in top_bar/overlay_panel/lens_switcher) and dynamic/param
colours are left as overrides — a Label variation would break a Button's
stylebox lookup.
Value-preserving (each variation carries the identical token colour; instance
font_size overrides are untouched). All changed files gdlint-clean. Pattern is
render-proven by the statistics + hotkey_sheet iterations; live world_map render
skipped here because the working tree carries a concurrent session's uncommitted
world_map.gd / tile_info_panel edits (left unstaged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate 5 font_color overrides to theme_type_variation
(LabelTitle/Secondary/Positive/Negative/Muted). Inline StyleBoxFlat (panel bg)
left for the StyleBox sub-sweep.
Value-preserving (variations carry the same token colours) + gdlint clean. No
direct render harness (this panel opens on in-game interaction, not coverable by
a proof scene or the magic-civ open_screen surface); the variation pattern is
render-proven by the statistics + hotkey_sheet iterations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate 15 add_theme_color_override("font_color", ...) calls in statistics.gd to
theme_type_variation (LabelTitle/Secondary/Muted/Disabled/Positive/Negative).
Widgets inherit the colour from ui_theme.tres instead of hand-setting it.
Value-preserving (variations carry the same token colours). Render-verified on
plum via statistics_proof — all 5 tabs render; Rankings shows title gold, metric
secondary, trend arrows green, no regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the type-variation foundation (build-tool emission + 9 Label variations)
and the knowledge_tree pilot (4 labels migrated, render-verified). Note the
remaining scene-by-scene sweep + redundant-default deletes + font-size/StyleBox
sub-sweeps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First override→inheritance migration, proving the pattern. Replace 4
add_theme_color_override("font_color", ...) calls with theme_type_variation:
cost→LabelMuted, hint→LabelDisabled, title→LabelTitle, flavor→LabelSecondary.
Widgets now inherit the colour from ui_theme.tres instead of hand-setting it.
Value-preserving (variations carry the same token colours). Render-verified on
plum via tech_tree_proof — cost labels render the muted colour correctly, tree
unchanged. Remaining accent-param labels (_make_pill/_make_section) stay as
overrides (dynamic colour). The redundant text.primary overrides (= Label
default) are a separate delete-pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for collapsing the ~188 add_theme_color_override calls onto Godot
Theme inheritance. Adds a `typeVariations` section to design-tokens.json and
emits each as a Godot type variation in ui_theme.tres.
9 Label variations covering the high-count font_color override patterns:
LabelTitle/Muted/Secondary/Disabled/Positive/Negative/Warning/Gold/Science
(→ text.title/muted/secondary/disabled, semantic.positive/negative/warning,
accent.gold/science). Widgets set `theme_type_variation = "LabelMuted"` to
inherit instead of `add_theme_color_override("font_color", ...)`.
Additive — nothing consumes them yet, zero visual change. Verified: variations
baked into ui_theme.tres, theme --check clean, headless load exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark the minimap biome-colour single-source path verified via the magic-civ
rendered driver (live world_map shows minimap with real biome colours), and
record phase 1d progress (city_proof + world_gen_lab_proof rerouted; 3 proof
copies remain — climate legend + 2 that don't load_theme).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the hardcoded TERRAIN_COLORS dict copies in city_proof.gd and
world_gen_lab_proof.gd; both now call DataLoader.get_biome_color(tile.biome_id)
(both load the theme, so the source is populated). Verified: city_proof renders
correct biome colours (ocean/forest/plains/mountains/volcano), no magenta.
Remaining phase-1d copies: climate_proof (also draws a colour legend off the
dict) + improvement_proof / world_map_proof (don't load_theme — would need a
theme-load added first). Tracked in p2-87.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete minimap.gd's divergent hardcoded TERRAIN_COLORS dict (+ DEFAULT_TERRAIN_COLOR);
terrain colour now comes from DataLoader.get_biome_color() — the same single
source (biome_colors.json) the main map renderer uses.
Also fixes a latent bug: the minimap keyed on `raw_tile.get("terrain_id")`, but
tiles only carry `biome_id` (map_loader.gd:119) — so the old terrain lookup
returned nothing and the dict's simple-name keys never matched the biome_id
namespace. Now reads biome_id, matching hex_renderer.
Value-preserving (get_biome_color returns hex_renderer's lifted values) and a
strict improvement over the broken terrain_id path. gdlint clean.
NOTE: populated-minimap visual proof deferred — a standalone mount can't
reproduce the minimap's world_map HUD context (camera/scale/GameMapScript); to
be confirmed via a real world_map render (magic-civ rendered driver).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete hex_renderer.gd's hardcoded 69-entry TERRAIN_COLORS dict; the terrain
sprite fallback now calls DataLoader.get_biome_color(biome_id) (the values were
lifted from this very dict in phase 1a, so value-preserving). One fewer copy of
the biome palette; file drops ~82 lines. gdlint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- render_client.ts: TCP client that spawns the rendered game via
render-driver-server.sh, then sends screenshot / open_screen / ping correlated
by id (distinct from the headless stdin HarnessClient).
- scripts/render-driver-server.sh: boots the REAL game (MC_AUTO_START +
MC_MCP_RENDER, gl_compatibility, not --headless) so the driver captures real
frames; client connects on MC_MCP_PORT.
- index.ts: magic_civ_screenshot + magic_civ_open_screen tools, render-client
holder + cleanup.
Verified end-to-end through the BUILT TypeScript (no Claude restart needed):
RenderClient -> game -> TCP ping {ok}, screenshot -> a real 3420x1923 PNG of the
live world map. dist/ is gitignored (built locally per .mcp.json); the tools
surface in a Claude session after the next restart.
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>
New mcp_render_driver autoload (active only on MC_MCP_RENDER=1): listens on a
localhost TCP socket polled in _process and handles screenshot / open_screen /
ping / quit against the LIVE rendered game.
TCP, not stdin: OS.read_string_from_stdin blocks an open pipe and would freeze a
rendered main loop (the player_api_main stdin pump only works --headless); a
polled TCPServer is non-blocking and also dodges Godot's windowed-stdout
buffering. Inert (set_process(false)) unless the env flag is set — zero cost in
normal play.
Verified end-to-end with NO MCP needed: MC_AUTO_START=1 MC_MCP_RENDER=1 godot +
a raw TCP client -> ping ok, screenshot -> real 3420x1923 PNG of the live world
map. Phase 2 (claude-player-mcp tools + npm install + .mcp.json + restart) next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 12 player colours were authored TWICE (drift-prone): palettes.json
default.player_colors (runtime source + colourblind variants) AND a hand-copied
design-tokens.json `player.*` group used by UI chrome. Same values, two files.
- build-ui-theme.py: generate `player.<name>` tokens from palettes.json's default
variant (PLAYER_COLOR_NAMES ↔ array index) and merge into the baked meta blob.
- design-tokens.json: remove the hand-authored `player.*` group — now generated.
Player colours now have ONE source (palettes.json). Value-preserving: meta-blob
player.* == palettes default for all 12; build --check clean; headless load exit 0;
UI consumers (player.purple toasts, diplomacy/arena overlays) resolve unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strategic umbrella over p2-73 (pipeline) + p2-74 (de-hardcode): unify all
colour onto design-tokens.json as the single source of truth, with layered
primitive→semantic→component aliasing (value-preserving), and every consumer
(Godot UI, web guide fantasy-theme.ts, accessibility palettes.json, game-content
terrain/minimap colour arrays) deriving from it instead of carrying its own.
Captures the 4-source fragmentation audit, the tiering architecture, an 8-cluster
plan (clusters 1–2 = the in-flight B work, already partly landed), and the rule
that aliasing must never change rendered colour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main.gd: MC_AUTO_START=1 skips the menu/setup and boots an interactive seeded
game (MC_AUTOSTART_SEED / _PLAYERS / _MAP_SIZE overrides), mirroring the AI_ARENA
auto-boot but NON-spectator (local slot stays interactively controllable).
Phase-1 prerequisite for the rendered MCP driver (p2-86), and independently
fixes proof-capture: screenshots can now reach in-game screens without a human
clicking "New Game". Verified: MC_AUTO_START=1 ./run play reaches
world_map._start_game / the prologue (menu skipped), gdlint clean, 0 errors.
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>
`GameState.get_tech_web()` no longer exists — the accessor moved to
`TurnManager.get_tech_web()` (turn_manager.gd:132, builds the graph lazily
after DataLoader loads a theme), which is what production tech_tree.gd uses.
- tech_tree_proof.gd:56 — was raising `SCRIPT ERROR: Invalid call.
Nonexistent function 'get_tech_web'` mid-setup; switched to
`TurnManager.get_tech_web() as TechWebScript`.
- end_game_stats.gd:241 — same stale call (guarded, so it silently fell to
null); switched to the live accessor.
culture_tree_proof.gd already used TurnManager.get_culture_web() — unchanged.
Verified on plum (headed render, warm cache): tech_tree_proof exits 0, ZERO
SCRIPT ERROR (was 1), diagnostics print "TechWeb: 115 techs, 10 pillars" +
all 10 domains; screenshot reviewed in conversation.
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>
- lair_overlay_renderer now loads sprites/lairs/<type_id>.png (via
DrawHelpers.scaled_sprite_size, POI fraction 0.45) and draws it in place of
the diamond marker when the asset is present.
- Enlarge + keep-labeled the diamond fallback (radius 18→26) so a lair is at
least legible until the art lands.
- Sprite path is a no-op until the standin pipeline emits sprites/lairs/*
(tracked as a follow-up objective).
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>
- Reveal the prologue spawn box in recalculate_vision: the player owns no units
during the turn -1/0/1 cold-open, so unit-derived fog left the whole map black
and the wanderers invisible. Reveal the local player's box (LOS from centroid +
each wanderer hex) whenever a prologue is active.
- Prologue overlay: draw the real dwarf_wanderer / dwarf_tribe sprites, centered
on the hex (+ hex_center, was drawing at the corner), scaled to match in-game
units. Marker glyph kept only as a missing-asset fallback.
- Scale all unit sprites to 0.6 of hex width and city sprites to 0.75 via
DrawHelpers.scaled_sprite_size — Civ-like tile dominance instead of the tiny
native 64px blit that read as unreadable tokens.
- Add toggleable leveled Log autoload (dev/info/warn/error via MC_LOG_LEVEL,
default info); route prologue diagnostics through it with durable tags
(prologue / prologue-overlay), not objective ids.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The de-hardcode convertible work is complete (clusters 1–5 landed; all
remaining inline Color() in live scenes are sanctioned carve-outs —
computed/fallback/transparent/game-content). Only the per-screen apricot
visual proofs remain, so the honest status is `partial`, not `stub`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>