diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index a627ce3f5..33e7fe19c 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -787,6 +787,7 @@ static func atlas_response_from_raw(raw: Variant) -> Variant: "status": status, "error": error, "layer1": raw.get("layer1"), + "district_grid": raw.get("district_grid"), } diff --git a/client/tests/test_atlas_overlays.gd b/client/tests/test_atlas_overlays.gd index 5333c649c..15b76feb3 100644 --- a/client/tests/test_atlas_overlays.gd +++ b/client/tests/test_atlas_overlays.gd @@ -9,7 +9,29 @@ 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"]) + 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: diff --git a/client/ui/implant/apps/atlas/atlas_marker_overlay.gd b/client/ui/implant/apps/atlas/atlas_marker_overlay.gd index 39e5c7e50..038d6c780 100644 --- a/client/ui/implant/apps/atlas/atlas_marker_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_marker_overlay.gd @@ -109,6 +109,13 @@ func _draw() -> void: if viewer.is_overlay_visible("corp_presence"): _draw_corp_presence(markers) + # Generation: district morphology grid (T-1046, D-226) — coarse area fill, + # drawn under the Layer-1 line/point overlays. + if viewer.is_overlay_visible("gen_district"): + var grid: Variant = viewer.get_generation_district_grid() + if grid is Dictionary: + _draw_gen_district(grid, tex_w, tex_h) + # Generation-cascade layers (#960, D-225) — from the proxied Layer1Output. # Draw order: basins (area) under rivers under attractors (point anchors). var layer1: Variant = viewer.get_generation_layer1() @@ -385,6 +392,60 @@ func _gen_pos(rc: Variant) -> Vector2: return Vector2(float(rc[1]) / _gen_grid_w * _gen_tex_w, float(rc[0]) / _gen_grid_h * _gen_tex_h) +## District morphology grid (T-1046, D-226). A coarse `cols × rows` area fill — +## each cell coloured by its MorphologyZone discriminant (D-239 §6), semi- +## transparent so the heightmap reads through. Planetary-scale map view (NOT the +## 2 km on-demand districts, which are Phase 5 in-world). +func _draw_gen_district(grid: Dictionary, tex_w: float, tex_h: float) -> void: + var cols: int = int(grid.get("cols", 0)) + var rows: int = int(grid.get("rows", 0)) + if cols <= 0 or rows <= 0: + return + var morphology: Variant = grid.get("morphology") + if not morphology is PackedByteArray: + return + var cw: float = tex_w / float(cols) + var ch: float = tex_h / float(rows) + var n: int = morphology.size() + for ry in range(rows): + for rx in range(cols): + var i: int = ry * cols + rx + if i >= n: + continue + # +0.5 overdraw avoids hairline seams between adjacent cells. + draw_rect(Rect2(rx * cw, ry * ch, cw + 0.5, ch + 0.5), _morphology_color(int(morphology[i]))) + + +## MorphologyZone discriminant → overlay colour (D-239 §6 order). ~0.55 alpha so +## the heightmap shows through: water blues, plains greens, uplands greys/browns, +## volcanic dark red. +const MORPHOLOGY_COLORS: Array = [ + Color(0.10, 0.20, 0.45, 0.55), # 0 OpenOcean + Color(0.20, 0.40, 0.65, 0.55), # 1 Lake + Color(0.45, 0.55, 0.50, 0.55), # 2 TidalFlat + Color(0.85, 0.78, 0.45, 0.55), # 3 DuneStrand + Color(0.50, 0.50, 0.55, 0.55), # 4 CliffCoast + Color(0.30, 0.40, 0.50, 0.55), # 5 Fjord + Color(0.40, 0.65, 0.60, 0.55), # 6 Delta + Color(0.30, 0.55, 0.55, 0.55), # 7 Estuarine + Color(0.30, 0.60, 0.30, 0.55), # 8 AlluvialPlain + Color(0.45, 0.70, 0.40, 0.55), # 9 RiverBank + Color(0.35, 0.60, 0.50, 0.55), # 10 MeanderReach + Color(0.55, 0.60, 0.45, 0.55), # 11 BraidedPlain + Color(0.50, 0.55, 0.30, 0.55), # 12 ValleyFloor + Color(0.55, 0.45, 0.30, 0.55), # 13 MountainPass + Color(0.80, 0.82, 0.85, 0.55), # 14 Alpine + Color(0.45, 0.15, 0.12, 0.55), # 15 Volcanic + Color(0.25, 0.45, 0.40, 0.55), # 16 Wetland +] + + +func _morphology_color(zone: int) -> Color: + if zone >= 0 and zone < MORPHOLOGY_COLORS.size(): + return MORPHOLOGY_COLORS[zone] + return Color(0.5, 0.5, 0.5, 0.4) + + func _draw_gen_rivers(layer1: Dictionary) -> void: var rn: Variant = layer1.get("river_network") if not rn is Dictionary: diff --git a/client/ui/implant/apps/atlas/atlas_viewer.gd b/client/ui/implant/apps/atlas/atlas_viewer.gd index b800fbdde..b4fa832b6 100644 --- a/client/ui/implant/apps/atlas/atlas_viewer.gd +++ b/client/ui/implant/apps/atlas/atlas_viewer.gd @@ -139,6 +139,12 @@ const OVERLAY_DEFS: Array = [ "group": "toggle", "tooltip": "Layer 1 — geographic attractors (generation overlay, #960)." }, + { + "id": "gen_district", + "label": "MRPH", + "group": "toggle", + "tooltip": "District morphology zones (generation overlay, T-1046/D-226)." + }, ] # ── Context (set by show_body) ───────────────────────────────────────────────── @@ -150,6 +156,7 @@ var _implant_theme = null var _heightmap_texture: Texture2D = null var _markers: Dictionary = {} var _generation_layer1: Variant = null # #960: Layer1Output from the proxy (D-225) +var _generation_district_grid: Variant = null # T-1046: coarse DistrictGridLayer (D-226) var _gen_pending: bool = false # #960: awaiting a Layer1 response (re-polls on Pending) var _gen_retries: int = 0 var _grid_w: float = 512.0 @@ -298,6 +305,18 @@ func get_generation_layer1() -> Variant: return _generation_layer1 +## T-1046: store the coarse district/morphology grid (DistrictGridLayer) from the +## proxy and redraw. The marker overlay reads it via get_generation_district_grid(). +func set_generation_district_grid(grid: Variant) -> void: + _generation_district_grid = grid + if _overlay_node: + _overlay_node.queue_redraw() + + +func get_generation_district_grid() -> Variant: + return _generation_district_grid + + ## #960: request the body's Layer-1 cascade output from the server proxy. ## No-op in test mode (SimBridge has no server connection) — the overlays ## simply stay empty, which is the correct serverless behavior. @@ -322,6 +341,7 @@ func _on_atlas_layers_received(response: Dictionary) -> void: _gen_retries = 0 _set_gen_indicator(false) set_generation_layer1(response.get("layer1")) + set_generation_district_grid(response.get("district_grid")) "Pending": if _gen_retries < GEN_MAX_RETRIES: _gen_retries += 1