feat: 两级存档系统——无限组/槽,建组建槽/自动档/两级浏览/读档还原/删除组与槽/游戏内加载(D8 完成)

This commit is contained in:
2026-08-09 19:17:21 +08:00
parent 2bb1486df6
commit b687ab153d
19 changed files with 851 additions and 63 deletions
+162 -4
View File
@@ -1,10 +1,168 @@
## 存档选择面板(挂载于 SavePanel.tscn)。
## 显示存档列表与"新建存档"入口,通过信号通知外部结果
## 存档面板(挂载于 SavePanel.tscn)。
## 两级浏览:左侧存档组列表 → 右侧该组槽位列表
## 存档模式(游戏内):点"新建存档"每次创建新槽位;读档模式(主菜单):点槽位读档。
extends PanelContainer
## 面板模式。
enum Mode { SAVE, LOAD }
## 请求关闭面板(返回上级界面)。
signal back_pressed
## 点击"返回"按钮
## 槽位条目场景
const SAVE_SLOT_SCENE := preload("res://src/UI/Panels/SaveSlot.tscn")
@onready var title_label: Label = %TitleLabel
@onready var group_container: VBoxContainer = %GroupContainer
@onready var slot_list: VBoxContainer = %SlotList
@onready var new_slot_button: Button = %Button_NewSlot
## 当前模式。
var mode: Mode = Mode.LOAD
## 当前浏览的存档组 ID。
var _current_profile: int = 0
## 打开面板前的暂停状态(存档模式关闭时恢复)。
var _was_paused := false
## 读档模式打开(主菜单,不暂停时间,自动选中第一个存档组)。
func open_load() -> void:
mode = Mode.LOAD
new_slot_button.visible = false
_current_profile = 0
var profiles := SaveSystem.get_profiles()
if profiles.size() > 0:
_current_profile = int(profiles[0]["id"])
_show_profiles()
visible = true
## 存档模式打开(游戏内,暂停时间,当前组固定高亮)。
## 点槽位 = 回到该进度点;点"新建存档" = 保存当前进度为新槽。
func open_save() -> void:
mode = Mode.SAVE
new_slot_button.visible = true
_current_profile = GameState.profile_id
_was_paused = TimeSystem.paused
TimeSystem.set_paused(true)
_show_profiles()
visible = true
## 关闭面板(恢复打开前的暂停状态)。
func close_panel() -> void:
visible = false
TimeSystem.set_paused(_was_paused)
## 点击"返回"。
func _on_button_esc_button_up() -> void:
back_pressed.emit()
close_panel()
if mode == Mode.LOAD:
back_pressed.emit()
## 点击"新建存档":在所属组下创建新槽位并刷新列表。
func _on_button_new_slot_button_up() -> void:
if SaveSystem.save_game(_current_profile) != "":
_show_slots()
## 左列:存档组列表(当前组高亮;存档模式下当前组固定)。
func _show_profiles() -> void:
title_label.text = "选择存档(存档模式)" if mode == Mode.SAVE else "选择存档"
_clear(group_container)
var profiles := SaveSystem.get_profiles()
if profiles.is_empty():
var label := Label.new()
label.text = "暂无存档组"
group_container.add_child(label)
_show_slots()
return
for p in profiles:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
var btn := Button.new()
btn.text = "%s %s" % [p["name"], p["last_date"]]
btn.toggle_mode = true
var is_current: bool = int(p["id"]) == _current_profile
btn.set_pressed_no_signal(is_current)
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if mode == Mode.SAVE and is_current:
btn.disabled = true
else:
btn.button_up.connect(_on_group_pressed.bind(int(p["id"])))
row.add_child(btn)
# 删除按钮:存档模式下当前组禁用(正在使用)
var del_btn := Button.new()
del_btn.text = ""
del_btn.disabled = mode == Mode.SAVE and is_current
del_btn.button_up.connect(_on_profile_delete_requested.bind(int(p["id"])))
row.add_child(del_btn)
group_container.add_child(row)
_show_slots()
## 请求删除存档组:直接删除并刷新(当前浏览组删除后自动切到第一组)。
## @param profile_id: 存档组 ID
func _on_profile_delete_requested(profile_id: int) -> void:
if not SaveSystem.delete_profile(profile_id):
return
if _current_profile == profile_id:
_current_profile = 0
var profiles := SaveSystem.get_profiles()
if profiles.size() > 0:
_current_profile = int(profiles[0]["id"])
_show_profiles()
## 点击存档组:切换浏览。
## @param profile_id: 存档组 ID
func _on_group_pressed(profile_id: int) -> void:
_current_profile = profile_id
_show_profiles()
## 右列:当前组槽位列表。
func _show_slots() -> void:
_clear(slot_list)
if _current_profile == 0:
return
var slots := SaveSystem.get_slots(_current_profile)
if slots.is_empty():
var label := Label.new()
label.text = "(暂无存档,点上方\"新建存档\"" if mode == Mode.SAVE else "(该组暂无存档)"
slot_list.add_child(label)
return
for i in slots.size():
_add_slot_entry(i + 1, slots[i])
## 添加一条槽位条目。
## @param index: 序号(1 起)
## @param slot: 槽位元数据 {name, date_text, disciple_count, spirit_stones}
func _add_slot_entry(index: int, slot: Dictionary) -> void:
var entry: PanelContainer = SAVE_SLOT_SCENE.instantiate()
slot_list.add_child(entry)
var slot_name := str(slot["name"])
var index_text := "自动" if slot_name == "auto" else str(index)
var name_text := "自动存档" if slot_name == "auto" else "存档 %d" % index
entry.setup(slot_name, index_text, name_text,
"%s 弟子%d" % [slot["date_text"], slot["disciple_count"]],
"灵石 %d" % slot["spirit_stones"])
entry.slot_pressed.connect(_on_slot_pressed)
entry.delete_requested.connect(_on_slot_delete_requested)
## 点击槽位:读档模式进入游戏;存档模式回到该进度点(当前未保存进度丢失)。
## @param slot_name: 槽文件名(不含 .json
func _on_slot_pressed(slot_name: String) -> void:
if mode == Mode.LOAD:
GameState.request_load_game(_current_profile, slot_name)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
return
# 存档模式:直接加载所选槽位(回到该进度点)
if SaveSystem.load_game(_current_profile, slot_name):
close_panel()
## 请求删除槽位:直接删除并刷新(确认弹窗后续接入)。
## @param slot_name: 槽文件名(不含 .json
func _on_slot_delete_requested(slot_name: String) -> void:
if SaveSystem.delete_slot(_current_profile, slot_name):
_show_slots()
## 清空容器。
## @param container: 目标容器
func _clear(container: VBoxContainer) -> void:
for child in container.get_children():
child.queue_free()