merge(mana): Mana 系统完整化(递增蓝耗+infinite卖血+多来源魔力虹吸+HUD+平衡)

11 任务全绿:balance/PlayerStats/PlayerManager/SpellEvaluator 统一扣蓝门控、
魔力虹吸多来源重算、wand_eternal/heavy_cost/mana_leech 数据、Core 编辑器可调、
HUD 蓝条数字+低蓝+过热、平衡调数值、docs 现状更新、复审 heat 覆盖 LOOP/CIRCUIT+死亡防重入。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:37:03 +08:00
co-authored by Claude Opus 4.8
19 changed files with 136 additions and 23 deletions
+5 -1
View File
@@ -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:
+3 -1
View File
@@ -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 }
}
+6 -5
View File
@@ -1,7 +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 }
}
+3 -1
View File
@@ -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 } },
@@ -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 } },
+1 -1
View File
@@ -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 (蓝耗)**
+1
View File
@@ -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)
* **问题**: 玩家构建出 `无限分裂` + `无限粒子`
+19
View File
@@ -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():
+22
View File
@@ -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)))
+14
View File
@@ -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]
+1
View File
@@ -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 法术
+7
View File
@@ -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)
@@ -256,6 +258,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:
+2
View File
@@ -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
@@ -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
@@ -9,6 +9,8 @@ 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),施法末结算
var mana_spent_this_cast: bool = false # 本次施法是否真实扣过蓝(含 LOOP/CIRCUIT 子路径)→ 决定 heat 上升
func _init() -> void:
stats = CastStats.new()
@@ -21,6 +23,8 @@ func reset() -> void:
stats.reset()
else:
stats = CastStats.new()
hp_cost_accum = 0.0
mana_spent_this_cast = false
# registers 小心:仅当 Core 没有 PERSISTENT_MEMORY Feature 时才清零
# 具体清空逻辑在 SpellContextPool.acquire_for_wand() 中处理
+35 -14
View File
@@ -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
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 deck: SpellDeck = compiled.make_runtime_deck()
var ops_count: int = 0
var max_ops: int = MAX_OPS_PER_CPU * 5
@@ -331,8 +335,8 @@ 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 → 停施
match node.type:
SpellNode.SpellType.ACTION:
_push_projectile(node, ctx, spawn_pos, -1)
@@ -341,7 +345,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 +353,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 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)
_release_ctx(ctx, persistent)
@@ -487,6 +493,19 @@ 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):
ctx.mana_spent_this_cast = true # 真实扣蓝才记(infinite 卖血不算)→ heat 只对真实施法上升
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 +523,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 +574,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 +585,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 +600,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 +616,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 +628,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
+3
View File
@@ -212,3 +212,6 @@ msgstr "Unlocked"
msgid "META_CLOSE"
msgstr "Close"
msgid "UI_MANA"
msgstr "MP %d/%d"
+3
View File
@@ -212,3 +212,6 @@ msgstr "解放済み"
msgid "META_CLOSE"
msgstr "閉じる"
msgid "UI_MANA"
msgstr "MP %d/%d"
+3
View File
@@ -212,3 +212,6 @@ msgstr "已解锁"
msgid "META_CLOSE"
msgstr "关闭"
msgid "UI_MANA"
msgstr "蓝 %d/%d"
+3
View File
@@ -212,3 +212,6 @@ msgstr "已解鎖"
msgid "META_CLOSE"
msgstr "關閉"
msgid "UI_MANA"
msgstr "藍 %d/%d"