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>
209 lines
8.0 KiB
GDScript
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
|