From 989c489c553f87e87b1510608bbe2cdbe9f9a37f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 08:03:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A0T4(=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=9D=A2=E7=BB=93=E6=9E=84=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E6=89=B9):=20=E6=95=88=E6=9E=9C=E5=BC=95=E6=93=8E=E5=9B=9B?= =?UTF-8?q?=E5=9F=9F=E6=8B=86=E5=88=86=E2=80=94=E2=80=94rebuild=E4=B8=89?= =?UTF-8?q?=E8=A3=85=E8=BD=BD=E5=9F=9F(=5Fload=5Ffeat=5Ftriggers/=5Fload?= =?UTF-8?q?=5Fattune=5Ftriggers/=5Fpush=5Ftree=5Frules+=E5=8D=95=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=8A=A9=E6=89=8B,=E8=B0=83=E7=94=A8=E5=BA=8Ffeats?= =?UTF-8?q?=E2=86=92treasure=E2=86=92tree=E9=80=90=E5=AD=97=3D=E8=A3=85?= =?UTF-8?q?=E8=BD=BD=E5=A3=B0=E6=98=8E=E5=BA=8F=E5=86=BB=E7=BB=93)/=5Fcond?= =?UTF-8?q?=5Fok=E5=9B=9B=E7=AE=97=E5=AD=90=E5=9F=9F(=5Fcmp=5Fok=E5=87=BA?= =?UTF-8?q?=E5=8F=82=E8=81=9A=E5=90=88/=5Fends=5Fwith=5Fok/=5Fhas=5Fok/=5F?= =?UTF-8?q?counter=5Fop=5Fok,unknown-op=E8=87=B4=E5=91=BD=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E4=B8=8E=E5=88=A4=E5=AE=9A=E8=AF=AD=E4=B9=89=E9=80=90?= =?UTF-8?q?=E5=AD=97)/bucket=5Fof=20elif=E6=94=B6=E5=8F=A3if=E5=BA=8F(?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E5=88=A4=E5=AE=9A=E5=BA=8Fround=E2=86=92lvl?= =?UTF-8?q?=E2=86=92adv=E2=86=92battle=E9=80=90=E5=AD=97)/validate=5Ftrigg?= =?UTF-8?q?er=5Frow=E4=B8=89=E9=94=AE=E5=9F=9F(=5Fvtr=5Fwhen=5Fkeys/=5Fvtr?= =?UTF-8?q?=5Fdo=5Fkeys+=5Fvtr=5Ffrom=5Frefs=E6=82=AC=E7=A9=BA=E5=9F=9F,st?= =?UTF-8?q?atic=E5=90=8C=E5=BD=A2,=E9=94=99=E8=AF=AF=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E5=BA=8F=E9=80=90=E5=AD=97);=E5=9B=9B=E8=AF=8D=E8=A1=A8?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E9=9B=B6diff,8=E9=94=AE=E5=85=A8=E6=B8=85,?= =?UTF-8?q?=E6=95=88=E6=9E=9C=E9=9D=A26=E5=A5=97=E4=BB=B6=E5=85=A8?= =?UTF-8?q?=E7=BB=BF(138checks=20adventure=5Ffeats)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/effect_engine.gd | 272 ++++++++++++++++++++++--------------- 1 file changed, 166 insertions(+), 106 deletions(-) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index db949132..1766db07 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -94,6 +94,13 @@ func _init(db_ref: ConfigDb = null) -> void: ## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随宿主字典引用跨战持久——无副本搬运)。 func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: _triggers.clear() + _load_feat_triggers(feat_ids) + _load_attune_triggers(item_ids) + _push_tree_rules(tree_rules) + + +## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段) +func _load_feat_triggers(feat_ids: Array) -> void: if db != null and db.feats != null: for row in db.feats.rows: var fid := String(row.get("feat_id", "")) @@ -101,6 +108,10 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: continue for tr in (row.get("triggers", []) as Array): _push_trigger("feat", fid, tr, row) + + +## 同调秘宝订阅装载域(treasure 同调行序→按装备集过滤→行内数组序——装载声明序第二段) +func _load_attune_triggers(item_ids: Array) -> void: if db != null: for row in db.treasure_attunement_rows(): var iid := String(row.get("item_id", "")) @@ -108,44 +119,51 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: continue for tr in (row.get("triggers", []) as Array): _push_trigger("attune", iid, tr, row) - # 树规则合成订阅(六键按 v0.8 三组双态):posture 两键=「每关首轮窗」语义 - # 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带〕; - # sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同 - # 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕; - # economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。 + + +## 树规则合成订阅域(六键按 v0.8 三组双态——装载声明序第三段):posture 两键=「每关首轮窗」语义 +## 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带〕; +## sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同 +## 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕; +## economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。 +func _push_tree_rules(tree_rules: Array) -> void: 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": "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": "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": "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": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}], - "do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used", - "value": 1}}, {}) + _push_tree_rule(String(key_v)) + + +## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动) +func _push_tree_rule(rk: String) -> void: + 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": "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": "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": "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": "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: @@ -203,13 +221,14 @@ func dispatch(ctx: EffectContext, event: String, payload: Dictionary, facts: Cal return hits -## 计数器归属桶解析(前缀规则唯一出处——round_/lvl_/adv_ 显式桶,其余落 battle 缺省桶)。 +## 计数器归属桶解析(前缀规则唯一出处——round_/lvl_/adv_ 显式桶,其余落 battle 缺省桶; +## 前缀判定序 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_"): + if counter_id.begins_with("lvl_"): return ctx.level_bucket - elif counter_id.begins_with("adv_"): + if counter_id.begins_with("adv_"): return ctx.adventure_bucket return ctx.battle_bucket @@ -262,41 +281,68 @@ func _cond_ok(ctx: EffectContext, c: Dictionary, payload: Dictionary, facts: Cal var val = c.get("value", null) var actual = _fact_value(ctx, 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) + "eq", "neq", "gt", "gte", "lt", "lte": + return _cmp_ok(op, actual, 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)) + return _ends_with_ok(actual, val) "has": - # 在场判定(fact 查询面落法——白名单覆盖):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 安全(敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错) + return _has_ok(c, actual) + "mod_zero", "mod_eq", "advantageous", "gte_ref", "lt_ref": + return _counter_op_ok(ctx, c, op, actual) + assert(false, "[效果引擎] when 算子出域: %s" % op) + return false + + +## 比较算子域(eq/neq/gt/gte/lt/lte 六比较——str 逐字与 int 投影语义逐字) +func _cmp_ok(op: String, actual: Variant, val: Variant) -> bool: + var out := false + match op: + "eq": + out = str(actual) == str(val) + "neq": + out = str(actual) != str(val) + "gt": + out = int(actual) > int(val) + "gte": + out = int(actual) >= int(val) + "lt": + out = int(actual) < int(val) + "lte": + out = int(actual) <= int(val) + return out + + +## 后缀匹配域(ends_with——值数组任一命中即真) +func _ends_with_ok(actual: Variant, val: Variant) -> bool: + 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 查询面落法,白名单覆盖):value 缺省=非空判定 +## (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场); +## value 给定=数组包含判定(保留泛用面) +func _has_ok(c: Dictionary, actual: Variant) -> bool: + if c.has("value"): + if actual is Array: + return (actual as Array).has(c.get("value")) + return false + if actual is Array: + return not (actual as Array).is_empty() + return actual != null and String(actual) != "" # null 安全(敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错) + + +## 计数器与矩阵算子域(mod_zero/mod_eq 取模、advantageous 克制矩阵、gte_ref/lt_ref 计数器比较——读经 ctx) +func _counter_op_ok(ctx: EffectContext, c: Dictionary, op: String, actual: Variant) -> bool: + match op: "mod_zero": - var m := int(val) + var m := int(c.get("value", null)) return m > 0 and int(actual) % m == 0 "mod_eq": - var me_m := int(val) + var me_m := int(c.get("value", null)) return me_m > 0 and int(actual) % me_m == int(c.get("ref", 0)) "advantageous": # 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立) @@ -305,7 +351,6 @@ func _cond_ok(ctx: EffectContext, c: Dictionary, payload: Dictionary, facts: Cal return int(actual) >= counter(ctx, String(c.get("ref", ""))) "lt_ref": return int(actual) < counter(ctx, String(c.get("ref", ""))) - assert(false, "[效果引擎] when 算子出域: %s" % op) return false @@ -350,44 +395,59 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, 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 出域(事实词汇封闭 %d 项): %s" % [rid, FACTS.size(), 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]}) - # 计数器前缀归属合法性(.tres 行路径——写错前缀加载致命;判据单点 counter_id_legal,与 _push_trigger 同源) - if do_d.has("counter_id"): - var cid := String(do_d.counter_id) - if not counter_id_legal(cid): - errs.append({"category": "enum", "message": "%s counter_id 前缀非法(meta_ 出域/引擎不接存档级): %s" % [rid, cid]}) - # 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]}) + _vtr_when_keys(rid, tr, errs) + _vtr_do_keys(rid, tr, row, errs) return mine_covered +## when 键域校验:键白名单/fact 词表/算子词表(追加序承原位——when 键→fact→op) +static func _vtr_when_keys(rid: String, tr: Dictionary, errs: Array) -> void: + 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 出域(事实词汇封闭 %d 项): %s" % [rid, FACTS.size(), fact]}) + if not OPS.has(String(c.get("op", "eq"))): + errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]}) + + +## do 键域校验:键白名单(零裸 flat 护栏)/效果类型闭集/计数器前缀合法性+value_from 引用悬空域 +static func _vtr_do_keys(rid: String, tr: Dictionary, row: Dictionary, errs: Array) -> void: + 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]}) + # 计数器前缀归属合法性(.tres 行路径——写错前缀加载致命;判据单点 counter_id_legal,与 _push_trigger 同源) + if do_d.has("counter_id"): + var cid := String(do_d.counter_id) + if not counter_id_legal(cid): + errs.append({"category": "enum", "message": "%s counter_id 前缀非法(meta_ 出域/引擎不接存档级): %s" % [rid, cid]}) + _vtr_from_refs(rid, tr, row, errs) + + +## value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)—— +## do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例) +static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: Array) -> void: + var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary + 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]}) + + ## 组合规则(需求卡片 §3.4 六族——效果类型的必填属性,发射点施加面引用) ## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、