diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 34f07e56..2b53cf76 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -308,6 +308,7 @@ | [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | P2 | Weather / observation lens switcher in the Godot HUD | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | P2 | Bind mc-observation gate_bits to player tech state — recording gates per-field | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-62](p2-62-procedural-unit-and-building-renderer.md) | 🔴 stub | P2 | Procedural unit/building renderer — alpha-only visual substitute | [unassigned](../team-leads/unassigned.md) | 🟢 | +| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | 🔴 stub | P2 | mc-flora generation: migrate biome filter to substrate_climate-aware path | [unassigned](../team-leads/unassigned.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) | 🟢 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 269052c4..b2a75be3 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -16,9 +16,9 @@ |---|---|---|---|---|---|---|---| | **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 | | **P1** | 47 | 1 | 13 | 3 | 7 | 1 | 72 | -| **P2** | 52 | 0 | 7 | 14 | 3 | 6 | 82 | +| **P2** | 52 | 0 | 7 | 15 | 3 | 6 | 83 | | **P3 (oos)** | 3 | 0 | 0 | 18 | 1 | 21 | 43 | -| **total** | **145** | **1** | **20** | **35** | **11** | **28** | **240** | +| **total** | **145** | **1** | **20** | **36** | **11** | **28** | **241** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [unassigned](../team-leads/unassigned.md) | 30 | +| [unassigned](../team-leads/unassigned.md) | 31 | | [warcouncil](../team-leads/warcouncil.md) | 6 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 5 | @@ -241,6 +241,7 @@ | [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | "Weather / observation lens switcher in the Godot HUD" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | | [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | "Bind mc-observation gate_bits to player tech state — recording gates per-field" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | | [p2-62](p2-62-procedural-unit-and-building-renderer.md) | 🔴 stub | "Procedural unit/building renderer — alpha-only visual substitute" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | +| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | 🔴 stub | "mc-flora generation: migrate biome filter to substrate_climate-aware path" | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | ## Out of Scope (Game 2 / Game 3) diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 9995077d..7cf055e9 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-05-04T04:50:40Z", + "generated_at": "2026-05-04T05:13:02Z", "totals": { "done": 146, "in_progress": 1, "partial": 20, - "stub": 35, + "stub": 36, "missing": 11, "oos": 28, - "total": 241 + "total": 242 }, "objectives": [ { @@ -2176,6 +2176,17 @@ "blocked_by": [], "summary": "" }, + { + "id": "p2-63", + "title": "mc-flora generation: migrate biome filter to substrate_climate-aware path", + "priority": "p2", + "status": "stub", + "scope": "game1", + "owner": "unassigned", + "updated_at": "2026-05-04", + "blocked_by": [], + "summary": "Authored flora JSON files have migrated from a top-level `biomes: [...]`\narray to the `substrate_climate` ontology\n(see `p2-52-substrate-flora-cover-ontology-split.md`). The biome-filter\nloop in `mc-flora/src/generation.rs` was not updated, so the\n`AuthoredSpeciesFile.biomes` field is now empty for every authored file\nand the candidate-pool query returns nothing.\n\nTwo pre-existing regression tests in `mc-flora` document the gap:\n\n- `load_authored_returns_species_for_known_biome` (generation.rs:645)\n- `generate_flora_for_biome_more_species_with_authored_files`\n (generation.rs:695)\n\nBoth fail because the biome filter at `generation.rs:508-510` still\nchecks `raw.biomes.iter().any(|b| b == biome_id)` while the JSON now\nencodes biome eligibility through `substrate_climate` blocks the loader\ndoes not currently inspect." + }, { "id": "g2-01", "title": "Ley lines — Game 2 (Age of Kzzykt)", @@ -2798,7 +2809,7 @@ "remaining_by_lead": [ { "owner": "unassigned", - "remaining": 30 + "remaining": 31 }, { "owner": "asset-sprite", diff --git a/.project/objectives/p1-55-tech-culture-domain-propagation.md b/.project/objectives/p1-55-tech-culture-domain-propagation.md index f772968a..1550e3b6 100644 --- a/.project/objectives/p1-55-tech-culture-domain-propagation.md +++ b/.project/objectives/p1-55-tech-culture-domain-propagation.md @@ -11,6 +11,12 @@ evidence: - "src/simulator/crates/mc-tech/src/web.rs:74" - "src/simulator/crates/mc-tech/src/web.rs:248" - "src/simulator/crates/mc-tech/src/web.rs:399" + - "src/simulator/api-gdext/src/lib.rs:4953 (GdTechWeb::domains/techs_by_domain/research_history_by_domain)" + - "src/game/engine/src/modules/tech/tech_web.gd:88 (get_domains/get_techs_by_domain/get_research_history_by_domain)" + - "src/game/engine/scenes/knowledge_tree/knowledge_tree.gd:96 (tab axis hooks + tab bar + float-to-top sort)" + - "src/game/engine/scenes/tech_tree/tech_tree.gd:36 (TechTree overrides _get_tab_axis_values/_node_in_tab)" + - "src/game/engine/tests/unit/test_knowledge_tree_domain_tabs.gd (3/3 GUT pass on apricot, headless)" + - "tools/validate-game-data.py:178 (validate_tech_domains: 10-value enum membership check)" assigned_by: simulator-infra --- ## Summary @@ -54,15 +60,39 @@ node. for culture data, matching the design note that culture uses `pillar` as its tab axis.) -- [ ] **GDExtension** — `GdTechWeb` exposes `domains() -> - PackedStringArray` and `techs_by_domain(domain) -> Array`. - `tech_data_json(id)` includes `domain` in returned dict. Build clean: - `bash src/simulator/build-gdext.sh`. +- [x] **GDExtension** — `GdTechWeb` exposes `domains() -> + PackedStringArray` (returns the canonical 10 `TechDomain::ALL` + variants regardless of loaded data) and `techs_by_domain(domain) -> + PackedStringArray` (parses the PascalCase string back to the typed + enum, then delegates to `mc_tech::TechWeb::techs_by_domain`). + `tech_data_json(id)` already includes `domain` for free via serde + (proven by the round-trip test in `mc-tech/src/web.rs:399`). + `research_history_by_domain(researched_techs) -> Dictionary` added + for the player-analysis bullet (Dictionary keyed by all 10 domain + strings, values are `PackedStringArray` of completed tech ids). + Built clean on apricot (`bash build-gdext.sh`, release profile, 17 + pre-existing doc warnings unrelated to this change). Files: + `src/simulator/api-gdext/src/lib.rs:4953-5031`. -- [ ] **GDScript** — `tech_web.gd` exposes `get_domains()` and - `get_nodes_by_domain(domain)`. `knowledge_tree.gd` renders a domain - tab bar above the existing pillar columns. Active tab causes matching - nodes to float to the top within each era column. +- [x] **GDScript** — `tech_web.gd` exposes `get_domains()`, + `get_techs_by_domain(domain)`, and + `get_research_history_by_domain(player_index)` — all pass-through + wrappers that call directly into the GDExtension bridge. No GDScript- + side enum or string list. `knowledge_tree.gd` gains an opt-in tab-axis + hook (`_get_tab_axis_values()` / `_node_in_tab()`), a tab bar above + the body, and float-to-top sort within each pillar column when an + explicit tab is active. The "All" sentinel preserves original order. + `tech_tree.gd` overrides the hooks to surface the 10 `TechDomain` + values from `TechWeb.get_domains()`. Culture trees opt out by leaving + the default empty hook; the tab bar hides itself when no axis is + declared. Files: `src/game/engine/src/modules/tech/tech_web.gd:88`, + `src/game/engine/scenes/knowledge_tree/knowledge_tree.gd:96`, + `src/game/engine/scenes/tech_tree/tech_tree.gd:36`. Headless GUT + proof: `test_knowledge_tree_domain_tabs.gd` (3/3 pass on apricot, + 108 asserts) — covers tab-bar rendering of all 10 domains + "All" + sentinel, float-to-top partition order with the Science tab active, + and round-trip equivalence of `get_domains()` with + `TechDomain::ALL` order. - [ ] **Gameplay proof** — `tech_tree_proof.tscn` captures a screenshot showing the domain tab bar, with one tab (e.g. "Science") active and @@ -78,9 +108,17 @@ node. in-game stats screen renders a domain-binned breakdown of a player's research history. Driven by deterministic replay fixture data. -- [ ] **Cross-reference validation** — `tools/` validator script (or new - one) confirms every tech in `public/resources/techs/*.json` has a - `domain` field set to one of the 10 canonical values. +- [x] **Cross-reference validation** — `tools/validate-game-data.py` + gained a `validate_tech_domains()` pass that walks + `public/resources/techs/*.json` and asserts every entry's `domain` + field exists and ∈ the canonical 10-value enum (mirrored from + `mc_core::TechDomain::ALL` as a class constant — update both sites + if the enum ever changes). Verified clean across the 60+ shipped + techs (`tech domain enum membership (10 canonical values)` section + in the validator output is failure-free). The legacy + `game_data/techs/` fallback was intentionally dropped from this + pass per post-p1-40 SSoT (single source at `resources/techs/`). + Files: `tools/validate-game-data.py:178`. - [ ] **Phase-gate proof screenshot** — captured per `phase-gate-protocol.md`. Archived under `.project/screenshots/`. diff --git a/.project/objectives/p1-56-civics-buildings-and-great-works.md b/.project/objectives/p1-56-civics-buildings-and-great-works.md index 209f35dc..38324f33 100644 --- a/.project/objectives/p1-56-civics-buildings-and-great-works.md +++ b/.project/objectives/p1-56-civics-buildings-and-great-works.md @@ -7,12 +7,11 @@ scope: game1 owner: simulator-infra updated_at: 2026-05-04 evidence: - - "src/simulator/crates/mc-core/src/ids.rs:1 — typed BuildingId/SpecialistId/GreatPersonClass/HarvestPolicyId newtypes (transparent serde, mc-core test suite green)" - - "src/simulator/crates/mc-core/src/gpp.rs:1 — closed GppType + GreatWorkType enums with effect-key mapping (snake_case round-trip)" - - "src/simulator/crates/mc-city/src/building.rs:42 — typed BuildingEffect enum covering gpp_* and great_work_slots_* with Other catch-all; specialist_slots: Vec; requires_buildings_all_cities: Vec" - - "src/simulator/crates/mc-city/src/building.rs:567 — test_building_deserialises_new_fields green for saga_arena + saga_chronicle" - - "src/simulator/crates/mc-city/src/building.rs:625 — test_all_authored_buildings_deserialize green over 178 building JSONs" - - "public/games/age-of-dwarves/docs/BUILDING_SCHEMA.md:99 — Civics extensions section documents new effect-array variants and typed wrappers" + - "src/simulator/crates/mc-city/src/great_works.rs:1 — static GreatWorkRegistry with 3/3 tests green over the 4 authored category JSONs (≥30 works)" + - "src/simulator/api-gdext/src/civics.rs:1 — GdSpecialistRegistry / GdHarvestPolicyRegistry / GdGreatWorkRegistry / GdBuildingCivics; cargo build -p magic-civ-physics-gdext clean on apricot" + - "src/game/engine/scenes/city/specialists_panel.gd:1 — VBoxContainer panel rendering specialist slots, 7-channel GPP/turn sums, 4-category great-work slot capacity from typed Rust accessors" + - src/game/engine/tests/unit/test_city_screen_specialist_slots.gd + test_city_screen_gpp.gd — 11 new tests; headless GUT 25/25 city_screen tests passing on apricot + - "src/simulator/crates/mc-city/src/harvest_policy.rs:91 — HarvestPolicyRegistry::all() accessor for UI dropdown enumeration" assigned_by: simulator-infra --- ## Summary @@ -232,6 +231,7 @@ Unit fields added (additive, optional): - Dependencies: GPP bullet. - Acceptance gate: `cargo test -p mc-city test_great_work_slot_assignment` green; removal on building destruction tested. - SOLID/DRY/SSoT rails: registry typed; effect chains in `mc-city`. +- **Sub-bullet (closed 2026-05-04 — registry only)** ✓ Static `GreatWorkRegistry` lives at `src/simulator/crates/mc-city/src/great_works.rs:1` with `from_json` / `from_json_files` / `by_type` / `get`, mirrors `SpecialistRegistry` shape. `mc-city` re-exports `GreatWork` + `GreatWorkRegistry` (`src/simulator/crates/mc-city/src/lib.rs:35`). Tests `great_works::tests::great_work_round_trips_via_json`, `all_authored_great_works_deserialize` (loads all 4 authored JSONs, ≥30 works), `by_type_filters_correctly` green (`cargo test -p mc-city --lib great_works` 3/3). Per-city occupation (which work fills which slot, removal on building destruction, GP-spawn assignment) NOT yet implemented — depends on per-city runtime state still missing in `mc-city`. ### Bullet: `mc-turn::process_buildings` calls per-turn GPP/specialist/harvest/great-work ticks @@ -246,6 +246,13 @@ Unit fields added (additive, optional): - Dependencies: Rust bullets above. - Acceptance gate: `bash src/simulator/build-gdext.sh` clean; smoke probe in proof scene. - SOLID/DRY/SSoT rails: bridge marshals JSON; tier-cap enforced server-side in Rust. +- **Sub-bullet (closed 2026-05-04 — static-registry surface only)** ✓ `src/simulator/api-gdext/src/civics.rs:1` registers four `RefCounted` GDExt classes: + - `GdSpecialistRegistry::from_json` / `get_specialist` / `for_building` / `count` — wraps `mc_city::SpecialistRegistry`. + - `GdHarvestPolicyRegistry::from_json` / `get_policy` / `ids` / `count` — wraps `mc_city::HarvestPolicyRegistry`. New `HarvestPolicyRegistry::all` accessor at `src/simulator/crates/mc-city/src/harvest_policy.rs:91`. + - `GdGreatWorkRegistry::from_json_files` / `get_great_work` / `by_type` / `count` — wraps `mc_city::GreatWorkRegistry`. + - `GdBuildingCivics::from_defs_json` / `insert_def_json` / `specialist_slots` / `gpp_yield` / `great_work_slots` / `sum_gpp_yield` / `sum_great_work_slots` — exposes the cycle-1 typed `BuildingDef` accessors per building and as city-wide sums for UI. + - Verified by `cargo build -p magic-civ-physics-gdext` clean (Finished `dev` in 23.18s on apricot, 17 doc warnings, 0 errors). + - NOT bridged this cycle: `GdGreatPersonAction.activate(...)` (no Rust runtime), `set_tile_policy` (no `Tile.harvest_policy` field), per-city GPP accumulator readback (no per-city state). Tier-cap enforcement, GP spawn dispatch, and great-work occupation depend on the Rust runtime bullets above and stay ❌. ### Bullet: Godot UI — specialist drag, harvest dropdown, great-person spawn modal, throne-room layer slots @@ -254,11 +261,19 @@ Unit fields added (additive, optional): - Dependencies: GDExtension bullet. - Acceptance gate: gdlint clean; manual smoke + proof scene. - SOLID/DRY/SSoT rails: presentation only; no game rules in GDScript. +- **Sub-bullet (closed 2026-05-04 — capacity panel only)** ✓ `src/game/engine/scenes/city/specialists_panel.gd:1` — VBoxContainer panel renders three sections: + - Specialist slots — one row per (building_id, specialist_id) sourced from `GdBuildingCivics.specialist_slots(bid)`. Specialist name resolved via `GdSpecialistRegistry.get_specialist(sid)`. + - GPP per turn — all 7 GPP types (`writing`, `music`, `art`, `statuary`, `scholarship`, `trade`, `engineering`) summed across the city's built buildings via `GdBuildingCivics.sum_gpp_yield`. + - Great-Work slot capacity — all 4 work types summed via `GdBuildingCivics.sum_great_work_slots`, rendered as `0 / cap` (occupants placeholder — runtime occupation not yet bridged). + - Vocab keys added at `public/games/age-of-dwarves/vocabulary.json:584` (`city_screen_section_specialists`, `city_screen_section_gpp`, `city_screen_section_great_works`, `city_screen_no_specialist_slots`). + - Pure presentation — no game rules in GDScript; `setup(specialist_registry, great_work_registry, building_civics)` accepts null registries and degrades gracefully (renders empty rows) when the GDExtension isn't loaded. + - Drag-to-employ, harvest-policy dropdown on tile rows, great-person spawn modal, and throne-room layer slot rendering remain ❌ — they require runtime employ/unemploy/policy-set/spawn APIs in mc-city. ### Bullet: GUT tests + proof screenshot - Files to touch: `src/game/engine/src/tests/test_specialist_slots.gd`, `test_gpp_accumulation.gd`, `test_great_person_spawn.gd`, `test_harvest_policy_yield.gd`, `test_great_work_slot_assignment.gd`, `test_national_wonder_requirement.gd`; proof scene `scenes/tests/proof_civics_buildings.tscn`. - Dependencies: all above. - Acceptance gate: `godot --headless --test ...` green; screenshot approved per `phase-gate-protocol.md`. +- **Sub-bullet (closed 2026-05-04 — UI-side tests for capacity panel)** ✓ `src/game/engine/tests/unit/test_city_screen_specialist_slots.gd` (6 tests, 6 passing) and `test_city_screen_gpp.gd` (5 tests, 5 passing) verify aggregation contract against mock GdBuildingCivics + GdSpecialistRegistry surfaces. Headless GUT run on apricot via `flatpak run org.godotengine.Godot --headless --script gut_cmdln.gd -gprefix=test_city_screen_`: `25/25 passed` across 4 city-screen test files (the 2 new + 2 pre-existing). Includes negative test `test_panel_does_not_render_runtime_progress` enforcing Rail-1: no GDScript shadow accumulator. Proof screenshot + per-system Rust tests (`test_specialist_yields_and_population`, `test_gpp_accumulation_and_spawn`, `test_great_person_spawn`, `test_harvest_policy_remove_chop_oneshot`, `test_great_work_slot_assignment`, `test_national_wonder_requirement`) remain ❌ — depend on Rust runtime bullets above. -Bullets remaining: 10 (deserialise bullet closed 2026-05-04). +Bullets remaining: 10 parents still ❌; this cycle landed three closed sub-bullets (great-works static registry, GDExt static-registry surface, capacity-only specialists panel + GUT tests). Next cycle: Rust runtime (Specialist yields, GPP accumulator, Tile.harvest_policy, national-wonder gates). diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index c1409365..3e35d886 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-05-04T04:53:17Z", + "generated_at": "2026-05-04T05:14:49Z", "totals": { - "in_progress": 1, - "missing": 11, - "done": 145, - "oos": 28, "partial": 20, - "stub": 35, - "total": 240 + "oos": 28, + "in_progress": 1, + "done": 145, + "missing": 11, + "stub": 36, + "total": 241 }, "objectives": [ { @@ -1980,6 +1980,16 @@ "updated_at": "2026-05-03", "summary": "" }, + { + "id": "p2-63", + "title": "\"mc-flora generation: migrate biome filter to substrate_climate-aware path\"", + "priority": "p2", + "status": "stub", + "scope": "game1", + "owner": "unassigned", + "updated_at": "2026-05-04", + "summary": "Authored flora JSON files have migrated from a top-level `biomes: [...]`\narray to the `substrate_climate` ontology\n(see `p2-52-substrate-flora-cover-ontology-split.md`). The biome-filter\nloop in `mc-flora/src/generation.rs` was not updated, so the\n`AuthoredSpeciesFile.biomes` field is now empty for every authored file\nand the candidate-pool query returns nothing.\n\nTwo pre-existing regression tests in `mc-flora` document the gap:\n\n- `load_authored_returns_species_for_known_biome` (generation.rs:645)\n- `generate_flora_for_biome_more_species_with_authored_files`\n (generation.rs:695)\n\nBoth fail because the biome filter at `generation.rs:508-510` still\nchecks `raw.biomes.iter().any(|b| b == biome_id)` while the JSON now\nencodes biome eligibility through `substrate_climate` blocks the loader\ndoes not currently inspect." + }, { "id": "g2-01", "title": "Ley lines — Game 2 (Age of Kzzykt)", diff --git a/src/game/engine/scenes/tech_tree/tech_tree.gd b/src/game/engine/scenes/tech_tree/tech_tree.gd index 2d59505b..3cd69b20 100644 --- a/src/game/engine/scenes/tech_tree/tech_tree.gd +++ b/src/game/engine/scenes/tech_tree/tech_tree.gd @@ -49,4 +49,5 @@ func _node_in_tab(node_id: String, tab: String) -> bool: if tab.is_empty() or _web == null: return true var data: Dictionary = (_web as TechWeb).get_node_data(node_id) - return String(data.get("domain", "")) == tab + var domain_val: String = str(data.get("domain", "")) + return domain_val == tab diff --git a/src/simulator/crates/mc-combat/tests/golden.rs b/src/simulator/crates/mc-combat/tests/golden.rs index f451c1ca..815b6145 100644 --- a/src/simulator/crates/mc-combat/tests/golden.rs +++ b/src/simulator/crates/mc-combat/tests/golden.rs @@ -173,6 +173,7 @@ fn outcome_str(o: CombatOutcome) -> &'static str { CombatOutcome::Captured => "captured", CombatOutcome::RansomOffered => "ransom_offered", CombatOutcome::Destroyed => "destroyed", + CombatOutcome::Devastated => "devastated", } }