feat(@projects/@magic-civilization): add new objective categories

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-01 01:50:19 -04:00
parent f780752a67
commit a7662c2e08
13 changed files with 482 additions and 43 deletions

View file

@ -14,12 +14,30 @@
|---|---|---|---|---|---|
| [p1-29](p1-29.md) | ❌ missing | P1 | Anti-early-domination: lift game-balance gates that p0-01 v1 measured | [combat-dev](../team-leads/combat-dev.md) | 🟢 |
## batch
| ID | Status | Priority | Title | Owner | Blocked |
|---|---|---|---|---|---|
| [p1-45](p1-45-batch-binary-freshness.md) | ❌ missing | P1 | Batch binary freshness: rebuild GDExt before every autoplay batch | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
## ci
| ID | Status | Priority | Title | Owner | Blocked |
|---|---|---|---|---|---|
| [p1-45](p1-45-batch-binary-freshness.md) | ❌ missing | P1 | Batch binary freshness: rebuild GDExt before every autoplay batch | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
## formation
| ID | Status | Priority | Title | Owner | Blocked |
|---|---|---|---|---|---|
| [p0-43](p0-43.md) | ✅ done | P0 | Formation AI — MCTS plans at formation level, not per-unit | [warcouncil](../team-leads/warcouncil.md) | 🟢 |
## gdext
| ID | Status | Priority | Title | Owner | Blocked |
|---|---|---|---|---|---|
| [p1-45](p1-45-batch-binary-freshness.md) | ❌ missing | P1 | Batch binary freshness: rebuild GDExt before every autoplay batch | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
## mcts
| ID | Status | Priority | Title | Owner | Blocked |
@ -56,6 +74,12 @@
|---|---|---|---|---|---|
| [p1-30](p1-30.md) | ❌ missing | P1 | Optimize `_build_tactical_state` — 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate | [warcouncil](../team-leads/warcouncil.md) | 🟢 |
## tooling
| ID | Status | Priority | Title | Owner | Blocked |
|---|---|---|---|---|---|
| [p1-45](p1-45-batch-binary-freshness.md) | ❌ missing | P1 | Batch binary freshness: rebuild GDExt before every autoplay batch | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
## (untagged)
| ID | Status | Priority | Title | Owner | Blocked |
@ -64,6 +88,12 @@
| [g2-02](g2-02-additional-races-oos.md) | ⚫ oos | P3 | Kzzykt playable race — Game 2 (Age of Kzzykt) | — | 🟢 |
| [g2-03](g2-03-green-school-oos.md) | ⚫ oos | P3 | Kzzykt Green school of magic — Game 2 (Age of Kzzykt) | — | 🟢 |
| [g2-04](g2-04-multi-gpu-batch-simulate-oos.md) | ⚫ oos | P3 | Multi-GPU sharding for batch_simulate_gpu — out-of-scope (Game 2) | [warcouncil](../team-leads/warcouncil.md) | 🟢 |
| [g2-05](g2-05-tectonics-lithology-oos.md) | ⚫ oos | P2 | Tectonics + lithology axes for procedural map generation (Game 2) | — | 🟢 |
| [g2-06](g2-06-soil-derivation-oos.md) | ⚫ oos | P2 | Soil derivation layer — emergent soil order from rock + climate + slope (Game 2) | — | 🟢 |
| [g2-07](g2-07-flora-lifecycle-transitions-oos.md) | ⚫ oos | P2 | Flora lifecycle transitions — climate-driven succession over turns (Game 2) | — | 🟢 |
| [g2-08](g2-08-fauna-population-dynamics-oos.md) | ⚫ oos | P2 | Fauna population dynamics — habitat_min, carrying_capacity, prey availability (Game 2) | — | 🟢 |
| [g2-09](g2-09-flora-tolerance-driven-selection-oos.md) | ⚫ oos | P2 | Flora tolerance-driven selection — drought / fire / cold tolerances feed selector (Game 2) | — | 🟢 |
| [g2-10](g2-10-fauna-migration-paths-oos.md) | ⚫ oos | P2 | Fauna migration paths — seasonal range shifts, reintroduction propagation (Game 2) | — | 🟢 |
| [g3-01](g3-01-archons-oos.md) | ⚫ oos | P3 | Archons — Game 3 (Age of Elves) | — | 🟢 |
| [g3-02](g3-02-life-school-oos.md) | ⚫ oos | P3 | Life school spellbook — Game 3 (Age of Elves) | — | 🟢 |
| [g3-03](g3-03-death-school-oos.md) | ⚫ oos | P3 | Death school spellbook — Game 3 (Age of Elves) | — | 🟢 |
@ -159,6 +189,20 @@
| [p1-36](p1-36-ai-personalities-t1-t10-coverage.md) | 🟡 partial | P1 | AI personalities — T1T10 build order coverage + clan_affinity routing | [warcouncil](../team-leads/warcouncil.md) | 🟢 |
| [p1-37](p1-37-mc-ai-clan-affinity-routing.md) | 🟡 partial | P1 | mc-ai clan_affinity routing — Rust AI reads unit clan_affinity at build-decision time | [warcouncil](../team-leads/warcouncil.md) | 🟢 |
| [p1-38](p1-38-biome-economy-coupling.md) | 🟡 partial | P1 | Biome → economy coupling — population & luxury driven by live ecology | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p1-40](p1-40-single-source-of-truth-resources.md) | ✅ done | P1 | Collapse data/<category>/ override layer into single source of truth at resources/ | — | 🟢 |
| [p1-41](p1-41-game-pack-subscription-manifest.md) | ✅ done | P1 | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | — | 🟢 |
| [p1-42](p1-42-ai-full-building-catalog.md) | ❌ missing | P1 | AI must consider the full 155-building catalog, not the hardcoded 8-id ladder | — | 🟢 |
| [p1-43](p1-43-building-stacking-upgrade.md) | ❌ missing | P1 | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | — | 🟢 |
| [p1-44](p1-44-buildings-as-producers.md) | ❌ missing | P1 | Buildings produce units, not the city center — per-building production queues | — | 🟢 |
| [p1-46](p1-46-design-lab-terrain-dimensions.md) | 🟡 partial | P1 | Terrain Dimensions Lab — fix ridginess, bind 149 flora species, add Whittaker plot | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-47](p1-47-river-hydrology-network.md) | 🟡 partial | P1 | River hydrology — D6 flow analysis, hydraulic erosion, multi-hex lakes, cross-tile rivers | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-48](p1-48-flora-species-renderer.md) | 🟡 partial | P1 | Flora species renderer — bind 149 species to world-map tile rendering (single source of truth) | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-49](p1-49-fauna-species-renderer.md) | 🟡 partial | P1 | Fauna species renderer — 61 Game-1 species visible on encounter and lair tiles | [terraformer](../team-leads/terraformer.md) | 🔒 p1-47 |
| [p1-50](p1-50-tectonic-prepass.md) | 🟡 partial | P1 | Tectonic prepass — voronoi plates + boundary classification seeding elevation | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-51](p1-51-worldgen-canonical-design-docs.md) | ✅ done | P1 | Worldgen canonical design docs — author the spec before any Rust | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-52](p1-52-api-wasm-build-fix.md) | ✅ done | P1 | api-wasm build fix — unblock WASM bundle for design-lab WASM consumption | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-53](p1-53-worldgen-layer-pages.md) | 🟡 partial | P1 | Worldgen layer pages — one playground per canonical doc, mirroring the layered Earth model | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p1-54](p1-54-hex-direction-rust-ts-mapping.md) | ✅ done | P1 | Hex direction-index translation — Rust pointy-top axial vs design-app flat-top canvas | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p2-01](p2-01-minimap-improvements.md) | ✅ done | P2 | Minimap — fog reflection and unit markers | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p2-02](p2-02-hud-tooltips.md) | ✅ done | P2 | Tooltips on all HUD elements | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p2-03](p2-03-hotkey-cheat-sheet.md) | ✅ done | P2 | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | 🟢 |
@ -201,10 +245,20 @@
| [p2-32](p2-32-guide-data-driven-enums.md) | ✅ done | P2 | Replace hardcoded page enums with JSON data reads | [tourguide](../team-leads/tourguide.md) | 🟢 |
| [p2-33](p2-33-sound-system-extension.md) | ✅ done | P1 | Sound system extension — categorical fallback, variant pools, per-entity routing | [asset-audio](../team-leads/asset-audio.md) | 🟢 |
| [p2-35](p2-35-palace-evolution-system.md) | ❌ missing | P2 | Palace evolution system — longhouse → great_hall → citadel → grand_citadel + function-shedding | — | 🟢 |
| [p2-36](p2-36-data-resources-building-duplicates.md) | 🟡 partial | P2 | Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/` | — | 🟢 |
| [p2-36](p2-36-data-resources-building-duplicates.md) | ✅ done | P2 | Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/` | — | 🟢 |
| [p2-37](p2-37-react-calculator-metadata-surface.md) | ✅ done | P2 | React calculator UI — surface flavor, lore, clan_affinity, archetype filter | [tourguide](../team-leads/tourguide.md) | 🟢 |
| [p2-38](p2-38-unit-audio-cues-stubs.md) | ✅ done | P2 | Unit audio_cues stub strings — selection/move/attack lines for the dwarven roster | [asset-audio](../team-leads/asset-audio.md) | 🟢 |
| [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | ✅ done | P2 | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | — | 🟢 |
| [p2-43](p2-43-culture-research-completion-event.md) | ❌ missing | P2 | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | 🟢 |
| [p2-44](p2-44-ai-promotion-selection.md) | ❌ missing | P2 | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | 🟢 |
| [p2-45](p2-45-elimination-reconciliation.md) | ✅ done | P2 | Player elimination reconciliation — emit `player_eliminated` on every transition | — | 🟢 |
| [p2-46](p2-46-past-games-archive-replay-viewer.md) | ❌ missing | P2 | Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p2-47](p2-47-in-game-statistics-screens.md) | ❌ missing | P2 | In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p2-48](p2-48-end-of-game-summary-screen.md) | ❌ missing | P2 | End-of-game summary screen — outcome banner, standings, score graph, awards, timeline, footer actions | [shipwright](../team-leads/shipwright.md) | 🟢 |
| [p2-49](p2-49-climate-axes-latitude-continentality.md) | 🟡 partial | P2 | Climate axes refactor — latitude + continentality + zonal winds as first-class per-hex inputs | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p2-50](p2-50-rng-determinism-pin.md) | 🟡 partial | P2 | Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p2-51](p2-51-world-shape-knobs.md) | 🟡 partial | P2 | Player-facing world-shape parameters on new-game screen | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p2-52](p2-52-substrate-flora-cover-ontology-split.md) | ❌ missing | P2 | Split terrain enum into substrate × flora-cover layers (resolve biome ontology) | [terraformer](../team-leads/terraformer.md) | 🟢 |
| [p3-01](p3-01-courier-diplomacy.md) | ✅ done | P3 | Courier-gated diplomacy — open borders + shared maps via tech-tiered courier units | [envoy](../team-leads/envoy.md) | 🟢 |
| [p3-02](p3-02-hybrid-merged-structures.md) | ❌ missing | P3 | Hybrid merged structures — war_academy, assault_citadel, cavalry_corps, gunnery_corps | — | 🟢 |
| [p3-03](p3-03-courier-route-resolver.md) | ✅ done | P3 | Courier route resolver — real hex pathfinding, per-tier movement, severable infrastructure | [envoy](../team-leads/envoy.md) | 🟢 |

