Files
spellforge/scripts/domain/spell_system/spell_evaluator.gd
T

631 lines
27 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## SpellEvaluator — 法术虚拟机(Autoload: SpellEvaluator
## S1compile_wand(LINEAR)execute_compiled(ACTION)
## S2MODIFIER 分支
## S3_flatten_linear 处理 TRIGGER、execute_sub、multicast_count、actions_remaining
## S5LOGIC 分支(条件门 + LOOP)、跨帧寄存器持久化(PERSISTENT_MEMORY Core
extends Node
const MAX_OPS_PER_CPU: int = 40
const MAX_TRIGGER_DEPTH: int = 3
## LOGIC 求值返回信号
const _LOGIC_CONTINUE: int = 0 # 条件通过 / 无副作用 → 继续执行后续节点
const _LOGIC_SKIP_REST: int = 1 # 条件不成立 → 跳过本次施法后续全部节点
## 跨帧持久寄存器上下文:caster_id → SpellContext(不入池,registers 跨施法保留)
## 仅 Core 带 CoreFeatureTag.PERSISTENT_MEMORY 时使用;compile_wand 时清空(换杖即重置记忆)
var _persistent_ctx: Dictionary = {}
## 共鸣配方表(§3.4.C):纯数据驱动,唯一来源 res://data/resonance.json
## 字段:pattern[2]、match("adjacent"/"anywhere_in_deck")、result_spell_id、consume_inputs
const RESONANCE_JSON: String = "res://data/resonance.json"
var _resonance_recipes: Array = []
func _ready() -> void:
if FileAccess.file_exists(RESONANCE_JSON):
var d = JSON.parse_string(FileAccess.get_file_as_string(RESONANCE_JSON))
if d is Array:
_resonance_recipes = d
else:
push_error("SpellEvaluator: resonance.json 格式错误(应为数组)")
else:
push_error("SpellEvaluator: 缺失 res://data/resonance.json(共鸣系统无配方)")
## compile_wand(core, raw_nodes) → CompiledDeck
## core 在前(P6-N70
func compile_wand(core: CoreDefinition, raw_nodes: Array) -> CompiledDeck:
SubPayloadRegistry.clear_for_compile()
_persistent_ctx.clear() # 换杖/重编译即重置跨帧寄存器记忆
# P6-N3 修复:若 Core 带 PERSISTENT_MEMORY,在非热路径的 compile_wand 阶段预分配 SpellContext
# 避免首次施法时在 _physics_process 热路径内 SpellContext.new()
if core.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY:
var prewarm := SpellContext.new()
prewarm.caster_id = 0 # 玩家 caster_id,下次 acquire 时覆写
_persistent_ctx[0] = prewarm # 预热 id=0(玩家唯一 id
var deck := CompiledDeck.new()
deck.topology_type = core.topology
deck.feature_tags = core.feature_tags # 执行期据此判断 PERSISTENT_MEMORY 等
match core.topology:
CoreDefinition.Topology.CIRCUIT:
_flatten_circuit(raw_nodes, core, deck)
CoreDefinition.Topology.MATRIX:
_flatten_matrix(raw_nodes, core, deck)
_:
_flatten_linear(raw_nodes, deck)
# 拓扑扁平化后做共鸣模式匹配(§3.4.C),可能注入结果节点并标记 consume 掩码
_check_resonance(deck)
deck.checksum = _calc_checksum(core, deck.nodes)
# 战斗开始时 CombatManager 调用 lock_for_battle()
return deck
## 扣除算法:处理 MODIFIER / TRIGGER / ACTION
## TRIGGER 节点消耗后续节点作为子荷载
func _flatten_linear(raw_nodes: Array, deck: CompiledDeck) -> void:
var i: int = 0
while i < raw_nodes.size():
var n: SpellNode = raw_nodes[i]
if not (n is SpellNode):
i += 1
continue
if n.type == SpellNode.SpellType.TRIGGER:
# 消耗 i+1 起的节点为子荷载(ACTION 节点也属于子荷载范围)
# 仅遇到下一个同级 TRIGGER 时才停止(不跨 TRIGGER 作用域)
var sub_nodes: Array = []
var j: int = i + 1
while j < raw_nodes.size():
var sn: SpellNode = raw_nodes[j]
if sn.type == SpellNode.SpellType.TRIGGER:
break # 下一个 TRIGGER 是独立作用域
sub_nodes.append(sn) # MODIFIER / ACTION 均归入子荷载
j += 1
# 注册子荷载
var payload_id: int = SubPayloadRegistry.register(sub_nodes)
# 克隆 TRIGGER 节点并注入 sub_payload_id
var trigger_clone: SpellNode = SpellNode.new()
trigger_clone.id = n.id
trigger_clone.type = n.type
trigger_clone.display_name = n.display_name
trigger_clone.description = n.description
trigger_clone.meta = n.meta.duplicate()
trigger_clone.meta["sub_payload_id"] = payload_id
deck.nodes.append(trigger_clone)
deck.sub_payload_ids.append(payload_id)
i = j # 跳过已消耗节点
else:
deck.nodes.append(n)
i += 1
func _calc_checksum(core: CoreDefinition, nodes: Array) -> int:
var h: int = core.topology ^ nodes.size() ^ hash(core.id)
for i in nodes.size():
h = h * 31 ^ hash(nodes[i].id)
return h
# ── CIRCUIT 拓扑扁平化(architecture_design.md §3.4.AP6-N57/63/64/66)─────
## 读 core.edges,对有向图做 Kahn 拓扑排序;分叉点(出度>1)展开为 SubPayload + LOGIC_FORK
## edges 为空 → 降级 LINEAR。结果写入 deck.nodes;分支 payload id 记入 deck.sub_payload_ids
func _flatten_circuit(raw_nodes: Array, core: CoreDefinition, deck: CompiledDeck) -> void:
var edges: Array = core.edges
if edges.is_empty():
push_warning("_flatten_circuit: core.edges 为空,降级 LINEARCore=%s" % core.id)
_flatten_linear(raw_nodes, deck)
return
var n: int = core.slot_count
# 1. 构建邻接表 + 入度
var in_degree: Array = []
var adj: Array = []
in_degree.resize(n)
adj.resize(n)
for i in n:
in_degree[i] = 0
adj[i] = []
for e in edges:
var ep: Vector2i = _edge_endpoints(e)
if ep.x < 0 or ep.x >= n or ep.y < 0 or ep.y >= n:
push_warning("_flatten_circuit: 非法边 %sslot_count=%d)跳过" % [str(e), n])
continue
adj[ep.x].append(ep.y)
in_degree[ep.y] += 1
# P6-N63Kahn 前保存原始入度快照,供 _collect_branch_path 判断汇聚点
var orig_in_degree: Array = in_degree.duplicate()
# 2. Kahn BFS 拓扑排序
var queue: Array = []
for i in n:
if in_degree[i] == 0:
queue.append(i)
var topo_order: Array = []
while not queue.is_empty():
var cur: int = queue.pop_front()
topo_order.append(cur)
for nxt in adj[cur]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
if topo_order.size() != n:
push_error("_flatten_circuit: 检测到环路,无法线性化!Core=%s" % core.id)
return # deck.nodes 保持空,该法杖无法施法但不崩溃
# 3. 按拓扑序输出;P6-N64in_branch_payload 防止分支节点被主链重复执行
var in_branch_payload: Dictionary = {}
for slot_idx in topo_order:
if in_branch_payload.has(slot_idx):
continue # 已被某分支 SubPayload 收纳
var node: SpellNode = raw_nodes[slot_idx] if slot_idx < raw_nodes.size() else null
if adj[slot_idx].size() > 1:
# 分叉点(先于 null 守卫判断:分叉槽通常为空 splitter)
if node != null:
deck.nodes.append(node) # 分叉槽自身节点先执行(通常空,允许 MODIFIER/ACTION
var branch_ids: Array = []
for neighbor_idx in adj[slot_idx]:
var bp: Array = _collect_branch_path(neighbor_idx, adj, orig_in_degree, raw_nodes, in_branch_payload)
if not bp.is_empty():
var pid: int = SubPayloadRegistry.register(bp)
branch_ids.append(pid)
deck.sub_payload_ids.append(pid)
if not branch_ids.is_empty():
deck.nodes.append(_make_fork_node(branch_ids))
elif node != null:
deck.nodes.append(node)
# else:空的非分叉槽,跳过
## 从 start_idx 沿单出边收集分支节点序列(P6-N66 嵌套分叉递归)
## 终止:叶节点(出度0/ 汇聚点(orig_in_degree>1,交主链处理)/ 嵌套分叉(出度>1,递归+插 LOGIC_FORK
func _collect_branch_path(start_idx: int, adj: Array, orig_in_degree: Array,
raw_nodes: Array, in_branch_payload: Dictionary) -> Array:
var path: Array = []
var cur: int = start_idx
var visited: Dictionary = {}
while cur >= 0 and not visited.has(cur):
visited[cur] = true
in_branch_payload[cur] = true # 登记已纳入 SubPayload,主链循环跳过
if cur < raw_nodes.size() and raw_nodes[cur] != null:
path.append(raw_nodes[cur])
if adj[cur].size() > 1:
# 嵌套分叉:每条子出边递归收集并注册,插入嵌套 LOGIC_FORK 后本路径结束
var nested_ids: Array = []
for nxt_idx in adj[cur]:
var np: Array = _collect_branch_path(nxt_idx, adj, orig_in_degree, raw_nodes, in_branch_payload)
if not np.is_empty():
nested_ids.append(SubPayloadRegistry.register(np))
if not nested_ids.is_empty():
path.append(_make_fork_node(nested_ids))
break
elif adj[cur].size() == 1:
var nxt: int = adj[cur][0]
if orig_in_degree[nxt] <= 1:
cur = nxt # 单入边,继续延伸
else:
break # 汇聚点(多入边),交主链处理
else:
break # 叶节点
return path
func _make_fork_node(branch_ids: Array) -> SpellNode:
var fork := SpellNode.new()
fork.type = SpellNode.SpellType.LOGIC
fork.id = "LOGIC_FORK"
fork.meta = {"fork_branch_ids": branch_ids}
return fork
## 解析单条边为 (from, to);兼容 {"from","to"} 字典(P6-N42 权威)与 [from, to] 数组对
func _edge_endpoints(e) -> Vector2i:
if e is Dictionary:
return Vector2i(int(e.get("from", -1)), int(e.get("to", -1)))
elif e is Array and e.size() >= 2:
return Vector2i(int(e[0]), int(e[1]))
return Vector2i(-1, -1)
# ── MATRIX 拓扑扁平化(architecture_design.md §3.4.A,邻接加成 P6-N13/P6-N20)────
## 仅 Row A(前 grid_cols 槽)进入执行序列;Row B 仅提供邻接加成,不独立执行(P6-N20)
func _flatten_matrix(raw_nodes: Array, core: CoreDefinition, deck: CompiledDeck) -> void:
var cols: int = max(1, core.grid_cols)
for i in cols:
var adj_idx: int = i + cols # Row B 中与 slot[i] 竖向对齐的槽
var row_a: SpellNode = raw_nodes[i] if i < raw_nodes.size() else null
var row_b: SpellNode = raw_nodes[adj_idx] if adj_idx < raw_nodes.size() else null
if row_a == null:
continue # 空槽不传递邻接(P6-N13
if row_b != null and _has_adjacency_bonus(row_a, row_b):
deck.nodes.append(_make_adjacency_mod(row_a, row_b)) # 注入隐式 MODIFIER(在 Row A 前)
deck.nodes.append(row_a) # 仅追加 Row A 节点
## 邻接加成是否成立(P6-N13;LOGIC / 空槽不参与)
func _has_adjacency_bonus(a: SpellNode, b: SpellNode) -> bool:
if a == null or b == null:
return false
if a.type == SpellNode.SpellType.LOGIC or b.type == SpellNode.SpellType.LOGIC:
return false
var A := SpellNode.SpellType
if a.type == A.ACTION and b.type == A.ACTION:
return a.id == b.id # 同 ID ACTION 对齐 → 强化同类弹
if a.type == A.ACTION and b.type == A.MODIFIER:
return true # 增幅器加倍
if a.type == A.MODIFIER and b.type == A.MODIFIER:
return a.id == b.id # 同 ID MODIFIER 对齐 → 共鸣修正
return false
## 生成隐式邻接 MODIFIER 节点(P6-N13 效果表)
func _make_adjacency_mod(a: SpellNode, b: SpellNode) -> SpellNode:
var m := SpellNode.new()
m.type = SpellNode.SpellType.MODIFIER
m.id = "implicit_adjacency"
m.display_name = "邻接加成"
var A := SpellNode.SpellType
if a.type == A.ACTION and b.type == A.ACTION:
m.meta = {"damage_mult": 1.5} # 同类弹 ×1.5
elif a.type == A.MODIFIER and b.type == A.MODIFIER:
# 同 ID MODIFIER 对齐 → 数值 ×2(平直叠加):复制 b 的可加字段
m.meta = b.meta.duplicate()
else:
# ACTION + MODIFIER:注入 B 的修正效果(作用于对齐的 Row A 动作)
m.meta = b.meta.duplicate()
return m
# ── 共鸣系统(§3.4.C_consumed 掩码 P6-N29)─────────────────────
## 在扁平化后的 deck.nodes 上做模式匹配;命中 adjacent 配方时注入结果节点并标记 consume
func _check_resonance(deck: CompiledDeck) -> void:
if _resonance_recipes.is_empty() or deck.nodes.is_empty():
return
var nodes: Array = deck.nodes
var consumed: PackedByteArray = PackedByteArray()
consumed.resize(nodes.size())
for recipe in _resonance_recipes:
if String(recipe.get("match", "adjacent")) != "adjacent":
continue # anywhere_in_deck 留 stub(仅传说配方)
var pattern: Array = recipe.get("pattern", [])
if pattern.size() < 2:
continue
var i: int = 0
while i < nodes.size():
if consumed[i] != 0 or not _node_has_tag(nodes[i], pattern[0]):
i += 1
continue
# 向右扫描 i+1..i+2(跳过 MODIFIER),寻找 pattern[1]
var hit_j: int = -1
for j in range(i + 1, min(i + 3, nodes.size())):
if consumed[j] != 0:
continue
if nodes[j].type == SpellNode.SpellType.MODIFIER:
continue # MODIFIER 不参与匹配,但不中断扫描
if _node_has_tag(nodes[j], pattern[1]):
hit_j = j
break # 遇到非 MODIFIER 非目标节点即停(不跨 ACTION/TRIGGER
if hit_j < 0:
i += 1
continue
var result: SpellNode = SpellRegistry.get_spell(String(recipe.get("result_spell_id", "")))
if result == null:
i += 1
continue
# 在 i 位置插入共鸣结果,同步扩展 consumed
nodes.insert(i, result)
consumed.insert(i, 0)
if bool(recipe.get("consume_inputs", false)):
consumed[i + 1] = int(1) # 原 pattern[0](现 i+1
consumed[hit_j + 1] = int(1) # 原 pattern[1](现 hit_j+1
i += 2 # 跳过刚插入的结果与已消费的 pattern[0]
# 收集 consume 索引供运行时 SpellDeck 跳过
for k in consumed.size():
if consumed[k] != 0:
deck.consumed_indices.append(k)
## 标签匹配:tag:xxx 查 element_tags;否则按 id 精确匹配
func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
if node == null:
return false
if tag_pattern.begins_with("tag:"):
return tag_pattern in node.element_tags
return node.id == tag_pattern
## execute_compiled(compiled, caster_id, spawn_pos)
## 每次施法可执行的 ACTION 数量 = 1 + multicast_count
func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2) -> void:
if compiled == null or compiled.is_empty():
return
var persistent: bool = compiled.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY
var ctx: SpellContext = _acquire_ctx(caster_id, persistent)
var deck: SpellDeck = compiled.make_runtime_deck()
var ops_count: int = 0
var max_ops: int = MAX_OPS_PER_CPU * 5
var actions_remaining: int = 1 # 初始为 1,由 multicast_count 加成
while deck.has_next() and ops_count < max_ops:
ops_count += 1
var node: SpellNode = deck.pop()
var mana_cost: float = float(node.meta.get("mana_cost", 0.0))
if mana_cost > 0.0 and not PlayerStats.spend_mana(mana_cost):
break # 蓝不足 → 停止本次施法(此节点不执行、剩余跳过)
match node.type:
SpellNode.SpellType.ACTION:
_push_projectile(node, ctx, spawn_pos, -1)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellNode.SpellType.MODIFIER:
_apply_modifier(node, ctx)
# multicast_count 已在 _apply_modifier 内更新
actions_remaining = 1 + ctx.stats.multicast_count
SpellNode.SpellType.TRIGGER:
_push_trigger(node, ctx, spawn_pos)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellNode.SpellType.LOGIC:
if node.meta.has("fork_branch_ids"):
# CIRCUIT 分叉:各分支在施法点立即并行执行
for bid in node.meta["fork_branch_ids"]:
_run_branch_payload(int(bid), ctx, spawn_pos)
elif String(node.meta.get("logic_op", "")) == "loop":
ops_count = _run_logic_loop(node, ctx, spawn_pos, deck, ops_count, max_ops)
break # LOOP 已消耗剩余节点
elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST:
break # 条件不成立 → 跳过后续法术
if ops_count >= max_ops:
push_warning("SpellEvaluator: MAX_OPS reached (caster=%d)" % caster_id)
_release_ctx(ctx, persistent)
## execute_sub — 子荷载执行(MAX_TRIGGER_DEPTH 限制)
## depth 表示当前嵌套深度(0=顶层子荷载)
func execute_sub(payload_id: int, hit_pos: Vector2, owner_id: int, depth: int) -> void:
if depth >= MAX_TRIGGER_DEPTH:
EventBus.emit(EventID.SPELL_DEPTH_EXCEEDED, {"owner_id": owner_id, "depth": depth})
return
var nodes: Array = SubPayloadRegistry.get_nodes(payload_id)
if nodes.is_empty():
return
var ctx: SpellContext = SpellContextPool.acquire(owner_id) # P-S3-04: 从池取用
var ops: int = 0
var actions_remaining: int = 1
for node in nodes:
ops += 1
if ops > MAX_OPS_PER_CPU * 2:
break
match node.type:
SpellNode.SpellType.ACTION:
_push_projectile(node, ctx, hit_pos, depth)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellNode.SpellType.MODIFIER:
_apply_modifier(node, ctx)
actions_remaining = 1 + ctx.stats.multicast_count
SpellNode.SpellType.TRIGGER:
_push_trigger(node, ctx, hit_pos, depth)
actions_remaining -= 1
if actions_remaining <= 0:
break
SpellContextPool.release(ctx)
# ── 内部方法 ──────────────────────────────────────────────
func _push_projectile(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, trigger_depth: int) -> void:
# ACTION 分派:zone(地面效果)/ summon(召唤物)/ 默认弹道
match String(node.meta.get("action_kind", "")):
"zone":
_spawn_zone_action(node, ctx, spawn_pos)
return
"summon":
_spawn_minion_action(node, ctx, spawn_pos)
return
var meta: Dictionary = node.meta
var speed: float = float(meta.get("speed", 300.0))
var lifetime: float = float(meta.get("lifetime", 4.0))
var radius: float = float(meta.get("radius", 6.0))
var base_dmg: float = float(meta.get("base_damage", 3.0))
var dtype: int = int(meta.get("damage_type", 0))
var spread: int = max(1, ctx.stats.spread_count)
var aim_dir: Vector2 = _get_aim_direction(spawn_pos)
for i in spread:
var angle_offset: float = 0.0
if spread > 1:
angle_offset = (float(i) / float(spread - 1) - 0.5) * 0.5
var vel: Vector2 = aim_dir.rotated(angle_offset) * speed * ctx.stats.speed_mult
var actual_dmg: float = (base_dmg + ctx.stats.damage_add) * ctx.stats.damage_mult
var actual_r: float = radius * ctx.stats.radius_mult
var pierce: int = int(meta.get("pierce", 0)) + ctx.stats.pierce_add
var cold: Dictionary = {}
if pierce > 0:
cold["pierce_remaining"] = pierce
# S3: 将 trigger_depth 写入冷数据供子弹命中时使用
if trigger_depth >= 0:
cold["trigger_depth"] = trigger_depth
if meta.has("apply_status_id"):
cold["apply_status_id"] = int(meta["apply_status_id"])
if meta.get("apply_combo_mark", false):
cold["apply_combo_mark"] = true
BulletManager.spawn_bullet(
spawn_pos, vel,
lifetime + ctx.stats.lifetime,
actual_r, actual_dmg, 1.0, dtype, ctx.caster_id,
0, 0.0, cold
)
## ACTION(zone):在施法点生成地面效果区域(action_poison_pool 等)
func _spawn_zone_action(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> void:
var meta: Dictionary = node.meta
var radius: float = float(meta.get("radius", 200.0)) * ctx.stats.radius_mult
ZoneManager.spawn_zone(
spawn_pos.x, spawn_pos.y, radius,
int(meta.get("status_id", StatusID.POISON)),
float(meta.get("duration", 5.0)) + ctx.stats.lifetime,
float(meta.get("tick_interval", 1.0)),
ctx.caster_id)
## ACTION(summon):在施法点召唤一个炮台(action_summon_turret 等)
func _spawn_minion_action(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> void:
var meta: Dictionary = node.meta
var def: Dictionary = {
"lifetime": float(meta.get("lifetime", 20.0)),
"range": float(meta.get("range", 350.0)),
"fire_interval": float(meta.get("fire_interval", 0.8)),
"damage": float(meta.get("base_damage", 4.0)) + ctx.stats.damage_add,
"bullet_speed": float(meta.get("speed", 360.0)),
"damage_type": int(meta.get("damage_type", 0)),
}
MinionManager.spawn_minion(def, ctx.caster_id, spawn_pos)
## TRIGGER 节点:发射子弹+子荷载参数
func _push_trigger(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, trigger_depth: int = 0) -> void:
var payload_id: int = int(node.meta.get("sub_payload_id", -1))
if payload_id < 0 or not SubPayloadRegistry.has_payload(payload_id):
return
var meta: Dictionary = node.meta
var speed: float = float(meta.get("speed", 250.0))
var lifetime: float = float(meta.get("lifetime", 4.0))
var radius: float = float(meta.get("radius", 6.0))
var base_dmg: float = float(meta.get("base_damage", 3.0))
var dtype: int = int(meta.get("damage_type", 0))
var aim_dir: Vector2 = _get_aim_direction(spawn_pos)
var vel: Vector2 = aim_dir * speed
var actual_dmg: float = (base_dmg + ctx.stats.damage_add) * ctx.stats.damage_mult
var actual_r: float = radius * ctx.stats.radius_mult
var cold: Dictionary = {
"on_hit_payload_id": payload_id,
"trigger_parent_depth": trigger_depth,
}
BulletManager.spawn_bullet(
spawn_pos, vel,
lifetime, actual_r, actual_dmg, 1.0, dtype, ctx.caster_id,
0, 0.0, cold
)
## MODIFIER 分支
func _apply_modifier(node: SpellNode, ctx: SpellContext) -> void:
var meta: Dictionary = node.meta
if meta.has("damage_add"):
ctx.stats.damage_add += float(meta["damage_add"])
if meta.has("damage_mult"):
ctx.stats.damage_mult *= float(meta["damage_mult"])
if meta.has("spread_add"):
ctx.stats.spread_count += int(meta["spread_add"])
if meta.has("speed_mult"):
ctx.stats.speed_mult *= float(meta["speed_mult"])
if meta.has("lifetime_add"):
ctx.stats.lifetime += float(meta["lifetime_add"])
if meta.has("multicast"):
ctx.stats.multicast_count += int(meta["multicast"])
if meta.has("pierce"):
ctx.stats.pierce_add += int(meta["pierce"])
## ── 上下文获取(区分跨帧持久 / 池化)─────────────────────────
func _acquire_ctx(caster_id: int, persistent: bool) -> SpellContext:
if persistent:
var ctx: SpellContext = _persistent_ctx.get(caster_id, null)
if ctx == null:
ctx = SpellContext.new()
_persistent_ctx[caster_id] = ctx
ctx.reset() # 重置 statsregisters 保留(reset() 不触碰 registers
ctx.caster_id = caster_id
return ctx
return SpellContextPool.acquire(caster_id, false)
func _release_ctx(ctx: SpellContext, persistent: bool) -> void:
if not persistent:
SpellContextPool.release(ctx)
# 持久上下文留在 _persistent_ctx 中,registers 跨施法保留
## ── LOGIC 条件门求值 ────────────────────────────────────────
## 返回 _LOGIC_CONTINUE(执行后续)或 _LOGIC_SKIP_REST(跳过后续)
## EVERY_N_SHOTS 读写 ctx.registersP6-N37);持久化由 PERSISTENT_MEMORY Core 保证
func _eval_logic(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> int:
var meta: Dictionary = node.meta
var op: String = String(meta.get("logic_op", ""))
match op:
"every_n_shots":
var n: int = max(1, int(meta.get("n", 3)))
var reg: int = clampi(int(meta.get("reg", 0)), 0, 3)
var count: int = int(ctx.registers[reg]) + 1
ctx.registers[reg] = float(count)
return _LOGIC_CONTINUE if (count % n == 0) else _LOGIC_SKIP_REST
"if_hp_below":
var threshold: float = float(meta.get("threshold", 0.5))
var frac: float = PlayerStats.hp / maxf(1.0, PlayerStats.hp_max)
return _LOGIC_CONTINUE if (frac < threshold) else _LOGIC_SKIP_REST
"if_enemy_nearby":
# 用 SpatialGrid 查询范围内是否有敌方实体(plan: LOGIC_IF_ENEMY_NEARBY
# 注意:query_circle 为格子粒度,略宽于精确圆,作为条件门可接受
var rng: float = float(meta.get("range", 200.0))
var near: bool = SpatialGrid.query_circle(spawn_pos, rng).size() > 0
return _LOGIC_CONTINUE if near else _LOGIC_SKIP_REST
_:
push_warning("SpellEvaluator: 未知 logic_op '%s'" % op)
return _LOGIC_CONTINUE
## ── LOGIC_LOOP:将后续剩余节点重复执行 count 次 ──────────────
## 简化实现: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:
var count: int = clampi(int(node.meta.get("count", 2)), 1, 16)
var body: Array = []
while deck.has_next():
body.append(deck.pop())
for _rep in count:
for n in body:
ops_count += 1
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
match n.type:
SpellNode.SpellType.ACTION:
_push_projectile(n, ctx, spawn_pos, -1)
SpellNode.SpellType.MODIFIER:
_apply_modifier(n, ctx)
SpellNode.SpellType.TRIGGER:
_push_trigger(n, ctx, spawn_pos)
# LOGIC:LOOP 体内嵌套逻辑顺延(S5 后续迭代)
return ops_count
## ── CIRCUIT 分支执行(LOGIC_FORK 触发)──────────────────────
## 在施法点立即执行某分支 SubPayload 的节点;分支内 MODIFIER 作用域隔离(不泄漏给兄弟分支/主链)
## 嵌套 LOGIC_FORK 递归展开(P6-N66
func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2) -> void:
var nodes: Array = SubPayloadRegistry.get_nodes(payload_id)
if nodes.is_empty():
return
# 快照分支前 CastStats(8 字段,无分配),分支结束后还原 → 分支局部 MODIFIER 不外泄
var s: CastStats = ctx.stats
var b_dadd: float = s.damage_add
var b_dmul: float = s.damage_mult
var b_spr: int = s.spread_count
var b_mc: int = s.multicast_count
var b_life: float = s.lifetime
var b_spd: float = s.speed_mult
var b_rad: float = s.radius_mult
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 # 蓝不足 → 停止本分支
match node.type:
SpellNode.SpellType.ACTION:
_push_projectile(node, ctx, spawn_pos, -1)
SpellNode.SpellType.MODIFIER:
_apply_modifier(node, ctx)
SpellNode.SpellType.TRIGGER:
_push_trigger(node, ctx, spawn_pos)
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)
elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST:
break
# 还原 stats
s.damage_add = b_dadd
s.damage_mult = b_dmul
s.spread_count = b_spr
s.multicast_count = b_mc
s.lifetime = b_life
s.speed_mult = b_spd
s.radius_mult = b_rad
s.crit_chance = b_crit
func _get_aim_direction(from_pos: Vector2) -> Vector2:
var target: Vector2 = EnemyManager.get_nearest_pos(from_pos, 9999.0)
if target.distance_to(from_pos) < 1.0:
return Vector2.RIGHT
return (target - from_pos).normalized()
func get_pool_stats() -> String:
return "SpellCtx: " + str(SpellContextPool.get_available_count()) + "/" + str(SpellContextPool.POOL_SIZE)