forked from datawhale/whale-town-front
Compare commits
1 Commits
e989b4adf1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603e7d9fc6 |
@@ -122,6 +122,10 @@ var _current_map: String = ""
|
|||||||
# 游戏 token
|
# 游戏 token
|
||||||
var _game_token: String = ""
|
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 字符串)
|
# 发送消息(JSON 字符串)
|
||||||
var json_string := JSON.stringify(message_data)
|
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()
|
_record_message_timestamp()
|
||||||
@@ -253,6 +260,24 @@ func send_chat_message(content: String, scope: String = "local") -> void:
|
|||||||
"is_self": true
|
"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)
|
print("📤 发送聊天消息: ", content)
|
||||||
|
|
||||||
# 消息发送完成回调
|
# 消息发送完成回调
|
||||||
@@ -427,6 +452,7 @@ func _on_history_loaded(messages: Array) -> void:
|
|||||||
"content": message.get("content", ""),
|
"content": message.get("content", ""),
|
||||||
"show_bubble": false,
|
"show_bubble": false,
|
||||||
"timestamp": message.get("timestamp", 0.0),
|
"timestamp": message.get("timestamp", 0.0),
|
||||||
|
"is_self": (not _current_username.is_empty() and message.get("from_user", "") == _current_username),
|
||||||
"is_history": true # 标记为历史消息
|
"is_history": true # 标记为历史消息
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -504,6 +530,8 @@ func _on_data_received(message: String) -> void:
|
|||||||
_handle_login_success(data)
|
_handle_login_success(data)
|
||||||
"login_error":
|
"login_error":
|
||||||
_handle_login_error(data)
|
_handle_login_error(data)
|
||||||
|
"chat":
|
||||||
|
_handle_chat_render(data)
|
||||||
"chat_sent":
|
"chat_sent":
|
||||||
_handle_chat_sent(data)
|
_handle_chat_sent(data)
|
||||||
"chat_error":
|
"chat_error":
|
||||||
@@ -580,10 +608,24 @@ func _handle_chat_error(data: Dictionary) -> void:
|
|||||||
|
|
||||||
# 处理接收到的聊天消息
|
# 处理接收到的聊天消息
|
||||||
func _handle_chat_render(data: Dictionary) -> void:
|
func _handle_chat_render(data: Dictionary) -> void:
|
||||||
var from_user: String = data.get("from", "")
|
# 兼容不同后端字段命名:
|
||||||
var content: String = data.get("txt", "")
|
# - chat_render: {from, txt, bubble, timestamp}
|
||||||
var show_bubble: bool = data.get("bubble", false)
|
# - chat: {content, scope, (可选 from/username/timestamp)}
|
||||||
var timestamp: float = float(data.get("timestamp", "0.0"))
|
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)
|
print("📨 收到聊天消息: ", from_user, " -> ", content)
|
||||||
|
|
||||||
@@ -592,7 +634,7 @@ func _handle_chat_render(data: Dictionary) -> void:
|
|||||||
"from_user": from_user,
|
"from_user": from_user,
|
||||||
"content": content,
|
"content": content,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"is_self": false
|
"is_self": is_self
|
||||||
})
|
})
|
||||||
|
|
||||||
# 发射信号
|
# 发射信号
|
||||||
@@ -603,9 +645,44 @@ func _handle_chat_render(data: Dictionary) -> void:
|
|||||||
"from_user": from_user,
|
"from_user": from_user,
|
||||||
"content": content,
|
"content": content,
|
||||||
"show_bubble": show_bubble,
|
"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:
|
func _handle_position_updated(data: Dictionary) -> void:
|
||||||
var stream: String = data.get("stream", "")
|
var stream: String = data.get("stream", "")
|
||||||
@@ -665,6 +742,24 @@ func _record_message_timestamp() -> void:
|
|||||||
var current_time := Time.get_unix_time_from_system()
|
var current_time := Time.get_unix_time_from_system()
|
||||||
_message_timestamps.append(current_time)
|
_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:
|
func _add_message_to_history(message: Dictionary) -> void:
|
||||||
_message_history.append(message)
|
_message_history.append(message)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
extends Panel
|
extends PanelContainer
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ChatMessage.gd - 聊天消息气泡组件
|
# ChatMessage.gd - 聊天消息气泡组件
|
||||||
@@ -49,22 +49,25 @@ var user_info_container: HBoxContainer
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# 使用 get_node 获取节点引用
|
_cache_node_refs()
|
||||||
username_label = get_node_or_null("VBoxContainer/UserInfoContainer/UsernameLabel")
|
|
||||||
timestamp_label = get_node_or_null("VBoxContainer/UserInfoContainer/TimestampLabel")
|
func _cache_node_refs() -> void:
|
||||||
content_label = get_node_or_null("VBoxContainer/ContentLabel")
|
if not username_label:
|
||||||
user_info_container = get_node_or_null("VBoxContainer/UserInfoContainer")
|
username_label = get_node_or_null("VBoxContainer/UserInfoContainer/UsernameLabel")
|
||||||
|
if not timestamp_label:
|
||||||
# 调试输出
|
timestamp_label = get_node_or_null("VBoxContainer/UserInfoContainer/TimestampLabel")
|
||||||
if username_label:
|
if not content_label:
|
||||||
print("✅ UsernameLabel found")
|
content_label = get_node_or_null("VBoxContainer/ContentLabel")
|
||||||
else:
|
if not user_info_container:
|
||||||
print("❌ UsernameLabel NOT found!")
|
user_info_container = get_node_or_null("VBoxContainer/UserInfoContainer")
|
||||||
# 打印所有子节点帮助调试
|
|
||||||
print("Available children: ", _get_all_children_names(self))
|
# 内容换行与自适应高度
|
||||||
|
if content_label:
|
||||||
# 应用最大宽度限制
|
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
custom_minimum_size.x = min(max_width, get_parent().size.x)
|
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)
|
# message.set_message("Alice", "Hello!", 1703500800.0, false)
|
||||||
func set_message(from_user: String, content: String, timestamp: float, is_self: bool = false) -> void:
|
func set_message(from_user: String, content: String, timestamp: float, is_self: bool = false) -> void:
|
||||||
_is_self = is_self
|
_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:
|
if username_label:
|
||||||
username_label.text = from_user
|
username_label.text = safe_from_user
|
||||||
else:
|
else:
|
||||||
push_error("ChatMessage: username_label is null!")
|
push_error("ChatMessage: username_label is null!")
|
||||||
return
|
return
|
||||||
@@ -100,7 +108,7 @@ func set_message(from_user: String, content: String, timestamp: float, is_self:
|
|||||||
# 设置内容
|
# 设置内容
|
||||||
if content_label:
|
if content_label:
|
||||||
content_label.clear() # 清除默认文本和所有内容
|
content_label.clear() # 清除默认文本和所有内容
|
||||||
content_label.bbcode_text = content # 使用 bbcode_text 因为 bbcode_enabled = true
|
content_label.append_text(content) # 作为纯文本追加,避免 BBCode 解析导致内容不显示
|
||||||
else:
|
else:
|
||||||
push_error("ChatMessage: content_label is null!")
|
push_error("ChatMessage: content_label is null!")
|
||||||
return
|
return
|
||||||
@@ -124,10 +132,6 @@ func _apply_style() -> void:
|
|||||||
if not username_label or not timestamp_label or not user_info_container:
|
if not username_label or not timestamp_label or not user_info_container:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 设置大小约束 - 宽度固定,高度适应内容
|
|
||||||
if get_parent():
|
|
||||||
custom_minimum_size.x = min(max_width, get_parent().size.x)
|
|
||||||
|
|
||||||
# 重要:设置垂直 size flags 让 Panel 适应内容高度
|
# 重要:设置垂直 size flags 让 Panel 适应内容高度
|
||||||
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
@@ -142,6 +146,8 @@ func _apply_style() -> void:
|
|||||||
# 设置文字颜色 - ID使用金色 #FFD700
|
# 设置文字颜色 - ID使用金色 #FFD700
|
||||||
username_label.add_theme_color_override("font_color", Color(1.0, 0.8431373, 0.0))
|
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))
|
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:
|
else:
|
||||||
# 他人的消息:左侧对齐,灰色背景
|
# 他人的消息:左侧对齐,灰色背景
|
||||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
@@ -153,11 +159,13 @@ func _apply_style() -> void:
|
|||||||
# 设置文字颜色 - ID使用蓝色 #69c0ff
|
# 设置文字颜色 - ID使用蓝色 #69c0ff
|
||||||
username_label.add_theme_color_override("font_color", Color(0.4117647, 0.7529412, 1.0))
|
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))
|
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:
|
func _get_self_style() -> StyleBoxFlat:
|
||||||
var style := StyleBoxFlat.new()
|
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_left = 10
|
||||||
style.corner_radius_top_right = 10
|
style.corner_radius_top_right = 10
|
||||||
style.corner_radius_bottom_left = 10
|
style.corner_radius_bottom_left = 10
|
||||||
@@ -171,7 +179,7 @@ func _get_self_style() -> StyleBoxFlat:
|
|||||||
# 获取他人消息的样式
|
# 获取他人消息的样式
|
||||||
func _get_other_style() -> StyleBoxFlat:
|
func _get_other_style() -> StyleBoxFlat:
|
||||||
var style := StyleBoxFlat.new()
|
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_left = 10
|
||||||
style.corner_radius_top_right = 10
|
style.corner_radius_top_right = 10
|
||||||
style.corner_radius_bottom_left = 2
|
style.corner_radius_bottom_left = 2
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" path="res://scenes/prefabs/ui/ChatMessage.gd" id="1"]
|
[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
|
offset_right = 400.0
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 6
|
size_flags_vertical = 6
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 2
|
||||||
anchors_preset = 15
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
theme_override_constants/separation = 4
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
[node name="UserInfoContainer" type="HBoxContainer" parent="VBoxContainer"]
|
[node name="UserInfoContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
|||||||
@@ -272,8 +272,12 @@ func _on_chat_message_received(data: Dictionary) -> void:
|
|||||||
var content: String = data.get("content", "")
|
var content: String = data.get("content", "")
|
||||||
var timestamp: float = data.get("timestamp", 0.0)
|
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:
|
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:
|
if not _is_chat_visible:
|
||||||
show_chat()
|
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()
|
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_node.set_message(from_user, content, timestamp, is_self)
|
||||||
|
|
||||||
# 添加到列表
|
|
||||||
message_list.add_child(message_node)
|
|
||||||
|
|
||||||
# 自动滚动到底部
|
# 自动滚动到底部
|
||||||
call_deferred("_scroll_to_bottom")
|
call_deferred("_scroll_to_bottom")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user