extends Control # ============================================================================ # CafeCompanionPanel.gd - 咖啡店陪伴机器人面板 # ============================================================================ # 点击咖啡店陪伴机器人后,先展示付费时长选择;购买成功后才进入聊天。 # ============================================================================ @onready var chatFrame: PanelContainer = %CafeCompanionFrame @onready var titleLabel: Label = %TitleLabel @onready var subtitleLabel: Label = %SubtitleLabel @onready var productList: VBoxContainer = %ProductList @onready var purchaseButton: Button = %PurchaseButton @onready var statusLabel: Label = %StatusLabel @onready var chatHistory: ScrollContainer = %ChatHistory @onready var messageList: VBoxContainer = %MessageList @onready var chatInput: LineEdit = %ChatInput @onready var sendButton: Button = %SendButton @onready var closeButton: Button = %CloseButton var _currentSessionId: String = "" var _currentTarget: Dictionary = {} var _products: Array = [] var _selectedMinutes: int = 0 var _waitingForReply: bool = false var _resignTarget: Dictionary = {} var _resignDialog: Control var _resignLabel: Label var _resignConfirmButton: Button var _resignCancelButton: Button func _ready() -> void: chatFrame.visible = false purchaseButton.pressed.connect(_on_purchase_pressed) sendButton.pressed.connect(_on_send_pressed) closeButton.pressed.connect(_on_close_pressed) chatInput.text_submitted.connect(_on_input_submitted) _build_resign_dialog() _connect_events() _set_chat_enabled(false) func _exit_tree() -> void: var eventSystem := get_node_or_null("/root/EventSystem") if eventSystem != null: eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_SELECTED, _on_cafe_companion_selected, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_SELF_SELECTED, _on_cafe_companion_self_selected, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_PRODUCTS_READY, _on_products_ready, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_chat_time_purchased, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_MESSAGE_RECEIVED, _on_message_received, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_employment_resigned, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_ERROR_OCCURRED, _on_companion_error, self) func _connect_events() -> void: var eventSystem := get_node_or_null("/root/EventSystem") if eventSystem == null: push_warning("CafeCompanionPanel: EventSystem autoload is not available.") return eventSystem.call("connect_event", EventNames.CAFE_COMPANION_SELECTED, _on_cafe_companion_selected, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_SELF_SELECTED, _on_cafe_companion_self_selected, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_PRODUCTS_READY, _on_products_ready, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_chat_time_purchased, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_MESSAGE_RECEIVED, _on_message_received, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_employment_resigned, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_ERROR_OCCURRED, _on_companion_error, self) func _on_cafe_companion_selected(data: Dictionary) -> void: _currentTarget = data.duplicate(true) _currentSessionId = "" _waitingForReply = false _selectedMinutes = 0 _clear_messages() titleLabel.text = _format_target_title(data) subtitleLabel.text = _format_target_subtitle(data) statusLabel.text = "请选择陪聊时长" purchaseButton.text = "购买聊天时间" purchaseButton.disabled = true chatFrame.visible = true _set_chat_enabled(false) var manager := _get_cafe_companion_manager() if manager == null or not manager.has_method("get_chat_time_products"): _set_error("CafeCompanionManager 未加载") return manager.call("get_chat_time_products") func _on_cafe_companion_self_selected(data: Dictionary) -> void: _resignTarget = data.duplicate(true) _resignLabel.text = _format_resign_message(data) _resignDialog.visible = true _resignDialog.modulate.a = 0.0 var tween := create_tween() tween.tween_property(_resignDialog, "modulate:a", 1.0, 0.12) func _on_employment_resigned(data: Dictionary) -> void: _hide_resign_dialog() _set_error(_format_resign_result(data)) chatFrame.visible = false func _on_products_ready(data: Dictionary) -> void: if not chatFrame.visible: return var productsVariant: Variant = data.get("products", []) _products = productsVariant if productsVariant is Array else [] _render_products() if _products.is_empty(): statusLabel.text = "暂时没有可购买的陪聊时长" else: statusLabel.text = "点击时长后购买,购买成功才会进入聊天" func _on_chat_time_purchased(data: Dictionary) -> void: var sessionData: Dictionary = data var sessionVariant: Variant = data.get("session", {}) if sessionVariant is Dictionary: sessionData = sessionVariant var servicePointId := str(sessionData.get("service_point_id", "")) if not _currentTarget.is_empty() and servicePointId != str(_currentTarget.get("service_point_id", "")): return _currentSessionId = str(sessionData.get("session_id", "")) _waitingForReply = false _set_chat_enabled(true) statusLabel.text = _format_purchase_status(data, sessionData) var companionVariant: Variant = sessionData.get("companion", {}) if companionVariant is Dictionary: var companion: Dictionary = companionVariant titleLabel.text = str(companion.get("persona_name", titleLabel.text)).strip_edges() _clear_messages() var messagesVariant: Variant = sessionData.get("messages", []) if messagesVariant is Array and not messagesVariant.is_empty(): for messageVariant in messagesVariant: if messageVariant is Dictionary: var message: Dictionary = messageVariant _add_message(str(message.get("role", "assistant")), str(message.get("content", ""))) else: var welcomeMessage := str(sessionData.get("welcome_message", "")).strip_edges() if not welcomeMessage.is_empty(): _add_message("assistant", welcomeMessage) chatInput.grab_focus() func _on_message_received(data: Dictionary) -> void: if str(data.get("session_id", "")) != _currentSessionId: return _waitingForReply = false _set_chat_enabled(true) statusLabel.text = _format_remaining_status(data) var messageVariant: Variant = data.get("assistant_message", {}) if messageVariant is Dictionary: var message: Dictionary = messageVariant _add_message("assistant", str(message.get("content", ""))) func _on_companion_error(data: Dictionary) -> void: if not chatFrame.visible and not (_resignDialog != null and _resignDialog.visible): return if _resignDialog != null and _resignDialog.visible and is_instance_valid(_resignLabel): _resignLabel.text = str(data.get("message", "咖啡店陪聊暂时不可用")) _resignConfirmButton.disabled = false return _set_error(str(data.get("message", "咖啡店陪聊暂时不可用"))) func _render_products() -> void: for child in productList.get_children(): child.queue_free() for productVariant in _products: if not (productVariant is Dictionary): continue var product: Dictionary = productVariant var minutes := int(product.get("minutes", 0)) if minutes <= 0: continue var button := Button.new() button.toggle_mode = true button.focus_mode = Control.FOCUS_NONE button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.text = _format_product_label(product) button.pressed.connect(func() -> void: _select_product(minutes) ) productList.add_child(button) func _select_product(minutes: int) -> void: _selectedMinutes = minutes for child in productList.get_children(): var button := child as Button if button != null: button.button_pressed = button.text.begins_with("%d分钟" % minutes) purchaseButton.disabled = false purchaseButton.text = "购买 %d 分钟" % minutes func _on_purchase_pressed() -> void: if _selectedMinutes <= 0: statusLabel.text = "请先选择陪聊时长" return var manager := _get_cafe_companion_manager() if manager == null or not manager.has_method("purchase_chat_time"): _set_error("CafeCompanionManager 未加载") return purchaseButton.disabled = true statusLabel.text = "正在购买陪聊时长..." manager.call("purchase_chat_time", _currentTarget, _selectedMinutes) func _on_send_pressed() -> void: _send_current_message() func _on_input_submitted(_text: String) -> void: _send_current_message() func _send_current_message() -> void: if _waitingForReply: return var content := chatInput.text.strip_edges() if content.is_empty(): return if _currentSessionId.is_empty(): _set_error("请先购买陪聊时长") return chatInput.clear() _add_message("user", content) _waitingForReply = true _set_chat_enabled(false) statusLabel.text = "对方正在回复..." var manager := _get_cafe_companion_manager() if manager == null or not manager.has_method("send_chat_message"): _set_error("CafeCompanionManager 未加载") return manager.call("send_chat_message", _currentSessionId, content) func _on_close_pressed() -> void: chatFrame.visible = false _waitingForReply = false chatInput.release_focus() func _confirm_resign() -> void: var servicePointId := str(_resignTarget.get("service_point_id", "")).strip_edges() if servicePointId.is_empty(): _set_error("离职目标不存在") return var manager := _get_cafe_companion_manager() if manager == null or not manager.has_method("resign_employment"): _set_error("CafeCompanionManager 未加载") return _resignConfirmButton.disabled = true _resignLabel.text = "正在办理离职..." manager.call("resign_employment", servicePointId) func _hide_resign_dialog() -> void: if is_instance_valid(_resignDialog): _resignDialog.visible = false if is_instance_valid(_resignConfirmButton): _resignConfirmButton.disabled = false func _set_error(message: String) -> void: _waitingForReply = false _set_chat_enabled(not _currentSessionId.is_empty()) purchaseButton.disabled = _selectedMinutes <= 0 statusLabel.text = message func _set_chat_enabled(enabled: bool) -> void: chatInput.editable = enabled sendButton.disabled = not enabled func _clear_messages() -> void: for child in messageList.get_children(): child.queue_free() func _add_message(role: String, content: String) -> void: var normalizedContent := content.strip_edges() if normalizedContent.is_empty(): return var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.alignment = BoxContainer.ALIGNMENT_END if role == "user" else BoxContainer.ALIGNMENT_BEGIN var bubble := PanelContainer.new() bubble.custom_minimum_size = Vector2(120, 0) bubble.size_flags_horizontal = 0 bubble.add_theme_stylebox_override("panel", _make_bubble_style(role)) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 14) margin.add_theme_constant_override("margin_top", 8) margin.add_theme_constant_override("margin_right", 14) margin.add_theme_constant_override("margin_bottom", 8) var label := Label.new() label.text = normalizedContent label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.custom_minimum_size = Vector2(300, 0) label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.add_theme_color_override("font_color", Color(0.16, 0.20, 0.25, 1)) label.add_theme_font_size_override("font_size", 16) margin.add_child(label) bubble.add_child(margin) row.add_child(bubble) messageList.add_child(row) call_deferred("_scroll_to_bottom") func _make_bubble_style(role: String) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.80, 0.92, 0.98, 1) if role == "user" else Color(1, 1, 1, 0.96) style.border_color = Color(0.55, 0.72, 0.82, 0.35) style.set_border_width_all(1) style.set_corner_radius_all(10) return style func _scroll_to_bottom() -> void: if is_instance_valid(chatHistory): chatHistory.scroll_vertical = int(chatHistory.get_v_scroll_bar().max_value) func _get_cafe_companion_manager() -> Node: return get_node_or_null("/root/CafeCompanionManager") func _build_resign_dialog() -> void: _resignDialog = Control.new() _resignDialog.name = "CafeEmploymentResignDialog" _resignDialog.set_anchors_preset(Control.PRESET_CENTER) _resignDialog.custom_minimum_size = Vector2(460, 240) _resignDialog.offset_left = -230 _resignDialog.offset_top = -120 _resignDialog.offset_right = 230 _resignDialog.offset_bottom = 120 _resignDialog.visible = false _resignDialog.mouse_filter = Control.MOUSE_FILTER_STOP add_child(_resignDialog) var bg := PanelContainer.new() bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.add_theme_stylebox_override("panel", _make_dialog_style()) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE _resignDialog.add_child(bg) var margin := MarginContainer.new() margin.set_anchors_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_left", 28) margin.add_theme_constant_override("margin_top", 28) margin.add_theme_constant_override("margin_right", 28) margin.add_theme_constant_override("margin_bottom", 24) _resignDialog.add_child(margin) var box := VBoxContainer.new() box.alignment = BoxContainer.ALIGNMENT_CENTER box.add_theme_constant_override("separation", 18) margin.add_child(box) _resignLabel = Label.new() _resignLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _resignLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _resignLabel.add_theme_font_size_override("font_size", 18) _resignLabel.add_theme_color_override("font_color", Color(0.16, 0.22, 0.28, 1)) box.add_child(_resignLabel) var row := HBoxContainer.new() row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 12) box.add_child(row) _resignCancelButton = _make_dialog_button("继续打工", false) _resignCancelButton.pressed.connect(_hide_resign_dialog) row.add_child(_resignCancelButton) _resignConfirmButton = _make_dialog_button("确认离职", true) _resignConfirmButton.pressed.connect(_confirm_resign) row.add_child(_resignConfirmButton) func _make_dialog_button(textValue: String, primary: bool) -> Button: var button := Button.new() button.text = textValue button.custom_minimum_size = Vector2(128, 42) button.focus_mode = Control.FOCUS_NONE button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.add_theme_font_size_override("font_size", 16) button.add_theme_color_override("font_color", Color.WHITE if primary else Color(0.16, 0.22, 0.28, 1)) if not primary: button.add_theme_color_override("font_hover_color", Color(0.258824, 0.627451, 0.913725)) button.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0)) button.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55)) button.add_theme_stylebox_override("normal", _make_dialog_button_style(primary, false)) button.add_theme_stylebox_override("hover", _make_dialog_button_style(primary, true)) button.add_theme_stylebox_override("pressed", _make_dialog_button_style(primary, false)) button.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) return button func _make_dialog_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.982, 0.995, 1.0, 0.98) style.border_color = Color(0.46, 0.62, 0.70, 0.28) style.set_border_width_all(1) style.set_corner_radius_all(16) style.shadow_color = Color(0.06, 0.09, 0.12, 0.22) style.shadow_size = 12 style.shadow_offset = Vector2(0, 5) return style func _make_dialog_button_style(primary: bool, hover: bool) -> StyleBoxFlat: var style := StyleBoxFlat.new() if primary: style.bg_color = Color(0.78, 0.26, 0.22, 1.0) if not hover else Color(0.88, 0.32, 0.26, 1.0) else: style.bg_color = Color(0.90, 0.96, 0.98, 1.0) if not hover else Color(0.84, 0.93, 0.97, 1.0) style.set_corner_radius_all(10) return style func _format_resign_message(data: Dictionary) -> String: var personaName := str(data.get("persona_name", "陪伴机器人")).strip_edges() var endsAt := str(data.get("employment_ends_at", "")).strip_edges() var message := "确认结束「%s」的咖啡店打工?" % personaName if not endsAt.is_empty(): message += "\n原定结束时间:%s" % endsAt message += "\n提前离职会按剩余工时扣回部分已获得收益。" return message func _format_resign_result(data: Dictionary) -> String: var penaltyVariant: Variant = data.get("penalty", {}) if penaltyVariant is Dictionary: var penalty: Dictionary = penaltyVariant var amount := int(penalty.get("amount", 0)) if amount > 0: return "已离职,扣回 %d 鲸币收益" % amount return "已结束咖啡店雇佣" func _format_target_title(data: Dictionary) -> String: var personaName := str(data.get("persona_name", "陪伴机器人")).strip_edges() return personaName if not personaName.is_empty() else "陪伴机器人" func _format_target_subtitle(data: Dictionary) -> String: var servicePointLabel := _format_service_point_label(str(data.get("service_point_id", ""))) return "鲸鱼咖啡馆 · %s" % servicePointLabel if not servicePointLabel.is_empty() else "鲸鱼咖啡馆" func _format_service_point_label(servicePointId: String) -> String: if servicePointId.begins_with("ServiceIdlePoint"): var pointNumber := _parse_point_number(servicePointId) if pointNumber > 0: return "%s号陪伴位" % _format_chinese_number(pointNumber) return "" func _format_product_label(product: Dictionary) -> String: var minutes := int(product.get("minutes", 0)) var price := int(product.get("price", 0)) return "%d分钟 · %d鲸币" % [minutes, price] func _format_purchase_status(data: Dictionary, sessionData: Dictionary) -> String: var price := 0 var purchaseVariant: Variant = data.get("purchase", {}) if purchaseVariant is Dictionary: price = int((purchaseVariant as Dictionary).get("price", 0)) elif data.has("product") and data.get("product") is Dictionary: price = int((data.get("product") as Dictionary).get("price", 0)) var remaining := _format_remaining_status(sessionData) if price > 0: return "已购买,花费%d鲸币,%s" % [price, remaining] return "已购买,%s" % remaining func _format_remaining_status(data: Dictionary) -> String: var seconds := int(data.get("remaining_seconds", 0)) if seconds <= 0: var expiresText := str(data.get("expires_at", "")).strip_edges() return "陪聊时间已开启" if expiresText.is_empty() else "陪聊结束时间 %s" % expiresText var minutes := int(ceil(seconds / 60.0)) return "剩余约 %d 分钟" % minutes func _parse_point_number(value: String) -> int: var digits := "" for index in range(value.length()): var character := value.substr(index, 1) if character >= "0" and character <= "9": digits += character return int(digits) if not digits.is_empty() else 0 func _format_chinese_number(value: int) -> String: const CHINESE_DIGITS: Array[String] = [ "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" ] if value <= 0: return str(value) if value < 10: return CHINESE_DIGITS[value] if value == 10: return "十" if value < 20: return "十%s" % CHINESE_DIGITS[value - 10] if value < 100: var tens := value / 10 var ones := value % 10 return "%s十%s" % [CHINESE_DIGITS[tens], CHINESE_DIGITS[ones]] if ones > 0 else "%s十" % CHINESE_DIGITS[tens] return str(value)