178 lines
6.1 KiB
GDScript
178 lines
6.1 KiB
GDScript
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 _accent_bar: ColorRect = $Content/Column/Header/AccentBar
|
|
@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)
|
|
|
|
_animate_accent_bar()
|
|
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
|
|
|
|
|
|
func _animate_accent_bar() -> void:
|
|
# Медленное «дыхание» акцентной черты под заголовком.
|
|
var tween := create_tween().set_loops().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
|
|
tween.tween_property(_accent_bar, "custom_minimum_size:x", 230.0, 2.6)
|
|
tween.tween_property(_accent_bar, "custom_minimum_size:x", 140.0, 2.6)
|