# PartyUI (P8/P11) —— 组队成员信息板(1:1 迁移 `assets/root/uiparty.py` 的 # `PartyMemberInfoBoard` + `PartyMemberInfoWindow`;布局照 `partymemberinfoboard.py` 的 # 每员 106×36 条:StateButton(角色图标) + NameSlot + Gauge(HP) + 附加图标行)。 # # var pu := preload("res://ui/party_ui.gd").new() # add_child(pu) # pu.setup(m2client, canvas_parent, dialogs) # dialogs 可空 # # 每员一条 strip:角色状态按钮(队长可点 → 分配角色 / 踢人)、名字(+★队长)、HP 条、 # 附加效果格(affects[7] 非零 → 小图标 + tooltip)。整条点击 → 选中该员 vid。 # 顶部:EXP 分配开关(均分 / 不均分 → party_set_distribute)+ 组队治疗(party_use_skill)。 # `party_changed` 刷新;`party_invite_ask(leader_pid)` → 确认框 → party_answer。 extends Node # PythonPlayer.h EPartyRole const ROLE_NORMAL := 0 const ROLE_ATTACKER := 2 const ROLE_TANKER := 3 const ROLE_BUFFER := 4 const ROLE_SKILL_MASTER := 5 const ROLE_HASTE := 6 const ROLE_DEFENDER := 7 const ROLE_LABEL := { 0: "普", 1: "队", 2: "攻", 3: "坦", 4: "辅", 5: "宗", 6: "速", 7: "防", } const ROLE_MENU := [ [ROLE_NORMAL, "普通"], [ROLE_ATTACKER, "攻击"], [ROLE_TANKER, "坦克"], [ROLE_HASTE, "迅捷"], [ROLE_BUFFER, "辅助"], [ROLE_SKILL_MASTER, "宗师"], [ROLE_DEFENDER, "防御"], ] # Packet.h EPartyExpDistributionType const EXP_NON_PARITY := 0 # 不均分(按贡献) const EXP_PARITY := 1 # 均分 # uiparty.PartyMemberInfoBoard.PARTY_SKILL_* const PARTY_SKILL_HEAL := 1 # affects[7] 槽位含义(暂定,按 partymemberinfoboard.py 图标顺序) const AFFECT_LABEL := ["经验", "攻击", "坦克", "辅助", "宗师", "迅捷", "防御"] var client: Node var dialogs: Node var _root: Control var _list: VBoxContainer var _dist_btn: Button var _heal_btn: Button var _notice: Label var _role_popup: Control = null func setup(m2client: Node, parent: Node, dlg: Node = null) -> void: client = m2client dialogs = dlg _build(parent) if client.has_signal("party_changed"): client.party_changed.connect(refresh) if client.has_signal("party_invite_ask"): client.party_invite_ask.connect(_on_invite) if client.has_signal("party_request_denied"): client.party_request_denied.connect(_on_request_denied) if client.has_signal("vitals_changed"): client.vitals_changed.connect(func(_v): if is_open(): refresh()) refresh() func is_open() -> bool: return _root != null and _root.visible func close() -> void: if _root: _root.visible = false # --- 로컬 플레이어가 파티장인가 --- func _local_is_leader() -> bool: if client == null or not client.has_method("get_main_vid"): return false var my_vid := int(client.get_main_vid()) for m in client.get_party(): if int(m.get("vid", 0)) == my_vid: return bool(m.get("leader", false)) return false # --- 새로고침 ------------------------------------------------------------ func refresh() -> void: if client == null or not client.has_method("get_party"): return _dismiss_role_popup() for c in _list.get_children(): _list.remove_child(c) c.queue_free() var members: Array = client.get_party() _root.visible = not members.is_empty() if members.is_empty(): return _refresh_dist_btn() for m in members: _list.add_child(_strip(m)) func _refresh_dist_btn() -> void: if not is_instance_valid(_dist_btn): return var mode := int(client.get_party_distribute_mode()) if client.has_method("get_party_distribute_mode") else 0 _dist_btn.text = "EXP:均分" if mode == EXP_PARITY else "EXP:不均分" _dist_btn.disabled = not _local_is_leader() if is_instance_valid(_heal_btn): var leadership := _leadership_level() _heal_btn.disabled = not _local_is_leader() or (leadership >= 0 and leadership < 18) _heal_btn.tooltip_text = "队长 Leadership 达到 18 级后可用" if _heal_btn.disabled and leadership >= 0 and leadership < 18 else "组队治疗" func _leadership_level() -> int: # The server's CParty::Update gates party roles/heal using the leader's # Leadership skill. -1 means that the snapshot has not arrived yet; in # that case the server remains authoritative and the controls stay usable. if client == null or not client.has_method("get_skills"): return -1 var skills: Array = client.get_skills() for skill in skills: if int(skill.get("id", -1)) == 121: # SKILL_LEADERSHIP in 40250 return int(skill.get("level", 0)) return -1 func _role_unlock_level(role: int) -> int: match role: ROLE_ATTACKER: return 10 ROLE_TANKER, ROLE_HASTE: return 20 ROLE_BUFFER: return 25 ROLE_SKILL_MASTER: return 35 ROLE_DEFENDER: return 40 _: return 0 func _role_allowed(role: int, current_role: int = -1) -> bool: if role == ROLE_NORMAL or role == current_role: return true var leadership := _leadership_level() return leadership < 0 or leadership >= _role_unlock_level(role) # --- 한 명 strip ------------------------------------------------------ func _strip(m: Dictionary) -> Control: var pid := int(m.get("pid", 0)) var vid := int(m.get("vid", 0)) var is_leader := bool(m.get("leader", false)) var role := int(m.get("state", 0)) & 0x7F var strip := PanelContainer.new() strip.custom_minimum_size = Vector2(206, 40) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 4) strip.add_child(row) # StateButton —— 角色图标(队长可点) var state_btn := Button.new() state_btn.custom_minimum_size = Vector2(22, 34) state_btn.text = "队" if is_leader else ROLE_LABEL.get(role, "普") state_btn.tooltip_text = "队长" if is_leader else ("角色:%s" % ROLE_LABEL.get(role, "普")) state_btn.disabled = not _local_is_leader() state_btn.pressed.connect(func() -> void: _open_role_popup(state_btn, pid, role)) row.add_child(state_btn) var col := VBoxContainer.new() col.add_theme_constant_override("separation", 1) col.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(col) # NameSlot / NamePrint var name_lbl := Button.new() name_lbl.flat = true name_lbl.alignment = HORIZONTAL_ALIGNMENT_LEFT name_lbl.text = ("★ " if is_leader else "") + String(m.get("name", "?")) name_lbl.add_theme_font_size_override("font_size", 12) name_lbl.pressed.connect(func() -> void: _select_member(vid)) col.add_child(name_lbl) # Gauge (HP) var bar := ProgressBar.new() bar.min_value = 0 bar.max_value = 100 bar.value = int(m.get("hp_pct", 0)) bar.show_percentage = false bar.custom_minimum_size = Vector2(180, 8) col.add_child(bar) # 附가효과 아이콘 행 var affects: Array = m.get("affects", []) var chips := HBoxContainer.new() chips.add_theme_constant_override("separation", 2) col.add_child(chips) for i in affects.size(): var v := int(affects[i]) if v == 0: continue var chip := Label.new() chip.text = AFFECT_LABEL[i] if i < AFFECT_LABEL.size() else "?%d" % i chip.add_theme_font_size_override("font_size", 9) chip.add_theme_color_override("font_color", Color(0.7, 0.9, 1.0)) chip.tooltip_text = "%s +%d" % [chip.text, v] chips.add_child(chip) return strip func _select_member(vid: int) -> void: if vid <= 0 or client == null: return if client.has_method("set_target"): client.set_target(vid) elif client.has_method("target"): client.target(vid) # --- 角色分配 팝업 (uiparty.__ShowStateButton + OnSelectState / OnExpel) --- func _open_role_popup(anchor: Control, pid: int, cur_role: int) -> void: _dismiss_role_popup() if not _local_is_leader(): return var pop := VBoxContainer.new() pop.add_theme_constant_override("separation", 2) var bg := PanelContainer.new() bg.add_child(pop) for entry in ROLE_MENU: var r: int = entry[0] var b := Button.new() b.text = entry[1] + (" ✓" if r == cur_role else "") b.custom_minimum_size = Vector2(84, 22) b.disabled = not _role_allowed(r, cur_role) if b.disabled: b.tooltip_text = "Leadership %d 级解锁" % _role_unlock_level(r) b.pressed.connect(func() -> void: if r == ROLE_NORMAL: # OnSelectState(-1): 清掉当前角色 client.party_set_state(pid, cur_role, false) else: client.party_set_state(pid, r, true) _dismiss_role_popup()) pop.add_child(b) var expel := Button.new() expel.text = "踢出队伍" expel.custom_minimum_size = Vector2(84, 22) expel.add_theme_color_override("font_color", Color(1, 0.5, 0.5)) expel.pressed.connect(func() -> void: client.party_leave(pid) # OnExpel -> SendPartyRemovePacket _dismiss_role_popup()) pop.add_child(expel) bg.position = anchor.get_global_position() + Vector2(24, 0) _root.get_parent().add_child(bg) _role_popup = bg func _dismiss_role_popup() -> void: if is_instance_valid(_role_popup): _role_popup.queue_free() _role_popup = null # --- 邀请确认 --------------------------------------------------------- func _on_invite(leader_pid: int) -> void: var accept := func() -> void: client.party_answer(leader_pid, true) var decline := func() -> void: client.party_answer(leader_pid, false) if dialogs and dialogs.has_method("confirm"): dialogs.confirm("有人邀请你加入队伍,接受?", accept, decline) else: accept.call() func _on_request_denied() -> void: if is_instance_valid(_notice): _notice.text = "组队请求被拒绝" # --- EXP 분배 / 组队治疗 -------------------------------------------- func _toggle_distribute() -> void: if client == null or not _local_is_leader(): return var mode := int(client.get_party_distribute_mode()) if client.has_method("get_party_distribute_mode") else 0 client.party_set_distribute(EXP_NON_PARITY if mode == EXP_PARITY else EXP_PARITY) func _party_heal() -> void: if client and client.has_method("party_use_skill"): client.party_use_skill(PARTY_SKILL_HEAL, 0) # OnPartyUseSkill # --- 레이아웃 ------------------------------------------------------------ func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_TOP_LEFT) _root.position = Vector2(16, 150) _root.size = Vector2(226, 320) _root.visible = false parent.add_child(_root) var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) var sb := StyleBoxFlat.new() sb.bg_color = Color(0.06, 0.07, 0.09, 0.85) sb.set_corner_radius_all(4) panel.add_theme_stylebox_override("panel", sb) _root.add_child(panel) var header := HBoxContainer.new() header.position = Vector2(8, 6) header.add_theme_constant_override("separation", 6) _root.add_child(header) _dist_btn = Button.new() _dist_btn.text = "EXP:均分" _dist_btn.add_theme_font_size_override("font_size", 11) _dist_btn.pressed.connect(_toggle_distribute) header.add_child(_dist_btn) var heal := Button.new() heal.text = "组队治疗" heal.add_theme_font_size_override("font_size", 11) heal.pressed.connect(_party_heal) _heal_btn = heal header.add_child(heal) _notice = Label.new() _notice.add_theme_font_size_override("font_size", 10) _notice.add_theme_color_override("font_color", Color(1, 0.65, 0.55)) header.add_child(_notice) _list = VBoxContainer.new() _list.position = Vector2(8, 32) _list.add_theme_constant_override("separation", 4) _root.add_child(_list)