using System; using System.Threading; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Managers.Interaction; using BriarQueen.Framework.Managers.UI.Base; using BriarQueen.Framework.Managers.UI.Events; using BriarQueen.Framework.Services.Settings; using BriarQueen.Framework.Services.Settings.Data; using BriarQueen.Game.Effects; using BriarQueen.UI.Menus.Components; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using VContainer; using AudioSettings = BriarQueen.Framework.Services.Settings.Data.AudioSettings; namespace BriarQueen.UI.Menus { public class SettingsWindow : MonoBehaviour, IUIWindow, IUIBackHandler { // ── Layout ──────────────────────────────────────────────────── [Header("Layout")] [SerializeField] private CanvasGroup _canvasGroup; [SerializeField] private RectTransform _windowRect; // ── Fog ─────────────────────────────────────────────────────── [Header("Fog")] [SerializeField] private UIFogReveal _fog; // ── Category buttons ────────────────────────────────────────── [Header("Category Buttons")] [SerializeField] private CanvasGroup _panelBordersGroup; [SerializeField] private CanvasGroup _leftColumnGroup; [SerializeField] private AnimatedSelectionButtonGroup _categoryButtonGroup; [SerializeField] private AnimatedSelectionButton _gameCategoryButton; [SerializeField] private AnimatedSelectionButton _visualCategoryButton; [SerializeField] private AnimatedSelectionButton _audioCategoryButton; // ── Category panels ─────────────────────────────────────────── [Header("Category Panels")] [SerializeField] private CanvasGroup _gamePanel; [SerializeField] private CanvasGroup _visualPanel; [SerializeField] private CanvasGroup _audioPanel; // ── Window buttons ──────────────────────────────────────────── [Header("Buttons")] [SerializeField] private Button _applyButton; [SerializeField] private Button _backButton; // ── Game settings ───────────────────────────────────────────── [Header("Game")] [SerializeField] private Slider _popupDisplayDurationSlider; [SerializeField] private TextMeshProUGUI _popupDisplayDurationText; [SerializeField] private Toggle _tutorialsEnabledToggle; [SerializeField] private Toggle _tooltipsEnabledToggle; [SerializeField] private Toggle _autoUseToolsToggle; // ── Visual settings ─────────────────────────────────────────── [Header("Visual")] [SerializeField] private Toggle _fullscreenToggle; [SerializeField] private Slider _vsyncSlider; [SerializeField] private TextMeshProUGUI _vsyncValueText; [SerializeField] private Slider _maxFramerateSlider; [SerializeField] private TextMeshProUGUI _maxFramerateValueText; // ── Audio settings ──────────────────────────────────────────── [Header("Audio")] [SerializeField] private Slider _masterVolumeSlider; [SerializeField] private TextMeshProUGUI _masterVolumeText; [SerializeField] private Slider _musicVolumeSlider; [SerializeField] private TextMeshProUGUI _musicVolumeText; [SerializeField] private Slider _sfxVolumeSlider; [SerializeField] private TextMeshProUGUI _sfxVolumeText; [SerializeField] private Slider _voiceVolumeSlider; [SerializeField] private TextMeshProUGUI _voiceVolumeText; // ── Pending changes ─────────────────────────────────────────── [Header("Unsaved Changes Warning")] [SerializeField] private TMP_Text _pendingChangesText; [SerializeField] private string _pendingChangesMessage = "You have unsaved changes."; // ── Discard confirm ─────────────────────────────────────────── [Header("Confirm Unapplied Changes")] [SerializeField] private ConfirmActionWindow _confirmUnappliedChangesWindow; [SerializeField] private string _discardChangesTitle = "Discard changes?"; [SerializeField] private string _discardChangesMessage = "You have unapplied settings. Leave without applying them?"; [SerializeField] private string _discardChangesConfirmText = "Discard"; [SerializeField] private string _discardChangesCancelText = "Cancel"; // ── Tween ───────────────────────────────────────────────────── [Header("Tween Settings")] [SerializeField] private TweenSettings _panelFadeTweenSettings = new() { duration = 0.25f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private float _panelCrossFadeDuration = 0.15f; // ── Slider ranges ───────────────────────────────────────────── [Header("Game Slider Ranges")] [SerializeField] private float _popupDisplayDurationMin = 1f; [SerializeField] private float _popupDisplayDurationMax = 10f; [Header("Visual Slider Ranges")] [SerializeField] private int _maxFramerateMin = 30; [SerializeField] private int _maxFramerateMax = 240; // ── Selection ───────────────────────────────────────────────── [Header("Selection")] [SerializeField] private Selectable _firstSelectedOnOpen; [Header("Window System")] [SerializeField] private GraphicRaycaster _graphicRaycaster; // ── Runtime state ───────────────────────────────────────────── private enum Category { Game, Visual, Audio } private Category _activeCategory = Category.Game; private CancellationTokenSource _cts; private CancellationTokenSource _panelCts; private Sequence _panelSequence; private AudioSettings _draftAudio; private GameSettings _draftGameSettings; private VisualSettings _draftVisual; private AudioSettings _loadedAudio; private GameSettings _loadedGameSettings; private VisualSettings _loadedVisual; private EventCoordinator _eventCoordinator; private SettingsService _settingsService; private bool _ignoreUiCallbacks; private bool _waitingChanges; private bool _raycasterRegistered; private InteractManager _interactManager; // ── IUIWindow ───────────────────────────────────────────────── public bool IsModal => true; public WindowType WindowType => WindowType.SettingsWindow; public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest; // ── Unity lifecycle ─────────────────────────────────────────── private void Awake() { if (_applyButton != null) _applyButton.onClick.AddListener(OnApplyClicked); if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked); // Individual buttons drive panel switching via SelectionRequested if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested += OnGameCategorySelected; if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += OnVisualCategorySelected; if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += OnAudioCategorySelected; HookSlider(_masterVolumeSlider, _masterVolumeText, v => _draftAudio.MasterVolume = v); HookSlider(_musicVolumeSlider, _musicVolumeText, v => _draftAudio.MusicVolume = v); HookSlider(_sfxVolumeSlider, _sfxVolumeText, v => _draftAudio.SfxVolume = v); HookSlider(_voiceVolumeSlider, _voiceVolumeText, v => _draftAudio.VoiceVolume = v); SetupGameSliders(); if (_popupDisplayDurationSlider != null) _popupDisplayDurationSlider.onValueChanged.AddListener(OnPopupDisplayDurationChanged); if (_tutorialsEnabledToggle != null) _tutorialsEnabledToggle.onValueChanged.AddListener(OnTutorialsToggleChanged); if (_tooltipsEnabledToggle != null) _tooltipsEnabledToggle.onValueChanged.AddListener(OnTooltipsToggleChanged); if (_autoUseToolsToggle != null) _autoUseToolsToggle.onValueChanged.AddListener(OnAutoToolsToggleChanged); if (_fullscreenToggle != null) _fullscreenToggle.onValueChanged.AddListener(OnFullscreenToggleChanged); SetupVsyncSlider(); if (_vsyncSlider != null) _vsyncSlider.onValueChanged.AddListener(OnVsyncSliderChanged); SetupMaxFramerateSlider(); if (_maxFramerateSlider != null) _maxFramerateSlider.onValueChanged.AddListener(OnMaxFramerateSliderChanged); SetPendingChangesVisible(false); SetPanelImmediate(_gamePanel, 0f, false); SetPanelImmediate(_visualPanel, 0f, false); SetPanelImmediate(_audioPanel, 0f, false); HideCategoryButtonsAndPanel(); _graphicRaycaster = GetComponent(); } private void OnDestroy() { StopAndResetCancellation(); CancelAndDisposePanelCts(); TryUnregisterRaycaster(); if (_applyButton != null) _applyButton.onClick.RemoveListener(OnApplyClicked); if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked); if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested -= OnGameCategorySelected; if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= OnVisualCategorySelected; if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= OnAudioCategorySelected; if (_popupDisplayDurationSlider != null) _popupDisplayDurationSlider.onValueChanged.RemoveListener(OnPopupDisplayDurationChanged); if (_tutorialsEnabledToggle != null) _tutorialsEnabledToggle.onValueChanged.RemoveListener(OnTutorialsToggleChanged); if (_tooltipsEnabledToggle != null) _tooltipsEnabledToggle.onValueChanged.RemoveListener(OnTooltipsToggleChanged); if (_autoUseToolsToggle != null) _autoUseToolsToggle.onValueChanged.RemoveListener(OnAutoToolsToggleChanged); if (_fullscreenToggle != null) _fullscreenToggle.onValueChanged.RemoveListener(OnFullscreenToggleChanged); if (_vsyncSlider != null) _vsyncSlider.onValueChanged.RemoveListener(OnVsyncSliderChanged); if (_maxFramerateSlider != null) _maxFramerateSlider.onValueChanged.RemoveListener(OnMaxFramerateSliderChanged); } // ── IUIWindow ───────────────────────────────────────────────── public async UniTask Show() { if (_settingsService != null) LoadSettings(_settingsService.Audio, _settingsService.Visual, _settingsService.Game); StopAndResetCancellation(); gameObject.SetActive(true); EnsureExclusiveRaycaster(); _canvasGroup.alpha = 1f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; SetPanelImmediate(_gamePanel, 0f, false); SetPanelImmediate(_visualPanel, 0f, false); SetPanelImmediate(_audioPanel, 0f, false); HideCategoryButtonsAndPanel(); _fog?.FogReset(); try { // Step 1: fog rolls in revealing the background if (_fog != null) await _fog.FogIn().AttachExternalCancellation(_cts.Token); // Step 2: fade in category buttons await FadeCategoryButtonsAndPanel(1f, _cts.Token); // Step 3: reset to game category via the group — handles // selection state and event system sync automatically _activeCategory = Category.Game; _categoryButtonGroup?.SelectButton(_gameCategoryButton, instant: true); // Step 4: fade in game panel _panelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_gamePanel, new TweenSettings { startValue = 0f, endValue = 1f, settings = _panelFadeTweenSettings })); await _panelSequence.ToUniTask(cancellationToken: _cts.Token); SetPanelImmediate(_gamePanel, 1f, true); Debug.Log($"[SettingsWindow] After SetPanelImmediate — interactable:{_gamePanel.interactable} blocksRaycasts:{_gamePanel.blocksRaycasts}"); } catch (OperationCanceledException) { return; } finally { _panelSequence = default; } _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = true; SelectDefault(); } public async UniTask Hide() { StopAndResetCancellation(); _confirmUnappliedChangesWindow?.CloseImmediate(); _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = false; try { // Step 1: fade out active panel var activePanel = GetPanel(_activeCategory); if (activePanel != null) { _panelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(activePanel, new TweenSettings { startValue = activePanel.alpha, endValue = 0f, settings = _panelFadeTweenSettings })); await _panelSequence.ToUniTask(cancellationToken: _cts.Token); SetPanelImmediate(activePanel, 0f, false); } // Step 2: fade out category buttons await FadeCategoryButtonsAndPanel(0f, _cts.Token); // Step 3: fog rolls out if (_fog != null) await _fog.FogOut().AttachExternalCancellation(_cts.Token); } catch (OperationCanceledException) { return; } finally { _panelSequence = default; } _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; TryUnregisterRaycaster(); gameObject.SetActive(false); } // ── DI ──────────────────────────────────────────────────────── [Inject] public void Construct(SettingsService settingsService, EventCoordinator eventCoordinator, InteractManager interactManager) { _settingsService = settingsService; _eventCoordinator = eventCoordinator; _interactManager = interactManager; } // ── IUIBackHandler ──────────────────────────────────────────── public bool HandleBackRequest() { TryCloseFromBack(); return true; } private void OnGameCategorySelected(AnimatedSelectionButton _) { SwitchCategory(Category.Game); } private void OnVisualCategorySelected(AnimatedSelectionButton _) { SwitchCategory(Category.Visual); } private void OnAudioCategorySelected(AnimatedSelectionButton _) { SwitchCategory(Category.Audio); } // ── Category switching ──────────────────────────────────────── private void SwitchCategory(Category category) { if (category == _activeCategory) return; var outgoing = GetPanel(_activeCategory); var incoming = GetPanel(category); _activeCategory = category; // Group handles the button highlight state — we just switch the panel CrossFadePanels(outgoing, incoming).Forget(); } private async UniTaskVoid CrossFadePanels(CanvasGroup outgoing, CanvasGroup incoming) { CancelAndDisposePanelCts(); _panelCts = new CancellationTokenSource(); if (outgoing != null) SetPanelInteractivity(outgoing, false); if (incoming != null) SetPanelInteractivity(incoming, false); try { if (outgoing != null) { await Tween.Alpha(outgoing, new TweenSettings { startValue = outgoing.alpha, endValue = 0f, settings = new TweenSettings { duration = _panelCrossFadeDuration, ease = Ease.OutSine, useUnscaledTime = true } }).ToUniTask(cancellationToken: _panelCts.Token); SetPanelImmediate(outgoing, 0f, false); } if (incoming != null) { incoming.alpha = 0f; await Tween.Alpha(incoming, new TweenSettings { startValue = 0f, endValue = 1f, settings = new TweenSettings { duration = _panelCrossFadeDuration, ease = Ease.InSine, useUnscaledTime = true } }).ToUniTask(cancellationToken: _panelCts.Token); SetPanelImmediate(incoming, 1f, true); } } catch (OperationCanceledException) { // Window closed mid-transition — fine } finally { if (_panelCts != null) { _panelCts.Dispose(); _panelCts = null; } } } private CanvasGroup GetPanel(Category category) => category switch { Category.Game => _gamePanel, Category.Visual => _visualPanel, Category.Audio => _audioPanel, _ => null }; private static void SetPanelImmediate(CanvasGroup panel, float alpha, bool interactable) { if (panel == null) return; panel.alpha = alpha; panel.interactable = interactable; panel.blocksRaycasts = interactable; Debug.Log($"[SetPanelImmediate] {panel.gameObject.name} — alpha:{alpha} interactable:{interactable} blocksRaycasts:{interactable}"); } private static void SetPanelInteractivity(CanvasGroup panel, bool interactable) { if (panel == null) return; panel.interactable = interactable; panel.blocksRaycasts = interactable; } private void HideCategoryButtonsAndPanel() { if (_leftColumnGroup == null) return; _leftColumnGroup.alpha = 0f; _leftColumnGroup.interactable = false; _leftColumnGroup.blocksRaycasts = false; if(_panelBordersGroup == null) return; _panelBordersGroup.alpha = 0f; _panelBordersGroup.interactable = false; _panelBordersGroup.blocksRaycasts = false; } private async UniTask FadeCategoryButtonsAndPanel(float target, CancellationToken token) { if (_leftColumnGroup == null) return; await Tween.Alpha(_leftColumnGroup, new TweenSettings { startValue = _leftColumnGroup.alpha, endValue = target, settings = _panelFadeTweenSettings }).ToUniTask(cancellationToken: token); _leftColumnGroup.alpha = target; _leftColumnGroup.interactable = target > 0f; _leftColumnGroup.blocksRaycasts = target > 0f; if(_panelBordersGroup == null) return; await Tween.Alpha(_panelBordersGroup, new TweenSettings { startValue = _panelBordersGroup.alpha, endValue = target, settings = _panelFadeTweenSettings }).ToUniTask(cancellationToken: token); _panelBordersGroup.alpha = target; _panelBordersGroup.interactable = target > 0f; _panelBordersGroup.blocksRaycasts = target > 0f; } // ── Settings callbacks ──────────────────────────────────────── private void OnTutorialsToggleChanged(bool value) { if (_ignoreUiCallbacks) return; _draftGameSettings.TutorialsEnabled = value; MarkDirty(); } private void OnTooltipsToggleChanged(bool value) { if (_ignoreUiCallbacks) return; _draftGameSettings.TooltipsEnabled = value; MarkDirty(); } private void OnAutoToolsToggleChanged(bool value) { if (_ignoreUiCallbacks) return; _draftGameSettings.AutoUseTools = value; MarkDirty(); } private void OnPopupDisplayDurationChanged(float value) { if (_ignoreUiCallbacks) return; _draftGameSettings.PopupDisplayDuration = Mathf.Clamp(value, _popupDisplayDurationMin, _popupDisplayDurationMax); UpdateSecondsLabel(_popupDisplayDurationText, _draftGameSettings.PopupDisplayDuration); MarkDirty(); } private void OnFullscreenToggleChanged(bool isFullscreen) { if (_ignoreUiCallbacks) return; _draftVisual.FullScreenMode = isFullscreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed; MarkDirty(); } private void OnVsyncSliderChanged(float value) { if (_ignoreUiCallbacks) return; var v = Mathf.Clamp(Mathf.RoundToInt(value), 0, 2); _draftVisual.VSyncCount = v; UpdateVsyncLabel(v); MarkDirty(); } private void OnMaxFramerateSliderChanged(float value) { if (_ignoreUiCallbacks) return; var fps = Mathf.Clamp(Mathf.RoundToInt(value), _maxFramerateMin, _maxFramerateMax); _draftVisual.MaxFramerate = fps; UpdateMaxFramerateLabel(fps); MarkDirty(); } private void OnApplyClicked() { _loadedAudio = CloneAudio(_draftAudio); _loadedVisual = CloneVisual(_draftVisual); _loadedGameSettings = CloneGameSettings(_draftGameSettings); _settingsService.Apply(_loadedAudio, _loadedVisual, _loadedGameSettings); _waitingChanges = false; SetPendingChangesVisible(false); } private void OnBackClicked() { TryCloseFromBack(); } // ── Back / close ────────────────────────────────────────────── private void TryCloseFromBack() { if (_confirmUnappliedChangesWindow != null && _confirmUnappliedChangesWindow.IsOpen) { _confirmUnappliedChangesWindow.CancelFromBack(); return; } if (!HasUnappliedChanges()) { CloseSettingsWindow(); return; } OpenDiscardChangesConfirm(); } private void OpenDiscardChangesConfirm() { if (_confirmUnappliedChangesWindow == null) { DiscardUnappliedChangesAndClose(); return; } _confirmUnappliedChangesWindow.Open( _discardChangesTitle, _discardChangesMessage, _discardChangesConfirmText, _discardChangesCancelText, DiscardUnappliedChangesAndClose, SelectDefault); } private void DiscardUnappliedChangesAndClose() { _draftAudio = CloneAudio(_loadedAudio); _draftVisual = CloneVisual(_loadedVisual); _draftGameSettings = CloneGameSettings(_loadedGameSettings); ApplySettingsToUI(); CloseSettingsWindow(); } private void CloseSettingsWindow() { _eventCoordinator.PublishImmediate(new UIToggleSettingsWindow(false)); } // ── Settings loading / UI sync ──────────────────────────────── private void LoadSettings(AudioSettings audio, VisualSettings visual, GameSettings game) { _loadedAudio = CloneAudio(audio ?? new AudioSettings()); _loadedVisual = CloneVisual(visual ?? new VisualSettings()); _loadedGameSettings = CloneGameSettings(game ?? new GameSettings()); _draftAudio = CloneAudio(_loadedAudio); _draftVisual = CloneVisual(_loadedVisual); _draftGameSettings = CloneGameSettings(_loadedGameSettings); ApplySettingsToUI(); } private void ApplySettingsToUI() { _ignoreUiCallbacks = true; SetSlider(_masterVolumeSlider, _masterVolumeText, _draftAudio.MasterVolume); SetSlider(_musicVolumeSlider, _musicVolumeText, _draftAudio.MusicVolume); SetSlider(_sfxVolumeSlider, _sfxVolumeText, _draftAudio.SfxVolume); SetSlider(_voiceVolumeSlider, _voiceVolumeText, _draftAudio.VoiceVolume); if (_tutorialsEnabledToggle != null) _tutorialsEnabledToggle.isOn = _draftGameSettings.TutorialsEnabled; if (_tooltipsEnabledToggle != null) _tooltipsEnabledToggle.isOn = _draftGameSettings.TooltipsEnabled; if (_autoUseToolsToggle != null) _autoUseToolsToggle.isOn = _draftGameSettings.AutoUseTools; if (_fullscreenToggle != null) _fullscreenToggle.isOn = _draftVisual.FullScreenMode != FullScreenMode.Windowed; if (_vsyncSlider != null) { _vsyncSlider.value = Mathf.Clamp(_draftVisual.VSyncCount, 0, 2); UpdateVsyncLabel(_draftVisual.VSyncCount); } if (_maxFramerateSlider != null) { var fps = Mathf.Clamp(_draftVisual.MaxFramerate, _maxFramerateMin, _maxFramerateMax); _maxFramerateSlider.value = fps; UpdateMaxFramerateLabel(fps); } if (_popupDisplayDurationSlider != null) { _popupDisplayDurationSlider.value = _draftGameSettings.PopupDisplayDuration; UpdateSecondsLabel(_popupDisplayDurationText, _draftGameSettings.PopupDisplayDuration); } _ignoreUiCallbacks = false; _waitingChanges = HasUnappliedChanges(); SetPendingChangesVisible(_waitingChanges); } // ── UI helpers ──────────────────────────────────────────────── private void SetupGameSliders() { if (_popupDisplayDurationSlider == null) return; _popupDisplayDurationSlider.minValue = _popupDisplayDurationMin; _popupDisplayDurationSlider.maxValue = _popupDisplayDurationMax; _popupDisplayDurationSlider.wholeNumbers = false; } private void SetupVsyncSlider() { if (_vsyncSlider == null) return; _vsyncSlider.minValue = 0; _vsyncSlider.maxValue = 2; _vsyncSlider.wholeNumbers = true; UpdateVsyncLabel((int)_vsyncSlider.value); } private void SetupMaxFramerateSlider() { if (_maxFramerateSlider == null) return; _maxFramerateSlider.minValue = _maxFramerateMin; _maxFramerateSlider.maxValue = _maxFramerateMax; _maxFramerateSlider.wholeNumbers = true; UpdateMaxFramerateLabel(Mathf.RoundToInt(_maxFramerateSlider.value)); } private void HookSlider(Slider slider, TextMeshProUGUI label, Action assignToDraft) { if (slider == null) return; slider.minValue = 0f; slider.maxValue = 1f; slider.onValueChanged.AddListener(v => { if (_ignoreUiCallbacks) return; var clamped = Mathf.Clamp01(v); assignToDraft?.Invoke(clamped); UpdatePercentLabel(label, clamped); MarkDirty(); }); } private void SetSlider(Slider slider, TextMeshProUGUI label, float value) { if (slider != null) slider.value = Mathf.Clamp01(value); UpdatePercentLabel(label, value); } private void UpdatePercentLabel(TextMeshProUGUI label, float value) { if (label == null) return; label.text = $"{Mathf.RoundToInt(value * 100f)}%"; } private void UpdateSecondsLabel(TextMeshProUGUI label, float value) { if (label == null) return; label.text = $"{value:0.0}s"; } private void UpdateVsyncLabel(int v) { if (_vsyncValueText == null) return; _vsyncValueText.text = v switch { 0 => "Off", 1 => "On", 2 => "Half", _ => "On" }; } private void UpdateMaxFramerateLabel(int fps) { if (_maxFramerateValueText == null) return; _maxFramerateValueText.text = $"{fps} FPS"; } private void MarkDirty() { if (_ignoreUiCallbacks) return; _waitingChanges = HasUnappliedChanges(); SetPendingChangesVisible(_waitingChanges); } private bool HasUnappliedChanges() { return !AudioEquals(_draftAudio, _loadedAudio) || !VisualEquals(_draftVisual, _loadedVisual) || !GameEquals(_draftGameSettings, _loadedGameSettings); } private void SetPendingChangesVisible(bool visible) { if (_pendingChangesText == null) return; _pendingChangesText.gameObject.SetActive(visible); if (visible) _pendingChangesText.text = _pendingChangesMessage; } private void SelectDefault() { if (EventSystem.current == null) return; if (_firstSelectedOnOpen != null) EventSystem.current.SetSelectedGameObject(_firstSelectedOnOpen.gameObject); else if (_gameCategoryButton != null) EventSystem.current.SetSelectedGameObject(_gameCategoryButton.GetSelectableGameObject()); } // ── CTS helpers ─────────────────────────────────────────────── private void StopAndResetCancellation() { if (_panelSequence.isAlive) _panelSequence.Stop(); if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); } _cts = new CancellationTokenSource(); } private void CancelAndDisposePanelCts() { if (_panelCts == null) return; try { _panelCts.Cancel(); } catch { } _panelCts.Dispose(); _panelCts = null; } // ── Equality / clone helpers ────────────────────────────────── private static bool AudioEquals(AudioSettings a, AudioSettings b) { if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; const float eps = 0.0001f; return Mathf.Abs(a.MasterVolume - b.MasterVolume) < eps && Mathf.Abs(a.MusicVolume - b.MusicVolume) < eps && Mathf.Abs(a.SfxVolume - b.SfxVolume) < eps && Mathf.Abs(a.VoiceVolume - b.VoiceVolume) < eps; } private static bool VisualEquals(VisualSettings a, VisualSettings b) { if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; return a.FullScreenMode == b.FullScreenMode && a.VSyncCount == b.VSyncCount && a.MaxFramerate == b.MaxFramerate; } private static bool GameEquals(GameSettings a, GameSettings b) { if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; const float eps = 0.0001f; return Mathf.Abs(a.PopupDisplayDuration - b.PopupDisplayDuration) < eps && a.TutorialsEnabled == b.TutorialsEnabled && a.TooltipsEnabled == b.TooltipsEnabled && a.AutoUseTools == b.AutoUseTools; } private static AudioSettings CloneAudio(AudioSettings a) { a ??= new AudioSettings(); return new AudioSettings { MasterVolume = a.MasterVolume, MusicVolume = a.MusicVolume, SfxVolume = a.SfxVolume, VoiceVolume = a.VoiceVolume, }; } private static VisualSettings CloneVisual(VisualSettings v) { v ??= new VisualSettings(); return new VisualSettings { FullScreenMode = v.FullScreenMode, VSyncCount = v.VSyncCount, MaxFramerate = v.MaxFramerate <= 0 ? 120 : v.MaxFramerate }; } private static GameSettings CloneGameSettings(GameSettings g) { g ??= new GameSettings(); return new GameSettings { PopupDisplayDuration = g.PopupDisplayDuration, TutorialsEnabled = g.TutorialsEnabled, TooltipsEnabled = g.TooltipsEnabled, AutoUseTools = g.AutoUseTools }; } private void EnsureExclusiveRaycaster() { if (_interactManager == null || _graphicRaycaster == null) return; if (!_raycasterRegistered) { _interactManager.AddUIRaycaster(_graphicRaycaster); _raycasterRegistered = true; } _interactManager.SetExclusiveRaycaster(_graphicRaycaster); } private void TryUnregisterRaycaster() { if (!_raycasterRegistered) return; if (_interactManager == null || _graphicRaycaster == null) return; _interactManager.RemoveUIRaycaster(_graphicRaycaster); _interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster); _raycasterRegistered = false; } } }