Files
settled-reach/client/ui/meta/screens/main_menu/main_menu.gd
T
jpmschweitzerandClaude Sonnet 4.6 698c5eea9b fix(client): parse-order fixes for Protocol autoload and MetaScreen extends
Protocol.gd is an autoload — replace Messagepack class_name refs with
inline load() calls via a static helper. main_menu.gd extends MetaScreen
by class_name which fails at parse time; switch to path-based extends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 09:27:22 +02:00

196 lines
6.5 KiB
GDScript

extends "res://ui/meta/meta_screen.gd"
## #258: Main menu — New Game / Continue / Load Game / Quit.
## New Game: opens character creation screen, then starts game.
## Continue: loads most recent save directory.
## Load Game: shows sorted save list for manual selection (#257).
##
## LoadingScreen lifecycle: this scene instantiates its own LoadingScreen child
## (see `_ensure_loading_screen`). main.tscn has a separate `$MetaLayer/LoadingScreen`.
## Safe today because main_menu.tscn and main.tscn never co-exist — the scene
## transition in `_start_game` replaces the tree wholesale. If that invariant
## ever changes (e.g. embedding the menu as an overlay), promote LoadingScreen
## to an autoload to enforce single-instance across the MetaStack.
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")
const SUBTITLE_COLOR := Color("#8890a0")
const BTN_NORMAL_COLOR := Color("#e8c547")
const BTN_DISABLED_COLOR := Color("#4a5060")
const FONT_SIZE_TITLE := 36
const FONT_SIZE_SUBTITLE := 14
const FONT_SIZE_BTN := 15
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
@onready var _load_game_btn: Button = $VBox/LoadGameBtn
@onready var _quit_btn: Button = $VBox/QuitBtn
@onready var _load_panel: Control = $LoadGamePanel
@onready var _saves_list: VBoxContainer = $LoadGamePanel/VBox/SavesScroll/SavesList
@onready var _load_back_btn: Button = $LoadGamePanel/VBox/BackBtn
func _ready() -> void:
_new_game_btn.pressed.connect(_on_new_game)
_continue_btn.pressed.connect(_on_continue)
_load_game_btn.pressed.connect(_on_load_game_browse)
_quit_btn.pressed.connect(_on_quit)
_load_back_btn.pressed.connect(_on_load_back)
_load_panel.visible = false
_refresh_continue_state()
func _refresh_continue_state() -> void:
var saves := SessionManager.list_game_dirs()
_continue_btn.disabled = saves.is_empty()
_load_game_btn.disabled = saves.is_empty()
func _on_new_game() -> void:
if _waiting_for_catalog:
return
var game_id := SessionManager.new_game()
if game_id.is_empty():
push_error("MainMenu: new_game() failed to create save directory — cannot start")
return
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 _process(_delta: float) -> void:
if _waiting_for_catalog:
SimBridge.poll_snapshot()
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:
GameState.pending_load_path = ""
var saves := SessionManager.list_game_dirs()
if saves.is_empty():
return
SessionManager.resume_game(saves[0].game_id)
get_tree().change_scene_to_file(GAME_SCENE)
func _on_load_game_browse() -> void:
if not _list_built:
_build_saves_list()
_list_built = true
_load_panel.visible = true
func _on_load_back() -> void:
_load_panel.visible = false
_list_built = false
func _on_quit() -> void:
get_tree().quit()
## Build the saves list panel from existing save directories, sorted by date (newest first).
func _build_saves_list() -> void:
for child in _saves_list.get_children():
child.queue_free()
var saves := SessionManager.list_game_dirs()
if saves.is_empty():
var lbl := Label.new()
lbl.text = UIStrings.get_text("menu.load_game_empty")
lbl.add_theme_color_override("font_color", Color(BTN_DISABLED_COLOR))
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_saves_list.add_child(lbl)
return
for save in saves:
var btn := Button.new()
btn.text = _format_save_entry(save)
btn.add_theme_font_size_override("font_size", FONT_SIZE_BTN)
var has_save_file: bool = not save.get("newest_save", "").is_empty()
if has_save_file:
btn.add_theme_color_override("font_color", BTN_NORMAL_COLOR)
btn.pressed.connect(_on_save_selected.bind(save))
else:
btn.add_theme_color_override("font_color", BTN_DISABLED_COLOR)
btn.disabled = true
_saves_list.add_child(btn)
func _on_save_selected(save: Dictionary) -> void:
var game_id: String = save.get("game_id", "")
var save_file: String = save.get("newest_save", "")
if save_file.is_empty():
push_error("MainMenu: save entry '%s' has no newest_save — load cancelled" % game_id)
return
SessionManager.resume_game(game_id)
GameState.pending_load_path = "user://saves/" + game_id + "/" + save_file
get_tree().change_scene_to_file(GAME_SCENE)
## Format a save entry for display. game_id format: YYYYMMDD-HHMMSS-hex6.
static func _format_save_entry(save: Dictionary) -> String:
var game_id: String = save.get("game_id", "")
var parts := game_id.split("-")
if parts.size() >= 2 and parts[0].length() == 8 and parts[1].length() == 6:
var d := parts[0]
var t := parts[1]
return (
"%s-%s-%s %s:%s:%s"
% [
d.substr(0, 4),
d.substr(4, 2),
d.substr(6, 2),
t.substr(0, 2),
t.substr(2, 2),
t.substr(4, 2),
]
)
return game_id