304 lines
11 KiB
GDScript
304 lines
11 KiB
GDScript
# ItemTooltipView —— 40250 标准 ThinBoard 悬浮窗控件。
|
|
#
|
|
# 严格遵循 40250 uitooltip.py 规范:
|
|
# 1. 继承 ui.ThinBoard 视觉:九宫格古铜金属边框、半透明黑底、固定初始宽度 190px。
|
|
# 2. 所有文本水平居中(SetHorizontalAlignCenter),带黑色文字描边。
|
|
# 3. 严格的 40250 配色体系:
|
|
# - 普通装备浅金象牙白,蓝字/强化装备亮金色
|
|
# - 攻击力、防御力加算公式及绿色高亮
|
|
# - 固有词条绿色/负值红色,附加属性 1~5 薄荷青绿,6~7 浅薄荷绿
|
|
# - 适用职业([ 穿戴职业 ] 及职业列表,不符合角色职业时警示红)
|
|
# - 等级限制(不满足角色等级时警示红)
|
|
# 4. 魂石凹槽图文解析:银色凹槽贴图 + 32x32 魂石图标 + 魂石名称与第一条属性描述。
|
|
# 5. 浮动跟随鼠标或在格子旁边即时展现(0 延迟)。
|
|
extends MarginContainer
|
|
|
|
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
|
const UiKit = preload("res://ui_kit.gd")
|
|
const UiAssets = preload("res://ui/ui_assets.gd")
|
|
|
|
var _vbox: VBoxContainer
|
|
var _bg: NinePatchRect
|
|
var _assets_root := ""
|
|
var _proto_node: Node = null
|
|
var _item_list: RefCounted = null
|
|
var _tooltip_builder: RefCounted
|
|
|
|
var follow_mouse := true
|
|
|
|
func _init() -> void:
|
|
name = "ItemTooltipView"
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
custom_minimum_size = Vector2(190, 0)
|
|
add_theme_constant_override("margin_left", 8)
|
|
add_theme_constant_override("margin_right", 8)
|
|
add_theme_constant_override("margin_top", 10)
|
|
add_theme_constant_override("margin_bottom", 10)
|
|
|
|
func setup(assets_root: String, proto: Node = null, ilist: RefCounted = null, builder: RefCounted = null) -> void:
|
|
_assets_root = assets_root
|
|
_proto_node = proto
|
|
_item_list = ilist
|
|
_tooltip_builder = builder if builder != null else ItemTooltip.new()
|
|
if builder == null and not _assets_root.is_empty():
|
|
_tooltip_builder.setup(_assets_root, "en", _proto_node)
|
|
_rebuild_background()
|
|
|
|
func _rebuild_background() -> void:
|
|
if _bg != null and is_instance_valid(_bg):
|
|
_bg.queue_free()
|
|
_bg = null
|
|
if not _assets_root.is_empty():
|
|
_bg = UiKit.board(_assets_root, "thinboard")
|
|
if _bg:
|
|
_bg.name = "ThinBoardBg"
|
|
_bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_bg.show_behind_parent = true
|
|
_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(_bg)
|
|
move_child(_bg, 0)
|
|
|
|
func show_item(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {},
|
|
player_ctx: Dictionary = {}) -> void:
|
|
if _tooltip_builder == null:
|
|
_tooltip_builder = ItemTooltip.new()
|
|
if not _assets_root.is_empty():
|
|
_tooltip_builder.setup(_assets_root, "en", _proto_node)
|
|
var model: Dictionary = _tooltip_builder.build_model(vnum, count, proto_data, instance_data, player_ctx)
|
|
populate_from_model(model)
|
|
visible = true
|
|
if is_inside_tree():
|
|
update_position()
|
|
|
|
func hide_tooltip() -> void:
|
|
visible = false
|
|
|
|
func populate_from_model(model: Dictionary) -> void:
|
|
if _vbox == null:
|
|
_vbox = VBoxContainer.new()
|
|
_vbox.name = "ContentVBox"
|
|
_vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_vbox.add_theme_constant_override("separation", 2)
|
|
add_child(_vbox)
|
|
|
|
for c in _vbox.get_children():
|
|
c.queue_free()
|
|
|
|
if model.is_empty():
|
|
return
|
|
|
|
# 1. 标题 (Item Title)
|
|
var title_text: String = model.get("title", "")
|
|
var title_col: Color = model.get("title_color", ItemTooltip.TITLE_COLOR)
|
|
_vbox.add_child(_make_label(title_text, title_col, 12, true))
|
|
|
|
# 2. 数量 (Count > 1)
|
|
var count: int = int(model.get("count", 1))
|
|
if count > 1:
|
|
_vbox.add_child(_make_label("数量:%d" % count, ItemTooltip.NORMAL_COLOR, 11))
|
|
|
|
# 3. 描述 (Description)
|
|
var desc: String = model.get("desc", "")
|
|
if not desc.is_empty():
|
|
_vbox.add_child(_make_label(desc, ItemTooltip.NORMAL_COLOR, 10, false, true))
|
|
|
|
# 4. 概要/条件 (Summary)
|
|
var summary: String = model.get("summary", "")
|
|
if not summary.is_empty():
|
|
_vbox.add_child(_make_label(summary, ItemTooltip.CONDITION_COLOR, 10, false, true))
|
|
|
|
# 5. 限制要求 (Limits)
|
|
var limits: Array = model.get("limits", [])
|
|
if not limits.is_empty():
|
|
_vbox.add_child(_make_spacer(3))
|
|
for lim in limits:
|
|
_vbox.add_child(_make_label(lim.get("text", ""), lim.get("color", ItemTooltip.NORMAL_COLOR), 11))
|
|
|
|
# 6. 基础战斗数值 (Stats: Attack Power, Defense)
|
|
var stats: Array = model.get("stats", [])
|
|
if not stats.is_empty():
|
|
_vbox.add_child(_make_spacer(3))
|
|
for s in stats:
|
|
_vbox.add_child(_make_label(s.get("text", ""), s.get("color", ItemTooltip.POSITIVE_COLOR), 11))
|
|
|
|
# 7. 固有属性 (Proto Applies)
|
|
var applies: Array = model.get("applies", [])
|
|
for a in applies:
|
|
_vbox.add_child(_make_label(a.get("text", ""), a.get("color", ItemTooltip.POSITIVE_COLOR), 11))
|
|
|
|
# 8. 附加属性 (Instance Attrs)
|
|
var attrs: Array = model.get("attrs", [])
|
|
for a in attrs:
|
|
_vbox.add_child(_make_label(a.get("text", ""), a.get("color", ItemTooltip.SPECIAL_POSITIVE_COLOR), 11))
|
|
|
|
# 9. 穿戴职业 (Wearable Info)
|
|
var wearable: Dictionary = model.get("wearable", {})
|
|
if not wearable.is_empty():
|
|
_vbox.add_child(_make_spacer(3))
|
|
var hdr: String = wearable.get("header", "[ 穿戴职业 ]")
|
|
_vbox.add_child(_make_label(hdr, ItemTooltip.NORMAL_COLOR, 11))
|
|
var jobs_text: String = wearable.get("jobs", "")
|
|
if not jobs_text.is_empty():
|
|
var j_col: Color = wearable.get("jobs_color", ItemTooltip.NORMAL_COLOR)
|
|
_vbox.add_child(_make_label(jobs_text, j_col, 11))
|
|
var gender_text: String = wearable.get("gender", "")
|
|
if not gender_text.is_empty():
|
|
var g_col: Color = wearable.get("gender_color", ItemTooltip.NORMAL_COLOR)
|
|
_vbox.add_child(_make_label(gender_text, g_col, 11))
|
|
|
|
# 10. 魂石凹槽 (Sockets)
|
|
var sockets: Array = model.get("sockets", [])
|
|
if not sockets.is_empty():
|
|
_vbox.add_child(_make_spacer(3))
|
|
for sock in sockets:
|
|
_vbox.add_child(_make_socket_row(sock))
|
|
|
|
# 11. 价格 (Price)
|
|
var price: int = int(model.get("price", 0))
|
|
if price > 0:
|
|
_vbox.add_child(_make_spacer(3))
|
|
_vbox.add_child(_make_label("价格:%s 金币" % _format_price(price), ItemTooltip.PRICE_COLOR, 11))
|
|
|
|
# 12. 限时剩余时间 (Realtime)
|
|
var realtime: String = model.get("realtime", "")
|
|
if not realtime.is_empty():
|
|
_vbox.add_child(_make_spacer(2))
|
|
_vbox.add_child(_make_label("剩余时间:%s" % realtime, ItemTooltip.NORMAL_COLOR, 11))
|
|
|
|
func _make_label(text: String, color: Color, font_size := 11, bold := false, autowrap := false) -> Label:
|
|
var lbl := Label.new()
|
|
lbl.text = text
|
|
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
lbl.add_theme_font_size_override("font_size", font_size)
|
|
lbl.add_theme_color_override("font_color", color)
|
|
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.95))
|
|
lbl.add_theme_constant_override("outline_size", 2)
|
|
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
if autowrap:
|
|
lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
lbl.custom_minimum_size.x = 174
|
|
return lbl
|
|
|
|
func _make_spacer(height: int) -> Control:
|
|
var sp := Control.new()
|
|
sp.custom_minimum_size = Vector2(0, height)
|
|
sp.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
return sp
|
|
|
|
func _make_socket_row(sock: Dictionary) -> Control:
|
|
var row := HBoxContainer.new()
|
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
row.add_theme_constant_override("separation", 6)
|
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
|
|
var icon_box := Control.new()
|
|
icon_box.custom_minimum_size = Vector2(36, 36)
|
|
icon_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
var slot_tex: Texture2D = null
|
|
if not _assets_root.is_empty():
|
|
slot_tex = UiAssets.load_tex(_assets_root, "d:/ymir work/ui/game/windows/metin_slot_silver.sub")
|
|
if slot_tex:
|
|
var slot_rect := TextureRect.new()
|
|
slot_rect.texture = slot_tex
|
|
slot_rect.custom_minimum_size = Vector2(36, 36)
|
|
slot_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
icon_box.add_child(slot_rect)
|
|
|
|
var stone_vnum: int = int(sock.get("vnum", 0))
|
|
if stone_vnum > 1:
|
|
var icon_tex := _resolve_item_icon(stone_vnum)
|
|
if icon_tex:
|
|
var icon_rect := TextureRect.new()
|
|
icon_rect.texture = icon_tex
|
|
icon_rect.position = Vector2(2, 2)
|
|
icon_rect.size = Vector2(32, 32)
|
|
icon_rect.custom_minimum_size = Vector2(32, 32)
|
|
icon_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
icon_rect.stretch_mode = TextureRect.STRETCH_SCALE
|
|
icon_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
icon_box.add_child(icon_rect)
|
|
|
|
row.add_child(icon_box)
|
|
|
|
var text_box := VBoxContainer.new()
|
|
text_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
text_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
text_box.add_theme_constant_override("separation", 1)
|
|
|
|
var name_lbl := Label.new()
|
|
name_lbl.text = String(sock.get("name", ""))
|
|
name_lbl.add_theme_font_size_override("font_size", 10)
|
|
name_lbl.add_theme_color_override("font_color", sock.get("name_color", ItemTooltip.NORMAL_COLOR))
|
|
name_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.95))
|
|
name_lbl.add_theme_constant_override("outline_size", 2)
|
|
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
text_box.add_child(name_lbl)
|
|
|
|
var affect_str: String = String(sock.get("affect", ""))
|
|
if not affect_str.is_empty():
|
|
var aff_lbl := Label.new()
|
|
aff_lbl.text = affect_str
|
|
aff_lbl.add_theme_font_size_override("font_size", 10)
|
|
aff_lbl.add_theme_color_override("font_color", sock.get("affect_color", ItemTooltip.POSITIVE_COLOR))
|
|
aff_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.95))
|
|
aff_lbl.add_theme_constant_override("outline_size", 2)
|
|
aff_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
text_box.add_child(aff_lbl)
|
|
|
|
row.add_child(text_box)
|
|
return row
|
|
|
|
func _resolve_item_icon(vnum: int) -> Texture2D:
|
|
if _item_list and _item_list.has_method("icon"):
|
|
var p: String = _item_list.icon(vnum)
|
|
if not p.is_empty() and not _assets_root.is_empty():
|
|
var tex := UiAssets.load_tex(_assets_root, p)
|
|
if tex:
|
|
return tex
|
|
if not _assets_root.is_empty():
|
|
var rel := "icon/item/%d.tga" % vnum
|
|
var cand := UiAssets.load_tex(_assets_root, rel)
|
|
if cand:
|
|
return cand
|
|
return null
|
|
|
|
func _format_price(val: int) -> String:
|
|
var s := str(val)
|
|
var res := ""
|
|
var cnt := 0
|
|
for i in range(s.length() - 1, -1, -1):
|
|
res = s[i] + res
|
|
cnt += 1
|
|
if cnt % 3 == 0 and i > 0:
|
|
res = "," + res
|
|
return res
|
|
|
|
func _process(_delta: float) -> void:
|
|
if visible and follow_mouse:
|
|
update_position()
|
|
|
|
func update_position(screen_size: Vector2 = Vector2.ZERO) -> void:
|
|
if not visible:
|
|
return
|
|
var mouse := get_global_mouse_position()
|
|
var w := size.x if size.x > 0 else custom_minimum_size.x
|
|
var h := size.y
|
|
if screen_size == Vector2.ZERO:
|
|
var vp := get_viewport()
|
|
if vp:
|
|
screen_size = vp.get_visible_rect().size
|
|
else:
|
|
screen_size = Vector2(1920, 1080)
|
|
|
|
var x := mouse.x - w / 2.0
|
|
var y := 0.0
|
|
if mouse.y < screen_size.y - 320.0:
|
|
y = mouse.y + 24.0
|
|
else:
|
|
y = mouse.y - h - 16.0
|
|
|
|
x = clampf(x, 8.0, maxf(8.0, screen_size.x - w - 8.0))
|
|
y = clampf(y, 8.0, maxf(8.0, screen_size.y - h - 8.0))
|
|
global_position = Vector2(x, y)
|