- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
145 lines
5.6 KiB
GDScript
145 lines
5.6 KiB
GDScript
# test_class_specialization_parity.gd —— 40250 角色 30 级转职与无损洗髓 1:1 回归测试套件
|
|
extends SceneTree
|
|
|
|
const ClassSpecializationSystem = preload("res://class_specialization_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_class_specialization_parity (Direction 3: Class Specialization 1:1) ===")
|
|
_test_spec_tree_configuration()
|
|
_test_level_30_requirement()
|
|
_test_choosing_specialization()
|
|
_test_skill_point_allocation()
|
|
_test_reset_specialization_lossless()
|
|
_finish()
|
|
|
|
func _test_spec_tree_configuration() -> void:
|
|
print("\n--- Test 1: 4 Classes x 2 Specializations Tree ---")
|
|
var sys := ClassSpecializationSystem.new()
|
|
for j in [0, 1, 2, 3]:
|
|
var specs = sys.get_available_specializations(j)
|
|
_assert(specs.size() == 2, "Job %d has exactly 2 specialization branches" % j)
|
|
_assert(specs.has(1) and specs.has(2), "Branches are 1 and 2")
|
|
_assert(specs[1]["skills"].size() == 5, "Branch 1 has 5 skills")
|
|
_assert(specs[2]["skills"].size() == 5, "Branch 2 has 5 skills")
|
|
|
|
func _test_level_30_requirement() -> void:
|
|
print("\n--- Test 2: Level 30 Requirement ---")
|
|
var sys := ClassSpecializationSystem.new()
|
|
var low_lvl_player := {"job": 0, "level": 25, "skill_group": 0}
|
|
|
|
var fail_res = sys.choose_specialization(1, low_lvl_player)
|
|
_assert(fail_res["ok"] == false and fail_res["reason"] == "LEVEL_TOO_LOW", "Level 25 rejected")
|
|
|
|
func _test_choosing_specialization() -> void:
|
|
print("\n--- Test 3: Choosing Specialization (Warrior Body) ---")
|
|
var sys := ClassSpecializationSystem.new()
|
|
var player := {"job": 0, "level": 35, "skill_group": 0, "skill_points": 35}
|
|
|
|
var spec_sig := {"fired": false, "job": -1, "spec_id": 0}
|
|
sys.specialization_chosen.connect(func(j: int, sid: int, _name: String):
|
|
spec_sig["fired"] = true
|
|
spec_sig["job"] = j
|
|
spec_sig["spec_id"] = sid
|
|
)
|
|
|
|
# Choose Arahan (Body Warrior)
|
|
var res = sys.choose_specialization(1, player)
|
|
_assert(res["ok"] == true, "Specialization chosen")
|
|
_assert(player["skill_group"] == 1, "skill_group set to 1")
|
|
_assert(player["skills"].size() == 5, "5 skills initialized")
|
|
_assert(player["skills"].has(4) and player["skills"][4]["name"] == "剑气", "Has Aura of Sword (4)")
|
|
_assert(spec_sig["fired"] == true and spec_sig["spec_id"] == 1, "specialization_chosen signal emitted")
|
|
|
|
# Duplicate choice without reset must fail
|
|
var fail_dup = sys.choose_specialization(2, player)
|
|
_assert(fail_dup["ok"] == false and fail_dup["reason"] == "ALREADY_SPECIALIZED", "Cannot re-choose without reset")
|
|
|
|
func _test_skill_point_allocation() -> void:
|
|
print("\n--- Test 4: Skill Point Allocation ---")
|
|
var sys := ClassSpecializationSystem.new()
|
|
var player := {"job": 0, "level": 35, "skill_group": 1, "skill_points": 5, "skills": {
|
|
4: {"name": "剑气", "level": 0}
|
|
}}
|
|
|
|
var alloc_sig := {"fired": false, "vnum": 0, "lvl": 0}
|
|
sys.skill_point_allocated.connect(func(v: int, l: int, _r: int):
|
|
alloc_sig["fired"] = true
|
|
alloc_sig["vnum"] = v
|
|
alloc_sig["lvl"] = l
|
|
)
|
|
|
|
# Allocate 3 points into Aura of Sword
|
|
sys.allocate_skill_point(4, player)
|
|
sys.allocate_skill_point(4, player)
|
|
sys.allocate_skill_point(4, player)
|
|
_assert(player["skills"][4]["level"] == 3, "Aura of Sword leveled to 3")
|
|
_assert(player["skill_points"] == 2, "Remaining points 5 - 3 = 2")
|
|
_assert(alloc_sig["fired"] == true and alloc_sig["lvl"] == 3, "skill_point_allocated emitted")
|
|
|
|
# Cap test: level 20 is max normal level
|
|
player["skills"][4]["level"] = 20
|
|
var fail_cap = sys.allocate_skill_point(4, player)
|
|
_assert(fail_cap["ok"] == false and fail_cap["reason"] == "MAX_NORMAL_LEVEL", "Cannot allocate past level 20")
|
|
|
|
func _test_reset_specialization_lossless() -> void:
|
|
print("\n--- Test 5: Lossless Reset with Scroll (71100) & Branch Switch ---")
|
|
var sys := ClassSpecializationSystem.new()
|
|
var player := {
|
|
"job": 0,
|
|
"level": 40,
|
|
"skill_group": 1, # Body
|
|
"skill_points": 5,
|
|
"skills": {
|
|
1: {"name": "三连斩", "level": 10},
|
|
4: {"name": "剑气", "level": 20}
|
|
}
|
|
}
|
|
# Total points invested: 10 + 20 = 30. Remaining: 5. After refund: 35.
|
|
|
|
var inv: Array = [
|
|
{"vnum": ClassSpecializationSystem.VNUM_RESET_SCROLL, "name": "洗髓卷轴", "count": 1}
|
|
]
|
|
|
|
var reset_sig := {"fired": false, "pts": 0}
|
|
sys.specialization_reset.connect(func(pts: int):
|
|
reset_sig["fired"] = true
|
|
reset_sig["pts"] = pts
|
|
)
|
|
|
|
var res = sys.reset_specialization(inv, player, false)
|
|
_assert(res["ok"] == true, "Reset succeeded")
|
|
_assert(inv[0] == null, "Reset scroll consumed")
|
|
_assert(player["skill_group"] == 0, "skill_group reset to 0")
|
|
_assert(player["skills"].is_empty(), "Old skills cleared")
|
|
_assert(player["skill_points"] == 35, "All 30 points refunded (5 + 30 = 35)")
|
|
_assert(reset_sig["fired"] == true and reset_sig["pts"] == 30, "specialization_reset emitted with 30 pts")
|
|
|
|
# Now player can choose the other branch: Partisan (Mental Warrior)
|
|
var switch_res = sys.choose_specialization(2, player)
|
|
_assert(switch_res["ok"] == true, "Successfully switched to Mental Warrior (Partisan)")
|
|
_assert(player["skill_group"] == 2, "skill_group is now 2")
|
|
_assert(player["skills"].has(16) and player["skills"][16]["name"] == "强身术", "Has Strong Body (16)")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("Class Specialization Parity Tests Finished:")
|
|
print("Passed: %d, Failed: %d" % [_passed, _failed])
|
|
print("==========================================")
|
|
if _failed > 0:
|
|
printerr("FAILED: Some tests did not pass.")
|
|
quit(1)
|
|
else:
|
|
print("SUCCESS: All Class Specialization parity tests passed!")
|
|
quit(0)
|