@@ -0,0 +1,619 @@
## 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 )