- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
171 lines
5.8 KiB
GDScript
171 lines
5.8 KiB
GDScript
# test_ground_pickup_parity.gd —— 40250 地面拾取与掉落物 1:1 纯净无头自检套件
|
|
# 验证:
|
|
# 1. 3.0m 拾取有效距离判定
|
|
# 2. 骑马状态下 5.0m 拾取距离拓展
|
|
# 3. 就近优先拾取 (按直线距离排序)
|
|
# 4. 500ms 发包冷却节流 (s_dwNextTCPTime)
|
|
# 5. 所有权校验 (c_szOwnerName != GetName() 拦截 + cannot_pick_item 信号)
|
|
# 6. 稀有道具与高强装备垂直光柱 (Drop Light Beam)
|
|
# 7. ArrangeTextTail 防重叠标签排布
|
|
extends SceneTree
|
|
|
|
const GroundItems = preload("res://ui/ground_items.gd")
|
|
|
|
class FakeClient extends Node:
|
|
signal ground_item_added(d: Dictionary)
|
|
signal ground_item_removed(vid: int)
|
|
|
|
var calls: Array = []
|
|
var main_vid := 1001
|
|
var char_name := "HeroPlayer"
|
|
|
|
func is_in_game() -> bool: return true
|
|
func get_main_vid() -> int: return main_vid
|
|
func get_entity(vid: int) -> Dictionary:
|
|
if vid == main_vid:
|
|
return {"name": char_name}
|
|
return {}
|
|
func pickup_item(vid: int) -> bool:
|
|
calls.append(["pickup", vid])
|
|
return true
|
|
|
|
class FakeProto extends Node:
|
|
func item(vnum: int) -> Dictionary:
|
|
if vnum >= 10 and vnum <= 19:
|
|
return {"name": "Sword+%d" % (vnum - 10), "type": 1} # 武器
|
|
if vnum == 25040:
|
|
return {"name": "Blessing Scroll", "type": 3}
|
|
return {"name": "Item %d" % vnum, "type": 0}
|
|
|
|
var _fail := 0
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
var _to_free: Array[Node] = []
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
for n in _to_free:
|
|
if is_instance_valid(n):
|
|
n.queue_free()
|
|
await process_frame
|
|
await process_frame
|
|
if _fail == 0:
|
|
print("PASS: test_ground_pickup_parity (40250 GroundItems 1:1 parity)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed in test_ground_pickup_parity" % _fail)
|
|
quit(1)
|
|
|
|
func _run() -> void:
|
|
var mount := Node3D.new()
|
|
get_root().add_child(mount)
|
|
var fc := FakeClient.new()
|
|
get_root().add_child(fc)
|
|
var proto := FakeProto.new()
|
|
get_root().add_child(proto)
|
|
|
|
var player_node := Node3D.new()
|
|
player_node.position = Vector3(0.0, 0.0, 0.0)
|
|
get_root().add_child(player_node)
|
|
|
|
var gi := GroundItems.new()
|
|
get_root().add_child(gi)
|
|
_to_free = [gi, player_node, proto, fc, mount]
|
|
gi.assets_override = "__none__"
|
|
gi.setup(fc, mount, func(): return player_node, proto)
|
|
|
|
var cannot_picks: Array[String] = []
|
|
gi.cannot_pick_item.connect(func(owner: String): cannot_picks.append(owner))
|
|
|
|
# --- 1. 距离与最近优先判定 ---
|
|
# 在地面添加 3 个物品:
|
|
# vid 1: 距离 2.0m (在 3m 内)
|
|
# vid 2: 距离 1.0m (最近)
|
|
# vid 3: 距离 4.5m (超出 3m)
|
|
gi._on_added({"vid": 1, "vnum": 10, "pos": Vector3(0.0, 0.0, 2.0), "owner": ""})
|
|
gi._on_added({"vid": 2, "vnum": 11, "pos": Vector3(1.0, 0.0, 0.0), "owner": ""})
|
|
gi._on_added({"vid": 3, "vnum": 12, "pos": Vector3(0.0, 0.0, 4.5), "owner": ""})
|
|
|
|
# 尝试拾取:必须拾取最近的 vid 2
|
|
var picked := gi.try_pickup()
|
|
_ck(picked == 2, "1. 优先拾取最近物品 (vid 2, 距离 1.0m)")
|
|
_ck(fc.calls == [["pickup", 2]], "2. 发出 pickup_item(2)")
|
|
fc.calls.clear()
|
|
|
|
# --- 2. 500ms 节流拦截 ---
|
|
# 立即再次拾取 (间隔 < 500ms)
|
|
var throttled := gi.try_pickup()
|
|
_ck(throttled == 0, "3. 500ms 内连续拾取被节流拦截")
|
|
_ck(fc.calls.is_empty(), "4. 未重复发送网络包")
|
|
|
|
# 服务端移除已被拾取的 vid 2
|
|
gi._on_removed(2)
|
|
|
|
# 手动快进冷却定时器
|
|
gi._next_tcp_time_ms = 0
|
|
picked = gi.try_pickup()
|
|
_ck(picked == 1, "5. 500ms 冷却后拾取次近物品 (vid 1, 距离 2.0m)")
|
|
_ck(fc.calls == [["pickup", 1]], "6. 发出 pickup_item(1)")
|
|
fc.calls.clear()
|
|
|
|
# 清理已拾取的 vid 1
|
|
gi._on_removed(1)
|
|
|
|
# --- 3. 步行 3m 边界与骑马 5m 判定 ---
|
|
# 此时场上只剩 vid 3 (距离 4.5m)
|
|
gi._next_tcp_time_ms = 0
|
|
picked = gi.try_pickup()
|
|
_ck(picked == 0, "7. 步行时 4.5m 超出 3.0m 拾取半径")
|
|
_ck(fc.calls.is_empty(), "8. 未发包")
|
|
|
|
# 主角上马
|
|
player_node.set_meta("mounted", true)
|
|
_ck(gi.get_pickable_distance() == 5.0, "9. 骑马时拾取半径扩展为 5.0m")
|
|
gi._next_tcp_time_ms = 0
|
|
picked = gi.try_pickup()
|
|
_ck(picked == 3, "10. 骑马状态成功拾取 4.5m 外物品 (vid 3)")
|
|
_ck(fc.calls == [["pickup", 3]], "11. 发出 pickup_item(3)")
|
|
fc.calls.clear()
|
|
gi._on_removed(3)
|
|
player_node.set_meta("mounted", false)
|
|
|
|
# --- 4. 所有权保护拦截 (40250 c_szOwnerName != GetName()) ---
|
|
# 添加属于他人的物品 (owner = EnemyPlayer)
|
|
gi._on_added({"vid": 4, "vnum": 10, "pos": Vector3(0.5, 0.0, 0.5), "owner": "EnemyPlayer"})
|
|
gi._next_tcp_time_ms = 0
|
|
picked = gi.try_pickup()
|
|
_ck(picked == 0, "12. 非本人所有权物品被拦截")
|
|
_ck(fc.calls.is_empty(), "13. 拦截时不发包")
|
|
_ck(cannot_picks == ["EnemyPlayer"], "14. 触发 cannot_pick_item 信号")
|
|
cannot_picks.clear()
|
|
|
|
# 添加属于自己的物品 (owner = HeroPlayer)
|
|
gi._on_added({"vid": 5, "vnum": 10, "pos": Vector3(0.2, 0.0, 0.2), "owner": "HeroPlayer"})
|
|
gi._next_tcp_time_ms = 0
|
|
picked = gi.try_pickup()
|
|
_ck(picked == 5, "15. 属于本人的保护物品成功拾取 (vid 5)")
|
|
_ck(fc.calls == [["pickup", 5]], "16. 正常发包")
|
|
fc.calls.clear()
|
|
gi._on_removed(4)
|
|
gi._on_removed(5)
|
|
|
|
# --- 5. 稀有掉落物光柱 (Drop Light Beam) ---
|
|
# 普通物品 (vnum 10 = Sword+0)
|
|
gi._on_added({"vid": 6, "vnum": 10, "pos": Vector3(1.0, 0.0, 1.0), "owner": ""})
|
|
var n6: Node3D = gi._by_vid[6]
|
|
_ck(n6.get_node_or_null("light_beam") == null, "17. 普通装备无光柱")
|
|
|
|
# 高强装备 (vnum 19 = Sword+9)
|
|
gi._on_added({"vid": 7, "vnum": 19, "pos": Vector3(2.0, 0.0, 1.0), "owner": ""})
|
|
var n7: Node3D = gi._by_vid[7]
|
|
_ck(n7.get_node_or_null("light_beam") != null, "18. +9 稀有装备生成 light_beam 冲天光柱")
|
|
|
|
# 祝福卷轴 (vnum 25040)
|
|
gi._on_added({"vid": 8, "vnum": 25040, "pos": Vector3(1.5, 0.0, 1.5), "owner": ""})
|
|
var n8: Node3D = gi._by_vid[8]
|
|
_ck(n8.get_node_or_null("light_beam") != null, "19. 祝福卷轴生成 light_beam 光柱")
|
|
gi.clear_for_map_change()
|