初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
@@ -0,0 +1,619 @@
## 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) != 0:
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) != 0
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))
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"])
## ── 上下文获取(区分跨帧持久 / 池化)─────────────────────────
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
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)