# 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") # --- 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" 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()) # --- open / close ----------------------------------------------------- func is_open() -> bool: 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 is_open(): ui.close(_win["root"]) _win = {} func open(state := "") -> void: if state != "" and state in STATES: _state = state 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 _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)