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

571 lines
22 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() -> CPythonItem::GetCloseItemDISTANCE_APPROX <= 300cm
#
# M2Client.ground_item_added(Dict{vid,vnum,pos,owner}) / ground_item_removed(vid) 驱动。
extends Node
signal cannot_pick_item(owner_name: String)
const PICKUP_RANGE := 3.0
# CPythonItem::GetCloseItem 的最终硬编码边界;参考端虽然从 Python 传入 300/500
# 但 GetCloseItem 实现实际固定用 300UserInterface/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 每物一个
# 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 _world_getter: Callable
var _world: Node
var _by_vid := {} # vid -> Node3D
var _next_tcp_time_ms := 0
func get_pickable_distance() -> float:
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p and bool(p.get_meta("mounted", false)):
return 5.0
return PICKUP_RANGE
func _get_main_player_name() -> String:
if client == null:
return ""
if client.has_method("get_main_vid") and client.has_method("get_entity"):
var vid := int(client.get_main_vid())
if vid > 0:
var e: Dictionary = client.get_entity(vid)
var n := String(e.get("name", ""))
if n != "":
return n
if client.has_method("get_character_name"):
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/yGodot 的世界平面对应 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)
func _is_rare_drop(vnum: int, d: Dictionary) -> bool:
if vnum == 1:
return false
if vnum in [25040, 50513, 70024, 70031]:
return true
if proto and proto.has_method("item"):
var pd: Dictionary = proto.item(vnum)
var itype := int(pd.get("type", 0))
if itype in [1, 2] and (vnum % 10) >= 7:
return true
if int(d.get("rarity", 0)) >= 2 or int(d.get("grade", 0)) >= 3:
return true
return false
var assets_override := ""
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 := "",
world_getter: Callable = Callable()) -> 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
_world_getter = world_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 _to_world_pos(raw_pos: Variant) -> Vector3:
if not (raw_pos is Vector3):
return Vector3.ZERO
var pos: Vector3 = MapCoord.to_world(raw_pos)
var w: Node = _world_getter.call() if _world_getter.is_valid() else _world
if w and w.has_method("sample_height"):
pos.y = float(w.call("sample_height", pos.x, pos.z)) + 0.1
return pos
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)
var pos := _to_world_pos(d.get("pos", Vector3.ZERO))
if _by_vid.has(vid):
var existing: Node3D = _by_vid[vid]
if is_instance_valid(existing):
existing.position = pos
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)
existing.set_meta("item_anti_flags", _item_anti_flags(vnum, d))
_apply_owner_tag(existing, owner, item_color)
return
var node := Node3D.new()
node.name = "drop_%d" % vid
node.position = pos
var root := "" if assets_override == "__none__" else (assets_override if assets_override != "" else (AssetRoot.path() if AssetRoot.available() else ""))
var model_path := ""
if root != "":
if vnum == 1:
for p in ["item/ymir work/item/etc/money.gr2", "item/etc/money.gr2"]:
var fp := root.path_join(p)
if FileAccess.file_exists(fp):
model_path = fp
break
else:
for p in ["item/ymir work/item/etc/item_bag.gr2", "item/etc/item_bag.gr2"]:
var fp := root.path_join(p)
if FileAccess.file_exists(fp):
model_path = fp
break
if model_path != "" and ClassDB.class_exists("Metin2Model"):
var model: Node3D = ClassDB.instantiate("Metin2Model")
model.name = "model"
model.set("flip_winding", true)
model.set("texture_dir", model_path.get_base_dir())
model.set("gr2_path", model_path)
node.add_child(model)
else:
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(0.25, 0.25, 0.25)
mesh.mesh = box
mesh.position.y = 0.15
var mat := StandardMaterial3D.new()
if vnum == 1:
mat.albedo_color = Color(1.0, 0.85, 0.2)
mat.metallic = 0.8
mat.roughness = 0.2
mat.emission_enabled = true
mat.emission = Color(0.5, 0.4, 0.1)
else:
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)
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()
beam.name = "light_beam"
var cyl := CylinderMesh.new()
cyl.top_radius = 0.08
cyl.bottom_radius = 0.12
cyl.height = 3.5
beam.mesh = cyl
beam.position.y = 1.75
var bmat := StandardMaterial3D.new()
bmat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
bmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
bmat.albedo_color = Color(1.0, 0.85, 0.2, 0.45)
bmat.emission_enabled = true
bmat.emission = Color(1.0, 0.85, 0.3)
beam.material_override = bmat
node.add_child(beam)
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 := _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()
# 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
# 移动端情境按钮用它判断附近是否有可拾取物,避免常驻占用 HUD。
func has_nearby_item() -> bool:
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
if p == null:
return false
var p_pos: Vector3 = p.global_position if p.is_inside_tree() else p.position
for node in _by_vid.values():
if not is_instance_valid(node):
continue
var n_pos: Vector3 = node.global_position if node.is_inside_tree() else node.position
if _distance_approx_cm(p_pos, n_pos) <= PICKUP_DISTANCE_CM:
return true
return false
# 40250 1:1 PickCloseItem + SendClickItemPacket500ms 节流、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:
return 0
var now_ms := Time.get_ticks_msec()
if now_ms < _next_tcp_time_ms:
return 0
var p_pos: Vector3 = p.global_position if p.is_inside_tree() else p.position
var best_vid := 0
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 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_node = n
if best_vid == 0:
return 0
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
_next_tcp_time_ms = now_ms + 500
if client.has_method("pickup_item"):
client.pickup_item(best_vid)
return best_vid
func try_pickup_vid(vid: int) -> bool:
if client == null or not _by_vid.has(vid):
return false
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", ""))
if not _can_pick_owned_item(best_owner, n):
_on_cannot_pick_item(best_owner)
return false
_next_tcp_time_ms = now_ms + 500
if client.has_method("pickup_item"):
client.pickup_item(vid)
return true
# 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:
return pick_at(camera, screen_pos) != 0
# Pick ground item at mouse screen position (40250 __Pick + __OnClickItem + CPythonTextTail::Pick).
# 同时支持点选地面模型、头顶名称标签(tag)与所有者标签(owner_tag)。
func pick_at(camera: Camera3D, screen_pos: Vector2) -> int:
if camera == null:
return 0
var best_d := 64.0
var best_vid := 0
for vid in _by_vid:
var node: Node3D = _by_vid[vid]
if not is_instance_valid(node):
continue
if camera.is_position_behind(node.global_position):
continue
var test_pts: Array[Vector3] = [
node.global_position + Vector3(0, 0.35, 0)
]
var tag: Node = node.get_node_or_null("tag")
if tag is Node3D and is_instance_valid(tag):
test_pts.append(tag.global_position)
var otag: Node = node.get_node_or_null("owner_tag")
if otag is Node3D and is_instance_valid(otag):
test_pts.append(otag.global_position)
for tp in test_pts:
if camera.is_position_behind(tp):
continue
var p := camera.unproject_position(tp)
var d := p.distance_to(screen_pos)
if d < best_d:
best_d = d
best_vid = int(vid)
return best_vid
func get_item_world_pos(vid: int) -> Variant:
var node: Node3D = _by_vid.get(vid, null)
if node and is_instance_valid(node):
return node.global_position if node.is_inside_tree() else node.position
return null
func _name_for(vnum: int) -> String:
if vnum == 1:
return "Yang"
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