完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+368
View File
@@ -0,0 +1,368 @@
# 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 != 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 == 7 or limit_type == 8:
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
else:
remaining = maxi(0, value)
if remaining <= 0:
lines.append("剩余时间:已超时")
return
var minutes := remaining / 60
var seconds := remaining % 60
var text := "%d" % seconds
if minutes > 0:
text = "%d%s" % [minutes, text] if seconds > 0 else "%d" % minutes
lines.append("剩余时间:%s" % 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])