feat(ui): 40250 item tooltip parity (thinboard, palette, sockets, stats)
This commit is contained in:
@@ -0,0 +1,256 @@
|
|||||||
|
# test_item_tooltip_parity.gd —— 装备属性面板 40250 官方规范 100% 对齐测试。
|
||||||
|
# 检验:
|
||||||
|
# 1. 调色板与 uitooltip.py 官方常量 1:1 对齐
|
||||||
|
# 2. 装备标题根据是否有强化附加属性判定白装 (#F2E7C1) / 金装 (#FFC800)
|
||||||
|
# 3. 武器攻防加算与防具防御力加算 (value[1] + value[5]*2),浅绿色 (#8AB98E)
|
||||||
|
# 4. 固有属性与百分比词条格式化,带正负符号与 %
|
||||||
|
# 5. 附加属性 1~5 薄荷青绿 (#B0DFB4),6~7 浅薄荷绿 (#E1FAE1),无非官方“附加”冗余前缀
|
||||||
|
# 6. 适用职业与性别判断,不符合时警示红 (#E67976)
|
||||||
|
# 7. 等级限制判断,玩家等级不足时警示红 (#E67976)
|
||||||
|
# 8. 魂石凹槽图文解析与空槽/损坏槽
|
||||||
|
# 9. ItemTooltipView 控件九宫格 ThinBoard 背景、居中对齐与文字描边
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
||||||
|
const ItemTooltipView = preload("res://ui/item_tooltip_view.gd")
|
||||||
|
|
||||||
|
class MockProto extends Node:
|
||||||
|
var items: Dictionary = {}
|
||||||
|
func item(vnum: int) -> Dictionary:
|
||||||
|
return items.get(vnum, {})
|
||||||
|
|
||||||
|
var _failed := 0
|
||||||
|
var _pass_count := 0
|
||||||
|
|
||||||
|
func _assert(cond: bool, msg: String) -> void:
|
||||||
|
if cond:
|
||||||
|
_pass_count += 1
|
||||||
|
print("PASS: %s" % msg)
|
||||||
|
else:
|
||||||
|
_failed += 1
|
||||||
|
printerr("FAIL: %s" % msg)
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
print("=== Starting test_item_tooltip_parity ===")
|
||||||
|
_test_palette_constants()
|
||||||
|
_test_weapon_model_parity()
|
||||||
|
_test_armor_model_parity()
|
||||||
|
_test_attrs_coloring_and_naming()
|
||||||
|
_test_wearable_job_and_level_warnings()
|
||||||
|
_test_socket_stone_resolution()
|
||||||
|
_test_item_tooltip_view_ui_structure()
|
||||||
|
|
||||||
|
if _failed == 0:
|
||||||
|
print("\n=== ALL %d ITEM TOOLTIP PARITY TESTS PASSED ===" % _pass_count)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
printerr("\n=== %d ITEM TOOLTIP PARITY TESTS FAILED ===" % _failed)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
# --- Test 1: 调色板与 40250 原版常量一致 ---
|
||||||
|
func _test_palette_constants() -> void:
|
||||||
|
print("\n--- Test 1: 40250 Palette Constants ---")
|
||||||
|
# uitooltip.py: TITLE_COLOR = grp.GenerateColor(0.9490, 0.9058, 0.7568, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.TITLE_COLOR.r, 0.9490) and is_equal_approx(ItemTooltip.TITLE_COLOR.g, 0.9058),
|
||||||
|
"TITLE_COLOR matches 40250 (0.9490, 0.9058, 0.7568)")
|
||||||
|
# uitooltip.py: SPECIAL_TITLE_COLOR = grp.GenerateColor(1.0, 0.7843, 0.0, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.SPECIAL_TITLE_COLOR.r, 1.0) and is_equal_approx(ItemTooltip.SPECIAL_TITLE_COLOR.g, 0.7843),
|
||||||
|
"SPECIAL_TITLE_COLOR matches 40250 (1.0, 0.7843, 0.0)")
|
||||||
|
# uitooltip.py: POSITIVE_COLOR = grp.GenerateColor(0.5411, 0.7254, 0.5568, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.POSITIVE_COLOR.r, 0.5411) and is_equal_approx(ItemTooltip.POSITIVE_COLOR.g, 0.7254),
|
||||||
|
"POSITIVE_COLOR matches 40250 (0.5411, 0.7254, 0.5568)")
|
||||||
|
# uitooltip.py: SPECIAL_POSITIVE_COLOR = grp.GenerateColor(0.6911, 0.8754, 0.7068, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.SPECIAL_POSITIVE_COLOR.r, 0.6911) and is_equal_approx(ItemTooltip.SPECIAL_POSITIVE_COLOR.g, 0.8754),
|
||||||
|
"SPECIAL_POSITIVE_COLOR matches 40250 (0.6911, 0.8754, 0.7068)")
|
||||||
|
# uitooltip.py: SPECIAL_POSITIVE_COLOR2 = grp.GenerateColor(0.8824, 0.9804, 0.8824, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.SPECIAL_POSITIVE_COLOR2.r, 0.8824) and is_equal_approx(ItemTooltip.SPECIAL_POSITIVE_COLOR2.g, 0.9804),
|
||||||
|
"SPECIAL_POSITIVE_COLOR2 matches 40250 (0.8824, 0.9804, 0.8824)")
|
||||||
|
# uitooltip.py: DISABLE_COLOR = grp.GenerateColor(0.9, 0.4745, 0.4627, 1.0)
|
||||||
|
_assert(is_equal_approx(ItemTooltip.DISABLE_COLOR.r, 0.9) and is_equal_approx(ItemTooltip.DISABLE_COLOR.g, 0.4745),
|
||||||
|
"DISABLE_COLOR matches 40250 (0.9, 0.4745, 0.4627)")
|
||||||
|
# uitooltip.py: PRICE_COLOR = 0xffFFB96D
|
||||||
|
_assert(is_equal_approx(ItemTooltip.PRICE_COLOR.r, 1.0) and is_equal_approx(ItemTooltip.PRICE_COLOR.b, 0.4275),
|
||||||
|
"PRICE_COLOR matches 40250 (0xffFFB96D)")
|
||||||
|
|
||||||
|
# --- Test 2: 武器数值计算与白装/金装标题颜色 ---
|
||||||
|
func _test_weapon_model_parity() -> void:
|
||||||
|
print("\n--- Test 2: Weapon Model & Attack Values ---")
|
||||||
|
var builder := ItemTooltip.new()
|
||||||
|
var sword_proto := {
|
||||||
|
"vnum": 19,
|
||||||
|
"name": "Sword+9",
|
||||||
|
"locale_name": "战斗剑 +9",
|
||||||
|
"type": 1, # WEAPON
|
||||||
|
"sub_type": 0, # WEAPON_SWORD
|
||||||
|
"values": [0, 50, 70, 100, 120, 15], # min_magic=50+15=65, max_magic=70+15=85, min_atk=100+15=115, max_atk=120+15=135
|
||||||
|
"limits": [{"type": 1, "value": 30}],
|
||||||
|
"applies": [{"type": 7, "value": 15}], # 攻击速度 +15%
|
||||||
|
"anti_flags": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. 普通白装 (无 attrs):标题颜色为 TITLE_COLOR
|
||||||
|
var plain_model := builder.build_model(19, 1, sword_proto, {})
|
||||||
|
_assert(plain_model["title"] == "战斗剑 +9", "Title is 战斗剑 +9")
|
||||||
|
_assert(plain_model["title_color"] == ItemTooltip.TITLE_COLOR, "Plain weapon title color is TITLE_COLOR")
|
||||||
|
_assert(plain_model["stats"].size() == 2, "Weapon has 2 stats (attack & magic attack)")
|
||||||
|
_assert(plain_model["stats"][0]["text"] == "攻击力 115 - 135", "Attack power calculation matches (100+15 - 120+15)")
|
||||||
|
_assert(plain_model["stats"][0]["color"] == ItemTooltip.POSITIVE_COLOR, "Attack power is POSITIVE_COLOR (green)")
|
||||||
|
_assert(plain_model["stats"][1]["text"] == "魔法攻击 65 - 85", "Magic attack calculation matches (50+15 - 70+15)")
|
||||||
|
_assert(plain_model["applies"][0]["text"] == "攻击速度 +15%", "Attack speed has % symbol")
|
||||||
|
_assert(plain_model["applies"][0]["color"] == ItemTooltip.POSITIVE_COLOR, "Attack speed bonus is POSITIVE_COLOR")
|
||||||
|
|
||||||
|
# 2. 带有强化附加属性 (蓝字):标题颜色升级为 SPECIAL_TITLE_COLOR (金黄色)
|
||||||
|
var enchanted_instance := {
|
||||||
|
"attrs": [
|
||||||
|
{"type": 5, "value": 12}, # STR +12
|
||||||
|
{"type": 15, "value": 10}, # 暴击概率 +10%
|
||||||
|
]
|
||||||
|
}
|
||||||
|
var enchanted_model := builder.build_model(19, 1, sword_proto, enchanted_instance)
|
||||||
|
_assert(enchanted_model["title_color"] == ItemTooltip.SPECIAL_TITLE_COLOR,
|
||||||
|
"Weapon with bonus attributes has SPECIAL_TITLE_COLOR (#FFC800)")
|
||||||
|
|
||||||
|
# --- Test 3: 防具防御力公式加算 ---
|
||||||
|
func _test_armor_model_parity() -> void:
|
||||||
|
print("\n--- Test 3: Armor Defense Formula ---")
|
||||||
|
var builder := ItemTooltip.new()
|
||||||
|
var armor_proto := {
|
||||||
|
"vnum": 11209,
|
||||||
|
"name": "Armor+9",
|
||||||
|
"locale_name": "修罗战甲 +9",
|
||||||
|
"type": 2, # ARMOR
|
||||||
|
"sub_type": 0, # ARMOR_BODY
|
||||||
|
"values": [15, 80, 0, 0, 0, 8], # magic_def=15, def = 80 + 8*2 = 96
|
||||||
|
"limits": [{"type": 1, "value": 42}],
|
||||||
|
"applies": [{"type": 8, "value": -5}], # 移动速度 -5%
|
||||||
|
"anti_flags": (1 << 2) | (1 << 3) | (1 << 5), # 仅修罗可穿 (ANTIFLAG_WARRIOR | ANTIFLAG_ASSASSIN | ANTIFLAG_SHAMAN)
|
||||||
|
}
|
||||||
|
var model := builder.build_model(11209, 1, armor_proto, {})
|
||||||
|
_assert(model["stats"].size() == 2, "Armor has 2 stats")
|
||||||
|
_assert(model["stats"][0]["text"] == "防御力 96", "Armor defense formula (value[1] + value[5]*2 = 80 + 16 = 96)")
|
||||||
|
_assert(model["stats"][0]["color"] == ItemTooltip.POSITIVE_COLOR, "Defense is POSITIVE_COLOR")
|
||||||
|
_assert(model["stats"][1]["text"] == "魔法防御 15", "Magic defense matches value[0]")
|
||||||
|
_assert(model["applies"][0]["text"] == "移动速度 -5%", "Moving speed negative apply has %")
|
||||||
|
_assert(model["applies"][0]["color"] == ItemTooltip.NEGATIVE_COLOR, "Negative moving speed is NEGATIVE_COLOR (red)")
|
||||||
|
|
||||||
|
# --- Test 4: 附加属性配色 (1~5条薄荷青绿,6~7条浅薄荷绿) 与去除非原版“附加”字样 ---
|
||||||
|
func _test_attrs_coloring_and_naming() -> void:
|
||||||
|
print("\n--- Test 4: Bonus Attributes Colors & Clean Naming ---")
|
||||||
|
var builder := ItemTooltip.new()
|
||||||
|
var proto := {"type": 1, "values": [0, 0, 0, 10, 20, 0]}
|
||||||
|
var instance := {
|
||||||
|
"attrs": [
|
||||||
|
{"type": 1, "value": 2000}, # 最大生命 +2000
|
||||||
|
{"type": 5, "value": 12}, # 力量 +12
|
||||||
|
{"type": 15, "value": 10}, # 暴击概率 +10%
|
||||||
|
{"type": 16, "value": 10}, # 穿透概率 +10%
|
||||||
|
{"type": 17, "value": 15}, # 对人族伤害 +15%
|
||||||
|
{"type": 71, "value": 8}, # 技能伤害 +8% (第6条)
|
||||||
|
{"type": 72, "value": 12}, # 普通攻击伤害 +12% (第7条)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
var model := builder.build_model(10, 1, proto, instance)
|
||||||
|
_assert(model["attrs"].size() == 7, "Model parsed 7 bonus attributes")
|
||||||
|
_assert(model["attrs"][0]["text"] == "最大生命 +2000", "Attribute 1 formatted cleanly without '附加' prefix")
|
||||||
|
_assert(model["attrs"][0]["color"] == ItemTooltip.SPECIAL_POSITIVE_COLOR, "Attribute 1 is SPECIAL_POSITIVE_COLOR (#B0DFB4)")
|
||||||
|
_assert(model["attrs"][2]["text"] == "暴击概率 +10%", "Attribute 3 (percent) has % sign")
|
||||||
|
_assert(model["attrs"][4]["color"] == ItemTooltip.SPECIAL_POSITIVE_COLOR, "Attribute 5 is SPECIAL_POSITIVE_COLOR")
|
||||||
|
_assert(model["attrs"][5]["text"] == "技能伤害 +8%", "Attribute 6 has % sign")
|
||||||
|
_assert(model["attrs"][5]["color"] == ItemTooltip.SPECIAL_POSITIVE_COLOR2, "Attribute 6 is SPECIAL_POSITIVE_COLOR2 (#E1FAE1)")
|
||||||
|
_assert(model["attrs"][6]["color"] == ItemTooltip.SPECIAL_POSITIVE_COLOR2, "Attribute 7 is SPECIAL_POSITIVE_COLOR2 (#E1FAE1)")
|
||||||
|
|
||||||
|
# --- Test 5: 穿戴职业与等级不足红字警示 ---
|
||||||
|
func _test_wearable_job_and_level_warnings() -> void:
|
||||||
|
print("\n--- Test 5: Wearable Jobs & Level Warning ---")
|
||||||
|
var builder := ItemTooltip.new()
|
||||||
|
var proto := {
|
||||||
|
"type": 1,
|
||||||
|
"values": [0, 0, 0, 10, 20, 0],
|
||||||
|
"limits": [{"type": 1, "value": 50}], # 要求等级 50
|
||||||
|
"anti_flags": (1 << 3) | (1 << 4) | (1 << 5), # 仅战士可穿 (非刺客/修罗/萨满)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. 玩家等级不足 (当前 35 < 50): 等级限制飘红
|
||||||
|
var ctx_low_level := {"level": 35, "job": 0, "sex": 0} # 战士,35级
|
||||||
|
var model_low := builder.build_model(10, 1, proto, {}, ctx_low_level)
|
||||||
|
_assert(model_low["limits"][0]["text"] == "要求等级: 50", "Limit text is 要求等级: 50")
|
||||||
|
_assert(model_low["limits"][0]["color"] == ItemTooltip.DISABLE_COLOR, "Low level triggers DISABLE_COLOR (red)")
|
||||||
|
_assert(model_low["wearable"]["jobs"] == "战士", "Only 战士 listed in wearable")
|
||||||
|
_assert(model_low["wearable"]["jobs_color"] == ItemTooltip.NORMAL_COLOR, "Warrior can wear, jobs_color is NORMAL_COLOR")
|
||||||
|
|
||||||
|
# 2. 玩家职业不符 (当前是刺客 job=1): 职业限制飘红
|
||||||
|
var ctx_wrong_job := {"level": 60, "job": 1, "sex": 0} # 刺客,60级
|
||||||
|
var model_wrong := builder.build_model(10, 1, proto, {}, ctx_wrong_job)
|
||||||
|
_assert(model_wrong["limits"][0]["color"] == ItemTooltip.NORMAL_COLOR, "Level 60 >= 50, limit is NORMAL_COLOR")
|
||||||
|
_assert(model_wrong["wearable"]["jobs_color"] == ItemTooltip.DISABLE_COLOR, "Assassin cannot wear warrior sword, jobs_color is DISABLE_COLOR (red)")
|
||||||
|
|
||||||
|
# --- Test 6: 魂石凹槽图文解析与空槽/损坏槽 ---
|
||||||
|
func _test_socket_stone_resolution() -> void:
|
||||||
|
print("\n--- Test 6: Socket Stones Resolution ---")
|
||||||
|
var proto_node := MockProto.new()
|
||||||
|
proto_node.items[28430] = {
|
||||||
|
"vnum": 28430,
|
||||||
|
"name": "Stone_of_Speed+4",
|
||||||
|
"locale_name": "加速之石 +4",
|
||||||
|
"applies": [{"type": 8, "value": 30}], # 移动速度 +30%
|
||||||
|
}
|
||||||
|
var builder := ItemTooltip.new()
|
||||||
|
var assets := AssetRoot.path()
|
||||||
|
builder.setup(assets, "en", proto_node)
|
||||||
|
|
||||||
|
var sword_proto := {"type": 1, "values": [0, 0, 0, 10, 20, 0]}
|
||||||
|
var instance := {
|
||||||
|
"sockets": [28430, 0, -1] # 1: 镶嵌加速之石, 2: 未使用空槽, 3: 损坏槽
|
||||||
|
}
|
||||||
|
var model := builder.build_model(10, 1, sword_proto, instance)
|
||||||
|
_assert(model["sockets"].size() == 3, "Weapon has 3 socket entries")
|
||||||
|
_assert(model["sockets"][0]["type"] == "stone", "Socket 0 is stone")
|
||||||
|
_assert(model["sockets"][0]["name"] == "加速之石 +4", "Socket 0 resolved stone name 加速之石 +4")
|
||||||
|
_assert(model["sockets"][0]["affect"] == "移动速度 +30%", "Socket 0 resolved stone affect 移动速度 +30%")
|
||||||
|
_assert(model["sockets"][0]["affect_color"] == ItemTooltip.POSITIVE_COLOR, "Stone affect is POSITIVE_COLOR")
|
||||||
|
_assert(model["sockets"][1]["type"] == "empty", "Socket 1 is empty slot")
|
||||||
|
_assert(model["sockets"][1]["name"] == "未使用的凹槽", "Empty socket displays 未使用的凹槽")
|
||||||
|
_assert(model["sockets"][2]["type"] == "broken", "Socket 2 is broken")
|
||||||
|
_assert(model["sockets"][2]["name"] == "损坏的凹槽", "Broken socket displays 损坏的凹槽")
|
||||||
|
_assert(model["sockets"][2]["name_color"] == ItemTooltip.DISABLE_COLOR, "Broken socket is DISABLE_COLOR (red)")
|
||||||
|
|
||||||
|
proto_node.free()
|
||||||
|
|
||||||
|
# --- Test 7: ItemTooltipView UI 控件结构与 ThinBoard 呈现 ---
|
||||||
|
func _test_item_tooltip_view_ui_structure() -> void:
|
||||||
|
print("\n--- Test 7: ItemTooltipView UI Structure & ThinBoard ---")
|
||||||
|
var assets := AssetRoot.path()
|
||||||
|
var view := ItemTooltipView.new()
|
||||||
|
view.setup(assets)
|
||||||
|
|
||||||
|
var model := {
|
||||||
|
"title": "屠龙刀 +9",
|
||||||
|
"title_color": ItemTooltip.SPECIAL_TITLE_COLOR,
|
||||||
|
"count": 1,
|
||||||
|
"desc": "上古神兵",
|
||||||
|
"summary": "天下无双",
|
||||||
|
"limits": [{"text": "要求等级: 75", "color": ItemTooltip.NORMAL_COLOR}],
|
||||||
|
"stats": [{"text": "攻击力 250 - 310", "color": ItemTooltip.POSITIVE_COLOR}],
|
||||||
|
"applies": [{"text": "半人伤害 +15%", "color": ItemTooltip.POSITIVE_COLOR}],
|
||||||
|
"attrs": [{"text": "暴击概率 +10%", "color": ItemTooltip.SPECIAL_POSITIVE_COLOR}],
|
||||||
|
"wearable": {"header": "[ 穿戴职业 ]", "jobs": "战士", "jobs_color": ItemTooltip.NORMAL_COLOR, "gender": ""},
|
||||||
|
"sockets": [
|
||||||
|
{"type": "stone", "name": "致命之石 +4", "affect": "暴击概率 +8%", "vnum": 28431, "name_color": ItemTooltip.NORMAL_COLOR, "affect_color": ItemTooltip.POSITIVE_COLOR}
|
||||||
|
],
|
||||||
|
"price": 250000,
|
||||||
|
"realtime": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
view.populate_from_model(model)
|
||||||
|
|
||||||
|
_assert(view.custom_minimum_size.x >= 190.0, "Tooltip custom_minimum_size.x >= 190px standard")
|
||||||
|
var vbox: VBoxContainer = view._vbox
|
||||||
|
_assert(vbox != null, "VBoxContainer created")
|
||||||
|
_assert(vbox.get_child_count() >= 8, "VBox contains all rendered sections")
|
||||||
|
|
||||||
|
var title_lbl: Label = vbox.get_child(0)
|
||||||
|
_assert(title_lbl.text == "屠龙刀 +9", "Title text is 屠龙刀 +9")
|
||||||
|
_assert(title_lbl.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER, "Title is centered")
|
||||||
|
_assert(title_lbl.get_theme_constant("outline_size") == 2, "Title has outline_size = 2")
|
||||||
|
|
||||||
|
view.free()
|
||||||
@@ -28,6 +28,7 @@ var context_consumer: Callable = Callable()
|
|||||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||||
const EquipRules = preload("res://equip_rules.gd")
|
const EquipRules = preload("res://equip_rules.gd")
|
||||||
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
||||||
|
const ItemTooltipView = preload("res://ui/item_tooltip_view.gd")
|
||||||
const ChestSystem = preload("res://chest_system.gd")
|
const ChestSystem = preload("res://chest_system.gd")
|
||||||
const ItemAttrSystem = preload("res://item_attr_system.gd")
|
const ItemAttrSystem = preload("res://item_attr_system.gd")
|
||||||
const MetinSocketSystem = preload("res://metin_socket_system.gd")
|
const MetinSocketSystem = preload("res://metin_socket_system.gd")
|
||||||
@@ -66,6 +67,7 @@ var uiscript_dir := ""
|
|||||||
var item_mouse: Node # MouseController(可空,兼容旧测试桩)
|
var item_mouse: Node # MouseController(可空,兼容旧测试桩)
|
||||||
|
|
||||||
var _win: Dictionary = {} # { root, nodes }
|
var _win: Dictionary = {} # { root, nodes }
|
||||||
|
var _local_tooltip_view: Control = null
|
||||||
var _cells := {} # ui_index:int -> Panel
|
var _cells := {} # ui_index:int -> Panel
|
||||||
var _drag_from := -1
|
var _drag_from := -1
|
||||||
var _combine_from := -1 # Shift 选中的“使用到物品”来源格
|
var _combine_from := -1 # Shift 选中的“使用到物品”来源格
|
||||||
@@ -122,6 +124,7 @@ func toggle() -> void:
|
|||||||
else: open()
|
else: open()
|
||||||
|
|
||||||
func close() -> void:
|
func close() -> void:
|
||||||
|
_hide_tooltip()
|
||||||
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog) and ui:
|
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog) and ui:
|
||||||
ui.close(_mobile_detail_dialog)
|
ui.close(_mobile_detail_dialog)
|
||||||
_mobile_detail_dialog = null
|
_mobile_detail_dialog = null
|
||||||
@@ -259,9 +262,51 @@ func _wire_cells() -> void:
|
|||||||
var cell: Panel = _cells[idx]
|
var cell: Panel = _cells[idx]
|
||||||
var cell_idx: int = int(idx)
|
var cell_idx: int = int(idx)
|
||||||
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(cell_idx, e))
|
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(cell_idx, e))
|
||||||
|
cell.mouse_entered.connect(func(): _on_cell_mouse_entered(cell_idx))
|
||||||
|
cell.mouse_exited.connect(func(): _on_cell_mouse_exited(cell_idx))
|
||||||
if item_mouse and item_mouse.has_method("register_target"):
|
if item_mouse and item_mouse.has_method("register_target"):
|
||||||
item_mouse.register_target(cell, func(payload: Dictionary): return _drop_mouse_item(payload, cell_idx), self)
|
item_mouse.register_target(cell, func(payload: Dictionary): return _drop_mouse_item(payload, cell_idx), self)
|
||||||
|
|
||||||
|
func _get_tooltip_view() -> Control:
|
||||||
|
if ui and ui.has_method("get_item_tooltip_view"):
|
||||||
|
return ui.get_item_tooltip_view(assets_root, proto, item_list)
|
||||||
|
if _local_tooltip_view == null or not is_instance_valid(_local_tooltip_view):
|
||||||
|
_local_tooltip_view = ItemTooltipView.new()
|
||||||
|
_local_tooltip_view.setup(assets_root, proto, item_list, item_tooltip)
|
||||||
|
_local_tooltip_view.visible = false
|
||||||
|
_local_tooltip_view.z_index = 200
|
||||||
|
if _win.has("root") and is_instance_valid(_win["root"]):
|
||||||
|
_win["root"].add_child(_local_tooltip_view)
|
||||||
|
return _local_tooltip_view
|
||||||
|
|
||||||
|
func _on_cell_mouse_entered(cell_idx: int) -> void:
|
||||||
|
if item_mouse and item_mouse.has_method("is_attached") and item_mouse.is_attached():
|
||||||
|
return
|
||||||
|
var cell: Panel = _cells.get(cell_idx, null)
|
||||||
|
if cell == null:
|
||||||
|
return
|
||||||
|
var vnum := int(cell.get_meta("vnum", 0))
|
||||||
|
if vnum <= 0:
|
||||||
|
return
|
||||||
|
var count := int(cell.get_meta("count", 1))
|
||||||
|
var instance_data: Dictionary = cell.get_meta("item_data", {})
|
||||||
|
var proto_data: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
|
||||||
|
var ctx: Dictionary = _equip_ctx()
|
||||||
|
if ctx.has("race"):
|
||||||
|
ctx["job"] = EquipRules.job_of_race(int(ctx["race"]))
|
||||||
|
ctx["sex"] = EquipRules.sex_of_race(int(ctx["race"]))
|
||||||
|
var tv := _get_tooltip_view()
|
||||||
|
if tv:
|
||||||
|
tv.show_item(vnum, count, proto_data, instance_data, ctx)
|
||||||
|
|
||||||
|
func _on_cell_mouse_exited(_cell_idx: int) -> void:
|
||||||
|
_hide_tooltip()
|
||||||
|
|
||||||
|
func _hide_tooltip() -> void:
|
||||||
|
var tv := _get_tooltip_view()
|
||||||
|
if tv:
|
||||||
|
tv.hide_tooltip()
|
||||||
|
|
||||||
func _wire_money() -> void:
|
func _wire_money() -> void:
|
||||||
_money_button = _win.get("nodes", {}).get("Money_Slot", null)
|
_money_button = _win.get("nodes", {}).get("Money_Slot", null)
|
||||||
if _money_button is BaseButton:
|
if _money_button is BaseButton:
|
||||||
|
|||||||
+363
-21
@@ -1,10 +1,24 @@
|
|||||||
# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一文字提示。
|
# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一属性提示与 40250 模型构建。
|
||||||
#
|
#
|
||||||
# 参考:ClientVS22/POC/assets/root/uitooltip.py::ItemToolTip.AddItemData。
|
# 参考:ClientVS22/POC/assets/root/uitooltip.py::ItemToolTip.AddItemData。
|
||||||
# Godot 的原生 tooltip 负责显示位置与换行;本类只负责把同一份物品快照
|
# 支持 40250 调色板、居中排版数据模型导出、攻防数值加算、固有属性与附加属性着色、
|
||||||
# 组织成标题、描述、限制、基础数值、附加属性、魂石槽和精炼关联。
|
# 适用职业判断(红字警示)、魂石槽图文解析与纯文本向下兼容。
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
|
# 40250 官方调色板 (uitooltip.py)
|
||||||
|
const TITLE_COLOR := Color(0.9490, 0.9058, 0.7568, 1.0) # #f2e7c1 浅金象牙白 (白装/普通)
|
||||||
|
const SPECIAL_TITLE_COLOR := Color(1.0, 0.7843, 0.0, 1.0) # #ffc800 亮金黄色 (有附加属性/稀有)
|
||||||
|
const NORMAL_COLOR := Color(0.7607, 0.7607, 0.7607, 1.0) # #c2c2c2 普通浅灰
|
||||||
|
const FONT_COLOR := Color(0.7607, 0.7607, 0.7607, 1.0)
|
||||||
|
const PRICE_COLOR := Color(1.0, 0.7255, 0.4275, 1.0) # #ffb96d 价格金橙色
|
||||||
|
const CONDITION_COLOR := Color(0.7451, 0.7059, 0.4902, 1.0) # #beb47d 说明/要求淡金
|
||||||
|
const ENABLE_COLOR := Color(0.7607, 0.7607, 0.7607, 1.0)
|
||||||
|
const DISABLE_COLOR := Color(0.9, 0.4745, 0.4627, 1.0) # #e67976 限制不满足/警示红
|
||||||
|
const POSITIVE_COLOR := Color(0.5411, 0.7254, 0.5568, 1.0) # #8ab98e 基础攻防与固有词条浅绿
|
||||||
|
const NEGATIVE_COLOR := Color(0.9, 0.4745, 0.4627, 1.0) # #e67976 负向词条警示红
|
||||||
|
const SPECIAL_POSITIVE_COLOR := Color(0.6911, 0.8754, 0.7068, 1.0) # #b0dfb4 附加属性1~5薄荷青绿
|
||||||
|
const SPECIAL_POSITIVE_COLOR2 := Color(0.8824, 0.9804, 0.8824, 1.0)# #e1fae1 附加属性6~7浅薄荷绿
|
||||||
|
|
||||||
const ITEM_TYPE_WEAPON := 1
|
const ITEM_TYPE_WEAPON := 1
|
||||||
const ITEM_TYPE_ARMOR := 2
|
const ITEM_TYPE_ARMOR := 2
|
||||||
const ITEM_TYPE_LOTTERY := 8
|
const ITEM_TYPE_LOTTERY := 8
|
||||||
@@ -13,10 +27,19 @@ const ITEM_TYPE_ROD := 13
|
|||||||
const ITEM_TYPE_UNIQUE := 16
|
const ITEM_TYPE_UNIQUE := 16
|
||||||
const ITEM_TYPE_PICK := 24
|
const ITEM_TYPE_PICK := 24
|
||||||
const ITEM_TYPE_BLEND := 27
|
const ITEM_TYPE_BLEND := 27
|
||||||
|
const ITEM_TYPE_COSTUME := 28
|
||||||
const ITEM_TYPE_DS := 29
|
const ITEM_TYPE_DS := 29
|
||||||
const ITEM_TYPE_RING := 33
|
const ITEM_TYPE_RING := 33
|
||||||
const ITEM_TYPE_BELT := 34
|
const ITEM_TYPE_BELT := 34
|
||||||
|
|
||||||
|
const CHARACTER_NAMES := ["战士", "刺客", "修罗", "萨满"]
|
||||||
|
const ANTIFLAG_FEMALE := 1 << 0
|
||||||
|
const ANTIFLAG_MALE := 1 << 1
|
||||||
|
const ANTIFLAG_WARRIOR := 1 << 2
|
||||||
|
const ANTIFLAG_ASSASSIN := 1 << 3
|
||||||
|
const ANTIFLAG_SURA := 1 << 4
|
||||||
|
const ANTIFLAG_SHAMAN := 1 << 5
|
||||||
|
|
||||||
const LIMIT_NAMES := {
|
const LIMIT_NAMES := {
|
||||||
1: "等级要求",
|
1: "等级要求",
|
||||||
2: "力量要求",
|
2: "力量要求",
|
||||||
@@ -160,14 +183,89 @@ const APPLY_NAMES := {
|
|||||||
91: "抗穿透",
|
91: "抗穿透",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 40250 uitooltip.py AFFECT_DICT 规定为百分比显示的词条
|
||||||
|
const PERCENT_APPLIES := {
|
||||||
|
7: true, # 攻击速度
|
||||||
|
8: true, # 移动速度
|
||||||
|
9: true, # 施法速度
|
||||||
|
10: true, # 生命恢复
|
||||||
|
11: true, # 法力恢复
|
||||||
|
12: true, # 中毒概率
|
||||||
|
13: true, # 眩晕概率
|
||||||
|
14: true, # 缓慢概率
|
||||||
|
15: true, # 暴击概率
|
||||||
|
16: true, # 穿透概率
|
||||||
|
17: true, # 对人族伤害
|
||||||
|
18: true, # 对动物伤害
|
||||||
|
19: true, # 对兽人伤害
|
||||||
|
20: true, # 对秘境伤害
|
||||||
|
21: true, # 对不死族伤害
|
||||||
|
22: true, # 对恶魔伤害
|
||||||
|
23: true, # 生命吸收
|
||||||
|
24: true, # 法力吸收
|
||||||
|
25: true, # 法力燃烧
|
||||||
|
26: true, # 伤害转法力
|
||||||
|
27: true, # 格挡
|
||||||
|
28: true, # 闪避
|
||||||
|
29: true, # 剑防御
|
||||||
|
30: true, # 双手防御
|
||||||
|
31: true, # 匕首防御
|
||||||
|
32: true, # 铃防御
|
||||||
|
33: true, # 扇防御
|
||||||
|
34: true, # 弓防御
|
||||||
|
35: true, # 火抗性
|
||||||
|
36: true, # 雷抗性
|
||||||
|
37: true, # 魔法抗性
|
||||||
|
38: true, # 风抗性
|
||||||
|
39: true, # 近战反射
|
||||||
|
40: true, # 诅咒反射
|
||||||
|
41: true, # 中毒抵抗
|
||||||
|
42: true, # 击杀恢复法力
|
||||||
|
43: true, # 经验加成
|
||||||
|
44: true, # 金币加成
|
||||||
|
45: true, # 掉落加成
|
||||||
|
46: true, # 药水效果
|
||||||
|
47: true, # 击杀恢复生命
|
||||||
|
57: true, # 诅咒概率
|
||||||
|
59: true, # 对战士伤害
|
||||||
|
60: true, # 对刺客伤害
|
||||||
|
61: true, # 对武士伤害
|
||||||
|
62: true, # 对萨满伤害
|
||||||
|
63: true, # 对怪物伤害
|
||||||
|
64: true, # 商城攻击加成
|
||||||
|
65: true, # 商城防御加成
|
||||||
|
66: true, # 商城经验加成
|
||||||
|
67: true, # 商城掉落加成
|
||||||
|
68: true, # 商城金币加成
|
||||||
|
69: true, # 最大生命百分比
|
||||||
|
70: true, # 最大法力百分比
|
||||||
|
71: true, # 技能伤害
|
||||||
|
72: true, # 普通攻击伤害
|
||||||
|
73: true, # 技能防御
|
||||||
|
74: true, # 普通攻击防御
|
||||||
|
78: true, # 战士抗性
|
||||||
|
79: true, # 刺客抗性
|
||||||
|
80: true, # 武士抗性
|
||||||
|
81: true, # 萨满抗性
|
||||||
|
85: true, # 魔法攻击百分比
|
||||||
|
86: true, # 近战魔法攻击百分比
|
||||||
|
87: true, # 冰抗性
|
||||||
|
88: true, # 土抗性
|
||||||
|
89: true, # 暗抗性
|
||||||
|
90: true, # 抗暴击
|
||||||
|
91: true, # 抗穿透
|
||||||
|
}
|
||||||
|
|
||||||
var _descriptions: Dictionary = {}
|
var _descriptions: Dictionary = {}
|
||||||
var _skill_names: Dictionary = {}
|
var _skill_names: Dictionary = {}
|
||||||
var _mob_proto: Object = null
|
var _proto_node: Object = null
|
||||||
|
var _assets_root: String = ""
|
||||||
|
|
||||||
func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool:
|
func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool:
|
||||||
_descriptions.clear()
|
_descriptions.clear()
|
||||||
_skill_names.clear()
|
_skill_names.clear()
|
||||||
_mob_proto = proto_node
|
_assets_root = assets_root
|
||||||
|
_proto_node = proto_node
|
||||||
var path := assets_root.path_join("locale/locale/%s/itemdesc.txt" % lang)
|
var path := assets_root.path_join("locale/locale/%s/itemdesc.txt" % lang)
|
||||||
if FileAccess.file_exists(path):
|
if FileAccess.file_exists(path):
|
||||||
var file := FileAccess.open(path, FileAccess.READ)
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
@@ -186,6 +284,249 @@ func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool
|
|||||||
_load_skill_names(assets_root, lang)
|
_load_skill_names(assets_root, lang)
|
||||||
return not _descriptions.is_empty() or not _skill_names.is_empty()
|
return not _descriptions.is_empty() or not _skill_names.is_empty()
|
||||||
|
|
||||||
|
func get_proto_node() -> Object:
|
||||||
|
return _proto_node
|
||||||
|
|
||||||
|
func get_stone_info(stone_vnum: int) -> Dictionary:
|
||||||
|
var name := "魂石 #%d" % stone_vnum
|
||||||
|
var affect := ""
|
||||||
|
var icon_path := ""
|
||||||
|
if _proto_node and _proto_node.has_method("item"):
|
||||||
|
var sp: Dictionary = _proto_node.item(stone_vnum)
|
||||||
|
if not sp.is_empty():
|
||||||
|
name = String(sp.get("locale_name", sp.get("name", name)))
|
||||||
|
var sa: Array = sp.get("applies", [])
|
||||||
|
if not sa.is_empty():
|
||||||
|
var atype := int(sa[0].get("type", 0))
|
||||||
|
var aval := int(sa[0].get("value", 0))
|
||||||
|
if atype != 0 and aval != 0:
|
||||||
|
affect = format_apply_string(atype, aval)
|
||||||
|
if name.begins_with("魂石 #") and _descriptions.has(stone_vnum):
|
||||||
|
name = String(_descriptions[stone_vnum].get("name", name))
|
||||||
|
return {"name": name, "affect": affect, "icon_path": icon_path}
|
||||||
|
|
||||||
|
func format_apply_string(apply_type: int, value: int) -> String:
|
||||||
|
var label: String = APPLY_NAMES.get(apply_type, "属性 %d" % apply_type)
|
||||||
|
if PERCENT_APPLIES.get(apply_type, false):
|
||||||
|
return "%s %+d%%" % [label, value]
|
||||||
|
return "%s %+d" % [label, value]
|
||||||
|
|
||||||
|
# 核心 40250 数据模型导出(供 ItemTooltipView 九宫格控件渲染)
|
||||||
|
func build_model(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {},
|
||||||
|
player_ctx: Dictionary = {}) -> Dictionary:
|
||||||
|
if vnum <= 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var name := String(proto_data.get("locale_name", proto_data.get("name", "物品 #%d" % vnum)))
|
||||||
|
if name.is_empty():
|
||||||
|
name = "物品 #%d" % vnum
|
||||||
|
var sockets: Array = instance_data.get("sockets", proto_data.get("sockets", []))
|
||||||
|
name = _special_title(vnum, name, sockets)
|
||||||
|
|
||||||
|
var attrs: Array = instance_data.get("attrs", proto_data.get("attrs", []))
|
||||||
|
var has_attrs := false
|
||||||
|
for a in attrs:
|
||||||
|
if int(a.get("type", 0)) != 0 and int(a.get("value", 0)) != 0:
|
||||||
|
has_attrs = true
|
||||||
|
break
|
||||||
|
|
||||||
|
var title_col := SPECIAL_TITLE_COLOR if has_attrs else TITLE_COLOR
|
||||||
|
|
||||||
|
var desc_info: Dictionary = _descriptions.get(vnum, {})
|
||||||
|
var desc_text := String(desc_info.get("description", ""))
|
||||||
|
var summary_text := String(desc_info.get("summary", ""))
|
||||||
|
|
||||||
|
# 限制要求
|
||||||
|
var limits_out: Array[Dictionary] = []
|
||||||
|
var player_level: int = int(player_ctx.get("level", 0))
|
||||||
|
for limit in proto_data.get("limits", []):
|
||||||
|
var limit_type := int(limit.get("type", 0))
|
||||||
|
var limit_value := int(limit.get("value", 0))
|
||||||
|
if limit_type == 6:
|
||||||
|
limits_out.append({"text": "PC房专用道具", "color": NORMAL_COLOR})
|
||||||
|
continue
|
||||||
|
if limit_type == 1 and limit_value > 0:
|
||||||
|
var col := DISABLE_COLOR if (player_level > 0 and player_level < limit_value) else NORMAL_COLOR
|
||||||
|
limits_out.append({"text": "要求等级: %d" % limit_value, "color": col, "limit_type": 1, "value": limit_value})
|
||||||
|
elif limit_type != 0 and limit_value > 0:
|
||||||
|
var lim_name: String = LIMIT_NAMES.get(limit_type, "限制 %d" % limit_type)
|
||||||
|
limits_out.append({"text": "%s: %d" % [lim_name, limit_value], "color": NORMAL_COLOR, "limit_type": limit_type, "value": limit_value})
|
||||||
|
|
||||||
|
# 基础战斗属性 (Weapon / Armor)
|
||||||
|
var stats_out: Array[Dictionary] = []
|
||||||
|
var item_type := int(proto_data.get("type", 0))
|
||||||
|
var item_subtype := int(proto_data.get("sub_type", proto_data.get("subtype", 0)))
|
||||||
|
var values: Array = proto_data.get("values", [])
|
||||||
|
if item_type == ITEM_TYPE_WEAPON and values.size() >= 6:
|
||||||
|
var min_attack := int(values[3]) + int(values[5])
|
||||||
|
var max_attack := int(values[4]) + int(values[5])
|
||||||
|
var min_magic := int(values[1]) + int(values[5])
|
||||||
|
var max_magic := int(values[2]) + int(values[5])
|
||||||
|
var atk_str := ""
|
||||||
|
if max_attack > min_attack:
|
||||||
|
atk_str = "攻击力 %d - %d" % [min_attack, max_attack]
|
||||||
|
elif min_attack > 0:
|
||||||
|
atk_str = "攻击力 %d" % min_attack
|
||||||
|
var mag_str := ""
|
||||||
|
if max_magic > min_magic:
|
||||||
|
mag_str = "魔法攻击 %d - %d" % [min_magic, max_magic]
|
||||||
|
elif min_magic > 0:
|
||||||
|
mag_str = "魔法攻击 %d" % min_magic
|
||||||
|
|
||||||
|
if item_subtype == 4: # WEAPON_FAN 扇子先魔法后物理
|
||||||
|
if not mag_str.is_empty():
|
||||||
|
stats_out.append({"text": mag_str, "color": POSITIVE_COLOR})
|
||||||
|
if not atk_str.is_empty():
|
||||||
|
stats_out.append({"text": atk_str, "color": POSITIVE_COLOR})
|
||||||
|
else:
|
||||||
|
if not atk_str.is_empty():
|
||||||
|
stats_out.append({"text": atk_str, "color": POSITIVE_COLOR})
|
||||||
|
if not mag_str.is_empty():
|
||||||
|
stats_out.append({"text": mag_str, "color": POSITIVE_COLOR})
|
||||||
|
elif item_type == ITEM_TYPE_ARMOR and values.size() >= 6:
|
||||||
|
var def_val := int(values[1]) + int(values[5]) * 2
|
||||||
|
if def_val > 0:
|
||||||
|
stats_out.append({"text": "防御力 %d" % def_val, "color": POSITIVE_COLOR})
|
||||||
|
if int(values[0]) > 0:
|
||||||
|
stats_out.append({"text": "魔法防御 %d" % int(values[0]), "color": POSITIVE_COLOR})
|
||||||
|
|
||||||
|
# 固有属性 (Proto Applies)
|
||||||
|
var applies_out: Array[Dictionary] = []
|
||||||
|
for apply in proto_data.get("applies", []):
|
||||||
|
var atype := int(apply.get("type", 0))
|
||||||
|
var aval := int(apply.get("value", 0))
|
||||||
|
if atype == 0 or aval == 0:
|
||||||
|
continue
|
||||||
|
var astr := format_apply_string(atype, aval)
|
||||||
|
var acol := POSITIVE_COLOR if aval > 0 else NEGATIVE_COLOR
|
||||||
|
applies_out.append({"text": astr, "color": acol, "type": atype, "value": aval})
|
||||||
|
|
||||||
|
# 附加属性 (Instance Attrs,蓝字/强化属性)
|
||||||
|
var attrs_out: Array[Dictionary] = []
|
||||||
|
for i in attrs.size():
|
||||||
|
var attr: Dictionary = attrs[i]
|
||||||
|
var atype := int(attr.get("type", 0))
|
||||||
|
var aval := int(attr.get("value", 0))
|
||||||
|
if atype == 0 or aval == 0:
|
||||||
|
continue
|
||||||
|
var astr := format_apply_string(atype, aval)
|
||||||
|
var acol := SPECIAL_POSITIVE_COLOR if i < 5 else SPECIAL_POSITIVE_COLOR2
|
||||||
|
if aval < 0:
|
||||||
|
acol = NEGATIVE_COLOR
|
||||||
|
attrs_out.append({"text": astr, "color": acol, "type": atype, "value": aval, "index": i})
|
||||||
|
|
||||||
|
# 穿戴职业 (Wearable Info)
|
||||||
|
var wearable_out: Dictionary = {}
|
||||||
|
if item_type == ITEM_TYPE_WEAPON or item_type == ITEM_TYPE_ARMOR or item_type == ITEM_TYPE_COSTUME:
|
||||||
|
var anti: int = int(proto_data.get("anti_flags", 0))
|
||||||
|
var jobs: Array[String] = []
|
||||||
|
var player_job: int = int(player_ctx.get("job", -1))
|
||||||
|
var player_job_can := true
|
||||||
|
for j in 4:
|
||||||
|
if (anti & (1 << (2 + j))) == 0:
|
||||||
|
jobs.append(CHARACTER_NAMES[j])
|
||||||
|
else:
|
||||||
|
if player_job == j:
|
||||||
|
player_job_can = false
|
||||||
|
var jobs_str := " ".join(jobs)
|
||||||
|
var jobs_col := DISABLE_COLOR if (player_job >= 0 and not player_job_can) else NORMAL_COLOR
|
||||||
|
var gender_str := ""
|
||||||
|
var gender_col := NORMAL_COLOR
|
||||||
|
var player_sex: int = int(player_ctx.get("sex", -1))
|
||||||
|
if (anti & ANTIFLAG_FEMALE) != 0:
|
||||||
|
gender_str = "男性专用"
|
||||||
|
if player_sex == 1:
|
||||||
|
gender_col = DISABLE_COLOR
|
||||||
|
elif (anti & ANTIFLAG_MALE) != 0:
|
||||||
|
gender_str = "女性专用"
|
||||||
|
if player_sex == 0:
|
||||||
|
gender_col = DISABLE_COLOR
|
||||||
|
wearable_out = {
|
||||||
|
"header": "[ 穿戴职业 ]",
|
||||||
|
"jobs": jobs_str,
|
||||||
|
"jobs_color": jobs_col,
|
||||||
|
"gender": gender_str,
|
||||||
|
"gender_color": gender_col,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 魂石凹槽
|
||||||
|
var sockets_out: Array[Dictionary] = []
|
||||||
|
var has_sockets := false
|
||||||
|
for s in sockets:
|
||||||
|
if int(s) != 0:
|
||||||
|
has_sockets = true
|
||||||
|
break
|
||||||
|
if has_sockets or item_type == ITEM_TYPE_WEAPON or item_type == ITEM_TYPE_ARMOR:
|
||||||
|
for i in sockets.size():
|
||||||
|
var sval := int(sockets[i])
|
||||||
|
if sval == 0:
|
||||||
|
sockets_out.append({
|
||||||
|
"index": i,
|
||||||
|
"type": "empty",
|
||||||
|
"name": "未使用的凹槽",
|
||||||
|
"name_color": NORMAL_COLOR,
|
||||||
|
"affect": "",
|
||||||
|
"affect_color": POSITIVE_COLOR,
|
||||||
|
"vnum": 0,
|
||||||
|
"icon_path": "",
|
||||||
|
})
|
||||||
|
elif sval < 0:
|
||||||
|
sockets_out.append({
|
||||||
|
"index": i,
|
||||||
|
"type": "broken",
|
||||||
|
"name": "损坏的凹槽",
|
||||||
|
"name_color": DISABLE_COLOR,
|
||||||
|
"affect": "",
|
||||||
|
"affect_color": POSITIVE_COLOR,
|
||||||
|
"vnum": sval,
|
||||||
|
"icon_path": "",
|
||||||
|
})
|
||||||
|
elif sval == 1:
|
||||||
|
sockets_out.append({
|
||||||
|
"index": i,
|
||||||
|
"type": "empty",
|
||||||
|
"name": "未使用的凹槽",
|
||||||
|
"name_color": NORMAL_COLOR,
|
||||||
|
"affect": "",
|
||||||
|
"affect_color": POSITIVE_COLOR,
|
||||||
|
"vnum": 1,
|
||||||
|
"icon_path": "",
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
var sinfo := get_stone_info(sval)
|
||||||
|
sockets_out.append({
|
||||||
|
"index": i,
|
||||||
|
"type": "stone",
|
||||||
|
"name": sinfo.get("name", "魂石 #%d" % sval),
|
||||||
|
"name_color": NORMAL_COLOR,
|
||||||
|
"affect": sinfo.get("affect", ""),
|
||||||
|
"affect_color": POSITIVE_COLOR,
|
||||||
|
"vnum": sval,
|
||||||
|
"icon_path": sinfo.get("icon_path", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
# 价格与限时
|
||||||
|
var price_val := int(instance_data.get("price", 0))
|
||||||
|
var realtime_str := _get_realtime_text(proto_data, sockets, instance_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vnum": vnum,
|
||||||
|
"title": name,
|
||||||
|
"title_color": title_col,
|
||||||
|
"count": count,
|
||||||
|
"desc": desc_text,
|
||||||
|
"summary": summary_text,
|
||||||
|
"limits": limits_out,
|
||||||
|
"stats": stats_out,
|
||||||
|
"applies": applies_out,
|
||||||
|
"attrs": attrs_out,
|
||||||
|
"wearable": wearable_out,
|
||||||
|
"sockets": sockets_out,
|
||||||
|
"price": price_val,
|
||||||
|
"realtime": realtime_str,
|
||||||
|
"item_type": item_type,
|
||||||
|
}
|
||||||
|
|
||||||
func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {}) -> String:
|
func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {}) -> String:
|
||||||
if vnum <= 0:
|
if vnum <= 0:
|
||||||
return ""
|
return ""
|
||||||
@@ -224,7 +565,6 @@ func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictio
|
|||||||
var limit_type := int(limit.get("type", 0))
|
var limit_type := int(limit.get("type", 0))
|
||||||
var limit_value := int(limit.get("value", 0))
|
var limit_value := int(limit.get("value", 0))
|
||||||
if limit_type == 6:
|
if limit_type == 6:
|
||||||
# LIMIT_PCBANG 只是“网吧专用”标记,不是装备门禁(服务器 item.cpp:1899)。
|
|
||||||
lines.append("PC房专用道具")
|
lines.append("PC房专用道具")
|
||||||
continue
|
continue
|
||||||
if limit_type != 0 and limit_value > 0:
|
if limit_type != 0 and limit_value > 0:
|
||||||
@@ -275,16 +615,15 @@ func _special_title(vnum: int, item_name: String, sockets: Array) -> String:
|
|||||||
if vnum == 50300 or vnum == 70037 or vnum == 70055:
|
if vnum == 50300 or vnum == 70037 or vnum == 70055:
|
||||||
var skill_name := String(_skill_names.get(int(sockets[0]), ""))
|
var skill_name := String(_skill_names.get(int(sockets[0]), ""))
|
||||||
return skill_name + " " + item_name if skill_name != "" else item_name
|
return skill_name + " " + item_name if skill_name != "" else item_name
|
||||||
if vnum >= 70103 and vnum <= 70106 and _mob_proto and _mob_proto.has_method("mob"):
|
if vnum >= 70103 and vnum <= 70106 and _proto_node and _proto_node.has_method("mob"):
|
||||||
var mob: Dictionary = _mob_proto.mob(int(sockets[0]))
|
var mob: Dictionary = _proto_node.mob(int(sockets[0]))
|
||||||
var mob_name := String(mob.get("locale_name", mob.get("name", "")))
|
var mob_name := String(mob.get("locale_name", mob.get("name", "")))
|
||||||
return mob_name + " " + item_name if mob_name != "" else item_name
|
return mob_name + " " + item_name if mob_name != "" else item_name
|
||||||
return item_name
|
return item_name
|
||||||
|
|
||||||
func _append_realtime_limit(lines: Array[String], proto_data: Dictionary, sockets: Array,
|
func _get_realtime_text(proto_data: Dictionary, sockets: Array, instance_data: Dictionary) -> String:
|
||||||
instance_data: Dictionary) -> void:
|
|
||||||
if bool(instance_data.get("realtime_rendered", false)) or sockets.is_empty():
|
if bool(instance_data.get("realtime_rendered", false)) or sockets.is_empty():
|
||||||
return
|
return ""
|
||||||
var limit_type := 0
|
var limit_type := 0
|
||||||
for limit in proto_data.get("limits", []):
|
for limit in proto_data.get("limits", []):
|
||||||
var candidate := int(limit.get("type", 0))
|
var candidate := int(limit.get("type", 0))
|
||||||
@@ -292,27 +631,30 @@ func _append_realtime_limit(lines: Array[String], proto_data: Dictionary, socket
|
|||||||
limit_type = candidate
|
limit_type = candidate
|
||||||
break
|
break
|
||||||
if limit_type == 0:
|
if limit_type == 0:
|
||||||
return
|
return ""
|
||||||
var value := int(sockets[0])
|
var value := int(sockets[0])
|
||||||
var remaining := 0
|
var remaining := 0
|
||||||
if limit_type == 8:
|
if limit_type == 8:
|
||||||
# REAL_TIME_START_FIRST_USE:socket1 = 使用计数。未启用(0)时 socket0 是
|
|
||||||
# 时长秒数而非到期时间戳;启用后服务器把 socket0 换成到期 unix ts
|
|
||||||
#(char_item.cpp:6100 尾部 cLimitRealTimeFirstUseIndex 分支)。
|
|
||||||
var used := int(sockets[1]) if sockets.size() > 1 else 0
|
var used := int(sockets[1]) if sockets.size() > 1 else 0
|
||||||
if used == 0:
|
if used == 0:
|
||||||
lines.append("首次使用后计时:%s" % _duration_text(value))
|
return "首次使用后计时:%s" % _duration_text(value)
|
||||||
return
|
|
||||||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||||||
elif limit_type == 7:
|
elif limit_type == 7:
|
||||||
# REAL_TIME:socket0 = 到期 unix ts(item_length.h:328-347)。
|
|
||||||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||||||
else:
|
else:
|
||||||
remaining = maxi(0, value)
|
remaining = maxi(0, value)
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
lines.append("剩余时间:已超时")
|
return "已超时"
|
||||||
return
|
return _duration_text(remaining)
|
||||||
lines.append("剩余时间:%s" % _duration_text(remaining))
|
|
||||||
|
func _append_realtime_limit(lines: Array[String], proto_data: Dictionary, sockets: Array,
|
||||||
|
instance_data: Dictionary) -> void:
|
||||||
|
var t := _get_realtime_text(proto_data, sockets, instance_data)
|
||||||
|
if not t.is_empty():
|
||||||
|
if t.begins_with("首次使用"):
|
||||||
|
lines.append(t)
|
||||||
|
else:
|
||||||
|
lines.append("剩余时间:%s" % t)
|
||||||
|
|
||||||
func _duration_text(seconds: int) -> String:
|
func _duration_text(seconds: int) -> String:
|
||||||
var minutes := seconds / 60
|
var minutes := seconds / 60
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
# 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)
|
||||||
@@ -14,6 +14,7 @@ extends Node
|
|||||||
|
|
||||||
const CursorManager = preload("res://ui/cursor_manager.gd")
|
const CursorManager = preload("res://ui/cursor_manager.gd")
|
||||||
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
const ItemTooltip = preload("res://ui/item_tooltip.gd")
|
||||||
|
const ItemTooltipView = preload("res://ui/item_tooltip_view.gd")
|
||||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||||
const UiKit = preload("res://ui_kit.gd")
|
const UiKit = preload("res://ui_kit.gd")
|
||||||
const UiBuild = preload("res://ui/ui_build.gd")
|
const UiBuild = preload("res://ui/ui_build.gd")
|
||||||
@@ -28,6 +29,7 @@ var proto: Node
|
|||||||
var item_list # ItemListDB (RefCounted)
|
var item_list # ItemListDB (RefCounted)
|
||||||
var _assets_root := ""
|
var _assets_root := ""
|
||||||
var _tooltip_builder: RefCounted
|
var _tooltip_builder: RefCounted
|
||||||
|
var _tooltip_view: Control = null
|
||||||
var _root: Control
|
var _root: Control
|
||||||
var _tabbar: HBoxContainer
|
var _tabbar: HBoxContainer
|
||||||
var _grid: GridContainer
|
var _grid: GridContainer
|
||||||
@@ -134,6 +136,7 @@ func open() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
func _close() -> void:
|
func _close() -> void:
|
||||||
|
_hide_slot_tooltip()
|
||||||
_has_open_pos = false
|
_has_open_pos = false
|
||||||
if item_mouse and item_mouse.has_method("unregister_owner"):
|
if item_mouse and item_mouse.has_method("unregister_owner"):
|
||||||
item_mouse.unregister_owner(self)
|
item_mouse.unregister_owner(self)
|
||||||
@@ -419,6 +422,9 @@ func _slot(it: Dictionary, pos: int) -> Button:
|
|||||||
slot.tooltip_text = _tooltip_builder.format(vnum, maxi(1, count), pd, it)
|
slot.tooltip_text = _tooltip_builder.format(vnum, maxi(1, count), pd, it)
|
||||||
slot.tooltip_text += "\n价格:%d" % int(it.get("price", 0))
|
slot.tooltip_text += "\n价格:%d" % int(it.get("price", 0))
|
||||||
|
|
||||||
|
slot.mouse_entered.connect(func(): _show_slot_tooltip(vnum, maxi(1, count), it))
|
||||||
|
slot.mouse_exited.connect(func(): _hide_slot_tooltip())
|
||||||
|
|
||||||
slot.pressed.connect(func() -> void: _buy_slot(pos, it))
|
slot.pressed.connect(func() -> void: _buy_slot(pos, it))
|
||||||
slot.gui_input.connect(func(event: InputEvent) -> void:
|
slot.gui_input.connect(func(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
@@ -428,6 +434,31 @@ func _slot(it: Dictionary, pos: int) -> Button:
|
|||||||
slot.disabled = true
|
slot.disabled = true
|
||||||
return slot
|
return slot
|
||||||
|
|
||||||
|
func _get_tooltip_view() -> Control:
|
||||||
|
if _root and _root.get_parent() and _root.get_parent().has_method("get_item_tooltip_view"):
|
||||||
|
return _root.get_parent().get_item_tooltip_view(_assets_root, proto, item_list)
|
||||||
|
if _tooltip_view == null or not is_instance_valid(_tooltip_view):
|
||||||
|
_tooltip_view = ItemTooltipView.new()
|
||||||
|
_tooltip_view.setup(_assets_root, proto, item_list, _tooltip_builder)
|
||||||
|
_tooltip_view.visible = false
|
||||||
|
_tooltip_view.z_index = 200
|
||||||
|
if _root and is_instance_valid(_root):
|
||||||
|
_root.add_child(_tooltip_view)
|
||||||
|
return _tooltip_view
|
||||||
|
|
||||||
|
func _show_slot_tooltip(vnum: int, count: int, item_data: Dictionary) -> void:
|
||||||
|
if vnum <= 0:
|
||||||
|
return
|
||||||
|
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
|
||||||
|
var tv := _get_tooltip_view()
|
||||||
|
if tv:
|
||||||
|
tv.show_item(vnum, count, pd, item_data)
|
||||||
|
|
||||||
|
func _hide_slot_tooltip() -> void:
|
||||||
|
var tv := _get_tooltip_view()
|
||||||
|
if tv:
|
||||||
|
tv.hide_tooltip()
|
||||||
|
|
||||||
func _direct_buy(pos: int, item: Dictionary) -> void:
|
func _direct_buy(pos: int, item: Dictionary) -> void:
|
||||||
if _mode != 1 or item.is_empty() or pos < 0:
|
if _mode != 1 or item.is_empty() or pos < 0:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -451,3 +451,29 @@ func _on_handle_input(win: Control, ev: InputEvent) -> void:
|
|||||||
_drag_win = null
|
_drag_win = null
|
||||||
elif ev is InputEventMouseMotion and _drag_win == win:
|
elif ev is InputEventMouseMotion and _drag_win == win:
|
||||||
win.position = win.get_global_mouse_position() - _drag_from
|
win.position = win.get_global_mouse_position() - _drag_from
|
||||||
|
|
||||||
|
# --- 40250 ItemToolTip 统一悬浮窗 ---
|
||||||
|
var _item_tooltip_view: Control = null
|
||||||
|
|
||||||
|
func get_item_tooltip_view(assets_root := "", proto: Node = null, ilist: RefCounted = null) -> Control:
|
||||||
|
if _item_tooltip_view == null or not is_instance_valid(_item_tooltip_view):
|
||||||
|
var view_cls = load("res://ui/item_tooltip_view.gd")
|
||||||
|
_item_tooltip_view = view_cls.new()
|
||||||
|
_item_tooltip_view.z_index = 200
|
||||||
|
_item_tooltip_view.visible = false
|
||||||
|
_ensure_root()
|
||||||
|
_root.add_child(_item_tooltip_view)
|
||||||
|
if not assets_root.is_empty():
|
||||||
|
_item_tooltip_view.setup(assets_root, proto, ilist)
|
||||||
|
return _item_tooltip_view
|
||||||
|
|
||||||
|
func show_item_tooltip(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {},
|
||||||
|
player_ctx: Dictionary = {}, assets_root := "", proto: Node = null, ilist: RefCounted = null) -> void:
|
||||||
|
var view = get_item_tooltip_view(assets_root, proto, ilist)
|
||||||
|
if view:
|
||||||
|
view.show_item(vnum, count, proto_data, instance_data, player_ctx)
|
||||||
|
|
||||||
|
func hide_item_tooltip() -> void:
|
||||||
|
if _item_tooltip_view and is_instance_valid(_item_tooltip_view):
|
||||||
|
_item_tooltip_view.hide_tooltip()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user