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:
@@ -381,6 +381,24 @@ pub struct EncodedStepCanvas {
|
||||
/// before the version tag has already forced a fresh fetch.
|
||||
#[serde(default)]
|
||||
pub lake_margin_q: EncodedField,
|
||||
/// T-1213 — LOCAL sub-district relief on a fixed metre scale (0–100, 50 =
|
||||
/// flat, 8 m per step). The signal the deep rungs draw relief texture from,
|
||||
/// because `elev_q` structurally cannot: `elev_q` spans the body's whole
|
||||
/// 8 km elevation range at 80 m per step, and a District canvas covers
|
||||
/// 2,048 m of ground where real relief is a fraction of one such step —
|
||||
/// measured at a mean neighbour delta of 0.02 there, with no pair in a
|
||||
/// 1290×540 frame differing by 2. Same static-geometry plane as `elev_q`
|
||||
/// (a pure function of position, not sim-state).
|
||||
///
|
||||
/// `50` at the orbital rungs, where `invent_primitives` is skipped by
|
||||
/// design and no sub-district detail is computed — Global and Region get
|
||||
/// their texture from `elev_q`, which is still varied at that scale.
|
||||
///
|
||||
/// `#[serde(default)]` for the same reason `lake_margin_q` carries it: an
|
||||
/// old-shape payload decodes rather than hard-failing, and the version-
|
||||
/// tagged cache forces a fresh fetch before anything reads it.
|
||||
#[serde(default)]
|
||||
pub relief_q: EncodedField,
|
||||
// --- Sim-state plane (2 dense fields, see struct doc) ---
|
||||
pub glaciation: EncodedField,
|
||||
pub flooded_q: EncodedField,
|
||||
@@ -469,6 +487,8 @@ struct StepCanvasCell {
|
||||
glaciation: u8,
|
||||
flooded_q: u8,
|
||||
lake_margin_q: u8,
|
||||
/// T-1213 local relief, fixed metre scale — see `EncodedStepCanvas::relief_q`.
|
||||
relief_q: u8,
|
||||
}
|
||||
|
||||
/// Derive one cell at `(wx, wy)` world metres, dispatching to the D-255(a)/(f)
|
||||
@@ -523,6 +543,7 @@ fn derive_step_canvas_cell(
|
||||
// every non-lake cell, same static-geometry-plane posture as
|
||||
// `elev_q`/`morphology` (a pure function of position, not sim-state).
|
||||
lake_margin_q: prof.lake_margin_q.clamp(0, 100) as u8,
|
||||
relief_q: prof.relief_q.clamp(0, 100) as u8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +572,8 @@ pub struct RawStepCanvas {
|
||||
/// tone source ocean coastlines already get for free from
|
||||
/// `ocean_fraction_q`. See `district_profile::DistrictProfile::lake_margin_q`.
|
||||
pub lake_margin_q: Vec<u8>,
|
||||
/// T-1213 — local sub-district relief, the deep rungs' texture source.
|
||||
pub relief_q: Vec<u8>,
|
||||
pub courses: Vec<RiverCourse>,
|
||||
pub cliffs: Vec<CliffSegment>,
|
||||
}
|
||||
@@ -1078,6 +1101,7 @@ pub fn build_step_canvas(
|
||||
let mut glaciation = vec![0u8; cells];
|
||||
let mut flooded_q = vec![0u8; cells];
|
||||
let mut lake_margin_q = vec![0u8; cells];
|
||||
let mut relief_q = vec![0u8; cells];
|
||||
|
||||
for (row, row_cells) in rows.into_iter().enumerate() {
|
||||
let base = row * width as usize;
|
||||
@@ -1091,6 +1115,7 @@ pub fn build_step_canvas(
|
||||
glaciation[i] = cell.glaciation;
|
||||
flooded_q[i] = cell.flooded_q;
|
||||
lake_margin_q[i] = cell.lake_margin_q;
|
||||
relief_q[i] = cell.relief_q;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,6 +1166,7 @@ pub fn build_step_canvas(
|
||||
glaciation,
|
||||
flooded_q,
|
||||
lake_margin_q,
|
||||
relief_q,
|
||||
courses,
|
||||
cliffs,
|
||||
}
|
||||
@@ -1190,6 +1216,7 @@ pub fn encode_step_canvas(raw: &RawStepCanvas) -> EncodedStepCanvas {
|
||||
values: raw.settlement_id.clone(),
|
||||
},
|
||||
lake_margin_q: png_encode_u8_plane(raw.width, raw.height, &raw.lake_margin_q),
|
||||
relief_q: png_encode_u8_plane(raw.width, raw.height, &raw.relief_q),
|
||||
glaciation: png_encode_u8_plane(raw.width, raw.height, &raw.glaciation),
|
||||
flooded_q: png_encode_u8_plane(raw.width, raw.height, &raw.flooded_q),
|
||||
courses: raw.courses.clone(),
|
||||
@@ -1213,6 +1240,7 @@ pub fn decode_step_canvas(enc: &EncodedStepCanvas) -> RawStepCanvas {
|
||||
vegetation: png_decode_u8_plane(&enc.vegetation),
|
||||
settlement_id: enc.settlement_id.values.clone(),
|
||||
lake_margin_q: png_decode_u8_plane(&enc.lake_margin_q),
|
||||
relief_q: png_decode_u8_plane(&enc.relief_q),
|
||||
glaciation: png_decode_u8_plane(&enc.glaciation),
|
||||
flooded_q: png_decode_u8_plane(&enc.flooded_q),
|
||||
courses: enc.courses.clone(),
|
||||
@@ -2149,6 +2177,7 @@ mod tests {
|
||||
vegetation: png_encode_u8_plane(1, 1, &[0]),
|
||||
settlement_id: EncodedSettlementField { values: vec![0] },
|
||||
lake_margin_q: png_encode_u8_plane(1, 1, &[0]),
|
||||
relief_q: png_encode_u8_plane(1, 1, &[50]),
|
||||
glaciation: png_encode_u8_plane(1, 1, &[0]),
|
||||
flooded_q: png_encode_u8_plane(1, 1, &[0]),
|
||||
courses: Vec::new(),
|
||||
@@ -2574,9 +2603,7 @@ mod tests {
|
||||
let spacing =
|
||||
rung.spacing_m(raw.width, raw.height, params.body_radius_km.unwrap_or(0.0));
|
||||
|
||||
let uniq_u8 = |v: &Vec<u8>| {
|
||||
v.iter().collect::<std::collections::BTreeSet<_>>().len()
|
||||
};
|
||||
let uniq_u8 = |v: &Vec<u8>| v.iter().collect::<std::collections::BTreeSet<_>>().len();
|
||||
let span_u8 = |v: &Vec<u8>| match (v.iter().min(), v.iter().max()) {
|
||||
(Some(a), Some(b)) => *b as i32 - *a as i32,
|
||||
_ => 0,
|
||||
@@ -2614,6 +2641,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// The same 4-cell gradient over relief_q — the T-1213 field that
|
||||
// exists precisely because elev_q's 80 m step cannot resolve
|
||||
// sub-district ground. If this does not move where elev_q's does
|
||||
// not, the field has bought nothing.
|
||||
let mut rsum = 0.0f64;
|
||||
let mut rn = 0.0f64;
|
||||
for r in 0..h {
|
||||
for c in 0..w.saturating_sub(4) {
|
||||
let a = raw.relief_q[r * w + c] as i32;
|
||||
let b = raw.relief_q[r * w + c + 4] as i32;
|
||||
rsum += (a - b).abs() as f64;
|
||||
rn += 1.0;
|
||||
}
|
||||
}
|
||||
let ruq = raw
|
||||
.relief_q
|
||||
.iter()
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len();
|
||||
grad += &format!(" RELIEF[uniq {ruq:3} d4mean {:.2}]", rsum / rn.max(1.0));
|
||||
|
||||
println!(
|
||||
"{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \
|
||||
morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}{grad}",
|
||||
|
||||
Reference in New Issue
Block a user