diff --git a/docs_dev/specs/2026-07-20-mana-system-mvp-design.md b/docs_dev/specs/2026-07-20-mana-system-mvp-design.md new file mode 100644 index 0000000..70f65ea --- /dev/null +++ b/docs_dev/specs/2026-07-20-mana-system-mvp-design.md @@ -0,0 +1,164 @@ +# Mana 魔力系统 —— 精实 MVP 设计规范 + +> **日期**:2026-07-20 · **状态**:设计已确认,待写实现计划 +> **背景**:Mana 是 `game_design §4.1` 声称的核心平衡阀(限制"无限加特林"),但代码 0% 实现——无 `mana` 变量、无 `mana_cost` 数据,连 `CoreDefinition` 都缺 mana 字段。本规范定义"精实 MVP":让 Mana 成为真实可玩的资源门槛,暂不实现进阶反作弊/边缘机制。 + +--- + +## 1. 目标与范围 + +**目标**:法术施放消耗魔力;魔力随时间回复;魔力耗尽时停止施法,形成"高射速/高爆发 ↔ 魔力"的平衡张力,并有 HUD 蓝条可视化。 + +**本次 MVP 包含**: +- 单一魔力池(取自当前激活法杖 Core 的 `mana_max` + `mana_regen/秒`)。 +- 每个法术节点消耗其 `mana_cost`(按 `numerical_design §2` 权威数值)。 +- 施法时逐节点扣蓝;某节点付不起 → **停下本次施法,跳过剩余节点**(已发射的动作保留)。 +- HUD 蓝条。 +- `cores.json` 加 mana 字段、`spells.json` 加 `mana_cost`、`CoreDefinition` 加字段。 + +**明确不做(后续迭代)**: +- 持续射击**递增蓝耗**(`numerical §5.A` 反"无限蓝机枪")。 +- `infinite_spells` Core 蓝空时**扣 HP**。 +- **全局蓝池 + 法杖上限取 Min**(MVP 只有法杖单池)。 +- `cast_delay`/施法后摇重构(保持现有 `cast_interval` 自动施法节奏不变)。 + +--- + +## 2. 架构:组件与职责 + +数据状态、回蓝驱动、消耗门控三者分离,各自单一职责。 + +### 2.1 `PlayerStats`(Autoload)—— 魔力状态与操作 +新增字段与方法: +``` +var mana: float = 100.0 +var mana_max: float = 100.0 +var mana_regen: float = 5.0 # 每秒回复 + +func set_mana_pool(max_v: float, regen: float) -> void # 换杖时调用,重置 max/regen 并把 mana 夹到 [0,max] +func spend_mana(cost: float) -> bool # 够则扣除返回 true,否则不变返回 false +func regen_mana(delta: float) -> void # mana = min(mana_max, mana + mana_regen*delta);变化时 emit stats_changed +``` +- `mana` 纳入 `stats_changed`(HUD 刷新用)。 +- `reset_for_run()` 增加 `mana = mana_max`(开局满蓝)。 +- **不纳入存档**(`get_save_data`/`load_save_data` 不含 mana)——回蓝以秒计,续玩从满蓝开始即可。 +- 职责边界:只管"当前有多少蓝、能不能花、怎么回",不知道谁在花。 + +### 2.2 `CoreDefinition`(+2 字段) +``` +@export var base_mana_max: float = 100.0 +@export var base_mana_regen: float = 5.0 +``` +由 `WandPreset.make_core_by_id()` 从 `cores.json` 读入(缺省用上面默认值,向后兼容旧数据)。 + +### 2.3 `PlayerManager`(Autoload)—— 驱动 +- `equip_wand(core, compiled)`:新增 `PlayerStats.set_mana_pool(core.base_mana_max, core.base_mana_regen)`(core 为 null 时用默认)。 +- `_physics_process(delta)`:新增一行 `PlayerStats.regen_mana(delta)`(回蓝持续进行,含商店/施法间隙)。 +- 职责边界:帧驱动器,只负责"按时回蓝、按时触发施法",不持有蓝量。 + +### 2.4 `SpellEvaluator.execute_compiled`(消耗门控) + +**核心规则:扣蓝发生在节点效果"实际生效处",而非盲目逐个遍历**——否则会给被控制流跳过、并未真正发射的节点误扣蓝(现有语义"每次施法仅首个 ACTION 生效,除非 multicast/double_cast")。 + +具体扣蓝点: +- **ACTION** 实际发射时(`_push_projectile` 及其 zone/summon 变体)扣其 `mana_cost`;若付不起 → **不发射该动作,且中止本次施法剩余部分**。 +- **MODIFIER** 在 `_apply_modifier` 应用时扣其 `mana_cost`;付不起同样中止。 +- **TRIGGER** 注册 SubPayload 时扣其 `mana_cost`;命中后 SubPayload(`execute_sub`,异步 on-hit)执行**不再扣蓝**(MVP 简化,避免命中回调里做资源判定)。 +- **被控制流跳过的节点不扣蓝**:超出 `actions_remaining` 的 ACTION、被 LOGIC 门跳过的分支、被共鸣 `_consumed` 标记的输入卡,均不产生蓝耗。 +- **中止信号机制**(`ctx` 上的 depleted 标志位 vs 效果函数返回 bool 令主循环 break)留实现计划敲定;语义目标一致:一旦某要生效的节点付不起蓝,本次施法就此打住,已生效部分保留。 +- **仅主施法流消耗魔力**(`execute_compiled` 这一遍);SubPayload 的 on-hit 执行不消耗。 + +### 2.5 `combat_s2` HUD —— 蓝条 +- 在 HUD 区新增蓝色 `ProgressBar`(或等效控件),`max_value = PlayerStats.mana_max`、`value = PlayerStats.mana`。 +- 订阅 `PlayerStats.stats_changed` 刷新(HUD 已订阅该信号刷新其他数值)。 +- 文案键走 i18n(如需文字标签,新增 `UI_MANA` 四语键)。 + +--- + +## 3. 数据流 + +``` +装备法杖 → PlayerStats 蓝池置满(max/regen 来自 Core) + │ +每帧 → PlayerManager.regen_mana(delta) → 蓝量上升至 max + │ +每 cast_interval 秒 → PlayerManager 触发 execute_compiled + │ +execute_compiled 逐节点 → spend_mana(cost)?付得起就执行,付不起就 break + │ +stats_changed → HUD 蓝条更新 +``` + +--- + +## 4. 数值(权威来源 `numerical_design §2`) + +**基础法杖**:`mana_max = 100`、`mana_regen = 5/秒`。 + +**每法术 `mana_cost`**(doc ID → 实际 data ID 映射): +| doc §2 | 实际 spells.json id | mana_cost | +| :--- | :--- | :--- | +| spark_bolt | action_spark_bolt | 5 | +| double_cast | modifier_double_cast | 2 | +| spread_mod | modifier_spread_mod | 0 | +| damage_plus | modifier_damage_plus | 15 | +| trigger_hit | trigger_on_hit | 10 | +| chain_bolt | action_chain_bolt | 60 | +| nuke | (若存在) action_nuke | 200 | +| homing | (若存在) modifier_homing | 40 | + +**赋值规则**(`spells.json` 中未被 §2 列出的法术):按类型/Tier 给缺省—— +- LOGIC / 纯控制节点:0 +- 小修正(如 pierce+):~8 +- Tier1 ACTION:5–10;Tier2 ACTION:20–30;Tier3 ACTION:60+ +- 召唤/地面(poison_pool / summon_turret):~25–35 +- 共鸣产物(plasma_storm,不在商店):0;且被 `_consumed` 标记的输入卡按 §2.4 不扣蓝 → **共鸣在 MVP 里整体免蓝**,作为凑出组合的奖励(可接受;后续如需再收费另议) + +> 实现时以实际 `data/spells.json` 的完整 id 列表为准逐条填 `meta.mana_cost`;本表为权威值锚点。 + +--- + +## 5. 边界与默认(已确认) + +- **开局/开波满蓝**:`reset_for_run` 置满;波间在商店持续回蓝。 +- **不存档**:续玩从满蓝开始。 +- **蓝空施法**:本次施法不产出任何东西,但 `cast_timer` 照常重置——形成"憋蓝"手感门槛。 +- **`nuke`(200) > 基础 max(100)**:基础法杖无法施放核弹,须换更高蓝上限的 Core(有意的构建门槛)。 +- **换杖夹值**:`set_mana_pool` 把当前 `mana` 夹到新 `[0, max]`。 + +--- + +## 6. 测试计划(Godot MCP 运行时实测,同 bug 修复流程) + +1. **回蓝**:`mana` 从低值随时间上升并封顶于 `mana_max`。 +2. **逐节点扣蓝**:满蓝下施放 `[modifier_damage_plus(15), action_spark_bolt(5)]` → `mana` 减少 20。 +3. **停下跳过(首节点付不起)**:`mana=3`、deck `[action_spark_bolt(5), ...]` → 无子弹产出(`BulletManager.get_active_count()==0`)。 +4. **部分施放**:`mana=8`、deck `[spark(5), spark(5)]` + double_cast 令两发都尝试 → 第 1 发发射、第 2 发被跳过。 +5. **HUD**:蓝条 `value` 随施放/回蓝变化;截图确认蓝条渲染。 +6. **回归**:既有法术链(伤害、pierce、persistent memory `[0,0,1,0]`)在加入扣蓝后行为不变(付得起时)。 + +--- + +## 7. 涉及文件清单 + +| 文件 | 改动 | +| :--- | :--- | +| `scripts/autoloads/player_stats.gd` | +mana 状态/方法、reset/stats_changed 集成 | +| `scripts/domain/spell_system/core_definition.gd` | +`base_mana_max`/`base_mana_regen` | +| `scripts/autoloads/wand_preset.gd` | 从 cores.json 读入两字段 | +| `scripts/domain/player_manager.gd` | equip 时置池、_physics_process 回蓝 | +| `scripts/domain/spell_system/spell_evaluator.gd` | execute_compiled 逐节点扣蓝门控 | +| `scenes/main/combat_s2.gd` | HUD 蓝条 + stats_changed 刷新 | +| `data/cores.json` | 每 Core +mana_max/mana_regen | +| `data/spells.json` | 每法术 +mana_cost | +| `translations/*.po` | (如需)`UI_MANA` 四语键 | + +--- + +## 8. 完成定义 (DoD) + +- 上述 6 项测试在 Godot 4.6 运行时全部通过、0 编译错误。 +- 基础法杖满蓝 100、回蓝 5/s;连打 spark_bolt 会见蓝下降并在停火后回满。 +- 纯 Modifier 或蓝不足时不产出子弹(与既有 Deck 防呆互补)。 +- HUD 蓝条可见并实时反映蓝量。 +- 文档 `numerical_design`/`core_wand_design` 的 Mana 现状 callout 更新为"已实现(MVP)"。