# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一文字提示。 # # 参考:ClientVS22/POC/assets/root/uitooltip.py::ItemToolTip.AddItemData。 # Godot 的原生 tooltip 负责显示位置与换行;本类只负责把同一份物品快照 # 组织成标题、描述、限制、基础数值、附加属性、魂石槽和精炼关联。 extends RefCounted 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_DS := 29 const ITEM_TYPE_RING := 33 const ITEM_TYPE_BELT := 34 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: "抗穿透", } var _descriptions: Dictionary = {} var _skill_names: Dictionary = {} var _mob_proto: Object = null func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool: _descriptions.clear() _skill_names.clear() _mob_proto = proto_node 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() func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {}) -> String: 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 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: # LIMIT_PCBANG 只是“网吧专用”标记,不是装备门禁(服务器 item.cpp:1899)。 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 _mob_proto and _mob_proto.has_method("mob"): var mob: Dictionary = _mob_proto.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 _append_realtime_limit(lines: Array[String], proto_data: Dictionary, sockets: Array, instance_data: Dictionary) -> void: 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: # 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 if used == 0: lines.append("首次使用后计时:%s" % _duration_text(value)) return remaining = maxi(0, value - int(Time.get_unix_time_from_system())) 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())) else: remaining = maxi(0, value) if remaining <= 0: lines.append("剩余时间:已超时") return lines.append("剩余时间:%s" % _duration_text(remaining)) 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])