feat(@projects/@magic-civilization): add mc-flora biome migration task

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-04 01:15:48 -04:00
parent d3c484a5a4
commit 62a74b9af4
8 changed files with 111 additions and 33 deletions

View file

@ -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) | 🟢 |

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -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)

View file

@ -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",

View file

@ -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<Dictionary>`.
`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/`.

View file

@ -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<SpecialistId>; requires_buildings_all_cities: Vec<BuildingId>"
- "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).

View file

@ -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)",

View file

@ -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

View file

@ -173,6 +173,7 @@ fn outcome_str(o: CombatOutcome) -> &'static str {
CombatOutcome::Captured => "captured",
CombatOutcome::RansomOffered => "ransom_offered",
CombatOutcome::Destroyed => "destroyed",
CombatOutcome::Devastated => "devastated",
}
}