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