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

26 KiB
Raw Permalink Blame History

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_errors0;新 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 之后、末尾 } 前)加两段(记得前一行补逗号):

  "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.gdvar _AETHER_THRESHOLDS: Array = [] 之后加:

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") ... 块之后加:

	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


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: 验证(MCPplay_scene(main)
_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
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.gdvar mana_regen: float = 5.0 之后加:

var mana_heat:  float = 0.0    # 递增蓝耗热值(0=无,1=蓝耗翻倍)
var mana_leech: float = 0.0    # 击杀回蓝量(换杖/换牌重算)
  • Step 2: PlayerStats 加方法

regen_mana() 之后加:


## 击杀回蓝(魔力虹吸)
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() 当前为:

func _ready() -> void:
	EventBus.subscribe(EventID.PLAYER_DAMAGED, _on_player_damaged)

改为:

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() 之后加:

var hp_cost_accum:   float = 0.0    # 本次施法累积 HP 代价(heavy_cost + infinite),施法末结算

reset()if stats: stats.reset() ... 之后加:

	hp_cost_accum = 0.0
  • Step 5: 验证(MCP
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
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)。在其后加:

	PlayerStats.mana_heat = maxf(0.0, PlayerStats.mana_heat - SettingsManager.mana_heat_decay() * delta)
  • Step 2: 换杖清零 heat

equip_wand()PlayerStats.set_mana_pool(...) 分支之后加(函数末尾):

	PlayerStats.mana_heat = 0.0
  • Step 3: 验证(MCP,需战斗自动施法或手动)
PlayerStats.mana_heat = 0.5
_mcp_print("set heat 0.5")

下一次调用(间隔真实时间,_physics_process 衰减):

_mcp_print("heat after wait=%s (expect < 0.5,衰减中)" % str(PlayerStats.mana_heat))

Expected: 第二次 < 0.5(趋向 0)。

  • Step 4: Commit
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 函数之前(## ── 内部方法 区)加:

## 统一扣蓝: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_compiledvar ctx: SpellContext = _acquire_ctx(...) 到 while 循环体,改为如下(新增 infinite/heat_mult/cast_fired,替换门控行 333-335,并把 loop/branch 调用加参数):

	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: 之前插入:

	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"): ... 之后加:

	if meta.has("heavy_cost"):
		ctx.hp_cost_accum += float(meta["heavy_cost"])
  • Step 5: _run_logic_loop — 加参数 + 改门控

签名改为:

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)替换为:

			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 — 加参数 + 改门控 + 递归传参

签名改为:

func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2, infinite: bool, heat_mult: float) -> void:

门控替换为:

		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
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
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 之后加:

@export var base_mana_leech: float = 0.0    # 击杀回蓝基础值(可叠加卡组内 meta.mana_leech
  • Step 2: WandPreset 读取

wand_preset.gd _core_from_dictc.base_mana_regen = float(d.get("mana_regen", 5.0)) 之后加:

	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(...) 之前(或函数末尾)加:

	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
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
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.jsoncircuit_fork 条目之后(末尾 } 前,补逗号)加:

  "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.jsonmodifier_pierce_plus 条目之后加两条(补逗号,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
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
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 之后加:

var _mana_leech: SpinBox

b_ready()_mana_regen = _row("回蓝/秒(Mana Regen)", UI.spin(0, 100, 0.5, 5)) 之后加:

	_mana_leech = _row("击杀回蓝(Mana Leech)", UI.spin(0, 100, 0.5, 0))

c_on_select()_mana_regen.value = float(d.get("mana_regen", 5)) 之后加:

	_mana_leech.value = float(d.get("mana_leech", 0))

d_on_apply() 的字典里,"mana_max": int(_mana_max.value), "mana_regen": _mana_regen.value, 之后加:

		"mana_leech": _mana_leech.value,

e_on_new() 的默认字典里 "mana_max": 100, "mana_regen": 5, 之后加 "mana_leech": 0,

  • Step 2: 验证(编辑器 MCP fresh-load 往返)
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
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/%den: MP %d/%dja: MP %d/%d。(各文件加一条,前空行分隔。)

  • Step 2: 蓝条叠数字 Label

combat_s2.gd 成员区 var _mana_bar: ProgressBar = null 之后加 var _mana_lbl: Label = null_setup_hud() 里创建 _mana_baradd_child 之后加:

	_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 = ... 块替换为:

	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();下一调用:

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
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.jsonwand_basic"mana_regen": 5 改为 "mana_regen": 8wand_memory/matrix_board/circuit_forkmana_regen 相应 +3(保持相对关系:memory 5→8、matrix 6→9、circuit 5→8wand_fast 4→6)。 data/spells.jsonaction_spark_bolt"mana_cost": 5 改为 "mana_cost": 3

  • Step 2: 验证(MCP 手感 + 数据)
_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
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「递增蓝耗」条目末尾加一行:

> **⚠️ 实现现状 (2026-07-21 → 已实现)**:递增蓝耗已落地(`PlayerStats.mana_heat``balance.json.mana_heat` 可调,平衡页签);未做「最小帧限制/过热强制冷却」(用递增蓝耗替代)。
  • Step 3: Commit
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_outputreload_projectget_editor_errors=0。

  • Step 2: 各机制 + 端到端play_scene(main)):

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

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_costSpellContext.hp_cost_accumSpellEvaluator._charge_mana(base_cost,ctx,infinite,heat_mult)+_run_logic_loop/_run_branch_payloadinfinite,heat_multCoreDefinition.base_mana_leechSettingsManager.mana_heat_*/infinite_*/hp_cost_cap_ratiobalance.json.mana_heat/infinite_spellscores.json.mana_leechspells.json meta.heavy_cost/mana_leech 全程一致。