first commit

This commit is contained in:
Leonid Pershin
2026-08-10 20:39:06 +03:00
commit f16f60445c
281 changed files with 43576 additions and 0 deletions
+177
View File
@@ -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)