# 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() -> 捡最近的一个(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 strOwnership(IDS_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-207:owner 存在时 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-745:owner 非空 → 一个独立的 CENTER 黄字 (1,1,0) text instance, # 文本 = owner + strOwnership(:722/:723);owner 为空 → 删该实例(:741-745)。 # poc:drop 节点下第二个 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