View file

@ -82,6 +82,11 @@
| [p1-31](p1-31-split-bundled-building-resources.md) | Split bundled `resources/buildings/<category>.json` into per-file pattern matching `resources/units/` | — | — | 2026-04-27 |
| [p1-34](p1-34-unit-metadata-expansion.md) | Unit metadata expansion — flavor, archetype, promotion_tree, clan_affinity fields | — | [shipwright](../team-leads/shipwright.md) | 2026-04-27 |
| [p1-35](p1-35-unit-lore-paragraphs.md) | Per-unit lore paragraphs — historical/cultural context for the dwarven roster | — | [shipwright](../team-leads/shipwright.md) | 2026-04-27 |
| [p1-40](p1-40-single-source-of-truth-resources.md) | Collapse data/<category>/ override layer into single source of truth at resources/ | — | — | 2026-04-29 |
| [p1-41](p1-41-game-pack-subscription-manifest.md) | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | — | — | 2026-04-29 |
| [p1-51](p1-51-worldgen-canonical-design-docs.md) | Worldgen canonical design docs — author the spec before any Rust | — | [terraformer](../team-leads/terraformer.md) | 2026-04-30 |
| [p1-52](p1-52-api-wasm-build-fix.md) | api-wasm build fix — unblock WASM bundle for design-lab WASM consumption | — | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p1-54](p1-54-hex-direction-rust-ts-mapping.md) | Hex direction-index translation — Rust pointy-top axial vs design-app flat-top canvas | — | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-06](p2-06-export-pipeline.md) | Export pipeline for Windows / macOS / Linux | — | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p2-28](p2-28-sprite-provenance-ledger.md) | Sprite provenance ledger — LICENSES.md per-file attribution | — | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 |
| [p2-33](p2-33-sound-system-extension.md) | Sound system extension — categorical fallback, variant pools, per-entity routing | — | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
@ -118,9 +123,11 @@
| [p2-30](p2-30-guide-shared-primitives.md) | Consolidate duplicate page styled-components into shared PagePrimitives | — | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p2-31](p2-31-guide-url-bound-state.md) | Migrate guide filter + tab state from useState to URL search params | — | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p2-32](p2-32-guide-data-driven-enums.md) | Replace hardcoded page enums with JSON data reads | — | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p2-36](p2-36-data-resources-building-duplicates.md) | Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/` | — | — | 2026-04-29 |
| [p2-37](p2-37-react-calculator-metadata-surface.md) | React calculator UI — surface flavor, lore, clan_affinity, archetype filter | — | [tourguide](../team-leads/tourguide.md) | 2026-04-27 |
| [p2-38](p2-38-unit-audio-cues-stubs.md) | Unit audio_cues stub strings — selection/move/attack lines for the dwarven roster | — | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
| [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | — | — | 2026-04-27 |
| [p2-45](p2-45-elimination-reconciliation.md) | Player elimination reconciliation — emit `player_eliminated` on every transition | — | — | 2026-04-30 |
## P3

View file

@ -1,13 +1,13 @@
{
"generated_at": "2026-04-29T19:17:29Z",
"generated_at": "2026-05-01T05:47:25Z",
"totals": {
"done": 108,
"done": 115,
"in_progress": 1,
"partial": 11,
"partial": 19,
"stub": 1,
"missing": 12,
"oos": 20,
"total": 153
"missing": 22,
"oos": 26,
"total": 184
},
"objectives": [
{
@ -917,9 +917,171 @@
"status": "partial",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-27",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "During p1-29 Round 3-5, warcouncil added a per-yield difficulty multiplier framework (gold_mult, culture_mult, luxury_mult, research_mult, production_mult, yield_per_turn_growth) plus a symmetric player handicap (Easy = player gets Hard-AI bonuses). Per Rail-1, the multiplier APPLICATION should live in Rust crates, not in GDScript turn_processor.gd / economy.gd / turn_processor_helpers.gd.\n\n**Gold yield port: DONE 2026-04-27.** `EconomyParams.yield_mult` field added to `api-gdext/src/lib.rs:2962-2992` with serde default 1.0; `GdEconomy::process_turn` now scales gross income before netting expenses. GDScript `economy.gd::_build_params_json` injects `yield_mult` from `GameState.get_effective_yield_mult(player, \"gold\")`; the GDScript-side multiplication is deleted. Validated via 10-seed Hard batch `.local/iter/p1-31-r5-hard-20260427_044618/` — 4/5 quality gates PASS (up from 3/5), clan diversity up from 3 to 5 distinct winners.\n\n**Research yield port: DONE 2026-04-27.** Added `process_research(player_json, yield_json, sci_modifier)` passthrough at `knowledge_web.gd:152` (delegates to `_bridge.call(\"process_research\", ...)`) and exposed at `tech_web.gd:39`. Refactored `turn_processor.gd::_process_research:143` to delegate fully — assembles JSON inputs (player_dict + per-city yields_arr), calls `tw.process_research()`, only handles completion side-effects (school_locked emit, _form_high_archon, tech_researched signal, resource reveals). No Rust rebuild needed (GdTechWeb::process_research already had sci_modifier as a direct parameter; only the wrapper-layer plumbing was missing).\n\nValidation: `.local/iter/p1-39-r6-hard-20260427_054348/` (10-seed Hard batch). 4/5 quality gates PASS: median winner_tier_peak=4.5 PASS (was 3 FAIL in R5 — research port LIFTED this), tier_peak_gap=5.0 FAIL (was 3.5 PASS — gates alternate, total still 4/5), max_peak_unit 10/10 PASS, wonders 7/10 PASS, combats 454 PASS. **All 10 games completed (vs 8/10 in R5)**, **6 distinct winners** (max diversity for 5-clan game).\n\n**Culture yield port: ATTEMPTED 2026-04-27, REVERTED.** Added `GdCity::process_culture_with_modifier(tile_yields_json, total_pct)` to api-gdext/src/lib.rs:1399 that mirrors the GDScript flow (process_culture → check raw_gain → add bonus → recheck can_expand). Math nominally identical. But R8 batch (Rust culture port) diverged from R6 (GDScript) on every seed (e.g. seed 1: R6=T251/tier=6/wonders=10, R8=T111/tier=2/wonders=0). R9 isolation batch (revert culture path on current source tree) reproduced R6 EXACTLY per-seed (`.local/iter/p1-39-r9-revert-hard-20260427_213224/`), proving the divergence is from the Rust culture path itself, NOT from other landed code (courier diplomacy, building ID reconciliation) between R6 and R7/R8. Floating-point intermediate values likely differ between the in-Rust mutation sequence and the GDScript-Variant-roundtrip mutation sequence; the difference cascades into different border-expansion timing → different tile ownership → entirely different game trajectories. Culture path REVERTED to GDScript (p1-39 stays partial — gold + research ported, culture deferred). Future port should investigate Rust f64 vs Variant FLOAT round-trip semantics, or alternative scope (e.g. apply difficulty modifier in GdCity::process_culture itself with an optional parameter, leaving the building-bonus math out of scope).\n\nThe fix for both: add `process_research` and `process_culture` passthrough methods to the GDScript wrapper layers, refactor the GDScript callers to delegate fully (matching the gold-port pattern). Estimated 2-3 hours including parity validation."
"summary": "During p1-29 Round 3-5, warcouncil added a per-yield difficulty multiplier framework (gold_mult, culture_mult, luxury_mult, research_mult, production_mult, yield_per_turn_growth) plus a symmetric player handicap (Easy = player gets Hard-AI bonuses). Per Rail-1, the multiplier APPLICATION should live in Rust crates, not in GDScript turn_processor.gd / economy.gd / turn_processor_helpers.gd.\n\n**Gold yield port: DONE 2026-04-27.** `EconomyParams.yield_mult` field added to `api-gdext/src/lib.rs:2962-2992` with serde default 1.0; `GdEconomy::process_turn` now scales gross income before netting expenses. GDScript `economy.gd::_build_params_json` injects `yield_mult` from `GameState.get_effective_yield_mult(player, \"gold\")`; the GDScript-side multiplication is deleted. Validated via 10-seed Hard batch `.local/iter/p1-31-r5-hard-20260427_044618/` — 4/5 quality gates PASS (up from 3/5), clan diversity up from 3 to 5 distinct winners.\n\n**Research yield port: DONE 2026-04-27.** Added `process_research(player_json, yield_json, sci_modifier)` passthrough at `knowledge_web.gd:152` (delegates to `_bridge.call(\"process_research\", ...)`) and exposed at `tech_web.gd:39`. Refactored `turn_processor.gd::_process_research:143` to delegate fully — assembles JSON inputs (player_dict + per-city yields_arr), calls `tw.process_research()`, only handles completion side-effects (school_locked emit, _form_high_archon, tech_researched signal, resource reveals). No Rust rebuild needed (GdTechWeb::process_research already had sci_modifier as a direct parameter; only the wrapper-layer plumbing was missing).\n\nValidation: `.local/iter/p1-39-r6-hard-20260427_054348/` (10-seed Hard batch). 4/5 quality gates PASS: median winner_tier_peak=4.5 PASS (was 3 FAIL in R5 — research port LIFTED this), tier_peak_gap=5.0 FAIL (was 3.5 PASS — gates alternate, total still 4/5), max_peak_unit 10/10 PASS, wonders 7/10 PASS, combats 454 PASS. **All 10 games completed (vs 8/10 in R5)**, **6 distinct winners** (max diversity for 5-clan game).\n\n**Culture yield port: COMPLETED 2026-04-29.** Root cause of R7/R8 divergence was NOT floating-point semantics. Two bugs caused the apparent divergence:\n\n1. **Stale GDExtension binary on apricot** — R7 and R8 batches ran against an old `.so` that lacked `process_culture_with_modifier`. Godot emitted `SCRIPT ERROR: Invalid call. Nonexistent function 'process_culture_with_modifier'` on every culture call per turn, culture never accumulated, games ended at T111 vs R6/R9's T251. Evidence: `game.log` in both R7 and R8 dirs contains this exact error; R9 (reverted to `process_culture`) worked because that symbol WAS in the old binary.\n\n2. **Missing GDScript bridge wrappers** — `city.gd` and `city_rust_bridge.gd` delegated `process_culture` but had no `process_culture_with_modifier` method. The delegation chain from `turn_processor.gd` → `city.gd` → `city_rust_bridge.gd` → `_gd_city.call(...)` was incomplete.\n\nThe \"f64 vs Variant FLOAT round-trip\" hypothesis in the revert comment was incorrect. Godot 4 Variant FLOAT is f64 (lossless), and the Rust math is algebraically identical to the original GDScript.\n\nFix (2026-04-29): added `process_culture_with_modifier` wrappers to `city.gd` and `city_rust_bridge.gd`; switched `turn_processor.gd::_process_culture` to call `c.process_culture_with_modifier(tile_json, total_pct)`; rebuilt GDExtension on apricot (binary dated 2026-04-29 21:43). R12 validation run: zero `process_culture_with_modifier` errors. Games terminate at T22 due to an unrelated AI regression (`set_map`/`captured_turn` errors from p1-29 changes) not from culture.\n\nAll three yield types (gold, research, culture) are now ported to Rust. The one remaining open acceptance bullet is replay parity — blocked on unrelated p1-29 AI regressions, not on the culture port itself."
},
{
"id": "p1-40",
"title": "Collapse data/<category>/ override layer into single source of truth at resources/",
"priority": "p1",
"status": "done",
"scope": "game1",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "Today `public/games/age-of-dwarves/data/{units,buildings,techs}/` mirrors `public/resources/{units,buildings,techs}/` with override semantics: the loader walks resources/ first, then game/data/ overwrites by id. This created three real bugs in the last few sessions:\n1. Audit blind-spot: 9 \"missing\" buildings and 6 \"missing\" food/processing buildings turned out to live in resources/buildings bundled files that the per-file audit missed.\n2. Silent semantic drift: 14 building IDs are defined in both layers with different cost/tech/effects; the data/ version wins by accident-of-loader-order.\n3. Broken tech gates: 6 of 8 ordinary-building duplicates have `tech_required` in resources/ pointing at non-existent techs (`military_doctrine`, `smelting`, `husbandry`, `scholarship`, `ancestor_rites`, `masonry`, `mathematics`). The data/ overrides are the only thing keeping those buildings buildable.\n\nThe right architecture is one source of truth at `public/resources/<category>/`, with `public/games/<game>/` carrying only **game-pack-specific configuration** (clan personalities, setup, vocab, difficulty) and a manifest declaring which resource IDs the game subscribes to. No more override layer.\n\nThis objective is the **safe mechanical phase** — move all entity files to resources/ canonical locations and resolve the duplicates. The behavioral phase (subscription manifest + loader filter) splits to `p1-41`."
},
{
"id": "p1-41",
"title": "Game-pack subscription manifest + loader filter (Phase B of resources/ unification)",
"priority": "p1",
"status": "done",
"scope": "game1",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "Phase A (`p1-40`) collapsed the data/ override layer into single source of truth at `public/resources/<category>/`. All 155 unit IDs and 159 building IDs now live in resources/, one file each. The data loader still walks both layers — `resources/` then `data/<category>/` — but the data/ side is now empty for those categories.\n\nPhase B introduces a per-game **subscription manifest** that declares which resource IDs each game uses, and a loader filter that restricts the in-memory `_data` dict to that subset. This is what makes the architecture viable when a second game exists: Age of Dwarves subscribes only to dwarf-relevant content; Age of Kzzykt (Game 2) subscribes to its own roster without inheriting dwarf-specific entities by accident.\n\nFor Game 1 alone, this objective is **architecturally correct but functionally redundant** — Age of Dwarves currently subscribes to 100% of resources/. The objective is filed at `p1` (not deferred to `p3`) because Game 2 work begins as soon as Game 1 ships EA, and the subscription mechanism needs to exist before content for Game 2 starts landing in `resources/<category>/` (otherwise Game 2 races and units would automatically appear in Age of Dwarves saves)."
},
{
"id": "p1-42",
"title": "AI must consider the full 155-building catalog, not the hardcoded 8-id ladder",
"priority": "p1",
"status": "missing",
"scope": "game1",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "`mc-ai/tactical/production.rs::ids` hardcodes 8 building/unit IDs (`WARRIOR`, `WORKER`, `FOUNDER`, `WALLS`, `FORGE`, `CASTLE`, `MARKETPLACE`, `GRANARY`) and the priority ladder picks among them. The human player sees all 155 buildings in the city UI (`city_buildable_helper.gd` iterates `DataLoader.get_all_buildings()` and gates by `city.can_build()`); the AI's mental model is a ~5% slice of that catalog.\n\nResult: AI never builds `library`, `temple`, `colosseum`, `barracks`, `siege_workshop`, `harbor`, `aqueduct`, any of the 62 wonders, or any of the 32 military buildings beyond walls. Production decisions are silently flat across most of the tech tree.\n\nThe fix is to evaluate the full catalog the same way `pick_best_melee` evaluates units: filter by tech / race / resource gates, score by category × personality × city-state, return best. Catalog comes from the subscription manifest (`p1-41`) so the AI honors the same scope as the human."
},
{
"id": "p1-43",
"title": "Building stacking — per-category upgrade chains (military / science / culture / production / etc.)",
"priority": "p1",
"status": "missing",
"scope": "game1",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place — `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library → scriptorium → academy), culture stacks (monument → bardic_circle → great_hall), production stacks (forge → iron_forge → grand_forge), etc. This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 → Lv2 → Lv3 chains within one slot.\n\nThe existing data already implies category-tier chains via the `tier` + `category` fields:\n\n| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |\n|---|---|---|---|\n| Production | `forge` t1 | (gap — `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |\n| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |\n| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |\n| Military | `barracks` t1 | (gap — `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |\n| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |\n| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |\n| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |\n| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |\n\nThe stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks → Infantry\"), avoids per-tile placement complexity for a v1."
},
{
"id": "p1-44",
"title": "Buildings produce units, not the city center — per-building production queues",
"priority": "p1",
"status": "missing",
"scope": "game1",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "User direction (2026-04-29): \"it would make sense to build those units in the appropriate buildings rather than the city center.\"\n\nToday every city has **one** production queue (`city.gd:57 production_queue: Array`) where the player picks \"warrior\", \"barracks\", \"wonder\", anything goes through the same FIFO. Building selection just gates the menu — the building doesn't actually *do* anything when production happens.\n\nThe user's model — and the model `PRODUCTION_CHAIN.md` already describes — is that buildings are **producers**:\n\n> Every citizen is a resource pulled between three competing demands:\n> - **Construction** — builds new buildings, upgrades existing ones (investment)\n> - **Buildings** — produces units, research, culture, equipment (output)\n> - **Tiles** — produces food, raw materials, gold (sustenance)\n\nConcretely:\n- **Barracks** has its own queue producing infantry / armory / military_academy lineage units.\n- **Stable** queues cavalry units.\n- **Siege Workshop** queues siege units.\n- **Library** queues sages / cartographers / engineers (science civilians) and accumulates research per turn.\n- **Temple** queues battle priests and accumulates culture/happiness.\n- **Harbor** queues naval units (and gates them — see `p1-33`).\n- **Airfield** queues aerial units.\n- **Construction queue** (city-level, distinct from building queues) builds NEW buildings and upgrades existing ones.\n\nEach producer building runs its queue independently per turn. Citizens / production allocation gets split across buildings (per `PRODUCTION_CHAIN.md` \"Three-Way Tension\"). The city's single global queue dies; build-vs-train becomes a structural distinction not a queue-item distinction.\n\nThis is a major engine refactor. Touches: `mc-city`, `mc-turn`, `city.gd`, `city_screen.gd`, `city_buildable_helper.gd`, save schema, AI's `production.rs` (now picks per producer-building, not per city), and the new themed-unit catalog (battle_priest, sage, bard, merchant, etc. — see `p1-43` open question 1)."
},
{
"id": "p1-45",
"title": "Batch binary freshness: rebuild GDExt before every autoplay batch",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": "simulator-infra",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Autoplay batches (`tools/autoplay-batch.sh`) run Godot against the installed GDExtension binary\n(`engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so`). When multiple agents or\ndevelopers land Rust changes between batches, the `.so` silently goes stale — the GDScript wrappers\ncall methods that don't exist yet in the binary, producing cryptic errors like:\n\n```\nInvalid call. Nonexistent function 'process_culture_with_modifier' in base 'RefCounted (City)'.\n```\n\nThis was observed on 2026-04-30 during the p1-29 anti-snowball batch cycle when the culture-port\nteammate's `process_culture_with_modifier` and p1-30's `set_map`/`update_tile` GDExt methods were\nlanded after the last build, causing R12 test failures at T22."
},
{
"id": "p1-46",
"title": "Terrain Dimensions Lab — fix ridginess, bind 149 flora species, add Whittaker plot",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "The Terrain Dimensions Lab at `/world-gen/lab` currently\nclassifies the 17 base biomes and renders cross-tile flora/minerals/fauna\noverlays driven by 4 biome sliders + 3 toggleable overlay layers. Three\nknown gaps:\n\n1. **Ridginess slider has zero effect at default elevation 0.65.** The\n classifier (`terrain.ts:118`) only consults ridginess when\n `elevation > 0.85 ∧ ridginess > 0.92` for the volcano case. Anywhere\n else, the slider does nothing — the user verified this on the live\n lab and flagged it.\n2. **The lab loads 0 of 149 flora species.** Trees / shrubs / ground\n cover are generic per terrain. The 149 species in\n `public/resources/ecology/flora/species/*.json` carry rich schema\n (`biomes[]`, `lineage`, `tags` with layer info, `quality_tier`,\n `canopy_contribution`) that the lab ignores entirely.\n3. **No Whittaker T×P plot inset** — the user can move sliders but has\n no visual map of where they are in biome space.\n\nThis objective is the **integration / proof surface** of the Wave-E\nterraformer bundle. It must land **after** p1-50, p2-49, p2-50 (Wave A),\np1-47 (Wave B), and p1-48/p1-49 (Wave C). The Whittaker plot, ridginess\nbehaviour, and flora binding all consume the post-refactor axes."
},
{
"id": "p1-47",
"title": "River hydrology — D6 flow analysis, hydraulic erosion, multi-hex lakes, cross-tile rivers",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Water bodies in the current map are per-hex terrain types\n(`ocean`, `coast`, `lake`, `inland_sea`) with no connectivity. Real\ngeographic water is **topological**: rivers are DAGs from headwaters to\nocean, lakes are multi-hex polygons, coastlines are polylines along\nland/water borders.\n\nThe data already encodes this in:\n\n- `public/games/age-of-dwarves/data/terrain/terrain_blends.json` —\n `riverside_forest`, `shore`, `cliff`, `bog_edge` ecotones (cross-tile)\n- 10+ fauna species with `river` / `lake` / `wetland` biomes\n (bald_eagle, alligator, otter, kingfisher, beaver, etc.)\n- 4 riparian flora species: `lotus`, `papyrus`, `giant_water_lily`,\n `pioneer_sedge`\n\nBut there is **no rendering pass** that uses the topology. This\nobjective adds:\n\n1. A single hydraulic-erosion iteration that carves valleys before\n drainage analysis (so rivers sit in valleys, not on ridges).\n2. D6 flow-direction analysis on the eroded elevation grid.\n3. Drainage-area accumulation, lake-basin filling.\n4. A cross-tile renderer that draws rivers as bezier paths through\n hex edges and lakes as multi-hex continuous fills.\n\nHydrology computes topology only. Riparian feedback into flora\nselection lives in p1-48; aquatic-domain fauna gating lives in p1-49.\n\n**Wave B delivery (2026-04-30):** All algorithmic bullets (erosion, D6 flow,\nPlanchon-Darboux, Strahler, riparian BFS, coarse-grid path) and bridge surfaces\n(GDExtension + WASM) implemented. TS twin `hydrology.ts` deleted per Rail 1.\nVisual-proof bullet relocated to p1-46 Wave E (lab integration captures the screenshot).\nAll `cargo test -p mc-mapgen` pass. `cargo check --workspace` clean."
},
{
"id": "p1-48",
"title": "Flora species renderer — bind 149 species to world-map tile rendering (single source of truth)",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "`public/resources/ecology/flora/species/*.json` defines **149 species**\nwith rich schema:\n\n- `biomes[]` — which terrain types support the species\n- `tags[]` including layer (`layer_canopy` / `layer_understory` /\n `layer_ground` / `layer_fungal`) and structure (`structure_woody` /\n `conifer` / `broadleaf` / `evergreen`)\n- `lineage` — taxonomic group (`conifers`, `broadleaf_trees`,\n `tropical_broadleaf`, `cacti`, `palms`, `aquatic_plants`,\n `mosses_lichens`, etc.)\n- `quality_tier` (010), `canopy_contribution`,\n `undergrowth_contribution`, `fungi_contribution`\n- `drought_tolerance`, `fire_resistance`, `growth_rate`\n\nWave C landed the Rust selector, WASM + GDExt bridges, and integration tests.\nThe visual-proof bullets (tooltip, lab integration) are Wave-E work — relocated\nto p1-46. The TS twin (`floraSpecies.ts`) was deleted; `Lab.tsx` has\n`TODO(p1-46)` markers where the WASM calls will be wired in.\n\n**Single source of truth (Rail 1):** the selector is implemented ONCE\nin `mc-ecology` (Rust), exposed to Godot via GDExtension and to the\ndesign-lab via WASM. NO TypeScript twin."
},
{
"id": "p1-49",
"title": "Fauna species renderer — 61 Game-1 species visible on encounter and lair tiles",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [
"p1-47"
],
"summary": "`public/games/age-of-dwarves/data/manifests/fauna.json` whitelists **61\nGame-1 species** with rich JSON schema in\n`public/resources/ecology/fauna/species/*.json`:\n\n- `domain` (land / air / marine / freshwater)\n- `trophic_level` (apex_predator / predator / herbivore / omnivore)\n- `biomes[]`\n- `prey[]` — actual food-web edges\n- `ecology_tier` (110 rarity / strength)\n- `forms_lairs` + `lair_type`\n- `lineage` (canines, ursids, cervids, raptors, etc.)\n- `traits[]` including `size_*`\n\nWave C landed the Rust selector with full trophic and domain rules,\n12-cluster glyph table, WASM + GDExt bridges, and integration tests.\nVisual-proof bullets (lair tile silhouette, proof scene screenshot)\nare Wave-E work — relocated to p1-46. The TS twin (`faunaSpecies.ts`)\nwas deleted; `Lab.tsx` has `TODO(p1-46)` markers."
},
{
"id": "p1-50",
"title": "Tectonic prepass — voronoi plates + boundary classification seeding elevation",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "The current `mc-mapgen` elevation field is pure fBm noise. Continents\nare amorphous, mountain ranges are noise-shaped blobs, and there is no\ngeological reason for a peak to be where it is. Real continents have\n**plate boundaries**: mountains arc along convergent edges, rifts and\nmid-ocean ridges along divergent edges, transform faults run linear.\n\nThis objective adds a lo-fi tectonic prepass that runs in <500 ms on a\n200×200 map and biases the existing fBm field. Full multi-step plate\nsimulation (`g2-05-tectonics-lithology`) stays deferred to Game 2; this\nis the cheap version that captures 80% of the visual win.\n\nThe prepass also produces `mountain_proximity` and `coast_proximity`\nfields that p2-49 (rain shadow) and p1-47 (drainage divides) consume\nas first-class inputs."
},
{
"id": "p1-51",
"title": "Worldgen canonical design docs — author the spec before any Rust",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "The Terraformer bundle (`p1-46`…`p2-51`) is an 8-objective procedural\nterrain pipeline spanning tectonics, hydrology, climate, ecology, RNG\ndeterminism, world-shape presets, and the design-app Terrain Dimensions\nLab. Per **Rail 1** (`Rust is the simulation source of truth`,\nCLAUDE.md:13) and the project's three-tier doc system (canonical →\nengineering → JSON, per `.project/designs/README.md`), every Rust crate\nmust mechanically implement an authored canonical specification — never\nthe reverse.\n\nThis Wave-0 objective authors the **7 canonical design docs** at\n`public/games/age-of-dwarves/docs/terrain/` (and `…/docs/` for ecology\nbinding) that gate all subsequent Wave AE implementation. Each Rust\ncrate's rustdoc references the canonical doc it implements; bridges\n(`api-gdext`, `api-wasm`) and consumers (Godot, design lab) consume\nwhat the canonical specs declare — not what someone interpolated.\n\nThe current TypeScript twins (`floraSpecies.ts`, `hydrology.ts`,\n`faunaSpecies.ts`) under `.project/designs/app/src/utils/worldGen/`\nexist precisely because this stage was skipped. Authoring the canonical\ndocs first prevents that recurrence."
},
{
"id": "p1-52",
"title": "api-wasm build fix — unblock WASM bundle for design-lab WASM consumption",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "**Resolved (2026-05-01).** Workspace `rand` downgraded from `0.9` to `0.8` in\n`[workspace.dependencies]`. This pulls `rand_core 0.6.x` → `getrandom 0.2.x`\n(known-good on wasm32 with the `js` feature already present in `api-wasm/Cargo.toml`),\nbreaking the dependency chain that forced `getrandom 0.3.x`. WASM build exits 0;\nall three tile_*_json exports confirmed in the `.d.ts`. All tests pass.\n\n`bash src/simulator/build-wasm.sh` previously failed with an upstream\ncompilation error in the `getrandom 0.3.x` line on `wasm32-unknown-unknown`\n(both `0.3.3` and `0.3.4` reference `backends::inner_u32` /\n`backends::inner_u64` symbols that don't exist in the wasm32 backend,\neven with `--cfg getrandom_backend=\"wasm_js\"`).\n\nRoot of the dep chain (from `cargo tree -p magic-civ-physics --target\nwasm32-unknown-unknown -i getrandom@0.3.4`):\n\n```\ngetrandom v0.3.4\n└── rand_core v0.9.5\n ├── rand v0.9.2\n │ └── mc-trade v0.1.0\n │ ├── mc-ai v0.1.0 → mc-turn → mc-mapgen → magic-civ-physics\n │ └── mc-turn ─────────────^\n └── rand_chacha v0.9.0 → rand v0.9.2\n```\n\nWave A pinned the workspace to `rand = \"0.9\"` (in `mc-trade`) which\nforces `rand_core 0.9.x` which forces `getrandom 0.3.x`. The native\nbuild (cargo test on the workspace) compiles fine because native\n`getrandom` doesn't take the broken backends path; only the\n`wasm32-unknown-unknown` target hits it.\n\nThis blocks the **rolling Wave E** plan: every Wave AD objective\nships Rust + GDExt + WASM bridges, but the WASM bundle can't be built,\nso the design lab can't consume WASM-built selectors and is stuck on\nthe TS twins. By extension, p1-46 (Wave E lab integration) cannot\nland until WASM builds are green.\n\nThis objective is **Wave A.5** — sits between Wave A (foundation\ncrates) and Waves BE (consumers). Lands as a small, focused workspace\nfix."
},
{
"id": "p1-53",
"title": "Worldgen layer pages — one playground per canonical doc, mirroring the layered Earth model",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "The 7 canonical worldgen design docs at\n`public/games/age-of-dwarves/docs/terrain/` (plus\n`ECOLOGY_BINDING.md`) describe the layered Earth model the simulator\nimplements: tectonics → erosion → hydrology → climate → biome →\nflora → fauna, with RNG and world-shape-presets as cross-cuts. Each\ndoc is a 1-to-1 contract for one Rust crate's behaviour.\n\nThe design app at `.project/designs/app/` currently has ONE\nworldgen-related interactive page (`/world-gen/lab`, soon to\nbe renamed `/world-gen/lab` per `p1-46`). That page tries to render\nEVERYTHING in one canvas, mixing tectonic plates, climate, hydrology,\nflora, and fauna into a single overloaded view.\n\nThis objective splits the design app's worldgen surface into **6\nfocused playground pages** — one per canonical doc — plus the existing\nintegration page (which `p1-46` covers separately). Each layer page\nis the **visual + interactive companion** to its canonical doc.\nTogether they make the layered Earth model navigable: a contributor\nopens `/world-gen/tectonics`, sees the canonical doc rendered inline\nbeside a Voronoi-plate canvas driven by the same Rust crate Godot\nuses, moves a slider, and watches the spec come alive."
},
{
"id": "p1-54",
"title": "Hex direction-index translation — Rust pointy-top axial vs design-app flat-top canvas",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "Two related hex-direction bugs surfaced after p1-53 Stage 2 wired the\nHydrology page to live Rust output:\n\n**Bug 1 — `Hydrology.tsx` flow-arrow rendering (lines 1113):**\n```typescript\nconst FLOW_DX = [1, 1, 0, -1, -1, 0];\nconst FLOW_DY_EVEN = [0, -1, -1, 0, 1, 1];\nconst FLOW_DY_ODD = [0, -1, -1, 0, 1, 1];\n```\n`FLOW_DY_EVEN` and `FLOW_DY_ODD` are identical. For any odd-q offset\ngrid they MUST differ — that's the entire point of the parity table.\nResult: flow-arrow overlay points to the wrong neighbour for half the\nhexes (whichever parity wasn't hand-checked when the values were\ncopied).\n\n**Bug 2 — Rust ↔ TS direction-label mismatch:**\n`mc-core/algorithms/hex.rs::AXIAL_DIRECTIONS` documents directions\n05 as `E, NE, NW, W, SW, SE` — the pointy-top axial convention with\nE and W as neighbour directions. But the design-app canvas uses\n**flat-top** geometry (`hexCorners` places corners at angles 0°, 60°,\n…, 300° → corners at E and W, edges/neighbours at NE/SE/S/SW/NW/N).\n\nSo when `WasmGrid::tile_hydrology(col, row).flow_out` returns `0`\n(\"E\" in Rust), the design-app canvas should interpret that as the\nflat-top **SE** direction (Rust's \"E\" is `(col+1, row)` axial, which\nin flat-top with odd cols shifted DOWN lands SE of the source cell\nfor even cols).\n\nThe two conventions need a documented translation table that both the\ncanvas and any future TS consumer of WASM hex output reads from.\n\n`hexCanvas.ts::EDGE_CORNERS` and `neighborCoords` were corrected\n2026-05-01 (out of band, before this objective existed) to use\nflat-top semantics — that fix is correct for design-app-only logic\nthat doesn't cross the WASM bridge. The remaining gap is the\n**bridge-crossing** label translation."
},
{
"id": "p2-06",
@ -1031,6 +1193,66 @@
"blocked_by": [],
"summary": "`AudioManager` (`p0-21`, done) ships 10 SFX events and 6 era-keyed music\ntracks. The current manifest is one stream per id, no variation, no\nfallback chain, and no story for the 91 units / 65 buildings / 600\nfauna species the game ships with — every entity that ever wants a\ndistinct sound has to add a hand-authored entry, which doesn't scale to\nlaunch.\n\nThis objective extends the manifest schema and `audio_manager.gd` so\nthe asset pack tracked by `p2-16` can land cleanly:\n\n* **Variant pools** — an entry can list `streams[]` with 2-3 paths;\n the player picks one uniformly to break repetition.\n* **Pitch jitter** — optional ±X% pitch randomisation per play.\n* **Categorical fallback ladder** — `play_for_entity(entity_id,\n event_kind)` resolves `<entity>.<event>` → `<category>.<event>` →\n `<event>`, so a fresh unit with no bespoke sound automatically routes\n to its category bucket (`unit.melee.attack`, `building.production.complete`,\n `fauna.apex.roar`, etc.). The category is read from existing JSON\n fields (`unit_type` / `category` / `trophic_class`) — no new schema\n fields on units / buildings / wilds.\n* **EventBus expansion** — wire the additional signals that already\n exist on `event_bus.gd` but aren't routed to audio yet\n (`combat_started`, `unit_destroyed`, `unit_promoted`, `city_grew`,\n `city_starved`, `golden_age_started`, `golden_age_ended`,\n `border_expanded`, `culture_researched`, `wild_creature_spawned`,\n `weather_event`, `tech_research_started`).\n\nThis is a **schema-and-code** objective. No `.ogg` files land here —\nthose are `p2-16`'s responsibility, which is `blockedBy: [p2-33]` so\nthe dependency-aware ordering surfaces this work first."
},
{
"id": "g2-05",
"title": "Tectonics + lithology axes for procedural map generation (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Game 1 ships with elevation as the single geological axis. Game 2's\nexpanded scope (additional races, leylines, planet-scale events) calls\nfor plate-tectonic simulation:\n\n- Per-hex `plate_id`, `plate_type`\n (cratonic / passive_margin / active_margin / volcanic_arc / rift /\n hotspot)\n- `lithology` (granite / basalt / limestone / sandstone / volcanic /\n metamorphic)\n\nDrives mountain shape, mineral suite filtering, soil-order derivation\n(g2-06), and volcanic-event placement."
},
{
"id": "g2-06",
"title": "Soil derivation layer — emergent soil order from rock + climate + slope (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Game 1's biome classifier has no soil layer; tile yield depends on\nbiome + deposits only. Real ecological behaviour requires soil — a\nMollisol grassland is far more productive than an Aridisol grassland.\n\nSoil is emergent from\n`lithology + mean_T + mean_P + slope + flora_succession_stage + time`\nand outputs one of the 8 USDA soil orders (or simplified subset):\n\n- Mollisol (grassland, deep dark, fertile)\n- Spodosol (boreal, acidic, leached)\n- Oxisol (tropical, ancient, weathered, low nutrient)\n- Aridisol (desert, calcium-rich, shallow)\n- Histosol (peat, saturated)\n- Andisol (volcanic, light, exceptional)\n- Inceptisol (young, weakly developed)\n- Entisol (very young, alluvial)\n\nGame 2 adds the soil layer as a drop-in fold over yield, fertility, and\nflora `growth_rate`."
},
{
"id": "g2-07",
"title": "Flora lifecycle transitions — climate-driven succession over turns (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "Every flora species in `public/resources/ecology/flora/species/*.json`\ncarries a `lifecycle` object with `formed_by[]` (what it grows from)\nand `transforms[]` (what it becomes when conditions change). Example\nfrom `european_beech.json::lifecycle.transforms[0]`:\n\n```json\n{\n \"to\": \"forest\",\n \"condition\": \"climate\",\n \"climate_field\": \"temperature\",\n \"climate_op\": \">=\",\n \"climate_value\": 0.62,\n \"climate_sustained_turns\": 200,\n \"description\": \"Warming reduces competitive advantage — replaced by more drought-tolerant species\"\n}\n```\n\n149 of 149 flora species carry this schema, but **no runtime consumer\nexists**. The Wave-C ecology selector (`mc-ecology::flora_select`) is\nsingle-pick from a static index — it doesn't read `lifecycle.transforms`\nor evolve flora between turns. The schema is data-rich, code-poor.\n\nThis objective wires the lifecycle machinery into `mc-climate`'s per-turn\ntick (already revived by `p0-31`), reading `climate_field` from\n`TileMeta` and triggering species transitions when sustained-turns\nthreshold is met. Closes the gap the user flagged: *\"all fauna and flora\nevolve from and relate to specific biomes... all the complexity of our\nnatural earth systems\"*."
},
{
"id": "g2-08",
"title": "Fauna population dynamics — habitat_min, carrying_capacity, prey availability (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "Every Game-1 fauna species in `public/resources/ecology/fauna/species/*.json`\nhas `habitat_min` (minimum habitat-quality threshold for survival) and\n`carrying_capacity` (max population fraction the tile supports). Plus\n`prey[]` arrays for 373 of 589 species form a real food-web DAG.\n\nExample from `grey_wolf.json`: `habitat_min = 0.2`, `carrying_capacity = 0.15`, `prey = [moose, red_deer, musk_ox, european_rabbit, wild_boar]`.\n\nThe Wave-C selector (`mc-ecology::fauna_select`) consumes only:\n- `biomes[]` for index keying\n- `prey[]` for the trophic-overlap rule (predator requires prey nearby)\n- `domain` for aquatic gating\n\nIt does NOT consume `habitat_min` (no quality-threshold filtering) or\n`carrying_capacity` (no per-tile population cap that decays when prey\npopulations decline). Result: fauna are spawn-once at map-gen and never\nrespond to ecosystem health changes.\n\nThis objective adds per-turn population dynamics:\n- Each fauna tile tracks `population: f32` (0..1, fraction of carrying_capacity)\n- Each turn: population grows toward carrying_capacity if habitat_quality > habitat_min and prey populations > threshold\n- Population shrinks if prey collapses (cascade effect)\n- Selector at map-gen seeds initial populations from `ecology_tier` weighting\n\nCouples directly with `g2-07` (flora lifecycle drives habitat_quality\nwhich drives fauna populations)."
},
{
"id": "g2-09",
"title": "Flora tolerance-driven selection — drought / fire / cold tolerances feed selector (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "Every flora species carries six independent tolerance/contribution\naxes: `drought_tolerance`, `fire_resistance`, `growth_rate`,\n`quality_tier`, `canopy_contribution`, `undergrowth_contribution`,\n`fungi_contribution`. Wave-C's `mc-ecology::flora_select` consumes\nonly `quality_tier × <layer>_contribution` — the other two tolerance\nfields (drought, fire) are unused.\n\nThis means a high-drought-tolerance species like `creosote_bush` and\na low-tolerance species like `european_beech` get equal weight in the\nselector if both list the same biome — even on a tile with high\naridity_index where only drought-tolerant species would actually survive.\n\nThis objective extends the selector to multiply species weights by:\n- `(1 - aridity_index) ↔ drought_tolerance` lookup curve\n- `fire_return_interval ↔ fire_resistance` curve (couples with `p2-32`'s WeatherScript fire events)\n\nSo the same biome tile in two different climate zones picks visibly\ndifferent species pools."
},
{
"id": "g2-10",
"title": "Fauna migration paths — seasonal range shifts, reintroduction propagation (Game 2)",
"priority": "p2",
"status": "oos",
"scope": "game2",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "Currently fauna are placed once at map-gen and stay there. Real\nspecies migrate seasonally (`musk_ox`, `red_deer`, `narwhal`,\n`peregrine_falcon` — each authored with multi-biome `biomes[]`\narrays implying range-shift). Plus reintroduction: extinct-then-\nrecovering populations re-seed from neighbouring tiles (the Yellowstone\nwolf trophic cascade described in `grey_wolf.json` lore).\n\nThis objective adds per-turn migration:\n- `seasonality` from `mc-climate` drives \"season\" enum (winter / summer / shoulder)\n- Per-species migration partner: `species.summer_biomes[]` vs `winter_biomes[]` (schema extension)\n- Each turn tile populations shift between summer-grounds and winter-grounds along shortest-path routes\n- Reintroduction: when a tile's population reaches 0 and an adjacent tile has population > carrying_capacity, surplus migrates in\n\nCouples with g2-08 (population dynamics) and g2-07 (climate-driven flora habitat shifts)."
},
{
"id": "p2-01",
"title": "Minimap — fog reflection and unit markers",
@ -1384,9 +1606,9 @@
"id": "p2-36",
"title": "Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/`",
"priority": "p2",
"status": "partial",
"status": "done",
"scope": "game1",
"updated_at": "2026-04-27",
"updated_at": "2026-04-29",
"blocked_by": [],
"summary": "After the `p1-31` per-file split, 14 building IDs are defined in **both** `public/resources/buildings/<id>.json` (engine defaults) and `public/games/age-of-dwarves/data/buildings/<...>.json` (Age-of-Dwarves overrides). The data loader iterates `resources/` first then `data/` and overwrites by id — the data/ definition silently wins. Every duplicate currently differs in `cost`, `tech_required`, or `tier`, so the resources/ side is dead code wherever the override exists.\n\n| ID | resources/ definition | data/ definition (wins) | Drift |\n|---|---|---|---|\n| `barracks` | cost=80, tech=military_doctrine, tier=1 | cost=50, tech=null, tier=1 | tech gate dropped |\n| `forge` | cost=100, tech=smelting, tier=2 | cost=60, tech=null, tier=1 | tech gate dropped, tier dropped |\n| `granary` | cost=60, tech=husbandry, tier=1 | cost=30, tech=null, tier=1 (in stub.json) | tech gate dropped |\n| `library` | cost=75, tech=scholarship, tier=1 | cost=60, tech=null, tier=1 | tech gate dropped |\n| `monument` | cost=40, tech=null, tier=1 | cost=30, tech=null, tier=1 | cost only |\n| `siege_workshop` | cost=120, tech=mathematics, tier=2 | cost=80, tech=siege_craft, tier=2 | different tech |\n| `temple` | cost=90, tech=ancestor_rites, tier=2 | cost=80, tech=null, tier=2 | tech gate dropped |\n| `walls` | cost=75, tech=masonry, tier=1 | cost=70, tech=null, tier=1 | tech gate dropped |\n| `clan_moot_stone` (wonder) | cost=80, tier=1 | cost=180, tier=2 (in mundane_wonders.json) | wonder cost/tier diverged |\n| `covenant_stone` (wonder) | cost=355, tier=4 | cost=600, tier=6 | wonder cost/tier diverged |\n| `grand_observatory` (wonder) | cost=220, tech=astronomy, tier=5 | cost=600, tech=astronomy, tier=6 | wonder cost/tier diverged |\n| `hall_of_ancestors` (wonder) | cost=360, tier=4 | cost=260, tier=3 | wonder cost/tier diverged |\n| `voice_of_ages` (wonder) | cost=780, tier=10 | cost=1200, tier=10 | wonder cost diverged |\n| `world_pillar` (wonder) | cost=540, tier=7 | cost=1040, tech=world_theory, tier=9 | wonder cost/tech/tier diverged |\n\nThe pattern is clear:\n- **Ordinary buildings (8)**: `data/` versions are cheaper and drop the tech gate. Likely a deliberate Age-of-Dwarves \"always-buildable starter\" simplification.\n- **Wonders (6)**: `data/buildings/mundane_wonders.json` versions are heavier-cost / higher-tier, suggesting `mundane_wonders.json` is the actual Game 1 wonder ladder and the per-file `resources/` versions are stale legacy entries."
},
@ -1422,6 +1644,113 @@
"blocked_by": [],
"summary": "The `chronicle_keeping` culture tech (era_4 oral_tradition pillar) declares\n`unlocks.buildings = [\"chronicle_hall\"]`, but no `chronicle_hall` building file\nexists anywhere in `public/resources/buildings/` or\n`public/games/age-of-dwarves/data/buildings/`. Surfaced by the p3-01 cycle 3\naudit when the era_4 Rune Scribe culture path tried to extend\n`chronicle_hall.enables_units` and discovered it was a phantom.\n\nThe actual building gated on `culture_required: \"chronicle_keeping\"` is\n`bardic_circle` (mundane_wonders.json:102 — tier 4, era 2 wonder). The phantom\nis a stale unlock string left over from an earlier design.\n\nThis is a pre-existing data integrity bug, separate from p3-01's courier scope."
},
{
"id": "p2-43",
"title": "Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit",
"priority": "p2",
"status": "missing",
"scope": "game1",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "`EventBus.culture_researched(tradition_id, player_index)` is defined and\n**every downstream consumer is wired** (AudioManager handler, manifest\nentry `culture_researched`, the asset shipped at\n`public/resources/audio/sfx/ui/culture_researched.ogg`). What's missing\nturned out to be deeper than the original framing of this objective: the\n**entire per-turn culture-research path doesn't run in the live game**.\n\n### Trace\n\n- `turn_manager.gd:246` calls `_process_culture(player, game_map)`\n- `turn_processor.gd:360 _process_culture` only handles **border\n expansion** via `city.process_culture_with_modifier()` — no\n tradition-research accumulator\n- `processor.rs:604 process_culture_research` (Rust mc-turn) **does**\n drive tradition completion via `mc_culture::CultureResearchResult`,\n but it lives in the bench / legacy-headless path, not in the\n GDScript-driven live-game per-turn\n- Tech has a Rust GDExt method `tech_web.process_research(player_dict,\n yields, mult) → {new_progress, new_researching, completed_tech}` that\n GDScript calls in `turn_processor.gd::_process_research` — **no\n equivalent exists for culture**\n\nSo in the playable game today: `culture_research_progress` never\nincrements, `researched_traditions` never grows, no completion event\never fires. `p1-28` shipped the UI and the data graph but not the\nruntime accumulator."
},
{
"id": "p2-44",
"title": "AI promotion selection — auto-pick + emit unit_promoted for AI units",
"priority": "p2",
"status": "missing",
"scope": "game1",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "`EventBus.unit_promoted(unit, promotion_id)` is wired end-to-end on the\naudio side: the handler in `AudioManager` plays a UI confirmation chime,\nand `audio.json` ships the `unit_promoted` entry. But the signal is only\nemitted from one place:\n`src/game/engine/scenes/combat/promotion_picker.gd:120` — the modal the\n**human** uses to pick a promotion.\n\nAI units never go through that picker. Rust has the eligibility check\n(`mc_combat::check_promotion`) and the validation\n(`mc_combat::validate_promotion_choice`) but **no AI selection logic** —\nzero callers of `unit.promote(id)` for AI-owned units, verified by\n`grep -rn '\\.promote(' src/`.\n\nSo in any AI-vs-AI engagement, level-ups happen silently — the XP bar\nfills but no promotion is ever applied or signalled."
},
{
"id": "p2-45",
"title": "Player elimination reconciliation — emit `player_eliminated` on every transition",
"priority": "p2",
"status": "done",
"scope": "game1",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "`EventBus.player_eliminated(player_index)` fires from exactly one place:\n`src/game/engine/src/modules/combat/combat_utils.gd:140` — when combat\nstrips a player's last city. Today every elimination path goes through\ncombat (the only way to lose your last city in Game 1 is a captor\ntakes it), so the signal does fire in practice.\n\nBut the contract is fragile:\n\n1. `victory_manager._check_elimination_winner()` recomputes `alive_players`\n each turn from `cities.size() > 0 || _has_living_founder(player)` — it\n knows who's eliminated but only emits `victory_achieved` for the\n sole survivor; it never re-emits per-eliminated-player signals\n2. Future paths (score-floor, surrender, starvation-to-zero-units,\n cultural-encroachment-displacement) won't go through combat_utils —\n they'd silently eliminate without firing the signal\n3. AudioManager's `_on_player_eliminated` is the only consumer that\n currently exists, but more listeners will land (chronicles, achievements,\n replays); they all depend on the signal being authoritative"
},
{
"id": "p2-46",
"title": "Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback",
"priority": "p2",
"status": "missing",
"scope": "game1-stretch",
"owner": "shipwright",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Persistent local archive of finished games, accessible from the main menu, with three surfaces:\n\n1. **Past Games index** — card grid (newest first), filters (outcome / map / version / date), per-card actions (Open Summary · Watch Replay · Rename · Export · Delete).\n2. **Replay viewer** — turn-by-turn playback against the live renderer, **projection-based not re-simulated** (reads pre-recorded snapshots + events), scrubber, speed selector, event ticker, optional stats overlay.\n3. **Compare view** — multi-select 24 games → overlapping score-graph + final-standings delta.\n\nFoundational for `p3-06` (statistics screens) and `p3-07` (end-of-game summary), both of which read the same `GameHistory` artefact this objective owns. **Ships first** of the three.\n\nDesign doc: [.project/designs/past-games-replays.md](../designs/past-games-replays.md)."
},
{
"id": "p2-47",
"title": "In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories)",
"priority": "p2",
"status": "missing",
"scope": "game1-stretch",
"owner": "shipwright",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Civ-style mid-game statistics modal opened from the HUD info button (or `F9`). Five tabs in one scene, all read-only views over the per-turn `TurnSnapshot` log produced by `mc-replay` (p3-05):\n\n1. **Demographics** — sortable single-turn table of every met clan.\n2. **Graphs** — multi-line chart, Y-axis selector (score / pop / cities / army / gold-per-turn / culture-per-turn / tech-count / land-area), X = turn.\n3. **Rankings** — top-N leaderboard for the selected metric, with trend arrow vs. previous turn.\n4. **Replay** — in-game preview of the post-game replay viewer (p3-05 surface), scoped to the current game's history.\n5. **Histories** — per-clan chronicle (founding turn, wars, wonders, eras, leaders).\n\nComposite score is recomputed every turn-end from JSON-driven weights, used for Rankings default and end-game ordering.\n\nDesign doc: [.project/designs/stats-screens.md](../designs/stats-screens.md)."
},
{
"id": "p2-48",
"title": "End-of-game summary screen — outcome banner, standings, score graph, awards, timeline, footer actions",
"priority": "p2",
"status": "missing",
"scope": "game1-stretch",
"owner": "shipwright",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Full-screen summary triggered when the game ends — by victory condition, last-clan-standing, turn-limit, or player resignation. Replaces the world-map HUD with:\n\n- **Hero strip** — outcome banner + winning-clan card + player's-clan card (player-second slot stable across victory/defeat).\n- **Section 1 — Final standings** — Demographics table from p3-06 frozen at final turn, plus `Outcome` and `Score breakdown` columns.\n- **Section 2 — Score graph** — full-game chart from p3-06's Graphs widget with event markers forced on.\n- **Section 3 — Awards** — JSON-driven per-category superlatives.\n- **Section 4 — Timeline** — Histories from p3-06 with fog lifted (every clan visible).\n- **Footer** — View Map · Watch Replay · Save to Archive · Export JSON · Main Menu.\n\nDesign doc: [.project/designs/end-game-summary.md](../designs/end-game-summary.md)."
},
{
"id": "p2-49",
"title": "Climate axes refactor — latitude + continentality + zonal winds as first-class per-hex inputs",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "`mc-mapgen::sampleCell` (and the design-lab twin in `terrain.ts:213`)\nderives `cold` from\n`abs(row/rows - 0.5) * 2 * (1 - climate)` — an implicit latitude proxy\nthat doesn't expose:\n\n- **Continentality** — graph distance from ocean, drives seasonal\n swing and base aridity (Siberia vs Ireland)\n- **Seasonality** — latitude amplitude (tropic vs arctic)\n- **Rain shadow** — windward / leeward of mountains (wet Pacific NW\n vs dry Great Basin)\n- **Elevation lapse rate** — ~6.5°C/km cooling\n- **West-coast asymmetry** — without ocean current simulation, mid-\n latitude west coasts must still feel maritime; east coasts must\n still feel continental.\n\nThe classifier consumes the collapsed `cold` value, producing biomes\nthat don't differentiate maritime temperate from continental temperate,\nor windward rainforest from leeward rain shadow.\n\nThis objective decomposes the climate input into independent per-hex\nfields — `latitude`, `continentality`, `wind_band` — then derives mean\nT, mean P, seasonality, evapotranspiration deficit. The biome\nclassifier consumes the derived values, exposing maritime / continental\nand rain-shadow distinctions at last."
},
{
"id": "p2-50",
"title": "Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "Every terraformer-owned objective claims \"deterministic from seed\", but\nno objective specifies (a) which RNG, (b) the seed-mixing function,\n(c) the version pin that survives `cargo update`. Today, the worldgen\npipeline calls `rand::thread_rng()` and `StdRng::seed_from_u64(seed)`\ninconsistently across crates. `StdRng` is explicitly documented as\n**not stable across rand versions** — saves break silently on dep\nbumps.\n\nThis objective pins a versioned RNG and a single seed-derivation\nfunction for all worldgen — tectonics, hydrology, climate, species\nselection. Lands as a small infrastructure objective so Wave A of the\nterraformer schedule (p1-50, p2-49) is built on a stable foundation,\nnot retrofitted later."
},
{
"id": "p2-51",
"title": "Player-facing world-shape parameters on new-game screen",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-04-30",
"blocked_by": [],
"summary": "The terraformer pipeline now exposes ~15 internal parameters\n(plate count, tectonic strength, fbm octaves, sea level, latitude\ngradient, continentality decay, rain-shadow factor, erosion\niterations, drainage threshold, etc.). Designers tune these in the\nforest lab; **players see none of them**. The new-game screen ships\n\"Map Size\" and not much else.\n\nIndustry baseline (Civ 6, Old World, Songs of Conquest) exposes 46\nhigh-level shape knobs. Each knob is a *preset* that derives several\ninternal parameters at once. This objective wires that surface from\nJSON presets through `mc-mapgen` parameters into the Godot game-setup\nscene."
},
{
"id": "p2-52",
"title": "Split terrain enum into substrate × flora-cover layers (resolve biome ontology)",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-05-01",
"blocked_by": [],
"summary": "The current `terrain.json` enum (16 IDs) conflates three orthogonal\necological layers into a single field:\n\n| Layer | Examples currently in terrain enum | Should live where |\n|---|---|---|\n| **Substrate** (geological / hydrological) | `mountains`, `hills`, `ocean`, `coast`, `lake`, `inland_sea`, `volcano`, `ice`, `snow` | terrain (correct) |\n| **Flora-cover** (emergent from species) | `forest`, `jungle`, `boreal_forest`, `grassland`, `swamp` | derived from flora selector |\n| **Substrate × climate composite** | `desert`, `tundra`, `plains` | derived label, not authored |\n\nThe `feature_type: \"foliage\"` field on `forest`/`jungle`/`boreal_forest`\nin `public/games/age-of-dwarves/data/terrain/land_forest.json` is the\ndata layer admitting these are flora cover masquerading as terrain.\n\nThis conflation propagates everywhere:\n- Flora species `biomes[]` arrays list flora-cover types as locking\n conditions (a beech tree's `biomes = [temperate_forest, forest]` —\n both are flora-cover labels, not substrates)\n- Climate classifier (`mc-climate::derive::classify_terrain_whittaker`)\n emits flora-cover names from T/P bands directly, skipping the\n substrate axis\n- Terrain blends (`terrain_blends.json`) mix substrate edges\n (`coast+plains → shore`) with flora-cover edges\n (`forest+plains → grass_fringe`) on equal footing\n\nThis objective restructures the data model into three independent layers\nthat the renderer composes into a final visual:\n\n```\nSubstrate ← Tectonics + Hydrology output\n × Climate ← (t_band, p_band, riparian_distance)\n × Flora-cover ← Ecology selector output (canopy/understory/ground/bare)\n = Biome label ← Display name only, derived not stored\n```"
},
{
"id": "g2-01",
"title": "Ley lines — Game 2 (Age of Kzzykt)",
@ -1659,8 +1988,19 @@
"summary": "Improvements ship as data files (`public/resources/improvements/*.json`) but the\nsimulation has no per-hex anchor for them. Improvements currently live on\n`PlayerState.city_improvements: Vec<Vec<String>>` (per-player / per-city,\nunanchored). The grid's per-tile struct stores terrain only — no improvement\nslot.\n\nThis blocks p3-03 acceptance bullets 4 (severance: pillaging Steam Track at hex\n(c,r) intercepts a courier whose route includes (c,r)), 5 (Hold-Network reroute\nwhen a Steam Track is severed), and the infrastructure-gating half of bullet 2\n(Steam Messenger requires Steam Track tiles on its route, Resonance\nTelegrapher requires Resonance Wire tiles).\n\nThis is also a foundational gap that will block other Game-1-stretch features\nbeyond couriers (tile-improvement pillaging, road network bonuses,\nfortification ZOC, defensive towers)."
}
],
"blocked": [],
"blocked": [
{
"id": "p1-49",
"blockedBy": [
"p1-47"
]
}
],
"remaining_by_lead": [
{
"owner": "terraformer",
"remaining": 10
},
{
"owner": "warcouncil",
"remaining": 7
@ -1671,7 +2011,7 @@
},
{
"owner": "shipwright",
"remaining": 2
"remaining": 5
},
{
"owner": "asset-audio",
@ -1681,6 +2021,10 @@
"owner": "combat-dev",
"remaining": 1
},
{
"owner": "simulator-infra",
"remaining": 1
},
{
"owner": "testwright",
"remaining": 1

View file

@ -20,6 +20,7 @@ evidence:
- .project/designs/app/src/pages/WorldGen.tsx
- .project/designs/app/src/utils/worldGen/terrain.ts
- .project/designs/app/src/utils/worldGen/hexCanvas.ts
- src/game/engine/scenes/tests/world_gen_lab/world_gen_lab_proof.tscn
---
## Summary
@ -101,12 +102,11 @@ behaviour, and flora binding all consume the post-refactor axes.
- ◻ **World-shape preset row** — top of the lab exposes the 5 preset
dropdowns from p2-51 alongside the existing raw sliders, behind an
"Advanced" toggle.
- ◻ **Headless screenshot fixture** — proof captures are wired into a
proof scene under `src/game/engine/scenes/tests/world_gen_lab/`
(Rail 5) and run via `tools/screenshot.sh`, NOT hand-captured.
Four configurations (forest / jungle / boreal / desert) committed
to `.project/screenshots/p1-46-*.png`, each showing flora species
names in info card.
- ◻ **Headless screenshot fixture** — proof scene authored at
`src/game/engine/scenes/tests/world_gen_lab/world_gen_lab_proof.tscn`
(Rail 5). Screenshot capture pending operator pass on apricot via
`tools/screenshot.sh` per phase-gate-protocol.md. PNGs not yet
committed to `.project/screenshots/p1-46-*.png`.
## Non-goals

View file

@ -13,6 +13,10 @@ coordinates_with:
- p1-49
- p1-50
- p2-50
evidence:
- src/simulator/crates/mc-mapgen/src/hydrology.rs
- src/simulator/crates/mc-mapgen/tests/hydrology.rs
- src/game/engine/scenes/tests/hydrology/hydrology_proof.tscn
---
## Summary
@ -111,12 +115,12 @@ All `cargo test -p mc-mapgen` pass. `cargo check --workspace` clean.
lake_ids. Frozen golden vector confirms headwater drainage_area=1 and
monotone downstream ordering.
Evidence: all integration tests pass.
- ◻ **Visual proof**screenshot showing connected river system
across 5+ hexes with multi-hex lake at the bottom and visible
carved valleys, captured by a proof scene under
`src/game/engine/scenes/tests/hydrology/` (Rail 5). Committed to
- ◻ **Visual proof**proof scene authored at
`src/game/engine/scenes/tests/hydrology/hydrology_proof.tscn` (Rail 5).
Screenshot capture pending operator pass on apricot via `tools/screenshot.sh`
per phase-gate-protocol.md. PNG not yet committed to
`.project/screenshots/p1-47-*.png`.
(Relocated to p1-46 Wave E — lab integration captures the screenshot.)
(Hydrology overlay in lab also satisfies this — see p1-46 Wave E.)
## Non-goals

View file

@ -13,6 +13,10 @@ coordinates_with:
- p1-47
- p2-49
- p2-50
evidence:
- src/simulator/crates/mc-ecology/src/flora_select.rs
- src/simulator/crates/mc-ecology/tests/flora_selection.rs
- src/game/engine/scenes/tests/fauna_render/fauna_render_proof.tscn
---
## Summary

View file

@ -15,6 +15,10 @@ coordinates_with:
- p1-46
- p2-49
- p2-50
evidence:
- src/simulator/crates/mc-ecology/src/fauna_glyphs.rs
- src/simulator/crates/mc-ecology/tests/fauna_selection.rs
- src/game/engine/scenes/tests/fauna_render/fauna_render_proof.tscn
---
## Summary
@ -64,8 +68,12 @@ was deleted; `Lab.tsx` has `TODO(p1-46)` markers.
trophic_level, ecology_tier, glyph_cluster}`.
- ✓ **Determinism gate**`mc-ecology/tests/fauna_selection.rs`: 8 tests
including determinism, apex cap, trophic rules, domain gates all pass.
- ◻ **Visual proof** — proof scene + screenshot.
**Relocated to p1-46 (Wave E)**.
- ◻ **Visual proof** — proof scene authored at
`src/game/engine/scenes/tests/fauna_render/fauna_render_proof.tscn`.
Screenshot capture pending operator pass on apricot via `tools/screenshot.sh`
per phase-gate-protocol.md. PNG not yet committed to
`.project/screenshots/p1-49-*.png`.
(Also satisfies p1-46 fauna-proof acceptance bullet when screenshot lands.)
## Non-goals

View file

@ -18,6 +18,7 @@ evidence:
- src/simulator/api-wasm/src/lib.rs (tile_tectonics_json method)
- src/simulator/crates/mc-core/src/grid/mod.rs (TileState fields)
- public/games/age-of-dwarves/data/tectonics.json
- src/game/engine/scenes/tests/tectonics/tectonics_proof.tscn
---
## Summary
@ -62,8 +63,11 @@ as first-class inputs.
500ms. `criterion` bench harness wired in Cargo.toml.
- ✓ **Determinism gate**`tests/tectonics.rs`: 3 seeds × 3 map sizes, full field
comparison; all 3 tests pass. Additional unit tests in `tectonics.rs` inline.
- ◻ **Visual proof** — screenshot deferred to Wave E (requires Godot proof scene).
Not satisfiable this wave.
- ◻ **Visual proof** — proof scene authored at
`src/game/engine/scenes/tests/tectonics/tectonics_proof.tscn`.
Screenshot capture pending operator pass on apricot via `tools/screenshot.sh`
per phase-gate-protocol.md. PNG not yet committed to
`.project/screenshots/p1-50-*.png`.
- ◻ **Lab integration** — lab sliders deferred to Wave E (p1-46). Not in scope
for Wave A Rust-only implementation.

View file

@ -19,6 +19,13 @@ coordinates_with:
- p2-50
- p2-51
canonical_doc: public/games/age-of-dwarves/docs/terrain/WORLDGEN_PIPELINE.md
evidence:
- src/game/engine/scenes/tests/tectonics/tectonics_proof.tscn
- src/game/engine/scenes/tests/hydrology/hydrology_proof.tscn
- src/game/engine/scenes/tests/fauna_render/fauna_render_proof.tscn
- src/game/engine/scenes/tests/world_gen_lab/world_gen_lab_proof.tscn
- src/game/engine/scenes/tests/climate_proof.tscn
- src/game/engine/scenes/tests/world_shape_preview.tscn
---
## Summary
@ -60,7 +67,11 @@ uses, moves a slider, and watches the spec come alive.
- ✓ **Sliders + canvas wired to WASM** — all 5 WASM-driven pages live; new exports `WasmGrid.generateForLab` and free function `seedDerive` added to `api-wasm/src/lib.rs`, built clean, verified in `magic_civ_physics.d.ts`. Evidence: WASM build exits 0.
- ✓ **Determinism / reproducibility** — every page exposes seed slider. Same `(seed, map_size)` → deterministic `WasmGrid` via `MapGenerator::generate`. Evidence: design of `generate_for_lab` delegates to `MapGenerator`.
- ✓ **Lab named** — URL `/world-gen/lab`, component `Lab.tsx`. Evidence: Stage 1 landed prior.
- ◻ **Visual proof** — canonical screenshots per layer page pending Stage 3 (phase-gate-protocol.md headless capture). Status: partial until screenshots captured.
- ◻ **Visual proof** — all 6 layer proof scenes authored:
`tectonics_proof.tscn`, `hydrology_proof.tscn`, `fauna_render_proof.tscn`,
`world_gen_lab_proof.tscn`, `climate_proof.tscn`, `world_shape_preview.tscn`.
Screenshot capture pending operator pass on apricot via `tools/screenshot.sh`
per phase-gate-protocol.md. PNGs not yet captured; status stays partial.
## Why this is its own objective

View file

@ -21,6 +21,7 @@ evidence:
- src/simulator/api-gdext/src/lib.rs (tile_climate method)
- src/simulator/api-wasm/src/lib.rs (tileClimateJson method)
- public/games/age-of-dwarves/data/climate.json
- src/game/engine/scenes/tests/climate_proof.tscn
---
## Summary

View file

@ -11,6 +11,11 @@ coordinates_with:
- p1-10
- p1-50
- p2-49
evidence:
- src/simulator/crates/mc-mapgen/src/world_shape.rs
- src/simulator/crates/mc-mapgen/tests/world_shape_compose.rs
- public/games/age-of-dwarves/data/world_shapes/manifest.json
- src/game/engine/scenes/tests/world_shape_preview.tscn
---
## Summary
@ -59,8 +64,13 @@ scene.
- ◻ **Lab parity**`Presets.tsx` WASM wiring is Wave-E / p1-53 Stage 2
territory (layer-pages-wasm-wiring agent). Relocated per Wave-D closing
checklist.
- ◻ **Visual proof** — screenshot grid of all 5 landmass values. Pending
thumbnail generation run above.
- ◻ **Visual proof** — proof scene authored at
`src/game/engine/scenes/tests/world_shape_preview.tscn`; renders 5
preset composites (earthlike, pangaea_hot_arid, archipelago_temperate_wet,
shattered_cold_lush, continents_extreme_dry) via `generate_with_shape()`.
Screenshot capture pending operator pass on apricot via `tools/screenshot.sh`
per phase-gate-protocol.md. PNGs not yet committed to
`public/games/age-of-dwarves/data/world_shapes/previews/`.
## Non-goals

View file

@ -2,19 +2,11 @@
id: p2-52
title: Split terrain enum into substrate × flora-cover layers (resolve biome ontology)
priority: p2
status: missing
status: in_progress
scope: game1
owner: terraformer
updated_at: 2026-05-01
canonical_doc: public/games/age-of-dwarves/docs/terrain/CLIMATE.md
coordinates_with:
- p1-46
- p1-48
- p1-50
- p2-49
- p2-51
---
## Summary
The current `terrain.json` enum (16 IDs) conflates three orthogonal

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-05-01T05:07:45Z",
"generated_at": "2026-05-01T05:47:28Z",
"totals": {
"partial": 19,
"done": 115,
"missing": 22,
"oos": 26,
"in_progress": 1,
"stub": 1,
"done": 115,
"in_progress": 1,
"total": 184
},
"objectives": [