- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
127 lines
6.4 KiB
GDScript
127 lines
6.4 KiB
GDScript
# test_consumables_parity.gd —— 验证方向 4:日月神水自充能、龙神特药与传送卷轴 40250 1:1 对齐
|
|
extends SceneTree
|
|
|
|
const ConsumableSystem = preload("res://consumable_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_consumables_parity (Direction 4: Consumables & Scrolls 1:1) ===")
|
|
test_elixir_identification()
|
|
test_elixir_toggle_active()
|
|
test_elixir_recovery_and_exhaustion()
|
|
test_dragon_god_potions()
|
|
test_town_and_return_scrolls()
|
|
|
|
print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed])
|
|
if _failed > 0:
|
|
printerr("TEST FAILED!")
|
|
quit(1)
|
|
else:
|
|
print("ALL TESTS PASSED!")
|
|
quit(0)
|
|
|
|
func test_elixir_identification() -> void:
|
|
print("\n--- Test 1: Sun & Moon Elixirs Identification ---")
|
|
_assert(ConsumableSystem.is_elixir(72723), "72723 (Sun Elixir S) is an elixir")
|
|
_assert(ConsumableSystem.is_hp_elixir(72723), "72723 is an HP elixir")
|
|
_assert(not ConsumableSystem.is_sp_elixir(72723), "72723 is not an SP elixir")
|
|
|
|
_assert(ConsumableSystem.is_elixir(72728), "72728 (Moon Elixir M) is an elixir")
|
|
_assert(ConsumableSystem.is_sp_elixir(72728), "72728 is an SP elixir")
|
|
_assert(not ConsumableSystem.is_hp_elixir(72728), "72728 is not an HP elixir")
|
|
|
|
_assert(not ConsumableSystem.is_elixir(27001), "27001 Red Potion is not an elixir")
|
|
_assert(ConsumableSystem.is_dragon_god_potion(71027), "71027 is Dragon God Potion")
|
|
_assert(not ConsumableSystem.is_dragon_god_potion(72723), "Elixir is not Dragon God Potion")
|
|
|
|
func test_elixir_toggle_active() -> void:
|
|
print("\n--- Test 2: Sun/Moon Elixir Toggle Active (40250 char_item.cpp:3950) ---")
|
|
var elixir_hp := {"vnum": 72723, "sockets": [0, 0, 0]}
|
|
|
|
# Initially inactive
|
|
var toggle1 := ConsumableSystem.toggle_elixir(elixir_hp)
|
|
_assert(toggle1.ok and toggle1.active, "1st click activates HP elixir")
|
|
_assert(elixir_hp.sockets[0] == 1, "Socket 0 is set to 1 (Active)")
|
|
_assert(toggle1.affect_type == ConsumableSystem.AFFECT_AUTO_HP_RECOVERY, "Affect type is AFFECT_AUTO_HP_RECOVERY (534)")
|
|
_assert(elixir_hp.sockets[2] == 1000000, "Initialized capacity to 1,000,000 HP")
|
|
|
|
# 2nd click deactivates
|
|
var toggle2 := ConsumableSystem.toggle_elixir(elixir_hp)
|
|
_assert(toggle2.ok and not toggle2.active, "2nd click deactivates HP elixir")
|
|
_assert(elixir_hp.sockets[0] == 0, "Socket 0 is set to 0 (Inactive)")
|
|
|
|
func test_elixir_recovery_and_exhaustion() -> void:
|
|
print("\n--- Test 3: Auto Recovery Tick & Capacity Depletion (40250 char_item.cpp:7326) ---")
|
|
# Elixir with 500 capacity, initially active
|
|
var small_elixir := {"vnum": 72723, "sockets": [1, 0, 500]}
|
|
|
|
# Player at 800/1000 HP (needs 200)
|
|
var tick1 := ConsumableSystem.process_elixir_recovery(small_elixir, 800, 1000)
|
|
_assert(tick1.recovered == 200, "Recovered 200 HP to full")
|
|
_assert(not tick1.exhausted, "Elixir not exhausted yet")
|
|
_assert(small_elixir.sockets[1] == 200, "Used amount increased to 200")
|
|
_assert(tick1.remaining == 300, "Remaining capacity is 300")
|
|
|
|
# Player at 1000/1000 HP (full HP)
|
|
var tick_full := ConsumableSystem.process_elixir_recovery(small_elixir, 1000, 1000)
|
|
_assert(tick_full.recovered == 0, "No recovery when already at full HP")
|
|
_assert(small_elixir.sockets[1] == 200, "Used amount unchanged")
|
|
|
|
# Player at 600/1000 HP (needs 400, but only 300 left in elixir)
|
|
var tick_deplete := ConsumableSystem.process_elixir_recovery(small_elixir, 600, 1000)
|
|
_assert(tick_deplete.recovered == 300, "Recovered all remaining 300 HP")
|
|
_assert(tick_deplete.exhausted, "Elixir is now exhausted (used >= capacity)")
|
|
_assert(small_elixir.sockets[1] == 500, "Used amount reached full 500")
|
|
|
|
func test_dragon_god_potions() -> void:
|
|
print("\n--- Test 4: Dragon God Potions Buff Specs (71027..71030) ---")
|
|
var p_att := ConsumableSystem.use_dragon_god_potion(ConsumableSystem.VNUM_DRAGON_GOD_ATTACK)
|
|
_assert(p_att.ok and p_att.bonus_type == "POINT_ATT_BONUS" and p_att.bonus_value == 15,
|
|
"Dragon God Attack gives +15% attack bonus")
|
|
_assert(p_att.duration == 1800.0, "Dragon God Attack lasts 1800 seconds (30 minutes)")
|
|
|
|
var p_def := ConsumableSystem.use_dragon_god_potion(ConsumableSystem.VNUM_DRAGON_GOD_DEFENSE)
|
|
_assert(p_def.ok and p_def.bonus_type == "POINT_DEF_BONUS" and p_def.bonus_value == 15,
|
|
"Dragon God Defense gives +15% defense bonus")
|
|
|
|
var p_life := ConsumableSystem.use_dragon_god_potion(ConsumableSystem.VNUM_DRAGON_GOD_LIFE)
|
|
_assert(p_life.ok and p_life.bonus_type == "POINT_MAX_HP_PCT" and p_life.bonus_value == 20,
|
|
"Dragon God Life gives +20% HP bonus")
|
|
|
|
var p_mana := ConsumableSystem.use_dragon_god_potion(ConsumableSystem.VNUM_DRAGON_GOD_MANA)
|
|
_assert(p_mana.ok and p_mana.bonus_type == "POINT_MAX_SP_PCT" and p_mana.bonus_value == 20,
|
|
"Dragon God Mana gives +20% SP bonus")
|
|
|
|
func test_town_and_return_scrolls() -> void:
|
|
print("\n--- Test 5: Town Scroll (22001) & Return Scroll (22010) ---")
|
|
# Town Scroll 22001
|
|
var dummy_item := {"vnum": ConsumableSystem.VNUM_TOWN_SCROLL}
|
|
var res_town := ConsumableSystem.use_warp_scroll(ConsumableSystem.VNUM_TOWN_SCROLL, dummy_item, 100.0, 200.0)
|
|
_assert(res_town.ok and res_town.action == "TOWN_WARP" and res_town.consumed,
|
|
"Town scroll triggers town warp and is consumed")
|
|
|
|
# Return Scroll 22010 (Empty) -> Records current coordinate (1234, 5678)
|
|
var return_scroll := {"vnum": ConsumableSystem.VNUM_RETURN_SCROLL, "sockets": [0, 0, 0]}
|
|
var res_rec := ConsumableSystem.use_warp_scroll(ConsumableSystem.VNUM_RETURN_SCROLL, return_scroll, 1234.0, 5678.0, 2)
|
|
_assert(res_rec.ok and res_rec.action == "RECORDED", "Empty return scroll records coordinate")
|
|
_assert(not res_rec.consumed, "Recording coordinate does not consume the scroll")
|
|
_assert(return_scroll.sockets[0] == 1234 and return_scroll.sockets[1] == 5678 and return_scroll.sockets[2] == 2,
|
|
"Recorded coordinates saved into sockets [1234, 5678, map 2]")
|
|
|
|
# Return Scroll 22010 (Recorded) -> Warps and is consumed
|
|
var res_warp := ConsumableSystem.use_warp_scroll(ConsumableSystem.VNUM_RETURN_SCROLL, return_scroll, 999.0, 888.0, 1)
|
|
_assert(res_warp.ok and res_warp.action == "WARP_TO_COORDS", "Recorded return scroll warps to saved location")
|
|
_assert(res_warp.consumed, "Using recorded return scroll consumes the scroll")
|
|
_assert(res_warp.target_x == 1234 and res_warp.target_y == 5678 and res_warp.target_map == 2,
|
|
"Target coordinates match recorded values")
|