## SpellEvaluator — 法术虚拟机(Autoload: SpellEvaluator) ## S1:compile_wand(LINEAR)+execute_compiled(ACTION) ## S2:MODIFIER 分支 ## S3:_flatten_linear 处理 TRIGGER、execute_sub、multicast_count、actions_remaining ## S5:LOGIC 分支(条件门 + 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.A,P6-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 为空,降级 LINEAR(Core=%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: 非法边 %s(slot_count=%d)跳过" % [str(e), n]) continue adj[ep.x].append(ep.y) in_degree[ep.y] += 1 # P6-N63:Kahn 前保存原始入度快照,供 _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-N64:in_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() 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() # 重置 stats,registers 保留(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.registers(P6-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 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: 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)