Files
gplan-2/game/core/effect_engine.gd
T
DKChan eea345882b feat(迭代27 T6+T7): 波五b 卫生件包+文档同步闸——对拍全同
T6 卫生件(P2 组收口):
- 死代码 13 函数删除(审计 16 处复核=ui_kit:486 实读现役不删,追认单#14)+FeatsEngine
  整文件退役(class_name 注销)
- 字符串 Callable×10 位→方法引用形态(编译期方法存在性校验;语法实证 METHOD_REF=true)
- 状态机 enum 化: adventure State(外部 String 契约 getter 词表逐位一致+外部纯读实证)/
  standee HeroState(register/set String 形参零变化+probe 面恒等)
- ConfigDB _fatal 退出归根(核心层去 quit/crash→_fatal_flag,决策归组合根 fail-fast 通道)
  +resolver 诊断/数据通道全面 Callable 化(ConfigDb↔Resolver 类型环断根)

T7 文档同步闸:
- 实现报告 design/qa/iter27-实现报告-2026-10-14.md(五波+闸表+追认单十五项+反哺点两条+目检五件)
- 06 迭代 27 条销号+GDD 反哺点两条新条;07 ㉖ 目检节+v1.0 版本行;roadmap ✅ 换代
- 审计报告本体按退役规则 git rm(吸收即死;修正计数由卡片承载);卡片来源行改指路

闸: 对拍三变体全同(第五跑)+冒烟 1081/0+run_all 27 套+probe click+render golden+static_scan 全绿
2026-10-15 01:41:37 +08:00

416 lines
24 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-10-06(迭代 17 卡⓪ 内容数据驱动扩展批 T1——效果引擎框架)
# 内容数据驱动扩展(需求卡片 design/research/需求卡片-内容数据驱动扩展批-2026-10-06.md;设计权威
# design/research/研究-内容数据驱动扩展架构-2026-10-06.md):把「内容的效果行为」从散布在结算
# 代码里的硬编码分支搬进内容数据行三个字段——订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do)
# ——由本解释器统一执行。完成后加一件「已有效果类型」的新专长/新秘宝=只改 .tres 数据行,零代码。
# 架构模型(R1 裁决):发布订阅——结算管线同步 emit 事件(无队列无异步,确定性优先)→ 本引擎遍历
# 持有订阅集匹配 → 返回按声明序排列的效果命中(do 列表),由发射点按组合规则施加。
# 多订阅与冲突(R3 裁决):一事件多订阅=常态;冲突在效果类型层用组合规则封死(§3.4 六族);遍历序
# =数据表声明序(feats 表序→treasure 表序→树规则合成行,装载时定死),写死保种子复放。
# 重建时机(v1.1 补):每战 begin_battle 从持有集/装备集重建 trigger 列表——冒险中途获得/换取的
# 专长下一战生效,同战内不热载,确定性优先。计数器账本随重建清零(battle 域)。
# 确定性契约:全同步调用、零 RNG、零 signal 时序依赖;同输入同命中序。
# 护栏保留:事件/效果/事实/算子四词表封闭(本文件常量),行内出域=加载期校验致命(schema 侧),
# 运行期 dispatch 对未知事件/效果类型 fail-fast(push_error+断言)。
class_name EffectEngine
extends RefCounted
## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕
## +battle_won 战斗胜利〔战地包扎〕;迭代 21 天赋树批 T3 扩签 18→19:+flag_missed 标错判定
## 〔危险直觉·守挂点——_judge_flag 去重闸后发射,契约 12 触发主语=当回合标错动作;
## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;迭代 22 秘宝实装批 T3 扩签 19→20:
## +board_installed 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕;原发射点不动)
const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle",
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "flag_missed",
"board_installed"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_settle", "round_end", "reward_roll"]
## 效果类型词汇(解释器 switch 闭集——需求卡片 §3.3 定稿 19 行;类型实数 24,含共享行拆型:
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
## bias_rewards/preview。需求卡片标题计 23 系笔误,落码按行内容全收——commit 留痕)
const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket",
"attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material",
"grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add",
"counter_spend", "reveal_tiles", "remove_mines", "refresh_pick",
"bias_rewards", "preview",
# 迭代 20 专长实装批 T1 扩签六型(奇物.md 基础件表 27 件声明清单——落法映射 commit 留痕):
# heal=回复生命〔血蜜/战地包扎〕;ac_bonus=护甲加值累积器〔封印桩本轮/石肤本场 cap〕;
# damage_reduce=受伤减免〔背水/安全帽〕;bonus_delta=冒险加算区增量〔血税 双向——battle 末汇入
# runner.bonus〕;blast_scaled=雷罚标定群伤〔陷阱爆发——值=事件伤害×mult〕;
# preview_area=资源类型预告〔示警烟火——3×3 邻域资源类型,不揭安危位置〕;
# target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕
"heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area",
"target_attack_bonus",
# 迭代 22 秘宝实装批 T3 扩签一型:mine_count_preview=危险格数量预告(巢穴预告——board_installed
# 订阅 do;只报数量不标位置、不碰危险格分布=棋盘事实不可变契约;参数面常时 kind 不入本词表——
# 它们无订阅行,schema 行内 effect_kinds 校验走 treasure_item_table 自有词表)
"mine_count_preview"]
# 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理;
# 编排器消费分支 T1 先行摘除)
## fact 条件词汇(封闭枚举——迭代 20 T1 扩签 9→17:payload 同名键直读面〔fact 取值序 payload
## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕;
## is_edge_cell=边缘格〔边陲矿工〕;is_blank_zero=空白连锁〔跳格采掘〕;dug_under_flag=被挖格
## 当时有旗〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水
## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕)
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter",
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_flag",
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct",
"battle_index"] # 迭代 21 T3 扩签——关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面)
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
## ends_with=后缀匹配〔对某甲族的泛条件〕、mod_eq=取模余数判定〔双管镐每第 2 次——value=模数
## ref=余数;dispatch 预评估口径〔when 于命中前读计数器现值〕下「每第 N 次」系用 ref=N-1 表达〕。
## 均系映射表现状条件装进 fact 的查询面落法,代拍板白名单覆盖,commit 留痕)
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
"advantageous", "gte_ref", "lt_ref", "mod_eq"]
## when 条件键白名单(trigger 行 when 数组元素键)
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面。
## 迭代 20 T1 扩签四键:value_event=事件载荷取值键名〔爆破会计——counter_add 值=本轮挖格花费
## payload.ap_cost〕;value_counter=计数器取值键〔疼痛记忆/弃图兑换——attack rider 值=账面层值〕;
## domain=计数器寿命域〔battle=战斗域重建清零〔缺省〕/adventure=跨战持久随 battle 末导出〕;
## axis=bonus_delta 加算区轴向)
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
"cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id",
"reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit",
"value_event", "value_counter", "domain", "axis"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
## 迭代 22 秘宝实装批 T3 扩签:+safe_dig 安全格直开(天眼——3×3 内安全格按普通挖格规则全额兑付
## 含连锁展开与格面资源,危险格仅真视显示不移除——棋盘事实不可变契约;免行动点即效果本身,C5 裁)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", "safe_dig"]
# ---- 订阅集与计数器账本 ----
var _triggers: Array[Variant] = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死)
var _counters: Dictionary[Variant, Variant] = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明)
var _reset_on_round_start: Array[String] = [] # round_start 重置点计数器名单(装载时收集)
var _adventure_counter_ids: Array[String] = [] # domain=adventure 计数器名单(T6——重建时收集、不清零;
# battle 末 export → 冒险账本、begin_battle seed 回灌)
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
func _init(db_ref: ConfigDb = null) -> void:
db = db_ref
## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器账本同步清零并按订阅行
## 声明收集重置点(round_start)。
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
_triggers.clear()
_reset_on_round_start.clear()
_adventure_counter_ids.clear()
if db != null and db.feats != null:
for row in db.feats.rows:
var fid := String(row.get("feat_id", ""))
if not feat_ids.has(fid):
continue
for tr in (row.get("triggers", []) as Array):
_push_trigger("feat", fid, tr, row)
if db != null:
for row in db.treasure_attunement_rows():
var iid := String(row.get("item_id", ""))
if not item_ids.has(iid):
continue
for tr in (row.get("triggers", []) as Array):
_push_trigger("attune", iid, tr, row)
# 树规则合成订阅(迭代 21 T3 换代——六键按 v0.8 三组双态):posture 两键=「每关首轮窗」语义
# 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带,原「每战首轮」
# 实态随窗语义换代〕;sense 两组=每关限次〔adventure 域计数器 lt 上限闸 + 命中即 counter_add
# 记账双行——同事件双行同预评估态见闸同记,序=装载序确定;生命周期四态:跨战持久=domain
# adventure 导出/回灌、按关重置=AdventureRunner._start_current_level 清零、新冒险清零=账本
# 归零、断点保留=断点快照随键〕;economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的
# tree_rules 成员判定(编排器/关卡 runner 读 enc 键集),不入事件面。数值全 provisional 归 M4。
for key_v in tree_rules:
var rk := String(key_v)
match rk:
"posture_first_strike":
_push_trigger("tree", rk, {"on": "round_start",
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
"posture_bulwark":
_push_trigger("tree", rk, {"on": "damage_taken",
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "shield", "value": 1}}, {})
"sense_sharp":
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
_push_trigger("tree", rk, {"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "damage_mult", "value": 50}}, {})
_push_trigger("tree", rk, {"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_sharp_used",
"value": 1, "domain": "adventure"}}, {})
"sense_guard":
# 守=标错返还行动点·每关限次(契约 12 失误补偿——flag_missed 由 _judge_flag 去重闸后
# 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立)
_push_trigger("tree", rk, {"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
_push_trigger("tree", rk, {"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_guard_used",
"value": 1, "domain": "adventure"}}, {})
# 计数器账本重建(T6——battle 域清零照旧;adventure 域由 counters_seed 回灌〔rebuild 后调用〕)
_counters = {}
func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void:
var on := String(tr.get("on", ""))
assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id])
var do_d: Dictionary[Variant, Variant] = (tr.get("do", {}) as Dictionary).duplicate(true)
# *_from 解析(rebuild 时把行字段引用定影为值——value_from:"magnitude" → value=行 magnitude)
for pair in [["value_from", "value"], ["delta_from", "delta"], ["mult_from", "mult"], ["cap_from", "cap"]]:
if do_d.has(pair[0]):
do_d[pair[1]] = int(row.get(String(do_d[pair[0]]), 0))
do_d.erase(pair[0])
# when value_from 同面解析(行字段引用条件——dawn_order effective_rounds 先例:字面量出行=数值
# 单一事实源破面,行引用随 rebuild 定影)
var when_d: Array[Variant] = (tr.get("when", []) as Array).duplicate(true)
for c_v in when_d:
var c: Dictionary[Variant, Variant] = c_v as Dictionary
if c.has("value_from"):
c["value"] = int(row.get(String(c["value_from"]), 0))
c.erase("value_from")
var t := {"source": source, "id": id, "on": on,
"when": when_d,
"do": do_d}
_triggers.append(t)
# 计数器重置点收集(counter_add/counter_spend 行声明 reset_at)
if do_d.has("counter_id") and String(do_d.get("reset_at", "")) == "round_start":
var cid := String(do_d.counter_id)
if not _reset_on_round_start.has(cid):
_reset_on_round_start.append(cid)
# 寿命域收集(T6——domain=adventure 计数器跨战持久:重建不清零、battle 末导出回冒险账本)
if do_d.has("counter_id") and String(do_d.get("domain", "")) == "adventure":
var aid := String(do_d.counter_id)
if not _adventure_counter_ids.has(aid):
_adventure_counter_ids.append(aid)
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存)
func has_effect_on(event: String, kind: String, mode: String = "") -> bool:
for tr in _triggers:
if String(tr.on) != event:
continue
var d: Dictionary[Variant, Variant] = tr.do
if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode):
# 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺)
return true
return false
## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。
## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(charge_layers/nest_count 等
## 运行态读数 Callable——payload 未带时回落)。
func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) -> Array:
assert(EVENTS.has(event), "[效果引擎] 未知事件: %s(发射点出域)" % event)
if event == "round_start":
for cid in _reset_on_round_start:
_counters[cid] = 0
var hits: Array[Variant] = []
for tr in _triggers:
if String(tr.on) != event:
continue
if not _when_ok(tr.when, payload, facts):
continue
hits.append(tr.do)
return hits
## 计数器读数(fact counter 与发射点配额面共用)
func counter(counter_id: String) -> int:
return int(_counters.get(counter_id, 0))
func counter_add(counter_id: String, value: int, set_mode: bool = false) -> void:
if set_mode:
_counters[counter_id] = value
else:
_counters[counter_id] = int(_counters.get(counter_id, 0)) + value
## 计数器消耗(counter_spend 组合规则——按订阅序消耗共享资源,耗尽即止):
## 返回 true=消耗成功;false=资源不足(调用方按 reason 通道拒绝)。
func counter_spend(counter_id: String, value: int) -> bool:
var cur := int(_counters.get(counter_id, 0))
if cur < value:
return false
_counters[counter_id] = cur - value
return true
## adventure 域计数器导出(T6——battle 末调用,快照回冒险账本;仅 domain=adventure 名单内键)
func counters_export() -> Dictionary:
var out := {}
for cid in _adventure_counter_ids:
out[cid] = int(_counters.get(cid, 0))
return out
## adventure 域计数器回灌(T6——rebuild 后由发射宿主调用:冒险账本值回填名单内键,缺省 0;
## 名单=本次 rebuild 装载行声明 domain=adventure 的 counter_id 集)
func counters_seed(seed: Dictionary) -> void:
for cid in _adventure_counter_ids:
_counters[cid] = maxi(0, int(seed.get(cid, 0)))
# ---- when 判定(条件谓词——零随机全确定) ----
func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool:
for c_v in when:
if not _cond_ok(c_v as Dictionary, payload, facts):
return false
return true
func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
var fact := String(c.get("fact", ""))
var op := String(c.get("op", "eq"))
var val = c.get("value", null)
var actual = _fact_value(fact, c, payload, facts)
match op:
"eq":
return str(actual) == str(val)
"neq":
return str(actual) != str(val)
"gt":
return int(actual) > int(val)
"gte":
return int(actual) >= int(val)
"lt":
return int(actual) < int(val)
"lte":
return int(actual) <= int(val)
"ends_with":
if val is Array:
for v in (val as Array):
if String(actual).ends_with(String(v)):
return true
return false
return String(actual).ends_with(String(val))
"has":
# 在场判定(fact 查询面落法——代拍板白名单,commit 留痕):value 缺省=非空判定
# (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场);
# value 给定=数组包含判定(保留泛用面)。
if c.has("value"):
if actual is Array:
return (actual as Array).has(val)
return false
if actual is Array:
return not (actual as Array).is_empty()
return actual != null and String(actual) != "" # null 安全(迭代 20 T2 修正——敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错;候选流变化后 devour 件首次被持有而暴露的潜伏缺陷)
"mod_zero":
var m := int(val)
return m > 0 and int(actual) % m == 0
"mod_eq":
var me_m := int(val)
return me_m > 0 and int(actual) % me_m == int(c.get("ref", 0))
"advantageous":
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
return _counter_delta_of(String(actual)) > 0
"gte_ref":
return int(actual) >= counter(String(c.get("ref", "")))
"lt_ref":
return int(actual) < counter(String(c.get("ref", "")))
assert(false, "[效果引擎] when 算子出域: %s" % op)
return false
func _fact_value(fact: String, c: Dictionary, payload: Dictionary, facts: Callable):
match fact:
"counter":
return counter(String(c.get("id", "")))
_:
# fact 取值序:payload 同名字段优先 → 补充事实面(发射点注入的运行态读数)
if payload.has(fact):
return payload[fact]
if facts.is_valid():
return facts.call(fact)
return null
## 克制矩阵读值(advantageous 算子消费——与伤害控制器 _counter_delta 同源同表)
func _counter_delta_of(matchup: String) -> int:
if db == null or matchup == "" or not matchup.contains("_vs_"):
return 0
var parts := matchup.split("_vs_")
if parts.size() != 2:
return 0
var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness
var row: Dictionary[Variant, Variant] = tbl.get(String(parts[0]), {})
return int(row.get(String(parts[1]), 0))
## ---- 加载期校验(schema 侧共用——单一事实源;悬空引用/词表出域=致命) ----
## trigger 行校验:返回该行是否含扫雷侧事件订阅(流派铁律聚合面)。
## row 用于 value_from 类引用的字段存在性判定(悬空=加载致命)。
static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, errs: Array) -> bool:
var mine_covered := false
for tr_v in triggers:
var tr: Dictionary[Variant, Variant] = tr_v as Dictionary
for k in tr.keys():
if not ["on", "when", "do"].has(String(k)):
errs.append({"category": "zero_flat", "message": "%s trigger 键出白名单: %s" % [rid, String(k)]})
var on := String(tr.get("on", ""))
if not EVENTS.has(on):
errs.append({"category": "enum", "message": "%s 订阅事件出域(事件词汇封闭): %s" % [rid, on]})
elif MINE_EVENTS.has(on):
mine_covered = true
for c_v in (tr.get("when", []) as Array):
var c: Dictionary[Variant, Variant] = c_v as Dictionary
for k in c.keys():
if not WHEN_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]})
var fact := String(c.get("fact", ""))
if not FACTS.has(fact):
errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 9 项): %s" % [rid, fact]})
if not OPS.has(String(c.get("op", "eq"))):
errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]})
var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary
for k in do_d.keys():
if not DO_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]})
var kind := String(do_d.get("kind", ""))
if not EFFECT_KINDS.has(kind):
errs.append({"category": "enum", "message": "%s 效果类型出域(解释器闭集): %s" % [rid, kind]})
# value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)——
# do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例)
for vf_k in ["value_from", "delta_from", "mult_from", "cap_from"]:
if do_d.has(vf_k):
var fld := String(do_d.get(vf_k, ""))
if fld != "" and not row.has(fld):
errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]})
for c_v in (tr.get("when", []) as Array):
var c_w: Dictionary[Variant, Variant] = c_v as Dictionary
if c_w.has("value_from"):
var wfld := String(c_w.get("value_from", ""))
if wfld != "" and not row.has(wfld):
errs.append({"category": "dangling", "message": "%s when.value_from 悬空(行无字段 %s)" % [rid, wfld]})
return mine_covered
## 组合规则(需求卡片 §3.4 六族——效果类型的必填属性,发射点施加面引用)
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、
## counter_add、shield——发射点逐条累加收集器。
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
## 任意真族(任一命中即生效):spawn_spell、overflow、spike。
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、
## rewrite_board、apply_status——发射点逐条执行。