# 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 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）"。
