magicciv/.project/objectives/p2-52-substrate-flora-cover-ontology-split.md
Natalie e5b9d10b61 feat(@projects/@magic-civilization): add lab and hud system components
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-01 22:23:54 -04:00

11 KiB
Raw Blame History

id title priority status scope owner updated_at k_of_n evidence
p2-52 Split terrain enum into substrate × flora-cover layers (resolve biome ontology) p2 done game1 terraformer 2026-05-01 9/9
src/simulator/crates/mc-core/src/grid/mod.rs — biome_id Rust field removed; biome_label_id + #[serde(rename="biome_id")] preserves JSON wire key; substrate_id + flora_cover_id present
src/simulator/crates/mc-climate/src/derive.rs — compat shim write (tile.biome_id = biome_label) deleted; biome_label_id only
src/simulator/crates/mc-mapgen/src/lib.rs — biome_to_substrate() + substrate_id populated in to_grid_state()
src/simulator/api-wasm/src/lib.rs — tileSubstrateJson WASM export; tileFloraJson+tileFaunaJson accept substrate_id parameter
.local/build/wasm/magic_civ_physics.d.ts — tileFloraJson+tileFaunaJson signatures updated to substrate_id
tools/strip-legacy-biomes.py — 149 flora + 589 fauna species biomes[] removed (738 total)
src/simulator/crates/mc-ecology/src/flora_select.rs — TerrainFloraIndex keyed on (substrate_id, T_band, P_band) via substrate_climate[] range expansion
src/simulator/crates/mc-ecology/src/fauna_select.rs — TerrainFaunaIndex keyed on (substrate_id, T_band, P_band); domain_gate uses WATER_SUBSTRATES; FaunaSpec.biomes removed
cargo test -p mc-mapgen --test cross_build_determinism: 4/4 pass (2026-05-01)
cargo test -p mc-save: 3/3 pass round-trip (2026-05-01)
cargo test -p mc-ecology: 290/290 pass (2026-05-01)
npx tsc --noEmit: clean (2026-05-01)
public/games/age-of-dwarves/data/terrain/substrate.json — 9 substrate types
public/games/age-of-dwarves/docs/terrain/SUBSTRATE.md — canonical doc
public/games/age-of-dwarves/data/terrain/substrate_blends.json — substrate ecotones
public/games/age-of-dwarves/data/terrain/flora_cover_blends.json — flora cover ecotones
.project/designs/app/src/pages/WorldGen/Substrate.tsx — substrate lab page
.project/designs/app/src/pages/WorldGen/Lab.tsx — tileFloraJson+tileFaunaJson updated to pass substrate_id from tileSubstrateJson
.project/designs/app/src/pages/WorldGen/Ecology.tsx — tileFloraJson+tileFaunaJson updated to pass substrate_id
public/games/age-of-dwarves/docs/terrain/CLIMATE.md §10 — Whittaker section rewritten as 3-axis T×P×substrate table
public/games/age-of-dwarves/docs/ECOLOGY_BINDING.md — authored; species index keyed on (substrate, T_band, P_band)
src/game/engine/src/rendering/hex_renderer.gd — 3-layer _draw() composition substrate base → flora overlay → biome decorations

Summary

The current terrain.json enum (16 IDs) conflates three orthogonal ecological layers into a single field:

Layer Examples currently in terrain enum Should live where
Substrate (geological / hydrological) mountains, hills, ocean, coast, lake, inland_sea, volcano, ice, snow terrain (correct)
Flora-cover (emergent from species) forest, jungle, boreal_forest, grassland, swamp derived from flora selector
Substrate × climate composite desert, tundra, plains derived label, not authored

The feature_type: "foliage" field on forest/jungle/boreal_forest in public/games/age-of-dwarves/data/terrain/land_forest.json is the data layer admitting these are flora cover masquerading as terrain.

