Files
mtgodot-poc/project/test_ground_pickup_parity.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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()