774 lines
26 KiB
GDScript
774 lines
26 KiB
GDScript
# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一属性提示与 40250 模型构建。
|
||
#
|
||
# 参考:ClientVS22/POC/assets/root/uitooltip.py::ItemToolTip.AddItemData。
|
||
# 支持 40250 调色板、居中排版数据模型导出、攻防数值加算、固有属性与附加属性着色、
|
||
# 适用职业判断(红字警示)、魂石槽图文解析与纯文本向下兼容。
|
||
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_ARMOR := 2
|
||
const ITEM_TYPE_LOTTERY := 8
|
||
const ITEM_TYPE_FISH := 12
|
||
const ITEM_TYPE_ROD := 13
|
||
const ITEM_TYPE_UNIQUE := 16
|
||
const ITEM_TYPE_PICK := 24
|
||
const ITEM_TYPE_BLEND := 27
|
||
const ITEM_TYPE_COSTUME := 28
|
||
const ITEM_TYPE_DS := 29
|
||
const ITEM_TYPE_RING := 33
|
||
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 := {
|
||
1: "等级要求",
|
||
2: "力量要求",
|
||
3: "敏捷要求",
|
||
4: "智力要求",
|
||
5: "体力要求",
|
||
6: "网吧要求",
|
||
7: "实时期限",
|
||
8: "首次使用期限",
|
||
9: "穿戴计时",
|
||
}
|
||
|
||
const TYPE_NAMES := {
|
||
1: "武器",
|
||
2: "防具",
|
||
3: "使用品",
|
||
4: "自动使用品",
|
||
5: "材料",
|
||
6: "特殊物品",
|
||
7: "工具",
|
||
8: "抽奖品",
|
||
9: "金币",
|
||
10: "魔石",
|
||
11: "容器",
|
||
12: "鱼",
|
||
13: "鱼竿",
|
||
14: "资源",
|
||
16: "独特物品",
|
||
17: "技能书",
|
||
18: "任务物品",
|
||
19: "变身物品",
|
||
20: "宝箱",
|
||
21: "宝箱钥匙",
|
||
22: "遗忘书",
|
||
23: "礼盒",
|
||
24: "镐",
|
||
25: "发型",
|
||
26: "图腾",
|
||
27: "混合物",
|
||
28: "时装",
|
||
29: "龙魂石",
|
||
30: "特殊龙魂石",
|
||
31: "提取物",
|
||
32: "副货币",
|
||
33: "戒指",
|
||
34: "腰带",
|
||
}
|
||
|
||
# APPLY_* 编号来自 ClientVS22/source/GameLib/ItemData.h::EApplyTypes。
|
||
const APPLY_NAMES := {
|
||
1: "最大生命",
|
||
2: "最大法力",
|
||
3: "体质",
|
||
4: "智力",
|
||
5: "力量",
|
||
6: "敏捷",
|
||
7: "攻击速度",
|
||
8: "移动速度",
|
||
9: "施法速度",
|
||
10: "生命恢复",
|
||
11: "法力恢复",
|
||
12: "中毒概率",
|
||
13: "眩晕概率",
|
||
14: "缓慢概率",
|
||
15: "暴击概率",
|
||
16: "穿透概率",
|
||
17: "对人族伤害",
|
||
18: "对动物伤害",
|
||
19: "对兽人伤害",
|
||
20: "对秘境伤害",
|
||
21: "对不死族伤害",
|
||
22: "对恶魔伤害",
|
||
23: "生命吸收",
|
||
24: "法力吸收",
|
||
25: "法力燃烧",
|
||
26: "伤害转法力",
|
||
27: "格挡",
|
||
28: "闪避",
|
||
29: "剑防御",
|
||
30: "双手防御",
|
||
31: "匕首防御",
|
||
32: "铃防御",
|
||
33: "扇防御",
|
||
34: "弓防御",
|
||
35: "火抗性",
|
||
36: "雷抗性",
|
||
37: "魔法抗性",
|
||
38: "风抗性",
|
||
39: "近战反射",
|
||
40: "诅咒反射",
|
||
41: "中毒抵抗",
|
||
42: "击杀恢复法力",
|
||
43: "经验加成",
|
||
44: "金币加成",
|
||
45: "掉落加成",
|
||
46: "药水效果",
|
||
47: "击杀恢复生命",
|
||
48: "免疫眩晕",
|
||
49: "免疫缓慢",
|
||
50: "免疫跌落",
|
||
51: "技能",
|
||
52: "弓箭距离",
|
||
53: "攻击等级",
|
||
54: "防御等级",
|
||
55: "魔法攻击等级",
|
||
56: "魔法防御等级",
|
||
57: "诅咒概率",
|
||
58: "最大耐力",
|
||
59: "对战士伤害",
|
||
60: "对刺客伤害",
|
||
61: "对武士伤害",
|
||
62: "对萨满伤害",
|
||
63: "对怪物伤害",
|
||
64: "商城攻击加成",
|
||
65: "商城防御加成",
|
||
66: "商城经验加成",
|
||
67: "商城掉落加成",
|
||
68: "商城金币加成",
|
||
69: "最大生命百分比",
|
||
70: "最大法力百分比",
|
||
71: "技能伤害",
|
||
72: "普通攻击伤害",
|
||
73: "技能防御",
|
||
74: "普通攻击防御",
|
||
75: "生命提取",
|
||
76: "网吧经验加成",
|
||
77: "网吧掉落加成",
|
||
78: "战士抗性",
|
||
79: "刺客抗性",
|
||
80: "武士抗性",
|
||
81: "萨满抗性",
|
||
82: "能量",
|
||
83: "防御",
|
||
84: "时装属性加成",
|
||
85: "魔法攻击百分比",
|
||
86: "近战魔法攻击百分比",
|
||
87: "冰抗性",
|
||
88: "土抗性",
|
||
89: "暗抗性",
|
||
90: "抗暴击",
|
||
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 _item_names: Dictionary = {}
|
||
var _skill_names: Dictionary = {}
|
||
var _proto_node: Object = null
|
||
var _assets_root: String = ""
|
||
|
||
func setup(assets_root: String, lang := "", proto_node: Object = null) -> bool:
|
||
_descriptions.clear()
|
||
_item_names.clear()
|
||
_skill_names.clear()
|
||
_assets_root = assets_root
|
||
_proto_node = proto_node
|
||
if lang.is_empty():
|
||
var LocaleScript = load("res://locale.gd")
|
||
if LocaleScript and LocaleScript.has_method("get_default_lang"):
|
||
lang = LocaleScript.get_default_lang()
|
||
else:
|
||
lang = "zh"
|
||
var names_path := assets_root.path_join("locale/locale/%s/item_names.txt" % lang)
|
||
if FileAccess.file_exists(names_path):
|
||
var nf := FileAccess.open(names_path, FileAccess.READ)
|
||
if nf:
|
||
while not nf.eof_reached():
|
||
var line := nf.get_line().strip_edges()
|
||
if line.is_empty() or line.begins_with("#") or line.begins_with("VNUM"):
|
||
continue
|
||
var tab := line.find("\t")
|
||
if tab < 0:
|
||
tab = line.find(" ")
|
||
if tab < 0:
|
||
continue
|
||
var k := line.substr(0, tab).strip_edges()
|
||
var v := line.substr(tab + 1).strip_edges()
|
||
if k.is_valid_int():
|
||
_item_names[int(k)] = v
|
||
var path := assets_root.path_join("locale/locale/%s/itemdesc.txt" % lang)
|
||
if FileAccess.file_exists(path):
|
||
var file := FileAccess.open(path, FileAccess.READ)
|
||
if file:
|
||
while not file.eof_reached():
|
||
var parts := file.get_line().split("\t", true)
|
||
if parts.size() < 2:
|
||
continue
|
||
var vnum := int(parts[0].strip_edges())
|
||
if vnum <= 0:
|
||
continue
|
||
_descriptions[vnum] = {
|
||
"name": parts[1].strip_edges(),
|
||
"description": parts[2].strip_edges() if parts.size() > 2 else "",
|
||
}
|
||
_load_skill_names(assets_root, lang)
|
||
return not _descriptions.is_empty() or not _skill_names.is_empty() or not _item_names.is_empty()
|
||
|
||
func get_proto_node() -> Object:
|
||
return _proto_node
|
||
|
||
func get_stone_info(stone_vnum: int) -> Dictionary:
|
||
var name := ""
|
||
if _item_names.has(stone_vnum):
|
||
name = String(_item_names[stone_vnum])
|
||
var affect := ""
|
||
var icon_path := ""
|
||
if name.is_empty() and _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", "")))
|
||
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)
|
||
elif not affect.is_empty() or (_proto_node and _proto_node.has_method("item")):
|
||
var sp: Dictionary = _proto_node.item(stone_vnum)
|
||
if not sp.is_empty():
|
||
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.is_empty() and _descriptions.has(stone_vnum):
|
||
name = String(_descriptions[stone_vnum].get("name", ""))
|
||
if name.is_empty():
|
||
name = "魂石 #%d" % stone_vnum
|
||
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 = _item_names.get(vnum, "")
|
||
if name.is_empty():
|
||
name = String(proto_data.get("locale_name", proto_data.get("name", "")))
|
||
if name.is_empty() and _descriptions.has(vnum):
|
||
name = String(_descriptions[vnum].get("name", ""))
|
||
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:
|
||
if vnum <= 0:
|
||
return ""
|
||
var name: String = _item_names.get(vnum, "")
|
||
if name.is_empty():
|
||
name = String(proto_data.get("locale_name", proto_data.get("name", "")))
|
||
if name.is_empty() and _descriptions.has(vnum):
|
||
name = String(_descriptions[vnum].get("name", ""))
|
||
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 lines: Array[String] = [name]
|
||
if count > 1:
|
||
lines.append("数量:%d" % count)
|
||
var desc: Dictionary = _descriptions.get(vnum, {})
|
||
if not String(desc.get("description", "")).is_empty():
|
||
lines.append(String(desc["description"]))
|
||
|
||
var item_type := int(proto_data.get("type", 0))
|
||
var type_name: String = TYPE_NAMES.get(item_type, "")
|
||
if not type_name.is_empty():
|
||
lines.append("类型:" + type_name)
|
||
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])
|
||
if min_attack > 0 or max_attack > 0:
|
||
lines.append("攻击力:%d - %d" % [min_attack, max_attack])
|
||
if int(values[1]) > 0 or int(values[2]) > 0:
|
||
lines.append("魔法攻击:%d - %d" % [int(values[1]), int(values[2])])
|
||
elif item_type == ITEM_TYPE_ARMOR and values.size() >= 6:
|
||
if int(values[1]) > 0:
|
||
lines.append("防御力:%d" % (int(values[1]) + int(values[5]) * 2))
|
||
if int(values[0]) > 0:
|
||
lines.append("魔法防御:%d" % int(values[0]))
|
||
_append_special_item_data(lines, item_type, vnum, values, proto_data, instance_data)
|
||
|
||
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:
|
||
lines.append("PC房专用道具")
|
||
continue
|
||
if limit_type != 0 and limit_value > 0:
|
||
lines.append("%s:%d" % [LIMIT_NAMES.get(limit_type, "限制 %d" % limit_type), limit_value])
|
||
for apply in proto_data.get("applies", []):
|
||
_append_bonus(lines, int(apply.get("type", 0)), int(apply.get("value", 0)))
|
||
for attr in instance_data.get("attrs", proto_data.get("attrs", [])):
|
||
_append_bonus(lines, int(attr.get("type", 0)), int(attr.get("value", 0)), "附加")
|
||
|
||
var has_socket := false
|
||
for socket in sockets:
|
||
if int(socket) != 0:
|
||
has_socket = true
|
||
break
|
||
if has_socket:
|
||
for i in sockets.size():
|
||
var socket_value := int(sockets[i])
|
||
lines.append("孔 %d:%s" % [i + 1, "空" if socket_value == 0 else ("损坏" if socket_value < 0 else "#%d" % socket_value)])
|
||
|
||
var refined_vnum := int(proto_data.get("refined_vnum", 0))
|
||
if refined_vnum > 0:
|
||
lines.append("精炼后:#%d" % refined_vnum)
|
||
var refine_set := int(proto_data.get("refine_set", 0))
|
||
if refine_set > 0:
|
||
lines.append("精炼套件:%d" % refine_set)
|
||
_append_realtime_limit(lines, proto_data, sockets, instance_data)
|
||
return "\n".join(lines)
|
||
|
||
func _load_skill_names(assets_root: String, lang: String) -> void:
|
||
var path := assets_root.path_join("locale/locale/%s/skilldesc.txt" % lang)
|
||
if not FileAccess.file_exists(path):
|
||
return
|
||
var file := FileAccess.open(path, FileAccess.READ)
|
||
if file == null:
|
||
return
|
||
while not file.eof_reached():
|
||
var parts := file.get_line().split("\t", false)
|
||
if parts.size() < 3:
|
||
continue
|
||
var skill_id := int(parts[0])
|
||
var skill_name := parts[2].strip_edges()
|
||
if skill_id > 0 and skill_name != "":
|
||
_skill_names[skill_id] = skill_name
|
||
|
||
func _special_title(vnum: int, item_name: String, sockets: Array) -> String:
|
||
if sockets.is_empty() or int(sockets[0]) <= 0:
|
||
return item_name
|
||
if vnum == 50300 or vnum == 70037 or vnum == 70055:
|
||
var skill_name := String(_skill_names.get(int(sockets[0]), ""))
|
||
return skill_name + " " + item_name if skill_name != "" else item_name
|
||
if vnum >= 70103 and vnum <= 70106 and _proto_node and _proto_node.has_method("mob"):
|
||
var mob: Dictionary = _proto_node.mob(int(sockets[0]))
|
||
var mob_name := String(mob.get("locale_name", mob.get("name", "")))
|
||
return mob_name + " " + item_name if mob_name != "" else item_name
|
||
return item_name
|
||
|
||
func _get_realtime_text(proto_data: Dictionary, sockets: Array, instance_data: Dictionary) -> String:
|
||
if bool(instance_data.get("realtime_rendered", false)) or sockets.is_empty():
|
||
return ""
|
||
var limit_type := 0
|
||
for limit in proto_data.get("limits", []):
|
||
var candidate := int(limit.get("type", 0))
|
||
if candidate == 7 or candidate == 8 or candidate == 9:
|
||
limit_type = candidate
|
||
break
|
||
if limit_type == 0:
|
||
return ""
|
||
var value := int(sockets[0])
|
||
var remaining := 0
|
||
if limit_type == 8:
|
||
var used := int(sockets[1]) if sockets.size() > 1 else 0
|
||
if used == 0:
|
||
return "首次使用后计时:%s" % _duration_text(value)
|
||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||
elif limit_type == 7:
|
||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||
else:
|
||
remaining = maxi(0, value)
|
||
if remaining <= 0:
|
||
return "已超时"
|
||
return _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:
|
||
var minutes := seconds / 60
|
||
var rest := seconds % 60
|
||
var text := "%d秒" % rest
|
||
if minutes > 0:
|
||
text = "%d分%s" % [minutes, text] if rest > 0 else "%d分" % minutes
|
||
return text
|
||
|
||
func _append_special_item_data(lines: Array[String], item_type: int, vnum: int,
|
||
values: Array, proto_data: Dictionary, instance_data: Dictionary) -> void:
|
||
var sockets: Array = instance_data.get("sockets", [])
|
||
if item_type == ITEM_TYPE_ROD or item_type == ITEM_TYPE_PICK:
|
||
if values.size() >= 3 and not sockets.is_empty():
|
||
var level := int(values[0]) / 10
|
||
var current_exp := int(sockets[0])
|
||
var max_exp := int(values[2])
|
||
lines.append("等级:%d" % level)
|
||
lines.append("经验:%d / %d" % [current_exp, max_exp])
|
||
if max_exp > 0 and current_exp >= max_exp:
|
||
lines.append("经验已满,可升级")
|
||
elif item_type == ITEM_TYPE_FISH:
|
||
if not sockets.is_empty() and int(sockets[0]) > 0:
|
||
lines.append("长度:%.2f cm" % (float(sockets[0]) / 100.0))
|
||
elif item_type == ITEM_TYPE_LOTTERY:
|
||
if sockets.size() >= 2 and (int(sockets[0]) != 0 or int(sockets[1]) != 0):
|
||
lines.append("彩票号码:%d" % int(sockets[0]))
|
||
lines.append("彩票步数:%d" % int(sockets[1]))
|
||
elif item_type == ITEM_TYPE_BLEND:
|
||
if sockets.size() >= 3 and int(sockets[0]) != 0:
|
||
var affect_type := int(sockets[0])
|
||
var affect_value := int(sockets[1])
|
||
_append_bonus(lines, affect_type, affect_value, "效果:")
|
||
var duration := int(sockets[2])
|
||
lines.append("持续时间:%d 秒" % duration if duration > 0 else "无持续时间")
|
||
elif item_type == ITEM_TYPE_UNIQUE:
|
||
_append_item_remaining(lines, proto_data, sockets, instance_data)
|
||
elif item_type == ITEM_TYPE_DS:
|
||
var step := (vnum / 100) % 10
|
||
var refine := (vnum / 10) % 10
|
||
if step >= 0 and step <= 4:
|
||
lines.append("龙魂石阶段:%d" % (step + 1))
|
||
lines.append("龙魂石强化:%d" % refine)
|
||
|
||
func _append_item_remaining(lines: Array[String], proto_data: Dictionary, sockets: Array,
|
||
instance_data: Dictionary) -> void:
|
||
var remaining := int(instance_data.get("remaining_seconds", instance_data.get("duration", 0)))
|
||
if remaining <= 0 and not sockets.is_empty():
|
||
var limit_type := int(instance_data.get("limit_type", 0))
|
||
if limit_type == 0:
|
||
for limit in proto_data.get("limits", []):
|
||
if int(limit.get("type", 0)) != 0:
|
||
limit_type = int(limit.get("type", 0))
|
||
break
|
||
if limit_type == 7 or limit_type == 8:
|
||
remaining = maxi(0, int(sockets[0]) - int(Time.get_unix_time_from_system()))
|
||
elif limit_type == 9:
|
||
remaining = int(sockets[0])
|
||
elif sockets.size() >= 3:
|
||
remaining = int(sockets[2]) * 60
|
||
if remaining > 0:
|
||
lines.append("剩余时间:%d 秒" % remaining)
|
||
|
||
func _append_bonus(lines: Array[String], apply_type: int, value: int, prefix := "") -> void:
|
||
if apply_type == 0 or value == 0:
|
||
return
|
||
var label: String = APPLY_NAMES.get(apply_type, "属性 %d" % apply_type)
|
||
if not prefix.is_empty():
|
||
label = prefix + label
|
||
lines.append("%s:%+d" % [label, value])
|