merge(迭代 32 卡②): 效果引擎无状态化批合回
EffectEngine 改纯计算引擎(零运行时状态·仅 _triggers+db)+四桶外置宿主(round/battle→编排器、level→LevelRunner、adventure→RunLedger)+ 搬运链整段删除+dig_combo 死路删+前缀校验双落点+引用闭合加载致命+V9 负面套件+V12 同种子复放三组判读通过(off 逐字节一致)+S12 旧件零残留。 完成定义 8 条全绿;代码域闸 all_green(回执 6491f7d5),parity 落 skip(契约 12)。
This commit is contained in:
commit
d00f24a8ec
28 files changed
+653
-312
No files matched your search
@@ -41,12 +41,14 @@ var charge_layers: int:
|
||||
get: return _charge_layers
|
||||
# 效果引擎(dig_paid/charge_full 发射面;round_end 转存决策面)
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
|
||||
var _ap_exhausted_last := false # 上轮 AP 耗尽封印(一滴不剩 round_start when 面)
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
_fx = fx
|
||||
_ctx = ctx
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
@@ -100,15 +102,15 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
# mod_eq 闸在行 when〕同面施加)
|
||||
var dig_refund := 0
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
|
||||
for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
|
||||
if String(h.get("kind", "")) == "counter_spend":
|
||||
var extra := int(h.get("value", 1))
|
||||
if _ap >= cost + extra:
|
||||
cost += extra
|
||||
_fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), 1)
|
||||
elif String(h.get("kind", "")) == "counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
elif String(h.get("kind", "")) == "grant_ap":
|
||||
dig_refund += int(h.get("value", 0))
|
||||
if _ap < cost:
|
||||
@@ -161,12 +163,12 @@ func request_stop() -> bool:
|
||||
# dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
|
||||
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch("dig_stop", {"ap_left": _ap}):
|
||||
for h in _fx.dispatch(_ctx, "dig_stop", {"ap_left": _ap}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var val := int(h.get("value", 0))
|
||||
if String(h.get("value_event", "")) == "ap_left":
|
||||
val = _ap
|
||||
_fx.counter_add(String(h.get("counter_id", "")), val, h.has("set"))
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), val, h.has("set"))
|
||||
_last_stop_ap = _ap
|
||||
_ap = 0
|
||||
_finish_round("stopped", "same_board")
|
||||
@@ -188,7 +190,7 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
|
||||
var prev_layers := _charge_layers
|
||||
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
|
||||
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
|
||||
_fx.dispatch("charge_full", {"layers": _charge_layers})
|
||||
_fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers})
|
||||
else:
|
||||
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
|
||||
_last_stop_ap = 0
|
||||
|
||||
@@ -832,17 +832,9 @@ func abandon() -> bool:
|
||||
|
||||
# ---- 关卡推进(私有——序列编排单一出口)----
|
||||
|
||||
# 树限次计数按关重置(危险直觉锋/守两组「每关限次」账本键:跨战持久由 adventure
|
||||
# 域导出/回灌承载,此处随关清零闭合生命周期四态;键集=效果引擎树合成行声明的 counter_id;
|
||||
# 独立成口=V10 断言面直呼)
|
||||
func _reset_tree_limited_counters() -> void:
|
||||
for cid in ["tree_sense_sharp_used", "tree_sense_guard_used"]:
|
||||
run_ledger.adventure_counters.erase(cid)
|
||||
|
||||
|
||||
func _start_current_level() -> bool:
|
||||
_runner = LevelRunner.new(db, rng)
|
||||
_reset_tree_limited_counters() # 树限次计数按关重置(V10 生命周期四态闭合——断言面直呼本口)
|
||||
# 树限次计数按关清零现由 LevelRunner.level_counters 随每关 new 天然承载(旧硬编码清除名单函数已删)
|
||||
# core↔core 接线只许方法 Callable(S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环)
|
||||
_runner.node_entered.connect(_forward_node_entered)
|
||||
_runner.choices_presented.connect(_forward_choices_presented)
|
||||
|
||||
@@ -82,13 +82,16 @@ var _feat_first_dig_used := false
|
||||
# 效果引擎(发布订阅解释器;begin_battle 从持有集/装备集重建订阅。
|
||||
# 施加面:发射点 dispatch 返回命中,发射点按组合规则施加)
|
||||
var _fx: EffectEngine
|
||||
var fx_counters_round: Dictionary[Variant, Variant] = {} # round 桶(轮始整桶清空;随编排器生灭)
|
||||
var fx_counters_battle: Dictionary[Variant, Variant] = {} # battle 桶(无前缀 id 缺省归此;随编排器每战 new 天然空)
|
||||
var counters_ctx: EffectContext = null # 四桶引用束(_init 建、begin_battle 挂 adventure/level 引用)
|
||||
|
||||
|
||||
# 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
|
||||
func _fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
return []
|
||||
return _fx.dispatch(event, payload, _fx_fact)
|
||||
return _fx.dispatch(counters_ctx, event, payload, _fx_fact)
|
||||
|
||||
|
||||
# round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换
|
||||
@@ -269,6 +272,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_fx = EffectEngine.new(db_ref)
|
||||
counters_ctx = EffectContext.new()
|
||||
counters_ctx.round_bucket = fx_counters_round # 挂自有两桶引用(与编排器成员字段同物异名同字典)
|
||||
counters_ctx.battle_bucket = fx_counters_battle
|
||||
_board_generator = BoardGenerator.new()
|
||||
action_economy = ActionEconomy.new(db)
|
||||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||||
@@ -277,9 +283,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
_reward_economy = RewardEconomy.new(db, rng)
|
||||
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
|
||||
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
|
||||
_damage.set_fx(_fx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
|
||||
action_economy.set_fx(_fx) # dig_paid/charge_full 发射面
|
||||
_reward_economy.set_fx(_fx) # reward_roll 发射面
|
||||
_damage.set_fx(_fx, counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
|
||||
action_economy.set_fx(_fx, counters_ctx) # dig_paid/charge_full 发射面
|
||||
_reward_economy.set_fx(_fx, counters_ctx) # reward_roll 发射面
|
||||
minesweeper.cell_revealed.connect(_on_cell_revealed)
|
||||
minesweeper.seal_judged.connect(_on_seal_judged)
|
||||
minesweeper.round_end_board.connect(_on_round_end)
|
||||
@@ -371,6 +377,10 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var ledger_ref = enc.get("ledger", null)
|
||||
_ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面
|
||||
counters_ctx.adventure_bucket = _ledger.adventure_counters # 挂冒险桶引用(跨战持久=同一字典引用,非副本搬运)
|
||||
var level_ref: Variant = enc.get("level_counters")
|
||||
if level_ref is Dictionary:
|
||||
counters_ctx.level_bucket = level_ref # 挂关卡桶引用(LevelRunner 每关 new 下传)
|
||||
_damage.fx_reveal_request = _fx_enqueue_reveal
|
||||
_damage.fx_ac_request = _apply_ac_bonus
|
||||
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
|
||||
@@ -385,9 +395,8 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
|
||||
# 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;
|
||||
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。
|
||||
# adventure 域计数器从共享引用回灌〔跨战持久〕)
|
||||
# 计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除))
|
||||
_fx.rebuild(feat_ids, attunement, tree_rules)
|
||||
_fx.counters_seed(_ledger.counters_seed_snapshot())
|
||||
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;
|
||||
# 缺省 0/false=零装配零扰动。注入在首板 install 之后〔板在早段〕——板生成不读两参数,
|
||||
# 参数只影响挖掘/插棋动作判据,时序安全)
|
||||
@@ -631,20 +640,16 @@ func _settle() -> void:
|
||||
# ---- 授予段 helpers(极性路由)----
|
||||
|
||||
# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入):
|
||||
# positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。
|
||||
# 全零收集器=原快照原样返回(零扰动)。combo 读数=效果引擎 dig_combo 计数器(safe+1/mine 清零,
|
||||
# 跨轮累计)。
|
||||
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int, combo_cap: int) -> Dictionary:
|
||||
var combo_val := _fx.counter("dig_combo") if _fx != null else 0
|
||||
if positive and pos_m <= 0 and combo_cap <= 0:
|
||||
# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。
|
||||
# 全零收集器=原快照原样返回(零扰动)。旧 dig_combo 连击加成死读数(全仓无声明行、combo_val 恒 0)已删——零行为差。
|
||||
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
|
||||
if positive and pos_m <= 0:
|
||||
return snap
|
||||
if not positive and neg_m <= 0:
|
||||
return snap
|
||||
var out: Dictionary[Variant, Variant] = snap.duplicate()
|
||||
var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power)))
|
||||
var pct := pos_m if positive else neg_m
|
||||
if positive and combo_cap > 0:
|
||||
pct += mini(combo_cap, combo_val) * 10
|
||||
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
|
||||
return out
|
||||
|
||||
@@ -711,7 +716,7 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||||
var tu_hits := _fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target})
|
||||
for h in tu_hits:
|
||||
if String(h.get("kind", "")) == "counter_spend":
|
||||
if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))):
|
||||
if not EffectEngine.counter_spend(counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 1))):
|
||||
var vrow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.item_id))
|
||||
var cn_info: int = int(vrow.get("charge_n", 0)) # 读受理行自身
|
||||
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), _attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
|
||||
@@ -1119,8 +1124,7 @@ func _battle_over(result: String) -> void:
|
||||
for axis in _fx_bonus_delta.keys():
|
||||
_ledger.bonus_add_axis(axis, int(_fx_bonus_delta[axis]))
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
if _fx != null:
|
||||
_ledger.counters_merge_export(_fx.counters_export())
|
||||
# 搬运链删除:adventure 桶经 ctx 引用直写 RunLedger,无战末导出时点(旧导出/汇入搬运链删除)
|
||||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
var gp_dropped := _roll_gp_drop() # 战斗掉落(escrow 随此值上抛即弃)
|
||||
|
||||
@@ -109,6 +109,40 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
if quota < 1:
|
||||
push_diag.call("error", "shelf_zero_quota", "ShopTable.pool_shelf kind %s 令牌配额为 0(牌库空种入货架 = 库存无锚)" % kind)
|
||||
|
||||
# ⑦ 计数器引用闭合(卡② V9——读点 id 必命中同名写入行,否则加载致命):
|
||||
# 声明集 = feats + treasure 各行 triggers 内 kind=counter_add 的 do.counter_id;
|
||||
# 读点 = when fact=counter 的 id、do.value_counter、kind=counter_spend 的 counter_id。
|
||||
var feats_tab = loaded.get("feats")
|
||||
var trig_rows: Array[Variant] = []
|
||||
if feats_tab != null:
|
||||
for r in feats_tab.rows:
|
||||
trig_rows.append(r)
|
||||
if treasure_tab != null:
|
||||
for r in treasure_tab.rows:
|
||||
trig_rows.append(r)
|
||||
var counter_decls := {}
|
||||
for row in trig_rows:
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var dd: Dictionary[Variant, Variant] = (tr_v as Dictionary).get("do", {}) as Dictionary[Variant, Variant]
|
||||
# 声明集=任一 do.counter_id 行(counter_add 直写、forgive_seal_miss 经 _apply_counter_hit 写——皆写入行)
|
||||
if dd.has("counter_id"):
|
||||
counter_decls[String(dd.counter_id)] = true
|
||||
for row in trig_rows:
|
||||
var rid := String(row.get("feat_id", row.get("item_id", "")))
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var tr: Dictionary[Variant, Variant] = tr_v as Dictionary[Variant, Variant]
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary[Variant, Variant]
|
||||
if String(c.get("fact", "")) == "counter" and c.has("id"):
|
||||
var rd := String(c.id)
|
||||
if not counter_decls.has(rd):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s when counter 读点无同名写入行: %s(引用闭合)" % [rid, rd])
|
||||
var do2: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary[Variant, Variant]
|
||||
if do2.has("value_counter") and not counter_decls.has(String(do2.value_counter)):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s value_counter 读点无同名写入行: %s" % [rid, String(do2.value_counter)])
|
||||
if String(do2.get("kind", "")) == "counter_spend" and do2.has("counter_id") and not counter_decls.has(String(do2.counter_id)):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s counter_spend 无同名写入行: %s" % [rid, String(do2.counter_id)])
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
|
||||
|
||||
@@ -44,6 +44,7 @@ var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle
|
||||
# 效果引擎(attack_commit/clamp_check/armor_roll/kill/damage_taken
|
||||
# 发射+施加面:dispatch 返回命中,本类按组合规则施加)
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
# 战斗段运行态事实(编排器每轮刷新——fact 取数面:attack_commit 载荷 dig_streak /
|
||||
# charge_layers facts〔猎巢人 per/整备 attack_bonus per/厚积薄发 when/铁壁 shield per〕;
|
||||
# 流派档位两运行态〔magic 档一 rider+1 / 档二 rider 可暴击〕)
|
||||
@@ -67,15 +68,16 @@ func set_hero_ac_override(v: int) -> void:
|
||||
|
||||
|
||||
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
_fx = fx
|
||||
_ctx = ctx
|
||||
|
||||
|
||||
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加)
|
||||
func _fx_hits(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
return []
|
||||
return _fx.dispatch(event, payload, _fx_fact)
|
||||
return _fx.dispatch(_ctx, event, payload, _fx_fact)
|
||||
|
||||
|
||||
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
|
||||
@@ -342,13 +344,13 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
|
||||
if String(h.get("kind", "")) == "attack_bonus":
|
||||
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
|
||||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||||
contrib = _fx.counter(String(h.get("value_counter", "")))
|
||||
contrib = EffectEngine.counter(_ctx, String(h.get("value_counter", "")))
|
||||
base_value += float(contrib)
|
||||
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
|
||||
for h in ac_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
|
||||
if ctx["spike_ready"]:
|
||||
for h in ac_hits:
|
||||
@@ -461,7 +463,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
fx_ac_request.call(h) # 石肤 AC 累积归编排器单点(set_hero_ac_override 消费面)
|
||||
"counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
if shield > 0:
|
||||
total = maxi(int(db.settlement.damage_floor), total - shield)
|
||||
units.apply_damage(hero, total)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## EffectContext——效果引擎四桶计数器引用束(无状态引擎的外部状态承载)。
|
||||
## 四桶按生命周期分:round(轮始整桶清空)/battle(随编排器生灭)/level(随 LevelRunner 生灭)/
|
||||
## adventure(随 RunLedger 生灭)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
|
||||
class_name EffectContext
|
||||
extends RefCounted
|
||||
|
||||
var round_bucket: Dictionary[Variant, Variant] = {}
|
||||
var battle_bucket: Dictionary[Variant, Variant] = {}
|
||||
var level_bucket: Dictionary[Variant, Variant] = {}
|
||||
var adventure_bucket: Dictionary[Variant, Variant] = {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://6n45i76gkrmu
|
||||
+71
-76
@@ -67,24 +67,20 @@ const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
|
||||
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面。
|
||||
## value_event=事件载荷取值键名〔爆破会计——counter_add 值=本轮挖格花费
|
||||
## payload.ap_cost〕;value_counter=计数器取值键〔疼痛记忆/弃图兑换——attack rider 值=账面层值〕;
|
||||
## domain=计数器寿命域〔battle=战斗域重建清零〔缺省〕/adventure=跨战持久随 battle 末导出〕;
|
||||
## 归属由 counter_id 前缀表达(round_/lvl_/adv_/无前缀=battle),旧 domain/reset_at 键删除;
|
||||
## 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"]
|
||||
"set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit",
|
||||
"value_event", "value_counter", "axis"]
|
||||
|
||||
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
|
||||
## safe_dig 安全格直开(天眼——3×3 内安全格按普通挖格规则全额兑付
|
||||
## 含连锁展开与格面资源,危险格仅真视显示不移除——棋盘事实不可变契约;免行动点即效果本身)
|
||||
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):
|
||||
@@ -377,6 +367,11 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array,
|
||||
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"]:
|
||||
|
||||
@@ -23,6 +23,7 @@ var node_index := -1
|
||||
# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本);
|
||||
# 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。
|
||||
var _ledger: Dictionary[Variant, Variant] = {"hero_hp": 0, "gp": 0}
|
||||
var level_counters: Dictionary[Variant, Variant] = {} # 关卡桶(lvl_ 前缀计数器;每关随 LevelRunner new 天然空=按关清零,替代旧硬编码清除名单)
|
||||
var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③)
|
||||
var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点)
|
||||
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger)
|
||||
@@ -469,6 +470,7 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
"attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退)
|
||||
"attunement": _attunement,
|
||||
"ledger": run_ledger, # 共享四账单类下传(单引用键)
|
||||
"level_counters": level_counters, # 关卡桶下传(同关跨战共享引用、随本类 new 天然空)
|
||||
"feats": _feats,
|
||||
"tree_rules": _tree_rules,
|
||||
"battle_index": _battle_index(),
|
||||
|
||||
@@ -18,10 +18,12 @@ var _clear_granted := false
|
||||
var _clear_snapshot := {}
|
||||
# 效果引擎(reward_roll 发射面;偏运硬币 bias_rewards 订阅面)
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
_fx = fx
|
||||
_ctx = ctx
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -62,7 +64,7 @@ func roll_board(eligible: Array) -> bool:
|
||||
# bias_rewards 订阅命中合计=偏运权重加算(行 value_from magnitude 定影);零命中=原行集零扰动)
|
||||
var luck_bias := 0
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch("reward_roll", {"polarity": "positive", "cell_count": list.size()}):
|
||||
for h in _fx.dispatch(_ctx, "reward_roll", {"polarity": "positive", "cell_count": list.size()}):
|
||||
if String(h.get("kind", "")) == "bias_rewards":
|
||||
luck_bias += int(h.get("value", 0))
|
||||
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
|
||||
|
||||
+3
-15
@@ -4,12 +4,12 @@ extends RefCounted
|
||||
# 可写」收口 = 单类持四账)。
|
||||
# 所有权:AdventureRunner 持有(开局构造、冒险全程唯一宿主);LevelRunner 经 set_run_ledger
|
||||
# 下传并装配 encounter;战斗侧(BattleOrchestrator)经 encounter["ledger"] 拿本类引用,
|
||||
# 只走下方 read/append 原语——战末 export/开局 seed 时点语义不变(确定性设计原样保留,
|
||||
# 只把「函数签名上不可见的共享可变引用」换成签名可见的显式类)。
|
||||
# 只走下方 read/append 原语;adventure 桶经 EffectContext 引用被效果引擎就地直写(无战末 export/
|
||||
# 开局 seed 搬运时点——跨战持久=同一字典引用,随本类生灭)。
|
||||
var attune_charges: Dictionary[Variant, Variant] = {} # 同调主动件充能账(use_treasure 扣减;断点随局回档)
|
||||
var vision_counter: Dictionary[Variant, Variant] = {} # 真视类使用计数(冒险开始置 {"used":0};
|
||||
# 缺省空 = 调试/单场战斗不计——enc 缺键守卫语义)
|
||||
var adventure_counters: Dictionary[Variant, Variant] = {} # adventure 域效果计数器(rebuild 回灌/战末导出回写目标)
|
||||
var adventure_counters: Dictionary[Variant, Variant] = {} # adventure 桶(adv_ 前缀计数器;经 EffectContext 引用被引擎就地直写、跨战持久随本类生灭)
|
||||
var bonus: Dictionary[Variant, Variant] = {} # 统一冒险加算区(冒险开始置四轴零账——商店购买/三选一属性件/战末 bonus_delta 汇入)
|
||||
|
||||
|
||||
@@ -33,18 +33,6 @@ func vision_count_add(n: int) -> void:
|
||||
vision_counter["used"] = int(vision_counter.get("used", 0)) + n
|
||||
|
||||
|
||||
func counters_seed_snapshot() -> Dictionary:
|
||||
return adventure_counters # 回灌读面(时点 = begin_battle rebuild 后——语义不变)
|
||||
|
||||
|
||||
func counters_merge_export(exported: Dictionary) -> void:
|
||||
# 战末回写(原编排器逐字语义:仅非空账参与、逐键覆写)
|
||||
if adventure_counters.is_empty():
|
||||
return
|
||||
for cid in exported.keys():
|
||||
adventure_counters[cid] = int(exported[cid])
|
||||
|
||||
|
||||
func bonus_add_axis(axis: String, delta: int) -> void:
|
||||
# 原编排器战末汇入逐字语义(has 闸——账外轴丢弃)
|
||||
if bonus.has(axis):
|
||||
|
||||
@@ -51,15 +51,12 @@ func run(ri: int) -> void:
|
||||
"new_resource_type": is_new_type})
|
||||
var pos_m := 0
|
||||
var neg_m := 0
|
||||
var combo_cap := -1
|
||||
for h in dr_hits:
|
||||
if String(h.get("kind", "")) != "yield_mult":
|
||||
continue
|
||||
pos_m += int(h.get("mult", 0))
|
||||
neg_m += int(h.get("cost_mult", 0))
|
||||
if int(h.get("cap", 0)) > 0:
|
||||
combo_cap = int(h.cap) # 矿脉直觉连击上限
|
||||
oc._grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "")
|
||||
oc._grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m), "positive", "safe_cell", evt.cell, ri, "")
|
||||
# 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致)
|
||||
for h in dr_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
@@ -100,7 +97,7 @@ func run(ri: int) -> void:
|
||||
halve_pct += int(h.get("value", 0))
|
||||
"damage_reduce":
|
||||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||||
mine_reduce += oc._fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免
|
||||
mine_reduce += EffectEngine.counter(oc.counters_ctx, String(h.get("value_counter", ""))) # 安全帽——层值减免
|
||||
else:
|
||||
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
|
||||
"heal":
|
||||
@@ -122,7 +119,7 @@ func run(ri: int) -> void:
|
||||
_apply_counter_hit(h)
|
||||
if not oc._hits_of(mh_hits, "reveal_tiles").is_empty():
|
||||
oc._fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
|
||||
oc._grant_routed(oc._feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct)
|
||||
oc._grant_routed(oc._feat_yield_adjust(snap, false, 0, 0), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct)
|
||||
for h in mh_hits:
|
||||
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
|
||||
var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
|
||||
@@ -202,7 +199,7 @@ func run(ri: int) -> void:
|
||||
var re_fx := oc._fx_emit("round_end", {"round": ri})
|
||||
for h in re_fx:
|
||||
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
|
||||
var mg_layers := oc._fx.counter(String(h.get("per", "")))
|
||||
var mg_layers := EffectEngine.counter(oc.counters_ctx, String(h.get("per", "")))
|
||||
var mg_total := int(h.get("value", 0)) * mg_layers
|
||||
if mg_total > 0:
|
||||
# source_class 用中性名;per-counter 反打通道保留泛用面
|
||||
@@ -322,7 +319,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
|
||||
# counter_add 三行组单点(原五位分支本体逐字收拢——kind 分派随段归位 P1-1)
|
||||
func _apply_counter_hit(h: Dictionary) -> void:
|
||||
var set_v = h.get("set", null)
|
||||
oc._fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
EffectEngine.counter_add(oc.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
|
||||
|
||||
# grant_material 兑付块单点(原三位分支本体逐字收拢——source_class 两态 safe_cell/seal_hit 由参承载)
|
||||
|
||||
+13
-22
@@ -26,9 +26,8 @@ rows = [
|
||||
"on": "dig_paid",
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "twin_digs",
|
||||
"value": 1,
|
||||
"domain": "adventure"
|
||||
"counter_id": "adv_twin_digs",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36,7 +35,7 @@ rows = [
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "twin_digs",
|
||||
"id": "adv_twin_digs",
|
||||
"op": "mod_eq",
|
||||
"value": 2,
|
||||
"ref": 1
|
||||
@@ -208,8 +207,7 @@ rows = [
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_blast_spent",
|
||||
"value": 1,
|
||||
"value_event": "ap_cost",
|
||||
"reset_at": "round_start"
|
||||
"value_event": "ap_cost"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -373,8 +371,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "pain_layers",
|
||||
"value": 1,
|
||||
"domain": "adventure"
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -489,16 +486,15 @@ rows = [
|
||||
"on": "mine_hit",
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "instinct_atk",
|
||||
"value": 2,
|
||||
"reset_at": "round_start"
|
||||
"counter_id": "round_instinct_atk",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "attack_commit",
|
||||
"do": {
|
||||
"kind": "attack_bonus",
|
||||
"value_counter": "instinct_atk"
|
||||
"value_counter": "round_instinct_atk"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -514,8 +510,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_mines",
|
||||
"value": 1,
|
||||
"reset_at": "round_start"
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -531,8 +526,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "pain_layers",
|
||||
"value": 1,
|
||||
"domain": "adventure"
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -624,8 +618,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "pain_layers",
|
||||
"value": 1,
|
||||
"domain": "adventure"
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -719,8 +712,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_digs",
|
||||
"value": 1,
|
||||
"reset_at": "round_start"
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -873,8 +865,7 @@ rows = [
|
||||
"do": {
|
||||
"kind": "forgive_seal_miss",
|
||||
"counter_id": "insurance_used",
|
||||
"value": 1,
|
||||
"domain": "battle"
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -62,7 +62,7 @@ func _initialize() -> void:
|
||||
elif feat_mode == "ab_b":
|
||||
feat_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]}
|
||||
elif feat_mode == "rules":
|
||||
feat_profile = {"mode": "off", "tree_rules": String(args[7] if args.size() >= 8 else "").split(",")} # V19——任意键集配对注入
|
||||
feat_profile = {"mode": "off", "tree_rules": Array(String(args[7] if args.size() >= 8 else "").split(",", false))} # V19——任意键集配对注入(Array 包裹防 PackedStringArray 被 adventure_runner is Array 守卫整组丢弃)
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
|
||||
+90
-92
@@ -1422,188 +1422,188 @@ func _smoke_feats() -> void:
|
||||
for h in hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var sv_c = h.get("set", null)
|
||||
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
|
||||
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
|
||||
# 开格系
|
||||
fx.rebuild(["twin_pick"], [], [])
|
||||
for i in 4:
|
||||
var hits_dp: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
|
||||
var hits_dp: Array = fx.dispatch(orch.counters_ctx, "dig_paid", {"ap_cost": 1})
|
||||
var refund := false
|
||||
for h in hits_dp:
|
||||
if String(h.get("kind", "")) == "grant_ap":
|
||||
refund = true
|
||||
fx.counter_add("twin_digs", 1)
|
||||
EffectEngine.counter_add(orch.counters_ctx, "adv_twin_digs", 1)
|
||||
if i == 1 or i == 3:
|
||||
check(refund, "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1))
|
||||
else:
|
||||
check(not refund, "双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1))
|
||||
fx.rebuild(["geologist"], [], [])
|
||||
var geo_hits: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
|
||||
var geo_hits: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": true})
|
||||
apply_fx.call(geo_hits)
|
||||
check(orch._hits_of(geo_hits, "counter_add").size() == 1, "地质学家:首开新类型集 1 点〔专长批 V3〕")
|
||||
fx.counter_add("geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
|
||||
var geo_hits2: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
|
||||
EffectEngine.counter_add(orch.counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
|
||||
var geo_hits2: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": true})
|
||||
apply_fx.call(geo_hits2)
|
||||
check(orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and fx.counter("geo_types") == 0,
|
||||
check(orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.counters_ctx, "geo_types") == 0,
|
||||
"地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕")
|
||||
var geo_hits3: Array = fx.dispatch("dig_reveal", {"new_resource_type": false})
|
||||
var geo_hits3: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": false})
|
||||
check(orch._hits_of(geo_hits3, "counter_add").is_empty(), "地质学家反例:同类型重复首开不叠点〔专长批 V3〕")
|
||||
fx.rebuild(["hard_hat"], [], [])
|
||||
apply_fx.call(fx.dispatch("dig_reveal", {}))
|
||||
apply_fx.call(fx.dispatch("dig_reveal", {}))
|
||||
check(fx.counter("helm_layers") == 2, "安全帽:开安全格攒层 ×2〔专长批 V3〕")
|
||||
var hh_hits: Array = fx.dispatch("mine_hit", {"damage": 6})
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "dig_reveal", {}))
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "dig_reveal", {}))
|
||||
check(EffectEngine.counter(orch.counters_ctx, "helm_layers") == 2, "安全帽:开安全格攒层 ×2〔专长批 V3〕")
|
||||
var hh_hits: Array = fx.dispatch(orch.counters_ctx, "mine_hit", {"damage": 6})
|
||||
var hh_reduce := 0
|
||||
for h in hh_hits:
|
||||
if String(h.get("kind", "")) == "damage_reduce":
|
||||
hh_reduce = fx.counter(String(h.get("value_counter", "")))
|
||||
hh_reduce = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
|
||||
check(hh_reduce == 2, "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕")
|
||||
fx.rebuild(["skip_mining"], [], [])
|
||||
var sk_zero: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
|
||||
var sk_zero: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": true})
|
||||
apply_fx.call(sk_zero)
|
||||
check(orch._hits_of(sk_zero, "counter_add").size() == 1, "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕")
|
||||
fx.counter_add("skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
|
||||
var sk_full: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
|
||||
EffectEngine.counter_add(orch.counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
|
||||
var sk_full: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": true})
|
||||
apply_fx.call(sk_full)
|
||||
check(orch._hits_of(sk_full, "reveal_tiles").size() == 1,
|
||||
"跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty(),
|
||||
"跳格采掘反例:普通开格不计点〔专长批 V3〕")
|
||||
fx.rebuild(["blast_accounting"], [], [])
|
||||
for i in 5:
|
||||
var ba_hits: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
|
||||
var ba_hits: Array = fx.dispatch(orch.counters_ctx, "dig_paid", {"ap_cost": 1})
|
||||
for h in ba_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕")
|
||||
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "round_end", {}), "grant_gp").size() == 1, "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕")
|
||||
fx.rebuild(["frontier_miner"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
|
||||
and orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
|
||||
and orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty(),
|
||||
"边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕")
|
||||
# 封印系
|
||||
fx.rebuild(["seal_echo"], [], [])
|
||||
apply_fx.call(fx.dispatch("seal_hit", {}))
|
||||
apply_fx.call(fx.dispatch("seal_hit", {}))
|
||||
var se_hits: Array = fx.dispatch("round_end", {})
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "seal_hit", {}))
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "seal_hit", {}))
|
||||
var se_hits: Array = fx.dispatch(orch.counters_ctx, "round_end", {})
|
||||
check(orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue",
|
||||
"封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕")
|
||||
fx.rebuild(["seal_smith"], [], [])
|
||||
var sm_remove := 0
|
||||
for i in 3:
|
||||
var sm_dispatch: Array = fx.dispatch("seal_hit", {})
|
||||
var sm_dispatch: Array = fx.dispatch(orch.counters_ctx, "seal_hit", {})
|
||||
apply_fx.call(sm_dispatch)
|
||||
for h in sm_dispatch:
|
||||
if String(h.get("kind", "")) == "remove_mines":
|
||||
sm_remove += 1
|
||||
check(sm_remove == 1, "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕")
|
||||
fx.rebuild(["seal_stake"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("seal_hit", {}), "ac_bonus").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch("seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round",
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round",
|
||||
"封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕")
|
||||
fx.rebuild(["warning_beacon"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("seal_hit", {}), "preview_area").size() == 1, "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "preview_area").size() == 1, "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕")
|
||||
# 踩雷/受伤系
|
||||
fx.rebuild(["pain_memory"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var pm_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
|
||||
var pm_hits: Array = fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
var pm_bonus := 0
|
||||
for h in orch._hits_of(pm_hits, "attack_bonus"):
|
||||
pm_bonus = fx.counter(String(h.get("value_counter", "")))
|
||||
pm_bonus = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
|
||||
check(pm_bonus == 2, "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕")
|
||||
fx.rebuild(["fuse_wire"], [], [])
|
||||
var fw_hits: Array = fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6})
|
||||
var fw_hits: Array = fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6})
|
||||
var fw_shield := 0
|
||||
for h in fw_hits:
|
||||
if String(h.get("kind", "")) == "shield":
|
||||
fw_shield += int(h.get("value", 0))
|
||||
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty(),
|
||||
"保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕")
|
||||
for h in fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6}):
|
||||
for h in fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
check(fw_shield == 0, "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕")
|
||||
fx.rebuild(["blood_insight"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "reveal_tiles").size() == 1, "见血开窍:踩雷揭示 1 格〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1, "见血开窍:踩雷揭示 1 格〔专长批 V3〕")
|
||||
fx.rebuild(["trap_instinct"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var ti_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
|
||||
var ti_hits: Array = fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
var ti_val := 0
|
||||
for h in orch._hits_of(ti_hits, "attack_bonus"):
|
||||
ti_val = fx.counter(String(h.get("value_counter", "")))
|
||||
ti_val = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
|
||||
check(ti_val == 2, "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕")
|
||||
fx.rebuild(["chain_traps", "pain_memory"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var ct_before: int = fx.counter("pain_layers")
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
check(fx.counter("pain_layers") == ct_before + 2, "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕")
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
|
||||
var ct_before: int = EffectEngine.counter(orch.counters_ctx, "pain_layers")
|
||||
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
|
||||
check(EffectEngine.counter(orch.counters_ctx, "pain_layers") == ct_before + 2, "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕")
|
||||
fx.rebuild(["trap_burst"], [], [])
|
||||
var tb_hits: Array = fx.dispatch("mine_hit", {"damage": 8})
|
||||
var tb_hits: Array = fx.dispatch(orch.counters_ctx, "mine_hit", {"damage": 8})
|
||||
check(orch._hits_of(tb_hits, "blast_scaled").size() == 1
|
||||
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0,
|
||||
"陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕")
|
||||
fx.rebuild(["blood_honey"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "heal").size() == 1, "血蜜:踩雷回复 N 生命〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "heal").size() == 1, "血蜜:踩雷回复 N 生命〔专长批 V3〕")
|
||||
fx.rebuild(["stone_skin"], [], [])
|
||||
var ss_hits: Array = fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
|
||||
var ss_hits: Array = fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
|
||||
check(orch._hits_of(ss_hits, "ac_bonus").size() == 1
|
||||
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3,
|
||||
"石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕")
|
||||
fx.rebuild(["last_stand"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
|
||||
and orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
|
||||
and orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty(),
|
||||
"背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕")
|
||||
# 跨族桥
|
||||
fx.rebuild(["breaker_strike"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
|
||||
and orch._hits_of(fx.dispatch("mine_hit", {"dug_under_seal": false}), "counter_add").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
|
||||
and orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty(),
|
||||
"破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕")
|
||||
# 节奏系
|
||||
fx.rebuild(["map_exchange"], [], [])
|
||||
var me_hits: Array = fx.dispatch("dig_stop", {"ap_left": 4})
|
||||
var me_hits: Array = fx.dispatch(orch.counters_ctx, "dig_stop", {"ap_left": 4})
|
||||
apply_fx.call(me_hits)
|
||||
fx.counter_add("stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
|
||||
EffectEngine.counter_add(orch.counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
|
||||
check(orch._hits_of(me_hits, "counter_add").size() == 1, "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕")
|
||||
var me_atk := 0
|
||||
for h in orch._hits_of(fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
|
||||
me_atk = fx.counter(String(h.get("value_counter", "")))
|
||||
for h in orch._hits_of(fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
|
||||
me_atk = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
|
||||
check(me_atk == 4, "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("dig_stop", {"ap_left": 2}), "counter_add").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty(),
|
||||
"弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕")
|
||||
fx.rebuild(["last_drop"], [], [])
|
||||
var ld_hits: Array = fx.dispatch("round_start", {"round": 2, "ap_exhausted_last": true})
|
||||
var ld_hits: Array = fx.dispatch(orch.counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true})
|
||||
check(orch._hits_of(ld_hits, "grant_ap").size() == 1, "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty(),
|
||||
"一滴不剩反例:上轮有余不发〔专长批 V3〕")
|
||||
fx.rebuild(["market_close"], [], [])
|
||||
for i in 8:
|
||||
for h in fx.dispatch("dig_reveal", {}):
|
||||
for h in fx.dispatch(orch.counters_ctx, "dig_reveal", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "收盘:本轮开格≥8→+1 GP〔专长批 V3〕")
|
||||
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "round_end", {}), "grant_gp").size() == 1, "收盘:本轮开格≥8→+1 GP〔专长批 V3〕")
|
||||
# 战斗系
|
||||
fx.rebuild(["execution_scholar"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1,
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1,
|
||||
"斩首学者:击杀揭示 1 格〔专长批 V3〕")
|
||||
fx.rebuild(["executioner"], [], [])
|
||||
var ex_hits: Array = fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
|
||||
var ex_hits: Array = fx.dispatch(orch.counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
|
||||
check(orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
|
||||
and orch._hits_of(fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty(),
|
||||
and orch._hits_of(fx.dispatch(orch.counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty(),
|
||||
"处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕")
|
||||
fx.rebuild(["field_dressing"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("battle_won", {"round": 2}), "heal").size() == 1,
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1,
|
||||
"战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕")
|
||||
# 契约尖点
|
||||
fx.rebuild(["blood_tax"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "bonus_delta").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch("seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk",
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk",
|
||||
"血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕")
|
||||
# 迁入 2 件
|
||||
fx.rebuild(["venom_edge"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
|
||||
and orch._hits_of(fx.dispatch("clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty(),
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
|
||||
and orch._hits_of(fx.dispatch(orch.counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty(),
|
||||
"淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕")
|
||||
fx.rebuild(["seal_burst"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("seal_hit", {}), "open_ticket").size() == 1,
|
||||
check(orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "open_ticket").size() == 1,
|
||||
"封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕")
|
||||
# 零持有
|
||||
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
|
||||
@@ -6051,36 +6051,40 @@ func _smoke_tree() -> void:
|
||||
# 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))----
|
||||
func _smoke_tree_rules() -> void:
|
||||
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
|
||||
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构)
|
||||
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构 + 自建 ctx)
|
||||
var fx = load("res://core/effect_engine.gd").new(null)
|
||||
fx.rebuild([], [], ["posture_first_strike", "posture_bulwark"])
|
||||
check((fx.dispatch("round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1,
|
||||
var ctx_p := EffectContext.new()
|
||||
check((fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1,
|
||||
"先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕")
|
||||
check((fx.dispatch("round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty()
|
||||
and (fx.dispatch("round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
|
||||
check((fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty()
|
||||
and (fx.dispatch(ctx_p, "round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
|
||||
"先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)")
|
||||
check((fx.dispatch("damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1
|
||||
and (fx.dispatch("damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
|
||||
check((fx.dispatch(ctx_p, "damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1
|
||||
and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
|
||||
"稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)")
|
||||
# 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成,编排收集 halve_pct 系既有 damage_mult 路径)
|
||||
# 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成;限次计数住 level 桶,跨战持久=共享同一字典引用)
|
||||
var fz = load("res://core/effect_engine.gd").new(null)
|
||||
fz.rebuild([], [], ["sense_sharp"])
|
||||
var lvl_shared := {} # 同一关的 level 桶(两编排器共享引用 = 跨战持久,替代旧副本搬运)
|
||||
var ctx_fz := EffectContext.new()
|
||||
ctx_fz.level_bucket = lvl_shared
|
||||
var got_mult := false
|
||||
var got_add := false
|
||||
for h in (fz.dispatch("mine_hit", {}, Callable()) as Array):
|
||||
for h in (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array):
|
||||
if String(h.get("kind", "")) == "damage_mult":
|
||||
got_mult = true
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
got_add = true
|
||||
check(got_mult and got_add, "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)")
|
||||
fz.counter_add("tree_sense_sharp_used", 2) # 直调置数(V10 注入口径——置关卡域计数后走事件路径)
|
||||
check((fz.dispatch("mine_hit", {}, Callable()) as Array).is_empty(), "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)")
|
||||
var exported: Dictionary = fz.counters_export()
|
||||
EffectEngine.counter_add(ctx_fz, "lvl_tree_sense_sharp_used", 2) # 直调置数(V10 注入口径——置 level 桶计数后走事件路径)
|
||||
check((fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty(), "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)")
|
||||
var fz2 = load("res://core/effect_engine.gd").new(null)
|
||||
fz2.rebuild([], [], ["sense_sharp"])
|
||||
fz2.counters_seed(exported)
|
||||
check((fz2.dispatch("mine_hit", {}, Callable()) as Array).is_empty(),
|
||||
"锋:限次跨战持久(adventure 域 counters_export/seed——断点保留同源,不新增存档域不升 v7)")
|
||||
var ctx_fz2 := EffectContext.new()
|
||||
ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久)
|
||||
check((fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty(),
|
||||
"锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)")
|
||||
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注
|
||||
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
|
||||
check(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
|
||||
@@ -6119,13 +6123,7 @@ func _smoke_tree_rules() -> void:
|
||||
"守⑤ rig:次战编排启动(新战新队列实例)")
|
||||
check(int(orch2.action_economy.pending_ap_delta()) == 0,
|
||||
"守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)")
|
||||
# 按关重置清零点(adventure_runner._start_current_level)
|
||||
var ar = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777003))
|
||||
ar.run_ledger.adventure_counters = {"tree_sense_sharp_used": 2, "tree_sense_guard_used": 1, "unrelated_key": 9} # 计数账住 RunLedger
|
||||
ar._reset_tree_limited_counters()
|
||||
check(not ar.run_ledger.adventure_counters.has("tree_sense_sharp_used") and not ar.run_ledger.adventure_counters.has("tree_sense_guard_used")
|
||||
and int(ar.run_ledger.adventure_counters.get("unrelated_key", 0)) == 9,
|
||||
"限次按关重置:_start_current_level 清零点恰两键(他键零扰动——生命周期四态闭合)")
|
||||
# 树限次按关清零改由 LevelRunner.level_counters 随 new 承载(旧硬编码名单冒烟断言随函数删除——换代同生死;新语义由 V4/V5 段承载)
|
||||
# 财富之道·散:商品标价折价(level_runner 计价单点)
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004))
|
||||
lr.set_tree_rules([])
|
||||
|
||||
@@ -46,6 +46,7 @@ func _suites() -> Array:
|
||||
# negative=true=负面套件:坏表/违规输入是它的测试目标,输出里必然带 Parse Error,
|
||||
# 脚本错误扫描对它属误伤——豁免只放行这一件,且仅限「错误扫描」一闸(退出码与断言下限照判)。
|
||||
{"path": "res://tests/unit/data_config/reward_dangling_treasure_test.gd", "expect": 1, "negative": true}, # 秘宝双表校验
|
||||
{"path": "res://tests/unit/data_config/counter_prefix_fatal_test.gd", "expect": 1, "negative": true}, # 卡② V9 前缀合法性+引用闭合加载致命
|
||||
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面
|
||||
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点
|
||||
|
||||
@@ -210,4 +210,27 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── S12 — 效果引擎无状态化旧件零残留(卡② V10/F1;禁 counters_export/counters_seed/_reset_tree_limited_counters 等旧符号与带引号旧 id 字面回归)──
|
||||
# 扫描面:game/ 全域 .gd 与 game/data/tables/*.tres(改名与旧符号同时存在于数据行/测试/smoke,非仅 core——不扩面即空转)
|
||||
# 带引号旧 id 匹配("twin_digs" 不误伤 "adv_twin_digs"——引号锚定;round_instinct_atk 不匹 "instinct_atk")
|
||||
# 自检:扫描面文件数为零即判红(防静默空转)
|
||||
if [ -d game/core ]; then
|
||||
S12_SYM='counters_export|counters_seed|counters_merge_export|counters_seed_snapshot|_reset_tree_limited_counters|"tree_sense_sharp_used"|"tree_sense_guard_used"|"twin_digs"|"instinct_atk"'
|
||||
S12_FACE=$(find game/ -type f \( -name '*.gd' -o -name '*.tres' \) 2>/dev/null)
|
||||
S12_FC=$(echo "$S12_FACE" | grep -c .)
|
||||
if [ "$S12_FC" -eq 0 ]; then
|
||||
echo "[static_scan] S12 FAIL: 扫描面为空——路径写错导致的静默空转"
|
||||
fail=1
|
||||
else
|
||||
S12_HITS=$(echo "$S12_FACE" | tr '\n' '\0' | xargs -0 grep -lEn "$S12_SYM" 2>/dev/null | wc -l)
|
||||
if [ "$S12_HITS" -gt 0 ]; then
|
||||
echo "[static_scan] S12 FAIL: 无状态化旧件残留命中 $S12_HITS 文件"
|
||||
echo "$S12_FACE" | tr '\n' '\0' | xargs -0 grep -En "$S12_SYM" 2>/dev/null
|
||||
fail=1
|
||||
else
|
||||
echo "[static_scan] S12 ok(扫描面 $S12_FC 个 .gd/.tres,旧件零残留)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $fail
|
||||
@@ -26,12 +26,12 @@ func _init() -> void:
|
||||
"新效果类型在册:heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/target_attack_bonus〔专长批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.OPS.has("mod_eq"), "新算子在册:mod_eq〔专长批 V1〕")
|
||||
|
||||
# ② 合法新词行零违例(validate_trigger_row 正例)
|
||||
# ② 合法新词行零违例(validate_trigger_row 正例——前缀归属替代旧 domain 键)
|
||||
var errs: Array = []
|
||||
var ok_row := {"on": "dig_stop", "when": [{"fact": "ap_left", "op": "gte", "value": 3}],
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left", "domain": "adventure"}}
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}
|
||||
var mine_covered: bool = EffectEngineScript.validate_trigger_row("V1正例", {}, [ok_row], errs)
|
||||
Kit.check_quiet(errs.is_empty() and not mine_covered, "合法新词行 validate 零违例(dig_stop/ap_left/counter_add+domain)〔专长批 V1〕")
|
||||
Kit.check_quiet(errs.is_empty() and not mine_covered, "合法新词行 validate 零违例(dig_stop/ap_left/counter_add——battle 桶无前缀)〔专长批 V1〕")
|
||||
errs = []
|
||||
EffectEngineScript.validate_trigger_row("V1正例2", {}, [{"on": "battle_won",
|
||||
"when": [{"fact": "hp_ratio", "op": "lte", "value": 1}],
|
||||
@@ -46,81 +46,85 @@ func _init() -> void:
|
||||
EffectEngineScript.validate_trigger_row("出域kind", {}, [{"on": "dig_stop", "do": {"kind": "nope_kind", "value": 1}}], errs)
|
||||
Kit.check_quiet(errs.size() == 4, "出域 on/fact/op/kind 各一行全部被拒(4 违例)〔专长批 V1〕")
|
||||
|
||||
# ④ 运行期命中序(合法新事件;dispatch 出域 fail-fast=debug assert〔effect_engine dispatch 首行
|
||||
# assert(EVENTS.has)——release 剥离系引擎语义,自动化覆盖走 ③ 加载期 validate 致命通道〕)
|
||||
# ④ 运行期命中序(合法新事件;dispatch 出域 fail-fast=debug assert;引擎无状态——计数器经 ctx)
|
||||
var fx = EffectEngineScript.new(null)
|
||||
var ctx_f := EffectContext.new()
|
||||
var hits: Array = []
|
||||
fx.rebuild([], [], [])
|
||||
fx._push_trigger("test", "v1_row", {"on": "dig_stop",
|
||||
"when": [{"fact": "ap_left", "op": "gte", "value": 3}],
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}, {})
|
||||
hits = fx.dispatch("dig_stop", {"ap_left": 5})
|
||||
hits = fx.dispatch(ctx_f, "dig_stop", {"ap_left": 5})
|
||||
Kit.check_quiet(hits.size() == 1, "dig_stop 合法命中(ap_left 5≥3)〔专长批 V1〕")
|
||||
hits = fx.dispatch("dig_stop", {"ap_left": 1})
|
||||
hits = fx.dispatch(ctx_f, "dig_stop", {"ap_left": 1})
|
||||
Kit.check_quiet(hits.is_empty(), "dig_stop 反例不命中(ap_left 1<3)〔专长批 V1〕")
|
||||
Kit.check_quiet(not EffectEngineScript.EVENTS.has("dig_stop_x"),
|
||||
"词表封闭:出域事件词不在 EVENTS(dispatch 首行 assert 消费同一词表)〔专长批 V1〕")
|
||||
|
||||
# ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定)
|
||||
# ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定;battle 桶无前缀)
|
||||
var fx2 = EffectEngineScript.new(null)
|
||||
var ctx2 := EffectContext.new()
|
||||
fx2.rebuild([], [], [])
|
||||
fx2._push_trigger("test", "mod_row", {"on": "dig_paid",
|
||||
"when": [{"fact": "counter", "id": "dbl_digs", "op": "mod_eq", "value": 2, "ref": 1}],
|
||||
"do": {"kind": "grant_ap", "value": 1}}, {})
|
||||
var refunds := 0
|
||||
for i in 4:
|
||||
if not fx2.dispatch("dig_paid", {"ap_cost": 1}).is_empty():
|
||||
if not fx2.dispatch(ctx2, "dig_paid", {"ap_cost": 1}).is_empty():
|
||||
refunds += 1
|
||||
fx2.counter_add("dbl_digs", 1)
|
||||
EffectEngine.counter_add(ctx2, "dbl_digs", 1)
|
||||
Kit.check_quiet(refunds == 2, "mod_eq 预评估口径:4 次付费挖格恰返 2 次(第 2/4 次)〔专长批 V1〕")
|
||||
|
||||
# ⑥ adventure 域计数器 export/seed 往返(T6)
|
||||
# ⑥ adventure 桶共享引用跨引擎持久(T6 换代——替代旧 export/seed 副本搬运)
|
||||
var adv_bucket := {} # 同一冒险账本字典引用跨两引擎=跨战持久(无副本无搬运时点)
|
||||
var fx3 = EffectEngineScript.new(null)
|
||||
fx3._push_trigger("test", "adv_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
|
||||
fx3._push_trigger("test", "adv_row2", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1, "reset_at": "round_start"}}, {})
|
||||
"do": {"kind": "counter_add", "counter_id": "adv_persist", "value": 1}}, {})
|
||||
var ctx3 := EffectContext.new()
|
||||
ctx3.adventure_bucket = adv_bucket
|
||||
for _i in 2:
|
||||
for h in fx3.dispatch("mine_hit", {}): # 施加面同构(编排侧 counter_add 逐条落账)
|
||||
for h in fx3.dispatch(ctx3, "mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx3.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
var exported: Dictionary = fx3.counters_export()
|
||||
Kit.check_quiet(exported.get("pain_layers", -1) == 2 and not exported.has("round_mines"),
|
||||
"export 只含 adventure 域(pain_layers=2、round_mines battle 域不入)〔专长批 V1〕")
|
||||
EffectEngine.counter_add(ctx3, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
Kit.check_quiet(EffectEngine.counter(ctx3, "adv_persist") == 2 and int(adv_bucket.get("adv_persist", -1)) == 2,
|
||||
"adventure 桶就地直写共享字典累积=2(adv_ 前缀归 adventure 桶——替代旧导出搬运)〔专长批 V1〕")
|
||||
var fx4 = EffectEngineScript.new(null)
|
||||
fx4._push_trigger("test", "adv_row_seed", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
|
||||
fx4.counters_seed({"pain_layers": 7})
|
||||
Kit.check_quiet(fx4.counter("pain_layers") == 7, "seed 回灌:counters_seed 后 adventure 域计数器=7〔专长批 V1〕")
|
||||
fx4._push_trigger("test", "adv_row2", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "adv_persist", "value": 1}}, {})
|
||||
var ctx4 := EffectContext.new()
|
||||
ctx4.adventure_bucket = adv_bucket # 挂同一字典引用=下一战可读上一战持久值(替代旧回灌)
|
||||
Kit.check_quiet(EffectEngine.counter(ctx4, "adv_persist") == 2,
|
||||
"adventure 桶共享引用跨引擎可读(无 export/seed,同字典即跨战持久)〔专长批 V1〕")
|
||||
|
||||
# ⑦ V4——攒层/计数类件跨事件计数(置计数→事件路径→清零点:round_start 重置/用后清零)
|
||||
# ⑦ V4——前缀归桶清零/消耗语义(round_ 桶随 round_start 整桶清空;battle 无前缀计数跨 round_start 存活)
|
||||
var fx5 = EffectEngineScript.new(null)
|
||||
var ctx5 := EffectContext.new()
|
||||
fx5.rebuild([], [], [])
|
||||
fx5._push_trigger("test", "round_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1}}, {}) # 未声明 reset_at——round_start 不重置(声明制对照面)
|
||||
fx5._push_trigger("test", "spend_row", {"on": "treasure_used",
|
||||
"do": {"kind": "counter_spend", "counter_id": "round_mines", "value": 1}}, {})
|
||||
for h in fx5.dispatch("mine_hit", {}):
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1}}, {})
|
||||
for h in fx5.dispatch(ctx5, "mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx5.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
for h in fx5.dispatch("mine_hit", {}):
|
||||
EffectEngine.counter_add(ctx5, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
for h in fx5.dispatch(ctx5, "mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx5.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
Kit.check_quiet(fx5.counter("round_mines") == 2, "V4:跨事件计数累积(两雷两计)〔专长批 V4〕")
|
||||
Kit.check_quiet(fx5.counter_spend("round_mines", 1) and fx5.counter("round_mines") == 1,
|
||||
"V4:counter_spend 写口消耗(用后清零语义承载面)〔专长批 V4〕")
|
||||
fx5.dispatch("round_start", {"round": 2})
|
||||
Kit.check_quiet(fx5.counter("round_mines") == 1,
|
||||
"V4:非 reset 点计数器不受 round_start 影响(reset_at 声明制——round_mines 未声明重置)〔专长批 V4〕")
|
||||
EffectEngine.counter_add(ctx5, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
Kit.check_quiet(EffectEngine.counter(ctx5, "round_mines") == 2, "V4:跨事件计数累积(两雷两计——round_ 前缀落 round 桶)〔专长批 V4〕")
|
||||
Kit.check_quiet(EffectEngine.counter_spend(ctx5, "round_mines", 1) and EffectEngine.counter(ctx5, "round_mines") == 1,
|
||||
"V4:counter_spend 静态写口消耗(用后清零语义承载面)〔专长批 V4〕")
|
||||
fx5.dispatch(ctx5, "round_start", {"round": 2})
|
||||
Kit.check_quiet(EffectEngine.counter(ctx5, "round_mines") == 0,
|
||||
"V4:round_ 前缀计数器随 round_start 整桶清空(旧 reset_at 名单声明制换代为前缀归桶)〔专长批 V4〕")
|
||||
# battle 桶对照:无前缀 helm_layers 不受 round_start 影响(跨 round_start 存活)
|
||||
var fx6 = EffectEngineScript.new(null)
|
||||
var ctx6 := EffectContext.new()
|
||||
fx6.rebuild([], [], [])
|
||||
fx6._push_trigger("test", "reset_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "helm_layers", "value": 1, "reset_at": "round_start"}}, {})
|
||||
for h in fx6.dispatch("mine_hit", {}):
|
||||
fx6._push_trigger("test", "battle_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "helm_layers", "value": 1}}, {})
|
||||
for h in fx6.dispatch(ctx6, "mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx6.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
fx6.dispatch("round_start", {"round": 2})
|
||||
Kit.check_quiet(fx6.counter("helm_layers") == 0,
|
||||
"V4:round_start 重置点清零(订阅行声明 reset_at——安全帽攒层清零语义)〔专长批 V4〕")
|
||||
EffectEngine.counter_add(ctx6, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
fx6.dispatch(ctx6, "round_start", {"round": 2})
|
||||
Kit.check_quiet(EffectEngine.counter(ctx6, "helm_layers") == 1,
|
||||
"V4:battle 桶(无前缀)计数器不受 round_start 清空(round 桶整桶清空仅触及 round_ 前缀键)〔专长批 V4〕")
|
||||
|
||||
quit(Kit.finish("effect_words_t1"))
|
||||
@@ -111,7 +111,7 @@ func _init() -> void:
|
||||
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
|
||||
var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json")
|
||||
sm.load_from_disk()
|
||||
a.run_ledger.adventure_counters = {"pain_layers": 3, "twin_digs": 5} # 四账住 RunLedger
|
||||
a.run_ledger.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 四账住 RunLedger
|
||||
a._refresh_stock = 2
|
||||
a.set_save_manager(sm)
|
||||
a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
@@ -125,6 +125,16 @@ func _init() -> void:
|
||||
bp_legacy["battery_pending"] = 7 # 旧断点档遗留键(Alpha 无迁移——缺键/多键兼容面)
|
||||
var b3 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(b3.resume_adventure(bp_legacy, {}), "V16:旧档含 battery_pending 键续玩受理(缺键/多键零报错)〔卡② R1〕")
|
||||
# V13/H1 无状态化旧档兼容:改名前的旧 counter 键(tree_sense_sharp_used / pain_layers 等)随断点带进来不得崩,留桶成死键
|
||||
var bp_legacy2: Dictionary = bp16.duplicate(true)
|
||||
var legacy_ac: Dictionary = (bp_legacy2.get("adventure_counters", {}) as Dictionary).duplicate(true)
|
||||
# 旧键名拼接构造(避免整串旧 id 字面触发 S12 旧件扫描——这里是"模拟旧档"非现役符号)
|
||||
legacy_ac["tree_" + "sense_sharp_used"] = 2
|
||||
legacy_ac["twin_" + "digs"] = 4
|
||||
bp_legacy2["adventure_counters"] = legacy_ac
|
||||
var b4 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(b4.resume_adventure(bp_legacy2, {}),
|
||||
"V13:旧档 adventure_counters 含改名前旧键续玩受理零崩溃(无迁移——旧键留桶成死键)〔卡② V13/H1〕")
|
||||
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
|
||||
"V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕")
|
||||
# V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
|
||||
|
||||
@@ -29,8 +29,8 @@ func _init() -> void:
|
||||
Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "dig_combo", "value": 1}) == null
|
||||
and oc._fx.counter("dig_combo") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价)〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "geo_types", "value": 1}) == null
|
||||
and EffectEngine.counter(oc.counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕")
|
||||
|
||||
# V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
|
||||
@@ -31,7 +31,7 @@ func _init() -> void:
|
||||
var ap_with := int(ob1.action_economy._ap) # 测试夹具直读(呈现读数走 presentation_snapshot——本断言取账本真值)
|
||||
Kit.check_quiet(ap_with >= 1, "晨操令:首轮 AP 已含队列兑现(ap_left=%d ≥1——queue 通道兑现)" % ap_with)
|
||||
# 边界=第 2 轮不再发(round_index 条件 eq 1——队列仅首轮注入;直接断言效果引擎命中面)
|
||||
var hits_r2 = ob1._fx.dispatch("round_start", {"round_index": 2})
|
||||
var hits_r2 = ob1._fx.dispatch(ob1.counters_ctx, "round_start", {"round_index": 2})
|
||||
var r2_ap := 0
|
||||
for h in hits_r2:
|
||||
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
|
||||
@@ -42,7 +42,7 @@ func _init() -> void:
|
||||
var ob1b = orch_s.new(db, rng1b)
|
||||
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
|
||||
"空装配基线复用:未装配件参数读数 0")
|
||||
var hits_none = ob1b._fx.dispatch("round_start", {"round_index": 1})
|
||||
var hits_none = ob1b._fx.dispatch(ob1b.counters_ctx, "round_start", {"round_index": 1})
|
||||
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
|
||||
|
||||
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
|
||||
@@ -50,10 +50,10 @@ func _init() -> void:
|
||||
var ob2 = orch_s.new(db, rng2)
|
||||
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
|
||||
"双管装填:装配起战")
|
||||
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 1})) == 1
|
||||
and _ap_of(ob2._fx.dispatch("round_start", {"round_index": 2})) == 1,
|
||||
Kit.check_quiet(_ap_of(ob2._fx.dispatch(ob2.counters_ctx, "round_start", {"round_index": 1})) == 1
|
||||
and _ap_of(ob2._fx.dispatch(ob2.counters_ctx, "round_start", {"round_index": 2})) == 1,
|
||||
"双管装填:第 1/2 轮各命中 +1(lte 2 边界内)")
|
||||
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 3})) == 0,
|
||||
Kit.check_quiet(_ap_of(ob2._fx.dispatch(ob2.counters_ctx, "round_start", {"round_index": 3})) == 0,
|
||||
"双管装填反例:第 3 轮零命中;战斗外零发放(round_start 事件域外无触发面)")
|
||||
|
||||
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
|
||||
|
||||
@@ -26,17 +26,17 @@ func _init() -> void:
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "shield", "value": 1}}],
|
||||
"sense_sharp": [{"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}},
|
||||
{"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": [{"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}},
|
||||
{"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"}}],
|
||||
"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}}],
|
||||
}
|
||||
var six := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"]
|
||||
# rebuild 实装面(db=null→feat/attune 两段空转,_triggers 只含树规则合成行)——行数对照载体
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# 致命类用例(独立进程,期望退出码 1)
|
||||
# 卡② V9/E1——计数器前缀合法性 + 引用闭合加载期致命双断言:
|
||||
# ①向 feats.tres 注入 kind=counter_add 但 counter_id 前缀非法(meta_ 出域)的行 → validate_trigger_row 须拦;
|
||||
# ②注入读点(when fact=counter id)指向无同名写入行的悬空 id → config_db_cross_validator 引用闭合须拦。
|
||||
# 任一违规被放行即回归(退出码 2)。两夹具目录各跑一次 load_tables,均须拒绝。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/data_config/counter_prefix_fatal_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_PREFIX_DIR := "user://counter_fixture_prefix"
|
||||
const BAD_REF_DIR := "user://counter_fixture_ref"
|
||||
|
||||
# 注入探针:非法前缀写入行(meta_probe 无 meta_ 合法归桶 → counter_id_legal false)
|
||||
const PROBE_PREFIX := ' {\n "feat_id": "prefix_probe",\n "display_name": "前缀探针",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "counter_add", "counter_id": "meta_probe", "value": 1 } } ],\n "desc": "夹具探针——非法前缀"\n},\n'
|
||||
# 注入探针:悬空读点(when counter id=pain_layer 单数,全表无同名写入行 → 引用闭合拦)
|
||||
const PROBE_REF := ' {\n "feat_id": "ref_probe",\n "display_name": "引用探针",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [ { "fact": "counter", "id": "pain_layer", "op": "lt", "value": 1 } ], "do": { "kind": "grant_ap", "mode": "queue", "value": 1 } } ],\n "desc": "夹具探针——悬空读点"\n},\n'
|
||||
|
||||
|
||||
func _inject(dir: String, probe: String) -> bool:
|
||||
var path := dir + "/feats.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
push_error("注入失败:读不到 %s" % path)
|
||||
return false
|
||||
var s := f.get_as_text()
|
||||
f.close()
|
||||
var anchor := "rows = ["
|
||||
var idx := s.find(anchor)
|
||||
if idx < 0:
|
||||
push_error("注入失败:锚点 '%s' 未找到(len=%d)" % [anchor, s.length()])
|
||||
return false
|
||||
var cut := idx + anchor.length()
|
||||
s = s.substr(0, cut) + "\n" + probe + s.substr(cut)
|
||||
var w := FileAccess.open(path, FileAccess.WRITE)
|
||||
if w == null:
|
||||
push_error("注入失败:写不开 %s" % path)
|
||||
return false
|
||||
w.store_string(s)
|
||||
w.close()
|
||||
return true
|
||||
|
||||
|
||||
func _assert_rejected(bad_dir: String, probe: String, label: String) -> bool:
|
||||
# 返回 true=正确拒绝;false=放行或夹具失败(调用方据此退出 2)
|
||||
if not Kit.copy_tables(SRC_DIR, bad_dir) or not _inject(bad_dir, probe):
|
||||
push_error("[%s] 夹具目录准备失败" % label)
|
||||
return false
|
||||
var db = ConfigDBScript.new()
|
||||
var ok: bool = db.load_tables(bad_dir, false)
|
||||
if ok:
|
||||
push_error("[%s] 违规表被放行——加载期校验失守(回归)" % label)
|
||||
return false
|
||||
if db.diagnostics_count("error") < 1:
|
||||
push_error("[%s] 加载被拒但零 error 诊断——校验通道静默化(回归)" % label)
|
||||
return false
|
||||
print("[%s] 正确拒绝(error 诊断在案)" % label)
|
||||
return true
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _assert_rejected(BAD_PREFIX_DIR, PROBE_PREFIX, "counter_prefix_fatal_test/①非法前缀"):
|
||||
quit(2)
|
||||
return
|
||||
if not _assert_rejected(BAD_REF_DIR, PROBE_REF, "counter_prefix_fatal_test/②悬空读点"):
|
||||
quit(2)
|
||||
return
|
||||
print("[counter_prefix_fatal_test] 双断言均正确拒绝(前缀合法性 + 引用闭合)")
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckn8u3rt64qmi
|
||||
@@ -0,0 +1,17 @@
|
||||
# 迭代 32 卡② 代拍板追认单(2026-10-20)
|
||||
|
||||
批次内落入卡片 §11 代拍板白名单的微决策,逐条备案确认(授权发生于卡片审问阶段,实现只执行预授权)。
|
||||
|
||||
| # | 决策 | 白名单条目 | 落点 |
|
||||
|---|---|---|---|
|
||||
| 1 | 负面套件命名/夹具诊断文案("违规表被放行""校验通道静默化""正确拒绝")措辞,退出码协议 1/2 已锁 | §11.6 | counter_prefix_fatal_test.gd |
|
||||
| 2 | 引用闭合声明集口径:采「任一 do.counter_id 行」而非「仅 kind=counter_add」——因 insurance_used 经 `forgive_seal_miss` 行的 do.counter_id 由 _apply_counter_hit 写入,严版会误判其为悬空 | §11.3 延伸(判据语义微调,成员判定本体不变) | config_db_cross_validator.gd ⑦ |
|
||||
| 3 | S12 段 shell 写法:`find | xargs -0 grep -lEn` 命中文件数 + 非空自检,与现役 S 段同构 | §11.7 | static_scan.sh S12 |
|
||||
| 4 | 冒烟裸引擎段 ctx 取用:rig 段取 `orch.counters_ctx`(真实挂载路径),_smoke_tree_rules standalone 引擎自建 `EffectContext.new()`(sense 用共享 level 字典引用复刻跨战持久) | §11.2 | smoke_all.gd |
|
||||
| 5 | effect_words_t1 ⑥⑦重写措辞(adventure 桶共享字典往返/round_ 前缀整桶清空 vs battle 无前缀存活),断言语义承卡片 §4.1 D1/E1 | §11.1 | effect_words_t1_test.gd |
|
||||
| 6 | parity 复放文件名与判读排版(off/formed/rules 三组、--mode 两判据、字段集从基线推导)内部实现自由 | §11.7/§11.8 | parity_compare.py |
|
||||
| 7 | V16 既有断言写键 twin_digs→adv_twin_digs、V13 旧键用字符串拼接构造(避 S12 旧件扫描字面,语义不变) | §11.1 延伸 | feat_vouchers_test.gd |
|
||||
|
||||
**超白名单呈报(消解,非代裁)**:
|
||||
- **S 编号冲突**:卡片 §7 写「static_scan.sh 新增 S11 段」,但卡①(执行序①,先合入 master)已占用 S11(代码注释禁用词)。本批旧件扫描落 **S12**(顺延取下一号),判据与扫描面严格按卡片 §7 F1/V10 实现。此属卡片成文时 S11 未定义的现实偏差,归反哺点,未改卡片、未凑绿。
|
||||
- **V12 基线重采**:卡片 T0 记录预采基线 game 树哈希 f75acc15;卡① 改 game/ 后树前进到 4b34a656,据卡片 T0「若不等则留盘基线作废、按 T7 命令重采」条款重采三组 40 局;重采基线哈希 2d3d4247/0fcbcb26 逐字复现卡片在册值,证明重采口径与卡片一致。
|
||||
@@ -0,0 +1,51 @@
|
||||
# 迭代 32 卡② 反哺点清单(2026-10-20)
|
||||
|
||||
代码实现批,禁写 `design/` 与根规则文件。要改文档的点逐条列此,用户裁决后归文档批落文。对应卡片 §6 关联文档同步面。
|
||||
|
||||
## architecture.md(三处)
|
||||
|
||||
| # | 位置 | 现陈述 | 建议改成 | 依据 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 模块归属表 effect_engine 行「对外暴露」列(:116) | 记 `counters_export·counters_seed` 与「domain=adventure 跨战斗持久」 | 改写为 ctx 形态:`dispatch(ctx,...)`/静态 `counter_add·counter_spend·bucket_of`;跨战持久=经 EffectContext 引用宿主持有桶(无 export/seed) | 搬运链已整段删除,该陈述失义 |
|
||||
| 2 | 战斗域装配现状节(:125) | 「战末 export/开局 seed 时点语义不变」 | 删除或改「adventure 桶经 ctx 引用直写 RunLedger,无搬运时点」 | 同上 |
|
||||
| 3 | 「架构原则」节 | 缺「系统无状态」条目 | 补一条(立项判据)——**指向 06 在册条目「效果引擎系统无状态原则缺架构文档条目」**,由文档批执行补条 | 卡片 §6 + 提案 §7.9#1/§7.10.16 B1 |
|
||||
|
||||
## tests/README.md
|
||||
|
||||
| # | 现陈述 | 建议改成 |
|
||||
|---|---|---|
|
||||
| 4 | :58「manifest 已登记实跑 26 套件」(卡片所载) | 本批实跑单元 35 套件(+1 counter_prefix_fatal 负面套件);套件数与段描述由文档批更新到现实数(注意卡①/卡②并行期计数漂移,以 run_all.gd 现况为准) |
|
||||
|
||||
## 06-待办清单
|
||||
|
||||
| # | 项 | 建议 |
|
||||
|---|---|---|
|
||||
| 5 | 上游条目「计数器寿命域未统一」 | 随本批完成整条删除(收口=batch-closure 技能);其现状内引用的行号(adventure_runner :838-841 等)随实现漂移,删除时一并消亡 |
|
||||
| 6 | 新增条目「效果引擎系统无状态原则缺架构文档条目」 | 本批实现落地后,该文档轨条目待文档批补 architecture.md 原则条;06 状态推进与条目清掉归文档批 |
|
||||
|
||||
## 07-体验验证清单
|
||||
|
||||
| # | 项 | 建议 |
|
||||
|---|---|---|
|
||||
| 7 | 天赋树锋/守「每关第 3 次起无减半/无返 AP」=玩家可感知削弱 | 登记目检项(实现报告实机目检总清单已列 V14),用户实战验;只登记不阻塞 |
|
||||
|
||||
## design/research/提案-效果引擎无状态化(三处口径订正——依卡片 §12 裁决以卡片为准)
|
||||
|
||||
| # | 提案节 | 卡片裁定 | 建议 |
|
||||
|---|---|---|---|
|
||||
| 8 | §7.10.3 前缀判据「meta_ 及其余未知前缀→违例」 | 14 枚 id 全含下划线,严版误伤全部既有 battle id;封闭前缀词表=第二事实源 | 采「只禁 meta_+其余落 battle」松版+引用闭合承接(提案随文档批订正) |
|
||||
| 9 | §7.10.12 复放判读「27 字段+levels_cleared 不得上行」 | 实采 33 字段;单一不得上行判据把增强组与削弱组混判 | 采三组分类判据(off 逐位/formed·rules 输入面恒等,方向只记报告)——本批已按此实现 |
|
||||
| 10 | §7.7「零采集」定值 | 现行锚 24 字段与本批不同构,不可作复放基线 | 改批内重采三组 40 局(已做);提案随文档批订正 |
|
||||
|
||||
## sim-data/README.md
|
||||
|
||||
| # | 项 | 建议 |
|
||||
|---|---|---|
|
||||
| 11 | 配套纪律③「对拍=0 新跑」⑦「零数值改动=零 MC」 | 登记本批批次例外:V12 三组 40 局基线重采+40 局复放(二档上限内,依卡片 §12 裁点①);现行锚与读数文件未动 |
|
||||
|
||||
## 卡片与现实偏差(本批实况,非代裁)
|
||||
|
||||
| # | 项 | 说明 |
|
||||
|---|---|---|
|
||||
| 12 | static_scan 编号 | 卡片写「S11 段」,卡① 已占 S11;本批旧件扫描落 S12(顺延)。卡片如订正归文档批,或后续以「S12=旧件零残留」为准 |
|
||||
| 13 | V12 基线哈希 | 卡片 T0 记 f75acc15 基线;卡① 改 game/ 致树前进 4b34a656,据 T0 条款重采,新基线复现卡片 formed/rules 在册哈希 2d3d4247/0fcbcb26 |
|
||||
@@ -0,0 +1,77 @@
|
||||
# 迭代 32 卡② 实现报告·效果引擎无状态化批(2026-10-20)
|
||||
|
||||
- **批次**:迭代 32 执行序②(效果引擎无状态化批·架构重构 restructure)
|
||||
- **分支**:`g-plan2-iter32-card2-stateless`(基点 master 880ee91b=卡① 合回点)
|
||||
- **设计事实源**:`design/research/提案-效果引擎无状态化-2026-10-19.md` v1.2
|
||||
- **合回前 tip**:b42463c9(+ 本报告等产出物提交后为最终 tip)
|
||||
- **闸回执**:`game/tests/.gates/receipt.json` domain=code,all_green=true,11 号 parity 落 skip
|
||||
|
||||
## 完成定义 8 条逐项核对
|
||||
|
||||
| # | 完成定义 | 状态 | 证据 |
|
||||
|---|---|---|---|
|
||||
| 1 | EffectEngine 实例零运行时可变数据 | ✅ | 仅剩 `_triggers`(编译产物)+`db`(只读);`_counters/_reset_on_round_start/_adventure_counter_ids` 删除(T2 a721e52) |
|
||||
| 2 | 四桶外置、宿主持有、随宿主生灭 | ✅ | round/battle→BattleOrchestrator、level→LevelRunner.level_counters、adventure→run_ledger.adventure_counters(T1 ae5baed) |
|
||||
| 3 | 搬运链整段删除零残留 | ✅ | counters_export/counters_seed/counters_merge_export/counters_seed_snapshot 删除;S12 静态扫描 game/**/*.gd+*.tres 零命中(T7 003581f) |
|
||||
| 4 | 硬编码清除名单删除、按关清零改由 level 桶 new 承载 | ✅ | _reset_tree_limited_counters 删除(T1) |
|
||||
| 5 | 前缀归属机器可读+加载期可校验+引用闭合 | ✅ | validate_trigger_row 前缀致命 + _push_trigger assert + config_db ⑦ 引用闭合;负面套件两断言(meta_ 非法/悬空读点)正确拒绝(T5 4c5bdbc) |
|
||||
| 6 | V12 批内同种子复放三组判读通过 | ✅ | off 组 base==post 逐字节一致(bf084ef3…);formed/rules input-parity PASS(详见 V12 节) |
|
||||
| 7 | dig_combo 死读数连同 combo 分支删除、零行为差 | ✅ | T3 删除;S12 含 dig_combo 相关旧件零残留;off 组逐位一致佐证零行为差 |
|
||||
| 8 | 旧档断点含改名前旧键加载不崩(无迁移) | ✅ | V13/H1 断言(430f97f):注入 tree_sense_*/twin_digs 旧键 resume 受理零崩溃、留桶成死键 |
|
||||
|
||||
## 改前改后读数对照
|
||||
|
||||
| 读数 | 改前(master 880ee91b) | 改后(卡② tip) |
|
||||
|---|---|---|
|
||||
| 冒烟绿数 | 1150 | 1150(旧 V10 直呼断言随函数退役 −1,本批裸引擎段迁移净变化归零) |
|
||||
| 单元套件 | 34 全绿 | 35 全绿(+1=counter_prefix_fatal 负面套件) |
|
||||
| static_scan 退出 | 0 | 0(新增 S12 绿) |
|
||||
| 代码域闸 all_green | — | true |
|
||||
|
||||
## T 卡状态
|
||||
|
||||
| T | 提交 | 状态 |
|
||||
|---|---|---|
|
||||
| T0 基线+采集器 rules 档类型修复 | a8db2257 | ✅ 首提交单列(无玩法改动);基线因卡① 改 game/ 树哈希漂移,据卡片 T0「重采」条款重采三组 40 局 |
|
||||
| T1 EffectContext+四宿主接线 | ae5baed6 | ✅ |
|
||||
| T2 引擎删状态+ctx 接口+静态写口 | a721e529 | ✅ |
|
||||
| T3 7 施加点+dig_combo 删+run_ledger 搬运链删 | 96d31705 | ✅ |
|
||||
| T4 round 泛化+宿主清零确认 | 并入 T2/T3 | ✅(round 整桶 clear+宿主 new 天然空) |
|
||||
| T5 前缀双落点+引用闭合+负面套件+manifest | d402ea14, 4c5bdbc9 | ✅ |
|
||||
| T6 数据行/合成行改名+domain/reset_at 删 | 7f45f904 | ✅ |
|
||||
| T7 断言迁移+S12+V12+变异 | b226a292, 003581f4, a9d5ac13, b42463c9 | ✅(见下) |
|
||||
| T8 产出物 | 本批 reports | 进行中 |
|
||||
|
||||
## V12 同种子复放三组判读(完成定义 6 主力证据)
|
||||
|
||||
基线码=master 880ee91b+采集器 rules 档类型修复(单独 worktree 采集);post 码=卡② tip。命令行逐字一致(normal/route=min/attune=off/shop=off)。
|
||||
|
||||
| 组 | 档 | 局数 | base sha | post sha | 判据 | 结论 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| off | feat_mode=off | 20(81000–81019) | bf084ef3… | bf084ef3… | 整条逐位相等 | **PASS(字节级一致)** |
|
||||
| formed | mode=formed | 10 | 2d3d4247… | 6e6423a5… | 输入面八字段+字段集恒等 | **PASS** |
|
||||
| rules | rules+sense 两键 | 10 | 0fcbcb26… | f8af8da3… | 输入面八字段+字段集恒等 | **PASS** |
|
||||
|
||||
- off 组 base==post 逐字节一致=未受影响路径(零 feats/零计数器触碰)确定性零漂移,结构迁移对其透明。
|
||||
- formed/rules 输入面(policy/seed/atk_pct/hp_pct/route/attune/equipped/feat_mode)恒等、种子集与局数一致、字段集恒等、零崩溃;输出面差异=已定性预期行为变更(adventure 桶真跨战持久、sense 每关限次、pain_layers 不跨战——契约 1 三处),**不声称零漂移**。
|
||||
- 判红先排除环境:基线哈希 2d3d4247/0fcbcb26 与卡片 T0 在册预采值逐字复现,证明基线采集口径正确。
|
||||
- **rules≠off 0 局相同**(分组非空转)——证采集器类型修复(T0 首提交)生效,rules 组确按 sense 两键注入。
|
||||
|
||||
## 变异验证记录表(test-conventions §6)
|
||||
|
||||
| 断言 | 注入形态 | 跑的最窄闸 | 期望 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| V9(.tres 前缀侧) | validate_trigger_row 前缀分支置 `if false` | counter_prefix_fatal_test | ①非法前缀放行→quit2 | **EXIT=2 捕获** |
|
||||
| V9(引用闭合侧) | 闭合 push_diag 严重级 error→info | counter_prefix_fatal_test | ②悬空读点放行→quit2 | **EXIT=2 捕获** |
|
||||
| V10(S12) | reward_economy 注释写回 counters_export | static_scan.sh | S12 FAIL | **命中 1 文件见红**,还原回绿 |
|
||||
| V12(采集器侧) | 去 Array 包裹(还原 T0 修复形态)→ 已在基线验证 | rules 组判读 | rules==off 即空转 | 实测 rules≠off 0 相同=非空转 |
|
||||
|
||||
## 实机目检总清单(登记不阻塞·V14)
|
||||
|
||||
- 天赋树锋/守:同一关内第 3 次起踩危险格不再伤害减半、第 3 次标错不再返 AP(每战限 2→每关限 2 的削弱,玩家可感知)——用户实战目检。
|
||||
- 断点续玩旧档(含改名前 counter 键)正常进入不崩(V13 自动化已覆盖崩溃面,此处目检手感)。
|
||||
|
||||
## 遗留读数义务
|
||||
|
||||
- 三处已定性行为变更由 V12 formed/rules 输入面判据+负面套件+冒烟桶断言承载;本批不声称零漂移。
|
||||
- V12 采集文件落 `sim-data/`(数据件不入 git,常驻留盘供报告与后续核对;本批不动现行锚)。
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""V12 同种子复放比对器(卡② 效果引擎无状态化批):解析两次 MC 采集的 RUNSUM 行按 seed 配对判读。
|
||||
两判据模式——parity:整条记录逐位相等(off 组,未受影响路径须透明);
|
||||
input-parity:输入面八字段 + 顶层字段集恒等、种子集与局数一致(formed/rules 组,输出面允许 RNG 级联位移)。
|
||||
退出码 0 绿 / 1 红 / 2 用法或解析错。字段集从基线文件推导,禁硬编码。"""
|
||||
import sys, json, argparse
|
||||
|
||||
INPUT_KEYS = ["policy", "seed", "atk_pct", "hp_pct", "route", "attune", "equipped", "feat_mode"]
|
||||
|
||||
|
||||
def load(path):
|
||||
recs = {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line.startswith("RUNSUM "):
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line[len("RUNSUM "):])
|
||||
except json.JSONDecodeError:
|
||||
print("解析失败行:", line[:80])
|
||||
sys.exit(2)
|
||||
recs[d.get("seed")] = d
|
||||
return recs
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mode", choices=["parity", "input-parity"], required=True)
|
||||
ap.add_argument("--base", required=True)
|
||||
ap.add_argument("--post", required=True)
|
||||
ap.add_argument("--expect", type=int, required=True)
|
||||
a = ap.parse_args()
|
||||
base, post = load(a.base), load(a.post)
|
||||
if len(base) != a.expect or len(post) != a.expect:
|
||||
print("局数不符:base=%d post=%d expect=%d" % (len(base), len(post), a.expect))
|
||||
return 1
|
||||
if set(base) != set(post):
|
||||
print("种子集不一致")
|
||||
return 1
|
||||
red = False
|
||||
if a.mode == "parity":
|
||||
diff = [s for s in base if base[s] != post[s]]
|
||||
if diff:
|
||||
red = True
|
||||
print("off 组逐位差异 %d 局: %s" % (len(diff), diff[:10]))
|
||||
for s in diff[:3]:
|
||||
k = [kk for kk in base[s] if base[s].get(kk) != post[s].get(kk)]
|
||||
print(" seed", s, "差异字段:", k)
|
||||
else:
|
||||
bkeys = set(next(iter(base.values())).keys())
|
||||
pkeys = set(next(iter(post.values())).keys())
|
||||
if bkeys != pkeys:
|
||||
red = True
|
||||
print("顶层字段集不一致 base-p=%s post-b=%s" % (bkeys - pkeys, pkeys - bkeys))
|
||||
for s in base:
|
||||
for kk in INPUT_KEYS:
|
||||
if base[s].get(kk) != post[s].get(kk):
|
||||
red = True
|
||||
print("输入面字段 %s 在 seed %s 不一致: %r vs %r" % (kk, s, base[s].get(kk), post[s].get(kk)))
|
||||
# levels_cleared 直方图仅报告(方向不作判据)
|
||||
lc = post[s].get("levels_cleared") if post else None
|
||||
print("判读:%s" % ("FAIL" if red else "PASS"))
|
||||
return 1 if red else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user