Files
spellforge/docs_dev/plans/2026-07-21-mana-complete.md
T
2026-07-21 15:03:35 +08:00

616 lines
26 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 完整化实现计划(递增蓝耗 + infinite_spells + 魔力虹吸 + UI + 平衡)
> **For agentic workers:** 用 superpowers:subagent-driven-development(推荐)逐任务实现。步骤用 `- [ ]` 复选框。
**Goal:** 把 Mana MVP 补成可发布完整功能:递增蓝耗防机枪、infinite_spells 卖血续命、多来源魔力虹吸、HUD 打磨、平衡。
**Architecture:** 扣蓝统一走 `SpellEvaluator._charge_mana()`(热值乘算 + infinite 转 HP 代价);heat/leech 状态在 PlayerStats,配置在 balance.jsonSettingsManager 加载);leech 换杖重算;HP 代价累加于 SpellContext、施法末结算(15% 上限)。
**Tech Stack:** Godot 4.6 GDScript · 纯 JSON 数据驱动 · 验证用 Godot MCP,无单元测试框架。
> **验证约定**:每任务 MCP 运行时断言。前置 Godot+MCP 连通。改码后 `reload_project`+`get_editor_errors`0;新 autoload/新常量引用误报以 `play_scene` 运行时为准)+ `play_scene(main)` 后 `execute_game_script`@tool 编辑器改动用 `execute_editor_script`+`CACHE_MODE_IGNORE` fresh-load。跑完 `stop_scene`。
---
### Task 1: balance.json 配置 + SettingsManager getters
**Files:** `data/balance.json`, `scripts/autoloads/settings_manager.gd`
- [ ] **Step 1: balance.json 加配置**
`data/balance.json` 顶层对象里(`aether_thresholds` 之后、末尾 `}` 前)加两段(记得前一行补逗号):
```json
"mana_heat": { "per_cast": 0.01, "decay_per_sec": 0.1, "max": 1.0 },
"infinite_spells": { "hp_per_shot": 1.0, "hp_cost_cap_ratio": 0.15 }
```
- [ ] **Step 2: SettingsManager 加载 + getters**
`settings_manager.gd``var _AETHER_THRESHOLDS: Array = []` 之后加:
```gdscript
var _MANA_HEAT: Dictionary = {"per_cast": 0.01, "decay_per_sec": 0.1, "max": 1.0}
var _INFINITE: Dictionary = {"hp_per_shot": 1.0, "hp_cost_cap_ratio": 0.15}
```
`_load_balance()``if data.has("aether_thresholds") ...` 块之后加:
```gdscript
if data.has("mana_heat") and data["mana_heat"] is Dictionary:
for k in data["mana_heat"]:
_MANA_HEAT[k] = data["mana_heat"][k]
if data.has("infinite_spells") and data["infinite_spells"] is Dictionary:
for k in data["infinite_spells"]:
_INFINITE[k] = data["infinite_spells"][k]
```
`aether_for_wave()` 之后加 getters
```gdscript
func mana_heat_per_cast() -> float: return float(_MANA_HEAT.get("per_cast", 0.01))
func mana_heat_decay() -> float: return float(_MANA_HEAT.get("decay_per_sec", 0.1))
func mana_heat_max() -> float: return float(_MANA_HEAT.get("max", 1.0))
func infinite_hp_per_shot() -> float: return float(_INFINITE.get("hp_per_shot", 1.0))
func hp_cost_cap_ratio() -> float: return float(_INFINITE.get("hp_cost_cap_ratio", 0.15))
```
- [ ] **Step 3: 验证(MCP**`play_scene(main)`
```gdscript
_mcp_print("heat per=%s decay=%s max=%s | inf hp=%s cap=%s (expect 0.01/0.1/1/1/0.15)" % [str(SettingsManager.mana_heat_per_cast()), str(SettingsManager.mana_heat_decay()), str(SettingsManager.mana_heat_max()), str(SettingsManager.infinite_hp_per_shot()), str(SettingsManager.hp_cost_cap_ratio())])
```
Expected: 0.01/0.1/1.0/1.0/0.15。
- [ ] **Step 4: Commit**
```bash
git add data/balance.json scripts/autoloads/settings_manager.gd
git commit -m "feat(mana): balance.json mana_heat/infinite_spells 配置 + SettingsManager getters"
```
---
### Task 2: PlayerStats + SpellContext 新状态
**Files:** `scripts/autoloads/player_stats.gd`, `scripts/domain/spell_system/spell_context.gd`
- [ ] **Step 1: PlayerStats 加字段**
`player_stats.gd``var mana_regen: float = 5.0` 之后加:
```gdscript
var mana_heat: float = 0.0 # 递增蓝耗热值(0=无,1=蓝耗翻倍)
var mana_leech: float = 0.0 # 击杀回蓝量(换杖/换牌重算)
```
- [ ] **Step 2: PlayerStats 加方法**
`regen_mana()` 之后加:
```gdscript
## 击杀回蓝(魔力虹吸)
func gain_mana(n: float) -> void:
if n <= 0.0:
return
mana = min(mana_max, mana + n)
stats_changed.emit()
## 主动 HP 代价(infinite_spells / heavy_cost)——直接扣血,不走难度减伤
func spend_hp_cost(amount: float) -> void:
if amount <= 0.0:
return
hp = max(0.0, hp - amount)
stats_changed.emit()
if hp <= 0.0:
EventBus.emit(EventID.PLAYER_DIED, {})
```
- [ ] **Step 3: PlayerStats 订阅击杀回蓝**
`_ready()` 当前为:
```gdscript
func _ready() -> void:
EventBus.subscribe(EventID.PLAYER_DAMAGED, _on_player_damaged)
```
改为:
```gdscript
func _ready() -> void:
EventBus.subscribe(EventID.PLAYER_DAMAGED, _on_player_damaged)
EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)
func _on_enemy_killed(_payload: Dictionary) -> void:
gain_mana(mana_leech)
```
`reset_for_run()` 已置 mana=mana_max;无需动 mana_heat/leech,它们由 PlayerManager/combat_manager 管。)
- [ ] **Step 4: SpellContext 加 hp_cost_accum**
`spell_context.gd`:在 `var registers: PackedFloat32Array = PackedFloat32Array()` 之后加:
```gdscript
var hp_cost_accum: float = 0.0 # 本次施法累积 HP 代价(heavy_cost + infinite),施法末结算
```
`reset()``if stats: stats.reset() ...` 之后加:
```gdscript
hp_cost_accum = 0.0
```
- [ ] **Step 5: 验证(MCP**
```gdscript
PlayerStats.set_mana_pool(100.0, 5.0); PlayerStats.mana = 50.0
PlayerStats.mana_leech = 4.0
PlayerStats.gain_mana(PlayerStats.mana_leech)
_mcp_print("gain_mana 4 -> mana=%s (expect 54)" % str(PlayerStats.mana))
PlayerStats.hp = 100.0; PlayerStats.hp_max = 100.0
PlayerStats.spend_hp_cost(11.0)
_mcp_print("spend_hp 11 -> hp=%s (expect 89, 直接扣不减伤)" % str(PlayerStats.hp))
var c = SpellContext.new(); c.hp_cost_accum = 5.0; c.reset()
_mcp_print("ctx reset -> hp_cost_accum=%s (expect 0)" % str(c.hp_cost_accum))
```
Expected: 54 / 89 / 0。`get_editor_errors` 运行时无(play_scene 能跑)。
- [ ] **Step 6: Commit**
```bash
git add scripts/autoloads/player_stats.gd scripts/domain/spell_system/spell_context.gd
git commit -m "feat(mana): PlayerStats mana_heat/leech+gain_mana/spend_hp_cost+击杀回蓝;SpellContext hp_cost_accum"
```
---
### Task 3: PlayerManager 每帧衰减 heat + 换杖清零
**Files:** `scripts/domain/player_manager.gd`
- [ ] **Step 1: 每帧衰减**
`_physics_process()` 当前首行是 `PlayerStats.regen_mana(delta)`。在其后加:
```gdscript
PlayerStats.mana_heat = maxf(0.0, PlayerStats.mana_heat - SettingsManager.mana_heat_decay() * delta)
```
- [ ] **Step 2: 换杖清零 heat**
`equip_wand()``PlayerStats.set_mana_pool(...)` 分支之后加(函数末尾):
```gdscript
PlayerStats.mana_heat = 0.0
```
- [ ] **Step 3: 验证(MCP,需战斗自动施法或手动)**
```gdscript
PlayerStats.mana_heat = 0.5
_mcp_print("set heat 0.5")
```
下一次调用(间隔真实时间,_physics_process 衰减):
```gdscript
_mcp_print("heat after wait=%s (expect < 0.5,衰减中)" % str(PlayerStats.mana_heat))
```
Expected: 第二次 < 0.5(趋向 0)。
- [ ] **Step 4: Commit**
```bash
git add scripts/domain/player_manager.gd
git commit -m "feat(mana): PlayerManager 每帧衰减 mana_heat + 换杖清零"
```
---
### Task 4: SpellEvaluator — 扣蓝 helper(热值乘算 + infinite 扣血)+ heavy_cost + HP 结算
**Files:** `scripts/domain/spell_system/spell_evaluator.gd`
- [ ] **Step 1: 加 `_charge_mana` helper**
`_apply_modifier` 函数之前(`## ── 内部方法` 区)加:
```gdscript
## 统一扣蓝:base_cost>0 时按热值乘算扣蓝;infinite Core 蓝空转累加 HP 代价并继续
## 返回 true=可执行该节点;false=蓝不足且非 infinite → 停施
func _charge_mana(base_cost: float, ctx: SpellContext, infinite: bool, heat_mult: float) -> bool:
if base_cost <= 0.0:
return true
if PlayerStats.spend_mana(base_cost * heat_mult):
return true
if infinite:
ctx.hp_cost_accum += SettingsManager.infinite_hp_per_shot()
return true
return false
```
- [ ] **Step 2: execute_compiled — 计算 infinite/heat + 改门控 + 结算**
`execute_compiled``var ctx: SpellContext = _acquire_ctx(...)` 到 while 循环体,改为如下(新增 infinite/heat_mult/cast_fired,替换门控行 333-335,并把 loop/branch 调用加参数):
```gdscript
var ctx: SpellContext = _acquire_ctx(caster_id, persistent)
ctx.hp_cost_accum = 0.0
var infinite: bool = compiled.feature_tags == CoreFeatureTag.INFINITE_SPELLS
var heat_mult: float = 1.0 + PlayerStats.mana_heat
var cast_fired: bool = false
var deck: SpellDeck = compiled.make_runtime_deck()
var ops_count: int = 0
var max_ops: int = MAX_OPS_PER_CPU * 5
var actions_remaining: int = 1
while deck.has_next() and ops_count < max_ops:
ops_count += 1
var node: SpellNode = deck.pop()
var mana_cost: float = float(node.meta.get("mana_cost", 0.0))
if not _charge_mana(mana_cost, ctx, infinite, heat_mult):
break # 蓝不足且非 infinite → 停施
if mana_cost > 0.0:
cast_fired = true
match node.type:
SpellNode.SpellType.ACTION:
_push_projectile(node, ctx, spawn_pos, -1)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellNode.SpellType.MODIFIER:
_apply_modifier(node, ctx)
actions_remaining = 1 + ctx.stats.multicast_count
SpellNode.SpellType.TRIGGER:
_push_trigger(node, ctx, spawn_pos)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellNode.SpellType.LOGIC:
if node.meta.has("fork_branch_ids"):
for bid in node.meta["fork_branch_ids"]:
_run_branch_payload(int(bid), ctx, spawn_pos, infinite, heat_mult)
elif String(node.meta.get("logic_op", "")) == "loop":
ops_count = _run_logic_loop(node, ctx, spawn_pos, deck, ops_count, max_ops, infinite, heat_mult)
break
elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST:
break
```
- [ ] **Step 3: execute_compiled — 施法末结算 HP + heat**
在 while 循环之后、`if ops_count >= max_ops:` 之前插入:
```gdscript
if ctx.hp_cost_accum > 0.0:
PlayerStats.spend_hp_cost(minf(ctx.hp_cost_accum, PlayerStats.hp_max * SettingsManager.hp_cost_cap_ratio()))
if cast_fired:
PlayerStats.mana_heat = minf(SettingsManager.mana_heat_max(), PlayerStats.mana_heat + SettingsManager.mana_heat_per_cast())
```
- [ ] **Step 4: _apply_modifier — heavy_cost 累加**
`_apply_modifier` 末尾 `if meta.has("multicast"): ...` 之后加:
```gdscript
if meta.has("heavy_cost"):
ctx.hp_cost_accum += float(meta["heavy_cost"])
```
- [ ] **Step 5: _run_logic_loop — 加参数 + 改门控**
签名改为:
```gdscript
func _run_logic_loop(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2,
deck: SpellDeck, ops_count: int, max_ops: int, infinite: bool, heat_mult: float) -> int:
```
把内部门控(`var n_cost ...` / `if n_cost > 0.0 and not PlayerStats.spend_mana(n_cost): return ops_count`)替换为:
```gdscript
var n_cost: float = float(n.meta.get("mana_cost", 0.0))
if not _charge_mana(n_cost, ctx, infinite, heat_mult):
return ops_count
```
- [ ] **Step 6: _run_branch_payload — 加参数 + 改门控 + 递归传参**
签名改为:
```gdscript
func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2, infinite: bool, heat_mult: float) -> void:
```
门控替换为:
```gdscript
var node_cost: float = float(node.meta.get("mana_cost", 0.0))
if not _charge_mana(node_cost, ctx, infinite, heat_mult):
break
```
LOGIC 分支里的递归调用 `_run_branch_payload(int(bid), ctx, spawn_pos)` 改为 `_run_branch_payload(int(bid), ctx, spawn_pos, infinite, heat_mult)`
> 不要改 `execute_sub`on-hit 子荷载)——它保持不扣蓝。
- [ ] **Step 7: 验证(MCP**
```gdscript
var core = WandPreset.make_core_by_id("wand_basic")
# 热值乘算:heat=0.5 时 spark(5) 实扣 7.5
PlayerStats.set_mana_pool(100.0, 5.0); PlayerStats.mana = 100.0; PlayerStats.mana_heat = 0.5
BulletManager.reset()
SpellEvaluator.execute_compiled(SpellEvaluator.compile_wand(core, [SpellRegistry.get_spell("action_spark_bolt")]), 0, Vector2(0,0))
_mcp_print("[heat] mana=%s (expect 92.5=100-7.5), bullets=%d(1)" % [str(PlayerStats.mana), BulletManager.get_active_count()])
# 堆热:cast_fired 后 heat 上升
_mcp_print("[heat] mana_heat=%s (expect 0.51=0.5+0.01)" % str(PlayerStats.mana_heat))
# 回归:heat=0 行为不变
PlayerStats.mana_heat = 0.0; PlayerStats.mana = 100.0; BulletManager.reset()
SpellEvaluator.execute_compiled(SpellEvaluator.compile_wand(core, [SpellRegistry.get_spell("modifier_damage_plus"), SpellRegistry.get_spell("action_spark_bolt")]), 0, Vector2(0,0))
_mcp_print("[regr] mana=%s(80) bullets=%d(1)" % [str(PlayerStats.mana), BulletManager.get_active_count()])
```
Expected: mana≈92.5/1, heat 0.51, 回归 80/1。`get_editor_errors` 运行时无。
- [ ] **Step 8: Commit**
```bash
git add scripts/domain/spell_system/spell_evaluator.gd
git commit -m "feat(mana): _charge_mana 统一门控(热值乘算+infinite扣血)+heavy_cost+HP结算"
```
---
### Task 5: 魔力虹吸运行时(CoreDefinition + WandPreset + combat_manager 重算)
**Files:** `scripts/domain/spell_system/core_definition.gd`, `scripts/autoloads/wand_preset.gd`, `scripts/domain/combat/combat_manager.gd`
- [ ] **Step 1: CoreDefinition 加字段**
`core_definition.gd``@export var base_mana_regen: float = 5.0` 之后加:
```gdscript
@export var base_mana_leech: float = 0.0 # 击杀回蓝基础值(可叠加卡组内 meta.mana_leech
```
- [ ] **Step 2: WandPreset 读取**
`wand_preset.gd` `_core_from_dict``c.base_mana_regen = float(d.get("mana_regen", 5.0))` 之后加:
```gdscript
c.base_mana_leech = float(d.get("mana_leech", 0.0))
```
- [ ] **Step 3: combat_manager 重算 leech**
`combat_manager.gd` `_rebuild_wand()` 里,构建 `nodes` 之后、`_equipped_compiled = SpellEvaluator.compile_wand(...)` 之前(或函数末尾)加:
```gdscript
var leech: float = _equipped_core.base_mana_leech if _equipped_core else 0.0
for n in nodes:
if n != null:
leech += float(n.meta.get("mana_leech", 0.0))
PlayerStats.mana_leech = leech
```
- [ ] **Step 4: 验证(MCP**
```gdscript
var core = WandPreset.make_core_by_id("wand_basic")
core.base_mana_leech = 2.0
# 模拟 _rebuild_wand 的求和逻辑
var nodes = [SpellRegistry.get_spell("action_spark_bolt")]
var leech = core.base_mana_leech
for n in nodes:
leech += float(n.meta.get("mana_leech", 0.0))
_mcp_print("core leech 2 + spark(0) = %s (expect 2.0)" % str(leech))
_mcp_print("wand_basic 默认 base_mana_leech=%s (expect 0)" % str(WandPreset.make_core_by_id("wand_basic").base_mana_leech))
```
Expected: 2.0 / 0.0。(求和 + 击杀回蓝的端到端在 Task 10 战斗内验。)
- [ ] **Step 5: Commit**
```bash
git add scripts/domain/spell_system/core_definition.gd scripts/autoloads/wand_preset.gd scripts/domain/combat/combat_manager.gd
git commit -m "feat(mana): 魔力虹吸多来源(Core base + 卡组 meta.mana_leech 求和,换杖重算)"
```
---
### Task 6: 数据内容(cores.json wand_eternal / spells.json heavy_cost·mana_leech
**Files:** `data/cores.json`, `data/spells.json`
- [ ] **Step 1: cores.json 加 wand_eternal + 续航 Core leech**
`data/cores.json``circuit_fork` 条目之后(末尾 `}` 前,补逗号)加:
```json
"wand_eternal": { "display_name": "永恒法杖", "slot_count": 8, "topology": 0, "cpu_limit": 6, "cast_interval": 0.5, "feature_tags": 5, "mana_max": 40, "mana_regen": 4, "mana_leech": 2 }
```
`feature_tags: 5` = INFINITE_SPELLS;低蓝逼卖血;自带 base_mana_leech 2 作续航向。其余 Core 不加 mana_leech = 默认 0。)
- [ ] **Step 2: spells.json 加 heavy_cost + mana_leech 修正器**
`data/spells.json``modifier_pierce_plus` 条目之后加两条(补逗号,JSON 合法):
```json
"modifier_heavy_cost": { "type": 1, "display_name": "Blood Sacrifice", "description": "鲜血献祭:伤害+50,但每次施法扣 10 HP。", "element_tags": [], "meta": { "damage_add": 50.0, "heavy_cost": 10, "mana_cost": 0, "shop_cost": 26 } },
"modifier_mana_leech": { "type": 1, "display_name": "Mana Leech", "description": "魔力虹吸:击杀回复 2 魔力(放入卡组即生效)。", "element_tags": [], "meta": { "mana_leech": 2, "mana_cost": 0, "shop_cost": 22 } }
```
- [ ] **Step 3: 验证(MCP**
```gdscript
var e = WandPreset.make_core_by_id("wand_eternal")
_mcp_print("wand_eternal feat=%d(5) mana_max=%s(40) leech=%s(2)" % [e.feature_tags, str(e.base_mana_max), str(e.base_mana_leech)])
var h = SpellRegistry.get_spell("modifier_heavy_cost")
var l = SpellRegistry.get_spell("modifier_mana_leech")
_mcp_print("heavy_cost meta.heavy_cost=%s(10) dmg=%s(50) | mana_leech meta.mana_leech=%s(2)" % [str(h.meta.get("heavy_cost")), str(h.meta.get("damage_add")), str(l.meta.get("mana_leech"))])
```
Expected: feat=5/40/2heavy 10/50leech 2。`get_editor_errors` 运行时无(JSON 合法)。
- [ ] **Step 4: Commit**
```bash
git add data/cores.json data/spells.json
git commit -m "feat(mana): wand_eternal(infinite)+modifier_heavy_cost/mana_leech 数据"
```
---
### Task 7: Core 编辑器加 mana_leech 栏(编辑器可调)
**Files:** `addons/game_designer/core_tab.gd`
- [ ] **Step 1: 加成员 + 表单栏 + 读写**
a)成员区 `var _mana_regen: SpinBox` 之后加:
```gdscript
var _mana_leech: SpinBox
```
b`_ready()``_mana_regen = _row("回蓝/秒(Mana Regen)", UI.spin(0, 100, 0.5, 5))` 之后加:
```gdscript
_mana_leech = _row("击杀回蓝(Mana Leech)", UI.spin(0, 100, 0.5, 0))
```
c`_on_select()``_mana_regen.value = float(d.get("mana_regen", 5))` 之后加:
```gdscript
_mana_leech.value = float(d.get("mana_leech", 0))
```
d`_on_apply()` 的字典里,`"mana_max": int(_mana_max.value), "mana_regen": _mana_regen.value,` 之后加:
```gdscript
"mana_leech": _mana_leech.value,
```
e`_on_new()` 的默认字典里 `"mana_max": 100, "mana_regen": 5,` 之后加 `"mana_leech": 0,`
- [ ] **Step 2: 验证(编辑器 MCP fresh-load 往返)**
```gdscript
var Tab = ResourceLoader.load("res://addons/game_designer/core_tab.gd", "GDScript", ResourceLoader.CACHE_MODE_IGNORE)
var t = Tab.new(); EditorInterface.get_base_control().add_child(t)
for i in t._list.item_count:
if String(t._list.get_item_metadata(i)) == "wand_eternal":
t._on_select(i); break
_mcp_print("wand_eternal 表单 leech=%s (expect 2)" % str(t._mana_leech.value))
t._on_apply()
_mcp_print("apply 后 data.mana_leech=%s (expect 2 保留)" % str(t._data.get("wand_eternal", {}).get("mana_leech", "DROP")))
t.free()
```
Expected: 2 / 2。`get_editor_errors` 清 stale 后 0。
- [ ] **Step 3: Commit**
```bash
git add addons/game_designer/core_tab.gd
git commit -m "feat(mana): Core 编辑器加击杀回蓝(mana_leech)栏"
```
---
### Task 8: HUD 打磨(蓝条数字 + 低蓝 + 过热)+ UI_MANA i18n
**Files:** `scenes/main/combat_s2.gd`, `translations/*.po`
- [ ] **Step 1: i18n UI_MANA4 语言末尾追加)**
zh_CN: `msgid "UI_MANA"` / `msgstr "蓝 %d/%d"`zh_TW: `藍 %d/%d`en: `MP %d/%d`ja: `MP %d/%d`。(各文件加一条,前空行分隔。)
- [ ] **Step 2: 蓝条叠数字 Label**
`combat_s2.gd` 成员区 `var _mana_bar: ProgressBar = null` 之后加 `var _mana_lbl: Label = null`
`_setup_hud()` 里创建 `_mana_bar``add_child` 之后加:
```gdscript
_mana_lbl = Label.new()
_mana_lbl.position = Vector2(200, 86)
_mana_lbl.size = Vector2(170, 16)
_mana_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_mana_lbl.add_theme_font_size_override("font_size", 11)
_hud_layer.add_child(_mana_lbl)
```
- [ ] **Step 3: _refresh_hud 更新数字 + 低蓝 + 过热**
`_refresh_hud()` 里现有 `if _mana_bar: _mana_bar.max_value = ...; _mana_bar.value = ...` 块替换为:
```gdscript
if _mana_bar:
_mana_bar.max_value = PlayerStats.mana_max
_mana_bar.value = PlayerStats.mana
var frac: float = PlayerStats.mana / maxf(1.0, PlayerStats.mana_max)
var fill := _mana_bar.get_theme_stylebox("fill") as StyleBoxFlat
if fill:
if frac < 0.2:
var a: float = 0.6 + 0.4 * absf(sin(float(Time.get_ticks_msec()) / 150.0))
fill.bg_color = Color(0.8, 0.2, 0.2, a) # 低蓝暗红闪烁
else:
fill.bg_color = Color(0.3, 0.55, 1.0) # 正常蓝
# 过热:mana_heat>0 时整条泛红
_mana_bar.modulate = Color(1.0, 1.0 - clampf(PlayerStats.mana_heat, 0.0, 0.7), 1.0 - clampf(PlayerStats.mana_heat, 0.0, 0.7))
if _mana_lbl:
_mana_lbl.text = tr("UI_MANA") % [int(PlayerStats.mana), int(PlayerStats.mana_max)]
```
- [ ] **Step 4: 验证(MCP,进战斗 + 截图)**
`play_scene(main)``SceneManager.start_new_game()`;下一调用:
```gdscript
var cs = get_tree().current_scene
_mcp_print("mana_lbl exists=%s" % str(cs._mana_lbl != null))
PlayerStats.mana = PlayerStats.mana_max * 0.1 # 低蓝
PlayerStats.mana_heat = 0.5 # 过热
_mcp_print("set low mana + heat; label=%s" % cs._mana_lbl.text)
```
`get_game_screenshot` 确认蓝条有数字、低蓝暗红、泛红。Expected: label 类似 "蓝 10/100"。
- [ ] **Step 5: Commit**
```bash
git add scenes/main/combat_s2.gd translations/zh_CN.po translations/zh_TW.po translations/en.po translations/ja.po
git commit -m "feat(mana): HUD 蓝条数字+低蓝暗红+过热泛红 + UI_MANA 键"
```
---
### Task 9: 平衡调数值(基础杖续航)
**Files:** `data/cores.json`, `data/spells.json`
- [ ] **Step 1: 提高基础回蓝 + 降 spark 蓝耗**
`data/cores.json``wand_basic``"mana_regen": 5` 改为 `"mana_regen": 8``wand_memory`/`matrix_board`/`circuit_fork``mana_regen` 相应 +3(保持相对关系:memory 5→8、matrix 6→9、circuit 5→8wand_fast 4→6)。
`data/spells.json``action_spark_bolt``"mana_cost": 5` 改为 `"mana_cost": 3`
- [ ] **Step 2: 验证(MCP 手感 + 数据)**
```gdscript
_mcp_print("basic regen=%s(8) spark cost=%s(3)" % [str(WandPreset.make_core_by_id("wand_basic").base_mana_regen), str(SpellRegistry.get_spell("action_spark_bolt").meta.get("mana_cost"))])
```
起战斗观测:基础杖只放 spark 时蓝条应稳定不见底(cost3×2/s=6 vs regen8 → 净回);连打贵法术/堆射速仍会憋蓝。用 `get_game_screenshot` 记录蓝条状态。Expected: regen=8 / cost=3;战斗中基础突突不空蓝。
- [ ] **Step 3: Commit**
```bash
git add data/cores.json data/spells.json
git commit -m "balance(mana): 提高基础回蓝(→8)+降 spark 蓝耗(→3),基础杖能持续突突"
```
---
### Task 10: 更新文档 callout
**Files:** `docs/design/game_design.md`, `docs/design/numerical_design.md`
- [ ] **Step 1: game_design §4.1 callout**
把 §4.1 现有 Mana callout 的「暂缺」句改为反映已实现:将其中 `**暂缺**:递增蓝耗、\`infinite_spells\` 扣血、全局+法杖取 Min、cast_delay。` 替换为 `**已补全**:递增蓝耗、\`infinite_spells\` 扣血、魔力虹吸(多来源)、HUD 打磨。**仍暂缺**:全局+法杖取 Min、cast_delay。`。
- [ ] **Step 2: numerical_design §5.A callout**
在 §5.A「递增蓝耗」条目末尾加一行:
```markdown
> **⚠️ 实现现状 (2026-07-21 → 已实现)**:递增蓝耗已落地(`PlayerStats.mana_heat``balance.json.mana_heat` 可调,平衡页签);未做「最小帧限制/过热强制冷却」(用递增蓝耗替代)。
```
- [ ] **Step 3: Commit**
```bash
git add docs/design/game_design.md docs/design/numerical_design.md
git commit -m "docs(mana): §4.1/§5.A 现状更新为 Mana 完整化已实现"
```
---
### Task 11: 全量回归验收
**Files:** 无(纯验证)
- [ ] **Step 1: 重载 + 0 错误**`clear_output`→`reload_project`→`get_editor_errors`=0。
- [ ] **Step 2: 各机制 + 端到端**`play_scene(main)`):
```gdscript
var core = WandPreset.make_core_by_id("wand_basic")
# 递增蓝耗 + 回归
PlayerStats.set_mana_pool(100,8); PlayerStats.mana=100; PlayerStats.mana_heat=0.5
BulletManager.reset(); SpellEvaluator.execute_compiled(SpellEvaluator.compile_wand(core,[SpellRegistry.get_spell("action_spark_bolt")]),0,Vector2(0,0))
_mcp_print("[heat] mana=%s (expect 95.5 = 100 - spark3 * heat1.5) bullets=%d(1)" % [str(PlayerStats.mana), BulletManager.get_active_count()])
# infinite 扣血
var e = WandPreset.make_core_by_id("wand_eternal"); PlayerStats.set_mana_pool(e.base_mana_max, e.base_mana_regen); PlayerStats.mana=0; PlayerStats.hp=100; PlayerStats.hp_max=100; PlayerStats.mana_heat=0
BulletManager.reset(); SpellEvaluator.execute_compiled(SpellEvaluator.compile_wand(e,[SpellRegistry.get_spell("action_spark_bolt")]),0,Vector2(0,0))
_mcp_print("[inf] 蓝空施法 hp=%s(<100 扣血) bullets=%d(1)" % [str(PlayerStats.hp), BulletManager.get_active_count()])
# heavy_cost + 15%上限
PlayerStats.hp=100; PlayerStats.mana=100; PlayerStats.mana_heat=0
BulletManager.reset(); SpellEvaluator.execute_compiled(SpellEvaluator.compile_wand(core,[SpellRegistry.get_spell("modifier_heavy_cost"),SpellRegistry.get_spell("action_spark_bolt")]),0,Vector2(0,0))
_mcp_print("[heavy] hp=%s(90=100-10) bullets=%d(1)" % [str(PlayerStats.hp), BulletManager.get_active_count()])
# 持久内存回归
var mc = WandPreset.make_core_by_id("wand_memory"); var cA=SpellEvaluator.compile_wand(mc,[SpellRegistry.get_spell("logic_every_n_shots"),SpellRegistry.get_spell("action_spark_bolt")])
var r=[]; PlayerStats.mana_heat=0
for i in 4:
PlayerStats.mana=100; BulletManager.reset(); SpellEvaluator.execute_compiled(cA,0,Vector2(0,0)); r.append(BulletManager.get_active_count())
_mcp_print("[regr] every_n=%s (expect [0,0,1,0])" % str(r))
```
Expected: 各机制符合注释;回归 [0,0,1,0]。
- [ ] **Step 3: 击杀回蓝 + UI(战斗内)**:`start_new_game`,装 wand_eternal 或塞 mana_leech 卡,杀怪观察 mana 回升;截图 HUD。
- [ ] **Step 4: DoD 核对**spec §10)。`stop_scene`。
- [ ] **Step 5: Commit**
```bash
git commit --allow-empty -m "chore(mana): 完整化验收通过(Godot 4.6 运行时实测各机制+回归全绿)"
```
---
## 自检结论
- **Spec 覆盖**P1→T1/T2/T3/T4P2→T1/T2/T4/T6P3→T2/T5/T6/T7P4→T8P5→T9docs→T10;测试→各任务+T11。全覆盖。
- **无占位**:每步精确文件/完整代码/期望值。
- **类型一致**`PlayerStats.mana_heat/mana_leech`+`gain_mana/spend_hp_cost`、`SpellContext.hp_cost_accum`、`SpellEvaluator._charge_mana(base_cost,ctx,infinite,heat_mult)`+`_run_logic_loop/_run_branch_payload` 加 `infinite,heat_mult`、`CoreDefinition.base_mana_leech`、`SettingsManager.mana_heat_*/infinite_*/hp_cost_cap_ratio`、`balance.json.mana_heat/infinite_spells`、`cores.json.mana_leech`、`spells.json meta.heavy_cost/mana_leech` 全程一致。