430 lines
19 KiB
GDScript
430 lines
19 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 定位:诊断工具·非验收件(2026-10-08 迭代 19 卡① T3 收编排标注)——A1 红灯=改前既有诊断面
|
||
# (design/qa/m4iter14-mineq-exit-report §四.7),不出入任何闸(probe_gate/render_gate/run_smoke 全不涉);
|
||
# roadmap M4 数值批引用其修复义务不受本标注影响。
|
||
# Date: 2026-09-13(playtest-2026-09-12 报告 Top1——零承伤根因分割探针)
|
||
# 2026-09-16 改掷点口径(护甲等级掷点批):defense_up 墙已随 AC 换装退役——
|
||
# H1 旧 defense_up 叠满墙失义(hero_defense 池现仅 off_guard 正向行,×0 墙结构面消失)
|
||
# H2 Boss/敌攻击链健康:fresh 池下非蓄力拍 d_tick = max(下限, 池乘区(掷点档位基线))(伏击附加随八型批 2026-09-20 退役)
|
||
# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 ac_up 当轮授予当轮衰减——
|
||
# 真人局 JSONL buffs[] 恒空 ≠ 状态为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。
|
||
# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/档位/下限全读表(数据配置 Rule 1——零数值字面量)。
|
||
# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘授予行为)
|
||
# level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面)
|
||
# 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42
|
||
# Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42
|
||
extends SceneTree
|
||
|
||
const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
|
||
|
||
var _checks := 0
|
||
var _fails := 0
|
||
|
||
|
||
func check(cond: bool, label: String) -> void:
|
||
_checks += 1
|
||
if cond:
|
||
print(" [PASS] " + label)
|
||
else:
|
||
_fails += 1
|
||
print(" [FAIL] " + label)
|
||
|
||
|
||
func _initialize() -> void:
|
||
print("=== 零承伤根因分割探针(playtest-2026-09-12 报告 Top1)===")
|
||
var db = load("res://core/config_db.gd").new()
|
||
if not db.load_tables("res://data/tables", false):
|
||
print("表加载失败")
|
||
quit(1)
|
||
return
|
||
var args := OS.get_cmdline_user_args()
|
||
var mode := "boss"
|
||
if args.size() >= 1 and (args[0] == "boss" or args[0] == "level"):
|
||
mode = args[0]
|
||
var seed_val := 42
|
||
if args.size() >= 2 and args[1].is_valid_int():
|
||
seed_val = int(args[1])
|
||
if mode == "boss":
|
||
_probe_boss(db, seed_val)
|
||
else:
|
||
_probe_level(db, seed_val)
|
||
print("=== 探针完成:%d 项检查,%d 项失败 ===" % [_checks, _fails])
|
||
quit(1 if _fails > 0 else 0)
|
||
|
||
|
||
# ==================== boss 模式:逐结算行打印 + 根因判定 ====================
|
||
|
||
func _probe_boss(db, seed_val: int) -> void:
|
||
var math = load("res://core/damage_math.gd")
|
||
var clamp_lo: int = int(db.settlement.pool_clamp_lo)
|
||
var clamp_hi: int = int(db.settlement.pool_clamp_hi)
|
||
var hero_def_negative := 0
|
||
for eff_id in db.settlement.effect_pool_map.keys():
|
||
if String(db.settlement.effect_pool_map[eff_id]) != "hero_defense":
|
||
continue
|
||
var row: Dictionary = db.status_effect_row(eff_id)
|
||
if not row.is_empty() and String(row.get("unit", "")) == "percent":
|
||
hero_def_negative = mini(hero_def_negative, int(row.get("magnitude", 0)))
|
||
print("表值面:hero_defense 池最负 percent 行 %d · pool_clamp [%d, %d] · damage_floor %d · AC 基准 %d(+ac_up×%d 层)" % [
|
||
hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))])
|
||
|
||
print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---")
|
||
var run_a := _run_boss(db, seed_val, false)
|
||
_print_rounds(db, math, run_a)
|
||
var a_ok := _assert_rounds(db, math, run_a, true)
|
||
check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack)
|
||
check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack)
|
||
check(String(run_a.result) == "defeat", "A3 攻击链致死可达:fresh 池 Boss 在 %d 轮内击倒英雄(result=%s——伤害确实落地)" % [run_a.snaps.size(), str(run_a.result)])
|
||
var run_a2 := _run_boss(db, seed_val, false)
|
||
check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同(含 armor_roll 掷点流)")
|
||
|
||
print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---")
|
||
var run_b := _run_boss(db, seed_val, true)
|
||
_print_rounds(db, math, run_b)
|
||
var b_ok := _assert_rounds(db, math, run_b, false)
|
||
check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack)
|
||
check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest])
|
||
check(hero_def_negative * 16 > clamp_lo or hero_def_negative >= 0, "B3 墙归零不可达(表值判定):hero_defense 池最负行 %d——叠满不可触池底 %d(×0 免伤墙结构面已随 AC 换装消失)" % [hero_def_negative, clamp_lo])
|
||
|
||
|
||
func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(seed_val)
|
||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||
var cap := {"rows": [], "snaps": [], "result": ""}
|
||
settlement.step_row.connect(func(row): cap["rows"].append(row))
|
||
settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
|
||
settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
|
||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
|
||
cap["result"] = "begin_failed"
|
||
return cap
|
||
var rounds := 0
|
||
while settlement.battle_result == "" and rounds < 40:
|
||
rounds += 1
|
||
if dig:
|
||
BattleDriver.drive_round(settlement)
|
||
else:
|
||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||
return cap
|
||
|
||
|
||
# 逐轮:授予行汇总 → 重建战斗段时点池值(effect_pool_map 表驱动归属)→ 敌行动行逐行打印
|
||
#(T6 收口批:原形参 verbose 为死参——打印行为无档位差异,随批删除)
|
||
func _print_rounds(db, math, run: Dictionary) -> void:
|
||
var groups := _round_groups(run.rows)
|
||
var beats := _played_beats(run, db)
|
||
var rounds: Array = groups.keys()
|
||
rounds.sort()
|
||
for ri in rounds:
|
||
var rows: Array = groups[ri]
|
||
var an := _analyze_round(db, math, rows, beats)
|
||
var grant_txt := "(无)"
|
||
if not an.grants.is_empty():
|
||
grant_txt = " · ".join(an.grants)
|
||
print(" [R%d] 授予: %s" % [ri, grant_txt])
|
||
for line in an.lines:
|
||
print(" " + String(line))
|
||
var hp_txt := ""
|
||
if int(ri) - 1 < run.snaps.size():
|
||
var s: Dictionary = run.snaps[int(ri) - 1]
|
||
var hero: Dictionary = s.get("hero", {})
|
||
hp_txt = " · hero %d/%d · verdict %s" % [int(hero.get("hp", -1)), int(hero.get("max_hp", -1)), String(s.get("verdict", ""))]
|
||
print(" [R%d] 池值@战斗段: hero_def=%d enemy_out=%d%s" % [ri, an.pd, an.pe, hp_txt])
|
||
|
||
|
||
func _assert_rounds(db, math, run: Dictionary, fresh_mode: bool) -> Dictionary:
|
||
var groups := _round_groups(run.rows)
|
||
var beats := _played_beats(run, db)
|
||
var rounds: Array = groups.keys()
|
||
rounds.sort()
|
||
var out := {"fresh_and_full": true, "arith_ok": true, "wall_tick_positive": true,
|
||
"n_attack": 0, "n_wall": 0, "deepest": 0}
|
||
for ri in rounds:
|
||
var an := _analyze_round(db, math, groups[ri], beats)
|
||
for t in an.ticks:
|
||
out.n_attack += 1
|
||
if fresh_mode and (an.pd != 0 or an.pe != 0):
|
||
out.fresh_and_full = false
|
||
if not bool(t.ok):
|
||
out.arith_ok = false
|
||
if fresh_mode and (int(t.tick) <= 0 or int(t.raw) <= 0):
|
||
out.fresh_and_full = false
|
||
if an.pd < 0:
|
||
out.n_wall += 1
|
||
out.deepest = mini(out.deepest, an.pd)
|
||
if int(t.tick) <= 0:
|
||
out.wall_tick_positive = false
|
||
return out
|
||
|
||
|
||
func _round_groups(rows: Array) -> Dictionary:
|
||
var groups := {}
|
||
for row in rows:
|
||
var ri := int(row.get("round_index", 0))
|
||
if not groups.has(ri):
|
||
groups[ri] = []
|
||
groups[ri].append(row)
|
||
return groups
|
||
|
||
|
||
func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
|
||
var last_pool := {}
|
||
var grants: Array = []
|
||
for row in rows:
|
||
if String(row.get("type", "")) == "effect":
|
||
var eid := String(row.get("effect_id", ""))
|
||
var pv := int(row.get("pool_value", 0))
|
||
last_pool[eid] = pv
|
||
grants.append("%s→池%d" % [String(row.get("display_name", "")), pv])
|
||
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径)
|
||
var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0}
|
||
for eid in last_pool:
|
||
var pool_name: String = String(db.settlement.effect_pool_map.get(eid, ""))
|
||
if pools.has(pool_name):
|
||
pools[pool_name] += int(last_pool[eid])
|
||
var clamp_lo: int = int(db.settlement.pool_clamp_lo)
|
||
var clamp_hi: int = int(db.settlement.pool_clamp_hi)
|
||
var pd: int = clampi(int(pools["hero_defense"]), clamp_lo, clamp_hi)
|
||
var pe: int = clampi(int(pools["enemy_output"]), clamp_lo, clamp_hi)
|
||
var lines: Array = []
|
||
var ticks: Array = []
|
||
for row in rows:
|
||
var t := String(row.get("type", ""))
|
||
if t == "cast":
|
||
lines.append("拍=cast(施放行零兑付——增益技不读池,技能池批由蓄力行改型)")
|
||
elif t == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||
var baseline: int = int(row.get("baseline", 0))
|
||
var attach: int = int(row.get("attach", 0))
|
||
# 掷点口径重建(2026-09-16 护甲等级掷点批):基线按行留档档位打折(暴击 ×倍率),附加必中
|
||
var tier: int = int(row.get("tier", 100))
|
||
var crit: bool = bool(row.get("crit", false))
|
||
var base_factor: float = float(tier) * (float(db.settlement.crit_multiplier) / 100.0 if crit else 1.0)
|
||
var rolled_base := int(floor(float(baseline) * base_factor / 100.0))
|
||
var raw: int = rolled_base + attach
|
||
var tick: int = int(row.get("value", 0))
|
||
var expected: int = maxi(int(db.settlement.damage_floor), math.pool_damage(raw, pd, pe))
|
||
var roll_txt := "掷%s" % str(int(row.get("roll", -1)))
|
||
var zeroed := "✓" if tick == expected else "✗ 算术不符!"
|
||
lines.append("拍=%s %s/档%d%s 基线%d→掷后%d 附加%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [
|
||
String(beats.get(int(row.get("round_index", 0)), "?")), roll_txt, tier, "暴击" if crit else "",
|
||
baseline, rolled_base, attach, pd, pe, tick, expected, zeroed])
|
||
ticks.append({"raw": raw, "tick": tick, "expected": expected, "ok": tick == expected})
|
||
elif t == "damage" and String(row.get("source_class", "")) == "hero_attack":
|
||
lines.append("主角攻击 %d(普通 %d + 穿刺 %d + 魔法 %d)→ %s" % [
|
||
int(row.get("value", 0)), int(row.get("normal", 0)), int(row.get("pierce", 0)), int(row.get("magic", 0)), String(row.get("target", ""))])
|
||
return {"pd": pd, "pe": pe, "grants": grants, "lines": lines, "ticks": ticks}
|
||
|
||
|
||
# 拍名重建(敌人技能池批):pending_skill 字段 = 该结算开轮摇定的动作 id(快照取在结算末尾)——
|
||
# round r 的拍 = 快照 r 的 enemies[0].pending_skill。快照 enemies 空时(敌死亡轮)回退 "?"——该轮敌已无行动行。
|
||
func _played_beats(run: Dictionary, db) -> Dictionary:
|
||
var beats := {}
|
||
for r in range(1, run.snaps.size() + 1):
|
||
var s: Dictionary = run.snaps[r - 1]
|
||
var en: Array = s.get("enemies", [])
|
||
if en.size() > 0:
|
||
beats[r] = String(en[0].get("pending_skill", "?"))
|
||
return beats
|
||
|
||
|
||
# ==================== level 模式:全关遭遇回放(Bug#1 证据面) ====================
|
||
|
||
func _probe_level(db, seed_val: int) -> void:
|
||
print("--- 全关回放 seed %d(战×2→道具→战×2→恢复→Boss)---" % seed_val)
|
||
var prediction := _encounter_prediction(db, seed_val)
|
||
var cap := _run_level(db, seed_val)
|
||
var battles: Array = cap.battles
|
||
for b in battles:
|
||
var depth: int = int(b.get("depth", -1))
|
||
var spawn: int = int(b.get("spawn", -1))
|
||
var predicted: int = int(prediction.get(depth, -1))
|
||
var verdict_txt := _battle_verdict_txt(cap, depth)
|
||
var match_txt := "✓" if spawn == predicted else "✗ 与遭遇骰预测不符!"
|
||
print(" d%d %s: spawn=%d(遭遇骰预测 %d %s)%s" % [depth, String(b.get("type", "")), spawn, predicted, match_txt, verdict_txt])
|
||
var all_match := true
|
||
var all_ge_1 := true
|
||
for b in battles:
|
||
var spawn2: int = int(b.get("spawn", -1))
|
||
if spawn2 < 1:
|
||
all_ge_1 = false
|
||
if spawn2 != int(prediction.get(int(b.get("depth", -1)), -2)):
|
||
all_match = false
|
||
check(all_ge_1, "全部战斗 spawn ≥ 1(难度曲线与Boss Rule 8 区间下限——空遭遇在当前实现不可达)")
|
||
check(all_match, "spawn 与独立遭遇骰预测逐战斗一致(encounter 封闭流同种子可回放)")
|
||
var expect_battles := _battle_nodes_upto(db, 1, int(cap.get("end_node", -1)))
|
||
check(battles.size() == expect_battles, "战斗推进数与节点链一致(实测 %d/预期 %d——败北截断或全链跑通)" % [battles.size(), expect_battles])
|
||
print(" 关卡结果: %s" % str(cap.result))
|
||
print("--- 多种子 spawn 扫描(报告 Bug#1 回归面:20 种子 × 5 战斗节点)---")
|
||
var scan := _scan_spawns(db, seed_val, 20)
|
||
check(scan.bad == 0, "%d 场战斗 spawn 全域内(∈[unit_count, unit_count_max] 且 ≥1)——越域 %d 场" % [scan.total, scan.bad])
|
||
|
||
|
||
func _encounter_prediction(db, seed_val: int) -> Dictionary:
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(seed_val)
|
||
var out := {}
|
||
var scope := 0
|
||
var lrow := _level_row(db, 1)
|
||
for node in lrow.get("nodes", []):
|
||
var t := String(node.get("type", ""))
|
||
if t != "battle" and t != "boss":
|
||
continue
|
||
var depth := int(node.get("node_depth", -1))
|
||
var row: Dictionary = db.resolve_enemy_rowset(1, depth)[0] # 行集首行(行集多行批 T2 机械适配)
|
||
var lo := int(row.get("unit_count", 1))
|
||
var hi := int(row.get("unit_count_max", lo))
|
||
var rolled := lo
|
||
if hi > lo:
|
||
var rg := RandomNumberGenerator.new()
|
||
rg.seed = rng.derive_at(scope)
|
||
scope += 1
|
||
rolled = lo + rg.randi_range(0, hi - lo)
|
||
out[depth] = rolled
|
||
return out
|
||
|
||
|
||
func _level_row(db, level_index: int) -> Dictionary:
|
||
for row in db.level.rows:
|
||
if int(row.level_index) == level_index:
|
||
return row
|
||
return {}
|
||
|
||
|
||
func _run_level(db, seed_val: int) -> Dictionary:
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(seed_val)
|
||
var runner = load("res://core/level_runner.gd").new(db, rng)
|
||
var cap := {"battles": [], "rows": [], "result": "", "end_node": -1}
|
||
runner.step_row.connect(func(row): cap["rows"].append(row))
|
||
runner.node_entered.connect(_on_level_node_entered.bind(runner, cap))
|
||
runner.round_started.connect(_on_level_round_started.bind(runner, cap))
|
||
runner.level_finished.connect(_on_level_finished.bind(cap))
|
||
runner.start_level(1, seed_val)
|
||
var guard_battles := 20
|
||
while cap["result"] == "" and guard_battles > 0:
|
||
guard_battles -= 1
|
||
var settlement = runner.current_battle()
|
||
if settlement == null:
|
||
break
|
||
_drive_battle(runner, settlement, cap)
|
||
return cap
|
||
|
||
|
||
func _on_level_finished(p: Dictionary, cap: Dictionary) -> void:
|
||
cap["result"] = String(p.get("result", ""))
|
||
cap["end_node"] = int(p.get("node_index", -1))
|
||
|
||
|
||
func _battle_nodes_upto(db, level_index: int, node_idx: int) -> int:
|
||
var n := 0
|
||
var nodes: Array = _level_row(db, level_index).get("nodes", [])
|
||
for i in range(mini(node_idx + 1, nodes.size())):
|
||
var t := String(nodes[i].get("type", ""))
|
||
if t == "battle" or t == "boss":
|
||
n += 1
|
||
return n
|
||
|
||
|
||
func _on_level_node_entered(node: Dictionary, _info: Dictionary, runner, cap: Dictionary) -> void:
|
||
var t := String(node.get("type", ""))
|
||
if t == "battle" or t == "boss":
|
||
cap["battles"].append({"type": t, "depth": int(node.get("node_depth", -1)), "spawn": -1})
|
||
else:
|
||
runner.continue_after_node()
|
||
|
||
|
||
func _on_level_round_started(ri: int, runner, cap: Dictionary) -> void:
|
||
if ri != 1 or cap["battles"].is_empty():
|
||
return
|
||
var settlement = runner.current_battle()
|
||
if settlement == null:
|
||
return
|
||
var s: Dictionary = settlement.presentation_snapshot()
|
||
cap["battles"][cap["battles"].size() - 1]["spawn"] = s.enemies.size()
|
||
|
||
|
||
func _drive_battle(runner, settlement, cap: Dictionary) -> void:
|
||
var round_guard := 60
|
||
while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||
round_guard -= 1
|
||
BattleDriver.drive_round(settlement, func() -> bool:
|
||
return runner.current_battle() == settlement and settlement.battle_result == "")
|
||
|
||
|
||
# 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」——
|
||
# 判据:首轮存在伤害行 = 有敌且被杀(wipe);零战斗行 = 空场(Bug#1 特征)
|
||
func _battle_verdict_txt(cap: Dictionary, depth: int) -> String:
|
||
# 逐战斗行段切分:round_index 回卷 = 新战斗(defense_scan 同口径)
|
||
var battle_rows: Array = []
|
||
var segs: Array = []
|
||
var last_round := 0
|
||
for row in cap.rows:
|
||
var r: int = int(row.get("round_index", 0))
|
||
if r < last_round:
|
||
segs.append(battle_rows)
|
||
battle_rows = []
|
||
last_round = r
|
||
battle_rows.append(row)
|
||
if not battle_rows.is_empty():
|
||
segs.append(battle_rows)
|
||
var target := _battle_index_of_depth(depth, cap)
|
||
if target >= 0 and target < segs.size():
|
||
return _summarize_rows(segs[target])
|
||
return ""
|
||
|
||
|
||
func _battle_index_of_depth(depth: int, cap: Dictionary) -> int:
|
||
for i in cap["battles"].size():
|
||
if int(cap["battles"][i].get("depth", -1)) == depth:
|
||
return i
|
||
return -1
|
||
|
||
|
||
func _summarize_rows(rows: Array) -> String:
|
||
var verdict := ""
|
||
var r1_rows: Array = []
|
||
for row in rows:
|
||
if String(row.get("type", "")) == "verdict":
|
||
verdict = String(row.get("display_name", ""))
|
||
if int(row.get("round_index", 0)) == 1:
|
||
r1_rows.append(row)
|
||
if verdict != "战斗胜利" or r1_rows.is_empty():
|
||
return "· %s(%d 行)" % [verdict, rows.size()]
|
||
var combat_rows := 0
|
||
var kills := 0
|
||
for row in r1_rows:
|
||
var t := String(row.get("type", ""))
|
||
if t == "damage":
|
||
combat_rows += 1
|
||
if bool(row.get("killed", false)):
|
||
kills += 1
|
||
if combat_rows > 0:
|
||
return "· round-1 胜利 = 首轮全灭(wipe):战斗行 ×%d、击杀 ×%d——非空场" % [combat_rows, kills]
|
||
return "· round-1 胜利且零战斗行 = 空场特征(Bug#1)!"
|
||
|
||
|
||
func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary:
|
||
var depths: Array = []
|
||
for node in _level_row(db, 1).get("nodes", []):
|
||
var t := String(node.get("type", ""))
|
||
if t == "battle" or t == "boss":
|
||
depths.append(int(node.get("node_depth", -1)))
|
||
var total := 0
|
||
var bad := 0
|
||
for i in range(n_seeds):
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(seed0 + i)
|
||
for depth in depths:
|
||
total += 1
|
||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||
if not settlement.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
|
||
bad += 1
|
||
continue
|
||
var n: int = settlement.presentation_snapshot().enemies.size()
|
||
var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))[0] # 行集首行(同上)
|
||
var lo := int(row.get("unit_count", 1))
|
||
var hi := int(row.get("unit_count_max", lo))
|
||
if n < 1 or n < lo or n > hi:
|
||
bad += 1
|
||
return {"total": total, "bad": bad}
|