- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
168 lines
6.4 KiB
GDScript
168 lines
6.4 KiB
GDScript
# test_status_skill_reset_parity.gd —— 40250 属性重修卷轴与忘却之书洗点 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const StatusSkillResetSystem = preload("res://status_skill_reset_system.gd")
|
|
|
|
var _passed := 0
|
|
var _failed := 0
|
|
|
|
func _assert(cond: bool, msg: String) -> void:
|
|
if cond:
|
|
_passed += 1
|
|
print(" [PASS] %s" % msg)
|
|
else:
|
|
_failed += 1
|
|
printerr(" [FAIL] %s" % msg)
|
|
|
|
func _init() -> void:
|
|
print("\n=== Running test_status_skill_reset_parity (Direction 2: Status & Skill Reset 1:1) ===")
|
|
_test_single_status_reset()
|
|
_test_all_status_reset()
|
|
_test_book_of_oblivion()
|
|
_test_all_skills_reset()
|
|
_finish()
|
|
|
|
func _test_single_status_reset() -> void:
|
|
print("\n--- Test 1: Single Status Redistribution (71002) ---")
|
|
var sys := StatusSkillResetSystem.new()
|
|
var player := {
|
|
"job": "warrior",
|
|
"vit": 16,
|
|
"int": 3,
|
|
"str": 50, # Base is 15 -> 35 points added
|
|
"dex": 5,
|
|
"unused_status_points": 0
|
|
}
|
|
var inv: Array = [
|
|
{"vnum": StatusSkillResetSystem.VNUM_STATUS_REDIST_SCROLL, "name": "属性重修卷轴", "count": 2}
|
|
]
|
|
|
|
var reset_sig := {"fired": false, "stat": "", "refunded": 0}
|
|
sys.status_reset.connect(func(s: String, r: int):
|
|
reset_sig["fired"] = true
|
|
reset_sig["stat"] = s
|
|
reset_sig["refunded"] = r
|
|
)
|
|
|
|
# Reset STR
|
|
var res_str = sys.reset_single_status("str", player, inv, 0)
|
|
_assert(res_str["ok"] == true, "STR reset successfully")
|
|
_assert(player["str"] == 15, "STR returned to warrior base 15")
|
|
_assert(player["unused_status_points"] == 35, "35 points refunded to unused pool")
|
|
_assert(inv[0]["count"] == 1, "Scroll count decremented 2 -> 1")
|
|
_assert(reset_sig["fired"] == true, "status_reset signal emitted")
|
|
_assert(reset_sig["stat"] == "str" and reset_sig["refunded"] == 35, "Signal reported correct stat & points")
|
|
|
|
# Try reset again when already at base
|
|
var res_base = sys.reset_single_status("str", player, inv, 0)
|
|
_assert(res_base["ok"] == false, "Reset rejected when already at base")
|
|
_assert(res_base["reason"] == "STAT_ALREADY_AT_BASE", "Reason is STAT_ALREADY_AT_BASE")
|
|
|
|
func _test_all_status_reset() -> void:
|
|
print("\n--- Test 2: All Status Reset (Blood Pill 71003) ---")
|
|
var sys := StatusSkillResetSystem.new()
|
|
var player := {
|
|
"job": "warrior",
|
|
"vit": 36, # Base 16 (+20)
|
|
"int": 3, # Base 3 (+0)
|
|
"str": 40, # Base 15 (+25)
|
|
"dex": 15, # Base 5 (+10)
|
|
"unused_status_points": 5 # total extra = 20 + 25 + 10 = 55
|
|
}
|
|
var inv: Array = [
|
|
{"vnum": StatusSkillResetSystem.VNUM_ALL_STATUS_RESET_SCROLL, "name": "全属性重置卷轴", "count": 1}
|
|
]
|
|
|
|
var all_sig := {"fired": false, "refunded": 0}
|
|
sys.all_status_reset.connect(func(r: int):
|
|
all_sig["fired"] = true
|
|
all_sig["refunded"] = r
|
|
)
|
|
|
|
var res_all = sys.reset_all_status(player, inv, 0)
|
|
_assert(res_all["ok"] == true, "All status reset succeeded")
|
|
_assert(player["vit"] == 16, "VIT back to base 16")
|
|
_assert(player["str"] == 15, "STR back to base 15")
|
|
_assert(player["dex"] == 5, "DEX back to base 5")
|
|
_assert(player["unused_status_points"] == 60, "Refunded 55 points (5 existing + 55 = 60 total)")
|
|
_assert(inv[0] == null, "Scroll consumed from slot 0")
|
|
_assert(all_sig["fired"] == true, "all_status_reset signal emitted")
|
|
_assert(all_sig["refunded"] == 55, "Reported 55 refunded points")
|
|
|
|
func _test_book_of_oblivion() -> void:
|
|
print("\n--- Test 3: Book of Oblivion (50311~50320) Single Skill Point Reset ---")
|
|
var sys := StatusSkillResetSystem.new()
|
|
var player_skills := {
|
|
1: 16, # 三连斩 Lv 16
|
|
2: 0, # 旋风斩 Lv 0
|
|
3: 20 # 剑气 Master (M1)
|
|
}
|
|
var player_data := {"unused_skill_points": 0}
|
|
var inv: Array = [
|
|
{"vnum": 50311, "name": "忘却之书(三连斩)", "target_skill": 1, "count": 2},
|
|
{"vnum": 50312, "name": "忘却之书(旋风斩)", "target_skill": 2, "count": 1},
|
|
{"vnum": 50313, "name": "忘却之书(剑气)", "target_skill": 3, "count": 1}
|
|
]
|
|
|
|
var sk_sig := {"fired": false, "id": 0, "new_lv": 0}
|
|
sys.skill_point_reset.connect(func(id: int, nlv: int):
|
|
sk_sig["fired"] = true
|
|
sk_sig["id"] = id
|
|
sk_sig["new_lv"] = nlv
|
|
)
|
|
|
|
# 1. Reset Skill 1 (16 -> 15)
|
|
var res1 = sys.reset_single_skill_point(1, player_skills, player_data, inv, 0)
|
|
_assert(res1["ok"] == true, "Skill 1 point reset succeeded")
|
|
_assert(player_skills[1] == 15, "Skill 1 level reduced 16 -> 15")
|
|
_assert(player_data["unused_skill_points"] == 1, "1 skill point refunded")
|
|
_assert(inv[0]["count"] == 1, "Book count decremented 2 -> 1")
|
|
_assert(sk_sig["fired"] == true, "skill_point_reset signal emitted")
|
|
_assert(sk_sig["id"] == 1 and sk_sig["new_lv"] == 15, "Signal reported skill 1 lv 15")
|
|
|
|
# 2. Skill level 0 cannot be reset
|
|
var res_zero = sys.reset_single_skill_point(2, player_skills, player_data, inv, 1)
|
|
_assert(res_zero["ok"] == false, "Skill lv 0 rejected")
|
|
_assert(res_zero["reason"] == "SKILL_LEVEL_ZERO", "Reason is SKILL_LEVEL_ZERO")
|
|
|
|
# 3. Master skill (>= 20) cannot be downgraded by Book of Oblivion (40250 rule)
|
|
var res_master = sys.reset_single_skill_point(3, player_skills, player_data, inv, 2)
|
|
_assert(res_master["ok"] == false, "Master skill rejected")
|
|
_assert(res_master["reason"] == "SKILL_IS_MASTER", "Reason is SKILL_IS_MASTER")
|
|
|
|
func _test_all_skills_reset() -> void:
|
|
print("\n--- Test 4: All Skills Reset Scroll (71001) ---")
|
|
var sys := StatusSkillResetSystem.new()
|
|
var player_skills := {
|
|
1: 15,
|
|
2: 18,
|
|
3: 7
|
|
} # Total invested: 15 + 18 + 7 = 40
|
|
var player_data := {"unused_skill_points": 2}
|
|
var inv: Array = [
|
|
{"vnum": StatusSkillResetSystem.VNUM_SKILL_RESET_SCROLL, "name": "技能重置卷轴", "count": 1}
|
|
]
|
|
|
|
var all_sk_sig := {"fired": false, "refunded": 0}
|
|
sys.all_skills_reset.connect(func(r: int):
|
|
all_sk_sig["fired"] = true
|
|
all_sk_sig["refunded"] = r
|
|
)
|
|
|
|
var res_all = sys.reset_all_skills(player_skills, player_data, inv, 0)
|
|
_assert(res_all["ok"] == true, "All skills reset succeeded")
|
|
_assert(player_skills[1] == 0 and player_skills[2] == 0 and player_skills[3] == 0, "All skills cleared to 0")
|
|
_assert(player_data["unused_skill_points"] == 42, "Refunded 40 points (2 existing + 40 = 42 total)")
|
|
_assert(inv[0] == null, "Scroll consumed")
|
|
_assert(all_sk_sig["fired"] == true, "all_skills_reset signal emitted")
|
|
_assert(all_sk_sig["refunded"] == 40, "Signal reported 40 refunded points")
|
|
|
|
func _finish() -> void:
|
|
print("\n=======================================================")
|
|
print("Status & Skill Reset Parity Tests: %d Passed, %d Failed" % [_passed, _failed])
|
|
print("=======================================================\n")
|
|
if _failed > 0:
|
|
quit(1)
|
|
else:
|
|
quit(0)
|