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