feat(ui): T-1118 climate overlay + T-1119 quarter glyphs — client halves, with file-size extractions
T-1118: gen_region_grid overlay (label TMP) — _draw_gen_region_grid copies _draw_gen_district's self-contained mapping (dims from the layer dict); mean-temp cold-to-hot ramp over -50..+50 C; REGION_TEMP_NONE_DC airless sentinel = skip-cell (undrawn, never an invented color); legend entry. T-1119 (touch points 3-6): quarter_footprints protocol passthrough; gen_l4_quarters overlay (label QTR) — density-scaled glyph anchored on the L3 settlement dot joined by city_id, shape = dominant district type (corner-tab/diamond marks for Commercial/Industrial/ Administrative), color = density ramp on the settlement-gold family, zoom-gated at SETTLEMENT_LABEL_MIN_ZOOM; landmark/corridor counts never drawn (D-226(d) tooltip-only ceiling); legend entry. Structure: atlas_viewer.gd and protocol.gd were over gdlint's 1000-line cap before this batch; cleanly-separable responsibilities extracted on existing precedent — atlas_generation_state.gd (per-layer data + accessors), atlas_generation_proxy.gd (polling/retry/pending machinery), atlas_overlay_colors.gd (pure ramp/shape lookups), atlas_map_protocol.gd (atlas/starmap/citynames codec, the browse_protocol.gd delegate pattern). Public APIs preserved exactly; _gen_state stays a field default (RefCounted, pre-_ready safe) because _ready()-construction breaks every bare AtlasViewer.new() test — documented inline. Tests: registration + round-trip for both overlays; pure-function suites for the temp ramp (endpoints/midpoint/clamp/sentinel) and quarter glyph (scaling, zoom gate, ramp, notch across all 9 DistrictTypes); Tier-2 replay asserts exact literals from the real server-generated fixture incl. the airless sentinel. Color.lerp(a,b, 1.0) is not bit-exact to b — endpoint assertions use per-component is_equal_approx. Full suite 3094/3094; gdlint zero warnings incl. the two previously-over-cap files. Live capture: legend grows to 7 sections, TMP/QTR toggles clean against a live server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,10 @@ extends GdUnitTestSuite
|
||||
# resource rather than a global identifier.
|
||||
const LegendPanelScript := preload("res://ui/implant/apps/atlas/atlas_legend_panel.gd")
|
||||
|
||||
# T-1118/T-1119 pure color-ramp/shape-selection helpers (no class_name, same
|
||||
# rationale as LegendPanelScript above) — see atlas_overlay_colors.gd.
|
||||
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
|
||||
|
||||
|
||||
func test_generation_overlays_registered() -> void:
|
||||
var ids: Array = []
|
||||
@@ -141,9 +145,193 @@ func test_settlements_round_trip() -> void:
|
||||
assert_that((decoded as Dictionary).get("settlements")).is_equal(settlements)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1118: gen_region_grid overlay (region climate grid, mean-temp channel)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_region_grid_overlay_registered() -> void:
|
||||
var ids: Array = []
|
||||
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
|
||||
ids.append(d["id"])
|
||||
assert_that(ids).contains(["gen_region_grid"])
|
||||
|
||||
|
||||
## RegionGridLayer shape pinned to server/src/atlas/layer_proxy.rs (T-1113):
|
||||
## dense row-major, mean_temp_dc is deci-degC with REGION_TEMP_NONE_DC
|
||||
## (i16::MIN) the airless sentinel.
|
||||
func test_region_grid_round_trips() -> void:
|
||||
var v: AtlasViewer = auto_free(AtlasViewer.new())
|
||||
assert_that(v.get_generation_region_grid()).is_null()
|
||||
var grid := {
|
||||
"cols": 2,
|
||||
"rows": 1,
|
||||
"season": [0, 3],
|
||||
"weather": [0, 2],
|
||||
"mean_temp_dc": [123, AtlasOverlayColors.REGION_TEMP_NONE_DC],
|
||||
"moisture_q": [80, 5],
|
||||
}
|
||||
v.set_generation_region_grid(grid)
|
||||
assert_that(v.get_generation_region_grid()).is_equal(grid)
|
||||
var decoded: Variant = Protocol.atlas_response_from_raw(
|
||||
{"body_id": "GJ1c", "status": "Ready", "region_grid": grid}
|
||||
)
|
||||
assert_that((decoded as Dictionary).get("region_grid")).is_equal(grid)
|
||||
|
||||
|
||||
## AtlasOverlayColors.region_temp_color() pure ramp — the one visual channel
|
||||
## this ticket ships (mean temp only; season/weather/moisture deferred).
|
||||
## Compares components with is_equal_approx() rather than whole-Color
|
||||
## is_equal(): Godot's Color.lerp(a, b, 1.0) is NOT bit-exact to b (confirmed
|
||||
## empirically — the two print identically but == is false at the ULP
|
||||
## level), so an exact Color equality check is the wrong tool at a lerp
|
||||
## boundary regardless of whether the ramp math itself is correct.
|
||||
func test_region_temp_color_ramp() -> void:
|
||||
# Cold end clamps to pure cold color.
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.region_temp_color(AtlasOverlayColors.REGION_TEMP_MIN_DC),
|
||||
AtlasOverlayColors.COLOR_REGION_TEMP_COLD
|
||||
)
|
||||
# Hot end clamps to pure hot color.
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.region_temp_color(AtlasOverlayColors.REGION_TEMP_MAX_DC),
|
||||
AtlasOverlayColors.COLOR_REGION_TEMP_HOT
|
||||
)
|
||||
# Midpoint (0.0 C) lands on the mid color.
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.region_temp_color(0), AtlasOverlayColors.COLOR_REGION_TEMP_MID
|
||||
)
|
||||
# Out-of-band readings clamp rather than extrapolate past the endpoints.
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.region_temp_color(-9999), AtlasOverlayColors.COLOR_REGION_TEMP_COLD
|
||||
)
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.region_temp_color(9999), AtlasOverlayColors.COLOR_REGION_TEMP_HOT
|
||||
)
|
||||
|
||||
|
||||
func _assert_color_approx(actual: Color, expected: Color) -> void:
|
||||
assert_float(actual.r).is_equal_approx(expected.r, 0.0001)
|
||||
assert_float(actual.g).is_equal_approx(expected.g, 0.0001)
|
||||
assert_float(actual.b).is_equal_approx(expected.b, 0.0001)
|
||||
assert_float(actual.a).is_equal_approx(expected.a, 0.0001)
|
||||
|
||||
|
||||
## The airless sentinel is a SKIP-CELL disposition (documented on
|
||||
## REGION_TEMP_NONE_DC and _draw_gen_region_grid) — the renderer never calls
|
||||
## region_temp_color() for it at all, so there is no "sentinel color" to
|
||||
## assert on. This test instead pins the sentinel's numeric identity, which
|
||||
## is what _draw_gen_region_grid's equality check depends on.
|
||||
func test_region_temp_none_sentinel_is_i16_min() -> void:
|
||||
assert_int(AtlasOverlayColors.REGION_TEMP_NONE_DC).is_equal(-32768)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1119: gen_l4_quarters overlay (quarter-footprint glyph, D-226 T-1112 amendment)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_quarter_footprints_overlay_registered() -> void:
|
||||
var ids: Array = []
|
||||
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
|
||||
ids.append(d["id"])
|
||||
assert_that(ids).contains(["gen_l4_quarters"])
|
||||
|
||||
|
||||
## QuarterFootprintLayer shape pinned to server/src/atlas/layer_proxy.rs
|
||||
## (D-226 T-1112 amendment SS1): entries keyed by city_id (BTreeMap<u64,_> on
|
||||
## the wire, decodes to a Dictionary with int keys), five scalar u8/enum
|
||||
## fields per entry, no per-block detail (SS2 hard ceiling).
|
||||
func test_quarter_footprints_round_trips() -> void:
|
||||
var v: AtlasViewer = auto_free(AtlasViewer.new())
|
||||
assert_that(v.get_generation_quarter_footprints()).is_null()
|
||||
var footprints := {
|
||||
"entries":
|
||||
{
|
||||
1:
|
||||
{
|
||||
"city_id": 1,
|
||||
"density_avg_pct": 62,
|
||||
"dominant_district_type": "Commercial",
|
||||
"dominant_zoning": "Commercial",
|
||||
"landmark_count": 3,
|
||||
"corridor_count": 4,
|
||||
},
|
||||
2:
|
||||
{
|
||||
"city_id": 2,
|
||||
"density_avg_pct": 18,
|
||||
"dominant_district_type": "Residential",
|
||||
"dominant_zoning": "Residential",
|
||||
"landmark_count": 0,
|
||||
"corridor_count": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
v.set_generation_quarter_footprints(footprints)
|
||||
assert_that(v.get_generation_quarter_footprints()).is_equal(footprints)
|
||||
var decoded: Variant = Protocol.atlas_response_from_raw(
|
||||
{"body_id": "GJ1c", "status": "Ready", "quarter_footprints": footprints}
|
||||
)
|
||||
assert_that((decoded as Dictionary).get("quarter_footprints")).is_equal(footprints)
|
||||
|
||||
|
||||
## AtlasOverlayColors.quarter_glyph_size() — density-scaled, clamped, and
|
||||
## forced to minimum when the zoom gate (show_notch) is off regardless of
|
||||
## density (D-226 T-1112 amendment SS3: "below [the threshold], draws at
|
||||
## minimum size with color only").
|
||||
func test_quarter_glyph_size() -> void:
|
||||
# 0% density, notch shown -> base size.
|
||||
assert_float(AtlasOverlayColors.quarter_glyph_size(0, true)).is_equal_approx(
|
||||
AtlasOverlayColors.QUARTER_GLYPH_MIN_SIZE, 0.001
|
||||
)
|
||||
# 100% density, notch shown -> max size (4.0 + 1.0*6.0 = 10.0, under the 12.0 cap).
|
||||
assert_float(AtlasOverlayColors.quarter_glyph_size(100, true)).is_equal_approx(10.0, 0.001)
|
||||
# Below the zoom gate: always minimum, regardless of density.
|
||||
assert_float(AtlasOverlayColors.quarter_glyph_size(100, false)).is_equal_approx(
|
||||
AtlasOverlayColors.QUARTER_GLYPH_MIN_SIZE, 0.001
|
||||
)
|
||||
# Fixture literals: city 1 (62%) and city 2 (18%), both notch-shown.
|
||||
assert_float(AtlasOverlayColors.quarter_glyph_size(62, true)).is_greater(
|
||||
AtlasOverlayColors.quarter_glyph_size(18, true)
|
||||
)
|
||||
|
||||
|
||||
## AtlasOverlayColors.quarter_glyph_color() — single-hue ramp within the
|
||||
## settlement-gold family, endpoints pinned to the D-226 T-1112 amendment
|
||||
## SS3's literal legend colors. Approx compare (see test_region_temp_color_ramp's
|
||||
## comment) — a lerp(a, b, 1.0) isn't guaranteed bit-exact to b for every
|
||||
## color pair, and exact equality shouldn't depend on which pair happens to
|
||||
## round losslessly.
|
||||
func test_quarter_glyph_color_ramp() -> void:
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.quarter_glyph_color(0), AtlasOverlayColors.COLOR_QUARTER_LOW_DENSITY
|
||||
)
|
||||
_assert_color_approx(
|
||||
AtlasOverlayColors.quarter_glyph_color(100), AtlasOverlayColors.COLOR_QUARTER_HIGH_DENSITY
|
||||
)
|
||||
|
||||
|
||||
## AtlasOverlayColors.quarter_notch_kind() — the 3-variant-plus-plain cap
|
||||
## (D-226 T-1112 amendment SS3: "a coarse skeleton read, not a legend of
|
||||
## every DistrictType"). Every other DistrictType — including an unknown/
|
||||
## empty string — falls through to "plain".
|
||||
func test_quarter_notch_kind_selection() -> void:
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind("Commercial")).is_equal("commercial")
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind("Industrial")).is_equal("industrial")
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind("Administrative")).is_equal("administrative")
|
||||
for other in ["LogisticsHub", "Residential", "Entertainment", "MixedUse", "Transit", "Specialized"]:
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind(other)).override_failure_message(
|
||||
"DistrictType '%s' should read as plain (not one of the 3 marked variants)" % other
|
||||
).is_equal("plain")
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind("")).is_equal("plain")
|
||||
assert_str(AtlasOverlayColors.quarter_notch_kind("SomeUnrecognizedFutureVariant")).is_equal("plain")
|
||||
|
||||
|
||||
## Tier-2 replay: a REAL server-generated msgpack blob (server/tests/gen_fixtures.rs
|
||||
## generate_atlas_layer_response_fixtures, regenerated 2026-07-14 with T-960's
|
||||
## road_graph/settlements populated) decoded through the actual client path —
|
||||
## generate_atlas_layer_response_fixtures, regenerated 2026-07-18 with T-960's
|
||||
## road_graph/settlements, T-1118's region_grid, and T-1119's
|
||||
## quarter_footprints all populated) decoded through the actual client path —
|
||||
## the strongest check that protocol.gd's decode matches the server's wire
|
||||
## encoding, not just a hand-authored Dictionary the client wrote itself.
|
||||
func test_atlas_response_ready_fixture_decodes_road_graph_and_settlements() -> void:
|
||||
@@ -180,6 +368,83 @@ func test_atlas_response_ready_fixture_decodes_road_graph_and_settlements() -> v
|
||||
assert_str(minor.get("size_class")).is_equal("Minor")
|
||||
assert_bool(minor.get("is_capital")).is_false()
|
||||
|
||||
# T-1118: region_grid, a 2x1 grid (dudley-depth's fixture literal,
|
||||
# 2026-07-18) — col0 Summer/Clear/12.3C(123 deci-C)/moisture 80, col1
|
||||
# Winter/Snow/airless-sentinel/moisture 5. season/weather/mean_temp_dc/
|
||||
# moisture_q are all "dense array of ints" fields (rmp_serde Vec<u8>/
|
||||
# Vec<i16> with no serde_bytes, so NOT bin_8/16/32 on the wire) — read
|
||||
# element-wise via int() rather than asserting a specific Godot container
|
||||
# type, since the addon decodes a plain msgpack array as Array, not
|
||||
# PackedByteArray (see _dense_int helper below).
|
||||
var region_grid: Dictionary = response.get("region_grid")
|
||||
assert_that(region_grid).is_not_null()
|
||||
assert_int(int(region_grid.get("cols", 0))).is_equal(2)
|
||||
assert_int(int(region_grid.get("rows", 0))).is_equal(1)
|
||||
assert_int(_dense_int(region_grid.get("mean_temp_dc"), 0)).is_equal(123)
|
||||
assert_int(_dense_int(region_grid.get("mean_temp_dc"), 1)).is_equal(
|
||||
AtlasOverlayColors.REGION_TEMP_NONE_DC
|
||||
)
|
||||
assert_int(_dense_int(region_grid.get("moisture_q"), 0)).is_equal(80)
|
||||
assert_int(_dense_int(region_grid.get("moisture_q"), 1)).is_equal(5)
|
||||
|
||||
# T-1119: quarter_footprints, keyed by city_id (BTreeMap<u64,_> on the
|
||||
# wire -> Dictionary with int keys). city_id 1 (Port Aldren, capital) gets
|
||||
# a "rich" entry; city_id 2 (Farmstead Rell, minor) gets a deliberately
|
||||
# sparse one (zero landmarks, one corridor) — both present, per
|
||||
# dudley-depth's fixture literal, exercising the subset-safety contract
|
||||
# (entries can legitimately be a SUBSET of settlements — not tested here
|
||||
# since both happen to be present in this fixture, but the accessor path
|
||||
# must not assume 1:1).
|
||||
var quarters: Dictionary = response.get("quarter_footprints")
|
||||
assert_that(quarters).is_not_null()
|
||||
var qf_entries: Dictionary = quarters.get("entries", {})
|
||||
assert_int(qf_entries.size()).is_equal(2)
|
||||
var rich: Dictionary = qf_entries[1]
|
||||
assert_int(int(rich.get("density_avg_pct", -1))).is_equal(62)
|
||||
assert_str(str(rich.get("dominant_district_type", ""))).is_equal("Commercial")
|
||||
assert_str(str(rich.get("dominant_zoning", ""))).is_equal("Commercial")
|
||||
assert_int(int(rich.get("landmark_count", -1))).is_equal(3)
|
||||
assert_int(int(rich.get("corridor_count", -1))).is_equal(4)
|
||||
var sparse: Dictionary = qf_entries[2]
|
||||
assert_int(int(sparse.get("density_avg_pct", -1))).is_equal(18)
|
||||
assert_str(str(sparse.get("dominant_district_type", ""))).is_equal("Residential")
|
||||
assert_str(str(sparse.get("dominant_zoning", ""))).is_equal("Residential")
|
||||
assert_int(int(sparse.get("landmark_count", -1))).is_equal(0)
|
||||
assert_int(int(sparse.get("corridor_count", -1))).is_equal(1)
|
||||
|
||||
|
||||
## pending/not_found fixtures carry region_grid/quarter_footprints as None too
|
||||
## (mirrors every other Option field's "unrun layer" treatment) — a quick
|
||||
## sanity check that the new fields don't silently break the OTHER two
|
||||
## fixtures' decode (they were regenerated in the same batch).
|
||||
func test_atlas_response_pending_and_not_found_fixtures_have_no_new_layers() -> void:
|
||||
for fixture_name in ["atlas_response_pending", "atlas_response_not_found"]:
|
||||
var path := "res://tests/fixtures/msgpack/%s.msgpack" % fixture_name
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
assert_that(f).override_failure_message("missing fixture %s" % fixture_name).is_not_null()
|
||||
var bytes := f.get_buffer(f.get_length())
|
||||
f.close()
|
||||
var decoded: Variant = Protocol.decode_atlas_layer_response(bytes)
|
||||
assert_that(decoded).is_not_null()
|
||||
var response: Dictionary = decoded
|
||||
assert_that(response.get("region_grid")).override_failure_message(
|
||||
"%s should carry no region_grid" % fixture_name
|
||||
).is_null()
|
||||
assert_that(response.get("quarter_footprints")).override_failure_message(
|
||||
"%s should carry no quarter_footprints" % fixture_name
|
||||
).is_null()
|
||||
|
||||
|
||||
## Reads element `i` from a decoded "dense numeric array" field regardless of
|
||||
## whether the messagepack addon produced a PackedByteArray (bin_8/16/32) or a
|
||||
## plain Array (fixarray/array_16/array_32) — rmp_serde without serde_bytes
|
||||
## encodes Vec<u8>/Vec<i16> as the latter, so this is the defensively-correct
|
||||
## read for region_grid's season/weather/mean_temp_dc/moisture_q.
|
||||
static func _dense_int(arr: Variant, i: int) -> int:
|
||||
if arr is Array or arr is PackedByteArray:
|
||||
return int(arr[i])
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# D-226 item 3: generation legend panel
|
||||
|
||||
Reference in New Issue
Block a user