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)
## 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)
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.
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.
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.
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.
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.
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
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.
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.
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
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.
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.
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.
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.
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.
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
Dead "no body data" fallback replaced with "—".
_draw() transform duplication — already addressed in the warnings pass (background rect only).
Misleading autoload parse-order comment removed from AtlasPanel; _build_heightmap_viewer now just calls AtlasViewer.new() directly.
Dict-equality per draw call replaced with a stable _city_key() (name → city_id → pos → lat/lon → hash). Ready for Phase 4 city counts.
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.
set_overlay_visible() now emits a push_warning on unknown overlay ids.
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).
## 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.
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.
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
CI-enforced.Makefile:377check-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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 35 client work — Atlas of the Reach (Phase 3 planetary viewer), all three stories in the chain.
implant/map/atlasFULLSCREEN app (z=20) with three nav levels: system picker (◄ ► cycle), orbital diagram (star-centred with bodies grouped byorbit_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.pyextended to emit per-systemcurrency_zone,orbit_bodies, andstationsarrays fromsystems.db;star_map_data.jsonregenerated deterministically. Wired intohud.tscn, toggled via the A key inmain.gd, insert-state propagated throughsnapshot_consumers.gd.AtlasViewercontrol loads a body'sterrain_referencePNG andmarkers.json, renders heightmap + markers in texture-space via anAtlasMarkerOverlayNode2D child of a transformed canvas (pan = offset, zoom = scale, cursor-centred wheel zoom). Handles emptymarkers.jsonand missingterrain_referencegracefully (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 emitseconomics_link_requested(system_id)—main.gdbridges this toEconomicsPanel.select_system()+HudGroups.open_app("implant/economics"), satisfying the D-191 Phase 2/3 cross-panel integration.AtlasOverlayBarHBoxContainer 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 throughAtlasViewer.set_overlay_visible(), which is the single entry point into the per-overlay visibility dict consumed byAtlasMarkerOverlay._draw().Implements D-191 criteria 1, 4, 5, 6, 8.
Notes for reviewers
#839has not yet populatedterrain_referenceinsystems.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 downstreamstar_map_data.jsonregen once#839lands.markers.jsonfiles for most bodies currently have emptycities/roads/rail/poisarrays — the overlay handles this without crashing. Once server#832/#833populate them, markers appear automatically with no client change.Test plan
terrain_referenceis populated: drag to pan, wheel to zoom, R to reset, click a city → sidebar, N → economics monitor opens pre-filtered to the systemAtlasOverlayBar 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.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:
gdlintreported no problems on all 6 touched files, headlessgodot --path client --quitproduced 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)
atlas_panel.gd._draw_header()andatlas_viewer.gd._draw_header()hand-roll the top-level title/hint withdraw_string(get_theme_default_font(), …, COLOR_TEXT)— bypasses the implant theme and duplicates ImplantHeader.implant/map/atlasin FULLSCREEN mode;open_appemitsgameplay_occluded;_on_app_changedroutes mode → visibility. Correct.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.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.terrain,infrastructure,named_features,gate_markers,population_densityrender real data.political_zonesdraws a flat tint (flagged as MVP placeholder in comment). 3 of 4 toggleable overlays (production_zones,shadow_economy,corp_presence) readmarkers.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._station_panelImplantPanel with name, operator, function, currency, deferred hint, no drill-down. Matches spec.Blocking — fix before merge
atlas_panel.gd:240–2444.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.atlas_viewer.gd:198–199(vs line 202 null check)_grid_w/_grid_hwhen a body has no heightmap after one that did._grid_wand_grid_hare set from_tex_w/_tex_hon lines 198–199 before theterrain_referencenull check at line 202. When showing a body withterrain_reference = nullafter 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 whenterrain_referenceis null.atlas_marker_overlay.gd:203–220_draw_production_zones,_draw_shadow_economy,_draw_corp_presencereadmarkers.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_presencefromcities[].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
atlas_panel.gd:288–291,atlas_viewer.gd:301–308draw_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.atlas_marker_overlay.gd:161, 170–179, 197viewer._city_canvas_pos(c),viewer._hovered_city,viewer._selected_city— all_-prefixed members accessed from outside. AtlasViewer already publishesget_heightmap_texture()/get_markers()/grid_to_canvas()as the public API. Promote_city_canvas_postocity_canvas_pos()and addget_hovered_city()/get_selected_city()(or inject them as args on a publicrender(...)method).vieweris untyped (var viewer = null), so renaming the private fields will silently break at runtime.atlas_viewer.gd:488–498vsmain.gd:187_unhandled_key_inputswallows KEY_N only when_selected_cityis non-empty. When no city is selected, KEY_N falls through tomain.gd:187which callseconomics_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 onnot HudGroups.is_app_active("implant/map/atlas").atlas_viewer.gd:81–97vsatlas_overlay_bar.gd:18–42_overlay_visibility/_overlay_lockedin AtlasViewer andOVERLAY_DEFSin AtlasOverlayBar both hardcode the same overlay id list. If they drift, the "locked" classification in the bar and the guard inset_overlay_visiblecan disagree silently. Single source of truth: move the overlay definitions to a shared const.atlas_overlay_bar.gd:53–56class_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 dropclass_nameand instance viaload(...)as atlas_panel already does for AtlasViewer. (Compare ImplantHeader/DataRow/TextBlock which all use the default-arg pattern.)generate-star-map-data.py:196–238terrain_referenceregen dependency is tribal knowledge. AtlasViewer's pivot from "data pending" → heightmap-ready is gated on this script having been re-run after server #839 populatesterrain_referencein systems.db. There is no Make target dependency or CI assertion for this. Addatlas-generate→ star-map-data regen as a make target chain, or a--checkassertion so the regen step becomes automatic instead of lore.Polish
atlas_panel.gd:591ImplantDataRow.new(sys.get("bodies", "no body data"))— the"no body data"fallback is dead code (generator always writesentry["bodies"]). Remove or replace with"—"to match the panel's empty-state convention.atlas_viewer.gd:293–298_draw()mutates child-node transform._draw()sets_canvas.position/_canvas.scaleevery redraw, duplicating what_apply_transform()already does. Causes extraNOTIFICATION_TRANSFORM_CHANGEDchurn. Remove the duplicate assignment from_draw().atlas_panel.gd:88,atlas_panel.gd:667–673load("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.atlas_marker_overlay.gd:177,178if c == selected/elif c == hoveredis O(fields) per city per redraw. Fine at 100 cities × 10 fields but balloons before Phase 4. Compare bybody_id/ index instead.atlas_panel.gd:476,484b["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 anis_empty()guard matches the defensive style elsewhere in the file.atlas_viewer.gd:149–156set_overlay_visible()silently returns on unknown overlay ids. Combined with #7's drift risk, a typo in the bar would do nothing. Addpush_warning("unknown overlay id %s" % overlay_id)on the unknown branch.atlas_overlay_bar.gd:79, 93–96pressedto a handler that re-setsbutton_pressed = trueon a non-toggle Button (no visual effect after first set). Simpler:disabled = truefor always-on buttons and drop the handler.generate-star-map-data.pyhelperscompute_gdp,infer_tier_from_population,build_adjacency,parse_wiki_index. Client-side test coverage is sparse project-wide (not a blocker), butcompute_gdp's boundary at exactly 1T Tr (BTr → TTr format switch) is one display-facing test worth adding.Pre-flight results (for the record)
gdlintonatlas_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 --quiton 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-pushand 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.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.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:
gdlintzero,make check-star-mapOK, headless godot no new script errors.gdformatreports one pre-existing advisory onclient/ui/stance_indicator.gdwhich is out of scope for this branch.Blockers →
1328db31AtlasPanel._compute_body_positionsnow groups moons per parent and dividesTAUby the per-parent count instead of the hardcoded4.0. Gas giants with 5+ moons no longer overlap satellites._load_heightmapresets_heightmap_texture/_tex_w/_tex_hto 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._draw_production_zones/_draw_shadow_economy/_draw_corp_presencenow read fromcities[](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 →
f8b81e1fAtlasPanelandAtlasViewernow compose top-of-screen title/hint from anImplantHeaderchild (_build_screen_header+_refresh_screen_header). Alldraw_string(get_theme_default_font(), …)calls in the atlas UI are gone; swapping the implant theme now cascades to the screen header.AtlasViewerexposescity_canvas_pos(),get_hovered_city(),get_selected_city(), andget_overlay_defs().AtlasMarkerOverlayno longer reaches into underscore-prefixed fields.AtlasViewer._unhandled_key_inputnow consumesKEY_Nunconditionally while the viewer is visible, andmain.gd's global economics-monitor toggle is gated on!HudGroups.is_app_active("implant/map/atlas"). No more atlas-closes-as-side-effect.OVERLAY_DEFSmoved toAtlasViewer;AtlasOverlayBarreadsviewer.get_overlay_defs()andAtlasViewer._overlay_visibility/_overlay_lockedderive from the same table at_ready(). Parallel lists eliminated.class_namefootgun —AtlasOverlayBardropsclass_name;AtlasViewerinstances it viaload("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.star-map-datatarget (regen) andcheck-star-mapis now wired intopre-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"no body data"fallback replaced with"—"._draw()transform duplication — already addressed in the warnings pass (background rect only).AtlasPanel;_build_heightmap_viewernow just callsAtlasViewer.new()directly._city_key()(name → city_id → pos → lat/lon → hash). Ready for Phase 4 city counts.b["body_id"]/s["station_id"]indexing guarded viastr(dict.get(..., ""))+is_empty()in_compute_body_positions,_draw_bodies,_draw_stations, and_handle_orbital_click.set_overlay_visible()now emits apush_warningon unknown overlay ids.AtlasOverlayBararedisabled = truewith a pinnedfont_disabled_coloroverride — no handler churn.Format →
22c17f1fRan
gdformaton 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
implant/mapchain (D-191 criterion 1 end-to-end).generate-star-map-data.pyhelpers (compute_gdp boundary, etc).Ready for re-review.
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). TheOVERLAY_DEFSconsolidation is particularly elegant — one const on AtlasViewer now feeds both the marker renderer (viais_overlay_visible) and the toggle bar (viaget_overlay_defs), eliminating the R1 drift risk and making #836 extensions a one-file change.D-decision re-compliance
draw_stringtitle/hint)atlas_viewer.gd:387+atlas_panel.gd:332composeImplantHeaderwithapply_implant_theme. On-canvas body/station labels outside D-169 scope.HudGroups.is_app_activeadded inmain.gd:191primary_function(PRODUCTION_FUNCTIONS whitelist) andcommission_presence— schema-compliant fields; doc comments acknowledge future explicit-schema promotionPer-finding resolution
4.0(5+ moons overlap)atlas_panel.gd:252–267groups moons by parent viamoons_by_parentdict, usessiblings.size()as the divisor; comment at 237–239 cites review #1_grid_w/_grid_hacross bodiesatlas_viewer.gd:260–268resets_tex_w=1024 / _tex_h=512unconditionally before any null guard;_load_markers()immediately assigns grid defaults before theterrain_referencenull check; comment at 285–287 cites review #2atlas_marker_overlay.gd:260–292derive production/shadow/corp overlays fromprimary_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 polygonsdraw_stringtitle/hintatlas_viewer.gd:383–402andatlas_panel.gd:328–373compose ImplantHeader viaset_content+apply_implant_theme(_implant_theme), matchingimplant_header.gdAPIcity_canvas_pos,get_hovered_city,get_selected_city,get_heightmap_texture,get_markers,get_overlay_defs,is_overlay_visibleat 235–252, 499); marker overlay calls public API throughoutatlas_viewer.gd:582–597_unhandled_key_inputunconditionally consumes KEY_N viaset_input_as_handled()while visible ANDmain.gd:191gates the global handler onnot HudGroups.is_app_active("implant/map/atlas")OVERLAY_DEFSatatlas_viewer.gd:58–125;atlas_overlay_bar.gd:34iterates via_viewer.get_overlay_defs(); no dict copy in the barclass_name AtlasOverlayBar+ required_initfootgunclass_nameremoved;_init(viewer_ref = null)with default at line 25; loaded viaload().new(self)atatlas_viewer.gd:640–641generate-star-map-data.pyterrain_reference tribal knowledgeMakefile:377check-star-maprunsgenerate-star-map-data.py --check(generator exits 1 on drift at lines 329, 336); wired intopre-pr-client(307) andpre-pr-validate(265). Any PR that mutatessystems.dbwithout regeneratingstar_map_data.jsonfails the pre-PR gate."no body data"dead fallbackatlas_panel.gd:649now uses"—"matching the panel empty-state convention_draw()mutates child transformatlas_viewer.gd:379–380_draw()now only draws the background rect; canvas mutation happens only in_apply_transform()atlas_panel.gd:88directload()with no misleading autoload comment;_viewer = AtlasViewer.new()at 723 via class_name directly_draw()callatlas_marker_overlay.gd:190–198uses_city_key()string comparison;_city_key(222–238) prefersname→city_id→ positional → hashb["body_id"]/s["station_id"]atlas_panel.gd:533str(b.get("body_id", "")), 541str(s.get("station_id", ""));_compute_body_positionsskips empty idsset_overlay_visible()silent return on unknown idatlas_viewer.gd:220–222push_warning("AtlasViewer: unknown overlay id '%s'" % overlay_id)atlas_overlay_bar.gd:46–50usesdisabled = true, button_pressed = truewithfont_disabled_coloroverride; no.toggled.connect()on always groupgenerate-star-map-data.pyhelpersRegressions
None. Line growth is all legitimate fix work:
atlas_viewer.gd+102: OVERLAY_DEFS const + getters (+67) + screen header composition (+26) + public API helpersatlas_panel.gd+59: screen header composition (+45) + moon grouping logic (+14)atlas_marker_overlay.gd+74: three schema-compliant overlay draw functions +_city_keyhelperatlas_overlay_bar.gd−35: const moved upstream,class_nameremoved, always-on handler droppedPR #128 is approved for merge.
Pull request closed