# 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)