Files
settled-reach/client/ui/implant/implant_app.gd
T
jpmschweitzerandClaude Fable 5 34dbac9bf6 feat(ui): AtlasAgentInterface — observe/act named-intent control channel (D-226, T-971)
D-226 layer 4, rebuilt against the post-D-255 stepped Atlas after the
Phase-1 reconciliation (the original intent list targeted the retired
continuous-zoom viewer). Eleven intents, each backed by the exact
production handler a click calls — select/open for systems and bodies
(extracted shared by-id tails so click and intent paths are one code
path), scroll_rung, reset_view, back, open/close_atlas, set_overlay —
plus two new first-class capabilities: jump_to_center (the fixed-center
revisit pattern proven by five eyeball drivers, via a new
StepCanvasViewer.jump_to seam that reuses _scroll_rung's exact request
tail — same extent cap, same cache keys) and get_current_canvas_summary
(allocation-light reads off the raw wire dict, courses-by-class,
draw-matched settlement dedup — no PNG decode). Contract shape: dumb
AtlasAgentBridge autoload holding the app handle (untyped per the
parse-order rule), all logic in the static AtlasAgentInterface class.
observe() is side-effect-free: current state + a generic Control-walk
affordance tree. In-process consumers only this ticket (documented);
the committed reference driver (atlas_agent_driver.gd, InputSwallower +
settle-until-ready from the T-1157 inventory) replaces the scratch
eyeball drivers as the sanctioned headless-drive pattern.
select_city/open_regional dropped with recorded rationale (no
settlement hit-test affordance exists post-D-255) — diff on the ticket.
33 new tests across three suites.

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

160 lines
4.9 KiB
GDScript

class_name ImplantApp
extends Control
## Base class for all implant apps. Absorbs HudGroups boilerplate; subclasses
## override lifecycle hooks only (#844, D-191).
##
## === Lifecycle ordering ===
##
## on_install() — called once from _ready(), after nav is created but BEFORE any
## HudGroups open event fires. The nav stack is EMPTY at this point. Use this
## hook to: construct screens and call register_screen(id, screen), set
## nav.set_default("..."), wire intra-screen signals. Do NOT rely on
## current_screen_id() here — no screen has been pushed yet.
##
## on_open(mode) — called every time HudGroups activates this app (FULLSCREEN or
## INSERT). By the time on_open fires, the base has ensured the nav stack is
## non-empty: if preserves_state=false, nav.reset_to_default() was called; if
## preserves_state=true and the stack was empty, nav.push_default() was called.
## nav.current() returns the visible screen id. Safe to read navigation state
## and trigger data refreshes here.
##
## on_close() — called every time HudGroups deactivates this app (GAMEPLAY mode
## or another app taking focus). Nav stack state is preserved here — do not
## push or pop screens in on_close. Use this hook for: pausing timers, stopping
## animations, unsubscribing from high-frequency feeds. The stack survives
## intact for the next on_open (if preserves_state=true).
##
## on_insert_deactivated() — called by SnapshotConsumers when the server drops
## insert state. The base implementation closes the app only if it is currently
## active in INSERT mode. FULLSCREEN apps inherit a no-op; override to add
## custom handling (e.g. save draft, emit warning). Do not call close_app()
## manually — call super() or replicate the guard condition.
##
## === Subclass _ready() pattern ===
## func _ready() -> void:
## manifest = load("res://ui/implant/apps/my_app/app.tres")
## super._ready()
## # additional init here if needed
signal app_opened(mode: int)
signal app_closed
var manifest: ImplantAppManifest = null
var nav: ImplantNavStack = null
var _screens: Dictionary = {} # screen_id → Control
var _current_screen_id: String = ""
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
visible = false
if manifest:
HudGroups.register(self, manifest.app_path)
HudGroups.app_changed.connect(_internal_app_changed)
nav = ImplantNavStack.new()
nav.screen_changed.connect(_on_screen_changed)
add_child(nav)
on_install()
func _internal_app_changed(app_path: String, mode: int) -> void:
if manifest == null:
return
if app_path != manifest.app_path:
if visible:
visible = false
on_close()
app_closed.emit()
return
match mode:
HudGroups.Mode.FULLSCREEN, HudGroups.Mode.INSERT:
if not visible:
visible = true
if not manifest.preserves_state:
nav.reset_to_default()
elif nav.is_empty():
nav.push_default()
on_open(mode)
app_opened.emit(mode)
HudGroups.Mode.GAMEPLAY:
if visible:
visible = false
on_close()
app_closed.emit()
# --- Lifecycle hooks — subclasses override ---
func on_install() -> void:
pass
func on_open(_mode: int) -> void:
pass
func on_close() -> void:
pass
func on_insert_deactivated() -> void:
# Default: close only if active in INSERT mode. FULLSCREEN apps override to customize.
if manifest and HudGroups.is_app_active(manifest.app_path):
if HudGroups.get_active_mode() == HudGroups.Mode.INSERT:
HudGroups.close_app()
# --- Screen management ---
## Register a screen under an id. Base adds it as a child and starts it hidden.
## Call from on_install(). Wire signals before calling register_screen.
func register_screen(id: String, screen: Control) -> void:
if _screens.has(id):
push_warning("ImplantApp: screen id '%s' already registered" % id)
return
_screens[id] = screen
screen.visible = false
if screen.get_parent() == null:
add_child(screen)
func current_screen_id() -> String:
return _current_screen_id
## T-971 (AtlasAgentInterface, D-226 item 4): a generic, read-only screen
## lookup by id — the same `_screens` dict register_screen() already
## maintains, exposed so an agent-control channel can reach a specific
## screen instance (StepCanvasViewer via RegionalScreen.get_viewer(), etc.)
## without every ImplantApp subclass growing its own bespoke getters. Returns
## null for an unregistered id, matching Dictionary.get()'s own default.
func get_screen(screen_id: String) -> Variant:
return _screens.get(screen_id, null)
func _on_screen_changed(new_id: String) -> void:
if _current_screen_id != new_id:
var old: Control = _screens.get(_current_screen_id, null)
if old:
if old.has_method("leave"):
old.leave()
old.visible = false
_current_screen_id = new_id
var s: Control = _screens.get(new_id, null)
if s:
s.visible = true
if s.has_method("enter"):
s.enter(nav.current_payload())
func handle_intent(_action: String, _params: Dictionary) -> void:
pass