feat(ui): introduce ImplantApp pattern — base class, registry, atlas + economics refactor (#844, #824, #836)
Establishes the ImplantApp faux-mobile-OS architecture (D-191):
Foundation:
- ImplantAppManifest (Resource): @export vars for app_path, scene_path, default_key, default_mode, preserves_state
- ImplantNavStack (Node): push/pop/replace/reset_to_default, synchronous screen_changed signal
- ImplantApp (Control base class): absorbs HudGroups boilerplate; on_install/on_open/on_close/on_insert_deactivated lifecycle hooks
- ImplantRegistry (autoload): lazy-scans res://ui/implant/apps/*/app.tres; avoids autoload parse-order trap
Atlas app (replaces atlas_panel + atlas_reach_map + atlas_system_map + atlas_planet_map + root viewer files):
- apps/atlas/app.tres — manifest (implant/map, FULLSCREEN, key=M)
- apps/atlas/atlas_app.gd — coordinator; 4-screen nav via ImplantNavStack
- apps/atlas/screens/{reach,system,planet,regional}_screen.gd — enter/leave interface
- apps/atlas/{atlas_viewer,atlas_marker_overlay,atlas_overlay_bar}.gd — moved from root
Economics app (replaces economics_panel):
- apps/economics/app.tres — manifest (implant/economics, INSERT, key=N)
- apps/economics/economics_app.gd — thin shell delegating to OverviewScreen
- apps/economics/screens/overview_screen.gd — full panel logic, enter/leave interface
Wiring:
- project.godot: add ImplantRegistry autoload after HudGroups
- main.gd: registry-driven key toggle loop; rename atlas_panel→atlas_app, economics_panel→economics_app
- hud.tscn: swap to new scene paths; remove legacy StarMap node
- snapshot_consumers.gd: on_insert_deactivated() uniformly; rename vars
- hud_groups.gd: remove stale starchart compat comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ FogState="*res://scripts/autoloads/fog_state.gd"
|
||||
AudioManager="*res://scripts/autoloads/audio_manager.gd"
|
||||
SessionManager="*res://scripts/autoloads/session_manager.gd"
|
||||
HudGroups="*res://scripts/autoloads/hud_groups.gd"
|
||||
ImplantRegistry="*res://ui/implant/implant_registry.gd"
|
||||
HardwareDetector="*res://ui/hardware_detector.gd"
|
||||
|
||||
[audio]
|
||||
|
||||
@@ -12,7 +12,6 @@ extends Node
|
||||
## Groups use hierarchical paths:
|
||||
## "gameplay" — HUD status, minimap, prompts, stance
|
||||
## "implant/map" — unified atlas (reach map → system → planet → regional, D-191)
|
||||
## "implant/map/starchart" — legacy hop-ring view (registered by StarMapRenderer, kept for compat)
|
||||
## "implant/wiki/gttr" — Drifter's Guide reader
|
||||
## "implant/journal" — knowledge journal
|
||||
## "implant/economics" — economics monitor (D-181, #824)
|
||||
|
||||
+26
-30
@@ -32,9 +32,8 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
||||
@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load
|
||||
@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console
|
||||
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
|
||||
@onready var star_map = $InsertOverlay/HUD/StarMap # #674: star map insert module (hop-ring view)
|
||||
@onready var economics_panel = $InsertOverlay/HUD/EconomicsPanel # #824: economics monitor (D-170)
|
||||
@onready var atlas_panel = $InsertOverlay/HUD/AtlasPanel # #834: atlas implant — system → orbital → body (D-191)
|
||||
@onready var economics_app = $InsertOverlay/HUD/EconomicsApp # #824: economics monitor (D-170)
|
||||
@onready var atlas_app = $InsertOverlay/HUD/AtlasApp # #844: atlas implant — reach → system → planet → regional (D-191)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -92,9 +91,8 @@ func _ready() -> void:
|
||||
"interaction_list": interaction_list,
|
||||
"interaction_prompt": interaction_prompt,
|
||||
"minimap": minimap,
|
||||
"star_map": star_map,
|
||||
"economics_panel": economics_panel,
|
||||
"atlas_panel": atlas_panel,
|
||||
"economics_app": economics_app,
|
||||
"atlas_app": atlas_app,
|
||||
},
|
||||
_screen_flash
|
||||
)
|
||||
@@ -171,38 +169,36 @@ func _ready() -> void:
|
||||
# #835 D-191: Atlas → Economics Monitor cross-link. The atlas city data panel
|
||||
# emits economics_link_requested(system_id); we pre-filter the monitor and
|
||||
# open it as an insert panel on top.
|
||||
if atlas_panel and economics_panel:
|
||||
atlas_panel.economics_link_requested.connect(_on_atlas_economics_link)
|
||||
if atlas_app and economics_app:
|
||||
atlas_app.economics_link_requested.connect(_on_atlas_economics_link)
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event.is_pressed() and not event.is_echo():
|
||||
if event is InputEventKey and event.keycode == KEY_M:
|
||||
# #844: M — unified atlas (implant/map). Reach map → system → planet → regional.
|
||||
if atlas_panel:
|
||||
atlas_panel.toggle_visible()
|
||||
elif event is InputEventKey and event.keycode == KEY_N:
|
||||
# #824: N — toggle Economics Monitor implant panel (E is bound to interact).
|
||||
# Gated on the atlas being inactive so the viewer's N → city-economics
|
||||
# cross-link isn't shadowed by this global toggle (review #6).
|
||||
if economics_panel and not HudGroups.is_app_active("implant/map"):
|
||||
economics_panel.toggle_visible()
|
||||
elif event is InputEventKey and event.keycode == KEY_BRACKETLEFT:
|
||||
# #824: [ — cycle economics panel system selector backward
|
||||
if economics_panel and HudGroups.is_app_active("implant/economics"):
|
||||
economics_panel.navigate(-1)
|
||||
elif event is InputEventKey and event.keycode == KEY_BRACKETRIGHT:
|
||||
# #824: ] — cycle economics panel system selector forward
|
||||
if economics_panel and HudGroups.is_app_active("implant/economics"):
|
||||
economics_panel.navigate(1)
|
||||
if not (event is InputEventKey) or not event.is_pressed() or event.is_echo():
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
# Registry-driven toggle: each manifest declares its own default_key.
|
||||
for manifest: Variant in ImplantRegistry.get_manifests():
|
||||
if manifest.get("default_key") == key_event.keycode:
|
||||
HudGroups.toggle_app(
|
||||
manifest.get("app_path"), manifest.get("default_mode", HudGroups.Mode.FULLSCREEN)
|
||||
)
|
||||
return
|
||||
# [ / ] — cycle economics system selector (app-specific, not generic enough for manifest).
|
||||
if key_event.keycode == KEY_BRACKETLEFT:
|
||||
if economics_app and HudGroups.is_app_active("implant/economics"):
|
||||
economics_app.navigate(-1)
|
||||
elif key_event.keycode == KEY_BRACKETRIGHT:
|
||||
if economics_app and HudGroups.is_app_active("implant/economics"):
|
||||
economics_app.navigate(1)
|
||||
|
||||
|
||||
func _on_atlas_economics_link(system_id: String) -> void:
|
||||
# #835 D-191: Pre-filter the economics monitor to the city's system and pop
|
||||
# the panel open. AtlasPanel closes itself before emitting this signal.
|
||||
if economics_panel == null:
|
||||
# the panel open. AtlasApp closes itself before emitting this signal.
|
||||
if economics_app == null:
|
||||
return
|
||||
economics_panel.select_system(system_id)
|
||||
economics_app.select_system(system_id)
|
||||
HudGroups.open_app("implant/economics", HudGroups.Mode.INSERT)
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@ var cursor_renderer: Node = null
|
||||
var interaction_list: Node = null
|
||||
var interaction_prompt: Node = null
|
||||
var minimap: Node = null
|
||||
var star_map: Node = null
|
||||
var economics_panel: Node = null # #824: economics monitor (D-181)
|
||||
var atlas_panel: Node = null # #834: atlas implant panel (D-191)
|
||||
var economics_app: Node = null # #824: economics monitor (D-181)
|
||||
var atlas_app: Node = null # #844: atlas implant app (D-191)
|
||||
|
||||
var _screen_flash_fn: Callable # Callable(color: Color, duration: float)
|
||||
|
||||
@@ -38,9 +37,8 @@ func init(refs: Dictionary, screen_flash: Callable) -> SnapshotConsumers:
|
||||
interaction_list = refs.get("interaction_list")
|
||||
interaction_prompt = refs.get("interaction_prompt")
|
||||
minimap = refs.get("minimap")
|
||||
star_map = refs.get("star_map")
|
||||
economics_panel = refs.get("economics_panel")
|
||||
atlas_panel = refs.get("atlas_panel")
|
||||
economics_app = refs.get("economics_app")
|
||||
atlas_app = refs.get("atlas_app")
|
||||
_screen_flash_fn = screen_flash
|
||||
return self
|
||||
|
||||
@@ -56,12 +54,11 @@ func propagate_insert_state() -> void:
|
||||
interaction_prompt.set_insert_active(insert_state)
|
||||
if minimap:
|
||||
minimap.set_insert_active(insert_state)
|
||||
if star_map:
|
||||
star_map.set_insert_active(insert_state)
|
||||
if economics_panel:
|
||||
economics_panel.set_insert_active(insert_state)
|
||||
if atlas_panel:
|
||||
atlas_panel.set_insert_active(insert_state)
|
||||
if not insert_state:
|
||||
if economics_app and economics_app.has_method("on_insert_deactivated"):
|
||||
economics_app.on_insert_deactivated()
|
||||
if atlas_app and atlas_app.has_method("on_insert_deactivated"):
|
||||
atlas_app.on_insert_deactivated()
|
||||
|
||||
|
||||
# D-057: Update interaction list from game state.
|
||||
@@ -181,12 +178,12 @@ func consume_debug_response() -> void:
|
||||
GameState.debug_response = null
|
||||
|
||||
|
||||
# #824: Forward economy_snapshot from server to the economics panel (D-181).
|
||||
# #824: Forward economy_snapshot from server to the economics app (D-181).
|
||||
func consume_economy_snapshot() -> void:
|
||||
if GameState.economy_snapshot == null or not economics_panel:
|
||||
if GameState.economy_snapshot == null or not economics_app:
|
||||
return
|
||||
if economics_panel.has_method("receive_economy_data"):
|
||||
economics_panel.receive_economy_data(GameState.economy_snapshot)
|
||||
if economics_app.has_method("receive_economy_data"):
|
||||
economics_app.receive_economy_data(GameState.economy_snapshot)
|
||||
GameState.economy_snapshot = null
|
||||
|
||||
|
||||
|
||||
+8
-14
@@ -1,9 +1,8 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cq1y5w3hmxr8b"]
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cq1y5w3hmxr8b"]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"]
|
||||
[ext_resource type="PackedScene" path="res://ui/star_map.tscn" id="2_starmap"]
|
||||
[ext_resource type="PackedScene" path="res://ui/implant/economics_panel.tscn" id="3_econ"]
|
||||
[ext_resource type="PackedScene" path="res://ui/implant/atlas_panel.tscn" id="4_atlas"]
|
||||
[ext_resource type="PackedScene" path="res://ui/implant/apps/economics/economics_app.tscn" id="2_econ"]
|
||||
[ext_resource type="PackedScene" path="res://ui/implant/apps/atlas/atlas_app.tscn" id="3_atlas"]
|
||||
|
||||
[node name="HUD" type="Control"]
|
||||
layout_mode = 3
|
||||
@@ -15,17 +14,12 @@ grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_hud")
|
||||
|
||||
; #674: Star map — legacy hop-ring node, kept for backward compat. Superseded by AtlasPanel REACH_MAP level (#844).
|
||||
[node name="StarMap" parent="." instance=ExtResource("2_starmap")]
|
||||
visible = false
|
||||
|
||||
; #824: Economics Monitor — implant/economics INSERT panel. Toggled via E key from main.gd.
|
||||
; #824: Economics Monitor — implant/economics INSERT panel. Toggled via N key (manifest.default_key).
|
||||
; Composes ImplantPanel from the D-169 component library. Placeholder data until #822 ships.
|
||||
[node name="EconomicsPanel" parent="." instance=ExtResource("3_econ")]
|
||||
[node name="EconomicsApp" parent="." instance=ExtResource("2_econ")]
|
||||
visible = false
|
||||
|
||||
; #834: Atlas implant panel — FULLSCREEN app at implant/map/atlas per D-170.
|
||||
; 3-level navigation: system picker → orbital diagram → body entry. Toggled via A key.
|
||||
[node name="AtlasPanel" parent="." instance=ExtResource("4_atlas")]
|
||||
; #844: Atlas implant app — FULLSCREEN app at implant/map per D-170.
|
||||
; Reach map → system orbital → planet entry → regional heightmap viewer.
|
||||
[node name="AtlasApp" parent="." instance=ExtResource("3_atlas")]
|
||||
visible = false
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_resource type="Resource" script_class="ImplantAppManifest" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/implant_app_manifest.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
app_path = "implant/map"
|
||||
display_name = "ATLAS"
|
||||
icon_path = ""
|
||||
scene_path = "res://ui/implant/apps/atlas/atlas_app.tscn"
|
||||
default_mode = 2
|
||||
default_key = 77
|
||||
preserves_state = true
|
||||
@@ -0,0 +1,204 @@
|
||||
class_name AtlasApp
|
||||
extends ImplantApp
|
||||
## Atlas implant app (#844, #836, D-191).
|
||||
## Reach map → system orbital → planet entry → regional heightmap viewer.
|
||||
## Registered as "implant/map" in FULLSCREEN mode.
|
||||
|
||||
signal economics_link_requested(system_id: String)
|
||||
|
||||
const STAR_MAP_DATA := "res://data/star_map_data.json"
|
||||
|
||||
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
|
||||
var _current_screen_id: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
manifest = load("res://ui/implant/apps/atlas/app.tres")
|
||||
super._ready()
|
||||
|
||||
|
||||
func on_install() -> void:
|
||||
_load_system_data()
|
||||
var implant_theme = load("res://ui/implant/default_implant.tres")
|
||||
|
||||
_reach_screen = ReachScreen.new()
|
||||
_reach_screen.name = "ReachScreen"
|
||||
_reach_screen.visible = false
|
||||
add_child(_reach_screen)
|
||||
_reach_screen.setup(implant_theme)
|
||||
_reach_screen.set_systems(_systems, _system_lookup)
|
||||
_reach_screen.system_selected.connect(_on_system_selected)
|
||||
|
||||
_system_screen = SystemScreen.new()
|
||||
_system_screen.name = "SystemScreen"
|
||||
_system_screen.visible = false
|
||||
add_child(_system_screen)
|
||||
_system_screen.setup(implant_theme)
|
||||
_system_screen.set_systems(_systems)
|
||||
_system_screen.body_selected.connect(_on_body_selected)
|
||||
|
||||
_planet_screen = PlanetScreen.new()
|
||||
_planet_screen.name = "PlanetScreen"
|
||||
_planet_screen.visible = false
|
||||
add_child(_planet_screen)
|
||||
_planet_screen.setup(implant_theme)
|
||||
|
||||
_regional_screen = RegionalScreen.new()
|
||||
_regional_screen.name = "RegionalScreen"
|
||||
_regional_screen.visible = false
|
||||
add_child(_regional_screen)
|
||||
_regional_screen.back_requested.connect(_on_regional_back)
|
||||
_regional_screen.economics_link_requested.connect(_forward_economics_link)
|
||||
|
||||
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 _on_screen_changed(new_id: String) -> void:
|
||||
# Skip leave/hide when replacing the same screen (e.g. picker→orbital transition).
|
||||
if _current_screen_id != new_id:
|
||||
var old := _get_screen(_current_screen_id)
|
||||
if old:
|
||||
old.leave()
|
||||
old.visible = false
|
||||
|
||||
_current_screen_id = new_id
|
||||
|
||||
var s := _get_screen(new_id)
|
||||
if s:
|
||||
s.visible = true
|
||||
s.enter(nav.current_payload())
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEventKey) -> void:
|
||||
if manifest == null or not HudGroups.is_app_active(manifest.app_path):
|
||||
return
|
||||
if not event.is_pressed() or event.is_echo():
|
||||
return
|
||||
if _current_screen_id == "regional":
|
||||
return # AtlasViewer handles its own keyboard input
|
||||
_handle_key(event)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _handle_key(event: InputEventKey) -> void:
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
if _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})
|
||||
"planet":
|
||||
if _planet_screen and _planet_screen.has_heightmap():
|
||||
nav.push("regional", {
|
||||
"body": _planet_screen.current_body(),
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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})
|
||||
|
||||
|
||||
func _on_body_selected(body: Dictionary) -> void:
|
||||
nav.push("planet", {
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
func _on_regional_back() -> void:
|
||||
nav.pop()
|
||||
|
||||
|
||||
func _forward_economics_link(system_id: String) -> void:
|
||||
economics_link_requested.emit(system_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _get_screen(screen_id: String) -> Control:
|
||||
match screen_id:
|
||||
"reach":
|
||||
return _reach_screen as Control
|
||||
"system":
|
||||
return _system_screen as Control
|
||||
"planet":
|
||||
return _planet_screen as Control
|
||||
"regional":
|
||||
return _regional_screen as Control
|
||||
_:
|
||||
return null
|
||||
|
||||
|
||||
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:
|
||||
if not FileAccess.file_exists(STAR_MAP_DATA):
|
||||
push_warning("AtlasApp: %s not found" % STAR_MAP_DATA)
|
||||
return
|
||||
var file := FileAccess.open(STAR_MAP_DATA, FileAccess.READ)
|
||||
if file == null:
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
file.close()
|
||||
if not (parsed is Dictionary):
|
||||
return
|
||||
for node: Dictionary in parsed.get("nodes", []):
|
||||
var sid: String = node.get("system_id", "")
|
||||
if not sid.is_empty():
|
||||
_systems.append(node)
|
||||
_system_lookup[sid] = node
|
||||
_systems.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var na: String = a.get("proper_name", a.get("system_id", ""))
|
||||
var nb: String = b.get("proper_name", b.get("system_id", ""))
|
||||
return na < nb
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/apps/atlas/atlas_app.gd" id="1_atlas_app"]
|
||||
|
||||
; #844: Atlas implant app — reach map → system orbital → planet entry → regional heightmap viewer.
|
||||
; FULLSCREEN app (z=20) at implant/map per D-170. Managed via ImplantApp/ImplantNavStack pattern.
|
||||
; Toggle with M key (manifest.default_key). Data from star_map_data.json.
|
||||
|
||||
[node name="AtlasApp" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_atlas_app")
|
||||
+1
-2
@@ -6,8 +6,7 @@ extends HBoxContainer
|
||||
## This script has no `class_name` on purpose: the owner (AtlasViewer) needs to
|
||||
## pass the viewer reference to _init() and a `class_name` + required-arg
|
||||
## _init() combo is a Godot editor footgun (review #8). Instance it via
|
||||
## load("res://ui/implant/atlas_overlay_bar.gd").new(self) like AtlasViewer
|
||||
## itself is instanced from AtlasPanel.
|
||||
## load("res://ui/implant/apps/atlas/atlas_overlay_bar.gd").new(self).
|
||||
##
|
||||
## Layout: [ALWAYS-ON] [TOGGLEABLE] [LOCKED]. Each row in viewer.get_overlay_defs()
|
||||
## produces exactly one button, so adding or retiring an overlay is a one-file
|
||||
@@ -3,9 +3,9 @@ extends Control
|
||||
|
||||
## Atlas regional viewer — heightmap PNG with pan/zoom + marker overlay (#835, D-191).
|
||||
##
|
||||
## Lives as a child of AtlasPanel, shown at Level.HEIGHTMAP_VIEWER. Receives
|
||||
## body/system context from AtlasPanel via show_body(). Emits back_pressed and
|
||||
## economics_link_requested signals so AtlasPanel can route them.
|
||||
## Lives as a child of RegionalScreen, shown when the atlas nav stack is at
|
||||
## "regional". Receives body/system context via show_body(). Emits back_pressed
|
||||
## and economics_link_requested so RegionalScreen can route them.
|
||||
##
|
||||
## Design notes:
|
||||
## - Heightmap texture is drawn on a Node2D _canvas child. Pan = _canvas.position,
|
||||
@@ -124,7 +124,7 @@ const OVERLAY_DEFS: Array = [
|
||||
},
|
||||
]
|
||||
|
||||
# ── Context (set by AtlasPanel.show_body) ─────────────────────────────────────
|
||||
# ── Context (set by show_body) ─────────────────────────────────────────────────
|
||||
var _body: Dictionary = {}
|
||||
var _system: Dictionary = {}
|
||||
var _implant_theme = null
|
||||
@@ -196,7 +196,7 @@ func _ready() -> void:
|
||||
_build_overlay_bar()
|
||||
|
||||
|
||||
## Called by AtlasPanel when entering the viewer for a specific body.
|
||||
## Called by RegionalScreen.enter() when entering the viewer for a specific body.
|
||||
func show_body(body: Dictionary, system: Dictionary) -> void:
|
||||
_body = body
|
||||
_system = system
|
||||
@@ -637,7 +637,7 @@ func _position_empty_notice() -> void:
|
||||
|
||||
|
||||
func _build_overlay_bar() -> void:
|
||||
var BarScript := load("res://ui/implant/atlas_overlay_bar.gd")
|
||||
var BarScript := load("res://ui/implant/apps/atlas/atlas_overlay_bar.gd")
|
||||
_overlay_bar = BarScript.new(self)
|
||||
_overlay_bar.name = "OverlayBar"
|
||||
add_child(_overlay_bar)
|
||||
+18
-48
@@ -1,10 +1,7 @@
|
||||
class_name AtlasPlanetMap
|
||||
class_name PlanetScreen
|
||||
extends Control
|
||||
## Level 3 BODY_ENTRY and Level 4 HEIGHTMAP_VIEWER widget for AtlasPanel (#844, D-191).
|
||||
## Shows body detail panel and delegates heightmap rendering to AtlasViewer.
|
||||
|
||||
signal back_to_viewer_body
|
||||
signal economics_link_requested(system_id: String)
|
||||
## Body entry screen for AtlasApp (#844, D-191).
|
||||
## Shows body detail panel. Heightmap viewer is in RegionalScreen.
|
||||
|
||||
const PANEL_WIDTH: float = 320.0
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
@@ -13,49 +10,40 @@ const COLOR_BG: Color = Color("#0d1117")
|
||||
var _selected_body: Dictionary = {}
|
||||
var _current_sys: Dictionary = {}
|
||||
var _body_panel = null # ImplantPanel
|
||||
var _viewer = null # AtlasViewer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _viewer == null or not _viewer.visible:
|
||||
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
|
||||
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
|
||||
|
||||
|
||||
func setup(implant_theme) -> void:
|
||||
_build_body_panel(implant_theme)
|
||||
_build_heightmap_viewer()
|
||||
|
||||
|
||||
func set_body(body: Dictionary, system: Dictionary) -> void:
|
||||
_selected_body = body
|
||||
_current_sys = system
|
||||
func enter(payload: Dictionary) -> void:
|
||||
_selected_body = payload.get("body", {})
|
||||
_current_sys = payload.get("system", {})
|
||||
_rebuild_body_panel()
|
||||
|
||||
|
||||
func show_body_mode() -> void:
|
||||
if _body_panel:
|
||||
_body_panel.visible = true
|
||||
if _viewer:
|
||||
_viewer.visible = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func show_viewer_mode() -> void:
|
||||
if _viewer == null:
|
||||
return
|
||||
_viewer.show_body(_selected_body, _current_sys)
|
||||
func leave() -> void:
|
||||
if _body_panel:
|
||||
_body_panel.visible = false
|
||||
_viewer.visible = true
|
||||
|
||||
|
||||
func has_heightmap() -> bool:
|
||||
return _selected_body.get("terrain_reference") != null
|
||||
|
||||
|
||||
func current_body() -> Dictionary:
|
||||
return _selected_body
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Panels
|
||||
# Panel
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@@ -83,7 +71,6 @@ func _rebuild_body_panel() -> void:
|
||||
var atmo: String = b.get("atmosphere", "none") if b.get("atmosphere") else "none"
|
||||
var inhabited: bool = bool(b.get("inhabited", false))
|
||||
var pop: int = int(b.get("population", 0))
|
||||
var has_heightmap: bool = b.get("terrain_reference") != null
|
||||
|
||||
var sys_name: String = _current_sys.get("proper_name", _current_sys.get("system_id", "—"))
|
||||
|
||||
@@ -102,7 +89,7 @@ func _rebuild_body_panel() -> void:
|
||||
|
||||
_body_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
if has_heightmap:
|
||||
if has_heightmap():
|
||||
_body_panel.add_component(ImplantTextBlock.new("enter view heightmap atlas"))
|
||||
else:
|
||||
_body_panel.add_component(ImplantTextBlock.new("atlas data pending (#839)"))
|
||||
@@ -110,23 +97,6 @@ func _rebuild_body_panel() -> void:
|
||||
_body_panel.add_component(ImplantTextBlock.new("esc back to orbital view"))
|
||||
|
||||
|
||||
func _build_heightmap_viewer() -> void:
|
||||
_viewer = AtlasViewer.new()
|
||||
_viewer.name = "AtlasViewer"
|
||||
_viewer.visible = false
|
||||
add_child(_viewer)
|
||||
_viewer.back_pressed.connect(_on_viewer_back)
|
||||
_viewer.economics_link_requested.connect(_on_viewer_economics_link)
|
||||
|
||||
|
||||
func _on_viewer_back() -> void:
|
||||
back_to_viewer_body.emit()
|
||||
|
||||
|
||||
func _on_viewer_economics_link(system_id: String) -> void:
|
||||
economics_link_requested.emit(system_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
+13
-5
@@ -1,10 +1,10 @@
|
||||
class_name AtlasReachMap
|
||||
class_name ReachScreen
|
||||
extends Control
|
||||
## Level 0 REACH_MAP widget for AtlasPanel (#844, D-191).
|
||||
## Level 0 REACH_MAP screen for AtlasApp (#844, D-191).
|
||||
## Hop-ring view of the Settled Reach gate network.
|
||||
## Emits enter_system(system_id) when the player commits to a system.
|
||||
## Emits system_selected when the player commits to a system.
|
||||
|
||||
signal enter_system(system_id: String)
|
||||
signal system_selected(system_id: String)
|
||||
|
||||
const REACH_MAP_CENTER_FRACTION := Vector2(0.5, 0.5)
|
||||
const REACH_MIN_RING_RADIUS: float = 30.0
|
||||
@@ -109,6 +109,14 @@ func trigger_enter() -> void:
|
||||
_reach_enter_selected()
|
||||
|
||||
|
||||
func enter(_payload: Dictionary) -> void:
|
||||
refresh_info_panel_visibility()
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Info panel
|
||||
# =============================================================================
|
||||
@@ -503,4 +511,4 @@ func _reach_enter_selected() -> void:
|
||||
if _reach_selected.is_empty():
|
||||
return
|
||||
if _reach_system_index(_reach_selected) >= 0:
|
||||
enter_system.emit(_reach_selected)
|
||||
system_selected.emit(_reach_selected)
|
||||
@@ -0,0 +1,37 @@
|
||||
class_name RegionalScreen
|
||||
extends Control
|
||||
## Regional heightmap viewer screen for AtlasApp (#844, D-191).
|
||||
## Thin wrapper around AtlasViewer; enter/leave are the nav interface.
|
||||
|
||||
signal back_requested
|
||||
signal economics_link_requested(system_id: String)
|
||||
|
||||
var _viewer: AtlasViewer = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_viewer = AtlasViewer.new()
|
||||
_viewer.name = "AtlasViewer"
|
||||
add_child(_viewer)
|
||||
_viewer.back_pressed.connect(_on_viewer_back)
|
||||
_viewer.economics_link_requested.connect(_on_viewer_economics_link)
|
||||
|
||||
|
||||
func enter(payload: Dictionary) -> void:
|
||||
var body: Dictionary = payload.get("body", {})
|
||||
var system: Dictionary = payload.get("system", {})
|
||||
_viewer.show_body(body, system)
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _on_viewer_back() -> void:
|
||||
back_requested.emit()
|
||||
|
||||
|
||||
func _on_viewer_economics_link(system_id: String) -> void:
|
||||
economics_link_requested.emit(system_id)
|
||||
+22
-8
@@ -1,10 +1,10 @@
|
||||
class_name AtlasSystemMap
|
||||
class_name SystemScreen
|
||||
extends Control
|
||||
## Level 1 SYSTEM_PICKER and Level 2 ORBITAL_DIAGRAM widget for AtlasPanel (#844, D-191).
|
||||
## Manages alphabetic system picker and orbital diagram rendering for the current system.
|
||||
## Emits enter_body(body) when the player clicks a body in orbital view.
|
||||
## System picker and orbital diagram screen for AtlasApp (#844, D-191).
|
||||
## Manages alphabetic system picker and orbital diagram for the current system.
|
||||
## Emits body_selected when the player clicks a body in orbital view.
|
||||
|
||||
signal enter_body(body: Dictionary)
|
||||
signal body_selected(body: Dictionary)
|
||||
|
||||
const ORBITAL_CENTER_FRACTION := Vector2(0.5, 0.55)
|
||||
const STAR_RADIUS: float = 12.0
|
||||
@@ -39,7 +39,7 @@ var _hovered_body: String = ""
|
||||
var _hovered_station: String = ""
|
||||
var _selected_station: Dictionary = {}
|
||||
var _dirty: bool = true
|
||||
var _in_orbital: bool = false # false = picker view, true = orbital view
|
||||
var _in_orbital: bool = false
|
||||
|
||||
var _picker_panel = null # ImplantPanel
|
||||
var _picker_nav_row = null # ImplantDataRow
|
||||
@@ -83,6 +83,10 @@ func current_system() -> Dictionary:
|
||||
return _systems[_selected_idx]
|
||||
|
||||
|
||||
func is_in_orbital() -> bool:
|
||||
return _in_orbital
|
||||
|
||||
|
||||
func get_orbital_body_count() -> int:
|
||||
var count: int = 0
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
@@ -128,6 +132,17 @@ func navigate_system(delta: int) -> void:
|
||||
_rebuild_picker_panel()
|
||||
|
||||
|
||||
func enter(payload: Dictionary) -> void:
|
||||
if payload.get("mode") == "orbital":
|
||||
load_and_show_orbital()
|
||||
else:
|
||||
show_picker_mode()
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Drawing
|
||||
# =============================================================================
|
||||
@@ -183,7 +198,6 @@ func _draw_bodies() -> void:
|
||||
color = COLOR_MOON
|
||||
radius = 3.5
|
||||
"oort_cloud":
|
||||
# Oort cloud shown as faint suggestion, not a solid dot
|
||||
color = COLOR_OORT
|
||||
radius = 2.0
|
||||
_:
|
||||
@@ -351,7 +365,7 @@ func _handle_orbital_click(pos: Vector2) -> void:
|
||||
if not bid.is_empty():
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
if str(b.get("body_id", "")) == bid:
|
||||
enter_body.emit(b)
|
||||
body_selected.emit(b)
|
||||
return
|
||||
|
||||
var sid: String = _find_nearest_station(pos)
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_resource type="Resource" script_class="ImplantAppManifest" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/implant_app_manifest.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
app_path = "implant/economics"
|
||||
display_name = "ECONOMICS MONITOR"
|
||||
icon_path = ""
|
||||
scene_path = "res://ui/implant/apps/economics/economics_app.tscn"
|
||||
default_mode = 1
|
||||
default_key = 78
|
||||
preserves_state = true
|
||||
@@ -0,0 +1,80 @@
|
||||
class_name EconomicsApp
|
||||
extends ImplantApp
|
||||
## Economics Monitor implant app (#824, D-170, D-181).
|
||||
## Registered as "implant/economics" in INSERT mode.
|
||||
## Delegates all data/rendering to OverviewScreen.
|
||||
|
||||
var _overview_screen = null # OverviewScreen
|
||||
var _current_screen_id: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
manifest = load("res://ui/implant/apps/economics/app.tres")
|
||||
super._ready()
|
||||
|
||||
|
||||
func on_install() -> void:
|
||||
_overview_screen = OverviewScreen.new()
|
||||
_overview_screen.name = "OverviewScreen"
|
||||
_overview_screen.visible = false
|
||||
add_child(_overview_screen)
|
||||
nav.set_default("overview")
|
||||
|
||||
|
||||
func _on_screen_changed(new_id: String) -> void:
|
||||
if _current_screen_id != new_id:
|
||||
var old := _get_screen(_current_screen_id)
|
||||
if old:
|
||||
old.leave()
|
||||
old.visible = false
|
||||
|
||||
_current_screen_id = new_id
|
||||
|
||||
var s := _get_screen(new_id)
|
||||
if s:
|
||||
s.visible = true
|
||||
s.enter(nav.current_payload())
|
||||
|
||||
|
||||
func _get_screen(screen_id: String) -> Control:
|
||||
if screen_id == "overview":
|
||||
return _overview_screen as Control
|
||||
return null
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Public API — delegates to OverviewScreen
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func receive_economy_data(data: Dictionary) -> void:
|
||||
if _overview_screen:
|
||||
_overview_screen.receive_economy_data(data)
|
||||
|
||||
|
||||
func select_system(system_id: String) -> void:
|
||||
if _overview_screen:
|
||||
_overview_screen.select_system(system_id)
|
||||
|
||||
|
||||
func navigate(delta: int) -> void:
|
||||
if _overview_screen:
|
||||
_overview_screen.navigate(delta)
|
||||
|
||||
|
||||
func get_history(system_id: String) -> Array:
|
||||
if _overview_screen:
|
||||
return _overview_screen.get_history(system_id)
|
||||
return []
|
||||
|
||||
|
||||
func get_latest(system_id: String) -> Dictionary:
|
||||
if _overview_screen:
|
||||
return _overview_screen.get_latest(system_id)
|
||||
return {}
|
||||
|
||||
|
||||
func get_known_systems() -> Array:
|
||||
if _overview_screen:
|
||||
return _overview_screen.get_known_systems()
|
||||
return []
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/apps/economics/economics_app.gd" id="1_econ_app"]
|
||||
|
||||
; #824: Economics Monitor implant app — price data and GDP for selected system.
|
||||
; INSERT mode (z=10) at implant/economics per D-170. Managed via ImplantApp/ImplantNavStack pattern.
|
||||
; Toggle with N key (manifest.default_key). Data flows from EconomySnapshot via snapshot_consumers.
|
||||
|
||||
[node name="EconomicsApp" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_econ_app")
|
||||
+30
-92
@@ -1,33 +1,19 @@
|
||||
class_name EconomicsPanel
|
||||
class_name OverviewScreen
|
||||
extends Control
|
||||
## Economics Monitor overview screen (#824, D-170, D-181).
|
||||
## Ported from economics_panel.gd into the ImplantApp screen pattern.
|
||||
##
|
||||
## Data architecture: ring buffer (last 20 ticks per system), 7 D-181 signals.
|
||||
## Signals 1-2 (price_current, price_trend) are Phase 2 deliverables;
|
||||
## signals 3-7 are parsed and stored but not yet displayed (Phase 3).
|
||||
|
||||
## Economics Monitor — implant insert panel (#824, D-170, D-181).
|
||||
##
|
||||
## Displays price data and GDP for a selected system. Receives economy_snapshot
|
||||
## from the server via snapshot_handler → GameState → snapshot_consumers pipeline.
|
||||
##
|
||||
## Data architecture:
|
||||
## - Ring buffer: last 20 ticks of economy data per system (for trend display)
|
||||
## - 7 D-181 signals per system: price_current, price_trend, trade_flow_volume,
|
||||
## corporate_presence, stockpile_weeks, production_vs_baseline, official_coverage_ratio
|
||||
## - Signals 1-2 (price_current, price_trend) are Phase 2 deliverables
|
||||
## - Signals 3-7 are parsed and stored but not yet displayed (Phase 3)
|
||||
##
|
||||
## Visual layer: ImplantPanel composition built in _ready() from component library (D-169).
|
||||
## System selector uses LEFT/RIGHT arrow keys to cycle through all 301 systems.
|
||||
## Placeholder commodity prices shown until #822 ships.
|
||||
|
||||
## Emitted when new economy data arrives for the selected system.
|
||||
signal economy_data_updated(system_id: String, data: Dictionary)
|
||||
|
||||
const APP_PATH := "implant/economics"
|
||||
const RING_BUFFER_SIZE: int = 20
|
||||
const STAR_MAP_DATA := "res://data/star_map_data.json"
|
||||
const PANEL_WIDTH: float = 340.0
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
|
||||
# Placeholder commodity rows shown until server ships EconomySnapshot (#822).
|
||||
# Commodity IDs match D-184 catalog.
|
||||
const PLACEHOLDER_COMMODITIES: Array[Dictionary] = [
|
||||
{"id": "fusion_fuel", "name": "FUSION FUEL", "price": 142, "trend": 1},
|
||||
{"id": "basic_goods", "name": "BASIC GOODS", "price": 58, "trend": 0},
|
||||
@@ -37,27 +23,19 @@ const PLACEHOLDER_COMMODITIES: Array[Dictionary] = [
|
||||
{"id": "pharmaceuticals", "name": "PHARMA", "price": 312, "trend": -1},
|
||||
]
|
||||
|
||||
## Currently selected system for detailed display. Empty = no selection.
|
||||
var selected_system: String = ""
|
||||
|
||||
## Ring buffer: system_id → Array[Dictionary] (most recent last, max RING_BUFFER_SIZE).
|
||||
## Each entry is one tick's worth of D-181 signals for that system.
|
||||
var _history: Dictionary = {}
|
||||
|
||||
var _insert_active: bool = true
|
||||
|
||||
# Visual panel state (D-169 component library)
|
||||
var _panel: ImplantPanel = null # root container
|
||||
var _panel: ImplantPanel = null
|
||||
var _implant_theme: ImplantTheme = null
|
||||
var _header: ImplantHeader = null # kept for set_content() on system change
|
||||
var _nav_row: ImplantDataRow = null # system selector nav hint
|
||||
var _gdp_row: ImplantDataRow = null # GDP value row
|
||||
var _commodity_rows: Array = [] # ImplantDataRow × 6, updated without full rebuild
|
||||
var _placeholder_notice: ImplantTextBlock = null # hidden once live data arrives
|
||||
var _header: ImplantHeader = null
|
||||
var _nav_row: ImplantDataRow = null
|
||||
var _gdp_row: ImplantDataRow = null
|
||||
var _commodity_rows: Array = []
|
||||
var _placeholder_notice: ImplantTextBlock = null
|
||||
|
||||
# System list for the selector (populated from STAR_MAP_DATA)
|
||||
var _systems: Array = [] # Array[Dictionary], sorted by proper_name
|
||||
var _selected_idx: int = 0 # index into _systems
|
||||
var _systems: Array = []
|
||||
var _selected_idx: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -65,21 +43,22 @@ func _ready() -> void:
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# D-170: Register with HUD layer groups
|
||||
HudGroups.register(self, APP_PATH)
|
||||
HudGroups.app_changed.connect(_on_app_changed)
|
||||
|
||||
_implant_theme = load("res://ui/implant/default_implant.tres") as ImplantTheme
|
||||
_load_system_list()
|
||||
_build_panel()
|
||||
economy_data_updated.connect(_on_economy_data_updated)
|
||||
|
||||
|
||||
## Called from SnapshotConsumers when economy_snapshot arrives in GameState.
|
||||
## data: Dictionary keyed by system_id → signal payload (D-181).
|
||||
func enter(_payload: Dictionary) -> void:
|
||||
pass
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func receive_economy_data(data: Dictionary) -> void:
|
||||
for system_id: String in data:
|
||||
var signals: Variant = data[system_id]
|
||||
@@ -92,12 +71,10 @@ func receive_economy_data(data: Dictionary) -> void:
|
||||
if buf.size() > RING_BUFFER_SIZE:
|
||||
_history[system_id] = buf.slice(buf.size() - RING_BUFFER_SIZE)
|
||||
|
||||
# Notify listeners if the selected system received new data
|
||||
if not selected_system.is_empty() and data.has(selected_system):
|
||||
economy_data_updated.emit(selected_system, data[selected_system])
|
||||
|
||||
|
||||
## Select a system for detailed display. Emits economy_data_updated if history exists.
|
||||
func select_system(system_id: String) -> void:
|
||||
selected_system = system_id
|
||||
if not selected_system.is_empty() and _history.has(selected_system):
|
||||
@@ -106,13 +83,10 @@ func select_system(system_id: String) -> void:
|
||||
economy_data_updated.emit(selected_system, buf[buf.size() - 1])
|
||||
|
||||
|
||||
## Get the full ring buffer for a system (for chart/sparkline rendering).
|
||||
## Returns empty array if no history exists.
|
||||
func get_history(system_id: String) -> Array:
|
||||
return _history.get(system_id, [])
|
||||
|
||||
|
||||
## Get the latest tick's signals for a system, or empty dict.
|
||||
func get_latest(system_id: String) -> Dictionary:
|
||||
var buf: Array = _history.get(system_id, [])
|
||||
if buf.size() > 0:
|
||||
@@ -120,41 +94,26 @@ func get_latest(system_id: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
## Get all system IDs that have received at least one tick of data.
|
||||
func get_known_systems() -> Array:
|
||||
return _history.keys()
|
||||
|
||||
|
||||
## Toggle via HUD layer system (D-170). INSERT mode — shares screen with gameplay.
|
||||
func toggle_visible() -> void:
|
||||
HudGroups.toggle_app(APP_PATH, HudGroups.Mode.INSERT)
|
||||
|
||||
|
||||
## Called from main.gd when insert state changes (D-170).
|
||||
func set_insert_active(active: bool) -> void:
|
||||
_insert_active = active
|
||||
if not active and HudGroups.is_app_active(APP_PATH):
|
||||
HudGroups.close_app()
|
||||
|
||||
|
||||
## Respond to app layer changes (D-170).
|
||||
func _on_app_changed(app_path: String, mode: int) -> void:
|
||||
if app_path != APP_PATH:
|
||||
func navigate(delta: int) -> void:
|
||||
if _systems.is_empty():
|
||||
return
|
||||
if mode == HudGroups.Mode.FULLSCREEN or mode == HudGroups.Mode.INSERT:
|
||||
visible = true
|
||||
else:
|
||||
visible = false
|
||||
_selected_idx = wrapi(_selected_idx + delta, 0, _systems.size())
|
||||
selected_system = _systems[_selected_idx].get("system_id", "")
|
||||
_rebuild_panel()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# System list — populated from star_map_data.json
|
||||
# System list
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_system_list() -> void:
|
||||
if not FileAccess.file_exists(STAR_MAP_DATA):
|
||||
push_warning("EconomicsPanel: %s not found" % STAR_MAP_DATA)
|
||||
push_warning("OverviewScreen: %s not found" % STAR_MAP_DATA)
|
||||
return
|
||||
var file := FileAccess.open(STAR_MAP_DATA, FileAccess.READ)
|
||||
if file == null:
|
||||
@@ -193,7 +152,6 @@ func _build_panel() -> void:
|
||||
_rebuild_panel()
|
||||
|
||||
|
||||
## Full rebuild of panel components. Called on system change and initial build.
|
||||
func _rebuild_panel() -> void:
|
||||
if not _panel:
|
||||
return
|
||||
@@ -205,29 +163,22 @@ func _rebuild_panel() -> void:
|
||||
var sys_id: String = node.get("system_id", "")
|
||||
var total: int = _systems.size()
|
||||
|
||||
# ── Header ────────────────────────────────────────────────────────────────
|
||||
_header = ImplantHeader.new("ECONOMICS MONITOR", sys_name)
|
||||
_panel.add_component(_header)
|
||||
|
||||
_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
# ── System selector nav ────────────────────────────────────────────────────
|
||||
var nav_hint := "◄ ► · %s [%d / %d]" % [sys_id, _selected_idx + 1, total]
|
||||
_nav_row = ImplantDataRow.new(nav_hint)
|
||||
_panel.add_component(_nav_row)
|
||||
|
||||
_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
# ── Population + GDP strip ────────────────────────────────────────────────
|
||||
var pop_str: String = node.get("population", "—")
|
||||
_panel.add_component(ImplantDataRow.new("pop " + pop_str))
|
||||
var gdp_str: String = node.get("gdp", "—")
|
||||
_gdp_row = ImplantDataRow.new("gdp " + gdp_str)
|
||||
_panel.add_component(_gdp_row)
|
||||
|
||||
_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
# ── Price table ───────────────────────────────────────────────────────────
|
||||
_panel.add_component(ImplantTextBlock.new("MARKET PRICES"))
|
||||
|
||||
var latest: Dictionary = get_latest(sys_id)
|
||||
@@ -238,7 +189,6 @@ func _rebuild_panel() -> void:
|
||||
var price: int = c.get("price", 0)
|
||||
var trend: int = c.get("trend", 0)
|
||||
|
||||
# Overlay live data when available (D-181 signal 1-2)
|
||||
for sig: Dictionary in commodity_signals:
|
||||
if sig.get("commodity_id", "") == cid:
|
||||
price = int(sig.get("price_current", price))
|
||||
@@ -250,7 +200,6 @@ func _rebuild_panel() -> void:
|
||||
_panel.add_component(row)
|
||||
_commodity_rows.append(row)
|
||||
|
||||
# ── Placeholder notice ────────────────────────────────────────────────────
|
||||
_panel.add_component(ImplantSeparator.new())
|
||||
var notice_text: String = (
|
||||
"[LIVE MARKET — #822 PENDING]" if _history.is_empty() else "LIVE DATA ACTIVE"
|
||||
@@ -277,7 +226,6 @@ func _trend_glyph(trend: int) -> String:
|
||||
return "—"
|
||||
|
||||
|
||||
## Respond to economy_data_updated signal — refresh the price table in-place.
|
||||
func _on_economy_data_updated(system_id: String, data: Dictionary) -> void:
|
||||
if not _panel or _commodity_rows.is_empty():
|
||||
return
|
||||
@@ -305,13 +253,3 @@ func _on_economy_data_updated(system_id: String, data: Dictionary) -> void:
|
||||
|
||||
if _placeholder_notice and not _history.is_empty():
|
||||
_placeholder_notice.text = "LIVE DATA ACTIVE"
|
||||
|
||||
|
||||
## Cycle the system selector by delta steps (+1 or -1).
|
||||
## Called from main.gd _unhandled_key_input — [ and ] keys when panel is active.
|
||||
func navigate(delta: int) -> void:
|
||||
if _systems.is_empty():
|
||||
return
|
||||
_selected_idx = wrapi(_selected_idx + delta, 0, _systems.size())
|
||||
selected_system = _systems[_selected_idx].get("system_id", "")
|
||||
_rebuild_panel()
|
||||
@@ -1,302 +0,0 @@
|
||||
class_name AtlasPanel
|
||||
extends Control
|
||||
## Atlas implant panel — unified 5-level nav chain (#844, D-191):
|
||||
## reach map → system picker → orbital diagram → body entry → regional viewer.
|
||||
## FULLSCREEN implant app (z=20) at implant/map per D-170.
|
||||
## Uses ImplantPanel component library (D-169). Data from star_map_data.json.
|
||||
##
|
||||
## Navigation:
|
||||
## Level 0 REACH_MAP — hop-ring view of the Reach gate network (AtlasReachMap)
|
||||
## Level 1 SYSTEM_PICKER — ◄ ► cycle systems alphabetically (AtlasSystemMap)
|
||||
## Level 2 ORBITAL_DIAGRAM — rendered via AtlasSystemMap._draw()
|
||||
## Level 3 BODY_ENTRY — body info panel (AtlasPlanetMap)
|
||||
## Level 4 HEIGHTMAP_VIEWER — AtlasViewer with pan/zoom + markers (AtlasPlanetMap)
|
||||
|
||||
## Emitted when the viewer's city-data panel requests the economics monitor for
|
||||
## the current system. main.gd bridges this to EconomicsPanel.select_system()
|
||||
## + HudGroups.open_app("implant/economics") — D-191 cross-panel integration.
|
||||
signal economics_link_requested(system_id: String)
|
||||
|
||||
enum Level {
|
||||
REACH_MAP = 0, SYSTEM_PICKER = 1, ORBITAL_DIAGRAM = 2, BODY_ENTRY = 3, HEIGHTMAP_VIEWER = 4
|
||||
}
|
||||
|
||||
const APP_PATH := "implant/map"
|
||||
const STAR_MAP_DATA := "res://data/star_map_data.json"
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
|
||||
var _level: Level = Level.REACH_MAP
|
||||
var _systems: Array = []
|
||||
var _reach_node_lookup: Dictionary = {}
|
||||
|
||||
var _implant_theme = null # loaded at runtime (autoload parse-order rule)
|
||||
var _screen_header: ImplantHeader = null
|
||||
var _reach_map: AtlasReachMap = null
|
||||
var _system_map: AtlasSystemMap = null
|
||||
var _planet_map: AtlasPlanetMap = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
HudGroups.register(self, APP_PATH)
|
||||
HudGroups.app_changed.connect(_on_app_changed)
|
||||
|
||||
_implant_theme = load("res://ui/implant/default_implant.tres")
|
||||
|
||||
_load_system_list()
|
||||
_build_widgets()
|
||||
_build_screen_header()
|
||||
_show_level(Level.REACH_MAP)
|
||||
|
||||
|
||||
## Toggle atlas panel. Called from main.gd on KEY_M.
|
||||
func toggle_visible() -> void:
|
||||
HudGroups.toggle_app(APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
|
||||
|
||||
## Called from main.gd when insert state changes.
|
||||
func set_insert_active(active: bool) -> void:
|
||||
if not active and HudGroups.is_app_active(APP_PATH):
|
||||
HudGroups.close_app()
|
||||
|
||||
|
||||
func _on_app_changed(app_path: String, mode: int) -> void:
|
||||
if app_path != APP_PATH:
|
||||
return
|
||||
if mode == HudGroups.Mode.FULLSCREEN:
|
||||
visible = true
|
||||
_active_widget().queue_redraw()
|
||||
else:
|
||||
visible = false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data loading
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_system_list() -> void:
|
||||
if not FileAccess.file_exists(STAR_MAP_DATA):
|
||||
push_warning("AtlasPanel: %s not found" % STAR_MAP_DATA)
|
||||
return
|
||||
var file := FileAccess.open(STAR_MAP_DATA, FileAccess.READ)
|
||||
if file == null:
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
file.close()
|
||||
if not (parsed is Dictionary):
|
||||
return
|
||||
for node: Dictionary in parsed.get("nodes", []):
|
||||
if not node.get("system_id", "").is_empty():
|
||||
_systems.append(node)
|
||||
_systems.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var na: String = a.get("proper_name", a.get("system_id", ""))
|
||||
var nb: String = b.get("proper_name", b.get("system_id", ""))
|
||||
return na < nb
|
||||
)
|
||||
for node: Dictionary in _systems:
|
||||
_reach_node_lookup[node.get("system_id", "")] = node
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Widget construction
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_widgets() -> void:
|
||||
_reach_map = AtlasReachMap.new()
|
||||
_reach_map.name = "ReachMap"
|
||||
add_child(_reach_map)
|
||||
_reach_map.setup(_implant_theme)
|
||||
_reach_map.set_systems(_systems, _reach_node_lookup)
|
||||
_reach_map.enter_system.connect(_on_reach_enter_system)
|
||||
|
||||
_system_map = AtlasSystemMap.new()
|
||||
_system_map.name = "SystemMap"
|
||||
add_child(_system_map)
|
||||
_system_map.setup(_implant_theme)
|
||||
_system_map.set_systems(_systems)
|
||||
_system_map.enter_body.connect(_on_system_enter_body)
|
||||
|
||||
_planet_map = AtlasPlanetMap.new()
|
||||
_planet_map.name = "PlanetMap"
|
||||
add_child(_planet_map)
|
||||
_planet_map.setup(_implant_theme)
|
||||
_planet_map.back_to_viewer_body.connect(_on_planet_back_to_body)
|
||||
_planet_map.economics_link_requested.connect(_on_planet_economics_link)
|
||||
|
||||
|
||||
func _build_screen_header() -> void:
|
||||
_screen_header = ImplantHeader.new()
|
||||
_screen_header.position = Vector2(PANEL_MARGIN, 16.0)
|
||||
_screen_header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_screen_header)
|
||||
if _implant_theme:
|
||||
_screen_header.apply_implant_theme(_implant_theme)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Widget signal handlers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _on_reach_enter_system(system_id: String) -> void:
|
||||
var idx := _find_system_idx(system_id)
|
||||
if idx >= 0:
|
||||
_system_map.set_selected_idx(idx)
|
||||
_system_map.load_and_show_orbital()
|
||||
_show_level(Level.ORBITAL_DIAGRAM)
|
||||
|
||||
|
||||
func _on_system_enter_body(body: Dictionary) -> void:
|
||||
_planet_map.set_body(body, _system_map.current_system())
|
||||
_show_level(Level.BODY_ENTRY)
|
||||
|
||||
|
||||
func _on_planet_back_to_body() -> void:
|
||||
_show_level(Level.BODY_ENTRY)
|
||||
|
||||
|
||||
func _on_planet_economics_link(system_id: String) -> void:
|
||||
economics_link_requested.emit(system_id)
|
||||
HudGroups.close_app()
|
||||
|
||||
|
||||
func _find_system_idx(system_id: String) -> int:
|
||||
for i: int in range(_systems.size()):
|
||||
if _systems[i].get("system_id", "") == system_id:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Navigation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _show_level(level: Level) -> void:
|
||||
_level = level
|
||||
|
||||
_reach_map.visible = (level == Level.REACH_MAP)
|
||||
if level == Level.REACH_MAP:
|
||||
_reach_map.refresh_info_panel_visibility()
|
||||
|
||||
_system_map.visible = (level == Level.SYSTEM_PICKER or level == Level.ORBITAL_DIAGRAM)
|
||||
if level == Level.SYSTEM_PICKER:
|
||||
_system_map.show_picker_mode()
|
||||
|
||||
_planet_map.visible = (level == Level.BODY_ENTRY or level == Level.HEIGHTMAP_VIEWER)
|
||||
if level == Level.BODY_ENTRY:
|
||||
_planet_map.show_body_mode()
|
||||
|
||||
_refresh_screen_header()
|
||||
|
||||
|
||||
func _navigate_back() -> void:
|
||||
match _level:
|
||||
Level.REACH_MAP:
|
||||
HudGroups.close_app()
|
||||
Level.SYSTEM_PICKER:
|
||||
_show_level(Level.REACH_MAP)
|
||||
Level.ORBITAL_DIAGRAM:
|
||||
_show_level(Level.REACH_MAP)
|
||||
Level.BODY_ENTRY:
|
||||
_show_level(Level.ORBITAL_DIAGRAM)
|
||||
Level.HEIGHTMAP_VIEWER:
|
||||
_show_level(Level.BODY_ENTRY)
|
||||
|
||||
|
||||
func _active_widget() -> Control:
|
||||
match _level:
|
||||
Level.REACH_MAP:
|
||||
return _reach_map
|
||||
Level.SYSTEM_PICKER, Level.ORBITAL_DIAGRAM:
|
||||
return _system_map
|
||||
_:
|
||||
return _planet_map
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Input
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and (event as InputEventKey).pressed and not event.is_echo():
|
||||
_handle_key(event as InputEventKey)
|
||||
|
||||
|
||||
func _handle_key(event: InputEventKey) -> void:
|
||||
if _level == Level.HEIGHTMAP_VIEWER:
|
||||
return
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
_navigate_back()
|
||||
KEY_B:
|
||||
HudGroups.close_app()
|
||||
KEY_LEFT:
|
||||
if _level == Level.SYSTEM_PICKER:
|
||||
_system_map.navigate_system(-1)
|
||||
_refresh_screen_header()
|
||||
KEY_RIGHT:
|
||||
if _level == Level.SYSTEM_PICKER:
|
||||
_system_map.navigate_system(1)
|
||||
_refresh_screen_header()
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
if _level == Level.REACH_MAP:
|
||||
_reach_map.trigger_enter()
|
||||
elif _level == Level.SYSTEM_PICKER:
|
||||
_system_map.load_and_show_orbital()
|
||||
_show_level(Level.ORBITAL_DIAGRAM)
|
||||
elif _level == Level.BODY_ENTRY:
|
||||
_planet_map.show_viewer_mode()
|
||||
_show_level(Level.HEIGHTMAP_VIEWER)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Screen header
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _refresh_screen_header() -> void:
|
||||
if _screen_header == null:
|
||||
return
|
||||
var title: String = ""
|
||||
var hint: String = ""
|
||||
match _level:
|
||||
Level.REACH_MAP:
|
||||
title = "THE REACH — NAVIGATOR"
|
||||
hint = "Concord Assembly Gate Network · click system · enter open orbital · esc close"
|
||||
Level.SYSTEM_PICKER:
|
||||
title = "ATLAS — SYSTEM SELECTION"
|
||||
hint = "select a system · ◄ ► cycle · enter open orbital view · esc back"
|
||||
Level.ORBITAL_DIAGRAM:
|
||||
var sys: Dictionary = _system_map.current_system()
|
||||
var sys_name: String = str(sys.get("proper_name", sys.get("system_id", "—")))
|
||||
title = "ATLAS — ORBITAL VIEW · " + sys_name.to_upper()
|
||||
var star_type: String = str(sys.get("star_type", ""))
|
||||
var top_count: int = _system_map.get_orbital_body_count()
|
||||
var subtitle: String = ""
|
||||
if not star_type.is_empty():
|
||||
subtitle = star_type + " · "
|
||||
subtitle += (
|
||||
"%d orbital bodies · %d stations" % [top_count, _system_map.get_station_count()]
|
||||
)
|
||||
hint = subtitle + " · click body → atlas entry · esc back"
|
||||
Level.BODY_ENTRY:
|
||||
var sys2: Dictionary = _system_map.current_system()
|
||||
var sys2_name: String = str(sys2.get("proper_name", sys2.get("system_id", "—")))
|
||||
title = "ATLAS — BODY ENTRY · " + sys2_name.to_upper()
|
||||
hint = "enter view atlas · esc back to orbital"
|
||||
Level.HEIGHTMAP_VIEWER:
|
||||
title = ""
|
||||
hint = ""
|
||||
_screen_header.set_content(title, hint)
|
||||
_screen_header.visible = (_level != Level.HEIGHTMAP_VIEWER)
|
||||
@@ -1,17 +0,0 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/atlas_panel.gd" id="1_atlas"]
|
||||
|
||||
; #844: Atlas implant panel — 5-level navigation: reach map → system picker → orbital → body entry → heightmap.
|
||||
; FULLSCREEN app (z=20) at implant/map per D-170. Shell delegates to AtlasReachMap / AtlasSystemMap / AtlasPlanetMap.
|
||||
; Toggle with M key from main.gd. Data from star_map_data.json.
|
||||
|
||||
[node name="AtlasPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_atlas")
|
||||
@@ -1,18 +0,0 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/economics_panel.gd" id="1_econ"]
|
||||
|
||||
; #824: Economics Monitor insert panel — price data and GDP for selected system.
|
||||
; Composed from ImplantPanel component library (D-169). Registered under implant/economics (D-170).
|
||||
; Toggle with E key in implant mode. Data flows from EconomySnapshot via snapshot_consumers.
|
||||
; Placeholder commodity prices shown until server ticket #822 ships.
|
||||
|
||||
[node name="EconomicsPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_econ")
|
||||
@@ -0,0 +1,87 @@
|
||||
class_name ImplantApp
|
||||
extends Control
|
||||
## Base class for all implant apps. Absorbs HudGroups boilerplate; subclasses
|
||||
## override lifecycle hooks only (#844, D-191).
|
||||
##
|
||||
## 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
|
||||
signal insert_deactivated
|
||||
|
||||
var manifest: ImplantAppManifest = null
|
||||
var nav: ImplantNavStack = null
|
||||
|
||||
|
||||
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:
|
||||
if manifest and HudGroups.is_app_active(manifest.app_path):
|
||||
HudGroups.close_app()
|
||||
|
||||
|
||||
func _on_screen_changed(_screen_id: String) -> void:
|
||||
pass
|
||||
|
||||
|
||||
func handle_intent(_action: String, _params: Dictionary) -> void:
|
||||
pass
|
||||
@@ -0,0 +1,12 @@
|
||||
class_name ImplantAppManifest
|
||||
extends Resource
|
||||
## Manifest resource for an implant app. Each app directory ships one app.tres
|
||||
## that declares identity and capabilities (#844, D-191).
|
||||
|
||||
@export var app_path: String = ""
|
||||
@export var display_name: String = ""
|
||||
@export var icon_path: String = ""
|
||||
@export var scene_path: String = ""
|
||||
@export var default_mode: int = 2 # HudGroups.Mode.FULLSCREEN
|
||||
@export var default_key: int = -1 # KEY_M = 77, KEY_N = 78; -1 = no binding
|
||||
@export var preserves_state: bool = true
|
||||
@@ -0,0 +1,69 @@
|
||||
class_name ImplantNavStack
|
||||
extends Node
|
||||
## Intra-app navigation stack for ImplantApp subclasses (#844, D-191).
|
||||
## Mutation is synchronous: screen_changed fires before push/pop/replace returns.
|
||||
|
||||
signal screen_changed(current_screen_id: String)
|
||||
|
||||
var _stack: Array[String] = []
|
||||
var _payloads: Array[Dictionary] = []
|
||||
var _default_screen_id: String = ""
|
||||
|
||||
|
||||
func set_default(screen_id: String) -> void:
|
||||
_default_screen_id = screen_id
|
||||
|
||||
|
||||
func push(screen_id: String, payload: Dictionary = {}) -> void:
|
||||
_stack.append(screen_id)
|
||||
_payloads.append(payload)
|
||||
screen_changed.emit(screen_id)
|
||||
|
||||
|
||||
func pop() -> void:
|
||||
if _stack.is_empty():
|
||||
push_warning("ImplantNavStack: pop() on empty stack")
|
||||
return
|
||||
_stack.pop_back()
|
||||
_payloads.pop_back()
|
||||
if not _stack.is_empty():
|
||||
screen_changed.emit(_stack.back())
|
||||
elif not _default_screen_id.is_empty():
|
||||
push(_default_screen_id)
|
||||
|
||||
|
||||
func replace(screen_id: String, payload: Dictionary = {}) -> void:
|
||||
if not _stack.is_empty():
|
||||
_stack.pop_back()
|
||||
_payloads.pop_back()
|
||||
_stack.append(screen_id)
|
||||
_payloads.append(payload)
|
||||
screen_changed.emit(screen_id)
|
||||
|
||||
|
||||
func reset_to_default() -> void:
|
||||
_stack.clear()
|
||||
_payloads.clear()
|
||||
if not _default_screen_id.is_empty():
|
||||
push(_default_screen_id)
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _stack.is_empty()
|
||||
|
||||
|
||||
func push_default() -> void:
|
||||
if not _default_screen_id.is_empty():
|
||||
push(_default_screen_id)
|
||||
|
||||
|
||||
func current() -> String:
|
||||
if _stack.is_empty():
|
||||
return ""
|
||||
return _stack.back()
|
||||
|
||||
|
||||
func current_payload() -> Dictionary:
|
||||
if _payloads.is_empty():
|
||||
return {}
|
||||
return _payloads.back()
|
||||
@@ -0,0 +1,43 @@
|
||||
extends Node
|
||||
## Lazy registry of installed implant apps (#844, D-191).
|
||||
## Autoload — scans apps/*/app.tres on first get_manifests() call.
|
||||
## Does NOT reference ImplantAppManifest class_name at load time (autoload
|
||||
## parse-order rule; see CLAUDE.md).
|
||||
|
||||
var _manifests: Array = [] # Array[ImplantAppManifest]
|
||||
var _scanned: bool = false
|
||||
|
||||
|
||||
func get_manifests() -> Array:
|
||||
if not _scanned:
|
||||
_scan()
|
||||
return _manifests
|
||||
|
||||
|
||||
func _scan() -> void:
|
||||
_scanned = true
|
||||
_manifests.clear()
|
||||
var dir := DirAccess.open("res://ui/implant/apps")
|
||||
if dir == null:
|
||||
push_warning("ImplantRegistry: could not open res://ui/implant/apps")
|
||||
return
|
||||
dir.list_dir_begin()
|
||||
var entry := dir.get_next()
|
||||
while not entry.is_empty():
|
||||
if dir.current_is_dir() and not entry.begins_with("."):
|
||||
var tres_path: String = "res://ui/implant/apps/%s/app.tres" % entry
|
||||
if ResourceLoader.exists(tres_path):
|
||||
var m = load(tres_path)
|
||||
if _is_valid_manifest(m):
|
||||
_manifests.append(m)
|
||||
else:
|
||||
push_warning("ImplantRegistry: invalid or missing app_path in %s" % tres_path)
|
||||
entry = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
|
||||
|
||||
func _is_valid_manifest(m: Variant) -> bool:
|
||||
if m == null:
|
||||
return false
|
||||
var app_path = m.get("app_path")
|
||||
return app_path != null and not (app_path as String).is_empty()
|
||||
Reference in New Issue
Block a user