Files
g-world/scripts/ui/save_menu.gd
T
2026-08-10 20:39:06 +03:00

221 lines
7.2 KiB
GDScript

class_name SaveMenuScreen
extends Control
## Экран слотов сохранения. Один и тот же и для записи, и для чтения —
## режим задаётся при открытии: open(Mode.SAVE) из паузы, open(Mode.LOAD)
## из главного меню.
##
## Опасные действия (перезапись и удаление) требуют второго нажатия:
## кнопка меняет подпись на «Точно?», строка статуса объясняет, что будет.
signal closed
enum Mode { SAVE, LOAD }
const MUTED := Color("#8fa5ba")
const THUMB_SIZE := Vector2(192, 108)
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _card: Control = $Center/Card
@onready var _heading: Label = $Center/Card/Body/Heading
@onready var _slots_box: VBoxContainer = $Center/Card/Body/Scroll/Slots
@onready var _status: Label = $Center/Card/Body/Actions/Status
@onready var _back_button: Button = $Center/Card/Body/Actions/BackButton
var _mode := Mode.LOAD
## {"action": "save" | "delete", "index": int} — ждём подтверждения.
var _pending := {}
func _ready() -> void:
_back_button.pressed.connect(close)
_refresh()
func _unhandled_input(event: InputEvent) -> void:
if not visible or not event.is_action_pressed("ui_cancel"):
return
close()
get_viewport().set_input_as_handled()
# --- Открытие / закрытие ----------------------------------------------------
func open(mode: int) -> void:
_mode = mode
_pending = {}
_heading.text = "СОХРАНЕНИЕ" if mode == Mode.SAVE else "ЗАГРУЗКА"
_status.text = "Выберите слот для записи." if mode == Mode.SAVE else "Выберите сохранение."
_refresh()
modulate.a = 0.0
visible = true
# Контейнеры раскладываются только после того, как узел стал видимым.
await get_tree().process_frame
_card.pivot_offset = _card.size * 0.5
_card.scale = Vector2(0.97, 0.97)
var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(self, "modulate:a", 1.0, 0.18)
tween.tween_property(_card, "scale", Vector2.ONE, 0.22)
_focus_first_action()
func close() -> void:
var tween := create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(self, "modulate:a", 0.0, 0.15)
await tween.finished
visible = false
closed.emit()
# --- Список слотов ----------------------------------------------------------
func _refresh() -> void:
for child in _slots_box.get_children():
child.queue_free()
for info: Dictionary in _saves.list_slots():
_slots_box.add_child(_make_row(info))
func _make_row(info: Dictionary) -> PanelContainer:
var index: int = info["index"]
var exists: bool = info["exists"]
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _row_style(exists))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 18)
panel.add_child(row)
var thumb := TextureRect.new()
thumb.custom_minimum_size = THUMB_SIZE
thumb.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
thumb.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
thumb.clip_contents = true
thumb.texture = info["thumbnail"]
row.add_child(thumb)
var info_box := VBoxContainer.new()
info_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER
info_box.add_theme_constant_override("separation", 4)
row.add_child(info_box)
var title := Label.new()
title.text = "Слот %d" % (index + 1)
title.add_theme_font_size_override("font_size", 22)
info_box.add_child(title)
var meta := Label.new()
meta.add_theme_font_size_override("font_size", 16)
meta.add_theme_color_override("font_color", MUTED)
if exists:
meta.text = "%s · %s" % [
SaveManagerService.format_timestamp(info["saved_at"]),
SaveManagerService.format_playtime(info["playtime"]),
]
else:
meta.text = "пусто"
info_box.add_child(meta)
row.add_child(_make_action_button(index, exists))
row.add_child(_make_delete_button(index, exists))
return panel
func _make_action_button(index: int, exists: bool) -> Button:
var button := Button.new()
button.custom_minimum_size.x = 230
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_font_size_override("font_size", 20)
if _is_pending("save", index):
button.text = "Точно?"
elif _mode == Mode.SAVE:
button.text = "Перезаписать" if exists else "Сохранить"
else:
button.text = "Загрузить"
button.disabled = not exists
button.pressed.connect(_on_action_pressed.bind(index))
return button
func _make_delete_button(index: int, exists: bool) -> Button:
var button := Button.new()
button.custom_minimum_size.x = 170
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_font_size_override("font_size", 20)
button.text = "Точно?" if _is_pending("delete", index) else "Удалить"
button.disabled = not exists
button.pressed.connect(_on_delete_pressed.bind(index))
return button
func _row_style(filled: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color("#ffffff0f") if filled else Color("#ffffff05")
style.border_color = Color("#ffffff1a")
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.content_margin_left = 14
style.content_margin_right = 14
style.content_margin_top = 12
style.content_margin_bottom = 12
return style
func _focus_first_action() -> void:
for row: Node in _slots_box.get_children():
for control: Node in row.get_child(0).get_children():
if control is Button and not (control as Button).disabled:
(control as Button).grab_focus()
return
_back_button.grab_focus()
# --- Действия ---------------------------------------------------------------
func _on_action_pressed(index: int) -> void:
if _mode == Mode.LOAD:
_status.text = "Загрузка слота %d…" % (index + 1)
# Не ждём: смена сцены уничтожит этот экран вместе со старым деревом.
_saves.load_slot(index)
return
var info := _saves.slot_info(index)
if info["exists"] and not _is_pending("save", index):
_set_pending("save", index, "Слот %d будет перезаписан — нажмите ещё раз." % (index + 1))
return
_pending = {}
if _saves.save_to_slot(index):
_status.text = "Сохранено в слот %d." % (index + 1)
else:
_status.text = "Не удалось сохранить в слот %d." % (index + 1)
_refresh()
func _on_delete_pressed(index: int) -> void:
if not _is_pending("delete", index):
_set_pending("delete", index, "Слот %d будет удалён — нажмите ещё раз." % (index + 1))
return
_pending = {}
_saves.delete_slot(index)
_status.text = "Слот %d удалён." % (index + 1)
_refresh()
func _set_pending(action: String, index: int, message: String) -> void:
_pending = {"action": action, "index": index}
_status.text = message
_refresh()
func _is_pending(action: String, index: int) -> bool:
return _pending.get("action", "") == action and _pending.get("index", -1) == index