feat(simulation): lake_margin_q depth band — lake shorelines gain gradient vocabulary (T-1188)
Lake edges rendered as hard step-edges while ocean coasts got multi-tone transition bands: every coastal-transition morphology gate keys on ocean_fraction_q, definitionally 0 inside a lake basin (hypothesis (b) of the ticket; (a) disproven first — a shoreline-crossing sweep at 2048/512/128m plus a 10m fine sweep all land on the same continuous crossing, so positional refinement was never broken). New DistrictProfile.lake_margin_q (0-100 settled-hydrology depth band, from the same bilinear filled/elevation pair the lake test already samples; ceiling calibrated just above the observed p90 depth on GJ338Bd's 5,043 flooded cells), threaded through both derive paths onto EncodedStepCanvas (serde-default for shape tolerance) and down the client: protocol decode, terrain-layer plane, colorize shades Lake cells by depth band instead of elev_q (bedrock-under-water, the wrong signal). project.yaml 0.4.0 -> 0.4.1: the new wire field must invalidate the client disk cache via its version tag (T-1183's D-192 mechanism). Acceptance gates green with the new field (lossless round-trip, cache-hit==cache-miss, every rung). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,10 +34,17 @@ class_name StepCanvasProtocol
|
||||
## (duplicated here per browse_protocol.gd's own "genuinely standalone"
|
||||
## precedent, not shared via a Callable).
|
||||
## EncodedStepCanvas — a map: {width, height, morphology, elev_q, temp_dc,
|
||||
## moisture_q, vegetation, settlement_id, glaciation, flooded_q, courses,
|
||||
## cliffs}. The six PNG-per-field dense planes (morphology/elev_q/
|
||||
## moisture_q/vegetation/glaciation/flooded_q) are each a map
|
||||
## {"png_bytes": [...]} — png_bytes is a Rust `Vec<u8>` with NO
|
||||
## moisture_q, vegetation, settlement_id, lake_margin_q, glaciation,
|
||||
## flooded_q, courses, cliffs}. `lake_margin_q` (T-1188) is a MessagePack
|
||||
## map key that did not exist before this codec version — an older server
|
||||
## build's payload simply omits it (`d.get("lake_margin_q")` below returns
|
||||
## null, decode_png_field() then returns an empty PackedByteArray, the
|
||||
## same "field absent -> draws as the colorize fallback" posture every
|
||||
## other optional plane on this wire already has); a client this new
|
||||
## talking to that old a server is not a supported combination anyway
|
||||
## (D-192 co-ship). The seven PNG-per-field dense planes (morphology/
|
||||
## elev_q/moisture_q/vegetation/lake_margin_q/glaciation/flooded_q) are
|
||||
## each a map {"png_bytes": [...]} — png_bytes is a Rust `Vec<u8>` with NO
|
||||
## serde_bytes annotation anywhere in this codebase (confirmed: grep for
|
||||
## serde_bytes across server/src returns nothing), so serde's blanket
|
||||
## Vec<T> impl serializes it via serialize_seq — a msgpack ARRAY of
|
||||
@@ -173,6 +180,7 @@ static func _decode_encoded_canvas(raw: Variant) -> Variant:
|
||||
if settlement_id_raw is Dictionary
|
||||
else []
|
||||
),
|
||||
"lake_margin_q": decode_png_field(d.get("lake_margin_q")),
|
||||
"glaciation": decode_png_field(d.get("glaciation")),
|
||||
"flooded_q": decode_png_field(d.get("flooded_q")),
|
||||
"courses": d.get("courses", []),
|
||||
|
||||
@@ -177,6 +177,12 @@ const GLACIATION_TINT_ALPHA: Dictionary = {
|
||||
4: 0.70, # IceCap
|
||||
}
|
||||
|
||||
## Lake-margin depth-band lightness endpoints (T-1188) — see
|
||||
## `lake_margin_lightness()`'s own doc, next to
|
||||
## `district_window_elevation_lightness()`, for the full rationale.
|
||||
const LAKE_MARGIN_LIGHTNESS_BASE: float = 0.7
|
||||
const LAKE_MARGIN_LIGHTNESS_RANGE: float = 0.3
|
||||
|
||||
|
||||
static func morphology_color(zone: int) -> Color:
|
||||
if zone >= 0 and zone < MORPHOLOGY_COLORS.size():
|
||||
@@ -289,6 +295,33 @@ static func district_window_elevation_lightness(base: Color, elev_q: int) -> Col
|
||||
return Color(base.r * lightness, base.g * lightness, base.b * lightness, base.a)
|
||||
|
||||
|
||||
## Lake-margin depth-band lightness (T-1188): the lake-shoreline counterpart
|
||||
## to `district_window_elevation_lightness()` above. Ocean coastlines get a
|
||||
## multi-tone transition band for free from `ocean_fraction_q`'s coastal
|
||||
## morphology gates (TidalFlat/DuneStrand/CliffCoast/Estuarine); a lake basin
|
||||
## sits above sea level, so `ocean_fraction_q` is always `0` there and none
|
||||
## of those gates can ever fire — the lake reads as one flat, hard-edged
|
||||
## color. `lake_margin_q` (`district_profile::DistrictProfile.lake_margin_q`,
|
||||
## `0` at/near the shoreline crossing, ramping toward `100` at a basin's deep
|
||||
## centre) is the server-derived depth signal this shades with: multiply RGB
|
||||
## by `LAKE_MARGIN_LIGHTNESS_BASE + LAKE_MARGIN_LIGHTNESS_RANGE *
|
||||
## (lake_margin_q/100)` — INVERTED relative to the elevation ramp (deep water
|
||||
## reads DARKER/richer, not lighter, matching the ocean-side reading that
|
||||
## `MORPHOLOGY_RGB_OPAQUE`'s own OpenOcean entry is already the darkest
|
||||
## water-family hue in the table). Caller-gated (only applied to Lake-zone
|
||||
## cells, matching `district_window_elevation_lightness`'s "caller decides
|
||||
## when" composition discipline — see step_canvas_colorize.gd's `_base_color`).
|
||||
## Endpoint constants declared with the rest of the const block near the top
|
||||
## of this file (gdlint's class-definitions-order rule).
|
||||
static func lake_margin_lightness(base: Color, lake_margin_q: int) -> Color:
|
||||
var clamped: int = clampi(lake_margin_q, 0, 100)
|
||||
var lightness: float = (
|
||||
(LAKE_MARGIN_LIGHTNESS_BASE + LAKE_MARGIN_LIGHTNESS_RANGE)
|
||||
- LAKE_MARGIN_LIGHTNESS_RANGE * (float(clamped) / 100.0)
|
||||
)
|
||||
return Color(base.r * lightness, base.g * lightness, base.b * lightness, base.a)
|
||||
|
||||
|
||||
## Vegetation green-family ramp (D-226 T-1124 amendment §5). Marine (6)
|
||||
## returns Color.TRANSPARENT — "lets the morphology water-blue show through"
|
||||
## per the amendment; the caller must skip the draw_rect entirely on a
|
||||
|
||||
@@ -35,16 +35,24 @@ const COLOR_MOISTURE_WET: Color = Color(0.25, 0.72, 0.65, 1.0)
|
||||
const REGION_TEMP_NONE_DC: int = AtlasOverlayColors.REGION_TEMP_NONE_DC
|
||||
|
||||
|
||||
## One decoded plane set, pre-extracted from the four L8 Images + the two
|
||||
## MorphologyZone::Lake discriminant (D-239 §6 order, T-1046) — matches
|
||||
## AtlasOverlayColors.MORPHOLOGY_LAKE (duplicated here per this file's own
|
||||
## "no cross-file constant sharing beyond the preloaded module" precedent,
|
||||
## same as step_canvas_protocol.gd's status-decode duplication rationale).
|
||||
const MORPHOLOGY_LAKE: int = 1
|
||||
|
||||
## 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
|
||||
## (see StepCanvasTerrainLayer.build_texture()), not re-decoded per pixel.
|
||||
## `temp_dc`/`settlement_id` stay as plain Arrays (their domain doesn't fit
|
||||
## a byte plane — see step_canvas_protocol.gd's own doc).
|
||||
## a byte plane — see step_canvas_protocol.gd's own doc). `lake_margin_q`
|
||||
## added T-1188 (the lake-shoreline tone source).
|
||||
class CellPlanes:
|
||||
var morphology: Image
|
||||
var elev_q: Image
|
||||
var moisture_q: Image
|
||||
var vegetation: Image
|
||||
var lake_margin_q: Image
|
||||
var glaciation: Image
|
||||
var temp_dc: Array
|
||||
var width: int
|
||||
@@ -72,8 +80,16 @@ static func cell_color(planes: CellPlanes, col: int, row: int, active_toggle: St
|
||||
|
||||
static func _base_color(planes: CellPlanes, col: int, row: int) -> Color:
|
||||
var zone: int = _l8_value(planes.morphology, col, row)
|
||||
var eq: int = _l8_value(planes.elev_q, col, row)
|
||||
var base: Color = AtlasOverlayColors.district_window_morphology_color(zone)
|
||||
# T-1188: Lake cells shade by lake_margin_q (the settled-hydrology depth
|
||||
# band), NOT elev_q — elev_q on a Lake cell is the ORIGINAL bedrock
|
||||
# elevation under the water (see DistrictProfile.elev_q's own doc), an
|
||||
# unrelated signal that would shade the wrong thing. Every other zone
|
||||
# keeps the existing elev_q lightness read unchanged.
|
||||
if zone == MORPHOLOGY_LAKE:
|
||||
var lmq: int = _l8_value(planes.lake_margin_q, col, row)
|
||||
return AtlasOverlayColors.lake_margin_lightness(base, lmq)
|
||||
var eq: int = _l8_value(planes.elev_q, col, row)
|
||||
return AtlasOverlayColors.district_window_elevation_lightness(base, eq)
|
||||
|
||||
|
||||
|
||||
@@ -86,9 +86,11 @@ func rebuild_from_canvas(canvas: Dictionary, rung: String, active_toggle: String
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Decode the four L8-plane PackedByteArrays (via Image.load_png_from_buffer,
|
||||
## per step_canvas_protocol.gd's own PNG-per-field wire note) plus the two
|
||||
## raw arrays into one CellPlanes bundle, once per rebuild.
|
||||
## Decode the five L8-plane PackedByteArrays (morphology/elev_q/moisture_q/
|
||||
## vegetation/lake_margin_q — glaciation is the sixth, decoded separately
|
||||
## below) via Image.load_png_from_buffer (per step_canvas_protocol.gd's own
|
||||
## PNG-per-field wire note) plus the raw temp_dc array into one CellPlanes
|
||||
## bundle, once per rebuild. `lake_margin_q` added T-1188.
|
||||
func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasColorize.CellPlanes:
|
||||
var planes := StepCanvasColorize.CellPlanes.new()
|
||||
planes.width = width
|
||||
@@ -97,6 +99,7 @@ func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasCo
|
||||
planes.elev_q = _decode_l8_plane(canvas.get("elev_q"))
|
||||
planes.moisture_q = _decode_l8_plane(canvas.get("moisture_q"))
|
||||
planes.vegetation = _decode_l8_plane(canvas.get("vegetation"))
|
||||
planes.lake_margin_q = _decode_l8_plane(canvas.get("lake_margin_q"))
|
||||
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
|
||||
planes.temp_dc = canvas.get("temp_dc", [])
|
||||
return planes
|
||||
|
||||
Reference in New Issue
Block a user