T1 schema 换代:MetaTreeTable FORK_GROUPS 4→3(fg_mastery 退役)/RULE_KEYS 2→6(sense/economy 四键扩签)/ fork 单行双态(effects_a/b+name_a/b、每组恰一校验)/row 字段退役(行门槛主语=节点等级 level_gate)/ requires 只留点亮前置(R1 裁撤案执行);meta_tree.tres 33→12 行重排(脊柱2/锻造2含彩蛋/钥石2/同调6, att_socket1 改根节点无前置,easter_ley 摘 atk/armor_class 分量=契约9) T2 购买与彩蛋面:tree_can_buy easter 拒购+行门槛换轴;tree_buy_fork=解锁默认A;tree_toggle_fork 零币切态; tree_rule_keys 按激活态 effects_a/b 投影;_check_easter_egg 特化判定(两线拉满+socket全解锁);营地树行板双态钮 T3 六键接线:EVENTS+flag_missed/FACTS+battle_index(L2 扩签两处);rebuild 六键合成订阅 (posture 窗=battle_index&&round_index 双闸/sense 两组=counter 闸+记账双行 domain:adventure 限次2次 provisional); level_runner battle_index 注入(visited battle 型计数);_judge_flag 去重闸后 flag_missed 发射(契约12); 聚=_roll_gp_drop 入账前×1.5 provisional(escrow 不乘);散=_spend_discounted ×80%(current_price/pool_price 双计价点,刷新价不折=§11②定义边界);_start_current_level 限次按关清零(_reset_tree_limited_counters 可测口) T4 注入换代:hero_override=树淬炼flat+FORGE_ROUTE_BONUS预留0(命中不注入=契约6);Attunement.slots 判定源切 树点亮数(谓词表退役);tree_buy(ks_refresh) 同账路由 buy_attune_refresh(V15 双入口) T5 旧档废弃与删除:save_manager tree_struct=2 结构版本标记+_normalize 白名单重建+树域废弃重置(禁迁移折算); migrate_tree/buy_forge/forge_flat/forge_level/forge_price/meta_forge_row/forge_products 行族/ attunement_growth 表+访问器/生涯页三投影/career forge_total 全删;treasure_item_table UNLOCK_COUNTER_KEYS 内联; verifier_batch 花费策略换代树节点;MC slots 画像常量3 T6 营地统一:四柜台并排横滚(树淬炼主界面/锻造工坊钥匙门+零交互占位面板/秘宝匣钥匙门+内容迁入/图鉴常开); 暂停层锻造段连带删;camp data forge 投影删 存量红修复:_flush_pending_reveals 三处空 source_class 补 fx_reveal+类级豁免(a57489e9 落地件,冒烟51行) 测试随批:smoke 树段全重写(V1/V2/V3/V8/V9/V16)+_smoke_tree_rules V10 行为段(窗/限次/守侧五场景编排rig/聚散 同种子对拍)+单元 tree_rules_v11_test(六键合成行闭集校验)+run_all 22 套;ui_probe/ui_click_probe 换代树点击路径 轻闸全绿:冒烟886/0+run_all 22套+probe --click 全绿
426 lines
23 KiB
GDScript
426 lines
23 KiB
GDScript
# 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〕;原发射点不动)
|
||
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"]
|
||
|
||
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥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"]
|
||
# 注: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 分发)
|
||
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
|
||
|
||
# ---- 订阅集与计数器账本 ----
|
||
var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死)
|
||
var _counters: Dictionary = {} # 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 = (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 = (tr.get("when", []) as Array).duplicate(true)
|
||
for c_v in when_d:
|
||
var c: Dictionary = 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 = 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 = []
|
||
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 = db.settlement.armor_effectiveness
|
||
var row: Dictionary = 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 = 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 = 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 = 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 = 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——发射点逐条执行。
|
||
static func combo_of(kind: String) -> String:
|
||
match kind:
|
||
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield", "heal", "ac_bonus", "damage_reduce", "bonus_delta":
|
||
return "additive"
|
||
"grant_ap":
|
||
return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法)
|
||
"yield_mult", "damage_mult":
|
||
return "multiplicative"
|
||
"spawn_spell", "overflow", "spike":
|
||
return "any_true"
|
||
"modify_ticket":
|
||
return "ordered_write"
|
||
"counter_spend":
|
||
return "ordered_spend"
|
||
"open_ticket", "apply_status", "blast_scaled":
|
||
return "immediate"
|
||
return "immediate" # 默认即时执行(每条命中各自执行一次)
|