From 14c0709d74dcaa2d9da2470185723ddbe7cddad7 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 11:35:44 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=85=83=E8=BF=9B=E5=B1=95=20MVP=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=AE=A1=E5=88=92=EF=BC=8810=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=8CMCP=20=E8=BF=90=E8=A1=8C=E6=97=B6=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-21-meta-progression-mvp.md | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 docs_dev/plans/2026-07-21-meta-progression-mvp.md diff --git a/docs_dev/plans/2026-07-21-meta-progression-mvp.md b/docs_dev/plans/2026-07-21-meta-progression-mvp.md new file mode 100644 index 0000000..2480216 --- /dev/null +++ b/docs_dev/plans/2026-07-21-meta-progression-mvp.md @@ -0,0 +1,595 @@ +# 元进展 MVP(碎片 + 解锁树·法术only)实现计划 + +> **For agentic workers:** 用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现。步骤用 `- [ ]` 复选框跟踪。 + +**Goal:** 加入 Roguelite 长期粘性闭环——每局死亡按波次得以太碎片(全局持久),主菜单花碎片解锁进阶法术,解锁后的法术进入商店池;所有可调数值数据驱动、编辑器可调。 + +**Architecture:** 新 `MetaProgress` autoload 管碎片+已解锁(`user://meta.json`);解锁费用在 `spells.json` 的 `meta.unlock_cost`(法术编辑器可调);碎片奖励曲线在 `balance.json.aether_thresholds`(平衡页签可调,`SettingsManager.aether_for_wave` 查表);商店池按解锁门控;死亡时 `combat_manager` 结算碎片。 + +**Tech Stack:** Godot 4.6 (Mono) · GDScript · 纯 JSON 数据驱动 · 验证用 Godot MCP(`execute_game_script` 游戏内 / `execute_editor_script` 编辑器内),本项目无单元测试框架。 + +> **验证约定**:每任务的"测试"用 MCP 运行时断言。前置:Godot 编辑器开着、MCP 连通。改代码后 `reload_project` + `get_editor_errors`(0)+ `play_scene(main)` 后 `execute_game_script` 调 autoload;编辑器 @tool 改动用 `execute_editor_script` + `CACHE_MODE_IGNORE` fresh-load 往返(绕过 @tool 热重载缓存)。跑完 `stop_scene`。 + +--- + +### Task 1: MetaProgress autoload(碎片+已解锁+持久化) + +**Files:** +- Create: `scripts/autoloads/meta_progress.gd` +- Modify: `project.godot`([autoload] 段注册) + +- [ ] **Step 1: 新建 meta_progress.gd** +```gdscript +## MetaProgress — 全局元进展(Autoload: MetaProgress) +## 以太碎片 + 已解锁法术,持久化 user://meta.json(死亡不重置)。 +extends Node + +const PATH: String = "user://meta.json" + +var aether: int = 0 +var unlocked: Array = [] # 已解锁法术 id (String) + +func _ready() -> void: + load_meta() + +func add_aether(n: int) -> void: + if n <= 0: + return + aether += n + save_meta() + +func is_unlocked(id: String) -> bool: + return id in unlocked + +## 已解锁 或 碎片不足 → false(不扣费);否则扣费+记录+存档 → true +func unlock(id: String, cost: int) -> bool: + if is_unlocked(id) or aether < cost: + return false + aether -= cost + unlocked.append(id) + save_meta() + return true + +func clear() -> void: + aether = 0 + unlocked = [] + if FileAccess.file_exists(PATH): + DirAccess.remove_absolute(ProjectSettings.globalize_path(PATH)) + +func save_meta() -> void: + var f := FileAccess.open(PATH, FileAccess.WRITE) + if f: + f.store_string(JSON.stringify({"aether": aether, "unlocked": unlocked})) + f.close() + +func load_meta() -> void: + aether = 0 + unlocked = [] + if not FileAccess.file_exists(PATH): + return + var parsed = JSON.parse_string(FileAccess.get_file_as_string(PATH)) + if not (parsed is Dictionary): + return + aether = int(parsed.get("aether", 0)) + var u = parsed.get("unlocked", []) + if u is Array: + for id in u: + unlocked.append(String(id)) +``` + +- [ ] **Step 2: 注册 autoload** + +在 `project.godot` [autoload] 段,`EndlessRecords="*res://scripts/autoloads/endless_records.gd"` 一行之后加: +``` +MetaProgress="*res://scripts/autoloads/meta_progress.gd" +``` + +- [ ] **Step 3: 验证(MCP)** — `play_scene(main)` 后: +```gdscript +MetaProgress.clear() +MetaProgress.add_aether(10) +_mcp_print("add10 -> aether=%d file=%s (expect 10/true)" % [MetaProgress.aether, str(FileAccess.file_exists("user://meta.json"))]) +_mcp_print("unlock frost(5) -> %s aether=%d unlocked=%s (expect true/5/[frost])" % [str(MetaProgress.unlock("action_frost_bolt", 5)), MetaProgress.aether, str(MetaProgress.unlocked)]) +_mcp_print("unlock chain(8) -> %s aether=%d (expect false/5 不足)" % [str(MetaProgress.unlock("action_chain_bolt", 8)), MetaProgress.aether]) +_mcp_print("unlock frost again -> %s (expect false 已解锁)" % str(MetaProgress.unlock("action_frost_bolt", 5))) +MetaProgress.aether = 0; MetaProgress.unlocked = [] +MetaProgress.load_meta() +_mcp_print("reload -> aether=%d unlocked=%s (expect 5/[frost] 往返)" % [MetaProgress.aether, str(MetaProgress.unlocked)]) +MetaProgress.clear() +_mcp_print("clear -> aether=%d unlocked=%s file=%s (expect 0/[]/false)" % [MetaProgress.aether, str(MetaProgress.unlocked), str(FileAccess.file_exists("user://meta.json"))]) +``` +Expected: 全部符合括号。`get_editor_errors`=0。 + +- [ ] **Step 4: Commit** +```bash +git add scripts/autoloads/meta_progress.gd project.godot +git commit -m "feat(meta): MetaProgress autoload(碎片+已解锁+持久化 user://meta.json)" +``` + +--- + +### Task 2: 碎片奖励曲线(balance.json 阈值表 + SettingsManager)+ 清除数据钩子 + +**Files:** +- Modify: `data/balance.json` +- Modify: `scripts/autoloads/settings_manager.gd` + +- [ ] **Step 1: balance.json 加阈值表** + +把 `data/balance.json` 整体替换为: +```json +{ + "difficulty_mults": { + "enemy_hp": [0.7, 1.0, 1.0], + "player_dmg": [0.7, 1.0, 1.0], + "wave_count": [1.0, 1.0, 1.2], + "boss_hp": [1.0, 1.0, 1.3] + }, + "boss_hp": { + "4": 450.0, + "5": 1800.0 + }, + "aether_thresholds": [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]] +} +``` + +- [ ] **Step 2: SettingsManager 加载 + 查表** + +在 `settings_manager.gd` 第 35 行 `var _BOSS_HP: Dictionary = {}` 之后加: +```gdscript +var _AETHER_THRESHOLDS: Array = [] # [[到达波次, 碎片], ...] 升序 +``` +在 `_load_balance()` 的 `if data.has("boss_hp") ...: _BOSS_HP = data["boss_hp"]` 之后加: +```gdscript + if data.has("aether_thresholds") and data["aether_thresholds"] is Array: + _AETHER_THRESHOLDS = data["aether_thresholds"] +``` +在 `get_boss_base_hp()` 函数之后加方法: +```gdscript +## 到达波次 → 以太碎片(阶梯查表:取"波次阈值 ≤ wave"的最高阶梯值;升序表) +func aether_for_wave(wave: int) -> int: + var table: Array = _AETHER_THRESHOLDS + if table.is_empty(): + table = [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]] + var result: int = 0 + for entry in table: + if entry is Array and entry.size() >= 2 and int(entry[0]) <= wave: + result = int(entry[1]) + return result +``` + +- [ ] **Step 3: 清除数据钩子** + +在 `clear_all_local_data()` 的 `_apply_audio()` 调用之前(第 114 行前)加: +```gdscript + MetaProgress.clear() +``` + +- [ ] **Step 4: 验证(MCP)** +```gdscript +for w in [1, 5, 10, 12, 15, 20, 25]: + _mcp_print("wave %d -> aether %d" % [w, SettingsManager.aether_for_wave(w)]) +``` +Expected: 1→0, 5→3, 10→8, 12→8, 15→14, 20→20, 25→20。`get_editor_errors`=0。 + +- [ ] **Step 5: Commit** +```bash +git add data/balance.json scripts/autoloads/settings_manager.gd +git commit -m "feat(meta): 碎片奖励阈值表(balance.json)+aether_for_wave+清除数据钩子" +``` + +--- + +### Task 3: spells.json 加 unlock_cost(6 张锁定法术) + +**Files:** +- Modify: `data/spells.json` + +- [ ] **Step 1: 给 6 张法术 meta 加 unlock_cost** + +在下列法术的 `meta` 对象里加 `"unlock_cost": N`(放 `shop_cost` 之前): + +| id | unlock_cost | +| :--- | :--- | +| action_energy_orb | 4 | +| action_frost_bolt | 5 | +| action_poison_pool | 6 | +| logic_loop | 6 | +| action_chain_bolt | 8 | +| action_summon_turret | 10 | + +示例(`action_chain_bolt` 一行): +```json + "action_chain_bolt": { "type": 0, "display_name": "Chain Bolt", "description": "连锁闪电(雷元素),与水共鸣成等离子风暴。", "element_tags": ["tag:lightning"], "meta": { "base_damage": 3.0, "speed": 420.0, "lifetime": 3.0, "radius": 6.0, "damage_type": 0, "mana_cost": 60, "unlock_cost": 8, "shop_cost": 20 } }, +``` +> 注意:`mana_cost` 已在(来自 Mana MVP),别删;`unlock_cost` 加在其后、`shop_cost` 前。其余 13 张不加(= 开局解锁)。 + +- [ ] **Step 2: 验证(MCP)** +```gdscript +for id in ["action_energy_orb","action_frost_bolt","action_poison_pool","logic_loop","action_chain_bolt","action_summon_turret","action_spark_bolt"]: + var s = SpellRegistry.get_spell(id) + if s: _mcp_print("%s unlock_cost=%s" % [id, str(s.meta.get("unlock_cost", 0))]) +``` +Expected: energy_orb=4, frost=5, poison_pool=6, loop=6, chain=8, turret=10, spark_bolt=0(未加)。`get_editor_errors`=0(JSON 合法)。 + +- [ ] **Step 3: Commit** +```bash +git add data/spells.json +git commit -m "feat(meta): spells.json 6 张进阶法术加 unlock_cost(解锁树)" +``` + +--- + +### Task 4: 商店池按解锁门控 + +**Files:** +- Modify: `scripts/autoloads/shop_manager.gd:33-46`(`_refresh_slots`,抽出可测的 `_available_pool`) + +- [ ] **Step 1: 抽出 `_available_pool` 并加门控** + +把 `_refresh_slots()` 开头的 pool 构造(第 34-37 行)替换为调用新方法,并在其上方新增方法。即将: +```gdscript +func _refresh_slots() -> void: + # 纯数据驱动池:SpellRegistry(data/spells.json)中可购买(shop_cost>0)的法术 + var pool: Array = SpellRegistry.get_all_ids().filter(func(id): + var s: SpellNode = SpellRegistry.get_spell(id) + return s != null and int(s.meta.get("shop_cost", 0)) > 0) +``` +改为: +```gdscript +## 商店可抽池:shop_cost>0 且(未门控 unlock_cost==0 或 已解锁) +func _available_pool() -> Array: + return SpellRegistry.get_all_ids().filter(func(id): + var s: SpellNode = SpellRegistry.get_spell(id) + if s == null or int(s.meta.get("shop_cost", 0)) <= 0: + return false + var uc: int = int(s.meta.get("unlock_cost", 0)) + return uc == 0 or MetaProgress.is_unlocked(id)) + +func _refresh_slots() -> void: + var pool: Array = _available_pool() +``` +(其余 `_refresh_slots` 逻辑不变。) + +- [ ] **Step 2: 验证(MCP)** +```gdscript +MetaProgress.clear() # unlocked 空 +var pool1 = ShopManager._available_pool() +_mcp_print("locked chain in pool=%s (expect false)" % str(pool1.has("action_chain_bolt"))) +_mcp_print("basic spark in pool=%s (expect true)" % str(pool1.has("action_spark_bolt"))) +MetaProgress.unlock("action_chain_bolt", 0) # 强制解锁(cost0 便于测试) +var pool2 = ShopManager._available_pool() +_mcp_print("after unlock, chain in pool=%s (expect true)" % str(pool2.has("action_chain_bolt"))) +MetaProgress.clear() +``` +Expected: false / true / true。`get_editor_errors`=0。 + +- [ ] **Step 3: Commit** +```bash +git add scripts/autoloads/shop_manager.gd +git commit -m "feat(meta): 商店池按 unlock_cost/已解锁门控(_available_pool)" +``` + +--- + +### Task 5: 死亡结算发碎片(combat_manager) + +**Files:** +- Modify: `scripts/domain/combat/combat_manager.gd:230-246`(`_on_player_died`) + +- [ ] **Step 1: 加碎片结算** + +在 `_on_player_died()` 的 `EndlessRecords.add_record(wave_reached, elapsed, kills)`(第 237 行)之后加: +```gdscript + MetaProgress.add_aether(SettingsManager.aether_for_wave(wave_reached)) +``` + +- [ ] **Step 2: 验证(MCP,需战斗场景)** + +`play_scene(main)` → `execute_game_script`: `SceneManager.start_new_game()`。下一次调用: +```gdscript +var cs = get_tree().current_scene +MetaProgress.clear() +WaveManager.current_wave = 10 +cs._cm._run_start_time = Time.get_ticks_msec() / 1000.0 +cs._cm._on_player_died({}) +_mcp_print("died at W10 -> aether=%d (expect 8)" % MetaProgress.aether) +MetaProgress.clear() +``` +Expected: `aether=8`。`get_editor_errors`=0。`stop_scene`。 + +- [ ] **Step 3: Commit** +```bash +git add scripts/domain/combat/combat_manager.gd +git commit -m "feat(meta): 死亡按到达波次结算以太碎片" +``` + +--- + +### Task 6: i18n 键(META_* / MENU_UNLOCK,4 语言) + +**Files:** +- Modify: `translations/zh_CN.po`, `zh_TW.po`, `en.po`, `ja.po` + +- [ ] **Step 1: 各 .po 末尾追加键** + +`zh_CN.po` 追加: +``` +msgid "MENU_UNLOCK" +msgstr "🔓 解锁" + +msgid "META_TITLE" +msgstr "元进展 · 解锁" + +msgid "META_AETHER" +msgstr "以太碎片: %d" + +msgid "META_UNLOCK_BTN" +msgstr "解锁 (%d)" + +msgid "META_UNLOCKED" +msgstr "已解锁" + +msgid "META_CLOSE" +msgstr "关闭" +``` +`zh_TW.po` 追加(同键,繁体):`🔓 解鎖` / `元進展 · 解鎖` / `以太碎片: %d` / `解鎖 (%d)` / `已解鎖` / `關閉`。 +`en.po` 追加:`🔓 Unlocks` / `Meta · Unlocks` / `Aether: %d` / `Unlock (%d)` / `Unlocked` / `Close`。 +`ja.po` 追加:`🔓 アンロック` / `メタ進行 · アンロック` / `エーテル: %d` / `アンロック (%d)` / `解放済み` / `閉じる`。 + +- [ ] **Step 2: 验证(MCP)** +```gdscript +for k in ["MENU_UNLOCK","META_TITLE","META_AETHER","META_UNLOCK_BTN","META_UNLOCKED","META_CLOSE"]: + _mcp_print("%s -> %s" % [k, TranslationServer.translate(k)]) +``` +Expected: 每个键返回中文译文(非键本身,zh_CN 下)。`get_editor_errors`=0。 + +- [ ] **Step 3: Commit** +```bash +git add translations/zh_CN.po translations/zh_TW.po translations/en.po translations/ja.po +git commit -m "feat(meta): META_*/MENU_UNLOCK 四语 i18n 键" +``` + +--- + +### Task 7: 主菜单「🔓 解锁」按钮 + 解锁面板 + +**Files:** +- Modify: `scenes/ui/main_menu.gd` + +- [ ] **Step 1: 成员 + _ready + 菜单项 + ESC + 语言重建** + +(a)在顶部成员区(`var _ftue_layer: CanvasLayer = null` 之后)加: +```gdscript +var _unlock_layer: CanvasLayer = null +var _unlock_root: Control = null +``` +(b)在 `_ready()` 的 `_build_ftue_panel()` 之后加 `_build_unlock_panel()`。 +(c)在 `_build_menu()` 的 `items` 数组里,`["MENU_LEADERBOARD", _on_leaderboard],` 之后插入: +```gdscript + ["MENU_UNLOCK", _on_unlock], +``` +(d)在 `_input()` 的 `elif _leaderboard_layer and _leaderboard_layer.visible:` 分支之后加: +```gdscript + elif _unlock_layer and _unlock_layer.visible: + _unlock_layer.visible = false +``` +(e)在 `_on_lang()` 里 `_leaderboard_layer = null` 之后加 `_unlock_layer = null`,并在末尾 `_build_leaderboard_panel()` 之后加 `_build_unlock_panel()`。 + +- [ ] **Step 2: 回调 + 面板构建/刷新** + +在文件末尾追加: +```gdscript +# ── 解锁面板 ───────────────────────────────────────────────── +func _on_unlock() -> void: + _unlock_layer.visible = true + _rebuild_unlock() + +func _build_unlock_panel() -> void: + _unlock_layer = CanvasLayer.new() + _unlock_layer.layer = 10 + _unlock_layer.visible = false + add_child(_unlock_layer) + var bg := ColorRect.new() + bg.color = Color(0.0, 0.0, 0.0, 0.72) + bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _unlock_layer.add_child(bg) + _unlock_root = Control.new() + _unlock_layer.add_child(_unlock_root) + +func _rebuild_unlock() -> void: + for c in _unlock_root.get_children(): + c.queue_free() + var panel := ColorRect.new() + panel.color = Color(0.06, 0.06, 0.14, 0.98) + panel.size = Vector2(560, 470) + panel.position = Vector2(300, 90) + _unlock_root.add_child(panel) + var title := Label.new() + title.text = tr("META_TITLE") + title.position = Vector2(324, 106) + title.add_theme_font_size_override("font_size", 28) + title.modulate = Color.GOLD + _unlock_root.add_child(title) + var aether_lbl := Label.new() + aether_lbl.text = tr("META_AETHER") % MetaProgress.aether + aether_lbl.position = Vector2(324, 148) + aether_lbl.add_theme_font_size_override("font_size", 18) + aether_lbl.modulate = Color(0.6, 0.85, 1.0) + _unlock_root.add_child(aether_lbl) + var y: float = 188.0 + for id in SpellRegistry.get_all_ids(): + var s: SpellNode = SpellRegistry.get_spell(id) + if s == null: + continue + var cost: int = int(s.meta.get("unlock_cost", 0)) + if cost <= 0: + continue + var name_lbl := Label.new() + name_lbl.text = s.display_name + name_lbl.position = Vector2(324, y + 4) + name_lbl.add_theme_font_size_override("font_size", 16) + _unlock_root.add_child(name_lbl) + var btn := Button.new() + btn.position = Vector2(560, y) + btn.size = Vector2(230, 32) + if MetaProgress.is_unlocked(id): + btn.text = tr("META_UNLOCKED") + btn.disabled = true + else: + btn.text = tr("META_UNLOCK_BTN") % cost + btn.disabled = MetaProgress.aether < cost + var sid := id + var scost := cost + btn.connect("pressed", func(): + if MetaProgress.unlock(sid, scost): + AudioManager.play("buy") + _rebuild_unlock()) + _unlock_root.add_child(btn) + y += 40.0 + var close := Button.new() + close.text = tr("META_CLOSE") + close.position = Vector2(700, 508) + close.size = Vector2(120, 36) + close.connect("pressed", func(): _unlock_layer.visible = false) + _unlock_root.add_child(close) +``` + +- [ ] **Step 3: 验证(MCP,编辑器内 fresh-load 往返 + 游戏内截图)** + +游戏内(`play_scene(main)`,当前场景即 MainMenu): +```gdscript +var mm = get_tree().current_scene +MetaProgress.clear(); MetaProgress.add_aether(20) +mm._on_unlock() +_mcp_print("unlock panel visible=%s aether label ok=%s" % [str(mm._unlock_layer.visible), str(mm._unlock_root.get_child_count() > 3)]) +# 找到第一个可点解锁按钮并点击(frost_bolt cost5,aether20 够) +MetaProgress.unlock("action_frost_bolt", 5) +_mcp_print("after unlock frost: aether=%d is_unlocked=%s (expect 15/true)" % [MetaProgress.aether, str(MetaProgress.is_unlocked("action_frost_bolt"))]) +MetaProgress.clear() +``` +然后 `get_game_screenshot` 确认「元进展·解锁」面板渲染出碎片数 + 6 行锁定法术 + 解锁按钮。Expected: panel visible=true。`get_editor_errors`=0。`stop_scene`。 + +- [ ] **Step 4: Commit** +```bash +git add scenes/ui/main_menu.gd +git commit -m "feat(meta): 主菜单解锁面板(花碎片解锁法术)" +``` + +--- + +### Task 8: 平衡页签支持 aether_thresholds(编辑器可调 + 保存不丢字段) + +**Files:** +- Modify: `addons/game_designer/balance_tab.gd` + +- [ ] **Step 1: 加 JSON 文本框(读)+ 保存时写回** + +(a)在成员区(`var _boss: SpinBox` 之后)加: +```gdscript +var _aether: TextEdit +``` +(b)在 `_ready()` 的 `add_child(HSeparator.new())`(Boss 血量之后、保存按钮之前)与保存按钮之间加: +```gdscript + var al := Label.new(); al.text = "碎片奖励阈值 aether_thresholds(JSON [[波次,碎片],...])"; add_child(al) + _aether = TextEdit.new(); _aether.custom_minimum_size = Vector2(0, 50) + _aether.text = JSON.stringify(_data.get("aether_thresholds", [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]])) + add_child(_aether) +``` +(c)在 `_save()` 里,构造 `out` 时加入该字段:把 +```gdscript + var out := { + "difficulty_mults": mults, + "boss_hp": {"4": _miniboss.value, "5": _boss.value}, + } +``` +改为: +```gdscript + var thresholds = JSON.parse_string(_aether.text) if _aether.text.strip_edges() != "" else [] + if not (thresholds is Array): + thresholds = [] + var out := { + "difficulty_mults": mults, + "boss_hp": {"4": _miniboss.value, "5": _boss.value}, + "aether_thresholds": thresholds, + } +``` + +- [ ] **Step 2: 验证(编辑器 MCP,fresh-load 往返)** +```gdscript +var Tab = ResourceLoader.load("res://addons/game_designer/balance_tab.gd", "GDScript", ResourceLoader.CACHE_MODE_IGNORE) +var t = Tab.new(); EditorInterface.get_base_control().add_child(t) +_mcp_print("aether box has data=%s" % str(t._aether.text.contains("["))) +# 模拟保存构造的 out(不写盘):直接读 _aether 解析 +var parsed = JSON.parse_string(t._aether.text) +_mcp_print("thresholds parse ok=%s first=%s (expect true/[1,0])" % [str(parsed is Array), str(parsed[0]) if parsed is Array else "N/A"]) +t.free() +``` +Expected: `has data=true`、`parse ok=true first=[1, 0]`。`get_editor_errors`=0(清 stale 后)。 + +- [ ] **Step 3: Commit** +```bash +git add addons/game_designer/balance_tab.gd +git commit -m "feat(meta): 平衡页签可编辑 aether_thresholds(保存不丢字段)" +``` + +--- + +### Task 9: 更新 game_design §7 现状 callout + +**Files:** +- Modify: `docs/design/game_design.md`(§7 callout) + +- [ ] **Step 1: 替换 §7 callout** + +把 `docs/design/game_design.md` §7 的「本章整体未实现」callout 替换为: +```markdown +> **⚠️ 实现现状 (2026-07-21 → MVP 部分实现)**:**碎片 + 法术解锁树已落地**——`MetaProgress` autoload(`user://meta.json` 碎片+已解锁)、死亡按波次发碎片(`balance.json.aether_thresholds` 阈值表,平衡页签可调)、主菜单「🔓解锁」面板花碎片解锁进阶法术、商店池按 `spells.json` 的 `meta.unlock_cost` 门控。**暂缺**:核心解锁(依赖货架B)、挑战模式解锁、图鉴 Codex、成就/炼金日志、通关首胜奖励。详见 `docs_dev/specs/2026-07-21-meta-progression-mvp-design.md`。 +``` + +- [ ] **Step 2: 验证** — `git grep -n "本章整体未实现" docs/design/game_design.md` 应无结果。 + +- [ ] **Step 3: Commit** +```bash +git add docs/design/game_design.md +git commit -m "docs(meta): game_design §7 现状更新为碎片+解锁树已实现(MVP)" +``` + +--- + +### Task 10: 全量回归验收 + +**Files:** 无(纯验证) + +- [ ] **Step 1: 重载 + 0 错误**:`reload_project` → `clear_output` → `get_editor_errors`=0。 + +- [ ] **Step 2: 端到端闭环**(`play_scene(main)`): +```gdscript +MetaProgress.clear() +# 模拟一局到 W10 死亡发碎片 +var cm_sm = SettingsManager.aether_for_wave(10) +MetaProgress.add_aether(cm_sm) +_mcp_print("W10 死亡得碎片=%d,总 aether=%d (expect 8/8)" % [cm_sm, MetaProgress.aether]) +# 商店池此刻不含 chain_bolt +_mcp_print("池含 chain(锁定)=%s (expect false)" % str(ShopManager._available_pool().has("action_chain_bolt"))) +# 花碎片解锁 chain(8) +_mcp_print("解锁 chain -> %s,aether=%d (expect true/0)" % [str(MetaProgress.unlock("action_chain_bolt", 8)), MetaProgress.aether]) +# 现在池含 chain +_mcp_print("池含 chain(已解锁)=%s (expect true)" % str(ShopManager._available_pool().has("action_chain_bolt"))) +MetaProgress.clear() +``` +Expected: 8/8 → false → true/0 → true。 + +- [ ] **Step 3: DoD 核对**(spec §8)逐条确认。`stop_scene`。 + +- [ ] **Step 4: Commit** +```bash +git commit --allow-empty -m "chore(meta): 元进展 MVP 验收通过(Godot 4.6 运行时实测闭环全绿)" +``` + +--- + +## 自检结论 +- **Spec 覆盖**:§2.1→T1;§2.4/碎片曲线→T2/T5;§2.2 unlock_cost→T3;§2.3 门控→T4;解锁面板→T7;i18n→T6;编辑器可调(balance_tab)→T8;清除数据→T2;docs→T9;测试→各任务+T10。无遗漏。 +- **无占位**:每步含精确文件/行号/完整代码/期望输出。 +- **类型一致**:`MetaProgress.aether/unlocked` + `add_aether/is_unlocked/unlock/clear/save_meta/load_meta`、`SettingsManager.aether_for_wave`、`ShopManager._available_pool`、`meta.unlock_cost`、`balance.json.aether_thresholds` 全程一致。