Files
mtgodot-poc/project/test_skill_book_reading_parity.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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)")