forked from xiangwang25/whale-town-front-v2
Initial WhaleTown V2 frontend
This commit is contained in:
2373
scenes/ui/AuthScene.gd
Normal file
2373
scenes/ui/AuthScene.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scenes/ui/AuthScene.gd.uid
Normal file
1
scenes/ui/AuthScene.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://drvhq1pc3630q
|
||||
12
scenes/ui/AuthScene.tscn
Normal file
12
scenes/ui/AuthScene.tscn
Normal file
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dkmpv7bgmot2e"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/AuthScene.gd" id="1_auth"]
|
||||
|
||||
[node name="AuthScene" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_auth")
|
||||
63
scenes/ui/BubbleSendButton.gd
Normal file
63
scenes/ui/BubbleSendButton.gd
Normal file
@@ -0,0 +1,63 @@
|
||||
extends Button
|
||||
|
||||
# 世界频道气泡发送按钮,线条风格与顶部 HUD 快捷入口保持一致。
|
||||
|
||||
const LINE_COLOR: Color = Color(0.592157, 0.72549, 0.835294, 0.88)
|
||||
const HOVER_COLOR: Color = Color(0.392157, 0.717647, 0.94902, 0.96)
|
||||
const LINE_WIDTH: float = 0.95
|
||||
const DETAIL_WIDTH: float = 0.82
|
||||
const BASE_SIZE: float = 27.0
|
||||
|
||||
var _iconScale: float = 1.0
|
||||
var _iconOffset: Vector2 = Vector2.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
text = ""
|
||||
queue_redraw()
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_THEME_CHANGED:
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
_iconScale = min(size.x, size.y) / BASE_SIZE
|
||||
_iconOffset = (size - Vector2(BASE_SIZE, BASE_SIZE) * _iconScale) * 0.5
|
||||
|
||||
var lineColor := HOVER_COLOR if is_hovered() else LINE_COLOR
|
||||
_draw_round_rect(Rect2(6.0, 7.0, 15.0, 11.0), 3.2, lineColor, LINE_WIDTH)
|
||||
_draw_polyline([
|
||||
Vector2(10.2, 18.0),
|
||||
Vector2(8.2, 21.2),
|
||||
Vector2(14.4, 18.0),
|
||||
], lineColor, LINE_WIDTH)
|
||||
_draw_line(Vector2(9.2, 11.7), Vector2(17.6, 11.7), lineColor, DETAIL_WIDTH)
|
||||
_draw_line(Vector2(9.2, 14.6), Vector2(14.8, 14.6), lineColor, DETAIL_WIDTH)
|
||||
|
||||
func _draw_round_rect(rect: Rect2, radius: float, color: Color, width: float) -> void:
|
||||
var left := rect.position.x
|
||||
var top := rect.position.y
|
||||
var right := rect.end.x
|
||||
var bottom := rect.end.y
|
||||
_draw_line(Vector2(left + radius, top), Vector2(right - radius, top), color, width)
|
||||
_draw_line(Vector2(right, top + radius), Vector2(right, bottom - radius), color, width)
|
||||
_draw_line(Vector2(right - radius, bottom), Vector2(left + radius, bottom), color, width)
|
||||
_draw_line(Vector2(left, bottom - radius), Vector2(left, top + radius), color, width)
|
||||
_draw_arc(Vector2(left + radius, top + radius), radius, PI, PI * 1.5, 8, color, width)
|
||||
_draw_arc(Vector2(right - radius, top + radius), radius, PI * 1.5, TAU, 8, color, width)
|
||||
_draw_arc(Vector2(right - radius, bottom - radius), radius, 0.0, PI * 0.5, 8, color, width)
|
||||
_draw_arc(Vector2(left + radius, bottom - radius), radius, PI * 0.5, PI, 8, color, width)
|
||||
|
||||
func _draw_polyline(points: Array[Vector2], color: Color, width: float) -> void:
|
||||
var packed: PackedVector2Array = []
|
||||
for point in points:
|
||||
packed.append(_p(point))
|
||||
draw_polyline(packed, color, width, true)
|
||||
|
||||
func _draw_line(from: Vector2, to: Vector2, color: Color, width: float) -> void:
|
||||
draw_line(_p(from), _p(to), color, width, true)
|
||||
|
||||
func _draw_arc(center: Vector2, radius: float, startAngle: float, endAngle: float, pointCount: int, color: Color, width: float) -> void:
|
||||
draw_arc(_p(center), radius * _iconScale, startAngle, endAngle, pointCount, color, width, true)
|
||||
|
||||
func _p(point: Vector2) -> Vector2:
|
||||
return _iconOffset + point * _iconScale
|
||||
1
scenes/ui/BubbleSendButton.gd.uid
Normal file
1
scenes/ui/BubbleSendButton.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w3fq3gc8kar2
|
||||
495
scenes/ui/CafeCompanionPanel.gd
Normal file
495
scenes/ui/CafeCompanionPanel.gd
Normal file
@@ -0,0 +1,495 @@
|
||||
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)
|
||||
1
scenes/ui/CafeCompanionPanel.gd.uid
Normal file
1
scenes/ui/CafeCompanionPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dm3qcn5gf44au
|
||||
219
scenes/ui/CafeCompanionPanel.tscn
Normal file
219
scenes/ui/CafeCompanionPanel.tscn
Normal file
@@ -0,0 +1,219 @@
|
||||
[gd_scene load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/CafeCompanionPanel.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Panel"]
|
||||
bg_color = Color(0.96, 0.975, 0.982, 0.97)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.46, 0.62, 0.70, 0.28)
|
||||
corner_radius_top_left = 12
|
||||
corner_radius_top_right = 12
|
||||
corner_radius_bottom_right = 12
|
||||
corner_radius_bottom_left = 12
|
||||
shadow_color = Color(0.06, 0.09, 0.12, 0.22)
|
||||
shadow_size = 12
|
||||
shadow_offset = Vector2(0, 5)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Input"]
|
||||
bg_color = Color(1, 1, 1, 0.96)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.55, 0.68, 0.76, 0.36)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
content_margin_left = 12
|
||||
content_margin_top = 4
|
||||
content_margin_right = 12
|
||||
content_margin_bottom = 4
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="Style_InputLine"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonNormal"]
|
||||
bg_color = Color(0.22, 0.56, 0.78, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonHover"]
|
||||
bg_color = Color(0.29, 0.64, 0.84, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonPressed"]
|
||||
bg_color = Color(0.16, 0.45, 0.66, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Close"]
|
||||
bg_color = Color(0.86, 0.91, 0.94, 0.85)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="Style_Empty"]
|
||||
|
||||
[node name="CafeCompanionPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="CafeCompanionFrame" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -560.0
|
||||
offset_top = -442.0
|
||||
offset_right = -28.0
|
||||
offset_bottom = -28.0
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 0
|
||||
mouse_filter = 0
|
||||
theme_override_styles/panel = SubResource("Style_Panel")
|
||||
|
||||
[node name="PanelMargin" type="MarginContainer" parent="CafeCompanionFrame"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 18
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 18
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="ContentVBox" type="VBoxContainer" parent="CafeCompanionFrame/PanelMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HeaderRow" type="HBoxContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TitleStack" type="VBoxContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="CafeCompanionFrame/PanelMargin/ContentVBox/HeaderRow/TitleStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.13, 0.21, 0.27, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "陪伴机器人"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="SubtitleLabel" type="Label" parent="CafeCompanionFrame/PanelMargin/ContentVBox/HeaderRow/TitleStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.40, 0.50, 0.57, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "鲸鱼咖啡馆"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CafeCompanionFrame/PanelMargin/ContentVBox/HeaderRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(0.20, 0.30, 0.36, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_styles/normal = SubResource("Style_Close")
|
||||
theme_override_styles/hover = SubResource("Style_Close")
|
||||
theme_override_styles/pressed = SubResource("Style_Close")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "x"
|
||||
|
||||
[node name="ProductList" type="VBoxContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="PurchaseButton" type="Button" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_styles/normal = SubResource("Style_ButtonNormal")
|
||||
theme_override_styles/hover = SubResource("Style_ButtonHover")
|
||||
theme_override_styles/pressed = SubResource("Style_ButtonPressed")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "购买聊天时间"
|
||||
|
||||
[node name="ChatHistory" type="ScrollContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 270)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="MessageList" type="VBoxContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox/ChatHistory"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 18)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.48, 0.38, 0.22, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = ""
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="InputRow" type="HBoxContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox"]
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="InputShell" type="PanelContainer" parent="CafeCompanionFrame/PanelMargin/ContentVBox/InputRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_styles/panel = SubResource("Style_Input")
|
||||
|
||||
[node name="ChatInput" type="LineEdit" parent="CafeCompanionFrame/PanelMargin/ContentVBox/InputRow/InputShell"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
placeholder_text = "购买后输入消息..."
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_styles/normal = SubResource("Style_InputLine")
|
||||
|
||||
[node name="SendButton" type="Button" parent="CafeCompanionFrame/PanelMargin/ContentVBox/InputRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(72, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_styles/normal = SubResource("Style_ButtonNormal")
|
||||
theme_override_styles/hover = SubResource("Style_ButtonHover")
|
||||
theme_override_styles/pressed = SubResource("Style_ButtonPressed")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "发送"
|
||||
439
scenes/ui/CafeCompanionRecruitmentPanel.gd
Normal file
439
scenes/ui/CafeCompanionRecruitmentPanel.gd
Normal file
@@ -0,0 +1,439 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# CafeCompanionRecruitmentPanel.gd - 咖啡店陪伴机器人招聘登记面板
|
||||
# ============================================================================
|
||||
# 点击咖啡馆吧台鲸鱼标识后打开,玩家提交陪伴 Agent 代理配置,
|
||||
# 后端登记为咖啡店陪伴服务点上的被雇佣角色。
|
||||
# ============================================================================
|
||||
|
||||
@onready var recruitmentFrame: PanelContainer = %CafeRecruitmentFrame
|
||||
@onready var titleLabel: Label = %TitleLabel
|
||||
@onready var subtitleLabel: Label = %SubtitleLabel
|
||||
@onready var closeButton: Button = %CloseButton
|
||||
@onready var servicePointOption: OptionButton = %ServicePointOption
|
||||
@onready var employmentDurationOption: OptionButton = %EmploymentDurationOption
|
||||
@onready var personaNameInput: LineEdit = %PersonaNameInput
|
||||
@onready var protocolOption: OptionButton = %ProtocolOption
|
||||
@onready var baseUrlLabel: Label = %BaseUrlLabel
|
||||
@onready var baseUrlInput: LineEdit = %BaseUrlInput
|
||||
@onready var tokenInput: LineEdit = %TokenInput
|
||||
@onready var modelInput: LineEdit = %ModelInput
|
||||
@onready var fetchModelsButton: Button = %FetchModelsButton
|
||||
@onready var modelOption: OptionButton = %ModelOption
|
||||
@onready var personaPromptInput: TextEdit = %PersonaPromptInput
|
||||
@onready var welcomeMessageInput: LineEdit = %WelcomeMessageInput
|
||||
@onready var enabledCheckBox: CheckBox = %EnabledCheckBox
|
||||
@onready var submitButton: Button = %SubmitButton
|
||||
@onready var statusLabel: Label = %StatusLabel
|
||||
|
||||
var _servicePoints: Array = []
|
||||
var _isSubmitting: bool = false
|
||||
var _isFetchingModels: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
recruitmentFrame.visible = false
|
||||
_setup_protocol_options()
|
||||
_setup_employment_duration_options()
|
||||
closeButton.pressed.connect(_on_close_pressed)
|
||||
submitButton.pressed.connect(_on_submit_pressed)
|
||||
fetchModelsButton.pressed.connect(_on_fetch_models_pressed)
|
||||
protocolOption.item_selected.connect(_on_protocol_selected)
|
||||
modelOption.item_selected.connect(_on_model_option_selected)
|
||||
_connect_events()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem == null:
|
||||
return
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, _on_recruitment_selected, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_SERVICE_POINTS_READY, _on_service_points_ready, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_agent_registered, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_MODELS_READY, _on_models_ready, 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("CafeCompanionRecruitmentPanel: EventSystem autoload is not available.")
|
||||
return
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, _on_recruitment_selected, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_SERVICE_POINTS_READY, _on_service_points_ready, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_agent_registered, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_MODELS_READY, _on_models_ready, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_ERROR_OCCURRED, _on_companion_error, self)
|
||||
|
||||
func _on_recruitment_selected(data: Dictionary) -> void:
|
||||
titleLabel.text = str(data.get("title", "咖啡店陪伴招聘"))
|
||||
subtitleLabel.text = "鲸鱼咖啡馆 · 吧台招聘终端"
|
||||
recruitmentFrame.visible = true
|
||||
_isSubmitting = false
|
||||
_isFetchingModels = false
|
||||
submitButton.disabled = false
|
||||
fetchModelsButton.disabled = false
|
||||
_apply_protocol_defaults(true)
|
||||
_set_status("正在读取陪伴位...", false)
|
||||
_request_service_points()
|
||||
|
||||
func _request_service_points() -> void:
|
||||
var manager := _get_cafe_companion_manager()
|
||||
if manager == null or not manager.has_method("get_service_points"):
|
||||
_set_status("CafeCompanionManager 未加载", true)
|
||||
return
|
||||
manager.call("get_service_points", true)
|
||||
|
||||
func _on_service_points_ready(data: Dictionary) -> void:
|
||||
if not recruitmentFrame.visible:
|
||||
return
|
||||
var servicePointsVariant: Variant = data.get("service_points", [])
|
||||
_servicePoints = servicePointsVariant if servicePointsVariant is Array else []
|
||||
_render_service_points()
|
||||
|
||||
func _on_agent_registered(data: Dictionary) -> void:
|
||||
if not recruitmentFrame.visible:
|
||||
return
|
||||
_isSubmitting = false
|
||||
submitButton.disabled = false
|
||||
fetchModelsButton.disabled = false
|
||||
tokenInput.clear()
|
||||
|
||||
var companionVariant: Variant = data.get("companion", {})
|
||||
if companionVariant is Dictionary:
|
||||
var companion: Dictionary = companionVariant
|
||||
_set_status("%s 已登记到 %s" % [
|
||||
str(companion.get("persona_name", "陪伴机器人")),
|
||||
_format_service_point_label(str(companion.get("service_point_id", ""))),
|
||||
], false)
|
||||
else:
|
||||
_set_status("咖啡店陪伴机器人登记成功", false)
|
||||
recruitmentFrame.visible = false
|
||||
release_focus()
|
||||
|
||||
func _on_companion_error(data: Dictionary) -> void:
|
||||
if not recruitmentFrame.visible:
|
||||
return
|
||||
_isSubmitting = false
|
||||
_isFetchingModels = false
|
||||
submitButton.disabled = false
|
||||
fetchModelsButton.disabled = false
|
||||
_set_status(str(data.get("message", "咖啡店招聘暂时不可用")), true)
|
||||
|
||||
func _render_service_points() -> void:
|
||||
servicePointOption.clear()
|
||||
var selectedIndex := -1
|
||||
for pointVariant in _servicePoints:
|
||||
if not (pointVariant is Dictionary):
|
||||
continue
|
||||
var point: Dictionary = pointVariant
|
||||
var pointId := str(point.get("id", "")).strip_edges()
|
||||
if pointId.is_empty():
|
||||
continue
|
||||
var index := servicePointOption.get_item_count()
|
||||
servicePointOption.add_item(_format_service_point_option(point))
|
||||
servicePointOption.set_item_metadata(index, pointId)
|
||||
if selectedIndex < 0 and not _point_has_companion(point):
|
||||
selectedIndex = index
|
||||
|
||||
if servicePointOption.get_item_count() <= 0:
|
||||
_set_status("当前没有开放的咖啡店陪伴位", true)
|
||||
submitButton.disabled = true
|
||||
return
|
||||
|
||||
servicePointOption.select(selectedIndex if selectedIndex >= 0 else 0)
|
||||
submitButton.disabled = _isSubmitting
|
||||
fetchModelsButton.disabled = _isSubmitting
|
||||
_set_status("填写人设和代理配置后可登记", false)
|
||||
|
||||
func _format_service_point_option(point: Dictionary) -> String:
|
||||
var pointId := str(point.get("id", "")).strip_edges()
|
||||
var label := _format_service_point_label(pointId)
|
||||
var companionVariant: Variant = point.get("companion", null)
|
||||
if companionVariant is Dictionary:
|
||||
var companion: Dictionary = companionVariant
|
||||
var personaName := str(companion.get("persona_name", "")).strip_edges()
|
||||
if not personaName.is_empty():
|
||||
return "%s · 当前:%s" % [label, personaName]
|
||||
return "%s · 空位" % label
|
||||
|
||||
func _point_has_companion(point: Dictionary) -> bool:
|
||||
var companionVariant: Variant = point.get("companion", null)
|
||||
return companionVariant is Dictionary and not (companionVariant as Dictionary).is_empty()
|
||||
|
||||
func _on_submit_pressed() -> void:
|
||||
if _isSubmitting:
|
||||
return
|
||||
var payload := _build_payload()
|
||||
if payload.is_empty():
|
||||
return
|
||||
|
||||
var manager := _get_cafe_companion_manager()
|
||||
if manager == null or not manager.has_method("register_employment_agent"):
|
||||
_set_status("CafeCompanionManager 未加载", true)
|
||||
return
|
||||
|
||||
_isSubmitting = true
|
||||
submitButton.disabled = true
|
||||
fetchModelsButton.disabled = true
|
||||
_set_status("正在验证代理并提交雇佣登记...", false)
|
||||
manager.call("register_employment_agent", payload)
|
||||
|
||||
func _on_fetch_models_pressed() -> void:
|
||||
if _isFetchingModels or _isSubmitting:
|
||||
return
|
||||
|
||||
var protocol := _selected_protocol()
|
||||
var baseUrl := baseUrlInput.text.strip_edges()
|
||||
var token := tokenInput.text.strip_edges()
|
||||
if not (baseUrl.begins_with("http://") or baseUrl.begins_with("https://")):
|
||||
_set_status("接口 URL 需要以 http:// 或 https:// 开头", true)
|
||||
baseUrlInput.grab_focus()
|
||||
return
|
||||
if token.is_empty():
|
||||
_set_status("请先填写接口 Token", true)
|
||||
tokenInput.grab_focus()
|
||||
return
|
||||
|
||||
var manager := _get_cafe_companion_manager()
|
||||
if manager == null or not manager.has_method("get_employment_models"):
|
||||
_set_status("CafeCompanionManager 未加载", true)
|
||||
return
|
||||
|
||||
_isFetchingModels = true
|
||||
fetchModelsButton.disabled = true
|
||||
_set_status("正在获取接口可用模型...", false)
|
||||
manager.call("get_employment_models", protocol, baseUrl, token)
|
||||
|
||||
func _on_models_ready(data: Dictionary) -> void:
|
||||
if not recruitmentFrame.visible:
|
||||
return
|
||||
_isFetchingModels = false
|
||||
fetchModelsButton.disabled = _isSubmitting
|
||||
|
||||
var modelsVariant: Variant = data.get("models", [])
|
||||
var models: Array = modelsVariant if modelsVariant is Array else []
|
||||
_render_models(models)
|
||||
|
||||
func _build_payload() -> Dictionary:
|
||||
var servicePointId := _selected_service_point_id()
|
||||
var personaName := personaNameInput.text.strip_edges()
|
||||
var employmentMinutes := _selected_employment_minutes()
|
||||
var protocol := _selected_protocol()
|
||||
var baseUrl := baseUrlInput.text.strip_edges()
|
||||
var token := tokenInput.text.strip_edges()
|
||||
var model := _selected_model_id()
|
||||
var personaPrompt := personaPromptInput.text.strip_edges()
|
||||
var welcomeMessage := welcomeMessageInput.text.strip_edges()
|
||||
|
||||
if servicePointId.is_empty():
|
||||
_set_status("请选择陪伴位", true)
|
||||
return {}
|
||||
if personaName.is_empty():
|
||||
_set_status("请填写人设名称", true)
|
||||
personaNameInput.grab_focus()
|
||||
return {}
|
||||
if employmentMinutes <= 0:
|
||||
_set_status("请选择打工时间", true)
|
||||
return {}
|
||||
if not (baseUrl.begins_with("http://") or baseUrl.begins_with("https://")):
|
||||
_set_status("接口 URL 需要以 http:// 或 https:// 开头", true)
|
||||
baseUrlInput.grab_focus()
|
||||
return {}
|
||||
if token.is_empty():
|
||||
_set_status("请填写接口 Token", true)
|
||||
tokenInput.grab_focus()
|
||||
return {}
|
||||
if model.is_empty():
|
||||
_set_status("请填写模型名称", true)
|
||||
modelInput.grab_focus()
|
||||
return {}
|
||||
if personaPrompt.is_empty():
|
||||
_set_status("请填写人设指令", true)
|
||||
personaPromptInput.grab_focus()
|
||||
return {}
|
||||
|
||||
var payload := {
|
||||
"service_point_id": servicePointId,
|
||||
"persona_name": personaName,
|
||||
"employment_minutes": employmentMinutes,
|
||||
"protocol": protocol,
|
||||
"base_url": baseUrl,
|
||||
"token": token,
|
||||
"model": model,
|
||||
"persona_prompt": personaPrompt,
|
||||
"enabled": enabledCheckBox.button_pressed,
|
||||
}
|
||||
if not welcomeMessage.is_empty():
|
||||
payload["welcome_message"] = welcomeMessage
|
||||
return payload
|
||||
|
||||
func _setup_employment_duration_options() -> void:
|
||||
employmentDurationOption.clear()
|
||||
var durations := [
|
||||
{"label": "30分钟", "minutes": 30},
|
||||
{"label": "1小时", "minutes": 60},
|
||||
{"label": "2小时", "minutes": 120},
|
||||
{"label": "4小时", "minutes": 240},
|
||||
{"label": "8小时", "minutes": 480},
|
||||
]
|
||||
for duration in durations:
|
||||
var index := employmentDurationOption.get_item_count()
|
||||
employmentDurationOption.add_item(str(duration.get("label", "")))
|
||||
employmentDurationOption.set_item_metadata(index, int(duration.get("minutes", 0)))
|
||||
employmentDurationOption.select(1)
|
||||
|
||||
func _selected_employment_minutes() -> int:
|
||||
var selectedIndex := employmentDurationOption.selected
|
||||
if selectedIndex < 0:
|
||||
return 0
|
||||
return int(employmentDurationOption.get_item_metadata(selectedIndex))
|
||||
|
||||
func _selected_service_point_id() -> String:
|
||||
var selectedIndex := servicePointOption.selected
|
||||
if selectedIndex < 0:
|
||||
return ""
|
||||
return str(servicePointOption.get_item_metadata(selectedIndex)).strip_edges()
|
||||
|
||||
func _setup_protocol_options() -> void:
|
||||
protocolOption.clear()
|
||||
protocolOption.add_item("OpenAI", 0)
|
||||
protocolOption.set_item_metadata(0, "openai")
|
||||
protocolOption.add_item("Anthropic", 1)
|
||||
protocolOption.set_item_metadata(1, "anthropic")
|
||||
protocolOption.select(0)
|
||||
_apply_protocol_defaults(true)
|
||||
|
||||
func _on_protocol_selected(_index: int) -> void:
|
||||
_apply_protocol_defaults(true)
|
||||
_set_status("%s 协议已选择,可填写接口并获取模型" % _selected_protocol_label(), false)
|
||||
|
||||
func _selected_protocol() -> String:
|
||||
var selectedIndex := protocolOption.selected
|
||||
if selectedIndex < 0:
|
||||
return "openai"
|
||||
var protocol := str(protocolOption.get_item_metadata(selectedIndex)).strip_edges()
|
||||
return protocol if not protocol.is_empty() else "openai"
|
||||
|
||||
func _selected_protocol_label() -> String:
|
||||
return "Anthropic" if _selected_protocol() == "anthropic" else "OpenAI"
|
||||
|
||||
func _apply_protocol_defaults(clearModelList: bool) -> void:
|
||||
var protocol := _selected_protocol()
|
||||
if protocol == "anthropic":
|
||||
baseUrlLabel.text = "Anthropic URL"
|
||||
baseUrlInput.placeholder_text = "https://api.anthropic.com"
|
||||
modelInput.placeholder_text = "claude-sonnet-4-5-20250929"
|
||||
if modelInput.text.strip_edges().is_empty() or modelInput.text.strip_edges() == "gpt-4o-mini":
|
||||
modelInput.text = "claude-sonnet-4-5-20250929"
|
||||
else:
|
||||
baseUrlLabel.text = "OpenAI-compatible URL"
|
||||
baseUrlInput.placeholder_text = "https://api.example.com/v1"
|
||||
modelInput.placeholder_text = "gpt-4o-mini"
|
||||
if modelInput.text.strip_edges().is_empty() or modelInput.text.strip_edges().begins_with("claude-"):
|
||||
modelInput.text = "gpt-4o-mini"
|
||||
if clearModelList:
|
||||
modelOption.clear()
|
||||
modelOption.visible = false
|
||||
|
||||
func _render_models(models: Array) -> void:
|
||||
modelOption.clear()
|
||||
var currentModel := modelInput.text.strip_edges()
|
||||
var selectedIndex := -1
|
||||
for modelVariant in models:
|
||||
var modelId := _model_id_from_variant(modelVariant)
|
||||
if modelId.is_empty():
|
||||
continue
|
||||
var index := modelOption.get_item_count()
|
||||
modelOption.add_item(_model_label_from_variant(modelVariant, modelId))
|
||||
modelOption.set_item_metadata(index, modelId)
|
||||
if modelId == currentModel:
|
||||
selectedIndex = index
|
||||
|
||||
if modelOption.get_item_count() <= 0:
|
||||
modelOption.visible = false
|
||||
_set_status("接口没有返回可用模型,可继续手动填写模型", true)
|
||||
return
|
||||
|
||||
modelOption.visible = true
|
||||
modelOption.select(selectedIndex if selectedIndex >= 0 else 0)
|
||||
if currentModel.is_empty() or selectedIndex < 0:
|
||||
modelInput.text = str(modelOption.get_item_metadata(modelOption.selected))
|
||||
_set_status("已获取 %d 个可用模型" % modelOption.get_item_count(), false)
|
||||
|
||||
func _on_model_option_selected(index: int) -> void:
|
||||
if index < 0:
|
||||
return
|
||||
modelInput.text = str(modelOption.get_item_metadata(index)).strip_edges()
|
||||
|
||||
func _selected_model_id() -> String:
|
||||
var model := modelInput.text.strip_edges()
|
||||
if not model.is_empty():
|
||||
return model
|
||||
if modelOption.visible and modelOption.selected >= 0:
|
||||
return str(modelOption.get_item_metadata(modelOption.selected)).strip_edges()
|
||||
return ""
|
||||
|
||||
func _model_id_from_variant(modelVariant: Variant) -> String:
|
||||
if modelVariant is String:
|
||||
return str(modelVariant).strip_edges()
|
||||
if modelVariant is Dictionary:
|
||||
var model: Dictionary = modelVariant
|
||||
return str(model.get("id", "")).strip_edges()
|
||||
return ""
|
||||
|
||||
func _model_label_from_variant(modelVariant: Variant, modelId: String) -> String:
|
||||
if not (modelVariant is Dictionary):
|
||||
return modelId
|
||||
var model: Dictionary = modelVariant
|
||||
var label := str(model.get("label", modelId)).strip_edges()
|
||||
var ownedBy := str(model.get("owned_by", "")).strip_edges()
|
||||
if not ownedBy.is_empty():
|
||||
return "%s · %s" % [label, ownedBy]
|
||||
return label
|
||||
|
||||
func _on_close_pressed() -> void:
|
||||
recruitmentFrame.visible = false
|
||||
_isSubmitting = false
|
||||
_isFetchingModels = false
|
||||
submitButton.disabled = false
|
||||
fetchModelsButton.disabled = false
|
||||
tokenInput.clear()
|
||||
|
||||
func _set_status(message: String, isError: bool) -> void:
|
||||
statusLabel.text = message
|
||||
statusLabel.add_theme_color_override("font_color", Color(0.68, 0.22, 0.18, 1) if isError else Color(0.32, 0.42, 0.48, 1))
|
||||
|
||||
func _get_cafe_companion_manager() -> Node:
|
||||
return get_node_or_null("/root/CafeCompanionManager")
|
||||
|
||||
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 servicePointId
|
||||
|
||||
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)
|
||||
1
scenes/ui/CafeCompanionRecruitmentPanel.gd.uid
Normal file
1
scenes/ui/CafeCompanionRecruitmentPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b58ngm7oese0r
|
||||
357
scenes/ui/CafeCompanionRecruitmentPanel.tscn
Normal file
357
scenes/ui/CafeCompanionRecruitmentPanel.tscn
Normal file
@@ -0,0 +1,357 @@
|
||||
[gd_scene load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/CafeCompanionRecruitmentPanel.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Panel"]
|
||||
bg_color = Color(0.96, 0.975, 0.982, 0.98)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.46, 0.62, 0.70, 0.28)
|
||||
corner_radius_top_left = 12
|
||||
corner_radius_top_right = 12
|
||||
corner_radius_bottom_right = 12
|
||||
corner_radius_bottom_left = 12
|
||||
shadow_color = Color(0.06, 0.09, 0.12, 0.22)
|
||||
shadow_size = 12
|
||||
shadow_offset = Vector2(0, 5)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Input"]
|
||||
bg_color = Color(1, 1, 1, 0.96)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.55, 0.68, 0.76, 0.36)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
content_margin_left = 12
|
||||
content_margin_top = 6
|
||||
content_margin_right = 12
|
||||
content_margin_bottom = 6
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="Style_InputLine"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonNormal"]
|
||||
bg_color = Color(0.22, 0.56, 0.78, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonHover"]
|
||||
bg_color = Color(0.29, 0.64, 0.84, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_ButtonPressed"]
|
||||
bg_color = Color(0.16, 0.45, 0.66, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_Close"]
|
||||
bg_color = Color(0.86, 0.91, 0.94, 0.85)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="Style_Empty"]
|
||||
|
||||
[node name="CafeCompanionRecruitmentPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="CafeRecruitmentFrame" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -320.0
|
||||
offset_top = -360.0
|
||||
offset_right = 320.0
|
||||
offset_bottom = 360.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 0
|
||||
theme_override_styles/panel = SubResource("Style_Panel")
|
||||
|
||||
[node name="PanelMargin" type="MarginContainer" parent="CafeRecruitmentFrame"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 14
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 14
|
||||
|
||||
[node name="ContentVBox" type="VBoxContainer" parent="CafeRecruitmentFrame/PanelMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="HeaderRow" type="HBoxContainer" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TitleStack" type="VBoxContainer" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/HeaderRow/TitleStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.13, 0.21, 0.27, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "咖啡店陪伴招聘"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="SubtitleLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/HeaderRow/TitleStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.40, 0.50, 0.57, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "鲸鱼咖啡馆 · 吧台招聘终端"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/HeaderRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(0.20, 0.30, 0.36, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_styles/normal = SubResource("Style_Close")
|
||||
theme_override_styles/hover = SubResource("Style_Close")
|
||||
theme_override_styles/pressed = SubResource("Style_Close")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "x"
|
||||
|
||||
[node name="ServicePointLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "陪伴位"
|
||||
|
||||
[node name="ServicePointOption" type="OptionButton" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
|
||||
[node name="EmploymentDurationLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "打工时间"
|
||||
|
||||
[node name="EmploymentDurationOption" type="OptionButton" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
|
||||
[node name="PersonaNameLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "人设名称"
|
||||
|
||||
[node name="PersonaNameInput" type="LineEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
placeholder_text = "例如:蓝莓摩卡"
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="ProtocolLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "协议"
|
||||
|
||||
[node name="ProtocolOption" type="OptionButton" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
|
||||
[node name="BaseUrlLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "OpenAI-compatible URL"
|
||||
|
||||
[node name="BaseUrlInput" type="LineEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
placeholder_text = "https://api.example.com/v1"
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="TokenLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Token"
|
||||
|
||||
[node name="TokenInput" type="LineEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
placeholder_text = "只提交给后端保存"
|
||||
secret = true
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="ModelLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "模型"
|
||||
|
||||
[node name="ModelRow" type="HBoxContainer" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="ModelInput" type="LineEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/ModelRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "gpt-4o-mini"
|
||||
placeholder_text = "gpt-4o-mini"
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="FetchModelsButton" type="Button" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox/ModelRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(104, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_styles/normal = SubResource("Style_ButtonNormal")
|
||||
theme_override_styles/hover = SubResource("Style_ButtonHover")
|
||||
theme_override_styles/pressed = SubResource("Style_ButtonPressed")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "获取模型"
|
||||
|
||||
[node name="ModelOption" type="OptionButton" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
|
||||
[node name="PersonaPromptLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "人设指令"
|
||||
|
||||
[node name="PersonaPromptInput" type="TextEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 76)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
placeholder_text = "写清楚角色性格、语气、边界和陪伴方式"
|
||||
wrap_mode = 1
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="WelcomeMessageLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "欢迎语"
|
||||
|
||||
[node name="WelcomeMessageInput" type="LineEdit" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 34)
|
||||
layout_mode = 2
|
||||
placeholder_text = "可选"
|
||||
caret_blink = true
|
||||
theme_override_colors/font_color = Color(0.16, 0.21, 0.27, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.48, 0.56, 0.62, 0.78)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("Style_Input")
|
||||
theme_override_styles/focus = SubResource("Style_Input")
|
||||
|
||||
[node name="EnabledCheckBox" type="CheckBox" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
theme_override_colors/font_color = Color(0.18, 0.26, 0.32, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "立即开始接待"
|
||||
|
||||
[node name="SubmitButton" type="Button" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_styles/normal = SubResource("Style_ButtonNormal")
|
||||
theme_override_styles/hover = SubResource("Style_ButtonHover")
|
||||
theme_override_styles/pressed = SubResource("Style_ButtonPressed")
|
||||
theme_override_styles/focus = SubResource("Style_Empty")
|
||||
text = "提交雇佣登记"
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="CafeRecruitmentFrame/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 18)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.32, 0.42, 0.48, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = ""
|
||||
text_overrun_behavior = 3
|
||||
130
scenes/ui/ChatBubble.gd
Normal file
130
scenes/ui/ChatBubble.gd
Normal file
@@ -0,0 +1,130 @@
|
||||
extends Control
|
||||
|
||||
const DEFAULT_DURATION: float = 5.0
|
||||
const TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
||||
const TARGET_TOP_GAP: float = 10.0
|
||||
const FALLBACK_TARGET_HEIGHT: float = 64.0
|
||||
const MIN_TEXT_WIDTH: float = 96.0
|
||||
const MAX_TEXT_WIDTH: float = 230.0
|
||||
const TEXT_FONT_SIZE: float = 18.0
|
||||
const LINE_HEIGHT: float = 24.0
|
||||
|
||||
@onready var label: Label = $PanelContainer/Label
|
||||
@onready var panel: PanelContainer = $PanelContainer
|
||||
|
||||
var _targetNode: Node2D
|
||||
var _targetOffset: Vector2 = TARGET_OFFSET
|
||||
var _originalText: String = ""
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _targetNode == null or not is_instance_valid(_targetNode):
|
||||
return
|
||||
|
||||
_update_position()
|
||||
|
||||
func set_text(text: String, targetNode: Node2D = null, targetOffset: Vector2 = TARGET_OFFSET) -> void:
|
||||
_originalText = text
|
||||
_targetNode = targetNode
|
||||
_targetOffset = targetOffset
|
||||
_update_size()
|
||||
|
||||
if _targetNode != null:
|
||||
_update_position()
|
||||
|
||||
await get_tree().create_timer(DEFAULT_DURATION).timeout
|
||||
queue_free()
|
||||
|
||||
func _update_size() -> void:
|
||||
if panel == null:
|
||||
return
|
||||
var metrics := _get_text_metrics(_originalText)
|
||||
label.text = str(metrics.get("display_text", _originalText))
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
label.custom_minimum_size = Vector2(float(metrics.get("width", MIN_TEXT_WIDTH)), float(metrics.get("height", LINE_HEIGHT)))
|
||||
label.size = label.custom_minimum_size
|
||||
panel.reset_size()
|
||||
reset_size()
|
||||
panel.size = panel.get_combined_minimum_size()
|
||||
size = panel.size
|
||||
|
||||
func _update_position() -> void:
|
||||
var anchor := _get_target_top_screen_position()
|
||||
var bubbleSize := _get_bubble_size()
|
||||
global_position = Vector2(
|
||||
anchor.x - bubbleSize.x * 0.5 + _targetOffset.x,
|
||||
anchor.y - bubbleSize.y - TARGET_TOP_GAP
|
||||
).round()
|
||||
|
||||
func _get_bubble_size() -> Vector2:
|
||||
if panel != null:
|
||||
var combined := panel.get_combined_minimum_size()
|
||||
if combined.x > 0.0 and combined.y > 0.0:
|
||||
return combined
|
||||
if size.x > 0.0 and size.y > 0.0:
|
||||
return size
|
||||
return Vector2(280.0, 52.0)
|
||||
|
||||
func _get_text_metrics(text: String) -> Dictionary:
|
||||
var lines: Array[String] = []
|
||||
var currentLine := ""
|
||||
var currentWidth := 0.0
|
||||
var maxLineWidth := 0.0
|
||||
for index in range(text.length()):
|
||||
var character := text.substr(index, 1)
|
||||
var characterWidth := _measure_text_width(character)
|
||||
if not currentLine.is_empty() and currentWidth + characterWidth > MAX_TEXT_WIDTH:
|
||||
lines.append(currentLine)
|
||||
maxLineWidth = max(maxLineWidth, currentWidth)
|
||||
currentLine = character
|
||||
currentWidth = characterWidth
|
||||
else:
|
||||
currentLine += character
|
||||
currentWidth += characterWidth
|
||||
|
||||
if not currentLine.is_empty() or lines.is_empty():
|
||||
lines.append(currentLine)
|
||||
maxLineWidth = max(maxLineWidth, currentWidth)
|
||||
|
||||
return {
|
||||
"display_text": "\n".join(lines),
|
||||
"width": clamp(maxLineWidth + 4.0, MIN_TEXT_WIDTH, MAX_TEXT_WIDTH),
|
||||
"height": LINE_HEIGHT * float(lines.size())
|
||||
}
|
||||
|
||||
func _measure_text_width(text: String) -> 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(TEXT_FONT_SIZE)
|
||||
var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, fontSize).x
|
||||
if measured > 0.0:
|
||||
return measured
|
||||
|
||||
var estimated := 0.0
|
||||
for index in range(text.length()):
|
||||
var code := text.unicode_at(index)
|
||||
estimated += TEXT_FONT_SIZE * 0.56 if code <= 0x7f else TEXT_FONT_SIZE
|
||||
return estimated
|
||||
|
||||
func _get_target_top_screen_position() -> Vector2:
|
||||
var targetScreenPosition: Vector2 = _targetNode.get_global_transform_with_canvas().origin
|
||||
var visualTopOffset := _get_target_visual_top_offset()
|
||||
return targetScreenPosition + visualTopOffset
|
||||
|
||||
func _get_target_visual_top_offset() -> Vector2:
|
||||
var topOffset := Vector2(0.0, -FALLBACK_TARGET_HEIGHT)
|
||||
if _targetNode == null:
|
||||
return topOffset
|
||||
|
||||
var sprite := _targetNode.get_node_or_null("Sprite2D") as Sprite2D
|
||||
if sprite == null:
|
||||
return topOffset
|
||||
|
||||
var rect := sprite.get_rect()
|
||||
var localTop := sprite.position + Vector2(0.0, rect.position.y * sprite.scale.y)
|
||||
var nodeToCanvas := _targetNode.get_global_transform_with_canvas()
|
||||
var topScreenPosition := nodeToCanvas * localTop
|
||||
var targetScreenPosition: Vector2 = nodeToCanvas.origin
|
||||
return topScreenPosition - targetScreenPosition
|
||||
1
scenes/ui/ChatBubble.gd.uid
Normal file
1
scenes/ui/ChatBubble.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d04vhx8vvh2h6
|
||||
41
scenes/ui/ChatBubble.tscn
Normal file
41
scenes/ui/ChatBubble.tscn
Normal file
@@ -0,0 +1,41 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/ChatBubble.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="2_theme"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_modern"]
|
||||
bg_color = Color(1, 1, 1, 0.94)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
content_margin_left = 14.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 14.0
|
||||
content_margin_bottom = 9.0
|
||||
shadow_color = Color(0, 0, 0, 0.2)
|
||||
shadow_size = 2
|
||||
|
||||
[node name="ChatBubble" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
script = ExtResource("1_script")
|
||||
theme = ExtResource("2_theme")
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||
layout_mode = 0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_bubble_modern")
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.1, 0.1, 0.1, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "..."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
mouse_filter = 2
|
||||
1071
scenes/ui/ChatUI.gd
Normal file
1071
scenes/ui/ChatUI.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scenes/ui/ChatUI.gd.uid
Normal file
1
scenes/ui/ChatUI.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c57ehdqxtlrrx
|
||||
350
scenes/ui/ChatUI.tscn
Normal file
350
scenes/ui/ChatUI.tscn
Normal file
@@ -0,0 +1,350 @@
|
||||
[gd_scene load_steps=15 format=3 uid="uid://bv7k2nan4xj8q"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/ChatUI.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://scenes/ui/BubbleSendButton.gd" id="2"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
||||
bg_color = Color(1, 0.996, 0.984, 0.97)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.78, 0.86, 0.93, 0.42)
|
||||
corner_radius_top_left = 26
|
||||
corner_radius_top_right = 26
|
||||
corner_radius_bottom_right = 26
|
||||
corner_radius_bottom_left = 26
|
||||
shadow_color = Color(0.12549, 0.282353, 0.407843, 0.18)
|
||||
shadow_size = 18
|
||||
shadow_offset = Vector2(0, 7)
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tab"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input"]
|
||||
bg_color = Color(1, 1, 1, 0.9)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.76, 0.85, 0.93, 0.9)
|
||||
corner_radius_top_left = 18
|
||||
corner_radius_top_right = 18
|
||||
corner_radius_bottom_right = 18
|
||||
corner_radius_bottom_left = 18
|
||||
content_margin_left = 16
|
||||
content_margin_top = 6
|
||||
content_margin_right = 16
|
||||
content_margin_bottom = 6
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_input_line"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_friend_button"]
|
||||
bg_color = Color(0.918, 0.961, 0.992, 0.96)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.73, 0.84, 0.93, 0.72)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_normal"]
|
||||
bg_color = Color(1, 1, 1, 0.94)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.72, 0.84, 0.93, 0.86)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_hover"]
|
||||
bg_color = Color(0.918, 0.961, 0.992, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.392, 0.718, 0.949, 0.8)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_pressed"]
|
||||
bg_color = Color(0.858, 0.925, 0.98, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.31, 0.64, 0.89, 0.86)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_send_normal"]
|
||||
bg_color = Color(0.258824, 0.627451, 0.913725, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56, 0.8, 0.98, 0.72)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_send_hover"]
|
||||
bg_color = Color(0.345, 0.69, 0.945, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.68, 0.87, 1, 0.9)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_send_pressed"]
|
||||
bg_color = Color(0.196, 0.52, 0.82, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.46, 0.72, 0.94, 0.9)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_send_focus"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_friend_header"]
|
||||
bg_color = Color(0.925, 0.965, 1, 0.96)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.77, 0.87, 0.95, 0.7)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[node name="ChatUI" type="Control"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ChatPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchor_left = 0.024
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.024
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -448.0
|
||||
offset_right = 548.0
|
||||
offset_bottom = -32.0
|
||||
grow_vertical = 0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
|
||||
|
||||
[node name="PanelMargin" type="MarginContainer" parent="ChatPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 18
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 18
|
||||
|
||||
[node name="ContentVBox" type="VBoxContainer" parent="ChatPanel/PanelMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Tabs" type="HBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="PopularTab" type="VBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox/Tabs"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PopularTabButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/PopularTab"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(0.258824, 0.627451, 0.913725, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_tab")
|
||||
text = "世界频道"
|
||||
|
||||
[node name="Underline" type="ColorRect" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/PopularTab"]
|
||||
custom_minimum_size = Vector2(0, 2)
|
||||
layout_mode = 2
|
||||
color = Color(0.258824, 0.627451, 0.913725, 1)
|
||||
|
||||
[node name="RecentTab" type="VBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox/Tabs"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="RecentTabButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/RecentTab"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(0.560784, 0.639216, 0.733333, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_tab")
|
||||
text = "悄悄话"
|
||||
|
||||
[node name="Underline" type="ColorRect" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/RecentTab"]
|
||||
custom_minimum_size = Vector2(0, 2)
|
||||
layout_mode = 2
|
||||
color = Color(1, 1, 1, 0)
|
||||
|
||||
[node name="FriendsTab" type="VBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox/Tabs"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="FriendsTabButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/FriendsTab"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_colors/font_color = Color(0.560784, 0.639216, 0.733333, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_tab")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_tab")
|
||||
text = "好友"
|
||||
|
||||
[node name="Underline" type="ColorRect" parent="ChatPanel/PanelMargin/ContentVBox/Tabs/FriendsTab"]
|
||||
custom_minimum_size = Vector2(0, 2)
|
||||
layout_mode = 2
|
||||
color = Color(1, 1, 1, 0)
|
||||
|
||||
[node name="ChatHistory" type="ScrollContainer" parent="ChatPanel/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="MessageList" type="VBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox/ChatHistory"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="FriendsListSurface" type="PanelContainer" parent="ChatPanel/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_friend_header")
|
||||
|
||||
[node name="FriendsList" type="HBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox/FriendsListSurface"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="AddFriendRow" type="HBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 36)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 2
|
||||
|
||||
[node name="AddFriendButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/AddFriendRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
tooltip_text = "添加好友"
|
||||
theme_override_colors/font_color = Color(0.258824, 0.627451, 0.913725, 1)
|
||||
theme_override_font_sizes/font_size = 17
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_friend_button")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_friend_button")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_friend_button")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_send_focus")
|
||||
text = "+"
|
||||
|
||||
[node name="InputRow" type="HBoxContainer" parent="ChatPanel/PanelMargin/ContentVBox"]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="InputShell" type="PanelContainer" parent="ChatPanel/PanelMargin/ContentVBox/InputRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 1
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_input")
|
||||
|
||||
[node name="ChatInput" type="LineEdit" parent="ChatPanel/PanelMargin/ContentVBox/InputRow/InputShell"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
placeholder_text = "输入消息..."
|
||||
localize_numeral_system = false
|
||||
theme_override_colors/font_color = Color(0.188, 0.294, 0.424, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.560784, 0.639216, 0.733333, 0.88)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_input_line")
|
||||
theme_override_styles/read_only = SubResource("StyleBoxEmpty_input_line")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_input_line")
|
||||
|
||||
[node name="BubbleSendButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/InputRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
tooltip_text = "发送并弹出气泡"
|
||||
theme_override_colors/font_color = Color(0.207843, 0.427451, 0.686275, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_bubble_normal")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_bubble_hover")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_bubble_pressed")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_send_focus")
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="SendButton" type="Button" parent="ChatPanel/PanelMargin/ContentVBox/InputRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_send_normal")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_send_hover")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_send_pressed")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_send_focus")
|
||||
text = "➤"
|
||||
516
scenes/ui/CourseBoardPanel.gd
Normal file
516
scenes/ui/CourseBoardPanel.gd
Normal file
@@ -0,0 +1,516 @@
|
||||
class_name CourseBoardPanel
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# CourseBoardPanel.gd - Datawhale 课程看板
|
||||
# ============================================================================
|
||||
# 展示 Datawhale 学习中心课程列表,由打工区 VirtualWhaleRecruitmentBoard 打开。
|
||||
# ============================================================================
|
||||
|
||||
const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"]
|
||||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||||
const API_PATH: String = "/course-resources/datawhale"
|
||||
const PANEL_SIZE: Vector2 = Vector2(1500, 1020)
|
||||
const MIN_MARGIN: Vector2 = Vector2(24, 24)
|
||||
const CARD_SIZE: Vector2 = Vector2(320, 348)
|
||||
const COVER_HEIGHT: float = 171.0
|
||||
const TEXT_COLOR: Color = Color(0.098, 0.140, 0.190)
|
||||
const MUTED_COLOR: Color = Color(0.380, 0.450, 0.540)
|
||||
const ACCENT_COLOR: Color = Color(0.000, 0.322, 0.851)
|
||||
const SOFT_BLUE: Color = Color(0.925, 0.965, 1.0, 1.0)
|
||||
const IMAGE_PLACEHOLDER: Color = Color(1.0, 0.965, 0.870, 1.0)
|
||||
|
||||
var _overlay: ColorRect
|
||||
var _panel: Control
|
||||
var _grid: GridContainer
|
||||
var _statusLabel: Label
|
||||
var _request: HTTPRequest
|
||||
var _isOpen: bool = false
|
||||
var _transitionTween: Tween
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(false)
|
||||
_buildUi()
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED and is_instance_valid(_panel):
|
||||
_positionPanel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
var keyEvent := event as InputEventKey
|
||||
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
|
||||
hide_panel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _isOpen:
|
||||
_releaseMovementActions()
|
||||
|
||||
func show_panel() -> void:
|
||||
if _isOpen:
|
||||
return
|
||||
_isOpen = true
|
||||
visible = true
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
set_process(true)
|
||||
_releaseMovementInputState()
|
||||
_fetchCourses()
|
||||
_animatePanel(true)
|
||||
|
||||
func hide_panel() -> void:
|
||||
if not _isOpen:
|
||||
return
|
||||
_isOpen = false
|
||||
_releaseMovementInputState()
|
||||
_animatePanel(false)
|
||||
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func _buildUi() -> void:
|
||||
_overlay = ColorRect.new()
|
||||
_overlay.name = "courseBoardDimOverlay"
|
||||
_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_overlay.color = Color(0.020, 0.036, 0.060, 0.64)
|
||||
_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_overlay.gui_input.connect(_onOverlayGuiInput)
|
||||
add_child(_overlay)
|
||||
|
||||
_panel = Control.new()
|
||||
_panel.name = "courseBoardPanel"
|
||||
_panel.custom_minimum_size = PANEL_SIZE
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(_panel)
|
||||
_positionPanel()
|
||||
|
||||
var frame := PanelContainer.new()
|
||||
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
frame.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.972, 0.988, 1.0, 1.0), 26, true))
|
||||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_panel.add_child(frame)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 34)
|
||||
margin.add_theme_constant_override("margin_top", 28)
|
||||
margin.add_theme_constant_override("margin_right", 34)
|
||||
margin.add_theme_constant_override("margin_bottom", 28)
|
||||
_panel.add_child(margin)
|
||||
|
||||
var root := VBoxContainer.new()
|
||||
root.add_theme_constant_override("separation", 18)
|
||||
margin.add_child(root)
|
||||
root.add_child(_buildHeader())
|
||||
root.add_child(_buildGridPanel())
|
||||
root.add_child(_buildFooter())
|
||||
|
||||
_request = HTTPRequest.new()
|
||||
_request.timeout = 18.0
|
||||
_request.request_completed.connect(_onRequestCompleted)
|
||||
add_child(_request)
|
||||
_updatePanelAlpha(0.0, 0.0)
|
||||
|
||||
func _buildHeader() -> Control:
|
||||
var header := PanelContainer.new()
|
||||
header.custom_minimum_size = Vector2(0, 106)
|
||||
header.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.000, 0.322, 0.851, 0.96), 24, false))
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 28)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_right", 20)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
header.add_child(margin)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
row.add_theme_constant_override("separation", 16)
|
||||
margin.add_child(row)
|
||||
|
||||
var titleStack := VBoxContainer.new()
|
||||
titleStack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
titleStack.add_theme_constant_override("separation", 4)
|
||||
row.add_child(titleStack)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Datawhale 课程看板"
|
||||
title.add_theme_font_size_override("font_size", 32)
|
||||
title.add_theme_color_override("font_color", Color.WHITE)
|
||||
titleStack.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "AI学习中心最新课程"
|
||||
subtitle.add_theme_font_size_override("font_size", 18)
|
||||
subtitle.add_theme_color_override("font_color", Color(0.835, 0.925, 1.0, 1.0))
|
||||
titleStack.add_child(subtitle)
|
||||
|
||||
var closeButton := Button.new()
|
||||
closeButton.text = "×"
|
||||
closeButton.custom_minimum_size = Vector2(54, 54)
|
||||
closeButton.focus_mode = Control.FOCUS_NONE
|
||||
closeButton.add_theme_font_size_override("font_size", 30)
|
||||
closeButton.add_theme_color_override("font_color", Color.WHITE)
|
||||
closeButton.add_theme_stylebox_override("normal", _createButtonStyle(Color(1.0, 1.0, 1.0, 0.12)))
|
||||
closeButton.add_theme_stylebox_override("hover", _createButtonStyle(Color(1.0, 1.0, 1.0, 0.22)))
|
||||
closeButton.add_theme_stylebox_override("pressed", _createButtonStyle(Color(1.0, 1.0, 1.0, 0.30)))
|
||||
closeButton.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
closeButton.pressed.connect(hide_panel)
|
||||
row.add_child(closeButton)
|
||||
return header
|
||||
|
||||
func _buildGridPanel() -> Control:
|
||||
var holder := PanelContainer.new()
|
||||
holder.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
holder.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.948, 0.978, 1.0, 0.92), 20, false))
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 18)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 18)
|
||||
holder.add_child(margin)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
margin.add_child(scroll)
|
||||
|
||||
_grid = GridContainer.new()
|
||||
_grid.columns = 4
|
||||
_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_grid.add_theme_constant_override("h_separation", 24)
|
||||
_grid.add_theme_constant_override("v_separation", 24)
|
||||
scroll.add_child(_grid)
|
||||
return holder
|
||||
|
||||
func _buildFooter() -> Control:
|
||||
var footer := HBoxContainer.new()
|
||||
footer.custom_minimum_size = Vector2(0, 34)
|
||||
footer.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
|
||||
_statusLabel = Label.new()
|
||||
_statusLabel.text = "靠近招募看板按 E 打开,点击课程卡片进入详情"
|
||||
_statusLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_statusLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_statusLabel.add_theme_font_size_override("font_size", 16)
|
||||
_statusLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
footer.add_child(_statusLabel)
|
||||
return footer
|
||||
|
||||
func _fetchCourses() -> void:
|
||||
_statusLabel.text = "正在同步 Datawhale 课程..."
|
||||
_clearChildren(_grid)
|
||||
var err := _request.request("%s%s" % [NetworkConfig.get_api_base_url(), API_PATH])
|
||||
if err != OK:
|
||||
_statusLabel.text = "课程加载失败:%s" % error_string(err)
|
||||
|
||||
func _onRequestCompleted(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||||
_statusLabel.text = "课程加载失败,请稍后再试"
|
||||
return
|
||||
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if not parsed is Dictionary:
|
||||
_statusLabel.text = "课程数据解析失败"
|
||||
return
|
||||
|
||||
var payload: Dictionary = parsed as Dictionary
|
||||
if not bool(payload.get("success", true)):
|
||||
_statusLabel.text = str(payload.get("message", "课程加载失败"))
|
||||
return
|
||||
|
||||
var data: Variant = payload.get("data", {})
|
||||
if not data is Dictionary:
|
||||
_statusLabel.text = "课程数据为空"
|
||||
return
|
||||
|
||||
var coursesVariant: Variant = (data as Dictionary).get("courses", [])
|
||||
if not coursesVariant is Array:
|
||||
_statusLabel.text = "课程数据为空"
|
||||
return
|
||||
|
||||
var courses: Array = coursesVariant as Array
|
||||
_renderCourses(courses)
|
||||
_statusLabel.text = "已同步 %d 门课程" % courses.size()
|
||||
|
||||
func _renderCourses(rows: Array) -> void:
|
||||
_clearChildren(_grid)
|
||||
for row in rows:
|
||||
if row is Dictionary:
|
||||
_grid.add_child(_buildCourseCard(row as Dictionary))
|
||||
|
||||
func _buildCourseCard(course: Dictionary) -> Button:
|
||||
var card := Button.new()
|
||||
card.custom_minimum_size = CARD_SIZE
|
||||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
card.focus_mode = Control.FOCUS_NONE
|
||||
card.clip_contents = true
|
||||
card.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
card.add_theme_stylebox_override("normal", _createCardStyle(false))
|
||||
card.add_theme_stylebox_override("hover", _createCardStyle(true))
|
||||
card.add_theme_stylebox_override("pressed", _createCardStyle(true))
|
||||
card.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
card.pressed.connect(_openCourse.bind(course))
|
||||
|
||||
var root := VBoxContainer.new()
|
||||
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_theme_constant_override("separation", 0)
|
||||
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
card.add_child(root)
|
||||
|
||||
var coverPanel := Control.new()
|
||||
coverPanel.custom_minimum_size = Vector2(0, COVER_HEIGHT)
|
||||
coverPanel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
coverPanel.clip_contents = true
|
||||
coverPanel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(coverPanel)
|
||||
|
||||
var coverBg := ColorRect.new()
|
||||
coverBg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
coverBg.color = IMAGE_PLACEHOLDER
|
||||
coverBg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
coverPanel.add_child(coverBg)
|
||||
|
||||
var coverImage := TextureRect.new()
|
||||
coverImage.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
coverImage.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
coverImage.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||
coverImage.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
coverPanel.add_child(coverImage)
|
||||
|
||||
var coverText := Label.new()
|
||||
coverText.text = _shortTitle(str(course.get("title", "课程")))
|
||||
coverText.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
coverText.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
coverText.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
coverText.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
coverText.add_theme_font_size_override("font_size", 22)
|
||||
coverText.add_theme_color_override("font_color", ACCENT_COLOR)
|
||||
coverText.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
coverPanel.add_child(coverText)
|
||||
_loadCoverImage(str(course.get("coverUrl", "")), coverImage, coverText)
|
||||
|
||||
var bodyMargin := MarginContainer.new()
|
||||
bodyMargin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bodyMargin.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
bodyMargin.add_theme_constant_override("margin_left", 16)
|
||||
bodyMargin.add_theme_constant_override("margin_top", 16)
|
||||
bodyMargin.add_theme_constant_override("margin_right", 16)
|
||||
bodyMargin.add_theme_constant_override("margin_bottom", 30)
|
||||
bodyMargin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(bodyMargin)
|
||||
|
||||
var info := VBoxContainer.new()
|
||||
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
info.clip_contents = true
|
||||
info.add_theme_constant_override("separation", 6)
|
||||
info.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bodyMargin.add_child(info)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = str(course.get("title", "未命名课程"))
|
||||
title.custom_minimum_size = Vector2(0, 46)
|
||||
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
title.add_theme_font_size_override("font_size", 18)
|
||||
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
info.add_child(title)
|
||||
|
||||
var intro := Label.new()
|
||||
intro.text = str(course.get("intro", ""))
|
||||
intro.custom_minimum_size = Vector2(0, 58)
|
||||
intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
intro.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
intro.add_theme_font_size_override("font_size", 13)
|
||||
intro.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
intro.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
info.add_child(intro)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
info.add_child(spacer)
|
||||
|
||||
var meta := HFlowContainer.new()
|
||||
meta.custom_minimum_size = Vector2(0, 34)
|
||||
meta.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
meta.add_theme_constant_override("h_separation", 6)
|
||||
meta.add_theme_constant_override("v_separation", 6)
|
||||
meta.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
info.add_child(meta)
|
||||
meta.add_child(_buildTag(str(course.get("difficultyLabel", "其他")), Color(0.918, 0.960, 1.0, 1.0), ACCENT_COLOR, 56.0))
|
||||
meta.add_child(_buildTag(str(course.get("categoryLabel", "其他")), Color(0.945, 0.950, 0.965, 1.0), MUTED_COLOR, 112.0))
|
||||
|
||||
var views := int(course.get("viewCount", 0))
|
||||
if views > 0:
|
||||
meta.add_child(_buildTag(_formatViews(views), Color(0.950, 0.980, 0.965, 1.0), Color(0.160, 0.510, 0.340), 82.0))
|
||||
|
||||
return card
|
||||
|
||||
func _buildTag(text: String, bgColor: Color, fontColor: Color, maxWidth: float) -> Label:
|
||||
var tag := Label.new()
|
||||
tag.text = text
|
||||
tag.custom_minimum_size = Vector2(maxWidth, 26)
|
||||
tag.clip_text = true
|
||||
tag.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
tag.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
tag.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
tag.add_theme_font_size_override("font_size", 12)
|
||||
tag.add_theme_color_override("font_color", fontColor)
|
||||
tag.add_theme_stylebox_override("normal", _createPanelStyle(bgColor, 12, false))
|
||||
tag.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return tag
|
||||
|
||||
func _loadCoverImage(url: String, textureRect: TextureRect, fallbackLabel: Label) -> void:
|
||||
var coverUrl := url.strip_edges()
|
||||
if coverUrl.is_empty():
|
||||
return
|
||||
|
||||
var request := HTTPRequest.new()
|
||||
request.timeout = 12.0
|
||||
request.request_completed.connect(_onCoverRequestCompleted.bind(textureRect, fallbackLabel, request))
|
||||
add_child(request)
|
||||
var err := request.request(coverUrl)
|
||||
if err != OK:
|
||||
request.queue_free()
|
||||
|
||||
func _onCoverRequestCompleted(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray, textureRect: TextureRect, fallbackLabel: Label, request: HTTPRequest) -> void:
|
||||
if is_instance_valid(request):
|
||||
request.queue_free()
|
||||
if not is_instance_valid(textureRect):
|
||||
return
|
||||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||||
return
|
||||
|
||||
var image := Image.new()
|
||||
var loadError := image.load_png_from_buffer(body)
|
||||
if loadError != OK:
|
||||
loadError = image.load_jpg_from_buffer(body)
|
||||
if loadError != OK:
|
||||
loadError = image.load_webp_from_buffer(body)
|
||||
if loadError != OK:
|
||||
return
|
||||
|
||||
textureRect.texture = ImageTexture.create_from_image(image)
|
||||
if is_instance_valid(fallbackLabel):
|
||||
fallbackLabel.visible = false
|
||||
|
||||
func _openCourse(course: Dictionary) -> void:
|
||||
var detailUrl := str(course.get("detailUrl", "")).strip_edges()
|
||||
if detailUrl.is_empty():
|
||||
return
|
||||
OS.shell_open(detailUrl)
|
||||
|
||||
func _onOverlayGuiInput(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.pressed and mouseEvent.button_index == MOUSE_BUTTON_LEFT:
|
||||
hide_panel()
|
||||
|
||||
func _positionPanel() -> void:
|
||||
var viewportSize := get_viewport_rect().size
|
||||
var width := minf(PANEL_SIZE.x, maxf(980.0, viewportSize.x - MIN_MARGIN.x * 2.0))
|
||||
var height := minf(PANEL_SIZE.y, maxf(760.0, viewportSize.y - MIN_MARGIN.y * 2.0))
|
||||
_panel.position = (viewportSize - Vector2(width, height)) * 0.5
|
||||
_panel.size = Vector2(width, height)
|
||||
_panel.pivot_offset = _panel.size * 0.5
|
||||
|
||||
func _animatePanel(opening: bool) -> void:
|
||||
if is_instance_valid(_transitionTween):
|
||||
_transitionTween.kill()
|
||||
_transitionTween = create_tween()
|
||||
_transitionTween.set_parallel(true)
|
||||
if opening:
|
||||
_updatePanelAlpha(0.0, 0.0)
|
||||
_panel.scale = Vector2(0.985, 0.985)
|
||||
_transitionTween.tween_property(_overlay, "modulate:a", 1.0, 0.16)
|
||||
_transitionTween.tween_property(_panel, "modulate:a", 1.0, 0.18)
|
||||
_transitionTween.tween_property(_panel, "scale", Vector2.ONE, 0.18)
|
||||
else:
|
||||
_transitionTween.tween_property(_overlay, "modulate:a", 0.0, 0.12)
|
||||
_transitionTween.tween_property(_panel, "modulate:a", 0.0, 0.12)
|
||||
_transitionTween.tween_property(_panel, "scale", Vector2(0.985, 0.985), 0.12)
|
||||
_transitionTween.finished.connect(_onCloseAnimationFinished)
|
||||
|
||||
func _onCloseAnimationFinished() -> void:
|
||||
if _isOpen:
|
||||
return
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(false)
|
||||
|
||||
func _updatePanelAlpha(overlayAlpha: float, panelAlpha: float) -> void:
|
||||
if is_instance_valid(_overlay):
|
||||
_overlay.modulate.a = overlayAlpha
|
||||
if is_instance_valid(_panel):
|
||||
_panel.modulate.a = panelAlpha
|
||||
|
||||
func _releaseMovementInputState() -> void:
|
||||
Input.flush_buffered_events()
|
||||
_releaseMovementActions()
|
||||
|
||||
func _releaseMovementActions() -> void:
|
||||
for action in MOVEMENT_ACTIONS:
|
||||
Input.action_release(action)
|
||||
|
||||
func _clearChildren(node: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func _shortTitle(title: String) -> String:
|
||||
var trimmed := title.strip_edges()
|
||||
if trimmed.length() <= 8:
|
||||
return trimmed
|
||||
return trimmed.substr(0, 8)
|
||||
|
||||
func _formatViews(views: int) -> String:
|
||||
if views >= 10000:
|
||||
var wan := float(views) / 10000.0
|
||||
return "%.1f万看过" % wan
|
||||
return "%d人看过" % views
|
||||
|
||||
func _createPanelStyle(color: Color, radius: int, shadow: bool) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_top = 7
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_bottom = 7
|
||||
if shadow:
|
||||
style.shadow_color = Color(0.030, 0.120, 0.250, 0.18)
|
||||
style.shadow_size = 16
|
||||
style.shadow_offset = Vector2(0, 5)
|
||||
return style
|
||||
|
||||
func _createButtonStyle(color: Color) -> StyleBoxFlat:
|
||||
var style := _createPanelStyle(color, 18, false)
|
||||
style.content_margin_left = 0
|
||||
style.content_margin_top = 0
|
||||
style.content_margin_right = 0
|
||||
style.content_margin_bottom = 0
|
||||
return style
|
||||
|
||||
func _createCardStyle(hover: bool) -> StyleBoxFlat:
|
||||
var style := _createPanelStyle(Color.WHITE if not hover else Color(0.986, 0.996, 1.0, 1.0), 18, false)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.815, 0.890, 0.960, 0.92) if not hover else Color(0.000, 0.322, 0.851, 0.72)
|
||||
style.shadow_color = Color(0.040, 0.150, 0.300, 0.08 if not hover else 0.14)
|
||||
style.shadow_size = 4 if not hover else 8
|
||||
style.shadow_offset = Vector2(0, 3)
|
||||
return style
|
||||
1
scenes/ui/CourseBoardPanel.gd.uid
Normal file
1
scenes/ui/CourseBoardPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ba886s1nvwl55
|
||||
13
scenes/ui/CourseBoardPanel.tscn
Normal file
13
scenes/ui/CourseBoardPanel.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=4 uid="uid://whaletown_course_board_panel"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/CourseBoardPanel.gd" id="1_course_board"]
|
||||
|
||||
[node name="CourseBoardPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_course_board")
|
||||
1371
scenes/ui/DatawhaleHonorRankingPanel.gd
Normal file
1371
scenes/ui/DatawhaleHonorRankingPanel.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scenes/ui/DatawhaleHonorRankingPanel.gd.uid
Normal file
1
scenes/ui/DatawhaleHonorRankingPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgj33ppqlxwl5
|
||||
647
scenes/ui/FriendListPanel.gd
Normal file
647
scenes/ui/FriendListPanel.gd
Normal file
@@ -0,0 +1,647 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# FriendListPanel.gd - 右下角好友列表面板
|
||||
# ============================================================================
|
||||
# 独立展示好友在线、好友请求和好友选择。点击好友后通过事件驱动 ChatUI
|
||||
# 切换到对应好友会话,不在面板内承载聊天内容。
|
||||
# ============================================================================
|
||||
|
||||
const PANEL_SIZE: Vector2 = Vector2(384, 492)
|
||||
const PANEL_MARGIN: Vector2 = Vector2(32, 32)
|
||||
const ONLINE_COLOR: Color = Color(0.286, 0.765, 0.486)
|
||||
const OFFLINE_COLOR: Color = Color(0.678, 0.725, 0.773)
|
||||
const TEXT_COLOR: Color = Color(0.188, 0.294, 0.424)
|
||||
const MUTED_COLOR: Color = Color(0.560, 0.639, 0.733)
|
||||
const ACCENT_COLOR: Color = Color(0.258824, 0.627451, 0.913725)
|
||||
const SURFACE_COLOR: Color = Color(1.0, 0.996, 0.984, 0.97)
|
||||
const SURFACE_RAISED_COLOR: Color = Color(1.0, 1.0, 1.0, 0.9)
|
||||
const BORDER_COLOR: Color = Color(0.788, 0.851, 0.910, 0.52)
|
||||
const FRIEND_AVATAR_SIZE: Vector2 = Vector2(48, 48)
|
||||
const FRIEND_AVATAR_CORNER_RADIUS: int = 12
|
||||
|
||||
@export var autoRequestFriendList: bool = true
|
||||
|
||||
var _panel: PanelContainer
|
||||
var _titleLabel: Label
|
||||
var _summaryLabel: Label
|
||||
var _closeButton: Button
|
||||
var _requestList: VBoxContainer
|
||||
var _friendList: VBoxContainer
|
||||
var _statusLabel: Label
|
||||
|
||||
var _friends: Array[Dictionary] = []
|
||||
var _requests: Array[Dictionary] = []
|
||||
var _supported: bool = true
|
||||
var _statusMessage: String = ""
|
||||
var _isOpen: bool = false
|
||||
var _hasRequestedFriendList: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
set_panel_open(false)
|
||||
_subscribe_to_events()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_FRIENDS_UPDATED, _on_friends_updated, self)
|
||||
eventSystem.call("disconnect_event", EventNames.HUD_FRIEND_LIST_TOGGLE, _on_friend_list_toggle, self)
|
||||
eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func set_panel_open(open: bool) -> void:
|
||||
_isOpen = open
|
||||
if is_instance_valid(_panel):
|
||||
_panel.visible = _isOpen
|
||||
if _isOpen and autoRequestFriendList and not _hasRequestedFriendList:
|
||||
_request_initial_friend_list()
|
||||
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func toggle_panel() -> void:
|
||||
set_panel_open(not _isOpen)
|
||||
|
||||
func _build_ui() -> void:
|
||||
_panel = PanelContainer.new()
|
||||
_panel.name = "FriendListCard"
|
||||
_panel.custom_minimum_size = PANEL_SIZE
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_panel.add_theme_stylebox_override("panel", _create_panel_style())
|
||||
add_child(_panel)
|
||||
|
||||
_anchor_panel()
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 18)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 18)
|
||||
_panel.add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 12)
|
||||
margin.add_child(content)
|
||||
|
||||
var header := HBoxContainer.new()
|
||||
header.custom_minimum_size = Vector2(0, 48)
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
content.add_child(header)
|
||||
|
||||
var titleStack := VBoxContainer.new()
|
||||
titleStack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
titleStack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
titleStack.add_theme_constant_override("separation", 1)
|
||||
header.add_child(titleStack)
|
||||
|
||||
_titleLabel = Label.new()
|
||||
_titleLabel.text = "好友"
|
||||
_titleLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
_titleLabel.add_theme_font_size_override("font_size", 22)
|
||||
titleStack.add_child(_titleLabel)
|
||||
|
||||
var subtitleLabel := Label.new()
|
||||
subtitleLabel.text = "和鲸落镇的伙伴保持联系"
|
||||
subtitleLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
subtitleLabel.add_theme_font_size_override("font_size", 12)
|
||||
titleStack.add_child(subtitleLabel)
|
||||
|
||||
_closeButton = Button.new()
|
||||
_closeButton.text = "×"
|
||||
_closeButton.tooltip_text = "收起好友列表"
|
||||
_closeButton.custom_minimum_size = Vector2(36, 36)
|
||||
_closeButton.focus_mode = Control.FOCUS_NONE
|
||||
_closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
_closeButton.add_theme_font_size_override("font_size", 22)
|
||||
_closeButton.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
_closeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||||
_closeButton.add_theme_color_override("font_pressed_color", ACCENT_COLOR.darkened(0.12))
|
||||
_closeButton.add_theme_color_override("font_disabled_color", Color(0.37, 0.48, 0.58, 0.55))
|
||||
_closeButton.add_theme_stylebox_override("normal", _create_empty_style())
|
||||
_closeButton.add_theme_stylebox_override("hover", _create_pill_style(Color(0.918, 0.961, 0.992, 0.96), 12))
|
||||
_closeButton.add_theme_stylebox_override("pressed", _create_pill_style(Color(0.858, 0.925, 0.980, 1.0), 12))
|
||||
_closeButton.add_theme_stylebox_override("focus", _create_empty_style())
|
||||
_closeButton.pressed.connect(func() -> void: set_panel_open(false))
|
||||
header.add_child(_closeButton)
|
||||
|
||||
var summaryCard := PanelContainer.new()
|
||||
summaryCard.custom_minimum_size = Vector2(0, 54)
|
||||
summaryCard.add_theme_stylebox_override("panel", _create_summary_style())
|
||||
content.add_child(summaryCard)
|
||||
|
||||
var summaryRow := HBoxContainer.new()
|
||||
summaryRow.add_theme_constant_override("separation", 10)
|
||||
summaryCard.add_child(summaryRow)
|
||||
|
||||
var onlineDot := PanelContainer.new()
|
||||
onlineDot.custom_minimum_size = Vector2(10, 10)
|
||||
onlineDot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
onlineDot.add_theme_stylebox_override("panel", _create_dot_style(ONLINE_COLOR))
|
||||
summaryRow.add_child(onlineDot)
|
||||
|
||||
_summaryLabel = Label.new()
|
||||
_summaryLabel.text = "暂无在线好友"
|
||||
_summaryLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_summaryLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_summaryLabel.add_theme_color_override("font_color", Color(0.207843, 0.427451, 0.686275))
|
||||
_summaryLabel.add_theme_font_size_override("font_size", 15)
|
||||
summaryRow.add_child(_summaryLabel)
|
||||
|
||||
var chatHint := Label.new()
|
||||
chatHint.text = "选择好友开始私聊"
|
||||
chatHint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
chatHint.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
chatHint.add_theme_font_size_override("font_size", 12)
|
||||
summaryRow.add_child(chatHint)
|
||||
|
||||
_statusLabel = Label.new()
|
||||
_statusLabel.visible = false
|
||||
_statusLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_statusLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
_statusLabel.add_theme_font_size_override("font_size", 15)
|
||||
content.add_child(_statusLabel)
|
||||
|
||||
_requestList = VBoxContainer.new()
|
||||
_requestList.add_theme_constant_override("separation", 8)
|
||||
content.add_child(_requestList)
|
||||
|
||||
var friendsHeading := Label.new()
|
||||
friendsHeading.text = "全部好友"
|
||||
friendsHeading.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
friendsHeading.add_theme_font_size_override("font_size", 13)
|
||||
content.add_child(friendsHeading)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.add_theme_stylebox_override("panel", _create_empty_style())
|
||||
content.add_child(scroll)
|
||||
|
||||
_friendList = VBoxContainer.new()
|
||||
_friendList.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_friendList.add_theme_constant_override("separation", 8)
|
||||
scroll.add_child(_friendList)
|
||||
|
||||
_render()
|
||||
|
||||
func _anchor_panel() -> void:
|
||||
_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
_panel.offset_left = -PANEL_SIZE.x - PANEL_MARGIN.x
|
||||
_panel.offset_top = -PANEL_SIZE.y - PANEL_MARGIN.y
|
||||
_panel.offset_right = -PANEL_MARGIN.x
|
||||
_panel.offset_bottom = -PANEL_MARGIN.y
|
||||
_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||
|
||||
func _subscribe_to_events() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("FriendListPanel: EventSystem autoload is not available.")
|
||||
return
|
||||
|
||||
eventSystem.call("connect_event", EventNames.CHAT_FRIENDS_UPDATED, _on_friends_updated, self)
|
||||
eventSystem.call("connect_event", EventNames.HUD_FRIEND_LIST_TOGGLE, _on_friend_list_toggle, self)
|
||||
eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func _request_initial_friend_list() -> void:
|
||||
_hasRequestedFriendList = true
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
if chatManager != null and chatManager.has_method("request_friend_list"):
|
||||
chatManager.call_deferred("request_friend_list")
|
||||
|
||||
func _on_friend_list_toggle(_data: Variant = null) -> void:
|
||||
toggle_panel()
|
||||
|
||||
func _on_friends_updated(data: Dictionary) -> void:
|
||||
_friends.clear()
|
||||
_requests.clear()
|
||||
_supported = bool(data.get("supported", true))
|
||||
_statusMessage = str(data.get("status", "")).strip_edges()
|
||||
|
||||
var friendsVariant: Variant = data.get("friends", [])
|
||||
if friendsVariant is Array:
|
||||
for friendVariant in friendsVariant:
|
||||
if friendVariant is Dictionary:
|
||||
_friends.append(friendVariant)
|
||||
|
||||
var requestsVariant: Variant = data.get("requests", [])
|
||||
if requestsVariant is Array:
|
||||
for requestVariant in requestsVariant:
|
||||
if requestVariant is Dictionary:
|
||||
_requests.append(requestVariant)
|
||||
|
||||
_render()
|
||||
|
||||
func _on_settings_changed(_data: Dictionary) -> void:
|
||||
_render()
|
||||
|
||||
func _render() -> void:
|
||||
if not is_instance_valid(_friendList) or not is_instance_valid(_requestList):
|
||||
return
|
||||
|
||||
_clear_children(_friendList)
|
||||
_clear_children(_requestList)
|
||||
|
||||
var onlineCount := _count_online_friends()
|
||||
_titleLabel.text = "好友"
|
||||
if is_instance_valid(_summaryLabel):
|
||||
if _friends.is_empty():
|
||||
_summaryLabel.text = "暂无好友,去广场认识新伙伴吧"
|
||||
else:
|
||||
_summaryLabel.text = "%d 位好友在线,共 %d 位伙伴" % [onlineCount, _friends.size()]
|
||||
|
||||
if not _supported:
|
||||
_show_status(_statusMessage if not _statusMessage.is_empty() else "当前后端暂不支持好友功能")
|
||||
return
|
||||
|
||||
_statusLabel.visible = false
|
||||
_render_requests()
|
||||
_render_friends()
|
||||
|
||||
func _render_requests() -> void:
|
||||
if _requests.is_empty() or not _settings_bool("friend_request_notifications", true):
|
||||
_requestList.visible = false
|
||||
return
|
||||
|
||||
_requestList.visible = true
|
||||
var titleCard := PanelContainer.new()
|
||||
titleCard.custom_minimum_size = Vector2(0, 34)
|
||||
titleCard.add_theme_stylebox_override("panel", _create_section_style())
|
||||
_requestList.add_child(titleCard)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "待处理请求 %d" % _requests.size()
|
||||
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
title.add_theme_color_override("font_color", Color(0.207843, 0.427451, 0.686275))
|
||||
title.add_theme_font_size_override("font_size", 13)
|
||||
titleCard.add_child(title)
|
||||
|
||||
for request in _requests:
|
||||
_requestList.add_child(_create_request_row(request))
|
||||
|
||||
func _render_friends() -> void:
|
||||
if _friends.is_empty():
|
||||
var emptyCard := PanelContainer.new()
|
||||
emptyCard.custom_minimum_size = Vector2(0, 100)
|
||||
emptyCard.add_theme_stylebox_override("panel", _create_empty_state_style())
|
||||
_friendList.add_child(emptyCard)
|
||||
|
||||
var emptyLabel := Label.new()
|
||||
emptyLabel.text = "暂无好友\n靠近玩家按 F 发送好友请求"
|
||||
emptyLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
emptyLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
emptyLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
emptyLabel.add_theme_font_size_override("font_size", 14)
|
||||
emptyCard.add_child(emptyLabel)
|
||||
return
|
||||
|
||||
for friend in _sorted_friends():
|
||||
_friendList.add_child(_create_friend_row(friend))
|
||||
|
||||
func _create_friend_row(friend: Dictionary) -> Control:
|
||||
var userId := _read_user_id(friend)
|
||||
var username := str(friend.get("username", "好友")).strip_edges()
|
||||
if username.is_empty():
|
||||
username = "好友"
|
||||
var online := bool(friend.get("online", false))
|
||||
|
||||
var button := Button.new()
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.disabled = userId.is_empty()
|
||||
button.custom_minimum_size = Vector2(0, 68)
|
||||
button.text = ""
|
||||
button.add_theme_stylebox_override("normal", _create_friend_row_style(false, online))
|
||||
button.add_theme_stylebox_override("hover", _create_friend_row_style(true, online))
|
||||
button.add_theme_stylebox_override("pressed", _create_friend_row_style(true, online, true))
|
||||
button.add_theme_stylebox_override("disabled", _create_friend_row_style(false, false))
|
||||
button.add_theme_stylebox_override("focus", _create_empty_style())
|
||||
button.pressed.connect(func() -> void: _select_friend(userId, username, online))
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
row.offset_left = 10
|
||||
row.offset_top = 8
|
||||
row.offset_right = -10
|
||||
row.offset_bottom = -8
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
button.add_child(row)
|
||||
|
||||
row.add_child(_create_avatar(username, online))
|
||||
|
||||
var textBox := VBoxContainer.new()
|
||||
textBox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
textBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
textBox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
textBox.add_theme_constant_override("separation", 3)
|
||||
row.add_child(textBox)
|
||||
|
||||
var nameLabel := Label.new()
|
||||
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
nameLabel.text = username
|
||||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
nameLabel.add_theme_color_override("font_color", TEXT_COLOR if online else MUTED_COLOR)
|
||||
nameLabel.add_theme_font_size_override("font_size", 16)
|
||||
textBox.add_child(nameLabel)
|
||||
|
||||
var statusLabel := Label.new()
|
||||
statusLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
statusLabel.text = "正在鲸落镇" if online else "暂时离线"
|
||||
statusLabel.add_theme_color_override("font_color", ONLINE_COLOR if online else MUTED_COLOR)
|
||||
statusLabel.add_theme_font_size_override("font_size", 12)
|
||||
textBox.add_child(statusLabel)
|
||||
|
||||
var dot := PanelContainer.new()
|
||||
dot.name = "FriendOnlineIndicator"
|
||||
dot.custom_minimum_size = Vector2(8, 8)
|
||||
dot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
dot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
dot.add_theme_stylebox_override("panel", _create_dot_style(ONLINE_COLOR if online else OFFLINE_COLOR))
|
||||
row.add_child(dot)
|
||||
|
||||
return button
|
||||
|
||||
func _create_request_row(request: Dictionary) -> Control:
|
||||
var userId := _read_user_id(request)
|
||||
var username := str(request.get("username", "玩家")).strip_edges()
|
||||
if username.is_empty():
|
||||
username = "玩家"
|
||||
|
||||
var rowPanel := PanelContainer.new()
|
||||
rowPanel.custom_minimum_size = Vector2(0, 48)
|
||||
rowPanel.add_theme_stylebox_override("panel", _create_request_style())
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
rowPanel.add_child(row)
|
||||
|
||||
var nameLabel := Label.new()
|
||||
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
nameLabel.text = username
|
||||
nameLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
row.add_child(nameLabel)
|
||||
|
||||
var acceptButton := _create_small_button("接受", Color(0.333, 0.761, 0.514))
|
||||
acceptButton.pressed.connect(func() -> void: _respond_request(userId, username, true))
|
||||
row.add_child(acceptButton)
|
||||
|
||||
var rejectButton := _create_small_button("拒绝", Color(0.812, 0.553, 0.553))
|
||||
rejectButton.pressed.connect(func() -> void: _respond_request(userId, username, false))
|
||||
row.add_child(rejectButton)
|
||||
|
||||
return rowPanel
|
||||
|
||||
func _select_friend(userId: String, username: String, _online: bool) -> void:
|
||||
if userId.strip_edges().is_empty():
|
||||
return
|
||||
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.CHAT_FRIEND_SELECTED, {
|
||||
"user_id": userId,
|
||||
"username": username
|
||||
})
|
||||
|
||||
func _respond_request(userId: String, username: String, accepted: bool) -> void:
|
||||
if userId.strip_edges().is_empty():
|
||||
return
|
||||
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
if chatManager == null:
|
||||
return
|
||||
|
||||
var ok := false
|
||||
if accepted and chatManager.has_method("accept_friend_request"):
|
||||
ok = bool(chatManager.call("accept_friend_request", userId, username))
|
||||
elif not accepted and chatManager.has_method("reject_friend_request"):
|
||||
ok = bool(chatManager.call("reject_friend_request", userId, username))
|
||||
|
||||
if ok:
|
||||
for i in range(_requests.size() - 1, -1, -1):
|
||||
if _read_user_id(_requests[i]) == userId:
|
||||
_requests.remove_at(i)
|
||||
_render()
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
_statusLabel.text = message
|
||||
_statusLabel.visible = true
|
||||
_requestList.visible = false
|
||||
|
||||
func _count_online_friends() -> int:
|
||||
var count := 0
|
||||
for friend in _friends:
|
||||
if bool(friend.get("online", false)):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
func _sorted_friends() -> Array[Dictionary]:
|
||||
var sortedFriends := _friends.duplicate(true)
|
||||
sortedFriends.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var onlineA := bool(a.get("online", false))
|
||||
var onlineB := bool(b.get("online", false))
|
||||
if onlineA != onlineB:
|
||||
return onlineA
|
||||
return str(a.get("username", "")).nocasecmp_to(str(b.get("username", ""))) < 0
|
||||
)
|
||||
return sortedFriends
|
||||
|
||||
func _read_user_id(data: Dictionary) -> String:
|
||||
return str(data.get("user_id", data.get("userId", ""))).strip_edges()
|
||||
|
||||
func _create_avatar(username: String, online: bool) -> PanelContainer:
|
||||
var avatar := PanelContainer.new()
|
||||
avatar.name = "FriendAvatarPanel"
|
||||
avatar.custom_minimum_size = FRIEND_AVATAR_SIZE
|
||||
avatar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
avatar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
avatar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
avatar.add_theme_stylebox_override("panel", _create_avatar_style(online))
|
||||
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.text = _avatar_text(username)
|
||||
label.custom_minimum_size = FRIEND_AVATAR_SIZE
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_color_override("font_color", Color.WHITE)
|
||||
label.add_theme_font_size_override("font_size", 16)
|
||||
avatar.add_child(label)
|
||||
|
||||
return avatar
|
||||
|
||||
func _avatar_text(username: String) -> String:
|
||||
var normalized := username.strip_edges()
|
||||
if normalized.is_empty():
|
||||
return "友"
|
||||
return normalized.substr(0, 1).to_upper()
|
||||
|
||||
func _create_small_button(text: String, color: Color) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = Vector2(52, 30)
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.add_theme_font_size_override("font_size", 13)
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_stylebox_override("normal", _create_pill_style(color, 9))
|
||||
button.add_theme_stylebox_override("hover", _create_pill_style(color.lightened(0.08), 9))
|
||||
button.add_theme_stylebox_override("pressed", _create_pill_style(color.darkened(0.08), 9))
|
||||
button.add_theme_stylebox_override("focus", _create_empty_style())
|
||||
return button
|
||||
|
||||
func _create_panel_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = SURFACE_COLOR
|
||||
style.border_width_left = 1
|
||||
style.border_width_top = 1
|
||||
style.border_width_right = 1
|
||||
style.border_width_bottom = 1
|
||||
style.border_color = BORDER_COLOR
|
||||
style.corner_radius_top_left = 20
|
||||
style.corner_radius_top_right = 20
|
||||
style.corner_radius_bottom_left = 20
|
||||
style.corner_radius_bottom_right = 20
|
||||
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.18)
|
||||
style.shadow_size = 18
|
||||
style.shadow_offset = Vector2(0, 7)
|
||||
return style
|
||||
|
||||
func _create_avatar_style(online: bool) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.361, 0.690, 0.914) if online else Color(0.678, 0.725, 0.773)
|
||||
style.corner_radius_top_left = FRIEND_AVATAR_CORNER_RADIUS
|
||||
style.corner_radius_top_right = FRIEND_AVATAR_CORNER_RADIUS
|
||||
style.corner_radius_bottom_left = FRIEND_AVATAR_CORNER_RADIUS
|
||||
style.corner_radius_bottom_right = FRIEND_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 _create_summary_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.918, 0.961, 0.992, 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.76, 0.86, 0.94, 0.62)
|
||||
style.corner_radius_top_left = 12
|
||||
style.corner_radius_top_right = 12
|
||||
style.corner_radius_bottom_left = 12
|
||||
style.corner_radius_bottom_right = 12
|
||||
style.content_margin_left = 14
|
||||
style.content_margin_right = 14
|
||||
return style
|
||||
|
||||
func _create_dot_style(color: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = 5
|
||||
style.corner_radius_top_right = 5
|
||||
style.corner_radius_bottom_left = 5
|
||||
style.corner_radius_bottom_right = 5
|
||||
return style
|
||||
|
||||
func _create_section_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.948, 0.974, 0.996, 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.87, 0.94, 0.54)
|
||||
style.corner_radius_top_left = 9
|
||||
style.corner_radius_top_right = 9
|
||||
style.corner_radius_bottom_left = 9
|
||||
style.corner_radius_bottom_right = 9
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_right = 12
|
||||
return style
|
||||
|
||||
func _create_empty_state_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.965, 0.982, 0.996, 0.86)
|
||||
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.80, 0.88, 0.94, 0.52)
|
||||
style.corner_radius_top_left = 12
|
||||
style.corner_radius_top_right = 12
|
||||
style.corner_radius_bottom_left = 12
|
||||
style.corner_radius_bottom_right = 12
|
||||
return style
|
||||
|
||||
func _create_request_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1, 1, 1, 0.94)
|
||||
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.86, 0.93, 0.64)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_bottom = 8
|
||||
return style
|
||||
|
||||
func _create_friend_row_style(isHovered: bool, isOnline: bool, isPressed: bool = false) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = SURFACE_RAISED_COLOR if isOnline else Color(0.965, 0.973, 0.982, 0.86)
|
||||
if isHovered:
|
||||
style.bg_color = Color(0.918, 0.961, 0.992, 0.98)
|
||||
if isPressed:
|
||||
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.55, 0.78, 0.95, 0.72) if isHovered else Color(0.80, 0.87, 0.93, 0.56)
|
||||
style.corner_radius_top_left = 12
|
||||
style.corner_radius_top_right = 12
|
||||
style.corner_radius_bottom_left = 12
|
||||
style.corner_radius_bottom_right = 12
|
||||
return style
|
||||
|
||||
func _create_pill_style(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_top = 5
|
||||
style.content_margin_bottom = 5
|
||||
return style
|
||||
|
||||
func _create_empty_style() -> StyleBoxEmpty:
|
||||
return StyleBoxEmpty.new()
|
||||
|
||||
func _clear_children(container: Node) -> void:
|
||||
for child in container.get_children():
|
||||
container.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
|
||||
func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
var settingsManager := get_node_or_null("/root/SettingsManager")
|
||||
if settingsManager != null and settingsManager.has_method("get_bool"):
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
1
scenes/ui/FriendListPanel.gd.uid
Normal file
1
scenes/ui/FriendListPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dgu3rem7rhdfa
|
||||
13
scenes/ui/FriendListPanel.tscn
Normal file
13
scenes/ui/FriendListPanel.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/FriendListPanel.gd" id="1"]
|
||||
|
||||
[node name="FriendListPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
143
scenes/ui/HudShortcutIcon.gd
Normal file
143
scenes/ui/HudShortcutIcon.gd
Normal file
@@ -0,0 +1,143 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# HudShortcutIcon.gd - 顶部快捷入口轻量线描图标
|
||||
# ============================================================================
|
||||
# 专门按 25px HUD 小尺寸绘制,保持细线、少细节、轻视觉重量。
|
||||
# ============================================================================
|
||||
|
||||
const LINE_COLOR: Color = Color(0.592157, 0.72549, 0.835294, 0.82)
|
||||
const DOT_COLOR: Color = Color(0.941176, 0.54902, 0.54902, 0.86)
|
||||
const DOT_BORDER_COLOR: Color = Color(1.0, 1.0, 1.0, 0.95)
|
||||
const LINE_WIDTH: float = 0.85
|
||||
const DETAIL_WIDTH: float = 0.75
|
||||
const BASE_SIZE: float = 27.0
|
||||
|
||||
var iconName: String = "map"
|
||||
var _iconScale: float = 1.0
|
||||
var _iconOffset: Vector2 = Vector2.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size = Vector2(31, 31)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func set_icon_name(value: String) -> void:
|
||||
iconName = value
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
_iconScale = min(size.x, size.y) / BASE_SIZE
|
||||
_iconOffset = (size - Vector2(BASE_SIZE, BASE_SIZE) * _iconScale) * 0.5
|
||||
|
||||
match iconName:
|
||||
"map":
|
||||
_draw_map()
|
||||
"task":
|
||||
_draw_task()
|
||||
"activity":
|
||||
_draw_activity()
|
||||
"backpack":
|
||||
_draw_backpack()
|
||||
"friends":
|
||||
_draw_friends()
|
||||
"settings":
|
||||
_draw_settings()
|
||||
_:
|
||||
_draw_map()
|
||||
|
||||
func _draw_map() -> void:
|
||||
_draw_polyline([
|
||||
Vector2(6.0, 7.0),
|
||||
Vector2(10.9, 5.4),
|
||||
Vector2(16.2, 7.1),
|
||||
Vector2(21.0, 5.5),
|
||||
Vector2(21.0, 20.1),
|
||||
Vector2(16.2, 21.6),
|
||||
Vector2(10.9, 19.9),
|
||||
Vector2(6.0, 21.5),
|
||||
Vector2(6.0, 7.0),
|
||||
])
|
||||
_draw_line(Vector2(10.9, 6.0), Vector2(10.9, 19.2), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(16.2, 7.7), Vector2(16.2, 21.0), DETAIL_WIDTH)
|
||||
|
||||
func _draw_task() -> void:
|
||||
_draw_round_rect(Rect2(7.3, 6.2, 12.4, 15.4), 2.2)
|
||||
_draw_line(Vector2(11.0, 5.0), Vector2(16.0, 5.0), LINE_WIDTH)
|
||||
_draw_line(Vector2(9.8, 11.0), Vector2(16.8, 11.0), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(9.8, 14.8), Vector2(16.8, 14.8), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(9.8, 18.6), Vector2(14.8, 18.6), DETAIL_WIDTH)
|
||||
_draw_red_dot(Vector2(20.0, 5.6))
|
||||
|
||||
func _draw_activity() -> void:
|
||||
_draw_round_rect(Rect2(6.2, 7.5, 14.1, 13.2), 2.2)
|
||||
_draw_line(Vector2(6.9, 11.5), Vector2(19.5, 11.5), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(9.6, 5.5), Vector2(9.6, 9.0), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(16.7, 5.5), Vector2(16.7, 9.0), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(10.0, 15.6), Vector2(12.0, 15.6), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(14.6, 15.6), Vector2(16.6, 15.6), DETAIL_WIDTH)
|
||||
_draw_red_dot(Vector2(20.4, 5.8))
|
||||
|
||||
func _draw_backpack() -> void:
|
||||
_draw_round_rect(Rect2(6.4, 8.4, 14.2, 12.6), 2.4)
|
||||
_draw_arc_poly(Vector2(13.5, 9.0), 4.0, PI + 0.18, TAU - 0.18, 18, DETAIL_WIDTH)
|
||||
_draw_line(Vector2(8.9, 12.4), Vector2(18.1, 12.4), DETAIL_WIDTH)
|
||||
_draw_round_rect(Rect2(10.4, 13.9, 6.2, 4.4), 1.2, DETAIL_WIDTH)
|
||||
_draw_line(Vector2(8.2, 10.2), Vector2(8.2, 20.0), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(18.8, 10.2), Vector2(18.8, 20.0), DETAIL_WIDTH)
|
||||
|
||||
func _draw_friends() -> void:
|
||||
_draw_arc(Vector2(10.2, 9.4), 3.1, 0.0, TAU, 28, LINE_WIDTH)
|
||||
_draw_arc_poly(Vector2(10.2, 21.6), 6.1, PI + 0.10, TAU - 0.10, 22)
|
||||
_draw_arc(Vector2(17.8, 10.6), 2.5, 0.0, TAU, 24, DETAIL_WIDTH)
|
||||
_draw_arc_poly(Vector2(17.8, 21.6), 4.7, PI + 0.16, TAU - 0.24, 18, DETAIL_WIDTH)
|
||||
|
||||
func _draw_settings() -> void:
|
||||
var center := Vector2(13.5, 13.5)
|
||||
for i in range(8):
|
||||
var angle := TAU * float(i) / 8.0
|
||||
var inner := center + Vector2(cos(angle), sin(angle)) * 5.0
|
||||
var outer := center + Vector2(cos(angle), sin(angle)) * 6.7
|
||||
_draw_line(inner, outer, DETAIL_WIDTH)
|
||||
_draw_arc(center, 4.9, 0.0, TAU, 32, LINE_WIDTH)
|
||||
_draw_arc(center, 2.0, 0.0, TAU, 24, DETAIL_WIDTH)
|
||||
|
||||
func _draw_round_rect(rect: Rect2, radius: float, width: float = LINE_WIDTH) -> void:
|
||||
var left := rect.position.x
|
||||
var top := rect.position.y
|
||||
var right := rect.end.x
|
||||
var bottom := rect.end.y
|
||||
_draw_line(Vector2(left + radius, top), Vector2(right - radius, top), width)
|
||||
_draw_line(Vector2(right, top + radius), Vector2(right, bottom - radius), width)
|
||||
_draw_line(Vector2(right - radius, bottom), Vector2(left + radius, bottom), width)
|
||||
_draw_line(Vector2(left, bottom - radius), Vector2(left, top + radius), width)
|
||||
_draw_arc(Vector2(left + radius, top + radius), radius, PI, PI * 1.5, 8, width)
|
||||
_draw_arc(Vector2(right - radius, top + radius), radius, PI * 1.5, TAU, 8, width)
|
||||
_draw_arc(Vector2(right - radius, bottom - radius), radius, 0.0, PI * 0.5, 8, width)
|
||||
_draw_arc(Vector2(left + radius, bottom - radius), radius, PI * 0.5, PI, 8, width)
|
||||
|
||||
func _draw_arc_poly(center: Vector2, radius: float, start: float, end: float, pointCount: int, width: float = LINE_WIDTH) -> void:
|
||||
var points: PackedVector2Array = []
|
||||
for i in range(pointCount + 1):
|
||||
var t := float(i) / float(pointCount)
|
||||
var angle := lerpf(start, end, t)
|
||||
points.append(_p(center + Vector2(cos(angle), sin(angle)) * radius))
|
||||
draw_polyline(points, LINE_COLOR, width, true)
|
||||
|
||||
func _draw_polyline(points: Array[Vector2], width: float = LINE_WIDTH) -> void:
|
||||
var packed: PackedVector2Array = []
|
||||
for point in points:
|
||||
packed.append(_p(point))
|
||||
draw_polyline(packed, LINE_COLOR, width, true)
|
||||
|
||||
func _draw_red_dot(center: Vector2) -> void:
|
||||
draw_circle(_p(center), 2.3 * _iconScale, DOT_BORDER_COLOR)
|
||||
draw_circle(_p(center), 1.55 * _iconScale, DOT_COLOR)
|
||||
|
||||
func _draw_line(from: Vector2, to: Vector2, width: float) -> void:
|
||||
draw_line(_p(from), _p(to), LINE_COLOR, width, true)
|
||||
|
||||
func _draw_arc(center: Vector2, radius: float, startAngle: float, endAngle: float, pointCount: int, width: float) -> void:
|
||||
draw_arc(_p(center), radius * _iconScale, startAngle, endAngle, pointCount, LINE_COLOR, width, true)
|
||||
|
||||
func _p(point: Vector2) -> Vector2:
|
||||
return _iconOffset + point * _iconScale
|
||||
1
scenes/ui/HudShortcutIcon.gd.uid
Normal file
1
scenes/ui/HudShortcutIcon.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://de885djlotpxs
|
||||
516
scenes/ui/MapPanel.gd
Normal file
516
scenes/ui/MapPanel.gd
Normal file
@@ -0,0 +1,516 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# MapPanel.gd - 右上角小镇地图弹窗
|
||||
# ============================================================================
|
||||
# 监听 HUD 地图入口,展示当前区域全图与关键建筑标识。
|
||||
# ============================================================================
|
||||
|
||||
const PANEL_SIZE: Vector2 = Vector2(548, 452)
|
||||
const PANEL_MARGIN_TOP: float = 122.0
|
||||
const PANEL_MARGIN_RIGHT: float = 56.0
|
||||
const MAP_VIEW_SIZE: Vector2 = Vector2(500, 375)
|
||||
const MAP_WORLD_SIZE: Vector2 = Vector2(2560, 1920)
|
||||
const MAP_CAMERA_ZOOM: Vector2 = Vector2(MAP_VIEW_SIZE.x / MAP_WORLD_SIZE.x, MAP_VIEW_SIZE.y / MAP_WORLD_SIZE.y)
|
||||
const MINIMAP_EXCLUDED_NODE_NAMES: Array[String] = [
|
||||
"Characters",
|
||||
"MultiplayerController",
|
||||
]
|
||||
|
||||
const TEXT_COLOR: Color = Color(0.188, 0.294, 0.424)
|
||||
const MUTED_COLOR: Color = Color(0.560, 0.639, 0.733)
|
||||
const ACCENT_COLOR: Color = Color(0.258824, 0.627451, 0.913725)
|
||||
const SOFT_BLUE: Color = Color(0.918, 0.961, 0.992, 0.94)
|
||||
const PIN_BLUE: Color = Color(0.244, 0.572, 0.862)
|
||||
|
||||
const MAP_CONFIGS: Dictionary = {
|
||||
"Square": {
|
||||
"title": "小镇地图",
|
||||
"sourceNodes": [
|
||||
"HDGrassBase",
|
||||
"GroundGrassLayer",
|
||||
"PathBaseLayer",
|
||||
"PathEdgeLayer",
|
||||
"PathEdgeCornerLayer",
|
||||
"PathEdgeJoinLayer",
|
||||
"PathCurbLayer",
|
||||
"YSortWorld",
|
||||
],
|
||||
"markers": [
|
||||
{"label": "码头", "icon": "anchor", "pos": Vector2(0.160, 0.395), "side": "right"},
|
||||
{"label": "总部", "icon": "home", "pos": Vector2(0.505, 0.188), "side": "right"},
|
||||
{"label": "广场", "icon": "whale", "pos": Vector2(0.505, 0.515), "side": "right"},
|
||||
{"label": "小屋", "icon": "home", "pos": Vector2(0.830, 0.400), "side": "right"},
|
||||
{"label": "工坊", "icon": "tool", "pos": Vector2(0.752, 0.760), "side": "left"},
|
||||
{"label": "公告", "icon": "notice", "pos": Vector2(0.288, 0.838), "side": "right"},
|
||||
{"label": "入口", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
|
||||
],
|
||||
},
|
||||
"WorkZone": {
|
||||
"title": "打工区地图",
|
||||
"sourceNodes": [
|
||||
"StageBackdrop",
|
||||
"RoadPavementLayer",
|
||||
"RoadAsphaltLayer",
|
||||
"RoadCrosswalkLayer",
|
||||
"RoadMarkingLayer",
|
||||
"YSortWorld",
|
||||
],
|
||||
"markers": [
|
||||
{"label": "商城", "icon": "home", "pos": Vector2(0.500, 0.180), "side": "right"},
|
||||
{"label": "咖啡店", "icon": "home", "pos": Vector2(0.092, 0.620), "side": "right"},
|
||||
{"label": "任务", "icon": "notice", "pos": Vector2(0.304, 0.600), "side": "right"},
|
||||
{"label": "课程", "icon": "notice", "pos": Vector2(0.694, 0.500), "side": "left"},
|
||||
{"label": "AI站", "icon": "tool", "pos": Vector2(0.676, 0.785), "side": "left"},
|
||||
{"label": "鲸币", "icon": "whale", "pos": Vector2(0.920, 0.785), "side": "left"},
|
||||
{"label": "入口", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
|
||||
],
|
||||
},
|
||||
"CafeInterior": {
|
||||
"title": "鲸鱼咖啡馆",
|
||||
"worldSize": Vector2(1536, 1024),
|
||||
"sourceNodes": [
|
||||
"StageBackdrop",
|
||||
"CafeServiceHallBase",
|
||||
],
|
||||
"markers": [
|
||||
{"label": "服务台", "icon": "whale", "pos": Vector2(0.500, 0.465), "side": "right"},
|
||||
{"label": "陪伴区", "icon": "notice", "pos": Vector2(0.240, 0.280), "side": "right"},
|
||||
{"label": "出口", "icon": "gate", "pos": Vector2(0.500, 0.855), "side": "right"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
var _panel: PanelContainer
|
||||
var _mapFrame: PanelContainer
|
||||
var _mapLayer: Control
|
||||
var _mapViewport: SubViewport
|
||||
var _mapWorld: Node2D
|
||||
var _mapCamera: Camera2D
|
||||
var _titleLabel: Label
|
||||
var _markerNodes: Array[Control] = []
|
||||
var _isOpen: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
set_panel_open(false)
|
||||
_subscribe_to_events()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.HUD_MAP_TOGGLE, _on_map_toggle, self)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED and is_instance_valid(_panel):
|
||||
_anchor_panel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
var keyEvent := event as InputEventKey
|
||||
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
|
||||
set_panel_open(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func set_panel_open(open: bool) -> void:
|
||||
_isOpen = open
|
||||
visible = _isOpen
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP if _isOpen else Control.MOUSE_FILTER_IGNORE
|
||||
if is_instance_valid(_panel):
|
||||
_panel.visible = _isOpen
|
||||
if _isOpen:
|
||||
_rebuild_minimap_world()
|
||||
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func toggle_panel() -> void:
|
||||
set_panel_open(not _isOpen)
|
||||
|
||||
func _build_ui() -> void:
|
||||
_panel = PanelContainer.new()
|
||||
_panel.name = "MapCard"
|
||||
_panel.custom_minimum_size = PANEL_SIZE
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_panel.add_theme_stylebox_override("panel", _create_panel_style())
|
||||
add_child(_panel)
|
||||
_anchor_panel()
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 24)
|
||||
margin.add_theme_constant_override("margin_top", 20)
|
||||
margin.add_theme_constant_override("margin_right", 24)
|
||||
margin.add_theme_constant_override("margin_bottom", 22)
|
||||
_panel.add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 12)
|
||||
margin.add_child(content)
|
||||
|
||||
content.add_child(_build_header())
|
||||
content.add_child(_build_map_view())
|
||||
|
||||
func _build_header() -> Control:
|
||||
var header := HBoxContainer.new()
|
||||
header.custom_minimum_size = Vector2(0, 42)
|
||||
header.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
|
||||
var icon := MapGlyph.new()
|
||||
icon.custom_minimum_size = Vector2(32, 32)
|
||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
header.add_child(icon)
|
||||
|
||||
var title := Label.new()
|
||||
title.name = "TitleLabel"
|
||||
_titleLabel = title
|
||||
title.text = "小镇地图"
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
header.add_child(title)
|
||||
|
||||
var closeButton := Button.new()
|
||||
closeButton.text = "×"
|
||||
closeButton.tooltip_text = "关闭地图"
|
||||
closeButton.custom_minimum_size = Vector2(42, 42)
|
||||
closeButton.focus_mode = Control.FOCUS_NONE
|
||||
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
closeButton.add_theme_font_size_override("font_size", 28)
|
||||
closeButton.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
closeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||||
closeButton.add_theme_color_override("font_pressed_color", ACCENT_COLOR.darkened(0.18))
|
||||
closeButton.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
|
||||
closeButton.add_theme_stylebox_override("normal", _create_round_style(SOFT_BLUE, 21))
|
||||
closeButton.add_theme_stylebox_override("hover", _create_round_style(Color(0.858, 0.925, 0.980, 1.0), 21))
|
||||
closeButton.add_theme_stylebox_override("pressed", _create_round_style(Color(0.778, 0.884, 0.965, 1.0), 21))
|
||||
closeButton.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
closeButton.pressed.connect(func() -> void: set_panel_open(false))
|
||||
header.add_child(closeButton)
|
||||
|
||||
return header
|
||||
|
||||
func _build_map_view() -> Control:
|
||||
_mapFrame = PanelContainer.new()
|
||||
_mapFrame.custom_minimum_size = MAP_VIEW_SIZE
|
||||
_mapFrame.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_mapFrame.add_theme_stylebox_override("panel", _create_map_frame_style())
|
||||
|
||||
_mapLayer = Control.new()
|
||||
_mapLayer.custom_minimum_size = MAP_VIEW_SIZE
|
||||
_mapLayer.clip_contents = true
|
||||
_mapLayer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mapFrame.add_child(_mapLayer)
|
||||
|
||||
var viewportContainer := SubViewportContainer.new()
|
||||
viewportContainer.name = "SquareMapViewportContainer"
|
||||
viewportContainer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
viewportContainer.stretch = true
|
||||
viewportContainer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mapLayer.add_child(viewportContainer)
|
||||
|
||||
_mapViewport = SubViewport.new()
|
||||
_mapViewport.name = "SquareMapViewport"
|
||||
_mapViewport.size = Vector2i(int(MAP_VIEW_SIZE.x), int(MAP_VIEW_SIZE.y))
|
||||
_mapViewport.render_target_update_mode = SubViewport.UPDATE_WHEN_VISIBLE
|
||||
_mapViewport.world_2d = World2D.new()
|
||||
viewportContainer.add_child(_mapViewport)
|
||||
|
||||
_mapWorld = Node2D.new()
|
||||
_mapWorld.name = "RenderedMapWorld"
|
||||
_mapViewport.add_child(_mapWorld)
|
||||
|
||||
_mapCamera = Camera2D.new()
|
||||
_mapCamera.name = "MinimapCamera"
|
||||
_mapCamera.position = Vector2.ZERO
|
||||
_mapCamera.zoom = MAP_CAMERA_ZOOM
|
||||
_mapCamera.enabled = true
|
||||
_mapViewport.add_child(_mapCamera)
|
||||
|
||||
var wash := ColorRect.new()
|
||||
wash.name = "MapSoftWash"
|
||||
wash.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
wash.color = Color(1.0, 0.985, 0.930, 0.10)
|
||||
wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mapLayer.add_child(wash)
|
||||
|
||||
_render_markers()
|
||||
|
||||
return _mapFrame
|
||||
|
||||
func _render_markers() -> void:
|
||||
for markerNode in _markerNodes:
|
||||
if is_instance_valid(markerNode):
|
||||
markerNode.queue_free()
|
||||
_markerNodes.clear()
|
||||
|
||||
for marker in _current_markers():
|
||||
var markerNode := _create_marker(marker)
|
||||
_markerNodes.append(markerNode)
|
||||
_mapLayer.add_child(markerNode)
|
||||
|
||||
func _create_marker(marker: Dictionary) -> Control:
|
||||
var button := Button.new()
|
||||
button.name = "Marker%s" % str(marker.get("label", ""))
|
||||
button.text = ""
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.custom_minimum_size = Vector2(82, 34)
|
||||
button.add_theme_stylebox_override("normal", _create_marker_style(Color(1.0, 1.0, 1.0, 0.93), Color(0.650, 0.792, 0.910, 0.88)))
|
||||
button.add_theme_stylebox_override("hover", _create_marker_style(Color(0.925, 0.966, 0.996, 0.98), ACCENT_COLOR))
|
||||
button.add_theme_stylebox_override("pressed", _create_marker_style(Color(0.858, 0.925, 0.980, 0.98), ACCENT_COLOR.darkened(0.05)))
|
||||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
row.offset_left = 8
|
||||
row.offset_right = -9
|
||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
row.add_theme_constant_override("separation", 5)
|
||||
button.add_child(row)
|
||||
|
||||
var icon := MarkerGlyph.new()
|
||||
icon.set("iconName", str(marker.get("icon", "pin")))
|
||||
icon.custom_minimum_size = Vector2(20, 20)
|
||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.add_child(icon)
|
||||
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.text = str(marker.get("label", "地点"))
|
||||
label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
label.add_theme_font_size_override("font_size", 14)
|
||||
row.add_child(label)
|
||||
|
||||
var position := marker.get("pos", Vector2.ZERO) as Vector2
|
||||
var buttonSize := button.custom_minimum_size
|
||||
var xOffset := 8.0 if str(marker.get("side", "right")) == "right" else -buttonSize.x - 8.0
|
||||
button.position = Vector2(
|
||||
position.x * MAP_VIEW_SIZE.x + xOffset,
|
||||
position.y * MAP_VIEW_SIZE.y - buttonSize.y * 0.5
|
||||
)
|
||||
return button
|
||||
|
||||
func _anchor_panel() -> void:
|
||||
_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
_panel.offset_left = -PANEL_SIZE.x - PANEL_MARGIN_RIGHT
|
||||
_panel.offset_top = PANEL_MARGIN_TOP
|
||||
_panel.offset_right = -PANEL_MARGIN_RIGHT
|
||||
_panel.offset_bottom = PANEL_MARGIN_TOP + PANEL_SIZE.y
|
||||
_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
|
||||
func _subscribe_to_events() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("MapPanel: EventSystem autoload is not available.")
|
||||
return
|
||||
eventSystem.call("connect_event", EventNames.HUD_MAP_TOGGLE, _on_map_toggle, self)
|
||||
|
||||
func _on_map_toggle(_data: Variant = null) -> void:
|
||||
toggle_panel()
|
||||
|
||||
func _rebuild_minimap_world() -> void:
|
||||
if not is_instance_valid(_mapWorld):
|
||||
return
|
||||
_clear_children(_mapWorld)
|
||||
|
||||
var sourceScene := _find_source_scene()
|
||||
if sourceScene == null:
|
||||
push_warning("MapPanel: source scene for minimap is unavailable.")
|
||||
return
|
||||
_apply_scene_config(sourceScene)
|
||||
|
||||
for nodeName in _current_source_node_names(sourceScene):
|
||||
var sourceNode := sourceScene.get_node_or_null(nodeName)
|
||||
if sourceNode == null:
|
||||
continue
|
||||
var clone := sourceNode.duplicate(0)
|
||||
if clone == null:
|
||||
continue
|
||||
_remove_excluded_minimap_nodes(clone)
|
||||
_mapWorld.add_child(clone)
|
||||
|
||||
func _find_source_scene() -> Node:
|
||||
var node: Node = self
|
||||
while node != null:
|
||||
if MAP_CONFIGS.has(str(node.name)):
|
||||
return node
|
||||
node = node.get_parent()
|
||||
return get_tree().current_scene
|
||||
|
||||
func _apply_scene_config(sourceScene: Node) -> void:
|
||||
var config := _map_config_for_scene(sourceScene)
|
||||
if is_instance_valid(_titleLabel):
|
||||
_titleLabel.text = str(config.get("title", "区域地图"))
|
||||
_configure_map_camera(config)
|
||||
_render_markers()
|
||||
|
||||
func _configure_map_camera(config: Dictionary) -> void:
|
||||
if not is_instance_valid(_mapCamera):
|
||||
return
|
||||
var worldSize := config.get("worldSize", MAP_WORLD_SIZE) as Vector2
|
||||
if worldSize.x <= 0.0 or worldSize.y <= 0.0:
|
||||
worldSize = MAP_WORLD_SIZE
|
||||
var zoomScale := minf(MAP_VIEW_SIZE.x / worldSize.x, MAP_VIEW_SIZE.y / worldSize.y)
|
||||
_mapCamera.zoom = Vector2.ONE * zoomScale
|
||||
|
||||
func _current_source_node_names(sourceScene: Node) -> Array:
|
||||
var config := _map_config_for_scene(sourceScene)
|
||||
var nodeNames: Array = config.get("sourceNodes", [])
|
||||
return nodeNames
|
||||
|
||||
func _current_markers() -> Array:
|
||||
var config := _map_config_for_scene(_find_source_scene())
|
||||
var markers: Array = config.get("markers", [])
|
||||
return markers
|
||||
|
||||
func _map_config_for_scene(sourceScene: Node) -> Dictionary:
|
||||
if sourceScene != null:
|
||||
var sceneName := str(sourceScene.name)
|
||||
if MAP_CONFIGS.has(sceneName):
|
||||
return MAP_CONFIGS[sceneName]
|
||||
return MAP_CONFIGS["Square"]
|
||||
|
||||
func _remove_excluded_minimap_nodes(root: Node) -> void:
|
||||
for nodeName in MINIMAP_EXCLUDED_NODE_NAMES:
|
||||
var node := root.find_child(nodeName, true, false)
|
||||
if node != null and node.get_parent() != null:
|
||||
node.get_parent().remove_child(node)
|
||||
node.queue_free()
|
||||
|
||||
func _clear_children(container: Node) -> void:
|
||||
for child in container.get_children():
|
||||
container.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
func _create_panel_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 1.0, 0.948)
|
||||
style.corner_radius_top_left = 28
|
||||
style.corner_radius_top_right = 28
|
||||
style.corner_radius_bottom_left = 28
|
||||
style.corner_radius_bottom_right = 28
|
||||
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.18)
|
||||
style.shadow_size = 22
|
||||
style.shadow_offset = Vector2(0, 9)
|
||||
return style
|
||||
|
||||
func _create_map_frame_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.918, 0.961, 0.992, 0.96)
|
||||
style.border_color = Color(0.431, 0.733, 0.929, 0.86)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.corner_radius_top_left = 16
|
||||
style.corner_radius_top_right = 16
|
||||
style.corner_radius_bottom_left = 16
|
||||
style.corner_radius_bottom_right = 16
|
||||
return style
|
||||
|
||||
func _create_marker_style(color: Color, borderColor: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.border_color = borderColor
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.corner_radius_top_left = 18
|
||||
style.corner_radius_top_right = 18
|
||||
style.corner_radius_bottom_left = 18
|
||||
style.corner_radius_bottom_right = 18
|
||||
style.shadow_color = Color(0.078, 0.223, 0.360, 0.14)
|
||||
style.shadow_size = 7
|
||||
style.shadow_offset = Vector2(0, 3)
|
||||
return style
|
||||
|
||||
func _create_round_style(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
return style
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
|
||||
class MapGlyph:
|
||||
extends Control
|
||||
|
||||
func _draw() -> void:
|
||||
var color := Color(0.258824, 0.627451, 0.913725)
|
||||
var width := 2.2
|
||||
draw_polyline(PackedVector2Array([
|
||||
Vector2(6, 9), Vector2(12, 7), Vector2(19, 9), Vector2(26, 7),
|
||||
Vector2(26, 25), Vector2(19, 27), Vector2(12, 25), Vector2(6, 27),
|
||||
Vector2(6, 9)
|
||||
]), color, width, true)
|
||||
draw_line(Vector2(12, 8), Vector2(12, 24), color, 1.6, true)
|
||||
draw_line(Vector2(19, 10), Vector2(19, 26), color, 1.6, true)
|
||||
|
||||
class MarkerGlyph:
|
||||
extends Control
|
||||
|
||||
var iconName: String = "pin"
|
||||
|
||||
func _draw() -> void:
|
||||
var color := Color(0.258824, 0.627451, 0.913725)
|
||||
var center := size * 0.5
|
||||
match iconName:
|
||||
"anchor":
|
||||
_draw_anchor(center, color)
|
||||
"home":
|
||||
_draw_home(center, color)
|
||||
"whale":
|
||||
_draw_whale(center, color)
|
||||
"tool":
|
||||
_draw_tool(center, color)
|
||||
"notice":
|
||||
_draw_notice(center, color)
|
||||
"gate":
|
||||
_draw_gate(center, color)
|
||||
_:
|
||||
draw_circle(center, 5.0, color)
|
||||
|
||||
func _draw_anchor(center: Vector2, color: Color) -> void:
|
||||
draw_line(center + Vector2(0, -7), center + Vector2(0, 7), color, 2.0, true)
|
||||
draw_circle(center + Vector2(0, -7), 2.8, color)
|
||||
draw_arc(center + Vector2(0, 2), 7.0, 0.20, PI - 0.20, 24, color, 2.0, true)
|
||||
draw_line(center + Vector2(-7, 3), center + Vector2(-10, 0), color, 2.0, true)
|
||||
draw_line(center + Vector2(7, 3), center + Vector2(10, 0), color, 2.0, true)
|
||||
|
||||
func _draw_home(center: Vector2, color: Color) -> void:
|
||||
draw_polyline(PackedVector2Array([
|
||||
center + Vector2(-8, -1), center + Vector2(0, -8), center + Vector2(8, -1)
|
||||
]), color, 2.0, true)
|
||||
draw_rect(Rect2(center + Vector2(-6, -1), Vector2(12, 10)), color, false, 2.0)
|
||||
draw_line(center + Vector2(-1, 9), center + Vector2(-1, 3), color, 2.0, true)
|
||||
|
||||
func _draw_whale(center: Vector2, color: Color) -> void:
|
||||
draw_arc(center + Vector2(-1, 2), 8.0, PI, TAU, 28, color, 2.0, true)
|
||||
draw_line(center + Vector2(6, 0), center + Vector2(11, -4), color, 2.0, true)
|
||||
draw_line(center + Vector2(6, 0), center + Vector2(11, 4), color, 2.0, true)
|
||||
draw_circle(center + Vector2(-4, 0), 1.2, color)
|
||||
|
||||
func _draw_tool(center: Vector2, color: Color) -> void:
|
||||
draw_line(center + Vector2(-7, 7), center + Vector2(7, -7), color, 2.4, true)
|
||||
draw_arc(center + Vector2(7, -7), 5.0, PI * 0.1, PI * 1.3, 18, color, 2.0, true)
|
||||
draw_circle(center + Vector2(-7, 7), 2.4, color)
|
||||
|
||||
func _draw_notice(center: Vector2, color: Color) -> void:
|
||||
draw_rect(Rect2(center + Vector2(-8, -7), Vector2(16, 13)), color, false, 2.0)
|
||||
draw_line(center + Vector2(-5, -2), center + Vector2(5, -2), color, 1.7, true)
|
||||
draw_line(center + Vector2(-5, 2), center + Vector2(2, 2), color, 1.7, true)
|
||||
|
||||
func _draw_gate(center: Vector2, color: Color) -> void:
|
||||
draw_rect(Rect2(center + Vector2(-9, -3), Vector2(18, 11)), color, false, 2.0)
|
||||
draw_line(center + Vector2(0, -3), center + Vector2(0, 8), color, 1.7, true)
|
||||
draw_line(center + Vector2(-11, 8), center + Vector2(11, 8), color, 2.0, true)
|
||||
1
scenes/ui/MapPanel.gd.uid
Normal file
1
scenes/ui/MapPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ckpyd48v7l4w8
|
||||
13
scenes/ui/MapPanel.tscn
Normal file
13
scenes/ui/MapPanel.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/MapPanel.gd" id="1"]
|
||||
|
||||
[node name="MapPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
129
scenes/ui/NoticeDialog.gd
Normal file
129
scenes/ui/NoticeDialog.gd
Normal file
@@ -0,0 +1,129 @@
|
||||
extends CanvasLayer
|
||||
class_name NoticeDialog
|
||||
|
||||
const DEFAULT_PAGES: Array[Dictionary] = [
|
||||
{
|
||||
"text": "[center][color=#2f6b73]小镇新闻[/color][/center]\n\n线上新闻接口接入后,这里会同步 Datawhale Town 的最新动态、活动公告和版本消息。\n\n当前请先通过欢迎板查看新人引导手册。",
|
||||
"image_path": "res://assets/maps/square/v1/props/center_whale_fountain_v2_hd_clean.png",
|
||||
},
|
||||
]
|
||||
|
||||
@onready var contentLabel: RichTextLabel = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/TextPanel/ContentLabel
|
||||
@onready var prevButton: Button = $CenterContainer/PanelContainer/VBoxContainer/Footer/PrevButton
|
||||
@onready var nextButton: Button = $CenterContainer/PanelContainer/VBoxContainer/Footer/NextButton
|
||||
@onready var dotsContainer: HBoxContainer = $CenterContainer/PanelContainer/VBoxContainer/Footer/DotsContainer
|
||||
@onready var contentContainer: Container = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer
|
||||
@onready var imageRect: TextureRect = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel/ImageRect
|
||||
@onready var imageLabel: Label = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel/ImageLabel
|
||||
|
||||
var pages: Array[Dictionary] = DEFAULT_PAGES.duplicate(true)
|
||||
var currentPage: int = 0
|
||||
var tween: Tween
|
||||
var _chatUi: Control
|
||||
var _chatUiPrevMouseFilter: Control.MouseFilter = Control.MOUSE_FILTER_STOP
|
||||
var _chatUiMouseDisabled: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().paused = true
|
||||
_disableChatUiMouseInput()
|
||||
|
||||
var closeButton := $CenterContainer/PanelContainer/VBoxContainer/Header/RightContainer/CloseButton as Button
|
||||
closeButton.pressed.connect(_onClosePressed)
|
||||
prevButton.pressed.connect(_onPrevPressed)
|
||||
nextButton.pressed.connect(_onNextPressed)
|
||||
|
||||
contentContainer.modulate.a = 1.0
|
||||
_setupDots()
|
||||
_updateUi(false)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_restoreChatUiMouseInput()
|
||||
|
||||
func _setupDots() -> void:
|
||||
for child in dotsContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for index in range(pages.size()):
|
||||
var dot := ColorRect.new()
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
dotsContainer.add_child(dot)
|
||||
|
||||
func _updateUi(animate: bool = true) -> void:
|
||||
if pages.is_empty():
|
||||
return
|
||||
|
||||
prevButton.disabled = currentPage == 0
|
||||
nextButton.disabled = currentPage == pages.size() - 1
|
||||
|
||||
var dots := dotsContainer.get_children()
|
||||
for index in range(dots.size()):
|
||||
var dot := dots[index] as ColorRect
|
||||
if dot == null:
|
||||
continue
|
||||
if index == currentPage:
|
||||
dot.color = Color(0.15, 0.36, 0.44, 1.0)
|
||||
dot.custom_minimum_size = Vector2(18, 12)
|
||||
else:
|
||||
dot.color = Color(0.72, 0.78, 0.72, 1.0)
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
|
||||
if animate:
|
||||
_animateContentChange()
|
||||
else:
|
||||
_setContentImmediate()
|
||||
|
||||
func _setContentImmediate() -> void:
|
||||
var page := pages[currentPage]
|
||||
contentLabel.text = page.get("text", "") as String
|
||||
|
||||
var imagePath := page.get("image_path", "") as String
|
||||
if imagePath != "" and ResourceLoader.exists(imagePath):
|
||||
imageRect.texture = load(imagePath) as Texture2D
|
||||
imageLabel.visible = false
|
||||
else:
|
||||
imageRect.texture = null
|
||||
imageLabel.visible = true
|
||||
imageLabel.text = "暂无图片"
|
||||
|
||||
func _animateContentChange() -> void:
|
||||
if tween != null and tween.is_valid():
|
||||
tween.kill()
|
||||
|
||||
tween = create_tween()
|
||||
tween.tween_property(contentContainer, "modulate:a", 0.0, 0.15)
|
||||
tween.tween_callback(_setContentImmediate)
|
||||
tween.tween_property(contentContainer, "modulate:a", 1.0, 0.15)
|
||||
|
||||
func _onPrevPressed() -> void:
|
||||
if currentPage > 0:
|
||||
currentPage -= 1
|
||||
_updateUi()
|
||||
|
||||
func _onNextPressed() -> void:
|
||||
if currentPage < pages.size() - 1:
|
||||
currentPage += 1
|
||||
_updateUi()
|
||||
|
||||
func _onClosePressed() -> void:
|
||||
get_tree().paused = false
|
||||
queue_free()
|
||||
|
||||
func _disableChatUiMouseInput() -> void:
|
||||
var currentScene := get_tree().current_scene
|
||||
if currentScene != null:
|
||||
_chatUi = currentScene.get_node_or_null("UILayer/ChatUI") as Control
|
||||
|
||||
if _chatUi == null:
|
||||
_chatUi = get_tree().root.find_child("ChatUI", true, false) as Control
|
||||
|
||||
if _chatUi != null:
|
||||
_chatUiPrevMouseFilter = _chatUi.mouse_filter
|
||||
_chatUi.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_chatUiMouseDisabled = true
|
||||
|
||||
func _restoreChatUiMouseInput() -> void:
|
||||
if _chatUiMouseDisabled and is_instance_valid(_chatUi):
|
||||
_chatUi.mouse_filter = _chatUiPrevMouseFilter
|
||||
|
||||
_chatUiMouseDisabled = false
|
||||
_chatUi = null
|
||||
1
scenes/ui/NoticeDialog.gd.uid
Normal file
1
scenes/ui/NoticeDialog.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cd8ueskjdp4g5
|
||||
489
scenes/ui/PlayerHud.gd
Normal file
489
scenes/ui/PlayerHud.gd
Normal file
@@ -0,0 +1,489 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# PlayerHud.gd - 右上角玩家头像与快捷入口
|
||||
# ============================================================================
|
||||
# 展示当前玩家头像入口,以及地图/任务/背包/好友/设置入口。
|
||||
# 好友入口只负责打开或收起右下角好友列表。
|
||||
# ============================================================================
|
||||
|
||||
const HUD_MARGIN: Vector2 = Vector2(16, 16)
|
||||
const SHORTCUT_BAR_SIZE: Vector2 = Vector2(350, 86)
|
||||
const PLAYER_PROFILE_BUTTON_SIZE: Vector2 = Vector2(210, 78)
|
||||
const PLAYER_AVATAR_SIZE: Vector2 = Vector2(58, 58)
|
||||
const HUD_SEPARATION: int = 18
|
||||
const HUD_TOTAL_WIDTH: float = SHORTCUT_BAR_SIZE.x + PLAYER_PROFILE_BUTTON_SIZE.x + HUD_SEPARATION
|
||||
const TEXT_COLOR: Color = Color(0.188, 0.294, 0.424)
|
||||
const MUTED_COLOR: Color = Color(0.560, 0.639, 0.733)
|
||||
const ACCENT_COLOR: Color = Color(0.258824, 0.627451, 0.913725)
|
||||
const WALLET_REFRESH_INTERVAL: float = 15.0
|
||||
const HUD_SHORTCUT_ICON_SCRIPT: Script = preload("res://scenes/ui/HudShortcutIcon.gd")
|
||||
|
||||
var _usernameLabel: Label
|
||||
var _walletLabel: Label
|
||||
var _avatarLabel: Label
|
||||
var _avatarPanel: PanelContainer
|
||||
var _walletRefreshTimer: Timer
|
||||
|
||||
var _currentUsername: String = "玩家"
|
||||
var _walletBalance: int = 0
|
||||
var _walletLoaded: bool = false
|
||||
var _walletLoading: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
_build_wallet_refresh_timer()
|
||||
_subscribe_to_events()
|
||||
_load_current_user()
|
||||
_refresh_wallet()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
|
||||
eventSystem.call("disconnect_event", EventNames.AUTH_LOGIN_SUCCESS, _on_auth_login_success, self)
|
||||
eventSystem.call("disconnect_event", EventNames.AUTH_REGISTER_SUCCESS, _on_auth_register_success, self)
|
||||
eventSystem.call("disconnect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self)
|
||||
eventSystem.call("disconnect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self)
|
||||
eventSystem.call("disconnect_event", EventNames.MALL_PURCHASE_SUCCEEDED, _on_wallet_mutation_event, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_wallet_mutation_event, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_wallet_mutation_event, self)
|
||||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||||
if playerStateManager != null and playerStateManager.has_signal("wallet_changed"):
|
||||
var walletCallback := Callable(self, "_on_player_wallet_changed")
|
||||
if playerStateManager.is_connected("wallet_changed", walletCallback):
|
||||
playerStateManager.disconnect("wallet_changed", walletCallback)
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager != null and authManager.has_signal("auth_state_changed"):
|
||||
var callback := Callable(self, "_on_auth_state_changed")
|
||||
if authManager.is_connected("auth_state_changed", callback):
|
||||
authManager.disconnect("auth_state_changed", callback)
|
||||
|
||||
func _build_ui() -> void:
|
||||
var rootRow := HBoxContainer.new()
|
||||
rootRow.name = "HudRoot"
|
||||
rootRow.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
rootRow.offset_left = -HUD_TOTAL_WIDTH - HUD_MARGIN.x
|
||||
rootRow.offset_top = HUD_MARGIN.y
|
||||
rootRow.offset_right = -HUD_MARGIN.x
|
||||
rootRow.add_theme_constant_override("separation", HUD_SEPARATION)
|
||||
add_child(rootRow)
|
||||
|
||||
rootRow.add_child(_build_shortcut_bar())
|
||||
rootRow.add_child(_build_player_avatar_button())
|
||||
|
||||
func _build_shortcut_bar() -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = SHORTCUT_BAR_SIZE
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
panel.add_theme_stylebox_override("panel", _create_panel_style(32))
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
panel.add_child(margin)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
margin.add_child(row)
|
||||
|
||||
row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed))
|
||||
row.add_child(_create_shortcut_button("task", "任务", _on_task_pressed))
|
||||
row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed))
|
||||
row.add_child(_create_shortcut_button("friends", "好友", _on_friends_pressed))
|
||||
row.add_child(_create_shortcut_button("settings", "设置", _on_settings_pressed))
|
||||
|
||||
return panel
|
||||
|
||||
func _build_player_avatar_button() -> Button:
|
||||
var button := Button.new()
|
||||
button.custom_minimum_size = PLAYER_PROFILE_BUTTON_SIZE
|
||||
button.tooltip_text = "角色设置"
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.text = ""
|
||||
button.add_theme_stylebox_override("normal", _create_avatar_button_style(Color(1, 1, 1, 0.94), 39))
|
||||
button.add_theme_stylebox_override("hover", _create_avatar_button_style(Color(0.918, 0.961, 0.992, 0.98), 39))
|
||||
button.add_theme_stylebox_override("pressed", _create_avatar_button_style(Color(0.858, 0.925, 0.980, 1.0), 39))
|
||||
button.add_theme_stylebox_override("focus", _create_empty_style())
|
||||
button.pressed.connect(_on_avatar_pressed)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 10)
|
||||
margin.add_theme_constant_override("margin_top", 10)
|
||||
margin.add_theme_constant_override("margin_right", 14)
|
||||
margin.add_theme_constant_override("margin_bottom", 10)
|
||||
button.add_child(margin)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
margin.add_child(row)
|
||||
|
||||
var avatar := PanelContainer.new()
|
||||
_avatarPanel = avatar
|
||||
avatar.custom_minimum_size = PLAYER_AVATAR_SIZE
|
||||
avatar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
avatar.clip_contents = true
|
||||
row.add_child(avatar)
|
||||
|
||||
_avatarLabel = Label.new()
|
||||
_avatarLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_avatarLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_avatarLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_avatarLabel.add_theme_color_override("font_color", Color.WHITE)
|
||||
_avatarLabel.add_theme_font_size_override("font_size", 22)
|
||||
avatar.add_child(_avatarLabel)
|
||||
_apply_current_avatar()
|
||||
|
||||
_usernameLabel = Label.new()
|
||||
_usernameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_usernameLabel.text = _currentUsername
|
||||
_usernameLabel.custom_minimum_size = Vector2(96, 0)
|
||||
_usernameLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_usernameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_usernameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_usernameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
_usernameLabel.add_theme_font_size_override("font_size", 18)
|
||||
|
||||
var info := VBoxContainer.new()
|
||||
info.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
info.add_theme_constant_override("separation", 2)
|
||||
row.add_child(info)
|
||||
info.add_child(_usernameLabel)
|
||||
|
||||
_walletLabel = Label.new()
|
||||
_walletLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_walletLabel.text = "鲸币 --"
|
||||
_walletLabel.custom_minimum_size = Vector2(112, 0)
|
||||
_walletLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_walletLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_walletLabel.add_theme_color_override("font_color", ACCENT_COLOR)
|
||||
_walletLabel.add_theme_font_size_override("font_size", 14)
|
||||
info.add_child(_walletLabel)
|
||||
|
||||
return button
|
||||
|
||||
func _create_shortcut_button(iconName: String, text: String, callback: Callable) -> Button:
|
||||
var button := Button.new()
|
||||
button.custom_minimum_size = Vector2(56, 60)
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.text = ""
|
||||
button.add_theme_stylebox_override("normal", _create_empty_style())
|
||||
button.add_theme_stylebox_override("hover", _create_pill_style(Color(0.918, 0.961, 0.992, 0.86), 18))
|
||||
button.add_theme_stylebox_override("pressed", _create_pill_style(Color(0.858, 0.925, 0.980, 0.98), 18))
|
||||
button.add_theme_stylebox_override("focus", _create_empty_style())
|
||||
button.pressed.connect(callback)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
content.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
content.add_theme_constant_override("separation", 1)
|
||||
button.add_child(content)
|
||||
|
||||
var icon := HUD_SHORTCUT_ICON_SCRIPT.new() as Control
|
||||
icon.set("iconName", iconName)
|
||||
icon.custom_minimum_size = Vector2(31, 31)
|
||||
content.add_child(icon)
|
||||
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.text = text
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
label.add_theme_font_size_override("font_size", 14)
|
||||
content.add_child(label)
|
||||
|
||||
button.mouse_entered.connect(func() -> void:
|
||||
if is_instance_valid(label):
|
||||
label.add_theme_color_override("font_color", ACCENT_COLOR)
|
||||
)
|
||||
button.mouse_exited.connect(func() -> void:
|
||||
if is_instance_valid(label):
|
||||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
)
|
||||
return button
|
||||
|
||||
func _subscribe_to_events() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("PlayerHud: EventSystem autoload is not available.")
|
||||
return
|
||||
|
||||
eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
|
||||
eventSystem.call("connect_event", EventNames.AUTH_LOGIN_SUCCESS, _on_auth_login_success, self)
|
||||
eventSystem.call("connect_event", EventNames.AUTH_REGISTER_SUCCESS, _on_auth_register_success, self)
|
||||
eventSystem.call("connect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self)
|
||||
eventSystem.call("connect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self)
|
||||
eventSystem.call("connect_event", EventNames.MALL_PURCHASE_SUCCEEDED, _on_wallet_mutation_event, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_wallet_mutation_event, self)
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_wallet_mutation_event, self)
|
||||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||||
if playerStateManager != null and playerStateManager.has_signal("wallet_changed"):
|
||||
var walletCallback := Callable(self, "_on_player_wallet_changed")
|
||||
if not playerStateManager.is_connected("wallet_changed", walletCallback):
|
||||
playerStateManager.connect("wallet_changed", walletCallback)
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager != null and authManager.has_signal("auth_state_changed"):
|
||||
var callback := Callable(self, "_on_auth_state_changed")
|
||||
if not authManager.is_connected("auth_state_changed", callback):
|
||||
authManager.connect("auth_state_changed", callback)
|
||||
|
||||
func _load_current_user() -> void:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager != null and authManager.has_method("get_current_username"):
|
||||
var username := str(authManager.call("get_current_username")).strip_edges()
|
||||
if not username.is_empty():
|
||||
_set_username(username)
|
||||
|
||||
func _on_chat_login_success(data: Dictionary) -> void:
|
||||
var username := str(data.get("username", "")).strip_edges()
|
||||
if not username.is_empty():
|
||||
_set_username(username)
|
||||
|
||||
func _on_auth_login_success(data: Dictionary) -> void:
|
||||
_apply_auth_user_payload(data)
|
||||
|
||||
func _on_auth_register_success(data: Dictionary) -> void:
|
||||
_apply_auth_user_payload(data)
|
||||
|
||||
func _on_auth_state_changed(isAuthenticated: bool, user: Dictionary) -> void:
|
||||
if not isAuthenticated:
|
||||
_set_username("玩家")
|
||||
_reset_wallet()
|
||||
return
|
||||
_apply_auth_user_payload({"user": user})
|
||||
_refresh_wallet()
|
||||
|
||||
func _apply_auth_user_payload(data: Dictionary) -> void:
|
||||
var userVariant: Variant = data.get("user", {})
|
||||
if userVariant is Dictionary:
|
||||
var username := str((userVariant as Dictionary).get("username", "")).strip_edges()
|
||||
if not username.is_empty():
|
||||
_set_username(username)
|
||||
|
||||
func _on_auth_logout(_data: Variant = null) -> void:
|
||||
_set_username("玩家")
|
||||
_reset_wallet()
|
||||
|
||||
func _on_appearance_avatar_changed(_data: Dictionary) -> void:
|
||||
_apply_current_avatar()
|
||||
|
||||
func _set_username(username: String) -> void:
|
||||
_currentUsername = username.strip_edges()
|
||||
if _currentUsername.is_empty():
|
||||
_currentUsername = "玩家"
|
||||
if is_instance_valid(_usernameLabel):
|
||||
_usernameLabel.text = _currentUsername
|
||||
_apply_current_avatar()
|
||||
|
||||
func _on_wallet_mutation_event(data: Dictionary) -> void:
|
||||
var balanceVariant: Variant = data.get("balance", null)
|
||||
if balanceVariant != null and (balanceVariant is int or balanceVariant is float):
|
||||
_apply_wallet_balance(int(balanceVariant))
|
||||
return
|
||||
_refresh_wallet()
|
||||
|
||||
func _build_wallet_refresh_timer() -> void:
|
||||
_walletRefreshTimer = Timer.new()
|
||||
_walletRefreshTimer.name = "HudWalletRefreshTimer"
|
||||
_walletRefreshTimer.wait_time = WALLET_REFRESH_INTERVAL
|
||||
_walletRefreshTimer.autostart = true
|
||||
_walletRefreshTimer.timeout.connect(_on_wallet_refresh_timer_timeout)
|
||||
add_child(_walletRefreshTimer)
|
||||
|
||||
func _on_wallet_refresh_timer_timeout() -> void:
|
||||
_refresh_wallet()
|
||||
|
||||
func _refresh_wallet() -> void:
|
||||
if not _is_wallet_available():
|
||||
_reset_wallet()
|
||||
return
|
||||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||||
if playerStateManager == null:
|
||||
_reset_wallet()
|
||||
return
|
||||
if playerStateManager.has_method("get_wallet"):
|
||||
var walletVariant: Variant = playerStateManager.call("get_wallet")
|
||||
if walletVariant is Dictionary and not (walletVariant as Dictionary).is_empty():
|
||||
_on_player_wallet_changed(walletVariant as Dictionary)
|
||||
return
|
||||
if _walletLoading:
|
||||
return
|
||||
_walletLoading = true
|
||||
_update_wallet_label()
|
||||
if playerStateManager.has_method("refresh_snapshot"):
|
||||
playerStateManager.call("refresh_snapshot")
|
||||
|
||||
func _on_player_wallet_changed(wallet: Dictionary) -> void:
|
||||
if wallet.is_empty():
|
||||
_reset_wallet()
|
||||
return
|
||||
_apply_wallet_balance(int(wallet.get("balance", 0)))
|
||||
|
||||
func _apply_wallet_balance(balance: int) -> void:
|
||||
_walletBalance = max(0, balance)
|
||||
_walletLoaded = true
|
||||
_walletLoading = false
|
||||
_update_wallet_label()
|
||||
|
||||
func _reset_wallet() -> void:
|
||||
_walletBalance = 0
|
||||
_walletLoaded = false
|
||||
_walletLoading = false
|
||||
_update_wallet_label()
|
||||
|
||||
func _update_wallet_label(overrideText: String = "") -> void:
|
||||
if not is_instance_valid(_walletLabel):
|
||||
return
|
||||
if not overrideText.is_empty():
|
||||
_walletLabel.text = "鲸币 %s" % overrideText
|
||||
_walletLabel.tooltip_text = "鲸币余额"
|
||||
return
|
||||
if _walletLoading:
|
||||
_walletLabel.text = "鲸币 读取中"
|
||||
elif _walletLoaded:
|
||||
_walletLabel.text = "鲸币 %s" % _format_number(_walletBalance)
|
||||
else:
|
||||
_walletLabel.text = "鲸币 --"
|
||||
_walletLabel.tooltip_text = "鲸币余额"
|
||||
|
||||
func _format_number(value: int) -> String:
|
||||
var text := str(value)
|
||||
var result := ""
|
||||
var count := 0
|
||||
for i in range(text.length() - 1, -1, -1):
|
||||
if count > 0 and count % 3 == 0:
|
||||
result = "," + result
|
||||
result = text[i] + result
|
||||
count += 1
|
||||
return result
|
||||
|
||||
func _is_wallet_available() -> bool:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated")) and authManager.has_method("get_access_token")
|
||||
|
||||
func _on_map_pressed() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_MAP_TOGGLE, {})
|
||||
|
||||
func _on_task_pressed() -> void:
|
||||
_emit_status_message("任务入口稍后接入")
|
||||
|
||||
func _on_backpack_pressed() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_BACKPACK_TOGGLE, {})
|
||||
|
||||
func _on_friends_pressed() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_FRIEND_LIST_TOGGLE, {})
|
||||
|
||||
func _on_settings_pressed() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {})
|
||||
|
||||
func _on_avatar_pressed() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {})
|
||||
|
||||
func _emit_status_message(message: String) -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.CHAT_MESSAGE_RECEIVED, {
|
||||
"from_user": "系统",
|
||||
"content": message,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"is_self": false,
|
||||
"scope": "system",
|
||||
"tab": "world"
|
||||
})
|
||||
|
||||
func _create_panel_style(radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1, 1, 1, 0.925)
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.15)
|
||||
style.shadow_size = 18
|
||||
style.shadow_offset = Vector2(0, 7)
|
||||
return style
|
||||
|
||||
func _create_avatar_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.361, 0.690, 0.914)
|
||||
style.corner_radius_top_left = 32
|
||||
style.corner_radius_top_right = 32
|
||||
style.corner_radius_bottom_left = 32
|
||||
style.corner_radius_bottom_right = 32
|
||||
return style
|
||||
|
||||
func _apply_current_avatar() -> void:
|
||||
if not is_instance_valid(_avatarPanel) or not is_instance_valid(_avatarLabel):
|
||||
return
|
||||
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", _avatarPanel, _avatarLabel, "", _currentUsername)
|
||||
_avatarPanel.add_theme_stylebox_override("panel", _create_round_avatar_style(_avatarPanel))
|
||||
return
|
||||
_avatarPanel.add_theme_stylebox_override("panel", _create_avatar_style())
|
||||
_avatarLabel.visible = true
|
||||
_avatarLabel.text = _currentUsername.substr(0, 1).to_upper()
|
||||
|
||||
func _create_avatar_button_style(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := _create_pill_style(color, radius)
|
||||
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.14)
|
||||
style.shadow_size = 16
|
||||
style.shadow_offset = Vector2(0, 6)
|
||||
return style
|
||||
|
||||
func _create_round_avatar_style(panel: PanelContainer) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.361, 0.690, 0.914)
|
||||
var radius := int(round(minf(panel.custom_minimum_size.x, panel.custom_minimum_size.y) * 0.5))
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(1, 1, 1, 0.78)
|
||||
return style
|
||||
|
||||
func _create_pill_style(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 5
|
||||
style.content_margin_bottom = 5
|
||||
return style
|
||||
|
||||
func _create_empty_style() -> StyleBoxEmpty:
|
||||
return StyleBoxEmpty.new()
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
1
scenes/ui/PlayerHud.gd.uid
Normal file
1
scenes/ui/PlayerHud.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgq6k8oxurhrr
|
||||
13
scenes/ui/PlayerHud.tscn
Normal file
13
scenes/ui/PlayerHud.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/PlayerHud.gd" id="1"]
|
||||
|
||||
[node name="PlayerHud" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
1315
scenes/ui/SettingsPanel.gd
Normal file
1315
scenes/ui/SettingsPanel.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scenes/ui/SettingsPanel.gd.uid
Normal file
1
scenes/ui/SettingsPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3vltb77vwxy
|
||||
13
scenes/ui/SettingsPanel.tscn
Normal file
13
scenes/ui/SettingsPanel.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/SettingsPanel.gd" id="1_settings"]
|
||||
|
||||
[node name="SettingsPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_settings")
|
||||
208
scenes/ui/SettingsPanelIcon.gd
Normal file
208
scenes/ui/SettingsPanelIcon.gd
Normal file
@@ -0,0 +1,208 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# SettingsPanelIcon.gd - 设置面板细线图标
|
||||
# ============================================================================
|
||||
# 用代码绘制设置面板内的小图标,保持和右上角 HUD 一致的轻线条风格。
|
||||
# ============================================================================
|
||||
|
||||
const BASE_SIZE: float = 32.0
|
||||
const INACTIVE_COLOR: Color = Color(0.356, 0.525, 0.690, 0.86)
|
||||
const ACTIVE_COLOR: Color = Color(0.259, 0.627, 0.914, 1.0)
|
||||
const MUTED_COLOR: Color = Color(0.650, 0.745, 0.827, 0.72)
|
||||
const LINE_WIDTH: float = 1.25
|
||||
const DETAIL_WIDTH: float = 1.05
|
||||
|
||||
@export var iconName: String = "basic"
|
||||
@export var active: bool = false
|
||||
|
||||
var _iconScale: float = 1.0
|
||||
var _iconOffset: Vector2 = Vector2.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func set_active(value: bool) -> void:
|
||||
active = value
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
_iconScale = min(size.x, size.y) / BASE_SIZE
|
||||
_iconOffset = (size - Vector2(BASE_SIZE, BASE_SIZE) * _iconScale) * 0.5
|
||||
|
||||
match iconName:
|
||||
"basic":
|
||||
_draw_basic()
|
||||
"audio":
|
||||
_draw_audio()
|
||||
"chat":
|
||||
_draw_chat()
|
||||
"controls":
|
||||
_draw_controls()
|
||||
"account":
|
||||
_draw_account()
|
||||
"volume":
|
||||
_draw_volume()
|
||||
"music":
|
||||
_draw_music()
|
||||
"effects":
|
||||
_draw_effects()
|
||||
"window":
|
||||
_draw_window()
|
||||
"eye":
|
||||
_draw_eye()
|
||||
"keyboard":
|
||||
_draw_keyboard()
|
||||
"whale":
|
||||
_draw_whale()
|
||||
_:
|
||||
_draw_basic()
|
||||
|
||||
func _draw_basic() -> void:
|
||||
_draw_polyline([
|
||||
Vector2(7.0, 17.0),
|
||||
Vector2(16.0, 8.6),
|
||||
Vector2(25.0, 17.0)
|
||||
])
|
||||
_draw_round_rect(Rect2(9.6, 16.3, 12.8, 9.0), 1.8)
|
||||
_draw_line(Vector2(16.0, 20.4), Vector2(16.0, 25.0), DETAIL_WIDTH)
|
||||
|
||||
func _draw_audio() -> void:
|
||||
_draw_music()
|
||||
_draw_line(Vector2(21.0, 8.0), Vector2(24.4, 11.4), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(24.4, 11.4), Vector2(21.0, 14.8), DETAIL_WIDTH)
|
||||
|
||||
func _draw_chat() -> void:
|
||||
_draw_round_rect(Rect2(6.5, 8.5, 19.0, 14.0), 4.0)
|
||||
_draw_polyline([
|
||||
Vector2(12.0, 22.0),
|
||||
Vector2(10.0, 26.0),
|
||||
Vector2(16.0, 22.5)
|
||||
], DETAIL_WIDTH)
|
||||
_draw_line(Vector2(11.0, 14.2), Vector2(21.0, 14.2), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(11.0, 18.0), Vector2(18.4, 18.0), DETAIL_WIDTH)
|
||||
|
||||
func _draw_controls() -> void:
|
||||
_draw_round_rect(Rect2(5.7, 12.0, 20.6, 10.8), 4.8)
|
||||
_draw_line(Vector2(10.5, 15.2), Vector2(10.5, 19.4), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(8.4, 17.3), Vector2(12.6, 17.3), DETAIL_WIDTH)
|
||||
draw_circle(_p(Vector2(19.6, 16.0)), 1.15 * _iconScale, _color())
|
||||
draw_circle(_p(Vector2(22.0, 19.0)), 1.15 * _iconScale, _color())
|
||||
|
||||
func _draw_account() -> void:
|
||||
_draw_arc(Vector2(16.0, 12.0), 4.4, 0.0, TAU, 32, LINE_WIDTH)
|
||||
_draw_arc_poly(Vector2(16.0, 28.5), 10.2, PI + 0.20, TAU - 0.20, 28)
|
||||
|
||||
func _draw_volume() -> void:
|
||||
_draw_polyline([
|
||||
Vector2(6.5, 14.0),
|
||||
Vector2(11.0, 14.0),
|
||||
Vector2(17.2, 9.0),
|
||||
Vector2(17.2, 23.0),
|
||||
Vector2(11.0, 18.0),
|
||||
Vector2(6.5, 18.0),
|
||||
Vector2(6.5, 14.0)
|
||||
])
|
||||
_draw_arc(Vector2(18.0, 16.0), 5.6, -0.62, 0.62, 14, DETAIL_WIDTH)
|
||||
_draw_arc(Vector2(18.0, 16.0), 8.8, -0.56, 0.56, 16, DETAIL_WIDTH)
|
||||
|
||||
func _draw_music() -> void:
|
||||
_draw_line(Vector2(12.0, 9.0), Vector2(12.0, 22.0), LINE_WIDTH)
|
||||
_draw_line(Vector2(22.0, 7.0), Vector2(22.0, 20.2), LINE_WIDTH)
|
||||
_draw_line(Vector2(12.0, 9.0), Vector2(22.0, 7.0), LINE_WIDTH)
|
||||
_draw_line(Vector2(12.0, 12.5), Vector2(22.0, 10.5), DETAIL_WIDTH)
|
||||
_draw_arc(Vector2(9.6, 23.0), 3.0, 0.0, TAU, 24, LINE_WIDTH)
|
||||
_draw_arc(Vector2(19.6, 21.0), 3.0, 0.0, TAU, 24, LINE_WIDTH)
|
||||
|
||||
func _draw_effects() -> void:
|
||||
_draw_polyline([
|
||||
Vector2(16.0, 6.5),
|
||||
Vector2(18.4, 13.2),
|
||||
Vector2(25.5, 13.4),
|
||||
Vector2(19.8, 17.7),
|
||||
Vector2(21.8, 24.6),
|
||||
Vector2(16.0, 20.4),
|
||||
Vector2(10.2, 24.6),
|
||||
Vector2(12.2, 17.7),
|
||||
Vector2(6.5, 13.4),
|
||||
Vector2(13.6, 13.2),
|
||||
Vector2(16.0, 6.5)
|
||||
])
|
||||
|
||||
func _draw_window() -> void:
|
||||
_draw_round_rect(Rect2(6.0, 8.0, 20.0, 16.0), 2.3)
|
||||
_draw_line(Vector2(6.6, 12.2), Vector2(25.4, 12.2), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(10.0, 16.2), Vector2(16.0, 16.2), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(10.0, 19.6), Vector2(20.8, 19.6), DETAIL_WIDTH)
|
||||
|
||||
func _draw_eye() -> void:
|
||||
_draw_arc_poly(Vector2(16.0, 17.0), 10.2, PI + 0.34, TAU - 0.34, 28)
|
||||
_draw_arc_poly(Vector2(16.0, 10.8), 10.2, 0.34, PI - 0.34, 28)
|
||||
_draw_arc(Vector2(16.0, 14.0), 2.8, 0.0, TAU, 24, DETAIL_WIDTH)
|
||||
|
||||
func _draw_keyboard() -> void:
|
||||
_draw_round_rect(Rect2(5.5, 9.0, 21.0, 14.0), 2.5)
|
||||
for y in [13.0, 17.0]:
|
||||
_draw_line(Vector2(9.0, y), Vector2(23.0, y), DETAIL_WIDTH)
|
||||
_draw_line(Vector2(11.0, 20.3), Vector2(21.0, 20.3), DETAIL_WIDTH)
|
||||
|
||||
func _draw_whale() -> void:
|
||||
var bodyColor := Color(0.424, 0.713, 0.918, 1.0)
|
||||
var finColor := Color(0.306, 0.612, 0.847, 1.0)
|
||||
var white := Color(1.0, 1.0, 1.0, 0.95)
|
||||
draw_arc(_p(Vector2(16.0, 18.0)), 9.0 * _iconScale, PI * 0.04, PI * 1.08, 40, bodyColor, 5.5 * _iconScale, true)
|
||||
draw_circle(_p(Vector2(16.0, 18.0)), 6.8 * _iconScale, bodyColor)
|
||||
draw_circle(_p(Vector2(18.2, 15.4)), 0.9 * _iconScale, Color(0.160, 0.286, 0.420, 0.9))
|
||||
_draw_line_colored(Vector2(8.3, 18.2), Vector2(4.5, 14.8), finColor, 1.8)
|
||||
_draw_line_colored(Vector2(8.4, 18.4), Vector2(4.6, 21.4), finColor, 1.8)
|
||||
_draw_arc_colored(Vector2(15.0, 8.4), 4.0, PI * 1.05, PI * 1.80, 14, finColor, 1.5)
|
||||
_draw_arc_colored(Vector2(18.0, 8.4), 4.0, PI * 1.20, PI * 1.95, 14, finColor, 1.5)
|
||||
draw_circle(_p(Vector2(20.8, 22.0)), 1.1 * _iconScale, white)
|
||||
draw_circle(_p(Vector2(12.7, 22.2)), 1.0 * _iconScale, white)
|
||||
|
||||
func _draw_round_rect(rect: Rect2, radius: float, width: float = LINE_WIDTH) -> void:
|
||||
var left := rect.position.x
|
||||
var top := rect.position.y
|
||||
var right := rect.end.x
|
||||
var bottom := rect.end.y
|
||||
_draw_line(Vector2(left + radius, top), Vector2(right - radius, top), width)
|
||||
_draw_line(Vector2(right, top + radius), Vector2(right, bottom - radius), width)
|
||||
_draw_line(Vector2(right - radius, bottom), Vector2(left + radius, bottom), width)
|
||||
_draw_line(Vector2(left, bottom - radius), Vector2(left, top + radius), width)
|
||||
_draw_arc(Vector2(left + radius, top + radius), radius, PI, PI * 1.5, 8, width)
|
||||
_draw_arc(Vector2(right - radius, top + radius), radius, PI * 1.5, TAU, 8, width)
|
||||
_draw_arc(Vector2(right - radius, bottom - radius), radius, 0.0, PI * 0.5, 8, width)
|
||||
_draw_arc(Vector2(left + radius, bottom - radius), radius, PI * 0.5, PI, 8, width)
|
||||
|
||||
func _draw_arc_poly(center: Vector2, radius: float, start: float, end: float, pointCount: int, width: float = LINE_WIDTH) -> void:
|
||||
var points: PackedVector2Array = []
|
||||
for i in range(pointCount + 1):
|
||||
var t := float(i) / float(pointCount)
|
||||
var angle := lerpf(start, end, t)
|
||||
points.append(_p(center + Vector2(cos(angle), sin(angle)) * radius))
|
||||
draw_polyline(points, _color(), width * _iconScale, true)
|
||||
|
||||
func _draw_polyline(points: Array[Vector2], width: float = LINE_WIDTH) -> void:
|
||||
var packed: PackedVector2Array = []
|
||||
for point in points:
|
||||
packed.append(_p(point))
|
||||
draw_polyline(packed, _color(), width * _iconScale, true)
|
||||
|
||||
func _draw_line(from: Vector2, to: Vector2, width: float) -> void:
|
||||
draw_line(_p(from), _p(to), _color(), width * _iconScale, true)
|
||||
|
||||
func _draw_line_colored(from: Vector2, to: Vector2, color: Color, width: float) -> void:
|
||||
draw_line(_p(from), _p(to), color, width * _iconScale, true)
|
||||
|
||||
func _draw_arc(center: Vector2, radius: float, startAngle: float, endAngle: float, pointCount: int, width: float) -> void:
|
||||
draw_arc(_p(center), radius * _iconScale, startAngle, endAngle, pointCount, _color(), width * _iconScale, true)
|
||||
|
||||
func _draw_arc_colored(center: Vector2, radius: float, startAngle: float, endAngle: float, pointCount: int, color: Color, width: float) -> void:
|
||||
draw_arc(_p(center), radius * _iconScale, startAngle, endAngle, pointCount, color, width * _iconScale, true)
|
||||
|
||||
func _p(point: Vector2) -> Vector2:
|
||||
return _iconOffset + point * _iconScale
|
||||
|
||||
func _color() -> Color:
|
||||
return ACTIVE_COLOR if active else INACTIVE_COLOR
|
||||
1
scenes/ui/SettingsPanelIcon.gd.uid
Normal file
1
scenes/ui/SettingsPanelIcon.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d0njk7403bexl
|
||||
83
scenes/ui/SettingsSlider.gd
Normal file
83
scenes/ui/SettingsSlider.gd
Normal file
@@ -0,0 +1,83 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# SettingsSlider.gd - 设置面板自绘滑条
|
||||
# ============================================================================
|
||||
# 使用白色圆形滑块和柔和轨道,贴近概念图里的轻量滑条样式。
|
||||
# ============================================================================
|
||||
|
||||
signal value_changed(value: float)
|
||||
|
||||
const TRACK_COLOR: Color = Color(0.842, 0.894, 0.936, 0.95)
|
||||
const FILL_COLOR: Color = Color(0.258824, 0.627451, 0.913725, 1.0)
|
||||
const KNOB_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0)
|
||||
const KNOB_SHADOW_COLOR: Color = Color(0.125, 0.282, 0.408, 0.16)
|
||||
|
||||
var value: float = 1.0
|
||||
var _dragging: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size = Vector2(360, 34)
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
|
||||
func set_value(newValue: float, emitSignal: bool = false) -> void:
|
||||
var clamped: float = clampf(newValue, 0.0, 1.0)
|
||||
if abs(value - clamped) < 0.001:
|
||||
return
|
||||
value = clamped
|
||||
queue_redraw()
|
||||
if emitSignal:
|
||||
value_changed.emit(value)
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
_dragging = mouseEvent.pressed
|
||||
if _dragging:
|
||||
_update_from_x(mouseEvent.position.x)
|
||||
accept_event()
|
||||
return
|
||||
|
||||
if event is InputEventMouseMotion and _dragging:
|
||||
var motionEvent := event as InputEventMouseMotion
|
||||
_update_from_x(motionEvent.position.x)
|
||||
accept_event()
|
||||
|
||||
func _draw() -> void:
|
||||
var trackHeight: float = 8.0
|
||||
var knobRadius: float = 12.0
|
||||
var left: float = knobRadius + 1.0
|
||||
var right: float = size.x - knobRadius - 1.0
|
||||
var centerY: float = size.y * 0.5
|
||||
var width: float = max(1.0, right - left)
|
||||
var knobX: float = left + width * value
|
||||
|
||||
var trackRect: Rect2 = Rect2(Vector2(left, centerY - trackHeight * 0.5), Vector2(width, trackHeight))
|
||||
draw_style_box(_round_box(TRACK_COLOR, int(trackHeight * 0.5)), trackRect)
|
||||
|
||||
var fillRect: Rect2 = Rect2(trackRect.position, Vector2(max(trackHeight, knobX - left), trackHeight))
|
||||
draw_style_box(_round_box(FILL_COLOR, int(trackHeight * 0.5)), fillRect)
|
||||
|
||||
var knobCenter: Vector2 = Vector2(knobX, centerY)
|
||||
draw_circle(knobCenter + Vector2(0, 1.4), knobRadius + 1.0, KNOB_SHADOW_COLOR)
|
||||
draw_circle(knobCenter, knobRadius, KNOB_COLOR)
|
||||
draw_arc(knobCenter, knobRadius, 0.0, TAU, 32, Color(0.790, 0.848, 0.902, 0.75), 1.0, true)
|
||||
|
||||
func _update_from_x(x: float) -> void:
|
||||
var knobRadius: float = 12.0
|
||||
var left: float = knobRadius + 1.0
|
||||
var right: float = size.x - knobRadius - 1.0
|
||||
var width: float = max(1.0, right - left)
|
||||
set_value((x - left) / width, true)
|
||||
|
||||
func _round_box(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
return style
|
||||
1
scenes/ui/SettingsSlider.gd.uid
Normal file
1
scenes/ui/SettingsSlider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jnvemq1xr6hr
|
||||
45
scenes/ui/SettingsToggle.gd
Normal file
45
scenes/ui/SettingsToggle.gd
Normal file
@@ -0,0 +1,45 @@
|
||||
extends Button
|
||||
|
||||
# ============================================================================
|
||||
# SettingsToggle.gd - 设置面板开关控件
|
||||
# ============================================================================
|
||||
# 轻量自绘开关,避免默认 CheckButton 的厚重视觉。
|
||||
# ============================================================================
|
||||
|
||||
const ON_COLOR: Color = Color(0.337, 0.690, 0.934, 1.0)
|
||||
const OFF_COLOR: Color = Color(0.805, 0.843, 0.882, 1.0)
|
||||
const KNOB_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0)
|
||||
const SHADOW_COLOR: Color = Color(0.114, 0.286, 0.420, 0.15)
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size = Vector2(74, 40)
|
||||
focus_mode = Control.FOCUS_NONE
|
||||
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
toggle_mode = true
|
||||
text = ""
|
||||
add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("hover", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("pressed", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
toggled.connect(func(_pressed: bool) -> void: queue_redraw())
|
||||
|
||||
func _draw() -> void:
|
||||
var trackRect: Rect2 = Rect2(Vector2(6, 7), Vector2(size.x - 12, size.y - 14))
|
||||
var radius: float = trackRect.size.y * 0.5
|
||||
var color: Color = ON_COLOR if button_pressed else OFF_COLOR
|
||||
draw_style_box(_create_round_box(color, int(radius)), trackRect)
|
||||
|
||||
var knobRadius: float = max(10.0, trackRect.size.y * 0.38)
|
||||
var x: float = trackRect.position.x + trackRect.size.x - radius if button_pressed else trackRect.position.x + radius
|
||||
var center: Vector2 = Vector2(x, trackRect.position.y + radius)
|
||||
draw_circle(center + Vector2(0, 1.2), knobRadius + 0.4, SHADOW_COLOR)
|
||||
draw_circle(center, knobRadius, KNOB_COLOR)
|
||||
|
||||
func _create_round_box(color: Color, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
return style
|
||||
1
scenes/ui/SettingsToggle.gd.uid
Normal file
1
scenes/ui/SettingsToggle.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bqxtvijb3kakm
|
||||
293
scenes/ui/WelcomeDialog.gd
Normal file
293
scenes/ui/WelcomeDialog.gd
Normal file
@@ -0,0 +1,293 @@
|
||||
extends CanvasLayer
|
||||
class_name WelcomeDialog
|
||||
|
||||
const GUIDE_PAGES: Array[Dictionary] = [
|
||||
{
|
||||
"text": "欢迎来到 [color=#3399ff]Datawhale Town[/color]!\n\n这里是开源学习者的家园。在这里,我们一同探索知识,分享成长。\n\n[center]WhaleTown[/center]",
|
||||
"image_path": "res://assets/maps/square/v1/props/bottom_entrance_left_task_props_v2_hd_clean.png",
|
||||
},
|
||||
{
|
||||
"text": "最新活动:\n\n- 镇长在公会大厅附近接待新伙伴。\n- 码头区域已经开放,可以和码头 NPC 交流。\n- 广场公告板后续会同步线上新闻动态。",
|
||||
"image_path": "res://assets/maps/square/v1/props/center_whale_fountain_v2_hd_clean.png",
|
||||
},
|
||||
{
|
||||
"text": "操作提示:\n\n- 按 [color=#ffaa00]E[/color] 键可以与 NPC、公告板和信息板互动。\n- 靠近目标后面向它,再按互动键。\n- 输入框获得焦点时,角色移动会暂停响应。",
|
||||
"image_path": "res://assets/maps/square/v1/props/bottom_entrance_right_service_props_v2_hd_clean.png",
|
||||
},
|
||||
]
|
||||
|
||||
@onready var panelContainer: PanelContainer = $CenterContainer/PanelContainer
|
||||
@onready var vboxContainer: VBoxContainer = $CenterContainer/PanelContainer/VBoxContainer
|
||||
@onready var titleLabel: Label = $CenterContainer/PanelContainer/VBoxContainer/Header/Title
|
||||
@onready var logoContainer: PanelContainer = $CenterContainer/PanelContainer/VBoxContainer/LogoContainer
|
||||
@onready var bodyText: Label = $CenterContainer/PanelContainer/VBoxContainer/BodyText
|
||||
@onready var actionContainer: CenterContainer = $CenterContainer/PanelContainer/VBoxContainer/ActionContainer
|
||||
|
||||
var _chatUi: Control
|
||||
var _chatUiPrevMouseFilter: Control.MouseFilter = Control.MOUSE_FILTER_STOP
|
||||
var _chatUiMouseDisabled: bool = false
|
||||
var _guideContentContainer: HBoxContainer
|
||||
var _guideImageRect: TextureRect
|
||||
var _guideImageLabel: Label
|
||||
var _guideContentLabel: RichTextLabel
|
||||
var _guideFooter: HBoxContainer
|
||||
var _guidePrevButton: Button
|
||||
var _guideNextButton: Button
|
||||
var _guideDotsContainer: HBoxContainer
|
||||
var _guideCurrentPage: int = 0
|
||||
var _guideTween: Tween
|
||||
|
||||
func _ready() -> void:
|
||||
_disableChatUiMouseInput()
|
||||
|
||||
var closeButton := find_child("CloseButton", true, false) as Button
|
||||
if closeButton != null:
|
||||
closeButton.pressed.connect(_onClosePressed)
|
||||
|
||||
var startButton := find_child("StartButton", true, false) as Button
|
||||
if startButton != null:
|
||||
startButton.pressed.connect(_onStartPressed)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_restoreChatUiMouseInput()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
queue_free()
|
||||
|
||||
func _onClosePressed() -> void:
|
||||
queue_free()
|
||||
|
||||
func _onStartPressed() -> void:
|
||||
_showGuide()
|
||||
|
||||
func _showGuide() -> void:
|
||||
panelContainer.custom_minimum_size = Vector2(720, 560)
|
||||
titleLabel.text = "新人引导手册"
|
||||
logoContainer.visible = false
|
||||
bodyText.visible = false
|
||||
actionContainer.visible = false
|
||||
|
||||
if _guideContentContainer == null:
|
||||
_buildGuideUi()
|
||||
|
||||
_guideContentContainer.visible = true
|
||||
_guideFooter.visible = true
|
||||
_guideCurrentPage = 0
|
||||
_setupGuideDots()
|
||||
_updateGuideUi(false)
|
||||
|
||||
func _buildGuideUi() -> void:
|
||||
_guideContentContainer = HBoxContainer.new()
|
||||
_guideContentContainer.name = "GuideContentContainer"
|
||||
_guideContentContainer.custom_minimum_size = Vector2(0, 360)
|
||||
_guideContentContainer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_guideContentContainer.add_theme_constant_override("separation", 22)
|
||||
vboxContainer.add_child(_guideContentContainer)
|
||||
|
||||
var imagePanel := PanelContainer.new()
|
||||
imagePanel.name = "ImagePanel"
|
||||
imagePanel.custom_minimum_size = Vector2(322, 0)
|
||||
imagePanel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
imagePanel.add_theme_stylebox_override("panel", _createImageStyle())
|
||||
_guideContentContainer.add_child(imagePanel)
|
||||
|
||||
_guideImageRect = TextureRect.new()
|
||||
_guideImageRect.name = "ImageRect"
|
||||
_guideImageRect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_guideImageRect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
imagePanel.add_child(_guideImageRect)
|
||||
|
||||
_guideImageLabel = Label.new()
|
||||
_guideImageLabel.name = "ImageLabel"
|
||||
_guideImageLabel.text = "暂无图片"
|
||||
_guideImageLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_guideImageLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_guideImageLabel.add_theme_color_override("font_color", Color(0.43, 0.48, 0.45, 1.0))
|
||||
_guideImageLabel.add_theme_font_size_override("font_size", 18)
|
||||
imagePanel.add_child(_guideImageLabel)
|
||||
|
||||
var textPanel := PanelContainer.new()
|
||||
textPanel.name = "TextPanel"
|
||||
textPanel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
textPanel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
textPanel.add_theme_stylebox_override("panel", _createTextStyle())
|
||||
_guideContentContainer.add_child(textPanel)
|
||||
|
||||
_guideContentLabel = RichTextLabel.new()
|
||||
_guideContentLabel.name = "ContentLabel"
|
||||
_guideContentLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_guideContentLabel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_guideContentLabel.bbcode_enabled = true
|
||||
_guideContentLabel.fit_content = true
|
||||
_guideContentLabel.scroll_active = false
|
||||
_guideContentLabel.add_theme_color_override("default_color", Color(0.18, 0.2, 0.18, 1.0))
|
||||
_guideContentLabel.add_theme_font_size_override("normal_font_size", 21)
|
||||
textPanel.add_child(_guideContentLabel)
|
||||
|
||||
_guideFooter = HBoxContainer.new()
|
||||
_guideFooter.name = "GuideFooter"
|
||||
_guideFooter.custom_minimum_size = Vector2(0, 58)
|
||||
_guideFooter.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
_guideFooter.add_theme_constant_override("separation", 22)
|
||||
vboxContainer.add_child(_guideFooter)
|
||||
|
||||
_guidePrevButton = _createGuideButton("<")
|
||||
_guidePrevButton.name = "PrevButton"
|
||||
_guidePrevButton.pressed.connect(_onGuidePrevPressed)
|
||||
_guideFooter.add_child(_guidePrevButton)
|
||||
|
||||
_guideDotsContainer = HBoxContainer.new()
|
||||
_guideDotsContainer.name = "DotsContainer"
|
||||
_guideDotsContainer.custom_minimum_size = Vector2(88, 0)
|
||||
_guideDotsContainer.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
_guideDotsContainer.add_theme_constant_override("separation", 10)
|
||||
_guideFooter.add_child(_guideDotsContainer)
|
||||
|
||||
_guideNextButton = _createGuideButton(">")
|
||||
_guideNextButton.name = "NextButton"
|
||||
_guideNextButton.pressed.connect(_onGuideNextPressed)
|
||||
_guideFooter.add_child(_guideNextButton)
|
||||
|
||||
func _createGuideButton(text: String) -> Button:
|
||||
var button := Button.new()
|
||||
button.custom_minimum_size = Vector2(56, 44)
|
||||
button.text = text
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_font_size_override("font_size", 22)
|
||||
button.add_theme_stylebox_override("normal", _createButtonStyle(Color(0.2, 0.43, 0.48, 1.0)))
|
||||
button.add_theme_stylebox_override("hover", _createButtonStyle(Color(0.28, 0.52, 0.55, 1.0)))
|
||||
button.add_theme_stylebox_override("pressed", _createButtonStyle(Color(0.28, 0.52, 0.55, 1.0)))
|
||||
button.add_theme_stylebox_override("disabled", _createButtonStyle(Color(0.72, 0.76, 0.72, 1.0)))
|
||||
return button
|
||||
|
||||
func _createImageStyle() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.content_margin_left = 10.0
|
||||
style.content_margin_top = 10.0
|
||||
style.content_margin_right = 10.0
|
||||
style.content_margin_bottom = 10.0
|
||||
style.bg_color = Color(0.875, 0.93, 0.92, 1.0)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.55, 0.68, 0.64, 1.0)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
return style
|
||||
|
||||
func _createTextStyle() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.content_margin_left = 20.0
|
||||
style.content_margin_top = 18.0
|
||||
style.content_margin_right = 20.0
|
||||
style.content_margin_bottom = 18.0
|
||||
style.bg_color = Color(1.0, 0.985, 0.935, 1.0)
|
||||
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.7, 0.55, 1.0)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
return style
|
||||
|
||||
func _createButtonStyle(color: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.content_margin_left = 16.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_right = 16.0
|
||||
style.content_margin_bottom = 8.0
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = 8
|
||||
style.corner_radius_top_right = 8
|
||||
style.corner_radius_bottom_right = 8
|
||||
style.corner_radius_bottom_left = 8
|
||||
return style
|
||||
|
||||
func _setupGuideDots() -> void:
|
||||
for child in _guideDotsContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for index in range(GUIDE_PAGES.size()):
|
||||
var dot := ColorRect.new()
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
_guideDotsContainer.add_child(dot)
|
||||
|
||||
func _updateGuideUi(animate: bool = true) -> void:
|
||||
_guidePrevButton.disabled = _guideCurrentPage == 0
|
||||
_guideNextButton.disabled = _guideCurrentPage == GUIDE_PAGES.size() - 1
|
||||
|
||||
var dots := _guideDotsContainer.get_children()
|
||||
for index in range(dots.size()):
|
||||
var dot := dots[index] as ColorRect
|
||||
if dot == null:
|
||||
continue
|
||||
if index == _guideCurrentPage:
|
||||
dot.color = Color(0.15, 0.36, 0.44, 1.0)
|
||||
dot.custom_minimum_size = Vector2(18, 12)
|
||||
else:
|
||||
dot.color = Color(0.72, 0.78, 0.72, 1.0)
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
|
||||
if animate:
|
||||
_animateGuideContentChange()
|
||||
else:
|
||||
_setGuideContentImmediate()
|
||||
|
||||
func _setGuideContentImmediate() -> void:
|
||||
var page := GUIDE_PAGES[_guideCurrentPage]
|
||||
_guideContentLabel.text = page.get("text", "") as String
|
||||
|
||||
var imagePath := page.get("image_path", "") as String
|
||||
if imagePath != "" and ResourceLoader.exists(imagePath):
|
||||
_guideImageRect.texture = load(imagePath) as Texture2D
|
||||
_guideImageLabel.visible = false
|
||||
else:
|
||||
_guideImageRect.texture = null
|
||||
_guideImageLabel.visible = true
|
||||
_guideImageLabel.text = "暂无图片"
|
||||
|
||||
func _animateGuideContentChange() -> void:
|
||||
if _guideTween != null and _guideTween.is_valid():
|
||||
_guideTween.kill()
|
||||
|
||||
_guideTween = create_tween()
|
||||
_guideTween.tween_property(_guideContentContainer, "modulate:a", 0.0, 0.15)
|
||||
_guideTween.tween_callback(_setGuideContentImmediate)
|
||||
_guideTween.tween_property(_guideContentContainer, "modulate:a", 1.0, 0.15)
|
||||
|
||||
func _onGuidePrevPressed() -> void:
|
||||
if _guideCurrentPage > 0:
|
||||
_guideCurrentPage -= 1
|
||||
_updateGuideUi()
|
||||
|
||||
func _onGuideNextPressed() -> void:
|
||||
if _guideCurrentPage < GUIDE_PAGES.size() - 1:
|
||||
_guideCurrentPage += 1
|
||||
_updateGuideUi()
|
||||
|
||||
func _disableChatUiMouseInput() -> void:
|
||||
var currentScene := get_tree().current_scene
|
||||
if currentScene != null:
|
||||
_chatUi = currentScene.get_node_or_null("UILayer/ChatUI") as Control
|
||||
|
||||
if _chatUi == null:
|
||||
_chatUi = get_tree().root.find_child("ChatUI", true, false) as Control
|
||||
|
||||
if _chatUi != null:
|
||||
_chatUiPrevMouseFilter = _chatUi.mouse_filter
|
||||
_chatUi.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_chatUiMouseDisabled = true
|
||||
|
||||
func _restoreChatUiMouseInput() -> void:
|
||||
if _chatUiMouseDisabled and is_instance_valid(_chatUi):
|
||||
_chatUi.mouse_filter = _chatUiPrevMouseFilter
|
||||
|
||||
_chatUiMouseDisabled = false
|
||||
_chatUi = null
|
||||
1
scenes/ui/WelcomeDialog.gd.uid
Normal file
1
scenes/ui/WelcomeDialog.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://s86vkjh20re5
|
||||
8
scenes/ui/datawhale_honor_ranking_panel.tscn
Normal file
8
scenes/ui/datawhale_honor_ranking_panel.tscn
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/DatawhaleHonorRankingPanel.gd" id="1_script"]
|
||||
|
||||
[node name="DatawhaleHonorRankingPanel" type="CanvasLayer"]
|
||||
process_mode = 3
|
||||
layer = 60
|
||||
script = ExtResource("1_script")
|
||||
177
scenes/ui/mall/MallItemCard.gd
Normal file
177
scenes/ui/mall/MallItemCard.gd
Normal file
@@ -0,0 +1,177 @@
|
||||
class_name MallItemCard
|
||||
extends Button
|
||||
|
||||
# ============================================================================
|
||||
# MallItemCard.gd - 商城商品卡片
|
||||
# ============================================================================
|
||||
# 展示商品槽位、状态、价格和选中态,由 MallPanel 统一驱动。
|
||||
# ============================================================================
|
||||
|
||||
signal item_selected(item: Dictionary)
|
||||
|
||||
const TEXT_COLOR: Color = Color(0.111, 0.231, 0.380)
|
||||
const MUTED_COLOR: Color = Color(0.486, 0.584, 0.694)
|
||||
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
|
||||
const LOCKED_COLOR: Color = Color(0.455, 0.510, 0.584)
|
||||
const COIN_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/branding_whale_coin.png")
|
||||
const CATALOG_SCRIPT: Script = preload("res://Config/MallCatalog.gd")
|
||||
|
||||
var itemData: Dictionary = {}
|
||||
var isSelected: bool = false
|
||||
|
||||
var _background: PanelContainer
|
||||
var _icon: TextureRect
|
||||
var _nameLabel: Label
|
||||
var _statusLabel: Label
|
||||
var _priceLabel: Label
|
||||
var _lockedOverlay: ColorRect
|
||||
|
||||
func _ready() -> void:
|
||||
focus_mode = Control.FOCUS_NONE
|
||||
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
custom_minimum_size = Vector2(218, 288)
|
||||
add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("hover", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("pressed", StyleBoxEmpty.new())
|
||||
add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
_build_ui()
|
||||
pressed.connect(_on_pressed)
|
||||
|
||||
func setup(data: Dictionary, selected: bool) -> void:
|
||||
itemData = data.duplicate(true)
|
||||
isSelected = selected
|
||||
if not is_inside_tree():
|
||||
return
|
||||
_render()
|
||||
|
||||
func set_selected(selected: bool) -> void:
|
||||
isSelected = selected
|
||||
if is_inside_tree():
|
||||
_render()
|
||||
|
||||
func _build_ui() -> void:
|
||||
_background = PanelContainer.new()
|
||||
_background.name = "background"
|
||||
_background.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_background)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.name = "contentMargin"
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 14)
|
||||
margin.add_theme_constant_override("margin_top", 14)
|
||||
margin.add_theme_constant_override("margin_right", 14)
|
||||
margin.add_theme_constant_override("margin_bottom", 14)
|
||||
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
content.add_theme_constant_override("separation", 7)
|
||||
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
margin.add_child(content)
|
||||
|
||||
_statusLabel = Label.new()
|
||||
_statusLabel.custom_minimum_size = Vector2(0, 24)
|
||||
_statusLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_statusLabel.add_theme_font_size_override("font_size", 15)
|
||||
_statusLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
content.add_child(_statusLabel)
|
||||
|
||||
_icon = TextureRect.new()
|
||||
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
_icon.custom_minimum_size = Vector2(152, 134)
|
||||
_icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(_icon)
|
||||
|
||||
_nameLabel = Label.new()
|
||||
_nameLabel.custom_minimum_size = Vector2(0, 48)
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_nameLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_nameLabel.add_theme_font_size_override("font_size", 19)
|
||||
_nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
content.add_child(_nameLabel)
|
||||
|
||||
var priceRow := HBoxContainer.new()
|
||||
priceRow.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
priceRow.add_theme_constant_override("separation", 4)
|
||||
priceRow.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(priceRow)
|
||||
|
||||
var coin := TextureRect.new()
|
||||
coin.texture = COIN_TEXTURE
|
||||
coin.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
coin.custom_minimum_size = Vector2(28, 28)
|
||||
coin.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
coin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
priceRow.add_child(coin)
|
||||
|
||||
_priceLabel = Label.new()
|
||||
_priceLabel.add_theme_font_size_override("font_size", 18)
|
||||
_priceLabel.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
priceRow.add_child(_priceLabel)
|
||||
|
||||
_lockedOverlay = ColorRect.new()
|
||||
_lockedOverlay.name = "lockedOverlay"
|
||||
_lockedOverlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_lockedOverlay.color = Color(0.350, 0.420, 0.510, 0.18)
|
||||
_lockedOverlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_lockedOverlay)
|
||||
|
||||
_render()
|
||||
|
||||
func _render() -> void:
|
||||
_background.add_theme_stylebox_override("panel", _create_card_style(isSelected))
|
||||
var status := str(itemData.get("status", CATALOG_SCRIPT.STATUS_COMING_SOON))
|
||||
var iconPath := str(itemData.get("icon", ""))
|
||||
_icon.texture = _load_texture(iconPath)
|
||||
_nameLabel.text = str(itemData.get("name", "商品槽位"))
|
||||
_statusLabel.text = CATALOG_SCRIPT.get_status_label(status)
|
||||
_priceLabel.text = _format_price(itemData)
|
||||
_lockedOverlay.visible = status == CATALOG_SCRIPT.STATUS_COMING_SOON or status == CATALOG_SCRIPT.STATUS_LOCKED
|
||||
_statusLabel.add_theme_color_override("font_color", ACCENT_COLOR if status == CATALOG_SCRIPT.STATUS_AVAILABLE else LOCKED_COLOR)
|
||||
|
||||
func _create_card_style(selected: bool) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.982, 0.995, 1.0, 1.0)
|
||||
style.corner_radius_top_left = 18
|
||||
style.corner_radius_top_right = 18
|
||||
style.corner_radius_bottom_left = 18
|
||||
style.corner_radius_bottom_right = 18
|
||||
style.border_width_left = 2 if not selected else 4
|
||||
style.border_width_top = 2 if not selected else 4
|
||||
style.border_width_right = 2 if not selected else 4
|
||||
style.border_width_bottom = 2 if not selected else 4
|
||||
style.border_color = Color(0.658, 0.835, 0.965, 0.86) if not selected else Color(0.086, 0.690, 0.960, 1.0)
|
||||
style.shadow_color = Color(0.086, 0.314, 0.520, 0.15 if selected else 0.08)
|
||||
style.shadow_size = 14 if selected else 8
|
||||
style.shadow_offset = Vector2(0, 4)
|
||||
return style
|
||||
|
||||
func _format_price(data: Dictionary) -> String:
|
||||
var status := str(data.get("status", ""))
|
||||
if status == CATALOG_SCRIPT.STATUS_OWNED:
|
||||
return "已购买"
|
||||
if status == CATALOG_SCRIPT.STATUS_COMING_SOON or status == CATALOG_SCRIPT.STATUS_LOCKED:
|
||||
return "暂未开放"
|
||||
return str(int(data.get("price", 0)))
|
||||
|
||||
func _load_texture(path: String) -> Texture2D:
|
||||
if path.is_empty():
|
||||
return null
|
||||
if FileAccess.file_exists("%s.import" % path):
|
||||
var texture := load(path) as Texture2D
|
||||
if texture != null:
|
||||
return texture
|
||||
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
|
||||
if image == null or image.is_empty():
|
||||
return null
|
||||
return ImageTexture.create_from_image(image)
|
||||
|
||||
func _on_pressed() -> void:
|
||||
item_selected.emit(itemData)
|
||||
1
scenes/ui/mall/MallItemCard.gd.uid
Normal file
1
scenes/ui/mall/MallItemCard.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://buh0lhtug6bw8
|
||||
1211
scenes/ui/mall/MallPanel.gd
Normal file
1211
scenes/ui/mall/MallPanel.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scenes/ui/mall/MallPanel.gd.uid
Normal file
1
scenes/ui/mall/MallPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bej4ywlrtg1tb
|
||||
13
scenes/ui/mall/MallPanel.tscn
Normal file
13
scenes/ui/mall/MallPanel.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=4 uid="uid://whaletown_mall_panel"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/mall/MallPanel.gd" id="1_mall_panel"]
|
||||
|
||||
[node name="MallPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_mall_panel")
|
||||
232
scenes/ui/notice_dialog.tscn
Normal file
232
scenes/ui/notice_dialog.tscn
Normal file
@@ -0,0 +1,232 @@
|
||||
[gd_scene load_steps=10 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/NoticeDialog.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="2_theme"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
||||
content_margin_left = 30.0
|
||||
content_margin_top = 24.0
|
||||
content_margin_right = 30.0
|
||||
content_margin_bottom = 24.0
|
||||
bg_color = Color(0.975, 0.955, 0.89, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.28, 0.43, 0.42, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
shadow_color = Color(0.05, 0.08, 0.08, 0.32)
|
||||
shadow_size = 18
|
||||
shadow_offset = Vector2(0, 8)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_image"]
|
||||
content_margin_left = 10.0
|
||||
content_margin_top = 10.0
|
||||
content_margin_right = 10.0
|
||||
content_margin_bottom = 10.0
|
||||
bg_color = Color(0.875, 0.93, 0.92, 1)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.55, 0.68, 0.64, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_text"]
|
||||
content_margin_left = 20.0
|
||||
content_margin_top = 18.0
|
||||
content_margin_right = 20.0
|
||||
content_margin_bottom = 18.0
|
||||
bg_color = Color(1, 0.985, 0.935, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.78, 0.7, 0.55, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.2, 0.43, 0.48, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.28, 0.52, 0.55, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_disabled"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.72, 0.76, 0.72, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_close"]
|
||||
bg_color = Color(0.86, 0.33, 0.27, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[node name="NoticeDialog" type="CanvasLayer"]
|
||||
process_mode = 3
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Dimmer" type="ColorRect" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.03, 0.04, 0.04, 0.58)
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(720, 560)
|
||||
layout_mode = 2
|
||||
theme = ExtResource("2_theme")
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 18
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 52)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="LeftSpacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Title" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.15, 0.26, 0.3, 1)
|
||||
theme_override_font_sizes/font_size = 30
|
||||
text = "公告板"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="RightContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
alignment = 2
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Header/RightContainer"]
|
||||
custom_minimum_size = Vector2(42, 42)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_close")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_close")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_close")
|
||||
text = "X"
|
||||
|
||||
[node name="ContentContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 360)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 22
|
||||
|
||||
[node name="ImagePanel" type="PanelContainer" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer"]
|
||||
custom_minimum_size = Vector2(322, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_image")
|
||||
|
||||
[node name="ImageRect" type="TextureRect" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel"]
|
||||
layout_mode = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="ImageLabel" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.43, 0.48, 0.45, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "暂无图片"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="TextPanel" type="PanelContainer" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_text")
|
||||
|
||||
[node name="ContentLabel" type="RichTextLabel" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/TextPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_colors/default_color = Color(0.18, 0.2, 0.18, 1)
|
||||
theme_override_font_sizes/normal_font_size = 21
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
text = "Announcement Content..."
|
||||
|
||||
[node name="Footer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 58)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 22
|
||||
alignment = 1
|
||||
|
||||
[node name="PrevButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
custom_minimum_size = Vector2(56, 44)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_button")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_hover")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxFlat_button_disabled")
|
||||
text = "<"
|
||||
|
||||
[node name="DotsContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
custom_minimum_size = Vector2(88, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
alignment = 1
|
||||
|
||||
[node name="NextButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
custom_minimum_size = Vector2(56, 44)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_button")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_hover")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxFlat_button_disabled")
|
||||
text = ">"
|
||||
169
scenes/ui/welcome_dialog.tscn
Normal file
169
scenes/ui/welcome_dialog.tscn
Normal file
@@ -0,0 +1,169 @@
|
||||
[gd_scene load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/WelcomeDialog.gd" id="1_script"]
|
||||
[ext_resource type="Texture2D" path="res://assets/maps/square/v1/props/bottom_entrance_right_service_props_v2_hd_clean.png" id="2_board"]
|
||||
[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="3_theme"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_card"]
|
||||
content_margin_left = 34.0
|
||||
content_margin_top = 28.0
|
||||
content_margin_right = 34.0
|
||||
content_margin_bottom = 28.0
|
||||
bg_color = Color(0.965, 0.95, 0.88, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.28, 0.42, 0.38, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
shadow_color = Color(0.05, 0.08, 0.08, 0.32)
|
||||
shadow_size = 18
|
||||
shadow_offset = Vector2(0, 8)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_image"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 12.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 12.0
|
||||
bg_color = Color(0.87, 0.93, 0.91, 1)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.54, 0.67, 0.62, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"]
|
||||
content_margin_left = 22.0
|
||||
content_margin_top = 10.0
|
||||
content_margin_right = 22.0
|
||||
content_margin_bottom = 10.0
|
||||
bg_color = Color(0.2, 0.43, 0.48, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
|
||||
content_margin_left = 22.0
|
||||
content_margin_top = 10.0
|
||||
content_margin_right = 22.0
|
||||
content_margin_bottom = 10.0
|
||||
bg_color = Color(0.28, 0.52, 0.55, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_close"]
|
||||
bg_color = Color(0.86, 0.33, 0.27, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[node name="WelcomeDialog" type="CanvasLayer"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.03, 0.04, 0.04, 0.52)
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(680, 520)
|
||||
layout_mode = 2
|
||||
theme = ExtResource("3_theme")
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 18
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 52)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Title" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.14, 0.25, 0.29, 1)
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Datawhale Town 信息板"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="Spacer2" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
custom_minimum_size = Vector2(42, 42)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_close")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_close")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_close")
|
||||
text = "X"
|
||||
|
||||
[node name="LogoContainer" type="PanelContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 190)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_image")
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="CenterContainer/PanelContainer/VBoxContainer/LogoContainer"]
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_board")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BodyText" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 150)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_colors/font_color = Color(0.18, 0.2, 0.18, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "连接、共生、见证。
|
||||
Datawhale Town 是学习者的赛博家园与精神坐标。
|
||||
|
||||
实时广场:看大家都在学什么。
|
||||
个人空间:展示你的学习笔记与作品。
|
||||
开源营地:更有氛围的组队学习体验。"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="ActionContainer" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 58)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StartButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/ActionContainer"]
|
||||
custom_minimum_size = Vector2(190, 48)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 19
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_button")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_hover")
|
||||
text = "开始探索"
|
||||
Reference in New Issue
Block a user