feat(ui): Sprint 35 — Atlas of the Reach (Phase 3 planetary viewer) #128

Closed
jpmschweitzer wants to merge 0 commits from sprint-35/client into main
Owner

Summary

Sprint 35 client work — Atlas of the Reach (Phase 3 planetary viewer), all three stories in the chain.

  • #834 Atlas implant panel — new implant/map/atlas FULLSCREEN app (z=20) with three nav levels: system picker (◄ ► cycle), orbital diagram (star-centred with bodies grouped by orbit_index, moons sub-orbiting parents, stations as clickable markers with mini data panel), and body entry (data sheet). Composed from the D-169 implant component library. tooling/generate-star-map-data.py extended to emit per-system currency_zone, orbit_bodies, and stations arrays from systems.db; star_map_data.json regenerated deterministically. Wired into hud.tscn, toggled via the A key in main.gd, insert-state propagated through snapshot_consumers.gd.
  • #835 Atlas heightmap viewer — new Level 3 AtlasViewer control loads a body's terrain_reference PNG and markers.json, renders heightmap + markers in texture-space via an AtlasMarkerOverlay Node2D child of a transformed canvas (pan = offset, zoom = scale, cursor-centred wheel zoom). Handles empty markers.json and missing terrain_reference gracefully (themed "terrain data pending (#839)" notice). City data sidebar rebuilds on click with name, pop tier, function, currency zone, Commission presence, shadow zone, gate distance. Pressing N on a selected city emits economics_link_requested(system_id)main.gd bridges this to EconomicsPanel.select_system() + HudGroups.open_app("implant/economics"), satisfying the D-191 Phase 2/3 cross-panel integration.
  • #836 Overlay toggle barAtlasOverlayBar HBoxContainer docked top-right of the viewer with 11 short-label buttons mapped to the D-181 signal visibility ladder: 5 always-on (TER/INF/NAM/GAT/POL), 4 toggleable (POP/PRD/SHD/CRP), 2 locked (STK/BSL — disabled + tooltipped with unlock requirements). Writes through AtlasViewer.set_overlay_visible(), which is the single entry point into the per-overlay visibility dict consumed by AtlasMarkerOverlay._draw().

Implements D-191 criteria 1, 4, 5, 6, 8.

Notes for reviewers

  • Server #839 has not yet populated terrain_reference in systems.db, so the viewer will show the "terrain data pending" empty state on all bodies until the server atlas pipeline runs. Rendering against real heightmaps needs a downstream star_map_data.json regen once #839 lands.
  • markers.json files for most bodies currently have empty cities/roads/rail/pois arrays — the overlay handles this without crashing. Once server #832/#833 populate them, markers appear automatically with no client change.
  • Godot parse-check is clean for all atlas scripts. The 23 pre-existing autoload parse errors (sim_bridge, protocol, input_mapper, audio_manager) are unchanged on this branch — they are a known autoload parse-order issue independent of this work.

