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

106 lines
5.9 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.
# tests/unit/battle_settlement/skill_unlock_test.gd — Boss 半血解锁技·单位容器机制单元测试
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/skill_unlock_test.gd
# 覆盖:门谓词(hp ≤ round(max_hp×pct/100))/闩不可逆(治疗抬血不解锁)/
# 双面过滤(rollable+special 不入锁定)/保底不逼锁定(保底集合排除锁定)/解锁即可摇(冷却初值 0)/
# 普通行无字段逐位零扰动(goblin 池快照零 unlock 键、过滤恒空转)/同种子确定性(容器零随机)。
# 红线:期望值由表读值推导(零玩法数值字面量);零自建 RNG。
extends SceneTree
const ConfigDBScript := preload("res://core/config_db.gd")
const UnitContainerScript := preload("res://core/unit_container.gd")
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
if load_ok:
_test_gate_predicate(db)
_test_latch_irreversible(db)
_test_plain_rows_zero_drift(db)
quit(Kit.finish("skill_unlock_test"))
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
func _boss(db, L: int, D: int, kind := "boss"):
var ec = UnitContainerScript.new(db)
var ok: bool = ec.spawn({"level_index": L, "node_depth": D, "hero_hp": int(db.combat.hero.max_hp), "row_kind": kind})
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
return ec
func _boss_u(ec) -> Dictionary:
return ec.all_enemies()[0]
func _has_sid(arr: Array, sid: String) -> bool:
for e in arr:
if String(e.skill_id) == sid:
return true
return false
func _test_gate_predicate(db) -> void:
var ec = _boss(db, 2, 9)
var u: Dictionary = _boss_u(ec)
var mhp: int = int(u.max_hp)
Kit.check("前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)", mhp == 130)
# 未到阈值:hp = 阈值+1 → 不解锁,锁定条目不入可摇/保底集合
ec.apply_damage(u, mhp - 66)
Kit.check("门谓词:hp=66 > 阈值 65 → 零解锁", ec.check_trigger_unlocks(u).is_empty())
Kit.check("双面过滤:未解锁 venom_curb 不入可摇集合", not _has_sid(ec.rollable_skills(u), "venom_curb"))
Kit.check("双面过滤:未解锁 venom_curb 不入保底特殊技集合", not _has_sid(ec.special_skills(u), "venom_curb"))
# 恰到阈值:hp = 65 ≤ round(130×0.5) → 解锁(判式取 ≤)
var hit65: bool = ec.apply_damage(u, 1)
Kit.check("前置:补一刀至阈值 65 受理", hit65 and int(u.hp) == 65)
var unlocked: Array = ec.check_trigger_unlocks(u)
Kit.check("门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次", unlocked.size() == 1 and String(unlocked[0]) == "venom_curb")
# 解锁即可摇(冷却初值 0)+ 保底特殊技集合同步可见
Kit.check("解锁即可摇:venom_curb 入可摇集合(冷却初值 0)", _has_sid(ec.rollable_skills(u), "venom_curb"))
Kit.check("解锁即可摇:venom_curb 入保底特殊技集合", _has_sid(ec.special_skills(u), "venom_curb"))
# 保底不逼锁定:锁定期保底集合 = web_snare 唯一(venom_burst 被过滤——保底不得逼出未解锁技)
var ec2 = _boss(db, 2, 9)
var u2: Dictionary = _boss_u(ec2)
ec2.apply_damage(u2, mhp - 66)
var specials: Array = ec2.special_skills(u2)
# 特殊技集合语义 = 全部非普攻池条目(含伤害技 heavy——保底「必出特殊技」口径;proposal §2.1)
Kit.check("保底不逼锁定:锁定期保底集合 = heavy+venom_burst+web_snare(venom_curb 过滤不出)", specials.size() == 3 and _has_sid(specials, "heavy") and _has_sid(specials, "web_snare") and _has_sid(specials, "venom_burst") and not _has_sid(specials, "venom_curb"))
# 闩不可逆:解锁后治疗抬回满血 → 复查零新解锁、可摇集合不回退
func _test_latch_irreversible(db) -> void:
var ec = _boss(db, 2, 9)
var u: Dictionary = _boss_u(ec)
ec.apply_damage(u, int(u.max_hp) / 2) # → 半血触发
Kit.check("前置:半血触发解锁", ec.check_trigger_unlocks(u).size() == 1)
ec.heal(u, int(u.max_hp)) # 抬回满血
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_trigger_unlocks(u).is_empty())
Kit.check("闩不可逆:可摇集合仍含 venom_curb", _has_sid(ec.rollable_skills(u), "venom_curb"))
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
func _test_plain_rows_zero_drift(db) -> void:
var ec = load("res://core/unit_container.gd").new(db)
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u: Dictionary = ec.all_enemies()[0]
var any_unlock_key := false
for e in u.pool:
if e.has("trigger") or e.has("trigger_latched"):
any_unlock_key = true
Kit.check("零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)", not any_unlock_key)
Kit.check("零扰动:满血复查零解锁", ec.check_trigger_unlocks(u).is_empty())
var rollable: Array = ec.rollable_skills(u)
var pool_count: int = (u.pool as Array).size()
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
# 同种子确定性:容器零随机——两实例同输入查询逐位同(同款零扰动载体)
var ec2 = load("res://core/unit_container.gd").new(db)
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u2: Dictionary = ec2.all_enemies()[0]
ec.apply_damage(u, 3)
ec2.apply_damage(u2, 3)
var a := JSON.stringify(ec.rollable_skills(u))
var b := JSON.stringify(ec2.rollable_skills(u2))
Kit.check("确定性:两实例同输入可摇集合逐位同(容器零随机纪律)", a == b)