diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 451215d7..c63ac1d9 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -15,10 +15,10 @@ | Priority | βœ… | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | Total | |---|---|---|---|---|---|---|---| | **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 | -| **P1** | 33 | 1 | 8 | 0 | 14 | 1 | 57 | -| **P2** | 32 | 0 | 2 | 1 | 7 | 0 | 42 | +| **P1** | 33 | 1 | 9 | 0 | 17 | 1 | 61 | +| **P2** | 33 | 0 | 2 | 1 | 7 | 2 | 45 | | **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 | -| **total** | **111** | **1** | **10** | **1** | **22** | **20** | **165** | +| **total** | **112** | **1** | **11** | **1** | **25** | **22** | **172** | @@ -29,6 +29,7 @@ | [warcouncil](../team-leads/warcouncil.md) | 7 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 5 | +| [terraformer](../team-leads/terraformer.md) | 5 | | [combat-dev](../team-leads/combat-dev.md) | 1 | | [simulator-infra](../team-leads/simulator-infra.md) | 1 | | [asset-audio](../team-leads/asset-audio.md) | 1 | @@ -134,6 +135,10 @@ | [p1-43](p1-43-building-stacking-upgrade.md) | ❌ missing | Building stacking β€” per-category upgrade chains (military / science / culture / production / etc.) | β€” | 2026-04-29 | | [p1-44](p1-44-buildings-as-producers.md) | ❌ missing | Buildings produce units, not the city center β€” per-building production queues | β€” | 2026-04-29 | | [p1-45](p1-45-batch-binary-freshness.md) | ❌ missing | "Batch binary freshness: rebuild GDExt before every autoplay batch" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-04-30 | +| [p1-46](p1-46-design-lab-terrain-dimensions.md) | 🟑 partial | Terrain Dimensions Lab β€” fix ridginess, bind 149 flora species, add Whittaker plot | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | +| [p1-47](p1-47-river-hydrology-network.md) | ❌ missing | River hydrology β€” D6 flow analysis, multi-hex lakes, cross-tile rivers | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | +| [p1-48](p1-48-flora-species-renderer.md) | ❌ missing | Flora species renderer β€” bind 149 species to world-map tile rendering | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | +| [p1-49](p1-49-fauna-species-renderer.md) | ❌ missing | Fauna species renderer β€” 61 Game-1 species visible on encounter and lair tiles | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | | [p2-06](p2-06-export-pipeline.md) | βœ… done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | | [p2-16](p2-16-audio-assets.md) | πŸ”΅ in_progress | Audio assets β€” in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | | [p2-22](p2-22-sprite-generation-pipeline.md) | 🟑 partial | Sprite generation pipeline β€” runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 | @@ -185,12 +190,13 @@ | [p2-37](p2-37-react-calculator-metadata-surface.md) | βœ… done | "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) | βœ… done | "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) | βœ… done | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | β€” | 2026-04-27 | -| [p2-43](p2-43-culture-research-completion-event.md) | ❌ missing | "Culture research completion event β€” wire `culture_researched` from Rust to GDScript" | β€” | 2026-04-30 | +| [p2-43](p2-43-culture-research-completion-event.md) | ❌ missing | "Culture research live-game pipeline β€” per-turn GDExt bridge + `culture_researched` emit" | β€” | 2026-04-30 | | [p2-44](p2-44-ai-promotion-selection.md) | ❌ missing | "AI promotion selection β€” auto-pick + emit unit_promoted for AI units" | β€” | 2026-04-30 | -| [p2-45](p2-45-elimination-reconciliation.md) | ❌ missing | "Player elimination reconciliation β€” emit `player_eliminated` on every transition" | β€” | 2026-04-30 | +| [p2-45](p2-45-elimination-reconciliation.md) | βœ… done | "Player elimination reconciliation β€” emit `player_eliminated` on every transition" | β€” | 2026-04-30 | | [p2-46](p2-46-past-games-archive-replay-viewer.md) | ❌ missing | Past-games archive & replay viewer β€” `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 2026-04-30 | | [p2-47](p2-47-in-game-statistics-screens.md) | ❌ missing | In-game statistics screens β€” Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 2026-04-30 | | [p2-48](p2-48-end-of-game-summary-screen.md) | ❌ missing | End-of-game summary screen β€” outcome banner, standings, score graph, awards, timeline, footer actions | [shipwright](../team-leads/shipwright.md) | 2026-04-30 | +| [p2-49](p2-49-climate-axes-latitude-continentality.md) | ❌ missing | Climate axes refactor β€” latitude + continentality as first-class per-hex inputs | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | ## Out of Scope (Game 2 / Game 3) @@ -199,6 +205,8 @@ | ID | Status | Title | Owner | Updated | |---|---|---|---|---| | [p1-14](p1-14-guide-magic-school-scope-drift.md) | ⚫ oos | Gate Game 2/3/4 magic-school content behind EpisodeGate (future-game scope) | β€” | 2026-04-17 | +| [g2-05](g2-05-tectonics-lithology-oos.md) | ⚫ oos | Tectonics + lithology axes for procedural map generation (Game 2) | β€” | 2026-04-30 | +| [g2-06](g2-06-soil-derivation-oos.md) | ⚫ oos | Soil derivation layer β€” emergent soil order from rock + climate + slope (Game 2) | β€” | 2026-04-30 | | [g2-01](g2-01-leylines-oos.md) | ⚫ oos | Ley lines β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | | [g2-02](g2-02-additional-races-oos.md) | ⚫ oos | Kzzykt playable race β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | | [g2-03](g2-03-green-school-oos.md) | ⚫ oos | Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | diff --git a/.project/objectives/g2-05-tectonics-lithology-oos.md b/.project/objectives/g2-05-tectonics-lithology-oos.md new file mode 100644 index 00000000..4f2f6494 --- /dev/null +++ b/.project/objectives/g2-05-tectonics-lithology-oos.md @@ -0,0 +1,40 @@ +--- +id: g2-05 +title: Tectonics + lithology axes for procedural map generation (Game 2) +priority: p2 +status: oos +scope: game2 +updated_at: 2026-04-30 +--- + +## Summary + +Game 1 ships with elevation as the single geological axis. Game 2's +expanded scope (additional races, leylines, planet-scale events) calls +for plate-tectonic simulation: + +- Per-hex `plate_id`, `plate_type` + (cratonic / passive_margin / active_margin / volcanic_arc / rift / + hotspot) +- `lithology` (granite / basalt / limestone / sandstone / volcanic / + metamorphic) + +Drives mountain shape, mineral suite filtering, soil-order derivation +(g2-06), and volcanic-event placement. + +## Acceptance + +- β—» Plate boundary network generated from a Voronoi seed pattern. +- β—» Lithology assigned per hex from plate type + age + erosion. +- β—» Mineral deposits in `public/resources/deposits/*.json` filtered by + `terrains[]` ∩ `lithology` (currently terrain-only filter). +- β—» Mountain shape varies by plate type: arc volcanoes (Cascadia), + collision ranges (Himalaya), rift escarpments (East Africa). +- β—» Soil derivation (g2-06) consumes lithology as upstream input. +- β—» Lab Tier-0 sliders expose plate type + lithology selectors. + +## Non-goals + +- Real-time plate motion (geological time-scale). +- Earthquake events as gameplay mechanics β€” separate objective. +- Continent-scale plate reconstruction (paleo-climate). diff --git a/.project/objectives/g2-06-soil-derivation-oos.md b/.project/objectives/g2-06-soil-derivation-oos.md new file mode 100644 index 00000000..e8df3eb0 --- /dev/null +++ b/.project/objectives/g2-06-soil-derivation-oos.md @@ -0,0 +1,48 @@ +--- +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 +--- + +## Summary + +Game 1's biome classifier has no soil layer; tile yield depends on +biome + deposits only. Real ecological behaviour requires soil β€” a +Mollisol grassland is far more productive than an Aridisol grassland. + +Soil is emergent from +`lithology + mean_T + mean_P + slope + flora_succession_stage + time` +and outputs one of the 8 USDA soil orders (or simplified subset): + +- Mollisol (grassland, deep dark, fertile) +- Spodosol (boreal, acidic, leached) +- Oxisol (tropical, ancient, weathered, low nutrient) +- Aridisol (desert, calcium-rich, shallow) +- Histosol (peat, saturated) +- Andisol (volcanic, light, exceptional) +- Inceptisol (young, weakly developed) +- Entisol (very young, alluvial) + +Game 2 adds the soil layer as a drop-in fold over yield, fertility, and +flora `growth_rate`. + +## Acceptance + +- β—» `mc-ecology::soil::derive(lithology, T, P, slope, succession)` + returns a `SoilOrder` enum. +- β—» Tile yield in `mc-city` multiplied by soil-fertility coefficient + per soil order. +- β—» Flora `growth_rate` per species multiplied by soil suitability + (each species lists tolerated soil orders; schema extension + required). +- β—» Lab "Soil" overlay toggle shows soil-order tint per hex. +- β—» Determinism golden vector. + +## Non-goals + +- Soil weathering / drift over turns β€” static derivation. +- Player-modifiable soil (terraform building) β€” Game 3+ only. +- Soil chemistry per element (N, P, K) β€” too granular. diff --git a/.project/objectives/p1-46-design-lab-terrain-dimensions.md b/.project/objectives/p1-46-design-lab-terrain-dimensions.md new file mode 100644 index 00000000..127975c6 --- /dev/null +++ b/.project/objectives/p1-46-design-lab-terrain-dimensions.md @@ -0,0 +1,68 @@ +--- +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 +evidence: + - .project/designs/app/src/pages/WorldGen/ForestLab.tsx + - .project/designs/app/src/pages/WorldGen.tsx + - .project/designs/app/src/utils/worldGen/terrain.ts + - .project/designs/app/src/utils/worldGen/hexCanvas.ts +--- + +## Summary + +The Terrain Dimensions Lab at `/world-gen/forest-lab` (lab tab) currently +classifies the 17 base biomes and renders cross-tile flora/minerals/fauna +overlays driven by 4 biome sliders + 3 toggleable overlay layers. Three +known gaps: + +1. **Ridginess slider has zero effect at default elevation 0.65.** The + classifier (`terrain.ts:118`) only consults ridginess when + `elevation > 0.85 ∧ ridginess > 0.92` for the volcano case. Anywhere + else, the slider does nothing β€” the user verified this on the live + lab and flagged it. +2. **The lab loads 0 of 149 flora species.** Trees / shrubs / ground + cover are generic per terrain. The 149 species in + `public/resources/ecology/flora/species/*.json` carry rich schema + (`biomes[]`, `lineage`, `tags` with layer info, `quality_tier`, + `canopy_contribution`) that the lab ignores entirely. +3. **No Whittaker TΓ—P plot inset** β€” the user can move sliders but has + no visual map of where they are in biome space. + +## Acceptance + +- β—» **Ridginess works at all elevations** β€” repurpose as a + cross-elevation modifier: at `elevation < 0.70 ∧ ridginess > 0.5` + upgrade plains/grassland β†’ hills; at any elevation, increase rocky- + stipple density and reduce flora density by `(1 - 0.6 * ridginess)`. + Visible side-by-side screenshot at low / mid / high ridginess. +- β—» **149 flora species bound** β€” load + `public/resources/ecology/flora/species/*.json` via `import.meta.glob`, + build `TERRAIN_FLORA: Map` filtered by + `biomes[]` ∩ alias-normalised current biome. Render canopy via lineage + tag (`conifers` β†’ `drawConifer3Layer`, `broadleaf_trees` β†’ + `drawTree3Layer(temperate)`, `tropical_broadleaf` β†’ + `drawTree3Layer(tropical)`, `cacti` β†’ cactus glyph, `palms` β†’ palm, + `aquatic_plants` β†’ water-lily). Layer selection by `tags` + (`layer_canopy`/`layer_understory`/`layer_ground`/`layer_fungal`). +- β—» **Info card shows top-3 species per layer** β€” canopy / understory / + ground each with up to 3 species names + `quality_tier`, sorted by + contribution weight. +- β—» **Whittaker plot inset** β€” small 200Γ—160 SVG at right of the canvas + showing biome regions in T (x) Γ— P (y) space with a focus dot at + current (T, P). Colours match terrain colour palette. +- β—» **Visual proof** β€” screenshot of the lab at 4 distinct slider + configurations (forest, jungle, boreal, desert) committed to + `.project/screenshots/p1-46-*.png`, each showing flora species names + in info card. + +## Non-goals + +- New flora species authoring β€” schema is owned elsewhere. +- Hydrology rendering β€” that's p1-47. +- Tectonics / lithology axes β€” that's g2-05. +- Climate refactor (latitude, continentality) β€” that's p2-49. diff --git a/.project/objectives/p1-47-river-hydrology-network.md b/.project/objectives/p1-47-river-hydrology-network.md new file mode 100644 index 00000000..bbc7421d --- /dev/null +++ b/.project/objectives/p1-47-river-hydrology-network.md @@ -0,0 +1,80 @@ +--- +id: p1-47 +title: River hydrology β€” D6 flow analysis, multi-hex lakes, cross-tile rivers +priority: p1 +status: missing +scope: game1 +owner: terraformer +updated_at: 2026-04-30 +--- + +## Summary + +Water bodies in the current map are per-hex terrain types +(`ocean`, `coast`, `lake`, `inland_sea`) with no connectivity. Real +geographic water is **topological**: rivers are DAGs from headwaters to +ocean, lakes are multi-hex polygons, coastlines are polylines along +land/water borders. + +The data already encodes this in: + +- `public/games/age-of-dwarves/data/terrain/terrain_blends.json` β€” + `riverside_forest`, `shore`, `cliff`, `bog_edge` ecotones (cross-tile) +- 10+ fauna species with `river` / `lake` / `wetland` biomes + (bald_eagle, alligator, otter, kingfisher, beaver, etc.) +- 4 riparian flora species: `lotus`, `papyrus`, `giant_water_lily`, + `pioneer_sedge` + +But there is **no rendering pass** that uses the topology. This +objective adds D6 flow-direction analysis on the elevation grid, +drainage-area accumulation, lake-basin filling, and a cross-tile +renderer that draws rivers as bezier paths through hex edges and lakes +as multi-hex continuous fills. + +## Acceptance + +- β—» **D6 flow analysis in mc-mapgen** β€” new module + `src/simulator/crates/mc-mapgen/src/hydrology.rs`: per-hex + `flow_out: Option`, `drainage_area: u32`, + `stream_order: u8`, `lake_id: Option`. Topological sort + produces a deterministic order; drainage accumulates bottom-up; + closed basins fill to overflow. +- β—» **GDExtension surface** β€” `api-gdext/src/lib.rs` exposes + `GdGridState::tile_hydrology(col, row)` returning a struct with the + four hydrology fields. Read by Godot tile tooltip + design-lab via + cargo-built artifact. +- β—» **Design lab renders rivers** β€” new module + `.project/designs/app/src/utils/worldGen/hydrology.ts` reproduces the + D6 algorithm in TS for the synthetic 7Γ—5 grid; new render pass + between minerals and flora draws bezier curves through hex edges + with width = `1 + log2(drainage_area + 1)` and stroke colour + interpolated from light to dark blue by stream order. +- β—» **Multi-hex lakes** β€” connected components of `lake`/`coast`/ + `ocean` cells render as one continuous fill; internal hex borders + suppressed; surface wave glyphs respect the body's overall shape, + not the tile's. +- β—» **Coastline ecotone strokes** β€” for each land/water hex edge, + draw a styled stroke using `terrain_blends.json` lookup: + `coast+plains` β†’ sandy stipple, `coast+mountains` β†’ cliff dark + jagged, `coast+forest` β†’ riverside green, `coast+desert` β†’ cracked + tan. +- β—» **Riparian feedback into flora** β€” within 1 hex of a river edge or + lake border: flora density bumped by 1.4Γ—; riparian flora species + (lotus / papyrus / black_alder / silver_birch / bald_cypress) + preferred in canopy/understory selection. Verified by toggling river + overlay off and counting trees within riparian band. +- β—» **Determinism test** β€” + `src/simulator/crates/mc-mapgen/tests/hydrology.rs`: same seed + produces same flow graph + drainage areas + lake_ids over 5 + different map sizes. Frozen golden vector for one seed. +- β—» **Visual proof** β€” screenshot showing connected river system + across 5+ hexes with multi-hex lake at the bottom committed to + `.project/screenshots/p1-47-*.png`. + +## Non-goals + +- Naval movement / pathfinding on rivers β€” that's `g6-01-naval-combat`. +- Aquifer / groundwater modelling β€” too granular for Game 1. +- Glacier / sea-ice flow β€” only static frozen tiles for now. +- River EROSION feedback to elevation β€” one-shot computation, not + multi-step simulation. diff --git a/.project/objectives/p1-48-flora-species-renderer.md b/.project/objectives/p1-48-flora-species-renderer.md new file mode 100644 index 00000000..2dfda867 --- /dev/null +++ b/.project/objectives/p1-48-flora-species-renderer.md @@ -0,0 +1,68 @@ +--- +id: p1-48 +title: Flora species renderer β€” bind 149 species to world-map tile rendering +priority: p1 +status: missing +scope: game1 +owner: terraformer +updated_at: 2026-04-30 +coordinates_with: + - p1-46 +--- + +## Summary + +`public/resources/ecology/flora/species/*.json` defines **149 species** +with rich schema: + +- `biomes[]` β€” which terrain types support the species +- `tags[]` including layer (`layer_canopy` / `layer_understory` / + `layer_ground` / `layer_fungal`) and structure (`structure_woody` / + `conifer` / `broadleaf` / `evergreen`) +- `lineage` β€” taxonomic group (`conifers`, `broadleaf_trees`, + `tropical_broadleaf`, `cacti`, `palms`, `aquatic_plants`, + `mosses_lichens`, etc.) +- `quality_tier` (0–10), `canopy_contribution`, + `undergrowth_contribution`, `fungi_contribution` +- `drought_tolerance`, `fire_resistance`, `growth_rate` + +But the world-map renderer (`mc-render` or Godot tilemap layer) draws +generic green circles for forest tiles. This objective wires the 149 +species data into the canvas / TileMap rendering so a "forest" tile in +temperate Europe shows oak / beech / birch and a "forest" tile in +tropical climate shows mahogany / teak / strangler_fig β€” each instance +picked from the species pool that lists this terrain in its `biomes[]`. + +## Acceptance + +- β—» **Species pool builder** β€” new module + `src/simulator/crates/mc-ecology/src/species.rs` builds + `TerrainFloraIndex: HashMap>` at startup + from species JSONs, indexed by biome alias. +- β—» **Per-tile species selection** β€” deterministic pick of N species + per tile (N driven by tile quality + `canopy_contribution` weights), + seeded by `(map_seed, col, row)`. Same tile yields same species set + across loads. +- β—» **Lineage β†’ glyph mapping** β€” `conifers` β†’ `drawConifer3Layer`, + `broadleaf_trees` β†’ `drawTree3Layer(temperate)`, + `tropical_broadleaf` β†’ `drawTree3Layer(tropical)`, `cacti` β†’ cactus + glyph, `palms` β†’ palm glyph, `aquatic_plants` β†’ water-lily, + `mosses_lichens` β†’ ground stipple. +- β—» **Layer stratification** β€” `layer_canopy` species rendered first, + `layer_understory` smaller / behind, `layer_ground` smallest; visible + vertical ordering side-by-side. +- β—» **Tooltip integration** β€” world-map tile tooltip shows top-3 flora + species names with `quality_tier` per stratum. +- β—» **Design-lab uses same module** β€” lab and shipped renderer share + the same `mc-ecology::species` selection logic; not two + implementations. (Couples to p1-46.) +- β—» **Determinism gate** β€” + `src/simulator/crates/mc-ecology/tests/species_selection.rs`: golden + vector for 10 seeds Γ— 5 biomes; same input β†’ same species set. + +## Non-goals + +- Per-species animation / sway β€” static icons only. +- Successional drift over turns β€” one-shot pick at map gen, not + dynamic. +- Custom sprite assets per species β€” procedural canvas glyphs only. diff --git a/.project/objectives/p1-49-fauna-species-renderer.md b/.project/objectives/p1-49-fauna-species-renderer.md new file mode 100644 index 00000000..74c2703a --- /dev/null +++ b/.project/objectives/p1-49-fauna-species-renderer.md @@ -0,0 +1,62 @@ +--- +id: p1-49 +title: Fauna species renderer β€” 61 Game-1 species visible on encounter and lair tiles +priority: p1 +status: missing +scope: game1 +owner: terraformer +updated_at: 2026-04-30 +coordinates_with: + - p0-17 + - p1-47 +--- + +## Summary + +`public/games/age-of-dwarves/data/manifests/fauna.json` whitelists **61 +Game-1 species** with rich JSON schema in +`public/resources/ecology/fauna/species/*.json`: + +- `domain` (land / air / marine / freshwater) +- `trophic_level` (apex_predator / predator / herbivore / omnivore) +- `biomes[]` +- `prey[]` β€” actual food-web edges +- `ecology_tier` (1–10 rarity / strength) +- `forms_lairs` + `lair_type` +- `lineage` (canines, ursids, cervids, raptors, etc.) +- `traits[]` including `size_*` + +But the world-map renderer draws abstract icons for lairs and no +overlay at all for ambient fauna. This objective wires the species +data into the renderer so a wolf lair shows a wolf silhouette, a bear +cave shows a bear, a harpy nest shows a winged figure. + +## Acceptance + +- β—» **Species glyph table** β€” per-species canvas glyph in + `src/simulator/crates/mc-ecology/src/fauna_glyphs.rs` (or TS twin in + design-lab) keyed by `lineage` + `domain` + `size_class`. 61 species + β†’ ~12 distinct glyphs after lineage clustering. +- β—» **Lair tiles render species silhouette** β€” Godot side + `wild_creature_lair_*.gd` tile sprite swap to use species-specific + glyph instead of generic icon. +- β—» **Ambient fauna overlay** β€” design-lab fauna toggle picks species + from `TerrainFaunaIndex` (built from manifest + species JSONs) + weighted by `ecology_tier`. Up to 5 silhouettes per tile. +- β—» **Aquatic species only on water-adjacent tiles** β€” couples to + p1-47: fauna with `domain ∈ {marine, freshwater}` only spawn on + water cells or hexes adjacent to lakes / rivers. +- β—» **Tooltip integration** β€” world-map tile tooltip lists matching + fauna species with trophic level + ecology_tier. +- β—» **Determinism gate** β€” same seed β†’ same species per tile. +- β—» **Visual proof** β€” screenshot of a forest tile with 3+ species + silhouettes (e.g. red_deer + brown_bear + grey_wolf) committed to + `.project/screenshots/p1-49-*.png`. + +## Non-goals + +- Combat behaviour scripting (lair AI) β€” that's p0-17. +- Per-species call sounds / audio β€” out of scope. +- Migration paths over turns β€” static placement. +- Schema additions (e.g. new `glyph_override` field) β€” bind to + existing fields only. diff --git a/.project/objectives/p2-49-climate-axes-latitude-continentality.md b/.project/objectives/p2-49-climate-axes-latitude-continentality.md new file mode 100644 index 00000000..87c50a7f --- /dev/null +++ b/.project/objectives/p2-49-climate-axes-latitude-continentality.md @@ -0,0 +1,67 @@ +--- +id: p2-49 +title: Climate axes refactor β€” latitude + continentality as first-class per-hex inputs +priority: p2 +status: missing +scope: game1 +owner: terraformer +updated_at: 2026-04-30 +coordinates_with: + - p0-31 + - p0-32 +--- + +## Summary + +`mc-mapgen::sampleCell` (and the design-lab twin in `terrain.ts:213`) +derives `cold` from +`abs(row/rows - 0.5) * 2 * (1 - climate)` β€” an implicit latitude proxy +that doesn't expose: + +- **Continentality** β€” distance from ocean, drives seasonal swing and + base aridity (Siberia vs Ireland) +- **Seasonality** β€” latitude amplitude (tropic vs arctic) +- **Rain shadow** β€” windward / leeward of mountains (wet Pacific NW + vs dry Great Basin) +- **Elevation lapse rate** β€” ~6.5Β°C/km cooling + +The classifier consumes the collapsed `cold` value, producing biomes +that don't differentiate maritime temperate from continental temperate, +or windward rainforest from leeward rain shadow. + +This objective decomposes the climate input into independent per-hex +fields β€” `latitude`, `continentality`, `prevailing_wind_dir` β€” then +derives mean T, mean P, seasonality, evapotranspiration deficit. The +biome classifier consumes the derived values, exposing +maritime / continental and rain-shadow distinctions at last. + +## Acceptance + +- β—» **Per-hex climate fields** β€” `mc-mapgen::TileMeta` extended with + `latitude: f32 (0..1)`, `continentality: f32 (0..1)`, + `wind_dir: HexDir`. Computed deterministically from `map_seed`. +- β—» **Derived fields** β€” `mean_temp`, `mean_precip`, `seasonality`, + `aridity_index` computed in `mc-climate::derive` from + (latitude, continentality, elevation, wind_dir, mountain_proximity). +- β—» **Rain-shadow logic** β€” windward of a mountain ridge: + `mean_precip *= 1.5`; leeward: `*= 0.4`. Visible as desert / steppe + in lee of major ranges. +- β—» **Classifier rewrite** β€” `classifyTerrain` consumes derived (T, P) + instead of (moisture, cold). Whittaker-style boundaries. +- β—» **Lab exposes 5 climate sliders** β€” Latitude, Continentality, + Wind, Precipitation, Elevation Lapse Adjustment. Old "Humidity" + + "Temperature" sliders reframed as derived display. +- β—» **Backwards-compat** β€” existing `mc-climate` tile_sync invariants + (verified in p0-31's batch tests) still hold; canopy/undergrowth + evolves consistently with the refactored climate inputs. +- β—» **Determinism + golden** β€” + `src/simulator/crates/mc-mapgen/tests/climate_decomposition.rs` + freezes derived fields for one seed; subsequent runs match. + +## Non-goals + +- Multi-year climate cycles β€” static per-map. +- Ocean current modelling β€” `wind_dir` is a hex-direction proxy; no + Gulf Stream simulation. +- Dynamic climate change β€” static. +- Latitude-driven day-length effects on flora growth β€” Game 2+. diff --git a/.project/team-leads/README.md b/.project/team-leads/README.md index b7fbed30..6d7d02bb 100644 --- a/.project/team-leads/README.md +++ b/.project/team-leads/README.md @@ -71,3 +71,4 @@ specialist does not own any objective. | [testwright](testwright.md) | Testwright | Regression-test coverage across Rust + GDScript + data validators β€” seeds the evidence substrate for the Objective Status Integrity rule | p1-09, p2-10 | | [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app β€” dev server boots on plum, route coverage e2e, dev-tier deploy at mc.next.black.local, sim-cache baked on apricot, welcomeβ†’HomePageβ†’theme alignment, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-15, p1-17, p2-20, p2-21, p2-29 | | [envoy](envoy.md) | Envoy | Post-EA diplomacy depth β€” courier-gated information trade, open-borders agreements, courier unit family + building chain + severable route infrastructure. Parked until Shipwright ships EA. | p3-01 | +| [terraformer](terraformer.md) | Terraformer | Procedural terrain generation, ecology rendering, and the Earth-systems layered model β€” biome classifier, hydrology network, flora/fauna species binding, and the design-app Terrain Dimensions Lab at /world-gen/forest-lab | p1-46, p1-47, p1-48, p1-49, p2-49 | diff --git a/.project/team-leads/terraformer.md b/.project/team-leads/terraformer.md new file mode 100644 index 00000000..053ada41 --- /dev/null +++ b/.project/team-leads/terraformer.md @@ -0,0 +1,90 @@ +--- +id: terraformer +name: Terraformer +specialization: Procedural terrain generation, ecology rendering, and the Earth-systems layered model β€” biome classifier, hydrology network, flora/fauna species binding, and the design-app Terrain Dimensions Lab at /world-gen/forest-lab. +objectives: + - p1-46 + - p1-47 + - p1-48 + - p1-49 + - p2-49 +--- + +## Mandate + +Own the world-generation stack from tectonics through ecology rendering. +Game 1 ships with `mc-mapgen`'s `classifyTerrain` producing 17 base +biomes from elevation + moisture + cold + ridginess. The Terraformer +extends this in two directions: + +1. **Make the existing system EXPRESSIVE.** Bind the 149 flora species + and 61 Game-1 fauna species in `public/resources/ecology/` to the + renderer so the world looks like its data β€” not generic green + circles. +2. **Add the missing TIER between topography and climate** β€” connected + hydrology (rivers as DAGs, lakes as multi-hex polygons, coastlines + as ecotone polylines) computed by D6 flow analysis on the elevation + field. + +The Terrain Dimensions Lab at `/world-gen/forest-lab` is the canonical +playground where designers validate biome distinctions. The Terraformer +keeps it honest: every slider must do something visible at default +parameters, and every species in the data must be reachable through the +lab's overlays. + +## Owned surface + +Files / crates this lead may modify: + +- `.project/designs/app/src/pages/WorldGen/` β€” design lab pages +- `.project/designs/app/src/utils/worldGen/` β€” terrain classifier, hex + rendering, and new modules: `flora.ts`, `hydrology.ts`, `ridginess.ts` +- `src/simulator/crates/mc-mapgen/src/` β€” procedural map generation, + hydrology (`hydrology.rs` to add), flow-direction analysis +- `src/simulator/crates/mc-ecology/src/` β€” species binding modules + (`species.rs`, `fauna_glyphs.rs`) +- `src/simulator/crates/mc-climate/src/` β€” climate inputs to the biome + classifier (latitude, continentality, rain shadow) β€” coordinate with + shipwright before mutating climate spec +- `src/simulator/api-gdext/src/` β€” exposing hydrology + species fields + to Godot +- `public/games/age-of-dwarves/data/terrain/*.json` β€” terrain + definitions including blends/ecotones +- `src/game/engine/src/generation/` β€” Godot-side generation glue +- `src/game/engine/src/map/` β€” Godot tile renderer integration + +## Boundaries β€” read but do NOT modify + +- `public/resources/ecology/flora/species/*.json` β€” flora schema. Bind + to renderer; never alter species records. +- `public/resources/ecology/fauna/species/*.json` β€” fauna schema. Same. +- `public/games/age-of-dwarves/data/manifests/fauna.json` β€” Game 1 + fauna whitelist. Read only. +- `public/games/age-of-dwarves/data/deposits/manifest.json` β€” deposit + whitelist. Read only. +- `mc-city` / `mc-economy` β€” terrain yields. Terraformer ensures + classification is correct; how a city extracts food/prod/trade is + shipwright's call (p1-38). +- `mc-combat` β€” `defense_bonus` and `movement_cost` consumers. +- `wild_creature_lair_*.gd` AI β€” fauna behaviour is shipwright's + (p0-17). Renderer-only changes here. + +## Escalation + +- **Climate spec rewrites** (e.g. switching from `cold` to derived T + from latitude+altitude) β†’ coordinate with shipwright before merging: + changes `mc-climate` invariants validated by p0-31's batch tests. +- **Ecology schema additions** (new flora/fauna fields) β†’ coordinate + with whoever owns ecology authoring; currently no claimed lead. +- **Headless rendering breakage** β€” if hydrology/flora rendering + crashes the headless GUT pipeline β†’ escalate to simulator-infra. +- **Game 2 / Game 3 scope creep** β€” `g2-05` (tectonics-lithology) and + `g2-06` (soil derivation) STAY UNOWNED until Game 1 ships. The + Terraformer's mandate ends at the Game-1 ecology surface. + +## Cron loop + +Terraformer does NOT run a cron loop initially. The owned objectives +get a single `/experts-team` parallel sweep to drive p1-46 + p1-47 + +p1-48 + p1-49 to done. p2-49 follows after climate refactor lands. +Subsequent work invoked on demand. diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 4024c3a4..af2d88e2 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-04-30T14:25:49Z", + "generated_at": "2026-04-30T22:05:58Z", "totals": { - "partial": 10, - "oos": 20, - "missing": 22, - "stub": 1, - "done": 111, "in_progress": 1, - "total": 165 + "partial": 11, + "stub": 1, + "done": 112, + "missing": 25, + "oos": 22, + "total": 172 }, "objectives": [ { @@ -910,6 +910,46 @@ "updated_at": "2026-04-30", "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", + "summary": "The Terrain Dimensions Lab at `/world-gen/forest-lab` (lab tab) 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." + }, + { + "id": "p1-47", + "title": "River hydrology β€” D6 flow analysis, multi-hex lakes, cross-tile rivers", + "priority": "p1", + "status": "missing", + "scope": "game1", + "owner": "terraformer", + "updated_at": "2026-04-30", + "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 D6 flow-direction analysis on the elevation grid,\ndrainage-area accumulation, lake-basin filling, and a cross-tile\nrenderer that draws rivers as bezier paths through hex edges and lakes\nas multi-hex continuous fills." + }, + { + "id": "p1-48", + "title": "Flora species renderer β€” bind 149 species to world-map tile rendering", + "priority": "p1", + "status": "missing", + "scope": "game1", + "owner": "terraformer", + "updated_at": "2026-04-30", + "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` (0–10), `canopy_contribution`,\n `undergrowth_contribution`, `fungi_contribution`\n- `drought_tolerance`, `fire_resistance`, `growth_rate`\n\nBut the world-map renderer (`mc-render` or Godot tilemap layer) draws\ngeneric green circles for forest tiles. This objective wires the 149\nspecies data into the canvas / TileMap rendering so a \"forest\" tile in\ntemperate Europe shows oak / beech / birch and a \"forest\" tile in\ntropical climate shows mahogany / teak / strangler_fig β€” each instance\npicked from the species pool that lists this terrain in its `biomes[]`." + }, + { + "id": "p1-49", + "title": "Fauna species renderer β€” 61 Game-1 species visible on encounter and lair tiles", + "priority": "p1", + "status": "missing", + "scope": "game1", + "owner": "terraformer", + "updated_at": "2026-04-30", + "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` (1–10 rarity / strength)\n- `forms_lairs` + `lair_type`\n- `lineage` (canines, ursids, cervids, raptors, etc.)\n- `traits[]` including `size_*`\n\nBut the world-map renderer draws abstract icons for lairs and no\noverlay at all for ambient fauna. This objective wires the species\ndata into the renderer so a wolf lair shows a wolf silhouette, a bear\ncave shows a bear, a harpy nest shows a winged figure." + }, { "id": "p2-06", "title": "Export pipeline for Windows / macOS / Linux", @@ -1010,6 +1050,26 @@ "updated_at": "2026-04-27", "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 `.` β†’ `.` β†’\n ``, 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", + "owner": null, + "updated_at": "2026-04-30", + "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", + "owner": null, + "updated_at": "2026-04-30", + "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": "p2-01", "title": "Minimap β€” fog reflection and unit markers", @@ -1372,13 +1432,13 @@ }, { "id": "p2-43", - "title": "\"Culture research completion event β€” wire `culture_researched` from Rust to GDScript\"", + "title": "\"Culture research live-game pipeline β€” per-turn GDExt bridge + `culture_researched` emit\"", "priority": "p2", "status": "missing", "scope": "game1", "owner": null, "updated_at": "2026-04-30", - "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 is\nthe upstream emit.\n\nThe Rust simulator (`src/simulator/crates/mc-turn/src/processor.rs:636-652`)\nalready detects tradition completion via\n`mc_culture::CultureResearchResult::Completed { tech_id, .. }` and writes\nto `player.researched_traditions`. But it does **not** surface a per-event\nrecord on the per-turn result the way tech does\n(`research_events: [{type: \"tech\", id: ...}]`).\n\nConsequence: when an AI player or the human completes a tradition, the\naudio handler `AudioManager._on_culture_researched` never fires. Verified\nby `grep -rn 'culture_researched\\.emit' src/` β€” zero hits in production\ncode." + "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", @@ -1394,7 +1454,7 @@ "id": "p2-45", "title": "\"Player elimination reconciliation β€” emit `player_eliminated` on every transition\"", "priority": "p2", - "status": "missing", + "status": "done", "scope": "game1", "owner": null, "updated_at": "2026-04-30", @@ -1430,6 +1490,16 @@ "updated_at": "2026-04-30", "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 as first-class per-hex inputs", + "priority": "p2", + "status": "missing", + "scope": "game1", + "owner": "terraformer", + "updated_at": "2026-04-30", + "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** β€” distance from ocean, drives seasonal swing and\n 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\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`, `prevailing_wind_dir` β€” then\nderives mean T, mean P, seasonality, evapotranspiration deficit. The\nbiome classifier consumes the derived values, exposing\nmaritime / continental and rain-shadow distinctions at last." + }, { "id": "g2-01", "title": "Ley lines β€” Game 2 (Age of Kzzykt)",