Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/UI/Menus/SettingsWindow.cs

726 lines
23 KiB
C#

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<float>
{
startValue = 0f,
endValue = 1f,
settings = _tweenSettings
};
var scale = new TweenSettings<Vector3>
{
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<float>
{
startValue = _canvasGroup.alpha,
endValue = 0f,
settings = _tweenSettings
};
var scale = new TweenSettings<Vector3>
{
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<float> 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,
};
}
}
}