using System; using System.Threading; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Managers.UI.Base; using BriarQueen.Framework.Managers.UI.Events; using BriarQueen.Framework.Services.Settings; using BriarQueen.Framework.Services.Settings.Data; 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 { [Header("UI Elements")] [SerializeField] private CanvasGroup _canvasGroup; [SerializeField] private RectTransform _windowRect; [Header("Buttons")] [SerializeField] private Button _applyButton; [SerializeField] private Button _backButton; [Header("Game")] [SerializeField] private Slider _popupDisplayDurationSlider; [SerializeField] private TextMeshProUGUI _popupDisplayDurationText; [SerializeField] private Toggle _tutorialsEnabledToggle; [SerializeField] private Toggle _tooltipsEnabledToggle; [SerializeField] private Toggle _autoUseToolsToggle; [Header("Visual")] [SerializeField] private Toggle _fullscreenToggle; [Tooltip("0 = VSync Off, 1 = Every V-Blank, 2 = Every 2nd V-Blank")] [SerializeField] private Slider _vsyncSlider; [SerializeField] private TextMeshProUGUI _vsyncValueText; [SerializeField] private Slider _maxFramerateSlider; [SerializeField] private TextMeshProUGUI _maxFramerateValueText; [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; [SerializeField] private Slider _ambienceVolumeSlider; [SerializeField] private TextMeshProUGUI _ambienceVolumeText; [SerializeField] private Slider _uiVolumeSlider; [SerializeField] private TextMeshProUGUI _uiVolumeText; [SerializeField] private Toggle _muteWhenUnfocusedToggle; [Header("Unsaved Changes Warning")] [SerializeField] private TMP_Text _pendingChangesText; [SerializeField] private string _pendingChangesMessage = "You have unsaved changes."; [Header("Tween Settings")] [SerializeField] private TweenSettings _tweenSettings = new() { duration = 0.25f, ease = Ease.InOutSine, useUnscaledTime = true }; [Header("Scale")] [SerializeField] private float _hiddenScale = 0.85f; [Header("Selection")] [SerializeField] private Selectable _firstSelectedOnOpen; [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; private CancellationTokenSource _cts; private AudioSettings _draftAudio; private GameSettings _draftGameSettings; private VisualSettings _draftVisual; private EventCoordinator _eventCoordinator; private bool _ignoreUiCallbacks; private AudioSettings _loadedAudio; private GameSettings _loadedGameSettings; private VisualSettings _loadedVisual; private Sequence _sequence; private SettingsService _settingsService; private bool _waitingChanges; public bool IsModal => true; public WindowType WindowType => WindowType.SettingsWindow; private void Awake() { if (_applyButton != null) _applyButton.onClick.AddListener(OnApplyClicked); if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked); 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); HookSlider(_ambienceVolumeSlider, _ambienceVolumeText, v => _draftAudio.AmbienceVolume = v); HookSlider(_uiVolumeSlider, _uiVolumeText, v => _draftAudio.UIVolume = 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 (_muteWhenUnfocusedToggle != null) _muteWhenUnfocusedToggle.onValueChanged.AddListener(OnMuteWhenUnfocusedChanged); 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); } private void OnDestroy() { StopAndResetCancellation(); if (_sequence.isAlive) _sequence.Stop(); if (_applyButton != null) _applyButton.onClick.RemoveListener(OnApplyClicked); if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked); if (_popupDisplayDurationSlider != null) _popupDisplayDurationSlider.onValueChanged.RemoveListener(OnPopupDisplayDurationChanged); if (_tutorialsEnabledToggle != null) _tutorialsEnabledToggle.onValueChanged.RemoveListener(OnTutorialsToggleChanged); if (_tooltipsEnabledToggle != null) _tooltipsEnabledToggle.onValueChanged.RemoveListener(OnTooltipsToggleChanged); if (_muteWhenUnfocusedToggle != null) _muteWhenUnfocusedToggle.onValueChanged.RemoveListener(OnMuteWhenUnfocusedChanged); if (_fullscreenToggle != null) _fullscreenToggle.onValueChanged.RemoveListener(OnFullscreenToggleChanged); if (_vsyncSlider != null) _vsyncSlider.onValueChanged.RemoveListener(OnVsyncSliderChanged); if (_maxFramerateSlider != null) _maxFramerateSlider.onValueChanged.RemoveListener(OnMaxFramerateSliderChanged); } public async UniTask Show() { if (_settingsService != null) LoadSettings(_settingsService.Audio, _settingsService.Visual, _settingsService.Game); StopAndResetCancellation(); gameObject.SetActive(true); _windowRect.localScale = Vector3.one * _hiddenScale; _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; var alpha = new TweenSettings { startValue = 0f, endValue = 1f, settings = _tweenSettings }; var scale = new TweenSettings { startValue = Vector3.one * _hiddenScale, endValue = Vector3.one, settings = _tweenSettings }; _sequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, alpha)) .Group(Tween.Scale(_windowRect, scale)); try { await _sequence.ToUniTask(cancellationToken: _cts.Token); } catch (OperationCanceledException) { return; } _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = true; SelectDefault(); } public async UniTask Hide() { StopAndResetCancellation(); _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; var alpha = new TweenSettings { startValue = _canvasGroup.alpha, endValue = 0f, settings = _tweenSettings }; var scale = new TweenSettings { startValue = _windowRect.localScale, endValue = Vector3.one * _hiddenScale, settings = _tweenSettings }; _sequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, alpha)) .Group(Tween.Scale(_windowRect, scale)); try { await _sequence.ToUniTask(cancellationToken: _cts.Token); } catch (OperationCanceledException) { return; } gameObject.SetActive(false); } [Inject] public void Construct(SettingsService settingsService, EventCoordinator eventCoordinator) { _settingsService = settingsService; _eventCoordinator = eventCoordinator; } 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 OnMuteWhenUnfocusedChanged(bool value) { if (_ignoreUiCallbacks) return; _draftAudio.MuteWhenUnfocused = value; 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() { if (HasUnappliedChanges()) { _draftAudio = CloneAudio(_loadedAudio); _draftVisual = CloneVisual(_loadedVisual); _draftGameSettings = CloneGameSettings(_loadedGameSettings); ApplySettingsToUI(); } _eventCoordinator.PublishImmediate(new UIToggleSettingsWindow(false)); } private void StopAndResetCancellation() { if (_sequence.isAlive) _sequence.Stop(); if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); } _cts = new CancellationTokenSource(); } private void SelectDefault() { if (EventSystem.current == null) return; if (_firstSelectedOnOpen != null) EventSystem.current.SetSelectedGameObject(_firstSelectedOnOpen.gameObject); else if (_applyButton != null) EventSystem.current.SetSelectedGameObject(_applyButton.gameObject); } 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 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); SetSlider(_ambienceVolumeSlider, _ambienceVolumeText, _draftAudio.AmbienceVolume); SetSlider(_uiVolumeSlider, _uiVolumeText, _draftAudio.UIVolume); if (_tutorialsEnabledToggle != null) _tutorialsEnabledToggle.isOn = _draftGameSettings.TutorialsEnabled; if (_tooltipsEnabledToggle != null) _tooltipsEnabledToggle.isOn = _draftGameSettings.TooltipsEnabled; if(_autoUseToolsToggle != null) _autoUseToolsToggle.isOn = _draftGameSettings.AutoUseTools; if (_muteWhenUnfocusedToggle != null) _muteWhenUnfocusedToggle.isOn = _draftAudio.MuteWhenUnfocused; 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); } 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 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 && Mathf.Abs(a.AmbienceVolume - b.AmbienceVolume) < eps && Mathf.Abs(a.UIVolume - b.UIVolume) < eps && a.MuteWhenUnfocused == b.MuteWhenUnfocused; } 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) { if (a == null) a = new AudioSettings(); return new AudioSettings { MasterVolume = a.MasterVolume, MusicVolume = a.MusicVolume, SfxVolume = a.SfxVolume, VoiceVolume = a.VoiceVolume, AmbienceVolume = a.AmbienceVolume, UIVolume = a.UIVolume, MuteWhenUnfocused = a.MuteWhenUnfocused }; } private static VisualSettings CloneVisual(VisualSettings v) { if (v == null) 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) { if (g == null) g = new GameSettings(); return new GameSettings { PopupDisplayDuration = g.PopupDisplayDuration, TutorialsEnabled = g.TutorialsEnabled, TooltipsEnabled = g.TooltipsEnabled, AutoUseTools = g.AutoUseTools, }; } } }