Files
gplan-2/game/core/effect_engine.gd
T
DKChan 7d157b3c4c 效果引擎框架落地:事件发布订阅解释器+16 发射点埋点(T1——内容数据驱动扩展批)
把「内容的效果行为」从结算代码硬编码分支搬进数据行的第一步:通用解释器 EffectEngine 落地。
本提交=框架与埋点,双轨过渡期——发射点仅观测命中、施加面仍走旧消费点,行为与迁移前逐位一致
(冒烟 783 项 0 失败实证、单元 17 套全绿)。

新增 game/core/effect_engine.gd:
- 事件词汇 16 个(任务书 §3.1 定稿)+扫雷侧事件集(流派扫雷触发铁律新形态)
- 效果类型词汇 24 型(任务书 §3.3 定稿 19 行;标题计 23 系笔误按行内容全收,代拍板留痕)
- fact 条件词汇 9 项+when 算子词汇封闭(mod_zero/advantageous/gte_ref/lt_ref/ends_with
  系现状条件装进 9 项 fact 的查询面落法——代拍板白名单覆盖:掘进蓄能取模、淬毒克制正面、
  深挖配额计数器比较、对某甲族泛条件)
- 订阅装载 rebuild(每战 begin_battle 重建——中途获得下一战生效,同战内不热载;
  装载序=feats 表序→treasure 表序→树规则合成行,写死保种子复放)
- 计数器账本(counter_add/counter_spend/reset_at=round_start 重置点声明)
- 组合规则六族(加法/乘法/任意真/按序改写/按序消耗/即时执行——发射点施加面引用)

16 发射点埋点(订阅空=零循环零扰动):
- 战斗编排器:dig_reveal/mine_hit/flag_hit/flag_settle/round_end/enemy_hp_threshold/
  treasure_used/damage_taken(kind=terrain)/round_start×2
- 伤害控制器:attack_commit(hero_attack 单算术前)/clamp_check×2(主角攻击+棋盘克制面)/
  armor_roll/kill/damage_taken(kind=enemy_skill)
- 行动经济:dig_paid(对价闸前)/charge_full(转蓄力满层沿——R6 框架先行,现无订阅者)
- 地图战斗资源:reward_roll(摇号前权重合成层)

行为等价细节(本批预演、T3 落地):
- 停手转存改记账制:request_stop 记剩余 AP 后清零,转存决策移至 round_end 发射点
  finish_round_stop(grant_ap(mode:transfer) 订阅定夺;双轨期旧旗并行,缺省 RAP 转存不变)
- attack_commit 发射点=管线算术前(载荷含 matchup/dig_streak/nest_count——连珠矢/执拗/
  猎巢人/淬毒订阅事实源)

设计权威:design/research/研究-内容数据驱动扩展架构-2026-10-06.md(案乙)。
任务书:design/research/任务书-内容数据驱动扩展批-2026-10-06.md §六 T1。
2026-10-06 21:22:33 +08:00

275 lines
13 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
## 事件词汇(16 个发射点——任务书 §3.1 定稿;4 个登记暂不做事件不入本表)
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"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥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", "rewrite_board", "reveal_tiles", "remove_mines", "refresh_pick",
"bias_rewards", "preview"]
## fact 条件词汇(封闭枚举 9 项——任务书 §四定稿)
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter"]
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法,
## 代拍板白名单覆盖,commit 留痕)
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
"advantageous", "gte_ref", "lt_ref"]
## when 条件键白名单(trigger 行 when 数组元素键)
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面)
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
"cap", "cap_from", "per", "divisor", "dtype", "mode", "channel", "counter_id", "reset_at",
"set", "status_id", "label", "fallback", "target_kinds"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;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 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()
_counters.clear()
_reset_on_round_start.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)
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)
# 树规则合成订阅(T5——规则键效果走同面:非数据行,词表同闭集)
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": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}})
"posture_bulwark":
_push_trigger("tree", rk, {"on": "damage_taken",
"do": {"kind": "shield", "value": 1}})
func _push_trigger(source: String, id: String, tr: Dictionary) -> void:
var on := String(tr.get("on", ""))
assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id])
var t := {"source": source, "id": id, "on": on,
"when": (tr.get("when", []) as Array).duplicate(true),
"do": (tr.get("do", {}) as Dictionary).duplicate(true)}
_triggers.append(t)
# 计数器重置点收集(counter_add/counter_spend 行声明 reset_at)
var do_d: Dictionary = t.do
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)
## 订阅查询面(发射点缺省行为仲裁用——如 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
# ---- 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":
if actual is Array:
return (actual as Array).has(val)
return false
"mod_zero":
var m := int(val)
return m > 0 and int(actual) % m == 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))
# ---- 组合规则(任务书 §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":
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", "rewrite_board", "apply_status":
return "immediate"
return "immediate" # 默认即时执行(每条命中各自执行一次)