docs: Mana 完整化实现计划(11 任务,MCP 运行时验证)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
# 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.json(SettingsManager 加载);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/2;heavy 10/50;leech 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_MANA(4 语言末尾追加)**
|
||||
|
||||
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→8;wand_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/T4;P2→T1/T2/T4/T6;P3→T2/T5/T6/T7;P4→T8;P5→T9;docs→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` 全程一致。
|
||||
Reference in New Issue
Block a user