Files
spellforge/docs_dev/specs/2026-07-21-meta-progression-mvp-design.md
T

143 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 元进展 MVP(碎片 + 解锁树 · 法术only)设计规范
> **日期**2026-07-21 · **状态**:设计已确认,待写实现计划
> **背景**`game_design §7` 的元进展(以太碎片 / 解锁树 / 图鉴 / 成就)代码 0% 实现。本规范定义元进展的**第一个子项目**:碎片货币 + 法术解锁树的完整赚→花→门控闭环。图鉴、成就、核心解锁、挑战模式为后续子项目。
---
## 1. 目标与范围
**目标**:给游戏加入 Roguelite 长期粘性闭环——每局结束按波次获得**以太碎片**(全局持久、死亡不失),在主菜单花碎片**解锁进阶法术**,解锁后的法术进入商店掉落池。即使失败也有收获感。
**本次 MVP 包含**
- 全局持久的碎片货币(`user://meta.json`)。
- 每局结束(死亡)按到达波次发放碎片。
- 主菜单「🔓 解锁」面板:显示碎片、花碎片解锁锁定法术。
- 内容门控:~6 张进阶法术**开局锁定**,其余开局可用;商店池只纳入"开局解锁 + 已解锁"的法术。
- ST-03「清除所有本地数据」一并清空 meta.json。
**明确不做(后续子项目/迭代)**
- 核心(Core)解锁(依赖未实现的商店货架 B)、挑战模式解锁。
- 图鉴 Codex、成就 / 炼金日志。
- 通关首胜额外奖励(通关流程本身未实现)。
- 存档防篡改(单机本地存档,MVP 用明文 JSONEndlessRecords 的 HMAC 不引入此处)。
---
## 2. 架构:组件与职责
### 2.1 `MetaProgress`(新 Autoload)—— 全局元进展状态
`scripts/autoloads/meta_progress.gd`,注册进 `project.godot`
```
const PATH := "user://meta.json"
var aether: int = 0
var unlocked: Array = [] # 已解锁法术 idString
func _ready() -> void: load_meta()
func add_aether(n: int) -> void # 累加并存档(n<=0 直接返回)
func is_unlocked(id: String) -> bool # id 在 unlocked 中
func unlock(id: String, cost: int) -> bool # 若已解锁 或 aether<cost → 返回 false(不扣费);否则扣费+加入 unlocked+存档→true
func load_meta() -> void # 读 meta.json;缺失/损坏→aether=0, unlocked=[]
func save_meta() -> void # 写 { "aether": int, "unlocked": [...] }
func clear() -> void # aether=0, unlocked=[], 删 meta.jsonST-03 调用)
```
- 职责边界:只管"有多少碎片、解锁了哪些、怎么存读"。不知道法术费用(费用来自 spells.json,调用方传入)、不碰商店/UI。
### 2.2 `spells.json` 门控字段
给 6 张进阶法术的 `meta``"unlock_cost": N`(放 **meta 内**,法术编辑器把 meta 当自由 JSON 保留,不会像顶层字段那样被 rebuild 丢弃)。无 `unlock_cost` 或 =0 → 开局解锁。
### 2.3 商店池门控(`shop_manager.gd`
`_refresh_slots` 现有过滤 `shop_cost>0`,改为:
```
shop_cost>0 且 (unlock_cost==0 或 MetaProgress.is_unlocked(id))
```
锁定且未解锁的法术不进商店池。
### 2.4 碎片结算(`combat_manager.gd`
`_on_player_died` 已在重置前捕获到达波次。新增:`MetaProgress.add_aether(_aether_for_wave(wave))`,其中
```
func _aether_for_wave(wave) -> int: return int(pow(float(wave), 1.4) / 3.0)
```
W1=0 / W5≈3 / W10≈8 / W20≈22,贴合 §7.1 的 0/3/8/20)。
### 2.5 解锁界面(`main_menu.gd`
主菜单新增「🔓 解锁」按钮 → 程序化 CanvasLayer 面板(比照现有 settings 面板写法):
- 顶部显示 `以太碎片: N`
- 遍历 `SpellRegistry``meta.unlock_cost>0` 的法术,逐行:`显示名 — 费用 X 碎片 — [解锁]按钮`
- 按钮态:已解锁→显示"已解锁"禁用;未解锁且碎片不足→禁用;足够→可点,点击 `MetaProgress.unlock(id, cost)` 成功后刷新面板。
- 关闭按钮返回菜单。文案走 i18n(新增 `META_*` 键,4 语言)。
---
## 3. 数据流
```
开始→游玩→死亡
│ combat_manager._on_player_died 捕获 wave
▼ MetaProgress.add_aether( _aether_for_wave(wave) ) → 存 meta.json(全局持久)
主菜单「🔓解锁」面板 → 花碎片 MetaProgress.unlock(id,cost) → unlocked[]+存档
下一局商店 _refresh_slots:池 = shop_cost>0 且 (unlock_cost==0 或 已解锁)
```
---
## 4. 数值
**默认锁定的 6 张法术 + 费用**(写入各自 `meta.unlock_cost`):
| 法术 id | unlock_cost | 说明 |
| :--- | :--- | :--- |
| action_energy_orb | 4 | 慢速高伤 |
| action_frost_bolt | 5 | 冻结 |
| action_poison_pool | 6 | 地面毒池(zone) |
| logic_loop | 6 | 循环(强力) |
| action_chain_bolt | 8 | 连锁闪电 |
| action_summon_turret | 10 | 召唤炮台 |
其余 ~13 张(spark_bolt / fire_bolt / poison_dart / laser_beam / water_wave / damage_plus / spread_mod / double_cast / pierce_plus / trigger_on_hit / every_n_shots / if_hp_below / if_enemy_nearby)开局可用。解锁全部 = 39 碎片 ≈ 5 局(W10 档)。
**碎片公式**`int(pow(wave, 1.4) / 3.0)`
> `action_plasma_storm` 是共鸣产物、`shop_cost=0`,本就不在商店池,无需门控。
---
## 5. 边界与默认
- `meta.json` 缺失/损坏 → 全新档(aether 0、unlocked 空),不崩溃。
- 已解锁法术再点解锁 → `is_unlocked` 提前拦截 / 按钮禁用,不重复扣费。
- 碎片不入局内存档(ProfileManager A/B)——它是全局的,只在 meta.json。
- ST-03 清除数据:`SettingsManager.clear_all_local_data` 增调 `MetaProgress.clear()`
- 门控只影响**商店掉落池**;玩家的初始预设卡组(action_spark_bolt)不受影响。
---
## 6. 测试计划(Godot MCP 运行时实测)
1. **持久往返**`MetaProgress.add_aether(10)``save_meta` → 新实例 `load_meta` → aether=10、unlocked 保留。
2. **解锁扣费**aether=5`unlock("action_frost_bolt", 5)`→true、aether=0、is_unlocked=true;再 `unlock("action_chain_bolt", 8)`→false(不足)、状态不变。
3. **商店池门控**unlocked 为空时 `_refresh_slots` 抽样多次,锁定法术(如 chain_bolt)**不出现**;解锁后再抽,可出现。
4. **碎片公式**`_aether_for_wave` 对 1/5/10/20 返回 0/3/8/22。
5. **清除数据**`MetaProgress.clear()` 后 aether=0/unlocked 空/文件删除。
6. **解锁面板**:主菜单点「解锁」渲染碎片数 + 6 行锁定法术;截图确认;点解锁按钮态正确变化。
---
## 7. 涉及文件清单
| 文件 | 改动 |
| :--- | :--- |
| `scripts/autoloads/meta_progress.gd` | **新建** MetaProgress autoload |
| `project.godot` | 注册 MetaProgress autoload |
| `data/spells.json` | 6 张法术 meta 加 `unlock_cost` |
| `scripts/autoloads/shop_manager.gd` | `_refresh_slots` 过滤加门控条件 |
| `scripts/domain/combat/combat_manager.gd` | `_on_player_died` 按波次发碎片 + `_aether_for_wave` |
| `scripts/autoloads/settings_manager.gd` | `clear_all_local_data` 增调 `MetaProgress.clear()` |
| `scenes/ui/main_menu.gd` | 「🔓解锁」按钮 + 解锁面板 |
| `translations/*.po` | `META_*` 四语键 |
---
## 8. 完成定义 (DoD)
- 上述 6 项测试在 Godot 4.6 运行时全绿、0 编译错误。
- 死亡后碎片按波次增加并持久(重启游戏仍在)。
- 主菜单解锁面板可花碎片解锁法术;解锁后该法术出现在商店池、未解锁前不出现。
- ST-03 清除数据把碎片/解锁清空。
- `game_design §7` 现状 callout 更新为「碎片+解锁树 已实现(MVP);图鉴/成就/核心解锁待后续」。