feat(卡② T2): EffectEngine 删运行时状态 + ctx 接口 + 静态写口

- 删成员 _counters/_reset_on_round_start/_adventure_counter_ids(引擎零运行时可变状态,仅 _triggers+db)
- dispatch(ctx, event, payload, facts);round_start 特判改 ctx.round_bucket.clear() 整桶就地清空
- counter/dispatch 读路径 _when_ok/_cond_ok/_fact_value 全经 ctx
- 新增 static bucket_of(ctx,id)(前缀→桶唯一出处)+ counter_id_legal(只禁 meta_)
- 写口改 static counter_add/counter_spend(算术逐字承旧);删实例版与 counters_export/counters_seed
- 树合成行 sense 两键改名 lvl_tree_sense_*,删 domain/reset_at 键;_push_trigger 增前缀 assert
- rebuild 签名不变(冒烟调用面零迁移)
调用点施加点迁移见 T3,数据行/测试冒烟迁移见 T6/T7(本提交为无状态内核检查点,全绿在批末)。
This commit is contained in:
DKChan committed 2026-10-20 22:57:44 +08:00
1 parent ae5baed699
commit a721e529ef
1 file changed
+63 -73
+63 -73
View File
@@ -79,12 +79,8 @@ const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult",
## 含连锁展开与格面资源,危险格仅真视显示不移除——棋盘事实不可变契约;免行动点即效果本身)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", "safe_dig"]
# ---- 订阅集与计数器账本 ----
# ---- 订阅集(编译产物=程序,非状态;计数器四桶经 ctx 外置宿主持有)----
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 计数器名单(重建时收集、不清零;
# battle 末 export → 冒险账本、begin_battle seed 回灌)
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
@@ -93,12 +89,11 @@ func _init(db_ref: ConfigDb = null) -> void:
## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器账本同步清零并按订阅行
## 声明收集重置点(round_start)。
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器四桶由宿主持有、随宿主生灭,
## 本方法只重建订阅集(运行时可变状态=零;battle/level 桶的清零由宿主 new 天然承载,round 桶清零
## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随 RunLedger 引用跨战持久——无副本搬运)。
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", ""))
@@ -115,11 +110,9 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
_push_trigger("attune", iid, tr, row)
# 树规则合成订阅(六键按 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。
# sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同
# 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕;
# economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。
for key_v in tree_rules:
var rk := String(key_v)
match rk:
@@ -137,30 +130,31 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
# 锋=踩中危险格伤害减半·每关限次——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}],
"when": [{"fact": "counter", "id": "lvl_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"}}, {})
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used",
"value": 1}}, {})
"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}],
"when": [{"fact": "counter", "id": "lvl_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"}}, {})
# 计数器账本重建(battle 域清零;adventure 域由 counters_seed 回灌〔rebuild 后调用〕)
_counters = {}
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used",
"value": 1}}, {})
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)
# 树合成行 counter_id 前缀校验(与 validate_trigger_row 同源判据;写错前缀调试期致命)
if do_d.has("counter_id") and not counter_id_legal(String(do_d.counter_id)):
assert(false, "[效果引擎] 合成行 counter_id 前缀非法: %s(%s %s)" % [String(do_d.counter_id), source, id])
# *_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]):
@@ -178,16 +172,6 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary)
"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)
# 寿命域收集(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 转存)
@@ -203,74 +187,80 @@ func has_effect_on(event: String, kind: String, mode: String = "") -> bool:
## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。
## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(charge_layers/nest_count 等
## 运行态读数 Callable——payload 未带时回落)。
func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) -> Array:
## ctx = 四桶引用束(引擎不自持状态,计数器读写全经 ctx);
## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(运行态读数 Callable)。
func dispatch(ctx: EffectContext, 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
ctx.round_bucket.clear() # 整桶就地清空(禁 ={} 重赋值断引用;round_ 前缀键集≡旧名单,逐位等价)
var hits: Array[Variant] = []
for tr in _triggers:
if String(tr.on) != event:
continue
if not _when_ok(tr.when, payload, facts):
if not _when_ok(ctx, 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))
## 计数器归属桶解析(前缀规则唯一出处——round_/lvl_/adv_ 显式桶,其余落 battle 缺省桶)。
static func bucket_of(ctx: EffectContext, counter_id: String) -> Dictionary[Variant, Variant]:
if counter_id.begins_with("round_"):
return ctx.round_bucket
elif counter_id.begins_with("lvl_"):
return ctx.level_bucket
elif counter_id.begins_with("adv_"):
return ctx.adventure_bucket
return ctx.battle_bucket
func counter_add(counter_id: String, value: int, set_mode: bool = false) -> void:
## counter_id 前缀合法性(加载期校验判定单点):round_/lvl_/adv_ 合法、无前缀落 battle 合法、meta_ 非法
## (引擎不接存档级);其余未知前缀合法(落 battle——封闭词表方案误伤全部既有 battle id,见卡② §12 裁点②)。
static func counter_id_legal(counter_id: String) -> bool:
if counter_id.begins_with("meta_"):
return false
return true
## 计数器读数(fact counter 与发射点配额面共用——纯查询读 ctx,引擎不自持)。
static func counter(ctx: EffectContext, counter_id: String) -> int:
return int(bucket_of(ctx, counter_id).get(counter_id, 0))
## 计数器累加/覆写(静态纯函数——写口语义单点,7 施加点共用;算术逐字承自旧实例方法)。
static func counter_add(ctx: EffectContext, counter_id: String, value: int, set_mode: bool = false) -> void:
var bucket := bucket_of(ctx, counter_id)
if set_mode:
_counters[counter_id] = value
bucket[counter_id] = value
else:
_counters[counter_id] = int(_counters.get(counter_id, 0)) + value
bucket[counter_id] = int(bucket.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))
static func counter_spend(ctx: EffectContext, counter_id: String, value: int) -> bool:
var bucket := bucket_of(ctx, counter_id)
var cur := int(bucket.get(counter_id, 0))
if cur < value:
return false
_counters[counter_id] = cur - value
bucket[counter_id] = cur - value
return true
## adventure 域计数器导出(battle 末调用,快照回冒险账本;仅 domain=adventure 名单内键)
func counters_export() -> Dictionary:
var out := {}
for cid in _adventure_counter_ids:
out[cid] = int(_counters.get(cid, 0))
return out
# ---- when 判定(条件谓词——零随机全确定;计数器读经 ctx) ----
## adventure 域计数器回灌(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:
func _when_ok(ctx: EffectContext, when: Array, payload: Dictionary, facts: Callable) -> bool:
for c_v in when:
if not _cond_ok(c_v as Dictionary, payload, facts):
if not _cond_ok(ctx, c_v as Dictionary, payload, facts):
return false
return true
func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
func _cond_ok(ctx: EffectContext, 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)
var actual = _fact_value(ctx, fact, c, payload, facts)
match op:
"eq":
return str(actual) == str(val)
@@ -312,17 +302,17 @@ func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
return _counter_delta_of(String(actual)) > 0
"gte_ref":
return int(actual) >= counter(String(c.get("ref", "")))
return int(actual) >= counter(ctx, String(c.get("ref", "")))
"lt_ref":
return int(actual) < counter(String(c.get("ref", "")))
return int(actual) < counter(ctx, String(c.get("ref", "")))
assert(false, "[效果引擎] when 算子出域: %s" % op)
return false
func _fact_value(fact: String, c: Dictionary, payload: Dictionary, facts: Callable):
func _fact_value(ctx: EffectContext, fact: String, c: Dictionary, payload: Dictionary, facts: Callable):
match fact:
"counter":
return counter(String(c.get("id", "")))
return counter(ctx, String(c.get("id", "")))
_:
# fact 取值序:payload 同名字段优先 → 补充事实面(发射点注入的运行态读数)
if payload.has(fact):