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()