feat: 两级存档系统——无限组/槽,建组建槽/自动档/两级浏览/读档还原/删除组与槽/游戏内加载(D8 完成)
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# 存档系统设计(D8:存档接入)
|
||||
|
||||
> 对应开发计划表 D8(8/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 | 读档箭头(点击读档) |
|
||||
|
||||
**交互规则**:
|
||||
|
||||
| 项 | 存档模式(游戏内) | 读档模式(主菜单) |
|
||||
|----|-------------------|-------------------|
|
||||
| 左侧组列表 | 当前组高亮(可切换查看,**组行带"删"按钮,当前组禁用**) | 可切换任意组(组行带"删"按钮) |
|
||||
| 槽位点击 | **回到该进度点**(加载所选槽,未保存进度丢失) | 点击读档进入游戏 |
|
||||
| 删除按钮 | 可删(确认后) | 可删(确认后) |
|
||||
| 暂停 | 打开时暂停,关闭恢复 | 不暂停 |
|
||||
|
||||
## 七、分步实现规划(小步可验证,每步独立跑通)
|
||||
|
||||
### 步骤 1:SaveSystem 骨架(新 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 自动刷新) |
|
||||
| 验证 | ✅ 存档文件内容完整(弟子/建筑/资源/日期全量) |
|
||||
|
||||
### 步骤 3:GameState + 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)` |
|
||||
| 验证 | 无头:存→清→读闭环,日期/资源/名录(境界修为饥饿)/建筑逐项一致 |
|
||||
|
||||
### 步骤 4:SavePanel 动态填充(复用现有手绘界面)✅
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 改动 | `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/2;4 依赖 1/3;5 依赖 4;6 为最终验证。
|
||||
|
||||
## 八、验收清单(D8 交付标准)
|
||||
|
||||
- [x] 主菜单"新游戏"自动创建新存档组(存档N)
|
||||
- [x] 游戏内"存档"面板点"新建存档"→ 该组新增一条槽位(文件生成,内容完整)
|
||||
- [x] 连续多次存档 → 同一组内多个槽位互不覆盖(序号命名,已验证 slot_1/2/3 内容独立)
|
||||
- [x] 返回主菜单自动写自动槽(auto.json)
|
||||
- [x] "读取存档"两级浏览:左侧组列表 → 右侧槽位列表(元数据显示)
|
||||
- [x] 读档后:日期/资源/名录(含境界/修为/饥饿)/建筑全部还原
|
||||
- [x] 读档进入**不弹招募窗、不暂停**
|
||||
- [x] 删除槽位后列表刷新;损坏/缺失 JSON 不崩溃(回退提示)
|
||||
- [x] 新游戏流程不受影响(仍弹开局招募);多组多槽共存
|
||||
Reference in New Issue
Block a user