From f24d08f7566fa1be04d24ea286b26440e8889145 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 19 Apr 2026 21:58:24 +0200 Subject: [PATCH] refactor(ui): migrate 6 meta screens to MetaScreen pattern (Workstream 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocates main_menu, character_creation, settings_dialog, debug_console, bug_report_dialog, loading_screen from flat client/ui/ into structured client/ui/meta/screens//. All six now extend MetaScreen instead of Control; the base handles open/close lifecycle, visibility, captures_input, and — for overlays — the sim-pause contract. Screen policies set per Tyre's proposal: - settings_dialog: pauses_sim=false, PUSHES onto MetaStack - debug_console: pauses_sim=true, PUSHES (D-088 routing via base) - bug_report_dialog: pauses_sim=true, PUSHES - loading_screen: closable_by_escape=false, PUSHES - main_menu, character_creation: scene-roots, extend MetaScreen for the lifecycle contract only, do NOT push onto the stack character_creation stays at its current surface (tabs, descriptor, creation_confirmed signal unchanged). Tab consolidation and CharacterProfile migration happen in Workstreams 5 and 6. Knock-on changes: - main.tscn ModalLayer CanvasLayer renamed to MetaLayer; main.gd @onready refs updated; constants.gd comment updated; test_client_p3 and test_ui_framework_sprint15 assertions updated; test_monologue_display and .tscn header comments updated. - OPEN_MENU handler now pushes settings_dialog onto MetaStack before calling open(). Full ESC priority chain lands in Workstream 4. - atlas_app.gd: _unhandled_key_input signature widened from InputEventKey to InputEvent with an is-check, per Godot 4 API. Pre- existing narrowing was silently tolerated until main.tscn started fully instantiating under the new pattern. - test_client_p3: entity_renderer type annotations corrected from ColorRect to Sprite2D (stale since a prior refactor); facing indicator rotation assertion switched to angle_difference() for modular-safe comparison. Verification: - gdlint client/scripts/ client/ui/ — zero problems - godot --headless --path client --quit — no SCRIPT ERROR - test_client_p3: 24/24 pass - test_ui_framework_sprint15: 54/54 pass - test_implant_nav_stack: 52/52 pass - test_implant_registry: 42/42 pass - test_implant_app_lifecycle: 36/36 pass Workstream 1 foundation (84105916) remains unchanged. Workstreams 3-8 follow: protocol layer, Option A sequencing, 3-tab restructure, Bookmark tab, location picker, Skills stub. Co-Authored-By: Claude Opus 4.6 --- client/scenes/character_creation.tscn | 2 +- client/scenes/main.tscn | 10 +-- client/scenes/main_menu.tscn | 2 +- client/scripts/constants.gd | 2 +- client/scripts/main.gd | 9 +-- client/tests/test_client_p3.gd | 19 +++--- client/tests/test_monologue_display.gd | 2 +- client/tests/test_ui_framework_sprint15.gd | 6 +- client/ui/bug_report_dialog.tscn | 4 +- client/ui/debug_console.tscn | 4 +- client/ui/implant/apps/atlas/atlas_app.gd | 6 +- client/ui/loading_screen.tscn | 2 +- .../screens/bug_report}/bug_report_dialog.gd | 68 ++++++------------- .../character_creation}/character_creation.gd | 2 +- .../screens/debug_console}/debug_console.gd | 37 ++++------ .../screens/loading}/loading_screen.gd | 10 +-- .../{ => meta/screens/main_menu}/main_menu.gd | 2 +- .../screens/settings}/settings_dialog.gd | 27 ++------ client/ui/settings_dialog.tscn | 2 +- 19 files changed, 86 insertions(+), 130 deletions(-) rename client/ui/{ => meta/screens/bug_report}/bug_report_dialog.gd (95%) rename client/ui/{ => meta/screens/character_creation}/character_creation.gd (99%) rename client/ui/{ => meta/screens/debug_console}/debug_console.gd (97%) rename client/ui/{ => meta/screens/loading}/loading_screen.gd (95%) rename client/ui/{ => meta/screens/main_menu}/main_menu.gd (99%) rename client/ui/{ => meta/screens/settings}/settings_dialog.gd (97%) diff --git a/client/scenes/character_creation.tscn b/client/scenes/character_creation.tscn index 22743b56e..0b2453998 100644 --- a/client/scenes/character_creation.tscn +++ b/client/scenes/character_creation.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://char_creation_scene_sr"] -[ext_resource type="Script" path="res://ui/character_creation.gd" id="1_charcreation"] +[ext_resource type="Script" path="res://ui/meta/screens/character_creation/character_creation.gd" id="1_charcreation"] ; #705: Character creation screen — 3D preview + 5-tab customisation panel. ; SubViewport renders CharacterVisual live. Tab panel: Body/Head/Hair/Clothing/Accessories. diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index f7e9ec716..607c2b36a 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -187,17 +187,17 @@ script = ExtResource("10_cursor") ; --- Modal layer (CanvasLayer 30) --- ; Full-screen overlays: pause menu, inventory modal, death screen. -[node name="ModalLayer" type="CanvasLayer" parent="."] +[node name="MetaLayer" type="CanvasLayer" parent="."] layer = 30 ; #495: WRONG button (F12) — bug report capture dialog -[node name="BugReportDialog" parent="ModalLayer" instance=ExtResource("19_bugreport")] +[node name="BugReportDialog" parent="MetaLayer" instance=ExtResource("19_bugreport")] ; #528: Audio settings dialog — 5-bus volume sliders, ESC/OPEN_MENU to toggle -[node name="SettingsDialog" parent="ModalLayer" instance=ExtResource("21_settings")] +[node name="SettingsDialog" parent="MetaLayer" instance=ExtResource("21_settings")] ; #257: Loading screen — full-screen overlay during save/load round-trip -[node name="LoadingScreen" parent="ModalLayer" instance=ExtResource("26_loading")] +[node name="LoadingScreen" parent="MetaLayer" instance=ExtResource("26_loading")] ; #581: Debug console — tilde key toggles, bottom 40% of screen -[node name="DebugConsole" parent="ModalLayer" instance=ExtResource("27_debug_console")] +[node name="DebugConsole" parent="MetaLayer" instance=ExtResource("27_debug_console")] diff --git a/client/scenes/main_menu.tscn b/client/scenes/main_menu.tscn index 09afdc2d1..53d040653 100644 --- a/client/scenes/main_menu.tscn +++ b/client/scenes/main_menu.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://main_menu_sr"] -[ext_resource type="Script" path="res://ui/main_menu.gd" id="1_mainmenu"] +[ext_resource type="Script" path="res://ui/meta/screens/main_menu/main_menu.gd" id="1_mainmenu"] ; Main menu — New Game / Continue / Quit. ; #258: D-085 per-game save directory created on New Game. diff --git a/client/scripts/constants.gd b/client/scripts/constants.gd index e7cfdf978..0099c7423 100644 --- a/client/scripts/constants.gd +++ b/client/scripts/constants.gd @@ -40,7 +40,7 @@ const CANVAS_UI: int = 20 # CanvasLayer number for UILayer # # MODAL SCOPE (CanvasLayer 30) # Full-screen overlays: pause, inventory modal, death screen. -const CANVAS_MODAL: int = 30 # CanvasLayer number for ModalLayer +const CANVAS_MODAL: int = 30 # CanvasLayer number for MetaLayer # # Rendering ceiling: 10 floors (25m) above current floor. # Above this: no sprites, ground shadows + environmental effects only. diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 488763b15..0c51f267f 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -30,10 +30,10 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers @onready var examine_display = $InsertOverlay/ExamineDisplay # #174: examine result overlay @onready var journal_panel = $InsertOverlay/JournalPanel # #264: knowledge journal (D-041) @onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay -@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button -@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU) -@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load -@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console +@onready var bug_report_dialog = $MetaLayer/BugReportDialog # #495: F12 WRONG button +@onready var settings_dialog = $MetaLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU) +@onready var loading_screen = $MetaLayer/LoadingScreen # #257: blocking overlay during load +@onready var debug_console = $MetaLayer/DebugConsole # #581: tilde debug console @onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7) @@ -271,6 +271,7 @@ func _process(delta: float) -> void: if settings_dialog.is_open(): settings_dialog.close() else: + MetaStack.push(settings_dialog) settings_dialog.open() continue if input.action == InputMapper.Action.INTERACT: diff --git a/client/tests/test_client_p3.gd b/client/tests/test_client_p3.gd index f8f14d1cb..e7e4c99d7 100644 --- a/client/tests/test_client_p3.gd +++ b/client/tests/test_client_p3.gd @@ -124,7 +124,7 @@ func test_z_ui_layer_above_world() -> void: var inst := _make_scene() var ui_layer = inst.get_node("UILayer") as CanvasLayer var insert_layer = inst.get_node("InsertOverlay") as CanvasLayer - var modal_layer = inst.get_node("ModalLayer") as CanvasLayer + var modal_layer = inst.get_node("MetaLayer") as CanvasLayer assert_that(insert_layer.layer).override_failure_message( "InsertOverlay must be CanvasLayer %d" % Constants.CANVAS_INSERT ).is_equal(Constants.CANVAS_INSERT) @@ -132,13 +132,13 @@ func test_z_ui_layer_above_world() -> void: "UILayer must be CanvasLayer %d" % Constants.CANVAS_UI ).is_equal(Constants.CANVAS_UI) assert_that(modal_layer.layer).override_failure_message( - "ModalLayer must be CanvasLayer %d" % Constants.CANVAS_MODAL + "MetaLayer must be CanvasLayer %d" % Constants.CANVAS_MODAL ).is_equal(Constants.CANVAS_MODAL) assert_that(ui_layer.layer > insert_layer.layer).override_failure_message( "UILayer must render above InsertOverlay" ).is_true() assert_that(modal_layer.layer > ui_layer.layer).override_failure_message( - "ModalLayer must render above UILayer" + "MetaLayer must render above UILayer" ).is_true() @@ -168,7 +168,7 @@ func test_entity_lerp_moves_toward_target() -> void: var entity := [{"entity_id": 11, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}}] renderer.update_entities(entity) - var node: ColorRect = renderer.entity_nodes[11] + var node: Sprite2D = renderer.entity_nodes[11] var start_pos: Vector2 = node.position # Move target to (6, 5) var entity_moved := [{"entity_id": 11, "x": 6.0, "y": 5.0, "z": 0, @@ -212,7 +212,7 @@ func test_entity_lerp_converges_within_300ms() -> void: # Simulate 0.3s at 60fps (18 frames × 0.016s ≈ 0.288s) for i in 20: renderer._process(0.016) - var final_node: ColorRect = renderer.entity_nodes[12] + var final_node: Sprite2D = renderer.entity_nodes[12] var final_pos: Vector2 = final_node.position # Should be within 5% of target (97% convergence at 0.3s) var dist: float = final_pos.distance_to(target) @@ -272,9 +272,10 @@ func test_facing_indicator_rotation_matches_input_mapper_angle() -> void: for angle in angles: InputMapper.facing_angle = angle renderer.update_entities(entity) - assert_that(indicator.rotation).override_failure_message( - "angle %.3f: expected rotation %.3f, got %.3f" % [angle, angles[angle], indicator.rotation] - ).is_equal_approx(angles[angle], 0.001) + var diff := absf(angle_difference(indicator.rotation, angles[angle])) + assert_that(diff).override_failure_message( + "angle %.3f: expected rotation %.3f, got %.3f (diff %.4f)" % [angle, angles[angle], indicator.rotation, diff] + ).is_less_equal(0.001) InputMapper.facing_angle = -PI / 2.0 # Reset to default renderer.queue_free() @@ -292,7 +293,7 @@ func test_lerp_weight_increases_with_delta() -> void: "kind": {"variant": "Npc", "data": null}}] renderer.update_entities(entity_moved) # Small delta step - var small_node: ColorRect = renderer.entity_nodes[20] + var small_node: Sprite2D = renderer.entity_nodes[20] var small_start: float = small_node.position.x renderer._process(0.008) var small_progress: float = small_node.position.x - small_start diff --git a/client/tests/test_monologue_display.gd b/client/tests/test_monologue_display.gd index 151f5fb77..4eee39917 100644 --- a/client/tests/test_monologue_display.gd +++ b/client/tests/test_monologue_display.gd @@ -472,7 +472,7 @@ func test_monologue_display_parented_to_canvas_layer_20_in_main_scene() -> void: ## D-049: Structural verification — MonologueDisplay must be a direct child of ## UILayer (CanvasLayer, layer=20) in the live scene tree, not the world layer. ## Catches regressions where the node gets accidentally moved to InsertOverlay - ## (layer=10) or ModalLayer (layer=30), or dropped into the world z-stack. + ## (layer=10) or MetaLayer (layer=30), or dropped into the world z-stack. ## ## Scene path verified: Game/UILayer/MonologueDisplay (main.tscn line 141). if not ResourceLoader.exists("res://scenes/main.tscn"): diff --git a/client/tests/test_ui_framework_sprint15.gd b/client/tests/test_ui_framework_sprint15.gd index 938aa98f3..f1164d1bb 100644 --- a/client/tests/test_ui_framework_sprint15.gd +++ b/client/tests/test_ui_framework_sprint15.gd @@ -66,13 +66,13 @@ func test_ui_layer_is_canvas_layer_20() -> void: func test_modal_layer_is_canvas_layer_30() -> void: - # D-049: ModalLayer = pause/inventory modal scope = CanvasLayer 30. + # D-049: MetaLayer = pause/inventory modal scope = CanvasLayer 30. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) - var modal_layer: CanvasLayer = _instance.get_node("ModalLayer") + var modal_layer: CanvasLayer = _instance.get_node("MetaLayer") assert_that(modal_layer).is_not_null() assert_that(modal_layer.layer).is_equal(Constants.CANVAS_MODAL) @@ -83,7 +83,7 @@ func test_ui_layer_above_insert_overlay() -> void: func test_modal_layer_above_ui_layer() -> void: - # D-049: ModalLayer (30) must render above UILayer (20). + # D-049: MetaLayer (30) must render above UILayer (20). assert_that(Constants.CANVAS_MODAL).is_greater(Constants.CANVAS_UI) diff --git a/client/ui/bug_report_dialog.tscn b/client/ui/bug_report_dialog.tscn index b4e8c8cab..cf1758a3e 100644 --- a/client/ui/bug_report_dialog.tscn +++ b/client/ui/bug_report_dialog.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://ui/bug_report_dialog.gd" id="1_bugreport"] +[ext_resource type="Script" path="res://ui/meta/screens/bug_report/bug_report_dialog.gd" id="1_bugreport"] -; #495: WRONG button (F12) — bug report capture dialog, ModalLayer +; #495: WRONG button (F12) — bug report capture dialog, MetaLayer [node name="BugReportDialog" type="Control"] layout_mode = 3 anchors_preset = 15 diff --git a/client/ui/debug_console.tscn b/client/ui/debug_console.tscn index 934729983..ee58b367e 100644 --- a/client/ui/debug_console.tscn +++ b/client/ui/debug_console.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=2 format=3 uid="uid://b2ndm9rvx8cqp"] -[ext_resource type="Script" uid="uid://c8pvt3xr7kmd2" path="res://ui/debug_console.gd" id="1_debug_console"] +[ext_resource type="Script" path="res://ui/meta/screens/debug_console/debug_console.gd" id="1_debug_console"] -; #581: In-game debug console. Tilde key toggles. ModalLayer. +; #581: In-game debug console. Tilde key toggles. MetaLayer. ; UI built programmatically in _ready() — scene contains only root node + script. [node name="DebugConsole" type="Control"] layout_mode = 3 diff --git a/client/ui/implant/apps/atlas/atlas_app.gd b/client/ui/implant/apps/atlas/atlas_app.gd index aaf8ff8d2..ad40d4b10 100644 --- a/client/ui/implant/apps/atlas/atlas_app.gd +++ b/client/ui/implant/apps/atlas/atlas_app.gd @@ -54,14 +54,16 @@ func on_open(_mode: int) -> void: _reach_screen.refresh_info_panel_visibility() -func _unhandled_key_input(event: InputEventKey) -> void: +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 if current_screen_id() == "regional": return # AtlasViewer handles its own keyboard input - _handle_key(event) + _handle_key(event as InputEventKey) get_viewport().set_input_as_handled() diff --git a/client/ui/loading_screen.tscn b/client/ui/loading_screen.tscn index 388e82640..fa01fbdbf 100644 --- a/client/ui/loading_screen.tscn +++ b/client/ui/loading_screen.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://b7rv9mkl4qpw3"] -[ext_resource type="Script" path="res://ui/loading_screen.gd" id="1_loading"] +[ext_resource type="Script" path="res://ui/meta/screens/loading/loading_screen.gd" id="1_loading"] ; #257: Loading screen — full-screen overlay shown during save/load round-trip. ; Blocks input; dismissed when save_result arrives from server. diff --git a/client/ui/bug_report_dialog.gd b/client/ui/meta/screens/bug_report/bug_report_dialog.gd similarity index 95% rename from client/ui/bug_report_dialog.gd rename to client/ui/meta/screens/bug_report/bug_report_dialog.gd index b9110b1b8..56c2bced9 100644 --- a/client/ui/bug_report_dialog.gd +++ b/client/ui/meta/screens/bug_report/bug_report_dialog.gd @@ -1,4 +1,4 @@ -extends Control +extends MetaScreen ## #507: WRONG button — full 60-tick capture: ring buffer, snapshot history, replay seed. ## Upgrade of the Sprint 9 MVP (#495). @@ -36,7 +36,6 @@ const PADDING := 16 const RING_SIZE := 60 var _line_edit: LineEdit = null -var _active: bool = false var _captured_screenshot: Image = null # #507: Pre-allocated ring buffers (no per-tick allocation after _ready). @@ -56,8 +55,9 @@ var _snapshot_count: int = 0 func _ready() -> void: + pauses_sim = true visible = false - mouse_filter = Control.MOUSE_FILTER_STOP + mouse_filter = Control.MOUSE_FILTER_IGNORE # Pre-allocate ring buffers — resize then fill sentinels. # The ring array itself never grows after _ready. Each write replaces the GDScript @@ -208,25 +208,15 @@ func _get_filled_snapshot_count() -> int: func start_capture() -> void: - if _active: + if is_open(): return # Capture screenshot BEFORE showing the dialog overlay _captured_screenshot = get_viewport().get_texture().get_image() - _active = true - visible = true + MetaStack.push(self) + open() - # Pause the simulation - ( - SimBridge - . send_input( - { - "action": InputMapper.Action.PAUSE, - "timestamp_msec": Time.get_ticks_msec(), - } - ) - ) - # Create the LineEdit dynamically +func on_open() -> void: _line_edit = LineEdit.new() _line_edit.placeholder_text = "Describe the issue..." _line_edit.size = Vector2(BOX_WIDTH - PADDING * 2, 30) @@ -240,41 +230,23 @@ func start_capture() -> void: _line_edit.grab_focus() -func _on_text_submitted(text: String) -> void: - _save_report(text) - _close() - capture_completed.emit() - - -func _unhandled_input(event: InputEvent) -> void: - if not _active: - return - if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: - _close() - capture_cancelled.emit() - get_viewport().set_input_as_handled() - - -func _close() -> void: - _active = false - visible = false +func on_close() -> void: if _line_edit: _line_edit.release_focus() _line_edit.queue_free() _line_edit = null - _captured_screenshot = null - # Unpause the simulation - ( - SimBridge - . send_input( - { - "action": InputMapper.Action.UNPAUSE, - "timestamp_msec": Time.get_ticks_msec(), - } - ) - ) + +func on_escape() -> bool: + capture_cancelled.emit() + return false # let MetaStack close + + +func _on_text_submitted(text: String) -> void: + _save_report(text) + close() + capture_completed.emit() func _save_report(description: String) -> void: @@ -456,7 +428,7 @@ func _render_snapshot_text() -> String: func _draw() -> void: - if not _active: + if not is_open(): return var viewport_size := get_viewport_rect().size # Full-screen dim @@ -489,4 +461,4 @@ func _draw() -> void: func is_active() -> bool: - return _active + return is_open() diff --git a/client/ui/character_creation.gd b/client/ui/meta/screens/character_creation/character_creation.gd similarity index 99% rename from client/ui/character_creation.gd rename to client/ui/meta/screens/character_creation/character_creation.gd index 56c8829a5..27cee6e39 100644 --- a/client/ui/character_creation.gd +++ b/client/ui/meta/screens/character_creation/character_creation.gd @@ -1,6 +1,6 @@ # gdlint:disable=max-file-lines class_name CharacterCreation -extends Control +extends MetaScreen ## #705: Character creation screen. ## Live 3D preview via SubViewport + 5-tab customisation panel (Body/Head/Hair/Clothing/Accessories). ## Emits creation_confirmed(descriptor) on start, creation_cancelled on back. diff --git a/client/ui/debug_console.gd b/client/ui/meta/screens/debug_console/debug_console.gd similarity index 97% rename from client/ui/debug_console.gd rename to client/ui/meta/screens/debug_console/debug_console.gd index ffcbbd6cd..c2166a646 100644 --- a/client/ui/debug_console.gd +++ b/client/ui/meta/screens/debug_console/debug_console.gd @@ -1,5 +1,5 @@ class_name DebugConsole -extends Control +extends MetaScreen ## In-game debug console (#581). Tilde key (`) toggles open/closed. ## Semi-transparent panel anchored to bottom ~40% of screen. @@ -23,7 +23,6 @@ const ERROR_COLOR := Color("#d45d5d") const INPUT_COLOR := Color("#e8c547") var _enabled: bool = true -var _open: bool = false var _log_lines: Array[String] = [] var _panel: PanelContainer = null var _output_log: RichTextLabel = null @@ -33,6 +32,7 @@ var _history_idx: int = -1 func _ready() -> void: + pauses_sim = true _load_prefs() visible = false mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -107,11 +107,11 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() _toggle() return - if _open: + if is_open(): # Consume all keyboard events — prevent movement/action leaking through get_viewport().set_input_as_handled() if event.keycode == KEY_ESCAPE: - _close() + close() func _on_input_key(event: InputEvent) -> void: @@ -126,34 +126,26 @@ func _on_input_key(event: InputEvent) -> void: func _toggle() -> void: - if _open: - _close() + if is_open(): + close() else: - _open_console() + MetaStack.push(self) + open() -func _open_console() -> void: - _open = true - visible = true - mouse_filter = Control.MOUSE_FILTER_STOP +func on_open() -> void: _input_line.clear() _input_line.grab_focus() _history_idx = -1 pause_requested.emit() # D-088: pause sim while typing debug commands -func _close() -> void: - _open = false - visible = false +func on_close() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE _input_line.release_focus() unpause_requested.emit() # D-088: resume sim when console closes -func is_open() -> bool: - return _open - - # -- Command input -- @@ -422,8 +414,9 @@ func append_response(response: Dictionary) -> void: var text: String = response.get("text", "") var color := SUCCESS_COLOR if success else ERROR_COLOR _append_text(text, color) - if not _open and _enabled: - _open_console() + if not is_open() and _enabled: + MetaStack.push(self) + open() # -- Log rendering -- @@ -492,8 +485,8 @@ func _history_down() -> void: func set_enabled(enabled: bool) -> void: _enabled = enabled - if not _enabled and _open: - _close() + if not _enabled and is_open(): + close() _save_prefs() diff --git a/client/ui/loading_screen.gd b/client/ui/meta/screens/loading/loading_screen.gd similarity index 95% rename from client/ui/loading_screen.gd rename to client/ui/meta/screens/loading/loading_screen.gd index 8a149f368..c7f343595 100644 --- a/client/ui/loading_screen.gd +++ b/client/ui/meta/screens/loading/loading_screen.gd @@ -1,4 +1,4 @@ -extends Control +extends MetaScreen ## #257: Loading screen overlay — blocks input during save/load round-trip. ## Shown when LOAD_GAME fires; hidden when save_result arrives (success or failure). ## Full-screen, dark overlay with centered status text. @@ -15,8 +15,9 @@ var _version_label: Label = null func _ready() -> void: + closable_by_escape = false visible = false - mouse_filter = Control.MOUSE_FILTER_STOP + mouse_filter = Control.MOUSE_FILTER_IGNORE set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _build_ui() @@ -76,9 +77,10 @@ func _read_client_version() -> String: func show_loading() -> void: - visible = true + MetaStack.push(self) + open() ## Hide the loading overlay. success=false is reserved for future failure-state UI. func hide_loading(_success: bool = true) -> void: - visible = false + close() diff --git a/client/ui/main_menu.gd b/client/ui/meta/screens/main_menu/main_menu.gd similarity index 99% rename from client/ui/main_menu.gd rename to client/ui/meta/screens/main_menu/main_menu.gd index d578c3114..f861851bc 100644 --- a/client/ui/main_menu.gd +++ b/client/ui/meta/screens/main_menu/main_menu.gd @@ -1,4 +1,4 @@ -extends Control +extends MetaScreen ## #258: Main menu — New Game / Continue / Load Game / Quit. ## New Game: opens character creation screen, then starts game. ## Continue: loads most recent save directory. diff --git a/client/ui/settings_dialog.gd b/client/ui/meta/screens/settings/settings_dialog.gd similarity index 97% rename from client/ui/settings_dialog.gd rename to client/ui/meta/screens/settings/settings_dialog.gd index 1583dcd5f..f06cdc8cb 100644 --- a/client/ui/settings_dialog.gd +++ b/client/ui/meta/screens/settings/settings_dialog.gd @@ -1,4 +1,4 @@ -extends Control +extends MetaScreen ## #528: Audio settings dialog — 5-bus volume sliders. ## #646: AI-Enhanced Dialogue toggle + hardware detection status (D-138). @@ -6,7 +6,6 @@ extends Control ## Volumes persist via AudioManager._save_prefs() on each slider change. ## AI Dialogue toggle persists via ConfigFile (client-local) + ChangeSettings IPC (server SQLite). -signal closed signal debug_console_toggled(enabled: bool) # #581: debug console enabled/disabled signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled @@ -37,7 +36,6 @@ const BUS_ROWS: Array = [ ["UI Sounds", "UISounds"], ] -var _active: bool = false var _container: VBoxContainer = null # #646: AI Dialogue hardware status and toggle node ref — used by testable API methods @@ -49,30 +47,17 @@ var _ai_battery_warning_label: Label = null # shown when on battery; toggle sta func _ready() -> void: visible = false - mouse_filter = Control.MOUSE_FILTER_STOP + mouse_filter = Control.MOUSE_FILTER_IGNORE -func open() -> void: - if _active: - return - _active = true - visible = true +func on_open() -> void: _build_ui() queue_redraw() -func close() -> void: - if not _active: - return - _active = false - visible = false +func on_close() -> void: _destroy_ui() queue_redraw() - closed.emit() - - -func is_open() -> bool: - return _active func _build_ui() -> void: @@ -139,7 +124,7 @@ func _build_ui() -> void: var debug_check := CheckButton.new() # Query live DebugConsole node if available; fall back to prefs file - var console_node := get_node_or_null("/root/Main/ModalLayer/DebugConsole") + var console_node := get_node_or_null("/root/Main/MetaLayer/DebugConsole") if console_node and console_node.has_method("is_enabled"): debug_check.button_pressed = console_node.is_enabled() else: @@ -351,7 +336,7 @@ func _save_ai_pref(enabled: bool) -> void: func _draw() -> void: - if not _active: + if not is_open(): return var viewport_size := get_viewport_rect().size diff --git a/client/ui/settings_dialog.tscn b/client/ui/settings_dialog.tscn index 77cf97319..571aa4e7c 100644 --- a/client/ui/settings_dialog.tscn +++ b/client/ui/settings_dialog.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://ui/settings_dialog.gd" id="1_settings"] +[ext_resource type="Script" path="res://ui/meta/screens/settings/settings_dialog.gd" id="1_settings"] ; #528: Audio settings dialog — 5-bus volume sliders, OPEN_MENU (ESC) to toggle [node name="SettingsDialog" type="Control"]