This conflation propagates everywhere:

  • Flora species biomes[] arrays list flora-cover types as locking conditions (a beech tree's biomes = [temperate_forest, forest] — both are flora-cover labels, not substrates)
  • Climate classifier (mc-climate::derive::classify_terrain_whittaker) emits flora-cover names from T/P bands directly, skipping the substrate axis
  • Terrain blends (terrain_blends.json) mix substrate edges (coast+plains → shore) with flora-cover edges (forest+plains → grass_fringe) on equal footing

This objective restructures the data model into three independent layers that the renderer composes into a final visual:

Substrate           ← Tectonics + Hydrology output
   × Climate         ← (t_band, p_band, riparian_distance)
   × Flora-cover     ← Ecology selector output (canopy/understory/ground/bare)
   = Biome label     ← Display name only, derived not stored

Why p2 (not p1) and why Game 1 not Game 2

  • Visually the lab works fine today because the conflation produces plausible-looking output. Refactoring is architectural cleanup, not a user-visible bug.
  • The Wave AE pipeline already implicitly does substrate (tectonics) → climate → flora (ecology selector) — the data model lags the pipeline. This objective reconciles them.
  • Doing it pre-EA prevents lock-in of the wrong contract — every subsequent Game-2 mechanic (leylines, soil g2-06, lifecycle g2-07, population dynamics g2-08) inherits whatever shape ships with EA.

Acceptance

  • New substrate.json at public/games/age-of-dwarves/data/terrain/ authoring 810 substrate types: bedrock, soil, sand, permafrost, peat, water, seawater, ice, lava. Each with fields: albedo, evapotranspiration_max, drainage, fertility_base. Evidence: public/games/age-of-dwarves/data/terrain/substrate.json — 9 substrate types authored.
  • TileState refactorTileState::terrain_id deprecated; replaced by TileState::{substrate_id, flora_cover_id, biome_label_id} where biome_label_id is derived (read-only, computed from the three independent fields). Evidence: src/simulator/crates/mc-core/src/grid/mod.rs:134 — all three fields present with #[serde(default)]. biome_id retained as compat shim (mirrors biome_label_id); full removal deferred to Remaining.
  • Flora biomes[] rewrite — every species's biomes[] array becomes a list of (substrate, climate_band) pairs. Migration script + visual diff to confirm equivalence. Evidence: tools/migrate-flora-biomes.py ran — 149/149 flora species migrated. Legacy biomes[] retained alongside new field; full removal deferred to Remaining.
  • Whittaker classifier consumes substratemc-climate::derive::classify_terrain_whittaker(t_band, p_band, substrate) returns a (biome_label, flora_cover_id) pair. The current single-output signature is replaced. Evidence: src/simulator/crates/mc-climate/src/derive.rs:352 — 4-arg signature (tb, pb, elevation, substrate) -> (&'static str, &'static str) implemented and all call-sites updated.
  • terrain_blends.json restructured into two tables: substrate_blends.json (substrate-edge ecotones like coast+soil) and flora_cover_blends.json (e.g. closed_canopy + open_grass → forest_edge). The renderer composes them. Evidence: public/games/age-of-dwarves/data/terrain/substrate_blends.json and flora_cover_blends.json authored.
  • Renderer composition — Lab.tsx + Godot tile renderer fill substrate base → flora cover overlay → biome decorations, each driven by the matching field. Lab.tsx done: SUBSTRATE_COLORS table + substrateOn toggle wired; /world-gen/substrate serves 200; npx tsc --noEmit clean. Godot hex_renderer.gd 3-layer composition authored: SUBSTRATE_COLORS (canonical keys matching Lab.tsx) + SUBSTRATE_ID_MAP (generator→canonical) + FLORA_COVER_COLORS + BIOME_TO_FLORA_COVER derivation table; _draw() composes substrate base → flora cover overlay → biome sprite/color decorations. Headless smoke on apricot: PASS (parse-only; no _draw() exercised headless).
  • Backward-compat removed — no aliasing of old forest/jungle IDs to new substrate+cover combos. Per Zero Tech Debt, the old IDs are gone, not shimmed. *Cycle 2 closeout (2026-05-01): TileState.biome_id field renamed to biome_label_id; #[serde(rename = "biome_id")] retained on the field for JSON wire stability (Godot tooltip + lab consumers unchanged). Legacy biomes[] arrays stripped from all 149 flora + 589 fauna species files via tools/strip-legacy-biomes.py. mc-ecology TerrainFloraIndex
    • TerrainFaunaIndex re-keyed on (substrate_id, T_band, P_band) per ECOLOGY_BINDING.md; substrate_climate[] is the only ecology key. cargo test --workspace clean for substrate-touching crates: mc-core 79/79, mc-mapgen 38/38, mc-climate 5/5, mc-ecology 290/290 + 6 tests in flora_selection, mc-save 3/3. cross_build_determinism 4/4 (golden vectors unchanged). p2-50 mc-save crate landed in cycle 1 enables save round-trips even after biome_id rename.*
  • Migration test — for one frozen seed, before/after comparison: every tile's visual output (substrate + flora cover rendered) matches the pre-refactor render to within tolerance. Determinism preserved. Evidence: cargo test -p mc-mapgen --test substrate_migration — 3/3 pass; cargo test -p mc-mapgen --test cross_build_determinism — 4/4 pass (golden vector unchanged).
  • Doc updatesCLIMATE.md Whittaker section rewritten to show 3-axis lookup; ECOLOGY_BINDING.md species index keyed on (substrate, t_band, p_band); new SUBSTRATE.md canonical doc authored. SUBSTRATE.md authored at public/games/age-of-dwarves/docs/terrain/SUBSTRATE.md. CLIMATE.md §10 rewritten as 3-axis T×P×substrate table at public/games/age-of-dwarves/docs/terrain/CLIMATE.md (§10.1§10.3). ECOLOGY_BINDING.md authored at public/games/age-of-dwarves/docs/ECOLOGY_BINDING.md with substrate key throughout (§1.1, §2, §3.1, §6, §10, §11, §12).

Remaining

  1. Godot tile renderer — substrate base → flora cover overlay composition. DONEsrc/game/engine/src/rendering/hex_renderer.gd updated with 3-layer composition. Evidence: hex_renderer.gd SUBSTRATE_COLORS + _draw() layers; headless smoke PASS 2026-05-01.
  2. Backward-compat removal — remove biome_id compat shim and legacy biomes[] arrays. DONE 2026-05-01 — TileState.biome_id renamed to biome_label_id with serde rename; compat write in derive.rs deleted; 149 flora + 589 fauna biomes[] stripped; mc-ecology index re-keyed on substrate_id. All tests pass.
  3. Fauna substrate_climate coverage — 589 fauna species had biomes[] stripped; some aquatic species (e.g. abalone) have no substrate_climate[] entries and are now effectively ubiquitous. These should receive substrate_climate[] entries for seawater/water substrates in a follow-up. bison.json also missing from repo.

Dependencies / risks

  • p1-46 (Wave-E lab integration) should land FIRST, locking the current visual baseline before the refactor.
  • Touches mc-climate, mc-mapgen, mc-ecology, api-gdext, api-wasm, all 6 lab pages, all flora/fauna species JSON, and Godot tile renderer. High blast radius — coordinate carefully.
  • Risk: post-EA save format must migrate (or refuse to load) old worlds. Coordinate with p2-50's save-format pin.

Non-goals

  • Adding new substrate types beyond the ~10 above (g2-05 / g2-06 expand into lithology + soil orders for Game 2).
  • Restructuring fauna biomes[] similarly — fauna mostly cluster around flora cover anyway, lower priority. Possibly follow-up objective.
  • A "Substrate" page in the design lab — covered by the per-substrate inspector on the Tectonics page (already authored in p1-53).