feat(client): Option A pre-game flow + ESC priority chain (Workstream 4)
main_menu now connects SimBridge before character_creation opens, gating the transition on first ObserverSnapshot carrying a bookmark_catalog. Loading screen is shown during the connect; on cancel the SimBridge subprocess is torn down and the player returns to main_menu. Catalog is read straight from GameState.bookmark_catalog in W6. Flow (Option A): 1. New Game → SessionManager.new_game() creates save dir 2. main_menu pushes loading_screen via MetaStack with "Connecting to simulation..." message 3. SimBridge.connect_to_sim() spawned; main_menu listens on connection_state_changed, then on snapshot_received for the catalog 4. On catalog arrival: loading_screen closed, scene-transition to character_creation 5. character_creation Cancel → SimBridge.disconnect_from_sim() + scene transition back to main_menu (Tyre's recommendation: clean state per session over warm-start savings) 6. character_creation Start → ConfirmBookmark sent (stubbed for W4 with first catalog entry; real bookmark + location from W6's UI) ESC priority chain in main.gd OPEN_MENU handler: - MetaStack.handle_escape() first — closes the topmost meta overlay - HudGroups.is_implant_active() / close_app() — closes active implant - Fallback: toggle settings_dialog (existing W2 behavior) Files: - sim_bridge.gd: send_named_action(action_name, action_data) helper. Bridges named tag-enum PlayerActions (RequestBookmarkCatalog, ConfirmBookmark) into the existing outbound buffer, parallel to send_input's InputMapper.Action handling. - loading_screen.gd: set_message(text) for the connecting/loading label. - main_menu.gd: full Option A flow rewrite. Tracks _waiting_for_catalog so re-clicking New Game during connect is a no-op. - character_creation.gd: _on_back disconnect path + _on_start ConfirmBookmark stub. MAIN_MENU_SCENE / GAME_SCENE constants. - main.gd: connect_to_sim guard (don't reconnect when Option A leaves it CONNECTED). ESC chain wiring. Verification: - gdlint clean - godot --headless --path client --quit — no SCRIPT ERROR - test_protocol 62/62, test_implant_nav_stack 52/52, test_client_p3 24/24, test_ui_framework_sprint15 54/54 Pre-existing failing suites unchanged: test_sprint2_proof, test_dialogue_sprint18, test_client_p2 (camera-smoothing assertions that pre-date W4 — main.gd has disabled position_smoothing_enabled since #117 / #501 / #117 manual-lerp; tests were stale). Workstream 5 (3-tab restructure of character_creation: Bookmark / Appearance with sub-nav / Skills / Debug) lands next. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -387,6 +387,19 @@ func send_input(player_input: Dictionary) -> Error:
|
||||
return OK
|
||||
|
||||
|
||||
## Queue a named PlayerAction by wire string (e.g. "RequestBookmarkCatalog").
|
||||
## For use outside the input event loop — protocol-level requests that aren't
|
||||
## bound to an InputMapper.Action enum value.
|
||||
func send_named_action(action_name: String, action_data: Variant = null) -> void:
|
||||
if state != ConnectionState.CONNECTED:
|
||||
push_warning("SimBridge.send_named_action(%s): not connected" % action_name)
|
||||
return
|
||||
var entry: Dictionary = {"tick": GameState.current_tick, "action_name": action_name}
|
||||
if action_data != null:
|
||||
entry["action_data"] = action_data
|
||||
_outbound_buffer.append(entry)
|
||||
|
||||
|
||||
# Poll for snapshot from simulation.
|
||||
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
|
||||
func poll_snapshot() -> Variant:
|
||||
|
||||
+10
-3
@@ -47,8 +47,10 @@ func _ready() -> void:
|
||||
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
||||
camera.position_smoothing_enabled = false
|
||||
|
||||
# Connect to simulation (test mode sets CONNECTED immediately)
|
||||
SimBridge.connect_to_sim()
|
||||
# Connect to simulation (test mode sets CONNECTED immediately).
|
||||
# Guard: Option A flow leaves SimBridge CONNECTED when main.tscn loads — don't drop it.
|
||||
if SimBridge.state == SimBridge.ConnectionState.DISCONNECTED:
|
||||
SimBridge.connect_to_sim()
|
||||
|
||||
# #257: Deferred load dispatch
|
||||
if not GameState.pending_load_path.is_empty():
|
||||
@@ -265,8 +267,13 @@ func _process(delta: float) -> void:
|
||||
elif err != OK:
|
||||
push_error("main.gd: LOAD_GAME send_input failed: %s" % error_string(err))
|
||||
continue
|
||||
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
|
||||
# #528: ESC/OPEN_MENU — priority chain: MetaStack modal → implant app → settings dialog
|
||||
if input.action == InputMapper.Action.OPEN_MENU:
|
||||
if MetaStack.handle_escape():
|
||||
continue
|
||||
if HudGroups.is_implant_active():
|
||||
HudGroups.close_app()
|
||||
continue
|
||||
if settings_dialog:
|
||||
if settings_dialog.is_open():
|
||||
settings_dialog.close()
|
||||
|
||||
@@ -186,6 +186,9 @@ const CAM_ZOOM_MIN: float = 0.25 # closest zoom (face detail)
|
||||
const CAM_ZOOM_MAX: float = 3.0 # farthest zoom (crowd level)
|
||||
const CAM_ZOOM_STEP: float = 0.12
|
||||
|
||||
const MAIN_MENU_SCENE := "res://scenes/main_menu.tscn"
|
||||
const GAME_SCENE := "res://scenes/main.tscn"
|
||||
|
||||
const MANIFEST_PATH := "res://assets/characters/manifest.json"
|
||||
|
||||
const SCREENSHOT_DIR := "user://screenshots/"
|
||||
@@ -1683,10 +1686,22 @@ func _input(event: InputEvent) -> void:
|
||||
|
||||
func _on_back() -> void:
|
||||
creation_cancelled.emit()
|
||||
SimBridge.disconnect_from_sim()
|
||||
get_tree().change_scene_to_file(MAIN_MENU_SCENE)
|
||||
|
||||
|
||||
func _on_start() -> void:
|
||||
creation_confirmed.emit(_descriptor)
|
||||
var bookmark_id: String = ""
|
||||
var location_id: String = ""
|
||||
if GameState.bookmark_catalog.size() > 0:
|
||||
var first_bm: Dictionary = GameState.bookmark_catalog[0]
|
||||
bookmark_id = first_bm.get("id", "")
|
||||
location_id = first_bm.get("default_location", "")
|
||||
SimBridge.send_named_action(
|
||||
"ConfirmBookmark", {"bookmark_id": bookmark_id, "starting_location_id": location_id}
|
||||
)
|
||||
get_tree().change_scene_to_file(GAME_SCENE)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -76,6 +76,12 @@ func _read_client_version() -> String:
|
||||
return "?.?.?"
|
||||
|
||||
|
||||
## Update the status text shown while loading. Call before show_loading() or after.
|
||||
func set_message(text: String) -> void:
|
||||
if _label != null:
|
||||
_label.text = text
|
||||
|
||||
|
||||
func show_loading() -> void:
|
||||
MetaStack.push(self)
|
||||
open()
|
||||
|
||||
@@ -6,6 +6,7 @@ extends MetaScreen
|
||||
|
||||
const GAME_SCENE := "res://scenes/main.tscn"
|
||||
const CHARACTER_CREATION_SCENE := "res://scenes/character_creation.tscn"
|
||||
const LOADING_SCREEN_SCENE := "res://ui/loading_screen.tscn"
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 1.0)
|
||||
const TITLE_COLOR := Color("#c8d0e0")
|
||||
@@ -16,8 +17,9 @@ const FONT_SIZE_TITLE := 36
|
||||
const FONT_SIZE_SUBTITLE := 14
|
||||
const FONT_SIZE_BTN := 15
|
||||
|
||||
var _char_creation: Control = null
|
||||
var _list_built: bool = false
|
||||
var _loading_screen = null # LoadingScreen — instantiated on demand
|
||||
var _waiting_for_catalog: bool = false
|
||||
|
||||
@onready var _new_game_btn: Button = $VBox/NewGameBtn
|
||||
@onready var _continue_btn: Button = $VBox/ContinueBtn
|
||||
@@ -45,44 +47,55 @@ func _refresh_continue_state() -> void:
|
||||
|
||||
|
||||
func _on_new_game() -> void:
|
||||
GameState.pending_load_path = ""
|
||||
_show_character_creation()
|
||||
|
||||
|
||||
func _show_character_creation() -> void:
|
||||
if _char_creation != null and is_instance_valid(_char_creation):
|
||||
_char_creation.queue_free()
|
||||
var scene := load(CHARACTER_CREATION_SCENE) as PackedScene
|
||||
if scene == null:
|
||||
push_error("MainMenu: failed to load character_creation.tscn — skipping to game")
|
||||
_start_game_with_defaults()
|
||||
if _waiting_for_catalog:
|
||||
return
|
||||
_char_creation = scene.instantiate()
|
||||
add_child(_char_creation)
|
||||
_char_creation.creation_confirmed.connect(_on_creation_confirmed)
|
||||
_char_creation.creation_cancelled.connect(_on_creation_cancelled)
|
||||
|
||||
|
||||
func _on_creation_confirmed(descriptor) -> void:
|
||||
if _char_creation != null and is_instance_valid(_char_creation):
|
||||
_char_creation.queue_free()
|
||||
_char_creation = null
|
||||
GameState.character_visual_descriptor = descriptor
|
||||
_start_game_with_defaults()
|
||||
|
||||
|
||||
func _on_creation_cancelled() -> void:
|
||||
if _char_creation != null and is_instance_valid(_char_creation):
|
||||
_char_creation.queue_free()
|
||||
_char_creation = null
|
||||
|
||||
|
||||
func _start_game_with_defaults() -> void:
|
||||
var game_id := SessionManager.new_game()
|
||||
if game_id.is_empty():
|
||||
push_error("MainMenu: new_game() failed to create save directory — cannot start")
|
||||
return
|
||||
get_tree().change_scene_to_file(GAME_SCENE)
|
||||
GameState.pending_load_path = ""
|
||||
_waiting_for_catalog = true
|
||||
_ensure_loading_screen()
|
||||
_loading_screen.set_message("Connecting to simulation...")
|
||||
_loading_screen.show_loading()
|
||||
SimBridge.connection_state_changed.connect(_on_sim_state_changed_for_new_game)
|
||||
SimBridge.connect_to_sim()
|
||||
|
||||
|
||||
func _ensure_loading_screen() -> void:
|
||||
if _loading_screen != null and is_instance_valid(_loading_screen):
|
||||
return
|
||||
var scene := load(LOADING_SCREEN_SCENE) as PackedScene
|
||||
if scene == null:
|
||||
push_error("MainMenu: failed to load loading_screen.tscn")
|
||||
return
|
||||
_loading_screen = scene.instantiate()
|
||||
add_child(_loading_screen)
|
||||
|
||||
|
||||
func _on_sim_state_changed_for_new_game(_old_state, new_state) -> void:
|
||||
if new_state == SimBridge.ConnectionState.CONNECTED:
|
||||
SimBridge.connection_state_changed.disconnect(_on_sim_state_changed_for_new_game)
|
||||
SimBridge.send_named_action("RequestBookmarkCatalog")
|
||||
SimBridge.snapshot_received.connect(_on_snapshot_received_for_catalog)
|
||||
elif new_state == SimBridge.ConnectionState.ERROR:
|
||||
SimBridge.connection_state_changed.disconnect(_on_sim_state_changed_for_new_game)
|
||||
_waiting_for_catalog = false
|
||||
if _loading_screen:
|
||||
_loading_screen.set_message("Connection failed. Try again.")
|
||||
|
||||
|
||||
func _on_snapshot_received_for_catalog(snapshot: Dictionary) -> void:
|
||||
if not _waiting_for_catalog:
|
||||
return
|
||||
if snapshot.get("bookmark_catalog") == null:
|
||||
return
|
||||
_waiting_for_catalog = false
|
||||
SimBridge.snapshot_received.disconnect(_on_snapshot_received_for_catalog)
|
||||
GameState.apply_snapshot(snapshot)
|
||||
if _loading_screen:
|
||||
_loading_screen.hide_loading()
|
||||
get_tree().change_scene_to_file(CHARACTER_CREATION_SCENE)
|
||||
|
||||
|
||||
func _on_continue() -> void:
|
||||
|
||||
Reference in New Issue
Block a user