Files
spellforge/docs_dev/specs/2026-07-20-mana-system-mvp-design.md
T

165 lines
8.9 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.
# 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` 主循环、**LOOP 体(`_run_logic_loop`**、**CIRCUIT 分支(`_run_branch_payload`)** 均逐节点扣蓝、蓝空即停(复审发现 LOOP/CIRCUIT 曾漏扣、已修 `8b9565e`);唯 **SubPayload 的 on-hit 执行(`execute_sub`)不消耗**——命中回调不做资源判定。
### 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 ACTION510Tier2 ACTION2030Tier3 ACTION60+
- 召唤/地面(poison_pool / summon_turret):~2535
- 共鸣产物(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"。