## Tests for the Phase-4 generation overlays in the Atlas viewer (#960, D-225). ## Referencing AtlasViewer forces both it and AtlasMarkerOverlay to compile, so ## this also guards against parse errors in the overlay rendering code. class_name TestAtlasOverlays extends GdUnitTestSuite # atlas_legend_panel.gd has no class_name (matches atlas_overlay_bar.gd, review # #8), so its GENERATION_LEGEND spec table is read off the preloaded Script # 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 = [] for d: Dictionary in AtlasViewer.OVERLAY_DEFS: ids.append(d["id"]) assert_that(ids).contains( ["gen_l1_rivers", "gen_l1_basins", "gen_l1_attractors", "gen_district"] ) ## T-1046: the district/morphology grid round-trips through the viewer and the ## protocol decode surfaces it; referencing AtlasMarkerOverlay compiles the draw. func test_district_grid_round_trips() -> void: var v: AtlasViewer = auto_free(AtlasViewer.new()) assert_that(v.get_generation_district_grid()).is_null() var grid := { "cols": 2, "rows": 1, "morphology": PackedByteArray([8, 14]), "elev_q": PackedByteArray([10, 90]), } v.set_generation_district_grid(grid) assert_that(v.get_generation_district_grid()).is_equal(grid) # The decoded response dict carries the district_grid key (protocol.gd, T-1046). var decoded: Variant = Protocol.atlas_response_from_raw( {"body_id": "GJ1c", "status": "Ready", "district_grid": grid} ) assert_that((decoded as Dictionary).get("district_grid")).is_equal(grid) func test_generation_data_round_trips() -> void: var v: AtlasViewer = auto_free(AtlasViewer.new()) assert_that(v.get_generation_layer1()).is_null() var mock := { "body_id": "GJ1c", "river_network": {"river_cells": [], "confluences": [], "mouths": []}, "drainage_basins": [], "attractors": [], } v.set_generation_layer1(mock) assert_that(v.get_generation_layer1()).is_equal(mock) func test_implant_pending_start_stop() -> void: var p: ImplantPending = auto_free(ImplantPending.new()) p.start("GENERATING LAYER 1") assert_bool(p.visible).is_true() p.stop() assert_bool(p.visible).is_false() # ============================================================================= # T-960: gen_l2_roads / gen_l3_settlements overlays # ============================================================================= func test_l2_l3_overlays_registered() -> void: var ids: Array = [] for d: Dictionary in AtlasViewer.OVERLAY_DEFS: ids.append(d["id"]) assert_that(ids).contains(["gen_l2_roads", "gen_l3_settlements"]) ## RoadGraphLayer shape pinned to dudley-atlas-server's contract (2026-07-14): ## nodes carry NO `degree` (trimmed as server-internal bookkeeping — the ## overlay derives it from edge endpoints instead, see _draw_gen_roads). func test_road_graph_round_trips() -> void: var v: AtlasViewer = auto_free(AtlasViewer.new()) assert_that(v.get_generation_road_graph()).is_null() var road_graph := { "nodes": [ {"city_id": 1, "position": [10, 20], "kind": "Settlement"}, {"city_id": null, "position": [12, 22], "kind": "Waypoint"}, ], "edges": [ { "from": 0, "to": 1, "path": [[10, 20], [12, 22]], "maintenance": "Administrative", "is_rail": false, "named_route_id": null, }, ], } v.set_generation_road_graph(road_graph) assert_that(v.get_generation_road_graph()).is_equal(road_graph) # Round-trips through the protocol decode too (T-1046 district_grid precedent). var decoded: Variant = Protocol.atlas_response_from_raw( {"body_id": "GJ1c", "status": "Ready", "road_graph": road_graph} ) assert_that((decoded as Dictionary).get("road_graph")).is_equal(road_graph) ## SettlementLayer shape pinned to dudley-atlas-server's contract ## (2026-07-14): wrapped under "settlements" (not "cities"); size_class is ## categorical (Major/Standard/Minor), and is_capital is authored, not ## population-derived. func test_settlements_round_trip() -> void: var v: AtlasViewer = auto_free(AtlasViewer.new()) assert_that(v.get_generation_settlements()).is_null() var settlements := { "settlements": [ { "city_id": 1, "name": "Ridgeback", "position": [30, 40], "size_class": "Minor", "is_capital": false, "is_port": false, }, { "city_id": 2, "name": "Capital City", "position": [35, 45], "size_class": "Major", "is_capital": true, "is_port": true, }, ], } v.set_generation_settlements(settlements) assert_that(v.get_generation_settlements()).is_equal(settlements) var decoded: Variant = Protocol.atlas_response_from_raw( {"body_id": "GJ1c", "status": "Ready", "settlements": settlements} ) 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 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-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: var path := "res://tests/fixtures/msgpack/atlas_response_ready.msgpack" var f := FileAccess.open(path, FileAccess.READ) assert_that(f).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_str(response.get("body_id")).is_equal("GJ1c") assert_str(response.get("status")).is_equal("Ready") var road_graph: Dictionary = response.get("road_graph") assert_that(road_graph).is_not_null() assert_int((road_graph.get("nodes", []) as Array).size()).is_equal(2) var edges: Array = road_graph.get("edges", []) assert_int(edges.size()).is_equal(1) assert_str((edges[0] as Dictionary).get("maintenance")).is_equal("Administrative") assert_bool((edges[0] as Dictionary).get("is_rail")).is_false() var settlements: Dictionary = response.get("settlements") assert_that(settlements).is_not_null() var entries: Array = settlements.get("settlements", []) assert_int(entries.size()).is_equal(2) var capital: Dictionary = entries[0] assert_str(capital.get("name")).is_equal("Port Aldren") assert_str(capital.get("size_class")).is_equal("Major") assert_bool(capital.get("is_capital")).is_true() var minor: Dictionary = entries[1] assert_str(minor.get("name")).is_equal("Farmstead Rell") 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/ # Vec 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 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/Vec 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 # ============================================================================= ## Data-driven claim check: every legend entry must reference a real overlay ## id, so a typo or a stale entry can't silently produce a dead legend ## section that never appears. func test_generation_legend_entries_reference_real_overlay_ids() -> void: var overlay_ids: Array = [] for d: Dictionary in AtlasViewer.OVERLAY_DEFS: overlay_ids.append(d["id"]) for spec: Dictionary in LegendPanelScript.GENERATION_LEGEND: assert_that(overlay_ids).contains([spec.get("overlay_id", "")]) assert_bool((spec.get("rows", []) as Array).is_empty()).override_failure_message( "legend entry '%s' has no rows" % spec.get("title", "?") ).is_false() ## Every gen_* toggleable overlay should have at least one legend entry so ## toggling it on never shows silent, unexplained map content. func test_every_gen_overlay_has_a_legend_entry() -> void: var legend_overlay_ids: Array = [] for spec: Dictionary in LegendPanelScript.GENERATION_LEGEND: legend_overlay_ids.append(spec.get("overlay_id", "")) for d: Dictionary in AtlasViewer.OVERLAY_DEFS: var overlay_id: String = d["id"] if overlay_id.begins_with("gen_"): assert_that(legend_overlay_ids).override_failure_message( "overlay '%s' has no GENERATION_LEGEND entry" % overlay_id ).contains([overlay_id]) ## Full-lifecycle test (add_child fires _ready(), which builds the legend ## panel) — invisible at rest, since no generation overlay starts active. func test_legend_panel_hidden_by_default() -> void: var v: AtlasViewer = AtlasViewer.new() add_child(v) assert_bool(v._legend_panel.visible).override_failure_message( "legend panel must be invisible when no generation overlay is active" ).is_false() v.queue_free() ## Toggling a gen_* overlay on must reveal the legend and populate its rows; ## toggling it back off must hide it again (invisible-when-not-needed, D-226). func test_legend_panel_shows_active_overlay_and_hides_when_toggled_off() -> void: var v: AtlasViewer = AtlasViewer.new() add_child(v) v.set_overlay_visible("gen_l3_settlements", true) assert_bool(v._legend_panel.visible).override_failure_message( "legend panel must show once a generation overlay is toggled on" ).is_true() assert_int(v._legend_panel.get_implant_children().size()).override_failure_message( "legend panel must have content rows once populated" ).is_greater(0) v.set_overlay_visible("gen_l3_settlements", false) assert_bool(v._legend_panel.visible).override_failure_message( "legend panel must hide again once its only active overlay is toggled off" ).is_false() v.queue_free() ## PR #186 regression: the overlay bar spans the full header-adjacent width ## so rows can wrap (ALIGNMENT_END right-aligns the chips), which leaves a ## wide EMPTY strip inside the container rect on wide screens. That strip ## must never eat map input: the container is MOUSE_FILTER_IGNORE (chip ## Buttons STOP their own events) and _is_over_ui() must not treat the bar ## rect as UI — both are pinned here because no other test exercises them. func test_overlay_bar_empty_area_does_not_block_map_input() -> void: var v: AtlasViewer = AtlasViewer.new() add_child(v) v.size = Vector2(1920.0, 1080.0) v._position_overlay_bar() await get_tree().process_frame assert_int(v._overlay_bar.mouse_filter).override_failure_message( "overlay bar container must be MOUSE_FILTER_IGNORE — STOP turns the" + " empty flow area into a dead strip that swallows map clicks" ).is_equal(Control.MOUSE_FILTER_IGNORE) # A point just inside the bar's top-left is empty flow area (chips are # right-aligned and occupy well under the full width at 1920px). var strip_point: Vector2 = v._overlay_bar.global_position + Vector2(8.0, 8.0) assert_bool(v._is_over_ui(strip_point)).override_failure_message( "_is_over_ui must not claim the overlay bar's empty strip — map" + " pan/click near the top edge would silently die there" ).is_false() v.queue_free()