Files
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00

299 lines
10 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/farCHAT_TYPE_COMMANDmarriage.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)