# VERTICAL SLICE - NOT FOR PRODUCTION # 触发条件机制单测(技能池情境条件触发器批——V1 出域拒绝/V2 双面过滤/V3 保底不逼/V4 闩型语义/ # V6 事实面供给/V7 池构成纪律/V8 低血防御/V9 停手偷袭/V10 缺省零扰动;V5 对拍由 skill_unlock_test # 迁移断言与 round_flow 编排集成断言承载)。 # 运行:godot --headless --path game --script res://tests/unit/battle_settlement/trigger_cond_test.gd(expect 0) extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") func _init() -> void: var db = load("res://core/config_db.gd").new() if not db.load_tables("res://data/tables", false): print("[trigger_cond_test] FAIL — 数值表加载失败") quit(1) return # ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)---- var skill = db.skill var base_rows: Array = skill.rows.duplicate(true) var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}} var bad_rows := [ {"skill_id": "t_bad_fact", "display_name": "坏事实", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "nope", "op": "eq", "value": 1}}, {"skill_id": "t_bad_op", "display_name": "坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "xor", "value": 1}}, {"skill_id": "t_bad_key", "display_name": "坏键", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": false, "extra": 1}}, {"skill_id": "t_bad_latch", "display_name": "坏闩", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": "yes"}}, {"skill_id": "t_bad_enum_op", "display_name": "枚举坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "last_round_end", "op": "gt", "value": "stopped"}}, {"skill_id": "t_bad_enum_val", "display_name": "枚举坏值", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "last_round_end", "op": "eq", "value": "win"}}, {"skill_id": "t_bad_basic", "display_name": "普攻坏配", "type": "basic", "trigger": {"fact": "round_index", "op": "eq", "value": 1}}, ] for bad in bad_rows: skill.rows = base_rows.duplicate(true) skill.rows.append(bad) var errs: Array = skill.validate() var hit := false for e in errs: if String((e as Dictionary).get("category", "")) != "empty_table" and String((e as Dictionary).get("message", "")).contains(String(bad.get("skill_id", ""))): hit = true Kit.check_quiet(hit, "V1 出域拒绝:%s(%s)〔触发器批 V1〕" % [String(bad.get("skill_id", "")), String(bad.get("display_name", ""))]) skill.rows = base_rows.duplicate(true) skill.rows.append(ok_row) Kit.check_quiet(skill.validate().is_empty(), "V1 合法触发行零 error〔触发器批 V1〕") skill.rows = base_rows # ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)---- var ec = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn") var u = ec.all_enemies()[0] Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)") var r_full: Array = ec.rollable_skills(u) var s_full: Array = ec.special_skills(u) Kit.check_quiet(not _has(r_full, "venom_curb") and not _has(s_full, "venom_curb"), "V2 条件假(满血):venom_curb 双面不入集合〔触发器批 V2〕") u["hp"] = 15 ec.check_trigger_unlocks(u) Kit.check_quiet(_has(ec.rollable_skills(u), "venom_curb") and _has(ec.special_skills(u), "venom_curb"), "V2 条件真(半血过闩):venom_curb 双面入集合〔触发器批 V2〕") # ---- V3 保底不逼条件不满足技(保底集合取 special_skills 已含排除——编排级: # 无条件特殊技 A w1+条件假技 B w9999+普攻;保底命中摇出的是 A,撤销过滤态同种子必中 B)---- var ec_v3 = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec_v3.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V3 前置:地精 spawn") var g3 = ec_v3.all_enemies()[0] g3["pool"] = [ {"skill_id": "attack", "weight": 9, "cooldown_left": 0}, {"skill_id": "heavy", "weight": 1, "cooldown_left": 0}, # A=无条件特殊技 {"skill_id": "desperate_guard", "weight": 9999, "cooldown_left": 0, "trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}, # B=条件假技 w9999 ] g3["basic_streak"] = int(db.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例) var specials_v3: Array = ec_v3.special_skills(g3) Kit.check_quiet(_has(specials_v3, "heavy") and not _has(specials_v3, "desperate_guard"), "V3 保底集合含 A 排除 B(special_skills 真实入口——条件假技不逼出)〔触发器批 V3〕") var drawn: String = "" for i in range(30): var stream_v3 = _rng(700123).open_stream("behavior_roll") var pick = load("res://core/skill_lottery.gd").draw(specials_v3, stream_v3) if String(pick) == "desperate_guard": drawn = "desperate_guard" break Kit.check_quiet(drawn == "", "V3 保底摇号 30 次零逼出 B(SkillLottery 真实摇号——撤销过滤态 w9999 同种子必中,红/绿两态由撤销证明承载)〔触发器批 V3〕") # ---- V4 闩型语义(过门恰一次/治疗抬血不回锁/阈值式判别 max_hp=8 value=35:hp=3 触发 hp=4 不触发)---- Kit.check_quiet(int(u.hp) == 15 and ec.check_trigger_unlocks(u).is_empty(), "V4 闩已置:二次复查零新解锁(过门恰一次)〔触发器批 V4〕") u["hp"] = int(u.max_hp) # 治疗抬回满血 Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"), "V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕") var ec8 = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)") var g = ec8.all_enemies()[0] g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0}, {"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0, "trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}] g["hp"] = 4 Kit.check_quiet(_not_has(ec8.rollable_skills(g), "desperate_guard"), "V4 阈值式判别:hp=4 > T=round(8×35/100)=3 不入可摇集合〔触发器批 V4〕") g["hp"] = 3 Kit.check_quiet(_has(ec8.rollable_skills(g), "desperate_guard"), "V4 阈值式判别:hp=3 ≤ T=3 入可摇集合(取整读数判别会在此漂移——阈值式钉死)〔触发器批 V4〕") # ---- V6 事实面供给(负向 blast 递增/正向 blast 不递增/轮末转存/开战清零+facts 组包四键在位)---- var RigS = load("res://core/battle_orchestrator.gd") var orch = RigS.new(db, _rng(700001)) Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理") # facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面) Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0, "V6 开战清零:三计数全零起〔触发器批 V6〕") # 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件 # ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits) var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1} orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2, "display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6) Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0, "V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕") # 负向 blast(雷)递增 orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2, "display_name": "警觉", "reward_id": "enemy_alert"}, att_v6) Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1, "V6 负向 blast(雷)递增恰一次〔触发器批 V6〕") # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- var cv = load("res://core/config_db_cross_validator.gd").new(db) var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) var comp_violations := 0 for e in pool_errs: if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"): comp_violations += 1 Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕") var enemy_backup: Array = db.enemy.rows.duplicate(true) var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true) var cond_pool: Array[Variant] = [] for entry in (cond_only_row.get("pool", []) as Array): var sid := String((entry as Dictionary).get("skill_id", "")) var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid) if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic": cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技) cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态 cond_only_row["pool"] = cond_pool db.enemy.rows[0] = cond_only_row var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) var hit2 := false for e in errs2: if String((e as Dictionary).get("message", "")).contains("无条件特殊技"): hit2 = true Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕") db.enemy.rows = enemy_backup # ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)---- var ec_v8 = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn") var g8 = ec_v8.all_enemies()[0] var has_dg := false for e in (g8.get("pool", []) as Array): if String((e as Dictionary).get("skill_id", "")) == "desperate_guard": has_dg = true Kit.check_quiet(has_dg and int(g8.max_hp) == 8, "V8 前置:地精池含孤注守势 w1(真实表读值——阈值 T=round(8×35/100)=3)〔触发器批 V8〕") Kit.check_quiet(_not_has(ec_v8.rollable_skills(g8), "desperate_guard"), "V8 hp=8>3 不入可摇集合〔触发器批 V8〕") g8["hp"] = 3 Kit.check_quiet(_has(ec_v8.rollable_skills(g8), "desperate_guard"), "V8 hp=3≤3 入可摇集合(真实入口开轮摇号判据)〔触发器批 V8〕") # ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)---- var ec_v9 = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn") var h9 = ec_v9.all_enemies()[0] Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"), "V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕") Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9, {"last_round_end": "cleared", "round_index": 2, "mines_hit_last": 0, "mines_hit_total": 0}), "sneak_ambush"), "V9 cleared 轮后不入集合〔触发器批 V9〕") Kit.check_quiet(_has(ec_v9.rollable_skills(h9, {"last_round_end": "stopped", "round_index": 2, "mines_hit_last": 0, "mines_hit_total": 0}), "sneak_ambush"), "V9 stopped 轮后入集合(真实表 w2)〔触发器批 V9〕") # ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)---- var ec_v10 = load("res://core/unit_container.gd").new(db) Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn") var g10 = ec_v10.all_enemies()[0] var stray := 0 for e in (g10.get("pool", []) as Array): for k in (e as Dictionary).keys(): if not ["skill_id", "weight", "cooldown_left", "trigger", "trigger_latched"].has(String(k)): stray += 1 Kit.check_quiet(stray == 0, "V10 无 trigger 行池快照零新键(快照键策略钉死——防未来给普行塞键)〔触发器批 V10〕") quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排集成承载)")) func _rng(seed_val: int): var r = load("res://core/rng_service.gd").new() r.set_master_seed(seed_val) return r func _has(arr: Array, sid: String) -> bool: for e in arr: if String((e as Dictionary).get("skill_id", "")) == sid: return true return false func _not_has(arr: Array, sid: String) -> bool: return not _has(arr, sid)