feat(simulation): district moisture spatial gradient (T-1080)
moisture_q was a single body constant (derive_moisture_q took only &BodyParams → 80 for every one of 2048 ocean-world districts), so vegetation/terrain/ecotones were uniform — "nothing to fuzz" (the T-1080 believability bug). - district_profile.rs: derive_moisture_q is now a body ceiling (hydrosphere+atmosphere) × per-district gradient — latitude (equator wet → pole dry), elevation (orographic/ rain-shadow), continentality (100 − ocean_fraction_q). Integer/D-010, no new plumbing (elev_q/ocean_fraction_q/latitude_deg already reach build_district_profile). Mirrors D-240's temperature model. Coefficients in ClimateConstants + climate_constants.toml [moisture_gradient] (provisional; Q-123 calibrates). Tuned so a wet body stays mostly green with drier patches and a frozen body clamps mostly barren. + gradient unit test. - region_profile.rs: region-tier moisture gets the region-latitude gradient (elevation/ continentality are district-scale). - Propagates to precipitation_class / vegetation_class / morphology_zone (all read moisture_q). D-239 §2 amended. Believability golden regenerated. Probe (Arbour @ yolo): moisture_q distinct 1 → 49 (spread 0 → 48); 5/7 → 6/7 criteria. Edict (frozen): moisture 0..20 ×21 — gradient within its low ceiling. Remaining fail (vegetation-present) is the metric dividing by all-sampled incl. water on a 2/3-ocean world — a Q-123 calibration concern, not a climate defect. clippy --all-targets -D warnings clean; 1580 lib tests + harnesses pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2717,3 +2717,25 @@ Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to d
|
||||
- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105.
|
||||
- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families.
|
||||
- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', NULL, '2026-06-28 13:10:56', '2026-06-28 13:10:56.408', '2026-06-28 13:10:56.408', NULL, 'f9737eab39f048ff84b8478ca11f5215', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJTXTADJRHYEE1WTAENYAG', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 13:42:27', '2026-06-28 13:42:27.248', '2026-06-28 13:42:27.248', NULL, '556cdb48b70c650c73c13cff9781817e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJTXTADJRHYEE1WTAENYAG', 'description', 'Probe evidence (Arbour @ yolo): moisture_q=80, precip=SuperHumid, veg_class=Forest in ~every one of 2048 districts; the voxel layer then derives terrain=Wetland 100% everywhere. The derivation is LAW-CORRECT (flat + wet -> Wetland, D-239 section 8) — the defect is upstream: the district-scale climate/moisture field has near-zero cross-district variance, so there is no gradient to drive ecotones, material variety, or vegetation bands. Without gradient the ''edge-fuzzed climate'' promise is invisible (nothing to fuzz). Direction to discuss: where moisture/precip SHOULD vary (latitude, orography/rain-shadow, continentality, distance-to-water) and how to introduce believable spatial gradient into the district derivation. Believability gap, not a crash.', 'Probe evidence (Arbour @ yolo): moisture_q=80, precip=SuperHumid, veg_class=Forest in ~every one of 2048 districts; the voxel layer then derives terrain=Wetland 100% everywhere. The derivation is LAW-CORRECT (flat + wet -> Wetland, D-239 section 8) — the defect is upstream: the district-scale climate/moisture field has near-zero cross-district variance, so there is no gradient to drive ecotones, material variety, or vegetation bands. Without gradient the ''edge-fuzzed climate'' promise is invisible (nothing to fuzz). Direction to discuss: where moisture/precip SHOULD vary (latitude, orography/rain-shadow, continentality, distance-to-water) and how to introduce believable spatial gradient into the district derivation. Believability gap, not a crash.
|
||||
|
||||
---
|
||||
Refinement (Si pass, 2026-06-28) — READY (no design fork; localized fix).
|
||||
|
||||
ROOT CAUSE: derive_moisture_q (district_profile.rs) takes only &BodyParams → returns a body-scale constant (ocean → 80) identical for all districts. Structurally cannot vary.
|
||||
|
||||
FIX: restructure derive_moisture_q to body-ceiling (from hydrosphere) modulated by per-district factors, mirroring derive_temperature_c''s shape (envelope → latitude lerp → elevation lapse → seed nudge), integer/D-010:
|
||||
- LATITUDE band: equator wetter → pole drier (uses latitude_deg, already threaded for temperature).
|
||||
- ELEVATION lapse: high elev_q → drier (orographic/rain-shadow).
|
||||
- CONTINENTALITY: far from water → drier, via TerrainAnalysis.water_dist (Chebyshev cells-to-water, already computed in features.rs; aggregate a per-district mean in the existing derive_district_profile cell loop).
|
||||
- small deterministic seed nudge (±N) for per-district variety.
|
||||
Signature: derive_moisture_q(params, elev_q, ocean_fraction_q, water_dist_q). build_district_profile forwards them; derive_district_profile accumulates water_dist.
|
||||
|
||||
PROPAGATION: precipitation_class (derive_precipitation_class_from_climate) + vegetation_class (derive_vegetation) + morphology Wetland gate (slope_q<=5 && moisture_q>=60) all read moisture_q → diversify automatically.
|
||||
|
||||
TUNING: add [moisture_gradient] to server/data/climate_constants.toml + matching ClimateConstants struct defaults (kept in sync by hand until T-1032 wires TOML loading, per the existing constraint).
|
||||
|
||||
DETERMINISM: integer (water_dist u16, integer division, clamp 0..100). D-010 ok.
|
||||
|
||||
VERIFY: believability harness gate moisture_q.distinct>=3 flips FAIL→PASS; vegetation-present should rise too. Magnitude calibration is Q-123 (provisional coefficients now). Files: district_profile.rs, climate_constants.toml.', NULL, '2026-06-28 13:50:47', '2026-06-28 13:50:47.912', '2026-06-28 13:50:47.912', NULL, '47bb1324b99af91b19f50ee8f8d3825a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -3650,3 +3650,25 @@ Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to d
|
||||
- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105.
|
||||
- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families.
|
||||
- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:10:56.408', NULL, '8f55fc31d54598d0756308b8e6ef645c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJTXTADJRHYEE1WTAENYAG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'District climate fields lack spatial gradient — uniform moisture/precip/vegetation yields monotone terrain', 'Probe evidence (Arbour @ yolo): moisture_q=80, precip=SuperHumid, veg_class=Forest in ~every one of 2048 districts; the voxel layer then derives terrain=Wetland 100% everywhere. The derivation is LAW-CORRECT (flat + wet -> Wetland, D-239 section 8) — the defect is upstream: the district-scale climate/moisture field has near-zero cross-district variance, so there is no gradient to drive ecotones, material variety, or vegetation bands. Without gradient the ''edge-fuzzed climate'' promise is invisible (nothing to fuzz). Direction to discuss: where moisture/precip SHOULD vary (latitude, orography/rain-shadow, continentality, distance-to-water) and how to introduce believable spatial gradient into the district derivation. Believability gap, not a crash.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:42.258', '2026-06-28 13:42:27.248', NULL, '8daa8d5b388dbc0aa004da1f0abd970f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJTXTADJRHYEE1WTAENYAG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'District climate fields lack spatial gradient — uniform moisture/precip/vegetation yields monotone terrain', 'Probe evidence (Arbour @ yolo): moisture_q=80, precip=SuperHumid, veg_class=Forest in ~every one of 2048 districts; the voxel layer then derives terrain=Wetland 100% everywhere. The derivation is LAW-CORRECT (flat + wet -> Wetland, D-239 section 8) — the defect is upstream: the district-scale climate/moisture field has near-zero cross-district variance, so there is no gradient to drive ecotones, material variety, or vegetation bands. Without gradient the ''edge-fuzzed climate'' promise is invisible (nothing to fuzz). Direction to discuss: where moisture/precip SHOULD vary (latitude, orography/rain-shadow, continentality, distance-to-water) and how to introduce believable spatial gradient into the district derivation. Believability gap, not a crash.
|
||||
|
||||
---
|
||||
Refinement (Si pass, 2026-06-28) — READY (no design fork; localized fix).
|
||||
|
||||
ROOT CAUSE: derive_moisture_q (district_profile.rs) takes only &BodyParams → returns a body-scale constant (ocean → 80) identical for all districts. Structurally cannot vary.
|
||||
|
||||
FIX: restructure derive_moisture_q to body-ceiling (from hydrosphere) modulated by per-district factors, mirroring derive_temperature_c''s shape (envelope → latitude lerp → elevation lapse → seed nudge), integer/D-010:
|
||||
- LATITUDE band: equator wetter → pole drier (uses latitude_deg, already threaded for temperature).
|
||||
- ELEVATION lapse: high elev_q → drier (orographic/rain-shadow).
|
||||
- CONTINENTALITY: far from water → drier, via TerrainAnalysis.water_dist (Chebyshev cells-to-water, already computed in features.rs; aggregate a per-district mean in the existing derive_district_profile cell loop).
|
||||
- small deterministic seed nudge (±N) for per-district variety.
|
||||
Signature: derive_moisture_q(params, elev_q, ocean_fraction_q, water_dist_q). build_district_profile forwards them; derive_district_profile accumulates water_dist.
|
||||
|
||||
PROPAGATION: precipitation_class (derive_precipitation_class_from_climate) + vegetation_class (derive_vegetation) + morphology Wetland gate (slope_q<=5 && moisture_q>=60) all read moisture_q → diversify automatically.
|
||||
|
||||
TUNING: add [moisture_gradient] to server/data/climate_constants.toml + matching ClimateConstants struct defaults (kept in sync by hand until T-1032 wires TOML loading, per the existing constraint).
|
||||
|
||||
DETERMINISM: integer (water_dist u16, integer division, clamp 0..100). D-010 ok.
|
||||
|
||||
VERIFY: believability harness gate moisture_q.distinct>=3 flips FAIL→PASS; vegetation-present should rise too. Magnitude calibration is Q-123 (provisional coefficients now). Files: district_profile.rs, climate_constants.toml.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:42.258', '2026-06-28 13:50:47.912', NULL, '4f5ee94f5fd8f5e447968619a520f915', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -1777,6 +1777,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Implementation note (T-1024, 2026-06-07):** §1 per-body `RIVER_THRESHOLD` is now a derived field (`derive_river_threshold`) on `RegionProfile`. §2 district temperature is a nullable `f32` on `RegionProfile` sampled from a per-region climate derivation (`derive_temperature_c`); moisture is a separate integer primitive (`derive_moisture_q`, 0–100). Both populate the `RegionProfile` carrier during the L4 cascade run. Climate inputs use a **hybrid strategy**: stellar luminosity and spectral class are sourced from the import pipeline (`bodies.axial_tilt_deg`, `star_systems.spectral_class`); greenhouse offsets and diurnal amplitudes are tunable at runtime via `server/data/climate_constants.toml` (source-canonical TOML, not hardcoded).
|
||||
- **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025).
|
||||
- **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch.
|
||||
- **Amendment (T-1080, 2026-06-28):** §2's **moisture** primitive is now a **body ceiling × per-district spatial gradient**, not a single body constant. The hydrosphere+atmosphere value (`derive_moisture_q`) is the *wettest* a district on the body can be; per-district **latitude** (equator wet → pole dry), **elevation** (orographic / rain-shadow), and **continentality** (`100 − ocean_fraction_q`, coast wet → interior dry) subtract from it. Integer (D-010); coefficients in `ClimateConstants` / `climate_constants.toml` `[moisture_gradient]` (provisional, Q-123 calibrates). This mirrors D-240's latitude-graded *temperature* model and fixes the T-1080 believability bug (`moisture_q = 80` for all 2 048 districts → uniform vegetation/terrain — "nothing to fuzz"). It **propagates**: `precipitation_class`, `vegetation_class`, and the `morphology_zone` Wetland gate all read `moisture_q`, so those diversify for free. Lives in `district_profile.rs::derive_moisture_q`; verified by the T-1083 believability harness (moisture-gradient flips PASS, `distinct` 1 → ~49 on Arbour). The body-scale ceiling keeps each world's character; the gradient varies it within.
|
||||
- **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract)
|
||||
|
||||
---
|
||||
|
||||
@@ -84,3 +84,16 @@ ice = 0.85 # frozen surface — slight moderation
|
||||
subsurface_liquid = 0.9
|
||||
subsurface = 0.95 # buried water — minimal surface effect
|
||||
subsurface_ice = 0.95
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
[moisture_gradient]
|
||||
# Per-district moisture spatial gradient (T-1080, D-239 §2). Integer points
|
||||
# subtracted from the body's hydrosphere moisture *ceiling* so the field varies
|
||||
# across the body instead of being a single constant (the bug: moisture_q = 80 for
|
||||
# every district -> uniform vegetation/terrain). Wet at coast/lowland/tropics, drier
|
||||
# toward interior/highland/poles. Provisional magnitudes — Q-123 calibrates. Mirrors
|
||||
# the embedded ClimateConstants::default(); not yet loaded at runtime (TOML wiring is
|
||||
# T-1032), kept in sync by hand like the other tables.
|
||||
lat_penalty = 20 # lost equator->pole (x |latitude|/90)
|
||||
elev_penalty = 15 # lost low->high elevation (x elev_q/100, rain-shadow)
|
||||
continental_penalty = 20 # lost coast->interior (x (100 - ocean_fraction_q)/100)
|
||||
|
||||
@@ -668,6 +668,20 @@ pub struct ClimateConstants {
|
||||
/// band); `< 1.0` compresses toward the band midpoint (water-rich worlds are
|
||||
/// milder at both ends). Absent/unknown = `1.0`.
|
||||
pub hydrosphere_maritime: std::collections::BTreeMap<String, f32>,
|
||||
|
||||
// ── Moisture gradient (T-1080, D-239 §2) ─────────────────────────────────
|
||||
// Per-district moisture spatial-gradient penalties — integer points subtracted
|
||||
// from the body's hydrosphere moisture *ceiling*. A living world is wet at the
|
||||
// coast / lowland / tropics and drier toward the interior / highland / poles;
|
||||
// without this gradient the climate field is a single body-constant (T-1080).
|
||||
// Provisional magnitudes — Q-123 calibrates. Mirror `[moisture_gradient]` in
|
||||
// `climate_constants.toml`.
|
||||
/// Moisture lost equator→pole (× |latitude|/90).
|
||||
pub moisture_lat_penalty: i32,
|
||||
/// Moisture lost low→high elevation (× elev_q/100) — orographic / rain-shadow.
|
||||
pub moisture_elev_penalty: i32,
|
||||
/// Moisture lost coast→interior (× (100 − ocean_fraction_q)/100) — continentality.
|
||||
pub moisture_continental_penalty: i32,
|
||||
}
|
||||
|
||||
impl Default for ClimateConstants {
|
||||
@@ -727,6 +741,13 @@ impl Default for ClimateConstants {
|
||||
greenhouse_offset_c: gh,
|
||||
diurnal_amplitude_c: da,
|
||||
hydrosphere_maritime: hm,
|
||||
// Moisture gradient (T-1080) — provisional; Q-123 calibrates. Tuned so a
|
||||
// wet body (high ceiling) stays mostly vegetated with drier patches rather
|
||||
// than cratering to near-desert; a dry/frozen body (low ceiling) still
|
||||
// clamps mostly barren. Total max penalty 55 < an ocean ceiling of ~80.
|
||||
moisture_lat_penalty: 20,
|
||||
moisture_elev_penalty: 15,
|
||||
moisture_continental_penalty: 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -942,17 +963,32 @@ pub fn derive_district_temperature_c(
|
||||
Some(t_lapse.clamp(cold, warm))
|
||||
}
|
||||
|
||||
/// Derive moisture primitive (0–100 integer).
|
||||
/// Derive the district moisture primitive (0–100 integer; 0 = arid, 100 = saturated).
|
||||
///
|
||||
/// D-239 §2: moisture is derived from hydrosphere + atmosphere; 0 = arid, 100 = saturated.
|
||||
/// Integer output (D-010).
|
||||
/// D-239 §2 / T-1080: moisture is a **body ceiling × per-district spatial gradient**.
|
||||
/// The body ceiling comes from `hydrosphere` + `atmosphere` (an ocean world is wetter
|
||||
/// on average); the gradient then varies it across the body so the climate field is
|
||||
/// not a single constant (the T-1080 bug: `moisture_q = 80` for all 2048 districts,
|
||||
/// which left vegetation/terrain/ecotones uniform). A living world is wetter at the
|
||||
/// **coast / lowland / tropics** and drier toward the **interior / highland / poles**:
|
||||
/// - **latitude** — equator wet → pole dry (`params.latitude_deg`, set per-district).
|
||||
/// - **elevation** — high ground drier (orographic / rain-shadow, `elev_q`).
|
||||
/// - **continentality** — interior drier than coast (`100 − ocean_fraction_q`).
|
||||
///
|
||||
/// Penalties are subtractive points from the ceiling, magnitudes tuned in
|
||||
/// [`ClimateConstants`] (`moisture_*_penalty`, provisional pending Q-123). Integer
|
||||
/// arithmetic throughout (D-010); the only float is the deterministic `latitude_deg`
|
||||
/// truncation at the decision boundary, mirroring the `slope_q`/`elev_q` aggregation.
|
||||
///
|
||||
/// The `hydro` arms use the **actual `bodies.hydrosphere` vocabulary in systems.db**
|
||||
/// — same canonical set the `[hydrosphere_maritime]` table (D-240) keys on — grouped
|
||||
/// by available surface moisture. Keying on a vocab the data doesn't use (e.g. only
|
||||
/// `"ocean"`, which is 19 bodies, while 175 use `"liquid_water"`) silently dropped most
|
||||
/// water worlds to the default (T-1034 — same root cause as D-240).
|
||||
pub fn derive_moisture_q(params: &BodyParams) -> i32 {
|
||||
/// — same set the `[hydrosphere_maritime]` table (D-240) keys on — grouped by available
|
||||
/// surface moisture (T-1034).
|
||||
pub fn derive_moisture_q(
|
||||
params: &BodyParams,
|
||||
elev_q: i32,
|
||||
ocean_fraction_q: i32,
|
||||
climate: &ClimateConstants,
|
||||
) -> i32 {
|
||||
let hydro = params.hydrosphere.as_deref().unwrap_or("none");
|
||||
let atmo = params.atmosphere.as_deref().unwrap_or("none");
|
||||
|
||||
@@ -980,7 +1016,20 @@ pub fn derive_moisture_q(params: &BodyParams) -> i32 {
|
||||
"dense" => 15,
|
||||
_ => 0,
|
||||
};
|
||||
(base + atmo_boost).clamp(0, 100)
|
||||
// Body moisture ceiling — the wettest a district on this body can be.
|
||||
let ceiling = (base + atmo_boost).clamp(0, 100);
|
||||
|
||||
// ── Per-district spatial gradient (T-1080) ────────────────────────────────
|
||||
// Latitude: equator (0) wet → pole (90) dry. `latitude_deg` is per-district.
|
||||
let lat_q = (params.latitude_deg.abs() as i32).clamp(0, 90) * 100 / 90; // 0..100
|
||||
let lat_penalty = lat_q * climate.moisture_lat_penalty / 100;
|
||||
// Elevation: high ground is drier (orographic uplift / rain-shadow / less retention).
|
||||
let elev_penalty = elev_q.clamp(0, 100) * climate.moisture_elev_penalty / 100;
|
||||
// Continentality: interior (low ocean fraction) is drier than coast / open water.
|
||||
let interiorness = (100 - ocean_fraction_q.clamp(0, 100)).max(0);
|
||||
let cont_penalty = interiorness * climate.moisture_continental_penalty / 100;
|
||||
|
||||
(ceiling - lat_penalty - elev_penalty - cont_penalty).clamp(0, 100)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1142,7 +1191,7 @@ fn build_district_profile(
|
||||
derive_temperature_c(&district_climate_params, climate, body_seed)
|
||||
}
|
||||
};
|
||||
let moisture_q = derive_moisture_q(body_params);
|
||||
let moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate);
|
||||
|
||||
// Climate-derived fields: computed from temperature + moisture primitives
|
||||
// (D-239 §2). This is the correct call order — temperature must be resolved
|
||||
@@ -2036,7 +2085,7 @@ mod tests {
|
||||
atmosphere: Some("breathable".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let q = derive_moisture_q(¶ms);
|
||||
let q = derive_moisture_q(¶ms, 0, 100, &ClimateConstants::default());
|
||||
assert!(q >= 70, "ocean + breathable moisture {q} should be >= 70");
|
||||
}
|
||||
|
||||
@@ -2050,7 +2099,7 @@ mod tests {
|
||||
atmosphere: Some("standard".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let q = derive_moisture_q(¶ms);
|
||||
let q = derive_moisture_q(¶ms, 0, 100, &ClimateConstants::default());
|
||||
assert!(
|
||||
q >= 70,
|
||||
"liquid_water moisture {q} should be >= 70 (surface-liquid band), not the default 30"
|
||||
@@ -2064,7 +2113,7 @@ mod tests {
|
||||
atmosphere: Some("none".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let q = derive_moisture_q(¶ms);
|
||||
let q = derive_moisture_q(¶ms, 0, 100, &ClimateConstants::default());
|
||||
assert_eq!(q, 0, "airless no-hydrosphere body moisture must be 0");
|
||||
}
|
||||
|
||||
@@ -2105,7 +2154,7 @@ mod tests {
|
||||
atmosphere: Some(a.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let q = derive_moisture_q(¶ms);
|
||||
let q = derive_moisture_q(¶ms, 0, 100, &ClimateConstants::default());
|
||||
assert!(
|
||||
(0..=100).contains(&q),
|
||||
"moisture_q {q} out of range for hydro={h} atmo={a}"
|
||||
@@ -2114,6 +2163,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moisture_q_has_spatial_gradient() {
|
||||
// T-1080: moisture must vary across the body, not be a single body constant.
|
||||
let climate = ClimateConstants::default();
|
||||
let ocean = |lat: f64| BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
latitude_deg: lat,
|
||||
..Default::default()
|
||||
};
|
||||
// Wettest: equatorial coastal lowland. Driest: polar interior highland.
|
||||
let wet = derive_moisture_q(&ocean(0.0), 0, 100, &climate);
|
||||
let dry = derive_moisture_q(&ocean(90.0), 90, 0, &climate);
|
||||
assert!(
|
||||
wet > dry,
|
||||
"equatorial coast ({wet}) must be wetter than polar interior ({dry})"
|
||||
);
|
||||
assert!(
|
||||
wet - dry >= 40,
|
||||
"moisture gradient ({} pts) should be substantial",
|
||||
wet - dry
|
||||
);
|
||||
// Each axis independently lowers moisture from the wet corner.
|
||||
assert!(
|
||||
derive_moisture_q(&ocean(90.0), 0, 100, &climate) < wet,
|
||||
"latitude lowers moisture"
|
||||
);
|
||||
assert!(
|
||||
derive_moisture_q(&ocean(0.0), 90, 100, &climate) < wet,
|
||||
"elevation lowers moisture"
|
||||
);
|
||||
assert!(
|
||||
derive_moisture_q(&ocean(0.0), 0, 0, &climate) < wet,
|
||||
"continentality lowers moisture"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn climate_constants_envelope_direct_lookup() {
|
||||
// Direct lookup returns the exact registered band.
|
||||
|
||||
@@ -304,8 +304,12 @@ pub fn build_region_profile(
|
||||
let body_seed = seed.seed();
|
||||
let baseline_temp_c = derive_region_baseline_c(®ion_params, constants, body_seed);
|
||||
|
||||
// Moisture at the region tier is body-scale (same as body_params).
|
||||
let moisture_q = crate::atlas::district_profile::derive_moisture_q(body_params);
|
||||
// Region-tier moisture = body ceiling × region-latitude gradient (T-1080). The
|
||||
// elevation + continentality components are district-scale, so at the region tier
|
||||
// elevation is sea level (0) and continentality is neutral (ocean_fraction 100);
|
||||
// the district derivation adds those on top.
|
||||
let moisture_q =
|
||||
crate::atlas::district_profile::derive_moisture_q(®ion_params, 0, 100, constants);
|
||||
|
||||
RegionProfile {
|
||||
pos: region_pos,
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"voxel_sampled_districts": 64,
|
||||
"contrast": {
|
||||
"moisture_q": {
|
||||
"min": 80,
|
||||
"min": 32,
|
||||
"max": 80,
|
||||
"distinct": 1
|
||||
"distinct": 49
|
||||
},
|
||||
"elev_q": {
|
||||
"min": 0,
|
||||
@@ -27,13 +27,13 @@
|
||||
},
|
||||
"morphology_zones": 9,
|
||||
"vegetation_classes": 3,
|
||||
"terrain_materials": 4
|
||||
"terrain_materials": 3
|
||||
},
|
||||
"coherence": {
|
||||
"water_districts": 46,
|
||||
"water_districts_wet": 43,
|
||||
"water_districts_wet": 44,
|
||||
"drainage_samples": 10,
|
||||
"drainage_monotonic": 8,
|
||||
"drainage_monotonic": 9,
|
||||
"vegetation_samples": 64,
|
||||
"vegetated_districts": 11
|
||||
}
|
||||
@@ -45,9 +45,9 @@
|
||||
"voxel_sampled_districts": 64,
|
||||
"contrast": {
|
||||
"moisture_q": {
|
||||
"min": 20,
|
||||
"min": 0,
|
||||
"max": 20,
|
||||
"distinct": 1
|
||||
"distinct": 21
|
||||
},
|
||||
"elev_q": {
|
||||
"min": 0,
|
||||
|
||||
Reference in New Issue
Block a user