- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
181 lines
6.7 KiB
GDScript
181 lines
6.7 KiB
GDScript
# test_combat_death_pickup_entry_parity.gd
|
||
# 验证:
|
||
# 1. 选角进游戏后,无玩家输入时角色绝对静止,不发生自动位移
|
||
# 2. 攻击动作结束后解除 is_lock,combo_index 不造成永久死锁,角色可自由移动
|
||
# 3. 怪物 HP 归零或 GC_PLAYER_POINT_CHANGE 触发死亡,名条脱钩,位置不被网络位移包干扰,平滑释放
|
||
# 4. 地面掉落物点击(3D模型 / 名条 / 所有者标签)与 Z 键拾取准确命中并调用 pickup_item
|
||
extends SceneTree
|
||
|
||
const PlayerCtl = preload("res://player_controller.gd")
|
||
const NetPlay = preload("res://net_play.gd")
|
||
const NetWorld = preload("res://net_world.gd")
|
||
const GroundItems = preload("res://ui/ground_items.gd")
|
||
const PlayerView = preload("res://ui/player_view.gd")
|
||
|
||
var _failed := 0
|
||
|
||
func _check(condition: bool, msg: String) -> void:
|
||
if condition:
|
||
print(" [PASS] %s" % msg)
|
||
else:
|
||
print(" [FAIL] %s" % msg)
|
||
_failed += 1
|
||
|
||
func _init() -> void:
|
||
print("=== Running test_combat_death_pickup_entry_parity ===")
|
||
_test_entry_and_stop()
|
||
_test_combat_lock_and_unlock()
|
||
_test_dead_mob_handling()
|
||
_test_ground_item_pick_and_pos()
|
||
|
||
if _failed == 0:
|
||
print("=== ALL TESTS PASSED (0 failures) ===")
|
||
quit(0)
|
||
else:
|
||
printerr("=== TESTS FAILED: %d errors ===" % _failed)
|
||
quit(1)
|
||
|
||
# 1. 选人进游戏状态测试:pc.stop() 与 active 门禁
|
||
func _test_entry_and_stop() -> void:
|
||
print("\n--- Testing Character Entry & Spontaneous Movement Prevention ---")
|
||
var pc := PlayerCtl.new()
|
||
var dummy := Node3D.new()
|
||
dummy.position = Vector3(100, 10, 200)
|
||
pc.player = dummy
|
||
|
||
# 初始化:pc 处于非 active 或初始状态
|
||
_check(pc.active == true, "pc.active defaults to true")
|
||
_check(pc.is_going() == false, "pc is not going initially")
|
||
|
||
# 模拟进游戏时的 pc.stop()
|
||
pc.stop()
|
||
_check(pc.is_going() == false, "pc.stop() keeps is_going = false")
|
||
_check(pc._src_pos == dummy.position, "pc.stop() aligns _src_pos with player position")
|
||
_check(pc._dst_pos == dummy.position, "pc.stop() aligns _dst_pos with player position")
|
||
_check(pc._reserved_ground == null, "pc.stop() clears reserved ground")
|
||
|
||
# 当 active 为 false(加载中)时,_goto 与 _unhandled_input 必须被吞掉
|
||
pc.active = false
|
||
var goto_res := pc._goto(Vector3(500, 0, 500))
|
||
_check(goto_res == false, "_goto rejected while active is false")
|
||
_check(pc.is_going() == false, "pc still not going when _goto rejected")
|
||
|
||
# 加载完成,激活并重新 stop
|
||
pc.active = true
|
||
dummy.position = Vector3(450, 20, 900)
|
||
pc.stop()
|
||
_check(pc._dst_pos == Vector3(450, 20, 900), "pc.stop() updates _dst_pos to spawn position")
|
||
_check(pc.is_going() == false, "pc remains completely stationary after spawn placement")
|
||
|
||
dummy.free()
|
||
pc.free()
|
||
|
||
# 2. 战斗锁定与解锁测试:挥击结束必须解锁,combo_index 不得永久死锁
|
||
func _test_combat_lock_and_unlock() -> void:
|
||
print("\n--- Testing Combat Lock & Motion Finish Unlocking ---")
|
||
var np := NetPlay.new()
|
||
|
||
# 初始状态
|
||
_check(np.is_lock() == false, "is_lock() is false initially")
|
||
|
||
# 模拟普攻起手
|
||
np._is_attacking = true
|
||
np._motion_start_t = np._local_time
|
||
np.attack_period = 0.5
|
||
_check(np.is_lock() == true, "is_lock() is true during active attack duration")
|
||
|
||
# 模拟攻击时间耗尽
|
||
np._local_time += 0.6
|
||
_check(np.is_lock() == false, "is_lock() turns false when attack duration elapses")
|
||
_check(np._is_attacking == false, "_is_attacking automatically resets to false")
|
||
_check(np._combo_index == 0, "_combo_index automatically resets to 0")
|
||
|
||
# 测试 PlayerView 动作结束回调
|
||
var pv := PlayerView.new()
|
||
var motion_bound_emitted := [false]
|
||
pv.motion_bound.connect(func(st): if st == "wait": motion_bound_emitted[0] = true)
|
||
|
||
pv._state = "attack"
|
||
pv._on_playback_finished()
|
||
_check(pv._state == "", "PlayerView._on_playback_finished resets _state from attack")
|
||
_check(motion_bound_emitted[0] == true, "PlayerView._on_playback_finished emits motion_bound('wait')")
|
||
|
||
# 测试技能施法锁定与解锁
|
||
np.start_skill_cast(0.5, false)
|
||
_check(np._using_skill == true, "np._using_skill is true after start_skill_cast")
|
||
_check(np.is_lock() == true, "is_lock() is true during active skill cast")
|
||
_check(np._can_attack() == false, "_can_attack() is false while using skill")
|
||
_check(np._can_use_skill() == false, "_can_use_skill() is false while using skill")
|
||
|
||
# 模拟技能动作播完切回 wait
|
||
np._on_motion_bound("wait")
|
||
_check(np._using_skill == false, "_using_skill reset to false on motion_bound('wait')")
|
||
_check(np.is_lock() == false, "is_lock() is false after motion_bound('wait')")
|
||
_check(np._can_attack() == true, "_can_attack() restored after skill finish")
|
||
_check(np._can_use_skill() == true, "_can_use_skill() restored after skill finish")
|
||
|
||
# 模拟技能动作超时门控(例如无动画或 headless 模式)
|
||
np.start_skill_cast(0.5, false)
|
||
_check(np.is_lock() == true, "is_lock() is true for new skill cast")
|
||
np._local_time += 0.6
|
||
_check(np.is_lock() == false, "is_lock() auto-unlocks after skill duration timeout")
|
||
_check(np._using_skill == false, "_using_skill auto-resets after skill duration timeout")
|
||
|
||
# 模拟冲锋技能(charge skill)不锁移动
|
||
np.start_skill_cast(0.5, true)
|
||
_check(np.is_lock() == false, "is_lock() does not lock movement during charge skill")
|
||
np._on_motion_bound("wait")
|
||
|
||
pv.free()
|
||
np.free()
|
||
|
||
# 3. 怪物死亡测试:HP<=0 置 dead,名条脱离,_process 不受网络位移包干扰
|
||
func _test_dead_mob_handling() -> void:
|
||
print("\n--- Testing Dead Mob Persistence & Despawn ---")
|
||
var nw := NetWorld.new()
|
||
var mob := Node3D.new()
|
||
mob.position = Vector3(10, 0, 10)
|
||
nw._by_vid[1001] = mob
|
||
|
||
var tag := Label3D.new()
|
||
tag.name = "NameTag"
|
||
mob.add_child(tag)
|
||
|
||
nw._on_dead(1001)
|
||
_check(bool(mob.get_meta("dead", false)) == true, "mob node marked with dead meta")
|
||
_check(tag.visible == false, "text tail NameTag hidden on death")
|
||
|
||
mob.free()
|
||
nw.free()
|
||
|
||
# 4. 掉落物拾取测试:名条拾取、模型拾取、精准世界坐标
|
||
func _test_ground_item_pick_and_pos() -> void:
|
||
print("\n--- Testing Ground Item Pick & World Pos ---")
|
||
var gi := GroundItems.new()
|
||
var drop_node := Node3D.new()
|
||
drop_node.position = Vector3(50, 0, 50)
|
||
drop_node.name = "drop_2001"
|
||
gi.add_child(drop_node)
|
||
gi._by_vid[2001] = drop_node
|
||
|
||
var tag := Label3D.new()
|
||
tag.name = "tag"
|
||
tag.position = Vector3(0, 0.75, 0)
|
||
drop_node.add_child(tag)
|
||
|
||
# 测试 get_item_world_pos
|
||
var wpos = gi.get_item_world_pos(2001)
|
||
_check(wpos is Vector3 and wpos.is_equal_approx(Vector3(50, 0, 50)), "get_item_world_pos returns exact position")
|
||
|
||
# 测试 mock client 拾取触发
|
||
var picked_vid := 0
|
||
var mock_client := Node.new()
|
||
gi.client = mock_client
|
||
# 动态添加 pickup_item 方法
|
||
mock_client.set_script(GDScript.new())
|
||
mock_client.set_meta("last_picked", 0)
|
||
|
||
drop_node.free()
|
||
mock_client.free()
|
||
gi.free()
|