Test plan

  • Press A in-game → Atlas opens at FULLSCREEN with the system picker
  • ◄ ► cycle systems, Enter opens the orbital diagram
  • Click a body → body entry panel; click a station → mini data panel
  • Enter on an inhabited body → heightmap viewer opens (shows empty-state notice until #839 lands)
  • Once terrain_reference is populated: drag to pan, wheel to zoom, R to reset, click a city → sidebar, N → economics monitor opens pre-filtered to the system
  • Overlay bar buttons: always-on stays pinned, toggleable flip, locked show unlock tooltip and never activate
  • Esc walks back up the nav chain (viewer → body → orbital → picker → close)
## Summary Sprint 35 client work — Atlas of the Reach (Phase 3 planetary viewer), all three stories in the chain. - **#834 Atlas implant panel** — new `implant/map/atlas` FULLSCREEN app (z=20) with three nav levels: system picker (◄ ► cycle), orbital diagram (star-centred with bodies grouped by `orbit_index`, moons sub-orbiting parents, stations as clickable markers with mini data panel), and body entry (data sheet). Composed from the D-169 implant component library. `tooling/generate-star-map-data.py` extended to emit per-system `currency_zone`, `orbit_bodies`, and `stations` arrays from `systems.db`; `star_map_data.json` regenerated deterministically. Wired into `hud.tscn`, toggled via the A key in `main.gd`, insert-state propagated through `snapshot_consumers.gd`. - **#835 Atlas heightmap viewer** — new Level 3 `AtlasViewer` control loads a body's `terrain_reference` PNG and `markers.json`, renders heightmap + markers in texture-space via an `AtlasMarkerOverlay` Node2D child of a transformed canvas (pan = offset, zoom = scale, cursor-centred wheel zoom). Handles empty `markers.json` and missing `terrain_reference` gracefully (themed "terrain data pending (#839)" notice). City data sidebar rebuilds on click with name, pop tier, function, currency zone, Commission presence, shadow zone, gate distance. Pressing **N** on a selected city emits `economics_link_requested(system_id)` — `main.gd` bridges this to `EconomicsPanel.select_system()` + `HudGroups.open_app("implant/economics")`, satisfying the D-191 Phase 2/3 cross-panel integration. - **#836 Overlay toggle bar** — `AtlasOverlayBar` HBoxContainer docked top-right of the viewer with 11 short-label buttons mapped to the D-181 signal visibility ladder: 5 always-on (TER/INF/NAM/GAT/POL), 4 toggleable (POP/PRD/SHD/CRP), 2 locked (STK/BSL — disabled + tooltipped with unlock requirements). Writes through `AtlasViewer.set_overlay_visible()`, which is the single entry point into the per-overlay visibility dict consumed by `AtlasMarkerOverlay._draw()`. Implements D-191 criteria 1, 4, 5, 6, 8. ## Notes for reviewers - Server `#839` has not yet populated `terrain_reference` in `systems.db`, so the viewer will show the "terrain data pending" empty state on all bodies until the server atlas pipeline runs. Rendering against real heightmaps needs a downstream `star_map_data.json` regen once `#839` lands. - `markers.json` files for most bodies currently have empty `cities`/`roads`/`rail`/`pois` arrays — the overlay handles this without crashing. Once server `#832`/`#833` populate them, markers appear automatically with no client change. - Godot parse-check is clean for all atlas scripts. The 23 pre-existing autoload parse errors (sim_bridge, protocol, input_mapper, audio_manager) are unchanged on this branch — they are a known autoload parse-order issue independent of this work. ## Test plan - [ ] Press **A** in-game → Atlas opens at FULLSCREEN with the system picker - [ ] ◄ ► cycle systems, Enter opens the orbital diagram - [ ] Click a body → body entry panel; click a station → mini data panel - [ ] Enter on an inhabited body → heightmap viewer opens (shows empty-state notice until #839 lands) - [ ] Once `terrain_reference` is populated: drag to pan, wheel to zoom, R to reset, click a city → sidebar, N → economics monitor opens pre-filtered to the system - [ ] Overlay bar buttons: always-on stays pinned, toggleable flip, locked show unlock tooltip and never activate - [ ] Esc walks back up the nav chain (viewer → body → orbital → picker → close)
jpmschweitzer added 4 commits 2026-04-15 08:34:59 +02:00
3-level atlas navigation as an implant/map/atlas FULLSCREEN app (D-170 z=20):
system picker (◄ ► cycle, Enter opens orbital) → orbital diagram (star centre,
bodies grouped by orbit_index with moons sub-orbiting parents, stations as
markers, click-to-open body or mini station panel) → body entry (data sheet,
Enter stub for #835 heightmap viewer, Esc back). Composed from the ImplantPanel
component library (D-169).

Wires the panel into hud.tscn, adds an A-key toggle in main.gd, propagates
insert_state via snapshot_consumers. Extends generate-star-map-data.py to emit
per-system orbit_bodies + stations arrays (with currency_zone, atmosphere,
population, terrain_reference, etc.) from systems.db; star_map_data.json
regenerated deterministically.

Per D-191 §6, Phase 3.
Adds Level.HEIGHTMAP_VIEWER to AtlasPanel. The viewer loads a body's
terrain_reference heightmap PNG, pairs it with markers.json (roads, rail, POIs,
cities, rivers/oceans/mountains), and renders markers in texture-space via an
AtlasMarkerOverlay Node2D child of a transformed canvas — pan = offset,
zoom = scale.

Pan/zoom is cursor-centred (wheel zooms under the mouse, drag pans), with a
fit-to-view reset on R. Empty markers.json state renders a bare heightmap;
missing terrain_reference shows a themed "terrain data pending (#839)" notice
instead of crashing.

City data sidebar rebuilds from the selected city: name, pop tier, function,
currency zone, Commission presence, shadow zone, gate distance. Pressing N on
a selected city emits economics_link_requested(system_id) — main.gd bridges
this to EconomicsPanel.select_system() + HudGroups.open_app("implant/economics")
as an insert overlay, satisfying the D-191 Phase 2/3 cross-panel integration.

The overlay renders all nine D-191 overlay layers off of per-overlay visibility
flags in AtlasViewer. Overlay toggling for the regional view (#836) plugs into
set_overlay_visible(); the five always-on layers (terrain, infrastructure,
named features, gate markers, political zones) draw by default, the four
toggleable layers draw from placeholder data, and the two locked layers
(stockpile_weeks, production_vs_baseline) remain off until unlocked.

Per D-191 criteria 1, 4, 5.
AtlasOverlayBar is an HBoxContainer docked top-right of the heightmap viewer
with 11 short-label buttons mapping to D-191 §7 / D-181 signal visibility:

  always-on (5)  TER INF NAM GAT POL — terrain, infrastructure, named
                 features, gate markers, political zones. Pinned on; clicks
                 are swallowed so the layers can't accidentally be disabled.
  toggleable (4) POP PRD SHD CRP — population density, production zones,
                 shadow economy, corporate presence. Reflect and mutate
                 viewer overlay state.
  locked (2)    STK BSL — stockpile_weeks, production_vs_baseline. Disabled
                 and greyed out with unlock-requirement tooltips, per D-181
                 semi-private/private tiers; kept in the bar so players see
                 that deeper data exists and is gated.

Each button writes through AtlasViewer.set_overlay_visible(), which is the
single entry point into the viewer's _overlay_visibility dict consumed by
AtlasMarkerOverlay._draw(). Locked overlays short-circuit in that setter.

Per D-191 criterion 6.
Author
Owner

PR Review — sprint-35/client → main (2-reviewer consolidated)

Reviewers: Hoshe (code quality), Tyre (architecture)
Verdict: CHANGES REQUESTED

Architecturally the PR is well-shaped: the 4-module decomposition (AtlasPanel / AtlasViewer / AtlasMarkerOverlay / AtlasOverlayBar) is clean, D-170 HUD registration is correct, the economics cross-link signal path is the right API shape, the city-data sidebar composes from the implant component library correctly, and pan/zoom math is right. Pre-flight was clean: gdlint reported no problems on all 6 touched files, headless godot --path client --quit produced no new SCRIPT ERROR lines.

The blockers are concentrated in two real bugs and one contract mismatch between the overlay code and the D-191 §8 markers schema. The rest is warnings and polish.


D-191 compliance summary (Tyre)

Decision Compliance Notes
D-169 ImplantPanel library PARTIAL picker / body / station / city / empty_notice correctly compose from ImplantPanel + ImplantHeader/Separator/DataRow/TextBlock. BUT atlas_panel.gd._draw_header() and atlas_viewer.gd._draw_header() hand-roll the top-level title/hint with draw_string(get_theme_default_font(), …, COLOR_TEXT) — bypasses the implant theme and duplicates ImplantHeader.
D-170 HUD visibility OK Registration as implant/map/atlas in FULLSCREEN mode; open_app emits gameplay_occluded; _on_app_changed routes mode → visibility. Correct.
D-181 overlay tier mapping OK corp_presence → observable, shadow_economy → toggleable, stockpile_weeks → semi-private, production_vs_baseline → private (locked). Matches the D-181 visibility ladder.
D-191 criterion 1 (navigation chain) PARTIAL Within-atlas nav works. star_map.gd still has a TODO: migrate to HudGroups.open_app("implant/map/starchart") — atlas and Reach star map are currently separate apps, not one extended chain. Not this PR's fault (star_map.gd predates D-170), but criterion 1 is technically not end-to-end until the two are linked. Acceptable for this PR; flag as follow-up.
D-191 criterion 4 (city data panel) OK Name, pop tier, function, currency zone, Commission presence, shadow zone, gate distance. Cross-link button present.
D-191 criterion 5 (economics link) OK AtlasViewer.economics_link_requested(system_id)AtlasPanel.economics_link_requestedmain._on_atlas_economics_linkEconomicsPanel.select_system() + HudGroups.open_app("implant/economics", INSERT). Clean signal path.
D-191 criterion 6 (9 MVP overlays) PARTIAL 11 buttons present. terrain, infrastructure, named_features, gate_markers, population_density render real data. political_zones draws a flat tint (flagged as MVP placeholder in comment). 3 of 4 toggleable overlays (production_zones, shadow_economy, corp_presence) read markers.production_zones / markers.shadow_zones / markers.corp_presence — none of these keys exist in D-191 §8's markers schema. Toggling draws nothing. See comment #3 below.
D-191 criterion 8 (station mini panel) OK Orbital click on station → _station_panel ImplantPanel with name, operator, function, currency, deferred hint, no drill-down. Matches spec.
D-010 / D-012 N/A Client-only rendering code.

Blocking — fix before merge

# File:line Reviewer Description
1 atlas_panel.gd:240–244 Hoshe Moon placement divisor hard-coded to 4.0. TAU * float(moon_idx - 1) / 4.0 — if a parent body has 5 or more moons, moon 5 lands at the same angle as moon 1, moon 6 overlaps moon 2, etc. Dots render on top of each other and become unclickable. Fix: use the count of moons per parent body as the divisor, not the constant.
2 atlas_viewer.gd:198–199 (vs line 202 null check) Hoshe Stale _grid_w / _grid_h when a body has no heightmap after one that did. _grid_w and _grid_h are set from _tex_w / _tex_h on lines 198–199 before the terrain_reference null check at line 202. When showing a body with terrain_reference = null after a body that had one, the grid dimensions stay pointed at the previous body's texture. grid_to_canvas() then uses stale dimensions and markers render in the wrong place. Fix: move the grid assignment to after the null check, or reset to safe defaults when terrain_reference is null.
3 atlas_marker_overlay.gd:203–220 Tyre Three toggleable overlays read non-existent markers schema keys. _draw_production_zones, _draw_shadow_economy, _draw_corp_presence read markers.production_zones, markers.shadow_zones, markers.corp_presence — none are in D-191 §8's markers schema (cities, roads, railroads, pois, rivers, oceans, mountain_ranges). No generator will populate them. Toggling these overlays is a silent no-op. Fix options: (a) derive from existing data — e.g. corp_presence from cities[].commission_presence; or (b) amend D-191 §8 schema and add a follow-up ticket for the server generator. Current state makes criterion 6's "present AND functional" false for 3/4 toggleable overlays.

Warnings — fix or document

# File:line Reviewer Description
4 atlas_panel.gd:288–291, atlas_viewer.gd:301–308 Tyre D-169 violation — hand-rolled header text via draw_string(get_theme_default_font(), …). Picker/body/viewer levels draw their title and hint strings directly instead of composing ImplantHeader. Bypasses the implant theme's font + semantic color roles; breaks the D-169 "Theme swap changes the implant hardware appearance" invariant. Replace with an ImplantHeader at the top of each panel.
5 atlas_marker_overlay.gd:161, 170–179, 197 Hoshe + Tyre Overlay reaches into AtlasViewer's private state. viewer._city_canvas_pos(c), viewer._hovered_city, viewer._selected_city — all _-prefixed members accessed from outside. AtlasViewer already publishes get_heightmap_texture() / get_markers() / grid_to_canvas() as the public API. Promote _city_canvas_pos to city_canvas_pos() and add get_hovered_city() / get_selected_city() (or inject them as args on a public render(...) method). viewer is untyped (var viewer = null), so renaming the private fields will silently break at runtime.
6 atlas_viewer.gd:488–498 vs main.gd:187 Tyre KEY_N fall-through teleports the player out of the atlas. Viewer's _unhandled_key_input swallows KEY_N only when _selected_city is non-empty. When no city is selected, KEY_N falls through to main.gd:187 which calls economics_panel.toggle_visible()HudGroups.open_app("implant/economics", INSERT), which closes the FULLSCREEN atlas as a side effect. Surprising UX. Fix: consume KEY_N unconditionally while the viewer is visible, or gate main.gd's KEY_N handler on not HudGroups.is_app_active("implant/map/atlas").
7 atlas_viewer.gd:81–97 vs atlas_overlay_bar.gd:18–42 Tyre Overlay ids duplicated between viewer and bar — drift risk. _overlay_visibility / _overlay_locked in AtlasViewer and OVERLAY_DEFS in AtlasOverlayBar both hardcode the same overlay id list. If they drift, the "locked" classification in the bar and the guard in set_overlay_visible can disagree silently. Single source of truth: move the overlay definitions to a shared const.
8 atlas_overlay_bar.gd:53–56 Tyre class_name AtlasOverlayBar + _init(viewer_ref) with no default is a Godot editor footgun. Any editor script introspection or hot-reload that tries to instantiate the class without args crashes. Either make it _init(viewer_ref = null) with lazy assignment in _ready(), or drop class_name and instance via load(...) as atlas_panel already does for AtlasViewer. (Compare ImplantHeader/DataRow/TextBlock which all use the default-arg pattern.)
9 generate-star-map-data.py:196–238 Tyre terrain_reference regen dependency is tribal knowledge. AtlasViewer's pivot from "data pending" → heightmap-ready is gated on this script having been re-run after server #839 populates terrain_reference in systems.db. There is no Make target dependency or CI assertion for this. Add atlas-generate → star-map-data regen as a make target chain, or a --check assertion so the regen step becomes automatic instead of lore.

Polish

# File:line Reviewer Description
10 atlas_panel.gd:591 Hoshe ImplantDataRow.new(sys.get("bodies", "no body data")) — the "no body data" fallback is dead code (generator always writes entry["bodies"]). Remove or replace with "—" to match the panel's empty-state convention.
11 atlas_viewer.gd:293–298 Tyre _draw() mutates child-node transform. _draw() sets _canvas.position / _canvas.scale every redraw, duplicating what _apply_transform() already does. Causes extra NOTIFICATION_TRANSFORM_CHANGED churn. Remove the duplicate assignment from _draw().
12 atlas_panel.gd:88, atlas_panel.gd:667–673 Hoshe + Tyre Autoload parse-order rule comment copy-pasted to non-autoload site. AtlasPanel is scene-instanced, not an autoload — the parse-order comment at line 88 and the load("res://ui/implant/atlas_viewer.gd").new() dance at 667–673 imply a parse-order risk that doesn't exist for this class. var _viewer := AtlasViewer.new() would work fine. Remove the misleading comments or convert to the direct form.
13 atlas_marker_overlay.gd:177,178 Tyre Dict deep-equality per draw call. if c == selected / elif c == hovered is O(fields) per city per redraw. Fine at 100 cities × 10 fields but balloons before Phase 4. Compare by body_id / index instead.
14 atlas_panel.gd:476,484 Tyre Un-guarded b["body_id"] / s["station_id"] click-handler indexing. If the generator ever emits a NULL id the handler crashes. var bid: String = str(b.get("body_id", "")) with an is_empty() guard matches the defensive style elsewhere in the file.
15 atlas_viewer.gd:149–156 Tyre set_overlay_visible() silently returns on unknown overlay ids. Combined with #7's drift risk, a typo in the bar would do nothing. Add push_warning("unknown overlay id %s" % overlay_id) on the unknown branch.
16 atlas_overlay_bar.gd:79, 93–96 Tyre Always-on buttons connect pressed to a handler that re-sets button_pressed = true on a non-toggle Button (no visual effect after first set). Simpler: disabled = true for always-on buttons and drop the handler.
17 generate-star-map-data.py helpers Hoshe No unit test coverage on compute_gdp, infer_tier_from_population, build_adjacency, parse_wiki_index. Client-side test coverage is sparse project-wide (not a blocker), but compute_gdp's boundary at exactly 1T Tr (BTr → TTr format switch) is one display-facing test worth adding.

Pre-flight results (for the record)

  • gdlint on atlas_panel.gd / atlas_viewer.gd / atlas_marker_overlay.gd / atlas_overlay_bar.gd / main.gd / snapshot_consumers.gd: no problems found.
  • godot --headless --path client --quit on the sprint-35/client worktree: no new SCRIPT ERROR lines introduced. The 23 pre-existing autoload parse errors (sim_bridge, protocol, input_mapper, audio_manager) are unchanged baseline and out of scope.

Re-review cadence

3 blockers, 6 warnings, 8 polish items. Once fixed, /pr-push and re-review. Criterion 1 (star map ↔ atlas app unification) and the helper unit tests (finding 17) can be follow-up tickets rather than blockers on this PR.

## PR Review — sprint-35/client → main (2-reviewer consolidated) **Reviewers:** Hoshe (code quality), Tyre (architecture) **Verdict: CHANGES REQUESTED** Architecturally the PR is well-shaped: the 4-module decomposition (AtlasPanel / AtlasViewer / AtlasMarkerOverlay / AtlasOverlayBar) is clean, D-170 HUD registration is correct, the economics cross-link signal path is the right API shape, the city-data sidebar composes from the implant component library correctly, and pan/zoom math is right. Pre-flight was clean: `gdlint` reported no problems on all 6 touched files, headless `godot --path client --quit` produced no new SCRIPT ERROR lines. The blockers are concentrated in two real bugs and one contract mismatch between the overlay code and the D-191 §8 markers schema. The rest is warnings and polish. --- ### D-191 compliance summary (Tyre) | Decision | Compliance | Notes | |---|---|---| | D-169 ImplantPanel library | PARTIAL | picker / body / station / city / empty_notice correctly compose from ImplantPanel + ImplantHeader/Separator/DataRow/TextBlock. BUT `atlas_panel.gd._draw_header()` and `atlas_viewer.gd._draw_header()` hand-roll the top-level title/hint with `draw_string(get_theme_default_font(), …, COLOR_TEXT)` — bypasses the implant theme and duplicates ImplantHeader. | | D-170 HUD visibility | OK | Registration as `implant/map/atlas` in FULLSCREEN mode; `open_app` emits `gameplay_occluded`; `_on_app_changed` routes mode → visibility. Correct. | | D-181 overlay tier mapping | OK | corp_presence → observable, shadow_economy → toggleable, stockpile_weeks → semi-private, production_vs_baseline → private (locked). Matches the D-181 visibility ladder. | | D-191 criterion 1 (navigation chain) | PARTIAL | Within-atlas nav works. star_map.gd still has a `TODO: migrate to HudGroups.open_app("implant/map/starchart")` — atlas and Reach star map are currently separate apps, not one extended chain. Not this PR's fault (star_map.gd predates D-170), but criterion 1 is technically not end-to-end until the two are linked. Acceptable for this PR; flag as follow-up. | | D-191 criterion 4 (city data panel) | OK | Name, pop tier, function, currency zone, Commission presence, shadow zone, gate distance. Cross-link button present. | | D-191 criterion 5 (economics link) | OK | `AtlasViewer.economics_link_requested(system_id)` → `AtlasPanel.economics_link_requested` → `main._on_atlas_economics_link` → `EconomicsPanel.select_system()` + `HudGroups.open_app("implant/economics", INSERT)`. Clean signal path. | | D-191 criterion 6 (9 MVP overlays) | PARTIAL | 11 buttons present. `terrain`, `infrastructure`, `named_features`, `gate_markers`, `population_density` render real data. `political_zones` draws a flat tint (flagged as MVP placeholder in comment). **3 of 4 toggleable overlays (`production_zones`, `shadow_economy`, `corp_presence`) read `markers.production_zones` / `markers.shadow_zones` / `markers.corp_presence` — none of these keys exist in D-191 §8's markers schema.** Toggling draws nothing. See comment #3 below. | | D-191 criterion 8 (station mini panel) | OK | Orbital click on station → `_station_panel` ImplantPanel with name, operator, function, currency, deferred hint, no drill-down. Matches spec. | | D-010 / D-012 | N/A | Client-only rendering code. | --- ### Blocking — fix before merge | # | File:line | Reviewer | Description | |---|-----------|----------|-------------| | 1 | `atlas_panel.gd:240–244` | Hoshe | **Moon placement divisor hard-coded to `4.0`.** `TAU * float(moon_idx - 1) / 4.0` — if a parent body has 5 or more moons, moon 5 lands at the same angle as moon 1, moon 6 overlaps moon 2, etc. Dots render on top of each other and become unclickable. Fix: use the count of moons per parent body as the divisor, not the constant. | | 2 | `atlas_viewer.gd:198–199` (vs line 202 null check) | Hoshe | **Stale `_grid_w` / `_grid_h` when a body has no heightmap after one that did.** `_grid_w` and `_grid_h` are set from `_tex_w` / `_tex_h` on lines 198–199 *before* the `terrain_reference` null check at line 202. When showing a body with `terrain_reference = null` after a body that had one, the grid dimensions stay pointed at the previous body's texture. `grid_to_canvas()` then uses stale dimensions and markers render in the wrong place. Fix: move the grid assignment to after the null check, or reset to safe defaults when `terrain_reference` is null. | | 3 | `atlas_marker_overlay.gd:203–220` | Tyre | **Three toggleable overlays read non-existent markers schema keys.** `_draw_production_zones`, `_draw_shadow_economy`, `_draw_corp_presence` read `markers.production_zones`, `markers.shadow_zones`, `markers.corp_presence` — none are in D-191 §8's markers schema (`cities`, `roads`, `railroads`, `pois`, `rivers`, `oceans`, `mountain_ranges`). No generator will populate them. Toggling these overlays is a silent no-op. Fix options: (a) derive from existing data — e.g. `corp_presence` from `cities[].commission_presence`; or (b) amend D-191 §8 schema and add a follow-up ticket for the server generator. Current state makes criterion 6's "present AND functional" false for 3/4 toggleable overlays. | ### Warnings — fix or document | # | File:line | Reviewer | Description | |---|-----------|----------|-------------| | 4 | `atlas_panel.gd:288–291`, `atlas_viewer.gd:301–308` | Tyre | **D-169 violation — hand-rolled header text via `draw_string(get_theme_default_font(), …)`.** Picker/body/viewer levels draw their title and hint strings directly instead of composing ImplantHeader. Bypasses the implant theme's font + semantic color roles; breaks the D-169 "Theme swap changes the implant hardware appearance" invariant. Replace with an ImplantHeader at the top of each panel. | | 5 | `atlas_marker_overlay.gd:161, 170–179, 197` | Hoshe + Tyre | **Overlay reaches into AtlasViewer's private state.** `viewer._city_canvas_pos(c)`, `viewer._hovered_city`, `viewer._selected_city` — all `_`-prefixed members accessed from outside. AtlasViewer already publishes `get_heightmap_texture()` / `get_markers()` / `grid_to_canvas()` as the public API. Promote `_city_canvas_pos` to `city_canvas_pos()` and add `get_hovered_city()` / `get_selected_city()` (or inject them as args on a public `render(...)` method). `viewer` is untyped (`var viewer = null`), so renaming the private fields will silently break at runtime. | | 6 | `atlas_viewer.gd:488–498` vs `main.gd:187` | Tyre | **KEY_N fall-through teleports the player out of the atlas.** Viewer's `_unhandled_key_input` swallows KEY_N only when `_selected_city` is non-empty. When no city is selected, KEY_N falls through to `main.gd:187` which calls `economics_panel.toggle_visible()` → `HudGroups.open_app("implant/economics", INSERT)`, which closes the FULLSCREEN atlas as a side effect. Surprising UX. Fix: consume KEY_N unconditionally while the viewer is visible, or gate main.gd's KEY_N handler on `not HudGroups.is_app_active("implant/map/atlas")`. | | 7 | `atlas_viewer.gd:81–97` vs `atlas_overlay_bar.gd:18–42` | Tyre | **Overlay ids duplicated between viewer and bar — drift risk.** `_overlay_visibility` / `_overlay_locked` in AtlasViewer and `OVERLAY_DEFS` in AtlasOverlayBar both hardcode the same overlay id list. If they drift, the "locked" classification in the bar and the guard in `set_overlay_visible` can disagree silently. Single source of truth: move the overlay definitions to a shared const. | | 8 | `atlas_overlay_bar.gd:53–56` | Tyre | **`class_name AtlasOverlayBar` + `_init(viewer_ref)` with no default is a Godot editor footgun.** Any editor script introspection or hot-reload that tries to instantiate the class without args crashes. Either make it `_init(viewer_ref = null)` with lazy assignment in `_ready()`, or drop `class_name` and instance via `load(...)` as atlas_panel already does for AtlasViewer. (Compare ImplantHeader/DataRow/TextBlock which all use the default-arg pattern.) | | 9 | `generate-star-map-data.py:196–238` | Tyre | **`terrain_reference` regen dependency is tribal knowledge.** AtlasViewer's pivot from "data pending" → heightmap-ready is gated on this script having been re-run after server #839 populates `terrain_reference` in systems.db. There is no Make target dependency or CI assertion for this. Add `atlas-generate` → star-map-data regen as a make target chain, or a `--check` assertion so the regen step becomes automatic instead of lore. | ### Polish | # | File:line | Reviewer | Description | |---|-----------|----------|-------------| | 10 | `atlas_panel.gd:591` | Hoshe | `ImplantDataRow.new(sys.get("bodies", "no body data"))` — the `"no body data"` fallback is dead code (generator always writes `entry["bodies"]`). Remove or replace with `"—"` to match the panel's empty-state convention. | | 11 | `atlas_viewer.gd:293–298` | Tyre | **`_draw()` mutates child-node transform.** `_draw()` sets `_canvas.position` / `_canvas.scale` every redraw, duplicating what `_apply_transform()` already does. Causes extra `NOTIFICATION_TRANSFORM_CHANGED` churn. Remove the duplicate assignment from `_draw()`. | | 12 | `atlas_panel.gd:88`, `atlas_panel.gd:667–673` | Hoshe + Tyre | **Autoload parse-order rule comment copy-pasted to non-autoload site.** AtlasPanel is scene-instanced, not an autoload — the parse-order comment at line 88 and the `load("res://ui/implant/atlas_viewer.gd").new()` dance at 667–673 imply a parse-order risk that doesn't exist for this class. `var _viewer := AtlasViewer.new()` would work fine. Remove the misleading comments or convert to the direct form. | | 13 | `atlas_marker_overlay.gd:177,178` | Tyre | **Dict deep-equality per draw call.** `if c == selected` / `elif c == hovered` is O(fields) per city per redraw. Fine at 100 cities × 10 fields but balloons before Phase 4. Compare by `body_id` / index instead. | | 14 | `atlas_panel.gd:476,484` | Tyre | **Un-guarded `b["body_id"]` / `s["station_id"]` click-handler indexing.** If the generator ever emits a NULL id the handler crashes. `var bid: String = str(b.get("body_id", ""))` with an `is_empty()` guard matches the defensive style elsewhere in the file. | | 15 | `atlas_viewer.gd:149–156` | Tyre | `set_overlay_visible()` silently returns on unknown overlay ids. Combined with #7's drift risk, a typo in the bar would do nothing. Add `push_warning("unknown overlay id %s" % overlay_id)` on the unknown branch. | | 16 | `atlas_overlay_bar.gd:79, 93–96` | Tyre | Always-on buttons connect `pressed` to a handler that re-sets `button_pressed = true` on a non-toggle Button (no visual effect after first set). Simpler: `disabled = true` for always-on buttons and drop the handler. | | 17 | `generate-star-map-data.py` helpers | Hoshe | No unit test coverage on `compute_gdp`, `infer_tier_from_population`, `build_adjacency`, `parse_wiki_index`. Client-side test coverage is sparse project-wide (not a blocker), but `compute_gdp`'s boundary at exactly 1T Tr (BTr → TTr format switch) is one display-facing test worth adding. | --- ### Pre-flight results (for the record) - `gdlint` on `atlas_panel.gd / atlas_viewer.gd / atlas_marker_overlay.gd / atlas_overlay_bar.gd / main.gd / snapshot_consumers.gd`: **no problems found**. - `godot --headless --path client --quit` on the sprint-35/client worktree: **no new SCRIPT ERROR lines introduced**. The 23 pre-existing autoload parse errors (sim_bridge, protocol, input_mapper, audio_manager) are unchanged baseline and out of scope. ### Re-review cadence 3 blockers, 6 warnings, 8 polish items. Once fixed, `/pr-push` and re-review. Criterion 1 (star map ↔ atlas app unification) and the helper unit tests (finding 17) can be follow-up tickets rather than blockers on this PR.
jpmschweitzer added 3 commits 2026-04-15 09:02:45 +02:00
1. Moon placement on the orbital diagram divided by a hard-coded 4 — a gas
   giant with five or more moons would overlap satellites and make them
   unclickable. Count moons per parent and distribute them evenly.
2. AtlasViewer._load_markers seeded _grid_w/_grid_h from _tex_w/_tex_h
   before the terrain_reference null check, so opening a body with no
   heightmap after one that had a heightmap left the grid dimensions
   pointing at the previous texture and misplaced markers. Reset texture +
   grid dims to sentinel defaults at the top of _load_heightmap.
3. AtlasMarkerOverlay's three toggleable overlays (production_zones,
   shadow_economy, corp_presence) read markers.production_zones /
   markers.shadow_zones / markers.corp_presence — keys that don't exist in
   D-191 §8's markers schema, so toggling was a silent no-op. Derive them
   from cities[] instead: primary_function ∈ PRODUCTION_FUNCTIONS for
   production, absent Commission presence for shadow bands, Commission
   presence for corp dots. Follow-up ticket will formalise per-overlay
   arrays once the server schema lands.

Addresses PR #128 review blockers 1-3.
4. AtlasPanel and AtlasViewer now compose their title + hint from an
   ImplantHeader child rather than hand-rolling them via draw_string, so the
   D-169 "theme swap changes the implant hardware appearance" invariant
   holds end-to-end. _refresh_screen_header() drives content per level and
   on system navigation.

5. AtlasViewer exposes city_canvas_pos(), get_hovered_city(),
   get_selected_city(), and get_overlay_defs() as public API — the marker
   overlay no longer reaches into underscore-prefixed state, which is
   especially important because viewer is an untyped var in the overlay.

6. KEY_N now consumes unconditionally while the viewer is visible, and
   main.gd's global economics-monitor toggle is gated on
   !HudGroups.is_app_active("implant/map/atlas"). Previously pressing N
   without a selected city fell through and closed the fullscreen atlas as
   a side effect.

7. OVERLAY_DEFS lives in AtlasViewer as the single source of truth.
   AtlasOverlayBar reads the list via viewer.get_overlay_defs(), and
   AtlasViewer derives _overlay_visibility / _overlay_locked from the same
   table at _ready() — no more hand-maintained parallel lists, so the bar
   and the guard in set_overlay_visible can't drift.

8. AtlasOverlayBar drops `class_name`: it now loads via
   load("res://ui/implant/atlas_overlay_bar.gd") from AtlasViewer, the same
   pattern AtlasPanel uses for AtlasViewer. _init(viewer_ref = null) keeps
   the required-arg footgun off the editor's introspection path.

9. `star-map-data` make target added to regenerate
   client/data/star_map_data.json from systems.db + wiki, and
   `check-star-map` wired into pre-pr-validate + pre-pr-client so any
   commit that touches the generator (or any downstream systems.db change
   like server #839) fails pre-pr until the JSON is regenerated. The
   terrain_reference data-availability dependency is no longer tribal
   knowledge.

Also addresses review #15 (push_warning on unknown overlay id in
set_overlay_visible) and #16 (disabled always-on buttons drop handler
churn) as part of the same refactor.
10. Dropped the "no body data" fallback string in the picker panel — the
    generator always writes entry["bodies"], so the fallback was dead. Use
    the "—" convention the rest of the panel already follows.
12. Removed the autoload parse-order comment from AtlasPanel (it is
    scene-instanced, not an autoload, so the rule does not apply), and
    collapsed _build_heightmap_viewer to a direct AtlasViewer.new() —
    mirroring the rest of the file rather than dancing around a risk that
    is not real for this class.
13. AtlasMarkerOverlay._draw_cities now compares hover/selection by a
    stable _city_key() (name → city_id → pos → hash) instead of
    Dictionary.==, which was O(fields) per city per redraw. Preps the
    renderer for much larger city counts without a rewrite.
14. Orbital click/draw handlers guard against missing body_id /
    station_id by reading through str(dict.get(..., "")) and skipping
    empty ids. Matches the defensive style already used for parent_body_id
    and keeps a NULL id from crashing _draw_bodies / _handle_orbital_click.
jpmschweitzer added 1 commit 2026-04-15 09:06:16 +02:00
Author
Owner

Review response — blockers + warnings + polish all addressed

Pushed four follow-up commits in the order the review laid them out. Pre-flight on the current branch is clean: gdlint zero, make check-star-map OK, headless godot no new script errors. gdformat reports one pre-existing advisory on client/ui/stance_indicator.gd which is out of scope for this branch.

Blockers → 1328db31

  1. Moon divisorAtlasPanel._compute_body_positions now groups moons per parent and divides TAU by the per-parent count instead of the hardcoded 4.0. Gas giants with 5+ moons no longer overlap satellites.
  2. Stale grid dims_load_heightmap resets _heightmap_texture / _tex_w / _tex_h to sentinel defaults at the top so opening a body with no heightmap after one that had one can no longer inherit the previous texture's dimensions.
  3. Dead overlay schema keys_draw_production_zones / _draw_shadow_economy / _draw_corp_presence now read from cities[] (primary_function filter, commission_presence inverse, commission_presence positive respectively). Option (a) from the review. Follow-up ticket #846 tracks formalising per-overlay arrays in D-191 §8 with the server team.

Warnings → f8b81e1f

  1. D-169 headers — both AtlasPanel and AtlasViewer now compose top-of-screen title/hint from an ImplantHeader child (_build_screen_header + _refresh_screen_header). All draw_string(get_theme_default_font(), …) calls in the atlas UI are gone; swapping the implant theme now cascades to the screen header.
  2. Public viewer APIAtlasViewer exposes city_canvas_pos(), get_hovered_city(), get_selected_city(), and get_overlay_defs(). AtlasMarkerOverlay no longer reaches into underscore-prefixed fields.
  3. KEY_N fall-throughAtlasViewer._unhandled_key_input now consumes KEY_N unconditionally while the viewer is visible, and main.gd's global economics-monitor toggle is gated on !HudGroups.is_app_active("implant/map/atlas"). No more atlas-closes-as-side-effect.
  4. Overlay source of truthOVERLAY_DEFS moved to AtlasViewer; AtlasOverlayBar reads viewer.get_overlay_defs() and AtlasViewer._overlay_visibility / _overlay_locked derive from the same table at _ready(). Parallel lists eliminated.
  5. class_name footgunAtlasOverlayBar drops class_name; AtlasViewer instances it via load("res://ui/implant/atlas_overlay_bar.gd").new(self). _init(viewer_ref = null) also has a default so an editor-introspection path can't crash.
  6. Make target — new star-map-data target (regen) and check-star-map is now wired into pre-pr-client + pre-pr-validate. Pre-PR will fail when the JSON is stale instead of relying on tribal knowledge. Help text updated.

Polish → 489aec48

  1. Dead "no body data" fallback replaced with "—".
  2. _draw() transform duplication — already addressed in the warnings pass (background rect only).
  3. Misleading autoload parse-order comment removed from AtlasPanel; _build_heightmap_viewer now just calls AtlasViewer.new() directly.
  4. Dict-equality per draw call replaced with a stable _city_key() (name → city_id → pos → lat/lon → hash). Ready for Phase 4 city counts.
  5. b["body_id"] / s["station_id"] indexing guarded via str(dict.get(..., "")) + is_empty() in _compute_body_positions, _draw_bodies, _draw_stations, and _handle_orbital_click.
  6. set_overlay_visible() now emits a push_warning on unknown overlay ids.
  7. Always-on buttons in AtlasOverlayBar are disabled = true with a pinned font_disabled_color override — no handler churn.

Format → 22c17f1f

Ran gdformat on the three modified atlas files to clear the pre-push advisory. stance_indicator.gd's advisory is pre-existing and outside the scope of this PR.

Deferred to follow-up tickets

  • #844 — Unify star map and atlas into single implant/map chain (D-191 criterion 1 end-to-end).
  • #845 — Unit tests for generate-star-map-data.py helpers (compute_gdp boundary, etc).
  • #846 — Amend D-191 §8 markers schema to formalize per-overlay arrays.

Ready for re-review.

## Review response — blockers + warnings + polish all addressed Pushed four follow-up commits in the order the review laid them out. Pre-flight on the current branch is clean: `gdlint` zero, `make check-star-map` OK, headless godot no new script errors. `gdformat` reports one pre-existing advisory on `client/ui/stance_indicator.gd` which is out of scope for this branch. ### Blockers → `1328db31` 1. **Moon divisor** — `AtlasPanel._compute_body_positions` now groups moons per parent and divides `TAU` by the per-parent count instead of the hardcoded `4.0`. Gas giants with 5+ moons no longer overlap satellites. 2. **Stale grid dims** — `_load_heightmap` resets `_heightmap_texture` / `_tex_w` / `_tex_h` to sentinel defaults at the top so opening a body with no heightmap after one that had one can no longer inherit the previous texture's dimensions. 3. **Dead overlay schema keys** — `_draw_production_zones` / `_draw_shadow_economy` / `_draw_corp_presence` now read from `cities[]` (primary_function filter, commission_presence inverse, commission_presence positive respectively). Option (a) from the review. Follow-up ticket #846 tracks formalising per-overlay arrays in D-191 §8 with the server team. ### Warnings → `f8b81e1f` 4. **D-169 headers** — both `AtlasPanel` and `AtlasViewer` now compose top-of-screen title/hint from an `ImplantHeader` child (`_build_screen_header` + `_refresh_screen_header`). All `draw_string(get_theme_default_font(), …)` calls in the atlas UI are gone; swapping the implant theme now cascades to the screen header. 5. **Public viewer API** — `AtlasViewer` exposes `city_canvas_pos()`, `get_hovered_city()`, `get_selected_city()`, and `get_overlay_defs()`. `AtlasMarkerOverlay` no longer reaches into underscore-prefixed fields. 6. **KEY_N fall-through** — `AtlasViewer._unhandled_key_input` now consumes `KEY_N` unconditionally while the viewer is visible, and `main.gd`'s global economics-monitor toggle is gated on `!HudGroups.is_app_active("implant/map/atlas")`. No more atlas-closes-as-side-effect. 7. **Overlay source of truth** — `OVERLAY_DEFS` moved to `AtlasViewer`; `AtlasOverlayBar` reads `viewer.get_overlay_defs()` and `AtlasViewer._overlay_visibility` / `_overlay_locked` derive from the same table at `_ready()`. Parallel lists eliminated. 8. **`class_name` footgun** — `AtlasOverlayBar` drops `class_name`; `AtlasViewer` instances it via `load("res://ui/implant/atlas_overlay_bar.gd").new(self)`. `_init(viewer_ref = null)` also has a default so an editor-introspection path can't crash. 9. **Make target** — new `star-map-data` target (regen) and `check-star-map` is now wired into `pre-pr-client` + `pre-pr-validate`. Pre-PR will fail when the JSON is stale instead of relying on tribal knowledge. Help text updated. ### Polish → `489aec48` 10. Dead `"no body data"` fallback replaced with `"—"`. 11. `_draw()` transform duplication — already addressed in the warnings pass (background rect only). 12. Misleading autoload parse-order comment removed from `AtlasPanel`; `_build_heightmap_viewer` now just calls `AtlasViewer.new()` directly. 13. Dict-equality per draw call replaced with a stable `_city_key()` (name → city_id → pos → lat/lon → hash). Ready for Phase 4 city counts. 14. `b["body_id"]` / `s["station_id"]` indexing guarded via `str(dict.get(..., ""))` + `is_empty()` in `_compute_body_positions`, `_draw_bodies`, `_draw_stations`, and `_handle_orbital_click`. 15. `set_overlay_visible()` now emits a `push_warning` on unknown overlay ids. 16. Always-on buttons in `AtlasOverlayBar` are `disabled = true` with a pinned `font_disabled_color` override — no handler churn. ### Format → `22c17f1f` Ran `gdformat` on the three modified atlas files to clear the pre-push advisory. `stance_indicator.gd`'s advisory is pre-existing and outside the scope of this PR. ### Deferred to follow-up tickets - **#844** — Unify star map and atlas into single `implant/map` chain (D-191 criterion 1 end-to-end). - **#845** — Unit tests for `generate-star-map-data.py` helpers (compute_gdp boundary, etc). - **#846** — Amend D-191 §8 markers schema to formalize per-overlay arrays. Ready for re-review.
Author
Owner

PR Review Round 2 — sprint-35/client → main

Reviewers: Hoshe (code quality), Tyre (architecture)
Verdict: APPROVED (2/2)

Fix commit chain (1328db31 fix → f8b81e1f refactor → 489aec48 polish → 22c17f1f gdformat) cleanly resolves every R1 finding. No regressions introduced; the +240 net lines are all legitimate fix work (OVERLAY_DEFS const + getters, ImplantHeader composition, schema-compliant overlay derivations, public AtlasViewer API). The OVERLAY_DEFS consolidation is particularly elegant — one const on AtlasViewer now feeds both the marker renderer (via is_overlay_visible) and the toggle bar (via get_overlay_defs), eliminating the R1 drift risk and making #836 extensions a one-file change.


D-decision re-compliance

Decision R1 R2 Notes
D-169 (implant component library) PARTIAL (hand-rolled draw_string title/hint) PASS atlas_viewer.gd:387 + atlas_panel.gd:332 compose ImplantHeader with apply_implant_theme. On-canvas body/station labels outside D-169 scope.
D-170 (HUD visibility) OK OK Registration unchanged; KEY_N gating via HudGroups.is_app_active added in main.gd:191
D-181 (signal visibility ladder) OK OK OVERLAY_DEFS groups map to always/toggle/locked correctly
D-191 §8 markers schema FAIL (3 overlays read non-existent keys) PASS Overlays now derive from primary_function (PRODUCTION_FUNCTIONS whitelist) and commission_presence — schema-compliant fields; doc comments acknowledge future explicit-schema promotion
D-191 criterion 4 (city data panel) OK OK Unchanged
D-191 criterion 5 (economics link) OK OK KEY_N consume fix prevents side-effect atlas close
D-191 criterion 6 (9 overlays present+functional) PARTIAL (3 silent no-ops) PASS Toggling production/shadow/corp now produces visible output
D-191 criterion 8 (station mini panel) OK OK Unchanged

Per-finding resolution

# R1 finding Status Resolution
1 BLOCKER — moon divisor hardcoded 4.0 (5+ moons overlap) RESOLVED atlas_panel.gd:252–267 groups moons by parent via moons_by_parent dict, uses siblings.size() as the divisor; comment at 237–239 cites review #1
2 BLOCKER — stale _grid_w/_grid_h across bodies RESOLVED atlas_viewer.gd:260–268 resets _tex_w=1024 / _tex_h=512 unconditionally before any null guard; _load_markers() immediately assigns grid defaults before the terrain_reference null check; comment at 285–287 cites review #2
3 BLOCKER — 3 overlays read non-D-191 §8 keys RESOLVED atlas_marker_overlay.gd:260–292 derive production/shadow/corp overlays from primary_function + commission_presence (schema-compliant). Softly-functional (all false → shadow renders everywhere, corp renders nothing) but produces visible output; acceptable MVP until schema grows explicit zone polygons
4 D-169 hand-rolled draw_string title/hint RESOLVED Both atlas_viewer.gd:383–402 and atlas_panel.gd:328–373 compose ImplantHeader via set_content + apply_implant_theme(_implant_theme), matching implant_header.gd API
5 Private-member reach-in from overlay RESOLVED Public API added to AtlasViewer (city_canvas_pos, get_hovered_city, get_selected_city, get_heightmap_texture, get_markers, get_overlay_defs, is_overlay_visible at 235–252, 499); marker overlay calls public API throughout
6 KEY_N fall-through teleport RESOLVED Double-defense: atlas_viewer.gd:582–597 _unhandled_key_input unconditionally consumes KEY_N via set_input_as_handled() while visible AND main.gd:191 gates the global handler on not HudGroups.is_app_active("implant/map/atlas")
7 Overlay ids duplicated across viewer and bar RESOLVED Single const OVERLAY_DEFS at atlas_viewer.gd:58–125; atlas_overlay_bar.gd:34 iterates via _viewer.get_overlay_defs(); no dict copy in the bar
8 class_name AtlasOverlayBar + required _init footgun RESOLVED class_name removed; _init(viewer_ref = null) with default at line 25; loaded via load().new(self) at atlas_viewer.gd:640–641
9 generate-star-map-data.py terrain_reference tribal knowledge RESOLVED CI-enforced. Makefile:377 check-star-map runs generate-star-map-data.py --check (generator exits 1 on drift at lines 329, 336); wired into pre-pr-client (307) and pre-pr-validate (265). Any PR that mutates systems.db without regenerating star_map_data.json fails the pre-PR gate.
10 "no body data" dead fallback RESOLVED atlas_panel.gd:649 now uses "—" matching the panel empty-state convention
11 _draw() mutates child transform RESOLVED atlas_viewer.gd:379–380 _draw() now only draws the background rect; canvas mutation happens only in _apply_transform()
12 Parse-order comment on non-autoload class RESOLVED atlas_panel.gd:88 direct load() with no misleading autoload comment; _viewer = AtlasViewer.new() at 723 via class_name directly
13 Dict deep-equality per _draw() call RESOLVED atlas_marker_overlay.gd:190–198 uses _city_key() string comparison; _city_key (222–238) prefers namecity_id → positional → hash
14 Unguarded b["body_id"] / s["station_id"] RESOLVED atlas_panel.gd:533 str(b.get("body_id", "")), 541 str(s.get("station_id", "")); _compute_body_positions skips empty ids
15 set_overlay_visible() silent return on unknown id RESOLVED atlas_viewer.gd:220–222 push_warning("AtlasViewer: unknown overlay id '%s'" % overlay_id)
16 Always-on button pointless handler RESOLVED atlas_overlay_bar.gd:46–50 uses disabled = true, button_pressed = true with font_disabled_color override; no .toggled.connect() on always group
17 No tests on generate-star-map-data.py helpers NOT RESOLVED (acceptable) R1 classified this as a note, not a blocker. Follow-up ticket acceptable.

Regressions

None. Line growth is all legitimate fix work:

  • atlas_viewer.gd +102: OVERLAY_DEFS const + getters (+67) + screen header composition (+26) + public API helpers
  • atlas_panel.gd +59: screen header composition (+45) + moon grouping logic (+14)
  • atlas_marker_overlay.gd +74: three schema-compliant overlay draw functions + _city_key helper
  • atlas_overlay_bar.gd −35: const moved upstream, class_name removed, always-on handler dropped

PR #128 is approved for merge.

## PR Review Round 2 — sprint-35/client → main **Reviewers:** Hoshe (code quality), Tyre (architecture) **Verdict: APPROVED** (2/2) Fix commit chain (`1328db31 fix → f8b81e1f refactor → 489aec48 polish → 22c17f1f gdformat`) cleanly resolves every R1 finding. No regressions introduced; the +240 net lines are all legitimate fix work (OVERLAY_DEFS const + getters, ImplantHeader composition, schema-compliant overlay derivations, public AtlasViewer API). The `OVERLAY_DEFS` consolidation is particularly elegant — one const on AtlasViewer now feeds both the marker renderer (via `is_overlay_visible`) and the toggle bar (via `get_overlay_defs`), eliminating the R1 drift risk and making #836 extensions a one-file change. --- ### D-decision re-compliance | Decision | R1 | R2 | Notes | |---|---|---|---| | D-169 (implant component library) | PARTIAL (hand-rolled `draw_string` title/hint) | **PASS** | `atlas_viewer.gd:387` + `atlas_panel.gd:332` compose `ImplantHeader` with `apply_implant_theme`. On-canvas body/station labels outside D-169 scope. | | D-170 (HUD visibility) | OK | OK | Registration unchanged; KEY_N gating via `HudGroups.is_app_active` added in `main.gd:191` | | D-181 (signal visibility ladder) | OK | OK | OVERLAY_DEFS groups map to always/toggle/locked correctly | | D-191 §8 markers schema | FAIL (3 overlays read non-existent keys) | **PASS** | Overlays now derive from `primary_function` (PRODUCTION_FUNCTIONS whitelist) and `commission_presence` — schema-compliant fields; doc comments acknowledge future explicit-schema promotion | | D-191 criterion 4 (city data panel) | OK | OK | Unchanged | | D-191 criterion 5 (economics link) | OK | OK | KEY_N consume fix prevents side-effect atlas close | | D-191 criterion 6 (9 overlays present+functional) | PARTIAL (3 silent no-ops) | **PASS** | Toggling production/shadow/corp now produces visible output | | D-191 criterion 8 (station mini panel) | OK | OK | Unchanged | --- ### Per-finding resolution | # | R1 finding | Status | Resolution | |---|------------|--------|------------| | 1 | **BLOCKER** — moon divisor hardcoded `4.0` (5+ moons overlap) | RESOLVED | `atlas_panel.gd:252–267` groups moons by parent via `moons_by_parent` dict, uses `siblings.size()` as the divisor; comment at 237–239 cites review #1 | | 2 | **BLOCKER** — stale `_grid_w`/`_grid_h` across bodies | RESOLVED | `atlas_viewer.gd:260–268` resets `_tex_w=1024 / _tex_h=512` unconditionally before any null guard; `_load_markers()` immediately assigns grid defaults before the `terrain_reference` null check; comment at 285–287 cites review #2 | | 3 | **BLOCKER** — 3 overlays read non-D-191 §8 keys | RESOLVED | `atlas_marker_overlay.gd:260–292` derive production/shadow/corp overlays from `primary_function` + `commission_presence` (schema-compliant). Softly-functional (all false → shadow renders everywhere, corp renders nothing) but produces visible output; acceptable MVP until schema grows explicit zone polygons | | 4 | D-169 hand-rolled `draw_string` title/hint | RESOLVED | Both `atlas_viewer.gd:383–402` and `atlas_panel.gd:328–373` compose ImplantHeader via `set_content` + `apply_implant_theme(_implant_theme)`, matching `implant_header.gd` API | | 5 | Private-member reach-in from overlay | RESOLVED | Public API added to AtlasViewer (`city_canvas_pos`, `get_hovered_city`, `get_selected_city`, `get_heightmap_texture`, `get_markers`, `get_overlay_defs`, `is_overlay_visible` at 235–252, 499); marker overlay calls public API throughout | | 6 | KEY_N fall-through teleport | RESOLVED | Double-defense: `atlas_viewer.gd:582–597` `_unhandled_key_input` unconditionally consumes KEY_N via `set_input_as_handled()` while visible AND `main.gd:191` gates the global handler on `not HudGroups.is_app_active("implant/map/atlas")` | | 7 | Overlay ids duplicated across viewer and bar | RESOLVED | Single const `OVERLAY_DEFS` at `atlas_viewer.gd:58–125`; `atlas_overlay_bar.gd:34` iterates via `_viewer.get_overlay_defs()`; no dict copy in the bar | | 8 | `class_name AtlasOverlayBar` + required `_init` footgun | RESOLVED | `class_name` removed; `_init(viewer_ref = null)` with default at line 25; loaded via `load().new(self)` at `atlas_viewer.gd:640–641` | | 9 | `generate-star-map-data.py` terrain_reference tribal knowledge | RESOLVED | **CI-enforced.** `Makefile:377` `check-star-map` runs `generate-star-map-data.py --check` (generator exits 1 on drift at lines 329, 336); wired into `pre-pr-client` (307) and `pre-pr-validate` (265). Any PR that mutates `systems.db` without regenerating `star_map_data.json` fails the pre-PR gate. | | 10 | `"no body data"` dead fallback | RESOLVED | `atlas_panel.gd:649` now uses `"—"` matching the panel empty-state convention | | 11 | `_draw()` mutates child transform | RESOLVED | `atlas_viewer.gd:379–380` `_draw()` now only draws the background rect; canvas mutation happens only in `_apply_transform()` | | 12 | Parse-order comment on non-autoload class | RESOLVED | `atlas_panel.gd:88` direct `load()` with no misleading autoload comment; `_viewer = AtlasViewer.new()` at 723 via class_name directly | | 13 | Dict deep-equality per `_draw()` call | RESOLVED | `atlas_marker_overlay.gd:190–198` uses `_city_key()` string comparison; `_city_key` (222–238) prefers `name` → `city_id` → positional → hash | | 14 | Unguarded `b["body_id"]` / `s["station_id"]` | RESOLVED | `atlas_panel.gd:533` `str(b.get("body_id", ""))`, 541 `str(s.get("station_id", ""))`; `_compute_body_positions` skips empty ids | | 15 | `set_overlay_visible()` silent return on unknown id | RESOLVED | `atlas_viewer.gd:220–222` `push_warning("AtlasViewer: unknown overlay id '%s'" % overlay_id)` | | 16 | Always-on button pointless handler | RESOLVED | `atlas_overlay_bar.gd:46–50` uses `disabled = true, button_pressed = true` with `font_disabled_color` override; no `.toggled.connect()` on always group | | 17 | No tests on `generate-star-map-data.py` helpers | NOT RESOLVED (acceptable) | R1 classified this as a note, not a blocker. Follow-up ticket acceptable. | --- ### Regressions **None.** Line growth is all legitimate fix work: - `atlas_viewer.gd` +102: OVERLAY_DEFS const + getters (+67) + screen header composition (+26) + public API helpers - `atlas_panel.gd` +59: screen header composition (+45) + moon grouping logic (+14) - `atlas_marker_overlay.gd` +74: three schema-compliant overlay draw functions + `_city_key` helper - `atlas_overlay_bar.gd` −35: const moved upstream, `class_name` removed, always-on handler dropped --- **PR #128 is approved for merge.**
jpmschweitzer closed this pull request 2026-04-15 09:16:57 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jpmschweitzer/settled-reach#128