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:
@@ -1 +1,2 @@
|
|||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXF1VDVQDQ8EFGTXX787M90R', 'T-1239', '2026-08-06 14:43:24.767', '2026-08-06 14:43:24.767', NULL, 'ae5ccb58f885727882f56ce2a32c91e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXF1VDVQDQ8EFGTXX787M90R', 'T-1239', '2026-08-06 14:43:24.767', '2026-08-06 14:43:24.767', NULL, 'ae5ccb58f885727882f56ce2a32c91e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXRSY7QWD8J5X6G1N86WMKEC', 'T-1240', '2026-08-07 13:26:56.708', '2026-08-07 13:26:56.708', NULL, 'dc79a976524b951a73f05532772ef86a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
|||||||
@@ -166,3 +166,4 @@ rivers vanish on descent. That is tracked separately as T-1239 and is NOT this
|
|||||||
ticket''s scope, but it will be visible in the same captures, so do not mistake it
|
ticket''s scope, but it will be visible in the same captures, so do not mistake it
|
||||||
for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:20.464', NULL, '94ccf78431914ea836ba5d59170a3395', 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);
|
for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:20.464', NULL, '94ccf78431914ea836ba5d59170a3395', 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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Biome/relief stipple-density layer on the terrain build (RimWorld technique 4)', 'Follow-up from T-1175''s assess-only item (2026-07-25, stig''s write-up): a stipple/texture-density layer carrying hills/mountains/forest on top of hue, driven by the already-wire-carried vegetation and elev_q L8 planes — presentation-only, D-255(e)-legal (texture-space dithering of already-derived per-cell values, deterministically seeded per cell coordinate + value so it is stable across cache hit/miss; never invents samples between server cells). Would live as a post-process in step_canvas_terrain_layer.gd::rebuild_from_canvas()''s Image.set_pixel build. Design questions to settle at pickup: (a) stipple dots inline in the existing per-cell loop (cheap, same O(wxh) pass) vs a second overlay pass (simpler code, doubles pixel-touch cost); (b) density from vegetation class directly vs a combination with elev_q — relief hachures and forest texture are two different visual grammars in the RimWorld reference, not one slider; (c) own legend toggle (TMP/MST/VEG overlay-bar pattern) vs always-on like the elevation lightness modifier. Reference: docs/design/references/rimworld-world-map-fluency.jpg. Related: T-1175, T-1162 (vegetation patchiness fields), D-255(e).', 'in_progress', 'low', NULL, 'client', NULL, '2026-07-25 13:24:54.152', '2026-08-07 12:44:21.620', NULL, 'c1560b624bd9a9d79fe919f282620f22', 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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Biome/relief stipple-density layer on the terrain build (RimWorld technique 4)', 'Follow-up from T-1175''s assess-only item (2026-07-25, stig''s write-up): a stipple/texture-density layer carrying hills/mountains/forest on top of hue, driven by the already-wire-carried vegetation and elev_q L8 planes — presentation-only, D-255(e)-legal (texture-space dithering of already-derived per-cell values, deterministically seeded per cell coordinate + value so it is stable across cache hit/miss; never invents samples between server cells). Would live as a post-process in step_canvas_terrain_layer.gd::rebuild_from_canvas()''s Image.set_pixel build. Design questions to settle at pickup: (a) stipple dots inline in the existing per-cell loop (cheap, same O(wxh) pass) vs a second overlay pass (simpler code, doubles pixel-touch cost); (b) density from vegetation class directly vs a combination with elev_q — relief hachures and forest texture are two different visual grammars in the RimWorld reference, not one slider; (c) own legend toggle (TMP/MST/VEG overlay-bar pattern) vs always-on like the elevation lightness modifier. Reference: docs/design/references/rimworld-world-map-fluency.jpg. Related: T-1175, T-1162 (vegetation patchiness fields), D-255(e).', 'in_progress', 'low', NULL, 'client', NULL, '2026-07-25 13:24:54.152', '2026-08-07 12:44:21.620', NULL, 'c1560b624bd9a9d79fe919f282620f22', 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local.', 'backlog', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-07 13:26:56.703', NULL, '45cb7553effbe05042fe7f3eb09e6483', 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);
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ const RUGGEDNESS_FULL_SCALE_Q: int = 4
|
|||||||
## has arrived but has not yet flattened into the regional average.
|
## has arrived but has not yet flattened into the regional average.
|
||||||
const RUGGEDNESS_BASELINE_CELLS: int = 4
|
const RUGGEDNESS_BASELINE_CELLS: int = 4
|
||||||
|
|
||||||
|
## relief_q deviation from flat (50) at which ground is "fully rugged". Each
|
||||||
|
## step is 8 m, so 25 = 200 m of local relief — a substantial ridge at the
|
||||||
|
## sub-district scale, and comfortably inside the range the field actually
|
||||||
|
## uses (Region spans all 101 values; District 35 of them).
|
||||||
|
const RELIEF_TEXTURE_FULL_SCALE_Q: int = 25
|
||||||
|
|
||||||
## One decoded plane set, pre-extracted from the five L8 Images + the two
|
## One decoded plane set, pre-extracted from the five L8 Images + the two
|
||||||
## raw-array fields a caller needs per cell — built once per arrived canvas
|
## raw-array fields a caller needs per cell — built once per arrived canvas
|
||||||
## (see StepCanvasTerrainLayer.build_texture()), not re-decoded per pixel.
|
## (see StepCanvasTerrainLayer.build_texture()), not re-decoded per pixel.
|
||||||
@@ -88,6 +94,7 @@ class CellPlanes:
|
|||||||
var moisture_q: Image
|
var moisture_q: Image
|
||||||
var vegetation: Image
|
var vegetation: Image
|
||||||
var lake_margin_q: Image
|
var lake_margin_q: Image
|
||||||
|
var relief_q: Image
|
||||||
var glaciation: Image
|
var glaciation: Image
|
||||||
var temp_dc: Array
|
var temp_dc: Array
|
||||||
var width: int
|
var width: int
|
||||||
@@ -172,6 +179,32 @@ static func _texture(planes: CellPlanes, col: int, row: int, base: Color) -> Col
|
|||||||
## Clamped at the edges by sampling the centre, so a canvas border neither
|
## Clamped at the edges by sampling the centre, so a canvas border neither
|
||||||
## invents relief nor loses it.
|
## invents relief nor loses it.
|
||||||
static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float:
|
static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float:
|
||||||
|
# PRIMARY: relief_q states local relief outright (T-1213) — 50 is flat and
|
||||||
|
# each step is 8 m — so the ruggedness is just how far from flat this ground
|
||||||
|
# is. No neighbour comparison: the field already IS the answer, measured on
|
||||||
|
# a fixed metre scale rather than against whatever happens to be in frame.
|
||||||
|
#
|
||||||
|
# This replaced 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 the one or vanishes on the other. A
|
||||||
|
# fixed metre scale is immune to that by construction.
|
||||||
|
# Absent plane reads FLAT, not zero. `_l8_value` zero-fills a missing image,
|
||||||
|
# and on this field 0 does not mean "no data" — it means maximum relief
|
||||||
|
# below flat, so a payload without the plane would render every cell fully
|
||||||
|
# rugged and stipple the entire map. That is reachable in practice: the
|
||||||
|
# field is `#[serde(default)]` server-side precisely so an old-shape payload
|
||||||
|
# decodes rather than hard-failing. Caught by two colorize tests whose
|
||||||
|
# fixtures predate the plane.
|
||||||
|
var from_relief: float = 0.0
|
||||||
|
if planes.relief_q != null:
|
||||||
|
var rel: int = _l8_value(planes.relief_q, col, row)
|
||||||
|
from_relief = absf(float(rel) - 50.0) / float(RELIEF_TEXTURE_FULL_SCALE_Q)
|
||||||
|
|
||||||
|
# FALLBACK: the orbital rungs skip the sub-district derive entirely, so
|
||||||
|
# relief_q is a flat 50 there and would texture nothing. elev_q is still
|
||||||
|
# richly varied at that scale (101 distinct values on Ferrath at Global), so
|
||||||
|
# Global keeps the gradient read. Taking the max means each rung is driven
|
||||||
|
# by whichever field actually carries signal, with no rung-name branching.
|
||||||
var here: int = _l8_value(planes.elev_q, col, row)
|
var here: int = _l8_value(planes.elev_q, col, row)
|
||||||
var worst: int = 0
|
var worst: int = 0
|
||||||
var b: int = RUGGEDNESS_BASELINE_CELLS
|
var b: int = RUGGEDNESS_BASELINE_CELLS
|
||||||
@@ -179,7 +212,9 @@ static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float:
|
|||||||
var c: int = clampi(col + d.x, 0, planes.width - 1)
|
var c: int = clampi(col + d.x, 0, planes.width - 1)
|
||||||
var r: int = clampi(row + d.y, 0, planes.height - 1)
|
var r: int = clampi(row + d.y, 0, planes.height - 1)
|
||||||
worst = maxi(worst, absi(_l8_value(planes.elev_q, c, r) - here))
|
worst = maxi(worst, absi(_l8_value(planes.elev_q, c, r) - here))
|
||||||
return clampf(float(worst) / float(RUGGEDNESS_FULL_SCALE_Q), 0.0, 1.0)
|
var from_elev: float = float(worst) / float(RUGGEDNESS_FULL_SCALE_Q)
|
||||||
|
|
||||||
|
return clampf(maxf(from_relief, from_elev), 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
## Deterministic [0,1) dither for a cell, salted per mark type so the relief and
|
## Deterministic [0,1) dither for a cell, salted per mark type so the relief and
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasCo
|
|||||||
planes.moisture_q = _decode_l8_plane(canvas.get("moisture_q"))
|
planes.moisture_q = _decode_l8_plane(canvas.get("moisture_q"))
|
||||||
planes.vegetation = _decode_l8_plane(canvas.get("vegetation"))
|
planes.vegetation = _decode_l8_plane(canvas.get("vegetation"))
|
||||||
planes.lake_margin_q = _decode_l8_plane(canvas.get("lake_margin_q"))
|
planes.lake_margin_q = _decode_l8_plane(canvas.get("lake_margin_q"))
|
||||||
|
planes.relief_q = _decode_l8_plane(canvas.get("relief_q"))
|
||||||
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
|
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
|
||||||
planes.temp_dc = canvas.get("temp_dc", [])
|
planes.temp_dc = canvas.get("temp_dc", [])
|
||||||
return planes
|
return planes
|
||||||
|
|||||||
@@ -807,6 +807,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Temperate,
|
precipitation_class: PrecipitationClass::Temperate,
|
||||||
slope_q,
|
slope_q,
|
||||||
elev_q,
|
elev_q,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q,
|
ocean_fraction_q,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Temperate,
|
precipitation_class: PrecipitationClass::Temperate,
|
||||||
slope_q: 5,
|
slope_q: 5,
|
||||||
elev_q: 20,
|
elev_q: 20,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 15,
|
ocean_fraction_q: 15,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
@@ -637,6 +638,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Arid,
|
precipitation_class: PrecipitationClass::Arid,
|
||||||
slope_q: 3,
|
slope_q: 3,
|
||||||
elev_q: 10,
|
elev_q: 10,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 0, // no water at all
|
ocean_fraction_q: 0, // no water at all
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 300,
|
river_threshold: 300,
|
||||||
|
|||||||
@@ -223,6 +223,30 @@ pub struct DistrictProfile {
|
|||||||
/// Average of `TerrainAnalysis.elev_pct` × 100 across district cells.
|
/// Average of `TerrainAnalysis.elev_pct` × 100 across district cells.
|
||||||
pub elev_q: i32,
|
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).
|
/// Ocean fraction for this district (0–100 scale, integer).
|
||||||
pub ocean_fraction_q: i32,
|
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).
|
/// span can replace it when body relief data is carried (T-1032).
|
||||||
const MAX_REGION_ELEVATION_KM: f64 = 8.0;
|
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
|
/// Climate tuning constants. `ClimateConstants::default()` holds the embedded
|
||||||
/// values and is the authoritative runtime source **today**; the source-canonical
|
/// values and is the authoritative runtime source **today**; the source-canonical
|
||||||
/// `server/data/climate_constants.toml` mirrors those same values and is the file
|
/// `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 {
|
struct InventedPrimitives {
|
||||||
slope_q: i32,
|
slope_q: i32,
|
||||||
elev_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,
|
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
|
// 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
|
// byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the
|
||||||
// cutoff discipline says it must be.
|
// 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 {
|
InventedPrimitives {
|
||||||
elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100),
|
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)
|
slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0)
|
||||||
.round() as i32)
|
.round() as i32)
|
||||||
.clamp(0, 100),
|
.clamp(0, 100),
|
||||||
@@ -1636,6 +1688,11 @@ fn build_district_profile(
|
|||||||
climate: &ClimateConstants,
|
climate: &ClimateConstants,
|
||||||
slope_q: i32,
|
slope_q: i32,
|
||||||
elev_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,
|
ocean_fraction_q: i32,
|
||||||
region_baseline_c: Option<f32>,
|
region_baseline_c: Option<f32>,
|
||||||
basin_direction: BasinDirection,
|
basin_direction: BasinDirection,
|
||||||
@@ -1733,6 +1790,7 @@ fn build_district_profile(
|
|||||||
precipitation_class,
|
precipitation_class,
|
||||||
slope_q,
|
slope_q,
|
||||||
elev_q,
|
elev_q,
|
||||||
|
relief_q,
|
||||||
ocean_fraction_q,
|
ocean_fraction_q,
|
||||||
lake_margin_q,
|
lake_margin_q,
|
||||||
river_threshold,
|
river_threshold,
|
||||||
@@ -1986,6 +2044,7 @@ fn derive_at_metres_with_riparian(
|
|||||||
climate,
|
climate,
|
||||||
prims.slope_q,
|
prims.slope_q,
|
||||||
prims.elev_q,
|
prims.elev_q,
|
||||||
|
prims.relief_q,
|
||||||
prims.ocean_fraction_q,
|
prims.ocean_fraction_q,
|
||||||
region_baseline_c,
|
region_baseline_c,
|
||||||
BasinDirection::default(),
|
BasinDirection::default(),
|
||||||
@@ -2182,6 +2241,14 @@ pub fn derive_orbital_at_metres(
|
|||||||
climate,
|
climate,
|
||||||
slope_q,
|
slope_q,
|
||||||
elev_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,
|
ocean_fraction_q,
|
||||||
region_baseline_c,
|
region_baseline_c,
|
||||||
BasinDirection::default(),
|
BasinDirection::default(),
|
||||||
|
|||||||
@@ -2381,6 +2381,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Arid,
|
precipitation_class: PrecipitationClass::Arid,
|
||||||
slope_q: 0,
|
slope_q: 0,
|
||||||
elev_q: elev,
|
elev_q: elev,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 0,
|
ocean_fraction_q: 0,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
|
|||||||
@@ -381,6 +381,24 @@ pub struct EncodedStepCanvas {
|
|||||||
/// before the version tag has already forced a fresh fetch.
|
/// before the version tag has already forced a fresh fetch.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lake_margin_q: EncodedField,
|
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) ---
|
// --- Sim-state plane (2 dense fields, see struct doc) ---
|
||||||
pub glaciation: EncodedField,
|
pub glaciation: EncodedField,
|
||||||
pub flooded_q: EncodedField,
|
pub flooded_q: EncodedField,
|
||||||
@@ -469,6 +487,8 @@ struct StepCanvasCell {
|
|||||||
glaciation: u8,
|
glaciation: u8,
|
||||||
flooded_q: u8,
|
flooded_q: u8,
|
||||||
lake_margin_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)
|
/// 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
|
// every non-lake cell, same static-geometry-plane posture as
|
||||||
// `elev_q`/`morphology` (a pure function of position, not sim-state).
|
// `elev_q`/`morphology` (a pure function of position, not sim-state).
|
||||||
lake_margin_q: prof.lake_margin_q.clamp(0, 100) as u8,
|
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
|
/// tone source ocean coastlines already get for free from
|
||||||
/// `ocean_fraction_q`. See `district_profile::DistrictProfile::lake_margin_q`.
|
/// `ocean_fraction_q`. See `district_profile::DistrictProfile::lake_margin_q`.
|
||||||
pub lake_margin_q: Vec<u8>,
|
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 courses: Vec<RiverCourse>,
|
||||||
pub cliffs: Vec<CliffSegment>,
|
pub cliffs: Vec<CliffSegment>,
|
||||||
}
|
}
|
||||||
@@ -1078,6 +1101,7 @@ pub fn build_step_canvas(
|
|||||||
let mut glaciation = vec![0u8; cells];
|
let mut glaciation = vec![0u8; cells];
|
||||||
let mut flooded_q = vec![0u8; cells];
|
let mut flooded_q = vec![0u8; cells];
|
||||||
let mut lake_margin_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() {
|
for (row, row_cells) in rows.into_iter().enumerate() {
|
||||||
let base = row * width as usize;
|
let base = row * width as usize;
|
||||||
@@ -1091,6 +1115,7 @@ pub fn build_step_canvas(
|
|||||||
glaciation[i] = cell.glaciation;
|
glaciation[i] = cell.glaciation;
|
||||||
flooded_q[i] = cell.flooded_q;
|
flooded_q[i] = cell.flooded_q;
|
||||||
lake_margin_q[i] = cell.lake_margin_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,
|
glaciation,
|
||||||
flooded_q,
|
flooded_q,
|
||||||
lake_margin_q,
|
lake_margin_q,
|
||||||
|
relief_q,
|
||||||
courses,
|
courses,
|
||||||
cliffs,
|
cliffs,
|
||||||
}
|
}
|
||||||
@@ -1190,6 +1216,7 @@ pub fn encode_step_canvas(raw: &RawStepCanvas) -> EncodedStepCanvas {
|
|||||||
values: raw.settlement_id.clone(),
|
values: raw.settlement_id.clone(),
|
||||||
},
|
},
|
||||||
lake_margin_q: png_encode_u8_plane(raw.width, raw.height, &raw.lake_margin_q),
|
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),
|
glaciation: png_encode_u8_plane(raw.width, raw.height, &raw.glaciation),
|
||||||
flooded_q: png_encode_u8_plane(raw.width, raw.height, &raw.flooded_q),
|
flooded_q: png_encode_u8_plane(raw.width, raw.height, &raw.flooded_q),
|
||||||
courses: raw.courses.clone(),
|
courses: raw.courses.clone(),
|
||||||
@@ -1213,6 +1240,7 @@ pub fn decode_step_canvas(enc: &EncodedStepCanvas) -> RawStepCanvas {
|
|||||||
vegetation: png_decode_u8_plane(&enc.vegetation),
|
vegetation: png_decode_u8_plane(&enc.vegetation),
|
||||||
settlement_id: enc.settlement_id.values.clone(),
|
settlement_id: enc.settlement_id.values.clone(),
|
||||||
lake_margin_q: png_decode_u8_plane(&enc.lake_margin_q),
|
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),
|
glaciation: png_decode_u8_plane(&enc.glaciation),
|
||||||
flooded_q: png_decode_u8_plane(&enc.flooded_q),
|
flooded_q: png_decode_u8_plane(&enc.flooded_q),
|
||||||
courses: enc.courses.clone(),
|
courses: enc.courses.clone(),
|
||||||
@@ -2149,6 +2177,7 @@ mod tests {
|
|||||||
vegetation: png_encode_u8_plane(1, 1, &[0]),
|
vegetation: png_encode_u8_plane(1, 1, &[0]),
|
||||||
settlement_id: EncodedSettlementField { values: vec![0] },
|
settlement_id: EncodedSettlementField { values: vec![0] },
|
||||||
lake_margin_q: png_encode_u8_plane(1, 1, &[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]),
|
glaciation: png_encode_u8_plane(1, 1, &[0]),
|
||||||
flooded_q: png_encode_u8_plane(1, 1, &[0]),
|
flooded_q: png_encode_u8_plane(1, 1, &[0]),
|
||||||
courses: Vec::new(),
|
courses: Vec::new(),
|
||||||
@@ -2574,9 +2603,7 @@ mod tests {
|
|||||||
let spacing =
|
let spacing =
|
||||||
rung.spacing_m(raw.width, raw.height, params.body_radius_km.unwrap_or(0.0));
|
rung.spacing_m(raw.width, raw.height, params.body_radius_km.unwrap_or(0.0));
|
||||||
|
|
||||||
let uniq_u8 = |v: &Vec<u8>| {
|
let uniq_u8 = |v: &Vec<u8>| v.iter().collect::<std::collections::BTreeSet<_>>().len();
|
||||||
v.iter().collect::<std::collections::BTreeSet<_>>().len()
|
|
||||||
};
|
|
||||||
let span_u8 = |v: &Vec<u8>| match (v.iter().min(), v.iter().max()) {
|
let span_u8 = |v: &Vec<u8>| match (v.iter().min(), v.iter().max()) {
|
||||||
(Some(a), Some(b)) => *b as i32 - *a as i32,
|
(Some(a), Some(b)) => *b as i32 - *a as i32,
|
||||||
_ => 0,
|
_ => 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!(
|
println!(
|
||||||
"{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \
|
"{: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}",
|
morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}{grad}",
|
||||||
|
|||||||
@@ -2290,6 +2290,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Temperate,
|
precipitation_class: PrecipitationClass::Temperate,
|
||||||
slope_q: 5,
|
slope_q: 5,
|
||||||
elev_q: 20,
|
elev_q: 20,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 15,
|
ocean_fraction_q: 15,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
@@ -2312,6 +2313,7 @@ mod tests {
|
|||||||
precipitation_class: PrecipitationClass::Temperate,
|
precipitation_class: PrecipitationClass::Temperate,
|
||||||
slope_q,
|
slope_q,
|
||||||
elev_q: 5,
|
elev_q: 5,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 90,
|
ocean_fraction_q: 90,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
@@ -2858,6 +2860,7 @@ mod tests {
|
|||||||
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
|
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
|
||||||
slope_q: 5,
|
slope_q: 5,
|
||||||
elev_q: 20, // low elevation
|
elev_q: 20, // low elevation
|
||||||
|
relief_q: 50, // flat — this test measures the elevation blend, not relief
|
||||||
ocean_fraction_q: 0, // no channel — simpler tile layout for elevation check
|
ocean_fraction_q: 0, // no channel — simpler tile layout for elevation check
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
@@ -2873,6 +2876,7 @@ mod tests {
|
|||||||
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
|
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
|
||||||
slope_q: 5,
|
slope_q: 5,
|
||||||
elev_q: 70, // high elevation — makes the blend measurable
|
elev_q: 70, // high elevation — makes the blend measurable
|
||||||
|
relief_q: 50, // flat — see the primary district above
|
||||||
ocean_fraction_q: 0,
|
ocean_fraction_q: 0,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: 200,
|
river_threshold: 200,
|
||||||
@@ -2976,6 +2980,7 @@ mod tests {
|
|||||||
ocean_fraction_q: 0,
|
ocean_fraction_q: 0,
|
||||||
slope_q: 8,
|
slope_q: 8,
|
||||||
elev_q: 30,
|
elev_q: 30,
|
||||||
|
relief_q: 50,
|
||||||
moisture_q: 20,
|
moisture_q: 20,
|
||||||
..alluvial_district()
|
..alluvial_district()
|
||||||
}
|
}
|
||||||
@@ -3057,6 +3062,7 @@ mod tests {
|
|||||||
glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2
|
glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2
|
||||||
slope_q: 60,
|
slope_q: 60,
|
||||||
elev_q: 80,
|
elev_q: 80,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 30,
|
ocean_fraction_q: 30,
|
||||||
moisture_q: 70,
|
moisture_q: 70,
|
||||||
vegetation_class: VegetationClass::Barren,
|
vegetation_class: VegetationClass::Barren,
|
||||||
@@ -3174,6 +3180,7 @@ mod tests {
|
|||||||
tectonic_class: TectonicClass::Active,
|
tectonic_class: TectonicClass::Active,
|
||||||
slope_q: 70,
|
slope_q: 70,
|
||||||
elev_q: 60,
|
elev_q: 60,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 40,
|
ocean_fraction_q: 40,
|
||||||
moisture_q: 50,
|
moisture_q: 50,
|
||||||
vegetation_class: VegetationClass::Barren,
|
vegetation_class: VegetationClass::Barren,
|
||||||
@@ -3259,6 +3266,7 @@ mod tests {
|
|||||||
morphology_zone: MorphologyZone::Delta,
|
morphology_zone: MorphologyZone::Delta,
|
||||||
slope_q: 2,
|
slope_q: 2,
|
||||||
elev_q: 4,
|
elev_q: 4,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 50,
|
ocean_fraction_q: 50,
|
||||||
moisture_q: 80,
|
moisture_q: 80,
|
||||||
vegetation_class: VegetationClass::Scrub,
|
vegetation_class: VegetationClass::Scrub,
|
||||||
@@ -3376,6 +3384,7 @@ mod tests {
|
|||||||
morphology_zone: MorphologyZone::DuneStrand,
|
morphology_zone: MorphologyZone::DuneStrand,
|
||||||
slope_q: 8,
|
slope_q: 8,
|
||||||
elev_q: 6,
|
elev_q: 6,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 30,
|
ocean_fraction_q: 30,
|
||||||
moisture_q: 40,
|
moisture_q: 40,
|
||||||
vegetation_class: VegetationClass::Barren,
|
vegetation_class: VegetationClass::Barren,
|
||||||
@@ -3470,6 +3479,7 @@ mod tests {
|
|||||||
tectonic_class: TectonicClass::Active,
|
tectonic_class: TectonicClass::Active,
|
||||||
slope_q: 80,
|
slope_q: 80,
|
||||||
elev_q: 90,
|
elev_q: 90,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 5,
|
ocean_fraction_q: 5,
|
||||||
moisture_q: 50,
|
moisture_q: 50,
|
||||||
vegetation_class: VegetationClass::Barren,
|
vegetation_class: VegetationClass::Barren,
|
||||||
@@ -3579,6 +3589,7 @@ mod tests {
|
|||||||
morphology_zone: MorphologyZone::MeanderReach,
|
morphology_zone: MorphologyZone::MeanderReach,
|
||||||
slope_q: 3,
|
slope_q: 3,
|
||||||
elev_q: 15,
|
elev_q: 15,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 20,
|
ocean_fraction_q: 20,
|
||||||
moisture_q: 70,
|
moisture_q: 70,
|
||||||
vegetation_class: VegetationClass::Forest,
|
vegetation_class: VegetationClass::Forest,
|
||||||
@@ -3623,6 +3634,7 @@ mod tests {
|
|||||||
morphology_zone: MorphologyZone::MeanderReach,
|
morphology_zone: MorphologyZone::MeanderReach,
|
||||||
slope_q: 5, // above wetland slope threshold
|
slope_q: 5, // above wetland slope threshold
|
||||||
elev_q: 15,
|
elev_q: 15,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 20,
|
ocean_fraction_q: 20,
|
||||||
moisture_q: 55, // below wetland moisture threshold
|
moisture_q: 55, // below wetland moisture threshold
|
||||||
vegetation_class: VegetationClass::Forest,
|
vegetation_class: VegetationClass::Forest,
|
||||||
@@ -3715,6 +3727,7 @@ mod tests {
|
|||||||
let shared_district_base = DistrictProfile {
|
let shared_district_base = DistrictProfile {
|
||||||
slope_q: 5,
|
slope_q: 5,
|
||||||
elev_q: 20,
|
elev_q: 20,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 20, // ensure has_active_channel = true
|
ocean_fraction_q: 20, // ensure has_active_channel = true
|
||||||
moisture_q: 55,
|
moisture_q: 55,
|
||||||
vegetation_class: VegetationClass::Forest,
|
vegetation_class: VegetationClass::Forest,
|
||||||
@@ -3843,6 +3856,7 @@ mod tests {
|
|||||||
glaciation_grade: GlaciationGrade::Moderate,
|
glaciation_grade: GlaciationGrade::Moderate,
|
||||||
slope_q: 50,
|
slope_q: 50,
|
||||||
elev_q: 40,
|
elev_q: 40,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 20,
|
ocean_fraction_q: 20,
|
||||||
moisture_q: 50,
|
moisture_q: 50,
|
||||||
..alluvial_district()
|
..alluvial_district()
|
||||||
@@ -3878,6 +3892,7 @@ mod tests {
|
|||||||
glaciation_grade: GlaciationGrade::Moderate,
|
glaciation_grade: GlaciationGrade::Moderate,
|
||||||
slope_q: 50,
|
slope_q: 50,
|
||||||
elev_q: 40,
|
elev_q: 40,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q: 5,
|
ocean_fraction_q: 5,
|
||||||
moisture_q: 30,
|
moisture_q: 30,
|
||||||
vegetation_class: VegetationClass::Barren,
|
vegetation_class: VegetationClass::Barren,
|
||||||
|
|||||||
@@ -2268,6 +2268,7 @@ fn make_region(
|
|||||||
precipitation_class: precip,
|
precipitation_class: precip,
|
||||||
slope_q,
|
slope_q,
|
||||||
elev_q,
|
elev_q,
|
||||||
|
relief_q: 50,
|
||||||
ocean_fraction_q,
|
ocean_fraction_q,
|
||||||
lake_margin_q: 0,
|
lake_margin_q: 0,
|
||||||
river_threshold: derive_river_threshold(tectonic, precip),
|
river_threshold: derive_river_threshold(tectonic, precip),
|
||||||
|
|||||||
Reference in New Issue
Block a user