651 lines
36 KiB
GDScript
651 lines
36 KiB
GDScript
# 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",
|
||
"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",
|
||
"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"))
|