extends PanelContainer # ============================================================================ # ChatMessage.gd - 聊天消息气泡组件 # ============================================================================ # 显示单条聊天消息的 UI 组件 # # 核心职责: # - 显示消息发送者、内容、时间戳 # - 区分自己和他人的消息样式 # - 自动格式化时间戳 # # 使用方式: # var message := chat_message_scene.instantiate() # message.set_message("PlayerName", "Hello!", timestamp, false) # # 注意事项: # - 使用 @onready 缓存节点引用 # - 最大宽度限制为 400 像素 # ============================================================================ class_name ChatMessage # ============================================================================ # 导出参数 # ============================================================================ # 最大宽度(像素) @export var max_width: int = 400 const MIN_BUBBLE_TEXT_WIDTH: float = 86.0 const BUBBLE_HORIZONTAL_MARGIN: float = 28.0 const HEADER_GAP_WIDTH: float = 8.0 const AVATAR_SIZE: float = 40.0 const AVATAR_CORNER_RADIUS: int = 12 # ============================================================================ # 节点引用 # ============================================================================ # 用户名标签 var username_label: Label # 时间戳标签 var timestamp_label: Label # 内容标签 var content_label: RichTextLabel # 用户信息容器 var user_info_container: HBoxContainer # 头像节点 var left_avatar_panel: PanelContainer var right_avatar_panel: PanelContainer var left_avatar_label: Label var right_avatar_label: Label var message_row: HBoxContainer var bubble_panel: PanelContainer var text_container: VBoxContainer # ============================================================================ # 生命周期方法 # ============================================================================ func _ready() -> void: _cache_node_refs() func _cache_node_refs() -> void: if not username_label: username_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow/UsernameLabel") if not timestamp_label: timestamp_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow/TimestampLabel") if not content_label: content_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/ContentLabel") if not user_info_container: user_info_container = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow") if not left_avatar_panel: left_avatar_panel = get_node_or_null("MessageRow/LeftAvatarPanel") if not right_avatar_panel: right_avatar_panel = get_node_or_null("MessageRow/RightAvatarPanel") if not left_avatar_label: left_avatar_label = get_node_or_null("MessageRow/LeftAvatarPanel/AvatarLabel") if not right_avatar_label: right_avatar_label = get_node_or_null("MessageRow/RightAvatarPanel/AvatarLabel") if not message_row: message_row = get_node_or_null("MessageRow") if not bubble_panel: bubble_panel = get_node_or_null("MessageRow/BubblePanel") if not text_container: text_container = get_node_or_null("MessageRow/BubblePanel/TextContainer") _configure_avatar(left_avatar_panel, left_avatar_label) _configure_avatar(right_avatar_panel, right_avatar_label) # 内容换行与自适应高度 if content_label: content_label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN 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 # ============================================================================ # 成员变量 # ============================================================================ # 是否为自己发送的消息 var _is_self: bool = false # ============================================================================ # 公共 API # ============================================================================ # 设置消息内容 # # 参数: # from_user: String - 发送者用户名 # content: String - 消息内容 # timestamp: float - Unix 时间戳 # 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 = safe_from_user else: push_error("ChatMessage: username_label is null!") return var avatar_text := _get_avatar_text(safe_from_user, is_self) if left_avatar_label: left_avatar_label.text = avatar_text if right_avatar_label: right_avatar_label.text = avatar_text # 设置内容 if content_label: content_label.clear() # 清除默认文本和所有内容 content_label.append_text(content) # 作为纯文本追加,避免 BBCode 解析导致内容不显示 else: push_error("ChatMessage: content_label is null!") return # 设置时间戳 if timestamp_label: timestamp_label.text = _format_timestamp(timestamp) else: push_error("ChatMessage: timestamp_label is null!") return _update_bubble_width(safe_from_user, content) # 应用样式 _apply_style() # ============================================================================ # 内部方法 - 样式处理 # ============================================================================ # 应用样式(自己和他人的消息不同) func _apply_style() -> void: if not username_label or not timestamp_label or not user_info_container: return # 重要:设置垂直 size flags 让 Panel 适应内容高度 size_flags_vertical = Control.SIZE_SHRINK_BEGIN size_flags_horizontal = Control.SIZE_EXPAND_FILL if _is_self: if message_row: message_row.alignment = BoxContainer.ALIGNMENT_END user_info_container.alignment = BoxContainer.ALIGNMENT_END _set_avatar_side(true) # 设置面板样式 if bubble_panel: bubble_panel.add_theme_stylebox_override("panel", _get_self_style()) if right_avatar_panel: right_avatar_panel.add_theme_stylebox_override("panel", _get_avatar_style(true)) _apply_selected_avatar(right_avatar_panel, right_avatar_label, username_label.text) # 设置文字颜色 - ID使用金色 #FFD700 username_label.add_theme_color_override("font_color", Color(0.258824, 0.627451, 0.913725)) timestamp_label.add_theme_color_override("font_color", Color(0.560784, 0.639216, 0.733333)) if right_avatar_label: right_avatar_label.add_theme_color_override("font_color", Color(1, 1, 1)) if content_label: content_label.add_theme_color_override("default_color", Color(0.188, 0.294, 0.424)) else: if message_row: message_row.alignment = BoxContainer.ALIGNMENT_BEGIN user_info_container.alignment = BoxContainer.ALIGNMENT_BEGIN _set_avatar_side(false) # 设置面板样式 if bubble_panel: bubble_panel.add_theme_stylebox_override("panel", _get_other_style()) if left_avatar_panel: left_avatar_panel.add_theme_stylebox_override("panel", _get_avatar_style(false)) # 设置文字颜色 - ID使用蓝色 #69c0ff username_label.add_theme_color_override("font_color", Color(0.207843, 0.427451, 0.686275)) timestamp_label.add_theme_color_override("font_color", Color(0.560784, 0.639216, 0.733333)) if left_avatar_label: left_avatar_label.add_theme_color_override("font_color", Color(1, 1, 1)) if content_label: content_label.add_theme_color_override("default_color", Color(0.27451, 0.407843, 0.541176)) func _set_avatar_side(is_self: bool) -> void: if left_avatar_panel: left_avatar_panel.visible = not is_self if right_avatar_panel: right_avatar_panel.visible = is_self func _configure_avatar(panel: PanelContainer, label: Label) -> void: if panel: panel.custom_minimum_size = Vector2(AVATAR_SIZE, AVATAR_SIZE) panel.size = Vector2(AVATAR_SIZE, AVATAR_SIZE) panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN panel.clip_contents = true if label: label.custom_minimum_size = Vector2(AVATAR_SIZE, AVATAR_SIZE) label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER func _update_bubble_width(from_user: String, content: String) -> void: var maxTextWidth := float(max_width) - BUBBLE_HORIZONTAL_MARGIN var contentWidth := _measure_label_text_width(content_label, content, 14.0) var headerWidth := _measure_label_text_width(username_label, from_user, 16.0) + _measure_label_text_width(timestamp_label, timestamp_label.text, 13.0) + HEADER_GAP_WIDTH var textWidth: float = clamp(max(contentWidth, headerWidth, MIN_BUBBLE_TEXT_WIDTH), MIN_BUBBLE_TEXT_WIDTH, maxTextWidth) if username_label: username_label.custom_minimum_size.x = min(_measure_label_text_width(username_label, from_user, 16.0), textWidth) if timestamp_label: timestamp_label.custom_minimum_size.x = _measure_label_text_width(timestamp_label, timestamp_label.text, 13.0) if content_label: content_label.custom_minimum_size.x = textWidth if bubble_panel: bubble_panel.custom_minimum_size.x = textWidth + BUBBLE_HORIZONTAL_MARGIN func _measure_label_text_width(label: Control, text: String, fallback_width: float) -> float: if label != null: var font := label.get_theme_font("font") if font != null: var fontSize := label.get_theme_font_size("font_size") if fontSize <= 0: fontSize = int(fallback_width) var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, fontSize).x if measured > 0.0: return measured + 2.0 var width := 0.0 for index in range(text.length()): var code := text.unicode_at(index) if code <= 0x7f: width += fallback_width * 0.58 else: width += fallback_width return width + 2.0 # 获取自己消息的样式 func _get_self_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.878431, 0.94902, 1.0, 0.96) 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.64, 0.82, 0.95, 0.78) style.corner_radius_top_left = 14 style.corner_radius_top_right = 8 style.corner_radius_bottom_left = 14 style.corner_radius_bottom_right = 14 style.content_margin_left = 14 style.content_margin_right = 14 style.content_margin_top = 9 style.content_margin_bottom = 9 return style # 获取他人消息的样式 func _get_other_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(1, 1, 1, 0.96) 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.78, 0.85, 0.92, 0.72) style.corner_radius_top_left = 8 style.corner_radius_top_right = 14 style.corner_radius_bottom_left = 14 style.corner_radius_bottom_right = 14 style.content_margin_left = 14 style.content_margin_right = 14 style.content_margin_top = 9 style.content_margin_bottom = 9 return style func _get_avatar_style(is_self: bool) -> StyleBoxFlat: if is_self: var appearanceManager := get_node_or_null("/root/AppearanceManager") if appearanceManager != null and appearanceManager.has_method("create_avatar_style") and appearanceManager.has_method("get_selected_avatar"): var avatar: Variant = appearanceManager.call("get_selected_avatar") if avatar is Dictionary: return appearanceManager.call("create_avatar_style", avatar as Dictionary, AVATAR_CORNER_RADIUS) var style := StyleBoxFlat.new() style.bg_color = Color(0.361, 0.690, 0.914) if is_self else Color(0.62, 0.75, 0.86) style.corner_radius_top_left = AVATAR_CORNER_RADIUS style.corner_radius_top_right = AVATAR_CORNER_RADIUS style.corner_radius_bottom_left = AVATAR_CORNER_RADIUS style.corner_radius_bottom_right = AVATAR_CORNER_RADIUS 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.82) return style func _get_avatar_text(from_user: String, is_self: bool) -> String: if is_self: var appearanceManager := get_node_or_null("/root/AppearanceManager") if appearanceManager != null and appearanceManager.has_method("has_custom_avatar") and bool(appearanceManager.call("has_custom_avatar")): return "" if appearanceManager != null and appearanceManager.has_method("get_avatar_label"): return str(appearanceManager.call("get_avatar_label")) return "我" var trimmed := from_user.strip_edges() if trimmed.is_empty(): return "鲸" return trimmed.substr(0, 1) func _apply_selected_avatar(panel: PanelContainer, label: Label, fallbackText: String) -> void: var appearanceManager := get_node_or_null("/root/AppearanceManager") if appearanceManager != null and appearanceManager.has_method("apply_avatar_to_panel"): appearanceManager.call("apply_avatar_to_panel", panel, label, "", fallbackText) # ============================================================================ # 内部方法 - 工具函数 # ============================================================================ # 格式化时间戳 # # 参数: # timestamp: float - Unix 时间戳 # # 返回值: # String - 格式化的时间字符串 func _format_timestamp(timestamp: float) -> String: if timestamp == 0: return "" var datetime := Time.get_datetime_dict_from_unix_time(timestamp) # 格式化为 HH:MM return "%02d:%02d" % [datetime.hour, datetime.minute]