feat(ui): 40250 item tooltip parity (thinboard, palette, sockets, stats)
This commit is contained in:
+363
-21
@@ -1,10 +1,24 @@
|
||||
# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一文字提示。
|
||||
# ItemTooltip —— item_proto + itemdesc + 物品实例状态的统一属性提示与 40250 模型构建。
|
||||
#
|
||||
# 参考:ClientVS22/POC/assets/root/uitooltip.py::ItemToolTip.AddItemData。
|
||||
# Godot 的原生 tooltip 负责显示位置与换行;本类只负责把同一份物品快照
|
||||
# 组织成标题、描述、限制、基础数值、附加属性、魂石槽和精炼关联。
|
||||
# 支持 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
|
||||
@@ -13,10 +27,19 @@ 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: "力量要求",
|
||||
@@ -160,14 +183,89 @@ const APPLY_NAMES := {
|
||||
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 _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:
|
||||
_descriptions.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)
|
||||
if FileAccess.file_exists(path):
|
||||
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)
|
||||
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:
|
||||
if vnum <= 0:
|
||||
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_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:
|
||||
@@ -275,16 +615,15 @@ func _special_title(vnum: int, item_name: String, sockets: Array) -> String:
|
||||
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]))
|
||||
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 _append_realtime_limit(lines: Array[String], proto_data: Dictionary, sockets: Array,
|
||||
instance_data: Dictionary) -> void:
|
||||
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
|
||||
return ""
|
||||
var limit_type := 0
|
||||
for limit in proto_data.get("limits", []):
|
||||
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
|
||||
break
|
||||
if limit_type == 0:
|
||||
return
|
||||
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
|
||||
return "首次使用后计时:%s" % _duration_text(value)
|
||||
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))
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user