- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
167 lines
6.8 KiB
GDScript
167 lines
6.8 KiB
GDScript
# test_skill_book_reading_parity.gd —— 40250 大师技能书悟道与进阶 1:1 回归测试
|
|
extends SceneTree
|
|
|
|
const SystemClass = preload("res://skill_book_reading_system.gd")
|
|
|
|
var _passed: int = 0
|
|
var _failed: int = 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_skill_book_reading_parity (Batch 26 Direction 1) ===")
|
|
_test_level_curve_and_book_requirements()
|
|
_test_exp_and_cooldown_mechanics()
|
|
_test_exorcism_and_hermit_scrolls()
|
|
_test_failure_handling()
|
|
_test_full_m1_to_g1_progression()
|
|
_test_edge_cases_and_rejections()
|
|
|
|
print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed])
|
|
if _failed == 0:
|
|
print("ALL TESTS PASSED!\n")
|
|
else:
|
|
printerr("SOME TESTS FAILED!\n")
|
|
quit(0 if _failed == 0 else 1)
|
|
|
|
func _test_level_curve_and_book_requirements() -> void:
|
|
print("\n--- Test 1: Level Curve & 40250 Need Books Formula ---")
|
|
var sys = SystemClass.new()
|
|
sys.init_skill(1, 15) # Normal
|
|
_assert(sys.get_skill_master_type(1) == SystemClass.SKILL_NORMAL, "Lv 15 is SKILL_NORMAL")
|
|
|
|
sys.init_skill(1, 20) # M1
|
|
_assert(sys.get_skill_master_type(1) == SystemClass.SKILL_MASTER, "Lv 20 is SKILL_MASTER (M1)")
|
|
_assert(sys.get_need_books_for_next_level(1) == 1, "M1 requires 1 book to M2")
|
|
|
|
sys.init_skill(1, 21) # M2
|
|
_assert(sys.get_need_books_for_next_level(1) == 2, "M2 requires 2 books to M3")
|
|
|
|
sys.init_skill(1, 24) # M5
|
|
_assert(sys.get_need_books_for_next_level(1) == 5, "M5 requires 5 books to M6")
|
|
|
|
sys.init_skill(1, 29) # M10
|
|
_assert(sys.get_need_books_for_next_level(1) == 10, "M10 requires 10 books to G1")
|
|
|
|
# Total books from M1 to G1: sum(1..10) = 55
|
|
var total_books: int = 0
|
|
for lvl in range(20, 30):
|
|
sys.init_skill(1, lvl)
|
|
total_books += sys.get_need_books_for_next_level(1)
|
|
_assert(total_books == 55, "Total successful books needed for M1->G1 is exactly 55")
|
|
|
|
func _test_exp_and_cooldown_mechanics() -> void:
|
|
print("\n--- Test 2: EXP Consumption & 24h Cooldown ---")
|
|
var sys = SystemClass.new(50000, 1000000)
|
|
sys.init_skill(1, 20) # M1
|
|
|
|
# Insufficient EXP check
|
|
sys.player_exp = 15000
|
|
var res1 = sys.learn_skill_by_book(1, 1, 1) # roll=1 (success)
|
|
_assert(res1["error_code"] == "INSUFFICIENT_EXP", "Blocks reading when EXP < 20,000")
|
|
_assert(sys.player_exp == 15000, "EXP not deducted when blocked")
|
|
|
|
# Sufficient EXP check
|
|
sys.player_exp = 50000
|
|
var res2 = sys.learn_skill_by_book(1, 1, 1)
|
|
_assert(res2["error_code"] == "OK" and res2["success"] == true, "Reading succeeds with sufficient EXP")
|
|
_assert(sys.player_exp == 30000, "20,000 EXP deducted on attempt (50000 -> 30000)")
|
|
_assert(res2["level_up"] == true, "M1 leveled up to M2")
|
|
|
|
# 24h Cooldown check (86400s)
|
|
_assert(sys.skills[1]["next_read_time"] == 1000000 + 86400, "Next read time set to now + 86400")
|
|
sys.current_time = 1000000 + 40000 # 40,000s passed, still < 86400
|
|
var res3 = sys.learn_skill_by_book(1, 1, 1)
|
|
_assert(res3["error_code"] == "COOLDOWN_ACTIVE", "Reading blocked during 24h cooldown")
|
|
|
|
func _test_exorcism_and_hermit_scrolls() -> void:
|
|
print("\n--- Test 3: Exorcism Scroll (71001) & Hermit Advice (71094) ---")
|
|
var sys = SystemClass.new(100000, 1000000)
|
|
sys.init_skill(1, 21) # M2 (needs 2 books)
|
|
sys.skills[1]["next_read_time"] = 1000000 + 50000 # In cooldown
|
|
|
|
# Use Exorcism Scroll (71001)
|
|
sys.use_exorcism_scroll()
|
|
_assert(sys.has_affect(SystemClass.EXORCISM_SCROLL_VNUM) == true, "Exorcism affect active")
|
|
|
|
# Reading while in cooldown with Exorcism
|
|
var res = sys.learn_skill_by_book(1, 1, 1)
|
|
_assert(res["error_code"] == "OK", "Exorcism scroll bypassed cooldown")
|
|
_assert(res["consumed_exorcism"] == true, "Exorcism scroll consumed")
|
|
_assert(sys.has_affect(SystemClass.EXORCISM_SCROLL_VNUM) == false, "Exorcism affect removed after read")
|
|
|
|
# Use Hermit Advice (71094)
|
|
sys.use_hermit_advice()
|
|
_assert(sys.has_affect(SystemClass.HERMIT_ADVICE_VNUM) == true, "Hermit advice affect active")
|
|
|
|
# Reading with roll=99 (which normally fails base 35%)
|
|
sys.use_exorcism_scroll()
|
|
var res2 = sys.learn_skill_by_book(1, 1, 99)
|
|
_assert(res2["success"] == true, "Hermit advice forced 100% success rate on roll=99")
|
|
_assert(res2["consumed_hermit"] == true, "Hermit advice consumed")
|
|
_assert(sys.has_affect(SystemClass.HERMIT_ADVICE_VNUM) == false, "Hermit affect removed after read")
|
|
|
|
func _test_failure_handling() -> void:
|
|
print("\n--- Test 4: Reading Failure Penalty & Retention ---")
|
|
var sys = SystemClass.new(100000, 1000000)
|
|
sys.init_skill(1, 22, 1) # M3, read_count = 1 (needs 3 books)
|
|
|
|
# Roll 90 without hermit advice -> Failure
|
|
var res = sys.learn_skill_by_book(1, 1, 90)
|
|
_assert(res["success"] == false, "Reading failed on roll 90")
|
|
_assert(res["error_code"] == "READ_FAIL", "Error code is READ_FAIL")
|
|
_assert(sys.skills[1]["level"] == 22, "Skill level remains unchanged at 22")
|
|
_assert(sys.skills[1]["read_count"] == 1, "Skill read_count preserved at 1")
|
|
_assert(sys.player_exp == 80000, "EXP still deducted on failure (20,000 EXP)")
|
|
_assert(sys.skills[1]["next_read_time"] == 1000000 + 86400, "Cooldown applied even on failure")
|
|
|
|
func _test_full_m1_to_g1_progression() -> void:
|
|
print("\n--- Test 5: Full M1 to G1 Progression (55 Books) ---")
|
|
var sys = SystemClass.new(5000000, 1000000)
|
|
sys.init_skill(1, 20, 0) # Start at M1 (Lv 20)
|
|
|
|
var books_succeeded: int = 0
|
|
var final_res: Dictionary = {}
|
|
|
|
# Loop through all 55 books with 100% success using scrolls
|
|
for step in range(55):
|
|
sys.use_exorcism_scroll()
|
|
sys.use_hermit_advice()
|
|
final_res = sys.learn_skill_by_book(1, 1)
|
|
_assert(final_res["success"] == true, "Book %d read succeeded" % (step + 1))
|
|
books_succeeded += 1
|
|
|
|
_assert(books_succeeded == 55, "Successfully read 55 books")
|
|
_assert(sys.skills[1]["level"] == 30, "Skill level reached 30 (G1)")
|
|
_assert(sys.get_skill_master_type(1) == SystemClass.SKILL_GRAND_MASTER, "Skill tier is SKILL_GRAND_MASTER")
|
|
_assert(final_res["breakthrough_g"] == true, "Final book triggered breakthrough_g flag")
|
|
|
|
func _test_edge_cases_and_rejections() -> void:
|
|
print("\n--- Test 6: Edge Cases & Rejections ---")
|
|
var sys = SystemClass.new(100000, 1000000)
|
|
|
|
# 1. Unknown skill
|
|
var r1 = sys.learn_skill_by_book(999, 999)
|
|
_assert(r1["error_code"] == "NOT_LEARNABLE", "Reject unregistered skill")
|
|
|
|
# 2. Skill mismatch on book socket
|
|
sys.init_skill(1, 20)
|
|
var r2 = sys.learn_skill_by_book(1, 2) # Socket says skill 2, but reading for skill 1
|
|
_assert(r2["error_code"] == "SKILL_MISMATCH", "Reject skill book with mismatched socket")
|
|
|
|
# 3. Normal tier skill (< 20)
|
|
sys.init_skill(2, 10)
|
|
var r3 = sys.learn_skill_by_book(2, 2)
|
|
_assert(r3["error_code"] == "NOT_MASTER_TIER", "Reject normal tier skill (< 20)")
|
|
|
|
# 4. G1 tier skill (>= 30)
|
|
sys.init_skill(3, 30)
|
|
var r4 = sys.learn_skill_by_book(3, 3)
|
|
_assert(r4["error_code"] == "ALREADY_G_OR_P", "Reject Grandmaster tier skill (>= 30)")
|