# 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") # --- 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_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 # 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 FACE_BY_JOB := ["face_warrior", "face_assassin", "face_sura", "face_shaman"] var ui: CanvasLayer # UiManager var client: Node # M2Client 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 func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void: ui = ui_manager client = m2client assets_root = assets 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") 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()) func set_mobile_mode(enabled: bool) -> void: if not enabled and _mobile_root != null: 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() 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 _wire_tabs() _wire_stat_buttons() _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 btn := _node(TAB_BUTTON[st]) if btn is BaseButton and btn.toggle_mode: btn.set_pressed_no_signal(on) # --- 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 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)