Files
mtgodot-poc/project/ui/ground_items.gd
T

320 lines
13 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.
# GroundItems (P2) —— 世界里的掉落物:GC_ITEM_GROUND_ADD/DEL → 小物体 + 名条 + 拾取。
#
# 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
#
# M2Client.ground_item_added(Dict{vid,vnum,pos,owner}) / ground_item_removed(vid) 驱动。
extends Node
const PICKUP_RANGE := 3.0
# §8.8 seam ⑩(CPythonTextTail::ArrangeTextTail, PythonTextTail.cpp:153-190):多条掉落
# 名条投影到屏幕后互相挤开防重叠。参考端在屏幕像素空间跑(y 向下),poc 每物一个
# billboard Label3D —— 把名条世界锚点投影到屏幕、按 Label3D 的真实字体度量取文本框,跑
# text_tail_arrange.arrange_item_tails 得到每条被下推的屏幕 y,再按 Label3D 的
# pixel_size 折回世界 y(屏幕向下 Δpx → 世界向下 Δpx*pixel_size)。
# 文本框宽 / 高取名条 Label3D 的真实字体度量(TextMetrics.label_width / label_height
# = GetTextSize 的 w / h1:1,增量 123 / 124TAG_CHAR_W_PX / TAG_LINE_H_PX 仅作无字体兜底)。
# 增量 125SetItemTextTailOwner, PythonTextTail.cpp:706-737):有 owner 的掉落,参考端
# owner 是与名字分开的 pOwnerTextInstance,单独 GetTextSize,令去重叠框 yEnd += ownerH + 4、
# x 两边按 owner 半宽 ±1 外扩 —— 去重叠框把「名字」「owner + 所有格」两子串分别用 tag 的
# 字体度量后走 TextTailArrange.item_box_with_owner 合成(框度量 1:1)。
# 增量 126SetItemTextTailOwner :712-745 + ArrangeTextTail :191-207):owner 后缀不再拼进
# 名字串,而是 drop 节点下第二个 Label3D "owner_tag" —— CENTER 对齐、黄字 (1,1,0)、文本
# = owner + strOwnership:722,本地化语素为空时字面 "'s",经 setup(possessive) 注入);
# owner 落在尾标基点、名字实例下移 15px:200 pTextInstance->SetPosition(x, y + 15.0f, z));
# owner 为空时删 owner_tag:741-745)。名字色仍走 _process 远近着色(参考名字实例取
# pInsertTextTail->Color 的道具品阶色)。
const TextTailArrange = preload("res://text_tail_arrange.gd")
const TextMetrics = preload("res://text_metrics.gd")
const TAG_BASE_Y := 0.75 # 名条相对掉落物的默认局部 y(未重叠时)
const TAG_CHAR_W_PX := 7.0 # 无字体时的每字符估宽兜底(有字体走 GetTextSize 的 w
const TAG_LINE_H_PX := 15.0 # 无字体时的单行估高兜底(有字体走 GetTextSize 的 h
const OWNER_POSSESSIVE := "'s" # SetItemTextTailOwner :722 strOwnershipIDS_POSSESSIVE_MORPHENE 为空时的字面值)
const OWNER_NAME_GAP_PX := 15.0 # ArrangeTextTail :200 pTextInstance->SetPosition(x, y + 15.0f, z)owner 行下方的名字行偏移
const DEFAULT_ITEM_COLOR := Color(1.0, 0.9, 0.5)
const ITEM_GRADE_COLORS := {
0: Color(1.0, 1.0, 1.0),
1: Color(0.55, 0.85, 1.0),
2: Color(1.0, 0.85, 0.25),
3: Color(0.85, 0.45, 1.0),
4: Color(1.0, 0.35, 0.35),
}
var client: Node
var mount: Node3D
var proto: Node
var item_list: RefCounted
var _player_getter: Callable
var _camera_getter: Callable
var _by_vid := {} # vid -> Node3D
var _possessive := OWNER_POSSESSIVE # setup(possessive) 注入;空 -> 字面 "'s"SetItemTextTailOwner :722
func setup(m2client: Node, mount_node: Node3D, player_getter: Callable,
proto_node: Node = null, il: RefCounted = null,
camera_getter: Callable = Callable(), possessive := "") -> void:
client = m2client
_possessive = possessive if possessive != "" else OWNER_POSSESSIVE # :722 == "" ? "'s" : loc
mount = mount_node
proto = proto_node
item_list = il
_player_getter = player_getter
_camera_getter = camera_getter
if client.has_signal("ground_item_added"):
client.ground_item_added.connect(_on_added)
if client.has_signal("ground_item_removed"):
client.ground_item_removed.connect(_on_removed)
# 已在场的(重连)
if client.has_method("get_ground_items"):
for d in client.get_ground_items():
_on_added(d)
func _on_added(d: Dictionary) -> void:
var vid := int(d.get("vid", 0))
if vid == 0:
return
var vnum := int(d.get("vnum", 0))
var owner := String(d.get("owner", ""))
var item_color := _item_color(vnum, d)
if _by_vid.has(vid):
var existing: Node3D = _by_vid[vid]
if is_instance_valid(existing):
var existing_tag := existing.get_node("tag") as Label3D
existing_tag.text = _name_for(vnum)
existing_tag.modulate = item_color
existing.set_meta("item_name", existing_tag.text)
existing.set_meta("item_owner", owner)
existing.set_meta("item_color", item_color)
_apply_owner_tag(existing, owner, item_color)
return
var node := Node3D.new()
node.name = "drop_%d" % vid
node.position = d.get("pos", Vector3.ZERO)
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(0.25, 0.25, 0.25)
mesh.mesh = box
mesh.position.y = 0.3
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.85, 0.3)
mat.emission_enabled = true
mat.emission = Color(0.6, 0.5, 0.1)
mesh.material_override = mat
node.add_child(mesh)
var tag := Label3D.new()
tag.name = "tag"
tag.text = _name_for(vnum) # 名字实例只放名字,owner 走独立 owner_tag:723 不再拼串)
tag.position.y = TAG_BASE_Y
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
tag.no_depth_test = true
tag.pixel_size = 0.005
tag.modulate = item_color
node.add_child(tag)
node.set_meta("vid", vid)
node.set_meta("item_name", tag.text)
node.set_meta("item_owner", owner)
node.set_meta("item_color", item_color)
_apply_owner_tag(node, owner, item_color)
mount.add_child(node)
_by_vid[vid] = node
func _on_removed(vid: int) -> void:
var n: Node3D = _by_vid.get(vid, null)
if n:
_by_vid.erase(vid)
n.queue_free()
func clear_for_map_change() -> void:
for vid in _by_vid.keys():
var node: Node3D = _by_vid[vid]
if is_instance_valid(node):
node.queue_free()
_by_vid.clear()
func _process(dt: float) -> void:
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p == null:
return
var t := Time.get_ticks_msec() / 1000.0
for vid in _by_vid:
var n: Node3D = _by_vid[vid]
if is_instance_valid(n):
var m := n.get_child(0) as MeshInstance3D
if m:
m.rotation.y = t * 2.0
# 近了名条高亮
var near := p.global_position.distance_to(n.global_position) <= PICKUP_RANGE
(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()
# ArrangeTextTail :153-190 的 item 块:把所有掉落名条投影到屏幕,跑 isIn 去重叠,
# 再把被下推的屏幕 Δy 折回各 Label3D 的世界局部 y。无相机 / 少于 2 条时全部回默认。
func _arrange_tags() -> void:
var cam: Camera3D = _camera_getter.call() if _camera_getter.is_valid() else null
if cam == null:
return
# m_ItemTextTailList 顺序:按 vid 排序保证稳定(≈ 注册先后)。
var vids: Array = _by_vid.keys()
vids.sort()
var tags: Array = [] # 与 boxes 平行:{tag, base_screen_y}
var boxes: Array = []
for vid in vids:
var n: Node3D = _by_vid[vid]
if not is_instance_valid(n):
continue
var tag := n.get_node_or_null("tag") as Label3D
if tag == null:
continue
var anchor := n.global_position + Vector3(0, TAG_BASE_Y, 0)
if cam.is_position_behind(anchor):
_place_tail(n, tag, 0.0)
continue
var sp := cam.unproject_position(anchor)
tags.append({"n": n, "tag": tag, "y0": sp.y})
# 名字子串 + owner 子串分开度量(SetItemTextTailOwner 的 pOwnerTextInstance),
# 再走 item_box_with_owner 合成去重叠框(yEnd += ownerH + 4)。
var iname := String(n.get_meta("item_name", tag.text))
var iowner := String(n.get_meta("item_owner", ""))
var owner_txt := (iowner + _possessive) if iowner != "" else ""
var nbox: Dictionary = TextTailArrange.item_box_with_owner(
TextMetrics.string_width_like(tag, iname, TAG_CHAR_W_PX),
TextMetrics.string_height_like(tag, iname, TAG_LINE_H_PX),
TextMetrics.string_width_like(tag, owner_txt, TAG_CHAR_W_PX),
TextMetrics.string_height_like(tag, owner_txt, TAG_LINE_H_PX))
boxes.append({
"x": sp.x, "y": sp.y,
"w": maxf(1.0, nbox["w"]),
"h": maxf(1.0, nbox["h"]),
})
if boxes.size() < 2:
if boxes.size() == 1:
_place_tail(tags[0]["n"], tags[0]["tag"], 0.0)
return
var arranged: Array = TextTailArrange.arrange_item_tails(boxes)
for i in tags.size():
var d_px: float = arranged[i] - float(tags[i]["y0"]) # 屏幕向下为正
_place_tail(tags[i]["n"], tags[i]["tag"], d_px)
# ArrangeTextTail :191-207owner 存在时 owner 实例落在尾标基点 (x, y)、名字实例下移 15px
# :200 pTextInstance->SetPosition(x, y + 15.0f, z));无 owner 时名字实例落在基点 (:206)。
# d_px = 去重叠把该尾标下推的屏幕像素(0 = 未被推)。屏幕向下 → 世界向下(* pixel_size)。
func _place_tail(n: Node3D, tag: Label3D, d_px: float) -> void:
var px: float = tag.pixel_size
var owner_tag := n.get_node_or_null("owner_tag") as Label3D
if owner_tag != null:
owner_tag.position.y = TAG_BASE_Y - d_px * px
tag.position.y = TAG_BASE_Y - (d_px + OWNER_NAME_GAP_PX) * px
else:
tag.position.y = TAG_BASE_Y - d_px * px
# 捡最近的一个(范围内)。返回捡的 vid,0 = 没有。
func try_pickup() -> int:
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p == null or client == null:
return 0
var best_vid := 0
var best_d := PICKUP_RANGE
for vid in _by_vid:
var n: Node3D = _by_vid[vid]
if not is_instance_valid(n):
continue
var d := p.global_position.distance_to(n.global_position)
if d < best_d:
best_d = d
best_vid = int(vid)
if best_vid != 0:
client.pickup_item(best_vid)
return best_vid
# Mouse hover probe used by PlayerController's cursor state. Ground drops are
# deliberately tested in screen space, so clicking a visible label / mesh
# selects PICK even when the item is not the nearest drop in world distance.
func hover_at(camera: Camera3D, screen_pos: Vector2) -> bool:
if camera == null:
return false
var best := INF
for node in _by_vid.values():
if not is_instance_valid(node):
continue
if camera.is_position_behind(node.global_position):
continue
var p := camera.unproject_position(node.global_position + Vector3(0, 0.35, 0))
var d := p.distance_to(screen_pos)
if d <= 28.0:
best = minf(best, d)
return best < INF
func _name_for(vnum: int) -> String:
if proto:
var pd: Dictionary = proto.item(vnum)
if not pd.is_empty():
return String(pd.get("locale_name", pd.get("name", "item %d" % vnum)))
if item_list and item_list.has(vnum):
return item_list.type_of(vnum)
return "item %d" % vnum
# SetItemTextTailOwner :712-745owner 非空 → 一个独立的 CENTER 黄字 (1,1,0) text instance
# 文本 = owner + strOwnership:722/:723);owner 为空 → 删该实例(:741-745)。
# pocdrop 节点下第二个 Label3D "owner_tag"(渲染顺序在名字之后,:373)。
func _apply_owner_tag(node: Node3D, owner: String, item_color := DEFAULT_ITEM_COLOR) -> void:
var owner_tag := node.get_node_or_null("owner_tag") as Label3D
if owner.is_empty():
if owner_tag != null:
node.remove_child(owner_tag)
owner_tag.queue_free()
return
if owner_tag == null:
owner_tag = Label3D.new()
owner_tag.name = "owner_tag"
owner_tag.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER # :727 HORIZONTAL_ALIGN_CENTER
owner_tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
owner_tag.no_depth_test = true
owner_tag.pixel_size = 0.005
owner_tag.position.y = TAG_BASE_Y
node.add_child(owner_tag)
owner_tag.modulate = item_color # :729 的 pInsertTextTail->Color
owner_tag.text = owner + _possessive
func _item_color(vnum: int, drop: Dictionary) -> Color:
# GC_ITEM_GROUND_ADD 只带 vnum/owner/pos;优先消费服务端或 fixture 显式携带
# 的颜色,其次消费 item proto 的扩展字段。老数据没有品阶字段时保留旧的
# 金色 fallback,而不会把 owner 行错误固定成纯黄。
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
for source in [drop, pd]:
for key in ["item_color", "rarity_color", "name_color", "color"]:
if source.has(key):
var parsed: Variant = _coerce_color(source[key])
if parsed != null:
return parsed
for key in ["item_grade", "rarity", "quality", "grade"]:
if source.has(key):
var grade := int(source[key])
if ITEM_GRADE_COLORS.has(grade):
return ITEM_GRADE_COLORS[grade]
return DEFAULT_ITEM_COLOR
func _coerce_color(value: Variant) -> Variant:
if value is Color:
return value
if value is Array and value.size() >= 3:
var scale := 255.0 if float(value[0]) > 1.0 or float(value[1]) > 1.0 \
or float(value[2]) > 1.0 else 1.0
var alpha := float(value[3]) if value.size() > 3 else 1.0
if alpha > 1.0:
alpha /= 255.0
return Color(float(value[0]) / scale, float(value[1]) / scale,
float(value[2]) / scale, alpha)
if value is Dictionary and value.has_all(["r", "g", "b"]):
var scale := 255.0 if float(value["r"]) > 1.0 or float(value["g"]) > 1.0 \
or float(value["b"]) > 1.0 else 1.0
var alpha := float(value.get("a", 1.0))
if alpha > 1.0:
alpha /= 255.0
return Color(float(value["r"]) / scale, float(value["g"]) / scale,
float(value["b"]) / scale, alpha)
if value is String and not String(value).strip_edges().is_empty():
return Color.from_string(String(value), DEFAULT_ITEM_COLOR)
return null