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