- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
161 lines
6.6 KiB
GDScript
161 lines
6.6 KiB
GDScript
# test_pet_companion_parity.gd —— 40250 伴随宠物系统 1:1 纯净回归测试套件
|
|
extends SceneTree
|
|
|
|
const PetCompanionSystem = preload("res://pet_companion_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_pet_companion_parity (Direction 2: Pet Companion System 1:1) ===")
|
|
_test_pet_table_and_validity()
|
|
_test_summon_and_bonuses()
|
|
_test_dismiss_and_toggle()
|
|
_test_3d_movement_and_states()
|
|
_test_time_expiry()
|
|
_finish()
|
|
|
|
func _test_pet_table_and_validity() -> void:
|
|
print("\n--- Test 1: Pet Table Configuration & Seal Validation ---")
|
|
var pet_sys := PetCompanionSystem.new()
|
|
|
|
# Invalid seal item
|
|
var invalid_res = pet_sys.toggle_pet_seal({"vnum": 99999}, Vector3.ZERO)
|
|
_assert(invalid_res["ok"] == false and invalid_res["reason"] == "NOT_A_PET_SEAL", "Invalid seal rejected")
|
|
|
|
# Check configured pets in PET_TABLE
|
|
_assert(PetCompanionSystem.PET_TABLE.has(53001), "Fire Phoenix (53001) in table")
|
|
_assert(PetCompanionSystem.PET_TABLE.has(53002), "Reindeer (53002) in table")
|
|
_assert(PetCompanionSystem.PET_TABLE.has(53003), "Ice Phoenix (53003) in table")
|
|
_assert(PetCompanionSystem.PET_TABLE.has(53005), "Bao Bao (53005) in table")
|
|
_assert(PetCompanionSystem.PET_TABLE.has(53006), "Leonidas (53006) in table")
|
|
|
|
var f_phoenix = PetCompanionSystem.PET_TABLE[53001]
|
|
_assert(f_phoenix["mob_vnum"] == 34001, "Fire Phoenix mob vnum is 34001")
|
|
_assert(f_phoenix["bonuses"]["max_hp"] == 1500, "Fire Phoenix bonus Max HP is 1500")
|
|
_assert(f_phoenix["bonuses"]["monster_dmg_pct"] == 15, "Fire Phoenix bonus monster dmg is 15%")
|
|
|
|
func _test_summon_and_bonuses() -> void:
|
|
print("\n--- Test 2: Summoning & Stat Injection ---")
|
|
var pet_sys := PetCompanionSystem.new()
|
|
var signal_data := {
|
|
"fired": false,
|
|
"name": "",
|
|
"vnum": 0
|
|
}
|
|
|
|
pet_sys.pet_summoned.connect(func(pname: String, vnum: int):
|
|
signal_data["fired"] = true
|
|
signal_data["name"] = pname
|
|
signal_data["vnum"] = vnum
|
|
)
|
|
|
|
var player_pos := Vector3(100.0, 0.0, 200.0)
|
|
var res = pet_sys.toggle_pet_seal({"vnum": 53001, "duration": 3600.0}, player_pos)
|
|
|
|
_assert(res["ok"] == true, "Fire Phoenix summon succeeded")
|
|
_assert(res["action"] == "summon", "Action is summon")
|
|
_assert(pet_sys.is_summoned == true, "is_summoned is true")
|
|
_assert(pet_sys.current_seal_vnum == 53001, "current_seal_vnum is 53001")
|
|
_assert(signal_data["fired"] == true, "pet_summoned signal emitted")
|
|
_assert(signal_data["name"] == "火凤凰", "Summoned pet name is 火凤凰")
|
|
_assert(signal_data["vnum"] == 53001, "Summoned pet vnum is 53001")
|
|
|
|
var bonuses = pet_sys.get_active_bonuses()
|
|
_assert(bonuses["max_hp"] == 1500, "Active bonus max_hp is 1500")
|
|
_assert(bonuses["monster_dmg_pct"] == 15, "Active bonus monster_dmg_pct is 15%")
|
|
|
|
func _test_dismiss_and_toggle() -> void:
|
|
print("\n--- Test 3: Dismiss & Switch Pets ---")
|
|
var pet_sys := PetCompanionSystem.new()
|
|
var dismiss_data := {
|
|
"fired": false,
|
|
"name": ""
|
|
}
|
|
|
|
pet_sys.pet_dismissed.connect(func(pname: String):
|
|
dismiss_data["fired"] = true
|
|
dismiss_data["name"] = pname
|
|
)
|
|
|
|
var player_pos := Vector3.ZERO
|
|
# Summon 53005 (Bao Bao)
|
|
pet_sys.toggle_pet_seal({"vnum": 53005}, player_pos)
|
|
_assert(pet_sys.is_summoned == true, "Bao Bao summoned")
|
|
_assert(pet_sys.get_active_bonuses()["critical_pct"] == 15, "Bao Bao grants 15% crit")
|
|
|
|
# Toggle 53005 again -> dismiss
|
|
var res = pet_sys.toggle_pet_seal({"vnum": 53005}, player_pos)
|
|
_assert(res["ok"] == true and res["action"] == "dismiss", "Toggling same seal dismisses pet")
|
|
_assert(pet_sys.is_summoned == false, "Pet is no longer summoned")
|
|
_assert(dismiss_data["fired"] == true, "pet_dismissed signal emitted")
|
|
_assert(dismiss_data["name"] == "小胖熊猫", "Dismissed name matches")
|
|
_assert(pet_sys.get_active_bonuses().is_empty(), "No bonuses active when pet dismissed")
|
|
|
|
# Summon 53006 (Leonidas) and then summon 53003 (Ice Phoenix) to switch
|
|
pet_sys.toggle_pet_seal({"vnum": 53006}, player_pos)
|
|
_assert(pet_sys.current_seal_vnum == 53006, "Leonidas active")
|
|
_assert(pet_sys.get_active_bonuses()["att_pct"] == 10, "Leonidas grants 10% att")
|
|
|
|
# Switch directly to 53003
|
|
var switch_res = pet_sys.toggle_pet_seal({"vnum": 53003}, player_pos)
|
|
_assert(switch_res["ok"] == true and switch_res["action"] == "summon", "Direct switch to Ice Phoenix succeeded")
|
|
_assert(pet_sys.current_seal_vnum == 53003, "Active pet is now Ice Phoenix")
|
|
_assert(pet_sys.get_active_bonuses()["def_pct"] == 15, "Ice Phoenix grants 15% def")
|
|
|
|
func _test_3d_movement_and_states() -> void:
|
|
print("\n--- Test 4: 3D Movement & State Machine ---")
|
|
var pet_sys := PetCompanionSystem.new()
|
|
var player_pos := Vector3(0.0, 0.0, 0.0)
|
|
pet_sys.toggle_pet_seal({"vnum": 53001}, player_pos)
|
|
|
|
# 1. When close (dist <= 2.0), state is wait
|
|
pet_sys.pet_position = Vector3(1.0, 0.0, 0.0) # dist = 1.0m <= 2.0m
|
|
pet_sys.update_movement(player_pos, 0.1)
|
|
_assert(pet_sys.pet_state == "wait", "Pet waits within 2.0m")
|
|
|
|
# 2. When player moves to (10, 0, 0), dist is 9.0m (> 2.0m)
|
|
player_pos = Vector3(10.0, 0.0, 0.0)
|
|
var prev_pos = pet_sys.pet_position
|
|
pet_sys.update_movement(player_pos, 0.5) # dt = 0.5s -> move speed 4.5m/s -> move 2.25m towards player
|
|
_assert(pet_sys.pet_state == "run", "Pet state is run when following player")
|
|
_assert(pet_sys.pet_position.x > prev_pos.x, "Pet moved towards player")
|
|
|
|
# 3. When player teleports far away (> 25.0m)
|
|
player_pos = Vector3(100.0, 0.0, 100.0)
|
|
pet_sys.update_movement(player_pos, 0.1)
|
|
var new_dist = pet_sys.pet_position.distance_to(player_pos)
|
|
_assert(new_dist < 5.0, "Pet instantly teleports near player when dist >= 25m")
|
|
_assert(pet_sys.pet_state == "wait", "Pet returns to wait state after teleport")
|
|
|
|
func _test_time_expiry() -> void:
|
|
print("\n--- Test 5: Pet Duration Expiration ---")
|
|
var pet_sys := PetCompanionSystem.new()
|
|
pet_sys.toggle_pet_seal({"vnum": 53001, "duration": 10.0}, Vector3.ZERO)
|
|
_assert(pet_sys.is_summoned == true, "Pet summoned with 10s duration")
|
|
|
|
pet_sys.update(4.0)
|
|
_assert(pet_sys.is_summoned == true, "Pet still active at 4s elapsed")
|
|
_assert(is_equal_approx(pet_sys.remaining_time, 6.0), "6s remaining")
|
|
|
|
pet_sys.update(7.0) # Exceeds remaining
|
|
_assert(pet_sys.is_summoned == false, "Pet dismissed automatically when duration expires")
|
|
_assert(pet_sys.get_active_bonuses().is_empty(), "No bonuses after expiration")
|
|
|
|
func _finish() -> void:
|
|
print("\n==========================================")
|
|
print("Pet Companion Parity Test Results: %d Passed, %d Failed" % [_passed, _failed])
|
|
print("==========================================")
|
|
if _failed > 0:
|
|
quit(1)
|
|
else:
|
|
quit(0)
|