- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
114 lines
5.1 KiB
GDScript
114 lines
5.1 KiB
GDScript
# movement_parity_test.gd —— 验证角色与怪物移动对齐 40250 的实现机制:
|
|
# 1. PlayerController 转向 45° 阻尼减速(5/12)与 ARRIVE_EPS 到达精度。
|
|
# 2. PlayerController 遇障碍物切线滑动判定。
|
|
# 3. EntityStore 网络命令队列 EMA 滤波与动作速度 fallback。
|
|
# 4. NetWorld 远端实体朝向收敛 45° 阻尼。
|
|
#
|
|
# godot --headless --path project --script movement_parity_test.gd
|
|
extends SceneTree
|
|
|
|
const PlayerController = preload("res://player_controller.gd")
|
|
const NetWorld = preload("res://net_world.gd")
|
|
const MobView = preload("res://ui/mob_view.gd")
|
|
|
|
var _fail := 0
|
|
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
if _fail == 0:
|
|
print("PASS: movement_parity_test (40250 rotation damping + arrival epsilon + collision tangent + EMA network gap)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed" % _fail)
|
|
quit(1)
|
|
|
|
func _run() -> void:
|
|
# =========================================================================
|
|
# 1. PlayerController: 转向 45° 阻尼减速测试 (40250 InstanceBaseMovement.cpp:142-154)
|
|
# =========================================================================
|
|
var pc := PlayerController.new()
|
|
var player := Node3D.new()
|
|
get_root().add_child(player)
|
|
get_root().add_child(pc)
|
|
pc.player = player
|
|
pc.rotation_speed_deg = 1200.0
|
|
|
|
# 角度差 > 45° 时(例如 90° = PI/2):转向速度应为全速 1200°/s
|
|
player.rotation.y = 0.0
|
|
var target_90 := deg_to_rad(90.0)
|
|
pc._turn_toward(target_90, 0.01) # dt = 10ms -> 预期前进 12°
|
|
var turned_90: float = rad_to_deg(player.rotation.y)
|
|
_ck(is_equal_approx(turned_90, 12.0), "Over 45 deg: full speed 1200 deg/s (turned %.2f, expected 12.0)" % turned_90)
|
|
|
|
# 角度差 <= 45° 时(例如 30°):转向速度应降为 5/12(500°/s)
|
|
player.rotation.y = 0.0
|
|
var target_30 := deg_to_rad(30.0)
|
|
pc._turn_toward(target_30, 0.01) # dt = 10ms -> 预期前进 5° (12 * 5/12 = 5)
|
|
var turned_30: float = rad_to_deg(player.rotation.y)
|
|
_ck(is_equal_approx(turned_30, 5.0), "Under 45 deg: damped speed 500 deg/s (turned %.2f, expected 5.0)" % turned_30)
|
|
|
|
# =========================================================================
|
|
# 2. PlayerController: ARRIVE_EPS 精准到达测试 (40250 InstanceBase.cpp:1600)
|
|
# =========================================================================
|
|
_ck(pc.ARRIVE_EPS <= 0.05, "ARRIVE_EPS is <= 0.05 (got %f)" % pc.ARRIVE_EPS)
|
|
player.position = Vector3(10.0, 0.0, 10.0)
|
|
pc._goto(Vector3(12.0, 0.0, 10.0)) # 距离目标点 2m (> 1m MOVABLE_GROUND_DISTANCE_M)
|
|
_ck(pc.is_going(), "_goto started (is_going == true)")
|
|
# 模拟角色走到了距目标 3cm 处
|
|
player.position = Vector3(11.97, 0.0, 10.0)
|
|
pc._process(0.016)
|
|
_ck(not pc.is_going(), "Within ARRIVE_EPS (3cm <= 5cm) stops immediately (is_going == false)")
|
|
_ck(is_equal_approx(player.position.x, 12.0), "Player position snapped to exact clicked target X")
|
|
|
|
# =========================================================================
|
|
# 3. PlayerController: 切线滑动测试 (40250 AdjustDynamicCollisionMovement)
|
|
# =========================================================================
|
|
var obstacle := Node3D.new()
|
|
get_root().add_child(obstacle)
|
|
# 障碍物放在 (11.0, 0, 10.0),半径 0.55m -> combined radius 1.1m
|
|
obstacle.position = Vector3(11.0, 0.0, 10.0)
|
|
obstacle.set_meta("kind", 0)
|
|
pc.pickables.append(obstacle)
|
|
player.position = Vector3(10.0, 0.0, 10.0)
|
|
|
|
# 阻挡判定:朝向 (10.5, 0.0, 10.0) 会撞击障碍物(距离 0.5m < 1.1m 且更趋近)
|
|
_ck(pc._actor_blocked(Vector3(10.5, 0.0, 10.0)), "Blocked when moving into obstacle body radius")
|
|
_ck(pc._get_blocking_actor(Vector3(10.5, 0.0, 10.0)) == obstacle, "_get_blocking_actor returns obstacle")
|
|
|
|
# =========================================================================
|
|
# 4. NetWorld: 远端实体朝向 45° 阻尼收敛验证
|
|
# =========================================================================
|
|
var nw := NetWorld.new()
|
|
get_root().add_child(nw)
|
|
var remote_node := Node3D.new()
|
|
remote_node.rotation.y = 0.0
|
|
var target_yaw := deg_to_rad(30.0)
|
|
var turn_rate := 1200.0
|
|
var diff: float = wrapf(target_yaw - remote_node.rotation.y, -PI, PI)
|
|
if absf(diff) <= deg_to_rad(45.0):
|
|
turn_rate *= 5.0 / 12.0
|
|
remote_node.rotation.y = rotate_toward(remote_node.rotation.y, target_yaw, deg_to_rad(turn_rate) * 0.01)
|
|
var nw_turned: float = rad_to_deg(remote_node.rotation.y)
|
|
_ck(is_equal_approx(nw_turned, 5.0), "NetWorld rotation damped within 45 deg: turned %.2f, expected 5.0" % nw_turned)
|
|
|
|
# =========================================================================
|
|
# 5. M2Client / EntityStore: 单动作 speed fallback 与 EMA 滤波测试
|
|
# =========================================================================
|
|
if ClassDB.class_exists("M2Client"):
|
|
var client = ClassDB.instantiate("M2Client")
|
|
get_root().add_child(client)
|
|
_ck(client.has_method("set_entity_motion_speed"), "M2Client has set_entity_motion_speed")
|
|
client.call("set_entity_motion_speed", 12345, 0.0, 350.0) # 仅有 run 速度
|
|
client.queue_free()
|
|
|
|
pc.queue_free()
|
|
player.queue_free()
|
|
obstacle.queue_free()
|
|
remote_node.queue_free()
|
|
nw.queue_free()
|