Restructured for new direction.
This commit is contained in:
@@ -10,11 +10,6 @@ namespace BriarQueen.UI.Codex
|
||||
[SerializeField]
|
||||
private Button _button;
|
||||
|
||||
[SerializeField]
|
||||
private Sprite _defaultBackground;
|
||||
[SerializeField]
|
||||
private Sprite _selectedBackground;
|
||||
|
||||
public CodexType Category { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
@@ -35,13 +30,6 @@ namespace BriarQueen.UI.Codex
|
||||
{
|
||||
Category = category;
|
||||
|
||||
SetSelected(false);
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (_selectedBackground != null)
|
||||
_button.image.sprite = selected ? _selectedBackground : _defaultBackground;
|
||||
}
|
||||
|
||||
private void HandleClicked()
|
||||
|
||||
@@ -1,85 +1,31 @@
|
||||
using System;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Codex
|
||||
{
|
||||
[ExecuteAlways]
|
||||
public class CodexEntryButton : MonoBehaviour
|
||||
{
|
||||
private const float MIN_WIDTH = 300f;
|
||||
private const float MAX_WIDTH = 400f;
|
||||
private const float FIXED_HEIGHT = 70f;
|
||||
[SerializeField] private Button _button;
|
||||
[SerializeField] private UnderlineButton _underlineButton;
|
||||
[SerializeField] private TextMeshProUGUI _label;
|
||||
|
||||
[SerializeField]
|
||||
private Button _button;
|
||||
public CodexEntrySo Entry { get; private set; }
|
||||
public UnderlineButton UnderlineButton => _underlineButton;
|
||||
|
||||
[SerializeField]
|
||||
private Image _buttonBackgroundImage;
|
||||
|
||||
[SerializeField]
|
||||
private Sprite _defaultSprite;
|
||||
|
||||
[SerializeField]
|
||||
private Sprite _selectedSprite;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _label;
|
||||
|
||||
[SerializeField]
|
||||
private float _leftPadding = 24f;
|
||||
|
||||
[SerializeField]
|
||||
private float _rightPadding = 24f;
|
||||
|
||||
[SerializeField]
|
||||
private float _extraWidthSafety = 20f;
|
||||
|
||||
[SerializeField]
|
||||
private float _autoSizeMinFontSize = 18f;
|
||||
|
||||
[SerializeField]
|
||||
private bool _debugInEditor = true;
|
||||
|
||||
private bool _defaultSpriteCachedFromImage;
|
||||
|
||||
private string _lastText;
|
||||
private float _lastWidth = -1f;
|
||||
|
||||
public CodexEntrySo Entry { get; private set; }
|
||||
public event Action<CodexEntrySo> OnEntryClicked;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
CacheDefaultSpriteFromImageIfNeeded();
|
||||
AddButtonListener();
|
||||
RefreshVisuals();
|
||||
ApplyCurrentBackground(false);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (_label == null)
|
||||
return;
|
||||
|
||||
if (_lastText != _label.text)
|
||||
RefreshVisuals();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ResolveReferences();
|
||||
CacheDefaultSpriteFromImageIfNeeded();
|
||||
AddButtonListener();
|
||||
RefreshVisuals();
|
||||
ApplyCurrentBackground(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -87,176 +33,38 @@ namespace BriarQueen.UI.Codex
|
||||
RemoveButtonListener();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
CacheDefaultSpriteFromImageIfNeeded();
|
||||
RefreshVisuals();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
ApplyCurrentBackground(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public event Action<CodexEntrySo> OnEntryClicked;
|
||||
|
||||
public void Initialize(CodexEntrySo entry)
|
||||
{
|
||||
Entry = entry;
|
||||
|
||||
ResolveReferences();
|
||||
CacheDefaultSpriteFromImageIfNeeded();
|
||||
|
||||
if (_label != null)
|
||||
_label.text = entry != null ? entry.Title : string.Empty;
|
||||
|
||||
RefreshVisuals();
|
||||
SetSelected(false);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
ApplyCurrentBackground(selected);
|
||||
// Selection state is now driven entirely by UnderlineButtonGroup
|
||||
// via UnderlineButton — nothing to do here directly
|
||||
}
|
||||
|
||||
private void HandleClicked()
|
||||
{
|
||||
if (Entry == null)
|
||||
return;
|
||||
|
||||
if (Entry == null) return;
|
||||
OnEntryClicked?.Invoke(Entry);
|
||||
}
|
||||
|
||||
private void RefreshVisuals()
|
||||
{
|
||||
ResizeToFitLabel();
|
||||
}
|
||||
|
||||
private void ResizeToFitLabel()
|
||||
{
|
||||
if (_button == null || _label == null)
|
||||
return;
|
||||
|
||||
var buttonRect = _button.GetComponent<RectTransform>();
|
||||
var labelRect = _label.rectTransform;
|
||||
|
||||
if (buttonRect == null || labelRect == null)
|
||||
return;
|
||||
|
||||
StretchLabelToButton(labelRect);
|
||||
|
||||
_label.margin = new Vector4(_leftPadding, _label.margin.y, _rightPadding, _label.margin.w);
|
||||
_label.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_label.overflowMode = TextOverflowModes.Overflow;
|
||||
_label.enableAutoSizing = false;
|
||||
|
||||
_label.ForceMeshUpdate();
|
||||
|
||||
var preferredSize = _label.GetPreferredValues(_label.text, Mathf.Infinity, Mathf.Infinity);
|
||||
var desiredWidth = preferredSize.x + _leftPadding + _rightPadding + _extraWidthSafety;
|
||||
var targetWidth = Mathf.Clamp(desiredWidth, MIN_WIDTH, MAX_WIDTH);
|
||||
|
||||
var needsAutoSizing = desiredWidth > MAX_WIDTH;
|
||||
|
||||
if (needsAutoSizing)
|
||||
{
|
||||
_label.enableAutoSizing = true;
|
||||
_label.fontSizeMin = _autoSizeMinFontSize;
|
||||
_label.fontSizeMax = _label.fontSize;
|
||||
_label.overflowMode = TextOverflowModes.Truncate;
|
||||
}
|
||||
|
||||
buttonRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetWidth);
|
||||
buttonRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, FIXED_HEIGHT);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (_debugInEditor && (!Mathf.Approximately(targetWidth, _lastWidth) || _lastText != _label.text))
|
||||
Debug.Log(
|
||||
$"[CodexEntryButton] '{_label.text}'\n" +
|
||||
$"Preferred: {preferredSize.x:F1} | Desired: {desiredWidth:F1} | Final: {targetWidth:F1}\n" +
|
||||
$"AutoSize: {needsAutoSizing}",
|
||||
this);
|
||||
#endif
|
||||
|
||||
_lastText = _label.text;
|
||||
_lastWidth = targetWidth;
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(buttonRect);
|
||||
_label.ForceMeshUpdate();
|
||||
}
|
||||
|
||||
private void StretchLabelToButton(RectTransform labelRect)
|
||||
{
|
||||
labelRect.anchorMin = new Vector2(0f, 0f);
|
||||
labelRect.anchorMax = new Vector2(1f, 1f);
|
||||
labelRect.offsetMin = Vector2.zero;
|
||||
labelRect.offsetMax = Vector2.zero;
|
||||
labelRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_buttonBackgroundImage == null && _button != null)
|
||||
_buttonBackgroundImage = _button.GetComponent<Image>();
|
||||
}
|
||||
|
||||
private void CacheDefaultSpriteFromImageIfNeeded()
|
||||
{
|
||||
if (_buttonBackgroundImage == null)
|
||||
return;
|
||||
|
||||
if (_defaultSprite != null)
|
||||
return;
|
||||
|
||||
if (_buttonBackgroundImage.sprite == null)
|
||||
return;
|
||||
|
||||
_defaultSprite = _buttonBackgroundImage.sprite;
|
||||
_defaultSpriteCachedFromImage = true;
|
||||
}
|
||||
|
||||
private void ApplyCurrentBackground(bool selected)
|
||||
{
|
||||
if (_buttonBackgroundImage == null)
|
||||
return;
|
||||
|
||||
Sprite targetSprite = null;
|
||||
|
||||
if (selected && _selectedSprite != null)
|
||||
targetSprite = _selectedSprite;
|
||||
else if (_defaultSprite != null)
|
||||
targetSprite = _defaultSprite;
|
||||
else if (_buttonBackgroundImage.sprite != null) targetSprite = _buttonBackgroundImage.sprite;
|
||||
|
||||
if (targetSprite != null)
|
||||
{
|
||||
_buttonBackgroundImage.enabled = true;
|
||||
_buttonBackgroundImage.sprite = targetSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"[CodexEntryButton] No default background sprite is available. " +
|
||||
"Assign _defaultSprite in the inspector or ensure the Image has a sprite on the prefab.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddButtonListener()
|
||||
{
|
||||
if (_button == null)
|
||||
return;
|
||||
|
||||
if (_button == null) return;
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
_button.onClick.AddListener(HandleClicked);
|
||||
}
|
||||
|
||||
private void RemoveButtonListener()
|
||||
{
|
||||
if (_button == null)
|
||||
return;
|
||||
|
||||
if (_button == null) return;
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace BriarQueen.UI.Codex
|
||||
[SerializeField]
|
||||
private bool _debugInEditor = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _refreshIcon;
|
||||
|
||||
private string _lastText;
|
||||
private float _lastWidth = -1f;
|
||||
|
||||
@@ -97,6 +100,9 @@ namespace BriarQueen.UI.Codex
|
||||
|
||||
private void RefreshVisuals()
|
||||
{
|
||||
if(!_refreshIcon)
|
||||
return;
|
||||
|
||||
ResizeToFitLabel();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,6 +73,7 @@ namespace BriarQueen.UI.HUD
|
||||
private SettingsService _settingsService;
|
||||
private RectTransform _textRect;
|
||||
private Camera _uiCamera;
|
||||
private string _displayedTooltipText = string.Empty;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -93,10 +94,11 @@ namespace BriarQueen.UI.HUD
|
||||
{
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
SetVisible(false);
|
||||
ClearTooltipText();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateTooltip();
|
||||
FollowCursor();
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ namespace BriarQueen.UI.HUD
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator, InputManager inputManager, SettingsService settingsService,
|
||||
private void Construct(EventCoordinator eventCoordinator, InputManager inputManager, SettingsService settingsService,
|
||||
UICursorService cursorService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
@@ -183,12 +185,13 @@ namespace BriarQueen.UI.HUD
|
||||
private void UpdateTooltip()
|
||||
{
|
||||
if (!_tooltipText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
ClearTooltipText();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,17 +200,34 @@ namespace BriarQueen.UI.HUD
|
||||
|
||||
if (!show)
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
ClearTooltipText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_displayedTooltipText, text))
|
||||
{
|
||||
SetVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
_displayedTooltipText = text;
|
||||
_tooltipText.text = text;
|
||||
SetVisible(true);
|
||||
|
||||
UpdateLayoutToText();
|
||||
}
|
||||
|
||||
private void ClearTooltipText()
|
||||
{
|
||||
if (_tooltipText)
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
}
|
||||
|
||||
_displayedTooltipText = string.Empty;
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
private string BuildTooltipText()
|
||||
{
|
||||
if (_hoveredInteractable == null)
|
||||
@@ -363,4 +383,4 @@ namespace BriarQueen.UI.HUD
|
||||
_rootRect.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace BriarQueen.UI.HUD
|
||||
private Sequence _hudSequence;
|
||||
private CancellationTokenSource _interactCancellationTokenSource;
|
||||
|
||||
private Sequence _interactErrorSequence;
|
||||
private Sequence _interactTextSequence;
|
||||
private UIManager _uiManager;
|
||||
|
||||
public CursorTooltip CursorTooltip => _cursorTooltip;
|
||||
@@ -132,10 +132,10 @@ namespace BriarQueen.UI.HUD
|
||||
try
|
||||
{
|
||||
// Fade in
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
_interactTextSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeIn));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
await _interactTextSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 1f;
|
||||
|
||||
@@ -145,10 +145,10 @@ namespace BriarQueen.UI.HUD
|
||||
cancellationToken: token);
|
||||
|
||||
// Fade out
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
_interactTextSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeOut));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
await _interactTextSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 0f;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ namespace BriarQueen.UI.HUD
|
||||
_interactCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
_interactErrorSequence = default;
|
||||
_interactTextSequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,10 +252,10 @@ namespace BriarQueen.UI.HUD
|
||||
|
||||
private void StopInteractErrorTween()
|
||||
{
|
||||
if (_interactErrorSequence.isAlive)
|
||||
_interactErrorSequence.Stop();
|
||||
if (_interactTextSequence.isAlive)
|
||||
_interactTextSequence.Stop();
|
||||
|
||||
_interactErrorSequence = default;
|
||||
_interactTextSequence = default;
|
||||
|
||||
if (_interactCancellationTokenSource != null)
|
||||
{
|
||||
|
||||
@@ -2,12 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
@@ -42,6 +43,7 @@ namespace BriarQueen.UI.HUD
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private PlayerManager _playerManager;
|
||||
private TutorialService _tutorialService;
|
||||
private DestructionService _destructionService;
|
||||
|
||||
private int _currentPage;
|
||||
private int _selectedIndex = -1;
|
||||
@@ -69,13 +71,15 @@ namespace BriarQueen.UI.HUD
|
||||
PlayerManager playerManager,
|
||||
AddressableManager addressableManager,
|
||||
AssetRegistry assetRegistry,
|
||||
TutorialService tutorialService)
|
||||
TutorialService tutorialService,
|
||||
DestructionService destructionService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_playerManager = playerManager;
|
||||
_addressableManager = addressableManager;
|
||||
_assetRegistry = assetRegistry;
|
||||
_tutorialService = tutorialService;
|
||||
_destructionService = destructionService;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
@@ -180,7 +184,7 @@ namespace BriarQueen.UI.HUD
|
||||
continue;
|
||||
|
||||
child.gameObject.SetActive(false);
|
||||
Destroy(child.gameObject);
|
||||
_destructionService.Destroy(child.gameObject).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,25 +246,41 @@ private async UniTask RebuildInventoryAsync()
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var slotObj = await _addressableManager.InstantiateAsync(slotRef, parent: _content);
|
||||
token.ThrowIfCancellationRequested();
|
||||
GameObject slotObj = null;
|
||||
|
||||
if (slotObj == null)
|
||||
try
|
||||
{
|
||||
Debug.LogWarning("[InventoryBar] AddressableManager returned null slot object.");
|
||||
continue;
|
||||
}
|
||||
slotObj = await _addressableManager.InstantiateAsync(slotRef, parent: _content);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var slot = slotObj.GetComponent<UIInventorySlot>();
|
||||
if (slot == null)
|
||||
if (slotObj == null)
|
||||
{
|
||||
Debug.LogWarning("[InventoryBar] AddressableManager returned null slot object.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var slot = slotObj.GetComponent<UIInventorySlot>();
|
||||
if (slot == null)
|
||||
{
|
||||
Debug.LogWarning("[InventoryBar] Instantiated slot prefab is missing UIInventorySlot.");
|
||||
await _destructionService.Destroy(slotObj);
|
||||
slotObj = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
slot.Initialize(this, item);
|
||||
_inventorySlots.Add(slot);
|
||||
|
||||
// Ownership has moved to the active slot list.
|
||||
slotObj = null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.LogWarning("[InventoryBar] Instantiated slot prefab is missing UIInventorySlot.");
|
||||
Destroy(slotObj);
|
||||
continue;
|
||||
}
|
||||
if (slotObj != null)
|
||||
await _destructionService.Destroy(slotObj);
|
||||
|
||||
slot.Initialize(this, item);
|
||||
_inventorySlots.Add(slot);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, token);
|
||||
@@ -563,4 +583,4 @@ private async UniTask RebuildInventoryAsync()
|
||||
SetSelectedIndex(prevIndex, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
400
Assets/Scripts/UI/Menus/Components/AnimatedSelectionButton.cs
Normal file
400
Assets/Scripts/UI/Menus/Components/AnimatedSelectionButton.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class AnimatedSelectionButton : MonoBehaviour,
|
||||
IPointerEnterHandler,
|
||||
IPointerExitHandler,
|
||||
IPointerClickHandler,
|
||||
ISelectHandler,
|
||||
IDeselectHandler
|
||||
{
|
||||
private const string DefaultRevealPropertyName = "_Reveal";
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Button _button;
|
||||
[SerializeField] private RectTransform _selectionBackground;
|
||||
[SerializeField] private TextMeshProUGUI _label;
|
||||
|
||||
[Header("Background Sizing")]
|
||||
[SerializeField] private Vector2 _backgroundPadding = new(24f, 6f);
|
||||
[SerializeField] private bool _matchBackgroundToLabelPosition = true;
|
||||
|
||||
[Header("Background Material")]
|
||||
[SerializeField] private bool _useShaderReveal = true;
|
||||
[SerializeField] private Material _selectionMaterial;
|
||||
[SerializeField] private Color _selectionColor = new(0.02f, 0.018f, 0.015f, 0.35f);
|
||||
[SerializeField] private string _revealPropertyName = "_Reveal";
|
||||
|
||||
[Header("Edit Preview")]
|
||||
[SerializeField] private bool _previewInEditMode;
|
||||
[SerializeField][Range(0f, 1f)] private float _previewReveal = 1f;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField] private float _selectDuration = 0.18f;
|
||||
[SerializeField] private float _deselectDuration = 0.12f;
|
||||
[SerializeField] private Ease _ease = Ease.InOutSine;
|
||||
[SerializeField] private bool _useUnscaledTime = true;
|
||||
|
||||
private CancellationTokenSource _selectionCts;
|
||||
private Graphic _selectionBackgroundGraphic;
|
||||
private Material _sourceSelectionMaterial;
|
||||
private Material _runtimeSelectionMaterial;
|
||||
private Sequence _selectionSequence;
|
||||
private int _animationVersion;
|
||||
private int _colorPropertyId;
|
||||
private int _revealPropertyId;
|
||||
private float _backgroundProgress;
|
||||
|
||||
private bool _isSelected;
|
||||
|
||||
// Group listens to these to coordinate highlight state
|
||||
public event Action<AnimatedSelectionButton> SelectionRequested;
|
||||
public event Action<AnimatedSelectionButton> HoverEntered;
|
||||
public event Action<AnimatedSelectionButton> HoverExited;
|
||||
|
||||
public Button Button
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_button == null)
|
||||
_button = GetComponent<Button>();
|
||||
return _button;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSelected => _isSelected;
|
||||
|
||||
// ── Unity lifecycle ───────────────────────────────────────────
|
||||
|
||||
private void Reset() => ResolveReferences();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
ApplyEditPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyInstant(false);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
DestroyRuntimeSelectionMaterial();
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
ApplyEditPreview();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying || !_previewInEditMode) return;
|
||||
ApplyEditPreview();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CancelSelectionTween();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelSelectionTween();
|
||||
DestroyRuntimeSelectionMaterial();
|
||||
}
|
||||
|
||||
// ── Pointer / selection events ────────────────────────────────
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
HoverEntered?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
HoverExited?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
HoverEntered?.Invoke(this);
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData)
|
||||
{
|
||||
HoverExited?.Invoke(this);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Called by the group to set whether this button's selected state.</summary>
|
||||
public void SetSelectedState(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
}
|
||||
|
||||
/// <summary>Called by the group to drive the highlight visual directly.</summary>
|
||||
public void SetHighlight(bool show, bool instant = false)
|
||||
{
|
||||
if (instant || !gameObject.activeInHierarchy)
|
||||
{
|
||||
CancelSelectionTween();
|
||||
RefreshBackgroundSize();
|
||||
ApplyBackgroundProgress(show ? 1f : 0f);
|
||||
if (_selectionBackground != null)
|
||||
_selectionBackground.gameObject.SetActive(show);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateSelection(show).Forget();
|
||||
}
|
||||
|
||||
/// <summary>Legacy — drives both state and highlight together for non-group usage.</summary>
|
||||
public void SetSelected(bool selected, bool instant = false)
|
||||
{
|
||||
_isSelected = selected;
|
||||
SetHighlight(selected, instant);
|
||||
}
|
||||
|
||||
public void ApplyInstant(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
RefreshBackgroundSize();
|
||||
ApplyBackgroundProgress(selected ? 1f : 0f);
|
||||
|
||||
if (_selectionBackground != null)
|
||||
_selectionBackground.gameObject.SetActive(selected);
|
||||
}
|
||||
|
||||
public GameObject GetSelectableGameObject() => gameObject;
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private async UniTaskVoid AnimateSelection(bool show)
|
||||
{
|
||||
CancelSelectionTween();
|
||||
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
RefreshBackgroundSize();
|
||||
|
||||
var duration = show ? _selectDuration : _deselectDuration;
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ApplyInstant(_isSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectionBackground.gameObject.SetActive(true);
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
_selectionCts = new CancellationTokenSource();
|
||||
_animationVersion++;
|
||||
|
||||
var localVersion = _animationVersion;
|
||||
var target = show ? 1f : 0f;
|
||||
var start = _backgroundProgress;
|
||||
|
||||
_selectionSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
start,
|
||||
target,
|
||||
duration,
|
||||
ApplyBackgroundProgress,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectionSequence.ToUniTask(cancellationToken: _selectionCts.Token);
|
||||
ApplyBackgroundProgress(target);
|
||||
|
||||
if (!show)
|
||||
_selectionBackground.gameObject.SetActive(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (localVersion == _animationVersion)
|
||||
{
|
||||
_selectionSequence = default;
|
||||
|
||||
if (_selectionCts != null)
|
||||
{
|
||||
_selectionCts.Dispose();
|
||||
_selectionCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBackgroundProgress(float progress)
|
||||
{
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
|
||||
var scale = _selectionBackground.localScale;
|
||||
_backgroundProgress = Mathf.Clamp01(progress);
|
||||
scale.x = UsesShaderReveal() ? 1f : _backgroundProgress;
|
||||
_selectionBackground.localScale = scale;
|
||||
|
||||
if (UsesShaderReveal())
|
||||
ApplyRuntimeMaterialProperties();
|
||||
}
|
||||
|
||||
private void RefreshBackgroundSize()
|
||||
{
|
||||
if (_selectionBackground == null || _label == null) return;
|
||||
|
||||
_label.ForceMeshUpdate();
|
||||
|
||||
var preferredSize = _label.GetPreferredValues(_label.text, Mathf.Infinity, Mathf.Infinity);
|
||||
var backgroundSize = preferredSize + _backgroundPadding;
|
||||
|
||||
_selectionBackground.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, backgroundSize.x);
|
||||
_selectionBackground.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, backgroundSize.y);
|
||||
|
||||
if (!_matchBackgroundToLabelPosition) return;
|
||||
|
||||
var labelRect = _label.rectTransform;
|
||||
_selectionBackground.position = labelRect.TransformPoint(labelRect.rect.center);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_button == null)
|
||||
_button = GetComponent<Button>();
|
||||
|
||||
if (_label == null)
|
||||
_label = GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
|
||||
if (_selectionBackground != null && _selectionBackgroundGraphic == null)
|
||||
_selectionBackgroundGraphic = _selectionBackground.GetComponent<Graphic>();
|
||||
|
||||
_revealPropertyId = Shader.PropertyToID(string.IsNullOrWhiteSpace(_revealPropertyName)
|
||||
? DefaultRevealPropertyName
|
||||
: _revealPropertyName);
|
||||
_colorPropertyId = Shader.PropertyToID("_Color");
|
||||
}
|
||||
|
||||
private void ApplyEditPreview()
|
||||
{
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
RefreshBackgroundSize();
|
||||
|
||||
if (!_previewInEditMode)
|
||||
{
|
||||
ApplyBackgroundProgress(0f);
|
||||
_selectionBackground.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectionBackground.gameObject.SetActive(true);
|
||||
ApplyBackgroundProgress(_previewReveal);
|
||||
}
|
||||
|
||||
private void EnsureRuntimeSelectionMaterial()
|
||||
{
|
||||
if (!_useShaderReveal || _selectionBackground == null) return;
|
||||
|
||||
if (_selectionBackgroundGraphic == null)
|
||||
_selectionBackgroundGraphic = _selectionBackground.GetComponent<Graphic>();
|
||||
|
||||
if (_selectionBackgroundGraphic == null || _runtimeSelectionMaterial != null) return;
|
||||
|
||||
var sourceMaterial = _selectionMaterial != null
|
||||
? _selectionMaterial
|
||||
: _selectionBackgroundGraphic.material;
|
||||
|
||||
if (sourceMaterial == null || !sourceMaterial.HasProperty(_revealPropertyId)) return;
|
||||
|
||||
_sourceSelectionMaterial = sourceMaterial;
|
||||
_runtimeSelectionMaterial = new Material(sourceMaterial)
|
||||
{
|
||||
name = $"{sourceMaterial.name} (Runtime)",
|
||||
hideFlags = HideFlags.DontSave
|
||||
};
|
||||
ApplyRuntimeMaterialProperties();
|
||||
_selectionBackgroundGraphic.material = _runtimeSelectionMaterial;
|
||||
}
|
||||
|
||||
private void ApplyRuntimeMaterialProperties()
|
||||
{
|
||||
if (_runtimeSelectionMaterial == null) return;
|
||||
|
||||
if (_runtimeSelectionMaterial.HasProperty(_revealPropertyId))
|
||||
_runtimeSelectionMaterial.SetFloat(_revealPropertyId, _backgroundProgress);
|
||||
|
||||
if (_runtimeSelectionMaterial.HasProperty(_colorPropertyId))
|
||||
_runtimeSelectionMaterial.SetColor(_colorPropertyId, _selectionColor);
|
||||
}
|
||||
|
||||
private bool UsesShaderReveal()
|
||||
{
|
||||
return _useShaderReveal
|
||||
&& _runtimeSelectionMaterial != null
|
||||
&& _runtimeSelectionMaterial.HasProperty(_revealPropertyId);
|
||||
}
|
||||
|
||||
private void DestroyRuntimeSelectionMaterial()
|
||||
{
|
||||
if (_runtimeSelectionMaterial == null) return;
|
||||
|
||||
if (_selectionBackgroundGraphic != null &&
|
||||
_selectionBackgroundGraphic.material == _runtimeSelectionMaterial)
|
||||
_selectionBackgroundGraphic.material = _sourceSelectionMaterial;
|
||||
|
||||
if (Application.isPlaying)
|
||||
Destroy(_runtimeSelectionMaterial);
|
||||
else
|
||||
DestroyImmediate(_runtimeSelectionMaterial);
|
||||
|
||||
_runtimeSelectionMaterial = null;
|
||||
_sourceSelectionMaterial = null;
|
||||
}
|
||||
|
||||
private void CancelSelectionTween()
|
||||
{
|
||||
if (_selectionSequence.isAlive)
|
||||
_selectionSequence.Stop();
|
||||
|
||||
_selectionSequence = default;
|
||||
|
||||
if (_selectionCts == null) return;
|
||||
|
||||
try { _selectionCts.Cancel(); } catch { }
|
||||
_selectionCts.Dispose();
|
||||
_selectionCts = null;
|
||||
_animationVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8fbce7c4a9464f1297b6045ed5c9e71b
|
||||
timeCreated: 1770375662
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
public class AnimatedSelectionButtonGroup : MonoBehaviour
|
||||
{
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private AnimatedSelectionButton[] _buttons = Array.Empty<AnimatedSelectionButton>();
|
||||
|
||||
[SerializeField]
|
||||
private int _defaultSelectedIndex;
|
||||
|
||||
[Header("Selection")]
|
||||
[SerializeField]
|
||||
private bool _selectDefaultOnEnable = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _syncEventSystemSelection = true;
|
||||
|
||||
public AnimatedSelectionButton Current { get; private set; }
|
||||
public AnimatedSelectionButton CurrentHovered { get; private set; }
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_buttons = GetComponentsInChildren<AnimatedSelectionButton>(true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
BindButtons();
|
||||
|
||||
if (_selectDefaultOnEnable)
|
||||
SelectIndex(_defaultSelectedIndex, true);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnbindButtons();
|
||||
CurrentHovered = null;
|
||||
}
|
||||
|
||||
public void SelectIndex(int index, bool instant = false)
|
||||
{
|
||||
if (_buttons == null || _buttons.Length == 0) return;
|
||||
SelectButton(_buttons[Mathf.Clamp(index, 0, _buttons.Length - 1)], instant);
|
||||
}
|
||||
|
||||
public void SelectButton(AnimatedSelectionButton selectedButton, bool instant = false)
|
||||
{
|
||||
if (selectedButton == null) return;
|
||||
if (Current == selectedButton && !instant) return;
|
||||
|
||||
Current = selectedButton;
|
||||
|
||||
RefreshAllButtonStates(instant);
|
||||
SyncEventSystemSelection(selectedButton);
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private void RefreshAllButtonStates(bool instant = false)
|
||||
{
|
||||
if (_buttons == null) return;
|
||||
|
||||
foreach (var button in _buttons)
|
||||
{
|
||||
if (button == null) continue;
|
||||
|
||||
// A button shows its highlight if it is selected OR it is the
|
||||
// hovered button AND no other button is being hovered that would
|
||||
// take visual priority. Selected button highlight only shows when
|
||||
// nothing is hovered, or when it itself is hovered.
|
||||
var isSelected = button == Current;
|
||||
var isHovered = button == CurrentHovered;
|
||||
|
||||
var showHighlight = isSelected
|
||||
? CurrentHovered == null || isHovered // selected: show unless something else is hovered
|
||||
: isHovered; // unselected: show only if hovered
|
||||
|
||||
button.SetHighlight(showHighlight, instant);
|
||||
button.SetSelectedState(isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSelectionRequested(AnimatedSelectionButton selectedButton)
|
||||
{
|
||||
SelectButton(selectedButton);
|
||||
}
|
||||
|
||||
private void HandleHoverEntered(AnimatedSelectionButton button)
|
||||
{
|
||||
CurrentHovered = button;
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
private void HandleHoverExited(AnimatedSelectionButton button)
|
||||
{
|
||||
if (CurrentHovered == button)
|
||||
CurrentHovered = null;
|
||||
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
private void BindButtons()
|
||||
{
|
||||
if (_buttons == null) return;
|
||||
|
||||
foreach (var button in _buttons)
|
||||
{
|
||||
if (button == null) continue;
|
||||
button.SelectionRequested += HandleSelectionRequested;
|
||||
button.HoverEntered += HandleHoverEntered;
|
||||
button.HoverExited += HandleHoverExited;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnbindButtons()
|
||||
{
|
||||
if (_buttons == null) return;
|
||||
|
||||
foreach (var button in _buttons)
|
||||
{
|
||||
if (button == null) continue;
|
||||
button.SelectionRequested -= HandleSelectionRequested;
|
||||
button.HoverEntered -= HandleHoverEntered;
|
||||
button.HoverExited -= HandleHoverExited;
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncEventSystemSelection(AnimatedSelectionButton selectedButton)
|
||||
{
|
||||
if (!_syncEventSystemSelection || selectedButton.Button == null || EventSystem.current == null)
|
||||
return;
|
||||
|
||||
var selectedGameObject = selectedButton.Button.gameObject;
|
||||
if (EventSystem.current.currentSelectedGameObject == selectedGameObject) return;
|
||||
|
||||
EventSystem.current.SetSelectedGameObject(selectedGameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7c446e43fa2491d888c8bb4bb48eb52
|
||||
timeCreated: 1770375663
|
||||
171
Assets/Scripts/UI/Menus/Components/SaveSlotHighlight.cs
Normal file
171
Assets/Scripts/UI/Menus/Components/SaveSlotHighlight.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
public class SaveSlotHighlight : MonoBehaviour
|
||||
{
|
||||
[Header("Lines")]
|
||||
[SerializeField] private RectTransform _topLine;
|
||||
[SerializeField] private RectTransform _bottomLine;
|
||||
|
||||
[Header("Colours")]
|
||||
[SerializeField] private Color _hoverColour = new(0.65f, 0.10f, 0.10f, 0.5f);
|
||||
[SerializeField] private Color _selectedColour = new(0.65f, 0.10f, 0.10f, 1.0f);
|
||||
|
||||
[Header("Tween")]
|
||||
[SerializeField] private float _duration = 0.25f;
|
||||
[SerializeField] private Ease _ease = Ease.OutQuint;
|
||||
[SerializeField] private bool _useUnscaledTime = true;
|
||||
|
||||
private Image _topImage;
|
||||
private Image _bottomImage;
|
||||
private Tween _topTween;
|
||||
private Tween _bottomTween;
|
||||
private Tween _topColourTween;
|
||||
private Tween _bottomColourTween;
|
||||
|
||||
private bool _isSelected;
|
||||
private bool _isHovered;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_topLine != null) _topImage = _topLine.GetComponent<Image>();
|
||||
if (_bottomLine != null) _bottomImage = _bottomLine.GetComponent<Image>();
|
||||
|
||||
// Start fully scaled out on X
|
||||
SetScaleImmediate(0f);
|
||||
SetColourImmediate(Color.clear);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_topTween.Stop();
|
||||
_bottomTween.Stop();
|
||||
_topColourTween.Stop();
|
||||
_bottomColourTween.Stop();
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
RefreshState();
|
||||
}
|
||||
|
||||
public void SetSelectedImmediate(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
StopAllTweens();
|
||||
var (scale, colour) = ResolveTargetState();
|
||||
SetScaleImmediate(scale);
|
||||
SetColourImmediate(colour);
|
||||
}
|
||||
|
||||
public void SetHovered(bool hovered)
|
||||
{
|
||||
_isHovered = hovered;
|
||||
RefreshState();
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private void RefreshState()
|
||||
{
|
||||
var (scale, colour) = ResolveTargetState();
|
||||
TweenToScale(scale);
|
||||
TweenToColour(colour);
|
||||
}
|
||||
|
||||
private (float scale, Color colour) ResolveTargetState()
|
||||
{
|
||||
if (_isSelected) return (1f, _selectedColour);
|
||||
if (_isHovered) return (1f, _hoverColour);
|
||||
return (0f, Color.clear);
|
||||
}
|
||||
|
||||
private void TweenToScale(float target)
|
||||
{
|
||||
_topTween.Stop();
|
||||
_bottomTween.Stop();
|
||||
|
||||
var currentScale = _topLine != null
|
||||
? _topLine.localScale.x
|
||||
: 0f;
|
||||
|
||||
if (_topLine != null)
|
||||
{
|
||||
_topTween = Tween.ScaleX(
|
||||
_topLine,
|
||||
currentScale,
|
||||
target,
|
||||
_duration,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime);
|
||||
}
|
||||
|
||||
if (_bottomLine != null)
|
||||
{
|
||||
_bottomTween = Tween.ScaleX(
|
||||
_bottomLine,
|
||||
currentScale,
|
||||
target,
|
||||
_duration,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void TweenToColour(Color target)
|
||||
{
|
||||
_topColourTween.Stop();
|
||||
_bottomColourTween.Stop();
|
||||
|
||||
if (_topImage != null)
|
||||
{
|
||||
_topColourTween = Tween.Color(
|
||||
_topImage,
|
||||
_topImage.color,
|
||||
target,
|
||||
_duration,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime);
|
||||
}
|
||||
|
||||
if (_bottomImage != null)
|
||||
{
|
||||
_bottomColourTween = Tween.Color(
|
||||
_bottomImage,
|
||||
_bottomImage.color,
|
||||
target,
|
||||
_duration,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetScaleImmediate(float scaleX)
|
||||
{
|
||||
if (_topLine != null)
|
||||
_topLine.localScale = new Vector3(scaleX, 1f, 1f);
|
||||
if (_bottomLine != null)
|
||||
_bottomLine.localScale = new Vector3(scaleX, 1f, 1f);
|
||||
}
|
||||
|
||||
private void SetColourImmediate(Color colour)
|
||||
{
|
||||
if (_topImage != null) _topImage.color = colour;
|
||||
if (_bottomImage != null) _bottomImage.color = colour;
|
||||
}
|
||||
|
||||
private void StopAllTweens()
|
||||
{
|
||||
_topTween.Stop();
|
||||
_bottomTween.Stop();
|
||||
_topColourTween.Stop();
|
||||
_bottomColourTween.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0e824631a104902b958bf53ff6803d1
|
||||
timeCreated: 1778342396
|
||||
25
Assets/Scripts/UI/Menus/Components/SaveSlotPointerRelay.cs
Normal file
25
Assets/Scripts/UI/Menus/Components/SaveSlotPointerRelay.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
public class SaveSlotPointerRelay : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
private SaveSlotUI _slotUI;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_slotUI = GetComponentInParent<SaveSlotUI>();
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
_slotUI?.OnPointerEnter(eventData);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
_slotUI?.OnPointerExit(eventData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c351000954cf4e44a18e71725f29b2b7
|
||||
timeCreated: 1778343589
|
||||
@@ -1,19 +1,14 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
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
|
||||
public class SaveSlotUI : MonoBehaviour,
|
||||
IPointerEnterHandler,
|
||||
IPointerExitHandler
|
||||
{
|
||||
[Header("Clickable Root")]
|
||||
[SerializeField]
|
||||
@@ -30,17 +25,21 @@ namespace BriarQueen.UI.Menus.Components
|
||||
[SerializeField]
|
||||
private Button _deleteButton;
|
||||
|
||||
[Header("Empty Visual")]
|
||||
[Header("Empty Text")]
|
||||
[SerializeField]
|
||||
private Image _emptyImage;
|
||||
private TextMeshProUGUI _emptyImage;
|
||||
|
||||
[Header("Highlight")]
|
||||
[SerializeField]
|
||||
private SaveSlotHighlight _highlight;
|
||||
|
||||
private Action<SaveFileInfo> _onDeleteClick;
|
||||
private Action _onEmptyClick;
|
||||
|
||||
private Action _onEmptyClick;
|
||||
private Action<SaveFileInfo> _onFilledClick;
|
||||
|
||||
public SaveFileInfo SaveInfo { get; private set; }
|
||||
public bool IsEmpty { get; private set; }
|
||||
public SaveFileInfo SaveInfo { get; private set; }
|
||||
public bool IsEmpty { get; private set; }
|
||||
public bool IsSelected { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -59,28 +58,52 @@ namespace BriarQueen.UI.Menus.Components
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_slotButton != null) _slotButton.onClick.RemoveAllListeners();
|
||||
if (_slotButton != null) _slotButton.onClick.RemoveAllListeners();
|
||||
if (_deleteButton != null) _deleteButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
// ── Pointer events ────────────────────────────────────────────
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
_highlight?.SetHovered(true);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
_highlight?.SetHovered(false);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
public GameObject GetSelectableGameObject()
|
||||
{
|
||||
return _slotButton != null ? _slotButton.gameObject : gameObject;
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected, bool immediate = false)
|
||||
{
|
||||
IsSelected = selected;
|
||||
|
||||
if (immediate)
|
||||
_highlight?.SetSelectedImmediate(selected);
|
||||
else
|
||||
_highlight?.SetSelected(selected);
|
||||
}
|
||||
|
||||
public void SetFilled(
|
||||
SaveFileInfo saveInfo,
|
||||
Action<SaveFileInfo> onClickFilled,
|
||||
Action<SaveFileInfo> onDelete)
|
||||
{
|
||||
SaveInfo = saveInfo;
|
||||
IsEmpty = false;
|
||||
IsEmpty = false;
|
||||
|
||||
_onFilledClick = onClickFilled;
|
||||
_onEmptyClick = null;
|
||||
_onEmptyClick = null;
|
||||
_onDeleteClick = onDelete;
|
||||
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(false);
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(false);
|
||||
|
||||
if (_saveNameText != null)
|
||||
{
|
||||
@@ -107,16 +130,16 @@ namespace BriarQueen.UI.Menus.Components
|
||||
public void SetEmpty(Action onClickEmpty)
|
||||
{
|
||||
SaveInfo = default;
|
||||
IsEmpty = true;
|
||||
IsEmpty = true;
|
||||
|
||||
_onFilledClick = null;
|
||||
_onEmptyClick = onClickEmpty;
|
||||
_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 (_emptyImage != null) _emptyImage.gameObject.SetActive(true);
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
@@ -125,20 +148,20 @@ namespace BriarQueen.UI.Menus.Components
|
||||
}
|
||||
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = true; // empty slot is still clickable
|
||||
_slotButton.interactable = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private void HandleSlotClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
@@ -149,8 +172,7 @@ namespace BriarQueen.UI.Menus.Components
|
||||
|
||||
private void HandleDeleteClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
return;
|
||||
if (IsEmpty) return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SaveInfo.FileName))
|
||||
_onDeleteClick?.Invoke(SaveInfo);
|
||||
@@ -159,12 +181,12 @@ namespace BriarQueen.UI.Menus.Components
|
||||
|
||||
public struct SaveFileInfo
|
||||
{
|
||||
public readonly string FileName;
|
||||
public readonly string FileName;
|
||||
public readonly DateTime LastModified;
|
||||
|
||||
public SaveFileInfo(string fileName, DateTime lastModified)
|
||||
{
|
||||
FileName = fileName;
|
||||
FileName = fileName;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
158
Assets/Scripts/UI/Menus/Components/UnderlineButton.cs
Normal file
158
Assets/Scripts/UI/Menus/Components/UnderlineButton.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using System;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class UnderlineButton : MonoBehaviour,
|
||||
IPointerEnterHandler,
|
||||
IPointerExitHandler,
|
||||
IPointerClickHandler,
|
||||
ISelectHandler,
|
||||
IDeselectHandler
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private TextMeshProUGUI _label;
|
||||
|
||||
private Button _button;
|
||||
private UnderlineButtonGroup _group;
|
||||
private bool _isHovered;
|
||||
|
||||
public event Action<UnderlineButton> SelectionRequested;
|
||||
public event Action<UnderlineButton> HoverEntered;
|
||||
public event Action<UnderlineButton> HoverExited;
|
||||
|
||||
public bool IsSelected { get; private set; }
|
||||
|
||||
// True when this button is registered with a group
|
||||
public bool IsGrouped => _group != null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_button = GetComponent<Button>();
|
||||
SetUnderline(false);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_isHovered = false;
|
||||
SetUnderline(false);
|
||||
}
|
||||
|
||||
// ── Group registration ────────────────────────────────────────
|
||||
|
||||
/// <summary>Called by UnderlineButtonGroup when this button is bound.</summary>
|
||||
internal void RegisterGroup(UnderlineButtonGroup group)
|
||||
{
|
||||
_group = group;
|
||||
}
|
||||
|
||||
/// <summary>Called by UnderlineButtonGroup when this button is unbound.</summary>
|
||||
internal void UnregisterGroup()
|
||||
{
|
||||
_group = null;
|
||||
}
|
||||
|
||||
// ── Pointer / Selection events ────────────────────────────────
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (_button != null && !_button.interactable) return;
|
||||
|
||||
_isHovered = true;
|
||||
|
||||
if (IsGrouped)
|
||||
HoverEntered?.Invoke(this);
|
||||
else
|
||||
RefreshStandaloneState();
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
_isHovered = false;
|
||||
|
||||
if (IsGrouped)
|
||||
HoverExited?.Invoke(this);
|
||||
else
|
||||
RefreshStandaloneState();
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (_button != null && !_button.interactable) return;
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
if (_button != null && !_button.interactable) return;
|
||||
|
||||
_isHovered = true;
|
||||
|
||||
if (IsGrouped)
|
||||
{
|
||||
HoverEntered?.Invoke(this);
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsSelected = true;
|
||||
RefreshStandaloneState();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData)
|
||||
{
|
||||
_isHovered = false;
|
||||
|
||||
if (IsGrouped)
|
||||
HoverExited?.Invoke(this);
|
||||
else
|
||||
{
|
||||
IsSelected = false;
|
||||
RefreshStandaloneState();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Called by the group to update logical selected state.</summary>
|
||||
public void SetSelectedState(bool selected)
|
||||
{
|
||||
IsSelected = selected;
|
||||
}
|
||||
|
||||
/// <summary>Called by the group to drive the underline visual directly.</summary>
|
||||
public void SetUnderline(bool underlined)
|
||||
{
|
||||
if (_label == null) return;
|
||||
|
||||
if (underlined)
|
||||
_label.fontStyle |= FontStyles.Underline;
|
||||
else
|
||||
_label.fontStyle &= ~FontStyles.Underline;
|
||||
}
|
||||
|
||||
public GameObject GetSelectableGameObject() => gameObject;
|
||||
|
||||
// ── Standalone logic ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Used when not in a group — the button manages its own
|
||||
/// underline based on hover and selected state directly.
|
||||
/// </summary>
|
||||
private void RefreshStandaloneState()
|
||||
{
|
||||
if (_button != null && !_button.interactable)
|
||||
{
|
||||
SetUnderline(false);
|
||||
return;
|
||||
}
|
||||
|
||||
SetUnderline(IsSelected || _isHovered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 107dd116a1c64bd8afb3a5e93a535c7b
|
||||
timeCreated: 1778344990
|
||||
175
Assets/Scripts/UI/Menus/Components/UnderlineButtonGroup.cs
Normal file
175
Assets/Scripts/UI/Menus/Components/UnderlineButtonGroup.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
public class UnderlineButtonGroup : MonoBehaviour
|
||||
{
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private UnderlineButton[] _initialButtons = Array.Empty<UnderlineButton>();
|
||||
|
||||
[SerializeField] private int _defaultSelectedIndex;
|
||||
[SerializeField] private bool _selectDefaultOnEnable = true;
|
||||
[SerializeField] private bool _syncEventSystemSelection = true;
|
||||
|
||||
// Runtime list — combines inspector-assigned and dynamically added buttons
|
||||
private readonly List<UnderlineButton> _buttons = new();
|
||||
|
||||
public UnderlineButton Current { get; private set; }
|
||||
public UnderlineButton CurrentHovered { get; private set; }
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_initialButtons = GetComponentsInChildren<UnderlineButton>(true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Register inspector-assigned buttons first
|
||||
foreach (var button in _initialButtons)
|
||||
AddButton(button);
|
||||
|
||||
if (_selectDefaultOnEnable && _buttons.Count > 0)
|
||||
SelectIndex(_defaultSelectedIndex, instant: true);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Remove all — clears both initial and runtime-added buttons
|
||||
for (var i = _buttons.Count - 1; i >= 0; i--)
|
||||
RemoveButton(_buttons[i]);
|
||||
|
||||
CurrentHovered = null;
|
||||
Current = null;
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Registers a button with the group at runtime.
|
||||
/// Safe to call multiple times — ignores already-registered buttons.
|
||||
/// </summary>
|
||||
public void AddButton(UnderlineButton button)
|
||||
{
|
||||
if (button == null || _buttons.Contains(button)) return;
|
||||
|
||||
_buttons.Add(button);
|
||||
button.RegisterGroup(this);
|
||||
button.SelectionRequested += HandleSelectionRequested;
|
||||
button.HoverEntered += HandleHoverEntered;
|
||||
button.HoverExited += HandleHoverExited;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a button from the group.
|
||||
/// Safe to call on buttons not in the group.
|
||||
/// </summary>
|
||||
public void RemoveButton(UnderlineButton button)
|
||||
{
|
||||
if (button == null || !_buttons.Contains(button)) return;
|
||||
|
||||
// If this was the selected or hovered button, clear those references
|
||||
if (Current == button) Current = null;
|
||||
if (CurrentHovered == button) CurrentHovered = null;
|
||||
|
||||
button.UnregisterGroup();
|
||||
button.SelectionRequested -= HandleSelectionRequested;
|
||||
button.HoverEntered -= HandleHoverEntered;
|
||||
button.HoverExited -= HandleHoverExited;
|
||||
button.SetUnderline(false);
|
||||
button.SetSelectedState(false);
|
||||
|
||||
_buttons.Remove(button);
|
||||
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
/// <summary>Removes all dynamically added buttons, leaving inspector-assigned ones.</summary>
|
||||
public void RemoveAllDynamicButtons()
|
||||
{
|
||||
for (var i = _buttons.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var button = _buttons[i];
|
||||
if (button == null) continue;
|
||||
|
||||
// Skip buttons that were assigned in the inspector
|
||||
var isInitial = System.Array.IndexOf(_initialButtons, button) >= 0;
|
||||
if (isInitial) continue;
|
||||
|
||||
RemoveButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectIndex(int index, bool instant = false)
|
||||
{
|
||||
if (_buttons == null || _buttons.Count == 0) return;
|
||||
SelectButton(_buttons[Mathf.Clamp(index, 0, _buttons.Count - 1)], instant);
|
||||
}
|
||||
|
||||
public void SelectButton(UnderlineButton button, bool instant = false)
|
||||
{
|
||||
if (button == null) return;
|
||||
if (Current == button && !instant) return;
|
||||
|
||||
Current = button;
|
||||
RefreshAllButtonStates();
|
||||
SyncEventSystemSelection(button);
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
Current = null;
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private void RefreshAllButtonStates()
|
||||
{
|
||||
foreach (var button in _buttons)
|
||||
{
|
||||
if (button == null) continue;
|
||||
|
||||
var isSelected = button == Current;
|
||||
var isHovered = button == CurrentHovered;
|
||||
|
||||
var showUnderline = isSelected
|
||||
? CurrentHovered == null || isHovered
|
||||
: isHovered;
|
||||
|
||||
button.SetSelectedState(isSelected);
|
||||
button.SetUnderline(showUnderline);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSelectionRequested(UnderlineButton button)
|
||||
{
|
||||
SelectButton(button);
|
||||
}
|
||||
|
||||
private void HandleHoverEntered(UnderlineButton button)
|
||||
{
|
||||
CurrentHovered = button;
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
private void HandleHoverExited(UnderlineButton button)
|
||||
{
|
||||
if (CurrentHovered == button)
|
||||
CurrentHovered = null;
|
||||
|
||||
RefreshAllButtonStates();
|
||||
}
|
||||
|
||||
private void SyncEventSystemSelection(UnderlineButton button)
|
||||
{
|
||||
if (!_syncEventSystemSelection || EventSystem.current == null) return;
|
||||
|
||||
var go = button.GetSelectableGameObject();
|
||||
if (go == null || EventSystem.current.currentSelectedGameObject == go) return;
|
||||
EventSystem.current.SetSelectedGameObject(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d80f3295d5743b08ad4e7de1cfc5555
|
||||
timeCreated: 1778519999
|
||||
315
Assets/Scripts/UI/Menus/ConfirmActionWindow.cs
Normal file
315
Assets/Scripts/UI/Menus/ConfirmActionWindow.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
public class ConfirmActionWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField]
|
||||
private GameObject _root;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _panelTransform;
|
||||
|
||||
[Header("Text")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _titleText;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _messageText;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _confirmLabel;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _cancelLabel;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _confirmButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _cancelButton;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.18f,
|
||||
ease = Ease.OutBack,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private float _hiddenScale = 0.92f;
|
||||
|
||||
private Action _onCancel;
|
||||
private Action _onConfirm;
|
||||
private CancellationTokenSource _cts;
|
||||
private Sequence _sequence;
|
||||
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
StopAndDispose();
|
||||
}
|
||||
|
||||
public void Open(
|
||||
string title,
|
||||
string message,
|
||||
string confirmText,
|
||||
string cancelText,
|
||||
Action onConfirm,
|
||||
Action onCancel)
|
||||
{
|
||||
_onConfirm = onConfirm;
|
||||
_onCancel = onCancel;
|
||||
|
||||
if (_titleText != null)
|
||||
{
|
||||
_titleText.text = title;
|
||||
}
|
||||
|
||||
if (_messageText != null)
|
||||
{
|
||||
_messageText.text = message;
|
||||
}
|
||||
|
||||
if (_confirmLabel != null)
|
||||
{
|
||||
_confirmLabel.text = confirmText;
|
||||
}
|
||||
|
||||
if (_cancelLabel != null)
|
||||
{
|
||||
_cancelLabel.text = cancelText;
|
||||
}
|
||||
|
||||
OpenInternal().Forget();
|
||||
}
|
||||
|
||||
public void CancelFromBack()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
StopAndDispose();
|
||||
|
||||
IsOpen = false;
|
||||
_onConfirm = null;
|
||||
_onCancel = null;
|
||||
|
||||
if (_root != null)
|
||||
{
|
||||
_root.SetActive(false);
|
||||
}
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
_panelTransform.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid OpenInternal()
|
||||
{
|
||||
ResolveReferences();
|
||||
StopAndDispose();
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
if (_root != null)
|
||||
{
|
||||
_root.SetActive(true);
|
||||
}
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
_panelTransform.localScale = Vector3.one * _hiddenScale;
|
||||
}
|
||||
|
||||
IsOpen = true;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_sequence.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
_sequence.Group(Tween.Scale(_panelTransform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = Vector3.one * _hiddenScale,
|
||||
endValue = Vector3.one,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.interactable = true;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
SelectCancelButton();
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var onConfirm = _onConfirm;
|
||||
CloseImmediate();
|
||||
onConfirm?.Invoke();
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var onCancel = _onCancel;
|
||||
CloseImmediate();
|
||||
onCancel?.Invoke();
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_root == null)
|
||||
{
|
||||
_root = gameObject;
|
||||
}
|
||||
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
_canvasGroup = _root.GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
if (_panelTransform == null)
|
||||
{
|
||||
_panelTransform = transform as RectTransform;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectCancelButton()
|
||||
{
|
||||
if (_cancelButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EventSystem.current != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(_cancelButton.gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_cancelButton.Select();
|
||||
}
|
||||
|
||||
private void StopAndDispose()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
{
|
||||
_sequence.Stop();
|
||||
}
|
||||
|
||||
_sequence = default;
|
||||
|
||||
if (_cts == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/ConfirmActionWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/ConfirmActionWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fbd9bbf0c284c69b03e68a1b82cc74e
|
||||
timeCreated: 1778300000
|
||||
@@ -70,6 +70,8 @@ namespace BriarQueen.UI.Menus
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
public bool IsOpen => _isOpen;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_confirmButton != null) _confirmButton.onClick.AddListener(Confirm);
|
||||
@@ -104,6 +106,11 @@ namespace BriarQueen.UI.Menus
|
||||
CloseAsync().Forget();
|
||||
}
|
||||
|
||||
public void CancelFromBack()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
StopAnim();
|
||||
@@ -338,4 +345,4 @@ namespace BriarQueen.UI.Menus
|
||||
_root = gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Effects;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using BriarQueen.Game.Effects;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Serialization;
|
||||
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;
|
||||
[FormerlySerializedAs("_mainMenuIntroScreenCanvasGroup")]
|
||||
private CanvasGroup _introScreenCanvas;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTextCanvasGroup;
|
||||
[FormerlySerializedAs("_introTextText")]
|
||||
private TextMeshProUGUI _pressStartText;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _introTextText;
|
||||
private UIDissolveImage _introGroupDissolveGroup;
|
||||
|
||||
[Header("Main Menu")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTitleCanvasGroup;
|
||||
[FormerlySerializedAs("_mainMenuWindowCanvasGroup")]
|
||||
private CanvasGroup _mainMenuGroup;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _buttonsGroup;
|
||||
|
||||
[SerializeField]
|
||||
private AnimatedSelectionButtonGroup _mainMenuSelectionGroup;
|
||||
|
||||
[SerializeField]
|
||||
private AnimatedSelectionButton _startGameSelectionButton;
|
||||
|
||||
[SerializeField]
|
||||
private AnimatedSelectionButton _settingsSelectionButton;
|
||||
|
||||
[SerializeField]
|
||||
private AnimatedSelectionButton _quitSelectionButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _startGameButton;
|
||||
|
||||
@@ -61,62 +67,62 @@ namespace BriarQueen.UI.Menus
|
||||
[SerializeField]
|
||||
private SelectSaveWindow _selectSaveWindow;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _selectSaveContainerCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _selectSaveWindowCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private UIFogReveal _selectSaveWindowFog;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _selectSaveTweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Intro Timing")]
|
||||
[SerializeField]
|
||||
private float _introLightPulseDuration = 2f;
|
||||
private TweenSettings _pressStartFadeTweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextDelaySeconds = 1.5f;
|
||||
private TweenSettings _pressStartPulseTweenSettings = new()
|
||||
{
|
||||
duration = 0.85f,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[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;
|
||||
[Range(0f, 1f)]
|
||||
private float _pressStartPulseMinimumAlpha = 0.25f;
|
||||
|
||||
private CancellationTokenSource _introCts;
|
||||
private CancellationTokenSource _selectSaveCts;
|
||||
|
||||
private Sequence _introLightPulseSequence;
|
||||
private Sequence _introTextPulseSequence;
|
||||
private Sequence _introTransitionSequence;
|
||||
private Sequence _selectSaveSequence;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private GameService _gameService;
|
||||
private InputManager _inputManager;
|
||||
private Sequence _pressStartFadeSequence;
|
||||
private Sequence _pressStartPulseSequence;
|
||||
private Sequence _selectSaveSequence;
|
||||
|
||||
private bool _introFinished;
|
||||
private bool _introTransitioning;
|
||||
private DeviceInputType _lastDeviceInputType;
|
||||
|
||||
[Inject]
|
||||
public void Construct(GameService gameService, EventCoordinator eventCoordinator, InputManager inputManager)
|
||||
{
|
||||
_gameService = gameService;
|
||||
_gameService = gameService;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
@@ -128,8 +134,6 @@ namespace BriarQueen.UI.Menus
|
||||
_selectSaveWindow.OnCloseWindow += CloseSelectSaveWindow;
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
UpdateSubmitText(force: true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -138,8 +142,23 @@ namespace BriarQueen.UI.Menus
|
||||
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(false));
|
||||
_inputManager?.BindSubmitForStart(OnIntroSubmit);
|
||||
_eventCoordinator?.Subscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
|
||||
StartIntroScreen().Forget();
|
||||
ApplyInitialVisualState();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mouse.current?.leftButton.wasPressedThisFrame == true ||
|
||||
Gamepad.current?.buttonSouth.wasPressedThisFrame == true)
|
||||
{
|
||||
TransitionFromIntroToMainMenu().Forget();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
@@ -147,6 +166,7 @@ namespace BriarQueen.UI.Menus
|
||||
UnbindButtons();
|
||||
|
||||
_inputManager?.ResetSubmitBind(OnIntroSubmit);
|
||||
_eventCoordinator?.Unsubscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
|
||||
StopIntroTweens();
|
||||
StopSelectSaveTween();
|
||||
@@ -155,300 +175,137 @@ namespace BriarQueen.UI.Menus
|
||||
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);
|
||||
var startGameButton = ResolveButton(_startGameSelectionButton, _startGameButton);
|
||||
if (startGameButton != null)
|
||||
{
|
||||
startGameButton.onClick.AddListener(OnStartClicked);
|
||||
}
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.AddListener(OnSettingsClicked);
|
||||
var settingsButton = ResolveButton(_settingsSelectionButton, _settingsButton);
|
||||
if (settingsButton != null)
|
||||
{
|
||||
settingsButton.onClick.AddListener(OnSettingsClicked);
|
||||
}
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.AddListener(OnQuitClicked);
|
||||
var quitButton = ResolveButton(_quitSelectionButton, _quitButton);
|
||||
if (quitButton != null)
|
||||
{
|
||||
quitButton.onClick.AddListener(OnQuitClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnbindButtons()
|
||||
{
|
||||
if (_startGameButton != null)
|
||||
_startGameButton.onClick.RemoveListener(OnStartClicked);
|
||||
var startGameButton = ResolveButton(_startGameSelectionButton, _startGameButton);
|
||||
if (startGameButton != null)
|
||||
{
|
||||
startGameButton.onClick.RemoveListener(OnStartClicked);
|
||||
}
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.RemoveListener(OnSettingsClicked);
|
||||
var settingsButton = ResolveButton(_settingsSelectionButton, _settingsButton);
|
||||
if (settingsButton != null)
|
||||
{
|
||||
settingsButton.onClick.RemoveListener(OnSettingsClicked);
|
||||
}
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.RemoveListener(OnQuitClicked);
|
||||
var quitButton = ResolveButton(_quitSelectionButton, _quitButton);
|
||||
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;
|
||||
_introFinished = false;
|
||||
_introTransitioning = false;
|
||||
|
||||
ResetIntroCtsAndCancelRunning();
|
||||
ApplyInitialVisualState();
|
||||
UpdateSubmitText(force: true);
|
||||
|
||||
try
|
||||
SetCanvasGroupState(_introScreenCanvas, 1f, true);
|
||||
if (_introScreenCanvas != null)
|
||||
{
|
||||
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
|
||||
}
|
||||
}));
|
||||
_introScreenCanvas.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
if (_introTextCanvasGroup != null)
|
||||
SetCanvasGroupState(_mainMenuGroup, 1f, false);
|
||||
if (_mainMenuGroup != null)
|
||||
{
|
||||
fadeInSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextFadeInDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
_mainMenuGroup.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
await fadeInSequence.ToUniTask(cancellationToken: token);
|
||||
SetCanvasGroupState(_selectSaveContainerCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
|
||||
if (_introTextCanvasGroup == null)
|
||||
return;
|
||||
if (_pressStartText != null)
|
||||
{
|
||||
var color = _pressStartText.color;
|
||||
color.a = 1f;
|
||||
_pressStartText.color = color;
|
||||
_pressStartText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
_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);
|
||||
StartPressStartPulse();
|
||||
_introGroupDissolveGroup?.SetDissolveAmount(0f);
|
||||
_selectSaveWindowFog?.FogSet(0f);
|
||||
}
|
||||
|
||||
private void OnIntroSubmit(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.performed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionFromIntroToMainMenu().Forget();
|
||||
}
|
||||
|
||||
private void OnBackRequested(UIBackRequestedEvent _)
|
||||
{
|
||||
if (!_introFinished || _selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_selectSaveWindow.gameObject.activeInHierarchy || !_selectSaveWindowCanvasGroup.interactable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectSaveWindow.HandleBackRequest();
|
||||
}
|
||||
|
||||
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);
|
||||
var fadeTextTask = FadeOutPressStartText(_introCts.Token);
|
||||
var dissolveTask = _introGroupDissolveGroup != null
|
||||
? _introGroupDissolveGroup.DissolveOut(false).AttachExternalCancellation(_introCts.Token)
|
||||
: UniTask.CompletedTask;
|
||||
|
||||
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);
|
||||
}
|
||||
await UniTask.WhenAll(fadeTextTask, dissolveTask);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -457,28 +314,73 @@ namespace BriarQueen.UI.Menus
|
||||
}
|
||||
finally
|
||||
{
|
||||
_introTransitionSequence = default;
|
||||
_pressStartFadeSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
SetCanvasGroupState(_introScreenCanvas, 0f, false);
|
||||
if (_introScreenCanvas != null)
|
||||
{
|
||||
var color = _introScreenLightImage.color;
|
||||
color.a = 1f;
|
||||
_introScreenLightImage.color = color;
|
||||
_introScreenCanvas.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
_introFinished = true;
|
||||
if (_mainMenuGroup != null)
|
||||
{
|
||||
_mainMenuGroup.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_mainMenuGroup, 1f, true);
|
||||
|
||||
_introFinished = true;
|
||||
_introTransitioning = false;
|
||||
_mainMenuSelectionGroup?.SelectIndex(0, true);
|
||||
}
|
||||
|
||||
private async UniTask FadeOutPressStartText(CancellationToken token)
|
||||
{
|
||||
if (_pressStartText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pressStartFadeSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_pressStartText, new TweenSettings<float>
|
||||
{
|
||||
startValue = _pressStartText.color.a,
|
||||
endValue = 0f,
|
||||
settings = _pressStartFadeTweenSettings
|
||||
}));
|
||||
|
||||
await _pressStartFadeSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
var color = _pressStartText.color;
|
||||
color.a = 0f;
|
||||
_pressStartText.color = color;
|
||||
_pressStartText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void StartPressStartPulse()
|
||||
{
|
||||
StopSequence(ref _pressStartPulseSequence);
|
||||
|
||||
if (_pressStartText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pressStartPulseSequence = Sequence.Create(
|
||||
useUnscaledTime: true,
|
||||
cycleMode: Sequence.SequenceCycleMode.Yoyo,
|
||||
cycles: -1)
|
||||
.Group(Tween.Alpha(_pressStartText, new TweenSettings<float>
|
||||
{
|
||||
startValue = _pressStartText.color.a,
|
||||
endValue = _pressStartPulseMinimumAlpha,
|
||||
settings = _pressStartPulseTweenSettings
|
||||
}));
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] Starting game");
|
||||
ShowSelectSaveWindow().Forget();
|
||||
}
|
||||
|
||||
@@ -494,40 +396,49 @@ namespace BriarQueen.UI.Menus
|
||||
|
||||
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);
|
||||
SetCanvasGroupState(_selectSaveContainerCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 1f, false); // visible but not interactable yet
|
||||
SetCanvasGroupInteractivity(_mainMenuGroup, 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
|
||||
}));
|
||||
if (_selectSaveWindowCanvasGroup != null)
|
||||
{
|
||||
_selectSaveWindowFog.FogAmount = 0.2f;
|
||||
_selectSaveWindowFog.MaxFog = 0.4f;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: fog rolls in fully
|
||||
if (_selectSaveWindowFog != null)
|
||||
{
|
||||
await _selectSaveWindowFog.FogIn().AttachExternalCancellation(_selectSaveCts.Token);
|
||||
}
|
||||
|
||||
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveContainerCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Alpha(_buttonsGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _buttonsGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -539,7 +450,9 @@ namespace BriarQueen.UI.Menus
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 1f, true);
|
||||
// Step 3: tween complete — enable interaction
|
||||
SetCanvasGroupInteractivity(_selectSaveContainerCanvasGroup, true);
|
||||
SetCanvasGroupInteractivity(_selectSaveWindowCanvasGroup, true);
|
||||
}
|
||||
|
||||
private void CloseSelectSaveWindow()
|
||||
@@ -550,28 +463,40 @@ namespace BriarQueen.UI.Menus
|
||||
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
|
||||
}));
|
||||
// Disable interaction immediately, leave alpha for tween to read
|
||||
SetCanvasGroupInteractivity(_selectSaveContainerCanvasGroup, false);
|
||||
SetCanvasGroupInteractivity(_selectSaveWindowCanvasGroup, false);
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: container fades out
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveContainerCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _selectSaveContainerCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Alpha(_buttonsGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _buttonsGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
|
||||
// Step 2: fog clears
|
||||
if (_selectSaveWindowFog != null)
|
||||
{
|
||||
await _selectSaveWindowFog.FogOut().AttachExternalCancellation(_selectSaveCts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -582,27 +507,27 @@ namespace BriarQueen.UI.Menus
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
_selectSaveWindowCanvasGroup.alpha = 0f;
|
||||
// Step 3: fully cleaned up
|
||||
SetCanvasGroupState(_selectSaveContainerCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
_selectSaveWindowFog?.FogSet(0f);
|
||||
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
SetCanvasGroupState(_mainMenuGroup, 1f, true);
|
||||
}
|
||||
|
||||
private void ResetIntroCtsAndCancelRunning()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
|
||||
StopSequence(ref _pressStartPulseSequence);
|
||||
StopSequence(ref _pressStartFadeSequence);
|
||||
CancelAndDispose(ref _introCts);
|
||||
_introCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopIntroTweens()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
StopSequence(ref _pressStartPulseSequence);
|
||||
StopSequence(ref _pressStartFadeSequence);
|
||||
CancelAndDispose(ref _introCts);
|
||||
}
|
||||
|
||||
@@ -622,7 +547,9 @@ namespace BriarQueen.UI.Menus
|
||||
private static void StopSequence(ref Sequence sequence)
|
||||
{
|
||||
if (sequence.isAlive)
|
||||
{
|
||||
sequence.Stop();
|
||||
}
|
||||
|
||||
sequence = default;
|
||||
}
|
||||
@@ -630,7 +557,9 @@ namespace BriarQueen.UI.Menus
|
||||
private static void CancelAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -644,13 +573,28 @@ namespace BriarQueen.UI.Menus
|
||||
cts = null;
|
||||
}
|
||||
|
||||
private static Button ResolveButton(AnimatedSelectionButton animatedSelectionButton, Button fallbackButton)
|
||||
{
|
||||
if (animatedSelectionButton != null && animatedSelectionButton.Button != null)
|
||||
{
|
||||
return animatedSelectionButton.Button;
|
||||
}
|
||||
|
||||
return fallbackButton;
|
||||
}
|
||||
|
||||
private static void SetCanvasGroupState(CanvasGroup group, float alpha, bool inputEnabled)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
if (group == null) return;
|
||||
group.alpha = alpha;
|
||||
group.interactable = inputEnabled;
|
||||
group.blocksRaycasts = inputEnabled;
|
||||
}
|
||||
|
||||
group.alpha = alpha;
|
||||
group.interactable = inputEnabled;
|
||||
private static void SetCanvasGroupInteractivity(CanvasGroup group, bool inputEnabled)
|
||||
{
|
||||
if (group == null) return;
|
||||
group.interactable = inputEnabled;
|
||||
group.blocksRaycasts = inputEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ namespace BriarQueen.UI.Menus
|
||||
public event Action OnCloseWindow;
|
||||
public event Action<string> OnSaveCreated;
|
||||
|
||||
public bool IsOpen => _isOpen;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager)
|
||||
{
|
||||
@@ -422,4 +424,4 @@ namespace BriarQueen.UI.Menus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using BriarQueen.Framework.Services.Game;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
@@ -41,6 +42,10 @@ namespace BriarQueen.UI.Menus
|
||||
[SerializeField]
|
||||
private Button _quitToDesktopButton;
|
||||
|
||||
[Header("Selection")]
|
||||
[SerializeField]
|
||||
private Selectable _firstSelectedOnOpen;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
@@ -96,7 +101,7 @@ namespace BriarQueen.UI.Menus
|
||||
if (_quitToDesktopButton != null) _quitToDesktopButton.onClick.AddListener(OnQuitToDesktopButtonClick);
|
||||
|
||||
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
@@ -169,6 +174,8 @@ namespace BriarQueen.UI.Menus
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
|
||||
SelectDefault();
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
@@ -260,6 +267,23 @@ namespace BriarQueen.UI.Menus
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
private void SelectDefault()
|
||||
{
|
||||
var selectable = _firstSelectedOnOpen != null ? _firstSelectedOnOpen : _resumeButton;
|
||||
if (selectable == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EventSystem.current != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(selectable.gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
selectable.Select();
|
||||
}
|
||||
|
||||
private void OnSaveButtonClick()
|
||||
{
|
||||
SaveGame().Forget();
|
||||
@@ -305,4 +329,4 @@ namespace BriarQueen.UI.Menus
|
||||
// TODO: Saved feedback popup/toast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,355 +1,373 @@
|
||||
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;
|
||||
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
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
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()
|
||||
/// <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
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
|
||||
private const int MAX_SLOTS = 3;
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
[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()
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow += HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated += HandleSaveCreatedAndStartGame;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
private void OnDestroy()
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete += HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel += HandleCancelDelete;
|
||||
_confirmDeleteWindow.Close();
|
||||
}
|
||||
}
|
||||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow -= HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated -= HandleSaveCreatedAndStartGame;
|
||||
}
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow -= HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated -= HandleSaveCreatedAndStartGame;
|
||||
if (_confirmDeleteWindow != null)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete -= HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel -= HandleCancelDelete;
|
||||
}
|
||||
|
||||
ClearSlots();
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
public event Action OnCloseWindow;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager, GameService gameService)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete -= HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel -= HandleCancelDelete;
|
||||
_saveManager = saveManager;
|
||||
_gameService = gameService;
|
||||
}
|
||||
|
||||
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++)
|
||||
/// <summary>Called by MainMenuWindow after enabling this GO.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
var slot = Instantiate(_saveSlotPrefab, _listContentParent);
|
||||
_instantiatedSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
if (_newSaveWindow != null)
|
||||
_newSaveWindow.CloseImmediate();
|
||||
|
||||
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);
|
||||
if (_confirmDeleteWindow != null)
|
||||
_confirmDeleteWindow.Close();
|
||||
|
||||
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))
|
||||
{
|
||||
EnsureThreeSlotsExist();
|
||||
RefreshSlotsData();
|
||||
return;
|
||||
SetBusy(false);
|
||||
}
|
||||
|
||||
LoadAndStartGame(saveInfo.FileName).Forget();
|
||||
}
|
||||
|
||||
private void OnEmptySlotClicked()
|
||||
{
|
||||
Debug.Log("[SelectSaveWindow] Empty slot clicked.");
|
||||
if (_isBusy)
|
||||
return;
|
||||
|
||||
if (_newSaveWindow == null)
|
||||
public bool HandleBackRequest()
|
||||
{
|
||||
Debug.LogWarning("[SelectSaveWindow] NewSaveWindow reference not set.");
|
||||
return;
|
||||
if (_newSaveWindow != null && _newSaveWindow.IsOpen)
|
||||
{
|
||||
_newSaveWindow.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null && _confirmDeleteWindow.IsOpen)
|
||||
{
|
||||
_confirmDeleteWindow.CancelFromBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
OnBackClicked();
|
||||
return true;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_newSaveWindow.Open();
|
||||
}
|
||||
|
||||
private async UniTask LoadAndStartGame(string profileName)
|
||||
{
|
||||
SetBusy(true);
|
||||
|
||||
try
|
||||
private void OnBackClicked()
|
||||
{
|
||||
await _saveManager.LoadGameData(profileName);
|
||||
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();
|
||||
}
|
||||
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)
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
return;
|
||||
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}");
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_confirmDeleteWindow.Open(saveInfo);
|
||||
}
|
||||
|
||||
private void HandleConfirmDelete(SaveFileInfo saveInfo)
|
||||
{
|
||||
try
|
||||
private void EnsureThreeSlotsExist()
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
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);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
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 HandleCancelDelete()
|
||||
{
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
}
|
||||
|
||||
private void TryDeleteAndRefresh(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return;
|
||||
|
||||
if (!_saveManager.DoesSaveExist(fileName))
|
||||
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();
|
||||
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)
|
||||
private void RestoreSelection()
|
||||
{
|
||||
SelectBackButton();
|
||||
return;
|
||||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||||
SelectSlot(_currentSelectionIndex);
|
||||
}
|
||||
|
||||
index = Mathf.Clamp(index, 0, _instantiatedSlots.Count - 1);
|
||||
_currentSelectionIndex = index;
|
||||
private void SelectBackButton()
|
||||
{
|
||||
if (_backButton == null) return;
|
||||
|
||||
var go = _instantiatedSlots[index]?.GetSelectableGameObject();
|
||||
if (go == null) return;
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(_backButton.gameObject);
|
||||
else
|
||||
_backButton.Select();
|
||||
}
|
||||
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(go);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,9 @@ namespace BriarQueen.UI.Menus
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _handleRect;
|
||||
|
||||
[SerializeField]
|
||||
private Image _scrollBarImage;
|
||||
|
||||
[Header("Scroll Settings")]
|
||||
[SerializeField]
|
||||
@@ -330,7 +333,10 @@ namespace BriarQueen.UI.Menus
|
||||
return;
|
||||
|
||||
if (_hideTrackWhenNotScrollable)
|
||||
{
|
||||
_trackRect.gameObject.SetActive(_isScrollable);
|
||||
_scrollBarImage.enabled = _isScrollable;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetContentBounds(out float top, out float bottom)
|
||||
|
||||
Reference in New Issue
Block a user