diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index b6c168fc1..3b2602abd 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -2702,3 +2702,18 @@ BASELINE @ believability-v1: Arbour 3/7, Edict 3/7. FAIL: moisture gradient (dis DEFERRED (ticket item 3 — explicitly optional): per-layer PNG maps (the T-960 Atlas-viewer stopgap). The contrast+coherence metrics are the core enforcer; maps are a follow-up if the human-gate review wants them before T-960. This is the D-245 enforcer; advisory now, ratchets to strict (BELIEVABILITY_STRICT) once Q-123 calibrates thresholds and T-1080/1081/1082 flip the criteria.', NULL, '2026-06-28 12:56:03', '2026-06-28 12:56:03.498', '2026-06-28 12:56:03.498', NULL, '73ed53bc02a79f3112f30dea16a82dd3', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 13:00:26', '2026-06-28 13:00:26.933', '2026-06-28 13:00:26.933', NULL, 'e840ecc301d33695e4866a7acdace1ce', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'status', 'in_progress', 'in_progress', NULL, '2026-06-28 13:01:05', '2026-06-28 13:01:05.826', '2026-06-28 13:01:05.826', NULL, '2db838e8ab9ce3cf6385ea00f484e382', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'description', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it. + +--- +Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to discuss'': + +- FAMILY: dedicated WaterBody family (9th D-239 family) for OpenOcean/Lake/TidalFlat, branching internally on morphology_zone (like IncisedGorge covers MountainPass/ValleyFloor/Alpine). Amend D-239 §5/§6 (8→9 families) + zone_to_family. +- FIDELITY: RICHER — a Shallow fringe along the district-anchored shoreline (ChunkContext.coast_anchor_m, as CliffCoast uses) + Deep toward the interior; TidalFlat = all Shallow (intertidal). +- SUBSTRATE: reuse existing TerrainMaterial (no new variant). Rock seabed/shore where slope_q is steep (Rock IS in the enum — rocky available both above and below water, per user), Sand where gentle; TidalFlat = Wetland (mud). Vegetation = Barren in all water voxels (fixes ''Forest on the sea''). +- SURFACE: water surface elevation_m = 0 (the sea-level convention the existing coastal families already use — raw heightmap sea_level does NOT reach the voxel tier; only ocean_fraction_q/elev_q do). Depth is conveyed by the Water axis (Deep/Shallow), not by elevation_m. +- COVER: reuse derive_cover so cold-body water gets Ice (frozen seas on Edict) — important for the frozen-world believability case. +- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105. +- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families. +- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', NULL, '2026-06-28 13:10:56', '2026-06-28 13:10:56.408', '2026-06-28 13:10:56.408', NULL, 'f9737eab39f048ff84b8478ca11f5215', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index e2df94cd8..bfb0aa5eb 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3635,3 +3635,18 @@ BASELINE @ believability-v1: Arbour 3/7, Edict 3/7. FAIL: moisture gradient (dis DEFERRED (ticket item 3 — explicitly optional): per-layer PNG maps (the T-960 Atlas-viewer stopgap). The contrast+coherence metrics are the core enforcer; maps are a follow-up if the human-gate review wants them before T-960. This is the D-245 enforcer; advisory now, ratchets to strict (BELIEVABILITY_STRICT) once Q-123 calibrates thresholds and T-1080/1081/1082 flip the criteria.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:55:19.712', '2026-06-28 12:56:03.498', NULL, 'e39a34b2d5f662cef1b8fee88a70c8f9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:00:26.933', NULL, '12c4956809610e71cdf625d28fc48bdb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:01:05.826', NULL, '330a98a5774e1be834fd8fef02e2f1ff', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it. + +--- +Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to discuss'': + +- FAMILY: dedicated WaterBody family (9th D-239 family) for OpenOcean/Lake/TidalFlat, branching internally on morphology_zone (like IncisedGorge covers MountainPass/ValleyFloor/Alpine). Amend D-239 §5/§6 (8→9 families) + zone_to_family. +- FIDELITY: RICHER — a Shallow fringe along the district-anchored shoreline (ChunkContext.coast_anchor_m, as CliffCoast uses) + Deep toward the interior; TidalFlat = all Shallow (intertidal). +- SUBSTRATE: reuse existing TerrainMaterial (no new variant). Rock seabed/shore where slope_q is steep (Rock IS in the enum — rocky available both above and below water, per user), Sand where gentle; TidalFlat = Wetland (mud). Vegetation = Barren in all water voxels (fixes ''Forest on the sea''). +- SURFACE: water surface elevation_m = 0 (the sea-level convention the existing coastal families already use — raw heightmap sea_level does NOT reach the voxel tier; only ocean_fraction_q/elev_q do). Depth is conveyed by the Water axis (Deep/Shallow), not by elevation_m. +- COVER: reuse derive_cover so cold-body water gets Ice (frozen seas on Edict) — important for the frozen-world believability case. +- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105. +- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families. +- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:10:56.408', NULL, '8f55fc31d54598d0756308b8e6ef645c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index b54cde02a..c8a025f60 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1776,6 +1776,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) was resolved against in favour of f64-to-voxel. - **Implementation note (T-1024, 2026-06-07):** §1 per-body `RIVER_THRESHOLD` is now a derived field (`derive_river_threshold`) on `RegionProfile`. §2 district temperature is a nullable `f32` on `RegionProfile` sampled from a per-region climate derivation (`derive_temperature_c`); moisture is a separate integer primitive (`derive_moisture_q`, 0–100). Both populate the `RegionProfile` carrier during the L4 cascade run. Climate inputs use a **hybrid strategy**: stellar luminosity and spectral class are sourced from the import pipeline (`bodies.axial_tilt_deg`, `star_systems.spectral_class`); greenhouse offsets and diurnal amplitudes are tunable at runtime via `server/data/climate_constants.toml` (source-canonical TOML, not hardcoded). - **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025). +- **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch. - **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract) --- diff --git a/server/src/atlas/believability.rs b/server/src/atlas/believability.rs index f92acc23a..8bf6aadc5 100644 --- a/server/src/atlas/believability.rs +++ b/server/src/atlas/believability.rs @@ -33,8 +33,9 @@ //! //! [`analyze`] is a pure, deterministic function of `(world_seed, body_id, districts)`: //! scalar/categorical contrast is computed over **all** districts; the voxel-derived -//! metrics sample the first [`VOXEL_SAMPLE_DISTRICTS`] in `BTreeMap` order (sorted → -//! stable). Suitable for golden-snapshot regression. +//! metrics sample a **seeded spread** of [`VOXEL_SAMPLE_DISTRICTS`] across the body +//! (deterministic from the world seed — D-245's "randomly-sampled locations", and +//! unbiased unlike a contiguous corner). Suitable for golden-snapshot regression. use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; @@ -60,8 +61,9 @@ const DEFAULT_SEA_LEVEL: f32 = 0.3; /// `ocean_fraction_q` at or above this marks a district water-present (D-239 §10 uses /// `ocean_fraction_q` as the chunk-scale water proxy). const WATER_PRESENCE_Q: i32 = 10; -/// Districts voxel-sampled for the derived metrics — the first N in `BTreeMap` order -/// (deterministic). Scalar/categorical contrast uses *all* districts (cheap). +/// Districts voxel-sampled for the derived metrics — a deterministic seeded spread +/// across the body (D-245 randomly-sampled-anywhere). Scalar/categorical contrast +/// uses *all* districts (cheap). const VOXEL_SAMPLE_DISTRICTS: usize = 64; /// Stride over a district's representative 64 m chunk (8 → an 8×8 = 64-voxel sample). const VOXEL_SAMPLE_STRIDE: usize = 8; @@ -171,14 +173,21 @@ pub fn analyze( terrain_materials: 0, // filled from the voxel sample below }; - // ── Voxel-derived metrics over a deterministic subset of districts ──────── + // ── Voxel-derived metrics over a deterministic seeded spread of districts ── + // Sampling must span the whole body, not a contiguous prefix: BTreeMap order is + // a spatial corner (e.g. an all-ocean edge), which would report a forested world + // as "0 vegetated". A seeded spread is both unbiased and matches D-245's + // "randomly-sampled locations across the body" wording. let mut terrain_set: BTreeSet = BTreeSet::new(); let mut coh = CoherenceMetrics::default(); let mut sampled = 0usize; - for (dp, prof) in districts.iter().take(VOXEL_SAMPLE_DISTRICTS) { + let keys: Vec = districts.keys().copied().collect(); + for idx in sample_indices(world_seed, keys.len(), VOXEL_SAMPLE_DISTRICTS) { + let dp = keys[idx]; + let prof = &districts[&dp]; sampled += 1; - let chunk = district_centre_chunk(*dp); + let chunk = district_centre_chunk(dp); let ctx = derive_chunk_context(world_seed, body_id, prof, chunk, None); let mut any_wet = false; @@ -339,6 +348,29 @@ fn district_centre_chunk(dp: DistrictPos) -> ChunkPos { ) } +/// Deterministic seeded spread of up to `take` distinct indices in `0..n` — a +/// reproducible "random" sample across the whole district list (D-245's +/// randomly-sampled-anywhere requirement), unbiased unlike a contiguous prefix. +/// Returns all of `0..n` when `n <= take`. splitmix64 mixing keyed on the world seed. +fn sample_indices(world_seed: u64, n: usize, take: usize) -> Vec { + if n <= take { + return (0..n).collect(); + } + let mut picked: BTreeSet = BTreeSet::new(); + let mut i: u64 = 0; + while picked.len() < take { + let mut z = world_seed + .wrapping_add(i) + .wrapping_add(0x9E37_79B9_7F4A_7C15); + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^= z >> 31; + picked.insert((z % n as u64) as usize); + i += 1; + } + picked.into_iter().collect() +} + // --------------------------------------------------------------------------- // Loader — resolve committed data + run the real cascade (shared by the probe // binary and the regression harness, so both measure the same thing) diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index aac07c9a9..fbaa37ae7 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -40,12 +40,13 @@ //! //! ## Family dispatch (D-239 §5, T-1028/T-1029) //! -//! The 8-family tree is dispatched through `MorphologyFamily`. All 8 family -//! generators are implemented: `AlluvialPlain` (the D-239 §5 fallback) landed -//! in T-1028; the other 7 (LavaField, FjordWall, CliffCoast, BraidedDelta, -//! DuneStrand, IncisedGorge, MeanderReach) landed in T-1029. Flat/water zones -//! without a dedicated family (OpenOcean, Lake, TidalFlat, Wetland) map to -//! `AlluvialPlain` per D-239 §5. +//! The family tree is dispatched through `MorphologyFamily`. All generators are +//! implemented: `AlluvialPlain` (the D-239 §5 fallback) landed in T-1028; the seven +//! landform families (LavaField, FjordWall, CliffCoast, BraidedDelta, DuneStrand, +//! IncisedGorge, MeanderReach) in T-1029; and `WaterBody` (OpenOcean / Lake / TidalFlat +//! — Deep/Shallow water + seabed) in T-1082, the 9th family that amends D-239 §5/§6's +//! original "8 families". Remaining flat *land* zones (AlluvialPlain, Wetland — a marsh, +//! not open water) still map to `AlluvialPlain` per D-239 §5. //! //! ## D-010 compliance //! @@ -269,6 +270,10 @@ enum MorphologyFamily { MeanderReach, /// Alluvial plain — the D-239 §5 fallback family (T-1028). AlluvialPlain, + /// Water body — ocean / lake / tidal flat (T-1082). Renders Deep/Shallow water + + /// seabed substrate where the zones used to fall through to dry AlluvialPlain land. + /// The 9th family (amends D-239 §5/§6's "8 families"). + WaterBody, } /// Map a `MorphologyZone` to the 8-family `MorphologyFamily` dispatch key. @@ -293,13 +298,14 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { MorphologyFamily::IncisedGorge } MorphologyZone::MeanderReach | MorphologyZone::RiverBank => MorphologyFamily::MeanderReach, - // AlluvialPlain fallback covers the flat/water zones without a dedicated - // family: AlluvialPlain, OpenOcean, Lake, TidalFlat, Wetland (D-239 §5). - MorphologyZone::AlluvialPlain - | MorphologyZone::OpenOcean - | MorphologyZone::Lake - | MorphologyZone::TidalFlat - | MorphologyZone::Wetland => MorphologyFamily::AlluvialPlain, + // Water bodies get the dedicated WaterBody family (T-1082): they render + // Deep/Shallow water + seabed, not the dry AlluvialPlain land they used to. + MorphologyZone::OpenOcean | MorphologyZone::Lake | MorphologyZone::TidalFlat => { + MorphologyFamily::WaterBody + } + // AlluvialPlain fallback covers the remaining flat *land* zones: AlluvialPlain + // and Wetland (a marsh — saturated land, not open water; D-239 §8 Wetland law). + MorphologyZone::AlluvialPlain | MorphologyZone::Wetland => MorphologyFamily::AlluvialPlain, } } @@ -416,6 +422,9 @@ pub fn derive_voxel_column( MorphologyFamily::MeanderReach => { generate_meander_reach(district_eff, chunk, voxel_pos, sub_chunk_seed) } + MorphologyFamily::WaterBody => { + generate_water_body(district_eff, chunk, voxel_pos, sub_chunk_seed) + } }; // ── 4. Seasonal cover overlay (D-239 §3, T-1030) ────────────────────── @@ -528,6 +537,78 @@ fn generate_alluvial_plain( } } +// --------------------------------------------------------------------------- +// WaterBody generator (T-1082, D-239 §5 amended — the 9th family) +// --------------------------------------------------------------------------- + +/// WaterBody voxel generator — ocean / lake / tidal flat (T-1082). +/// +/// Renders the predominantly-submerged zones the AlluvialPlain fallback used to paint +/// as dry forested land (the D-245 / T-1082 bug: `water=Dry` + `Forest` on the sea): +/// - `Water::Deep` open water with a `Water::Shallow` shoal band straddling the +/// district-anchored coast line (`chunk.coast_anchor_m`, T-1041) — one continuous +/// shallows per district, wider for shallower (higher-`elev_q`) bodies. TidalFlat +/// is all `Shallow` (an intertidal flat). +/// - Seabed `TerrainMaterial`: `Wetland` (tidal mud), else `Rock` on steep districts +/// / `Sand` on gentle ones (D-239 §8 — rock faces where steep, sandy floor where not). +/// - `Vegetation::Barren` — nothing grows in open water. +/// - Water surface at `elevation_m = 0` (the sea-level convention the coastal families +/// already use, D-239 §8 "mouths at sea level"; the raw heightmap `sea_level` float +/// does not reach this tier). Depth is carried by the `Water` axis, not `elevation_m`. +/// +/// Seasonal `cover` (Ice on cold bodies — frozen seas/lakes) is applied centrally by +/// `derive_cover` in [`derive_voxel_column`]. Integer arithmetic only (D-010); the only +/// f64 is the upstream domain warp. +fn generate_water_body( + district: &DistrictProfile, + chunk: &ChunkContext, + voxel_pos: VoxelPos, + sub_chunk_seed: u64, +) -> VoxelColumn { + let is_tidal = matches!(district.morphology_zone, MorphologyZone::TidalFlat); + + // Seabed substrate: tidal mud, else rocky on steep districts / sandy on gentle. + let terrain = if is_tidal { + TerrainMaterial::Wetland + } else if district.slope_q >= 18 { + TerrainMaterial::Rock + } else { + TerrainMaterial::Sand + }; + + // Depth: TidalFlat is all Shallow; ocean/lake get a Shallow shoal band on the + // district-anchored coast line (along-axis ∥ basin, T-1041), Deep beyond. + let water = if is_tidal { + Water::Shallow + } else { + let along = match chunk.basin_direction { + crate::atlas::chunk_context::BasinDirection::North + | crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1, + crate::atlas::chunk_context::BasinDirection::East + | crate::atlas::chunk_context::BasinDirection::West => voxel_pos.0, + }; + let coast_d = (along - chunk.coast_anchor_m).abs(); + // Shoal half-width: wider for shallower (higher elev_q) bodies, with a little + // sub-chunk jaggedness so the shallows edge is not a straight line. + let band_noise = (sub_chunk_seed & 0x7) as i32 - 3; // [−3, +4] + let shallow_band = (8 + district.elev_q / 4 + band_noise).max(2); + if coast_d <= shallow_band { + Water::Shallow + } else { + Water::Deep + } + }; + + VoxelColumn { + terrain, + floor: FloorMaterial::None, + vegetation: Vegetation::Barren, + water, + elevation_m: 0, // water surface = sea level + cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column + } +} + // --------------------------------------------------------------------------- // LavaField generator (T-1029, D-239 §5 / §8 Lava law) // --------------------------------------------------------------------------- @@ -2062,6 +2143,98 @@ mod tests { derive_chunk_context(42, "GJ1c", district, (10, 20), None) } + fn water_district(zone: MorphologyZone, slope_q: i32) -> DistrictProfile { + DistrictProfile { + morphology_zone: zone, + tectonic_class: TectonicClass::Stable, + glaciation_grade: GlaciationGrade::None, + precipitation_class: PrecipitationClass::Temperate, + slope_q, + elev_q: 5, + ocean_fraction_q: 90, + river_threshold: 200, + temperature_c: Some(10.0), + moisture_q: 80, + vegetation_class: VegetationClass::Forest, + basin_direction: BasinDirection::South, + } + } + + // ── WaterBody family (T-1082) ──────────────────────────────────────────── + + #[test] + fn water_body_open_ocean_is_wet_and_barren() { + // T-1082: OpenOcean used to render as dry forested AlluvialPlain land. It must + // now render wet (Deep/Shallow), Barren, on a sandy/rocky seabed at sea level. + let district = water_district(MorphologyZone::OpenOcean, 3); + let chunk = derive_chunk_context(42, "ocean", &district, (5, 5), None); + let mut saw_deep = false; + for sx in (0..64).step_by(4) { + for sy in (0..64).step_by(4) { + let col = + derive_voxel_column(42, "ocean", &district, &chunk, 5 * 64 + sx, 5 * 64 + sy); + assert_ne!(col.water, Water::Dry, "open ocean must not render dry"); + assert_eq!( + col.vegetation, + Vegetation::Barren, + "no vegetation in open water" + ); + assert!(matches!( + col.terrain, + TerrainMaterial::Sand | TerrainMaterial::Rock + )); + assert_eq!(col.elevation_m, 0, "water surface at sea level"); + if col.water == Water::Deep { + saw_deep = true; + } + } + } + assert!( + saw_deep, + "open ocean should have deep water away from the shoal" + ); + } + + #[test] + fn water_body_tidal_flat_is_shallow_wetland() { + let district = water_district(MorphologyZone::TidalFlat, 2); + let chunk = derive_chunk_context(7, "tf", &district, (3, 3), None); + for sx in (0..64).step_by(8) { + for sy in (0..64).step_by(8) { + let col = derive_voxel_column(7, "tf", &district, &chunk, 3 * 64 + sx, 3 * 64 + sy); + assert_eq!( + col.water, + Water::Shallow, + "tidal flat is intertidal (Shallow)" + ); + assert_eq!(col.terrain, TerrainMaterial::Wetland, "tidal flat = mud"); + assert_eq!(col.vegetation, Vegetation::Barren); + } + } + } + + #[test] + fn water_body_steep_district_has_rock_seabed() { + let district = water_district(MorphologyZone::OpenOcean, 40); + let chunk = derive_chunk_context(1, "rock", &district, (2, 2), None); + let col = derive_voxel_column(1, "rock", &district, &chunk, 2 * 64 + 10, 2 * 64 + 10); + assert_eq!( + col.terrain, + TerrainMaterial::Rock, + "steep water district → rocky seabed" + ); + assert_ne!(col.water, Water::Dry); + } + + #[test] + fn water_body_is_deterministic() { + let district = water_district(MorphologyZone::Lake, 10); + let chunk = derive_chunk_context(99, "lake", &district, (4, 4), None); + let a = derive_voxel_column(99, "lake", &district, &chunk, 4 * 64 + 20, 4 * 64 + 30); + let b = derive_voxel_column(99, "lake", &district, &chunk, 4 * 64 + 20, 4 * 64 + 30); + assert_eq!(a, b); + } + // ----------------------------------------------------------------------- // TerrainMaterial discriminant pins // ----------------------------------------------------------------------- diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index ae1468691..08bb99dda 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -27,15 +27,15 @@ }, "morphology_zones": 9, "vegetation_classes": 3, - "terrain_materials": 1 + "terrain_materials": 4 }, "coherence": { - "water_districts": 64, - "water_districts_wet": 4, - "drainage_samples": 16, - "drainage_monotonic": 4, + "water_districts": 46, + "water_districts_wet": 43, + "drainage_samples": 10, + "drainage_monotonic": 8, "vegetation_samples": 64, - "vegetated_districts": 64 + "vegetated_districts": 11 } }, { @@ -66,13 +66,13 @@ }, "morphology_zones": 9, "vegetation_classes": 1, - "terrain_materials": 2 + "terrain_materials": 4 }, "coherence": { "water_districts": 20, - "water_districts_wet": 1, - "drainage_samples": 4, - "drainage_monotonic": 1, + "water_districts_wet": 17, + "drainage_samples": 6, + "drainage_monotonic": 6, "vegetation_samples": 64, "vegetated_districts": 0 }