# FriendUI (P8) —— 好友 / 私聊名单窗(M 键)。 # # var fu := preload("res://ui/friend_ui.gd").new() # add_child(fu) # fu.setup(m2client, canvas_parent) # fu.toggle() # M 键 # # 每行 = 在线点 + 名字 + [删除]。底部输入框 + [添加好友]。 # `friends_changed` 刷新。双击某行 → emit `whisper_to(name)`(chat_ui 可接)。 extends Node signal whisper_to(name: String) var client: Node var _root: Control var _list: VBoxContainer var _name_edit: LineEdit var _invite_dialog: ConfirmationDialog var _pending_invite := "" var _mobile_mode := false var _title: Label var _bottom: HBoxContainer var _add_button: Button func setup(m2client: Node, parent: Node) -> void: client = m2client _build(parent) if client.has_signal("friends_changed"): client.friends_changed.connect(refresh) if client.has_signal("friend_invite_ask"): client.friend_invite_ask.connect(_on_friend_invite) # 原版 messenger 是三固定分组(uimessenger.py MessengerFriendGroup/GuildGroup/ # FamilyGroup);公会与情侣组数据变化时同样刷新。 for sig in ["guild_changed", "lover_changed"]: if client.has_signal(sig): client.connect(sig, refresh) func is_open() -> bool: return _root != null and _root.visible func get_mobile_window() -> Control: return _root func set_mobile_mode(enabled: bool) -> void: _mobile_mode = enabled _apply_mobile_layout() if _invite_dialog and _invite_dialog.visible: _popup_invite_dialog() func close() -> void: if _root: _root.visible = false if _invite_dialog: _invite_dialog.hide() func toggle() -> void: _root.visible = not _root.visible if _root.visible: refresh() func refresh() -> void: if not is_open() or client == null: return for c in _list.get_children(): c.queue_free() # 三固定分组(对齐原版 MessengerWindow):好友 / 公会 / 情侣。 _section("好友") var friends: Array = client.get_friends() if client.has_method("get_friends") else [] if friends.is_empty(): var e := Label.new() e.text = "(好友列表为空)" e.add_theme_font_size_override("font_size", 11) e.modulate = Color(0.6, 0.6, 0.6) _list.add_child(e) else: for f in friends: _list.add_child(_row(f)) _section("公会") if client.has_method("get_guild") and client.get_guild().get("in_guild", false) \ and client.has_method("get_guild_members"): var members: Array = client.get_guild_members() members.sort_custom(func(a, b): return int(a.get("grade", 99)) < int(b.get("grade", 99))) for m in members: _list.add_child(_guild_row(m)) else: var g := Label.new() g.text = "(未加入公会)" g.add_theme_font_size_override("font_size", 11) g.modulate = Color(0.6, 0.6, 0.6) _list.add_child(g) _section("情侣") if client.has_method("get_lover") and client.get_lover().get("valid", false): _list.add_child(_lover_row(client.get_lover())) else: var l := Label.new() l.text = "(暂无)" l.add_theme_font_size_override("font_size", 11) l.modulate = Color(0.6, 0.6, 0.6) _list.add_child(l) func _section(title: String) -> void: var h := Label.new() h.text = "— %s —" % title h.add_theme_font_size_override("font_size", 12) h.modulate = Color(0.75, 0.82, 0.95) _list.add_child(h) # 公会组成员:在线态来自 GC_GUILD_LOGIN/LOGOUT;点按同样发起私聊。 func _guild_row(m: Dictionary) -> Control: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 46) if _mobile_mode else Vector2(260, 0) var dot := Label.new() dot.text = "●" dot.custom_minimum_size = Vector2(28, 42) if _mobile_mode else Vector2.ZERO dot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER dot.modulate = Color(0.4, 0.9, 0.4) if m.get("online", false) else Color(0.4, 0.4, 0.4) row.add_child(dot) var nm := Button.new() nm.text = String(m.get("name", "?")) nm.flat = true nm.custom_minimum_size = Vector2(450, 42) if _mobile_mode else Vector2(150, 0) nm.alignment = HORIZONTAL_ALIGNMENT_LEFT nm.pressed.connect(func() -> void: whisper_to.emit(String(m.get("name", "")))) row.add_child(nm) if int(m.get("grade", 1)) == 0: var lead := Label.new() lead.text = "会长" lead.modulate = Color(0.95, 0.8, 0.4) lead.add_theme_font_size_override("font_size", 10) row.add_child(lead) return row # 情侣组成员:lover_login/logout/near/far(CHAT_TYPE_COMMAND,marriage.cpp)。 func _lover_row(lover: Dictionary) -> Control: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 46) if _mobile_mode else Vector2(260, 0) var dot := Label.new() dot.text = "♥" dot.custom_minimum_size = Vector2(28, 42) if _mobile_mode else Vector2.ZERO dot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER dot.modulate = Color(0.95, 0.4, 0.55) if lover.get("online", false) else Color(0.4, 0.4, 0.4) row.add_child(dot) var nm := Button.new() nm.text = String(lover.get("name", "?")) nm.flat = true nm.custom_minimum_size = Vector2(450, 42) if _mobile_mode else Vector2(150, 0) nm.alignment = HORIZONTAL_ALIGNMENT_LEFT nm.pressed.connect(func() -> void: whisper_to.emit(String(lover.get("name", "")))) row.add_child(nm) if lover.get("near", false): var near := Label.new() near.text = "在身边" near.modulate = Color(1.0, 0.6, 0.7) near.add_theme_font_size_override("font_size", 10) row.add_child(near) return row func _row(f: Dictionary) -> Control: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(640, 46) if _mobile_mode else Vector2(260, 0) var dot := Label.new() dot.text = "●" dot.custom_minimum_size = Vector2(28, 42) if _mobile_mode else Vector2.ZERO dot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER dot.modulate = Color(0.4, 0.9, 0.4) if f.get("online", false) else Color(0.4, 0.4, 0.4) row.add_child(dot) var nm := Button.new() nm.text = String(f.get("name", "?")) nm.flat = true nm.custom_minimum_size = Vector2(450, 42) if _mobile_mode else Vector2(150, 0) nm.alignment = HORIZONTAL_ALIGNMENT_LEFT nm.pressed.connect(func() -> void: whisper_to.emit(String(f.get("name", "")))) row.add_child(nm) if f.get("mobile", false): var mobile := Label.new() mobile.text = "手机" mobile.modulate = Color(0.45, 0.8, 1.0) mobile.add_theme_font_size_override("font_size", 10) row.add_child(mobile) var del := Button.new() del.text = "×" del.custom_minimum_size = Vector2(64, 42) if _mobile_mode else Vector2.ZERO del.pressed.connect(func() -> void: client.remove_friend(String(f.get("name", "")))) row.add_child(del) return row func _on_add() -> void: var nm := _name_edit.text.strip_edges() if nm != "": client.add_friend(nm) _name_edit.clear() func _on_friend_invite(name: String) -> void: if client == null or name.strip_edges() == "": return _pending_invite = name _invite_dialog.dialog_text = "接受“%s”的好友请求?" % name _popup_invite_dialog() func _answer_invite(accept: bool) -> void: var name := _pending_invite _pending_invite = "" if _invite_dialog: _invite_dialog.hide() if name != "" and client != null and client.has_method("friend_answer"): client.friend_answer(name, accept) func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_CENTER) _root.position = Vector2(-150, -180) _root.size = Vector2(300, 360) _root.visible = false _root.set_meta("is_titlebar", true) _root.set_meta("mobile_title", "好友") 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.07, 0.08, 0.1, 0.96) sb.set_corner_radius_all(4) panel.add_theme_stylebox_override("panel", sb) _root.add_child(panel) _title = Label.new() _title.text = "好友" _title.position = Vector2(12, 8) _root.add_child(_title) _list = VBoxContainer.new() _list.position = Vector2(12, 34) _list.size = Vector2(276, 278) _list.add_theme_constant_override("separation", 3) _root.add_child(_list) _bottom = HBoxContainer.new() _bottom.position = Vector2(12, 320) _bottom.custom_minimum_size = Vector2(276, 0) _root.add_child(_bottom) _name_edit = LineEdit.new() _name_edit.placeholder_text = "角色名" _name_edit.custom_minimum_size = Vector2(190, 0) _bottom.add_child(_name_edit) _add_button = Button.new() _add_button.text = "添加" _add_button.pressed.connect(_on_add) _bottom.add_child(_add_button) _invite_dialog = ConfirmationDialog.new() _invite_dialog.name = "FriendInviteDialog" _invite_dialog.title = "好友请求" _invite_dialog.ok_button_text = "接受" _invite_dialog.cancel_button_text = "拒绝" _invite_dialog.confirmed.connect(func() -> void: _answer_invite(true)) _invite_dialog.canceled.connect(func() -> void: _answer_invite(false)) # Keep the dialog outside the toggleable friend panel: requests must still # be visible when the friend list window itself is closed. parent.add_child(_invite_dialog) if parent.has_method("track_mobile_modal"): parent.track_mobile_modal(_invite_dialog) _apply_mobile_layout() func _popup_invite_dialog() -> void: if not is_instance_valid(_invite_dialog): return if _mobile_mode: # Native Window remains the owner of the confirmation signals, while its # mobile footprint is large enough for landscape touch targets and the # software keyboard-safe input area. _invite_dialog.set_meta("mobile_title", "好友请求") _invite_dialog.min_size = Vector2i(480, 220) _invite_dialog.size = Vector2i(480, 220) _invite_dialog.popup_centered(Vector2i(480, 220)) else: _invite_dialog.popup_centered() func _apply_mobile_layout() -> void: if _root == null or _title == null or _bottom == null: return if _mobile_mode: _root.size = Vector2(680, 344) _root.position = Vector2(-340, -172) _title.position = Vector2(18, 48) _title.add_theme_font_size_override("font_size", 16) _list.position = Vector2(18, 88) _list.size = Vector2(644, 190) _bottom.position = Vector2(18, 288) _bottom.custom_minimum_size = Vector2(644, 44) _name_edit.custom_minimum_size = Vector2(520, 44) _add_button.custom_minimum_size = Vector2(112, 44) else: _root.size = Vector2(300, 360) _root.position = Vector2(-150, -180) _title.position = Vector2(12, 8) _title.remove_theme_font_size_override("font_size") _list.position = Vector2(12, 34) _list.size = Vector2(276, 278) _bottom.position = Vector2(12, 320) _bottom.custom_minimum_size = Vector2(276, 0) _name_edit.custom_minimum_size = Vector2(190, 0) _add_button.custom_minimum_size = Vector2(0, 0)