diff --git a/_Core/managers/ChatManager.gd b/_Core/managers/ChatManager.gd index bb43a86..22ec3f2 100644 --- a/_Core/managers/ChatManager.gd +++ b/_Core/managers/ChatManager.gd @@ -122,6 +122,10 @@ var _current_map: String = "" # 游戏 token var _game_token: String = "" +# 发送后本地回显去重(避免服务端也回发导致重复显示) +const SELF_ECHO_DEDUPE_WINDOW: float = 10.0 +var _pending_self_messages: Array[Dictionary] = [] + # ============================================================================ # 生命周期方法 # ============================================================================ @@ -240,7 +244,10 @@ func send_chat_message(content: String, scope: String = "local") -> void: # 发送消息(JSON 字符串) var json_string := JSON.stringify(message_data) - _websocket_manager.send_message(json_string) + var send_err: Error = _websocket_manager.send_message(json_string) + if send_err != OK: + _handle_error("SEND_FAILED", "WebSocket send failed: %s" % error_string(send_err)) + return # 记录发送时间 _record_message_timestamp() @@ -253,6 +260,24 @@ func send_chat_message(content: String, scope: String = "local") -> void: "is_self": true }) + var now_timestamp: float = Time.get_unix_time_from_system() + + # 记录待去重的“自己消息”(如果服务端也回发 chat_render,则避免重复显示) + _pending_self_messages.append({ + "content": content, + "expires_at": now_timestamp + SELF_ECHO_DEDUPE_WINDOW + }) + + # 本地回显:UI 目前只订阅 CHAT_MESSAGE_RECEIVED,所以这里也发一次 received + chat_message_received.emit(_current_username, content, true, now_timestamp) + EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { + "from_user": _current_username, + "content": content, + "show_bubble": true, + "timestamp": now_timestamp, + "is_self": true + }) + print("📤 发送聊天消息: ", content) # 消息发送完成回调 @@ -427,6 +452,7 @@ func _on_history_loaded(messages: Array) -> void: "content": message.get("content", ""), "show_bubble": false, "timestamp": message.get("timestamp", 0.0), + "is_self": (not _current_username.is_empty() and message.get("from_user", "") == _current_username), "is_history": true # 标记为历史消息 }) @@ -504,6 +530,8 @@ func _on_data_received(message: String) -> void: _handle_login_success(data) "login_error": _handle_login_error(data) + "chat": + _handle_chat_render(data) "chat_sent": _handle_chat_sent(data) "chat_error": @@ -580,10 +608,24 @@ func _handle_chat_error(data: Dictionary) -> void: # 处理接收到的聊天消息 func _handle_chat_render(data: Dictionary) -> void: - var from_user: String = data.get("from", "") - var content: String = data.get("txt", "") - var show_bubble: bool = data.get("bubble", false) - var timestamp: float = float(data.get("timestamp", "0.0")) + # 兼容不同后端字段命名: + # - chat_render: {from, txt, bubble, timestamp} + # - chat: {content, scope, (可选 from/username/timestamp)} + var from_user: String = data.get("from", data.get("from_user", data.get("username", ""))) + var content: String = data.get("txt", data.get("content", "")) + var show_bubble: bool = bool(data.get("bubble", data.get("show_bubble", false))) + + var timestamp: float = _parse_chat_timestamp_to_unix(data.get("timestamp", 0.0)) + + var is_self: bool = (not _current_username.is_empty() and from_user == _current_username) + if is_self and _consume_pending_self_message(content): + # 已经本地回显过,避免重复显示 + return + + # 如果服务端没带发送者信息,但内容匹配最近自己发送的消息,则认为是自己消息 + if from_user.is_empty() and _consume_pending_self_message(content): + from_user = _current_username + is_self = true print("📨 收到聊天消息: ", from_user, " -> ", content) @@ -592,7 +634,7 @@ func _handle_chat_render(data: Dictionary) -> void: "from_user": from_user, "content": content, "timestamp": timestamp, - "is_self": false + "is_self": is_self }) # 发射信号 @@ -603,9 +645,44 @@ func _handle_chat_render(data: Dictionary) -> void: "from_user": from_user, "content": content, "show_bubble": show_bubble, - "timestamp": timestamp + "timestamp": timestamp, + "is_self": is_self }) +# 解析聊天消息时间戳(兼容 unix 秒 / ISO 8601 字符串) +func _parse_chat_timestamp_to_unix(timestamp_raw: Variant) -> float: + if typeof(timestamp_raw) == TYPE_INT or typeof(timestamp_raw) == TYPE_FLOAT: + var ts := float(timestamp_raw) + return ts if ts > 0.0 else Time.get_unix_time_from_system() + + var ts_str := str(timestamp_raw) + if ts_str.strip_edges().is_empty(): + return Time.get_unix_time_from_system() + + # 纯数字字符串(必须整串都是数字/小数点,避免把 ISO 字符串前缀 "2026" 误判成时间戳) + var numeric_regex := RegEx.new() + numeric_regex.compile("^\\s*-?\\d+(?:\\.\\d+)?\\s*$") + if numeric_regex.search(ts_str) != null: + var ts_num := float(ts_str) + return ts_num if ts_num > 0.0 else Time.get_unix_time_from_system() + + # ISO 8601: 2026-01-19T15:15:43.930Z + var regex := RegEx.new() + regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})") + var result := regex.search(ts_str) + if result == null: + return Time.get_unix_time_from_system() + + var utc_dict := { + "year": int(result.get_string(1)), + "month": int(result.get_string(2)), + "day": int(result.get_string(3)), + "hour": int(result.get_string(4)), + "minute": int(result.get_string(5)), + "second": int(result.get_string(6)) + } + return Time.get_unix_time_from_datetime_dict(utc_dict) + # 处理位置更新成功 func _handle_position_updated(data: Dictionary) -> void: var stream: String = data.get("stream", "") @@ -665,6 +742,24 @@ func _record_message_timestamp() -> void: var current_time := Time.get_unix_time_from_system() _message_timestamps.append(current_time) +# 消费一个待去重的“自己消息”(允许相同内容多次发送:每次消费一个) +func _consume_pending_self_message(content: String) -> bool: + var now := Time.get_unix_time_from_system() + + # 先清理过期项 + for i in range(_pending_self_messages.size() - 1, -1, -1): + var item: Dictionary = _pending_self_messages[i] + if float(item.get("expires_at", 0.0)) < now: + _pending_self_messages.remove_at(i) + + # 再匹配内容 + for i in range(_pending_self_messages.size() - 1, -1, -1): + if str(_pending_self_messages[i].get("content", "")) == content: + _pending_self_messages.remove_at(i) + return true + + return false + # 添加消息到当前会话历史 func _add_message_to_history(message: Dictionary) -> void: _message_history.append(message) diff --git a/scenes/prefabs/ui/ChatMessage.gd b/scenes/prefabs/ui/ChatMessage.gd index 0e57d84..04655e5 100644 --- a/scenes/prefabs/ui/ChatMessage.gd +++ b/scenes/prefabs/ui/ChatMessage.gd @@ -1,4 +1,4 @@ -extends Panel +extends PanelContainer # ============================================================================ # ChatMessage.gd - 聊天消息气泡组件 @@ -49,22 +49,25 @@ var user_info_container: HBoxContainer # ============================================================================ func _ready() -> void: - # 使用 get_node 获取节点引用 - username_label = get_node_or_null("VBoxContainer/UserInfoContainer/UsernameLabel") - timestamp_label = get_node_or_null("VBoxContainer/UserInfoContainer/TimestampLabel") - content_label = get_node_or_null("VBoxContainer/ContentLabel") - user_info_container = get_node_or_null("VBoxContainer/UserInfoContainer") - - # 调试输出 - if username_label: - print("✅ UsernameLabel found") - else: - print("❌ UsernameLabel NOT found!") - # 打印所有子节点帮助调试 - print("Available children: ", _get_all_children_names(self)) - - # 应用最大宽度限制 - custom_minimum_size.x = min(max_width, get_parent().size.x) + _cache_node_refs() + +func _cache_node_refs() -> void: + if not username_label: + username_label = get_node_or_null("VBoxContainer/UserInfoContainer/UsernameLabel") + if not timestamp_label: + timestamp_label = get_node_or_null("VBoxContainer/UserInfoContainer/TimestampLabel") + if not content_label: + content_label = get_node_or_null("VBoxContainer/ContentLabel") + if not user_info_container: + user_info_container = get_node_or_null("VBoxContainer/UserInfoContainer") + + # 内容换行与自适应高度 + if content_label: + content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + content_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + content_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + content_label.fit_content = true + content_label.scroll_active = false # ============================================================================ # 成员变量 @@ -89,10 +92,15 @@ var _is_self: bool = false # message.set_message("Alice", "Hello!", 1703500800.0, false) func set_message(from_user: String, content: String, timestamp: float, is_self: bool = false) -> void: _is_self = is_self + _cache_node_refs() + + var safe_from_user := from_user + if safe_from_user.strip_edges().is_empty(): + safe_from_user = "我" if is_self else "玩家" # 设置用户名(带空值检查) if username_label: - username_label.text = from_user + username_label.text = safe_from_user else: push_error("ChatMessage: username_label is null!") return @@ -100,7 +108,7 @@ func set_message(from_user: String, content: String, timestamp: float, is_self: # 设置内容 if content_label: content_label.clear() # 清除默认文本和所有内容 - content_label.bbcode_text = content # 使用 bbcode_text 因为 bbcode_enabled = true + content_label.append_text(content) # 作为纯文本追加,避免 BBCode 解析导致内容不显示 else: push_error("ChatMessage: content_label is null!") return @@ -124,10 +132,6 @@ func _apply_style() -> void: if not username_label or not timestamp_label or not user_info_container: return - # 设置大小约束 - 宽度固定,高度适应内容 - if get_parent(): - custom_minimum_size.x = min(max_width, get_parent().size.x) - # 重要:设置垂直 size flags 让 Panel 适应内容高度 size_flags_vertical = Control.SIZE_SHRINK_BEGIN @@ -142,6 +146,8 @@ func _apply_style() -> void: # 设置文字颜色 - ID使用金色 #FFD700 username_label.add_theme_color_override("font_color", Color(1.0, 0.8431373, 0.0)) timestamp_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) + if content_label: + content_label.add_theme_color_override("default_color", Color(0.95, 0.97, 1.0)) else: # 他人的消息:左侧对齐,灰色背景 size_flags_horizontal = Control.SIZE_SHRINK_BEGIN @@ -153,11 +159,13 @@ func _apply_style() -> void: # 设置文字颜色 - ID使用蓝色 #69c0ff username_label.add_theme_color_override("font_color", Color(0.4117647, 0.7529412, 1.0)) timestamp_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5)) + if content_label: + content_label.add_theme_color_override("default_color", Color(0.15, 0.15, 0.15)) # 获取自己消息的样式 func _get_self_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() - style.bg_color = Color(0.2, 0.6, 1.0, 0.3) + style.bg_color = Color(0.2, 0.6, 1.0, 0.8) style.corner_radius_top_left = 10 style.corner_radius_top_right = 10 style.corner_radius_bottom_left = 10 @@ -171,7 +179,7 @@ func _get_self_style() -> StyleBoxFlat: # 获取他人消息的样式 func _get_other_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() - style.bg_color = Color(0.9, 0.9, 0.9, 0.5) + style.bg_color = Color(0.9, 0.9, 0.9, 0.8) style.corner_radius_top_left = 10 style.corner_radius_top_right = 10 style.corner_radius_bottom_left = 2 diff --git a/scenes/prefabs/ui/ChatMessage.tscn b/scenes/prefabs/ui/ChatMessage.tscn index 308bdb5..68b4928 100644 --- a/scenes/prefabs/ui/ChatMessage.tscn +++ b/scenes/prefabs/ui/ChatMessage.tscn @@ -2,19 +2,15 @@ [ext_resource type="Script" path="res://scenes/prefabs/ui/ChatMessage.gd" id="1"] -[node name="ChatMessage" type="Panel"] +[node name="ChatMessage" type="PanelContainer"] +layout_mode = 2 offset_right = 400.0 size_flags_horizontal = 3 size_flags_vertical = 6 script = ExtResource("1") [node name="VBoxContainer" type="VBoxContainer" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 +layout_mode = 2 theme_override_constants/separation = 4 [node name="UserInfoContainer" type="HBoxContainer" parent="VBoxContainer"] diff --git a/scenes/ui/ChatUI.gd b/scenes/ui/ChatUI.gd index f63eb38..a693a6d 100644 --- a/scenes/ui/ChatUI.gd +++ b/scenes/ui/ChatUI.gd @@ -272,8 +272,12 @@ func _on_chat_message_received(data: Dictionary) -> void: 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 + # 添加到消息历史 - add_message_to_history(from_user, content, timestamp, false) + add_message_to_history(from_user, content, timestamp, is_self) # 处理聊天错误 func _on_chat_error(data: Dictionary) -> void: @@ -307,15 +311,23 @@ func add_message_to_history(from_user: String, content: String, timestamp: float if not _is_chat_visible: show_chat() + # 每条消息用一行容器包起来,方便左右对齐且不挤在一起 + var row := HBoxContainer.new() + row.layout_mode = 2 # 让 VBoxContainer 接管布局,否则会重叠在同一位置 + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + row.alignment = BoxContainer.ALIGNMENT_END if is_self else BoxContainer.ALIGNMENT_BEGIN + # 创建消息节点 var message_node: ChatMessage = chat_message_scene.instantiate() + # 先加入场景树,再设置内容(避免 ChatMessage._ready 尚未执行导致节点引用为空) + message_list.add_child(row) + row.add_child(message_node) + # 设置消息内容 message_node.set_message(from_user, content, timestamp, is_self) - # 添加到列表 - message_list.add_child(message_node) - # 自动滚动到底部 call_deferred("_scroll_to_bottom")