Files
gplan-2/game/tests/unit/battle_settlement/refresh_feats_test.gd
T

651 lines
36 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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"))