merge master(d3fa887 技能池④+⑤提灯)入分支:07清单⑭双登冲突解(技能池⑭保留+秘宝目检改⑮);四文件auto-merge带旧词族零带入(全查仅存档键合法例外)

This commit is contained in:
DKChan committed 2026-10-05 15:47:33 +08:00
commit a9a2e25a0a
17 files changed
+342 -28

No files matched your search

+1
View File
@@ -38,6 +38,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 迭代 7 hotfix——奖励节点 UI 直验(盲区补漏)
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # 迭代14④ T6——Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
@@ -0,0 +1,103 @@
# tests/unit/battle_settlement/skill_unlock_test.gd — Boss 半血解锁技·单位容器机制单元测试
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/skill_unlock_test.gd
# 覆盖(迭代14④ 技能池执行批 T6):门谓词(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):
var ec = UnitContainerScript.new(db)
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp))
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_skill_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_skill_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_skill_unlocks(u).size() == 1)
ec.heal(u, int(u.max_hp)) # 抬回满血
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_skill_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 = _boss(db, 1, 0)
var u: Dictionary = _boss_u(ec)
var any_unlock_key := false
for e in u.pool:
if e.has("unlock_hp_pct") or e.has("unlocked"):
any_unlock_key = true
Kit.check("零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)", not any_unlock_key)
Kit.check("零扰动:满血复查零解锁", ec.check_skill_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 = _boss(db, 1, 0)
var u2: Dictionary = _boss_u(ec2)
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)