From a721e529ef9c930a9cd33c6c43e9cce30b35397b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 20 Oct 2026 22:57:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8D=A1=E2=91=A1=20T2):=20EffectEngine?= =?UTF-8?q?=20=E5=88=A0=E8=BF=90=E8=A1=8C=E6=97=B6=E7=8A=B6=E6=80=81=20+?= =?UTF-8?q?=20ctx=20=E6=8E=A5=E5=8F=A3=20+=20=E9=9D=99=E6=80=81=E5=86=99?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删成员 _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(本提交为无状态内核检查点,全绿在批末)。 --- game/core/effect_engine.gd | 136 +++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 73 deletions(-) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 59461b6a..6b37cf99 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -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):