Files
mtgodot-poc/project/test_ground_pickup_parity.gd
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

218 lines
8.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# test_ground_pickup_parity.gd —— 40250 地面拾取与掉落物 1:1 纯净无头自检套件
# 验证:
# 1. 40250 GetCloseItem 的 300cm 近似距离判定
# 2. GetCloseItem 同距时按最高 VID 取胜
# 3. 40250 SendClickItemPacket 的队伍成员 / anti-flag 所有权判定
# 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"
var party: Array = []
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 get_party() -> Array: return party
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. GetCloseItem 的固定 300cm 边界(包括骑马状态) ---
# 此时场上只剩 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. 未发包")
# PythonPlayerInput 的 __GetPickableDistance 虽然骑马返回 500
# 但 CPythonItem::GetCloseItem 实际硬编码 dwCloseItemDistance > 300。
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 == 0, "10. 骑马时仍遵守 GetCloseItem 的 300cm 固定边界")
_ck(fc.calls.is_empty(), "11. 骑马超出 300cm 时不发 pickup_item")
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)
# --- 4b. 40250 队伍成员所有权:无 DROP/GIVE anti-flag 时允许拾取 ---
fc.party = [{"name": "PartyMate"}]
gi._on_added({"vid": 9, "vnum": 10, "pos": Vector3(0.2, 0.0, 0.2), "owner": "PartyMate"})
gi._next_tcp_time_ms = 0
picked = gi.try_pickup()
_ck(picked == 9, "15a. 队伍成员且无 DROP/GIVE anti-flag 的物品允许拾取")
_ck(fc.calls == [["pickup", 9]], "15b. 队伍成员物品正常发包")
fc.calls.clear()
gi._on_removed(9)
# 队伍成员如果设置 DROP anti-flag,参考端仍然拒绝。
gi._on_added({"vid": 10, "vnum": 20, "pos": Vector3(0.2, 0.0, 0.2), "owner": "PartyMate", "anti_flags": 1 << 7})
gi._next_tcp_time_ms = 0
picked = gi.try_pickup()
_ck(picked == 0, "15c. 队伍成员带 DROP anti-flag 时拒绝拾取")
_ck(fc.calls.is_empty(), "15d. anti-flag 拦截时不发包")
gi._on_removed(10)
fc.party.clear()
# --- 4c. GetCloseItem:近似距离允许欧氏距离略超 3m 的对角点 ---
gi._on_added({"vid": 11, "vnum": 10, "pos": Vector3(2.19, 0.0, 2.19), "owner": "HeroPlayer"})
gi._next_tcp_time_ms = 0
picked = gi.try_pickup()
_ck(picked == 11, "15e. 40250 DISTANCE_APPROX <= 300cm 的对角物品可拾取")
fc.calls.clear()
gi._on_removed(11)
# --- 4d. GetCloseItem:相同近似距离时最终选择最高 VID ---
gi._on_added({"vid": 20, "vnum": 10, "pos": Vector3(1.0, 0.0, 0.0), "owner": "HeroPlayer"})
gi._on_added({"vid": 21, "vnum": 10, "pos": Vector3(-1.0, 0.0, 0.0), "owner": "HeroPlayer"})
gi._next_tcp_time_ms = 0
picked = gi.try_pickup()
_ck(picked == 21, "15f. 相同距离按最高 VID 选择")
fc.calls.clear()
gi._on_removed(20)
gi._on_removed(21)
# --- 4e. 点击拾取链路使用 __OnPressItem 的 150cm 阈值 ---
gi._on_added({"vid": 22, "vnum": 10, "pos": Vector3(2.0, 0.0, 0.0), "owner": "HeroPlayer"})
gi._next_tcp_time_ms = 0
_ck(not gi.try_pickup_vid(22), "15g. 直接点击入口超过 150cm 时不应直接发拾取包")
_ck(fc.calls.is_empty(), "15h. 点击超出 150cm 时不发包")
gi._on_removed(22)
# --- 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()