feat(ui): PR #131 review round 4 — scene_path, schema_version, screens absorption
Three architectural review comments implemented per Tyre's proposals: #3 — scene_path consumption (full, Option A) - ImplantRegistry.instantiate_all(parent) loads and instantiates all registered apps with a declared scene_path. Manifests with empty scene_path are treated as metadata-only (silently skipped). - ImplantRegistry.get_app_instance(app_path) returns the live instance. - hud.tscn no longer direct-instances AtlasApp or EconomicsApp — an AppsContainer Control holds the registry-managed children. - hud.gd._ready() calls ImplantRegistry.instantiate_all($AppsContainer). - main.gd drops @onready vars for atlas_app/economics_app; looks up both from the registry at the top of _ready(). #6c — schema_version on manifest - ImplantAppManifest: @export var schema_version: int = 1 (first field). - ImplantRegistry: const CURRENT_SCHEMA_VERSION := 1; tiered check in _scan() — older-than-current emits print_verbose, newer-than-current emits push_warning, both proceed best-effort. - apps/atlas/app.tres + apps/economics/app.tres: schema_version = 1. #10 — absorb _on_screen_changed boilerplate into ImplantApp - Base class gains _screens: Dictionary, _current_screen_id: String, register_screen(id, screen), current_screen_id(), and a real default _on_screen_changed implementation that handles leave+hide+enter+show with has_method guards and same-screen-replace detection. - atlas_app: deletes _current_screen_id, _get_screen(), _on_screen_changed() override; on_install() collapses to construct → setup → wire → register_screen(id, screen) per screen. Preserves direct screen refs for atlas-specific signal wiring and method calls. - economics_app: deletes same scaffolding; on_install() reduces to three lines (construct overview_screen, register_screen, nav.set_default). Also: replaced Resource.get(name, default) dict-style calls with direct property access on typed ImplantAppManifest reads (2-arg get() is Dictionary-only; causes "Too many arguments" parse errors on Resources). _is_valid_manifest gained a Resource type guard and a property-exists check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ extends Node2D
|
||||
|
||||
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
|
||||
|
||||
var economics_app = null # EconomicsApp — populated in _ready() via ImplantRegistry
|
||||
var atlas_app = null # AtlasApp — populated in _ready() via ImplantRegistry
|
||||
|
||||
var _camera_anchored: bool = false
|
||||
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay
|
||||
var _teleport_in_progress: bool = false # #501/#117: forces camera snap on next frame
|
||||
@@ -32,13 +35,15 @@ 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 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:
|
||||
print("The Settled Reach — client initialized")
|
||||
|
||||
# #844 D-191: Populate app refs from registry (hud._ready already called instantiate_all).
|
||||
atlas_app = ImplantRegistry.get_app_instance("implant/map")
|
||||
economics_app = ImplantRegistry.get_app_instance("implant/economics")
|
||||
|
||||
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
||||
camera.position_smoothing_enabled = false
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ var _perception_row: ImplantDataRow
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
ImplantRegistry.instantiate_all($AppsContainer)
|
||||
|
||||
# Remove the old raw MarginContainer/labels if present
|
||||
var old := get_node_or_null("MarginContainer")
|
||||
if old:
|
||||
|
||||
+9
-12
@@ -1,8 +1,6 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cq1y5w3hmxr8b"]
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cq1y5w3hmxr8b"]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"]
|
||||
[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
|
||||
@@ -14,12 +12,11 @@ grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_hud")
|
||||
|
||||
; #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="EconomicsApp" parent="." instance=ExtResource("2_econ")]
|
||||
visible = false
|
||||
|
||||
; #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
|
||||
[node name="AppsContainer" type="Control" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
schema_version = 1
|
||||
app_path = "implant/map"
|
||||
scene_path = "res://ui/implant/apps/atlas/atlas_app.tscn"
|
||||
default_mode = "fullscreen"
|
||||
|
||||
@@ -13,7 +13,6 @@ 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:
|
||||
@@ -26,65 +25,41 @@ func on_install() -> void:
|
||||
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)
|
||||
register_screen("reach", _reach_screen)
|
||||
|
||||
_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)
|
||||
register_screen("system", _system_screen)
|
||||
|
||||
_planet_screen = PlanetScreen.new()
|
||||
_planet_screen.name = "PlanetScreen"
|
||||
_planet_screen.visible = false
|
||||
add_child(_planet_screen)
|
||||
_planet_screen.setup(implant_theme)
|
||||
register_screen("planet", _planet_screen)
|
||||
|
||||
_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)
|
||||
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:
|
||||
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":
|
||||
if current_screen_id() == "regional":
|
||||
return # AtlasViewer handles its own keyboard input
|
||||
_handle_key(event)
|
||||
get_viewport().set_input_as_handled()
|
||||
@@ -93,22 +68,22 @@ func _unhandled_key_input(event: InputEventKey) -> void:
|
||||
func _handle_key(event: InputEventKey) -> void:
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
if _current_screen_id == "reach":
|
||||
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:
|
||||
if current_screen_id() == "system" and _system_screen:
|
||||
_system_screen.navigate_system(-1)
|
||||
KEY_BRACKETRIGHT:
|
||||
if _current_screen_id == "system" and _system_screen:
|
||||
if current_screen_id() == "system" and _system_screen:
|
||||
_system_screen.navigate_system(1)
|
||||
|
||||
|
||||
func _handle_enter() -> void:
|
||||
match _current_screen_id:
|
||||
match current_screen_id():
|
||||
"reach":
|
||||
if _reach_screen and _reach_screen.has_selection():
|
||||
_reach_screen.trigger_enter()
|
||||
@@ -169,20 +144,6 @@ func _forward_economics_link(system_id: String) -> void:
|
||||
# =============================================================================
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
schema_version = 1
|
||||
app_path = "implant/economics"
|
||||
scene_path = "res://ui/implant/apps/economics/economics_app.tscn"
|
||||
default_mode = "insert"
|
||||
|
||||
@@ -5,7 +5,6 @@ extends ImplantApp
|
||||
## Delegates all data/rendering to OverviewScreen.
|
||||
|
||||
var _overview_screen = null # OverviewScreen
|
||||
var _current_screen_id: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -15,33 +14,10 @@ func _ready() -> void:
|
||||
|
||||
func on_install() -> void:
|
||||
_overview_screen = OverviewScreen.new()
|
||||
_overview_screen.name = "OverviewScreen"
|
||||
_overview_screen.visible = false
|
||||
add_child(_overview_screen)
|
||||
register_screen("overview", _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
|
||||
# =============================================================================
|
||||
|
||||
@@ -15,6 +15,9 @@ 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)
|
||||
@@ -81,8 +84,38 @@ func on_insert_deactivated() -> void:
|
||||
HudGroups.close_app()
|
||||
|
||||
|
||||
func _on_screen_changed(_screen_id: String) -> void:
|
||||
pass
|
||||
# --- 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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 schema_version: int = 1
|
||||
@export var app_path: String = ""
|
||||
@export var scene_path: String = ""
|
||||
@export var default_mode: String = "fullscreen"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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).
|
||||
## Does NOT reference ImplantAppManifest or ImplantApp class_names at load time
|
||||
## (autoload parse-order rule; see CLAUDE.md).
|
||||
|
||||
const CURRENT_SCHEMA_VERSION := 1
|
||||
const _MODE_MAP: Dictionary = {"gameplay": 0, "insert": 1, "fullscreen": 2}
|
||||
|
||||
var _manifests: Array = [] # Array[ImplantAppManifest]
|
||||
var _resolved_modes: Dictionary = {} # app_path -> HudGroups.Mode int
|
||||
var _instances: Dictionary = {} # app_path -> ImplantApp
|
||||
var _scanned: bool = false
|
||||
|
||||
|
||||
@@ -23,6 +25,33 @@ func get_resolved_mode(app_path: String) -> int:
|
||||
return _resolved_modes.get(app_path, HudGroups.Mode.FULLSCREEN)
|
||||
|
||||
|
||||
## Instantiate all registered apps whose manifest declares a scene_path and add
|
||||
## them as children of parent. Manifests without scene_path are metadata-only
|
||||
## and are silently skipped. Called from hud.gd._ready() — not from _ready() here.
|
||||
func instantiate_all(parent: Node) -> void:
|
||||
for m in get_manifests():
|
||||
var scene_path: String = m.scene_path
|
||||
if scene_path.is_empty():
|
||||
continue
|
||||
if not ResourceLoader.exists(scene_path):
|
||||
push_warning(
|
||||
"ImplantRegistry: scene_path not found for %s: %s" % [m.app_path, scene_path]
|
||||
)
|
||||
continue
|
||||
var packed = load(scene_path)
|
||||
if not (packed is PackedScene):
|
||||
push_warning("ImplantRegistry: %s is not a PackedScene" % scene_path)
|
||||
continue
|
||||
var instance = packed.instantiate()
|
||||
_instances[m.app_path] = instance
|
||||
parent.add_child(instance)
|
||||
|
||||
|
||||
## Return the live ImplantApp instance for app_path, or null if not instantiated.
|
||||
func get_app_instance(app_path: String): # returns ImplantApp
|
||||
return _instances.get(app_path, null)
|
||||
|
||||
|
||||
func _scan() -> void:
|
||||
_scanned = true
|
||||
_manifests.clear()
|
||||
@@ -42,9 +71,23 @@ func _scan() -> void:
|
||||
if not _is_valid_manifest(m):
|
||||
push_warning("ImplantRegistry: invalid or missing app_path in %s" % tres_path)
|
||||
else:
|
||||
var app_path: String = m.get("app_path")
|
||||
var mode_str: String = m.get("default_mode", "fullscreen")
|
||||
var default_key: int = m.get("default_key", -1)
|
||||
var app_path: String = m.app_path
|
||||
var mode_str: String = m.default_mode
|
||||
var default_key: int = m.default_key
|
||||
var schema_ver: int = m.schema_version
|
||||
# Schema version check — best-effort in both directions
|
||||
if schema_ver < CURRENT_SCHEMA_VERSION:
|
||||
print_verbose(
|
||||
"ImplantRegistry: backfilled manifest from v%d at %s" % [
|
||||
schema_ver, tres_path
|
||||
]
|
||||
)
|
||||
elif schema_ver > CURRENT_SCHEMA_VERSION:
|
||||
push_warning(
|
||||
"ImplantRegistry: manifest at %s declares schema_version %d; this build supports v%d; proceeding best-effort" % [
|
||||
tres_path, schema_ver, CURRENT_SCHEMA_VERSION
|
||||
]
|
||||
)
|
||||
if not _MODE_MAP.has(mode_str):
|
||||
push_warning(
|
||||
"ImplantRegistry: invalid default_mode \"%s\" in %s — skipping" % [
|
||||
@@ -69,5 +112,8 @@ func _scan() -> void:
|
||||
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()
|
||||
if not (m is Resource):
|
||||
return false
|
||||
if not "app_path" in m:
|
||||
return false
|
||||
return not (m.app_path as String).is_empty()
|
||||
|
||||
Reference in New Issue
Block a user