# 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(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", 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_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, "battle") 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, "battle") 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)