extends Control ## Главное меню GWorld. ## ## Визуал собран из трёх частей: анимированный фон (shaders/menu_background.gdshader), ## тема кнопок (themes/main_menu.theme.tres) и этот скрипт — появление, наведение ## и вызов экранов настроек, сохранений и «Об игре». ## Сцена, в которую уходим по «Новая игра». @export var game_scene: PackedScene ## Игрок нажал «Продолжить» — до того, как начнётся загрузка слота. signal continue_requested ## Игрок нажал «Новая игра». signal new_game_requested const INTRO_TIME := 0.5 const HOVER_SCALE := 1.035 @onready var _saves: SaveManagerService = get_node("/root/SaveManager") @onready var _header: Control = $Content/Column/Header @onready var _menu: VBoxContainer = $Content/Column/Menu @onready var _footer: Control = $Footer @onready var _continue_button: Button = $Content/Column/Menu/ContinueButton @onready var _load_button: Button = $Content/Column/Menu/LoadButton @onready var _settings_screen: Control = $SettingsScreen @onready var _save_screen: SaveMenuScreen = $SaveScreen @onready var _about_overlay: Control = $AboutOverlay func _ready() -> void: # В меню время не идёт: сессия считается только внутри игровой сцены. _saves.end_session() var has_saves := _saves.has_any_save() _continue_button.visible = has_saves _load_button.visible = has_saves _wire_menu_buttons() _settings_screen.closed.connect(_focus_first_button) _save_screen.closed.connect(_focus_first_button) var about_back: Button = $AboutOverlay/Center/Card/Body/Actions/AboutBackButton about_back.pressed.connect(_close_about) await _play_intro() _focus_first_button() func _unhandled_input(event: InputEvent) -> void: if not _about_overlay.visible or not event.is_action_pressed("ui_cancel"): return _close_about() get_viewport().set_input_as_handled() # --- Меню ------------------------------------------------------------------- func _wire_menu_buttons() -> void: for button: Button in _menu.get_children(): # Мышь и клавиатура делят одну подсветку: наведение = фокус. button.mouse_entered.connect(button.grab_focus) button.focus_entered.connect(_on_button_focus.bind(button, true)) button.focus_exited.connect(_on_button_focus.bind(button, false)) button.pressed.connect(_on_menu_pressed.bind(button.name)) func _on_button_focus(button: Button, focused: bool) -> void: button.pivot_offset = Vector2(0.0, button.size.y * 0.5) var target := Vector2.ONE * (HOVER_SCALE if focused else 1.0) var tween := create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) tween.tween_property(button, "scale", target, 0.18) func _on_menu_pressed(button_name: StringName) -> void: match String(button_name): "ContinueButton": continue_requested.emit() _continue_latest() "NewGameButton": new_game_requested.emit() _start_game() "LoadButton": _save_screen.open(SaveMenuScreen.Mode.LOAD) "SettingsButton": _settings_screen.open() "AboutButton": _open_about() "QuitButton": _quit() func _focus_first_button() -> void: for button: Button in _menu.get_children(): if button.visible: button.grab_focus() return func _continue_latest() -> void: var slot := _saves.latest_slot() if slot < 0: return # Не ждём: смена сцены уничтожит это меню вместе со старым деревом. _saves.load_slot(slot) func _start_game() -> void: if game_scene == null: push_warning("MainMenu: game_scene не назначена — назначьте её в инспекторе.") return get_tree().change_scene_to_packed(game_scene) func _quit() -> void: var tween := create_tween() tween.tween_property(self, "modulate:a", 0.0, 0.25) await tween.finished get_tree().quit() # --- «Об игре» -------------------------------------------------------------- func _open_about() -> void: _about_overlay.modulate.a = 0.0 _about_overlay.visible = true # Контейнеры раскладываются только после того, как узел стал видимым. await get_tree().process_frame var card: Control = _about_overlay.get_node("Center/Card") card.pivot_offset = card.size * 0.5 card.scale = Vector2(0.96, 0.96) var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) tween.tween_property(_about_overlay, "modulate:a", 1.0, 0.18) tween.tween_property(card, "scale", Vector2.ONE, 0.22) var back_button: Button = card.get_node("Body/Actions").get_child(0) back_button.grab_focus() func _close_about() -> void: var tween := create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_CUBIC) tween.tween_property(_about_overlay, "modulate:a", 0.0, 0.15) await tween.finished _about_overlay.visible = false _focus_first_button() # --- Анимация --------------------------------------------------------------- func _play_intro() -> void: var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) _header.modulate.a = 0.0 tween.tween_property(_header, "modulate:a", 1.0, INTRO_TIME) _footer.modulate.a = 0.0 tween.tween_property(_footer, "modulate:a", 1.0, INTRO_TIME).set_delay(0.35) var index := 0 for button: Button in _menu.get_children(): if not button.visible: continue button.modulate.a = 0.0 tween.tween_property(button, "modulate:a", 1.0, 0.35).set_delay(0.2 + index * 0.07) index += 1 await tween.finished