feat(simulation): relief_q — a local relief signal the deep rungs can resolve (T-1213)

The District and Quarter rungs rendered as flat colour, and the cause was not
the biome work everyone assumed. Measured on Ferrath through the production
canvas builder: at District the mean |elev_q delta| between neighbouring
gridunits is 0.02, and NOT ONE PAIR in a 1290x540 frame differs by 2.

elev_q spans 0-100 across the body's whole 8 km elevation range, so ONE STEP IS
80 METRES. A District canvas covers 2,048 m of ground, where the rolling relief
a walker navigates by is metres to tens of metres -- a fraction of a single
step. The sub-district detail IS generated (invent_primitives' scatter and
relief bands compute it) and then rounded away. Confirmed by running the
diagnostic with the octave cutoff disabled: still 0.02.

relief_q carries that same invented fine component against a scale chosen to
resolve it: 0-100 about a flat 50, RELIEF_FULL_SCALE_M = 400 m either side, so
8 m per step -- ten times finer than elev_q. elev_q keeps its body-absolute
meaning and the Atlas legend stays true.

Measured effect, elev_q vs relief_q (distinct values / mean 4-cell delta):

  Region     49 / 2.38   ->   101 / 21.86
  District   10 / 0.08   ->    35 / 0.35
  Quarter     8 / 0.02   ->    19 / 0.06

FIXED metre scale, never per-canvas normalization: the value for a piece of
ground must not depend on what else is in frame, or the same hillside changes
tone as the viewer pans. And it excludes elev_pct deliberately -- this is the
departure from the surrounding land, not height above sea level; including the
base would re-introduce the body-scale dominance that makes elev_q unusable
down here.

50 at the orbital rungs, which skip invent_primitives by design. Nothing is
lost: Global and Region still have varied elev_q (101 and 49 distinct values),
and the client takes whichever field carries signal via a max, with no
rung-name branching.

The client's ruggedness driver changes with it. It was an elev_q GRADIENT,
which cannot work across rungs -- the same 4-cell delta reads 21.86 at Region
and 0.08 at District, so any single full-scale constant either saturates one or
vanishes on the other. relief_q states relief outright, so |relief_q - 50| is
the answer directly and a fixed metre scale is immune to that by construction.

An absent plane reads FLAT, not zero -- 0 on this field means maximum relief
BELOW flat, so a payload without it would have stippled the entire map. That is
reachable: the field is #[serde(default)] so old-shape payloads decode. Two
colorize tests whose fixtures predate the plane caught it.

