1389 lines
54 KiB
GDScript
1389 lines
54 KiB
GDScript
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()
|