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: 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/data/cores.json b/data/cores.json index 61b0f28..8296371 100644 --- a/data/cores.json +++ b/data/cores.json @@ -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 } } diff --git a/data/spells.json b/data/spells.json index 0bfd217..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 } }, @@ -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 } }, 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) * **问题**: 玩家构建出 `无限分裂` + `无限粒子`。 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/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/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] 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..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) @@ -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: 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 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) diff --git a/scripts/domain/spell_system/spell_context.gd b/scripts/domain/spell_system/spell_context.gd index 6bfe0f2..4fd599f 100644 --- a/scripts/domain/spell_system/spell_context.gd +++ b/scripts/domain/spell_system/spell_context.gd @@ -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() 中处理 diff --git a/scripts/domain/spell_system/spell_evaluator.gd b/scripts/domain/spell_system/spell_evaluator.gd index 5cc10c2..4ac3bb2 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 + 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 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"