extends Control # ============================================================================ # ChatUI.gd - 聊天界面控制器(T 唤起 / Enter 发送) # ============================================================================ # 聊天系统的用户界面控制器 # # 核心职责: # - 显示聊天消息历史 # - 处理用户输入 # - 显示连接状态 # - T 唤起聊天框,Enter 发送消息 # - 点击聊天框外部隐藏聊天框 # - 失焦后 5 秒自动隐藏 # - 显示/隐藏带 0.5s 过渡动画 # - Call Down: 通过 EventSystem 订阅聊天事件 # # 使用方式: # var chat_ui := preload("res://scenes/ui/ChatUI.tscn").instantiate() # add_child(chat_ui) # # 注意事项: # - 遵循 "Signal Up, Call Down" 架构 # - 使用 @onready 缓存节点引用 # - 所有 UI 操作通过 ChatManager # ============================================================================ # ============================================================================ # 节点引用 # ============================================================================ # 聊天面板 @onready var chat_panel: Control = %ChatPanel # 聊天历史容器 @onready var chat_history: ScrollContainer = %ChatHistory # 消息列表 @onready var message_list: VBoxContainer = %MessageList # 聊天输入框 @onready var chat_input: LineEdit = %ChatInput # 发送按钮 @onready var send_button: Button = %SendButton # 气泡发送按钮(世界频道专用) @onready var bubble_send_button: Button = %BubbleSendButton # 频道标签 @onready var popular_tab_button: Button = %PopularTabButton @onready var recent_tab_button: Button = %RecentTabButton @onready var friends_tab_button: Button = %FriendsTabButton @onready var input_shell: Control = %InputShell @onready var friends_list: HBoxContainer = %FriendsList @onready var friends_list_surface: Control = %FriendsListSurface @onready var add_friend_row: HBoxContainer = %AddFriendRow @onready var add_friend_button: Button = %AddFriendButton # ============================================================================ # 预加载资源 # ============================================================================ # 聊天消息场景 @onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn") const CHAT_TRANSITION_DURATION: float = 0.5 const TAB_ACTIVE_COLOR: Color = Color(0.258824, 0.627451, 0.913725, 1.0) const TAB_INACTIVE_COLOR: Color = Color(0.560784, 0.639216, 0.733333, 1.0) const TAB_UNDERLINE_HIDDEN: Color = Color(1.0, 1.0, 1.0, 0.0) const TAB_ACTIVE_BACKGROUND: Color = Color(0.918, 0.961, 0.992, 0.98) const TAB_IDLE_BACKGROUND: Color = Color(1.0, 1.0, 1.0, 0.54) const TAB_BORDER_COLOR: Color = Color(0.788, 0.851, 0.910, 0.38) const CHAT_INPUT_PLACEHOLDER: String = "输入消息..." const SYSTEM_SENDER_NAME: String = "系统" const FRIENDS_UNSUPPORTED_TEXT: String = "当前后端暂不支持好友功能" const FRIEND_CHAT_EMPTY_TEXT: String = "从右下角好友列表选择一位好友开始聊天" const TAB_WORLD: String = "world" const TAB_WHISPER: String = "whisper" const TAB_FRIENDS: String = "friends" const MAX_DISPLAYED_MESSAGES: int = 100 const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"] # ============================================================================ # 成员变量 # ============================================================================ # 是否显示聊天框 var _is_chat_visible: bool = false # 隐藏计时器 var _hide_timer: Timer = null # 是否在输入中(输入时不隐藏) var _is_typing: bool = false # 显示/隐藏过渡动画 var _transition_tween: Tween = null # 当前用户名 var _current_username: String = "" # 当前频道标签 var _current_tab: String = TAB_WORLD # 悄悄话目标(靠近玩家按 E 后设置) var _whisper_target_user_id: String = "" var _whisper_target_username: String = "" # 好友私聊目标(好友列表接入后复用同一私聊协议) var _friend_target_user_id: String = "" var _friend_target_username: String = "" var _friends: Array[Dictionary] = [] var _friends_supported: bool = true var _friends_status_message: String = "" # 当前会话消息缓存,用于切换标签时重新筛选渲染 var _messages: Array[Dictionary] = [] var _send_failure_handled_by_ui: bool = false # ============================================================================ # 生命周期方法 # ============================================================================ # 准备就绪 func _ready() -> void: add_to_group("whaletown_escape_dismissible") _configure_mouse_focus() # 初始隐藏聊天框 hide_chat(true) # 创建隐藏计时器 _create_hide_timer() # 订阅事件(Call Down via EventSystem) _subscribe_to_events() # 连接 UI 信号 _connect_ui_signals() # 尽可能保持回车提交后输入框继续编辑状态(Godot 4.6+ 支持该属性) _enable_keep_editing_on_submit() _update_tab_visuals() _update_bubble_send_button_visibility() # 清理 func _exit_tree() -> void: # 取消事件订阅 var eventSystem: Node = _get_event_system() if eventSystem != null: eventSystem.call("disconnect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self) eventSystem.call("disconnect_event", EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self) eventSystem.call("disconnect_event", EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self) eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self) eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_FAILED, _on_login_failed, self) eventSystem.call("disconnect_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, _on_private_target_selected, self) eventSystem.call("disconnect_event", EventNames.CHAT_FRIEND_SELECTED, _on_friend_selected, self) eventSystem.call("disconnect_event", EventNames.CHAT_FRIENDS_UPDATED, _on_friends_updated, self) eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self) # 清理计时器 if _hide_timer: _hide_timer.queue_free() if is_instance_valid(_transition_tween): _transition_tween.kill() _transition_tween = null func _get_event_system() -> Node: return get_node_or_null("/root/EventSystem") func _get_chat_manager() -> Node: return get_node_or_null("/root/ChatManager") func _get_settings_manager() -> Node: return get_node_or_null("/root/SettingsManager") # ============================================================================ # 输入处理 # ============================================================================ # 处理全局输入 func _input(event: InputEvent) -> void: if get_viewport().is_input_handled(): return if event is InputEventMouseButton: _handle_global_mouse_button_input(event as InputEventMouseButton) return if not (event is InputEventKey): return var key_event := event as InputEventKey if not key_event.pressed or key_event.echo: return # T 键用于唤起聊天(输入框聚焦时不拦截) if key_event.keycode == KEY_T and not chat_input.has_focus(): _handle_t_pressed() return # Enter 键用于发送聊天 if key_event.keycode == KEY_ENTER or key_event.keycode == KEY_KP_ENTER: _handle_enter_pressed() func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: _handle_unhandled_mouse_button_input(event as InputEventMouseButton) func _handle_unhandled_mouse_button_input(mouse_event: InputEventMouseButton) -> void: _handle_global_mouse_button_input(mouse_event) func _handle_global_mouse_button_input(mouse_event: InputEventMouseButton) -> void: if not mouse_event.pressed or mouse_event.button_index != MOUSE_BUTTON_LEFT: return if not _is_chat_visible: return if is_instance_valid(chat_panel) and chat_panel.get_global_rect().has_point(mouse_event.position): return hide_chat() # 处理 T 键按下 func _handle_t_pressed() -> void: if not _is_chat_visible: show_chat() get_viewport().set_input_as_handled() # 处理 Enter 键按下 func _handle_enter_pressed() -> void: # 聊天框未显示时不处理 Enter if not _is_chat_visible: return # 输入框有焦点时,发送逻辑交给 LineEdit 的 text_submitted 统一处理 if chat_input.has_focus(): return # 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时) chat_input.grab_focus() # 延迟获取输入框焦点(避免事件冲突) func _grab_input_focus() -> void: if chat_input: chat_input.grab_focus() func _configure_mouse_focus() -> void: if is_instance_valid(chat_input): chat_input.mouse_filter = Control.MOUSE_FILTER_STOP _update_input_placeholder() if is_instance_valid(input_shell): input_shell.mouse_filter = Control.MOUSE_FILTER_STOP if is_instance_valid(friends_list): friends_list.mouse_filter = Control.MOUSE_FILTER_STOP func _update_input_placeholder() -> void: if not is_instance_valid(chat_input): return if chat_input.has_focus() or not chat_input.text.strip_edges().is_empty(): chat_input.placeholder_text = "" return if _current_tab == TAB_WHISPER and not _whisper_target_username.is_empty(): chat_input.placeholder_text = "对 %s 说点什么..." % _whisper_target_username return if _current_tab == TAB_FRIENDS and not _friend_target_username.is_empty(): chat_input.placeholder_text = "对 %s 说点什么..." % _friend_target_username return chat_input.placeholder_text = CHAT_INPUT_PLACEHOLDER # 处理 GUI 输入(鼠标点击) func _gui_input(event: InputEvent) -> void: # 检查鼠标点击 if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_LEFT: _handle_click_outside() # 处理点击聊天框外部区域 func _handle_click_outside() -> void: if not _is_chat_visible: return # 检查点击是否在聊天面板外部 if not chat_panel.get_global_rect().has_point(get_global_mouse_position()): hide_chat() # 延迟释放输入框焦点(由 call_deferred 调用) func _release_input_focus() -> void: _release_chat_focus() func _release_chat_focus() -> void: var releasedTextFocus := false if is_instance_valid(chat_input) and chat_input.has_focus(): releasedTextFocus = true chat_input.release_focus() var viewport := get_viewport() if viewport != null and viewport.gui_get_focus_owner() == chat_input: releasedTextFocus = true viewport.gui_release_focus() if releasedTextFocus: _reset_movement_input_state() func _reset_movement_input_state() -> void: Input.flush_buffered_events() for action in MOVEMENT_ACTIONS: Input.action_release(action) # ============================================================================ # 显示/隐藏逻辑 # ============================================================================ # 显示聊天框 func show_chat(immediate: bool = false) -> void: _is_chat_visible = true _update_input_placeholder() # 停止隐藏计时器 _stop_hide_timer() if not is_instance_valid(chat_panel): return _stop_transition_tween() chat_panel.show() if immediate: chat_panel.modulate.a = 1.0 return chat_panel.modulate.a = 0.0 _transition_tween = create_tween() _transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) _transition_tween.tween_property(chat_panel, "modulate:a", 1.0, CHAT_TRANSITION_DURATION) _transition_tween.finished.connect(_on_show_transition_finished) # 隐藏聊天框 func hide_chat(immediate: bool = false) -> void: _is_chat_visible = false _is_typing = false _release_chat_focus() _update_input_placeholder() # 停止隐藏计时器 _stop_hide_timer() if not is_instance_valid(chat_panel): return _stop_transition_tween() if immediate: chat_panel.hide() chat_panel.modulate.a = 1.0 return if not chat_panel.visible: chat_panel.modulate.a = 1.0 return _transition_tween = create_tween() _transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) _transition_tween.tween_property(chat_panel, "modulate:a", 0.0, CHAT_TRANSITION_DURATION) _transition_tween.finished.connect(_on_hide_transition_finished) func is_escape_dismissible() -> bool: return _is_chat_visible func get_escape_priority() -> int: return 480 func request_escape_close() -> void: hide_chat() # 创建隐藏计时器 func _create_hide_timer() -> void: _hide_timer = Timer.new() _hide_timer.wait_time = 5.0 # 5 秒 _hide_timer.one_shot = true _hide_timer.timeout.connect(_on_hide_timeout) add_child(_hide_timer) # 开始隐藏倒计时 func _start_hide_timer() -> void: if _is_typing: return # 输入时不隐藏 if not is_instance_valid(_hide_timer): return if not _hide_timer.is_inside_tree(): return _stop_hide_timer() # 先停止之前的计时器 _hide_timer.start() # 停止隐藏倒计时 func _stop_hide_timer() -> void: if is_instance_valid(_hide_timer) and _hide_timer.is_inside_tree(): _hide_timer.stop() # 隐藏计时器超时 func _on_hide_timeout() -> void: hide_chat() func _on_show_transition_finished() -> void: _transition_tween = null func _on_hide_transition_finished() -> void: _transition_tween = null if not _is_chat_visible and is_instance_valid(chat_panel): chat_panel.hide() chat_panel.modulate.a = 1.0 func _stop_transition_tween() -> void: if is_instance_valid(_transition_tween): _transition_tween.kill() _transition_tween = null # ============================================================================ # UI 事件处理 # ============================================================================ # 连接 UI 信号 func _connect_ui_signals() -> void: # 输入框回车 chat_input.text_submitted.connect(_on_chat_input_submitted) chat_input.gui_input.connect(_on_chat_input_gui_input) # 输入框焦点变化 chat_input.focus_entered.connect(_on_input_focus_entered) chat_input.focus_exited.connect(_on_input_focus_exited) if is_instance_valid(send_button): send_button.pressed.connect(_on_send_button_pressed) if is_instance_valid(bubble_send_button): bubble_send_button.pressed.connect(_on_bubble_send_button_pressed) if is_instance_valid(add_friend_button): add_friend_button.pressed.connect(_on_add_friend_button_pressed) if is_instance_valid(popular_tab_button): popular_tab_button.pressed.connect(func() -> void: select_tab(TAB_WORLD)) if is_instance_valid(recent_tab_button): recent_tab_button.pressed.connect(func() -> void: select_tab(TAB_WHISPER)) if is_instance_valid(friends_tab_button): friends_tab_button.pressed.connect(func() -> void: select_tab(TAB_FRIENDS)) if is_instance_valid(input_shell): input_shell.gui_input.connect(_on_input_shell_gui_input) # 输入框获得焦点 func _on_input_focus_entered() -> void: _is_typing = true _update_input_placeholder() _stop_hide_timer() # 停止隐藏计时器 func _on_chat_input_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mouse_event := event as InputEventMouseButton if mouse_event.pressed and mouse_event.button_index == MOUSE_BUTTON_LEFT: call_deferred("_grab_input_focus") func _on_input_shell_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mouse_event := event as InputEventMouseButton if mouse_event.pressed and mouse_event.button_index == MOUSE_BUTTON_LEFT: call_deferred("_grab_input_focus") input_shell.accept_event() # 输入框失去焦点 func _on_input_focus_exited() -> void: _is_typing = false _update_input_placeholder() if not is_inside_tree(): return # 开始 5 秒倒计时 if not _is_chat_visible: return _start_hide_timer() # 发送按钮点击处理 func _on_send_button_pressed() -> void: _send_input_message(false) func _on_bubble_send_button_pressed() -> void: if _current_tab != TAB_WORLD: return if not _settings_bool("show_chat_bubbles", true): _add_system_message("聊天气泡已在设置中关闭") return _send_input_message(true) func _send_input_message(show_bubble: bool) -> void: var content: String = chat_input.text.strip_edges() if content.is_empty(): _add_system_message("请先输入消息") call_deferred("_focus_input_after_send") return # 发送消息 var chatManager: Node = _get_chat_manager() if chatManager == null: push_warning("ChatUI: ChatManager autoload is not available.") _add_system_message("聊天系统未加载") call_deferred("_focus_input_after_send") return _send_failure_handled_by_ui = false var sent: bool = _send_current_tab_message(chatManager, content, show_bubble) if not sent: if not _send_failure_handled_by_ui: _add_system_message("消息未发送,请确认已经登录") call_deferred("_focus_input_after_send") return # 清空输入框 chat_input.clear() # 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点 call_deferred("_focus_input_after_send") func _on_add_friend_button_pressed() -> void: add_whisper_target_as_friend() func _focus_input_after_send() -> void: if not _is_chat_visible: return if not is_instance_valid(chat_input): return chat_input.grab_focus() # 聊天输入提交(回车键)处理 func _on_chat_input_submitted(_text: String) -> void: _send_input_message(false) # 即便提交的是空串,也保持输入焦点,便于连续输入 call_deferred("_focus_input_after_send") func _enable_keep_editing_on_submit() -> void: if not is_instance_valid(chat_input): return for property in chat_input.get_property_list(): if property.get("name", "") == "keep_editing_on_text_submit": chat_input.set("keep_editing_on_text_submit", true) return # ============================================================================ # 事件订阅(Call Down) # ============================================================================ # 订阅事件 func _subscribe_to_events() -> void: var eventSystem: Node = _get_event_system() if eventSystem == null: push_warning("ChatUI: EventSystem autoload is not available.") return # 订阅聊天消息接收事件 eventSystem.call("connect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self) # 订阅聊天错误事件 eventSystem.call("connect_event", EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self) # 订阅连接状态变化事件 eventSystem.call("connect_event", EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self) # 订阅登录成功事件 eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self) # 订阅登录失败事件 eventSystem.call("connect_event", EventNames.CHAT_LOGIN_FAILED, _on_login_failed, self) # 订阅近身悄悄话目标选择事件 eventSystem.call("connect_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, _on_private_target_selected, self) # 订阅右下角好友列表的好友选择事件 eventSystem.call("connect_event", EventNames.CHAT_FRIEND_SELECTED, _on_friend_selected, self) # 订阅好友列表更新事件 eventSystem.call("connect_event", EventNames.CHAT_FRIENDS_UPDATED, _on_friends_updated, self) eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self) # ============================================================================ # 事件处理器 # ============================================================================ # 处理接收到的聊天消息 func _on_chat_message_received(data: Dictionary) -> void: if not _should_show_incoming_message(data): return var from_user: String = data.get("from_user", "") var content: String = data.get("content", "") var timestamp: float = data.get("timestamp", 0.0) var is_self: bool = bool(data.get("is_self", false)) if not data.has("is_self") and not _current_username.is_empty() and from_user == _current_username: is_self = true # 添加到消息历史 var message := data.duplicate(true) message["from_user"] = from_user message["content"] = content message["timestamp"] = timestamp message["is_self"] = is_self _add_message_data(message) # 处理聊天错误 func _on_chat_error(data: Dictionary) -> void: var error_code: String = data.get("error_code", "") var message: String = data.get("message", "") # 聊天发送失败通常是业务校验(频率/内容)导致,不应按引擎级错误处理。 push_warning("ChatUI: [%s] %s" % [error_code, message]) if _is_friend_status_error(error_code): _friends_status_message = message if _current_tab == TAB_FRIENDS: _render_friend_conversation_header() return if not message.strip_edges().is_empty(): _add_system_message(message) # 处理连接状态变化 func _on_connection_state_changed(_data: Dictionary) -> void: # 连接状态变化处理(当前不更新UI) pass # 处理登录成功 func _on_login_success(data: Dictionary) -> void: _current_username = data.get("username", "") func _on_login_failed(data: Dictionary) -> void: var message: String = str(data.get("message", "聊天登录失败,请重新登录")) _add_system_message(message) # 选择聊天频道标签。 func select_tab(tab_name: String) -> void: var normalized_tab := _normalize_tab_name(tab_name) if normalized_tab.is_empty(): return _current_tab = normalized_tab _update_tab_visuals() _update_friends_list_visibility() _update_add_friend_row_visibility() _update_bubble_send_button_visibility() _update_input_placeholder() _rerender_messages() func get_current_tab() -> String: return _current_tab func _update_tab_visuals() -> void: _apply_tab_visual(popular_tab_button, TAB_WORLD) _apply_tab_visual(recent_tab_button, TAB_WHISPER) _apply_tab_visual(friends_tab_button, TAB_FRIENDS) func _apply_tab_visual(button: Button, tab_name: String) -> void: if not is_instance_valid(button): return var is_active := tab_name == _current_tab var color := TAB_ACTIVE_COLOR if is_active else TAB_INACTIVE_COLOR button.add_theme_color_override("font_color", color) button.add_theme_color_override("font_hover_color", TAB_ACTIVE_COLOR) button.add_theme_color_override("font_pressed_color", TAB_ACTIVE_COLOR) button.add_theme_stylebox_override("normal", _create_tab_style(is_active, false)) button.add_theme_stylebox_override("hover", _create_tab_style(is_active, true)) button.add_theme_stylebox_override("pressed", _create_tab_style(is_active, true, true)) var underline := button.get_parent().get_node_or_null("Underline") as ColorRect if underline != null: underline.color = TAB_ACTIVE_COLOR if is_active else TAB_UNDERLINE_HIDDEN func _create_tab_style(is_active: bool, is_hovered: bool, is_pressed: bool = false) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = TAB_ACTIVE_BACKGROUND if is_active else TAB_IDLE_BACKGROUND if is_hovered: style.bg_color = style.bg_color.lightened(0.12) if is_pressed: style.bg_color = style.bg_color.darkened(0.12) style.border_width_left = 1 style.border_width_top = 1 style.border_width_right = 1 style.border_width_bottom = 1 style.border_color = Color(0.56, 0.78, 0.94, 0.76) if is_active else TAB_BORDER_COLOR style.corner_radius_top_left = 14 style.corner_radius_top_right = 14 style.corner_radius_bottom_left = 14 style.corner_radius_bottom_right = 14 return style func _normalize_tab_name(tab_name: String) -> String: match tab_name: TAB_WORLD, "popular": return TAB_WORLD TAB_WHISPER, "recent": return TAB_WHISPER TAB_FRIENDS: return TAB_FRIENDS _: return "" func _send_current_tab_message(chatManager: Node, content: String, show_bubble: bool = false) -> bool: match _current_tab: TAB_WORLD: return bool(chatManager.call("send_chat_message", content, "global", show_bubble)) TAB_WHISPER: if _whisper_target_user_id.is_empty(): _add_system_message("请靠近玩家按 E 发起悄悄话") _send_failure_handled_by_ui = true return false return bool(chatManager.call("send_private_message", content, _whisper_target_user_id, _whisper_target_username, TAB_WHISPER)) TAB_FRIENDS: if _friend_target_user_id.is_empty(): _add_system_message("请从右下角好友列表选择一位好友") _send_failure_handled_by_ui = true return false return bool(chatManager.call("send_private_message", content, _friend_target_user_id, _friend_target_username, TAB_FRIENDS)) _: return false func _on_private_target_selected(data: Dictionary) -> void: var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() if user_id.is_empty(): return var username := str(data.get("username", "")).strip_edges() start_whisper(user_id, username) func _on_friend_selected(data: Dictionary) -> void: var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() if user_id.is_empty(): return var username := str(data.get("username", "")).strip_edges() select_friend_private_target(user_id, username) func start_whisper(user_id: String, username: String = "") -> void: var normalized_user_id := user_id.strip_edges() if normalized_user_id.is_empty(): return _whisper_target_user_id = normalized_user_id _whisper_target_username = username.strip_edges() if _whisper_target_username.is_empty(): _whisper_target_username = "玩家" select_tab(TAB_WHISPER) _update_add_friend_row_visibility() show_chat(true) call_deferred("_focus_input_after_send") func add_whisper_target_as_friend() -> bool: if _whisper_target_user_id.is_empty(): _add_system_message("请先靠近玩家按 F") return false var chatManager: Node = _get_chat_manager() if chatManager == null or not chatManager.has_method("request_friend"): _add_system_message("好友系统未加载") return false return bool(chatManager.call("request_friend", _whisper_target_user_id, _whisper_target_username)) func request_friend_with_target(user_id: String, username: String = "") -> bool: var normalized_user_id := user_id.strip_edges() if normalized_user_id.is_empty(): _add_system_message("请选择玩家后再发送好友请求") return false _whisper_target_user_id = normalized_user_id _whisper_target_username = username.strip_edges() if _whisper_target_username.is_empty(): _whisper_target_username = "玩家" var chatManager: Node = _get_chat_manager() if chatManager == null or not chatManager.has_method("request_friend"): _add_system_message("好友系统未加载") return false show_chat(true) return bool(chatManager.call("request_friend", _whisper_target_user_id, _whisper_target_username)) func select_friend_private_target(user_id: String, username: String = "") -> void: var normalized_user_id := user_id.strip_edges() if normalized_user_id.is_empty(): return _friend_target_user_id = normalized_user_id _friend_target_username = username.strip_edges() if _friend_target_username.is_empty(): _friend_target_username = "好友" select_tab(TAB_FRIENDS) _render_friend_conversation_header() show_chat(true) call_deferred("_focus_input_after_send") func _on_friends_updated(data: Dictionary) -> void: _friends.clear() _friends_supported = bool(data.get("supported", true)) _friends_status_message = str(data.get("status", "")).strip_edges() var friends_variant: Variant = data.get("friends", []) if friends_variant is Array: for friend_variant in friends_variant: if friend_variant is Dictionary: _friends.append(friend_variant) _render_friend_conversation_header() func _on_settings_changed(_data: Dictionary) -> void: _update_bubble_send_button_visibility() func _update_friends_list_visibility() -> void: if not is_instance_valid(friends_list): return friends_list.visible = _current_tab == TAB_FRIENDS if is_instance_valid(friends_list_surface): friends_list_surface.visible = _current_tab == TAB_FRIENDS if friends_list.visible: _render_friend_conversation_header() func _update_add_friend_row_visibility() -> void: if not is_instance_valid(add_friend_row): return add_friend_row.visible = _current_tab == TAB_WHISPER and not _whisper_target_user_id.is_empty() func _update_bubble_send_button_visibility() -> void: if not is_instance_valid(bubble_send_button): return bubble_send_button.visible = _current_tab == TAB_WORLD and _settings_bool("show_chat_bubbles", true) func _render_friend_conversation_header() -> void: if not is_instance_valid(friends_list): return for child in friends_list.get_children(): friends_list.remove_child(child) child.queue_free() if not _friends_supported: _render_friends_status_label(_friends_status_message if not _friends_status_message.is_empty() else FRIENDS_UNSUPPORTED_TEXT) return if _friend_target_user_id.is_empty(): _render_friends_status_label(FRIEND_CHAT_EMPTY_TEXT) return var avatar := PanelContainer.new() avatar.name = "FriendConversationAvatarPanel" avatar.custom_minimum_size = Vector2(34, 34) avatar.size_flags_vertical = Control.SIZE_SHRINK_CENTER avatar.add_theme_stylebox_override("panel", _create_friend_avatar_style()) friends_list.add_child(avatar) var avatar_label := Label.new() avatar_label.text = _friend_target_username.substr(0, 1).to_upper() avatar_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER avatar_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER avatar_label.add_theme_color_override("font_color", Color.WHITE) avatar_label.add_theme_font_size_override("font_size", 14) avatar.add_child(avatar_label) var title_box := VBoxContainer.new() title_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL title_box.add_theme_constant_override("separation", 0) friends_list.add_child(title_box) var name_label := Label.new() name_label.text = _friend_target_username name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS name_label.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424)) name_label.add_theme_font_size_override("font_size", 15) title_box.add_child(name_label) var hint_label := Label.new() hint_label.text = "好友私聊" hint_label.add_theme_color_override("font_color", TAB_INACTIVE_COLOR) hint_label.add_theme_font_size_override("font_size", 12) title_box.add_child(hint_label) func _render_friends_status_label(message: String) -> void: var label := Label.new() label.text = message label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.add_theme_color_override("font_color", TAB_INACTIVE_COLOR) label.add_theme_font_size_override("font_size", 14) friends_list.add_child(label) func _is_friend_status_error(error_code: String) -> bool: match error_code: "FRIEND_REQUEST_SENT", "FRIEND_REQUEST_REJECTED": return true _: return false func _create_friend_avatar_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.361, 0.690, 0.914) style.border_width_left = 1 style.border_width_top = 1 style.border_width_right = 1 style.border_width_bottom = 1 style.border_color = Color(1, 1, 1, 0.78) style.corner_radius_top_left = 8 style.corner_radius_top_right = 8 style.corner_radius_bottom_left = 8 style.corner_radius_bottom_right = 8 return style # ============================================================================ # 公共 API - 消息管理 # ============================================================================ # 添加消息到历史 # # 参数: # from_user: String - 发送者用户名 # content: String - 消息内容 # timestamp: float - 时间戳 # is_self: bool - 是否为自己发送的消息(默认 false) func add_message_to_history(from_user: String, content: String, timestamp: float, is_self: bool) -> void: _add_message_data({ "from_user": from_user, "content": content, "timestamp": timestamp, "is_self": is_self, "scope": "system" if from_user == SYSTEM_SENDER_NAME else "global", "tab": _current_tab }) func _add_message_data(message: Dictionary) -> void: _messages.append(message.duplicate(true)) if _messages.size() > MAX_DISPLAYED_MESSAGES: _messages.pop_front() if not _message_matches_current_tab(message): return _render_message(message) func _render_message(message: Dictionary) -> void: # 如果聊天框隐藏,自动显示 if not _is_chat_visible: show_chat() # 每条消息用一行容器包起来,方便左右对齐且不挤在一起 var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN row.alignment = BoxContainer.ALIGNMENT_END if bool(message.get("is_self", false)) else BoxContainer.ALIGNMENT_BEGIN # 创建消息节点 var message_node: Control = chat_message_scene.instantiate() as Control if message_node == null: return # 先加入场景树,再设置内容(避免 ChatMessage._ready 尚未执行导致节点引用为空) message_list.add_child(row) row.add_child(message_node) # 设置消息内容 if message_node.has_method("set_message"): message_node.call( "set_message", _get_display_sender_name(message), str(message.get("content", "")), float(message.get("timestamp", 0.0)), bool(message.get("is_self", false)) ) # 自动滚动到底部 call_deferred("_scroll_to_bottom") func _rerender_messages() -> void: if not is_instance_valid(message_list): return for child in message_list.get_children(): message_list.remove_child(child) child.queue_free() for message in _messages: if _message_matches_current_tab(message): _render_message(message) func _message_matches_current_tab(message: Dictionary) -> bool: var scope := str(message.get("scope", "local")).strip_edges().to_lower() var is_private := bool(message.get("is_private", false)) or scope == "private" if scope == "system": return str(message.get("tab", _current_tab)) == _current_tab if _current_tab == TAB_WORLD: return not is_private if _current_tab == TAB_WHISPER or _current_tab == TAB_FRIENDS: return _private_message_matches_current_tab(message) return true func _private_message_matches_current_tab(message: Dictionary) -> bool: var scope := str(message.get("scope", "local")).strip_edges().to_lower() var is_private := bool(message.get("is_private", false)) or scope == "private" if not is_private: return false var private_context := str(message.get("private_context", message.get("privateContext", ""))).strip_edges().to_lower() if private_context == TAB_WHISPER: return _current_tab == TAB_WHISPER and _private_message_matches_target(message, _whisper_target_user_id) if private_context == TAB_FRIENDS: return _current_tab == TAB_FRIENDS and _private_message_matches_target(message, _friend_target_user_id) if _current_tab == TAB_WHISPER: return _private_message_matches_target(message, _whisper_target_user_id) if _current_tab == TAB_FRIENDS: return _private_message_matches_target(message, _friend_target_user_id) return false func _private_message_matches_target(message: Dictionary, target_user_id: String) -> bool: var normalized_target := target_user_id.strip_edges() if normalized_target.is_empty(): return false var from_user_id := str(message.get("from_user_id", message.get("fromUserId", ""))).strip_edges() var to_user_id := str(message.get("to_user_id", message.get("toUserId", ""))).strip_edges() return from_user_id == normalized_target or to_user_id == normalized_target func _get_display_sender_name(message: Dictionary) -> String: var from_user := str(message.get("from_user", "")) var scope := str(message.get("scope", "local")).strip_edges().to_lower() var is_private := bool(message.get("is_private", false)) or scope == "private" if not is_private: return from_user if bool(message.get("is_self", false)): return "我" if from_user.strip_edges().is_empty(): from_user = "玩家" return from_user func _add_system_message(message: String) -> void: var normalized_message := message.strip_edges() if normalized_message.is_empty(): return if not _is_chat_visible: show_chat(true) add_message_to_history(SYSTEM_SENDER_NAME, normalized_message, Time.get_unix_time_from_system(), false) func _should_show_incoming_message(data: Dictionary) -> bool: if bool(data.get("is_self", false)): return true var scope := str(data.get("scope", "")).strip_edges().to_lower() var is_private := bool(data.get("is_private", false)) or scope == "private" if is_private: return _settings_bool("private_notifications", true) return _settings_bool("world_notifications", true) func _settings_bool(key: String, defaultValue: bool) -> bool: var settingsManager := _get_settings_manager() if settingsManager != null and settingsManager.has_method("get_bool"): return bool(settingsManager.call("get_bool", key)) return defaultValue # ============================================================================ # 内部方法 - UI 更新 # ============================================================================ # 滚动到底部 func _scroll_to_bottom() -> void: # 等待一帧,确保 UI 更新完成 await get_tree().process_frame # 滚动到底部 if is_instance_valid(chat_history): chat_history.scroll_vertical = chat_history.get_v_scroll_bar().max_value