extends Control ## #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). const GAME_SCENE := "res://scenes/main.tscn" const CHARACTER_CREATION_SCENE := "res://scenes/character_creation.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 _char_creation: Control = null var _list_built: 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: 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() 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) 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