forked from xiangwang25/whale-town-front-v2
1072 lines
36 KiB
GDScript
1072 lines
36 KiB
GDScript
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:
|
||
_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 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 _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
|