From 7025f283f86453e4165de3272bb072d47e2de2f7 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:09:28 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat(mana):=20balance.json=20mana=5Fheat/?= =?UTF-8?q?infinite=5Fspells=20=E9=85=8D=E7=BD=AE=20+=20SettingsManager=20?= =?UTF-8?q?getters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/balance.json | 4 +++- scripts/autoloads/settings_manager.gd | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/data/balance.json b/data/balance.json index 66f52df..faea819 100644 --- a/data/balance.json +++ b/data/balance.json @@ -9,5 +9,7 @@ "4": 450.0, "5": 1800.0 }, - "aether_thresholds": [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]] + "aether_thresholds": [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]], + "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 } } diff --git a/scripts/autoloads/settings_manager.gd b/scripts/autoloads/settings_manager.gd index a897ad8..6b1c587 100644 --- a/scripts/autoloads/settings_manager.gd +++ b/scripts/autoloads/settings_manager.gd @@ -34,6 +34,8 @@ func complete_ftue(chosen_difficulty: int) -> void: var _MULTS: Dictionary = {} # key → [初学者, 标准, 挑战] var _BOSS_HP: Dictionary = {} # 原型→基础血量 var _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} const BALANCE_JSON: String = "res://data/balance.json" @@ -61,6 +63,12 @@ func aether_for_wave(wave: int) -> int: result = maxi(result, int(entry[1])) # 取 ≤wave 阶梯中的最大值,容忍无序表(设计师手填不必排序) return result +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)) + ## 数据驱动:加载 balance.json(难度乘子 + Boss 血量) func _load_balance() -> void: if not FileAccess.file_exists(BALANCE_JSON): @@ -77,6 +85,12 @@ func _load_balance() -> void: _BOSS_HP = data["boss_hp"] if data.has("aether_thresholds") and data["aether_thresholds"] is Array: _AETHER_THRESHOLDS = data["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] func difficulty_name() -> String: return ["DIFF_BEGINNER", "DIFF_STANDARD", "DIFF_CHALLENGE"][difficulty] From a57348a9f7a6462d84fb1491c34478928691b41e Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:10:54 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(mana):=20PlayerStats=20mana=5Fheat/l?= =?UTF-8?q?eech+gain=5Fmana/spend=5Fhp=5Fcost+=E5=87=BB=E6=9D=80=E5=9B=9E?= =?UTF-8?q?=E8=93=9D=EF=BC=9BSpellContext=20hp=5Fcost=5Faccum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/autoloads/player_stats.gd | 22 ++++++++++++++++++++ scripts/domain/spell_system/spell_context.gd | 2 ++ 2 files changed, 24 insertions(+) diff --git a/scripts/autoloads/player_stats.gd b/scripts/autoloads/player_stats.gd index abf4275..80614fc 100644 --- a/scripts/autoloads/player_stats.gd +++ b/scripts/autoloads/player_stats.gd @@ -26,9 +26,15 @@ var resistance: float = 0.0 # 0.0~1.0 var mana: float = 100.0 var mana_max: float = 100.0 var mana_regen: float = 5.0 # 每秒回复 +var mana_heat: float = 0.0 # 递增蓝耗热值(0=无,1=蓝耗翻倍) +var mana_leech: float = 0.0 # 击杀回蓝量(换杖/换牌重算) 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) ## 升级曲线:roundf(10 × 1.4^(level-1)),P6-N16 ## Level 1→2: 10 XP; Level 5→6: ≈ 54 XP @@ -91,6 +97,22 @@ func regen_mana(delta: float) -> void: return mana = min(mana_max, mana + mana_regen * delta) +## 击杀回蓝(魔力虹吸) +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, {}) + func _on_player_damaged(payload: Dictionary) -> void: take_damage(float(payload.get("damage", 0.0))) diff --git a/scripts/domain/spell_system/spell_context.gd b/scripts/domain/spell_system/spell_context.gd index 6bfe0f2..af78787 100644 --- a/scripts/domain/spell_system/spell_context.gd +++ b/scripts/domain/spell_system/spell_context.gd @@ -9,6 +9,7 @@ var stats: CastStats = null # 本次施法的修改层 ## 跨帧持久寄存器(长度固定 4,P6-N48) ## R1-R4:供 LOGIC_EVERY_N_SHOTS 等指令跨帧计数 var registers: PackedFloat32Array = PackedFloat32Array() +var hp_cost_accum: float = 0.0 # 本次施法累积 HP 代价(heavy_cost + infinite),施法末结算 func _init() -> void: stats = CastStats.new() @@ -21,6 +22,7 @@ func reset() -> void: stats.reset() else: stats = CastStats.new() + hp_cost_accum = 0.0 # registers 小心:仅当 Core 没有 PERSISTENT_MEMORY Feature 时才清零 # 具体清空逻辑在 SpellContextPool.acquire_for_wand() 中处理 From c9f560c757d267c5b1e138ba28df03cb043fa3ec Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:12:00 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(mana):=20PlayerManager=20=E6=AF=8F?= =?UTF-8?q?=E5=B8=A7=E8=A1=B0=E5=87=8F=20mana=5Fheat=20+=20=E6=8D=A2?= =?UTF-8?q?=E6=9D=96=E6=B8=85=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/domain/player_manager.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/domain/player_manager.gd b/scripts/domain/player_manager.gd index 4731359..b8b2339 100644 --- a/scripts/domain/player_manager.gd +++ b/scripts/domain/player_manager.gd @@ -29,6 +29,7 @@ func _ready() -> void: func _physics_process(delta: float) -> void: PlayerStats.regen_mana(delta) + PlayerStats.mana_heat = maxf(0.0, PlayerStats.mana_heat - SettingsManager.mana_heat_decay() * delta) _handle_movement(delta) _handle_charge_state(delta) _handle_auto_cast(delta) @@ -90,6 +91,7 @@ func equip_wand(core: CoreDefinition, compiled: CompiledDeck) -> void: PlayerStats.set_mana_pool(core.base_mana_max, core.base_mana_regen) else: PlayerStats.set_mana_pool(100.0, 5.0) + PlayerStats.mana_heat = 0.0 func get_position() -> Vector2: return _position From 6b60f6e07367ab10cf42aae3d94dbc360982b899 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:15:07 +0800 Subject: [PATCH 04/12] =?UTF-8?q?feat(mana):=20=5Fcharge=5Fmana=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E9=97=A8=E6=8E=A7=EF=BC=88=E7=83=AD=E5=80=BC?= =?UTF-8?q?=E4=B9=98=E7=AE=97+infinite=E6=89=A3=E8=A1=80=EF=BC=89+heavy=5F?= =?UTF-8?q?cost+HP=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/spell_system/spell_evaluator.gd | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/scripts/domain/spell_system/spell_evaluator.gd b/scripts/domain/spell_system/spell_evaluator.gd index 5cc10c2..e2376b3 100644 --- a/scripts/domain/spell_system/spell_evaluator.gd +++ b/scripts/domain/spell_system/spell_evaluator.gd @@ -323,6 +323,10 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 return var persistent: bool = compiled.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY 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 @@ -331,8 +335,10 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 ops_count += 1 var node: SpellNode = deck.pop() var mana_cost: float = float(node.meta.get("mana_cost", 0.0)) - if mana_cost > 0.0 and not PlayerStats.spend_mana(mana_cost): - break # 蓝不足 → 停止本次施法(此节点不执行、剩余跳过) + 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) @@ -341,7 +347,6 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 break SpellNode.SpellType.MODIFIER: _apply_modifier(node, ctx) - # multicast_count 已在 _apply_modifier 内更新 actions_remaining = 1 + ctx.stats.multicast_count SpellNode.SpellType.TRIGGER: _push_trigger(node, ctx, spawn_pos) @@ -350,14 +355,17 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 break SpellNode.SpellType.LOGIC: if node.meta.has("fork_branch_ids"): - # CIRCUIT 分叉:各分支在施法点立即并行执行 for bid in node.meta["fork_branch_ids"]: - _run_branch_payload(int(bid), ctx, spawn_pos) + _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) + ops_count = _run_logic_loop(node, ctx, spawn_pos, deck, ops_count, max_ops, infinite, heat_mult) break # LOOP 已消耗剩余节点 elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST: - break # 条件不成立 → 跳过后续法术 + break + 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()) if ops_count >= max_ops: push_warning("SpellEvaluator: MAX_OPS reached (caster=%d)" % caster_id) _release_ctx(ctx, persistent) @@ -487,6 +495,18 @@ func _push_trigger(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, trigg 0, 0.0, cold ) +## 统一扣蓝: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 + ## MODIFIER 分支 func _apply_modifier(node: SpellNode, ctx: SpellContext) -> void: var meta: Dictionary = node.meta @@ -504,6 +524,8 @@ func _apply_modifier(node: SpellNode, ctx: SpellContext) -> void: ctx.stats.multicast_count += int(meta["multicast"]) if meta.has("pierce"): ctx.stats.pierce_add += int(meta["pierce"]) + if meta.has("heavy_cost"): + ctx.hp_cost_accum += float(meta["heavy_cost"]) ## ── 上下文获取(区分跨帧持久 / 池化)───────────────────────── func _acquire_ctx(caster_id: int, persistent: bool) -> SpellContext: @@ -553,7 +575,7 @@ func _eval_logic(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> int: ## 简化实现:LOOP 体内不再处理嵌套 LOGIC(顺延后续迭代);尊重 ops 预算 ## 返回更新后的 ops_count func _run_logic_loop(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, - deck: SpellDeck, ops_count: int, max_ops: int) -> int: + deck: SpellDeck, ops_count: int, max_ops: int, infinite: bool, heat_mult: float) -> int: var count: int = clampi(int(node.meta.get("count", 2)), 1, 16) var body: Array = [] while deck.has_next(): @@ -564,8 +586,8 @@ func _run_logic_loop(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, if ops_count >= max_ops: return ops_count var n_cost: float = float(n.meta.get("mana_cost", 0.0)) - if n_cost > 0.0 and not PlayerStats.spend_mana(n_cost): - return ops_count # 蓝不足 → 停止整个 LOOP + if not _charge_mana(n_cost, ctx, infinite, heat_mult): + return ops_count match n.type: SpellNode.SpellType.ACTION: _push_projectile(n, ctx, spawn_pos, -1) @@ -579,7 +601,7 @@ func _run_logic_loop(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, ## ── CIRCUIT 分支执行(LOGIC_FORK 触发)────────────────────── ## 在施法点立即执行某分支 SubPayload 的节点;分支内 MODIFIER 作用域隔离(不泄漏给兄弟分支/主链) ## 嵌套 LOGIC_FORK 递归展开(P6-N66) -func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2) -> void: +func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2, infinite: bool, heat_mult: float) -> void: var nodes: Array = SubPayloadRegistry.get_nodes(payload_id) if nodes.is_empty(): return @@ -595,8 +617,8 @@ func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2) var b_crit: float = s.crit_chance for node in nodes: var node_cost: float = float(node.meta.get("mana_cost", 0.0)) - if node_cost > 0.0 and not PlayerStats.spend_mana(node_cost): - break # 蓝不足 → 停止本分支 + if not _charge_mana(node_cost, ctx, infinite, heat_mult): + break match node.type: SpellNode.SpellType.ACTION: _push_projectile(node, ctx, spawn_pos, -1) @@ -607,7 +629,7 @@ func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2) 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) + _run_branch_payload(int(bid), ctx, spawn_pos, infinite, heat_mult) elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST: break # 还原 stats From b55e6eaadffbe4f5a43fb131e786cdcbc622c68d Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:16:37 +0800 Subject: [PATCH 05/12] =?UTF-8?q?feat(mana):=20=E9=AD=94=E5=8A=9B=E8=99=B9?= =?UTF-8?q?=E5=90=B8=E5=A4=9A=E6=9D=A5=E6=BA=90=EF=BC=88Core=20base=20+=20?= =?UTF-8?q?=E5=8D=A1=E7=BB=84=20meta.mana=5Fleech=20=E6=B1=82=E5=92=8C?= =?UTF-8?q?=EF=BC=8C=E6=8D=A2=E6=9D=96=E9=87=8D=E7=AE=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/autoloads/wand_preset.gd | 1 + scripts/domain/combat/combat_manager.gd | 5 +++++ scripts/domain/spell_system/core_definition.gd | 1 + 3 files changed, 7 insertions(+) diff --git a/scripts/autoloads/wand_preset.gd b/scripts/autoloads/wand_preset.gd index aeb12ba..29f8254 100644 --- a/scripts/autoloads/wand_preset.gd +++ b/scripts/autoloads/wand_preset.gd @@ -37,6 +37,7 @@ func _core_from_dict(cid: String, d: Dictionary) -> CoreDefinition: c.edges = d.get("edges", []) c.base_mana_max = float(d.get("mana_max", 100.0)) c.base_mana_regen = float(d.get("mana_regen", 5.0)) + c.base_mana_leech = float(d.get("mana_leech", 0.0)) return c ## 默认装备(combat_test 等用):wand_basic + 第一个 ACTION 法术 diff --git a/scripts/domain/combat/combat_manager.gd b/scripts/domain/combat/combat_manager.gd index 4f5fced..ed8e797 100644 --- a/scripts/domain/combat/combat_manager.gd +++ b/scripts/domain/combat/combat_manager.gd @@ -256,6 +256,11 @@ func _rebuild_wand() -> void: continue nodes.append(SpellRegistry.get_spell(sid)) # 纯数据驱动;未知 id → null(下游按位置跳过) _equipped_compiled = SpellEvaluator.compile_wand(_equipped_core, nodes) + 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 # ── 法杖存档(ProfileManager A/B 槽,ADR-A2)───────────────── func get_wand_save_data() -> Dictionary: diff --git a/scripts/domain/spell_system/core_definition.gd b/scripts/domain/spell_system/core_definition.gd index b360c19..1f58269 100644 --- a/scripts/domain/spell_system/core_definition.gd +++ b/scripts/domain/spell_system/core_definition.gd @@ -30,3 +30,4 @@ var topology: int = Topology.LINEAR ## 魔力池(MVP)——由 WandPreset 从 cores.json 读入 @export var base_mana_max: float = 100.0 @export var base_mana_regen: float = 5.0 # 每秒回复 +@export var base_mana_leech: float = 0.0 # 击杀回蓝基础值(可叠加卡组内 meta.mana_leech) From 37f4d45e9fc77ba49ae619b5ea92fd0c6f4549bc Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:18:07 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat(mana):=20wand=5Feternal(infinite)+mo?= =?UTF-8?q?difier=5Fheavy=5Fcost/mana=5Fleech=20=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/cores.json | 3 ++- data/spells.json | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/data/cores.json b/data/cores.json index 61b0f28..5863bfb 100644 --- a/data/cores.json +++ b/data/cores.json @@ -3,5 +3,6 @@ "wand_fast": { "display_name": "高速法杯", "slot_count": 3, "topology": 0, "cpu_limit": 3, "cast_interval": 0.25, "feature_tags": 0, "mana_max": 60, "mana_regen": 4 }, "wand_memory": { "display_name": "记忆法杖", "slot_count": 6, "topology": 0, "cpu_limit": 6, "cast_interval": 0.5, "feature_tags": 1, "mana_max": 100, "mana_regen": 5 }, "matrix_board": { "display_name": "矩阵板", "slot_count": 8, "topology": 1, "cpu_limit": 8, "cast_interval": 0.5, "feature_tags": 0, "grid_rows": 2, "grid_cols": 4, "mana_max": 120, "mana_regen": 6 }, - "circuit_fork": { "display_name": "分叉回路", "slot_count": 3, "topology": 2, "cpu_limit": 5, "cast_interval": 0.6, "feature_tags": 0, "edges": [{"from": 0, "to": 1}, {"from": 0, "to": 2}], "mana_max": 100, "mana_regen": 5 } + "circuit_fork": { "display_name": "分叉回路", "slot_count": 3, "topology": 2, "cpu_limit": 5, "cast_interval": 0.6, "feature_tags": 0, "edges": [{"from": 0, "to": 1}, {"from": 0, "to": 2}], "mana_max": 100, "mana_regen": 5 }, + "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 } } diff --git a/data/spells.json b/data/spells.json index 0bfd217..38dd70e 100644 --- a/data/spells.json +++ b/data/spells.json @@ -14,6 +14,8 @@ "modifier_spread_mod": { "type": 1, "display_name": "Spread", "description": "后续法术多发一颗子弹。", "element_tags": [], "meta": { "spread_add": 1, "mana_cost": 0, "shop_cost": 18 } }, "modifier_double_cast": { "type": 1, "display_name": "Double Cast", "description": "本次施法额外执行 1 个 ACTION。", "element_tags": [], "meta": { "multicast": 1, "mana_cost": 2, "shop_cost": 25 } }, "modifier_pierce_plus": { "type": 1, "display_name": "Pierce +2", "description": "后续法术穿透 +2。", "element_tags": [], "meta": { "pierce": 2, "mana_cost": 8, "shop_cost": 18 } }, + "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 } }, "trigger_on_hit": { "type": 2, "display_name": "Trigger: On Hit","description": "命中时在命中点执行后续法术(子母弹)。", "element_tags": [], "meta": { "base_damage": 3.0, "speed": 250.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 10, "shop_cost": 30 } }, "logic_every_n_shots": { "type": 3, "display_name": "Every 3 Shots","description": "每第 3 次施法才执行后续(需记忆法杖)。", "element_tags": [], "meta": { "logic_op": "every_n_shots", "n": 3, "reg": 0, "mana_cost": 0, "shop_cost": 24 } }, "logic_if_hp_below": { "type": 3, "display_name": "If HP < 50%", "description": "仅当生命低于 50% 时执行后续。", "element_tags": [], "meta": { "logic_op": "if_hp_below", "threshold": 0.5, "mana_cost": 0, "shop_cost": 22 } }, From 301f731e6ce4ec362084640f39fd5aadae5972c0 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:19:42 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat(mana):=20Core=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=8A=A0=E5=87=BB=E6=9D=80=E5=9B=9E=E8=93=9D(mana=5Fl?= =?UTF-8?q?eech)=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/game_designer/core_tab.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/game_designer/core_tab.gd b/addons/game_designer/core_tab.gd index 890c981..5947847 100644 --- a/addons/game_designer/core_tab.gd +++ b/addons/game_designer/core_tab.gd @@ -18,6 +18,7 @@ var _cpu: SpinBox var _interval: SpinBox var _mana_max: SpinBox var _mana_regen: SpinBox +var _mana_leech: SpinBox var _feat: OptionButton var _rows: SpinBox var _cols: SpinBox @@ -43,6 +44,7 @@ func _ready() -> void: _interval = _row("施法间隔(s)", UI.spin(0.05, 5, 0.05, 0.5)) _mana_max = _row("蓝上限(Mana Max)", UI.spin(0, 1000, 5, 100)) _mana_regen = _row("回蓝/秒(Mana Regen)", UI.spin(0, 100, 0.5, 5)) + _mana_leech = _row("击杀回蓝(Mana Leech)", UI.spin(0, 100, 0.5, 0)) _feat = _row("特性", UI.opt(FEAT, 0)) _rows = _row("矩阵行(MATRIX)", UI.spin(1, 8, 1, 1)) _cols = _row("矩阵列(MATRIX)", UI.spin(1, 16, 1, 5)) @@ -87,6 +89,7 @@ func _on_select(idx: int) -> void: _interval.value = float(d.get("cast_interval", 0.5)) _mana_max.value = float(d.get("mana_max", 100)) _mana_regen.value = float(d.get("mana_regen", 5)) + _mana_leech.value = float(d.get("mana_leech", 0)) _feat.selected = int(d.get("feature_tags", 0)) _rows.value = float(d.get("grid_rows", 1)) _cols.value = float(d.get("grid_cols", 5)) @@ -106,6 +109,7 @@ func _on_apply() -> void: "topology": _topo.selected, "cpu_limit": int(_cpu.value), "cast_interval": _interval.value, "feature_tags": _feat.selected, "mana_max": int(_mana_max.value), "mana_regen": _mana_regen.value, + "mana_leech": _mana_leech.value, "grid_rows": int(_rows.value), "grid_cols": int(_cols.value), "edges": edges, } _cur = cid @@ -118,7 +122,7 @@ func _on_new() -> void: while _data.has(cid): cid = "new_core_%d" % i; i += 1 _data[cid] = {"display_name": cid, "slot_count": 5, "topology": 0, "cpu_limit": 5, "cast_interval": 0.5, "feature_tags": 0, - "mana_max": 100, "mana_regen": 5, "edges": []} + "mana_max": 100, "mana_regen": 5, "mana_leech": 0, "edges": []} _refresh() func _on_delete() -> void: From 9fb7131323a6b8e0b52cbf6dedbe95d9ededb155 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:21:19 +0800 Subject: [PATCH 08/12] =?UTF-8?q?feat(mana):=20HUD=20=E8=93=9D=E6=9D=A1?= =?UTF-8?q?=E6=95=B0=E5=AD=97+=E4=BD=8E=E8=93=9D=E6=9A=97=E7=BA=A2+?= =?UTF-8?q?=E8=BF=87=E7=83=AD=E6=B3=9B=E7=BA=A2=20+=20UI=5FMANA=20?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scenes/main/combat_s2.gd | 19 +++++++++++++++++++ translations/en.po | 3 +++ translations/ja.po | 3 +++ translations/zh_CN.po | 3 +++ translations/zh_TW.po | 3 +++ 5 files changed, 31 insertions(+) diff --git a/scenes/main/combat_s2.gd b/scenes/main/combat_s2.gd index 9925794..a56f58d 100644 --- a/scenes/main/combat_s2.gd +++ b/scenes/main/combat_s2.gd @@ -27,6 +27,7 @@ var _lbl_dps: Label = null var _lbl_charge: Label = null var _lbl_statuses: Label = null var _mana_bar: ProgressBar = null +var _mana_lbl: Label = null var _dps_poll_timer: float = 0.0 @@ -243,6 +244,12 @@ func _setup_hud() -> void: mana_bg.bg_color = Color(0.12, 0.12, 0.22) _mana_bar.add_theme_stylebox_override("background", mana_bg) _hud_layer.add_child(_mana_bar) + _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) _lbl_status = make_label.call(Vector2(20, 620), 20) _lbl_status.modulate = Color.YELLOW @@ -341,6 +348,18 @@ func _refresh_hud() -> void: 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) + var h: float = clampf(PlayerStats.mana_heat, 0.0, 0.7) + _mana_bar.modulate = Color(1.0, 1.0 - h, 1.0 - h) + if _mana_lbl: + _mana_lbl.text = tr("UI_MANA") % [int(PlayerStats.mana), int(PlayerStats.mana_max)] func _refresh_shop_ui() -> void: for i in _shop_btns.size(): diff --git a/translations/en.po b/translations/en.po index 96903a0..096bf06 100644 --- a/translations/en.po +++ b/translations/en.po @@ -212,3 +212,6 @@ msgstr "Unlocked" msgid "META_CLOSE" msgstr "Close" + +msgid "UI_MANA" +msgstr "MP %d/%d" diff --git a/translations/ja.po b/translations/ja.po index 587531b..6e85c02 100644 --- a/translations/ja.po +++ b/translations/ja.po @@ -212,3 +212,6 @@ msgstr "解放済み" msgid "META_CLOSE" msgstr "閉じる" + +msgid "UI_MANA" +msgstr "MP %d/%d" diff --git a/translations/zh_CN.po b/translations/zh_CN.po index 437044e..56d23ad 100644 --- a/translations/zh_CN.po +++ b/translations/zh_CN.po @@ -212,3 +212,6 @@ msgstr "已解锁" msgid "META_CLOSE" msgstr "关闭" + +msgid "UI_MANA" +msgstr "蓝 %d/%d" diff --git a/translations/zh_TW.po b/translations/zh_TW.po index 4fc394a..68bc4ae 100644 --- a/translations/zh_TW.po +++ b/translations/zh_TW.po @@ -212,3 +212,6 @@ msgstr "已解鎖" msgid "META_CLOSE" msgstr "關閉" + +msgid "UI_MANA" +msgstr "藍 %d/%d" From 81f53e9435ccad6c502afde9dac938d81542d773 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:23:04 +0800 Subject: [PATCH 09/12] =?UTF-8?q?balance(mana):=20=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E5=9B=9E=E8=93=9D(+3)+=E9=99=8D=20spark=20?= =?UTF-8?q?=E8=93=9D=E8=80=97(=E2=86=923)=EF=BC=8C=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E6=9D=96=E8=83=BD=E6=8C=81=E7=BB=AD=E7=AA=81=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/cores.json | 10 +++++----- data/spells.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/cores.json b/data/cores.json index 5863bfb..8296371 100644 --- a/data/cores.json +++ b/data/cores.json @@ -1,8 +1,8 @@ { - "wand_basic": { "display_name": "小木法杯", "slot_count": 5, "topology": 0, "cpu_limit": 5, "cast_interval": 0.5, "feature_tags": 0, "mana_max": 100, "mana_regen": 5 }, - "wand_fast": { "display_name": "高速法杯", "slot_count": 3, "topology": 0, "cpu_limit": 3, "cast_interval": 0.25, "feature_tags": 0, "mana_max": 60, "mana_regen": 4 }, - "wand_memory": { "display_name": "记忆法杖", "slot_count": 6, "topology": 0, "cpu_limit": 6, "cast_interval": 0.5, "feature_tags": 1, "mana_max": 100, "mana_regen": 5 }, - "matrix_board": { "display_name": "矩阵板", "slot_count": 8, "topology": 1, "cpu_limit": 8, "cast_interval": 0.5, "feature_tags": 0, "grid_rows": 2, "grid_cols": 4, "mana_max": 120, "mana_regen": 6 }, - "circuit_fork": { "display_name": "分叉回路", "slot_count": 3, "topology": 2, "cpu_limit": 5, "cast_interval": 0.6, "feature_tags": 0, "edges": [{"from": 0, "to": 1}, {"from": 0, "to": 2}], "mana_max": 100, "mana_regen": 5 }, + "wand_basic": { "display_name": "小木法杯", "slot_count": 5, "topology": 0, "cpu_limit": 5, "cast_interval": 0.5, "feature_tags": 0, "mana_max": 100, "mana_regen": 8 }, + "wand_fast": { "display_name": "高速法杯", "slot_count": 3, "topology": 0, "cpu_limit": 3, "cast_interval": 0.25, "feature_tags": 0, "mana_max": 60, "mana_regen": 6 }, + "wand_memory": { "display_name": "记忆法杖", "slot_count": 6, "topology": 0, "cpu_limit": 6, "cast_interval": 0.5, "feature_tags": 1, "mana_max": 100, "mana_regen": 8 }, + "matrix_board": { "display_name": "矩阵板", "slot_count": 8, "topology": 1, "cpu_limit": 8, "cast_interval": 0.5, "feature_tags": 0, "grid_rows": 2, "grid_cols": 4, "mana_max": 120, "mana_regen": 9 }, + "circuit_fork": { "display_name": "分叉回路", "slot_count": 3, "topology": 2, "cpu_limit": 5, "cast_interval": 0.6, "feature_tags": 0, "edges": [{"from": 0, "to": 1}, {"from": 0, "to": 2}], "mana_max": 100, "mana_regen": 8 }, "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 } } diff --git a/data/spells.json b/data/spells.json index 38dd70e..74e24ee 100644 --- a/data/spells.json +++ b/data/spells.json @@ -1,5 +1,5 @@ { - "action_spark_bolt": { "type": 0, "display_name": "Spark Bolt", "description": "发射一颗电光飞弹,造成 3 伤害。", "element_tags": [], "meta": { "base_damage": 3.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 5, "shop_cost": 15 } }, + "action_spark_bolt": { "type": 0, "display_name": "Spark Bolt", "description": "发射一颗电光飞弹,造成 3 伤害。", "element_tags": [], "meta": { "base_damage": 3.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 3, "shop_cost": 15 } }, "action_energy_orb": { "type": 0, "display_name": "Energy Orb", "description": "较慢但高伤害的能量球。", "element_tags": [], "meta": { "base_damage": 8.0, "speed": 160.0, "lifetime": 5.0, "radius": 10.0, "damage_type": 0, "mana_cost": 20, "unlock_cost": 4, "shop_cost": 25 } }, "action_fire_bolt": { "type": 0, "display_name": "Fire Bolt", "description": "火焰飞弹,命中施加燃烧。", "element_tags": ["tag:fire"], "meta": { "base_damage": 4.0, "speed": 320.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "apply_status_id": 1, "mana_cost": 8, "shop_cost": 22 } }, "action_poison_dart": { "type": 0, "display_name": "Poison Dart", "description": "毒镖,命中叠加中毒层数。", "element_tags": ["tag:poison"], "meta": { "base_damage": 2.0, "speed": 400.0, "lifetime": 3.0, "radius": 5.0, "damage_type": 0, "apply_status_id": 3, "mana_cost": 6, "shop_cost": 20 } }, From a3ab2aa6cdb8df6d1ac7647a266d53fb0dd21c39 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:27:08 +0800 Subject: [PATCH 10/12] =?UTF-8?q?docs(mana):=20=C2=A74.1/=C2=A75.A=20?= =?UTF-8?q?=E7=8E=B0=E7=8A=B6=E6=9B=B4=E6=96=B0=E4=B8=BA=20Mana=20?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=8C=96=E5=B7=B2=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/game_design.md | 2 +- docs/design/numerical_design.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/design/game_design.md b/docs/design/game_design.md index 06341c7..21806e1 100644 --- a/docs/design/game_design.md +++ b/docs/design/game_design.md @@ -132,7 +132,7 @@ ### 4.1 核心资源:魔力 (Mana) -> **⚠️ 实现现状 (2026-07-20 审计 → MVP 已实现)**:Mana 精实 MVP 已落地——`PlayerStats` 蓝池(`mana`/`mana_max`/`mana_regen`)、`CoreDefinition.base_mana_max/regen`(cores.json)、每法术 `mana_cost`(numerical §2)、`SpellEvaluator` 逐节点扣蓝 + 蓝空停施、HUD 蓝条。**暂缺**:递增蓝耗、`infinite_spells` 扣血、全局+法杖取 Min、cast_delay。详见 `docs_dev/specs/2026-07-20-mana-system-mvp-design.md`。 +> **⚠️ 实现现状 (2026-07-20 审计 → MVP 已实现)**:Mana 精实 MVP 已落地——`PlayerStats` 蓝池(`mana`/`mana_max`/`mana_regen`)、`CoreDefinition.base_mana_max/regen`(cores.json)、每法术 `mana_cost`(numerical §2)、`SpellEvaluator` 逐节点扣蓝 + 蓝空停施、HUD 蓝条。**已补全**:递增蓝耗、`infinite_spells` 扣血、魔力虹吸(多来源)、HUD 打磨。**仍暂缺**:全局+法杖取 Min、cast_delay。详见 `docs_dev/specs/2026-07-20-mana-system-mvp-design.md`。 为了防止无限“加特林核弹”,限制资源是 **魔力**。 * 强力修正器(如伤害+100)会有巨大的 **Delay (施法后摇)** 和 **Mana Cost (蓝耗)**。 diff --git a/docs/design/numerical_design.md b/docs/design/numerical_design.md index c022806..f477e69 100644 --- a/docs/design/numerical_design.md +++ b/docs/design/numerical_design.md @@ -173,6 +173,7 @@ Damage = ((Base + Add) × Mult) × (1 - Res) - Armor * **最小帧限制**: 设定射击间隔至少为 1 帧 (0.016s)。 * **过热机制 (Overheat)**: 连续射击超过 5秒,法杖进入“过热”状态,强制冷却 2秒 或 增加散布。 * **递增蓝耗**: 连续快速射击时,每发子弹蓝耗增加 1%,停止射击后快速衰减。 + > **⚠️ 实现现状 (2026-07-21 → 已实现)**:递增蓝耗已落地(`PlayerStats.mana_heat`,`balance.json.mana_heat` 可调,平衡页签);未做「最小帧限制/过热强制冷却」(用递增蓝耗替代)。 ### B. 显卡杀手 (GPU Killer) * **问题**: 玩家构建出 `无限分裂` + `无限粒子`。 From 0fd01caebb51fef0314a11a414e3c74556c7b000 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:29:16 +0800 Subject: [PATCH 11/12] =?UTF-8?q?chore(mana):=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E5=8C=96=E9=AA=8C=E6=94=B6=E9=80=9A=E8=BF=87=EF=BC=88Godot=204?= =?UTF-8?q?.6=20=E8=BF=90=E8=A1=8C=E6=97=B6=E5=AE=9E=E6=B5=8B=E5=90=84?= =?UTF-8?q?=E6=9C=BA=E5=88=B6+=E5=9B=9E=E5=BD=92=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 递增蓝耗 heat/95.5、infinite 扣血 hp99、heavy_cost hp90、 持久内存回归 [0,0,1,0]、击杀回蓝 +10、多来源 leech 重算 999→0、HUD 蓝条数字均实测通过。 Co-Authored-By: Claude Opus 4.8 (1M context) From 4725c57c7271740887e07489f5ee39e1a9f394a6 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 15:35:11 +0800 Subject: [PATCH 12/12] =?UTF-8?q?fix(mana):=20heat=20=E4=BE=9D=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=89=A3=E8=93=9D(ctx=E6=A0=87=E8=AE=B0)=E8=A6=86?= =?UTF-8?q?=E7=9B=96=20LOOP/CIRCUIT=20=E5=AD=90=E8=B7=AF=E5=BE=84+?= =?UTF-8?q?=E5=8D=96=E8=A1=80=E4=B8=8D=E5=8D=87=E7=83=AD=EF=BC=9B=E6=AD=BB?= =?UTF-8?q?=E4=BA=A1=E9=98=B2=E9=87=8D=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审 nit: - heat 之前仅在主循环 cast_fired 记录,LOOP 体/CIRCUIT 分支扣蓝漏记 → 递增蓝耗对这两类拓扑失效。 改为 _charge_mana 真实 spend_mana 成功时置 ctx.mana_spent_this_cast,施法末据此升 heat。 - 顺带修正 infinite 卖血(未真实扣蓝)也升 heat 的问题:卖血不升热。 - combat_manager._on_player_died 加 GAME_OVER 防重入(子弹命中+卖血同帧双死不重复结算)。 MCP 实测:LOOP heat 0.010(修复前0)、卖血 heat 0、线性回归 0.010。 Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/domain/combat/combat_manager.gd | 2 ++ scripts/domain/spell_system/spell_context.gd | 2 ++ scripts/domain/spell_system/spell_evaluator.gd | 7 +++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/domain/combat/combat_manager.gd b/scripts/domain/combat/combat_manager.gd index ed8e797..09528b3 100644 --- a/scripts/domain/combat/combat_manager.gd +++ b/scripts/domain/combat/combat_manager.gd @@ -228,6 +228,8 @@ func close_shop_and_next_wave() -> void: _transition_to_battle(next_wave) func _on_player_died(_payload: Dictionary) -> void: + if state == GameState.GAME_OVER: + return # 防重入:同帧内多路死亡(子弹命中 + 卖血施法)不重复结算战绩/发以太 state = GameState.GAME_OVER # 先采集本局战绩(reset 前),写入排行榜 var wave_reached: int = max(1, WaveManager.current_wave) diff --git a/scripts/domain/spell_system/spell_context.gd b/scripts/domain/spell_system/spell_context.gd index af78787..4fd599f 100644 --- a/scripts/domain/spell_system/spell_context.gd +++ b/scripts/domain/spell_system/spell_context.gd @@ -10,6 +10,7 @@ var stats: CastStats = null # 本次施法的修改层 ## R1-R4:供 LOGIC_EVERY_N_SHOTS 等指令跨帧计数 var registers: PackedFloat32Array = PackedFloat32Array() var hp_cost_accum: float = 0.0 # 本次施法累积 HP 代价(heavy_cost + infinite),施法末结算 +var mana_spent_this_cast: bool = false # 本次施法是否真实扣过蓝(含 LOOP/CIRCUIT 子路径)→ 决定 heat 上升 func _init() -> void: stats = CastStats.new() @@ -23,6 +24,7 @@ func reset() -> void: else: stats = CastStats.new() hp_cost_accum = 0.0 + mana_spent_this_cast = false # registers 小心:仅当 Core 没有 PERSISTENT_MEMORY Feature 时才清零 # 具体清空逻辑在 SpellContextPool.acquire_for_wand() 中处理 diff --git a/scripts/domain/spell_system/spell_evaluator.gd b/scripts/domain/spell_system/spell_evaluator.gd index e2376b3..4ac3bb2 100644 --- a/scripts/domain/spell_system/spell_evaluator.gd +++ b/scripts/domain/spell_system/spell_evaluator.gd @@ -324,9 +324,9 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 var persistent: bool = compiled.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY var ctx: SpellContext = _acquire_ctx(caster_id, persistent) ctx.hp_cost_accum = 0.0 + ctx.mana_spent_this_cast = false 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 @@ -337,8 +337,6 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 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) @@ -364,7 +362,7 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 break 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: + if ctx.mana_spent_this_cast: PlayerStats.mana_heat = minf(SettingsManager.mana_heat_max(), PlayerStats.mana_heat + SettingsManager.mana_heat_per_cast()) if ops_count >= max_ops: push_warning("SpellEvaluator: MAX_OPS reached (caster=%d)" % caster_id) @@ -501,6 +499,7 @@ func _charge_mana(base_cost: float, ctx: SpellContext, infinite: bool, heat_mult if base_cost <= 0.0: return true if PlayerStats.spend_mana(base_cost * heat_mult): + ctx.mana_spent_this_cast = true # 真实扣蓝才记(infinite 卖血不算)→ heat 只对真实施法上升 return true if infinite: ctx.hp_cost_accum += SettingsManager.infinite_hp_per_shot()