2004 server tests, 1838 client tests, 0 failed. clippy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:11:23 +02:00
co-authored by Claude Opus 5
parent 566b566519
commit 5eb394b36f
11 changed files with 178 additions and 5 deletions
+67
View File
@@ -223,6 +223,30 @@ pub struct DistrictProfile {
/// Average of `TerrainAnalysis.elev_pct` × 100 across district cells.
pub elev_q: i32,
/// **Local** relief on a FIXED fine metre scale — 0–100 with 50 = flat,
/// each step [`RELIEF_FULL_SCALE_M`] / 50 metres (T-1213, 2026-08-07).
///
/// WHY THIS IS NOT `elev_q`. `elev_q` spans the body's whole elevation
/// range: 0–100 across [`MAX_REGION_ELEVATION_KM`] = 8 km, so **one step is
/// 80 metres**. That is the right granularity for a whole-body map and
/// useless below it — a District canvas covers 2,048 m of ground, where the
/// rolling relief a walker navigates by is metres to tens of metres, i.e.
/// a fraction of one `elev_q` step. Measured on Ferrath through the
/// production canvas builder: at District the mean |elev_q delta| between
/// neighbouring gridunits is **0.02**, and not one pair in a 1290×540 frame
/// differs by 2. The sub-district detail IS generated — `scatter` and
/// `relief` below compute it — and then rounded away.
///
/// So this field carries that same invented fine component against a scale
/// chosen to resolve it, rather than against the body. It is the signal the
/// deep rungs draw texture from; `elev_q` keeps its body-absolute meaning
/// and the Atlas legend stays true.
///
/// Signed about 50 rather than a magnitude, because a hollow and a rise are
/// different ground and a consumer that wants only roughness can take
/// `|relief_q - 50|` — the reverse is not recoverable.
pub relief_q: i32,
/// Ocean fraction for this district (0–100 scale, integer).
pub ocean_fraction_q: i32,
@@ -722,6 +746,17 @@ pub fn derive_morphology_zone(
/// span can replace it when body relief data is carried (T-1032).
const MAX_REGION_ELEVATION_KM: f64 = 8.0;
/// Full-scale swing of [`DistrictProfile::relief_q`], in metres either side of
/// flat. 400 m spans everything from a gentle roll to a substantial ridge at
/// the sub-district scale, and puts one quantization step at **8 m** — ten
/// times finer than `elev_q`'s 80 m, which is what makes the deep rungs
/// resolvable at all.
///
/// A FIXED metre scale on purpose, never per-canvas normalization: the value
/// for a piece of ground must not depend on what else is in frame, or the same
/// hillside would change tone as the viewer pans and the map would shimmer.
const RELIEF_FULL_SCALE_M: f64 = 400.0;
/// Climate tuning constants. `ClimateConstants::default()` holds the embedded
/// values and is the authoritative runtime source **today**; the source-canonical
/// `server/data/climate_constants.toml` mirrors those same values and is the file
@@ -1145,6 +1180,9 @@ const WINDOW_RELIEF_SALT: u64 = 0x5EED_C0DE;
struct InventedPrimitives {
slope_q: i32,
elev_q: i32,
/// Local sub-district relief on the fixed [`RELIEF_FULL_SCALE_M`] scale —
/// see [`DistrictProfile::relief_q`].
relief_q: i32,
ocean_fraction_q: i32,
}
@@ -1360,8 +1398,22 @@ fn invent_primitives(
// 0.0 at every cutoff ≥ 2,048 m (District and coarser), so this sum is
// byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the
// cutoff discipline says it must be.
// The SAME invented fine component the elevation sum above folds in, kept
// on its own fixed metre scale instead of the body's (T-1213). `scatter`
// and `relief` are normalized fractions of MAX_REGION_ELEVATION_KM, so
// scaling by it gives metres; RELIEF_FULL_SCALE_M then maps +/-400 m onto
// 0..100 about a flat 50, i.e. 8 m per step against elev_q's 80 m.
//
// Deliberately excludes `elev_pct`: this is LOCAL relief, the departure
// from the surrounding land, not height above sea level. Including the
// base would re-introduce exactly the body-scale dominance that makes
// elev_q unusable down here.
let relief_m = (scatter + relief) * MAX_REGION_ELEVATION_KM * 1_000.0;
let relief_q = ((50.0 + (relief_m / RELIEF_FULL_SCALE_M) * 50.0).round() as i32).clamp(0, 100);
InventedPrimitives {
elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100),
relief_q,
slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0)
.round() as i32)
.clamp(0, 100),
@@ -1636,6 +1688,11 @@ fn build_district_profile(
climate: &ClimateConstants,
slope_q: i32,
elev_q: i32,
// Local sub-district relief, fixed metre scale — see
// `DistrictProfile::relief_q`. Inert to every other field's derivation (a
// presentation-tier signal, not a classification input), so it is carried
// straight through to the output.
relief_q: i32,
ocean_fraction_q: i32,
region_baseline_c: Option<f32>,
basin_direction: BasinDirection,
@@ -1733,6 +1790,7 @@ fn build_district_profile(
precipitation_class,
slope_q,
elev_q,
relief_q,
ocean_fraction_q,
lake_margin_q,
river_threshold,
@@ -1986,6 +2044,7 @@ fn derive_at_metres_with_riparian(
climate,
prims.slope_q,
prims.elev_q,
prims.relief_q,
prims.ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
@@ -2182,6 +2241,14 @@ pub fn derive_orbital_at_metres(
climate,
slope_q,
elev_q,
// 50 = flat. The orbital rung skips `invent_primitives` altogether
// (that is the whole point of this path — envelope-only, no scatter,
// no warp), so no sub-district relief is computed here and reporting a
// neutral midpoint is the honest answer rather than a fabricated one.
// Nothing is lost visually: at Global and Region `elev_q` still varies
// properly — 101 and 49 distinct values respectively on Ferrath — and
// it is only below them that it flattens and `relief_q` takes over.
50,
ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),