# VERTICAL SLICE - NOT FOR PRODUCTION # 换图系双基座七件专项宿主(迭代 46 卡A——对照表 V1–V12 按轮落地:V5=T2 全局受伤链路、 # V2/V3=T3 两基座、V4/V10=T4 成功事实与 GP、V6–V9=T5 五件后续、V11=T6 数据与池; # V12 无专长基线回归归 round_flow/seal_polarity 既有断言承载)。 # 临时承伤计数专长经现行 ConfigDB 注入(表行追加+持有集传 id——正常数据配置路径,卡片 §12); # 真实入口驱动:AutoPlayer 盲挖循环+停手/封印动作;判据绑可观测提交面(damage_taken 命中计数器 # 与 target=hero 明细行)。 # 运行:godot --headless --path game --script res://tests/unit/battle_settlement/refresh_feats_test.gd(expect 0) extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript = preload("res://core/config_db.gd") const EffectEngineScript = preload("res://core/effect_engine.gd") const AutoPlayer := preload("res://sim/auto_player.gd") # 直接伤型资源类型(负向极性兑付=主角直伤单据的三型——reward 表 pierce/magic/heal 负向行) const DIRECT_TYPES := ["pierce", "magic", "heal"] var _db func _init() -> void: _db = ConfigDBScript.new() if not _db.load_tables("res://data/tables", true): print("[refresh_feats_test] FAIL — 数值表加载失败") quit(1) return _install_dt_probe() test_all_hero_damage_paths_emit_once() test_no_duplicate_damage_taken_event() test_reserve_on_accepted_dig() test_refresh_discount_gate_and_charge() test_double_base_stack() test_no_feats_baseline() test_board_refresh_fact_only_on_success() test_mine_profit_gp() test_stone_mitigation_all_damage() test_rest_rotation_cap_and_clear() test_charge_layers_consumed_same_round() test_blast_defers_to_combat_segment() test_seven_feats_validate_and_pool() quit(Kit.finish("refresh_feats_test")) # ---- 夹具族(真实入口驱动——settle_batch/stop/封印动作;直构只用于编排器受理)---- # 临时承伤计数专长(damage_taken → counter_add battle 桶——每张主角承伤单据恰 +1) func _install_dt_probe() -> void: if _db.crossrun.feats.row_by_id("probe_dt_counter").is_empty(): (_db.crossrun.feats.rows as Array).append({ "feat_id": "probe_dt_counter", "display_name": "承伤计数探针", "rarity": "common", "cap": 1, "desc": "测试夹具:每次主角承伤计数 +1", "triggers": [{"on": "damage_taken", "do": {"kind": "counter_add", "counter_id": "dt_probe_hits", "value": 1}}], }) func _orch(seed_v: int): var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_v) return load("res://core/battle_orchestrator.gd").new(_db, rng) func _enc(feats: Array, voucher_rap: int = 0, armor: int = 10, hero_hp: int = 200) -> Dictionary: return {"level_index": 1, "node_depth": 4, "hero_hp": hero_hp, "hero_atk": 8, "hero_max_hp": 200, "hero_armor_class": armor, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": {}, "battle_index": 1, "voucher_rap": voucher_rap, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}} func _probe_count(orch) -> int: return EffectEngineScript.counter(orch.fx_host().counters_ctx, "dt_probe_hits") # 主角承伤明细行收集(type=damage 且 target=hero——敌方技与棋盘主角直伤两类单据的行面) func _collect_hero_rows(orch) -> Array: var rows: Array = [] orch.step_row.connect(func(row): if String(row.get("type", "")) == "damage" and String(row.get("target", "")) == "hero": rows.append(row)) return rows # 按来源类分桶计数(rows 收集器的只读后处理) func _count_by_source(rows: Array, source: String) -> int: var n := 0 for r_v in rows: if String((r_v as Dictionary).get("source_class", "")) == source: n += 1 return n # AutoPlayer 盲挖循环直到 cell_revealed 谓词命中(真实挖掘入口;返回 {"cell","evt"} 或空) func _dig_until(orch, pred: Callable) -> Dictionary: var ms = orch.minesweeper var hit := {} ms.cell_revealed.connect(func(evt): if hit.is_empty() and bool(pred.call(evt)): hit["cell"] = evt.cell hit["evt"] = evt) var guard := 40 while hit.is_empty() and guard > 0: guard -= 1 var plan = AutoPlayer.choose_dig(ms) if plan.is_empty(): break if plan.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": plan.hero_cell}]) ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) return hit # 直接伤型快照判定(未揭示格可查——reward_economy 装板期摇定身份,示警烟火同源读面) func _is_direct_type(orch, c: Vector2i) -> bool: var snap_v = orch.reward_grant().reward_economy.query_full_snapshot(c) return snap_v is Dictionary and DIRECT_TYPES.has(String((snap_v as Dictionary).get("cell_type", ""))) # 可封印的直接伤型安全格扫描(未揭示+非雷+直接伤型+邻接可达 revealed 格——封印动作真实落点位) func _find_sealable_direct(orch) -> Vector2i: var ms = orch.minesweeper var b = ms.current_board() var revealed := {} for c_v in ms.revealed_cells(): revealed[c_v] = true for y in b.height: for x in b.width: var c := Vector2i(x, y) if ms.is_revealed(c) or b.is_mine(c) or ms.has_seal(c) or not _is_direct_type(orch, c): continue for n_v in b.neighbors8(c): var n := Vector2i(n_v) if revealed.has(n) and (ms.compute_move_path(n).size() > 0 or n == ms.hero_pos()): return c return Vector2i(-1, -1) # 盲挖扩板直至出现可封印直接伤型安全格(有界;返回格位或 (-1,-1)) func _seal_direct_with_expansion(orch) -> Vector2i: var c := _find_sealable_direct(orch) var guard := 12 while c.x < 0 and guard > 0: guard -= 1 var plan = AutoPlayer.choose_dig(orch.minesweeper) if plan.is_empty(): break if plan.hero_cell != orch.minesweeper.hero_pos(): orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) c = _find_sealable_direct(orch) return c # ---- V5 全局受伤链路(迭代 46 卡A T2)---- # 四条真实路径(敌方攻击/危险格/错误封印/地形)逐路驱动:每条至少一张主角承伤单据在案, # 且承伤计数器 == 主角承伤单据行数(每单据恰发射一次的全量判据——重复发射即计数器>行数)。 func test_all_hero_damage_paths_emit_once() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (换图生存:全局固定减伤全伤害路径恰一次组) print("--- V5 全局 damage_taken:四路径逐路恰一次 ---") # ① 敌方攻击路径(Boss 战零挖掘连停三轮——干净样本:零地形零资源单据;多轮保证伤害技在案) var orch1 = _orch(46001) var rows1 := _collect_hero_rows(orch1) Kit.check("V5① 前置:战斗受理", orch1.begin_battle(_enc(["probe_dt_counter"]))) for _i in 3: orch1.minesweeper.settle_batch([{"action": "stop"}]) var enemy_rows := _count_by_source(rows1, "enemy_action") Kit.check("V5① 敌方攻击路径:承伤单据在案且逐单恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [enemy_rows, _probe_count(orch1)], enemy_rows >= 1 and _probe_count(orch1) == enemy_rows) # ② 地形路径(AutoPlayer 真挖至伤害型变异格开出——发射在授予段开单点、单据标 hero_damage_consumed) var orch2 = _orch(46002) var rows2 := _collect_hero_rows(orch2) Kit.check("V5② 前置:战斗受理", orch2.begin_battle(_enc(["probe_dt_counter"]))) var terr := _dig_until(orch2, func(evt): for vid_v in (evt.get("variant_ids", []) as Array): if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage": return true return false) Kit.check("V5② 前置:伤害型变异格已挖开(%s)" % str(terr.get("cell", Vector2i(-1, -1))), not terr.is_empty()) orch2.minesweeper.settle_batch([{"action": "stop"}]) var terrain_rows := _count_by_source(rows2, "地形变异") Kit.check("V5② 地形路径:地形单据在案且全单据逐张恰一次(行 %d=计数 %d——盲挖途经单据一并计入)〔换图系批 V5〕" % [rows2.size(), _probe_count(orch2)], terrain_rows >= 1 and _probe_count(orch2) == rows2.size()) # ③ 危险格路径(AutoPlayer 真挖直接伤型危险格——负向直伤单据) var orch3 = _orch(46003) var rows3 := _collect_hero_rows(orch3) Kit.check("V5③ 前置:战斗受理", orch3.begin_battle(_enc(["probe_dt_counter"]))) var mine_hit := _dig_until(orch3, func(evt): return String(evt.get("kind", "")) == "mine" and _is_direct_type(orch3, evt.cell)) Kit.check("V5③ 前置:直接伤型危险格已挖开(%s)" % str(mine_hit.get("cell", Vector2i(-1, -1))), not mine_hit.is_empty()) orch3.minesweeper.settle_batch([{"action": "stop"}]) var danger_rows := _count_by_source(rows3, "mine_hit") Kit.check("V5③ 危险格路径:直伤单据在案且全单据逐张恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [rows3.size(), _probe_count(orch3)], danger_rows >= 1 and _probe_count(orch3) == rows3.size()) # ④ 错误封印路径(扩板→封直接伤型安全格不挖→停手轮末判标错——负向直伤单据) var orch4 = _orch(46004) var rows4 := _collect_hero_rows(orch4) Kit.check("V5④ 前置:战斗受理", orch4.begin_battle(_enc(["probe_dt_counter"]))) var scell := _seal_direct_with_expansion(orch4) Kit.check("V5④ 前置:可封直接伤型安全格在位(%s)" % str(scell), scell.x >= 0) if scell.x >= 0: var plan4 = AutoPlayer.choose_dig(orch4.minesweeper) # 封印动作真实落点:先就位到目标邻接可达格(封印距离同挖掘距离) var seal_plan := _plan_seal(orch4, scell) if not seal_plan.is_empty(): if seal_plan["hero_cell"] != orch4.minesweeper.hero_pos(): orch4.minesweeper.settle_batch([{"action": "move", "cell": seal_plan["hero_cell"]}]) var seal_out: Array = orch4.minesweeper.settle_batch([{"action": "seal_place", "cell": scell}]) Kit.check("V5④ 前置:封印受理", not seal_out.is_empty() and bool((seal_out[0] as Dictionary).get("accepted", false))) orch4.minesweeper.settle_batch([{"action": "stop"}]) var sealwrong_rows := _count_by_source(rows4, "seal_miss") Kit.check("V5④ 错误封印路径:直伤单据在案且全单据逐张恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [rows4.size(), _probe_count(orch4)], sealwrong_rows >= 1 and _probe_count(orch4) == rows4.size()) # 封印落点计划(目标格邻接可达 revealed 位——AutoPlayer._plan_on 同构最小私有形) func _plan_seal(orch, target: Vector2i) -> Dictionary: var ms = orch.minesweeper for n_v in ms.current_board().neighbors8(target): var n := Vector2i(n_v) if ms.is_revealed(n) and (ms.compute_move_path(n).size() > 0 or n == ms.hero_pos()): return {"hero_cell": n} return {} # 同一伤害不得出现两次消费(重复发射=计数器>行数):敌方路径与地形路径两样本逐位核对—— # 地形单据带 hero_damage_consumed 标记,战斗段末 _resolve_board_flat_hero 跳过二次发射由此钉住。 func test_no_duplicate_damage_taken_event() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (受击事件零重复组) print("--- V5 全局 damage_taken:无重复消费 ---") var orch1 = _orch(46011) var rows1 := _collect_hero_rows(orch1) Kit.check("ND 前置:战斗受理", orch1.begin_battle(_enc(["probe_dt_counter"]))) for _i in 2: orch1.minesweeper.settle_batch([{"action": "stop"}]) Kit.check("ND 敌方路径零重复(行 %d=计数 %d——重复消费即计数>行)〔换图系批 V5〕" % [rows1.size(), _probe_count(orch1)], rows1.size() >= 1 and _probe_count(orch1) == rows1.size()) var orch2 = _orch(46012) var rows2 := _collect_hero_rows(orch2) Kit.check("ND 前置:战斗受理", orch2.begin_battle(_enc(["probe_dt_counter"]))) var terr2 := _dig_until(orch2, func(evt): for vid_v in (evt.get("variant_ids", []) as Array): if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage": return true return false) Kit.check("ND 前置:伤害型变异格已挖开(%s)" % str(terr2.get("cell", Vector2i(-1, -1))), not terr2.is_empty()) orch2.minesweeper.settle_batch([{"action": "stop"}]) Kit.check("ND 地形路径零重复(行 %d=计数 %d——context 标记闸失效即计数翻倍)〔换图系批 V5〕" % [rows2.size(), _probe_count(orch2)], rows2.size() >= 1 and _probe_count(orch2) == rows2.size()) # ---- V2/V3 两件基座(迭代 46 卡A T3)---- # 表驱动停手攒 RAP(每轮 1:1 转存——无转存率订阅缺省) func _bank_rap(orch, rounds_n: int) -> void: for _i in rounds_n: orch.minesweeper.settle_batch([{"action": "stop"}]) # AutoPlayer 单次付费挖格(真实挖掘入口——移位+dig 一动作) func _auto_dig(orch) -> void: var plan = AutoPlayer.choose_dig(orch.minesweeper) if not plan.is_empty(): if plan.hero_cell != orch.minesweeper.hero_pos(): orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) # V2 矿脉储能:受理的付费挖格 RAP 恰 +1、AP 按表扣、板资源不变 func test_reserve_on_accepted_dig() -> void: # 判据出处: design/gdd/systems/排雷循环/行动经济.md:256 (换图基座:挖格产 RAP 组) print("--- V2 矿脉储能:付费挖格产 RAP ---") var orch = _orch(46021) Kit.check("V2 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve"]))) _bank_rap(orch, 1) var rap_before: int = orch.action_economy.rap var ap_before: int = orch.action_economy.ap _auto_dig(orch) var dig_cost: int = int(_db.run.action_economy.dig_ap_cost) Kit.check("V2 受理付费挖格 RAP 恰 +1(%d→%d)〔换图系批 V2〕" % [rap_before, orch.action_economy.rap], orch.action_economy.rap == rap_before + 1) Kit.check("V2 AP 按表扣(%d→%d,dig 价 %d)〔换图系批 V2〕" % [ap_before, orch.action_economy.ap, dig_cost], orch.action_economy.ap == ap_before - dig_cost) Kit.check("V2 受理记账闭合:AP 耗尽轮 RAP 增量恰等于受理挖数(AP 消耗数——每受理恰 +1,未受理零入池)〔换图系批 V2〕", _drain_round_rap_accounting(orch)) # V2 辅证:挖到 AP 耗尽——RAP 增量 == 受理挖数(每受理恰 +1;移位耗 AP 不入账故按挖计数) func _drain_round_rap_accounting(orch) -> bool: var rap0: int = orch.action_economy.rap var digs := 0 var guard := 24 while orch.action_economy.phase == "Active" and guard > 0: guard -= 1 var plan = AutoPlayer.choose_dig(orch.minesweeper) if plan.is_empty(): break if plan.hero_cell != orch.minesweeper.hero_pos(): orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) if orch.action_economy.phase != "Active": break # 移位耗尽 AP 收轮——本轮不再有受理挖 orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) digs += 1 return digs > 0 and orch.action_economy.rap - rap0 == digs # V3 省力周转:折价门槛与扣费(can 与执行同源;base 8→扣 4) func test_refresh_discount_gate_and_charge() -> void: # 判据出处: design/gdd/systems/排雷循环/行动经济.md:256 (刷新折扣门控与充能组) print("--- V3 省力周转:折价门槛与扣费 ---") var orch = _orch(46022) Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["easy_swap"]))) _bank_rap(orch, 1) var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) var discounted: int = int(floor(float(base_cost) * 50.0 / 100.0)) var rap0: int = orch.action_economy.rap Kit.check("V3 门同源:can_refresh 与折后价同源(RAP %d ≥ 折后 %d)〔换图系批 V3〕" % [rap0, discounted], orch.can_refresh()) Kit.check("V3 第 1 次刷新受理", orch.player_request_refresh()) Kit.check("V3 折后单次扣费(%d→%d,−%d)〔换图系批 V3〕" % [rap0, orch.action_economy.rap, discounted], orch.action_economy.rap == rap0 - discounted) Kit.check("V3 第 2 次刷新受理(RAP=%d)" % orch.action_economy.rap, orch.player_request_refresh()) Kit.check("V3 两次折后扣费累计(RAP=%d=%d−2×%d)〔换图系批 V3〕" % [orch.action_economy.rap, rap0, discounted], orch.action_economy.rap == rap0 - 2 * discounted) Kit.check("V3 门槛拒:RAP 不足折后价零受理(can=false 且执行拒)〔换图系批 V3〕", orch.action_economy.rap < discounted and not orch.can_refresh() and not orch.player_request_refresh()) # V3 双基座叠加:挖格产 RAP 与刷新折价同场生效 func test_double_base_stack() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (双基座叠加组) print("--- V3 双基座叠加 ---") var orch = _orch(46023) Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve", "easy_swap"]))) _bank_rap(orch, 1) var rap_before: int = orch.action_economy.rap var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) _auto_dig(orch) Kit.check("V3 双基座:挖格产 RAP +1(%d→%d)〔换图系批 V3〕" % [rap_before, orch.action_economy.rap], orch.action_economy.rap == rap_before + 1) var rap_after_dig: int = orch.action_economy.rap Kit.check("V3 双基座:刷新按折后价扣(−%d)〔换图系批 V3〕" % int(base_cost / 2), orch.player_request_refresh() and orch.action_economy.rap == rap_after_dig - int(base_cost / 2)) # V12(refresh_feats 侧):无专长基线——挖格零产 RAP、刷新按表基线扣、AP 保留、上限照旧 func test_no_feats_baseline() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (零专长基线组) print("--- V12 无专长基线(refresh_feats 侧) ---") var orch = _orch(46024) Kit.check("V12 前置:战斗受理", orch.begin_battle(_enc([]))) _bank_rap(orch, 3) var rap_before: int = orch.action_economy.rap var ap_before: int = orch.action_economy.ap var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) _auto_dig(orch) Kit.check("V12 无基座:挖格零产 RAP(%d 不变)〔换图系批 V12〕" % rap_before, orch.action_economy.rap == rap_before) Kit.check("V12 刷新受理并按表基线扣(−%d)〔换图系批 V12〕" % base_cost, orch.player_request_refresh() and orch.action_economy.rap == rap_before - base_cost) Kit.check("V12 AP 保留、相位 Active(基线刷新不重置)〔换图系批 V12〕", orch.action_economy.ap == ap_before - int(_db.run.action_economy.dig_ap_cost) and orch.action_economy.phase == "Active") Kit.check("V12 每轮上限照旧:第 3 次拒(RAP 足额仍拒)〔换图系批 V12〕", orch.player_request_refresh() and not orch.player_request_refresh()) # ---- V4/V10 成功刷新事实与命中施加(迭代 46 卡A T4)---- # 成功刷新事实探针专长安装(board_refreshed → counter_add——成功事实的可观测计数面) func _install_br_probe() -> void: if _db.crossrun.feats.row_by_id("probe_br_counter").is_empty(): (_db.crossrun.feats.rows as Array).append({ "feat_id": "probe_br_counter", "display_name": "刷新事实探针", "rarity": "common", "cap": 1, "desc": "测试夹具:每次成功刷新计数 +1", "triggers": [{"on": "board_refreshed", "do": {"kind": "counter_add", "counter_id": "br_probe_hits", "value": 1}}], }) func _br_count(orch) -> int: return EffectEngineScript.counter(orch.fx_host().counters_ctx, "br_probe_hits") # V4 成功事实只在新板就位后发射:成功恰一次;非 Active/每轮上限/RAP 不足三拒面全零事实零换图 func test_board_refresh_fact_only_on_success() -> void: # 判据出处: design/gdd/systems/排雷循环/行动经济.md:256 (弃图刷新事实面仅成功态组) print("--- V4 成功刷新事实:拒绝零事实 ---") _install_br_probe() var orch = _orch(46031) var swaps := {"n": 0} var waiting_rejected := {"v": false} orch.board_changed.connect(func(): swaps["n"] += 1) # 非 Active 探针(round_flow 同款:借衰减行在结算段内同步探测一次——Waiting 相位刷新必拒) orch.step_row.connect(func(row): if String(row.get("type", "")) == "decay" and not bool(waiting_rejected["v"]): waiting_rejected["v"] = not orch.player_request_refresh()) Kit.check("V4 前置:战斗受理", orch.begin_battle(_enc(["probe_br_counter"]))) _bank_rap(orch, 3) # RAP=3×AP;首轮结算衰减行触发 Waiting 探针 var swaps0: int = swaps["n"] Kit.check("V4 非 Active 拒零事实(结算段内探测拒绝)〔换图系批 V4〕", bool(waiting_rejected["v"]) and _br_count(orch) == 0 and int(swaps["n"]) == swaps0) # 成功 ×2:事实计数与换图信号逐次 +1 Kit.check("V4 第 1 次成功(事实 +1 换图 +1)〔换图系批 V4〕", orch.player_request_refresh() and _br_count(orch) == 1 and int(swaps["n"]) == swaps0 + 1) Kit.check("V4 第 2 次成功(事实 +1 换图 +1)〔换图系批 V4〕", orch.player_request_refresh() and _br_count(orch) == 2 and int(swaps["n"]) == swaps0 + 2) # 每轮上限拒:used=2 满门、RAP 足额——零事实零换图 Kit.check("V4 上限拒零事实(RAP %d 足额仍拒)〔换图系批 V4〕" % orch.action_economy.rap, not orch.can_refresh() and not orch.player_request_refresh() and _br_count(orch) == 2 and int(swaps["n"]) == swaps0 + 2) # RAP 不足拒:停手进新轮(used 重置、停手再入 RAP)→ 消费至 RAP 0 → 拒(零事实零换图; # 纯 RAP 不足拒绝面已由 V3 门槛拒单测钉住,此处并拦形态) orch.minesweeper.settle_batch([{"action": "stop"}]) Kit.check("V4 新轮受理两次(used 重置、RAP 复入)〔换图系批 V4〕", orch.action_economy.phase == "Active" and orch.player_request_refresh() and orch.player_request_refresh() and _br_count(orch) == 4) Kit.check("V4 RAP 不足拒零事实(RAP %d < 价 %d)〔换图系批 V4〕" % [orch.action_economy.rap, int(_db.run.action_economy.rap_refresh_cost)], orch.action_economy.rap < int(_db.run.action_economy.rap_refresh_cost) and not orch.can_refresh() and not orch.player_request_refresh() and _br_count(orch) == 4 and int(swaps["n"]) == swaps0 + 4) # V10 矿道红利:每次成功刷新 escrow 恰 +1 且 fx_grant_gp 行在案;拒绝零变化 func test_mine_profit_gp() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (换图资源:刷新固定 GP 收益组) print("--- V10 矿道红利:刷新固定 GP ---") var orch = _orch(46032) var gp_rows := {"n": 0} orch.step_row.connect(func(row): if String(row.get("type", "")) == "effect" and String(row.get("effect_id", "")) == "fx_grant_gp": gp_rows["n"] += 1) Kit.check("V10 前置:战斗受理", orch.begin_battle(_enc(["mine_profit"]))) _bank_rap(orch, 2) var escrow0: int = orch.reward_grant().escrow Kit.check("V10 第 1 次成功 escrow +1(%d→%d)〔换图系批 V10〕" % [escrow0, orch.reward_grant().escrow], orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 1) Kit.check("V10 fx_grant_gp 行在案(%d 行)〔换图系批 V10〕" % int(gp_rows["n"]), int(gp_rows["n"]) == 1) Kit.check("V10 第 2 次成功 escrow +1(%d→%d)〔换图系批 V10〕" % [orch.reward_grant().escrow, orch.reward_grant().escrow + 1], orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 2) # 拒绝零变化:RAP 不足 Kit.check("V10 拒绝零变化(escrow=%d)〔换图系批 V10〕" % orch.reward_grant().escrow, not orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 2 and int(gp_rows["n"]) == 2) # ---- V6/V8/V9/V7 五件后续效果(迭代 46 卡A T5)---- func _counter(orch, counter_id: String) -> int: return EffectEngineScript.counter(orch.fx_host().counters_ctx, counter_id) # 伤害明细行泛收集(type=damage 全量——按来源类与值断言) func _collect_damage_rows(orch) -> Array: var rows: Array = [] orch.step_row.connect(func(row): if String(row.get("type", "")) == "damage": rows.append(row)) return rows func _damage_rows_by_source(rows: Array, source: String) -> Array: var out: Array = [] for r_v in rows: var r := r_v as Dictionary if String(r.get("source_class", "")) == source: out.append(r) return out # V6 石层缓冲:全局固定减伤(危险格 pierce 8→6 两档钉死:单刷减 2·同轮多刷不叠加·未刷新轮不减; # 地形路径减后值=max(下限, effect_value−2)——含下限保持;敌方路径照常单次发射) func test_stone_mitigation_all_damage() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (全局减伤全覆盖组) print("--- V6 石层缓冲:全局固定减伤 ---") # S1 同轮双刷 → 危险格 pierce 8 减 2=6(叠加即 4——多刷不增钉死) var orch1 = _orch(46041) var rows1 := _collect_damage_rows(orch1) Kit.check("V6 S1 前置:战斗受理", orch1.begin_battle(_enc(["stone_mitigation"]))) _bank_rap(orch1, 3) Kit.check("V6 S1 同轮双刷受理", orch1.player_request_refresh() and orch1.player_request_refresh()) Kit.check("V6 S1 guard 累计 2(同轮双刷)", _counter(orch1, "round_swap_guard") == 2) var mine1 := _dig_until(orch1, func(evt): return String(evt.get("kind", "")) == "mine" and _is_direct_type(orch1, evt.cell)) Kit.check("V6 S1 前置:直接伤型危险格已挖开(%s)" % str(mine1.get("cell", Vector2i(-1, -1))), not mine1.is_empty()) orch1.minesweeper.settle_batch([{"action": "stop"}]) var pierce_rows1 := _damage_rows_by_source(rows1, "mine_hit") Kit.check("V6 S1 危险格减伤恰 2 且多刷不叠加(行值 %s∈[6]——叠加态应为 4)〔换图系批 V6〕" % str(pierce_rows1.map(func(r): return int(r.value))), pierce_rows1.size() >= 1 and int((pierce_rows1[0] as Dictionary).get("value", -1)) == 6) # S2 未刷新轮:guard 随 round_start 清空 → 同型危险格不减(pierce/magic 表幂 8——heal 型幂 6 不同锚勿混) var mine2 := _dig_until(orch1, func(evt): var snap_v2 = orch1.reward_grant().reward_economy.query_full_snapshot(evt.cell) return String(evt.get("kind", "")) == "mine" and snap_v2 is Dictionary \ and ["pierce", "magic"].has(String((snap_v2 as Dictionary).get("cell_type", "")))) Kit.check("V6 S2 前置:第二直接伤型危险格已挖开(%s)" % str(mine2.get("cell", Vector2i(-1, -1))), not mine2.is_empty()) orch1.minesweeper.settle_batch([{"action": "stop"}]) var pierce_rows2 := _damage_rows_by_source(rows1, "mine_hit") Kit.check("V6 S2 未刷新轮不减伤(新轮行值 %s 含 8——round 桶轮始清空)〔换图系批 V6〕" % str(pierce_rows2.map(func(r): return int(r.value))), pierce_rows2.size() >= 2 and int((pierce_rows2[pierce_rows2.size() - 1] as Dictionary).get("value", -1)) == 8) # S3 地形路径:减后值 = max(下限, effect_value−2)——下限保持与定值减一并钉死 # (种子 46002=V5② 已证含 flat_damage 变异板;刷新换板后变异重布—— digging guard 内可达) var orch3 = _orch(46002) var rows3 := _collect_damage_rows(orch3) Kit.check("V6 S3 前置:战斗受理", orch3.begin_battle(_enc(["stone_mitigation"]))) _bank_rap(orch3, 2) Kit.check("V6 S3 单刷受理", orch3.player_request_refresh()) var terr3 := _dig_until(orch3, func(evt): for vid_v in (evt.get("variant_ids", []) as Array): if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage": return true return false) Kit.check("V6 S3 前置:伤害型变异格已挖开(%s)" % str(terr3.get("cell", Vector2i(-1, -1))), not terr3.is_empty()) orch3.minesweeper.settle_batch([{"action": "stop"}]) var terrain_rows := _damage_rows_by_source(rows3, "地形变异") var floor_v: int = int(_db.combat.settlement.damage_roll.damage_floor) var ev_ok := true for tr_v in terrain_rows: var tr := tr_v as Dictionary var vid := String(tr.get("variant_id", "")) var ev: int = int(_db.combat.variation.row_by_id(vid).get("effect_value", 0)) if int(tr.get("value", -1)) != maxi(floor_v, ev - 2): ev_ok = false Kit.check("V6 S3 地形减伤=max(下限,effect_value−2) 逐单闭合(%d 单)〔换图系批 V6〕" % terrain_rows.size(), terrain_rows.size() >= 1 and ev_ok) # V9 坑道小憩:轮末合计回复(白盒预置承载上限 3——计数器直改属数据面注入; # 自然累计 2 与满血未生效两形态分战场钉死) func test_rest_rotation_cap_and_clear() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (轮末恢复 cap 与清账组) print("--- V9 坑道小憩:轮末恢复 ---") # R1 白盒预置 3 层 + 刷新闸(when<3 拒第 4 层)→ 停手回复 min(3, 缺血)——护甲降档保缺血 ≥3 var orch1 = _orch(46051) var rows1: Array = [] orch1.step_row.connect(func(row): rows1.append(row)) Kit.check("V9 R1 前置:战斗受理", orch1.begin_battle(_enc(["tunnel_rest"], 0, 10, 100))) _bank_rap(orch1, 1) # 开局血差 100(hero_hp 100/max 200)——缺血 ≥3 恒成立,回复面确定 var missing: int = 200 - int(orch1._enemy_system.hero_unit().get("hp", 0)) EffectEngineScript.counter_add(orch1.fx_host().counters_ctx, "round_swap_rest", 3) Kit.check("V9 R1 预置 3 层后刷新被闸(when<3——上限 3)", orch1.player_request_refresh() and _counter(orch1, "round_swap_rest") == 3) orch1.minesweeper.settle_batch([{"action": "stop"}]) var heal_rows1: Array = [] var idle_rows1 := 0 for r_v in rows1: var r := r_v as Dictionary if String(r.get("type", "")) == "heal" and String(r.get("source_class", "")) == "fx_board_refresh": heal_rows1.append(r) if String(r.get("type", "")) == "reward_ineffective" and String(r.get("batch_key", "")) == "round_swap_rest": idle_rows1 += 1 Kit.check("V9 R1 轮末回复=min(3,缺血%d) 且层清 0(heal 行值 %s)〔换图系批 V9〕" % [missing, str(heal_rows1.map(func(r): return int(r.value)))], missing >= 3 and heal_rows1.size() == 1 and int((heal_rows1[0] as Dictionary).get("value", -1)) == 3 and _counter(orch1, "round_swap_rest") == 0) # R2 满血未生效:首轮刷新后停手——授予段 heal 时点主角仍满血(combat 段还手在其后) var orch2 = _orch(46052) var rows2: Array = [] orch2.step_row.connect(func(row): rows2.append(row)) Kit.check("V9 R2 前置:战斗受理", orch2.begin_battle(_enc(["tunnel_rest"], 8))) Kit.check("V9 R2 首轮刷新受理(voucher_rap 入池)", orch2.player_request_refresh()) orch2.minesweeper.settle_batch([{"action": "stop"}]) var heal_rows2 := 0 var idle2 := 0 for r2_v in rows2: var r2 := r2_v as Dictionary if String(r2.get("type", "")) == "heal" and String(r2.get("source_class", "")) == "fx_board_refresh": heal_rows2 += 1 if String(r2.get("type", "")) == "reward_ineffective" and String(r2.get("batch_key", "")) == "round_swap_rest": idle2 += 1 Kit.check("V9 R2 满血未生效行在案且零 heal 行(未生效 %d·heal %d)〔换图系批 V9〕" % [idle2, heal_rows2], idle2 == 1 and heal_rows2 == 0 and _counter(orch2, "round_swap_rest") == 0) # R3 自然累计:同轮双刷 → 回复 2(上限 3 的闸面由 R1 白盒钉住,此处证自然路径) var orch3 = _orch(46053) var rows3: Array = [] orch3.step_row.connect(func(row): rows3.append(row)) Kit.check("V9 R3 前置:战斗受理", orch3.begin_battle(_enc(["tunnel_rest"]))) _bank_rap(orch3, 2) Kit.check("V9 R3 同轮双刷受理", orch3.player_request_refresh() and orch3.player_request_refresh()) Kit.check("V9 R3 自然累计 2 层", _counter(orch3, "round_swap_rest") == 2) orch3.minesweeper.settle_batch([{"action": "stop"}]) var heal3 := 0 for r3_v in rows3: if String((r3_v as Dictionary).get("type", "")) == "heal" and String((r3_v as Dictionary).get("source_class", "")) == "fx_board_refresh": heal3 += 1 Kit.check("V9 R3 轮末 heal 行值 2(自然累计路径)〔换图系批 V9〕", heal3 == 1 and _counter(orch3, "round_swap_rest") == 0) # V8 蓄力周转:当轮攻击消费全部层数(上限 3 闸=白盒预置;战斗新建自然清空) func test_charge_layers_consumed_same_round() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (充能当轮消耗组) print("--- V8 蓄力周转:攻击消费层 ---") var orch1 = _orch(46061) var rows1 := _collect_damage_rows(orch1) Kit.check("V8 前置:战斗受理", orch1.begin_battle(_enc(["charge_rotation"], 16))) # 上限 3 闸:白盒预置 3 层后刷新被拒层(when<3) EffectEngineScript.counter_add(orch1.fx_host().counters_ctx, "swap_charge_layers", 3) Kit.check("V8 上限 3 闸(预置 3 刷新不加层)", orch1.player_request_refresh() and _counter(orch1, "swap_charge_layers") == 3) # 当轮攻击消费:停手 → 主角先手攻击 → 层数全消费清零 orch1.minesweeper.settle_batch([{"action": "stop"}]) var attack_rows := 0 for r_v in rows1: if String((r_v as Dictionary).get("source_class", "")) == "hero_attack": attack_rows += 1 Kit.check("V8 当轮攻击消费全部层(攻击行在案 %d·层清 0)〔换图系批 V8〕" % attack_rows, attack_rows >= 1 and _counter(orch1, "swap_charge_layers") == 0) # 自然累计:双刷 2 层(voucher_rap 16 入池)→ 停手消费归零 var orch2 = _orch(46062) Kit.check("V8 前置:战斗受理", orch2.begin_battle(_enc(["charge_rotation"], 16))) Kit.check("V8 战斗新建层 0(引擎重建自然清空)", _counter(orch2, "swap_charge_layers") == 0) Kit.check("V8 双刷累计 2 层", orch2.player_request_refresh() and orch2.player_request_refresh() and _counter(orch2, "swap_charge_layers") == 2) orch2.minesweeper.settle_batch([{"action": "stop"}]) Kit.check("V8 攻击后层清 0(消费面闭合)〔换图系批 V8〕", _counter(orch2, "swap_charge_layers") == 0) # V7 崩壁冲击:延迟群伤单(刷新时点零伤害行——战斗段末按存活目标结账) func test_blast_defers_to_combat_segment() -> void: # 判据出处: design/gdd/systems/战斗循环/战斗结算.md:362 (刷新群伤让位战斗段组) print("--- V7 崩壁冲击:延迟群伤 ---") var orch = _orch(46071) var rows := _collect_damage_rows(orch) Kit.check("V7 前置:战斗受理", orch.begin_battle(_enc(["wall_blast"], 8))) Kit.check("V7 刷新受理(voucher_rap 入池)", orch.player_request_refresh()) Kit.check("V7 刷新时点零伤害行(授予段不结账——案C 延迟)〔换图系批 V7〕", _damage_rows_by_source(rows, "fx_board_refresh").is_empty()) orch.minesweeper.settle_batch([{"action": "stop"}]) var blast_rows := _damage_rows_by_source(rows, "fx_board_refresh") Kit.check("V7 战斗段末群伤单恰一单(全体存活敌 ×2——行值 2 count≥1)〔换图系批 V7〕", blast_rows.size() == 1 and int((blast_rows[0] as Dictionary).get("value", -1)) == 2 and int((blast_rows[0] as Dictionary).get("count", 0)) >= 1) # ---- V11 数据与池(迭代 46 卡A T6)---- # 七件 validate/在册/稀有度 cap 对表/跨表计数器引用闭合/desc 禁词 func test_seven_feats_validate_and_pool() -> void: # 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (换图系七件校验与池组) print("--- V11 七件数据与池 ---") var seven := {"vein_reserve": "excellent", "easy_swap": "excellent", "wall_blast": "excellent", "charge_rotation": "excellent", "stone_mitigation": "common", "tunnel_rest": "common", "mine_profit": "rare"} Kit.check("V11 feats.validate 零违例(七行全过词表/行键白名单/前缀校验)〔换图系批 V11〕", _db.crossrun.feats.validate().is_empty()) var all_in := true var desc_bad := 0 for fid in seven.keys(): var row: Dictionary = _db.crossrun.feats.row_by_id(String(fid)) if row.is_empty() or String(row.get("rarity", "")) != String(seven[fid]) or int(row.get("cap", 0)) != 1: all_in = false if String(row.get("desc", "")).contains("雷") or String(row.get("desc", "")).contains("旗"): desc_bad += 1 Kit.check("V11 七行在册且稀有度/cap 逐件对表〔换图系批 V11〕", all_in) Kit.check("V11 七件 desc 禁词零命中(雷/旗字面)〔换图系批 V11〕", desc_bad == 0) # 跨表计数器引用闭合(判据域=换图系七件自身:value_counter/when-id 读取全部有 counter_add 写入方; # 预存行的消费点记账形态〔如完美保险 insurance_used 由授予段落账〕不在此判据域) var written := {} for row_v in (_db.crossrun.feats.rows as Array): for tv in ((row_v as Dictionary).get("triggers", []) as Array): var do_d: Dictionary = (tv as Dictionary).get("do", {}) if String(do_d.get("kind", "")) == "counter_add": written[String(do_d.get("counter_id", ""))] = true var refs_ok := true for fid2 in seven.keys(): var row2: Dictionary = _db.crossrun.feats.row_by_id(String(fid2)) for tv2 in ((row2.get("triggers", []) as Array)): var tr2 := tv2 as Dictionary var do2: Dictionary = tr2.get("do", {}) for reader in [String(do2.get("value_counter", "")), String(do2.get("value_from", ""))]: if reader != "" and not written.has(reader): refs_ok = false for c_v in (tr2.get("when", []) as Array): var cid := String((c_v as Dictionary).get("id", "")) if (c_v as Dictionary).get("fact", "") == "counter" and cid != "" and not written.has(cid): refs_ok = false Kit.check("V11 七件跨表计数器引用闭合(value_counter/when-id 全部有 counter_add 写入方)〔换图系批 V11〕", refs_ok) Kit.check("V11 换图系三计数器在写入集(swap_charge_layers/round_swap_guard/round_swap_rest)〔换图系批 V11〕", written.has("swap_charge_layers") and written.has("round_swap_guard") and written.has("round_swap_rest"))