//! VoxelColumn — 1 m derivation tier of the D-239 refinement chain (T-1028). //! //! A `VoxelColumn` is the finest derivation tier: derived on-demand from a //! `ChunkContext` (64 m) and the covering `DistrictProfile` (~1 km). Never stored, //! never persisted — cached in `VoxelCache` (D-227). //! //! ## D-228 composite tile axes //! //! Each `VoxelColumn` is a bundle of **orthogonal axes** (D-228): //! - `terrain`: `TerrainMaterial` — permanent natural ground (Soil/Sand/Gravel/Rock/Wetland/Lava) //! - `floor`: `FloorMaterial` — built surface (None for now; Phase 5) //! - `vegetation`: `Vegetation` — ground cover (from `VegetationClass`) //! - `water`: `Water` — local depth state (Dry/Shallow/Deep) //! - `elevation_m`: scalar metres //! - `cover`: `SeasonalCover` — seasonal cover overlay (Snow/Ice, D-228/D-239 §3) //! //! **Snow/Ice are NOT `TerrainMaterial`** — they are a seasonal cover overlay //! per D-228. `derive_cover` (T-1030) computes the static mean-state cover from //! the district's integer temperature + water/terrain type + spatially-coherent //! cluster scatter (D-239 §3). The transient/clock-bound part (cover forms in the //! cold phase, melts in the warm phase) is deferred to Q-105. //! //! ## Domain warp (D-239 §4) //! //! The warp is applied **before** the integer voxel address is computed. The //! caller passes the logical tile position `(tile_x, tile_y)` in metres; the //! derivation function calls `domain_warp(seed, body_id, (tile_x, tile_y))` to //! get `(dx, dy)` in f64, then truncates to the integer voxel address with //! `as i32` (cast, not comparison — IEEE-754 deterministic, D-239 §4). //! //! The warp is **the only f64** in the structural derivation path; all //! material/morphology decisions downstream operate on the integer voxel address. //! //! ## Cache (D-227) //! //! `VoxelCache` is a bounded LRU-style cache backed by a `BTreeMap` and a //! monotonic generation counter. Same (seed, body, pos) → same column; //! eviction is by generation-counter LRU (oldest entry evicted when capacity //! is reached). Never written to disk. //! //! ## Family dispatch (D-239 §5, T-1028/T-1029) //! //! 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 //! //! All material/morphology decisions are integer. The only f64 path is the //! domain warp displacement (positional math, truncated before any decision). use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::atlas::chunk_context::ChunkContext; use crate::atlas::district_profile::{DistrictProfile, VegetationClass}; use crate::atlas::domain_warp::domain_warp; use crate::seed::{SeedChain, SeedDomain}; use crate::simulation::generator::MorphologyZone; // --------------------------------------------------------------------------- // D-228 tile axes // --------------------------------------------------------------------------- /// Permanent natural ground material (D-228). /// /// **Snow and Ice are NOT here** — they are a seasonal cover overlay (Q-105). /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] pub enum TerrainMaterial { /// Loamy soil — rolling/floodplain (D-239 §8 Soil law). #[default] Soil = 0, /// Sand — ≤~32° angle of repose, dunes not cliffs (D-239 §8 Sand law). Sand = 1, /// Gravel — braided channels/fans, not single-thread meander (D-239 §8). Gravel = 2, /// Rock — vertical faces on steep steps (D-239 §8 Rock law). Rock = 3, /// Wetland substrate — saturated organic soil, ≤5° flats (D-239 §8). Wetland = 4, /// Lava — sheets/shield slopes + tubes, immature drainage (D-239 §8). Lava = 5, } /// Built surface over natural ground (D-228). /// /// `None` is the only implemented value for Phase 4 — built surfaces are /// Phase 5 (player control). The vocabulary is declared here so the type system /// enforces the distinction from `TerrainMaterial`. /// /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] pub enum FloorMaterial { /// No built surface — natural ground exposed. #[default] None = 0, // Phase 5 variants: Concrete = 1, Pavement = 2, Carpet = 3, Metal = 4, … } /// Ground cover (D-228). Carries cover/concealment, movement sound profile, /// and economic yield (timber/crops). /// /// Derived from the district's `VegetationClass` (D-239 §8 climate→vegetation law). /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] pub enum Vegetation { /// No vegetation — above treeline or hyper-arid. #[default] Barren = 0, /// Grass — open ground cover, low concealment. Grass = 1, /// Scrub — transitional shrubs, moderate concealment. Scrub = 2, /// Thicket — dense low cover, high concealment. Thicket = 3, /// Forest — closed canopy, full concealment. Forest = 4, /// Crop — managed agriculture (Phase 6 economic layer). Crop = 5, /// Cleared — formerly vegetated, stripped (player/sim action). Cleared = 6, } impl Vegetation { /// Map a district-level `VegetationClass` to the per-voxel `Vegetation` axis. /// /// The district class establishes the dominant cover; per-voxel scatter /// (Grass vs Scrub at the margin) is handled by the voxel generator using /// the sub-chunk seed. This function gives the canonical deterministic /// baseline before scatter is applied. pub fn from_vegetation_class(vc: VegetationClass) -> Self { match vc { VegetationClass::Absent => Vegetation::Barren, VegetationClass::Barren => Vegetation::Barren, VegetationClass::Scrub => Vegetation::Scrub, VegetationClass::Forest => Vegetation::Forest, VegetationClass::RiparianScrub => Vegetation::Thicket, VegetationClass::RiparianThicket => Vegetation::Thicket, } } } /// Local depth state (D-228). /// /// `Water` is dynamic — changes with season/tide (Q-105) — but for Phase 4 /// derivation we use the static base state (the seasonal overlay is deferred). /// /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] pub enum Water { /// No standing water. #[default] Dry = 0, /// Shallow water — passable with effort; grants concealment penalty. Shallow = 1, /// Deep water — impassable without vessel; full concealment penalty. Deep = 2, } /// Seasonal cover overlay (D-228 / D-239 §3, T-1030). /// /// Snow and Ice are **not** `TerrainMaterial` — they are a derived overlay that /// sits on top of the permanent ground material. This axis captures the /// **static mean-state** cover: whether the district's mean-annual temperature /// puts this voxel firmly in the frozen zone. The **transient, clock-bound** part /// (cover forms in the cold diurnal/seasonal phase, melts in the warm phase → /// time-of-day passability shifts) is out of scope here. /// /// ## Q-105 forward contract /// /// D-239 §3 states: "Transient: because temperature is clock-bound (season + /// day/night), in the marginal band ice/snow forms in the cold phase and melts in /// the warm phase — dawn frost burns off, a stream iced at dawn is crossable by /// noon. Passability is therefore time-of-day dynamic." This dynamic behaviour /// requires the cheap district seasonal/clock state described in Q-105 — the /// `cover` field here captures only the mean-state freeze. When Q-105 is /// implemented, consumers must additionally consult the regional clock-phase /// before treating `cover` as a passability gate. The static `SeasonalCover` /// output of `derive_cover` remains valid as the base state; Q-105 applies a /// time-dependent modifier on top of it. /// /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] pub enum SeasonalCover { /// No seasonal cover — ground is bare. #[default] None = 0, /// Snow — cold + wet land surface; reduces mobility, provides concealment. Snow = 1, /// Ice — frozen water surface or permanent ice; alters passability. Ice = 2, } // --------------------------------------------------------------------------- // VoxelColumn // --------------------------------------------------------------------------- /// The finest derivation tier — the D-228 composite tile axes for one 1 m column. /// /// Derived on demand from `(seed, body_id, district, chunk_context, voxel_pos)`. /// Never stored, never persisted. Cached in `VoxelCache` (D-227). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VoxelColumn { /// Permanent natural ground material (D-228). pub terrain: TerrainMaterial, /// Built surface over the ground (D-228). `None` in Phase 4. pub floor: FloorMaterial, /// Ground cover (D-228). Derived from district `VegetationClass`. pub vegetation: Vegetation, /// Local depth state (D-228). pub water: Water, /// Surface elevation in integer metres (D-010; no f64 in the stored value). pub elevation_m: i32, /// Seasonal cover overlay (D-228 / D-239 §3, T-1030). /// /// Static mean-state: whether mean-annual temperature puts this voxel in /// the frozen zone, gated by surface type + spatially-coherent cluster /// scatter. The transient clock-bound layer (Q-105) is not represented here. pub cover: SeasonalCover, } // --------------------------------------------------------------------------- // Voxel position (integer metres, D-010) // --------------------------------------------------------------------------- /// Absolute voxel position — integer metres. The unit coordinate of D-222. /// /// After domain warp is applied (`tile_x + dx as i32`), the result is the /// integer voxel address used for all material/morphology decisions (D-239 §4). pub type VoxelPos = (i32, i32); // --------------------------------------------------------------------------- // Family dispatch (D-239 §5) // --------------------------------------------------------------------------- /// Internal family identifier for the 8-family morphology dispatch (D-239 §5). /// /// Derived from `MorphologyZone` by `zone_to_family`. The dispatch gates are /// pre-computed at zone classification time (DistrictProfile); the family is the /// structural decision that drives voxel geometry. /// /// All 8 families have dedicated generators: `AlluvialPlain` (T-1028, also the /// D-239 §5 fallback for flat/water zones) and the other 7 (T-1029). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MorphologyFamily { /// Lava field / shield slope. Requires TectonicClass::Volcanic (D-239 §5). LavaField, /// Fjord wall. Requires GlaciationGrade ≥ 2 (D-239 §5). FjordWall, /// Cliff coast. High slope + coastal (D-239 §5). CliffCoast, /// Braided delta. Very flat + low elevation + coastal (D-239 §5). BraidedDelta, /// Dune strand. Low slope + coastal + arid (D-239 §5). DuneStrand, /// Incised gorge / mountain pass. High slope + inland + high elev (D-239 §5). IncisedGorge, /// Meander reach. Gentle slope + water presence (D-239 §5). 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. /// /// Sub-classified zones (TidalFlat, Estuarine, Alpine, Wetland) map to their /// parent family for voxel geometry — their distinction is captured in the /// district zone label, not in the voxel generator dispatch. /// /// BraidedPlain maps to BraidedDelta (its parent at district scale, per the /// D-239 §6 implementation note — BraidedPlain is deferred from DistrictProfile /// to ChunkContext sub-classification). fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { match zone { MorphologyZone::Volcanic => MorphologyFamily::LavaField, MorphologyZone::Fjord => MorphologyFamily::FjordWall, MorphologyZone::CliffCoast => MorphologyFamily::CliffCoast, MorphologyZone::Delta | MorphologyZone::BraidedPlain | MorphologyZone::Estuarine => { MorphologyFamily::BraidedDelta } MorphologyZone::DuneStrand => MorphologyFamily::DuneStrand, MorphologyZone::MountainPass | MorphologyZone::ValleyFloor | MorphologyZone::Alpine => { MorphologyFamily::IncisedGorge } MorphologyZone::MeanderReach | MorphologyZone::RiverBank => MorphologyFamily::MeanderReach, // 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, } } // --------------------------------------------------------------------------- // Voxel derivation entry point // --------------------------------------------------------------------------- /// Derive a `VoxelColumn` for the tile at integer metre position `(tile_x, tile_y)`. /// /// ## Warp wiring (D-239 §4) /// /// 1. Call `domain_warp(seed, body_id, (tile_x, tile_y))` → `(dx, dy)` in f64. /// 2. Apply `voxel_x = (tile_x as f64 + dx) as i32` (and likewise for `voxel_y`). /// 3. The integer `(voxel_x, voxel_y)` is used for all downstream decisions. /// This is the **only** f64 in the structural path. /// /// ## Parameters /// /// - `world_seed` — master world seed. /// - `body_id` — stable string body identifier (used for domain separation). /// - `district` — the covering `DistrictProfile` (~1 km). /// - `chunk` — the covering `ChunkContext` (64 m). /// - `tile_x`, `tile_y` — logical tile position in **integer metres** (D-010). /// /// ## Returns /// /// A fully derived `VoxelColumn` with all D-228 axes populated. /// T-1081: the metre span of the voxel-tier mid-scale relief field — the amplitude /// ceiling for the ≈0.13–1 km rolling/ridged hills added to the family base /// elevation. Actual relief at a voxel is `voxel_relief(...) * SPAN`, which the /// envelope (district ruggedness) scales down on gentle terrain, so this is the /// steep-terrain ceiling, not a uniform amplitude. Provisional — tuned against the /// believability probe (T-1079) like the moisture gradient; per-body *absolute* /// elevation span is a later refinement (the T-1080 climate-fields lineage). /// /// Kept modest so relief stays mostly *below* the family base elevation: the base is /// the compressed `elev_q/N` scale (max ~50 m) and `elevation_m` clamps at 0 (sea /// level), so an oversized span clamps away on low ground — drowning the relief and /// biasing it positive. A larger span belongs with the deferred per-body absolute /// elevation model that would give the base real headroom. const VOXEL_RELIEF_SPAN_M: i32 = 100; pub fn derive_voxel_column( world_seed: u64, body_id: &str, district: &DistrictProfile, chunk: &ChunkContext, tile_x: i32, tile_y: i32, ) -> VoxelColumn { // ── 1. Domain warp (D-239 §4) ───────────────────────────────────────── // Apply warp and truncate to integer voxel address. This is the ONLY f64 // in the structural path; all decisions below use integer voxel coords. // The warp is applied BEFORE the blend boundary, so the seam cannot align // with a chunk edge (T-1042: "reusing the same warp offset so the blend // seam cannot align with the chunk edge"). let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y)); let voxel_x = (tile_x as f64 + dx) as i32; let voxel_y = (tile_y as f64 + dy) as i32; let voxel_pos: VoxelPos = (voxel_x, voxel_y); // ── 2. Sub-chunk seed (for features with wavelength < 64 m) ─────────── // Keyed on integer voxel address after warp — deterministic (D-010). Uses the // dedicated SeedDomain::Voxel (not ChunkContext) so the per-voxel stream can // never collide with the district-scale meander seed (D-224 domain separation). let sub_chunk_seed = SeedChain::for_body(world_seed, body_id) .derive(SeedDomain::Voxel, voxel_pos_to_id(voxel_pos)) .seed(); // ── 2b. Cross-district terrain blending (T-1042, D-239 §4/§7) ───────── // When a secondary district is present AND blend_weight < 255, blend the // continuous terrain scalars (`elev_q`, `moisture_q`) between the primary // and secondary district. This is done BEFORE family dispatch so the // blended values propagate through ALL family generators. // // Morphology family is NOT changed — family dispatch always uses the // primary district's `morphology_zone` (D-239 §7: morphology seams stay // sharp). Only `elev_q` and `moisture_q` are blended; all structural // fields (glaciation_grade, tectonic_class, precipitation_class, etc.) // stay primary. // // Integer blend formula: `(a * w + b * (255 - w) + 127) / 255` (D-010). // This is a zero-cost branch when blend_weight == 255 (no secondary active). let effective_district: std::borrow::Cow = if chunk.blend_weight < 255 { if let Some(ref sec) = chunk.secondary { let w = chunk.blend_weight as i32; let w_sec = 255 - w; let blended_elev_q = (district.elev_q * w + sec.elev_q * w_sec + 127) / 255; let blended_moisture_q = (district.moisture_q * w + sec.moisture_q * w_sec + 127) / 255; let mut blended = district.clone(); blended.elev_q = blended_elev_q; blended.moisture_q = blended_moisture_q; std::borrow::Cow::Owned(blended) } else { std::borrow::Cow::Borrowed(district) } } else { std::borrow::Cow::Borrowed(district) }; let district_eff: &DistrictProfile = &effective_district; // ── 3. Family dispatch (D-239 §5) ───────────────────────────────────── // Family selection uses the PRIMARY district's morphology_zone (D-239 §7: // seams stay sharp). Blended `district_eff` is passed to the generators // so their `elev_q`/`moisture_q` reads get the blended values. let family = zone_to_family(&district.morphology_zone); let mut column = match family { MorphologyFamily::AlluvialPlain => { generate_alluvial_plain(district_eff, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::LavaField => generate_lava_field(district_eff, voxel_pos, sub_chunk_seed), MorphologyFamily::FjordWall => { generate_fjord_wall(district_eff, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::CliffCoast => { generate_cliff_coast(district_eff, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::BraidedDelta => { generate_braided_delta(district_eff, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::DuneStrand => { generate_dune_strand(district_eff, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::IncisedGorge => { generate_incised_gorge(district_eff, chunk, voxel_pos, sub_chunk_seed) } 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) } }; // ── 3b. Voxel-tier mid-scale relief (T-1081, D-227 / D-243 §2) ──────── // The family generators set a coarse district-derived base elevation plus only // ±4 m of per-voxel micro-scatter, leaving the walkable surface near-flat — no // hills or landmarks to navigate by (the T-1081 symptom). Add the mid-scale // (≈0.25–2 km) rolling/ridged relief the district→voxel seam does not otherwise // reach (D-243 §2): detail-scatter enveloped by district ruggedness (slope_q) so // an authored plain stays flat and rugged terrain gains real relief. The relief // field is body-global (SeedDomain::VoxelRelief, position-keyed); the f64 // perturbation is truncated to integer metres before assignment (D-010, // truncate-before-decision — the same sanctioned f64 path as the domain warp). // // Skipped for the WaterBody family: open ocean / lake / tidal-flat surfaces stay // at sea level (elevation 0). River channels inside the land families ride up // with their banks — value noise is smooth at 1 m steps, so a channel voxel and // its bank get near-identical relief and the family's relative channel cut holds. // Only the FLAT families take the mid-scale relief. The dramatic families // (CliffCoast / FjordWall / IncisedGorge) already generate strong internal relief // from their own geometry — layering a position-varying field over them would warp // those features (e.g. drown a gorge's wall-to-floor drop under a relief swell). // WaterBody stays at sea level. The flat families are the ones that read flat and // need invented relief; their only feature is a 3–15 m channel, narrow enough that // the smooth relief shifts it bodily with its banks. let takes_mid_scale_relief = matches!( family, MorphologyFamily::AlluvialPlain | MorphologyFamily::LavaField | MorphologyFamily::BraidedDelta | MorphologyFamily::DuneStrand | MorphologyFamily::MeanderReach ); if takes_mid_scale_relief { let relief_seed = SeedChain::for_body(world_seed, body_id) .derive(SeedDomain::VoxelRelief, 0) .seed(); // Relief envelope (T-1081): the coarse heightmap (~40–78 km/px) carries almost // no slope at this tier (slope_q tops out near 0.1 on a real body), so gating on // slope_q alone would kill the invented relief everywhere. Combine slope with // elevation — high ground reads rugged, coastal lowlands stay gentle — so // detail-scatter raises hills where the body has high terrain and keeps flats // soft. (Tectonic-class ridge sharpening is a later refinement.) let signal = (district_eff.slope_q * 3 + district_eff.elev_q).clamp(0, 100) as f64 / 100.0; let relief = crate::atlas::detail_scatter::voxel_relief( relief_seed, voxel_x as f64, voxel_y as f64, signal, signal, ); let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010) column.elevation_m = (column.elevation_m + relief_m).max(0); } // ── 4. Seasonal cover overlay (D-239 §3, T-1030) ────────────────────── // Applied AFTER family dispatch: the 8 family generators produce the base // axes (terrain/water/vegetation/elevation); cover is a separate orthogonal // axis derived from the district's mean temperature + water/terrain + coherent // cluster scatter. One site, set here — no family generator needs changing. // Cover derives from the primary district's temperature (not blended) since // temperature blending is the sibling ticket's domain (T-1078, D-243 §4). column.cover = derive_cover(world_seed, body_id, district, &column, voxel_pos); column } // --------------------------------------------------------------------------- // AlluvialPlain generator (T-1028, D-239 §5 fallback) // --------------------------------------------------------------------------- /// AlluvialPlain voxel generator — the D-239 §5 fallback family (T-1028). /// /// Flat floodplain with: /// - `Soil` terrain material (D-239 §8 Soil law: rolling/floodplain) /// - Vegetation from the district's `vegetation_class` (D-239 §2) /// - Flat elevation derived from district `elev_q` + small local scatter /// - Meander channel cut from `ChunkContext` (D-239 §10: one basin direction /// + global meander params, NOT a per-tile flow_direction grid) /// - Water state: Shallow/Deep in channel, Dry elsewhere /// /// ## Meander channel placement /// /// The channel is a sine-wave approximation running along `basin_direction`, /// centred on the district-anchored `channel_anchor_m` (T-1040). The /// perpendicular distance from the voxel to the wave determines whether the /// voxel is in-channel. This avoids per-tile flow grids while producing a /// spatially coherent channel continuous across chunk boundaries (D-239 §10). /// /// Channel width is `chunk.channel_width_m` (game-feel range 3–15 m). fn generate_alluvial_plain( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Terrain material ─────────────────────────────────────────────────── // AlluvialPlain → Soil (D-239 §8 Soil law). Wetland sub-zone → Wetland // substrate when moisture is very high (§8 Wetland ≤5° flats law). let terrain = if matches!(district.morphology_zone, MorphologyZone::Wetland) || (district.slope_q <= 5 && district.moisture_q >= 60) { TerrainMaterial::Wetland } else { TerrainMaterial::Soil }; // ── Vegetation ───────────────────────────────────────────────────────── // Base vegetation from district class; sub-chunk scatter (a few Grass/Scrub // variations) uses `sub_chunk_seed` — wavelength < 64 m. let base_veg = Vegetation::from_vegetation_class(district.vegetation_class); let vegetation = scatter_vegetation(base_veg, sub_chunk_seed); // ── Base elevation ────────────────────────────────────────────────────── // Convert district `elev_q` (0–100) to metres. We scale 0–100 → 0–50 m here // (walking-skeleton fidelity: a rough body-relative elevation). // Sub-chunk micro-relief: small integer scatter in [-4, +3] m from the seed // (3 low bits, minus 4). The final elevation is clamped to >= 0 below. let base_elev_m = district.elev_q / 2; let micro_relief = (sub_chunk_seed & 0x7) as i32 - 4; // 3 bits → [-4, +3] let elevation_m = (base_elev_m + micro_relief).max(0); // ── Channel (meander, D-239 §10) ─────────────────────────────────────── // Check if this voxel falls within the meander channel. The channel is a // sine-wave path through the chunk, parameterised by meander_phase and // meander_wavelength_m. We compute the perpendicular distance from the voxel // to the channel centreline; if it's within half the channel width, the voxel // is in-channel. // // All comparison is integer (D-010): we compute a scaled integer distance. let (in_channel, is_deep) = if chunk.has_active_channel { compute_channel_state(voxel_pos, chunk, sub_chunk_seed) } else { (false, false) }; let water = if is_deep { Water::Deep } else if in_channel { Water::Shallow } else { Water::Dry }; // ── Channel elevation adjustment ─────────────────────────────────────── // In-channel voxels are slightly lower than the surrounding floodplain // (the channel is cut below the levee/floodplain surface). This satisfies // D-239 §9: "channels fall below and levees above the high-water threshold". // We use a fixed 2 m cut (integer, D-010). let elevation_m = if in_channel { (elevation_m - 2).max(0) } else { elevation_m }; VoxelColumn { terrain, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // 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) // --------------------------------------------------------------------------- /// LavaField voxel generator. /// /// Shield-slope topology with lava sheets and tube voids: /// - `Lava` terrain everywhere (D-239 §8 Lava law). /// - `Barren` vegetation — fresh lava is lifeless. /// - Low-relief base elevation (shield — gentle radiating slopes, not peaks). /// - Micro-relief: coarse lava block texture from seed bits. /// - Tube voids: seed-gated depressions 1–3 m below the surface (immature /// drainage; no active river network — D-239 §8 immature drainage law). /// - No active water channel regardless of `chunk.has_active_channel` — /// immature drainage means no organised river network on fresh lava. fn generate_lava_field( district: &DistrictProfile, _voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Base elevation ───────────────────────────────────────────────────── // Shield slope: gentle radiating dome. Use elev_q for the regional height; // the shield adds a small parabolic falloff from the centre. At voxel scale // we simulate this as a low-amplitude multi-scale noise on a flat base. // Integer arithmetic throughout (D-010). let base_elev_m = district.elev_q / 2; // Coarse lava block texture: bits [0:2] of seed → ±2 m vertical scatter. let lava_block = (sub_chunk_seed & 0x7) as i32 - 3; // [−3, +4] // Lava tube void: 1-in-8 chance (bits [3:5] == 0b000) of a depression. // Tubes are below-surface drainage (D-239 §8 immature drainage). let tube_depression = if (sub_chunk_seed >> 3) & 0x7 == 0 { let depth = ((sub_chunk_seed >> 6) & 0x3) as i32 + 1; // 1–4 m depression -depth } else { 0 }; let elevation_m = (base_elev_m + lava_block + tube_depression).max(0); // ── Tube void water state ────────────────────────────────────────────── // A lava tube depression may collect a shallow puddle (moisture-gated). // Very rare — only in humid zones (moisture_q ≥ 70). No Deep water on // shield slopes (immature drainage has no deep organised channels). let water = if tube_depression < -2 && district.moisture_q >= 70 { Water::Shallow } else { Water::Dry }; VoxelColumn { terrain: TerrainMaterial::Lava, floor: FloorMaterial::None, vegetation: Vegetation::Barren, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // FjordWall generator (T-1029, D-239 §5 / §8 glaciation→form law) // --------------------------------------------------------------------------- /// FjordWall voxel generator. /// /// Glacial U-valley topology: /// - `Rock` terrain (D-239 §8 Rock→vertical faces law). /// - Steep valley walls meeting deep water in the channel (the fjord). /// - U-valley cross-section: flat (deep) bottom + near-vertical walls. /// - GlaciationGrade ≥ 2 produces cirques (D-239 §5/§8 glaciation→form law). /// - Moraines (debris mounds) at the valley sides for Grade ≥ 1. /// - Valley floor is narrow (chokepoint — D-239 §9) with `Deep` water. /// - Walls rise steeply; rocky, very little vegetation. /// /// The fjord trough runs along `basin_direction` at the district-anchored /// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE valley spans /// the district's chunks. Cross-section: wall | moraine | Deep water | moraine | wall. fn generate_fjord_wall( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Cross-valley coordinate ─────────────────────────────────────────── // Fjord runs along basin_direction; cross = perpendicular distance from // the centre-line. let (along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; // Cross-channel distance from the district-anchored trough centreline // (T-1041): continuous world coordinates — folding into the 64 m chunk // frame repeated a complete fjord in every chunk (D-239 §10 violation). let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); // ── U-valley geometry ───────────────────────────────────────────────── // The fjord bottom is 4–8 m wide (chokepoint, D-239 §9 "gorge floors 2–8 m"). // Deep water within floor_half m of centre; moraine band beyond; wall beyond that. // // GlaciationGrade modifies the floor width, wall steepness, and cirque presence. // Upstream classifier gate guarantees GlaciationGrade ≥ 2 for FjordWall (D-239 §5). let glacier_grade = district.glaciation_grade as i32; // guaranteed ≥ 2 // Fjord floor half-width: 2–4 m (total 4–8 m, D-239 §9 compliant). // Stronger glaciation carves a wider U-trough, but stays within §9 spec. let floor_half = glacier_grade.clamp(2, 4); // total 4–8 m // Moraine band extends from floor edge to moraine_outer. let moraine_outer = floor_half + 6 + glacier_grade; // 10–14 m from centre // Base elevation for the wall platform — high ground context. let wall_elev_base = (district.elev_q / 2).max(20); // at least 20 m walls let (elevation_m, water, vegetation) = if cross_from_centre <= floor_half { // ── Fjord floor — deep water ───────────────────────────────────── // Below sea level (drainage monotonicity: mouth at sea level). // Elevation 0 m (sea level). let floor_noise = (sub_chunk_seed & 0x1) as i32; // ±0–1 m let elev = floor_noise; // 0 or 1 m at most (elev, Water::Deep, Vegetation::Barren) } else if cross_from_centre <= moraine_outer { // ── Moraine band — rocky debris mound ──────────────────────────── // Moraines always present — upstream classifier gate guarantees // GlaciationGrade ≥ 2 (D-239 §5). Slight elevation bump; Rock substrate. let moraine_noise = ((sub_chunk_seed >> 2) & 0x3) as i32; // 0–3 m let moraine_dist = cross_from_centre - floor_half; // Bell-shape: rises then falls across the band. let half_band = (moraine_outer - floor_half) / 2; let moraine_height = if moraine_dist <= half_band { (moraine_dist * 3) / half_band.max(1) // 0 → 3 m rise } else { 3 - ((moraine_dist - half_band) * 3) / half_band.max(1) // 3 → 0 m fall }; let elev = (wall_elev_base / 4 + moraine_height + moraine_noise).max(0); // Moraines hold some Shallow pools (meltwater) in humid zones. let water = if district.moisture_q >= 60 && moraine_noise >= 2 { Water::Shallow } else { Water::Dry }; (elev, water, Vegetation::Barren) } else { // ── Valley wall — near-vertical face ───────────────────────────── // Rock→vertical faces (D-239 §8). Elevation rises steeply from // moraine_outer to the wall top. let wall_dist = cross_from_centre - moraine_outer; // Steep linear rise: 6 m per metre of cross-distance (approximates // vertical), saturating at a 120 m plateau 20 m out — cross distance // is district-scale post-T-1041, so the rise must not grow unbounded to // the district edge (clamp before multiplying: i32 overflow otherwise). let wall_rise = wall_dist.min(20) * 6; // Cirques (grade ≥ 2): occasional hollowed pocket (2–4 m depression) at // upper wall. seed-gated, ~1-in-8 frequency. let cirque_depression = if glacier_grade >= 2 && (sub_chunk_seed >> 6) & 0x7 == 0 { // 2–5 m ((sub_chunk_seed >> 9) & 0x3) as i32 + 2 } else { 0 }; // Wall noise: jagged rock face (bits [1:3]). let wall_noise = ((sub_chunk_seed >> 1) & 0x7) as i32 - 4; // [−4, +3] let elev = (wall_elev_base + wall_rise + wall_noise - cirque_depression).max(0); // Very sparse vegetation on high rocky walls. let veg = match district.vegetation_class { VegetationClass::Forest | VegetationClass::Scrub => { if (sub_chunk_seed >> 12) & 0xF < 2 { Vegetation::Scrub } else { Vegetation::Barren } } _ => Vegetation::Barren, }; (elev, Water::Dry, veg) }; // ── Along-axis variation (avalanche debris / longitudinal texture) ──── // Use along coordinate modulo the meander wavelength for slow variation. let _along_used = along; // structural reference; prevents unused-variable warning VoxelColumn { terrain: TerrainMaterial::Rock, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // CliffCoast generator (T-1029, D-239 §5 / §8 Rock law) // --------------------------------------------------------------------------- /// CliffCoast voxel generator. /// /// Vertical rock face at the coastal water edge: /// - `Rock` terrain (D-239 §8 Rock→vertical faces law). /// - Sharp elevation drop to `Shallow`/`Deep` water at the base. /// - The coast face sits on the district-anchored line `chunk.coast_anchor_m` /// along the seaward axis (T-1041, D-239 §10): ONE continuous coast line /// per district (warp-displaced), not a 64 m sawtooth. Inland of the line is /// high ground; seaward is ocean. /// - Narrow ledge/platform at the cliff base (passable ground, Shallow). /// - Virtually no flat ground (D-239 §8 — Rock→vertical, not terraced). fn generate_cliff_coast( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Coast orientation ───────────────────────────────────────────────── // basin_direction points seaward (water flows to ocean). coast_d is the // signed seaward distance from the district-anchored coast line in // continuous world metres (T-1041): negative = inland, positive = seaward. let coast_d = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North => chunk.coast_anchor_m - voxel_pos.1, crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1 - chunk.coast_anchor_m, crate::atlas::chunk_context::BasinDirection::East => voxel_pos.0 - chunk.coast_anchor_m, crate::atlas::chunk_context::BasinDirection::West => chunk.coast_anchor_m - voxel_pos.0, }; // ── Cliff face ──────────────────────────────────────────────────────── // Inland (coast_d < 0): high ground, rocky top. // Face (0..8): steep drop (>6 m per metre — near-vertical, D-239 §8). // Ledge (8..13): rocky beach platform, Shallow. // Ocean (≥13): Deep water. // // Cliff height derived from district elevation. let cliff_top_elev = (district.elev_q / 2).max(15); // at least 15 m cliff // Rock-face jaggedness noise: bits [0:2] → ±3 m. let face_noise = (sub_chunk_seed & 0x7) as i32 - 4; // Ledge variation noise: bits [3:5] → 0–2 m. let ledge_noise = ((sub_chunk_seed >> 3) & 0x3) as i32; let (elevation_m, water) = if coast_d < 0 { // Inland high ground — rocky plateau. let inland_noise = ((sub_chunk_seed >> 5) & 0xF) as i32 - 7; // [−7, +8] let elev = (cliff_top_elev + inland_noise).max(cliff_top_elev / 4); (elev, Water::Dry) } else if coast_d < 8 { // Cliff face — steep drop. Each metre seaward drops ~cliff_top/8 metres. let face_dist = coast_d; // 0–7 let drop = (face_dist * cliff_top_elev) / 8; let elev = (cliff_top_elev - drop + face_noise).max(1); (elev, Water::Dry) } else if coast_d < 13 { // Rocky ledge / splash zone. let elev = (1 + ledge_noise).max(0); (elev, Water::Shallow) } else { // Ocean — drainage monotonicity: mouths at sea level (elev 0). (0, Water::Deep) }; // Vegetation: nearly barren on rock faces; tiny scatter of Scrub on the // inland top (≥18 m / ≥28 m inland of the face — same offsets as the old // chunk-frame layout, now measured from the district-anchored coast line). let vegetation = match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket => { if coast_d < -18 && (sub_chunk_seed >> 10) & 0x7 < 3 { Vegetation::Scrub } else { Vegetation::Barren } } VegetationClass::Scrub | VegetationClass::RiparianScrub => { if coast_d < -28 && (sub_chunk_seed >> 10) & 0x3 == 0 { Vegetation::Scrub } else { Vegetation::Barren } } _ => Vegetation::Barren, }; VoxelColumn { terrain: TerrainMaterial::Rock, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // BraidedDelta generator (T-1029, D-239 §5 / §8 Gravel law) // --------------------------------------------------------------------------- /// BraidedDelta voxel generator. /// /// Braided channel / fan topology: /// - `Gravel` terrain (D-239 §8 Gravel→braided channels/fans law). /// - Multiple shallow distributary channels (NOT single-thread meander). /// - Very low elevation — near sea level (D-239 §8 drainage monotonicity: /// mouths at sea level). /// - Channel determination: 3 independent braided threads anastomosing /// across a fan belt centred on the district-anchored axis /// `chunk.channel_anchor_m` (T-1041, D-239 §10), each thread 3–8 m wide. /// Thread offsets are seeded at district scale (meander_phase encodes them), /// so the threads run continuously across the district's chunks instead of /// restarting every 64 m. /// - `Shallow` in-channel; `Dry` on gravel bars between threads. fn generate_braided_delta( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Base elevation ──────────────────────────────────────────────────── // Delta is near sea level. elev_q typically very low in delta zones; cap // at 5 m to honour drainage monotonicity (mouths at sea level). let base_elev_m = (district.elev_q / 4).min(5); // 0–5 m; near sea level // Gravel bar micro-relief: ±1 m scatter on bars between channels. let bar_noise = (sub_chunk_seed & 0x3) as i32; // 0–3 // ── Braided thread geometry ─────────────────────────────────────────── // Cross-channel coordinate (perpendicular to basin_direction). let (_along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; // Three braided threads with well-separated centres derived from meander_phase, // spread across a 64 m fan belt centred on the district-anchored axis // (T-1041 — world cross coordinates, constant for all chunks of the district). // Each centre uses independent bit-mixing (splitmix64-style) so threads are // provably separated regardless of phase value — no correlated bit-slices. let phase_u64 = chunk.meander_phase as u64; // Mix A: phase itself through splitmix64 finaliser. let mix_a = { let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15); h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb); h ^ (h >> 31) }; // Mix B: phase XOR'd with a distinct salt before mixing. let mix_b = { let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15) ^ 0xdeadbeefcafe1234; h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb); h ^ (h >> 31) }; // Mix C: phase XOR'd with a second distinct salt. let mix_c = { let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15) ^ 0xfeedface0badcafe; h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb); h ^ (h >> 31) }; let thread_centres: [i32; 3] = [ chunk.channel_anchor_m - 32 + (mix_a % 64) as i32, chunk.channel_anchor_m - 32 + (mix_b % 64) as i32, chunk.channel_anchor_m - 32 + (mix_c % 64) as i32, ]; // Thread width: 3–6 m half-width derived from channel_width_m. let thread_half = (chunk.channel_width_m / 2).clamp(2, 4); // Check if voxel is in any braided thread — continuous world-coordinate // distance (the old [0, 64) ring fold restarted the threads every chunk). let in_any_thread = thread_centres .iter() .any(|¢re| (cross - centre).abs() <= thread_half); let (elevation_m, water) = if in_any_thread { // In a distributary channel — at or just below base elevation. // Channels are cut 1 m below gravel bars (D-239 §9 ElevationDelta). let channel_elev = (base_elev_m - 1).max(0); (channel_elev, Water::Shallow) } else { // Gravel bar between channels — slightly above channel floor. (base_elev_m + bar_noise.min(1), Water::Dry) }; // Near the delta mouth (elev = 0), some bars are submerged (Shallow). let water = if elevation_m == 0 && !in_any_thread && bar_noise == 0 { Water::Shallow } else { water }; // Vegetation: riparian scrub on stable bars; barren on active channel sediment. let vegetation = if !in_any_thread && district.moisture_q >= 50 { match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket | VegetationClass::RiparianScrub => { if (sub_chunk_seed >> 4) & 0x7 < 3 { Vegetation::Scrub } else { Vegetation::Barren } } VegetationClass::Scrub => { if (sub_chunk_seed >> 4) & 0x3 == 0 { Vegetation::Scrub } else { Vegetation::Barren } } _ => Vegetation::Barren, } } else { Vegetation::Barren }; VoxelColumn { terrain: TerrainMaterial::Gravel, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // DuneStrand generator (T-1029, D-239 §5 / §8 Sand law) // --------------------------------------------------------------------------- /// DuneStrand voxel generator. /// /// Wind-formed dune topology: /// - `Sand` terrain (D-239 §8 Sand→≤~32° angle of repose, dunes not cliffs). /// - Wind-aligned ridges (orientation derived from `basin_direction`). /// - Dune crests and troughs — amplitude ≤ what a 32° slope permits at this /// scale (slope constraint: at most 1 m rise per ~1.6 m horizontal run). /// - Mostly `Dry`; `Shallow` at the seaward toe where surf meets sand. /// - `Barren` or very sparse vegetation (coastal strand). fn generate_dune_strand( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Dune orientation ────────────────────────────────────────────────── // Dunes are wind-aligned; dune ridges run perpendicular to basin_direction // (wind blows onshore, ridges parallel the shoreline). // Along-wind = basin_direction; dune crest height varies along-wind. let (along_wind, cross_wind) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; // ── Dune wavelength ─────────────────────────────────────────────────── // Dune spacing is shorter than meander wavelength; derive from slope_q. // Low slope → long dunes (8–20 m wavelength); high slope → shorter (4–8 m). // Integer arithmetic (D-010). let dune_wavelength = { let slope_clamped = district.slope_q.clamp(0, 40); 20 - slope_clamped / 2 // 20 m flat → 4 m at slope 32 } .max(4); // Phase offset from meander_phase (district-scale, D-239 §10). let phase = chunk.meander_phase as i32; let along_mod = (along_wind + phase).rem_euclid(dune_wavelength); // ── Dune cross-section height ───────────────────────────────────────── // Triangle-wave: ramp from trough to crest and back over one wavelength. // D-239 §8 Sand law: ≤32° angle of repose enforced by the integer physics cap: // tan(32°) ≈ 0.625 → max_height ≤ 0.625 × (wavelength/2). // The integer form `(wavelength * 625) / 2000` directly encodes this. // Also capped by slope_q/4 (district terrain height proxy) to give local variety. let max_height = { let physics_cap = (dune_wavelength * 625) / 2000; // tan(32°)×wavelength/2, integer let district_cap = (district.slope_q / 4).max(1); // district terrain height proxy physics_cap.min(district_cap).max(1) }; let half_wl = dune_wavelength / 2; let dune_height = if along_mod < half_wl { (2 * max_height * along_mod) / dune_wavelength.max(1) // 0 → max } else { max_height - ((along_mod - half_wl) * max_height) / half_wl.max(1) // max → 0 }; // ── Base elevation ──────────────────────────────────────────────────── // Beach/strand is near sea level; add dune height above that base. // Cross-wind variation: sub-dune bar variation (±1 m). let cross_noise = cross_wind.abs() & 0x1; // 0 or 1 // Bits [4:6] of seed → ±1 m additional scatter on the dune surface. let surface_scatter = ((sub_chunk_seed >> 4) & 0x3) as i32 - 1; // [−1, +2] let base_strand_elev = (district.elev_q / 8).max(0); // 0–12 m strand base let elevation_m = (base_strand_elev + dune_height + cross_noise + surface_scatter).max(0); // ── Water state ─────────────────────────────────────────────────────── // Shallow at the strand toe (base elev = 0) and in dune troughs near // the coast. Dry elsewhere. let water = if elevation_m == 0 || (dune_height == 0 && base_strand_elev <= 1) { Water::Shallow // surf line / wave-washed troughs } else { Water::Dry }; // ── Vegetation ──────────────────────────────────────────────────────── // Dunes are mostly barren; scattered dune grass on stable crests // in humid zones. No trees (wind/salt prevents forest). let vegetation = if district.moisture_q >= 55 && dune_height >= max_height / 2 { if (sub_chunk_seed >> 8) & 0x7 < 2 { Vegetation::Grass // dune grass on humid crests } else { Vegetation::Barren } } else { Vegetation::Barren }; VoxelColumn { terrain: TerrainMaterial::Sand, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // IncisedGorge generator (T-1029, D-239 §5 / §8 / §9) // --------------------------------------------------------------------------- /// IncisedGorge voxel generator — shared by MountainPass zones. /// /// Deep narrow gorge: /// - `Rock` terrain (D-239 §8 Rock→vertical faces). /// - Steep walls; single incised channel at the gorge floor. /// - **Gorge floor 2–8 m wide** (D-239 §9 chokepoint constraint). /// - Massive elevation differential: walls are 20–60 m above the floor. /// - `Shallow` water in the incised channel at the floor. /// - Cross-section: wall | wall | narrow floor (with channel) | wall | wall. /// - The gorge runs along `basin_direction` at the district-anchored /// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE gorge /// spans the district's chunks. fn generate_incised_gorge( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Cross-gorge coordinate ───────────────────────────────────────────── let (_along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; // Distance from the district-anchored gorge centreline (T-1041): continuous // world coordinates — folding into the 64 m chunk frame carved a gorge in // every chunk (D-239 §10 violation). let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); // ── Gorge floor width (D-239 §9: 2–8 m) ────────────────────────────── // Floor half-width: 1–4 m (total 2–8 m). Derived from channel_width_m // (capped to 4 m half-width to stay within §9). let floor_half = chunk.channel_width_m.clamp(1, 4); // 1–4 m half-width // ── Wall geometry ───────────────────────────────────────────────────── // The wall rises steeply: each metre away from the floor adds ~8 m of // height. Wall top at district elev_q. let wall_top = (district.elev_q / 2).max(30); // at least 30 m wall height let floor_elev = (district.elev_q / 8).max(0); // floor is much lower than surroundings // Rock wall jaggedness: bits [0:3] → ±4 m. let wall_noise = (sub_chunk_seed & 0xF) as i32 - 8; // [−8, +7] // Floor noise: bits [4:5] → ±1 m floor scatter. let floor_noise = ((sub_chunk_seed >> 4) & 0x3) as i32 - 1; // [−1, +2] let (elevation_m, water) = if cross_from_centre <= floor_half { // ── Gorge floor — narrow and deep ───────────────────────────────── // Active incised channel (D-239 §9: "single incised channel"). // The channel runs along the floor; water is Shallow. let elev = (floor_elev + floor_noise).max(0); (elev, Water::Shallow) } else { // ── Gorge wall — near-vertical face ─────────────────────────────── // Elevation rises steeply: each metre of cross-distance from floor edge // adds wall_step metres. We model this as a steep ramp. let wall_dist = cross_from_centre - floor_half; // Steep wall: 8 m rise per metre of wall distance (near-vertical, §8 // Rock law). The rise saturates at the wall_top clamp below; clamp the // district-scale distance (T-1041) before multiplying — i32 overflow at // extreme coordinates otherwise. let wall_rise = wall_dist.min(16) * 8; let elev = (floor_elev + wall_rise + wall_noise).clamp(floor_elev, wall_top); (elev, Water::Dry) }; // Vegetation: barren in the gorge (shadow / rock). Tiny scatter of Scrub // on the upper wall rim in humid temperate zones. let vegetation = if cross_from_centre > (floor_half + 20) && district.moisture_q >= 60 { match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket => { if (sub_chunk_seed >> 8) & 0x7 < 2 { Vegetation::Scrub } else { Vegetation::Barren } } VegetationClass::Scrub => { if (sub_chunk_seed >> 8) & 0x3 == 0 { Vegetation::Scrub } else { Vegetation::Barren } } _ => Vegetation::Barren, } } else { Vegetation::Barren }; VoxelColumn { terrain: TerrainMaterial::Rock, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // MeanderReach generator (T-1029, D-239 §5 / §8 / §9) // --------------------------------------------------------------------------- /// MeanderReach voxel generator. /// /// Single-thread meandering river floodplain: /// - `Soil` terrain (D-239 §8 Soil→rolling/floodplain law). /// - Stronger single meander than AlluvialPlain (higher sinuosity). /// - `river crossings 3–15 m` width (D-239 §9 game-feel constraint). /// - Levees above / channel below the floodplain surface /// (D-239 §9 ElevationDelta: channels below, levees above high-water threshold). /// - Floodplain vegetation from district class with riparian band at waterway. fn generate_meander_reach( district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Terrain material ─────────────────────────────────────────────────── // MeanderReach → Soil. Wetland sub-zone possible when very wet and flat. let terrain = if matches!(district.morphology_zone, MorphologyZone::Wetland) || (district.slope_q <= 3 && district.moisture_q >= 70) { TerrainMaterial::Wetland } else { TerrainMaterial::Soil }; // ── Base elevation ────────────────────────────────────────────────────── // MeanderReach is a low-gradient floodplain; similar to AlluvialPlain but // typically lower (mature river valley). Map elev_q → metres with a // slightly lower ceiling than AlluvialPlain. let base_elev_m = district.elev_q / 3; // 0–33 m range (lower than Alluvial's /2) let micro_relief = (sub_chunk_seed & 0x7) as i32 - 4; // [−4, +3] m let floodplain_elev = (base_elev_m + micro_relief).max(0); // ── Stronger meander channel (D-239 §9: river crossings 3–15 m) ─────── // MeanderReach uses higher sinuosity than AlluvialPlain. We achieve this // by increasing the effective amplitude multiplier from 1/6 → 1/4 of // wavelength, producing ~25% sinuosity (vs ~17% for AlluvialPlain). // // Channel computation mirrors AlluvialPlain's compute_channel_state but // with the stronger amplitude. let (in_channel, is_deep) = if chunk.has_active_channel { compute_meander_reach_channel(voxel_pos, chunk, sub_chunk_seed) } else { (false, false) }; // ── Levee / channel elevation (D-239 §9 ElevationDelta) ─────────────── // Levees are natural berms built up by flood deposition, elevated above // the floodplain. Channels cut below. Elevation relationship: // levee_top > floodplain > channel_floor (D-239 §9: "levees above, // channels below the high-water threshold"). let (elevation_m, water, vegetation) = if is_deep { // Deep channel thalweg — 3 m below floodplain. let elev = (floodplain_elev - 3).max(0); (elev, Water::Deep, Vegetation::Thicket) } else if in_channel { // Shallow channel margin — 2 m below floodplain. let elev = (floodplain_elev - 2).max(0); // Riparian thicket along the channel bank (D-239 §8 climate→vegetation). let veg = match district.vegetation_class { VegetationClass::Absent | VegetationClass::Barren => Vegetation::Barren, _ => Vegetation::Thicket, }; (elev, Water::Shallow, veg) } else { // ── Off-channel floodplain ──────────────────────────────────────── // Check for levee proximity: within 3 m of channel edge → levee. // Levee is elevated 1–2 m above floodplain. let levee_elev = { let levee_extra = if in_levee_band(voxel_pos, chunk, sub_chunk_seed) { // 1–4 m ((sub_chunk_seed >> 10) & 0x3) as i32 + 1 } else { 0 }; floodplain_elev + levee_extra }; // Vegetation from district class; riparian thicket near the levee. let base_veg = Vegetation::from_vegetation_class(district.vegetation_class); let vegetation = scatter_vegetation(base_veg, sub_chunk_seed); (levee_elev, Water::Dry, vegetation) }; VoxelColumn { terrain, floor: FloorMaterial::None, vegetation, water, elevation_m, cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column } } // --------------------------------------------------------------------------- // SeasonalCover derivation (D-239 §3, T-1030) // --------------------------------------------------------------------------- /// Cluster size for spatially-coherent freeze scatter (D-239 §3). /// /// Neighbouring voxels within the same CLUSTER_M × CLUSTER_M cell share a /// per-cluster scatter offset, producing ragged clustered patches rather than /// per-tile salt-and-pepper. The spec calls for ~4–8 m; we use 6 m. const CLUSTER_M: i32 = 6; /// Freshwater freeze band upper edge (district mean °C, D-239 §3). /// /// Night-frost can ice-over the coldest/most-exposed freshwater tiles even when /// the district mean is still positive (diurnal swing). +5 °C is the onset of /// the scatter band. const FRESH_BAND_HIGH_C: i32 = 5; /// Freshwater freeze band lower edge (D-239 §3). /// /// Below −10 °C the entire freshwater surface is frozen across the full /// day/night cycle. No scatter below this threshold. const FRESH_BAND_LOW_C: i32 = -10; /// Salt-water (sea ice) freeze onset temperature (D-239 §3). /// /// Seawater freezes at ≈ −2 °C due to dissolved salt. const SALT_ONSET_C: i32 = -2; /// Salt-water freeze band width in integer °C (D-239 §3). /// /// The sea-ice band is lower and wider than the freshwater band. We model it /// as [SALT_ONSET_C, SALT_ONSET_C − SALT_BAND_WIDTH]. Below that, permanent /// pack ice everywhere (no scatter). const SALT_BAND_WIDTH: i32 = 12; /// Snow (land) onset temperature — same scattered band as freshwater (D-239 §3). /// /// On land, cover transitions from bare frozen ground to Snow as temp descends /// through this band, but only when moisture_q is above `SNOW_MOISTURE_GATE`. const SNOW_BAND_HIGH_C: i32 = 2; /// Snow moisture gate — moisture_q threshold below which cold land stays bare. /// /// Cold + dry → no snow accumulation (D-239 §3 "gated on moisture"). const SNOW_MOISTURE_GATE: i32 = 30; /// Classify a `MorphologyZone` as salt (ocean/sea) vs fresh water vs land. /// /// D-239 §3: "salt vs fresh: derive from `morphology_zone`". /// - OpenOcean / Sea zones → salt water. /// - Lake / RiverBank / MeanderReach / Delta / BraidedPlain / Estuarine / /// TidalFlat → fresh water. /// - All other zones → land (no water-freeze branch). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SurfaceClass { SaltWater, FreshWater, Land, } fn classify_surface(zone: &MorphologyZone, water: Water) -> SurfaceClass { match zone { // Salt water: open ocean. MorphologyZone::OpenOcean => SurfaceClass::SaltWater, // Fresh surface water: lakes and all river/wetland zones with standing water. // We additionally check Water state — a RiverBank tile that happens to be // Dry at this voxel is functionally land from a freeze perspective. MorphologyZone::Lake => SurfaceClass::FreshWater, MorphologyZone::RiverBank | MorphologyZone::MeanderReach | MorphologyZone::Delta | MorphologyZone::BraidedPlain | MorphologyZone::Estuarine | MorphologyZone::TidalFlat => { if water != Water::Dry { SurfaceClass::FreshWater } else { SurfaceClass::Land } } // Everything else is land (including Fjord walls, DuneStrand, etc.). _ => SurfaceClass::Land, } } /// Per-cluster scatter offset (D-239 §3). /// /// Hash a coarse cluster cell `(cx, cy)` — derived from `(voxel_x, voxel_y)` /// by `div_euclid(CLUSTER_M)` — into a body-scoped u64 scatter value via /// `SeedChain`. All voxels that share the same cluster cell get the **same** /// scatter value (coherent patches). Different world seeds and body ids yield /// different freeze patterns. /// /// ## Key correctness properties /// /// - The cluster-cell coordinates may be negative (voxels at negative positions). /// We zigzag-encode each signed `cx`/`cy` to a non-negative `u64` before /// Cantor-pairing, giving a collision-free bijection for the full signed range. /// (The old cast-to-u64 was wrong for negative inputs — huge two's-complement /// values broke the "no collision" claim.) /// - The seed is derived through `SeedChain::for_body(world_seed, body_id) /// .derive(SeedDomain::Cover, cluster_pair_id)` — a body-scoped, domain- /// separated derivation, not a raw `splitmix64` on the per-voxel sub_chunk_seed. /// This is the root fix for the coherence defect: `sub_chunk_seed` is per-voxel, /// so passing it here gave each voxel its own scatter value → salt-and-pepper. /// `SeedChain::for_body` is body-scoped and the cluster id is a cluster-cell /// property — all voxels in the same cell derive the same seed. #[inline] fn cluster_scatter(world_seed: u64, body_id: &str, voxel_x: i32, voxel_y: i32) -> u64 { use crate::seed::{SeedChain, SeedDomain}; // Coarse cluster cell index (Euclidean div so negatives map cleanly). let cx_i = voxel_x.div_euclid(CLUSTER_M); let cy_i = voxel_y.div_euclid(CLUSTER_M); // Zigzag encode signed → unsigned (same pattern as voxel_pos_to_id and // domain_warp::pos_to_id — bijective over the full i32 range, no collisions // between positive and negative cluster coords). let zz = |v: i32| -> u64 { let v = v as i64; ((v << 1) ^ (v >> 63)) as u64 }; let cx = zz(cx_i); let cy = zz(cy_i); // Cantor-pairing (now collision-free because both inputs are non-negative u64). let s = cx.wrapping_add(cy); let cluster_pair_id = s .wrapping_mul(s.wrapping_add(1)) .wrapping_div(2) .wrapping_add(cy); // Body-scoped, domain-separated derivation via SeedChain (D-224 / D-010). // All voxels sharing this cluster cell → same cluster_pair_id → same seed. SeedChain::for_body(world_seed, body_id) .derive(SeedDomain::Cover, cluster_pair_id) .seed() } /// Derive the `SeasonalCover` overlay for a single voxel (D-239 §3, T-1030). /// /// ## Rules (D-239 §3 verbatim mapping) /// /// ### Airless body /// `temperature_c == None` → `SeasonalCover::None`. No climate branch. /// /// ### Fresh water (lakes / rivers) /// Scatter band **+5 °C → −10 °C** (district mean temperature_c as i32). /// - Above +5 °C: `None` (open water). /// - Within the band [−10, +5]: cluster-scatter decides `Ice` vs `None`. /// The scatter is driven by a per-cluster hash; more of the band → more Ice. /// - Below −10 °C: `Ice` everywhere (permanent). /// /// ### Salt water (sea ice) /// Band onset ≈ −2 °C, width 12 °C (so solid below −14 °C). /// - Above −2 °C: `None`. /// - Within the band [−14, −2]: cluster-scatter → `Ice` vs `None`. /// Pack-ice pattern: larger coherent sheets + occasional open "lead" gaps. /// Leads use a separate coarser cluster size (3× CLUSTER_M) to produce /// sheet-scale coherence rather than fine-grained scatter. /// - Below −14 °C: `Ice` everywhere (permanent pack ice). /// /// ### Land (snow) /// Moisture-gated: `moisture_q >= SNOW_MOISTURE_GATE` (30) required. /// Band **+2 °C → −10 °C** (same low edge as freshwater). /// - Above +2 °C, or dry (moisture_q < 30): `None` (bare frozen ground if cold). /// - Within the band [−10, +2] and moist: cluster-scatter → `Snow` vs `None`. /// - Below −10 °C and moist: `Snow` everywhere (permanent). /// /// ## Spatial coherence (D-239 §3) /// /// Coherence is implemented by hashing the **coarse cluster cell** /// `(voxel_x.div_euclid(CLUSTER_M), voxel_y.div_euclid(CLUSTER_M))` into a /// per-cluster scatter offset. All voxels in the same 6 m × 6 m cluster share /// the same scatter decision, producing ragged but spatially coherent freeze /// patches — "clustered patches, NEVER per-tile dice" (D-239 §3). /// /// ## Q-105 forward contract /// /// This function models the **static mean-state** only. The transient /// (clock-bound) cover — dawn frost that burns off by noon, a stream iced at /// dawn that is crossable by midday — requires the cheap regional clock state /// described in Q-105. When Q-105 is implemented, callers should treat the /// `cover` field as the base state and apply the Q-105 modifier on top. /// /// ## D-010 compliance /// /// All decisions are integer. `temperature_c` is cast to `i32` once (positional /// quantisation, not a structural float gate). All scatter is hash/integer only. pub fn derive_cover( world_seed: u64, body_id: &str, district: &DistrictProfile, column: &VoxelColumn, voxel_pos: VoxelPos, ) -> SeasonalCover { // ── Airless body (D-239 §2) ─────────────────────────────────────────── // No atmosphere → no climate branch → no seasonal cover. let Some(temp_f) = district.temperature_c else { return SeasonalCover::None; }; // Integer cast: all gating is on i32 (D-010; f32→i32 cast is positional // quantisation, not a structural float comparison). let temp_i: i32 = temp_f as i32; let (voxel_x, voxel_y) = voxel_pos; // Per-cluster scatter: keyed ONLY on (world, body, cluster-cell) — never // on the per-voxel sub_chunk_seed. All voxels in the same 6×6 m cell share // this value, producing ragged clustered patches (D-239 §3). let scatter = cluster_scatter(world_seed, body_id, voxel_x, voxel_y); let surface = classify_surface(&district.morphology_zone, column.water); match surface { // ── Fresh water (lakes / rivers) ────────────────────────────────── SurfaceClass::FreshWater => { if temp_i >= FRESH_BAND_HIGH_C { // Above band — open water. SeasonalCover::None } else if temp_i < FRESH_BAND_LOW_C { // Below band — permanently frozen. SeasonalCover::Ice } else { // Within the scatter band [FRESH_BAND_LOW_C, FRESH_BAND_HIGH_C). // Threshold: scatter % band_width < depth_into_band → Ice. // depth 1 (just below +5°C) ≈ 1/15 ≈ 7% Ice. // depth 14 (just above −10°C) ≈ 14/15 ≈ 93% Ice. // Ice-fraction is monotonically increasing as temp drops. let band_width = (FRESH_BAND_HIGH_C - FRESH_BAND_LOW_C) as u64; // 15 let depth_into_band = (FRESH_BAND_HIGH_C - temp_i) as u64; // 1..=14 if scatter % band_width < depth_into_band { SeasonalCover::Ice } else { SeasonalCover::None } } } // ── Salt water (sea ice) ────────────────────────────────────────── SurfaceClass::SaltWater => { let salt_band_low = SALT_ONSET_C - SALT_BAND_WIDTH; // −14 °C if temp_i >= SALT_ONSET_C { // Above onset — open ocean. SeasonalCover::None } else if temp_i < salt_band_low { // Below band — permanent pack ice. SeasonalCover::Ice } else { // Within the sea-ice scatter band [−14, −2). // // Ice-fraction is monotonically increasing as temp drops // (same linear model as freshwater: scatter % band_width < // depth_into_band). At onset (depth 1) ≈ 8% ice; at depth 11 // (just above −14°C) ≈ 92% ice. // // Pack-ice sheets + leads structure: on the cold, mostly-frozen // end we apply a 3× coarser cluster (super-cell = 3×CLUSTER_M) // to carve occasional open "lead" gaps within what would otherwise // be solid ice. A lead forms when the super-cell scatter indicates // a gap AND the voxel is in the cold zone where ice would normally // be solid. This gives sheet-scale coherence (large contiguous ice // panels) with a small fraction of narrow persistent leads. let depth_into_band = (SALT_ONSET_C - temp_i) as u64; // 1..=11 let band_width = SALT_BAND_WIDTH as u64; // 12 // Primary ice decision (monotonic, same logic as freshwater). let is_ice = scatter % band_width < depth_into_band; if is_ice { // In the cold half of the band (depth > band_width/2), apply // the pack-ice lead pattern using a coarser super-cell cluster. // The lead cluster is keyed on the super-cell index directly, // not on pre-quantized coordinates, so the intent is clear. let cold_threshold = band_width / 2; if depth_into_band > cold_threshold { // Super-cell index: one step per 3×CLUSTER_M metres. let super_cell_size = CLUSTER_M * 3; let scx = voxel_x.div_euclid(super_cell_size); let scy = voxel_y.div_euclid(super_cell_size); let sheet_scatter = cluster_scatter( world_seed, body_id, scx * super_cell_size, scy * super_cell_size, ); // ~8% open leads in the cold zone (persistent gaps in pack ice). let is_lead = sheet_scatter % 100 < 8; if is_lead { SeasonalCover::None } else { SeasonalCover::Ice } } else { SeasonalCover::Ice } } else { SeasonalCover::None } } } // ── Land (snow) ─────────────────────────────────────────────────── SurfaceClass::Land => { // Moisture gate: cold + dry → bare frozen ground (no snow). if district.moisture_q < SNOW_MOISTURE_GATE { return SeasonalCover::None; } if temp_i >= SNOW_BAND_HIGH_C { // Above snow band — too warm for snow accumulation. SeasonalCover::None } else if temp_i < FRESH_BAND_LOW_C { // Below snow band — permanent snow (same low edge as freshwater). SeasonalCover::Snow } else { // Within the scatter band [FRESH_BAND_LOW_C, SNOW_BAND_HIGH_C). // Same monotonic model: ice-fraction increases as temp drops. let band_width = (SNOW_BAND_HIGH_C - FRESH_BAND_LOW_C) as u64; // 12 let depth_into_band = (SNOW_BAND_HIGH_C - temp_i) as u64; // 1..=11 if scatter % band_width < depth_into_band { SeasonalCover::Snow } else { SeasonalCover::None } } } } } /// Compute channel state for MeanderReach — higher sinuosity than AlluvialPlain. /// /// Uses amplitude = wavelength/4 (~25% sinuosity) vs AlluvialPlain's /6 (~17%). /// This produces a more pronounced meander — a wider swing across the floodplain, /// creating more tactical variety (D-239 §9 game-feel). fn compute_meander_reach_channel( voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64, ) -> (bool, bool) { let (vx, vy) = voxel_pos; let (along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (vy, vx), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (vx, vy), }; let wavelength_m = (chunk.meander_wavelength_m as i32).max(10); let phase_offset = chunk.meander_phase as i32; let angle_mod = (along + phase_offset).rem_euclid(wavelength_m); // MeanderReach amplitude: 1/4 of wavelength (stronger meander than AlluvialPlain). let amplitude = (wavelength_m / 4).max(3); // at least ±3 m amplitude let half_wl = wavelength_m / 2; let meander_displacement = if angle_mod < half_wl { (2 * amplitude * angle_mod) / half_wl.max(1) - amplitude } else { amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; // District-anchored centreline (T-1040) — same anchor as AlluvialPlain. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Edge jitter from sub-chunk seed (±1 m), same as AlluvialPlain. let edge_jitter = (sub_chunk_seed & 0x3) as i32; let half_width = (chunk.channel_width_m / 2).max(2); // MeanderReach channels at least 4 m total let channel_edge = half_width + edge_jitter; let deep_edge = (half_width / 2).max(1); let in_channel = perp_distance <= channel_edge; let is_deep = perp_distance <= deep_edge; (in_channel, is_deep) } /// Determine if a voxel is in the levee band adjacent to the MeanderReach channel. /// /// Levees are natural berms 3–8 m wide on each bank, just outside the channel /// edge. They are elevated 1–4 m above the floodplain. The levee band is the /// zone from channel_edge to channel_edge + levee_width. fn in_levee_band(voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64) -> bool { let (vx, vy) = voxel_pos; let (along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (vy, vx), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (vx, vy), }; let wavelength_m = (chunk.meander_wavelength_m as i32).max(10); let phase_offset = chunk.meander_phase as i32; let angle_mod = (along + phase_offset).rem_euclid(wavelength_m); let amplitude = (wavelength_m / 4).max(3); let half_wl = wavelength_m / 2; let meander_displacement = if angle_mod < half_wl { (2 * amplitude * angle_mod) / half_wl.max(1) - amplitude } else { amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; // District-anchored centreline (T-1040) — same anchor as the channel itself. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); let edge_jitter = (sub_chunk_seed & 0x3) as i32; let half_width = (chunk.channel_width_m / 2).max(2); let channel_edge = half_width + edge_jitter; let levee_width = 4 + ((sub_chunk_seed >> 6) & 0x3) as i32; // 4–7 m levee band // In levee band: just outside channel, within levee_width metres of channel edge. perp_distance > channel_edge && perp_distance <= channel_edge + levee_width } /// Determine if a voxel is in the meander channel, and if it is deep. /// /// The channel is a sine-wave path oriented along `chunk.basin_direction`. /// We compute the perpendicular distance (integer, D-010) from the voxel to /// the wave centreline. Within half the channel width → in-channel; within /// one-third of the channel width → deep water. /// /// ## Integer arithmetic (D-010) /// /// We use a 10× fixed-point approximation for the sine: `sin(θ) ≈ phase_term / 10` /// where `phase_term` is derived from the meander phase and the voxel's /// along-channel coordinate. This avoids any float comparison while producing /// a spatially coherent (though imprecise) meander curve. fn compute_channel_state( voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64, ) -> (bool, bool) { let (vx, vy) = voxel_pos; // Along-channel (parallel) and cross-channel (perpendicular) coordinate. // basin_direction determines the primary axis. let (along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (vy, vx), crate::atlas::chunk_context::BasinDirection::East | crate::atlas::chunk_context::BasinDirection::West => (vx, vy), }; // Meander wavelength in metres (integer approximation: truncate f64 → i32). // The f64 wavelength is position math (D-239 §4), not a gate comparison — // we truncate to integer here before any structural decision. let wavelength_m = (chunk.meander_wavelength_m as i32).max(10); // Phase angle in [0, wavelength_m). Deterministic integer modulo. let phase_offset = chunk.meander_phase as i32; let angle_mod = (along + phase_offset).rem_euclid(wavelength_m); // Integer sine approximation: map angle_mod to a cross-channel displacement // in [−amplitude, +amplitude]. We use a triangle wave (cheaper than a // true sine table but still spatially coherent for a walking skeleton). // // Triangle wave on [0, wavelength_m]: // - [0, wavelength_m/2]: displacement = 2*amplitude * angle / wavelength_m − amplitude // - [wavelength_m/2, wavelength_m]: displacement = amplitude − 2*amplitude * (angle − wl/2) / wl // // Amplitude: ~15% of wavelength for a moderate sinuosity. let amplitude = wavelength_m / 6; // ~17% sinuosity let amplitude = amplitude.max(2); // at least ±2 m amplitude let half_wl = wavelength_m / 2; let meander_displacement = if angle_mod < half_wl { // Rising half: ramps from -amplitude to +amplitude. (2 * amplitude * angle_mod) / half_wl - amplitude } else { // Falling half: ramps from +amplitude to -amplitude. amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl }; // Perpendicular distance from voxel to the district-anchored channel // centreline (T-1040): the channel axis is `chunk.channel_anchor_m` on the // cross axis — a district-scale world coordinate, never the world origin. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Sub-chunk noise: ±1 m jitter on the channel edge (wavelength < 64 m). // Use low bits of seed for integer jitter — D-010. let edge_jitter = (sub_chunk_seed & 0x3) as i32; // [0, 3] let half_width = (chunk.channel_width_m / 2).max(1); let channel_edge = half_width + edge_jitter; let deep_edge = (half_width / 2).max(1); let in_channel = perp_distance <= channel_edge; let is_deep = perp_distance <= deep_edge; (in_channel, is_deep) } /// Apply sub-chunk vegetation scatter using the sub-chunk seed. /// /// The base class from the district establishes the dominant cover type; the /// scatter adds variety within the class (e.g. some Grass in a Forest zone /// at clearings). Wavelength < 64 m — seeded from sub_chunk_seed (D-239 §10). /// /// All integer arithmetic (D-010). fn scatter_vegetation(base: Vegetation, sub_chunk_seed: u64) -> Vegetation { // Use bits [4:7] of the seed (4-bit mask, range [0, 15]) for scatter — // independent from the channel // computation which uses bits [0:3]. let scatter_bits = ((sub_chunk_seed >> 4) & 0xF) as u8; match base { Vegetation::Forest => { // 15% Grass clearings, 15% Scrub margin, 70% Forest. if scatter_bits < 3 { Vegetation::Grass } else if scatter_bits < 6 { Vegetation::Scrub } else { Vegetation::Forest } } Vegetation::Scrub => { // 20% Grass, 80% Scrub. if scatter_bits < 4 { Vegetation::Grass } else { Vegetation::Scrub } } Vegetation::Thicket => { // Riparian thicket: mostly Thicket with some Forest. if scatter_bits < 5 { Vegetation::Thicket } else { Vegetation::Forest } } // Barren, Grass, Crop, Cleared: no scatter. other => other, } } /// Fold a `VoxelPos` into a u64 id for seed derivation. /// /// Same bijective approach as `chunk_context::pos_to_id` and /// `domain_warp::pos_to_id` — zigzag encode + Cantor pairing. Integer-only (D-010). #[inline] fn voxel_pos_to_id(pos: VoxelPos) -> u64 { let zz = |v: i32| -> u64 { let v = v as i64; ((v << 1) ^ (v >> 63)) as u64 }; let x = zz(pos.0); let y = zz(pos.1); let s = x.wrapping_add(y); s.wrapping_mul(s.wrapping_add(1)) .wrapping_div(2) .wrapping_add(y) } // --------------------------------------------------------------------------- // VoxelCache — LRU-style on-demand cache (D-227) // --------------------------------------------------------------------------- /// Cache key: `(body_id, voxel_pos_after_warp)`. /// /// The body_id is stored as a u64 hash (FNV-1a) to keep the key `Copy` /// and `Ord`-able (D-010: `BTreeMap` key). /// `(world_seed, body_id_hash, post-warp voxel address)`. world_seed is part of /// the voxel's identity — a cache must never return another world's column. type CacheKey = (u64, u64, VoxelPos); /// One cache entry: the derived column plus an access generation counter. struct CacheEntry { column: VoxelColumn, /// Monotonically increasing access counter — the LRU eviction key. access_gen: u64, } /// Bounded LRU-style cache for derived `VoxelColumn`s (D-227). /// /// ## Invariants /// /// - At most `capacity` entries are stored at any time. /// - Entries are evicted in LRU order (lowest `access_gen`). /// - Nothing is persisted to disk; eviction forces re-derivation. /// /// ## Caller precondition — chunk context must cover the tile (T-1042) /// /// After T-1042, `derive_voxel_column` reads `chunk.blend_weight` and /// `chunk.secondary` to apply cross-district terrain blending. The cache /// key is `(seed, body_id_hash, post-warp voxel position)` — it does NOT /// capture the blend parameters. The cache invariant therefore holds only /// under the following precondition: /// /// **The `chunk` argument passed to `get_or_derive` must be the /// `ChunkContext` derived for the chunk that actually contains the tile.** /// Concretely: `chunk = derive_chunk_context(seed, body_id, district, /// tile_pos.div_euclid(CHUNK_M), secondary)`. A boundary tile derived with /// a blended context (blend_weight < 255) and later re-requested with a /// pure-primary context (blend_weight = 255) would produce a different /// column — the cache would return the first (blended) value, silently /// giving the wrong result. The caller must ensure the same context /// construction is used on every call for the same tile. /// /// **In practice this is satisfied by construction**: callers derive one /// `ChunkContext` per chunk and call `get_or_derive` only for tiles within /// that chunk. The precondition is made explicit here so Phase-5 render-loop /// callers do not inadvertently mix contexts across calls. /// /// ## D-010 compliance /// /// Uses `BTreeMap` (ordered) for the cache store. Iteration order is /// deterministic; eviction scans for the minimum `access_gen`. pub struct VoxelCache { capacity: usize, entries: BTreeMap, gen: u64, } impl VoxelCache { /// Create a new cache with the given capacity. /// /// The default capacity in production is [`DEFAULT_VOXEL_CACHE_CAPACITY`]. pub fn new(capacity: usize) -> Self { Self { capacity: capacity.max(1), entries: BTreeMap::new(), gen: 0, } } /// Look up or derive the `VoxelColumn` for the given position. /// /// Cache hit: returns a clone of the stored column, bumps its access /// generation (LRU recency). /// Cache miss: derives the column, inserts it (evicting LRU if full), /// returns a clone. /// /// The body-id hash is computed internally via the canonical /// [`crate::seed::fnv1a_64`] — callers pass the raw `body_id`. /// /// ## Precondition — context must cover the tile (T-1042) /// /// `chunk` must be the `ChunkContext` derived for the chunk that /// contains `(tile_x, tile_y)`. The cache key does not capture /// `chunk.blend_weight` or `chunk.secondary`; if the same tile is /// requested with a different context (e.g. once blended, once /// unblended) the cache will return the first-derived column for both /// calls. See the struct-level doc for the full constraint. pub fn get_or_derive( &mut self, world_seed: u64, body_id: &str, district: &DistrictProfile, chunk: &ChunkContext, tile_x: i32, tile_y: i32, ) -> VoxelColumn { // Apply domain warp to get the canonical voxel address (same as in // derive_voxel_column — the cache key is the POST-warp voxel address). let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y)); let voxel_x = (tile_x as f64 + dx) as i32; let voxel_y = (tile_y as f64 + dy) as i32; let key: CacheKey = ( world_seed, crate::seed::fnv1a_64(body_id), (voxel_x, voxel_y), ); self.gen += 1; let current_gen = self.gen; if let Some(entry) = self.entries.get_mut(&key) { entry.access_gen = current_gen; return entry.column.clone(); } // Cache miss — derive and insert. let column = derive_voxel_column(world_seed, body_id, district, chunk, tile_x, tile_y); // Evict LRU if at capacity. if self.entries.len() >= self.capacity { self.evict_lru(); } self.entries.insert( key, CacheEntry { column: column.clone(), access_gen: current_gen, }, ); column } /// Number of entries currently in the cache. #[cfg(test)] pub fn len(&self) -> usize { self.entries.len() } /// Whether the cache holds no entries. #[cfg(test)] pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Test helper: is the voxel for this `(world_seed, body_id, tile)` currently /// cached? Recomputes the canonical post-warp key without mutating LRU state, /// so tests can assert exactly which entry survived or was evicted. #[cfg(test)] pub fn contains_voxel(&self, world_seed: u64, body_id: &str, tile_x: i32, tile_y: i32) -> bool { let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y)); let voxel_x = (tile_x as f64 + dx) as i32; let voxel_y = (tile_y as f64 + dy) as i32; let key: CacheKey = ( world_seed, crate::seed::fnv1a_64(body_id), (voxel_x, voxel_y), ); self.entries.contains_key(&key) } /// Evict the entry with the lowest `access_gen` (the LRU entry). fn evict_lru(&mut self) { if self.entries.is_empty() { return; } // Scan for the minimum access_gen. BTreeMap iteration is ordered by // key (deterministic, D-010), so a tie is broken by key order. let lru_key = self .entries .iter() .min_by_key(|(_, v)| v.access_gen) .map(|(k, _)| *k) .expect("entries is non-empty"); self.entries.remove(&lru_key); } } /// Default voxel cache capacity for production use. /// /// 64×64 = 4096 voxels per chunk; with a 4-chunk radius lookahead (common /// streaming pattern) that is ~65k voxels in flight. 8k entries keeps the /// most-recently-used chunk and a partial second chunk warm without blowing /// the memory budget (~200 bytes/entry × 8192 = ~1.6 MB). pub const DEFAULT_VOXEL_CACHE_CAPACITY: usize = 8192; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use crate::atlas::chunk_context::{derive_chunk_context, BasinDirection}; use crate::atlas::district_profile::{ GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass, }; // ----------------------------------------------------------------------- // Test fixtures // ----------------------------------------------------------------------- /// The ChunkContext of the district-(0,0) chunk whose cross-range contains /// the channel anchor — guaranteed inside the T-1040 channel band. /// Feature placement is district-anchored, so tests sample around /// `chunk.channel_anchor_m` instead of assuming chunk-frame positions. fn anchor_chunk(seed: u64, body: &str, district: &DistrictProfile) -> ChunkContext { let probe = derive_chunk_context(seed, body, district, (0, 0), None); let idx = probe.channel_anchor_m.div_euclid(64); let pos = match probe.basin_direction { BasinDirection::North | BasinDirection::South => (idx, 0), BasinDirection::East | BasinDirection::West => (0, idx), }; derive_chunk_context(seed, body, district, pos, None) } /// Map a (cross, along) coordinate pair to (tile_x, tile_y) for the /// chunk's basin axes — cross ⊥ basin_direction, along ∥ basin_direction. fn cross_along_to_tile(chunk: &ChunkContext, cross: i32, along: i32) -> (i32, i32) { match chunk.basin_direction { BasinDirection::North | BasinDirection::South => (cross, along), BasinDirection::East | BasinDirection::West => (along, cross), } } fn alluvial_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: TectonicClass::Stable, glaciation_grade: GlaciationGrade::None, precipitation_class: PrecipitationClass::Temperate, slope_q: 5, elev_q: 20, ocean_fraction_q: 15, river_threshold: 200, temperature_c: Some(18.0), moisture_q: 55, vegetation_class: VegetationClass::Forest, basin_direction: BasinDirection::South, } } fn alluvial_chunk(district: &DistrictProfile) -> ChunkContext { 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); } // ── Voxel-tier mid-scale relief (T-1081) ───────────────────────────────── #[test] fn voxel_relief_gives_rugged_land_navigable_hills() { // T-1081: a rugged land district must produce mid-scale relief across a ~2 km // span far exceeding the ±4 m micro-scatter (the "flat 0–3 m, no hills" symptom). let mut district = alluvial_district(); district.slope_q = 60; // rugged → relief headroom let chunk = alluvial_chunk(&district); let mut elevs = Vec::new(); for i in 0..12 { let p = i * 256; // step across the voxel-relief band (0.25–2 km) let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); if col.water == Water::Dry { elevs.push(col.elevation_m); } } assert!(elevs.len() >= 4, "expected mostly dry-land samples"); let min = *elevs.iter().min().unwrap(); let max = *elevs.iter().max().unwrap(); assert!( max - min > 20, "rugged district should have navigable mid-scale relief, got {min}..{max} m" ); } #[test] fn voxel_relief_respects_low_flat_envelope() { // No slope AND no elevation → no terrain signal → no invented relief (the // envelope rule: don't sprout hills on a flat sea-level plain). Elevation stays // the family base (elev_q/2 = 0 m) plus only the ±4 m micro-scatter / channel cut. let mut district = alluvial_district(); district.slope_q = 0; district.elev_q = 0; let chunk = alluvial_chunk(&district); for i in 0..12 { let p = i * 256; let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); assert!( col.elevation_m <= 4, "low-flat district gained mid-scale relief: {} m", col.elevation_m ); } } #[test] fn voxel_relief_from_elevation_even_with_zero_slope() { // The T-1081 root cause: the coarse heightmap gives slope_q ≈ 0 even on high // terrain, so ELEVATION must drive the relief envelope. A high district with // slope_q == 0 must still gain navigable hills (else high ground reads flat). let mut district = alluvial_district(); district.slope_q = 0; district.elev_q = 90; // high ground, coarse-heightmap-flat let chunk = alluvial_chunk(&district); let mut elevs = Vec::new(); for i in 0..12 { let p = i * 256; let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); if col.water == Water::Dry { elevs.push(col.elevation_m); } } let range = elevs.iter().max().unwrap() - elevs.iter().min().unwrap(); assert!( range > 20, "high terrain must gain relief despite slope_q=0, got range {range} m" ); } #[test] fn voxel_relief_is_deterministic() { let mut district = alluvial_district(); district.slope_q = 50; let chunk = alluvial_chunk(&district); let a = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234); let b = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234); assert_eq!(a.elevation_m, b.elevation_m); } // ----------------------------------------------------------------------- // TerrainMaterial discriminant pins // ----------------------------------------------------------------------- #[test] fn terrain_material_discriminants_pinned() { // Append-only invariant (D-010). assert_eq!(TerrainMaterial::Soil as u8, 0); assert_eq!(TerrainMaterial::Sand as u8, 1); assert_eq!(TerrainMaterial::Gravel as u8, 2); assert_eq!(TerrainMaterial::Rock as u8, 3); assert_eq!(TerrainMaterial::Wetland as u8, 4); assert_eq!(TerrainMaterial::Lava as u8, 5); } #[test] fn water_discriminants_pinned() { assert_eq!(Water::Dry as u8, 0); assert_eq!(Water::Shallow as u8, 1); assert_eq!(Water::Deep as u8, 2); } // ----------------------------------------------------------------------- // AlluvialPlain properties // ----------------------------------------------------------------------- #[test] fn alluvial_plain_produces_soil_terrain() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); // Sample a voxel that shouldn't be in a wetland zone. let col = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); assert_eq!( col.terrain, TerrainMaterial::Soil, "AlluvialPlain must produce Soil terrain" ); } #[test] fn alluvial_plain_floor_is_none() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let col = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); assert_eq!(col.floor, FloorMaterial::None, "Phase 4 floor must be None"); } #[test] fn alluvial_plain_vegetation_from_district_class() { let district = alluvial_district(); // vegetation_class = Forest let chunk = alluvial_chunk(&district); // Multiple samples to account for sub-chunk scatter; all should be // in the Forest/Scrub/Grass range (D-239 §8: no-skip law — no Barren in Forest zone). for tx in [100, 101, 102, 103, 104] { let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, 100); assert!( matches!( col.vegetation, Vegetation::Forest | Vegetation::Scrub | Vegetation::Grass ), "Forest district must produce Forest/Scrub/Grass vegetation, got {:?}", col.vegetation ); } } #[test] fn elevation_is_non_negative() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); assert!( col.elevation_m >= 0, "elevation must be >= 0, got {}", col.elevation_m ); } } // ----------------------------------------------------------------------- // Determinism — the core T-1028 contract // ----------------------------------------------------------------------- /// End-to-end determinism: same (seed, body, district) → same voxel columns. /// Two calls at the same position must produce identical results. #[test] fn voxel_derivation_is_deterministic() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let positions = [(100, 100), (0, 0), (-50, 25), (200, -10), (64, 64)]; for (tx, ty) in positions { let a = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); let b = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); assert_eq!( a, b, "voxel at ({tx},{ty}) must be identical on repeated derivation" ); } } /// Different positions must (in general) produce different columns. #[test] fn different_positions_produce_different_outputs() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let a = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); let b = derive_voxel_column(42, "GJ1c", &district, &chunk, 1000, 2000); // Not strictly guaranteed (hash collision possible) but overwhelmingly likely. assert!( a != b || a.elevation_m != b.elevation_m, "positions 100m apart should produce different columns" ); } /// Different world seeds produce different results. #[test] fn different_seeds_produce_different_outputs() { let district = alluvial_district(); let chunk1 = derive_chunk_context(1, "GJ1c", &district, (10, 20), None); let chunk2 = derive_chunk_context(2, "GJ1c", &district, (10, 20), None); let a = derive_voxel_column(1, "GJ1c", &district, &chunk1, 100, 100); let b = derive_voxel_column(2, "GJ1c", &district, &chunk2, 100, 100); assert!( a != b, "different world seeds must produce different voxel outputs" ); } // ----------------------------------------------------------------------- // Cache behaviour (D-227) // ----------------------------------------------------------------------- #[test] fn cache_returns_same_column_as_direct_derivation() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let mut cache = VoxelCache::new(64); let cached = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); let direct = derive_voxel_column(42, body_id, &district, &chunk, 100, 100); assert_eq!( cached, direct, "cache must return same result as direct derivation" ); } #[test] fn cache_second_call_is_a_hit() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let mut cache = VoxelCache::new(64); let first = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); assert_eq!(cache.len(), 1, "first call must insert one entry"); let second = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); assert_eq!( cache.len(), 1, "second call must be a cache hit, not a new insertion" ); assert_eq!(first, second, "cache hit must return same column"); } #[test] fn cache_evicts_lru_on_overflow() { let district = alluvial_district(); let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let capacity = 8; let mut cache = VoxelCache::new(capacity); // Fill cache to capacity with distinct positions. for i in 0..capacity as i32 { cache.get_or_derive(42, body_id, &district, &chunk, i * 64, 0); } assert_eq!(cache.len(), capacity); // Access position 0 to refresh it (not the LRU). cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); // Insert a new entry — should evict the LRU (position 64, not the // just-refreshed position 0). cache.get_or_derive(42, body_id, &district, &chunk, 999, 999); assert_eq!( cache.len(), capacity, "cache size must stay at capacity after eviction" ); // Verify the RIGHT entry was evicted, not just that some eviction happened. assert!( cache.contains_voxel(42, body_id, 0, 0), "refreshed entry (pos 0) must survive eviction" ); assert!( !cache.contains_voxel(42, body_id, 64, 0), "LRU entry (pos 64) must have been the one evicted" ); assert!( cache.contains_voxel(42, body_id, 999, 999), "newly inserted entry must be present" ); } #[test] fn cache_eviction_does_not_break_derivation() { // After eviction, re-deriving the evicted entry must produce the same result. let district = alluvial_district(); let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let capacity = 2; let mut cache = VoxelCache::new(capacity); // Populate the cache fully. let col0 = cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); let _col1 = cache.get_or_derive(42, body_id, &district, &chunk, 100, 0); // Insert a third entry — evicts (0,0) which is the LRU. let _col2 = cache.get_or_derive(42, body_id, &district, &chunk, 200, 0); // Re-derive position (0,0) — cache miss, must re-derive correctly. let col0_again = cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); assert_eq!( col0, col0_again, "re-derived column after cache eviction must be identical" ); } #[test] fn cache_boundary_tile_blend_is_stable() { // T-1042 / H3: VoxelCache.get_or_derive must return identical blended // output on two successive calls for the same boundary tile. This test // exercises the `Cow::Owned` path (blend_weight=128, secondary=Some(...)) // through the cache and guards against future cache-key drift where a // second call with a different context returns a stale unblended entry. // // Precondition (VoxelCache struct doc, T-1042): both calls use the SAME // ChunkContext — the one derived for the chunk containing the tile. This // is the correct call pattern; the test intentionally validates it. use crate::atlas::chunk_context::district_boundary_blend_weight; let district_primary = DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: crate::atlas::district_profile::TectonicClass::Stable, glaciation_grade: crate::atlas::district_profile::GlaciationGrade::None, precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate, slope_q: 5, elev_q: 20, // low elevation ocean_fraction_q: 0, // no channel — simpler tile layout for elevation check river_threshold: 200, temperature_c: Some(18.0), moisture_q: 55, vegetation_class: VegetationClass::Forest, basin_direction: BasinDirection::South, }; let district_secondary = DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: crate::atlas::district_profile::TectonicClass::Stable, glaciation_grade: crate::atlas::district_profile::GlaciationGrade::None, precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate, slope_q: 5, elev_q: 70, // high elevation — makes the blend measurable ocean_fraction_q: 0, river_threshold: 200, temperature_c: Some(18.0), moisture_q: 55, vegetation_class: VegetationClass::Forest, basin_direction: BasinDirection::South, }; let (seed, body_id) = (42u64, "blend_cache_test"); // Chunk (31, 0): the last chunk in district (0, 0) — at the district boundary. let boundary_chunk_pos = (31i32, 0i32); let (near, blend_w) = district_boundary_blend_weight(boundary_chunk_pos); assert!(near, "chunk (31, 0) must be detected as a boundary chunk"); assert_eq!(blend_w, 128); let chunk = derive_chunk_context( seed, body_id, &district_primary, boundary_chunk_pos, Some((&district_secondary, blend_w)), ); // A tile inside the boundary chunk — world position = chunk_x * 64 + offset. let tile_x = boundary_chunk_pos.0 * 64 + 16; let tile_y = boundary_chunk_pos.1 * 64 + 16; let mut cache = VoxelCache::new(64); // First call: cache miss → derives the blended column and inserts it. let col1 = cache.get_or_derive(seed, body_id, &district_primary, &chunk, tile_x, tile_y); assert_eq!(cache.len(), 1, "first call must produce one cache entry"); // Second call: cache hit → must return the same blended column. let col2 = cache.get_or_derive(seed, body_id, &district_primary, &chunk, tile_x, tile_y); assert_eq!( cache.len(), 1, "second call must be a cache hit (len unchanged)" ); assert_eq!( col1.elevation_m, col2.elevation_m, "cache hit must return identical blended elevation for boundary tile" ); assert_eq!( col1.terrain, col2.terrain, "cache hit must return identical terrain for boundary tile" ); // Sanity: the blended elevation must sit between the two district values. // primary elev_q=20 → base_elev_m=10; secondary elev_q=70 → base_elev_m=35. // At 50-50 blend the blended elev_q = (20*128 + 70*127 + 127)/255 = 44 (rounded). // base_elev_m = 44/2 = 22. With micro-relief ±4 m, range is [18, 25] m. // This asserts the blend actually ran (not the pure-primary 10 m result). assert!( col1.elevation_m > 15, "blended boundary tile elevation ({}) must be above pure-primary level (~10 m) \ — blend did not apply", col1.elevation_m ); } // ----------------------------------------------------------------------- // Domain warp sanity // ----------------------------------------------------------------------- #[test] fn domain_warp_is_applied_before_derivation() { // The warp should produce different columns at nearby integer tile // coordinates — the warp displaces the lookup point, so adjacent tiles // should not always look identical even when terrain is homogeneous. let district = alluvial_district(); let chunk = alluvial_chunk(&district); // Sample a grid of 4 neighbouring tiles; at least some should differ. let cols: Vec = (0..4) .map(|i| derive_voxel_column(42, "GJ1c", &district, &chunk, i, 0)) .collect(); let all_same = cols.windows(2).all(|w| w[0] == w[1]); assert!( !all_same, "adjacent tiles should differ after domain warp (elevation scatter)" ); } // ----------------------------------------------------------------------- // Family dispatch stubs — must not panic // ----------------------------------------------------------------------- // ----------------------------------------------------------------------- // T-1029 — LavaField // ----------------------------------------------------------------------- fn lava_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::Volcanic, tectonic_class: TectonicClass::Volcanic, vegetation_class: VegetationClass::Barren, ocean_fraction_q: 0, slope_q: 8, elev_q: 30, moisture_q: 20, ..alluvial_district() } } #[test] fn lava_field_terrain_is_lava() { let district = lava_district(); let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None); for pos in [(10, 10), (0, 0), (50, 25), (-5, 5)] { let col = derive_voxel_column(42, "Io", &district, &chunk, pos.0, pos.1); assert_eq!( col.terrain, TerrainMaterial::Lava, "LavaField must produce Lava terrain at {:?}", pos ); } } #[test] fn lava_field_vegetation_is_barren() { let district = lava_district(); let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None); for tx in 0..20i32 { let col = derive_voxel_column(42, "Io", &district, &chunk, tx, 10); assert_eq!( col.vegetation, Vegetation::Barren, "LavaField must be Barren at x={tx}" ); } } #[test] fn lava_field_no_deep_water() { // Immature drainage law (D-239 §8): no deep organised channels on lava. let district = lava_district(); let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None); for (tx, ty) in (0..50).map(|i| (i * 7, i * 3)) { let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty); assert_ne!( col.water, Water::Deep, "LavaField must have no Deep water (immature drainage)" ); } } #[test] fn lava_field_is_deterministic() { let district = lava_district(); let chunk = derive_chunk_context(7, "Io", &district, (5, 3), None); for (tx, ty) in [(0, 0), (10, 20), (-5, 7)] { let a = derive_voxel_column(7, "Io", &district, &chunk, tx, ty); let b = derive_voxel_column(7, "Io", &district, &chunk, tx, ty); assert_eq!(a, b, "LavaField must be deterministic at ({tx},{ty})"); } } #[test] fn lava_field_elevation_non_negative() { let district = lava_district(); let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "LavaField elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — FjordWall // ----------------------------------------------------------------------- fn fjord_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::Fjord, tectonic_class: TectonicClass::Active, glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2 slope_q: 60, elev_q: 80, ocean_fraction_q: 30, moisture_q: 70, vegetation_class: VegetationClass::Barren, ..alluvial_district() } } #[test] fn fjord_wall_terrain_is_rock() { let district = fjord_district(); let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, "FjordWall must produce Rock terrain at ({tx},{ty})" ); } } #[test] fn fjord_wall_centre_has_deep_water() { // The fjord trough (district-anchored centreline, T-1041) must have Deep // water. Sample a small window around the anchor — the domain warp can // displace any single tile off the 4–8 m floor. let district = fjord_district(); let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None); let anchor = chunk.channel_anchor_m; let any_deep = (anchor - 4..=anchor + 4).any(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).water == Water::Deep }); assert!( any_deep, "FjordWall centreline (anchor {anchor}) must have Deep water (the fjord inlet)" ); } #[test] fn fjord_wall_walls_are_high_relative_to_floor() { // Wall elevation must be substantially higher than fjord floor. let district = fjord_district(); let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None); let anchor = chunk.channel_anchor_m; // Floor: the lowest tile in the trough window around the anchor. let floor_elev = (anchor - 4..=anchor + 4) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).elevation_m }) .min() .unwrap(); // Wall: 30 m off the centreline (past the moraine band at ≤14 m). let (wx, wy) = cross_along_to_tile(&chunk, anchor - 30, 50); let wall_col = derive_voxel_column(42, "Fjordheim", &district, &chunk, wx, wy); assert!( wall_col.elevation_m > floor_elev, "FjordWall walls ({}) must be higher than the floor ({})", wall_col.elevation_m, floor_elev ); } #[test] fn fjord_wall_is_deterministic() { let district = fjord_district(); let chunk = derive_chunk_context(11, "Fjordheim", &district, (2, 3), None); for (tx, ty) in [(32, 50), (0, 50), (63, 20)] { let a = derive_voxel_column(11, "Fjordheim", &district, &chunk, tx, ty); let b = derive_voxel_column(11, "Fjordheim", &district, &chunk, tx, ty); assert_eq!(a, b, "FjordWall must be deterministic at ({tx},{ty})"); } } #[test] fn fjord_wall_elevation_non_negative() { let district = fjord_district(); let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "FjordWall elevation must be >= 0"); } } #[test] fn fjord_wall_floor_width_in_spec() { // D-239 §9: fjord floor (Deep water) must be 2–8 m wide across a cross-section. // With floor_half = glacier_grade.clamp(2,4) and grade ≥ 2 (upstream gate), // total Deep-water width = 2×floor_half + noise ∈ [4, 8] m. // Cross-section spans the district-anchored trough (T-1041). let district = fjord_district(); // glaciation_grade = Moderate (= 2) let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None); let anchor = chunk.channel_anchor_m; // Count Deep-water tiles across a 64-tile cross-section around the anchor. let deep_tiles = (anchor - 32..anchor + 32) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).water == Water::Deep }) .count(); assert!( (2..=8).contains(&deep_tiles), "FjordWall floor width {deep_tiles} m outside [2, 8] m spec (D-239 §9)" ); } // ----------------------------------------------------------------------- // T-1029 — CliffCoast // ----------------------------------------------------------------------- fn cliff_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::CliffCoast, tectonic_class: TectonicClass::Active, slope_q: 70, elev_q: 60, ocean_fraction_q: 40, moisture_q: 50, vegetation_class: VegetationClass::Barren, ..alluvial_district() } } #[test] fn cliff_coast_terrain_is_rock() { let district = cliff_district(); let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, "CliffCoast must produce Rock terrain at ({tx},{ty})" ); } } #[test] fn cliff_coast_steep_elevation_drop_near_water() { // Verify the CliffCoast structural property: there must be tiles with Deep // water AND there must be a significant elevation range (high ground + ocean). // The coast face sits on the district-anchored line `coast_anchor_m` along // the seaward (basin) axis (T-1041) — sample a transect across it. let district = cliff_district(); let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None); let coast = chunk.coast_anchor_m; // Sample 64 positions along the seaward axis, crossing the coast line. let cols: Vec = (coast - 32..coast + 32) .map(|a| { let (tx, ty) = cross_along_to_tile(&chunk, 50, a); derive_voxel_column(42, "Velen", &district, &chunk, tx, ty) }) .collect(); let has_deep = cols.iter().any(|c| c.water == Water::Deep); let max_elev = cols.iter().map(|c| c.elevation_m).max().unwrap(); let ocean_elev = cols .iter() .filter(|c| c.water == Water::Deep) .map(|c| c.elevation_m) .min() .unwrap_or(0); assert!( has_deep, "CliffCoast must have at least one tile with Deep water" ); assert!( max_elev > ocean_elev + 5, "CliffCoast: max elevation ({max_elev}) must be >> ocean elevation ({ocean_elev}) — vertical face law" ); } #[test] fn cliff_coast_is_deterministic() { let district = cliff_district(); let chunk = derive_chunk_context(5, "Velen", &district, (1, 2), None); for (tx, ty) in [(5, 50), (30, 20), (62, 50)] { let a = derive_voxel_column(5, "Velen", &district, &chunk, tx, ty); let b = derive_voxel_column(5, "Velen", &district, &chunk, tx, ty); assert_eq!(a, b, "CliffCoast must be deterministic at ({tx},{ty})"); } } #[test] fn cliff_coast_elevation_non_negative() { let district = cliff_district(); let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "CliffCoast elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — BraidedDelta // ----------------------------------------------------------------------- fn delta_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::Delta, slope_q: 2, elev_q: 4, ocean_fraction_q: 50, moisture_q: 80, vegetation_class: VegetationClass::Scrub, ..alluvial_district() } } #[test] fn braided_delta_terrain_is_gravel() { let district = delta_district(); let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Gravel, "BraidedDelta must produce Gravel terrain at ({tx},{ty})" ); } } #[test] fn braided_delta_low_elevation() { // Delta is near sea level — D-239 §8 drainage monotonicity: mouths at sea level. let district = delta_district(); let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5), (63, 0)] { let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert!( col.elevation_m <= 8, "BraidedDelta must be near sea level (<= 8 m), got {} at ({tx},{ty})", col.elevation_m ); } } #[test] fn braided_delta_has_multiple_shallow_channels() { // Gravel→braided (D-239 §8): NOT single-thread. Multiple channels means // a range of positions across the fan belt should contain some Shallow // water. The belt is centred on the district-anchored fan axis (T-1041). let district = delta_district(); let chunk = anchor_chunk(42, "delta_body", &district); let anchor = chunk.channel_anchor_m; // Sample 80 positions across the fan belt (anchor ± 40 covers the // 64 m belt plus thread width). let shallow_count = (anchor - 40..anchor + 40) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty).water == Water::Shallow }) .count(); // Three braided threads, each 3–8 m wide → expect at least 6 shallow tiles // in the belt transect. assert!( shallow_count >= 6, "BraidedDelta must have multiple braided channels; only {shallow_count} shallow in transect" ); } #[test] fn braided_delta_no_single_thread() { // BraidedDelta must NOT look like a single-thread meander — the channel // should appear in more than one spatial cluster across the cross-section. // We verify by checking that Shallow water appears in at least two // disjoint groups separated by Dry ground. let district = delta_district(); let chunk = anchor_chunk(42, "delta_body", &district); let anchor = chunk.channel_anchor_m; let waters: Vec = (anchor - 40..anchor + 40) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty).water }) .collect(); // Count Dry→Shallow transitions = number of channel entrances. let transitions = waters .windows(2) .filter(|w| w[0] == Water::Dry && w[1] == Water::Shallow) .count(); assert!( transitions >= 2, "BraidedDelta must have ≥2 braided channel threads; only {transitions} transitions" ); } #[test] fn braided_delta_is_deterministic() { let district = delta_district(); let chunk = derive_chunk_context(99, "delta_body", &district, (3, 1), None); for (tx, ty) in [(10, 10), (0, 100), (-5, 7)] { let a = derive_voxel_column(99, "delta_body", &district, &chunk, tx, ty); let b = derive_voxel_column(99, "delta_body", &district, &chunk, tx, ty); assert_eq!(a, b, "BraidedDelta must be deterministic at ({tx},{ty})"); } } #[test] fn braided_delta_elevation_non_negative() { let district = delta_district(); let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "BraidedDelta elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — DuneStrand // ----------------------------------------------------------------------- fn dune_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::DuneStrand, slope_q: 8, elev_q: 6, ocean_fraction_q: 30, moisture_q: 40, vegetation_class: VegetationClass::Barren, ..alluvial_district() } } #[test] fn dune_strand_terrain_is_sand() { let district = dune_district(); let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Sand, "DuneStrand must produce Sand terrain at ({tx},{ty})" ); } } #[test] fn dune_strand_respects_angle_of_repose() { // D-239 §8 Sand law: ≤~32° angle of repose. For a dune with period W, // the maximum allowed height H satisfies H ≤ tan(32°) × (W/2) ≈ 0.625 × W/2. // We verify by checking that elevation differences between adjacent tiles // don't exceed the physics cap. let district = dune_district(); let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None); // Sample a row of tiles along the dune wave direction. let elevations: Vec = (0..40i32) .map(|y| derive_voxel_column(42, "dune_body", &district, &chunk, 10, y).elevation_m) .collect(); // Max elevation in the sample. let max_elev = *elevations.iter().max().unwrap(); // Dune wavelength for slope_q=8: 20 - 8/2 = 16 m. Physics cap per wave = 0.625 × 8 = 5 m. // Allow a margin of 2 for scatter noise. let physics_cap = 8; // generous upper bound including noise assert!( max_elev <= physics_cap, "DuneStrand max elevation {} exceeds angle-of-repose physics cap {}", max_elev, physics_cap ); } #[test] fn dune_strand_mostly_dry() { // Dunes are mostly dry; only the strand toe may be Shallow. let district = dune_district(); let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None); let dry_or_shallow_count = (0..100i32) .filter(|&x| { let col = derive_voxel_column(42, "dune_body", &district, &chunk, x, 50); col.water != Water::Deep // no Deep water on dunes }) .count(); assert_eq!( dry_or_shallow_count, 100, "DuneStrand must have no Deep water" ); } #[test] fn dune_strand_is_deterministic() { let district = dune_district(); let chunk = derive_chunk_context(3, "dune_body", &district, (1, 0), None); for (tx, ty) in [(10, 10), (0, 30), (-5, 7)] { let a = derive_voxel_column(3, "dune_body", &district, &chunk, tx, ty); let b = derive_voxel_column(3, "dune_body", &district, &chunk, tx, ty); assert_eq!(a, b, "DuneStrand must be deterministic at ({tx},{ty})"); } } #[test] fn dune_strand_elevation_non_negative() { let district = dune_district(); let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "DuneStrand elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — IncisedGorge // ----------------------------------------------------------------------- fn gorge_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::MountainPass, tectonic_class: TectonicClass::Active, slope_q: 80, elev_q: 90, ocean_fraction_q: 5, moisture_q: 50, vegetation_class: VegetationClass::Barren, ..alluvial_district() } } #[test] fn incised_gorge_terrain_is_rock() { let district = gorge_district(); let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, "IncisedGorge must produce Rock terrain at ({tx},{ty})" ); } } #[test] fn incised_gorge_floor_width_in_spec() { // D-239 §9: gorge floors 2–8 m wide. We verify by checking that the // total width of the Shallow water zone (the incised channel on the floor) // across a cross-section is within [2, 8] m. Cross-section spans the // district-anchored gorge centreline (T-1041). let district = gorge_district(); let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None); let anchor = chunk.channel_anchor_m; // The floor's intrinsic width is the D-239 §9 quantity. A single warped // cross-section is noisy (the ±8 m domain warp can split the band at some // along-rows), so measure across several rows and take the widest — the // floor's true cross-section, independent of where the anchor falls. let floor_tiles = (60..160) .step_by(7) .map(|along| { (anchor - 32..anchor + 32) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, along); derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty).water == Water::Shallow }) .count() }) .max() .unwrap(); assert!( (2..=8).contains(&floor_tiles), "IncisedGorge floor width {floor_tiles} m outside [2, 8] m spec (D-239 §9)" ); } #[test] fn incised_gorge_walls_much_higher_than_floor() { // Walls must be substantially higher than the gorge floor. let district = gorge_district(); let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None); let anchor = chunk.channel_anchor_m; // Floor: the lowest tile in the trough window around the anchor // (the warp can displace any single tile off the 2–8 m floor). let floor_elev = (anchor - 4..=anchor + 4) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty).elevation_m }) .min() .unwrap(); // Wall: 30 m off the centreline. let (wx, wy) = cross_along_to_tile(&chunk, anchor + 30, 100); let wall_col = derive_voxel_column(42, "gorge_body", &district, &chunk, wx, wy); assert!( wall_col.elevation_m > floor_elev + 10, "IncisedGorge walls ({}) must be >> gorge floor ({})", wall_col.elevation_m, floor_elev ); } #[test] fn incised_gorge_is_deterministic() { let district = gorge_district(); let chunk = derive_chunk_context(17, "gorge_body", &district, (4, 2), None); for (tx, ty) in [(32, 100), (62, 100), (10, 50)] { let a = derive_voxel_column(17, "gorge_body", &district, &chunk, tx, ty); let b = derive_voxel_column(17, "gorge_body", &district, &chunk, tx, ty); assert_eq!(a, b, "IncisedGorge must be deterministic at ({tx},{ty})"); } } #[test] fn incised_gorge_elevation_non_negative() { let district = gorge_district(); let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "IncisedGorge elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — MeanderReach // ----------------------------------------------------------------------- fn meander_district() -> DistrictProfile { DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, slope_q: 3, elev_q: 15, ocean_fraction_q: 20, moisture_q: 70, vegetation_class: VegetationClass::Forest, ..alluvial_district() } } #[test] fn meander_reach_terrain_is_soil_or_wetland() { // MeanderReach terrain is Soil (or Wetland sub-zone on very flat/wet ground). // D-239 §8 Soil law: rolling/floodplain. Wetland is a valid sub-classification // for saturated floodplains (slope_q ≤ 3, moisture_q ≥ 70 in the meander_district). // This test checks that NO other material appears (no Rock, Sand, Gravel, Lava). let district = meander_district(); let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0), None); for (tx, ty) in [ (0, 50), (5, 50), (10, 50), (15, 50), (100, 50), (200, 50), (300, 50), (400, 50), ] { let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); assert!( matches!( col.terrain, TerrainMaterial::Soil | TerrainMaterial::Wetland ), "MeanderReach terrain must be Soil or Wetland, got {:?} at ({tx},{ty})", col.terrain ); } } #[test] fn meander_reach_non_wetland_region_produces_soil() { // A less-wet MeanderReach district (moisture_q < 70) should produce Soil, not Wetland. let district = DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, slope_q: 5, // above wetland slope threshold elev_q: 15, ocean_fraction_q: 20, moisture_q: 55, // below wetland moisture threshold vegetation_class: VegetationClass::Forest, ..alluvial_district() }; let chunk = derive_chunk_context(42, "meander_body2", &district, (0, 0), None); let mut soil_count = 0; for tx in [0, 5, 10, 15, 100, 200] { let col = derive_voxel_column(42, "meander_body2", &district, &chunk, tx, 50); if col.terrain == TerrainMaterial::Soil { soil_count += 1; } } assert!( soil_count >= 5, "Non-wetland MeanderReach must be mostly Soil, got {soil_count}/6" ); } #[test] fn meander_reach_has_active_channel() { // MeanderReach must have a channel with Shallow/Deep water somewhere in // a transect across the district-anchored channel band (T-1040). let district = meander_district(); let chunk = anchor_chunk(42, "meander_body", &district); // The anchor-covering chunk has an active channel (ocean_fraction_q=20 >= 10). assert!( chunk.has_active_channel, "MeanderReach anchor chunk must have active channel" ); let anchor = chunk.channel_anchor_m; // Sample a wide cross-section and find at least one wet tile. let wet_count = (anchor - 200..anchor + 200) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); col.water != Water::Dry }) .count(); assert!( wet_count >= 3, "MeanderReach must have a visible channel with wet tiles in cross-section; got {wet_count}" ); } #[test] fn meander_reach_channel_below_levee() { // D-239 §9 ElevationDelta: channel tiles must sit below off-channel // tiles in the vicinity. Per-voxel micro-relief is ±4 m, so a single // wet/dry tile pair is noise — compare the channel bottom (min wet) // against the floodplain/levee top (max dry) across the transect. let district = meander_district(); let chunk = anchor_chunk(42, "meander_body", &district); let anchor = chunk.channel_anchor_m; let mut min_wet: Option = None; let mut max_dry: Option = None; for c in anchor - 200..anchor + 200 { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); if col.water != Water::Dry { min_wet = Some(min_wet.map_or(col.elevation_m, |e| e.min(col.elevation_m))); } else { max_dry = Some(max_dry.map_or(col.elevation_m, |e| e.max(col.elevation_m))); } } // Both tile kinds must exist — a transect across the anchor that finds // no channel means the placement regressed (the law must be exercised). let (min_wet, max_dry) = ( min_wet.expect("transect across the anchor must contain channel tiles"), max_dry.expect("transect must contain floodplain tiles"), ); assert!( min_wet <= max_dry, "MeanderReach: channel bottom ({min_wet}) must be <= floodplain/levee top ({max_dry})" ); } #[test] fn meander_reach_stronger_sinuosity_than_alluvial() { // MeanderReach uses channel amplitude = wavelength/4 vs AlluvialPlain's // wavelength/6. Same district params, same seed, same body — the ONLY // difference is the morphology_zone driving the dispatch. Different // amplitudes shift the channel centreline position differently across the // chunk, so the set of wet-tile x-positions must differ. If MeanderReach // ever regresses to AlluvialPlain's amplitude (or falls back to the same // code path), the wet-tile sets become identical and this test fails. // // We use the SAME elev_q, slope_q, ocean_fraction_q, and seed for both — // so the only driver of the difference is the sinuosity amplitude. let shared_district_base = DistrictProfile { slope_q: 5, elev_q: 20, ocean_fraction_q: 20, // ensure has_active_channel = true moisture_q: 55, vegetation_class: VegetationClass::Forest, ..alluvial_district() }; let meander_reg = DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, ..shared_district_base.clone() }; let alluvial_reg = DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, ..shared_district_base }; // Same seed + body → same ChunkContext (same wavelength, phase, anchor, // channel_width). Use the anchor-covering chunk (T-1040 band gating). let meander_chunk = anchor_chunk(42, "sinuosity_body", &meander_reg); let alluvial_chunk = anchor_chunk(42, "sinuosity_body", &alluvial_reg); // Verify the chunks share the same meander params (confirming the test setup). assert_eq!( meander_chunk.meander_phase, alluvial_chunk.meander_phase, "test setup requires identical chunk params" ); assert_eq!( meander_chunk.channel_anchor_m, alluvial_chunk.channel_anchor_m, "test setup requires identical channel anchors" ); assert!( meander_chunk.has_active_channel && alluvial_chunk.has_active_channel, "both chunks must have active channels for the test to be meaningful" ); // Collect wet-tile cross-positions over a transect at along=50, centred // on the shared anchor. We scan far enough to capture the full meander // swing — amplitude for MeanderReach is wl/4, for AlluvialPlain wl/6, // so over a ±400 sweep both complete multiple full cycles and their // centreline positions diverge. let anchor = meander_chunk.channel_anchor_m; let meander_wet: Vec = (anchor - 400..anchor + 400) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&meander_chunk, c, 50); derive_voxel_column(42, "sinuosity_body", &meander_reg, &meander_chunk, tx, ty) .water != Water::Dry }) .collect(); let alluvial_wet: Vec = (anchor - 400..anchor + 400) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&alluvial_chunk, c, 50); derive_voxel_column(42, "sinuosity_body", &alluvial_reg, &alluvial_chunk, tx, ty) .water != Water::Dry }) .collect(); assert!( !meander_wet.is_empty(), "MeanderReach must have a wet channel" ); assert!( !alluvial_wet.is_empty(), "AlluvialPlain must have a wet channel" ); // The wet-tile position sets must differ — proving MeanderReach uses a // distinct sinuosity from AlluvialPlain. If this fails, the two generators // produce identical channel positions, meaning MeanderReach has regressed. assert_ne!( meander_wet, alluvial_wet, "MeanderReach and AlluvialPlain must produce different wet-tile positions \ (different channel amplitude = different sinuosity); they were identical, \ which means MeanderReach has regressed to AlluvialPlain amplitude" ); } #[test] fn meander_reach_is_deterministic() { let district = meander_district(); let chunk = derive_chunk_context(23, "meander_body", &district, (2, 5), None); for (tx, ty) in [(0, 50), (50, 200), (-20, 100)] { let a = derive_voxel_column(23, "meander_body", &district, &chunk, tx, ty); let b = derive_voxel_column(23, "meander_body", &district, &chunk, tx, ty); assert_eq!(a, b, "MeanderReach must be deterministic at ({tx},{ty})"); } } #[test] fn meander_reach_elevation_non_negative() { let district = meander_district(); let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0), None); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "MeanderReach elevation must be >= 0"); } } // ----------------------------------------------------------------------- // T-1029 — Cross-family: no panics, correct materials, all deterministic // ----------------------------------------------------------------------- /// All families (7 T-1029 landform + the T-1082 WaterBody zones) must not panic on /// any sample position, including extreme coordinates. #[test] fn all_families_no_panic_at_extreme_positions() { let zones = [ MorphologyZone::Volcanic, MorphologyZone::Fjord, MorphologyZone::CliffCoast, MorphologyZone::Delta, MorphologyZone::DuneStrand, MorphologyZone::MountainPass, MorphologyZone::MeanderReach, // T-1082 WaterBody zones. MorphologyZone::OpenOcean, MorphologyZone::Lake, MorphologyZone::TidalFlat, ]; let positions = [ (0, 0), (i32::MAX / 4, 0), (0, i32::MAX / 4), (-100, -100), (1000, 2000), (-500, 300), ]; for zone in &zones { let district = DistrictProfile { morphology_zone: *zone, tectonic_class: TectonicClass::Volcanic, glaciation_grade: GlaciationGrade::Moderate, slope_q: 50, elev_q: 40, ocean_fraction_q: 20, moisture_q: 50, ..alluvial_district() }; let chunk = derive_chunk_context(42, "stress_body", &district, (0, 0), None); for (tx, ty) in &positions { // Must not panic. let col = derive_voxel_column(42, "stress_body", &district, &chunk, *tx, *ty); assert!( col.elevation_m >= 0, "elevation must be non-negative for {:?} at ({tx},{ty})", zone ); } } } /// Each T-1029 family produces the expected TerrainMaterial (D-239 §8 lithology law). #[test] fn family_terrain_materials_match_lithology_law() { let cases: &[(MorphologyZone, TerrainMaterial)] = &[ (MorphologyZone::Volcanic, TerrainMaterial::Lava), (MorphologyZone::Fjord, TerrainMaterial::Rock), (MorphologyZone::CliffCoast, TerrainMaterial::Rock), (MorphologyZone::Delta, TerrainMaterial::Gravel), (MorphologyZone::DuneStrand, TerrainMaterial::Sand), (MorphologyZone::MountainPass, TerrainMaterial::Rock), ]; for (zone, expected_terrain) in cases { let district = DistrictProfile { morphology_zone: *zone, tectonic_class: TectonicClass::Volcanic, glaciation_grade: GlaciationGrade::Moderate, slope_q: 50, elev_q: 40, ocean_fraction_q: 5, moisture_q: 30, vegetation_class: VegetationClass::Barren, ..alluvial_district() }; let chunk = derive_chunk_context(42, "mat_check", &district, (0, 0), None); let col = derive_voxel_column(42, "mat_check", &district, &chunk, 10, 10); assert_eq!( col.terrain, *expected_terrain, "Zone {:?} must produce {:?} terrain", zone, expected_terrain ); } } // ----------------------------------------------------------------------- // T-1030 — SeasonalCover: discriminant pins and derive_cover tests // ----------------------------------------------------------------------- #[test] fn seasonal_cover_discriminants_pinned() { // Append-only invariant (D-010). assert_eq!(SeasonalCover::None as u8, 0); assert_eq!(SeasonalCover::Snow as u8, 1); assert_eq!(SeasonalCover::Ice as u8, 2); } // ── Helpers for derive_cover tests ────────────────────────────────────── /// Build a minimal VoxelColumn with a given Water state for cover testing. /// The cover field is irrelevant (derive_cover is called externally in tests). fn make_column(water: Water) -> VoxelColumn { VoxelColumn { terrain: TerrainMaterial::Soil, floor: FloorMaterial::None, vegetation: Vegetation::Grass, water, elevation_m: 5, cover: SeasonalCover::None, } } /// Build a DistrictProfile for cover tests with explicit temperature and moisture. fn cover_district(zone: MorphologyZone, temp_c: f32, moisture_q: i32) -> DistrictProfile { DistrictProfile { morphology_zone: zone, temperature_c: Some(temp_c), moisture_q, ..alluvial_district() } } // ── Airless body ───────────────────────────────────────────────────────── #[test] fn cover_airless_body_is_none() { // D-239 §2: no atmosphere → temperature_c == None → cover None always. let mut district = alluvial_district(); district.temperature_c = None; let col = make_column(Water::Dry); let cover = derive_cover(42, "test_body", &district, &col, (0, 0)); assert_eq!( cover, SeasonalCover::None, "airless body must always produce None cover" ); } #[test] fn cover_airless_body_even_with_water() { let mut district = cover_district(MorphologyZone::Lake, 15.0, 80); district.temperature_c = None; let col = make_column(Water::Deep); let cover = derive_cover(42, "test_body", &district, &col, (50, 50)); assert_eq!( cover, SeasonalCover::None, "airless + lake must still be None" ); } // ── Freshwater band edges ───────────────────────────────────────────────── #[test] fn cover_freshwater_above_band_is_none() { // Above +5 °C: always None (open water), regardless of scatter. let district = cover_district(MorphologyZone::Lake, 6.0, 60); let col = make_column(Water::Deep); // Sample many positions — all must be None above the band. for i in 0..50i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 7, i * 3)); assert_eq!( cover, SeasonalCover::None, "freshwater at temp +6°C must be None (above band) at pos ({},{})", i * 7, i * 3 ); } } #[test] fn cover_freshwater_below_band_is_all_ice() { // Below −10 °C: always Ice (no scatter). let district = cover_district(MorphologyZone::Lake, -11.0, 60); let col = make_column(Water::Deep); for i in 0..50i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 11, i * 5)); assert_eq!( cover, SeasonalCover::Ice, "freshwater at temp −11°C must be Ice (below band) at pos ({},{})", i * 11, i * 5 ); } } #[test] fn cover_freshwater_within_band_has_mix_of_ice_and_none() { // Within the scatter band [−10, +5]: should produce a mix of Ice and None // across different positions. Not all-Ice, not all-None. let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Deep); let mut ice_count = 0; let mut none_count = 0; for i in 0..200i32 { // Spread across a large area to span many cluster cells. let pos = (i * CLUSTER_M * 2, i * CLUSTER_M); let cover = derive_cover(42, "test_body", &district, &col, pos); match cover { SeasonalCover::Ice => ice_count += 1, SeasonalCover::None => none_count += 1, SeasonalCover::Snow => panic!("freshwater should not produce Snow"), } } assert!( ice_count > 0, "scatter band at −3°C must produce some Ice tiles; got 0 Ice in 200 samples" ); assert!( none_count > 0, "scatter band at −3°C must produce some None tiles; got 0 None in 200 samples" ); } // ── Sea ice (salt water) ───────────────────────────────────────────────── #[test] fn cover_salt_above_onset_is_none() { // Above −2 °C: no sea ice. let district = cover_district(MorphologyZone::OpenOcean, 0.0, 50); let col = make_column(Water::Deep); for i in 0..30i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 13, i * 7)); assert_eq!( cover, SeasonalCover::None, "salt water at 0°C must be None (above onset −2°C)" ); } } #[test] fn cover_salt_below_band_is_all_ice() { // Below −14 °C: permanent pack ice everywhere. let district = cover_district(MorphologyZone::OpenOcean, -15.0, 50); let col = make_column(Water::Deep); for i in 0..30i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 17, i * 9)); assert_eq!( cover, SeasonalCover::Ice, "salt water at −15°C must be Ice (below band)" ); } } #[test] fn cover_freshwater_not_triggered_for_salt_zone() { // OpenOcean should use the salt-water band (onset −2°C), NOT the // freshwater band (onset +5°C). At temp = +3°C, ocean must be None. let district = cover_district(MorphologyZone::OpenOcean, 3.0, 50); let col = make_column(Water::Deep); for i in 0..20i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 7, i * 5)); assert_eq!( cover, SeasonalCover::None, "ocean at +3°C must be None (salt onset is −2°C, not +5°C)" ); } } // ── Snow (land) — moisture gating ──────────────────────────────────────── #[test] fn cover_snow_cold_wet_land_produces_snow() { // Cold + wet land must produce Snow in the permanent zone (below −10°C). let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 60); let col = make_column(Water::Dry); for i in 0..30i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 9, i * 3)); assert_eq!( cover, SeasonalCover::Snow, "cold (−12°C) + moist land must produce Snow (permanent zone)" ); } } #[test] fn cover_snow_cold_dry_land_produces_none() { // Cold + dry (moisture_q < SNOW_MOISTURE_GATE) → bare frozen ground, not Snow. let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 20); let col = make_column(Water::Dry); for i in 0..30i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 9, i * 3)); assert_eq!( cover, SeasonalCover::None, "cold (−12°C) + dry land must produce None (bare frozen ground, not Snow)" ); } } #[test] fn cover_snow_warm_land_produces_none() { // Warm land (above snow band): no snow. let district = cover_district(MorphologyZone::AlluvialPlain, 10.0, 80); let col = make_column(Water::Dry); for i in 0..30i32 { let cover = derive_cover(42, "test_body", &district, &col, (i * 5, i * 2)); assert_eq!( cover, SeasonalCover::None, "warm (+10°C) land must produce None even with high moisture" ); } } #[test] fn cover_snow_within_band_has_mix() { // Within the snow scatter band (e.g. −5°C) + moist: mix of Snow and None. let district = cover_district(MorphologyZone::AlluvialPlain, -5.0, 60); let col = make_column(Water::Dry); let mut snow_count = 0; let mut none_count = 0; for i in 0..200i32 { let pos = (i * CLUSTER_M * 2, i * CLUSTER_M); let cover = derive_cover(42, "test_body", &district, &col, pos); match cover { SeasonalCover::Snow => snow_count += 1, SeasonalCover::None => none_count += 1, SeasonalCover::Ice => panic!("land should not produce Ice"), } } assert!( snow_count > 0, "scatter band at −5°C moist land must produce some Snow; got 0 Snow" ); assert!( none_count > 0, "scatter band at −5°C moist land must produce some None; got 0 None" ); } // ── Land water-state: Dry riverbank should be land, not freshwater ──────── #[test] fn cover_dry_riverbank_is_land_not_freshwater() { // A RiverBank tile that is Water::Dry should be treated as land (snow), // not freshwater (ice). D-239 §3: fresh water only when the voxel is wet. let district = cover_district(MorphologyZone::RiverBank, -12.0, 60); let dry_col = make_column(Water::Dry); let wet_col = make_column(Water::Shallow); let dry_cover = derive_cover(42, "test_body", &district, &dry_col, (0, 0)); let wet_cover = derive_cover(42, "test_body", &district, &wet_col, (0, 0)); assert_eq!( dry_cover, SeasonalCover::Snow, "dry RiverBank at −12°C + moist must be Snow (land branch)" ); assert_eq!( wet_cover, SeasonalCover::Ice, "wet RiverBank at −12°C must be Ice (freshwater branch)" ); } // ── Spatial coherence ──────────────────────────────────────────────────── /// Coherence test: exercises the REAL `derive_cover` production signature /// (with `world_seed` + `body_id` — the fixed API) to catch the defect from /// PR #163. /// /// The defect: in the broken code, `cluster_scatter` received `sub_chunk_seed` /// (a per-voxel value), so every voxel had a distinct scatter hash → salt-and- /// pepper noise, NOT clustered patches. This test would FAIL against that code /// because intra-cluster agreement would be ~50% (random coin flip) instead of /// 100%. It PASSES only with the fix: `cluster_scatter` uses /// `SeedChain::for_body(world_seed, body_id).derive(Cover, cluster_cell_id)`, /// keyed on (world, body, cluster-cell) so every voxel in the same cell gets /// the same hash. /// /// ## Why we test `derive_cover` directly rather than via `derive_voxel_column` /// /// `derive_voxel_column` applies domain warp (±8 m) before computing the voxel /// address. Two logically-adjacent tiles (1 m apart) may warp to positions that /// fall in *different* 6 m cluster cells — which is correct behavior, not a /// coherence failure. Testing coherence at the `derive_voxel_column` level would /// require knowing the post-warp cluster-cell boundaries, making the test /// fragile. Testing `derive_cover` directly with positions that are explicitly /// within the same cluster cell is the clean contract-level test. #[test] fn cover_cluster_coherence_not_per_tile() { // Use a cold lake district so cover is in the scatter band (mix of Ice/None) // — solid-frozen would pass trivially regardless of coherence. let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Shallow); let world_seed: u64 = 42; let body_id = "coherence_body"; let mut cluster_agreements = 0; let mut cluster_total = 0; // Walk a grid of cluster cells. For each cell, sample 4 voxels that are // explicitly inside that cell: offsets (0,0), (1,0), (0,1), (CLUSTER_M-1, CLUSTER_M-1). // All 4 must produce identical cover — same cluster-cell → same SeedChain hash. for cx in -10..10i32 { for cy in -10..10i32 { let base_x = cx * CLUSTER_M; let base_y = cy * CLUSTER_M; let offsets = [(0, 0), (1, 0), (0, 1), (CLUSTER_M - 1, CLUSTER_M - 1)]; let covers: Vec = offsets .iter() .map(|&(dx, dy)| { derive_cover( world_seed, body_id, &district, &col, (base_x + dx, base_y + dy), ) }) .collect(); // All 4 must agree. if covers.windows(2).all(|w| w[0] == w[1]) { cluster_agreements += 1; } cluster_total += 1; } } assert_eq!( cluster_agreements, cluster_total, "ALL voxels within the same cluster cell must agree (same cluster-cell hash); \ {}/{} cells disagreed — this means cluster_scatter is NOT keyed solely on the \ cluster-cell coordinates (salt-and-pepper bug)", cluster_total - cluster_agreements, cluster_total ); // Also verify that cover varies across different cluster cells in the scatter // band — confirming the hash is not degenerate (not all-Ice or all-None). let cell_covers: Vec = (-25..25i32) .map(|cx| derive_cover(world_seed, body_id, &district, &col, (cx * CLUSTER_M, 0))) .collect(); let has_ice = cell_covers.contains(&SeasonalCover::Ice); let has_none = cell_covers.contains(&SeasonalCover::None); assert!( has_ice && has_none, "cover must vary across cluster cells (both Ice and None expected in scatter \ band −3°C); got has_ice={has_ice} has_none={has_none} — hash may be degenerate" ); } // ── Determinism ───────────────────────────────────────────────────────── #[test] fn cover_derivation_is_deterministic() { // Same inputs → same cover, always. let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Shallow); for i in 0..50i32 { let pos = (i * CLUSTER_M, i * 3); let a = derive_cover(42, "test_body", &district, &col, pos); let b = derive_cover(42, "test_body", &district, &col, pos); assert_eq!( a, b, "derive_cover must be deterministic at pos ({},{})", i * CLUSTER_M, i * 3 ); } } #[test] fn cover_end_to_end_via_derive_voxel_column_is_deterministic() { // Cover must be deterministic through the full derive_voxel_column path. let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 60); let chunk = derive_chunk_context(42, "cold_body", &district, (0, 0), None); for (tx, ty) in [(0, 0), (50, 100), (-20, 30), (200, -10)] { let a = derive_voxel_column(42, "cold_body", &district, &chunk, tx, ty); let b = derive_voxel_column(42, "cold_body", &district, &chunk, tx, ty); assert_eq!( a.cover, b.cover, "cover must be deterministic at ({tx},{ty})" ); } } }