first commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user