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:
@@ -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
@@ -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
@@ -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
@@ -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 } },
|
||||
|
||||
@@ -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 (蓝耗)**。
|
||||
|
||||
@@ -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)
|
||||
* **问题**: 玩家构建出 `无限分裂` + `无限粒子`。
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 法术
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() 中处理
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -212,3 +212,6 @@ msgstr "Unlocked"
|
||||
|
||||
msgid "META_CLOSE"
|
||||
msgstr "Close"
|
||||
|
||||
msgid "UI_MANA"
|
||||
msgstr "MP %d/%d"
|
||||
|
||||
@@ -212,3 +212,6 @@ msgstr "解放済み"
|
||||
|
||||
msgid "META_CLOSE"
|
||||
msgstr "閉じる"
|
||||
|
||||
msgid "UI_MANA"
|
||||
msgstr "MP %d/%d"
|
||||
|
||||
@@ -212,3 +212,6 @@ msgstr "已解锁"
|
||||
|
||||
msgid "META_CLOSE"
|
||||
msgstr "关闭"
|
||||
|
||||
msgid "UI_MANA"
|
||||
msgstr "蓝 %d/%d"
|
||||
|
||||
@@ -212,3 +212,6 @@ msgstr "已解鎖"
|
||||
|
||||
msgid "META_CLOSE"
|
||||
msgstr "關閉"
|
||||
|
||||
msgid "UI_MANA"
|
||||
msgstr "藍 %d/%d"
|
||||
|
||||
Reference in New Issue
Block a user