First commit for private source control. Older commits available on Github.
This commit is contained in:
3
Assets/Scripts/UI/Menus/Components.meta
Normal file
3
Assets/Scripts/UI/Menus/Components.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84e0529b59f0487a849dde7ed79f008e
|
||||
timeCreated: 1769794667
|
||||
171
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs
Normal file
171
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Save Slot UI:
|
||||
/// - No Load button.
|
||||
/// - Slot itself is clickable:
|
||||
/// - Filled => click loads
|
||||
/// - Empty => click creates (via SelectSaveWindow opening NewSaveWindow)
|
||||
/// - Delete button remains for filled saves.
|
||||
/// </summary>
|
||||
public class SaveSlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("Clickable Root")]
|
||||
[SerializeField]
|
||||
private Button _slotButton;
|
||||
|
||||
[Header("Labels")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _saveNameText;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _saveDateText;
|
||||
|
||||
[Header("Delete")]
|
||||
[SerializeField]
|
||||
private Button _deleteButton;
|
||||
|
||||
[Header("Empty Visual")]
|
||||
[SerializeField]
|
||||
private Image _emptyImage;
|
||||
|
||||
private Action<SaveFileInfo> _onDeleteClick;
|
||||
private Action _onEmptyClick;
|
||||
|
||||
private Action<SaveFileInfo> _onFilledClick;
|
||||
|
||||
public SaveFileInfo SaveInfo { get; private set; }
|
||||
public bool IsEmpty { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_slotButton != null)
|
||||
{
|
||||
_slotButton.onClick.RemoveAllListeners();
|
||||
_slotButton.onClick.AddListener(HandleSlotClicked);
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.onClick.RemoveAllListeners();
|
||||
_deleteButton.onClick.AddListener(HandleDeleteClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_slotButton != null) _slotButton.onClick.RemoveAllListeners();
|
||||
if (_deleteButton != null) _deleteButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
public GameObject GetSelectableGameObject()
|
||||
{
|
||||
return _slotButton != null ? _slotButton.gameObject : gameObject;
|
||||
}
|
||||
|
||||
public void SetFilled(
|
||||
SaveFileInfo saveInfo,
|
||||
Action<SaveFileInfo> onClickFilled,
|
||||
Action<SaveFileInfo> onDelete)
|
||||
{
|
||||
SaveInfo = saveInfo;
|
||||
IsEmpty = false;
|
||||
|
||||
_onFilledClick = onClickFilled;
|
||||
_onEmptyClick = null;
|
||||
_onDeleteClick = onDelete;
|
||||
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(false);
|
||||
|
||||
if (_saveNameText != null)
|
||||
{
|
||||
_saveNameText.gameObject.SetActive(true);
|
||||
_saveNameText.text = saveInfo.FileName;
|
||||
}
|
||||
|
||||
if (_saveDateText != null)
|
||||
{
|
||||
_saveDateText.gameObject.SetActive(true);
|
||||
_saveDateText.text = saveInfo.LastModified.ToString("g");
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.gameObject.SetActive(true);
|
||||
_deleteButton.interactable = true;
|
||||
}
|
||||
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = true;
|
||||
}
|
||||
|
||||
public void SetEmpty(Action onClickEmpty)
|
||||
{
|
||||
SaveInfo = default;
|
||||
IsEmpty = true;
|
||||
|
||||
_onFilledClick = null;
|
||||
_onEmptyClick = onClickEmpty;
|
||||
_onDeleteClick = null;
|
||||
|
||||
if (_saveNameText != null) _saveNameText.text = string.Empty;
|
||||
if (_saveDateText != null) _saveDateText.text = string.Empty;
|
||||
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(true);
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.gameObject.SetActive(false);
|
||||
_deleteButton.interactable = false;
|
||||
}
|
||||
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = true; // empty slot is still clickable
|
||||
}
|
||||
|
||||
public void SetInteractable(bool interactable)
|
||||
{
|
||||
// Slot click should still work even when empty; we fully disable during "busy" only.
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = interactable;
|
||||
|
||||
// Delete only for filled saves.
|
||||
if (_deleteButton != null)
|
||||
_deleteButton.interactable = interactable && !IsEmpty;
|
||||
}
|
||||
|
||||
private void HandleSlotClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
_onEmptyClick?.Invoke();
|
||||
else if (!string.IsNullOrWhiteSpace(SaveInfo.FileName))
|
||||
_onFilledClick?.Invoke(SaveInfo);
|
||||
}
|
||||
|
||||
private void HandleDeleteClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SaveInfo.FileName))
|
||||
_onDeleteClick?.Invoke(SaveInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public struct SaveFileInfo
|
||||
{
|
||||
public readonly string FileName;
|
||||
public readonly DateTime LastModified;
|
||||
|
||||
public SaveFileInfo(string fileName, DateTime lastModified)
|
||||
{
|
||||
FileName = fileName;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 738235190e6d42eb93f533aa05cf98c2
|
||||
timeCreated: 1769794667
|
||||
341
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs
Normal file
341
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirm delete modal with tweened show/hide (fade + scale).
|
||||
/// - Open(): animates in
|
||||
/// - Close(): animates out
|
||||
/// - CloseImmediate(): hard hide (safe for OnDisable/OnDestroy)
|
||||
/// Notes:
|
||||
/// - Uses unscaled time so it still animates while paused.
|
||||
/// - Gates input during transitions.
|
||||
/// </summary>
|
||||
public class ConfirmDeleteWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField]
|
||||
private GameObject _root;
|
||||
|
||||
[Header("Animation Targets")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _panelTransform;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _titleText;
|
||||
|
||||
[SerializeField]
|
||||
private Button _confirmButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _cancelButton;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private float _duration = 0.18f;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _easeIn = Ease.OutBack;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _easeOut = Ease.InQuad;
|
||||
|
||||
[SerializeField]
|
||||
private bool _useUnscaledTime = true;
|
||||
|
||||
[Header("Scale")]
|
||||
[SerializeField]
|
||||
private float _fromScale = 0.92f;
|
||||
|
||||
[SerializeField]
|
||||
private float _toScale = 1.0f;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _isAnimating;
|
||||
|
||||
private bool _isOpen;
|
||||
|
||||
private SaveFileInfo _pending;
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_confirmButton != null) _confirmButton.onClick.AddListener(Confirm);
|
||||
if (_cancelButton != null) _cancelButton.onClick.AddListener(Cancel);
|
||||
|
||||
CloseImmediate();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_confirmButton != null) _confirmButton.onClick.RemoveListener(Confirm);
|
||||
if (_cancelButton != null) _cancelButton.onClick.RemoveListener(Cancel);
|
||||
|
||||
StopAnim();
|
||||
}
|
||||
|
||||
public event Action<SaveFileInfo> OnConfirmDelete;
|
||||
public event Action OnCancel;
|
||||
|
||||
public void Open(SaveFileInfo info)
|
||||
{
|
||||
_pending = info;
|
||||
|
||||
if (_titleText != null)
|
||||
_titleText.text = $"Delete '{info.FileName}'?";
|
||||
|
||||
OpenAsync().Forget();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
CloseAsync().Forget();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
StopAnim();
|
||||
|
||||
_pending = default;
|
||||
_isOpen = false;
|
||||
_isAnimating = false;
|
||||
|
||||
if (_root != null) _root.SetActive(false);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
var s = _toScale;
|
||||
_panelTransform.localScale = new Vector3(s, s, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask OpenAsync()
|
||||
{
|
||||
if (_isOpen && !_isAnimating) return;
|
||||
|
||||
EnsureRefs();
|
||||
|
||||
StopAnim();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
if (_root != null) _root.SetActive(true);
|
||||
|
||||
_isAnimating = true;
|
||||
|
||||
// Prep start state
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
var s = _fromScale;
|
||||
_panelTransform.localScale = new Vector3(s, s, 1f);
|
||||
}
|
||||
|
||||
// Build sequence (fade + scale)
|
||||
_sequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
_sequence.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeIn,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
if (_panelTransform != null)
|
||||
_sequence.Group(Tween.Scale(_panelTransform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = new Vector3(_fromScale, _fromScale, 1f),
|
||||
endValue = new Vector3(_toScale, _toScale, 1f),
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeIn,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
_isAnimating = false;
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
// Enable input once fully open
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.interactable = true;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
// Optional: focus cancel for controller/keyboard flows
|
||||
_cancelButton?.Select();
|
||||
}
|
||||
|
||||
private async UniTask CloseAsync()
|
||||
{
|
||||
if (!_isOpen && !_isAnimating) return;
|
||||
|
||||
EnsureRefs();
|
||||
|
||||
StopAnim();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
_isAnimating = true;
|
||||
|
||||
// Disable input immediately while animating out
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
var startAlpha = _canvasGroup != null ? _canvasGroup.alpha : 1f;
|
||||
var startScale = _panelTransform != null ? _panelTransform.localScale : Vector3.one;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
_sequence.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = startAlpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeOut,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
if (_panelTransform != null)
|
||||
_sequence.Group(Tween.Scale(_panelTransform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = startScale,
|
||||
endValue = new Vector3(_fromScale, _fromScale, 1f),
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeOut,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
_isAnimating = false;
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
// Fully hidden
|
||||
if (_canvasGroup != null) _canvasGroup.alpha = 0f;
|
||||
if (_root != null) _root.SetActive(false);
|
||||
|
||||
_pending = default;
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (_isAnimating) return;
|
||||
|
||||
var info = _pending;
|
||||
Close();
|
||||
OnConfirmDelete?.Invoke(info);
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
if (_isAnimating) return;
|
||||
|
||||
Close();
|
||||
OnCancel?.Invoke();
|
||||
}
|
||||
|
||||
private void StopAnim()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
{
|
||||
_sequence.Stop();
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRefs()
|
||||
{
|
||||
// If you forgot to wire these, try to find sensible defaults.
|
||||
if (_canvasGroup == null && _root != null)
|
||||
_canvasGroup = _root.GetComponent<CanvasGroup>();
|
||||
|
||||
if (_panelTransform == null)
|
||||
_panelTransform = GetComponentInChildren<RectTransform>(true);
|
||||
|
||||
// Root fallback: if none specified, use this GO
|
||||
if (_root == null)
|
||||
_root = gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c66bd69629f472f91038ee13ec204b9
|
||||
timeCreated: 1769796211
|
||||
657
Assets/Scripts/UI/Menus/MainMenuWindow.cs
Normal file
657
Assets/Scripts/UI/Menus/MainMenuWindow.cs
Normal file
@@ -0,0 +1,657 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Main Menu flow:
|
||||
/// - Starts on intro screen
|
||||
/// - Intro light pulses
|
||||
/// - Intro text/title fade in after delay
|
||||
/// - Submit fades intro text/title out, pushes light to full alpha, then fades main menu in
|
||||
/// - Start Game => opens SelectSaveWindow
|
||||
/// - Settings => opens settings menu
|
||||
/// - Quit => quits app
|
||||
/// </summary>
|
||||
public class MainMenuWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Main Menu Window")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainMenuIntroScreenCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainMenuWindowCanvasGroup;
|
||||
|
||||
[Header("Intro Screen")]
|
||||
[SerializeField]
|
||||
private Image _introScreenLightImage;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTextCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _introTextText;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTitleCanvasGroup;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _startGameButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _settingsButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _quitButton;
|
||||
|
||||
[Header("Select Save Window")]
|
||||
[SerializeField]
|
||||
private SelectSaveWindow _selectSaveWindow;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _selectSaveWindowCanvasGroup;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _selectSaveTweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Intro Timing")]
|
||||
[SerializeField]
|
||||
private float _introLightPulseDuration = 2f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextDelaySeconds = 1.5f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextFadeInDuration = 0.8f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextPulseDuration = 1.4f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introSubmitTextFadeOutDuration = 0.25f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introSubmitLightToFullDuration = 0.75f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introToMenuCrossfadeDuration = 0.6f;
|
||||
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private GameService _gameService;
|
||||
private InputManager _inputManager;
|
||||
|
||||
private CancellationTokenSource _introCts;
|
||||
private CancellationTokenSource _selectSaveCts;
|
||||
|
||||
private Sequence _introLightPulseSequence;
|
||||
private Sequence _introTextPulseSequence;
|
||||
private Sequence _introTransitionSequence;
|
||||
private Sequence _selectSaveSequence;
|
||||
|
||||
private bool _introFinished;
|
||||
private bool _introTransitioning;
|
||||
private DeviceInputType _lastDeviceInputType;
|
||||
|
||||
[Inject]
|
||||
public void Construct(GameService gameService, EventCoordinator eventCoordinator, InputManager inputManager)
|
||||
{
|
||||
_gameService = gameService;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ApplyInitialVisualState();
|
||||
|
||||
if (_selectSaveWindow != null)
|
||||
{
|
||||
_selectSaveWindow.OnCloseWindow += CloseSelectSaveWindow;
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
UpdateSubmitText(force: true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
BindButtons();
|
||||
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(false));
|
||||
_inputManager?.BindSubmitForStart(OnIntroSubmit);
|
||||
|
||||
StartIntroScreen().Forget();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnbindButtons();
|
||||
|
||||
_inputManager?.ResetSubmitBind(OnIntroSubmit);
|
||||
|
||||
StopIntroTweens();
|
||||
StopSelectSaveTween();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_selectSaveWindow != null)
|
||||
_selectSaveWindow.OnCloseWindow -= CloseSelectSaveWindow;
|
||||
|
||||
StopIntroTweens();
|
||||
StopSelectSaveTween();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
UpdateSubmitText();
|
||||
}
|
||||
|
||||
private void BindButtons()
|
||||
{
|
||||
if (_startGameButton != null)
|
||||
_startGameButton.onClick.AddListener(OnStartClicked);
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.AddListener(OnSettingsClicked);
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.AddListener(OnQuitClicked);
|
||||
}
|
||||
|
||||
private void UnbindButtons()
|
||||
{
|
||||
if (_startGameButton != null)
|
||||
_startGameButton.onClick.RemoveListener(OnStartClicked);
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.RemoveListener(OnSettingsClicked);
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.RemoveListener(OnQuitClicked);
|
||||
}
|
||||
|
||||
private void ApplyInitialVisualState()
|
||||
{
|
||||
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 1f, true);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
var color = _introScreenLightImage.color;
|
||||
color.a = 0f;
|
||||
_introScreenLightImage.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSubmitText(bool force = false)
|
||||
{
|
||||
if (_introTextText == null || _inputManager == null)
|
||||
return;
|
||||
|
||||
var currentDevice = _inputManager.DeviceInputType;
|
||||
if (!force && currentDevice == _lastDeviceInputType)
|
||||
return;
|
||||
|
||||
_lastDeviceInputType = currentDevice;
|
||||
|
||||
var isKeyboardMouse = currentDevice == DeviceInputType.KeyboardAndMouse;
|
||||
_introTextText.text = isKeyboardMouse
|
||||
? "Press Enter to begin."
|
||||
: "Press Start to begin.";
|
||||
}
|
||||
|
||||
private async UniTaskVoid StartIntroScreen()
|
||||
{
|
||||
_introFinished = false;
|
||||
_introTransitioning = false;
|
||||
|
||||
ResetIntroCtsAndCancelRunning();
|
||||
ApplyInitialVisualState();
|
||||
UpdateSubmitText(force: true);
|
||||
|
||||
try
|
||||
{
|
||||
StartIntroLightPulse(_introCts.Token);
|
||||
await StartIntroTextFlow(_introCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void StartIntroLightPulse(CancellationToken token)
|
||||
{
|
||||
if (_introScreenLightImage == null)
|
||||
return;
|
||||
|
||||
_introLightPulseSequence = Sequence.Create(
|
||||
useUnscaledTime: true,
|
||||
cycleMode: Sequence.SequenceCycleMode.Yoyo,
|
||||
cycles: -1)
|
||||
.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introScreenLightImage.color.a,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introLightPulseDuration,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
_introLightPulseSequence.ToUniTask(cancellationToken: token).Forget();
|
||||
}
|
||||
|
||||
private async UniTask StartIntroTextFlow(CancellationToken token)
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(_introTextDelaySeconds), cancellationToken: token);
|
||||
|
||||
var fadeInSequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_introTitleCanvasGroup != null)
|
||||
{
|
||||
fadeInSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextFadeInDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introTextCanvasGroup != null)
|
||||
{
|
||||
fadeInSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextFadeInDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await fadeInSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
if (_introTextCanvasGroup == null)
|
||||
return;
|
||||
|
||||
_introTextPulseSequence = Sequence.Create(
|
||||
useUnscaledTime: true,
|
||||
cycleMode: Sequence.SequenceCycleMode.Yoyo,
|
||||
cycles: -1)
|
||||
.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTextCanvasGroup.alpha,
|
||||
endValue = 0.1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextPulseDuration,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await _introTextPulseSequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
|
||||
private void OnIntroSubmit(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
return;
|
||||
|
||||
if (!ctx.performed)
|
||||
return;
|
||||
|
||||
TransitionFromIntroToMainMenu().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid TransitionFromIntroToMainMenu()
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
return;
|
||||
|
||||
_introTransitioning = true;
|
||||
|
||||
ResetIntroCtsAndCancelRunning();
|
||||
|
||||
try
|
||||
{
|
||||
// Phase 1: fade intro title + text fully out together, while pushing light to full.
|
||||
var introElementsSequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_introTitleCanvasGroup != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTitleCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitTextFadeOutDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introTextCanvasGroup != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTextCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitTextFadeOutDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introScreenLightImage.color.a,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitLightToFullDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await introElementsSequence.ToUniTask(cancellationToken: _introCts.Token);
|
||||
|
||||
// Ensure intro text/title are fully gone before menu begins fading in.
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
|
||||
|
||||
// Phase 2: only after intro text/title have finished fading, crossfade to main menu.
|
||||
if (_mainMenuIntroScreenCanvasGroup != null && _mainMenuWindowCanvasGroup != null)
|
||||
{
|
||||
_introTransitionSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainMenuIntroScreenCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainMenuIntroScreenCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await _introTransitionSequence.ToUniTask(cancellationToken: _introCts.Token);
|
||||
}
|
||||
else if (_mainMenuWindowCanvasGroup != null)
|
||||
{
|
||||
var menuFade = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await menuFade.ToUniTask(cancellationToken: _introCts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_introTransitioning = false;
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_introTransitionSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
var color = _introScreenLightImage.color;
|
||||
color.a = 1f;
|
||||
_introScreenLightImage.color = color;
|
||||
}
|
||||
|
||||
_introFinished = true;
|
||||
_introTransitioning = false;
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] Starting game");
|
||||
ShowSelectSaveWindow().Forget();
|
||||
}
|
||||
|
||||
private void OnSettingsClicked()
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleSettingsWindow(true));
|
||||
}
|
||||
|
||||
private void OnQuitClicked()
|
||||
{
|
||||
_gameService?.QuitGame();
|
||||
}
|
||||
|
||||
private async UniTask ShowSelectSaveWindow()
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] Showing select save window");
|
||||
|
||||
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] SelectSaveWindow references not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
ResetSelectSaveCtsAndCancelRunning();
|
||||
|
||||
_selectSaveWindow.gameObject.SetActive(true);
|
||||
_selectSaveWindowCanvasGroup.alpha = 0f;
|
||||
_selectSaveWindow.transform.localScale = Vector3.zero;
|
||||
_selectSaveWindow.Refresh();
|
||||
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, _mainMenuWindowCanvasGroup != null ? _mainMenuWindowCanvasGroup.alpha : 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _selectSaveWindowCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _selectSaveWindow.transform.localScale,
|
||||
endValue = Vector3.one,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 1f, true);
|
||||
}
|
||||
|
||||
private void CloseSelectSaveWindow()
|
||||
{
|
||||
CloseSelectSaveWindowInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask CloseSelectSaveWindowInternal()
|
||||
{
|
||||
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
|
||||
return;
|
||||
|
||||
ResetSelectSaveCtsAndCancelRunning();
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, _selectSaveWindowCanvasGroup.alpha, false);
|
||||
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _selectSaveWindowCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _selectSaveWindow.transform.localScale,
|
||||
endValue = Vector3.zero,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
_selectSaveWindowCanvasGroup.alpha = 0f;
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
}
|
||||
|
||||
private void ResetIntroCtsAndCancelRunning()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
|
||||
CancelAndDispose(ref _introCts);
|
||||
_introCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopIntroTweens()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
CancelAndDispose(ref _introCts);
|
||||
}
|
||||
|
||||
private void ResetSelectSaveCtsAndCancelRunning()
|
||||
{
|
||||
StopSequence(ref _selectSaveSequence);
|
||||
CancelAndDispose(ref _selectSaveCts);
|
||||
_selectSaveCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopSelectSaveTween()
|
||||
{
|
||||
StopSequence(ref _selectSaveSequence);
|
||||
CancelAndDispose(ref _selectSaveCts);
|
||||
}
|
||||
|
||||
private static void StopSequence(ref Sequence sequence)
|
||||
{
|
||||
if (sequence.isAlive)
|
||||
sequence.Stop();
|
||||
|
||||
sequence = default;
|
||||
}
|
||||
|
||||
private static void CancelAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
|
||||
private static void SetCanvasGroupState(CanvasGroup group, float alpha, bool inputEnabled)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
group.alpha = alpha;
|
||||
group.interactable = inputEnabled;
|
||||
group.blocksRaycasts = inputEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/MainMenuWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/MainMenuWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2251b1b163bc42aeaeaeba8a4442363c
|
||||
timeCreated: 1769784245
|
||||
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// New Save modal window:
|
||||
/// - Opens over SelectSaveWindow
|
||||
/// - User enters a save name
|
||||
/// - Create -> SaveManager.CreateNewSaveGame(name) (and typically sets CurrentSave)
|
||||
/// - Then immediately LoadGameData(name) (optional but robust), and raises OnSaveCreated
|
||||
/// </summary>
|
||||
public class NewSaveWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Input")]
|
||||
[SerializeField]
|
||||
private TMP_InputField _nameInput;
|
||||
|
||||
[SerializeField]
|
||||
private Button _createButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _cancelButton;
|
||||
|
||||
[Header("Error UI")]
|
||||
[SerializeField]
|
||||
private GameObject _errorBox;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _errorText;
|
||||
|
||||
[Header("Validation")]
|
||||
[SerializeField]
|
||||
private int _minNameLength = 1;
|
||||
|
||||
[SerializeField]
|
||||
private int _maxNameLength = 24;
|
||||
|
||||
[SerializeField]
|
||||
private bool _trimWhitespace = true;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _isBusy;
|
||||
|
||||
private bool _isOpen;
|
||||
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private Sequence _seq;
|
||||
|
||||
private bool _tutorialsEnabled;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_createButton != null) _createButton.onClick.AddListener(OnCreateClicked);
|
||||
if (_cancelButton != null) _cancelButton.onClick.AddListener(Close);
|
||||
|
||||
HideError();
|
||||
CloseImmediate();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_createButton != null) _createButton.onClick.RemoveListener(OnCreateClicked);
|
||||
if (_cancelButton != null) _cancelButton.onClick.RemoveListener(Close);
|
||||
|
||||
StopTween();
|
||||
}
|
||||
|
||||
public event Action OnCloseWindow;
|
||||
public event Action<string> OnSaveCreated;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_isOpen)
|
||||
return;
|
||||
|
||||
Debug.Log($"Opening {nameof(NewSaveWindow)}");
|
||||
OpenInternal().Forget();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!_isOpen || _isBusy) return;
|
||||
CloseInternal().Forget();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
_isOpen = false;
|
||||
_isBusy = false;
|
||||
|
||||
if (_nameInput != null) _nameInput.text = string.Empty;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
HideError();
|
||||
}
|
||||
|
||||
private async UniTask OpenInternal()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
_isOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("Opening Internal...");
|
||||
Debug.Log($"{gameObject} is {gameObject.activeSelf}");
|
||||
|
||||
ResetCtsAndCancelRunning();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
Debug.Log($"{gameObject} is now {gameObject.activeSelf}");
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
|
||||
_isOpen = true;
|
||||
_isBusy = true;
|
||||
|
||||
if (_nameInput != null) _nameInput.text = string.Empty;
|
||||
HideError();
|
||||
|
||||
Debug.Log("Opening - Creating Sequence");
|
||||
|
||||
_seq = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log("Opening - Sequence Running.");
|
||||
await _seq.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log($"Opening - Sequence Error: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Debug.Log("Opening - Sequence over.");
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.interactable = true;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
|
||||
_isBusy = false;
|
||||
FocusInput();
|
||||
}
|
||||
|
||||
private async UniTask CloseInternal()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
CloseImmediate();
|
||||
OnCloseWindow?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
ResetCtsAndCancelRunning();
|
||||
|
||||
_isBusy = true;
|
||||
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
|
||||
_seq = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _seq.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_isOpen = false;
|
||||
_isBusy = false;
|
||||
|
||||
gameObject.SetActive(false);
|
||||
OnCloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
private void FocusInput()
|
||||
{
|
||||
if (_nameInput == null) return;
|
||||
|
||||
_nameInput.ActivateInputField();
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(_nameInput.gameObject);
|
||||
}
|
||||
|
||||
|
||||
private void OnCreateClicked()
|
||||
{
|
||||
if (_isBusy) return;
|
||||
CreateSave().Forget();
|
||||
}
|
||||
|
||||
private async UniTask CreateSave()
|
||||
{
|
||||
HideError();
|
||||
|
||||
if (_saveManager == null)
|
||||
{
|
||||
ShowError("Save system not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var raw = _nameInput != null ? _nameInput.text : string.Empty;
|
||||
var name = _trimWhitespace ? (raw ?? string.Empty).Trim() : raw ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ShowError("Please enter a save name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.Length < _minNameLength)
|
||||
{
|
||||
ShowError($"Save name must be at least {_minNameLength} character(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_maxNameLength > 0 && name.Length > _maxNameLength)
|
||||
{
|
||||
ShowError($"Save name must be {_maxNameLength} characters or fewer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContainsIllegalFileNameChars(name, out var illegalChars))
|
||||
{
|
||||
ShowError(illegalChars.Length == 1
|
||||
? $"That name contains an illegal character: '{illegalChars[0]}'."
|
||||
: $"That name contains illegal characters: {string.Join(" ", illegalChars.Select(c => $"'{c}'"))}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsWindowsReservedFileName(name))
|
||||
{
|
||||
ShowError("That name is reserved by the operating system. Please choose a different name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_saveManager.DoesSaveExist(name))
|
||||
{
|
||||
ShowError("A save with that name already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
_isBusy = true;
|
||||
SetButtonsInteractable(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _saveManager.CreateNewSaveGame(name);
|
||||
|
||||
// Tell SelectSaveWindow to start game.
|
||||
OnSaveCreated?.Invoke(name);
|
||||
|
||||
// Close ourselves immediately (caller will close SelectSaveWindow)
|
||||
CloseImmediate();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ShowError("Failed to create save. Please try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBusy = false;
|
||||
SetButtonsInteractable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetButtonsInteractable(bool interactable)
|
||||
{
|
||||
if (_createButton != null) _createButton.interactable = interactable;
|
||||
if (_cancelButton != null) _cancelButton.interactable = interactable;
|
||||
if (_nameInput != null) _nameInput.interactable = interactable;
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
if (_errorText != null) _errorText.text = message;
|
||||
if (_errorBox != null) _errorBox.SetActive(true);
|
||||
}
|
||||
|
||||
private void HideError()
|
||||
{
|
||||
if (_errorBox != null) _errorBox.SetActive(false);
|
||||
if (_errorText != null) _errorText.text = string.Empty;
|
||||
}
|
||||
|
||||
private static bool ContainsIllegalFileNameChars(string name, out char[] illegalChars)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
illegalChars = name.Where(c => invalid.Contains(c)).Distinct().ToArray();
|
||||
return illegalChars.Length > 0;
|
||||
}
|
||||
|
||||
private static bool IsWindowsReservedFileName(string name)
|
||||
{
|
||||
var trimmed = (name ?? string.Empty).Trim().TrimEnd('.', ' ');
|
||||
if (string.IsNullOrEmpty(trimmed)) return true;
|
||||
|
||||
var upper = trimmed.ToUpperInvariant();
|
||||
|
||||
if (upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL") return true;
|
||||
|
||||
if (upper.Length == 4)
|
||||
{
|
||||
if (upper.StartsWith("COM") && char.IsDigit(upper[3]) && upper[3] != '0') return true;
|
||||
if (upper.StartsWith("LPT") && char.IsDigit(upper[3]) && upper[3] != '0') return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResetCtsAndCancelRunning()
|
||||
{
|
||||
if (_seq.isAlive)
|
||||
{
|
||||
_seq.Stop();
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
{
|
||||
if (_seq.isAlive) _seq.Stop();
|
||||
_seq = default;
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/NewSaveWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/NewSaveWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98d490ab66c54c3d8bafdfeae4945734
|
||||
timeCreated: 1770232259
|
||||
308
Assets/Scripts/UI/Menus/PauseMenuWindow.cs
Normal file
308
Assets/Scripts/UI/Menus/PauseMenuWindow.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
public class PauseMenuWindow : MonoBehaviour, IUIWindow
|
||||
{
|
||||
[Header("Root UI")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _windowRect;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _resumeButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _saveButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _settingsButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _exitButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _quitToDesktopButton;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Scale")]
|
||||
[SerializeField]
|
||||
private float _hiddenScale = 0.85f;
|
||||
|
||||
[Header("Internal")]
|
||||
[SerializeField]
|
||||
private GraphicRaycaster _graphicRaycaster;
|
||||
|
||||
private AudioManager _audioManager;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private GameService _gameService;
|
||||
private InteractManager _interactManager;
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
public bool IsModal => true;
|
||||
|
||||
public WindowType WindowType => WindowType.PauseMenuWindow;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Start hidden by default
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
if (_windowRect != null)
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_resumeButton != null) _resumeButton.onClick.AddListener(OnResumeButtonClick);
|
||||
if (_saveButton != null) _saveButton.onClick.AddListener(OnSaveButtonClick);
|
||||
if (_settingsButton != null) _settingsButton.onClick.AddListener(OnSettingsButtonClick);
|
||||
if (_exitButton != null) _exitButton.onClick.AddListener(OnExitButtonClick);
|
||||
if (_quitToDesktopButton != null) _quitToDesktopButton.onClick.AddListener(OnQuitToDesktopButtonClick);
|
||||
|
||||
_interactManager.AddUIRaycaster(_graphicRaycaster);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_resumeButton != null) _resumeButton.onClick.RemoveListener(OnResumeButtonClick);
|
||||
if (_saveButton != null) _saveButton.onClick.RemoveListener(OnSaveButtonClick);
|
||||
if (_settingsButton != null) _settingsButton.onClick.RemoveListener(OnSettingsButtonClick);
|
||||
if (_exitButton != null) _exitButton.onClick.RemoveListener(OnExitButtonClick);
|
||||
if (_quitToDesktopButton != null) _quitToDesktopButton.onClick.RemoveListener(OnQuitToDesktopButtonClick);
|
||||
|
||||
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopAndResetCancellation();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_canvasGroup == null || _windowRect == null)
|
||||
{
|
||||
Debug.LogError("[PauseMenuWindow] Missing CanvasGroup or WindowRect reference.");
|
||||
gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
StopAndResetCancellation();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
|
||||
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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 1f;
|
||||
_windowRect.localScale = Vector3.one;
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_canvasGroup == null || _windowRect == null)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StopAndResetCancellation();
|
||||
|
||||
// Block clicks immediately
|
||||
_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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator, SaveManager saveManager, GameService gameService,
|
||||
InteractManager interactManager, AudioManager audioManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_saveManager = saveManager;
|
||||
_gameService = gameService;
|
||||
_interactManager = interactManager;
|
||||
_audioManager = audioManager;
|
||||
}
|
||||
|
||||
private void StopAndResetCancellation()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void OnResumeButtonClick()
|
||||
{
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
private void OnSaveButtonClick()
|
||||
{
|
||||
SaveGame().Forget();
|
||||
}
|
||||
|
||||
private void OnSettingsButtonClick()
|
||||
{
|
||||
_eventCoordinator.Publish(new UIToggleSettingsWindow(true));
|
||||
}
|
||||
|
||||
private void OnExitButtonClick()
|
||||
{
|
||||
_eventCoordinator.Publish(new FadeEvent(false, 1f));
|
||||
ExitButtonInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask ExitButtonInternal()
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(1));
|
||||
_audioManager.StopAllAudio();
|
||||
await SaveGame();
|
||||
_eventCoordinator.Publish(new PauseButtonClickedEvent());
|
||||
await _gameService.LoadMainMenu();
|
||||
}
|
||||
|
||||
private void OnQuitToDesktopButtonClick()
|
||||
{
|
||||
QuitButtonInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask QuitButtonInternal()
|
||||
{
|
||||
_eventCoordinator.Publish(new FadeEvent(false, 1f));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(1));
|
||||
await SaveGame();
|
||||
_gameService.QuitGame();
|
||||
}
|
||||
|
||||
private async UniTask SaveGame()
|
||||
{
|
||||
if (_saveManager == null) return;
|
||||
await _saveManager.SaveGameDataLatest();
|
||||
// TODO: Saved feedback popup/toast
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/PauseMenuWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/PauseMenuWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19b9bc67046d4238ac544f6fe36a6066
|
||||
timeCreated: 1769707615
|
||||
355
Assets/Scripts/UI/Menus/SelectSaveWindow.cs
Normal file
355
Assets/Scripts/UI/Menus/SelectSaveWindow.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Select Save Window:
|
||||
/// - Shows EXACTLY 3 slots (filled first, then empty).
|
||||
/// - Clicking a slot:
|
||||
/// - Filled slot => loads that save, then StartGame
|
||||
/// - Empty slot => opens NewSaveWindow (create + load), then StartGame
|
||||
/// - Delete is still supported (per-slot delete button + confirm modal).
|
||||
/// - Back closes this window (handled by MainMenuWindow).
|
||||
/// </summary>
|
||||
public class SelectSaveWindow : MonoBehaviour
|
||||
{
|
||||
private const int MAX_SLOTS = 3;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private RectTransform _listContentParent;
|
||||
|
||||
[SerializeField]
|
||||
private SaveSlotUI _saveSlotPrefab;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _backButton;
|
||||
|
||||
[Header("New Save Window")]
|
||||
[SerializeField]
|
||||
private NewSaveWindow _newSaveWindow;
|
||||
|
||||
[Header("Confirm Delete Window (optional but recommended)")]
|
||||
[SerializeField]
|
||||
private ConfirmDeleteWindow _confirmDeleteWindow;
|
||||
|
||||
private readonly List<SaveSlotUI> _instantiatedSlots = new();
|
||||
|
||||
private int _currentSelectionIndex;
|
||||
private GameService _gameService;
|
||||
private bool _isBusy;
|
||||
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow += HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated += HandleSaveCreatedAndStartGame;
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete += HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel += HandleCancelDelete;
|
||||
_confirmDeleteWindow.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow -= HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated -= HandleSaveCreatedAndStartGame;
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete -= HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel -= HandleCancelDelete;
|
||||
}
|
||||
|
||||
ClearSlots();
|
||||
}
|
||||
|
||||
public event Action OnCloseWindow;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager, GameService gameService)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
_gameService = gameService;
|
||||
}
|
||||
|
||||
/// <summary>Called by MainMenuWindow after enabling this GO.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (_newSaveWindow != null)
|
||||
_newSaveWindow.CloseImmediate();
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
_confirmDeleteWindow.Close();
|
||||
|
||||
EnsureThreeSlotsExist();
|
||||
RefreshSlotsData();
|
||||
SetBusy(false);
|
||||
}
|
||||
|
||||
private void OnBackClicked()
|
||||
{
|
||||
if (_isBusy)
|
||||
return;
|
||||
|
||||
// Check if Confirm Delete or New Save Window is open.
|
||||
|
||||
OnCloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleNewSaveClosed()
|
||||
{
|
||||
// NewSaveWindow was closed (cancel/back) => return control to the list.
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
RefreshSlotsData();
|
||||
}
|
||||
|
||||
private void HandleSaveCreatedAndStartGame(string _)
|
||||
{
|
||||
// NewSaveWindow already created + loaded the save.
|
||||
OnCloseWindow?.Invoke();
|
||||
_gameService?.StartGame().Forget();
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
Debug.Log("[SelectSaveWindow] SetBusy: " + busy);
|
||||
_isBusy = busy;
|
||||
|
||||
if (_backButton != null)
|
||||
_backButton.interactable = !busy;
|
||||
|
||||
foreach (var slot in _instantiatedSlots)
|
||||
if (slot != null)
|
||||
slot.SetInteractable(!busy);
|
||||
|
||||
Debug.Log($"[SelectSaveWindow] Finished set busy: {busy}");
|
||||
}
|
||||
|
||||
private void EnsureThreeSlotsExist()
|
||||
{
|
||||
if (_listContentParent == null || _saveSlotPrefab == null)
|
||||
return;
|
||||
|
||||
if (_instantiatedSlots.Count == MAX_SLOTS)
|
||||
return;
|
||||
|
||||
ClearSlots();
|
||||
|
||||
for (var i = 0; i < MAX_SLOTS; i++)
|
||||
{
|
||||
var slot = Instantiate(_saveSlotPrefab, _listContentParent);
|
||||
_instantiatedSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshSlotsData()
|
||||
{
|
||||
// Always show 3 slots; if save system is missing, they’ll all appear empty/disabled.
|
||||
if (_saveManager == null)
|
||||
{
|
||||
for (var i = 0; i < _instantiatedSlots.Count; i++)
|
||||
_instantiatedSlots[i]?.SetEmpty(OnEmptySlotClicked);
|
||||
|
||||
SelectBackButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Newest first, cap at 3
|
||||
var saveFiles = _saveManager.GetAvailableSaves();
|
||||
var infos = (saveFiles ?? new List<(string, DateTime)>())
|
||||
.Select(sf => new SaveFileInfo(sf.FileName, sf.LastModified))
|
||||
.OrderByDescending(i => i.LastModified)
|
||||
.Take(MAX_SLOTS)
|
||||
.ToArray();
|
||||
|
||||
for (var i = 0; i < MAX_SLOTS; i++)
|
||||
{
|
||||
var slot = _instantiatedSlots.ElementAtOrDefault(i);
|
||||
if (slot == null) continue;
|
||||
|
||||
if (i < infos.Length)
|
||||
slot.SetFilled(infos[i], OnFilledSlotClicked, OnSlotDeleteClicked);
|
||||
else
|
||||
slot.SetEmpty(OnEmptySlotClicked);
|
||||
}
|
||||
|
||||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||||
SelectSlot(_currentSelectionIndex);
|
||||
}
|
||||
|
||||
private void ClearSlots()
|
||||
{
|
||||
foreach (var slot in _instantiatedSlots)
|
||||
if (slot != null)
|
||||
Destroy(slot.gameObject);
|
||||
|
||||
_instantiatedSlots.Clear();
|
||||
_currentSelectionIndex = 0;
|
||||
}
|
||||
|
||||
private void OnFilledSlotClicked(SaveFileInfo saveInfo)
|
||||
{
|
||||
if (_isBusy) return;
|
||||
if (_saveManager == null) return;
|
||||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||||
|
||||
if (!_saveManager.DoesSaveExist(saveInfo.FileName))
|
||||
{
|
||||
RefreshSlotsData();
|
||||
return;
|
||||
}
|
||||
|
||||
LoadAndStartGame(saveInfo.FileName).Forget();
|
||||
}
|
||||
|
||||
private void OnEmptySlotClicked()
|
||||
{
|
||||
Debug.Log("[SelectSaveWindow] Empty slot clicked.");
|
||||
if (_isBusy)
|
||||
return;
|
||||
|
||||
if (_newSaveWindow == null)
|
||||
{
|
||||
Debug.LogWarning("[SelectSaveWindow] NewSaveWindow reference not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_newSaveWindow.Open();
|
||||
}
|
||||
|
||||
private async UniTask LoadAndStartGame(string profileName)
|
||||
{
|
||||
SetBusy(true);
|
||||
|
||||
try
|
||||
{
|
||||
await _saveManager.LoadGameData(profileName);
|
||||
OnCloseWindow?.Invoke();
|
||||
_gameService?.StartGame().Forget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SelectSaveWindow] Failed to load profile '{profileName}': {ex}");
|
||||
SetBusy(false);
|
||||
RefreshSlotsData();
|
||||
RestoreSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotDeleteClicked(SaveFileInfo saveInfo)
|
||||
{
|
||||
if (_isBusy) return;
|
||||
if (_saveManager == null) return;
|
||||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||||
|
||||
// No confirm window wired? Do a direct delete.
|
||||
if (_confirmDeleteWindow == null)
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
return;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_confirmDeleteWindow.Open(saveInfo);
|
||||
}
|
||||
|
||||
private void HandleConfirmDelete(SaveFileInfo saveInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCancelDelete()
|
||||
{
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
}
|
||||
|
||||
private void TryDeleteAndRefresh(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return;
|
||||
|
||||
if (!_saveManager.DoesSaveExist(fileName))
|
||||
{
|
||||
RefreshSlotsData();
|
||||
return;
|
||||
}
|
||||
|
||||
var deleted = _saveManager.Delete(fileName);
|
||||
if (!deleted)
|
||||
Debug.LogWarning($"[SelectSaveWindow] Failed to delete save '{fileName}'.");
|
||||
|
||||
RefreshSlotsData();
|
||||
}
|
||||
|
||||
private void RestoreSelection()
|
||||
{
|
||||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||||
SelectSlot(_currentSelectionIndex);
|
||||
}
|
||||
|
||||
private void SelectBackButton()
|
||||
{
|
||||
if (_backButton == null) return;
|
||||
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(_backButton.gameObject);
|
||||
else
|
||||
_backButton.Select();
|
||||
}
|
||||
|
||||
private void SelectSlot(int index)
|
||||
{
|
||||
if (_instantiatedSlots.Count == 0)
|
||||
{
|
||||
SelectBackButton();
|
||||
return;
|
||||
}
|
||||
|
||||
index = Mathf.Clamp(index, 0, _instantiatedSlots.Count - 1);
|
||||
_currentSelectionIndex = index;
|
||||
|
||||
var go = _instantiatedSlots[index]?.GetSelectableGameObject();
|
||||
if (go == null) return;
|
||||
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/SelectSaveWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/SelectSaveWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06df6456fa0544518f3c8d3a6802fc5c
|
||||
timeCreated: 1770231671
|
||||
726
Assets/Scripts/UI/Menus/SettingsWindow.cs
Normal file
726
Assets/Scripts/UI/Menus/SettingsWindow.cs
Normal file
@@ -0,0 +1,726 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/SettingsWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/SettingsWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37445792122d444baf3ac50efe28744f
|
||||
timeCreated: 1769787064
|
||||
364
Assets/Scripts/UI/Menus/VerticalScrollbar.cs
Normal file
364
Assets/Scripts/UI/Menus/VerticalScrollbar.cs
Normal file
@@ -0,0 +1,364 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class VerticalScrollbar : MonoBehaviour, IDragHandler, IPointerDownHandler
|
||||
{
|
||||
private static readonly Vector3[] Corners = new Vector3[4];
|
||||
|
||||
[Header("Hierarchy")]
|
||||
[SerializeField]
|
||||
private RectTransform _viewport;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _content;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _trackRect;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _handleRect;
|
||||
|
||||
[Header("Scroll Settings")]
|
||||
[SerializeField]
|
||||
private float _wheelPixels = 80f;
|
||||
|
||||
[SerializeField]
|
||||
private float _padSpeed = 900f;
|
||||
|
||||
[SerializeField]
|
||||
private float _inputSystemWheelScale = 0.05f;
|
||||
|
||||
[Header("Handle")]
|
||||
[SerializeField]
|
||||
private bool _useCustomHandleSizing;
|
||||
|
||||
[SerializeField]
|
||||
private float _minHandleHeight = 24f;
|
||||
|
||||
[Header("Alignment")]
|
||||
[SerializeField]
|
||||
private bool _centerContentWhenNotScrollable = true;
|
||||
|
||||
[SerializeField]
|
||||
private float _topInset = 6f;
|
||||
|
||||
[SerializeField]
|
||||
private float _bottomInset = 6f;
|
||||
|
||||
[Header("Track")]
|
||||
[SerializeField]
|
||||
private bool _hideTrackWhenNotScrollable = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Header("Editor Debug")]
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _debugNormalized;
|
||||
#endif
|
||||
|
||||
private bool _isScrollable;
|
||||
private float _scrollRange;
|
||||
private Camera _uiCamera;
|
||||
|
||||
public float Normalized { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
_uiCamera = canvas.worldCamera;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleMouseWheel();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_viewport == null || _content == null || _trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
if (!TryGetContentBounds(out var top, out var bottom))
|
||||
return;
|
||||
|
||||
var contentHeight = top - bottom;
|
||||
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
||||
|
||||
_isScrollable = contentHeight > viewportHeight;
|
||||
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
||||
Normalized = Mathf.Clamp01(_debugNormalized);
|
||||
|
||||
if (_centerContentWhenNotScrollable && !_isScrollable)
|
||||
{
|
||||
CenterContent(top, bottom);
|
||||
Normalized = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
||||
SetContentY(offset);
|
||||
|
||||
if (Normalized <= 0.0001f)
|
||||
AlignFirstChildToTop();
|
||||
}
|
||||
|
||||
UpdateTrackVisibility();
|
||||
UpdateHandle();
|
||||
}
|
||||
#endif
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
DragHandle(eventData);
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
DragHandle(eventData);
|
||||
}
|
||||
|
||||
public void Rebuild()
|
||||
{
|
||||
if (_viewport == null || _content == null)
|
||||
return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
if (!TryGetContentBounds(out var top, out var bottom))
|
||||
return;
|
||||
|
||||
var contentHeight = top - bottom;
|
||||
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
||||
|
||||
_isScrollable = contentHeight > viewportHeight;
|
||||
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
||||
|
||||
if (_centerContentWhenNotScrollable && !_isScrollable)
|
||||
{
|
||||
CenterContent(top, bottom);
|
||||
Normalized = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetNormalized(Normalized);
|
||||
}
|
||||
|
||||
UpdateTrackVisibility();
|
||||
UpdateHandle();
|
||||
}
|
||||
|
||||
public void SetNormalized(float normalized)
|
||||
{
|
||||
Normalized = Mathf.Clamp01(normalized);
|
||||
|
||||
if (!_isScrollable)
|
||||
return;
|
||||
|
||||
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
||||
SetContentY(offset);
|
||||
|
||||
if (Normalized <= 0.0001f)
|
||||
AlignFirstChildToTop();
|
||||
|
||||
UpdateHandle();
|
||||
}
|
||||
|
||||
private void CenterContent(float top, float bottom)
|
||||
{
|
||||
var contentCenter = (top + bottom) * 0.5f;
|
||||
var viewportCenter = (_viewport.rect.yMin + _viewport.rect.yMax) * 0.5f;
|
||||
|
||||
var delta = viewportCenter - contentCenter;
|
||||
|
||||
var position = _content.anchoredPosition;
|
||||
position.y += delta;
|
||||
_content.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void AlignFirstChildToTop()
|
||||
{
|
||||
RectTransform first = null;
|
||||
|
||||
for (var i = 0; i < _content.childCount; i++)
|
||||
{
|
||||
var child = _content.GetChild(i) as RectTransform;
|
||||
if (child != null && child.gameObject.activeSelf)
|
||||
{
|
||||
first = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first == null)
|
||||
return;
|
||||
|
||||
first.GetWorldCorners(Corners);
|
||||
|
||||
var childTop = _viewport.InverseTransformPoint(Corners[1]).y;
|
||||
var targetTop = _viewport.rect.yMax - _topInset;
|
||||
|
||||
var delta = targetTop - childTop;
|
||||
|
||||
var position = _content.anchoredPosition;
|
||||
position.y += delta;
|
||||
_content.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void ScrollByPixels(float pixels)
|
||||
{
|
||||
if (!_isScrollable)
|
||||
return;
|
||||
|
||||
var current = Normalized * _scrollRange;
|
||||
var next = Mathf.Clamp(current + pixels, 0f, _scrollRange);
|
||||
|
||||
Normalized = _scrollRange > 0f ? next / _scrollRange : 0f;
|
||||
SetNormalized(Normalized);
|
||||
}
|
||||
|
||||
private void DragHandle(PointerEventData eventData)
|
||||
{
|
||||
if (!_isScrollable || _trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_trackRect, eventData.position, _uiCamera,
|
||||
out var localPoint))
|
||||
return;
|
||||
|
||||
var halfHandleHeight = _handleRect.rect.height * 0.5f;
|
||||
|
||||
var min = _trackRect.rect.yMin + halfHandleHeight;
|
||||
var max = _trackRect.rect.yMax - halfHandleHeight;
|
||||
|
||||
var y = Mathf.Clamp(localPoint.y, min, max);
|
||||
var normalized = 1f - Mathf.InverseLerp(min, max, y);
|
||||
|
||||
SetNormalized(normalized);
|
||||
}
|
||||
|
||||
private void HandleMouseWheel()
|
||||
{
|
||||
var wheel = ReadMouseWheelDelta();
|
||||
if (Mathf.Abs(wheel) > 0.01f)
|
||||
ScrollByPixels(-wheel * _wheelPixels);
|
||||
}
|
||||
|
||||
private float ReadMouseWheelDelta()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
if (Mouse.current != null)
|
||||
return Mouse.current.scroll.ReadValue().y * _inputSystemWheelScale;
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
return Input.mouseScrollDelta.y;
|
||||
#endif
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void SetContentY(float y)
|
||||
{
|
||||
var position = _content.anchoredPosition;
|
||||
position.y = y;
|
||||
_content.anchoredPosition = position;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateHandle()
|
||||
{
|
||||
if (_trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (!_isScrollable)
|
||||
{
|
||||
_handleRect.anchoredPosition = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCustomHandleSizing)
|
||||
{
|
||||
var ratio = Mathf.Clamp01(_viewport.rect.height / (_scrollRange + _viewport.rect.height));
|
||||
var height = Mathf.Max(_trackRect.rect.height * ratio, _minHandleHeight);
|
||||
_handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
}
|
||||
|
||||
var half = _handleRect.rect.height * 0.5f;
|
||||
|
||||
var min = _trackRect.rect.yMin + half;
|
||||
var max = _trackRect.rect.yMax - half;
|
||||
|
||||
var y = Mathf.Lerp(max, min, Normalized);
|
||||
|
||||
var position = _handleRect.anchoredPosition;
|
||||
position.y = y;
|
||||
_handleRect.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void UpdateTrackVisibility()
|
||||
{
|
||||
if (_trackRect == null)
|
||||
return;
|
||||
|
||||
if (_hideTrackWhenNotScrollable)
|
||||
_trackRect.gameObject.SetActive(_isScrollable);
|
||||
}
|
||||
|
||||
private bool TryGetContentBounds(out float top, out float bottom)
|
||||
{
|
||||
top = float.MinValue;
|
||||
bottom = float.MaxValue;
|
||||
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < _content.childCount; i++)
|
||||
{
|
||||
var child = _content.GetChild(i) as RectTransform;
|
||||
if (child == null || !child.gameObject.activeSelf)
|
||||
continue;
|
||||
|
||||
child.GetWorldCorners(Corners);
|
||||
|
||||
for (var c = 0; c < 4; c++)
|
||||
{
|
||||
var local = _viewport.InverseTransformPoint(Corners[c]);
|
||||
top = Mathf.Max(top, local.y);
|
||||
bottom = Mathf.Min(bottom, local.y);
|
||||
}
|
||||
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/Menus/VerticalScrollbar.cs.meta
Normal file
2
Assets/Scripts/UI/Menus/VerticalScrollbar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d868730b4606b4100910e781df521d52
|
||||
Reference in New Issue
Block a user