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

935 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();
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 = false;
_canvasGroup.interactable = false;
TryUnregisterRaycaster();
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;
}
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;
}
}
}