Files
whale-town-front-v2/scenes/ui/SettingsPanel.gd
2026-07-22 13:41:28 +08:00

1389 lines
54 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Control
# ============================================================================
# SettingsPanel.gd - V2 游戏设置面板
# ============================================================================
# 监听右上角 HUD 设置入口,展示与当前 HUD/聊天一致的轻量设置弹窗。
# ============================================================================
const PANEL_SIZE: Vector2 = Vector2(1120, 1060)
const MIN_PANEL_MARGIN: Vector2 = Vector2(54, 54)
const PANEL_SHIFT: Vector2 = Vector2(70, 18)
const SIDEBAR_WIDTH: float = 222.0
const ICON_SCRIPT: Script = preload("res://scenes/ui/SettingsPanelIcon.gd")
const TOGGLE_SCRIPT: Script = preload("res://scenes/ui/SettingsToggle.gd")
const SLIDER_SCRIPT: Script = preload("res://scenes/ui/SettingsSlider.gd")
const BADGE_TEXTURE_PATH: String = "res://assets/ui/settings/settings_whale_badge_asset_v1.png"
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.92)
const DANGER_COLOR: Color = Color(0.894, 0.392, 0.357)
const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"]
const AVATAR_NATIVE_FILE_FILTERS: Array[String] = ["*.png,*.jpg,*.jpeg,*.webp"]
const CATEGORIES: Array[Dictionary] = [
{"id": "basic", "label": "基础", "icon": "basic"},
{"id": "chat", "label": "聊天社交", "icon": "chat"},
{"id": "appearance", "label": "外观", "icon": "account"},
{"id": "controls", "label": "操作", "icon": "controls"},
{"id": "account", "label": "账户", "icon": "account"},
]
const DEFAULT_SETTINGS: Dictionary = {
"master_volume": 0.80,
"music_volume": 0.60,
"effects_volume": 0.90,
"ui_scale": 1.00,
"fullscreen": false,
"show_interaction_hints": true,
"show_interaction_points": false,
"show_name_always": false,
"show_chat_bubbles": true,
"world_notifications": true,
"private_notifications": true,
"friend_request_notifications": true,
"allow_nearby_private": true,
"allow_nearby_friend_requests": true,
"allow_nearby_profile": true,
"room_visit_policy": "friends",
"mute_ui_sfx": false,
}
var _overlay: ColorRect
var _panel: PanelContainer
var _badge: TextureRect
var _content: VBoxContainer
var _titleLabel: Label
var _subtitleLabel: Label
var _usernameLabel: Label
var _accountLabel: Label
var _statusLabel: Label
var _categoryButtons: Dictionary = {}
var _categoryLabels: Dictionary = {}
var _categoryIcons: Dictionary = {}
var _scaleButtons: Dictionary = {}
var _sliderValueLabels: Dictionary = {}
var _toggles: Dictionary = {}
var _currentCategory: String = "basic"
var _settings: Dictionary = DEFAULT_SETTINGS.duplicate(true)
var _savedSettings: Dictionary = DEFAULT_SETTINGS.duplicate(true)
var _isOpen: bool = false
var _transitionTween: Tween
var _avatarUploadPending: bool = false
func _ready() -> void:
add_to_group("whaletown_escape_dismissible")
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_process(false)
_load_settings()
_build_ui()
_subscribe_to_events()
func _exit_tree() -> void:
var eventSystem := _get_event_system()
if eventSystem != null:
eventSystem.call("disconnect_event", EventNames.HUD_SETTINGS_REQUESTED, _on_settings_requested, self)
eventSystem.call("disconnect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self)
eventSystem.call("disconnect_event", EventNames.APPEARANCE_PROFILE_CHANGED, _on_appearance_profile_changed, self)
if is_instance_valid(_transitionTween):
_transitionTween.kill()
_transitionTween = null
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED and is_instance_valid(_panel):
_position_panel()
func _input(event: InputEvent) -> void:
if get_viewport().is_input_handled():
return
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(true)
get_viewport().set_input_as_handled()
func _process(_delta: float) -> void:
if _isOpen:
_release_movement_actions()
func show_panel() -> void:
if _isOpen:
return
_isOpen = true
visible = true
mouse_filter = Control.MOUSE_FILTER_STOP
set_process(true)
_release_movement_input_state()
_load_settings()
_load_current_user()
_render_content()
_update_category_visuals()
_update_panel_position_and_alpha(0.0, 0.97)
_animate_panel(true)
func hide_panel(saveBeforeClose: bool = false) -> void:
if not _isOpen:
return
if saveBeforeClose:
_save_settings()
else:
_restore_saved_settings()
_isOpen = false
_release_movement_input_state()
_animate_panel(false)
func is_panel_open() -> bool:
return _isOpen
func is_escape_dismissible() -> bool:
return _isOpen
func get_escape_priority() -> int:
return 700
func request_escape_close() -> void:
hide_panel(true)
func _build_ui() -> void:
_overlay = ColorRect.new()
_overlay.name = "SettingsDimOverlay"
_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
_overlay.color = Color(0.118, 0.176, 0.235, 0.42)
_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
_overlay.gui_input.connect(_on_overlay_gui_input)
add_child(_overlay)
_panel = PanelContainer.new()
_panel.name = "SettingsCard"
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
_panel.add_theme_stylebox_override("panel", _create_panel_style(Color(1.0, 0.992, 0.968, 0.975), 38, true))
add_child(_panel)
_position_panel()
_badge = TextureRect.new()
_badge.name = "WhaleBadge"
_badge.texture = load(BADGE_TEXTURE_PATH) as Texture2D
_badge.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
_badge.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
_badge.z_index = 3
add_child(_badge)
_position_badge()
var outerMargin := MarginContainer.new()
outerMargin.add_theme_constant_override("margin_left", 24)
outerMargin.add_theme_constant_override("margin_top", 20)
outerMargin.add_theme_constant_override("margin_right", 24)
outerMargin.add_theme_constant_override("margin_bottom", 22)
_panel.add_child(outerMargin)
var root := VBoxContainer.new()
root.add_theme_constant_override("separation", 14)
outerMargin.add_child(root)
root.add_child(_build_header())
var body := HBoxContainer.new()
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 18)
root.add_child(body)
body.add_child(_build_sidebar())
body.add_child(_build_content_area())
root.add_child(_build_footer())
_render_content()
_update_category_visuals()
func _build_header() -> Control:
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 72)
header.alignment = BoxContainer.ALIGNMENT_CENTER
header.add_theme_constant_override("separation", 12)
var leftSpacer := Control.new()
leftSpacer.custom_minimum_size = Vector2(54, 54)
header.add_child(leftSpacer)
var centerSpacer := Control.new()
centerSpacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(centerSpacer)
var closeButton := Button.new()
closeButton.text = "×"
closeButton.tooltip_text = "关闭设置"
closeButton.custom_minimum_size = Vector2(54, 54)
closeButton.focus_mode = Control.FOCUS_NONE
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
closeButton.add_theme_font_size_override("font_size", 34)
closeButton.add_theme_color_override("font_color", MUTED_COLOR)
closeButton.add_theme_stylebox_override("normal", _create_round_style(Color(1, 1, 1, 0.72), 27))
closeButton.add_theme_stylebox_override("hover", _create_round_style(SOFT_BLUE, 27))
closeButton.add_theme_stylebox_override("pressed", _create_round_style(Color(0.858, 0.925, 0.980, 1.0), 27))
closeButton.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
closeButton.pressed.connect(func() -> void: hide_panel(true))
header.add_child(closeButton)
return header
func _build_sidebar() -> Control:
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(SIDEBAR_WIDTH, 0)
panel.add_theme_stylebox_override("panel", _create_panel_style(Color(1.0, 1.0, 1.0, 0.62), 28, false))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 16)
margin.add_theme_constant_override("margin_top", 20)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_bottom", 20)
panel.add_child(margin)
var list := VBoxContainer.new()
list.add_theme_constant_override("separation", 14)
margin.add_child(list)
for category in CATEGORIES:
var categoryId := str(category.get("id", ""))
list.add_child(_create_category_button(categoryId, str(category.get("label", "")), str(category.get("icon", "basic"))))
return panel
func _build_content_area() -> Control:
var area := VBoxContainer.new()
area.size_flags_horizontal = Control.SIZE_EXPAND_FILL
area.size_flags_vertical = Control.SIZE_EXPAND_FILL
area.add_theme_constant_override("separation", 12)
var scroll := ScrollContainer.new()
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
area.add_child(scroll)
_content = VBoxContainer.new()
_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_content.add_theme_constant_override("separation", 14)
scroll.add_child(_content)
return area
func _build_footer() -> Control:
var footer := HBoxContainer.new()
footer.custom_minimum_size = Vector2(0, 46)
footer.add_theme_constant_override("separation", 12)
var linkRow := HBoxContainer.new()
linkRow.size_flags_horizontal = Control.SIZE_EXPAND_FILL
linkRow.alignment = BoxContainer.ALIGNMENT_CENTER
linkRow.add_theme_constant_override("separation", 24)
footer.add_child(linkRow)
for text in ["用户协议", "隐私政策", "客服中心"]:
var label := Label.new()
label.text = text
label.add_theme_color_override("font_color", MUTED_COLOR)
label.add_theme_font_size_override("font_size", 14)
linkRow.add_child(label)
return footer
func _create_category_button(categoryId: String, labelText: String, iconName: String) -> Button:
var button := Button.new()
button.custom_minimum_size = Vector2(0, 72)
button.text = ""
button.focus_mode = Control.FOCUS_NONE
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
button.add_theme_stylebox_override("hover", _create_round_style(SOFT_BLUE, 17))
button.add_theme_stylebox_override("pressed", _create_round_style(Color(0.858, 0.925, 0.980, 0.98), 17))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
button.pressed.connect(func() -> void: _select_category(categoryId))
var row := HBoxContainer.new()
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.set_anchors_preset(Control.PRESET_FULL_RECT)
row.offset_left = 16
row.offset_right = -12
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 14)
button.add_child(row)
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", iconName)
icon.custom_minimum_size = Vector2(34, 34)
row.add_child(icon)
var label := Label.new()
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.text = labelText
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.add_theme_font_size_override("font_size", 19)
row.add_child(label)
_categoryButtons[categoryId] = button
_categoryLabels[categoryId] = label
_categoryIcons[categoryId] = icon
return button
func _render_content() -> void:
if not is_instance_valid(_content):
return
_clear_children(_content)
_sliderValueLabels.clear()
_toggles.clear()
_scaleButtons.clear()
match _currentCategory:
"basic":
_render_basic_content()
"chat":
_render_chat_content()
"appearance":
_render_appearance_content()
"controls":
_render_controls_content()
"account":
_render_account_content()
_:
_render_basic_content()
func _render_basic_content() -> void:
_content.add_child(_create_section("基础设置", [
_create_toggle_row("window", "全屏显示", "fullscreen", "适合专注游玩,窗口模式方便调试"),
_create_toggle_row("eye", "显示互动提示", "show_interaction_hints", "靠近 NPC、好友或公告板时显示按键提示"),
_create_toggle_row("eye", "显示交互点", "show_interaction_points", "用白色圆圈直接标出当前地图内的交互目标"),
_create_toggle_row("account", "始终显示名字", "show_name_always", "关闭时只在需要时显示玩家名称"),
]))
_content.add_child(_create_section("音频设置", [
_create_slider_row("volume", "主音量", "master_volume"),
_create_slider_row("music", "背景音乐", "music_volume"),
_create_slider_row("effects", "音效", "effects_volume"),
_create_toggle_row("effects", "静音提醒音效", "mute_ui_sfx", "关闭聊天、私聊和好友请求的提示音"),
]))
_content.add_child(_create_section("聊天显示", [
_create_toggle_row("chat", "显示聊天气泡", "show_chat_bubbles", "用气泡按钮发送时在角色头顶显示"),
_create_toggle_row("eye", "屏蔽陌生人私聊", "allow_nearby_private", "关闭后附近玩家不能直接发起悄悄话", true),
]))
_content.add_child(_create_section("界面设置", [
_create_scale_row(),
_create_hint_row("当前缩放会立即应用到游戏界面,并在下次进入时保留。"),
]))
func _render_chat_content() -> void:
_content.add_child(_create_section("聊天与社交", [
_create_toggle_row("chat", "世界频道提醒", "world_notifications", "收到世界频道消息时保留轻提示"),
_create_toggle_row("chat", "私聊提醒", "private_notifications", "好友或附近玩家私聊时提示"),
_create_toggle_row("account", "好友请求提醒", "friend_request_notifications", "附近玩家发起好友申请时显示在好友列表"),
_create_toggle_row("chat", "显示聊天气泡", "show_chat_bubbles", "气泡发送会同时进入世界频道"),
]))
_content.add_child(_create_section("附近玩家权限", [
_create_toggle_row("eye", "允许查看我的名片", "allow_nearby_profile", "关闭后陌生玩家无法在附近打开你的社区名片"),
_create_toggle_row("chat", "允许附近私聊", "allow_nearby_private", "附近玩家按 E 可以发起悄悄话"),
_create_toggle_row("account", "允许好友申请", "allow_nearby_friend_requests", "附近玩家可通过互动列表发送好友申请"),
]))
_content.add_child(_create_section("个人空间访问", [
_create_room_visit_policy_row(),
]))
func _render_controls_content() -> void:
_content.add_child(_create_section("操作说明", [
_create_key_row("W A S D", "移动角色"),
_create_key_row("方向键", "附近互动列表选择"),
_create_key_row("E", "执行选中的互动"),
_create_key_row("Esc", "关闭当前界面或结束当前互动"),
_create_key_row("T", "打开聊天输入"),
_create_key_row("Enter", "发送聊天消息"),
]))
_content.add_child(_create_section("操作辅助", [
_create_toggle_row("eye", "显示互动提示", "show_interaction_hints", "靠近可交互目标时显示按键提示"),
_create_toggle_row("eye", "显示交互点", "show_interaction_points", "用白色圆圈直接标出当前地图内的交互目标"),
_create_hint_row("当前版本使用固定键位;这里的开关会控制地图内的互动提示显示。"),
]))
func _render_account_content() -> void:
_content.add_child(_create_account_detail_section())
_content.add_child(_create_avatar_picker_section())
func _render_appearance_content() -> void:
_content.add_child(_create_skin_picker_section())
_content.add_child(_create_section("外观说明", [
_create_hint_row("这里仅保留真正替换角色素材的皮肤;单纯改颜色的变体不再展示。"),
]))
func _create_section(title: String, rows: Array[Control]) -> PanelContainer:
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override("panel", _create_panel_style(Color(1.0, 1.0, 1.0, 0.875), 18, false))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 28)
margin.add_theme_constant_override("margin_top", 17)
margin.add_theme_constant_override("margin_right", 28)
margin.add_theme_constant_override("margin_bottom", 18)
panel.add_child(margin)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 11)
margin.add_child(box)
var titleLabel := Label.new()
titleLabel.text = title
titleLabel.add_theme_color_override("font_color", TEXT_COLOR)
titleLabel.add_theme_font_size_override("font_size", 18)
box.add_child(titleLabel)
for row in rows:
box.add_child(row)
return panel
func _create_slider_row(iconName: String, labelText: String, key: String) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 44)
row.add_theme_constant_override("separation", 18)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", iconName)
icon.set("active", true)
icon.custom_minimum_size = Vector2(34, 34)
row.add_child(icon)
var label := Label.new()
label.text = labelText
label.custom_minimum_size = Vector2(126, 0)
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_font_size_override("font_size", 17)
row.add_child(label)
var slider := SLIDER_SCRIPT.new() as Control
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.call("set_value", float(_settings.get(key, DEFAULT_SETTINGS.get(key, 1.0))))
row.add_child(slider)
var valueLabel := Label.new()
valueLabel.custom_minimum_size = Vector2(58, 0)
valueLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
valueLabel.text = _format_percent(float(slider.get("value")))
valueLabel.add_theme_color_override("font_color", MUTED_COLOR)
valueLabel.add_theme_font_size_override("font_size", 16)
row.add_child(valueLabel)
_sliderValueLabels[key] = valueLabel
slider.connect("value_changed", func(value: float) -> void:
_settings[key] = value
valueLabel.text = _format_percent(value)
_apply_preview_settings()
)
return row
func _create_toggle_row(iconName: String, labelText: String, key: String, detailText: String, invertValue: bool = false) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 50)
row.add_theme_constant_override("separation", 16)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", iconName)
icon.set("active", true)
icon.custom_minimum_size = Vector2(34, 34)
row.add_child(icon)
var textBox := VBoxContainer.new()
textBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
textBox.alignment = BoxContainer.ALIGNMENT_CENTER
textBox.add_theme_constant_override("separation", 2)
row.add_child(textBox)
var label := Label.new()
label.text = labelText
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_font_size_override("font_size", 17)
textBox.add_child(label)
var detail := Label.new()
detail.text = detailText
detail.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
detail.add_theme_color_override("font_color", MUTED_COLOR)
detail.add_theme_font_size_override("font_size", 13)
textBox.add_child(detail)
var toggle := TOGGLE_SCRIPT.new() as Button
var value := bool(_settings.get(key, DEFAULT_SETTINGS.get(key, false)))
toggle.toggle_mode = true
toggle.button_pressed = not value if invertValue else value
toggle.toggled.connect(func(pressed: bool) -> void:
_settings[key] = not pressed if invertValue else pressed
_apply_preview_settings()
)
row.add_child(toggle)
_toggles[key] = toggle
return row
func _create_room_visit_policy_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 54)
row.add_theme_constant_override("separation", 16)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", "account")
icon.set("active", true)
icon.custom_minimum_size = Vector2(34, 34)
row.add_child(icon)
var text_box := VBoxContainer.new()
text_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
text_box.alignment = BoxContainer.ALIGNMENT_CENTER
text_box.add_theme_constant_override("separation", 2)
row.add_child(text_box)
var label := Label.new()
label.text = "谁可以参观我的房间"
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_font_size_override("font_size", 17)
text_box.add_child(label)
var detail := Label.new()
detail.text = "拉黑关系始终无法访问;房主始终可编辑。"
detail.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
detail.add_theme_color_override("font_color", MUTED_COLOR)
detail.add_theme_font_size_override("font_size", 13)
text_box.add_child(detail)
var selector := OptionButton.new()
selector.custom_minimum_size = Vector2(126, 38)
selector.focus_mode = Control.FOCUS_NONE
selector.add_item("仅好友")
selector.set_item_metadata(0, "friends")
selector.add_item("公开")
selector.set_item_metadata(1, "public")
selector.add_item("关闭访问")
selector.set_item_metadata(2, "closed")
var current_policy: String = str(_settings.get("room_visit_policy", "friends"))
for index in range(selector.item_count):
if str(selector.get_item_metadata(index)) == current_policy:
selector.select(index)
break
selector.item_selected.connect(func(index: int) -> void:
_settings["room_visit_policy"] = str(selector.get_item_metadata(index))
_apply_preview_settings()
)
row.add_child(selector)
return row
func _create_scale_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 48)
row.add_theme_constant_override("separation", 18)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", "window")
icon.set("active", true)
icon.custom_minimum_size = Vector2(34, 34)
row.add_child(icon)
var label := Label.new()
label.text = "UI 缩放"
label.custom_minimum_size = Vector2(126, 0)
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_font_size_override("font_size", 17)
row.add_child(label)
var segment := PanelContainer.new()
segment.size_flags_horizontal = Control.SIZE_EXPAND_FILL
segment.add_theme_stylebox_override("panel", _create_round_style(Color(1, 1, 1, 0.90), 20))
row.add_child(segment)
var segmentMargin := MarginContainer.new()
segmentMargin.add_theme_constant_override("margin_left", 6)
segmentMargin.add_theme_constant_override("margin_top", 4)
segmentMargin.add_theme_constant_override("margin_right", 6)
segmentMargin.add_theme_constant_override("margin_bottom", 4)
segment.add_child(segmentMargin)
var segmentRow := HBoxContainer.new()
segmentRow.add_theme_constant_override("separation", 0)
segmentMargin.add_child(segmentRow)
for value in [0.80, 0.90, 1.00, 1.10, 1.20]:
var button := Button.new()
button.text = "%d%%" % int(round(value * 100.0))
button.custom_minimum_size = Vector2(86, 36)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.focus_mode = Control.FOCUS_NONE
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.add_theme_font_size_override("font_size", 15)
button.pressed.connect(func() -> void: _select_ui_scale(value))
segmentRow.add_child(button)
_scaleButtons[value] = button
_update_scale_buttons()
return row
func _create_key_row(keys: String, actionText: String) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 52)
row.add_theme_constant_override("separation", 18)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", "keyboard")
icon.set("active", true)
icon.custom_minimum_size = Vector2(32, 32)
row.add_child(icon)
var keyLabel := Label.new()
keyLabel.text = keys
keyLabel.custom_minimum_size = Vector2(150, 36)
keyLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
keyLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
keyLabel.add_theme_color_override("font_color", ACCENT_COLOR)
keyLabel.add_theme_font_size_override("font_size", 16)
keyLabel.add_theme_stylebox_override("normal", _create_round_style(Color(0.918, 0.961, 0.992, 0.86), 14))
row.add_child(keyLabel)
var actionLabel := Label.new()
actionLabel.text = actionText
actionLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
actionLabel.add_theme_color_override("font_color", TEXT_COLOR)
actionLabel.add_theme_font_size_override("font_size", 17)
row.add_child(actionLabel)
return row
func _create_hint_row(text: String) -> Control:
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _create_round_style(Color(0.918, 0.961, 0.992, 0.60), 14))
var label := Label.new()
label.text = text
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.add_theme_color_override("font_color", MUTED_COLOR)
label.add_theme_font_size_override("font_size", 14)
panel.add_child(label)
return panel
func _create_account_detail_section() -> PanelContainer:
var rows: Array[Control] = []
rows.append(_create_account_summary_row(true))
rows.append(_create_connection_row())
rows.append(_create_account_action_row())
return _create_section("账户状态", rows)
func _create_skin_picker_section() -> PanelContainer:
var rows: Array[Control] = []
var grid := GridContainer.new()
grid.columns = 2
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_theme_constant_override("h_separation", 14)
grid.add_theme_constant_override("v_separation", 14)
var appearanceManager := _get_appearance_manager()
var skins: Array = appearanceManager.call("get_owned_skin_catalog") if appearanceManager != null and appearanceManager.has_method("get_owned_skin_catalog") else []
for skinVariant in skins:
if skinVariant is Dictionary and not bool((skinVariant as Dictionary).get("hidden", false)):
grid.add_child(_create_skin_card(skinVariant as Dictionary))
if grid.get_child_count() == 0:
var emptyLabel := Label.new()
emptyLabel.text = "暂无可用角色皮肤"
emptyLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
emptyLabel.add_theme_color_override("font_color", MUTED_COLOR)
emptyLabel.add_theme_font_size_override("font_size", 15)
grid.add_child(emptyLabel)
rows.append(grid)
return _create_section("角色皮肤", rows)
func _create_avatar_picker_section() -> PanelContainer:
var rows: Array[Control] = []
rows.append(_create_current_avatar_preview_row())
rows.append(_create_avatar_upload_row())
return _create_section("头像替换", rows)
func _create_skin_card(skin: Dictionary) -> Button:
var appearanceManager := _get_appearance_manager()
var skinId := str(skin.get("id", ""))
var selectedSkinId := str(appearanceManager.call("get_selected_skin_id")) if appearanceManager != null and appearanceManager.has_method("get_selected_skin_id") else ""
var isSelected := skinId == selectedSkinId
var button := Button.new()
button.name = "SkinCard_%s" % skinId
button.custom_minimum_size = Vector2(0, 128)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.focus_mode = Control.FOCUS_NONE
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.text = ""
var normalColor := Color(0.925, 0.966, 0.996, 0.92) if isSelected else Color(1, 1, 1, 0.70)
button.add_theme_stylebox_override("normal", _create_button_style(normalColor, 18, ACCENT_COLOR if isSelected else Color(0.788, 0.851, 0.910, 0.34)))
button.add_theme_stylebox_override("hover", _create_button_style(Color(0.918, 0.961, 0.992, 1.0), 18, ACCENT_COLOR))
button.add_theme_stylebox_override("pressed", _create_button_style(Color(0.858, 0.925, 0.980, 1.0), 18, ACCENT_COLOR))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
button.pressed.connect(func() -> void: _select_skin(skinId))
var row := HBoxContainer.new()
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.set_anchors_preset(Control.PRESET_FULL_RECT)
row.offset_left = 16
row.offset_top = 14
row.offset_right = -16
row.offset_bottom = -14
row.add_theme_constant_override("separation", 14)
button.add_child(row)
row.add_child(_create_skin_preview(skin))
var info := VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info.alignment = BoxContainer.ALIGNMENT_CENTER
info.add_theme_constant_override("separation", 5)
row.add_child(info)
var nameLabel := Label.new()
nameLabel.text = str(skin.get("name", "皮肤"))
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
nameLabel.add_theme_color_override("font_color", ACCENT_COLOR if isSelected else TEXT_COLOR)
nameLabel.add_theme_font_size_override("font_size", 17)
info.add_child(nameLabel)
var descLabel := Label.new()
descLabel.text = str(skin.get("desc", ""))
descLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
descLabel.add_theme_color_override("font_color", MUTED_COLOR)
descLabel.add_theme_font_size_override("font_size", 13)
info.add_child(descLabel)
var statusLabel := Label.new()
statusLabel.text = "使用中" if isSelected else "点击切换"
statusLabel.add_theme_color_override("font_color", ACCENT_COLOR if isSelected else MUTED_COLOR)
statusLabel.add_theme_font_size_override("font_size", 13)
info.add_child(statusLabel)
return button
func _create_skin_preview(skin: Dictionary) -> PanelContainer:
var preview := PanelContainer.new()
preview.custom_minimum_size = Vector2(76, 92)
preview.add_theme_stylebox_override("panel", _create_round_style(Color(0.918, 0.961, 0.992, 0.88), 16))
var sprite := Sprite2D.new()
var appearanceManager := _get_appearance_manager()
if appearanceManager != null and appearanceManager.has_method("apply_skin_to_sprite"):
appearanceManager.call("apply_skin_to_sprite", sprite, str(skin.get("id", "")))
else:
sprite.texture = load(str(skin.get("texture", "res://assets/characters/player_pixel_spritesheet.png"))) as Texture2D
sprite.hframes = int(skin.get("hframes", 4))
sprite.vframes = int(skin.get("vframes", 4))
sprite.frame = 0
sprite.scale = Vector2(0.32, 0.32)
sprite.position = Vector2(38, 51)
sprite.modulate = Color.WHITE
sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
preview.add_child(sprite)
return preview
func _create_current_avatar_preview_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 66)
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 16)
var avatar := PanelContainer.new()
avatar.custom_minimum_size = Vector2(54, 54)
row.add_child(avatar)
var avatarLabel := Label.new()
avatarLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
avatarLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
avatarLabel.add_theme_font_size_override("font_size", 19)
avatar.add_child(avatarLabel)
var appearanceManager := _get_appearance_manager()
if appearanceManager != null and appearanceManager.has_method("apply_avatar_to_panel"):
appearanceManager.call("apply_avatar_to_panel", avatar, avatarLabel, "", _current_username_initial())
var textBox := VBoxContainer.new()
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 titleLabel := Label.new()
titleLabel.text = "当前头像"
titleLabel.add_theme_color_override("font_color", TEXT_COLOR)
titleLabel.add_theme_font_size_override("font_size", 17)
textBox.add_child(titleLabel)
var hintLabel := Label.new()
hintLabel.text = "会同步到右上角玩家胶囊和自己发送的聊天消息"
hintLabel.add_theme_color_override("font_color", MUTED_COLOR)
hintLabel.add_theme_font_size_override("font_size", 13)
textBox.add_child(hintLabel)
return row
func _create_avatar_upload_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 58)
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 14)
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", "account")
icon.set("active", true)
icon.custom_minimum_size = Vector2(32, 32)
row.add_child(icon)
var textBox := VBoxContainer.new()
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 titleLabel := Label.new()
titleLabel.text = "上传本地图片"
titleLabel.add_theme_color_override("font_color", TEXT_COLOR)
titleLabel.add_theme_font_size_override("font_size", 17)
textBox.add_child(titleLabel)
var hintLabel := Label.new()
hintLabel.text = "支持 PNG/JPG/WebP会自动裁成圆角矩形头像"
hintLabel.add_theme_color_override("font_color", MUTED_COLOR)
hintLabel.add_theme_font_size_override("font_size", 13)
textBox.add_child(hintLabel)
var uploadButton := _create_action_button("选择图片", ACCENT_COLOR, _on_upload_avatar_pressed, true)
uploadButton.custom_minimum_size = Vector2(138, 42)
row.add_child(uploadButton)
return row
func _create_account_summary_row(showFull: bool = false) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 68)
row.add_theme_constant_override("separation", 16)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var avatar := PanelContainer.new()
avatar.custom_minimum_size = Vector2(54, 54)
row.add_child(avatar)
var avatarLabel := Label.new()
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", 20)
avatar.add_child(avatarLabel)
_apply_current_avatar_to_panel(avatar, avatarLabel, _current_username_initial())
var info := VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info.alignment = BoxContainer.ALIGNMENT_CENTER
info.add_theme_constant_override("separation", 4)
row.add_child(info)
_usernameLabel = Label.new()
_usernameLabel.text = _current_username()
_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)
info.add_child(_usernameLabel)
_accountLabel = Label.new()
_accountLabel.text = _current_account_text()
_accountLabel.add_theme_color_override("font_color", MUTED_COLOR)
_accountLabel.add_theme_font_size_override("font_size", 13)
info.add_child(_accountLabel)
if not showFull:
var switchButton := _create_action_button("切换账号", ACCENT_COLOR, _on_switch_account_pressed, true)
switchButton.custom_minimum_size = Vector2(138, 42)
row.add_child(switchButton)
var logoutButton := _create_action_button("退出登录", DANGER_COLOR, _on_logout_pressed, true)
logoutButton.custom_minimum_size = Vector2(138, 42)
row.add_child(logoutButton)
return row
func _create_connection_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 52)
row.add_theme_constant_override("separation", 12)
row.alignment = BoxContainer.ALIGNMENT_CENTER
var icon := ICON_SCRIPT.new() as Control
icon.set("iconName", "chat")
icon.set("active", true)
icon.custom_minimum_size = Vector2(32, 32)
row.add_child(icon)
_statusLabel = Label.new()
_statusLabel.text = _connection_status_text()
_statusLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_statusLabel.add_theme_color_override("font_color", MUTED_COLOR)
_statusLabel.add_theme_font_size_override("font_size", 16)
row.add_child(_statusLabel)
var reconnectButton := _create_action_button("重新连接", ACCENT_COLOR, _on_reconnect_pressed, true)
reconnectButton.custom_minimum_size = Vector2(126, 40)
row.add_child(reconnectButton)
return row
func _create_account_action_row() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 54)
row.alignment = BoxContainer.ALIGNMENT_END
row.add_theme_constant_override("separation", 14)
var switchButton := _create_action_button("切换账号", ACCENT_COLOR, _on_switch_account_pressed, true)
switchButton.custom_minimum_size = Vector2(146, 42)
row.add_child(switchButton)
var logoutButton := _create_action_button("退出登录", DANGER_COLOR, _on_logout_pressed, true)
logoutButton.custom_minimum_size = Vector2(146, 42)
row.add_child(logoutButton)
return row
func _create_action_button(text: String, color: Color, callback: Callable, outline: bool = false) -> Button:
var button := Button.new()
button.text = text
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 if outline else Color.WHITE)
if outline:
button.add_theme_color_override("font_hover_color", color)
button.add_theme_color_override("font_pressed_color", color.darkened(0.18))
button.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
var normalColor := Color(1.0, 1.0, 1.0, 0.90) if outline else color
var hoverColor := color.lightened(0.50) if outline else color.lightened(0.08)
var pressedColor := color.lightened(0.42) if outline else color.darkened(0.08)
button.add_theme_stylebox_override("normal", _create_button_style(normalColor, 16, color if outline else Color.TRANSPARENT))
button.add_theme_stylebox_override("hover", _create_button_style(hoverColor, 16, color if outline else Color.TRANSPARENT))
button.add_theme_stylebox_override("pressed", _create_button_style(pressedColor, 16, color if outline else Color.TRANSPARENT))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
button.pressed.connect(callback)
return button
func _select_category(categoryId: String) -> void:
if categoryId == _currentCategory:
return
_currentCategory = categoryId
_render_content()
_update_category_visuals()
func _select_ui_scale(value: float) -> void:
_settings["ui_scale"] = value
_update_scale_buttons()
_apply_preview_settings()
func _select_skin(skinId: String) -> void:
var appearanceManager := _get_appearance_manager()
if appearanceManager == null or not appearanceManager.has_method("set_selected_skin"):
return
if bool(appearanceManager.call("set_selected_skin", skinId)):
_render_content()
_update_category_visuals()
func _update_category_visuals() -> void:
for category in CATEGORIES:
var categoryId := str(category.get("id", ""))
var active: bool = categoryId == _currentCategory
var button := _categoryButtons.get(categoryId) as Button
var label := _categoryLabels.get(categoryId) as Label
var icon := _categoryIcons.get(categoryId) as Control
if is_instance_valid(button):
var style: StyleBox = _create_round_style(Color(0.918, 0.961, 0.992, 0.98), 17) if active else StyleBoxEmpty.new()
button.add_theme_stylebox_override("normal", style)
if is_instance_valid(label):
label.add_theme_color_override("font_color", ACCENT_COLOR if active else TEXT_COLOR)
if is_instance_valid(icon) and icon.has_method("set_active"):
icon.call("set_active", active)
func _update_scale_buttons() -> void:
var currentScale := float(_settings.get("ui_scale", 1.0))
for key in _scaleButtons.keys():
var button := _scaleButtons[key] as Button
if not is_instance_valid(button):
continue
var active: bool = abs(float(key) - currentScale) < 0.01
button.add_theme_color_override("font_color", Color.WHITE if active else MUTED_COLOR)
button.add_theme_color_override("font_hover_color", Color.WHITE if active else ACCENT_COLOR)
button.add_theme_color_override("font_pressed_color", Color.WHITE if active else ACCENT_COLOR.darkened(0.18))
button.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
button.add_theme_stylebox_override("normal", _create_round_style(ACCENT_COLOR if active else Color(1, 1, 1, 0.84), 18))
button.add_theme_stylebox_override("hover", _create_round_style(ACCENT_COLOR.lightened(0.08) if active else SOFT_BLUE, 18))
button.add_theme_stylebox_override("pressed", _create_round_style(ACCENT_COLOR.darkened(0.05), 18))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
func _on_settings_requested(_data: Variant = null) -> void:
show_panel()
func _on_overlay_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mouseEvent := event as InputEventMouseButton
if mouseEvent.pressed and mouseEvent.button_index == MOUSE_BUTTON_LEFT:
hide_panel(true)
accept_event()
func _on_reconnect_pressed() -> void:
var chatManager := get_node_or_null("/root/ChatManager")
var authManager := get_node_or_null("/root/AuthManager")
if chatManager != null and authManager != null:
var token := str(authManager.call("get_access_token")) if authManager.has_method("get_access_token") else ""
if not token.is_empty() and chatManager.has_method("set_game_token"):
chatManager.call("set_game_token", token)
if chatManager.has_method("connect_to_chat_server"):
chatManager.call("connect_to_chat_server")
if is_instance_valid(_statusLabel):
_statusLabel.text = "已请求重新连接聊天服务"
func _on_upload_avatar_pressed() -> void:
if _open_macos_avatar_file_dialog():
return
if _try_open_native_avatar_file_picker():
_set_account_status("请选择头像图片")
return
_set_account_status("系统文件选择器暂时不可用")
func _try_open_native_avatar_file_picker() -> bool:
if DisplayServer.get_name() == "headless":
return false
var err := DisplayServer.file_dialog_show(
"选择头像图片",
_default_avatar_picker_dir(),
"",
false,
DisplayServer.FILE_DIALOG_MODE_OPEN_FILE,
PackedStringArray(AVATAR_NATIVE_FILE_FILTERS),
_on_native_avatar_file_selected
)
return err == OK
func _on_native_avatar_file_selected(status: bool, selectedPaths: PackedStringArray, _selectedFilterIndex: int) -> void:
if not status or selectedPaths.is_empty():
_set_account_status("已取消头像选择")
return
_on_avatar_file_selected(selectedPaths[0])
func _on_avatar_file_selected(path: String) -> void:
if not _sync_avatar_file_to_backend(path):
_set_account_status("头像读取失败,或当前账号未登录")
return
_set_account_status("正在保存头像...")
func _open_macos_avatar_file_dialog() -> bool:
if OS.get_name() != "macOS":
return false
var output: Array = []
var exitCode := OS.execute("/usr/bin/osascript", [
"-e",
"set selectedFile to choose file of type {\"public.image\"} with prompt \"选择头像图片\"",
"-e",
"POSIX path of selectedFile",
], output, true)
if exitCode != OK:
var errorText := "".join(PackedStringArray(output)).strip_edges()
if errorText.contains("User canceled") or errorText.contains("用户已取消"):
_on_native_avatar_file_selected(false, PackedStringArray(), 0)
else:
_set_account_status("macOS 文件选择器启动失败")
return true
var selectedPath := "".join(PackedStringArray(output)).strip_edges()
_on_native_avatar_file_selected(not selectedPath.is_empty(), PackedStringArray([selectedPath]) if not selectedPath.is_empty() else PackedStringArray(), 0)
return true
func _set_account_status(message: String) -> void:
if is_instance_valid(_statusLabel):
_statusLabel.text = message
func _sync_avatar_file_to_backend(path: String) -> bool:
var authManager := get_node_or_null("/root/AuthManager")
if authManager == null or not authManager.has_method("is_authenticated") or not bool(authManager.call("is_authenticated")):
return false
if not authManager.has_method("update_profile"):
return false
var image := Image.load_from_file(path)
if image == null:
return false
var side: int = mini(image.get_width(), image.get_height())
if side <= 0:
return false
var cropX: int = int((image.get_width() - side) * 0.5)
var cropY: int = int((image.get_height() - side) * 0.5)
var cropped := image.get_region(Rect2i(cropX, cropY, side, side))
cropped.resize(256, 256, Image.INTERPOLATE_LANCZOS)
if cropped.get_format() != Image.FORMAT_RGBA8:
cropped.convert(Image.FORMAT_RGBA8)
_avatarUploadPending = true
authManager.call("update_profile", {
"avatar_image_base64": Marshalls.raw_to_base64(cropped.save_png_to_buffer()),
"avatar_mime_type": "image/png",
})
return true
func _on_switch_account_pressed() -> void:
_logout_and_return_to_auth()
func _on_logout_pressed() -> void:
_logout_and_return_to_auth()
func _logout_and_return_to_auth() -> void:
var chatManager := get_node_or_null("/root/ChatManager")
if chatManager != null:
if chatManager.has_method("disconnect_from_chat_server"):
chatManager.call("disconnect_from_chat_server")
if chatManager.has_method("set_game_token"):
chatManager.call("set_game_token", "")
var authManager := get_node_or_null("/root/AuthManager")
if authManager != null and authManager.has_method("logout"):
authManager.call("logout")
var sceneManager := get_node_or_null("/root/SceneManager")
if sceneManager != null and sceneManager.has_method("change_scene"):
sceneManager.call("change_scene", "auth", false)
func _load_settings() -> void:
var settingsManager := _get_settings_manager()
if settingsManager != null and settingsManager.has_method("get_settings"):
var currentSettings: Variant = settingsManager.call("get_settings")
_settings = currentSettings.duplicate(true) if currentSettings is Dictionary else DEFAULT_SETTINGS.duplicate(true)
else:
_settings = DEFAULT_SETTINGS.duplicate(true)
_savedSettings = _settings.duplicate(true)
func _save_settings() -> void:
var settingsManager := _get_settings_manager()
if settingsManager != null and settingsManager.has_method("set_settings"):
settingsManager.call("set_settings", _settings, false)
else:
push_warning("SettingsPanel: SettingsManager autoload is not available.")
var authManager := get_node_or_null("/root/AuthManager")
if authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated")) and authManager.has_method("update_profile"):
authManager.call("update_profile", {
"settings": _settings.duplicate(true),
})
else:
push_warning("SettingsPanel: 未登录后端账号,设置仅在本次会话内生效。")
_savedSettings = _settings.duplicate(true)
func _restore_saved_settings() -> void:
_settings = _savedSettings.duplicate(true)
_apply_preview_settings()
if is_instance_valid(_content):
_render_content()
_update_category_visuals()
func _apply_preview_settings() -> void:
var settingsManager := _get_settings_manager()
if settingsManager != null and settingsManager.has_method("set_settings"):
settingsManager.call("set_settings", _settings, false)
func _load_current_user() -> void:
if is_instance_valid(_usernameLabel):
_usernameLabel.text = _current_username()
if is_instance_valid(_accountLabel):
_accountLabel.text = _current_account_text()
func _current_username() -> String:
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():
return username
return "玩家"
func _current_username_initial() -> String:
var username := _current_username()
return username.substr(0, 1).to_upper() if not username.is_empty() else ""
func _current_account_text() -> String:
var authManager := get_node_or_null("/root/AuthManager")
if authManager == null or not authManager.has_method("get_current_user"):
return "后端账号:未登录"
var user_variant: Variant = authManager.call("get_current_user")
if not (user_variant is Dictionary):
return "后端账号:未登录"
var user: Dictionary = user_variant
var userId := str(user.get("id", "")).strip_edges()
if userId.is_empty():
return "后端账号:已登录"
return "后端账号 ID%s" % userId
func _connection_status_text() -> String:
var chatManager := get_node_or_null("/root/ChatManager")
if chatManager == null:
return "聊天服务:未加载"
if chatManager.has_method("is_chat_connected"):
return "聊天服务:已连接" if bool(chatManager.call("is_chat_connected")) else "聊天服务:未连接"
return "聊天服务:可重新连接"
func _position_panel() -> void:
var viewportSize: Vector2 = get_viewport_rect().size
var width: float = min(PANEL_SIZE.x, max(760.0, viewportSize.x - MIN_PANEL_MARGIN.x * 2.0))
var height: float = min(PANEL_SIZE.y, max(620.0, viewportSize.y - MIN_PANEL_MARGIN.y * 2.0))
var shiftX: float = clampf(PANEL_SHIFT.x, -viewportSize.x * 0.08, viewportSize.x * 0.08)
var shiftY: float = clampf(PANEL_SHIFT.y, -viewportSize.y * 0.05, viewportSize.y * 0.05)
_panel.custom_minimum_size = Vector2(width, height)
_panel.set_anchors_preset(Control.PRESET_CENTER)
_panel.offset_left = -width * 0.5 + shiftX
_panel.offset_right = width * 0.5 + shiftX
_panel.offset_top = -height * 0.5 + shiftY
_panel.offset_bottom = height * 0.5 + shiftY
_position_badge()
func _position_badge() -> void:
if not is_instance_valid(_badge) or not is_instance_valid(_panel):
return
var panelCenterX: float = get_viewport_rect().size.x * 0.5 + (_panel.offset_left + _panel.offset_right) * 0.5
var panelTop: float = get_viewport_rect().size.y * 0.5 + _panel.offset_top
_badge.set_anchors_preset(Control.PRESET_TOP_LEFT)
_badge.offset_left = panelCenterX - 210.0
_badge.offset_top = panelTop - 74.0
_badge.offset_right = panelCenterX + 210.0
_badge.offset_bottom = panelTop + 62.0
func _animate_panel(opening: bool) -> void:
if is_instance_valid(_transitionTween):
_transitionTween.kill()
_transitionTween = create_tween()
_transitionTween.set_parallel(true)
if opening:
_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).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
return
_transitionTween.tween_property(_overlay, "modulate:a", 0.0, 0.14)
_transitionTween.tween_property(_panel, "modulate:a", 0.0, 0.14)
_transitionTween.tween_property(_panel, "scale", Vector2(0.985, 0.985), 0.14)
_transitionTween.finished.connect(func() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_process(false)
_update_panel_position_and_alpha(0.0, 0.97)
)
func _update_panel_position_and_alpha(overlayAlpha: float, panelAlpha: float) -> void:
_position_panel()
_overlay.modulate.a = overlayAlpha
_panel.modulate.a = panelAlpha
_panel.scale = Vector2(0.985, 0.985)
_panel.pivot_offset = PANEL_SIZE * 0.5
func _release_movement_input_state() -> void:
Input.flush_buffered_events()
_release_movement_actions()
func _release_movement_actions() -> void:
for action in MOVEMENT_ACTIONS:
Input.action_release(action)
func _clear_children(node: Node) -> void:
for child in node.get_children():
child.queue_free()
func _format_percent(value: float) -> String:
return "%d%%" % int(round(clampf(value, 0.0, 1.0) * 100.0))
func _create_panel_style(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 = 8
style.content_margin_top = 8
style.content_margin_right = 8
style.content_margin_bottom = 8
if shadow:
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.18)
style.shadow_size = 24
style.shadow_offset = Vector2(0, 9)
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
style.content_margin_left = 10
style.content_margin_top = 6
style.content_margin_right = 10
style.content_margin_bottom = 6
return style
func _create_button_style(color: Color, radius: int, borderColor: Color) -> StyleBoxFlat:
var style := _create_round_style(color, radius)
if borderColor.a > 0.0:
style.border_color = borderColor
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
return style
func _default_avatar_picker_dir() -> String:
var picturesDir := OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
if not picturesDir.strip_edges().is_empty() and DirAccess.dir_exists_absolute(picturesDir):
return picturesDir
var desktopDir := OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
if not desktopDir.strip_edges().is_empty() and DirAccess.dir_exists_absolute(desktopDir):
return desktopDir
return OS.get_user_data_dir()
func _get_event_system() -> Node:
return get_node_or_null("/root/EventSystem")
func _get_appearance_manager() -> Node:
return get_node_or_null("/root/AppearanceManager")
func _get_settings_manager() -> Node:
return get_node_or_null("/root/SettingsManager")
func _apply_current_avatar_to_panel(panel: PanelContainer, label: Label, fallbackText: String) -> void:
var appearanceManager := _get_appearance_manager()
if appearanceManager != null and appearanceManager.has_method("apply_avatar_to_panel"):
appearanceManager.call("apply_avatar_to_panel", panel, label, "", fallbackText)
return
panel.add_theme_stylebox_override("panel", _create_round_style(ACCENT_COLOR, 14))
if label != null:
label.text = fallbackText
func _subscribe_to_events() -> void:
var eventSystem := _get_event_system()
if eventSystem == null:
push_warning("SettingsPanel: EventSystem autoload is not available.")
return
eventSystem.call("connect_event", EventNames.HUD_SETTINGS_REQUESTED, _on_settings_requested, self)
eventSystem.call("connect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self)
eventSystem.call("connect_event", EventNames.APPEARANCE_PROFILE_CHANGED, _on_appearance_profile_changed, self)
func _on_appearance_avatar_changed(_data: Variant = null) -> void:
_avatarUploadPending = false
if _isOpen:
_render_content()
_update_category_visuals()
func _on_appearance_profile_changed(_data: Variant = null) -> void:
if _avatarUploadPending:
_avatarUploadPending = false
if _isOpen:
_render_content()
_update_category_visuals()