Files
settled-reach/client/ui/implant/apps/atlas/atlas_app.gd
T
jpmschweitzerandClaude Fable 5 52304d3e37 fix(ui): PR #209 review round — current-screen guards, pending-aware settle, one body guard (T-971)
Every screen-targeted intent now routes through one
_require_current_screen() check and returns the structured error shape
instead of silently mutating an off-screen viewer (hoshe's finding:
scroll_rung from the reach screen fired real IPC and reported ok). The
reference driver's fixed 4-frame settle becomes is_pending()-aware with
a 600-frame bound, the keep-waiting decision extracted as a pure
testable function — restoring the proven eyeball-driver discipline. The
terrain_reference guard moves into AtlasApp._on_body_selected(), the
shared tail for double-click, Enter, AND the intent path — closing a
pre-existing click/Enter divergence hoshe caught this PR formalizing;
the intent layer pre-checks via the new SystemScreen.find_body() and
reports structured errors for unknown ids and terrain-less bodies.
after_test() resets AtlasAgentBridge.current_app (tyre's freed-pending
footgun). Suites 58/58 + 14/14; full suite 3,638.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 19:14:02 +02:00

209 lines
8.0 KiB
GDScript

class_name AtlasApp
extends ImplantApp
## Atlas implant app (#844, #836, D-191).
## Reach map → system orbital → planet entry → regional zoom ladder.
## Registered as "implant/map" in FULLSCREEN mode.
##
## T-1153 (D-226 T-1143-rulings amendment): the "district" nav-stack screen
## (T-1138's windowed drill-down, a separate nav.push() hop from "regional")
## RETIRES as a nav-stack level — the continuous cursor-anchored zoom ladder
## means descent/ascent through every rung (Region -> District -> Quarter)
## happens INSIDE the "regional" screen via zoom, not by pushing a new
## screen. D-013 "the zoom gesture owns spatial descent" is restored for
## this seam (Jeroen's ruling) — descent is a continuous gesture, not a
## discrete nav hop. Esc from anywhere in the ladder is therefore a single
## nav.pop() back to "system", exactly the same _handle_key() KEY_ESCAPE
## branch every other non-"reach" screen already uses; there is no longer a
## "district" screen entry in the match/registration table.
signal economics_link_requested(system_id: String)
var _systems: Array = []
var _system_lookup: Dictionary = {} # system_id → system dict
var _reach_screen = null # ReachScreen
var _system_screen = null # SystemScreen
var _planet_screen = null # PlanetScreen
var _regional_screen = null # RegionalScreen — now the whole zoom ladder (T-1153)
func _ready() -> void:
manifest = load("res://ui/implant/apps/atlas/app.tres")
super._ready()
func on_install() -> void:
# T-971: publish this instance to the dumb AtlasAgentBridge autoload holder
# so AtlasAgentInterface (a static class, loaded on demand by whatever
# in-process driver calls it) has a stable address to start from — see
# atlas_agent_bridge.gd's own doc for why this is a plain field set, not a
# class_name-typed autoload reference.
AtlasAgentBridge.current_app = self
_load_system_data()
# T-949: the star map now arrives over the bridge, possibly after this app
# is already installed (or before the handshake completes at all) — refresh
# once it lands instead of assuming _load_system_data's first call has data.
SimBridge.star_map_received.connect(_on_star_map_received)
var implant_theme = load("res://ui/implant/default_implant.tres")
_reach_screen = ReachScreen.new()
_reach_screen.setup(implant_theme)
_reach_screen.set_systems(_systems, _system_lookup)
_reach_screen.system_selected.connect(_on_system_selected)
register_screen("reach", _reach_screen)
_system_screen = SystemScreen.new()
_system_screen.setup(implant_theme)
_system_screen.set_systems(_systems)
_system_screen.body_selected.connect(_on_body_selected)
register_screen("system", _system_screen)
_planet_screen = PlanetScreen.new()
_planet_screen.setup(implant_theme)
register_screen("planet", _planet_screen)
_regional_screen = RegionalScreen.new()
_regional_screen.back_requested.connect(_on_regional_back)
register_screen("regional", _regional_screen)
nav.set_default("reach")
func on_open(_mode: int) -> void:
# Base class handles nav.push_default() on first open.
if current_screen_id() == "reach" and _reach_screen:
_reach_screen.refresh_info_panel_visibility()
func _unhandled_key_input(event: InputEvent) -> void:
if not event is InputEventKey:
return
if manifest == null or not HudGroups.is_app_active(manifest.app_path):
return
if not event.is_pressed() or event.is_echo():
return
# "regional" (the whole stepped zoom ladder, StepCanvasViewer, T-1182)
# handles its own Esc via _gui_input — a stray M/other unhandled key on
# that screen must not ALSO fire this app's own _handle_key underneath
# the viewer's own handling.
if current_screen_id() == "regional":
return
_handle_key(event as InputEventKey)
get_viewport().set_input_as_handled()
func _handle_key(event: InputEventKey) -> void:
match event.physical_keycode:
KEY_M:
HudGroups.close_app()
KEY_ESCAPE:
var has_open_panel: bool = _system_screen and (
_system_screen.has_body_panel_open() or _system_screen.has_station_panel_open()
)
if current_screen_id() == "system" and has_open_panel:
_system_screen.close_panels()
elif current_screen_id() == "reach":
HudGroups.close_app()
else:
nav.pop()
KEY_ENTER, KEY_KP_ENTER:
_handle_enter()
KEY_BRACKETLEFT:
if current_screen_id() == "system" and _system_screen:
_system_screen.navigate_system(-1)
KEY_BRACKETRIGHT:
if current_screen_id() == "system" and _system_screen:
_system_screen.navigate_system(1)
func _handle_enter() -> void:
match current_screen_id():
"reach":
if _reach_screen and _reach_screen.has_selection():
_reach_screen.trigger_enter()
"system":
if _system_screen and not _system_screen.is_in_orbital():
var sys: Dictionary = _system_screen.current_system()
nav.replace("system", {"mode": "orbital", "system": sys})
elif _system_screen and _system_screen.has_body_panel_open():
var body: Dictionary = _system_screen.get_selected_body()
_on_body_selected(body)
# =============================================================================
# Signal handlers
# =============================================================================
func _on_system_selected(system_id: String) -> void:
var idx: int = _system_idx_by_id(system_id)
if _system_screen:
_system_screen.set_selected_idx(idx)
var system: Dictionary = _system_lookup.get(system_id, {"system_id": system_id})
nav.push("system", {"mode": "orbital", "system": system})
## PR #209 review (Hoshe finding 3, lead ruling): the SHARED tail every path
## that "opens" a body funnels through — double-click (SystemScreen.
## body_selected), the Enter-key path (_handle_enter()'s "system" branch,
## which used to gate on terrain_reference itself and now just calls this),
## and AtlasAgentInterface's open_body/open_body_by_id intent (also via this
## same signal). Gating HERE, once, closes a pre-existing divergence: the
## Enter-key path already required `terrain_reference != null` before this
## PR, but double-click (and therefore the new open_body intent) did not —
## both silently pushed "regional" for a body with no heightmap. Formalizing
## the intent contract is what surfaced it, so this PR fixes it for both
## callers at once rather than reintroducing the same split.
##
## A guarded body (no terrain_reference) is a silent no-op here — same
## behavior the Enter path always had (it simply never called nav.push() for
## that case). AtlasAgentInterface's open_body/open_body_by_id wrap this at
## the intent layer with a STRUCTURED error instead of a silent no-op (see
## atlas_agent_interface.gd's own _act_open_body()) — an agent driver must be
## able to tell "nothing happened" from "call succeeded", which a bare click
## has no way to report either way.
func _on_body_selected(body: Dictionary) -> void:
if body.get("terrain_reference") == null:
return
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
})
func _on_regional_back() -> void:
nav.pop()
## T-949: the star map arrived — cache it, rebuild the local system
## list/lookup, and push it into whichever screens already exist. set_systems()
## is safe to call again after initial setup (ReachScreen/SystemScreen both
## just recompute their layout from the new data).
func _on_star_map_received(response: Dictionary) -> void:
SystemIndex.ingest(response)
_load_system_data()
if _reach_screen:
_reach_screen.set_systems(_systems, _system_lookup)
if _system_screen:
_system_screen.set_systems(_systems)
# =============================================================================
# Helpers
# =============================================================================
func _system_idx_by_id(system_id: String) -> int:
for i: int in range(_systems.size()):
if _systems[i].get("system_id", "") == system_id:
return i
return 0
func _load_system_data() -> void:
SystemIndex.request_refresh()
_systems = SystemIndex.get_sorted_systems()
_system_lookup.clear()
for node: Dictionary in _systems:
_system_lookup[node.get("system_id", "")] = node