939 lines
37 KiB
C#
939 lines
37 KiB
C#
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;
|
|
|
|
// ── 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 += _ => SwitchCategory(Category.Game);
|
|
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Visual);
|
|
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Audio);
|
|
|
|
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<GraphicRaycaster>();
|
|
}
|
|
|
|
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 -= _ => SwitchCategory(Category.Game);
|
|
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Visual);
|
|
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Audio);
|
|
|
|
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);
|
|
TryRegisterRaycaster();
|
|
|
|
_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<float>
|
|
{
|
|
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<float>
|
|
{
|
|
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;
|
|
}
|
|
|
|
// ── 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<float>
|
|
{
|
|
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<float>
|
|
{
|
|
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<float>
|
|
{
|
|
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<float>
|
|
{
|
|
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<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 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 TryRegisterRaycaster()
|
|
{
|
|
if (_raycasterRegistered)
|
|
return;
|
|
|
|
if (_interactManager == null || _graphicRaycaster == null)
|
|
return;
|
|
|
|
_interactManager.AddUIRaycaster(_graphicRaycaster);
|
|
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
|
|
_raycasterRegistered = true;
|
|
}
|
|
|
|
private void TryUnregisterRaycaster()
|
|
{
|
|
if (!_raycasterRegistered)
|
|
return;
|
|
|
|
if (_interactManager == null || _graphicRaycaster == null)
|
|
return;
|
|
|
|
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
|
|
_interactManager.ClearExclusiveRaycaster();
|
|
_raycasterRegistered = false;
|
|
}
|
|
}
|
|
}
|