fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
+76 -17
View File
@@ -3,7 +3,7 @@
# var gi := preload("res://ui/ground_items.gd").new()
# add_child(gi)
# gi.setup(m2client, mount_node, player_getter, proto, item_list)
# # 按拾取键时: gi.try_pickup() -> 捡最近的一个(<PICKUP_RANGE m)
# # 按拾取键时: gi.try_pickup() -> CPythonItem::GetCloseItem(DISTANCE_APPROX <= 300cm)
#
# M2Client.ground_item_added(Dict{vid,vnum,pos,owner}) / ground_item_removed(vid) 驱动。
extends Node
@@ -11,6 +11,13 @@ extends Node
signal cannot_pick_item(owner_name: String)
const PICKUP_RANGE := 3.0
# CPythonItem::GetCloseItem 的最终硬编码边界;参考端虽然从 Python 传入 300/500,
# 但 GetCloseItem 实现实际固定用 300(UserInterface/PythonItem.cpp:650)。
const PICKUP_DISTANCE_CM := 300
# CPythonPlayer::__OnPressItem -> CInstanceBase::NEW_IsClickableDistanceDestPixelPosition。
const CLICK_PICKUP_DISTANCE_CM := 150
const ITEM_ANTIFLAG_DROP := 1 << 7
const ITEM_ANTIFLAG_GIVE := 1 << 13
# §8.8 seam ⑩(CPythonTextTail::ArrangeTextTail, PythonTextTail.cpp:153-190):多条掉落
# 名条投影到屏幕后互相挤开防重叠。参考端在屏幕像素空间跑(y 向下),poc 每物一个
@@ -76,6 +83,50 @@ func _get_main_player_name() -> String:
return String(client.get_character_name())
return ""
func _is_party_member_by_name(name: String) -> bool:
if name == "" or client == null or not client.has_method("get_party"):
return false
var party: Variant = client.get_party()
if not (party is Array):
return false
for member in party:
if member is Dictionary and String(member.get("name", "")) == name:
return true
return false
func _item_anti_flags(vnum: int, d: Dictionary) -> int:
if d.has("anti_flags"):
return int(d.get("anti_flags", 0))
if proto and proto.has_method("item"):
var pd: Dictionary = proto.item(vnum)
return int(pd.get("anti_flags", 0))
return 0
# CPythonPlayer::SendClickItemPacket:自己的物品直接允许;其他所有者只有在
# 属于本队且物品没有 ITEM_ANTIFLAG_DROP / ITEM_ANTIFLAG_GIVE 时允许。
func _can_pick_owned_item(owner: String, node: Node3D) -> bool:
if owner == "":
return true
var my_name := _get_main_player_name()
if owner == my_name:
return true
if not _is_party_member_by_name(owner):
return false
var anti_flags := int(node.get_meta("item_anti_flags", 0))
return (anti_flags & (ITEM_ANTIFLAG_DROP | ITEM_ANTIFLAG_GIVE)) == 0
func _distance_approx_cm(a: Vector3, b: Vector3) -> int:
# 40250 DISTANCE_APPROX 使用地图平面 x/y;Godot 的世界平面对应 x/z。
var dx := int(roundf(absf((a.x - b.x) * 100.0)))
var dy := int(roundf(absf((a.z - b.z) * 100.0)))
var min_d: int = dx if dx < dy else dy
var max_d: int = dy if dx < dy else dx
return (((max_d << 8) + (max_d << 3) - (max_d << 4) - (max_d << 1) +
(min_d << 7) - (min_d << 5) + (min_d << 3) - (min_d << 1)) >> 8)
func _node_world_position(node: Node3D) -> Vector3:
return node.global_position if node.is_inside_tree() else node.position
func _on_cannot_pick_item(owner: String) -> void:
cannot_pick_item.emit(owner)
print("[拾取] 这不是你的物品,无法拾取!(所有者: %s)" % owner)
@@ -145,6 +196,7 @@ func _on_added(d: Dictionary) -> void:
existing.set_meta("item_name", existing_tag.text)
existing.set_meta("item_owner", owner)
existing.set_meta("item_color", item_color)
existing.set_meta("item_anti_flags", _item_anti_flags(vnum, d))
_apply_owner_tag(existing, owner, item_color)
return
var node := Node3D.new()
@@ -207,6 +259,8 @@ func _on_added(d: Dictionary) -> void:
node.set_meta("item_name", tag.text)
node.set_meta("item_owner", owner)
node.set_meta("item_color", item_color)
node.set_meta("item_vnum", vnum)
node.set_meta("item_anti_flags", _item_anti_flags(vnum, d))
_apply_owner_tag(node, owner, item_color)
if _is_rare_drop(vnum, d):
var beam := MeshInstance3D.new()
@@ -253,7 +307,7 @@ func _process(dt: float) -> void:
if m:
m.rotation.y = t * 2.0
# 近了名条高亮
var near := p.global_position.distance_to(n.global_position) <= PICKUP_RANGE
var near := _distance_approx_cm(p.global_position, n.global_position) <= PICKUP_DISTANCE_CM
(n.get_node("tag") as Label3D).modulate = Color(0.4, 1.0, 0.4) if near else Color(1.0, 0.9, 0.5)
_arrange_tags()
@@ -327,11 +381,12 @@ func has_nearby_item() -> bool:
if not is_instance_valid(node):
continue
var n_pos: Vector3 = node.global_position if node.is_inside_tree() else node.position
if p_pos.distance_to(n_pos) < PICKUP_RANGE:
if _distance_approx_cm(p_pos, n_pos) <= PICKUP_DISTANCE_CM:
return true
return false
# 40250 1:1 PickCloseItem: 500ms 节流 + 距离 (步行 3m / 骑马 5m) + 所有权保护预检
# 40250 1:1 PickCloseItem + SendClickItemPacket:500ms 节流、DISTANCE_APPROX、
# 同距最高 VID 以及队伍 / anti-flag 所有权保护。
func try_pickup() -> int:
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p == null or client == null:
@@ -342,26 +397,26 @@ func try_pickup() -> int:
var p_pos: Vector3 = p.global_position if p.is_inside_tree() else p.position
var best_vid := 0
var pick_dist := get_pickable_distance()
var best_d := pick_dist
var best_owner := ""
for vid in _by_vid:
var best_d := PICKUP_DISTANCE_CM + 1
var best_node: Node3D = null
# std::map<DWORD, ...> 按 VID 升序遍历;使用 <= 后,同距时后遍历的最高 VID 胜出。
var vids: Array = _by_vid.keys()
vids.sort()
for vid in vids:
var n: Node3D = _by_vid[vid]
if not is_instance_valid(n):
continue
var n_pos: Vector3 = n.global_position if n.is_inside_tree() else n.position
var d := p_pos.distance_to(n_pos)
if d < best_d:
var d := _distance_approx_cm(p_pos, _node_world_position(n))
if d <= PICKUP_DISTANCE_CM and d <= best_d:
best_d = d
best_vid = int(vid)
best_owner = String(n.get_meta("item_owner", ""))
best_node = n
if best_vid == 0:
return 0
var my_name := _get_main_player_name()
if best_owner != "" and my_name != "" and best_owner != my_name:
var best_owner := String(best_node.get_meta("item_owner", ""))
if not _can_pick_owned_item(best_owner, best_node):
_on_cannot_pick_item(best_owner)
return 0
@@ -376,14 +431,18 @@ func try_pickup_vid(vid: int) -> bool:
var n: Node3D = _by_vid[vid]
if not is_instance_valid(n):
return false
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p == null:
return false
if _distance_approx_cm(_node_world_position(p), _node_world_position(n)) > CLICK_PICKUP_DISTANCE_CM:
return false
var now_ms := Time.get_ticks_msec()
if now_ms < _next_tcp_time_ms:
return false
var best_owner := String(n.get_meta("item_owner", ""))
var my_name := _get_main_player_name()
if best_owner != "" and my_name != "" and best_owner != my_name:
if not _can_pick_owned_item(best_owner, n):
_on_cannot_pick_item(best_owner)
return false