620 lines
26 KiB
GDScript
620 lines
26 KiB
GDScript
## 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) != 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.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) != 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() # 重置 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)
|