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
+210
View File
@@ -0,0 +1,210 @@
# 存档系统设计(D8:存档接入)
> 对应开发计划表 D88/10):① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据。
> 交付标准:读档后数据还原。本文档只描述设计,不包含代码。
> 状态:全部步骤已实现(SaveSystem/序列化/GameState+MainGame 分支/SavePanel 双模式/自动存档/两级浏览/读档还原/删除与容错/游戏内加载进度点/删除存档组),验收 1–9 全部通过
---
## 一、两级存档结构(无限组 / 无限槽)
**第一级:存档组 SaveGroup(一个角色/一次轮回,UI 显示"存档N"****第二级:存档槽 SaveSlot(该组下任意多个进度快照)**
- **每个新游戏 → 创建一个 SaveGroup**(主菜单"新游戏"时自动生成,id 自增)
- **每次点击存档 → 在所属 SaveGroup 下创建一个新 SaveSlot**(不覆盖、不固定槽位)
- **组和槽的数量均无上限**
```
user://saves/
├── profiles.json # 存档组列表 [{id, name, created, last_date}]
├── profile_1/ # 存档组 1
│ ├── auto.json # 自动存档(返回主菜单时写入,特殊槽)
│ ├── slot_1.json # 手动槽(组内序号递增命名)
│ └── slot_2.json
└── profile_2/ ...
```
- 槽位命名:**组内序号递增**`slot_1``slot_2`…),同一组内永不重名、互不覆盖;删除槽位后序号从当前最大 +1 继续(不重用)
- 自动槽 `auto.json` 固定名(续档用)
## 二、可存档数据清单
| 系统 | 数据 | 说明 |
|------|------|------|
| GameState | `year/month/day` | 游戏日期(TimeSystem.set_date 恢复) |
| SectManager | `spirit_stones` / `reputation` / `food` | 三资源 |
| DiscipleManager | `disciples` + `_next_id` | 名录;候选池不存 |
| BuildingManager | `buildings` | 建筑类型/等级 |
> 修为/境界/饥饿天数都在 Disciple 字段内,随名录序列化。
## 三、SaveSystem(新 Autoload
存放位置:`src/Core/SaveSystem.gd`,注册于 project.godot(依赖各管理器,声明在最后)。
| 接口 | 说明 |
|------|------|
| `create_profile() -> int` | 新建存档组,返回 id(名"存档N" |
| `get_profiles() -> Array[Dictionary]` | 存档组列表(id/name/created/last_date |
| `save_game(profile_id) -> String` | **新建一个槽位**并写入当前游戏数据,返回槽文件名;失败返回空串 |
| `save_auto(profile_id) -> bool` | 写自动槽(返回主菜单时调用) |
| `get_slots(profile_id) -> Array[Dictionary]` | 该组全部槽位(槽文件名 + 元数据),按时间升序 |
| `load_game(profile_id, slot_name) -> bool` | 读取槽位 JSON → 还原各系统 |
| `delete_profile(profile_id) -> bool` | 删除存档组(列表移除 + 递归删目录含全部槽位) |
| `delete_slot(profile_id, slot_name) -> bool` | 删除槽位文件 |
| `reset()` | 清空内存游戏数据(各管理器 reset + 资源清零) |
## 四、序列化设计
### 1. 数据模型加 to_dict/from_dict
- `Disciple.to_dict()` / `static Disciple.from_dict(data)`14 字段全量,枚举转 int
- `Building.to_dict()` / `static from_dict(data)`2 字段)
### 2. 槽位 JSON 结构(slot_1754734800.json
```json
{
"version": 1,
"profile_id": 1,
"saved_date": {"year": 3, "month": 5, "day": 12},
"date": {"year": 3, "month": 5, "day": 12},
"resources": {"spirit_stones": 1230, "reputation": 45, "food": 210},
"next_disciple_id": 7,
"disciples": [
{"id": 1, "name": "林清玄", "age": 16, "aptitude": 2, "roots": [0, 1],
"is_heavenly": false, "join_year": 1, "join_month": 1, "join_day": 1,
"realm_index": 0, "sub_realm_index": 4, "cultivation_exp": 512.5, "hunger_days": 0}
],
"buildings": [
{"type": 0, "level": 2},
{"type": 1, "level": 1}
]
}
```
- `saved_date`/`resources` 顶层冗余字段 = 槽位元数据(列表显示用,免解析全档)
- `version` 为格式版本,预留兼容
### 3. profiles.json 结构
```json
{"profiles": [{"id": 1, "name": "存档1", "created": "2026-08-10", "last_date": "3年5月12日"}]}
```
## 五、存档/读档流程
### 存档(游戏内,每次=新建槽)
1. TopBar 新增"存档"按钮 → SavePanel(存档模式,时间暂停)
2. 左侧显示存档组(当前组高亮),右侧显示该组全部槽位
3. 点"新建存档"按钮 → `save_game(profile_id)` 创建新槽 → 列表新增一条
4. 关闭面板恢复时间
### 读档(主菜单,两级浏览)
1. 主菜单"读取存档" → SavePanel(读档模式)
2. **左侧存档组列表**(存档N + 最后日期)→ 点组 → **右侧显示该组槽位列表**(自动+手动,元数据齐全)
3. 点槽位 → `GameState.request_load_game(profile_id, slot_name)` → 进入 MainGame
### MainGame 分支(改造 _ready
```
NEW_GAME → 现有流程:start_new_game + 开局资源 + 招募弹窗(暂停)
LOAD_GAME → SaveSystem.load_game(profile_id, slot_name) → 还原 → 不弹窗、不暂停
```
### 自动存档
TopBar"返回"主菜单时(sign_return_mainmenu 处理前)→ `save_auto(profile_id)` 自动槽——保证"退→再进"能续档。
## 六、UI:SavePanel(复用现有手绘界面)
```
SavePanel (PanelContainer 全屏)
└── VBoxContainer
├── Header: TitleLabel("选择存档") + Spacer + Button_Esc("返回")
├── 存档模式:Button_NewSlot("新建存档") ← 每次点击创建新槽位
└── ScrollContainer
└── HBoxContainer
├── SaveGroup 区(VBoxContainer) ← 左侧:存档组按钮列表(动态生成"存档N"
└── SlotList 区(VBoxContainer) ← 右侧:该组槽位条目列表(动态生成 SaveSlot 实例)
```
**SaveSlot 条目**(复用现有场景,无空槽概念——每个条目都是真实存档):
| 节点 | 填充内容 |
|------|---------|
| SlotIndexLabel | 序号(自动槽=0,手动槽按时间序 1..N) |
| SaveNameLabel | "自动存档" / "存档 N" |
| MetaLine1 | "X年X月X日 弟子N人" |
| MetaLine2 | "灵石 M" |
| DeleteButton | 删除该槽位(删除前确认) |
| ArrowIcon | 读档箭头(点击读档) |
**交互规则**
| 项 | 存档模式(游戏内) | 读档模式(主菜单) |
|----|-------------------|-------------------|
| 左侧组列表 | 当前组高亮(可切换查看,**组行带"删"按钮,当前组禁用**) | 可切换任意组(组行带"删"按钮) |
| 槽位点击 | **回到该进度点**(加载所选槽,未保存进度丢失) | 点击读档进入游戏 |
| 删除按钮 | 可删(确认后) | 可删(确认后) |
| 暂停 | 打开时暂停,关闭恢复 | 不暂停 |
## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SaveSystem 骨架(新 Autoload)✅
| 项 | 内容 |
|----|------|
| 改动 | 新建 `src/Core/SaveSystem.gd``create_profile()`(建组,名"存档N")、`get_profiles()``save_game(profile_id)`(新建槽,文件名 `slot_{组内序号}.json`,返回槽名)、`save_auto(profile_id)``get_slots(profile_id)`(自动槽最前+手动按序号升序,含元数据)、`load_game(profile_id, slot_name)``delete_slot()``reset()``project.godot` 注册(声明在最后) |
| 验证 | ✅ 建组(存档N 自增)→ 存档 → slot_1/2/3 生成 → `get_slots` 读回;删除后序号不重用 |
### 步骤 2:数据模型序列化 ✅
| 项 | 内容 |
|----|------|
| 改动 | `Disciple.to_dict()/static from_dict()`14 字段,枚举转 int);`Building.to_dict()/static from_dict()``DiscipleManager``get/set_next_disciple_id()``SectManager``set_spirit_stones/set_reputation/set_food()`(带信号,读档后 HUD 自动刷新) |
| 验证 | ✅ 存档文件内容完整(弟子/建筑/资源/日期全量) |
### 步骤 3GameState + MainGame 分支 🟡(GameState 已改,MainGame 分支待接)
| 项 | 内容 |
|----|------|
| 改动 | `GameState``profile_id: int``save_slot: String`(槽文件名);`request_new_game(pid)``request_load_game(pid, slot_name)``MainGame._ready` 分支 NEW_GAME(原流程)/ LOAD_GAME`load_game` 还原,不弹窗不暂停,失败回退新游戏);`sign_return_mainmenu``save_auto(profile_id)` |
| 验证 | 无头:存→清→读闭环,日期/资源/名录(境界修为饥饿)/建筑逐项一致 |
### 步骤 4SavePanel 动态填充(复用现有手绘界面)✅
| 项 | 内容 |
|----|------|
| 改动 | `SavePanel.gd` 双模式:存档模式(游戏内,暂停,当前组高亮 + 槽位列表 + "新建存档"按钮 → `save_game` → 列表新增);读档模式(主菜单,左列组列表 → 点组 → 右列槽位列表,点槽读档)。`SaveSlot.gd` 填充:序号/名称/MetaLine1/MetaLine2/删除(确认后删)/箭头(读档)。TopBar 加"存档"按钮 + `sign_open_save` 信号 |
| 验证 | ✅ 存档模式:打开暂停 → 新建存档 → 槽位生成(内容完整)→ 关闭恢复;读档模式待 F5 手测 |
### 步骤 5:接线闭环 🟡(MainMenu 建组已接,读档模式接线+自动存档待做)
| 项 | 内容 |
|----|------|
| 改动 | `MainMenu`"新游戏"→ `create_profile()` 建组 → 进游戏;"读取存档"→ 面板读档模式;`MainGame.tscn` 挂 SavePanel 实例 |
| 验证 | 完整闭环:新游戏(建组)→ 招弟子 → 挂机 → 存档(建槽)→ 返回(自动槽)→ 主菜单读档 → 数据还原 |
### 步骤 6:整体验证 + 提交
| 项 | 内容 |
|----|------|
| 改动 | 无(仅验证) |
| 验证 | 对照八节验收清单逐项勾选;git 提交 |
> 依赖关系:1→2 顺序执行(序列化被 SaveSystem 消费);3 依赖 1/24 依赖 1/3;5 依赖 4;6 为最终验证。
## 八、验收清单(D8 交付标准)
- [x] 主菜单"新游戏"自动创建新存档组(存档N)
- [x] 游戏内"存档"面板点"新建存档"→ 该组新增一条槽位(文件生成,内容完整)
- [x] 连续多次存档 → 同一组内多个槽位互不覆盖(序号命名,已验证 slot_1/2/3 内容独立)
- [x] 返回主菜单自动写自动槽(auto.json)
- [x] "读取存档"两级浏览:左侧组列表 → 右侧槽位列表(元数据显示)
- [x] 读档后:日期/资源/名录(含境界/修为/饥饿)/建筑全部还原
- [x] 读档进入**不弹招募窗、不暂停**
- [x] 删除槽位后列表刷新;损坏/缺失 JSON 不崩溃(回退提示)
- [x] 新游戏流程不受影响(仍弹开局招募);多组多槽共存