Completes player→ecology feedback. EcologyEngine::deplete_flora_at(col,row,amount)
depletes a tile's Producer-diet (flora) populations (registry-identified);
GdFaunaEcology.deplete_flora_at exposes it; EcologyState._on_tile_improved fires
it when a flora-clearing improvement (deforestation) completes — so clear-cutting
a forest removes its flora (the terrain→grassland change also drives the gradual
die-off). With the fauna half (over-hunting → extinction), the living world now
reacts to player pressure both ways. Logic stays in mc_ecology (Rail 1).
Test: deplete_flora_at_targets_producer_species_only (mc-ecology green); dylib
rebuilt + deployed; canonical GUT 745/0 (wiring loads, no regression).
p3-19 → done. Next: p3-20 (weather→scouting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the player→ecology coupling for fauna (the USP: the living world reacts to
the player). GdFaunaEcology.deplete_species #[func] resolves the string species id
→ numeric via the species library and calls EcologyEngine::deplete_population.
combat_utils._roll_wild_creature_loot now passes the slain creature's tile into
item_system.roll_fauna_drops, which calls EcologyState.fauna_ecology.deplete_species
on every fauna kill — so sustained hunting drives a species to local extinction
(is_extinct), and the engine's growth/emergence recover it once pressure eases.
Logic stays in mc_ecology (Rail 1); GDScript only triggers + passes the tile.
Verified: mc-ecology cargo green; dylib rebuilt + deployed; canonical GUT 745/0
(new roll_fauna_drops signature + caller load cleanly, deplete_species callable).
p3-19 stays partial — flora-harvest half (chop/intensive → flora population) next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Rust foundation for player→ecology feedback: PopulationSlot::deplete(amount)
(floors at 0, can cross the is_extinct threshold) + EcologyEngine::deplete_population
(col,row,species_id,amount) → post-depletion population, safe 0.0 no-op for a
missing tile/species. This is the hook hunting/harvesting will call to make
over-harvest drive local extinction; the engine's existing growth/emergence then
recovers abundance once pressure eases.
Tests: deplete_reduces_floors_at_zero_and_can_extinct (PopulationSlot) +
deplete_population_reduces_tile_species_and_can_extinct (engine); mc-ecology green.
p3-19 → partial. Remaining: GdEcologyEngine #[func] + GDScript kill/harvest wiring
+ dylib/GUT (loop continues).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pick_for_city gains a scout branch (after the early-military floor, before
expansion): when dwarf_scout is buildable (its tech/race/resource/building gates
met — mirrors pick_best_unit_of_type) and the capital owns no scout, the capital
queues one. Single scout, capital-only; the existing frontier-seek + scout-sweep
maneuvers (movement.rs) already drive dwarf_scout, so the AI stops diverting
combat units to scouting.
Tests: ai_builds_scout_when_buildable_and_none_owned +
ai_does_not_build_scout_without_its_tech; mc-ai 289/0 green. p3-22 → done.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Game 1 was NOT actually complete; the green dashboard overstated it. Created
objectives for the verified, untracked gaps (all confirmed 2026-06-25 against the
code, both Rust + GDScript paths):
- p3-19 player→ecology feedback (over-harvest/hunt deplete live populations; extinction)
- p3-20 weather affects scouting (vision penalty, not just movement)
- p3-21 weather-driven migration (migration ignores weather today)
- p3-22 AI builds dedicated scouts (only frontier-seeks with idle military)
- p3-23 trade richness (luxury swaps only; no gold/strategic trades)
- p3-24 Rail-1 port of economy/happiness/climate per-turn glue logic from GDScript
Dashboard regenerated. (Systems I'd wrongly doubted — ecology engine ticking, AI
worker improvements, naval harbor-gating — are confirmed working; not reopened.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/objectives-report.py after marking p3-18 (water crossing) done. Keeps the
dashboard fresh for verify step 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1 hardcoded the tech ids ("shipbuilding"/"ocean_navigation") in Rust — a Rail-2
violation (JSON is the canonical content store) and not single-source. Per owner
direction ("should be a player-level config setting"), the embark grant now lives
in data and is cached per player:
- mc_core::EmbarkLevel moves to the shared base crate (was mc-pathfinding) so
PlayerState can hold it; mc-pathfinding re-exports it. Adds from_mechanic_key
(the ONLY place the embark_* mechanic-key strings live).
- The naval techs carry the grant in JSON via unlocks.mechanics: shipbuilding →
embark_coast, ocean_navigation → embark_ocean. Which tech grants embark is now
authored data, not Rust.
- TechWeb::embark_level(researched) derives the strongest grant across a player's
researched techs (None < Coast < Ocean).
- PlayerState gains a cached embark_level field; process_science recomputes it
each turn from the researched set (idempotent → save-load / tech injection
covered). The move handler reads the cache (no per-move tech parsing).
Tests: mc-core EmbarkLevel ordering + mapping; mc-tech embark_level method
(inline web) + a real-data guard that authored naval.json carries the mechanics;
mc-pathfinding 9/9 unchanged. All green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`tools/objectives-report.py --check` was failing (README/objectives.json stale vs
per-objective frontmatter after recent objective edits). Regenerated both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the TechWeb fix: research advanced to 109 techs but every city
still spawned tier-1 dwarf_warrior. Five stacked defects severed the
tech→unit→production chain; all are fixed here. Proven in a 599-turn duel
self-play (seed 42): slot 0 fields a 222-strong tier-8 dwarf_adamantine_champion
army; both clans now CHOOSE tier-2..8 units by tech (was tier-1 only).
1. units_catalog (#[serde(skip)]) was never stamped onto the dispatch
GdPlayerApi — only GdGameState. The harness comment claimed 'both must be
re-stamped at boot' but only did GdGameState. Added
GdPlayerApi::set_units_runtime_catalog_json + a post-load harness re-stamp.
Without it apply_queue_production/try_spawn_unit ran catalog-blind.
2. apply_queue_production classified queued ids as unit-vs-building by a
starts_with("dwarf_") prefix. Replaced with an authoritative
units_catalog.get() lookup (prefix kept only as empty-catalog fallback).
The prefix leaked dwarf_-prefixed BUILDINGS (dwarf_deep_forge) onto the map
as units and misfiled every non-dwarf unit.
3. project_tactical_player hardcoded race_id: None, filtering out every
race_required:dwarf unit. Added PlayerState.race_id (race gates production →
it is sim state, not pure presentation per p2-72a), stamped it in
set_player_presentation_json + the headless harness (sourced from
setup.json::default_race), and projected it.
4. build_unit_catalog loaded faction:"wild" monsters (ancient_hydra, …) and
freepeople into the AI production catalog. With race_required:null they
passed every race filter and, being high-tier, the picker preferred them.
Excluded non-player factions from the production catalog (runtime catalog
still carries them for encounters).
5. Generic warrior/spearmen/archer carried clan_affinity to ALL FIVE clans at
tier 1, so clan_affinity_score=2 dominated tier and every named clan locked
onto tier-1 'warrior'. Cleared their affinity (they are neutral baselines,
not clan signatures).
mc-player-api 132 + mc-state 12 tests green. Known next layer (not this fix):
production VOLUME — try_spawn_unit's empty-queue dwarf_warrior auto-spawn still
floods tier-1, and the loser snowballs; the picker is correct, army composition
tuning is separate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tech unlocks + a unit tech-gate referenced ids absent from the loaded set:
- improvement renames (techs used stale ids): irrigation_channel→irrigation,
stone_road→road, fortress→fort
- improvement refs with no existing target removed: orchard, aqueduct_channel,
bridge, fishing_boats (no such improvement is authored)
- flight units (dwarf_gyrocopter/iron_hawk/mithril_hawk) exist + their unlock
techs are in-scope, but weren't in the manifest's units subscription → added
- dwarf_master_engineer.tech_required deep_engineering (nonexistent) → total_war
(its grand_engineer upgrade-from's gate, which is subscribed)
GUT: test_data_integrity 0 dangling refs (724 passing / 2 failing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two diplomacy models contradicted each other in the written record: p1-01
diplomacy-lite ('all pairs start at war, missing key → war') vs the newer
courier-diplomacy (COMMUNICATIONS.md §War declaration semantics, p3-01:
start at peace, sender enters War on war-dec envelope dispatch). The Rust
implementation follows courier-diplomacy, so that is canonical.
- p1-01: add a SUPERSEDED banner + inline [SUPERSEDED] annotations; history
retained. Canonical rule is start-at-peace, war via dispatched war-dec.
- COMMUNICATIONS.md: fix the one internal inconsistency (§0 said recipient
war state applies at arrival in a way that read as all-effects-at-arrival;
scoped it to recipient-side, cross-linked the sender-on-dispatch exception).
- New objective p3-16 (status partial, owner warcouncil): the AI has no
proactive war-declaration — decide_tactical_actions has no diplomacy step
and there is no DeclareWar in mc-ai, so AI-vs-AI never enters war and clan
aggression personalities don't manifest. Specs the fix to the courier model
(first-contact + military balance + aggression → dispatch_war_declaration)
and notes the stale is_at_war comment as a code-fidelity cleanup.
- Register p3-16 under warcouncil; regen objectives dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Setup gains a name field per slot (humans editable; AI slots named after their
clan via PersonalityAssigner, deduped); player_names flows payload → loading_screen
→ player_name. GameState.randomize_turn_order() adds seeded Fisher-Yates ordering
that next_player() rotates through. Minimap now fogs to the local player's
knowledge. Adds hotseat_view_proof (same game, two fogged worlds) + a turn-order
unit test; refreshes the p3-15 acceptance evidence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two+ humans sharing one device get a pass-the-device hand-off (hotseat_handoff)
gated by GameState.is_hotseat(). Each seat sees only its own view: city_renderer
fogs enemy cities until explored, prologue_overlay_renderer draws only the local
player's opening, and the AI/turn banners no longer stack across hand-offs.
Documents the flow in TURN_SEQUENCE.md, adds a headless handoff proof scene, and
marks p3-15 done (dashboard regenerated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>