refactor(ui): migrate 6 meta screens to MetaScreen pattern (Workstream 2)

Relocates main_menu, character_creation, settings_dialog, debug_console,
bug_report_dialog, loading_screen from flat client/ui/ into structured
client/ui/meta/screens/<name>/. 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 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:58:24 +02:00
co-authored by Claude Opus 4.6
parent 84105916cd
commit f24d08f756
19 changed files with 86 additions and 130 deletions
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+4 -2
View File
@@ -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()
+1 -1
View File
@@ -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.
@@ -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()
@@ -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.
@@ -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()
@@ -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()
@@ -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.
@@ -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
+1 -1
View File
@@ -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"]