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