- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
1247 lines
44 KiB
GDScript
1247 lines
44 KiB
GDScript
# CharStatusUI (P11) —— 角色状态窗(1:1 迁移 `assets/root/uicharacter.py` 的 STATUS 页)。
|
||
#
|
||
# var cs := preload("res://ui/char_status_ui.gd").new()
|
||
# add_child(cs)
|
||
# cs.setup(ui_manager, m2client)
|
||
# cs.toggle() # V 键
|
||
#
|
||
# 布局直接走 `assets/uiscript/uiscript/characterwindow.py`(UiScript → UiBuild),
|
||
# 数值绑定逐字对照 uicharacter.py.RefreshStatus:
|
||
# Level/Exp/RestExp、HP/SP、STR/DEX/HTH/INT、ATT/DEF、MATT/MDEF、ASPD/MSPD/CSPD/ER。
|
||
# 加点:HTH/INT/STR/DEX 的 + / - 按钮发聊天命令 `/stat ht` `/stat- ht`(与原客户端
|
||
# `statusPlusCommandDict` 完全一致)。points_changed 信号来时刷新。
|
||
#
|
||
# 技能 / 表情 / 任务三页只做页签切换(保留 uiscript 静态布局),实际数据在各自
|
||
# 的专用窗(SkillUI / QuestLog)里。
|
||
extends Node
|
||
|
||
const UiScript = preload("res://ui/uiscript.gd")
|
||
const UiBuild = preload("res://ui/ui_build.gd")
|
||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||
const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd")
|
||
const SkillSlotMap = preload("res://skill_slot_map.gd")
|
||
|
||
# --- EPointTypes(m2dev Packet.h),只列本窗要用的 ---
|
||
const P_LEVEL := 1
|
||
const P_EXP := 3
|
||
const P_NEXT_EXP := 4
|
||
const P_HP := 5
|
||
const P_MAX_HP := 6
|
||
const P_SP := 7
|
||
const P_MAX_SP := 8
|
||
const P_ST := 12 # 근력 STR
|
||
const P_HT := 13 # 체력 HTH
|
||
const P_DX := 14 # 민첩 DEX
|
||
const P_IQ := 15 # 정신 INT
|
||
const P_ATT_POWER := 16
|
||
const P_ATT_SPEED := 17
|
||
const P_EVADE_RATE := 18
|
||
const P_MOV_SPEED := 19
|
||
const P_DEF_GRADE := 20
|
||
const P_CASTING_SPEED := 21
|
||
const P_MAGIC_ATT_GRADE := 22
|
||
const P_MAGIC_DEF_GRADE := 23
|
||
const P_STAT := 26 # 남은 능력치 포인트
|
||
const P_SKILL := 27 # 남은 스킬 포인트
|
||
const P_MIN_ATK := 29
|
||
const P_MAX_ATK := 30
|
||
const P_PARTY_ATT_GRADE := 91 # player.ATTACKER_BONUS
|
||
const P_ATT_GRADE_BONUS := 95 # player.ATT_BONUS
|
||
const P_DEF_GRADE_BONUS := 96 # player.DEF_BONUS
|
||
const P_MIN_MAGIC_WEP := 202
|
||
const P_MAX_MAGIC_WEP := 203
|
||
|
||
const EMOTE_NAMES := {
|
||
1: "喝彩", 2: "拍手", 3: "肯定", 4: "原谅",
|
||
5: "愤怒", 6: "诱惑", 7: "悲伤", 8: "害羞",
|
||
9: "欢呼", 10: "嘲讽", 11: "思考", 12: "跳舞 1",
|
||
13: "跳舞 2", 14: "跳舞 3", 15: "跳舞 4", 16: "跳舞 5",
|
||
17: "跳舞 6", 18: "大笑",
|
||
51: "亲吻", 52: "法式深吻", 53: "耳光",
|
||
}
|
||
|
||
const EMOTE_ICONS := {
|
||
1: "ETC/ymir work/ui/game/windows/emotion_cheers_1.sub",
|
||
2: "ETC/ymir work/ui/game/windows/emotion_clap.sub",
|
||
3: "icon/icon/action/congratulation.tga",
|
||
4: "icon/icon/action/forgive.tga",
|
||
5: "icon/icon/action/angry.tga",
|
||
6: "icon/icon/action/attractive.tga",
|
||
7: "icon/icon/action/sad.tga",
|
||
8: "icon/icon/action/shy.tga",
|
||
9: "icon/icon/action/cheerup.tga",
|
||
10: "icon/icon/action/banter.tga",
|
||
11: "icon/icon/action/joy.tga",
|
||
12: "icon/icon/action/dance1.tga",
|
||
13: "icon/icon/action/dance2.tga",
|
||
14: "icon/icon/action/dance3.tga",
|
||
15: "icon/icon/action/dance4.tga",
|
||
16: "icon/icon/action/dance5.tga",
|
||
17: "icon/icon/action/dance6.tga",
|
||
18: "ETC/ymir work/ui/game/windows/emotion_cheers_2.sub",
|
||
51: "ETC/ymir work/ui/game/windows/emotion_kiss.sub",
|
||
52: "ETC/ymir work/ui/game/windows/emotion_french_kiss.sub",
|
||
53: "ETC/ymir work/ui/game/windows/emotion_slap.sub",
|
||
}
|
||
|
||
# uicharacter.py statusPlusCommandDict / statusMinusCommandDict
|
||
const PLUS_CMD := {"HTH": "/stat ht", "INT": "/stat iq", "STR": "/stat st", "DEX": "/stat dx"}
|
||
const MINUS_CMD := {"HTH": "/stat- ht", "INT": "/stat- iq", "STR": "/stat- st", "DEX": "/stat- dx"}
|
||
|
||
const STATES := ["STATUS", "SKILL", "EMOTICON", "QUEST"]
|
||
const PAGE := {"STATUS": "Character_Page", "SKILL": "Skill_Page", "EMOTICON": "Emoticon_Page", "QUEST": "Quest_Page"}
|
||
const TITLEBAR := {"STATUS": "Character_TitleBar", "SKILL": "Skill_TitleBar", "EMOTICON": "Emoticon_TitleBar", "QUEST": "Quest_TitleBar"}
|
||
const TAB_BUTTON := {"STATUS": "Tab_Button_01", "SKILL": "Tab_Button_02", "EMOTICON": "Tab_Button_03", "QUEST": "Tab_Button_04"}
|
||
const TAB_IMAGE := {"STATUS": "Tab_01", "SKILL": "Tab_02", "EMOTICON": "Tab_03", "QUEST": "Tab_04"}
|
||
const FACE_BY_JOB := ["face_warrior", "face_assassin", "face_sura", "face_shaman"]
|
||
|
||
var ui: CanvasLayer # UiManager
|
||
var client: Node # M2Client
|
||
var skill_table: RefCounted # SkillTable
|
||
var item_mouse: Node # MouseController
|
||
var drag_skill_id := 0 # Quickbar 可读取的待拖拽技能
|
||
var _preview_group := 1 # 40250 uicharacter.py curSelectedSkillGroup (group==0 时预览分支)
|
||
var assets_root := ""
|
||
var uiscript_dir := ""
|
||
|
||
var _win: Dictionary = {} # { root, nodes }
|
||
var _state := "STATUS"
|
||
var _mobile_mode := false
|
||
var _mobile_root: Control
|
||
var _mobile_status_page: Control
|
||
var _mobile_emote_page: Control
|
||
var _mobile_labels := {}
|
||
var _mobile_stats := {}
|
||
var _mobile_equipment_labels: Array[Label] = []
|
||
var _mobile_hint: Label
|
||
var _quest_showing_start_index := 0
|
||
|
||
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "", st_table: RefCounted = null, imouse: Node = null) -> void:
|
||
ui = ui_manager
|
||
client = m2client
|
||
assets_root = assets
|
||
if imouse != null:
|
||
item_mouse = imouse
|
||
if assets_root == "" and ui and "assets_root" in ui:
|
||
assets_root = ui.assets_root
|
||
uiscript_dir = assets_root.path_join("uiscript/uiscript")
|
||
if not DirAccess.dir_exists_absolute(uiscript_dir):
|
||
uiscript_dir = assets_root.path_join("uiscript")
|
||
skill_table = st_table
|
||
if skill_table == null:
|
||
var st_script := load("res://ui/skill_table.gd")
|
||
if st_script:
|
||
skill_table = st_script.new()
|
||
for lang in ["en", "common"]:
|
||
var desc_path := assets_root.path_join("locale/locale/%s/skilldesc.txt" % lang)
|
||
if not FileAccess.file_exists(desc_path):
|
||
desc_path = assets_root.path_join("locale/%s/skilldesc.txt" % lang)
|
||
if FileAccess.file_exists(desc_path) and skill_table.has_method("load_file"):
|
||
skill_table.load_file(desc_path)
|
||
break
|
||
for lang in ["en", "common"]:
|
||
var tbl_path := assets_root.path_join("locale/locale/%s/skilltable.txt" % lang)
|
||
if not FileAccess.file_exists(tbl_path):
|
||
tbl_path = assets_root.path_join("locale/%s/skilltable.txt" % lang)
|
||
if FileAccess.file_exists(tbl_path) and skill_table.has_method("load_table"):
|
||
skill_table.load_table(tbl_path)
|
||
break
|
||
elif skill_table != null and skill_table.has_method("load_table") and skill_table.has_method("is_passive"):
|
||
if not skill_table.is_passive(121):
|
||
for lang in ["en", "common"]:
|
||
var tbl_path := assets_root.path_join("locale/locale/%s/skilltable.txt" % lang)
|
||
if not FileAccess.file_exists(tbl_path):
|
||
tbl_path = assets_root.path_join("locale/%s/skilltable.txt" % lang)
|
||
if FileAccess.file_exists(tbl_path):
|
||
skill_table.load_table(tbl_path)
|
||
break
|
||
if client and client.has_signal("points_changed"):
|
||
client.points_changed.connect(_on_points_changed)
|
||
if client and client.has_signal("entity_main_set"):
|
||
client.entity_main_set.connect(func(_v): if is_open(): _refresh())
|
||
if client and client.has_signal("skills_changed"):
|
||
client.skills_changed.connect(func(): if is_open(): _refresh())
|
||
if client and client.has_signal("skill_group_changed"):
|
||
client.skill_group_changed.connect(func(_g): if is_open(): _refresh())
|
||
if ui and ui.has_signal("window_closed"):
|
||
ui.window_closed.connect(func(w):
|
||
if not _win.is_empty() and _win.get("root") == w:
|
||
_win = {}
|
||
)
|
||
|
||
func set_job(_j: String) -> void:
|
||
pass
|
||
|
||
func set_mobile_mode(enabled: bool) -> void:
|
||
if enabled != _mobile_mode and is_open():
|
||
var old_mode := _mobile_mode
|
||
_mobile_mode = true
|
||
close()
|
||
_mobile_mode = old_mode
|
||
_mobile_mode = enabled
|
||
|
||
# --- open / close -----------------------------------------------------
|
||
|
||
func is_open() -> bool:
|
||
if _mobile_mode:
|
||
return _mobile_root != null and is_instance_valid(_mobile_root)
|
||
return not _win.is_empty() and is_instance_valid(_win.get("root"))
|
||
|
||
func toggle() -> void:
|
||
if is_open(): close()
|
||
else: open("STATUS")
|
||
|
||
func close() -> void:
|
||
if _mobile_mode:
|
||
if _mobile_root and is_instance_valid(_mobile_root) and ui:
|
||
ui.close(_mobile_root)
|
||
_mobile_root = null
|
||
_mobile_status_page = null
|
||
_mobile_emote_page = null
|
||
_mobile_labels.clear()
|
||
_mobile_stats.clear()
|
||
_mobile_equipment_labels.clear()
|
||
return
|
||
if is_open():
|
||
ui.close(_win["root"])
|
||
_win = {}
|
||
|
||
func open(state := "") -> void:
|
||
if state != "" and state in STATES:
|
||
_state = state
|
||
if _mobile_mode:
|
||
_open_mobile()
|
||
return
|
||
if is_open():
|
||
_set_state(_state)
|
||
_refresh()
|
||
return
|
||
var path := uiscript_dir.path_join("characterwindow.py")
|
||
if not FileAccess.file_exists(path):
|
||
push_warning("CharStatusUI: no characterwindow.py at " + path)
|
||
return
|
||
_win = ui.open_script(path, assets_root)
|
||
if not is_open():
|
||
return
|
||
var root_w: Control = _win.get("root") as Control
|
||
if root_w:
|
||
var vp_size := Vector2(800.0, 600.0)
|
||
if root_w.is_inside_tree():
|
||
vp_size = root_w.get_viewport_rect().size
|
||
elif ui and ui.is_inside_tree():
|
||
vp_size = ui.get_viewport().get_visible_rect().size
|
||
var target_y: float = (vp_size.y - 37.0 - 361.0) / 2.0
|
||
root_w.position = Vector2(24.0, maxf(10.0, target_y))
|
||
_wire_titlebars()
|
||
_wire_tabs()
|
||
_wire_stat_buttons()
|
||
_wire_skills()
|
||
_wire_emoticons()
|
||
_wire_quests()
|
||
_set_state(_state)
|
||
_refresh()
|
||
|
||
func _open_mobile() -> void:
|
||
if is_open():
|
||
_mobile_set_state(_state)
|
||
_refresh_mobile()
|
||
return
|
||
_mobile_root = Control.new()
|
||
_mobile_root.name = "MobileCharacterWindow"
|
||
_mobile_root.size = Vector2(760, 340)
|
||
_mobile_root.set_meta("mobile_title", "角色")
|
||
_mobile_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_build_mobile_view()
|
||
ui.open(_mobile_root)
|
||
_mobile_set_state(_state)
|
||
_refresh_mobile()
|
||
|
||
func _build_mobile_view() -> void:
|
||
var panel := Panel.new()
|
||
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
panel.add_theme_stylebox_override("panel", _mobile_panel_style())
|
||
_mobile_root.add_child(panel)
|
||
|
||
var tabs := HBoxContainer.new()
|
||
tabs.position = Vector2(14, 48)
|
||
tabs.size = Vector2(250, 38)
|
||
tabs.add_theme_constant_override("separation", 6)
|
||
_mobile_root.add_child(tabs)
|
||
var status_tab := MobileTouchButton.new()
|
||
status_tab.setup("属性 / 装备", Color(0.65, 0.78, 0.98, 0.95))
|
||
status_tab.set_rect_style()
|
||
status_tab.custom_minimum_size = Vector2(118, 36)
|
||
status_tab.size = Vector2(118, 36)
|
||
status_tab.pressed.connect(func(): _mobile_set_state("STATUS"))
|
||
tabs.add_child(status_tab)
|
||
var emote_tab := MobileTouchButton.new()
|
||
emote_tab.setup("表情", Color(0.65, 0.78, 0.98, 0.95))
|
||
emote_tab.set_rect_style()
|
||
emote_tab.custom_minimum_size = Vector2(86, 36)
|
||
emote_tab.size = Vector2(86, 36)
|
||
emote_tab.pressed.connect(func(): _mobile_set_state("EMOTICON"))
|
||
tabs.add_child(emote_tab)
|
||
|
||
_mobile_status_page = Control.new()
|
||
_mobile_status_page.name = "StatusPage"
|
||
_mobile_status_page.position = Vector2(14, 92)
|
||
_mobile_status_page.size = Vector2(732, 236)
|
||
_mobile_root.add_child(_mobile_status_page)
|
||
_build_mobile_profile()
|
||
_build_mobile_stats()
|
||
_build_mobile_equipment()
|
||
|
||
_mobile_emote_page = Control.new()
|
||
_mobile_emote_page.name = "EmotePage"
|
||
_mobile_emote_page.position = Vector2(14, 92)
|
||
_mobile_emote_page.size = Vector2(732, 236)
|
||
_mobile_root.add_child(_mobile_emote_page)
|
||
_build_mobile_emotes()
|
||
|
||
func _build_mobile_profile() -> void:
|
||
var profile := Panel.new()
|
||
profile.position = Vector2(0, 0)
|
||
profile.size = Vector2(190, 236)
|
||
profile.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
|
||
_mobile_status_page.add_child(profile)
|
||
var title := Label.new()
|
||
title.text = "角色信息"
|
||
title.position = Vector2(12, 10)
|
||
title.add_theme_font_size_override("font_size", 14)
|
||
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
|
||
profile.add_child(title)
|
||
for data in [["name", "角色"], ["guild", "公会"], ["level", "等级"], ["exp", "经验"], ["hp", "生命"], ["sp", "法力"]]:
|
||
var row := HBoxContainer.new()
|
||
row.position = Vector2(12, 38 + _mobile_labels.size() * 29)
|
||
row.size = Vector2(166, 24)
|
||
var caption := Label.new()
|
||
caption.text = String(data[1])
|
||
caption.custom_minimum_size = Vector2(44, 22)
|
||
caption.add_theme_font_size_override("font_size", 11)
|
||
row.add_child(caption)
|
||
var value := Label.new()
|
||
value.name = String(data[0])
|
||
value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
value.add_theme_font_size_override("font_size", 11)
|
||
value.add_theme_color_override("font_color", Color(0.82, 0.9, 1.0))
|
||
row.add_child(value)
|
||
profile.add_child(row)
|
||
_mobile_labels[String(data[0])] = value
|
||
|
||
func _build_mobile_stats() -> void:
|
||
var stats := Panel.new()
|
||
stats.position = Vector2(200, 0)
|
||
stats.size = Vector2(270, 236)
|
||
stats.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
|
||
_mobile_status_page.add_child(stats)
|
||
var title := Label.new()
|
||
title.text = "属性"
|
||
title.position = Vector2(12, 10)
|
||
title.add_theme_font_size_override("font_size", 14)
|
||
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
|
||
stats.add_child(title)
|
||
var points := Label.new()
|
||
points.name = "stat_points"
|
||
points.position = Vector2(146, 10)
|
||
points.size = Vector2(110, 22)
|
||
points.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
points.add_theme_font_size_override("font_size", 11)
|
||
points.add_theme_color_override("font_color", Color(0.96, 0.78, 0.35))
|
||
stats.add_child(points)
|
||
_mobile_labels["stat_points"] = points
|
||
for i in 4:
|
||
var key: String = ["STR", "HTH", "DEX", "INT"][i]
|
||
var row := HBoxContainer.new()
|
||
row.position = Vector2(12 + (i % 2) * 128, 44 + (i / 2) * 58)
|
||
row.size = Vector2(118, 48)
|
||
var caption := Label.new()
|
||
caption.text = key
|
||
caption.custom_minimum_size = Vector2(34, 42)
|
||
caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
caption.add_theme_font_size_override("font_size", 12)
|
||
row.add_child(caption)
|
||
var value := Label.new()
|
||
value.name = "value"
|
||
value.custom_minimum_size = Vector2(34, 42)
|
||
value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
value.add_theme_font_size_override("font_size", 13)
|
||
row.add_child(value)
|
||
var minus := MobileTouchButton.new()
|
||
minus.setup("-", Color(0.72, 0.74, 0.82, 0.9))
|
||
minus.set_rect_style()
|
||
minus.custom_minimum_size = Vector2(24, 42)
|
||
minus.size = Vector2(24, 42)
|
||
minus.pressed.connect(func(): _send_stat(MINUS_CMD[key]))
|
||
row.add_child(minus)
|
||
var plus := MobileTouchButton.new()
|
||
plus.setup("+", Color(0.95, 0.72, 0.3, 0.95))
|
||
plus.set_rect_style()
|
||
plus.custom_minimum_size = Vector2(24, 42)
|
||
plus.size = Vector2(24, 42)
|
||
plus.pressed.connect(func(): _send_stat(PLUS_CMD[key]))
|
||
row.add_child(plus)
|
||
stats.add_child(row)
|
||
_mobile_stats[key] = {"value": value, "minus": minus, "plus": plus}
|
||
var hint := Label.new()
|
||
hint.text = "分配属性点会通过服务器确认"
|
||
hint.position = Vector2(12, 174)
|
||
hint.size = Vector2(245, 40)
|
||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
hint.add_theme_font_size_override("font_size", 10)
|
||
hint.add_theme_color_override("font_color", Color(0.62, 0.72, 0.84))
|
||
stats.add_child(hint)
|
||
|
||
func _build_mobile_equipment() -> void:
|
||
var equipment := Panel.new()
|
||
equipment.position = Vector2(480, 0)
|
||
equipment.size = Vector2(252, 236)
|
||
equipment.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
|
||
_mobile_status_page.add_child(equipment)
|
||
var title := Label.new()
|
||
title.text = "装备"
|
||
title.position = Vector2(12, 10)
|
||
title.add_theme_font_size_override("font_size", 14)
|
||
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
|
||
equipment.add_child(title)
|
||
var grid := GridContainer.new()
|
||
grid.name = "equipment_grid"
|
||
grid.columns = 4
|
||
grid.position = Vector2(10, 38)
|
||
grid.size = Vector2(232, 188)
|
||
grid.add_theme_constant_override("h_separation", 5)
|
||
grid.add_theme_constant_override("v_separation", 5)
|
||
equipment.add_child(grid)
|
||
for i in 24:
|
||
var slot := Panel.new()
|
||
slot.custom_minimum_size = Vector2(53, 36)
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.08, 0.11, 0.17, 0.95)
|
||
style.border_color = Color(0.32, 0.45, 0.62, 0.8)
|
||
style.set_border_width_all(1)
|
||
style.set_corner_radius_all(4)
|
||
slot.add_theme_stylebox_override("panel", style)
|
||
var value := Label.new()
|
||
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
value.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
value.add_theme_font_size_override("font_size", 9)
|
||
value.text = str(i + 1)
|
||
value.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
slot.add_child(value)
|
||
grid.add_child(slot)
|
||
_mobile_equipment_labels.append(value)
|
||
|
||
func _build_mobile_emotes() -> void:
|
||
var title := Label.new()
|
||
title.text = "点击发送表情"
|
||
title.position = Vector2(8, 8)
|
||
title.add_theme_font_size_override("font_size", 14)
|
||
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
|
||
_mobile_emote_page.add_child(title)
|
||
var grid := GridContainer.new()
|
||
grid.position = Vector2(8, 42)
|
||
grid.columns = 5
|
||
grid.add_theme_constant_override("h_separation", 8)
|
||
grid.add_theme_constant_override("v_separation", 8)
|
||
_mobile_emote_page.add_child(grid)
|
||
for i in 9:
|
||
var button := MobileTouchButton.new()
|
||
button.setup("表情 %d" % (i + 1), Color(0.45, 0.7, 0.95, 0.92))
|
||
button.set_rect_style()
|
||
button.custom_minimum_size = Vector2(112, 52)
|
||
button.size = Vector2(112, 52)
|
||
var emote_id := i + 1
|
||
button.pressed.connect(func():
|
||
if client and client.has_method("send_emoticon"):
|
||
client.send_emoticon(emote_id)
|
||
if _mobile_hint: _mobile_hint.text = "已发送表情 %d" % emote_id)
|
||
grid.add_child(button)
|
||
_mobile_hint = Label.new()
|
||
_mobile_hint.position = Vector2(8, 192)
|
||
_mobile_hint.size = Vector2(500, 28)
|
||
_mobile_hint.add_theme_font_size_override("font_size", 11)
|
||
_mobile_hint.add_theme_color_override("font_color", Color(0.7, 0.8, 0.94))
|
||
_mobile_emote_page.add_child(_mobile_hint)
|
||
|
||
func _mobile_set_state(state: String) -> void:
|
||
if state in STATES:
|
||
_state = state
|
||
if _mobile_status_page:
|
||
_mobile_status_page.visible = _state != "EMOTICON"
|
||
if _mobile_emote_page:
|
||
_mobile_emote_page.visible = _state == "EMOTICON"
|
||
|
||
func _refresh_mobile() -> void:
|
||
if not is_open() or client == null:
|
||
return
|
||
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
|
||
var pts: Array = pd.get("points", [])
|
||
var get := func(i: int) -> int: return int(pts[i]) if i >= 0 and i < pts.size() else 0
|
||
_mobile_set_label("level", "Lv %d" % int(pd.get("level", get.call(P_LEVEL))))
|
||
_mobile_set_label("exp", "%d / %d" % [_u32(int(pd.get("exp", get.call(P_EXP)))), _u32(int(pd.get("next_exp", get.call(P_NEXT_EXP))))])
|
||
_mobile_set_label("hp", "%d / %d" % [int(pd.get("hp", get.call(P_HP))), int(pd.get("max_hp", get.call(P_MAX_HP)))])
|
||
_mobile_set_label("sp", "%d / %d" % [int(pd.get("sp", get.call(P_SP))), int(pd.get("max_sp", get.call(P_MAX_SP)))])
|
||
var vid := int(client.get_main_vid()) if client.has_method("get_main_vid") else 0
|
||
var ent: Dictionary = client.get_entity(vid) if vid != 0 and client.has_method("get_entity") else {}
|
||
_mobile_set_label("name", String(ent.get("name", "冒险者")))
|
||
var guild_id := int(ent.get("guild", 0))
|
||
var guild_name := String(client.get_guild_name(guild_id)) if guild_id != 0 and client.has_method("get_guild_name") else "无"
|
||
_mobile_set_label("guild", guild_name if guild_name != "" else "无")
|
||
_mobile_set_label("stat_points", "可用点数:%d" % max(0, get.call(P_STAT)))
|
||
for key in _mobile_stats:
|
||
var source: int = int({"STR": P_ST, "HTH": P_HT, "DEX": P_DX, "INT": P_IQ}.get(key, 0))
|
||
_mobile_stats[key]["value"].text = str(get.call(int(source)))
|
||
_mobile_stats[key]["plus"].visible = get.call(P_STAT) > 0
|
||
_mobile_stats[key]["minus"].visible = get.call(P_STAT) > 0
|
||
var equipment: Array = client.get_equipment() if client.has_method("get_equipment") else []
|
||
for i in _mobile_equipment_labels.size():
|
||
var item: Dictionary = equipment[i] if i < equipment.size() else {}
|
||
var vnum := int(item.get("vnum", 0))
|
||
_mobile_equipment_labels[i].text = ("#%d" % vnum) if vnum != 0 else str(i + 1)
|
||
|
||
func _mobile_set_label(key: String, value: String) -> void:
|
||
if _mobile_labels.has(key) and is_instance_valid(_mobile_labels[key]):
|
||
_mobile_labels[key].text = value
|
||
|
||
func _mobile_panel_style(bg := Color(0.045, 0.065, 0.1, 0.96), border := Color(0.52, 0.68, 0.9, 0.82)) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = bg
|
||
style.border_color = border
|
||
style.set_border_width_all(1)
|
||
style.set_corner_radius_all(8)
|
||
return style
|
||
|
||
func _node(nm: String) -> Control:
|
||
if _win.is_empty():
|
||
return null
|
||
var nodes: Dictionary = _win.get("nodes", {})
|
||
var n = nodes.get(nm, null)
|
||
return n if n is Control else null
|
||
|
||
# --- tab switching (uicharacter.py.SetState) -------------------------
|
||
|
||
func _wire_tabs() -> void:
|
||
for st: String in STATES:
|
||
var key := st
|
||
var btn := _node(TAB_BUTTON[key])
|
||
if btn is BaseButton:
|
||
btn.pressed.connect(func(): _set_state(key))
|
||
|
||
func _set_state(state: String) -> void:
|
||
if not state in STATES:
|
||
return
|
||
_state = state
|
||
for st: String in STATES:
|
||
var on := (st == state)
|
||
var page := _node(PAGE[st])
|
||
if page:
|
||
page.visible = on
|
||
var bar := _node(TITLEBAR[st])
|
||
if bar:
|
||
bar.visible = on
|
||
var tab_img := _node(TAB_IMAGE[st])
|
||
if tab_img:
|
||
tab_img.visible = on
|
||
var btn := _node(TAB_BUTTON[st])
|
||
if btn is BaseButton and btn.toggle_mode:
|
||
btn.set_pressed_no_signal(on)
|
||
match state:
|
||
"SKILL": _refresh_skills()
|
||
"EMOTICON": _refresh_emotes()
|
||
"QUEST": _refresh_quests()
|
||
"STATUS": _refresh()
|
||
|
||
# --- stat +/- buttons ----------------------------------------------------
|
||
|
||
func _wire_stat_buttons() -> void:
|
||
for key: String in PLUS_CMD:
|
||
var pb := _node(key + "_Plus")
|
||
if pb is BaseButton:
|
||
var cmd: String = PLUS_CMD[key]
|
||
pb.pressed.connect(func(): _send_stat(cmd))
|
||
var mb := _node(key + "_Minus")
|
||
if mb is BaseButton:
|
||
var mcmd: String = MINUS_CMD[key]
|
||
mb.pressed.connect(func(): _send_stat(mcmd))
|
||
|
||
func _send_stat(cmd: String) -> void:
|
||
if client and client.has_method("say"):
|
||
client.say(0, cmd) # CHAT_TYPE_TALKING —— 与 net.SendChatPacket 一致
|
||
|
||
# --- value binding (uicharacter.py.RefreshStatus) ----------------------
|
||
|
||
func _on_points_changed(_pts: Dictionary) -> void:
|
||
if is_open():
|
||
_refresh()
|
||
|
||
func _u32(v: int) -> int:
|
||
return v & 0xFFFFFFFF
|
||
|
||
func _refresh() -> void:
|
||
if not is_open() or client == null:
|
||
return
|
||
_refresh_status()
|
||
if _state == "SKILL":
|
||
_refresh_skills()
|
||
elif _state == "EMOTICON":
|
||
_refresh_emotes()
|
||
elif _state == "QUEST":
|
||
_refresh_quests()
|
||
|
||
func _refresh_status() -> void:
|
||
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
|
||
if pd.is_empty():
|
||
return
|
||
var pts: Array = pd.get("points", [])
|
||
var get := func(i: int) -> int:
|
||
return int(pts[i]) if i >= 0 and i < pts.size() else 0
|
||
|
||
_set_text("Level_Value", str(get.call(P_LEVEL)))
|
||
_set_text("Exp_Value", str(_u32(int(pd.get("exp", get.call(P_EXP))))))
|
||
var rest_exp := _u32(int(pd.get("next_exp", get.call(P_NEXT_EXP)))) - _u32(int(pd.get("exp", get.call(P_EXP))))
|
||
_set_text("RestExp_Value", str(rest_exp))
|
||
|
||
var hp := int(pd.get("hp", get.call(P_HP)))
|
||
var max_hp := int(pd.get("max_hp", get.call(P_MAX_HP)))
|
||
var sp := int(pd.get("sp", get.call(P_SP)))
|
||
var max_sp := int(pd.get("max_sp", get.call(P_MAX_SP)))
|
||
_set_text("HP_Value", "%d/%d" % [hp, max_hp])
|
||
_set_text("SP_Value", "%d/%d" % [sp, max_sp])
|
||
|
||
_set_text("STR_Value", str(get.call(P_ST)))
|
||
_set_text("DEX_Value", str(get.call(P_DX)))
|
||
_set_text("HTH_Value", str(get.call(P_HT)))
|
||
_set_text("INT_Value", str(get.call(P_IQ)))
|
||
|
||
_set_text("ATT_Value", _att_text(get))
|
||
_set_text("DEF_Value", _def_text(get))
|
||
_set_text("MATT_Value", _matt_text(get))
|
||
_set_text("MDEF_Value", str(get.call(P_MAGIC_DEF_GRADE)))
|
||
|
||
_set_text("ASPD_Value", str(get.call(P_ATT_SPEED)))
|
||
_set_text("MSPD_Value", str(get.call(P_MOV_SPEED)))
|
||
_set_text("CSPD_Value", str(get.call(P_CASTING_SPEED)))
|
||
_set_text("ER_Value", str(get.call(P_EVADE_RATE)))
|
||
|
||
_refresh_stat_points(get.call(P_STAT))
|
||
_refresh_identity()
|
||
|
||
# uicharacter.__GetTotalAtkText: (min|max) + ATT_BONUS + ATTACKER_BONUS
|
||
func _att_text(get: Callable) -> String:
|
||
var lo := int(get.call(P_MIN_ATK))
|
||
var hi := int(get.call(P_MAX_ATK))
|
||
var bonus := int(get.call(P_ATT_GRADE_BONUS)) + int(get.call(P_PARTY_ATT_GRADE))
|
||
if lo == 0 and hi == 0:
|
||
# 服务器未下发 MIN/MAX_ATK 时退回等效攻击力(原客户端此处会显示 0)
|
||
var eff := int(get.call(P_ATT_POWER)) + bonus
|
||
return str(eff)
|
||
if lo == hi:
|
||
return str(lo + bonus)
|
||
return "%d-%d" % [lo + bonus, hi + bonus]
|
||
|
||
# uicharacter.__GetTotalDefText: DEF_GRADE (+ DEF_BONUS if ADD_DEF_BONUS_ENABLE)
|
||
func _def_text(get: Callable) -> String:
|
||
var d := int(get.call(P_DEF_GRADE))
|
||
var db := int(get.call(P_DEF_GRADE_BONUS))
|
||
if db != 0:
|
||
d += db
|
||
return str(d)
|
||
|
||
# uicharacter.__GetTotalMagAtkText: MAG_ATT + (MIN|MAX)_MAGIC_WEP
|
||
func _matt_text(get: Callable) -> String:
|
||
var base := int(get.call(P_MAGIC_ATT_GRADE))
|
||
var lo := base + int(get.call(P_MIN_MAGIC_WEP))
|
||
var hi := base + int(get.call(P_MAX_MAGIC_WEP))
|
||
if lo == hi:
|
||
return str(lo)
|
||
return "%d-%d" % [lo, hi]
|
||
|
||
# uicharacter.__RefreshStatusPlusButtonList
|
||
func _refresh_stat_points(stat_points: int) -> void:
|
||
_set_text("Status_Plus_Value", str(max(0, stat_points)))
|
||
var lbl := _node("Status_Plus_Label")
|
||
if lbl:
|
||
lbl.visible = stat_points > 0
|
||
for key: String in PLUS_CMD:
|
||
var pb := _node(key + "_Plus")
|
||
if pb:
|
||
pb.visible = stat_points > 0
|
||
|
||
func _refresh_identity() -> void:
|
||
var vid := int(client.get_main_vid()) if client.has_method("get_main_vid") else 0
|
||
var ent: Dictionary = client.get_entity(vid) if (vid != 0 and client.has_method("get_entity")) else {}
|
||
_set_text("Character_Name", String(ent.get("name", "")))
|
||
var guild_id := int(ent.get("guild", 0))
|
||
var guild_name := ""
|
||
if guild_id != 0 and client.has_method("get_guild_name"):
|
||
guild_name = String(client.get_guild_name(guild_id))
|
||
_set_text("Guild_Name", guild_name)
|
||
# 职业头像:race % 4 -> warrior/assassin/sura/shaman
|
||
var face := _node("Face_Image")
|
||
if face is TextureRect:
|
||
var job := int(ent.get("race", 0)) % 4
|
||
var tex := UiAssets.load_tex(assets_root,
|
||
"d:/ymir work/ui/game/windows/%s.sub" % FACE_BY_JOB[job])
|
||
if tex:
|
||
face.texture = tex
|
||
|
||
func _set_text(nm: String, value: String) -> void:
|
||
var n := _node(nm)
|
||
if n == null:
|
||
return
|
||
if n is Label:
|
||
n.text = value
|
||
elif n is Button:
|
||
n.text = value
|
||
elif n.has_method("set_text"):
|
||
n.set_text(value)
|
||
|
||
func _wire_titlebars() -> void:
|
||
for tb_name: String in TITLEBAR.values():
|
||
var tb := _node(tb_name)
|
||
if tb:
|
||
for btn in tb.find_children("*", "BaseButton", true, false):
|
||
if not btn.pressed.is_connected(close):
|
||
btn.pressed.connect(close)
|
||
|
||
func _wire_skills() -> void:
|
||
var g1 := _node("Skill_Group_Button_1")
|
||
if g1 is BaseButton and not g1.pressed.is_connected(_on_group1_pressed):
|
||
g1.pressed.connect(_on_group1_pressed)
|
||
var g2 := _node("Skill_Group_Button_2")
|
||
if g2 is BaseButton and not g2.pressed.is_connected(_on_group2_pressed):
|
||
g2.pressed.connect(_on_group2_pressed)
|
||
|
||
func _on_group1_pressed() -> void:
|
||
if _skill_group() == 0:
|
||
_preview_group = 1
|
||
_refresh_skills()
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/skillgroup 1")
|
||
|
||
func _on_group2_pressed() -> void:
|
||
if _skill_group() == 0:
|
||
_preview_group = 2
|
||
_refresh_skills()
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/skillgroup 2")
|
||
|
||
func _wire_emoticons() -> void:
|
||
pass
|
||
|
||
func _wire_quests() -> void:
|
||
var sb := _node("Quest_ScrollBar")
|
||
if sb is Range:
|
||
if not sb.value_changed.is_connected(_on_quest_scroll_changed):
|
||
sb.value_changed.connect(_on_quest_scroll_changed)
|
||
|
||
func _on_quest_scroll_changed(val: float) -> void:
|
||
_quest_showing_start_index = int(val)
|
||
_refresh_quests()
|
||
|
||
func _refresh_skills() -> void:
|
||
if not is_open() or client == null:
|
||
return
|
||
var race := _main_race()
|
||
var group := _skill_group()
|
||
var job := race & 3
|
||
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
|
||
var pts: Array = pd.get("points", [])
|
||
var stat_skill: int = int(pts[P_SKILL]) if pts.size() > P_SKILL else 0
|
||
_set_text("Active_Skill_Point_Value", str(max(0, stat_skill)))
|
||
_set_text("Support_Skill_Point_Value", "0")
|
||
var sk_pt_lbl := _node("Active_Skill_Point_Label")
|
||
if sk_pt_lbl:
|
||
sk_pt_lbl.visible = stat_skill > 0
|
||
|
||
var grp_name := _node("Active_Skill_Group_Name")
|
||
var g1_btn := _node("Skill_Group_Button_1")
|
||
var g2_btn := _node("Skill_Group_Button_2")
|
||
var g3_btn := _node("Skill_Group_Button_3")
|
||
if g3_btn:
|
||
g3_btn.visible = false
|
||
|
||
var display_group := group
|
||
if group == 0:
|
||
# 40250 uicharacter.py: 当未选流派时显示两组按钮,供玩家预览切换
|
||
if grp_name: grp_name.visible = false
|
||
if g1_btn:
|
||
g1_btn.visible = true
|
||
if "text" in g1_btn: g1_btn.text = _get_skill_group_name(job, 1)
|
||
if g1_btn is BaseButton: g1_btn.set_pressed_no_signal(_preview_group == 1)
|
||
if g2_btn:
|
||
g2_btn.visible = true
|
||
if "text" in g2_btn: g2_btn.text = _get_skill_group_name(job, 2)
|
||
if g2_btn is BaseButton: g2_btn.set_pressed_no_signal(_preview_group == 2)
|
||
display_group = _preview_group
|
||
else:
|
||
# 40250 uicharacter.py: 已选流派时隐藏切换按钮,只展示当前流派名
|
||
if g1_btn: g1_btn.visible = false
|
||
if g2_btn: g2_btn.visible = false
|
||
if grp_name:
|
||
grp_name.visible = true
|
||
grp_name.text = _get_skill_group_name(job, group)
|
||
|
||
var active_slot := _node("Skill_Active_Slot")
|
||
if active_slot:
|
||
for slot_idx in range(1, 9):
|
||
var sid := SkillSlotMap.skill_id_for_slot(race, display_group, slot_idx)
|
||
var cell := active_slot.get_node_or_null("slot_%d" % slot_idx)
|
||
if cell:
|
||
_populate_skill_cell(cell, sid, slot_idx)
|
||
var plus_cell := active_slot.get_node_or_null("slot_%d" % (20 + slot_idx))
|
||
if plus_cell:
|
||
var pts_alloc := stat_skill if group > 0 else 0
|
||
_populate_skill_plus(plus_cell, sid, pts_alloc)
|
||
|
||
var etc_slot := _node("Skill_ETC_Slot")
|
||
if etc_slot:
|
||
for slot_idx in range(101, 113):
|
||
var sid := SkillSlotMap.skill_id_for_slot(race, display_group, slot_idx)
|
||
var cell := etc_slot.get_node_or_null("slot_%d" % slot_idx)
|
||
if cell:
|
||
_populate_skill_cell(cell, sid, slot_idx)
|
||
|
||
func _populate_skill_cell(cell: Control, skill_id: int, slot_idx: int) -> void:
|
||
var btn: TextureButton = cell.get_node_or_null("Btn")
|
||
if btn == null:
|
||
btn = TextureButton.new()
|
||
btn.name = "Btn"
|
||
btn.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
btn.ignore_texture_size = true
|
||
btn.stretch_mode = TextureButton.STRETCH_SCALE
|
||
btn.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
cell.add_child(btn)
|
||
|
||
var lvl_lbl: Label = cell.get_node_or_null("Lvl")
|
||
if lvl_lbl == null:
|
||
lvl_lbl = Label.new()
|
||
lvl_lbl.name = "Lvl"
|
||
lvl_lbl.position = Vector2(2, 16)
|
||
lvl_lbl.size = Vector2(28, 14)
|
||
lvl_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
lvl_lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||
lvl_lbl.add_theme_font_size_override("font_size", 10)
|
||
lvl_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
cell.add_child(lvl_lbl)
|
||
|
||
for conn in btn.gui_input.get_connections():
|
||
btn.gui_input.disconnect(conn["callable"])
|
||
|
||
if skill_id <= 0:
|
||
btn.texture_normal = null
|
||
lvl_lbl.text = ""
|
||
cell.tooltip_text = ""
|
||
btn.set_drag_forwarding(Callable(), Callable(), Callable())
|
||
return
|
||
|
||
btn.texture_normal = _skill_icon(skill_id)
|
||
var sk_data := _get_player_skill(skill_id)
|
||
var lvl: int = int(sk_data.get("level", 0))
|
||
var master: int = int(sk_data.get("master", 0))
|
||
if master == 1:
|
||
lvl_lbl.text = "M%d" % lvl
|
||
lvl_lbl.modulate = Color(1.0, 0.4, 0.4)
|
||
elif master == 2:
|
||
lvl_lbl.text = "G%d" % lvl
|
||
lvl_lbl.modulate = Color(0.4, 0.7, 1.0)
|
||
elif master == 3:
|
||
lvl_lbl.text = "P"
|
||
lvl_lbl.modulate = Color(0.9, 0.3, 1.0)
|
||
elif lvl > 0:
|
||
lvl_lbl.text = str(lvl)
|
||
lvl_lbl.modulate = Color(1.0, 1.0, 1.0)
|
||
else:
|
||
lvl_lbl.text = ""
|
||
|
||
var sk_name := _skill_name(skill_id)
|
||
cell.tooltip_text = "%s (Lv %d)" % [sk_name, lvl] if lvl > 0 else sk_name
|
||
|
||
btn.gui_input.connect(_on_skill_btn_input.bind(skill_id, slot_idx))
|
||
|
||
btn.set_drag_forwarding(
|
||
func(_at_pos: Vector2):
|
||
if not _can_drag_skill(skill_id):
|
||
return null
|
||
drag_skill_id = skill_id
|
||
var preview := TextureRect.new()
|
||
preview.texture = btn.texture_normal
|
||
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
preview.custom_minimum_size = Vector2(32, 32)
|
||
preview.size = Vector2(32, 32)
|
||
preview.position = -Vector2(16, 16)
|
||
btn.set_drag_preview(preview)
|
||
return {
|
||
"type": "skill",
|
||
"kind": "skill",
|
||
"id": skill_id,
|
||
"skill_id": skill_id,
|
||
"slot": slot_idx,
|
||
},
|
||
Callable(),
|
||
Callable()
|
||
)
|
||
|
||
func _can_drag_skill(skill_id: int) -> bool:
|
||
if skill_id <= 0:
|
||
return false
|
||
if skill_table and skill_table.has_method("is_passive") and skill_table.is_passive(skill_id):
|
||
return false
|
||
return true
|
||
|
||
func _find_quickbar() -> Node:
|
||
var root: Node = null
|
||
if is_inside_tree():
|
||
var tree := get_tree()
|
||
if tree: root = tree.root
|
||
elif ui != null and is_instance_valid(ui) and ui.is_inside_tree():
|
||
var tree := ui.get_tree()
|
||
if tree: root = tree.root
|
||
elif get_parent():
|
||
root = get_parent()
|
||
if root:
|
||
for n in root.find_children("*", "Quickbar", true, false):
|
||
return n
|
||
for n in root.find_children("*", "Node", true, false):
|
||
if n.has_method("assign_mobile") and n.has_method("assign"):
|
||
return n
|
||
return null
|
||
|
||
func _find_item_mouse() -> Node:
|
||
if item_mouse != null and is_instance_valid(item_mouse):
|
||
return item_mouse
|
||
var root: Node = null
|
||
if is_inside_tree():
|
||
var tree := get_tree()
|
||
if tree: root = tree.root
|
||
elif ui != null and is_instance_valid(ui) and ui.is_inside_tree():
|
||
var tree := ui.get_tree()
|
||
if tree: root = tree.root
|
||
elif get_parent():
|
||
root = get_parent()
|
||
if root:
|
||
for n in root.find_children("*", "MouseController", true, false):
|
||
return n
|
||
for n in root.find_children("*", "Node", true, false):
|
||
if n.has_method("attach_skill") and n.has_method("attach_item"):
|
||
return n
|
||
return null
|
||
|
||
func _on_skill_btn_input(ev: InputEvent, skill_id: int, slot_idx: int) -> void:
|
||
if not (ev is InputEventMouseButton and ev.pressed):
|
||
return
|
||
if ev.button_index == MOUSE_BUTTON_RIGHT:
|
||
_cast_skill(skill_id)
|
||
elif ev.button_index == MOUSE_BUTTON_LEFT:
|
||
if not _can_drag_skill(skill_id):
|
||
return
|
||
drag_skill_id = skill_id
|
||
# 40250 uicharacter.py: Ctrl + Left Click adds to first empty quickslot
|
||
if ev.ctrl_pressed or Input.is_key_pressed(KEY_CTRL):
|
||
var qb := _find_quickbar()
|
||
if qb and qb.has_method("assign_mobile"):
|
||
if qb.assign_mobile("skill", skill_id):
|
||
return
|
||
# 40250 mouseController.AttachObject(self, player.SLOT_TYPE_SKILL, srcSlotIndex, selectedSkillIndex)
|
||
var im := item_mouse if item_mouse else _find_item_mouse()
|
||
if im and im.has_method("attach_skill"):
|
||
im.attach_skill(skill_id, slot_idx, _skill_icon(skill_id), _skill_name(skill_id))
|
||
|
||
func _populate_skill_plus(cell: Control, skill_id: int, stat_skill: int) -> void:
|
||
var plus_btn: Button = cell.get_node_or_null("PlusBtn")
|
||
if plus_btn == null:
|
||
plus_btn = Button.new()
|
||
plus_btn.name = "PlusBtn"
|
||
plus_btn.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
plus_btn.text = "+"
|
||
plus_btn.add_theme_font_size_override("font_size", 10)
|
||
plus_btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
cell.add_child(plus_btn)
|
||
|
||
var sk_data := _get_player_skill(skill_id)
|
||
var lvl: int = int(sk_data.get("level", 0))
|
||
var master: int = int(sk_data.get("master", 0))
|
||
var can_up := (skill_id > 0 and stat_skill > 0 and master == 0 and lvl < 20)
|
||
plus_btn.visible = can_up
|
||
for conn in plus_btn.pressed.get_connections():
|
||
plus_btn.pressed.disconnect(conn["callable"])
|
||
if can_up:
|
||
plus_btn.pressed.connect(func():
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/skillup %d" % skill_id))
|
||
|
||
func _refresh_emotes() -> void:
|
||
var solo := _node("SoloEmotionSlot")
|
||
if solo:
|
||
for id in range(1, 19):
|
||
var cell := solo.get_node_or_null("slot_%d" % id)
|
||
if cell:
|
||
_populate_emote_cell(cell, id)
|
||
var dual := _node("DualEmotionSlot")
|
||
if dual:
|
||
for id in [51, 52, 53]:
|
||
var cell := dual.get_node_or_null("slot_%d" % id)
|
||
if cell:
|
||
_populate_emote_cell(cell, id)
|
||
|
||
func _populate_emote_cell(cell: Control, emote_id: int) -> void:
|
||
var btn: TextureButton = cell.get_node_or_null("Btn")
|
||
if btn == null:
|
||
btn = TextureButton.new()
|
||
btn.name = "Btn"
|
||
btn.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
btn.ignore_texture_size = true
|
||
btn.stretch_mode = TextureButton.STRETCH_SCALE
|
||
btn.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
cell.add_child(btn)
|
||
for conn in btn.gui_input.get_connections():
|
||
btn.gui_input.disconnect(conn["callable"])
|
||
for conn in btn.pressed.get_connections():
|
||
btn.pressed.disconnect(conn["callable"])
|
||
|
||
var rel: String = EMOTE_ICONS.get(emote_id, "")
|
||
var tex: Texture2D = UiAssets.load_tex(assets_root, rel) if rel != "" else null
|
||
if tex:
|
||
btn.texture_normal = tex
|
||
var nm: String = EMOTE_NAMES.get(emote_id, "动作 %d" % emote_id)
|
||
cell.tooltip_text = nm
|
||
|
||
btn.gui_input.connect(func(ev: InputEvent):
|
||
if not (ev is InputEventMouseButton and ev.pressed):
|
||
return
|
||
if ev.button_index == MOUSE_BUTTON_RIGHT:
|
||
if client and client.has_method("send_emoticon"):
|
||
client.send_emoticon(emote_id)
|
||
elif ev.button_index == MOUSE_BUTTON_LEFT:
|
||
if ev.ctrl_pressed or Input.is_key_pressed(KEY_CTRL):
|
||
var qb := _find_quickbar()
|
||
if qb and qb.has_method("assign_mobile"):
|
||
if qb.assign_mobile("emote", emote_id):
|
||
return
|
||
var im := item_mouse if item_mouse else _find_item_mouse()
|
||
if im and im.has_method("attach_emotion"):
|
||
im.attach_emotion(emote_id, tex, nm)
|
||
)
|
||
|
||
btn.set_drag_forwarding(
|
||
func(_at_pos: Vector2):
|
||
var preview := TextureRect.new()
|
||
preview.texture = tex
|
||
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
preview.custom_minimum_size = Vector2(32, 32)
|
||
preview.size = Vector2(32, 32)
|
||
preview.position = -Vector2(16, 16)
|
||
btn.set_drag_preview(preview)
|
||
return {
|
||
"type": "emotion",
|
||
"kind": "emote",
|
||
"id": emote_id,
|
||
"emote_id": emote_id,
|
||
},
|
||
Callable(),
|
||
Callable()
|
||
)
|
||
|
||
func _refresh_quests() -> void:
|
||
var quest_slot := _node("Quest_Slot")
|
||
if quest_slot == null or client == null:
|
||
return
|
||
var quests: Array = []
|
||
if client.has_method("get_quests"):
|
||
var raw_quests: Array = client.get_quests()
|
||
for q in raw_quests:
|
||
if String(q.get("title", q.get("name", ""))).strip_edges() != "":
|
||
quests.append(q)
|
||
|
||
var quest_count := quests.size()
|
||
var sb := _node("Quest_ScrollBar")
|
||
if sb is Range:
|
||
if quest_count > 5:
|
||
sb.visible = true
|
||
sb.max_value = maxi(0, quest_count - 5)
|
||
sb.value = _quest_showing_start_index
|
||
else:
|
||
sb.visible = false
|
||
_quest_showing_start_index = 0
|
||
|
||
for i in range(5):
|
||
var data_idx := _quest_showing_start_index + i
|
||
var nm_node := _node("Quest_Name_0%d" % i)
|
||
var time_node := _node("Quest_LastTime_0%d" % i)
|
||
var cnt_node := _node("Quest_LastCount_0%d" % i)
|
||
var cell := quest_slot.get_node_or_null("slot_%d" % i)
|
||
if cell == null and i < quest_slot.get_child_count():
|
||
cell = quest_slot.get_child(i)
|
||
|
||
if data_idx < quest_count:
|
||
var q: Dictionary = quests[data_idx]
|
||
var q_title := String(q.get("title", q.get("name", "任务 %d" % data_idx)))
|
||
var q_idx := int(q.get("index", data_idx))
|
||
var counter_name := String(q.get("counter_name", ""))
|
||
var counter_val := int(q.get("counter_value", 0))
|
||
var clock_name := String(q.get("clock_name", ""))
|
||
var clock_val := int(q.get("clock_value", 0))
|
||
|
||
if nm_node:
|
||
nm_node.visible = true
|
||
nm_node.text = q_title
|
||
nm_node.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
nm_node.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
for conn in nm_node.gui_input.get_connections():
|
||
nm_node.gui_input.disconnect(conn["callable"])
|
||
nm_node.gui_input.connect(func(ev: InputEvent):
|
||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||
_select_quest(q_idx)
|
||
)
|
||
|
||
if cnt_node:
|
||
cnt_node.visible = true
|
||
if counter_name != "":
|
||
cnt_node.text = "%s : %d" % [counter_name, counter_val]
|
||
else:
|
||
cnt_node.text = ""
|
||
|
||
if time_node:
|
||
time_node.visible = true
|
||
if clock_name != "" and clock_val > 0:
|
||
var m := clock_val / 60
|
||
var s := clock_val % 60
|
||
time_node.text = "%s %02d:%02d" % [clock_name, m, s]
|
||
else:
|
||
time_node.text = ""
|
||
|
||
if cell:
|
||
cell.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
cell.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
for conn in cell.gui_input.get_connections():
|
||
cell.gui_input.disconnect(conn["callable"])
|
||
cell.gui_input.connect(func(ev: InputEvent):
|
||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||
_select_quest(q_idx)
|
||
)
|
||
else:
|
||
if nm_node:
|
||
nm_node.visible = false
|
||
nm_node.text = ""
|
||
if cnt_node:
|
||
cnt_node.visible = false
|
||
cnt_node.text = ""
|
||
if time_node:
|
||
time_node.visible = false
|
||
time_node.text = ""
|
||
if cell:
|
||
for conn in cell.gui_input.get_connections():
|
||
cell.gui_input.disconnect(conn["callable"])
|
||
|
||
func _select_quest(quest_idx: int) -> void:
|
||
if client and client.has_method("script_button"):
|
||
# 40250 uicharacter.py:655 event.QuestButtonClick(-2147483648 + questIndex)
|
||
client.script_button(-2147483648 + quest_idx)
|
||
|
||
func _main_race() -> int:
|
||
if client and client.has_method("get_main_vid") and client.has_method("get_entity"):
|
||
var vid := int(client.get_main_vid())
|
||
if vid != 0:
|
||
return int(client.get_entity(vid).get("race", 0))
|
||
return 0
|
||
|
||
const SKILL_GROUP_NAMES := {
|
||
0: {1: "剑宗", 2: "气宗"},
|
||
1: {1: "锋刀", 2: "百羿"},
|
||
2: {1: "幻舞", 2: "黑魔"},
|
||
3: {1: "潜龙", 2: "狂雷"},
|
||
}
|
||
|
||
func _get_skill_group_name(job: int, grp: int) -> String:
|
||
var names: Dictionary = SKILL_GROUP_NAMES.get(job, {})
|
||
return String(names.get(grp, "技能组 %d" % grp))
|
||
|
||
func _skill_group() -> int:
|
||
var grp := int(client.get_skill_group()) if client and client.has_method("get_skill_group") else 0
|
||
if grp > 0:
|
||
return grp
|
||
return _infer_skill_group()
|
||
|
||
func _infer_skill_group() -> int:
|
||
if client == null or not client.has_method("get_skills"):
|
||
return 0
|
||
var race := _main_race()
|
||
for sk in client.get_skills():
|
||
var sid := int(sk.get("id", 0))
|
||
var lvl := int(sk.get("level", 0))
|
||
if lvl <= 0 and int(sk.get("master", 0)) == 0:
|
||
continue
|
||
for g in [1, 2]:
|
||
var slot := SkillSlotMap.skill_slot_for_id(race, g, sid)
|
||
if slot >= 1 and slot <= 8:
|
||
return g
|
||
return 0
|
||
|
||
func _skill_icon(skill_id: int) -> Texture2D:
|
||
if assets_root == "":
|
||
return null
|
||
var data: Dictionary = skill_table.entry(skill_id) if skill_table and skill_table.has_method("entry") else {}
|
||
var job := String(data.get("job", "")).to_upper()
|
||
var motion := String(data.get("motion", "")).strip_edges()
|
||
if motion == "":
|
||
return null
|
||
var candidates: Array[String] = []
|
||
const JOB_DIR := { "WARRIOR": "warrior", "ASSASSIN": "assassin", "SURA": "sura", "SHAMAN": "shaman" }
|
||
var grade := _skill_grade(skill_id)
|
||
var suffixes: Array[String] = ["_01", "_02", "_03"]
|
||
if grade == 1:
|
||
suffixes = ["_02", "_01", "_03"]
|
||
elif grade >= 2:
|
||
suffixes = ["_03", "_02", "_01"]
|
||
if JOB_DIR.has(job):
|
||
var dir := String(JOB_DIR[job])
|
||
for sfx in suffixes:
|
||
candidates.append("ETC/ymir work/ui/skill/%s/%s%s.sub" % [dir, motion, sfx])
|
||
elif job == "SUPPORT":
|
||
for sfx in suffixes:
|
||
candidates.append("ETC/ymir work/ui/skill/common/support/%s%s.sub" % [motion, sfx])
|
||
candidates.append("ETC/ymir work/ui/skill/common/support/%s.sub" % motion)
|
||
elif job == "HORSE":
|
||
candidates.append("ETC/ymir work/ui/skill/common/horse/%s.sub" % motion)
|
||
elif job == "GUILD":
|
||
candidates.append("ETC/ymir work/ui/skill/common/guild/%s.sub" % motion)
|
||
for rel in candidates:
|
||
var tex: Texture2D = UiAssets.load_tex(assets_root, rel)
|
||
if tex != null:
|
||
return tex
|
||
return null
|
||
|
||
func _skill_grade(skill_id: int) -> int:
|
||
if client and client.has_method("get_skills"):
|
||
for skill in client.get_skills():
|
||
if int(skill.get("id", 0)) == skill_id:
|
||
return clampi(int(skill.get("master", 0)), 0, 3)
|
||
return 0
|
||
|
||
func _get_player_skill(skill_id: int) -> Dictionary:
|
||
if client and client.has_method("get_skills"):
|
||
for skill in client.get_skills():
|
||
if int(skill.get("id", 0)) == skill_id:
|
||
return skill
|
||
return {}
|
||
|
||
func _skill_name(skill_id: int) -> String:
|
||
if skill_table and skill_table.has_method("name_of"):
|
||
var nm := String(skill_table.name_of(skill_id))
|
||
if nm != "":
|
||
return nm
|
||
return "技能 %d" % skill_id
|
||
|
||
func _cast_skill(skill_id: int) -> void:
|
||
var tree := get_tree()
|
||
if tree and tree.root:
|
||
for n in tree.root.find_children("*", "Node", true, false):
|
||
if n.has_method("activate_skill_direct"):
|
||
n.activate_skill_direct(skill_id)
|
||
return
|
||
if client and client.has_method("use_skill"):
|
||
client.use_skill(skill_id, 0)
|