feat(ui): T-1138 regional map screen — click-through descent, fixed planetary view, windowed composite (D-226 T-1124 SS5)

Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now
FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_*
capture API survives for the golden harness); hover shows a
not-to-scale bracket reticle with the real extent labeled (a true
n=32 rectangle is sub-pixel on the planetary canvas — the honest
representation given the morph transition is deferred), and a click
that misses every city marker descends (city-click wins — one
gesture, two contextual reads, no modifier). Descent pushes a new
'district' nav screen centered on the click point's DistrictPos via
atlas_descend_geometry.district_pos_at (the pixel-to-district inverse
of the server mapping, verified against scale.rs).

Regional mode: atlas_window_viewer draws the composite (morphology x
elev_q lightness base; temp/moisture/veg toggles — temp reuses the
region-ramp colorizer exactly; Marine=6 transparent; glaciation
always-on tint matching apply_ice_tint's REAL gate, None|Light no-op,
over the amendment's looser prose — documented); pan-on-held-composite
with edge-crossing refetch + border-fade during the queue-based
derive wait; zoom never refetches. atlas_window_cache: LRU keyed
(body_id, center, n), touch-on-read, evict-only, no freshness (D-227).
atlas_window_request mirrors the generation-proxy pending-retry shape
for None-until-derived. Codec: window params omitted from the wire
when absent — byte-identical for every existing caller.

Live-verified against the T-1137 server in-worktree: real round-trip
on a GJ380c coastal district (6 fields x 1024 cells), echo staleness
guard, genuine ~1.4s background-derive wait, pan-edge refetch to an
adjacent window, cache-hit on re-descent with zero network. Full
client suite 3194/3194; gdlint clean on all 18 files.

Open follow-ups flagged in-code: header location label always falls
back to coordinates (nearest-settlement needs a join the district
window does not carry); atlas_standalone.gd's 'atlas_app.gd is never
modified' doc line is now imprecise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:32:06 +02:00
co-authored by Claude Fable 5
parent 172ce124c8
commit 9e1e6db614
19 changed files with 2228 additions and 72 deletions
+70
View File
@@ -12,6 +12,12 @@
class_name TestAtlasDataDelivery
extends GdUnitTestSuite
# T-1138: REGION_TEMP_NONE_DC sentinel reused verbatim for district_window's
# temp_dc field (D-226 T-1124 amendment §2 — one colorizer/sentinel scheme
# across both zoom levels). No class_name on atlas_overlay_colors.gd (review
# #8 precedent elsewhere in this suite) — preloaded by path.
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
# =============================================================================
# Encode/decode round-trips
@@ -177,6 +183,70 @@ func test_atlas_response_road_graph_and_settlements_default_null() -> void:
assert_that((decoded as Dictionary).get("settlements")).is_null()
# =============================================================================
# T-1138 (D-226 T-1124 amendment): windowed district-resolution regional map
# =============================================================================
## §1: window_center/window_n are OMITTED (not sent as null) when no window is
## requested — this is what makes an old-shaped call (every whole-body-layer
## call site) byte-identical to pre-T-1138 wire traffic.
func test_encode_atlas_layer_request_omits_window_fields_by_default() -> void:
var bytes := Protocol.encode_atlas_layer_request("GJ1c", "Topography")
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
assert_bool(decoded.value.has("window_center")).is_false()
assert_bool(decoded.value.has("window_n")).is_false()
## §1: a windowed request carries window_center as a [row, col] pair (the same
## int-pair convention every other position field on this channel already
## uses — road_graph nodes, settlements, Layer-1 river cells) and window_n
## verbatim, unclamped (the server owns the [1, DISTRICT_WINDOW_MAX_N] clamp).
func test_encode_atlas_layer_request_carries_window_params() -> void:
var bytes := Protocol.encode_atlas_layer_request(
"GJ1c", "Topography", Vector2i(140, 260), 32
)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
assert_that(decoded.value.get("body_id")).is_equal("GJ1c")
assert_that(decoded.value.get("window_center")).is_equal([140, 260])
assert_that(decoded.value.get("window_n")).is_equal(32)
## §2: district_window is a distinct payload (echoes center/n for the
## client's staleness guard) but the codec passthrough is the same shape as
## every sibling layer — raw.get(), no reshaping. Field types follow the
## established district_grid/region_grid convention: u8 arrays decode as
## PackedByteArray, i16 (temp_dc, REGION_TEMP_NONE_DC sentinel scheme) as a
## plain Array (test_region_grid_round_trips' mean_temp_dc precedent).
func test_atlas_response_district_window_passthrough() -> void:
var window := {
"center": [140, 260],
"n": 32,
"morphology": PackedByteArray([8, 14, 0, 5]),
"elev_q": PackedByteArray([40, 62, 5, 88]),
"temp_dc": [120, 95, AtlasOverlayColors.REGION_TEMP_NONE_DC, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
var raw := {"body_id": "GJ1c", "status": "Ready", "district_window": window}
var decoded: Variant = Protocol.atlas_response_from_raw(raw)
assert_that(decoded).is_not_null()
assert_that((decoded as Dictionary).get("district_window")).is_equal(window)
## A body with no window requested (or not yet derived — §1's background-queue
## serving model: the completion may not have landed yet) must decode with
## district_window absent -> null, same "layer hasn't produced yet" contract
## every other Option layer already has.
func test_atlas_response_district_window_default_null() -> void:
var decoded: Variant = Protocol.atlas_response_from_raw({"body_id": "GJ1c", "status": "Ready"})
assert_that(decoded).is_not_null()
assert_that((decoded as Dictionary).get("district_window")).is_null()
# =============================================================================
# SimBridge routing — receive_bytes emits the right signal with the right value
# =============================================================================
+162
View File
@@ -0,0 +1,162 @@
## T-1138 (D-226 T-1124 amendment §5 entry revision): tests for the planetary
## AtlasViewer's click-through descent — the fixed view (no drag-pan/wheel-
## zoom), the pixel-to-DistrictPos inverse mapping (atlas_descend_geometry.gd),
## and the city-click-wins disambiguation rule.
class_name TestAtlasDescendEntry
extends GdUnitTestSuite
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
# =============================================================================
# atlas_descend_geometry.gd — pure geometry (no scene tree needed)
# =============================================================================
## The default n=32 window's real footprint is 32 * 2.048 km = 65.536 km, so
## the label reads "~66 x 66 km" (rounded) — pins the number the amendment's
## "honest labeling" resolution depends on.
func test_reticle_label_extent_matches_district_window_default_n() -> void:
var label: Dictionary = AtlasDescendGeometry.reticle_label(Vector2(100.0, 100.0))
var expected_km: float = 32.0 * 2048.0 / 1000.0
assert_str(label["text"]).is_equal("~%.0f × %.0f km" % [expected_km, expected_km])
func test_reticle_label_position_offsets_from_center() -> void:
var center := Vector2(50.0, 60.0)
var label: Dictionary = AtlasDescendGeometry.reticle_label(center)
var pos: Vector2 = label["position"]
assert_float(pos.x).is_greater(center.x) # offset to the right, per §5's resolution
## Eight segments (four L-shaped bracket corners, two arms each) — every
## segment's "from" endpoint is exactly one of the four corner anchors, and no
## segment has zero length (a degenerate reticle would be invisible).
func test_reticle_segments_has_eight_nonzero_segments() -> void:
var segments: Array = AtlasDescendGeometry.reticle_segments(Vector2(200.0, 150.0))
assert_int(segments.size()).is_equal(8)
for seg: Array in segments:
assert_that(seg[0]).override_failure_message(
"a reticle segment must not be zero-length"
).is_not_equal(seg[1])
## The reticle's overall bounding box is centered on `center` and sized by
## DESCEND_RETICLE_SIZE — a regression guard against an off-center or
## mis-scaled bracket.
func test_reticle_segments_centered_on_input_point() -> void:
var center := Vector2(300.0, 300.0)
var segments: Array = AtlasDescendGeometry.reticle_segments(center)
var min_pt := Vector2(INF, INF)
var max_pt := Vector2(-INF, -INF)
for seg: Array in segments:
for p: Vector2 in seg:
min_pt.x = minf(min_pt.x, p.x)
min_pt.y = minf(min_pt.y, p.y)
max_pt.x = maxf(max_pt.x, p.x)
max_pt.y = maxf(max_pt.y, p.y)
var bbox_center: Vector2 = (min_pt + max_pt) * 0.5
assert_float(bbox_center.x).is_equal_approx(center.x, 0.01)
assert_float(bbox_center.y).is_equal_approx(center.y, 0.01)
# =============================================================================
# district_pos_at — pixel-to-DistrictPos inverse mapping
# =============================================================================
## No radius (tiny test body fallback, matches the server's own derive_district
## fallback): the district grid IS the heightmap grid 1:1, so a canvas point
## maps to the nearest integer district coordinate directly.
func test_district_pos_at_no_radius_is_1to1_pixel_mapping() -> void:
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(12.4, 7.6), 100.0, 100.0, 0.0
)
assert_that(pos).is_equal(Vector2i(12, 8))
func test_district_pos_at_zero_texture_size_is_safe() -> void:
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(Vector2(10.0, 10.0), 0.0, 0.0, 6371.0)
assert_that(pos).is_equal(Vector2i.ZERO)
## The equatorial center of the texture (px = tex_w/2) is district column
## district_cols/2 on a body with a radius (equator wraps at district (0,0)
## per the server's own doc: "district (0,0) sits at lon 0 / the equator").
## The vertical center (py = tex_h/2) is the equator row (row 0).
func test_district_pos_at_center_of_texture_is_near_equator_row_zero() -> void:
var radius_km := 6371.0
var tex_w := 1024.0
var tex_h := 512.0
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(0.0, tex_h * 0.5), tex_w, tex_h, radius_km
)
assert_int(pos.y).override_failure_message(
"the vertical texture center must map to row 0 (the equator)"
).is_equal(0)
## Panning across the texture's full width sweeps through the FULL district
## column range (not clamped to a tiny sub-range) — a coarse sanity check that
## district_cols is actually being derived from the body's circumference, not
## left at some degenerate default.
func test_district_pos_at_sweeps_full_column_range_across_texture_width() -> void:
var radius_km := 6371.0
var tex_w := 1024.0
var tex_h := 512.0
var left: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(0.0, tex_h * 0.5), tex_w, tex_h, radius_km
)
var right: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(tex_w - 1.0, tex_h * 0.5), tex_w, tex_h, radius_km
)
# An Earth-radius body has thousands of equatorial districts (circumference
# ~40,075 km / 2.048 km per district ≈ 19,568) — near-full-width should
# sweep a large fraction of that, not a handful of columns.
assert_int(absi(right.x - left.x)).is_greater(1000)
# =============================================================================
# AtlasViewer — fixed view (no drag-pan/wheel-zoom) + click-through descent
# =============================================================================
## T-1120 capture API (set_view/get_view_offset/get_view_zoom) must survive
## the removal of user pan/zoom — this is the ticket's own explicit note, and
## the visual-golden harness depends on it.
func test_set_view_still_works_after_pan_zoom_removal() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v.set_view(3.0, Vector2(50.0, -20.0))
assert_that(v.get_view_zoom()).is_equal_approx(3.0, 0.001)
assert_that(v.get_view_offset()).is_equal(Vector2(50.0, -20.0))
## Clicking (with no heightmap loaded — the guard AtlasViewer's _gui_input
## checks first) must not emit a descend request — there is nothing to
## descend into yet.
func test_no_descend_signal_without_a_loaded_heightmap() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
# _heightmap_texture stays null (no show_body() call) — _gui_input's
# early-return guard should prevent any click handling at all.
var mb := InputEventMouseButton.new()
mb.button_index = MOUSE_BUTTON_LEFT
mb.pressed = true
mb.position = Vector2(100.0, 100.0)
v._gui_input(mb)
assert_int(received.size()).is_equal(0)
## RegionalScreen forwards AtlasViewer's district_descend_requested verbatim —
## the nav-stack wiring atlas_app.gd depends on.
func test_regional_screen_forwards_district_descend_requested() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
var received: Array = []
screen.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
screen._viewer.district_descend_requested.emit(Vector2i(5, 7))
assert_int(received.size()).is_equal(1)
assert_that(received[0]).is_equal(Vector2i(5, 7))
+121
View File
@@ -0,0 +1,121 @@
## T-1138 (D-226 T-1124 amendment §4): tests for the client-side
## DistrictWindowLayer LRU cache — keyed on (body_id, center, n), LRU-evict
## only (D-227 determinism means no freshness check is ever needed).
class_name TestAtlasWindowCache
extends GdUnitTestSuite
const AtlasWindowCache := preload("res://ui/implant/apps/atlas/atlas_window_cache.gd")
func test_make_key_distinguishes_body_center_and_n() -> void:
var k1 := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
var k2 := AtlasWindowCache.make_key("GJ1d", Vector2i(10, 20), 32) # different body
var k3 := AtlasWindowCache.make_key("GJ1c", Vector2i(11, 20), 32) # different center
var k4 := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 64) # different n
assert_str(k1).is_not_equal(k2)
assert_str(k1).is_not_equal(k3)
assert_str(k1).is_not_equal(k4)
func test_miss_returns_null_and_has_reports_false() -> void:
var cache := AtlasWindowCache.new()
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_null()
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).is_false()
func test_put_then_get_round_trips_exact_window() -> void:
var cache := AtlasWindowCache.new()
var window := {"center": [10, 20], "n": 32, "morphology": PackedByteArray([1, 2, 3])}
cache.put("GJ1c", Vector2i(10, 20), 32, window)
assert_bool(cache.has("GJ1c", Vector2i(10, 20), 32)).is_true()
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32)).is_equal(window)
## D-227: a window fetched once is valid FOREVER for that (body, center, n) —
## no expiry, no invalidation path. Repeated get_window() calls across a
## simulated time gap (just repeated calls here — there is no clock in this
## cache at all) must keep returning the same stored value.
func test_cached_window_never_expires() -> void:
var cache := AtlasWindowCache.new()
var window := {"center": [0, 0], "n": 32}
cache.put("GJ1c", Vector2i(0, 0), 32, window)
for _i in range(50):
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_equal(window)
## Different (body_id, center, n) tuples never collide — this is the whole
## point of the composite key (§4: "cacheable client-side keyed on
## (body_id, center, n)").
func test_different_bodies_same_center_do_not_collide() -> void:
var cache := AtlasWindowCache.new()
var window_a := {"body": "GJ1c"}
var window_b := {"body": "GJ1d"}
cache.put("GJ1c", Vector2i(10, 20), 32, window_a)
cache.put("GJ1d", Vector2i(10, 20), 32, window_b)
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32)).is_equal(window_a)
assert_that(cache.get_window("GJ1d", Vector2i(10, 20), 32)).is_equal(window_b)
## Overwriting the same key replaces the value (e.g. a re-fetch of an
## already-cached window from a server that somehow returns a different
## payload — should never happen under D-227, but the cache itself must not
## silently keep the stale one on an explicit put()).
func test_put_overwrites_existing_key() -> void:
var cache := AtlasWindowCache.new()
cache.put("GJ1c", Vector2i(0, 0), 32, {"v": 1})
cache.put("GJ1c", Vector2i(0, 0), 32, {"v": 2})
assert_int(cache.size()).is_equal(1)
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_equal({"v": 2})
# =============================================================================
# LRU eviction
# =============================================================================
func test_eviction_drops_least_recently_used_on_overflow() -> void:
var cache := AtlasWindowCache.new(2) # max 2 entries
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
cache.put("GJ1c", Vector2i(2, 0), 32, {"id": "c"}) # evicts (0,0) — oldest, untouched
assert_int(cache.size()).is_equal(2)
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).override_failure_message(
"oldest entry should have been evicted"
).is_false()
assert_bool(cache.has("GJ1c", Vector2i(1, 0), 32)).is_true()
assert_bool(cache.has("GJ1c", Vector2i(2, 0), 32)).is_true()
## get_window() touches an entry (moves it to most-recently-used) — reading an
## entry must protect it from the NEXT eviction, otherwise "LRU" degrades to
## FIFO the moment anything reads from the cache (the common case: Esc-then-
## re-enter and pan-back are exactly "read a recently-cached window", §4).
func test_get_touches_entry_and_protects_it_from_eviction() -> void:
var cache := AtlasWindowCache.new(2)
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
cache.get_window("GJ1c", Vector2i(0, 0), 32) # touch (0,0) — now most-recently-used
cache.put("GJ1c", Vector2i(2, 0), 32, {"id": "c"}) # should evict (1,0), not (0,0)
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).override_failure_message(
"touched entry should survive eviction"
).is_true()
assert_bool(cache.has("GJ1c", Vector2i(1, 0), 32)).override_failure_message(
"untouched entry should be the one evicted"
).is_false()
func test_max_entries_clamped_to_at_least_one() -> void:
var cache := AtlasWindowCache.new(0)
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
assert_int(cache.size()).is_equal(1)
func test_clear_empties_the_cache() -> void:
var cache := AtlasWindowCache.new()
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
cache.clear()
assert_int(cache.size()).is_equal(0)
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).is_false()
+170
View File
@@ -0,0 +1,170 @@
## T-1138 (D-226 T-1124 amendment §5): pure color-ramp tests for the
## regional-window base layer + toggle overlays. Every function under test
## lives on atlas_overlay_colors.gd (no class_name — preloaded by path,
## matching test_atlas_overlays.gd's own AtlasOverlayColors const).
class_name TestAtlasWindowColors
extends GdUnitTestSuite
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
# =============================================================================
# Base layer: morphology hue + elev_q lightness modulation
# =============================================================================
## MORPHOLOGY_RGB_OPAQUE must be the SAME 17-entry hue table as the planetary
## overlay's MORPHOLOGY_COLORS (D-226 T-1124 amendment §5: "reusing the T-1123
## probe's 17-entry MORPHOLOGY_RGB hues verbatim") — only alpha differs (the
## window base is opaque; the planetary overlay draws semi-transparent over a
## heightmap texture).
func test_district_window_morphology_hues_match_planetary_overlay() -> void:
assert_int(AtlasOverlayColors.MORPHOLOGY_RGB_OPAQUE.size()).is_equal(
AtlasOverlayColors.MORPHOLOGY_COLORS.size()
)
for zone in range(AtlasOverlayColors.MORPHOLOGY_RGB_OPAQUE.size()):
var opaque: Color = AtlasOverlayColors.MORPHOLOGY_RGB_OPAQUE[zone]
var overlay: Color = AtlasOverlayColors.MORPHOLOGY_COLORS[zone]
assert_float(opaque.r).override_failure_message(
"zone %d hue mismatch (r)" % zone
).is_equal_approx(overlay.r, 0.005)
assert_float(opaque.g).override_failure_message(
"zone %d hue mismatch (g)" % zone
).is_equal_approx(overlay.g, 0.005)
assert_float(opaque.b).override_failure_message(
"zone %d hue mismatch (b)" % zone
).is_equal_approx(overlay.b, 0.005)
assert_float(opaque.a).override_failure_message(
"district-window base layer must be OPAQUE (a=1.0), zone %d" % zone
).is_equal_approx(1.0, 0.001)
func test_district_window_morphology_color_out_of_range_is_magenta_sentinel() -> void:
var c: Color = AtlasOverlayColors.district_window_morphology_color(999)
assert_float(c.r).is_equal_approx(1.0, 0.001)
assert_float(c.g).is_equal_approx(0.0, 0.001)
assert_float(c.b).is_equal_approx(1.0, 0.001)
## elev_q=0 -> 0.7x lightness; elev_q=100 -> 1.0x lightness (full base color);
## elev_q=50 -> 0.85x (the midpoint of the documented [0.7, 1.0] range).
func test_district_window_elevation_lightness_modulation() -> void:
var base := Color(0.4, 0.4, 0.4, 1.0)
var low: Color = AtlasOverlayColors.district_window_elevation_lightness(base, 0)
var mid: Color = AtlasOverlayColors.district_window_elevation_lightness(base, 50)
var high: Color = AtlasOverlayColors.district_window_elevation_lightness(base, 100)
assert_float(low.r).is_equal_approx(0.4 * 0.7, 0.001)
assert_float(mid.r).is_equal_approx(0.4 * 0.85, 0.001)
assert_float(high.r).is_equal_approx(0.4 * 1.0, 0.001)
# Alpha is untouched by the lightness multiply.
assert_float(low.a).is_equal_approx(1.0, 0.001)
func test_district_window_elevation_lightness_clamps_out_of_range_elev_q() -> void:
var base := Color(0.4, 0.4, 0.4, 1.0)
var below: Color = AtlasOverlayColors.district_window_elevation_lightness(base, -20)
var above: Color = AtlasOverlayColors.district_window_elevation_lightness(base, 500)
assert_float(below.r).is_equal_approx(0.4 * 0.7, 0.001)
assert_float(above.r).is_equal_approx(0.4 * 1.0, 0.001)
# =============================================================================
# Vegetation ramp (Marine transparent, D-226 T-1124 amendment §3/§5)
# =============================================================================
## Marine (VegetationClass discriminant 6, T-1126) MUST render transparent —
## "non-negotiable inclusion, not a nice-to-have" per the amendment §3. This is
## the one assertion in this suite with the highest stakes: a regression here
## re-introduces the exact ocean-blind-vegetation bug T-1126 was created to fix.
func test_vegetation_marine_is_transparent() -> void:
var c: Color = AtlasOverlayColors.vegetation_color(AtlasOverlayColors.VEGETATION_MARINE)
assert_that(c).is_equal(Color.TRANSPARENT)
assert_int(AtlasOverlayColors.VEGETATION_MARINE).is_equal(6)
## Every VegetationClass discriminant (0 Absent .. 5 RiparianThicket, excluding
## 6 Marine which is transparent) must resolve to a DISTINCT, non-transparent
## color — the amendment's exhaustive-disposition mandate (§3) extended to the
## full field-list, matching quarter_notch_kind's own "every variant reads as
## something" test precedent in test_atlas_overlays.gd.
func test_vegetation_ramp_is_exhaustive_and_distinct() -> void:
var seen: Array = []
for vc in range(0, 6): # 0..5, Marine (6) handled separately above
var c: Color = AtlasOverlayColors.vegetation_color(vc)
assert_float(c.a).override_failure_message(
"VegetationClass %d must not be transparent" % vc
).is_greater(0.0)
assert_that(seen).override_failure_message(
"VegetationClass %d collides with an earlier entry's color" % vc
).not_contains([c])
seen.append(c)
func test_vegetation_color_unrecognized_falls_back_to_absent_reading() -> void:
var unknown: Color = AtlasOverlayColors.vegetation_color(999)
var absent: Color = AtlasOverlayColors.vegetation_color(0)
assert_that(unknown).is_equal(absent)
# =============================================================================
# Glaciation ice-tint modifier (D-226 T-1124 amendment §5, ported apply_ice_tint)
# =============================================================================
## None (0) and Light (1) must return the base color UNCHANGED — this is the
## load-bearing divergence from the amendment's own prose summary ("gated on
## glaciation_grade >= Light"): the REFERENCE implementation the amendment
## names for porting (aliveness_probe::apply_ice_tint) excludes Light too,
## because grade 1 is glacial-erosion signatures, not visible ice. This test
## pins that the port, not the summary, is what shipped.
func test_glaciation_tint_none_and_light_are_no_ops() -> void:
var base := Color(0.3, 0.5, 0.3, 0.8)
_assert_color_approx(AtlasOverlayColors.glaciation_tint(base, 0), base) # None
_assert_color_approx(AtlasOverlayColors.glaciation_tint(base, 1), base) # Light
## Moderate (2) / Heavy (3) / IceCap (4) blend increasingly toward
## GLACIATION_ICE_WHITE — each grade strictly closer to white than the last
## (monotonic tint strength), matching apply_ice_tint's 0.30/0.50/0.70 alphas.
func test_glaciation_tint_increases_with_grade() -> void:
var base := Color(0.1, 0.1, 0.1, 1.0) # far from ice-white, maximizes signal
var white: Color = AtlasOverlayColors.GLACIATION_ICE_WHITE
var moderate: Color = AtlasOverlayColors.glaciation_tint(base, 2)
var heavy: Color = AtlasOverlayColors.glaciation_tint(base, 3)
var ice_cap: Color = AtlasOverlayColors.glaciation_tint(base, 4)
var dist_moderate: float = _color_distance(base, moderate)
var dist_heavy: float = _color_distance(base, heavy)
var dist_ice_cap: float = _color_distance(base, ice_cap)
assert_float(dist_heavy).override_failure_message(
"Heavy should tint more than Moderate"
).is_greater(dist_moderate)
assert_float(dist_ice_cap).override_failure_message(
"IceCap should tint more than Heavy"
).is_greater(dist_heavy)
# IceCap (alpha 0.70) should land noticeably closer to white than to base.
assert_float(_color_distance(ice_cap, white)).is_less(_color_distance(ice_cap, base))
func test_glaciation_tint_out_of_range_grade_is_no_op() -> void:
var base := Color(0.3, 0.5, 0.3, 0.8)
_assert_color_approx(AtlasOverlayColors.glaciation_tint(base, 999), base)
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)
## Godot 4's Color has no distance_to() — plain Euclidean over RGB (alpha
## deliberately excluded: the tint blend leaves alpha untouched, so including
## it would just add a constant offset with no signal).
static func _color_distance(a: Color, b: Color) -> float:
return Vector3(a.r, a.g, a.b).distance_to(Vector3(b.r, b.g, b.b))
+204
View File
@@ -0,0 +1,204 @@
## T-1138 (D-226 T-1124 amendment §1-§5): tests for AtlasWindowViewer + its
## companion request/cache orchestration (atlas_window_request.gd) — pure
## logic against hand-built AtlasLayerResponse-shaped dicts, matching the
## ticket's "unit tests against hand-built response dicts" instruction. Live
## end-to-end verification against a real spawned server is separate
## (companion-run evidence, not gdUnit — this file never touches SimBridge's
## live-mode path, only the response-handling/cache/overlay logic that path
## eventually feeds).
class_name TestAtlasWindowViewer
extends GdUnitTestSuite
# atlas_window_request.gd has no class_name (review #8 precedent throughout
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape
## district_grid/region_grid fixtures already use elsewhere in this suite).
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# AtlasWindowViewer — entry + overlay defs
# =============================================================================
func test_enter_with_no_response_leaves_window_null_and_pending() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20))
assert_that(v.get_district_window()).is_null()
## Feeding a matching Ready response (via the SAME SimBridge.atlas_layers_received
## routing path the viewer subscribes to in _ready()) must populate the window.
func test_enter_then_matching_response_populates_window() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).is_equal(window)
## A response for a DIFFERENT body must not populate the window — the
## body_id scoping AtlasWindowRequest.on_response() checks.
func test_response_for_different_body_is_ignored() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window))
assert_that(v.get_district_window()).is_null()
## A response whose echoed (center, n) does NOT match what was last asked for
## is stale — §2's race-condition guard. Simulates a superseded-by-a-later-pan
## response arriving after the fact.
func test_response_with_mismatched_echo_is_discarded_as_stale() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var stale_window: Dictionary = _mock_window(Vector2i(99, 99), 2) # wrong center
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window))
assert_that(v.get_district_window()).is_null()
## §1: an as-yet-underived window rides as `district_window: None` inside a
## Ready response — this is NOT an error, the viewer just keeps waiting
## (get_district_window() stays null, no crash, no window content shown).
func test_ready_response_with_null_district_window_keeps_waiting() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", null))
assert_that(v.get_district_window()).is_null()
func test_overlay_defs_include_the_three_toggle_ids() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).contains(["gen_dw_temp", "gen_dw_moisture", "gen_dw_veg"])
## Glaciation is explicitly NOT a toggle id (§5: "an always-on modifier, not
## a toggle") — a regression here would silently re-introduce it as a switch.
func test_overlay_defs_do_not_include_glaciation() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).not_contains(["gen_dw_glaciation", "gen_dw_ice"])
func test_set_overlay_visible_toggles_and_is_overlay_visible_reflects_it() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_false()
v.set_overlay_visible("gen_dw_temp", true)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
func test_set_overlay_visible_unknown_id_is_a_noop() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_overlay_visible("not_a_real_overlay", true)
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
# =============================================================================
# T-1120 capture-API parity (the ticket's explicit note: must survive here too)
# =============================================================================
func test_set_view_and_getters_round_trip() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(2.5, Vector2(30.0, -10.0))
assert_that(v.get_view_zoom()).is_equal_approx(2.5, 0.001)
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
func test_set_view_clamps_to_min_max_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(0.01, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001)
v.set_view(1000.0, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
# =============================================================================
# AtlasWindowRequest — cache reuse (§4's Esc-then-re-enter / pan-back hit)
# =============================================================================
func test_window_request_cache_hit_emits_synchronously_no_pending() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
# Prime the cache directly (bypassing the network path) — the ticket's
# own instruction: unit test against hand-built response dicts.
req.get_cache().put("GJ380c", Vector2i(1, 1), 2, _mock_window(Vector2i(1, 1), 2))
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(1, 1), 2)
assert_int(received.size()).is_equal(1)
assert_bool(req.is_pending()).override_failure_message(
"a cache hit must never leave the request pending"
).is_false()
func test_window_request_cache_miss_leaves_pending_true() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(5, 5), 2)
assert_bool(req.is_pending()).is_true()
## on_response() with a matching Ready+window response resolves the pending
## request AND populates the cache — verified by a second request_now() call
## for the same (center, n) becoming a cache hit with zero additional pending.
func test_on_response_resolves_and_populates_cache_for_next_request() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
req.on_response(_mock_response("GJ380c", window))
assert_bool(req.is_pending()).is_false()
# Re-request the SAME (body, center, n) — must be a cache hit, no pending.
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).override_failure_message(
"a second request for an already-resolved window must hit the cache"
).is_false()