## VERTICAL SLICE - NOT FOR PRODUCTION ## 效果引擎:内容行 on/when/do 的发布订阅解释器——加一件「已有效果类型」的专长/秘宝只写 .tres 数据行零代码(垂直切片验证:内容数据驱动可复现)。 ## 全同步调用、零 RNG、零信号时序依赖;命中序=装载声明序(feats→treasure→树规则)写死保种子复放。 ## 事件/效果/事实/算子四词表封闭在本文件常量——新词先登记,行内出域=加载期校验致命、dispatch 未知词 fail-fast。 class_name EffectEngine extends RefCounted ## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕 ## +battle_won 战斗胜利〔战地包扎〕;迭代 21 天赋树批 T3 扩签 18→19:+seal_missed 标错判定 ## 〔危险直觉·守挂点——_judge_seal 去重闸后发射,契约 12 触发主语=当回合标错动作; ## 命名避让 _emit_row 在册 source_class 名 seal_miss〕;迭代 22 秘宝实装批 T3 扩签 19→20: ## +board_installed 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕;原发射点不动) const EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "attack_commit", "clamp_check", "armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "seal_settle", "round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "seal_missed", "board_installed"] ## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。 ## 原 MINE_HOOKS 的同义迁移:挖掘/封印/轮末结算面事件) const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_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_seal=被挖格 ## 当时有封印〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水 ## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕) const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class", "seals_count", "charge_layers", "nest_count", "round_index", "counter", "new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_seal", "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 失误补偿——seal_missed 由 _judge_seal 去重闸后 # 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立) _push_trigger("tree", rk, {"on": "seal_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": "seal_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——发射点逐条执行。