First commit for private source control. Older commits available on Github.
This commit is contained in:
24
Assets/Scripts/UI/BriarQueen.UI.asmdef
Normal file
24
Assets/Scripts/UI/BriarQueen.UI.asmdef
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "BriarQueen.UI",
|
||||
"rootNamespace": "BriarQueen",
|
||||
"references": [
|
||||
"GUID:ac1be664c635c449eb9f3f52cf5c97f5",
|
||||
"GUID:bdf0eff65032c4178bf18aa9c96b1c70",
|
||||
"GUID:80ecb87cae9c44d19824e70ea7229748",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f",
|
||||
"GUID:9e24947de15b9834991c9d8411ea37cf",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:75469ad4d38634e559750d17036d5f7c",
|
||||
"GUID:d525ad6bd40672747bde77962f1c401e"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Scripts/UI/BriarQueen.UI.asmdef.meta
Normal file
7
Assets/Scripts/UI/BriarQueen.UI.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbf8e360025c642d9ab65c9f6e98e5c5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/UI/Codex.meta
Normal file
3
Assets/Scripts/UI/Codex.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28c4e41fa6af42919d5479a5faeb8192
|
||||
timeCreated: 1773683598
|
||||
50
Assets/Scripts/UI/Codex/CodexCategoryButton.cs
Normal file
50
Assets/Scripts/UI/Codex/CodexCategoryButton.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Codex
|
||||
{
|
||||
public class CodexCategoryButton : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private Button _button;
|
||||
|
||||
[SerializeField]
|
||||
private Image _selectedBackground;
|
||||
|
||||
public CodexType Category { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_button != null)
|
||||
_button.onClick.AddListener(HandleClicked);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_button != null)
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
}
|
||||
|
||||
public event Action<CodexType> OnCategoryClicked;
|
||||
|
||||
public void Initialize(CodexType category)
|
||||
{
|
||||
Category = category;
|
||||
|
||||
SetSelected(false);
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (_selectedBackground != null)
|
||||
_selectedBackground.enabled = selected;
|
||||
}
|
||||
|
||||
private void HandleClicked()
|
||||
{
|
||||
OnCategoryClicked?.Invoke(Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Codex/CodexCategoryButton.cs.meta
Normal file
3
Assets/Scripts/UI/Codex/CodexCategoryButton.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cfb06e8e46d402e86eb8dc6e9f6c96e
|
||||
timeCreated: 1773684333
|
||||
263
Assets/Scripts/UI/Codex/CodexEntryButton.cs
Normal file
263
Assets/Scripts/UI/Codex/CodexEntryButton.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
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 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; }
|
||||
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void HandleClicked()
|
||||
{
|
||||
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;
|
||||
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
_button.onClick.AddListener(HandleClicked);
|
||||
}
|
||||
|
||||
private void RemoveButtonListener()
|
||||
{
|
||||
if (_button == null)
|
||||
return;
|
||||
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Codex/CodexEntryButton.cs.meta
Normal file
3
Assets/Scripts/UI/Codex/CodexEntryButton.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcb40c65f84248078513824345046145
|
||||
timeCreated: 1773684301
|
||||
179
Assets/Scripts/UI/Codex/CodexLocationButton.cs
Normal file
179
Assets/Scripts/UI/Codex/CodexLocationButton.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Codex
|
||||
{
|
||||
[ExecuteAlways]
|
||||
public class CodexLocationButton : 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 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 string _lastText;
|
||||
private float _lastWidth = -1f;
|
||||
|
||||
public Location Location { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
AddButtonListener();
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (_label == null)
|
||||
return;
|
||||
|
||||
if (_lastText != _label.text)
|
||||
RefreshVisuals();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
AddButtonListener();
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RemoveButtonListener();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
public event Action<Location> OnLocationClicked;
|
||||
|
||||
public void Initialize(Location location, string displayText = null)
|
||||
{
|
||||
Location = location;
|
||||
|
||||
if (_label != null)
|
||||
_label.text = string.IsNullOrWhiteSpace(displayText) ? location.ToString() : displayText;
|
||||
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
private void HandleClicked()
|
||||
{
|
||||
OnLocationClicked?.Invoke(Location);
|
||||
}
|
||||
|
||||
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(
|
||||
$"[CodexLocationButton] '{_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 AddButtonListener()
|
||||
{
|
||||
if (_button == null)
|
||||
return;
|
||||
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
_button.onClick.AddListener(HandleClicked);
|
||||
}
|
||||
|
||||
private void RemoveButtonListener()
|
||||
{
|
||||
if (_button == null)
|
||||
return;
|
||||
|
||||
_button.onClick.RemoveListener(HandleClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Codex/CodexLocationButton.cs.meta
Normal file
3
Assets/Scripts/UI/Codex/CodexLocationButton.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2ac52c1bcad4ad3bb23ad8fa61d0e04
|
||||
timeCreated: 1773685516
|
||||
1289
Assets/Scripts/UI/Codex/CodexWindow.cs
Normal file
1289
Assets/Scripts/UI/Codex/CodexWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
3
Assets/Scripts/UI/Codex/CodexWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Codex/CodexWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21589b43d12d4df8b6f58a8dba281eac
|
||||
timeCreated: 1773683605
|
||||
3
Assets/Scripts/UI/HUD.meta
Normal file
3
Assets/Scripts/UI/HUD.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6b7ef755c724a8eaaf5f0c00cedc8b7
|
||||
timeCreated: 1769713926
|
||||
366
Assets/Scripts/UI/HUD/CursorTooltip.cs
Normal file
366
Assets/Scripts/UI/HUD/CursorTooltip.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Input;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// Cursor-follow tooltip that positions itself in the ROOT canvas coordinate space,
|
||||
/// sizes its background to the text, and clamps to screen/canvas bounds.
|
||||
/// </summary>
|
||||
public class CursorTooltip : MonoBehaviour
|
||||
{
|
||||
[Header("Tooltips")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _tooltipText;
|
||||
|
||||
[SerializeField]
|
||||
private Image _tooltipBackground;
|
||||
|
||||
[Header("Follow Cursor")]
|
||||
|
||||
[SerializeField]
|
||||
private bool _clampToScreen = true;
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _screenPadding = new(12f, 12f);
|
||||
|
||||
[Header("Background Sizing")]
|
||||
[Tooltip("Padding added around the text when sizing the background.")]
|
||||
[SerializeField]
|
||||
private Vector2 _backgroundPadding = new(14f, 8f);
|
||||
|
||||
[Tooltip("Clamp tooltip width so long strings wrap instead of creating a huge background.")]
|
||||
[SerializeField]
|
||||
private float _maxTooltipWidth = 420f;
|
||||
|
||||
[Tooltip("Minimum background size so it doesn't look tiny on short words.")]
|
||||
[SerializeField]
|
||||
private Vector2 _minBackgroundSize = new(60f, 28f);
|
||||
|
||||
[Tooltip("Maximum background size as a final safety clamp.")]
|
||||
[SerializeField]
|
||||
private Vector2 _maxBackgroundSize = new(520f, 200f);
|
||||
|
||||
[Header("Canvas")]
|
||||
[Tooltip("Optional override. If set, this canvas (or its rootCanvas) will be used for positioning/clamping.")]
|
||||
[SerializeField]
|
||||
private Canvas _canvasOverride;
|
||||
|
||||
private RectTransform _bgRect;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private IInteractable _hoveredInteractable;
|
||||
private InputManager _inputManager;
|
||||
private UICursorService _cursorService;
|
||||
|
||||
// IMPORTANT: We always position relative to the ROOT canvas.
|
||||
private Canvas _rootCanvas;
|
||||
private RectTransform _rootCanvasRect;
|
||||
private RectTransform _rootRect;
|
||||
private ItemDataSo _selectedItem;
|
||||
private ToolID _selectedTool = ToolID.None;
|
||||
private SettingsService _settingsService;
|
||||
private RectTransform _textRect;
|
||||
private Camera _uiCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rootRect = transform as RectTransform;
|
||||
|
||||
if (_tooltipBackground) _bgRect = _tooltipBackground.rectTransform;
|
||||
if (_tooltipText) _textRect = _tooltipText.rectTransform;
|
||||
|
||||
ResolveRootCanvas();
|
||||
|
||||
if (_tooltipBackground)
|
||||
_tooltipBackground.transform.SetAsFirstSibling();
|
||||
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
FollowCursor();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_eventCoordinator == null)
|
||||
return;
|
||||
|
||||
_eventCoordinator.Subscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Subscribe<SelectedToolChangedEvent>(OnSelectedToolChanged);
|
||||
_eventCoordinator.Subscribe<HoverInteractableChangedEvent>(OnHoveredInteractableChanged);
|
||||
_eventCoordinator.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
|
||||
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_eventCoordinator == null)
|
||||
return;
|
||||
|
||||
_eventCoordinator.Unsubscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Unsubscribe<SelectedToolChangedEvent>(OnSelectedToolChanged);
|
||||
_eventCoordinator.Unsubscribe<HoverInteractableChangedEvent>(OnHoveredInteractableChanged);
|
||||
_eventCoordinator.Unsubscribe<SettingsChangedEvent>(OnSettingsChanged);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator, InputManager inputManager, SettingsService settingsService,
|
||||
UICursorService cursorService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
_settingsService = settingsService;
|
||||
_cursorService = cursorService;
|
||||
}
|
||||
|
||||
private void ResolveRootCanvas()
|
||||
{
|
||||
var candidate = _canvasOverride != null ? _canvasOverride : GetComponentInParent<Canvas>();
|
||||
|
||||
if (candidate == null)
|
||||
{
|
||||
_rootCanvas = null;
|
||||
_rootCanvasRect = null;
|
||||
_uiCamera = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_rootCanvas = candidate.rootCanvas;
|
||||
_rootCanvasRect = _rootCanvas.transform as RectTransform;
|
||||
_uiCamera = _rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _rootCanvas.worldCamera;
|
||||
}
|
||||
|
||||
private void OnSelectedItemChanged(SelectedItemChangedEvent args)
|
||||
{
|
||||
_selectedItem = args.Item;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnSelectedToolChanged(SelectedToolChangedEvent args)
|
||||
{
|
||||
// Change args.Tool if your event exposes a different property name.
|
||||
_selectedTool = args.SelectedTool;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnHoveredInteractableChanged(HoverInteractableChangedEvent args)
|
||||
{
|
||||
_hoveredInteractable = args.Interactable;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(SettingsChangedEvent _)
|
||||
{
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private bool AreTooltipsEnabled()
|
||||
{
|
||||
return _settingsService == null || _settingsService.AreTooltipsEnabled();
|
||||
}
|
||||
|
||||
private void UpdateTooltip()
|
||||
{
|
||||
if (!_tooltipText)
|
||||
return;
|
||||
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var text = BuildTooltipText();
|
||||
var show = !string.IsNullOrWhiteSpace(text);
|
||||
|
||||
if (!show)
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_tooltipText.text = text;
|
||||
SetVisible(true);
|
||||
|
||||
UpdateLayoutToText();
|
||||
}
|
||||
|
||||
private string BuildTooltipText()
|
||||
{
|
||||
if (_hoveredInteractable == null)
|
||||
return string.Empty;
|
||||
|
||||
var targetName = GetInteractableName(_hoveredInteractable);
|
||||
if (string.IsNullOrWhiteSpace(targetName))
|
||||
return string.Empty;
|
||||
|
||||
var selectedItemName = GetSelectedItemName();
|
||||
if (!string.IsNullOrWhiteSpace(selectedItemName))
|
||||
return $"Use {selectedItemName} on {targetName}";
|
||||
|
||||
var selectedToolName = GetSelectedToolName();
|
||||
if (!string.IsNullOrWhiteSpace(selectedToolName))
|
||||
return $"Use {selectedToolName} on {targetName}";
|
||||
|
||||
return targetName;
|
||||
}
|
||||
|
||||
private string GetSelectedItemName()
|
||||
{
|
||||
return _selectedItem != null && !string.IsNullOrWhiteSpace(_selectedItem.ItemName)
|
||||
? _selectedItem.ItemName
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private string GetSelectedToolName()
|
||||
{
|
||||
return GetToolDisplayName(_selectedTool);
|
||||
}
|
||||
|
||||
private static string GetInteractableName(IInteractable interactable)
|
||||
{
|
||||
return interactable == null ? string.Empty : interactable.InteractableName;
|
||||
}
|
||||
|
||||
private static string GetToolDisplayName(ToolID toolID)
|
||||
{
|
||||
return toolID switch
|
||||
{
|
||||
ToolID.None => string.Empty,
|
||||
ToolID.Knife => "Knife",
|
||||
_ => toolID.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private void SetVisible(bool visible)
|
||||
{
|
||||
if (_tooltipText) _tooltipText.enabled = visible;
|
||||
if (_tooltipBackground) _tooltipBackground.enabled = visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sizes the text rect (clamped) and the background rect (text size + padding),
|
||||
/// so the bubble never looks comically huge or tiny.
|
||||
/// </summary>
|
||||
private void UpdateLayoutToText()
|
||||
{
|
||||
if (!_tooltipText || !_tooltipBackground || !_textRect || !_bgRect)
|
||||
return;
|
||||
|
||||
_tooltipText.textWrappingMode = TextWrappingModes.Normal;
|
||||
|
||||
_tooltipText.ForceMeshUpdate();
|
||||
var preferred = _tooltipText.GetPreferredValues(_tooltipText.text, _maxTooltipWidth, 0f);
|
||||
|
||||
var textW = Mathf.Min(preferred.x, _maxTooltipWidth);
|
||||
var textH = preferred.y;
|
||||
|
||||
_textRect.sizeDelta = new Vector2(textW, textH);
|
||||
|
||||
var bgW = textW + _backgroundPadding.x * 2f;
|
||||
var bgH = textH + _backgroundPadding.y * 2f;
|
||||
|
||||
bgW = Mathf.Clamp(bgW, _minBackgroundSize.x, _maxBackgroundSize.x);
|
||||
bgH = Mathf.Clamp(bgH, _minBackgroundSize.y, _maxBackgroundSize.y);
|
||||
|
||||
_bgRect.sizeDelta = new Vector2(bgW, bgH);
|
||||
}
|
||||
|
||||
private void FollowCursor()
|
||||
{
|
||||
if (_rootRect == null || _inputManager == null)
|
||||
return;
|
||||
|
||||
var pointerScreen = _inputManager.PointerPosition;
|
||||
var cursorOffset = _cursorService.CurrentStyleEntry.TooltipOffset;
|
||||
var targetScreen = pointerScreen + cursorOffset;
|
||||
|
||||
if (_rootCanvasRect != null)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
_rootCanvasRect,
|
||||
targetScreen,
|
||||
_uiCamera,
|
||||
out var localPoint
|
||||
);
|
||||
|
||||
_rootRect.anchoredPosition = localPoint;
|
||||
|
||||
if (_clampToScreen)
|
||||
ClampAnchoredToRootCanvas();
|
||||
}
|
||||
else
|
||||
{
|
||||
_rootRect.position = targetScreen;
|
||||
|
||||
if (_clampToScreen)
|
||||
ClampWorldToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampAnchoredToRootCanvas()
|
||||
{
|
||||
if (_rootCanvasRect == null || _bgRect == null)
|
||||
return;
|
||||
|
||||
var bgSize = _bgRect.rect.size;
|
||||
var canvasRect = _rootCanvasRect.rect;
|
||||
|
||||
var pos = _rootRect.anchoredPosition;
|
||||
|
||||
var minX = canvasRect.xMin + _screenPadding.x;
|
||||
var maxX = canvasRect.xMax - _screenPadding.x - bgSize.x;
|
||||
|
||||
var minY = canvasRect.yMin + _screenPadding.y + bgSize.y;
|
||||
var maxY = canvasRect.yMax - _screenPadding.y;
|
||||
|
||||
pos.x = Mathf.Clamp(pos.x, minX, maxX);
|
||||
pos.y = Mathf.Clamp(pos.y, minY, maxY);
|
||||
|
||||
_rootRect.anchoredPosition = pos;
|
||||
}
|
||||
|
||||
private void ClampWorldToScreen()
|
||||
{
|
||||
if (_bgRect == null)
|
||||
return;
|
||||
|
||||
var bgSize = _bgRect.rect.size;
|
||||
Vector2 pos = _rootRect.position;
|
||||
|
||||
var minX = _screenPadding.x;
|
||||
var maxX = Screen.width - _screenPadding.x - bgSize.x;
|
||||
|
||||
var minY = _screenPadding.y + bgSize.y;
|
||||
var maxY = Screen.height - _screenPadding.y;
|
||||
|
||||
pos.x = Mathf.Clamp(pos.x, minX, maxX);
|
||||
pos.y = Mathf.Clamp(pos.y, minY, maxY);
|
||||
|
||||
_rootRect.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/CursorTooltip.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/CursorTooltip.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: feab9a2f47fa4c39836de3ffbcd9aa7e
|
||||
timeCreated: 1769719100
|
||||
282
Assets/Scripts/UI/HUD/HUDContainer.cs
Normal file
282
Assets/Scripts/UI/HUD/HUDContainer.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimal HUD holder:
|
||||
/// - Registers itself with UIManager
|
||||
/// - Handles HUD visibility (alpha tween only)
|
||||
/// </summary>
|
||||
public class HUDContainer : MonoBehaviour, IHud
|
||||
{
|
||||
private const float DISPLAY_TIME = 4f;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private CursorTooltip _cursorTooltip;
|
||||
[SerializeField]
|
||||
private InteractTextUI _interactText;
|
||||
|
||||
[Header("Inventory")]
|
||||
[SerializeField]
|
||||
private InventoryBar _inventoryBar;
|
||||
|
||||
[Header("Components")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainCanvasGroup;
|
||||
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _interactTextCanvasGroup;
|
||||
|
||||
[Header("Tweens")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.2f,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private GraphicRaycaster _graphicRaycaster;
|
||||
public GraphicRaycaster Raycaster => _graphicRaycaster;
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
private Sequence _hudSequence;
|
||||
private CancellationTokenSource _interactCancellationTokenSource;
|
||||
|
||||
private Sequence _interactErrorSequence;
|
||||
private UIManager _uiManager;
|
||||
|
||||
public CursorTooltip CursorTooltip => _cursorTooltip;
|
||||
public InventoryBar InventoryBar => _inventoryBar;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_mainCanvasGroup != null)
|
||||
{
|
||||
_mainCanvasGroup.alpha = 1f;
|
||||
_mainCanvasGroup.interactable = true;
|
||||
_mainCanvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
if (_interactTextCanvasGroup != null)
|
||||
{
|
||||
_interactTextCanvasGroup.alpha = 0f;
|
||||
_interactTextCanvasGroup.blocksRaycasts = false;
|
||||
_interactTextCanvasGroup.interactable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopTween();
|
||||
|
||||
StopInteractErrorTween();
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(UIManager uiManager)
|
||||
{
|
||||
_uiManager = uiManager;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Visibility
|
||||
// --------------------
|
||||
|
||||
public async UniTask DisplayInteractText(string interactText)
|
||||
{
|
||||
if (_interactTextCanvasGroup == null || _interactText == null)
|
||||
return;
|
||||
|
||||
// Cancel any in-flight error display and restart from current alpha.
|
||||
StopInteractErrorTween();
|
||||
|
||||
// Set text immediately.
|
||||
var text = interactText ?? string.Empty;
|
||||
|
||||
_interactText.SetText(text);
|
||||
|
||||
_interactCancellationTokenSource = new CancellationTokenSource();
|
||||
var token = _interactCancellationTokenSource.Token;
|
||||
|
||||
// Make sure it can show (but don't let it steal clicks).
|
||||
_interactTextCanvasGroup.blocksRaycasts = false;
|
||||
_interactTextCanvasGroup.interactable = false;
|
||||
|
||||
var fadeIn = new TweenSettings<float>
|
||||
{
|
||||
startValue = _interactTextCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var fadeOut = new TweenSettings<float>
|
||||
{
|
||||
startValue = 1f,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Fade in
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeIn));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 1f;
|
||||
|
||||
// Hold
|
||||
if (DISPLAY_TIME > 0f)
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(DISPLAY_TIME), DelayType.UnscaledDeltaTime,
|
||||
cancellationToken: token);
|
||||
|
||||
// Fade out
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeOut));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 0f;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// If interrupted by a newer DisplayError call or destroy, just stop cleanly.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_interactCancellationTokenSource != null)
|
||||
{
|
||||
_interactCancellationTokenSource.Dispose();
|
||||
_interactCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
_interactErrorSequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_mainCanvasGroup == null)
|
||||
return;
|
||||
|
||||
StopTween();
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_mainCanvasGroup.blocksRaycasts = true;
|
||||
_mainCanvasGroup.interactable = true;
|
||||
|
||||
var alphaTween = new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_hudSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainCanvasGroup, alphaTween));
|
||||
|
||||
try
|
||||
{
|
||||
await _hudSequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// interrupted by Hide / destroy — safe to ignore
|
||||
}
|
||||
|
||||
_mainCanvasGroup.alpha = 1f;
|
||||
}
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_mainCanvasGroup == null)
|
||||
return;
|
||||
|
||||
StopTween();
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_mainCanvasGroup.blocksRaycasts = false;
|
||||
_mainCanvasGroup.interactable = false;
|
||||
|
||||
var alphaTween = new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_hudSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainCanvasGroup, alphaTween));
|
||||
|
||||
try
|
||||
{
|
||||
await _hudSequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// interrupted — safe
|
||||
}
|
||||
|
||||
_mainCanvasGroup.alpha = 0f;
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
{
|
||||
if (_hudSequence.isAlive)
|
||||
_hudSequence.Stop();
|
||||
|
||||
_hudSequence = default;
|
||||
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void StopInteractErrorTween()
|
||||
{
|
||||
if (_interactErrorSequence.isAlive)
|
||||
_interactErrorSequence.Stop();
|
||||
|
||||
_interactErrorSequence = default;
|
||||
|
||||
if (_interactCancellationTokenSource != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_interactCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_interactCancellationTokenSource.Dispose();
|
||||
_interactCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapClicked()
|
||||
{
|
||||
// Placeholder for later:
|
||||
// - open map window
|
||||
// - toggle map overlay
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/HUDContainer.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/HUDContainer.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8b77a48c3af4a8c8858359a537523a9
|
||||
timeCreated: 1769713926
|
||||
433
Assets/Scripts/UI/HUD/InfoPopup.cs
Normal file
433
Assets/Scripts/UI/HUD/InfoPopup.cs
Normal file
@@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
public class InfoPopup : MonoBehaviour, IPopup
|
||||
{
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _rectTransform;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Sizing")]
|
||||
[SerializeField]
|
||||
private float _minWidth = 160f;
|
||||
|
||||
[SerializeField]
|
||||
private float _maxWidth = 500f;
|
||||
|
||||
[SerializeField]
|
||||
private float _minHeight = 60f;
|
||||
|
||||
[SerializeField]
|
||||
private float _extraWidthSafety = 4f;
|
||||
|
||||
[SerializeField]
|
||||
private float _extraHeightSafety = 4f;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField]
|
||||
private float _slideDuration = 1.2f;
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _hiddenAnchoredPosition = new(0f, -250f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _shownAnchoredPosition = new(0f, 0f);
|
||||
|
||||
[SerializeField]
|
||||
private float _shownAlpha = 1f;
|
||||
|
||||
[Header("Queue")]
|
||||
[SerializeField]
|
||||
private bool _suppressDuplicateMessages = true;
|
||||
|
||||
[Header("Editor Debug")]
|
||||
[SerializeField]
|
||||
private bool _debugResizeInEditor = true;
|
||||
|
||||
private readonly Queue<PopupRequest> _queue = new();
|
||||
|
||||
private string _currentMessage = string.Empty;
|
||||
|
||||
private CancellationTokenSource _destroyCts;
|
||||
private bool _isProcessingQueue;
|
||||
private string _lastEditorText = string.Empty;
|
||||
private float _lastHeight = -1f;
|
||||
private float _lastWidth = -1f;
|
||||
|
||||
private Sequence _sequence;
|
||||
private CancellationTokenSource _sequenceCts;
|
||||
|
||||
public bool IsModal => false;
|
||||
public GameObject GameObject => gameObject;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_destroyCts = new CancellationTokenSource();
|
||||
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (_rectTransform != null)
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
if (_lastEditorText != _text.text)
|
||||
ResizeToFitText();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_destroyCts != null)
|
||||
{
|
||||
_destroyCts.Cancel();
|
||||
_destroyCts.Dispose();
|
||||
_destroyCts = null;
|
||||
}
|
||||
|
||||
_queue.Clear();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
ResizeToFitText();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_rectTransform == null || _canvasGroup == null)
|
||||
return;
|
||||
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
_canvasGroup.alpha = 0f;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings<Vector2>
|
||||
{
|
||||
startValue = _hiddenAnchoredPosition,
|
||||
endValue = _shownAnchoredPosition,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.OutCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = _shownAlpha,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.OutCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_rectTransform.anchoredPosition = _shownAnchoredPosition;
|
||||
_canvasGroup.alpha = _shownAlpha;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_rectTransform == null || _canvasGroup == null)
|
||||
return;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings<Vector2>
|
||||
{
|
||||
startValue = _rectTransform.anchoredPosition,
|
||||
endValue = _hiddenAnchoredPosition,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.InCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.InCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (_text != null)
|
||||
_text.text = text ?? string.Empty;
|
||||
|
||||
ResizeToFitText();
|
||||
}
|
||||
|
||||
public UniTask Play(string text, float duration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
if (_suppressDuplicateMessages && IsDuplicateMessage(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
_queue.Enqueue(new PopupRequest(text, duration));
|
||||
|
||||
if (!_isProcessingQueue)
|
||||
ProcessQueue().Forget();
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void ClearQueue()
|
||||
{
|
||||
_queue.Clear();
|
||||
_currentMessage = string.Empty;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_rectTransform != null)
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
SetText(string.Empty);
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
|
||||
private bool IsDuplicateMessage(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentMessage) &&
|
||||
string.Equals(_currentMessage, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
foreach (var queued in _queue)
|
||||
if (string.Equals(queued.Text, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessQueue()
|
||||
{
|
||||
if (_isProcessingQueue)
|
||||
return;
|
||||
|
||||
_isProcessingQueue = true;
|
||||
|
||||
try
|
||||
{
|
||||
while (_destroyCts != null && !_destroyCts.IsCancellationRequested && _queue.Count > 0)
|
||||
{
|
||||
var request = _queue.Dequeue();
|
||||
_currentMessage = request.Text;
|
||||
|
||||
SetText(request.Text);
|
||||
await Show();
|
||||
|
||||
if (request.Duration > 0f)
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(request.Duration),
|
||||
cancellationToken: _destroyCts.Token);
|
||||
|
||||
await Hide();
|
||||
_currentMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentMessage = string.Empty;
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResizeToFitText()
|
||||
{
|
||||
if (_text == null || _rectTransform == null)
|
||||
return;
|
||||
|
||||
_text.ForceMeshUpdate();
|
||||
|
||||
var margin = _text.margin;
|
||||
|
||||
var leftInset = margin.x;
|
||||
var topInset = margin.y;
|
||||
var rightInset = margin.z;
|
||||
var bottomInset = margin.w;
|
||||
|
||||
var totalHorizontalInset = leftInset + rightInset;
|
||||
var totalVerticalInset = topInset + bottomInset;
|
||||
|
||||
var maxTextWidth = Mathf.Max(0f, _maxWidth - totalHorizontalInset - _extraWidthSafety);
|
||||
|
||||
var preferredSize = _text.GetPreferredValues(_text.text, maxTextWidth, Mathf.Infinity);
|
||||
|
||||
var width = Mathf.Clamp(
|
||||
preferredSize.x + totalHorizontalInset + _extraWidthSafety,
|
||||
_minWidth,
|
||||
_maxWidth);
|
||||
|
||||
var availableTextWidth = Mathf.Max(0f, width - totalHorizontalInset - _extraWidthSafety);
|
||||
|
||||
var wrappedPreferredSize = _text.GetPreferredValues(_text.text, availableTextWidth, Mathf.Infinity);
|
||||
|
||||
var height = Mathf.Max(
|
||||
_minHeight,
|
||||
wrappedPreferredSize.y + totalVerticalInset + _extraHeightSafety);
|
||||
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_rectTransform);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var changed =
|
||||
!Mathf.Approximately(width, _lastWidth) ||
|
||||
!Mathf.Approximately(height, _lastHeight) ||
|
||||
_lastEditorText != _text.text;
|
||||
|
||||
if (_debugResizeInEditor && changed)
|
||||
Debug.Log(
|
||||
$"[InfoPopup] '{_text.text}'\n" +
|
||||
$"Preferred: {preferredSize.x:F1} x {preferredSize.y:F1}\n" +
|
||||
$"Wrapped: {wrappedPreferredSize.x:F1} x {wrappedPreferredSize.y:F1}\n" +
|
||||
$"Insets: L{leftInset:F1} T{topInset:F1} R{rightInset:F1} B{bottomInset:F1}\n" +
|
||||
$"Final Size: {width:F1} x {height:F1}",
|
||||
this);
|
||||
#endif
|
||||
|
||||
_lastEditorText = _text.text;
|
||||
_lastWidth = width;
|
||||
_lastHeight = height;
|
||||
}
|
||||
|
||||
private void CancelTweenIfRunning()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
_sequence = default;
|
||||
|
||||
if (_sequenceCts != null)
|
||||
{
|
||||
_sequenceCts.Cancel();
|
||||
_sequenceCts.Dispose();
|
||||
_sequenceCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct PopupRequest
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly float Duration;
|
||||
|
||||
public PopupRequest(string text, float duration)
|
||||
{
|
||||
Text = text;
|
||||
Duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InfoPopup.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InfoPopup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79bc6a48c54849a295fba449628a13ed
|
||||
timeCreated: 1773762669
|
||||
166
Assets/Scripts/UI/HUD/InteractTextUI.cs
Normal file
166
Assets/Scripts/UI/HUD/InteractTextUI.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
[RequireComponent(typeof(LayoutElement))]
|
||||
public class InteractTextUI : MonoBehaviour
|
||||
{
|
||||
[Header("UI Elements")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private Image _background;
|
||||
|
||||
[Header("Sizing")]
|
||||
[SerializeField]
|
||||
private Vector2 _backgroundPadding = new(20f, 10f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _minBackgroundSize = new(80f, 36f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _maxBackgroundSize = new(1400f, 240f);
|
||||
|
||||
private RectTransform _backgroundRect;
|
||||
|
||||
private string _lastText = string.Empty;
|
||||
private LayoutElement _rootLayoutElement;
|
||||
|
||||
private RectTransform _rootRect;
|
||||
private RectTransform _textRect;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _text != null ? _text.text : string.Empty;
|
||||
set
|
||||
{
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
var safeValue = value ?? string.Empty;
|
||||
if (_text.text == safeValue)
|
||||
return;
|
||||
|
||||
_text.text = safeValue;
|
||||
RefreshLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CacheRefs();
|
||||
RefreshLayout();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
if (_lastText != _text.text)
|
||||
RefreshLayout();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
CacheRefs();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
RefreshLayout();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void CacheRefs()
|
||||
{
|
||||
_rootRect = transform as RectTransform;
|
||||
_rootLayoutElement = GetComponent<LayoutElement>();
|
||||
|
||||
if (_text != null)
|
||||
_textRect = _text.rectTransform;
|
||||
|
||||
if (_background != null)
|
||||
_backgroundRect = _background.rectTransform;
|
||||
}
|
||||
|
||||
public void SetText(string value)
|
||||
{
|
||||
Text = value;
|
||||
}
|
||||
|
||||
public void ClearText()
|
||||
{
|
||||
Text = string.Empty;
|
||||
}
|
||||
|
||||
public void RefreshLayout()
|
||||
{
|
||||
if (_rootRect == null || _rootLayoutElement == null || _textRect == null || _backgroundRect == null ||
|
||||
_text == null || _background == null)
|
||||
return;
|
||||
|
||||
_lastText = _text.text ?? string.Empty;
|
||||
|
||||
var hasText = !string.IsNullOrWhiteSpace(_lastText);
|
||||
|
||||
_text.enabled = hasText;
|
||||
_background.enabled = hasText;
|
||||
|
||||
if (!hasText)
|
||||
return;
|
||||
|
||||
_backgroundRect.anchorMin = Vector2.zero;
|
||||
_backgroundRect.anchorMax = Vector2.one;
|
||||
_backgroundRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
_backgroundRect.offsetMin = Vector2.zero;
|
||||
_backgroundRect.offsetMax = Vector2.zero;
|
||||
_backgroundRect.anchoredPosition = Vector2.zero;
|
||||
_background.transform.SetAsFirstSibling();
|
||||
|
||||
_textRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
_textRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
_textRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
_textRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
_text.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
_text.enableAutoSizing = false;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
_text.ForceMeshUpdate();
|
||||
|
||||
var preferred = _text.GetPreferredValues(_lastText);
|
||||
var textWidth = preferred.x;
|
||||
var textHeight = preferred.y;
|
||||
|
||||
_textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, textWidth);
|
||||
_textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, textHeight);
|
||||
|
||||
var rootWidth = Mathf.Clamp(
|
||||
textWidth + _backgroundPadding.x * 2f,
|
||||
_minBackgroundSize.x,
|
||||
_maxBackgroundSize.x);
|
||||
|
||||
var rootHeight = Mathf.Clamp(
|
||||
textHeight + _backgroundPadding.y * 2f,
|
||||
_minBackgroundSize.y,
|
||||
_maxBackgroundSize.y);
|
||||
|
||||
_rootRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rootWidth);
|
||||
_rootRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rootHeight);
|
||||
|
||||
_rootLayoutElement.minWidth = rootWidth;
|
||||
_rootLayoutElement.preferredWidth = rootWidth;
|
||||
_rootLayoutElement.flexibleWidth = 0f;
|
||||
_rootLayoutElement.minHeight = rootHeight;
|
||||
_rootLayoutElement.preferredHeight = rootHeight;
|
||||
_rootLayoutElement.flexibleHeight = 0f;
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_rootRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InteractTextUI.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InteractTextUI.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e23149ea93ce4ccf9c43ed9e88896dfc
|
||||
timeCreated: 1773669738
|
||||
566
Assets/Scripts/UI/HUD/InventoryBar.cs
Normal file
566
Assets/Scripts/UI/HUD/InventoryBar.cs
Normal file
@@ -0,0 +1,566 @@
|
||||
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.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
public class InventoryBar : MonoBehaviour
|
||||
{
|
||||
private const int ITEMS_PER_PAGE = 4;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _nextButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _prevButton;
|
||||
|
||||
[Header("Paging")]
|
||||
[SerializeField]
|
||||
private RectTransform _content;
|
||||
|
||||
[SerializeField]
|
||||
private float _pageTweenSeconds = 5f;
|
||||
|
||||
private readonly List<UIInventorySlot> _inventorySlots = new();
|
||||
|
||||
private AddressableManager _addressableManager;
|
||||
private AssetRegistry _assetRegistry;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private PlayerManager _playerManager;
|
||||
private TutorialService _tutorialService;
|
||||
|
||||
private int _currentPage;
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
private CancellationTokenSource _pageCts;
|
||||
private CancellationTokenSource _rebuildCts;
|
||||
private Sequence _pageSequence;
|
||||
|
||||
private float _pageZeroFirstSlotX;
|
||||
|
||||
private int PageCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_inventorySlots.Count <= 0)
|
||||
return 0;
|
||||
|
||||
return (_inventorySlots.Count + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(
|
||||
EventCoordinator eventCoordinator,
|
||||
PlayerManager playerManager,
|
||||
AddressableManager addressableManager,
|
||||
AssetRegistry assetRegistry,
|
||||
TutorialService tutorialService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_playerManager = playerManager;
|
||||
_addressableManager = addressableManager;
|
||||
_assetRegistry = assetRegistry;
|
||||
_tutorialService = tutorialService;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_nextButton)
|
||||
_nextButton.onClick.AddListener(OnNextClicked);
|
||||
|
||||
if (_prevButton)
|
||||
_prevButton.onClick.AddListener(OnPrevClicked);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_eventCoordinator != null)
|
||||
{
|
||||
_eventCoordinator.Subscribe<InventoryChangedEvent>(OnInventoryChanged);
|
||||
_eventCoordinator.Subscribe<SelectedItemChangedEvent>(OnSelectedItemCleared);
|
||||
_eventCoordinator.Subscribe<OnNextItemClickedEvent>(OnNextItem);
|
||||
_eventCoordinator.Subscribe<OnPreviousItemClickedEvent>(OnPreviousItem);
|
||||
}
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_eventCoordinator != null)
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<InventoryChangedEvent>(OnInventoryChanged);
|
||||
_eventCoordinator.Unsubscribe<SelectedItemChangedEvent>(OnSelectedItemCleared);
|
||||
_eventCoordinator.Unsubscribe<OnNextItemClickedEvent>(OnNextItem);
|
||||
_eventCoordinator.Unsubscribe<OnPreviousItemClickedEvent>(OnPreviousItem);
|
||||
}
|
||||
|
||||
StopPageTween();
|
||||
StopRebuild();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_nextButton)
|
||||
_nextButton.onClick.RemoveListener(OnNextClicked);
|
||||
|
||||
if (_prevButton)
|
||||
_prevButton.onClick.RemoveListener(OnPrevClicked);
|
||||
|
||||
StopPageTween();
|
||||
StopRebuild();
|
||||
}
|
||||
|
||||
private void OnInventoryChanged(InventoryChangedEvent evt)
|
||||
{
|
||||
RebuildInventoryAsync().Forget();
|
||||
}
|
||||
|
||||
private void OnSelectedItemCleared(SelectedItemChangedEvent evt)
|
||||
{
|
||||
if (evt.Item != null)
|
||||
return;
|
||||
|
||||
ClearSelectionVisualOnly();
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
ClearSelectionVisualOnly();
|
||||
_eventCoordinator?.Publish(new SelectedItemChangedEvent(null));
|
||||
}
|
||||
|
||||
public void SelectIndex(int index)
|
||||
{
|
||||
SetSelectedIndex(index, true);
|
||||
}
|
||||
|
||||
public void OnSlotInteracted(UIInventorySlot slot)
|
||||
{
|
||||
if (slot == null || slot.Item == null)
|
||||
return;
|
||||
|
||||
var index = _inventorySlots.IndexOf(slot);
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
if (_selectedIndex == index)
|
||||
{
|
||||
ClearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
SelectIndex(index);
|
||||
}
|
||||
|
||||
private void ClearContentChildrenImmediateVisual()
|
||||
{
|
||||
if (!_content)
|
||||
return;
|
||||
|
||||
for (var i = _content.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = _content.GetChild(i);
|
||||
if (child == null)
|
||||
continue;
|
||||
|
||||
child.gameObject.SetActive(false);
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask RebuildInventoryAsync()
|
||||
{
|
||||
StopRebuild();
|
||||
_rebuildCts = new CancellationTokenSource();
|
||||
var token = _rebuildCts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_content)
|
||||
{
|
||||
ClearSelectionVisualOnly();
|
||||
_eventCoordinator?.Publish(new SelectedItemChangedEvent(null));
|
||||
UpdateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_assetRegistry ||
|
||||
!_assetRegistry.TryGetReference(AssetKeyIdentifiers.Get(UIKey.InventorySlot), out var slotRef))
|
||||
{
|
||||
ClearSelectionVisualOnly();
|
||||
_eventCoordinator?.Publish(new SelectedItemChangedEvent(null));
|
||||
UpdateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
StopPageTween();
|
||||
|
||||
var inventoryItems = _playerManager.GetInventoryItems();
|
||||
var items = new List<ItemDataSo>();
|
||||
|
||||
if (inventoryItems != null)
|
||||
{
|
||||
for (var i = 0; i < inventoryItems.Count; i++)
|
||||
{
|
||||
var item = inventoryItems[i];
|
||||
if (item != null)
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Rebuilding inventory with {items.Count} valid items");
|
||||
|
||||
_inventorySlots.Clear();
|
||||
|
||||
ClearContentChildrenImmediateVisual();
|
||||
await UniTask.NextFrame(token);
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var item = items[i];
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var slotObj = await _addressableManager.InstantiateAsync(slotRef, parent: _content);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
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.");
|
||||
Destroy(slotObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
slot.Initialize(this, item);
|
||||
_inventorySlots.Add(slot);
|
||||
}
|
||||
|
||||
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, token);
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
CachePageAnchorData();
|
||||
|
||||
_currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, PageCount - 1));
|
||||
SnapToPage(_currentPage);
|
||||
|
||||
ClearSelectionVisualOnly();
|
||||
_eventCoordinator.Publish(new SelectedItemChangedEvent(null));
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void StopRebuild()
|
||||
{
|
||||
_rebuildCts?.Cancel();
|
||||
_rebuildCts?.Dispose();
|
||||
_rebuildCts = null;
|
||||
}
|
||||
|
||||
private void SetSelectedIndex(int index, bool scrollToSelection)
|
||||
{
|
||||
if (_inventorySlots.Count == 0)
|
||||
{
|
||||
ClearSelectionVisualOnly();
|
||||
_eventCoordinator.Publish(new SelectedItemChangedEvent(null));
|
||||
UpdateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
ClearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
index = Mathf.Clamp(index, 0, _inventorySlots.Count - 1);
|
||||
|
||||
if (_selectedIndex >= 0 && _selectedIndex < _inventorySlots.Count)
|
||||
_inventorySlots[_selectedIndex].SetSelected(false);
|
||||
|
||||
_selectedIndex = index;
|
||||
_inventorySlots[_selectedIndex].SetSelected(true);
|
||||
|
||||
_eventCoordinator.Publish(new SelectedItemChangedEvent(_inventorySlots[_selectedIndex].Item));
|
||||
CheckSelectedItemTutorial();
|
||||
|
||||
if (scrollToSelection)
|
||||
{
|
||||
var targetPage = IndexToPage(_selectedIndex);
|
||||
GoToPage(targetPage).Forget();
|
||||
}
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
private void CheckSelectedItemTutorial()
|
||||
{
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.ItemsAway);
|
||||
}
|
||||
|
||||
private void ClearSelectionVisualOnly()
|
||||
{
|
||||
if (_selectedIndex >= 0 && _selectedIndex < _inventorySlots.Count)
|
||||
_inventorySlots[_selectedIndex].SetSelected(false);
|
||||
|
||||
_selectedIndex = -1;
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
private int IndexToPage(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
return 0;
|
||||
|
||||
return index / ITEMS_PER_PAGE;
|
||||
}
|
||||
|
||||
private void CachePageAnchorData()
|
||||
{
|
||||
_pageZeroFirstSlotX = 0f;
|
||||
|
||||
if (_inventorySlots.Count == 0)
|
||||
return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
var first = _inventorySlots[0].transform as RectTransform;
|
||||
if (!first)
|
||||
return;
|
||||
|
||||
_pageZeroFirstSlotX = first.anchoredPosition.x;
|
||||
}
|
||||
|
||||
private float PageToAnchoredX(int pageIndex)
|
||||
{
|
||||
if (_inventorySlots.Count == 0)
|
||||
return 0f;
|
||||
|
||||
pageIndex = Mathf.Clamp(pageIndex, 0, Mathf.Max(0, PageCount - 1));
|
||||
|
||||
var firstIndexOnPage = pageIndex * ITEMS_PER_PAGE;
|
||||
if (firstIndexOnPage < 0 || firstIndexOnPage >= _inventorySlots.Count)
|
||||
return 0f;
|
||||
|
||||
var pageFirstSlot = _inventorySlots[firstIndexOnPage].transform as RectTransform;
|
||||
if (!pageFirstSlot)
|
||||
return 0f;
|
||||
|
||||
return _pageZeroFirstSlotX - pageFirstSlot.anchoredPosition.x;
|
||||
}
|
||||
|
||||
private async UniTask GoToPage(int pageIndex)
|
||||
{
|
||||
if (!_content)
|
||||
return;
|
||||
|
||||
var pages = PageCount;
|
||||
if (pages <= 1)
|
||||
{
|
||||
_currentPage = 0;
|
||||
SnapToPage(0);
|
||||
UpdateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
var clamped = Mathf.Clamp(pageIndex, 0, pages - 1);
|
||||
if (clamped == _currentPage && !_pageSequence.isAlive)
|
||||
{
|
||||
UpdateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPage = clamped;
|
||||
|
||||
StopPageTween();
|
||||
_pageCts = new CancellationTokenSource();
|
||||
|
||||
var targetX = PageToAnchoredX(_currentPage);
|
||||
|
||||
var from = _content.anchoredPosition;
|
||||
var to = from;
|
||||
to.x = targetX;
|
||||
|
||||
var tweenSettings = new TweenSettings
|
||||
{
|
||||
duration = Mathf.Max(0f, _pageTweenSeconds),
|
||||
ease = Ease.OutCubic,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
TweenSettings<Vector2> vecSettings = new TweenSettings<Vector2>
|
||||
{
|
||||
startValue = from,
|
||||
endValue = to,
|
||||
settings = tweenSettings
|
||||
};
|
||||
|
||||
|
||||
_pageSequence = Sequence.Create(useUnscaledTime: true).
|
||||
Group(Tween.Custom(vecSettings, newValue => _content.anchoredPosition = newValue));
|
||||
try
|
||||
{
|
||||
await _pageSequence.ToUniTask(cancellationToken: _pageCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pageSequence = default;
|
||||
UpdateButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
private void SnapToPage(int pageIndex)
|
||||
{
|
||||
if (!_content)
|
||||
return;
|
||||
|
||||
var pages = PageCount;
|
||||
if (pages <= 1)
|
||||
{
|
||||
var p0 = _content.anchoredPosition;
|
||||
p0.x = 0f;
|
||||
_content.anchoredPosition = p0;
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
pageIndex = Mathf.Clamp(pageIndex, 0, pages - 1);
|
||||
|
||||
var pos = _content.anchoredPosition;
|
||||
pos.x = PageToAnchoredX(pageIndex);
|
||||
_content.anchoredPosition = pos;
|
||||
}
|
||||
|
||||
private void StopPageTween()
|
||||
{
|
||||
if (_pageSequence.isAlive)
|
||||
_pageSequence.Stop();
|
||||
|
||||
_pageSequence = default;
|
||||
|
||||
_pageCts?.Cancel();
|
||||
_pageCts?.Dispose();
|
||||
_pageCts = null;
|
||||
}
|
||||
|
||||
private void OnNextClicked()
|
||||
{
|
||||
if (PageCount <= 1)
|
||||
return;
|
||||
|
||||
GoToPage(_currentPage + 1).Forget();
|
||||
}
|
||||
|
||||
private void OnPrevClicked()
|
||||
{
|
||||
if (PageCount <= 1)
|
||||
return;
|
||||
|
||||
GoToPage(_currentPage - 1).Forget();
|
||||
}
|
||||
|
||||
private void UpdateButtonState()
|
||||
{
|
||||
var pages = PageCount;
|
||||
var showNav = pages > 1;
|
||||
|
||||
if (_prevButton)
|
||||
{
|
||||
var showPrev = showNav && _currentPage > 0;
|
||||
_prevButton.gameObject.SetActive(showPrev);
|
||||
}
|
||||
|
||||
if (_nextButton)
|
||||
{
|
||||
var showNext = showNav && _currentPage < pages - 1;
|
||||
_nextButton.gameObject.SetActive(showNext);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNextItem(OnNextItemClickedEvent e)
|
||||
{
|
||||
if (_inventorySlots.Count == 0)
|
||||
return;
|
||||
|
||||
int nextIndex;
|
||||
|
||||
if (_selectedIndex < 0)
|
||||
{
|
||||
nextIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextIndex = _selectedIndex + 1;
|
||||
|
||||
if (nextIndex >= _inventorySlots.Count)
|
||||
nextIndex = 0;
|
||||
}
|
||||
|
||||
SetSelectedIndex(nextIndex, true);
|
||||
}
|
||||
|
||||
private void OnPreviousItem(OnPreviousItemClickedEvent e)
|
||||
{
|
||||
if (_inventorySlots.Count == 0)
|
||||
return;
|
||||
|
||||
int prevIndex;
|
||||
|
||||
if (_selectedIndex < 0)
|
||||
{
|
||||
prevIndex = _inventorySlots.Count - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevIndex = _selectedIndex - 1;
|
||||
|
||||
if (prevIndex < 0)
|
||||
prevIndex = _inventorySlots.Count - 1;
|
||||
}
|
||||
|
||||
SetSelectedIndex(prevIndex, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InventoryBar.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InventoryBar.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21400b75a7244d418a6315a802dc66f7
|
||||
timeCreated: 1769714388
|
||||
291
Assets/Scripts/UI/HUD/TutorialPopup.cs
Normal file
291
Assets/Scripts/UI/HUD/TutorialPopup.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
public class TutorialPopup : MonoBehaviour, IPopup
|
||||
{
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField]
|
||||
private float _fadeDuration = 0.3f;
|
||||
|
||||
[Header("Queue")]
|
||||
[SerializeField]
|
||||
private bool _suppressDuplicateMessages = true;
|
||||
|
||||
private readonly Queue<PopupRequest> _queue = new();
|
||||
private string _currentMessage = string.Empty;
|
||||
|
||||
private CancellationTokenSource _destroyCts;
|
||||
|
||||
private bool _isProcessingQueue;
|
||||
private Sequence _sequence;
|
||||
private CancellationTokenSource _sequenceCts;
|
||||
|
||||
public bool IsModal => false;
|
||||
|
||||
public GameObject GameObject => gameObject;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_destroyCts = new CancellationTokenSource();
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_destroyCts != null)
|
||||
{
|
||||
_destroyCts.Cancel();
|
||||
_destroyCts.Dispose();
|
||||
_destroyCts = null;
|
||||
}
|
||||
|
||||
_queue.Clear();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
return;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _fadeDuration,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_canvasGroup.alpha = 1f;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another tween or destroy.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
return;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _fadeDuration,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another tween or destroy.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
Hide().Forget();
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (_text != null)
|
||||
_text.text = text ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue a popup to be shown in order. Does not interrupt the current popup.
|
||||
/// Duplicate messages can be suppressed if they are currently showing or already queued.
|
||||
/// </summary>
|
||||
public UniTask Play(string text, float duration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
if (_suppressDuplicateMessages && IsDuplicateMessage(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
_queue.Enqueue(new PopupRequest(text, duration));
|
||||
|
||||
if (!_isProcessingQueue)
|
||||
ProcessQueue().Forget();
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears pending popups and hides the current popup immediately.
|
||||
/// Useful for scene transitions or hard UI resets.
|
||||
/// </summary>
|
||||
public void ClearQueue()
|
||||
{
|
||||
_queue.Clear();
|
||||
_currentMessage = string.Empty;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
SetText(string.Empty);
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
|
||||
private bool IsDuplicateMessage(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentMessage) &&
|
||||
string.Equals(_currentMessage, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
foreach (var queued in _queue)
|
||||
if (string.Equals(queued.Text, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessQueue()
|
||||
{
|
||||
if (_isProcessingQueue)
|
||||
return;
|
||||
|
||||
_isProcessingQueue = true;
|
||||
|
||||
try
|
||||
{
|
||||
while (_destroyCts != null && !_destroyCts.IsCancellationRequested && _queue.Count > 0)
|
||||
{
|
||||
var request = _queue.Dequeue();
|
||||
_currentMessage = request.Text;
|
||||
|
||||
SetText(request.Text);
|
||||
await Show();
|
||||
|
||||
if (request.Duration > 0f)
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(request.Duration),
|
||||
cancellationToken: _destroyCts.Token);
|
||||
|
||||
await Hide();
|
||||
_currentMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Object destroyed or queue processing canceled.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentMessage = string.Empty;
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelTweenIfRunning()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
_sequence = default;
|
||||
|
||||
if (_sequenceCts != null)
|
||||
{
|
||||
_sequenceCts.Cancel();
|
||||
_sequenceCts.Dispose();
|
||||
_sequenceCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct PopupRequest
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly float Duration;
|
||||
|
||||
public PopupRequest(string text, float duration)
|
||||
{
|
||||
Text = text;
|
||||
Duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/TutorialPopup.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/TutorialPopup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1451fbb217493e8e7d5fb086195021
|
||||
timeCreated: 1772821477
|
||||
114
Assets/Scripts/UI/HUD/UIInventorySlot.cs
Normal file
114
Assets/Scripts/UI/HUD/UIInventorySlot.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// UI slot is now an IInteractable so InteractManager (UI raycast path) can drive hover + click.
|
||||
/// No IPointerEnter/Exit, no OnClickEvent subscription.
|
||||
/// </summary>
|
||||
public class UIInventorySlot : MonoBehaviour, IInteractable
|
||||
{
|
||||
[SerializeField]
|
||||
private Image _slotBase;
|
||||
|
||||
[SerializeField]
|
||||
private Image _selectedVisual;
|
||||
|
||||
[SerializeField]
|
||||
private Image _icon;
|
||||
|
||||
public ItemDataSo Item;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
|
||||
private InventoryBar _owner;
|
||||
|
||||
private PlayerManager _playerManager;
|
||||
|
||||
// Cursor while hovering inventory slots (tweak if you have a “hand” cursor etc.)
|
||||
public UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.UseItem;
|
||||
public string InteractableName => Item.ItemName;
|
||||
|
||||
public UniTask EnterHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public UniTask ExitHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async UniTask OnInteract(ItemDataSo selectedItem = null)
|
||||
{
|
||||
Debug.Log("[UI Inventory Slot] Interacted");
|
||||
|
||||
if (Item == null)
|
||||
{
|
||||
_eventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(UIInteractKey.EmptySlot)));
|
||||
return;
|
||||
}
|
||||
|
||||
// normal click: selection
|
||||
if (selectedItem == null)
|
||||
{
|
||||
_owner?.OnSlotInteracted(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// use selected item on this item (prefer selected item's logic)
|
||||
if (selectedItem.Interaction != null)
|
||||
{
|
||||
var handled = await selectedItem.Interaction.TryUseWith(
|
||||
selectedItem, Item, _playerManager, _eventCoordinator);
|
||||
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback: allow target to handle it
|
||||
if (Item.Interaction != null)
|
||||
{
|
||||
var handled = await Item.Interaction.TryUseWith(
|
||||
Item, selectedItem, _playerManager, _eventCoordinator);
|
||||
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(PlayerManager playerManager, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_playerManager = playerManager;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public void Initialize(InventoryBar owner, ItemDataSo item)
|
||||
{
|
||||
Debug.Log("Initializing Slot");
|
||||
_owner = owner;
|
||||
Item = item;
|
||||
|
||||
if (_icon != null && Item != null)
|
||||
_icon.sprite = Item.Icon;
|
||||
|
||||
SetSelected(false);
|
||||
Debug.Log($"Set Slot to {Item}");
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (_selectedVisual != null) _selectedVisual.gameObject.SetActive(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/UIInventorySlot.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/UIInventorySlot.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75a4776058b6492394b866b5e04bc73f
|
||||
timeCreated: 1769714789
|
||||
3
Assets/Scripts/UI/Menus.meta
Normal file
3
Assets/Scripts/UI/Menus.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f373062d2284007a2999e0c9ead9ca9
|
||||
timeCreated: 1769707583
|
||||
3
Assets/Scripts/UI/Menus/Components.meta
Normal file
3
Assets/Scripts/UI/Menus/Components.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84e0529b59f0487a849dde7ed79f008e
|
||||
timeCreated: 1769794667
|
||||
171
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs
Normal file
171
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Save Slot UI:
|
||||
/// - No Load button.
|
||||
/// - Slot itself is clickable:
|
||||
/// - Filled => click loads
|
||||
/// - Empty => click creates (via SelectSaveWindow opening NewSaveWindow)
|
||||
/// - Delete button remains for filled saves.
|
||||
/// </summary>
|
||||
public class SaveSlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("Clickable Root")]
|
||||
[SerializeField]
|
||||
private Button _slotButton;
|
||||
|
||||
[Header("Labels")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _saveNameText;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _saveDateText;
|
||||
|
||||
[Header("Delete")]
|
||||
[SerializeField]
|
||||
private Button _deleteButton;
|
||||
|
||||
[Header("Empty Visual")]
|
||||
[SerializeField]
|
||||
private Image _emptyImage;
|
||||
|
||||
private Action<SaveFileInfo> _onDeleteClick;
|
||||
private Action _onEmptyClick;
|
||||
|
||||
private Action<SaveFileInfo> _onFilledClick;
|
||||
|
||||
public SaveFileInfo SaveInfo { get; private set; }
|
||||
public bool IsEmpty { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_slotButton != null)
|
||||
{
|
||||
_slotButton.onClick.RemoveAllListeners();
|
||||
_slotButton.onClick.AddListener(HandleSlotClicked);
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.onClick.RemoveAllListeners();
|
||||
_deleteButton.onClick.AddListener(HandleDeleteClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_slotButton != null) _slotButton.onClick.RemoveAllListeners();
|
||||
if (_deleteButton != null) _deleteButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
public GameObject GetSelectableGameObject()
|
||||
{
|
||||
return _slotButton != null ? _slotButton.gameObject : gameObject;
|
||||
}
|
||||
|
||||
public void SetFilled(
|
||||
SaveFileInfo saveInfo,
|
||||
Action<SaveFileInfo> onClickFilled,
|
||||
Action<SaveFileInfo> onDelete)
|
||||
{
|
||||
SaveInfo = saveInfo;
|
||||
IsEmpty = false;
|
||||
|
||||
_onFilledClick = onClickFilled;
|
||||
_onEmptyClick = null;
|
||||
_onDeleteClick = onDelete;
|
||||
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(false);
|
||||
|
||||
if (_saveNameText != null)
|
||||
{
|
||||
_saveNameText.gameObject.SetActive(true);
|
||||
_saveNameText.text = saveInfo.FileName;
|
||||
}
|
||||
|
||||
if (_saveDateText != null)
|
||||
{
|
||||
_saveDateText.gameObject.SetActive(true);
|
||||
_saveDateText.text = saveInfo.LastModified.ToString("g");
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.gameObject.SetActive(true);
|
||||
_deleteButton.interactable = true;
|
||||
}
|
||||
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = true;
|
||||
}
|
||||
|
||||
public void SetEmpty(Action onClickEmpty)
|
||||
{
|
||||
SaveInfo = default;
|
||||
IsEmpty = true;
|
||||
|
||||
_onFilledClick = null;
|
||||
_onEmptyClick = onClickEmpty;
|
||||
_onDeleteClick = null;
|
||||
|
||||
if (_saveNameText != null) _saveNameText.text = string.Empty;
|
||||
if (_saveDateText != null) _saveDateText.text = string.Empty;
|
||||
|
||||
if (_emptyImage != null) _emptyImage.gameObject.SetActive(true);
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.gameObject.SetActive(false);
|
||||
_deleteButton.interactable = false;
|
||||
}
|
||||
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = true; // empty slot is still clickable
|
||||
}
|
||||
|
||||
public void SetInteractable(bool interactable)
|
||||
{
|
||||
// Slot click should still work even when empty; we fully disable during "busy" only.
|
||||
if (_slotButton != null)
|
||||
_slotButton.interactable = interactable;
|
||||
|
||||
// Delete only for filled saves.
|
||||
if (_deleteButton != null)
|
||||
_deleteButton.interactable = interactable && !IsEmpty;
|
||||
}
|
||||
|
||||
private void HandleSlotClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
_onEmptyClick?.Invoke();
|
||||
else if (!string.IsNullOrWhiteSpace(SaveInfo.FileName))
|
||||
_onFilledClick?.Invoke(SaveInfo);
|
||||
}
|
||||
|
||||
private void HandleDeleteClicked()
|
||||
{
|
||||
if (IsEmpty)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SaveInfo.FileName))
|
||||
_onDeleteClick?.Invoke(SaveInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public struct SaveFileInfo
|
||||
{
|
||||
public readonly string FileName;
|
||||
public readonly DateTime LastModified;
|
||||
|
||||
public SaveFileInfo(string fileName, DateTime lastModified)
|
||||
{
|
||||
FileName = fileName;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/Components/SaveSlotUI.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 738235190e6d42eb93f533aa05cf98c2
|
||||
timeCreated: 1769794667
|
||||
341
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs
Normal file
341
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirm delete modal with tweened show/hide (fade + scale).
|
||||
/// - Open(): animates in
|
||||
/// - Close(): animates out
|
||||
/// - CloseImmediate(): hard hide (safe for OnDisable/OnDestroy)
|
||||
/// Notes:
|
||||
/// - Uses unscaled time so it still animates while paused.
|
||||
/// - Gates input during transitions.
|
||||
/// </summary>
|
||||
public class ConfirmDeleteWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField]
|
||||
private GameObject _root;
|
||||
|
||||
[Header("Animation Targets")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _panelTransform;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _titleText;
|
||||
|
||||
[SerializeField]
|
||||
private Button _confirmButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _cancelButton;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private float _duration = 0.18f;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _easeIn = Ease.OutBack;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _easeOut = Ease.InQuad;
|
||||
|
||||
[SerializeField]
|
||||
private bool _useUnscaledTime = true;
|
||||
|
||||
[Header("Scale")]
|
||||
[SerializeField]
|
||||
private float _fromScale = 0.92f;
|
||||
|
||||
[SerializeField]
|
||||
private float _toScale = 1.0f;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _isAnimating;
|
||||
|
||||
private bool _isOpen;
|
||||
|
||||
private SaveFileInfo _pending;
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_confirmButton != null) _confirmButton.onClick.AddListener(Confirm);
|
||||
if (_cancelButton != null) _cancelButton.onClick.AddListener(Cancel);
|
||||
|
||||
CloseImmediate();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_confirmButton != null) _confirmButton.onClick.RemoveListener(Confirm);
|
||||
if (_cancelButton != null) _cancelButton.onClick.RemoveListener(Cancel);
|
||||
|
||||
StopAnim();
|
||||
}
|
||||
|
||||
public event Action<SaveFileInfo> OnConfirmDelete;
|
||||
public event Action OnCancel;
|
||||
|
||||
public void Open(SaveFileInfo info)
|
||||
{
|
||||
_pending = info;
|
||||
|
||||
if (_titleText != null)
|
||||
_titleText.text = $"Delete '{info.FileName}'?";
|
||||
|
||||
OpenAsync().Forget();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
CloseAsync().Forget();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
StopAnim();
|
||||
|
||||
_pending = default;
|
||||
_isOpen = false;
|
||||
_isAnimating = false;
|
||||
|
||||
if (_root != null) _root.SetActive(false);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
var s = _toScale;
|
||||
_panelTransform.localScale = new Vector3(s, s, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask OpenAsync()
|
||||
{
|
||||
if (_isOpen && !_isAnimating) return;
|
||||
|
||||
EnsureRefs();
|
||||
|
||||
StopAnim();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
if (_root != null) _root.SetActive(true);
|
||||
|
||||
_isAnimating = true;
|
||||
|
||||
// Prep start state
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
if (_panelTransform != null)
|
||||
{
|
||||
var s = _fromScale;
|
||||
_panelTransform.localScale = new Vector3(s, s, 1f);
|
||||
}
|
||||
|
||||
// Build sequence (fade + scale)
|
||||
_sequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
_sequence.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeIn,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
if (_panelTransform != null)
|
||||
_sequence.Group(Tween.Scale(_panelTransform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = new Vector3(_fromScale, _fromScale, 1f),
|
||||
endValue = new Vector3(_toScale, _toScale, 1f),
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeIn,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
_isAnimating = false;
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
// Enable input once fully open
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.interactable = true;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
// Optional: focus cancel for controller/keyboard flows
|
||||
_cancelButton?.Select();
|
||||
}
|
||||
|
||||
private async UniTask CloseAsync()
|
||||
{
|
||||
if (!_isOpen && !_isAnimating) return;
|
||||
|
||||
EnsureRefs();
|
||||
|
||||
StopAnim();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
_isAnimating = true;
|
||||
|
||||
// Disable input immediately while animating out
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
var startAlpha = _canvasGroup != null ? _canvasGroup.alpha : 1f;
|
||||
var startScale = _panelTransform != null ? _panelTransform.localScale : Vector3.one;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
_sequence.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = startAlpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeOut,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
if (_panelTransform != null)
|
||||
_sequence.Group(Tween.Scale(_panelTransform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = startScale,
|
||||
endValue = new Vector3(_fromScale, _fromScale, 1f),
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _duration,
|
||||
ease = _easeOut,
|
||||
useUnscaledTime = _useUnscaledTime
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
_isAnimating = false;
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
// Fully hidden
|
||||
if (_canvasGroup != null) _canvasGroup.alpha = 0f;
|
||||
if (_root != null) _root.SetActive(false);
|
||||
|
||||
_pending = default;
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (_isAnimating) return;
|
||||
|
||||
var info = _pending;
|
||||
Close();
|
||||
OnConfirmDelete?.Invoke(info);
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
if (_isAnimating) return;
|
||||
|
||||
Close();
|
||||
OnCancel?.Invoke();
|
||||
}
|
||||
|
||||
private void StopAnim()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
{
|
||||
_sequence.Stop();
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRefs()
|
||||
{
|
||||
// If you forgot to wire these, try to find sensible defaults.
|
||||
if (_canvasGroup == null && _root != null)
|
||||
_canvasGroup = _root.GetComponent<CanvasGroup>();
|
||||
|
||||
if (_panelTransform == null)
|
||||
_panelTransform = GetComponentInChildren<RectTransform>(true);
|
||||
|
||||
// Root fallback: if none specified, use this GO
|
||||
if (_root == null)
|
||||
_root = gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/ConfirmDeleteWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c66bd69629f472f91038ee13ec204b9
|
||||
timeCreated: 1769796211
|
||||
657
Assets/Scripts/UI/Menus/MainMenuWindow.cs
Normal file
657
Assets/Scripts/UI/Menus/MainMenuWindow.cs
Normal file
@@ -0,0 +1,657 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Main Menu flow:
|
||||
/// - Starts on intro screen
|
||||
/// - Intro light pulses
|
||||
/// - Intro text/title fade in after delay
|
||||
/// - Submit fades intro text/title out, pushes light to full alpha, then fades main menu in
|
||||
/// - Start Game => opens SelectSaveWindow
|
||||
/// - Settings => opens settings menu
|
||||
/// - Quit => quits app
|
||||
/// </summary>
|
||||
public class MainMenuWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Main Menu Window")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainMenuIntroScreenCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainMenuWindowCanvasGroup;
|
||||
|
||||
[Header("Intro Screen")]
|
||||
[SerializeField]
|
||||
private Image _introScreenLightImage;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTextCanvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _introTextText;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _introTitleCanvasGroup;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _startGameButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _settingsButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _quitButton;
|
||||
|
||||
[Header("Select Save Window")]
|
||||
[SerializeField]
|
||||
private SelectSaveWindow _selectSaveWindow;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _selectSaveWindowCanvasGroup;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _selectSaveTweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Intro Timing")]
|
||||
[SerializeField]
|
||||
private float _introLightPulseDuration = 2f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextDelaySeconds = 1.5f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextFadeInDuration = 0.8f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introTextPulseDuration = 1.4f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introSubmitTextFadeOutDuration = 0.25f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introSubmitLightToFullDuration = 0.75f;
|
||||
|
||||
[SerializeField]
|
||||
private float _introToMenuCrossfadeDuration = 0.6f;
|
||||
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private GameService _gameService;
|
||||
private InputManager _inputManager;
|
||||
|
||||
private CancellationTokenSource _introCts;
|
||||
private CancellationTokenSource _selectSaveCts;
|
||||
|
||||
private Sequence _introLightPulseSequence;
|
||||
private Sequence _introTextPulseSequence;
|
||||
private Sequence _introTransitionSequence;
|
||||
private Sequence _selectSaveSequence;
|
||||
|
||||
private bool _introFinished;
|
||||
private bool _introTransitioning;
|
||||
private DeviceInputType _lastDeviceInputType;
|
||||
|
||||
[Inject]
|
||||
public void Construct(GameService gameService, EventCoordinator eventCoordinator, InputManager inputManager)
|
||||
{
|
||||
_gameService = gameService;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ApplyInitialVisualState();
|
||||
|
||||
if (_selectSaveWindow != null)
|
||||
{
|
||||
_selectSaveWindow.OnCloseWindow += CloseSelectSaveWindow;
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
UpdateSubmitText(force: true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
BindButtons();
|
||||
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(false));
|
||||
_inputManager?.BindSubmitForStart(OnIntroSubmit);
|
||||
|
||||
StartIntroScreen().Forget();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnbindButtons();
|
||||
|
||||
_inputManager?.ResetSubmitBind(OnIntroSubmit);
|
||||
|
||||
StopIntroTweens();
|
||||
StopSelectSaveTween();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_selectSaveWindow != null)
|
||||
_selectSaveWindow.OnCloseWindow -= CloseSelectSaveWindow;
|
||||
|
||||
StopIntroTweens();
|
||||
StopSelectSaveTween();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
UpdateSubmitText();
|
||||
}
|
||||
|
||||
private void BindButtons()
|
||||
{
|
||||
if (_startGameButton != null)
|
||||
_startGameButton.onClick.AddListener(OnStartClicked);
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.AddListener(OnSettingsClicked);
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.AddListener(OnQuitClicked);
|
||||
}
|
||||
|
||||
private void UnbindButtons()
|
||||
{
|
||||
if (_startGameButton != null)
|
||||
_startGameButton.onClick.RemoveListener(OnStartClicked);
|
||||
|
||||
if (_settingsButton != null)
|
||||
_settingsButton.onClick.RemoveListener(OnSettingsClicked);
|
||||
|
||||
if (_quitButton != null)
|
||||
_quitButton.onClick.RemoveListener(OnQuitClicked);
|
||||
}
|
||||
|
||||
private void ApplyInitialVisualState()
|
||||
{
|
||||
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 1f, true);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
var color = _introScreenLightImage.color;
|
||||
color.a = 0f;
|
||||
_introScreenLightImage.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSubmitText(bool force = false)
|
||||
{
|
||||
if (_introTextText == null || _inputManager == null)
|
||||
return;
|
||||
|
||||
var currentDevice = _inputManager.DeviceInputType;
|
||||
if (!force && currentDevice == _lastDeviceInputType)
|
||||
return;
|
||||
|
||||
_lastDeviceInputType = currentDevice;
|
||||
|
||||
var isKeyboardMouse = currentDevice == DeviceInputType.KeyboardAndMouse;
|
||||
_introTextText.text = isKeyboardMouse
|
||||
? "Press Enter to begin."
|
||||
: "Press Start to begin.";
|
||||
}
|
||||
|
||||
private async UniTaskVoid StartIntroScreen()
|
||||
{
|
||||
_introFinished = false;
|
||||
_introTransitioning = false;
|
||||
|
||||
ResetIntroCtsAndCancelRunning();
|
||||
ApplyInitialVisualState();
|
||||
UpdateSubmitText(force: true);
|
||||
|
||||
try
|
||||
{
|
||||
StartIntroLightPulse(_introCts.Token);
|
||||
await StartIntroTextFlow(_introCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void StartIntroLightPulse(CancellationToken token)
|
||||
{
|
||||
if (_introScreenLightImage == null)
|
||||
return;
|
||||
|
||||
_introLightPulseSequence = Sequence.Create(
|
||||
useUnscaledTime: true,
|
||||
cycleMode: Sequence.SequenceCycleMode.Yoyo,
|
||||
cycles: -1)
|
||||
.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introScreenLightImage.color.a,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introLightPulseDuration,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
_introLightPulseSequence.ToUniTask(cancellationToken: token).Forget();
|
||||
}
|
||||
|
||||
private async UniTask StartIntroTextFlow(CancellationToken token)
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(_introTextDelaySeconds), cancellationToken: token);
|
||||
|
||||
var fadeInSequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_introTitleCanvasGroup != null)
|
||||
{
|
||||
fadeInSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextFadeInDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introTextCanvasGroup != null)
|
||||
{
|
||||
fadeInSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextFadeInDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await fadeInSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
if (_introTextCanvasGroup == null)
|
||||
return;
|
||||
|
||||
_introTextPulseSequence = Sequence.Create(
|
||||
useUnscaledTime: true,
|
||||
cycleMode: Sequence.SequenceCycleMode.Yoyo,
|
||||
cycles: -1)
|
||||
.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTextCanvasGroup.alpha,
|
||||
endValue = 0.1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introTextPulseDuration,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await _introTextPulseSequence.ToUniTask(cancellationToken: token);
|
||||
}
|
||||
|
||||
private void OnIntroSubmit(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
return;
|
||||
|
||||
if (!ctx.performed)
|
||||
return;
|
||||
|
||||
TransitionFromIntroToMainMenu().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid TransitionFromIntroToMainMenu()
|
||||
{
|
||||
if (_introFinished || _introTransitioning)
|
||||
return;
|
||||
|
||||
_introTransitioning = true;
|
||||
|
||||
ResetIntroCtsAndCancelRunning();
|
||||
|
||||
try
|
||||
{
|
||||
// Phase 1: fade intro title + text fully out together, while pushing light to full.
|
||||
var introElementsSequence = Sequence.Create(useUnscaledTime: true);
|
||||
|
||||
if (_introTitleCanvasGroup != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTitleCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitTextFadeOutDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introTextCanvasGroup != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introTextCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitTextFadeOutDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
introElementsSequence.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
|
||||
{
|
||||
startValue = _introScreenLightImage.color.a,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introSubmitLightToFullDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await introElementsSequence.ToUniTask(cancellationToken: _introCts.Token);
|
||||
|
||||
// Ensure intro text/title are fully gone before menu begins fading in.
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
|
||||
|
||||
// Phase 2: only after intro text/title have finished fading, crossfade to main menu.
|
||||
if (_mainMenuIntroScreenCanvasGroup != null && _mainMenuWindowCanvasGroup != null)
|
||||
{
|
||||
_introTransitionSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainMenuIntroScreenCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainMenuIntroScreenCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await _introTransitionSequence.ToUniTask(cancellationToken: _introCts.Token);
|
||||
}
|
||||
else if (_mainMenuWindowCanvasGroup != null)
|
||||
{
|
||||
var menuFade = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _introToMenuCrossfadeDuration,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
await menuFade.ToUniTask(cancellationToken: _introCts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_introTransitioning = false;
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_introTransitionSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 0f, false);
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
|
||||
if (_introScreenLightImage != null)
|
||||
{
|
||||
var color = _introScreenLightImage.color;
|
||||
color.a = 1f;
|
||||
_introScreenLightImage.color = color;
|
||||
}
|
||||
|
||||
_introFinished = true;
|
||||
_introTransitioning = false;
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] Starting game");
|
||||
ShowSelectSaveWindow().Forget();
|
||||
}
|
||||
|
||||
private void OnSettingsClicked()
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleSettingsWindow(true));
|
||||
}
|
||||
|
||||
private void OnQuitClicked()
|
||||
{
|
||||
_gameService?.QuitGame();
|
||||
}
|
||||
|
||||
private async UniTask ShowSelectSaveWindow()
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] Showing select save window");
|
||||
|
||||
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
|
||||
{
|
||||
Debug.Log("[MainMenuWindow] SelectSaveWindow references not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
ResetSelectSaveCtsAndCancelRunning();
|
||||
|
||||
_selectSaveWindow.gameObject.SetActive(true);
|
||||
_selectSaveWindowCanvasGroup.alpha = 0f;
|
||||
_selectSaveWindow.transform.localScale = Vector3.zero;
|
||||
_selectSaveWindow.Refresh();
|
||||
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, _mainMenuWindowCanvasGroup != null ? _mainMenuWindowCanvasGroup.alpha : 0f, false);
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
|
||||
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _selectSaveWindowCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _selectSaveWindow.transform.localScale,
|
||||
endValue = Vector3.one,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 1f, true);
|
||||
}
|
||||
|
||||
private void CloseSelectSaveWindow()
|
||||
{
|
||||
CloseSelectSaveWindowInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask CloseSelectSaveWindowInternal()
|
||||
{
|
||||
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
|
||||
return;
|
||||
|
||||
ResetSelectSaveCtsAndCancelRunning();
|
||||
SetCanvasGroupState(_selectSaveWindowCanvasGroup, _selectSaveWindowCanvasGroup.alpha, false);
|
||||
|
||||
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _selectSaveWindowCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _selectSaveTweenSettings
|
||||
}))
|
||||
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _selectSaveWindow.transform.localScale,
|
||||
endValue = Vector3.zero,
|
||||
settings = _selectSaveTweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_selectSaveSequence = default;
|
||||
}
|
||||
|
||||
_selectSaveWindowCanvasGroup.alpha = 0f;
|
||||
_selectSaveWindow.gameObject.SetActive(false);
|
||||
|
||||
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
|
||||
}
|
||||
|
||||
private void ResetIntroCtsAndCancelRunning()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
|
||||
CancelAndDispose(ref _introCts);
|
||||
_introCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopIntroTweens()
|
||||
{
|
||||
StopSequence(ref _introLightPulseSequence);
|
||||
StopSequence(ref _introTextPulseSequence);
|
||||
StopSequence(ref _introTransitionSequence);
|
||||
CancelAndDispose(ref _introCts);
|
||||
}
|
||||
|
||||
private void ResetSelectSaveCtsAndCancelRunning()
|
||||
{
|
||||
StopSequence(ref _selectSaveSequence);
|
||||
CancelAndDispose(ref _selectSaveCts);
|
||||
_selectSaveCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopSelectSaveTween()
|
||||
{
|
||||
StopSequence(ref _selectSaveSequence);
|
||||
CancelAndDispose(ref _selectSaveCts);
|
||||
}
|
||||
|
||||
private static void StopSequence(ref Sequence sequence)
|
||||
{
|
||||
if (sequence.isAlive)
|
||||
sequence.Stop();
|
||||
|
||||
sequence = default;
|
||||
}
|
||||
|
||||
private static void CancelAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
|
||||
private static void SetCanvasGroupState(CanvasGroup group, float alpha, bool inputEnabled)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
group.alpha = alpha;
|
||||
group.interactable = inputEnabled;
|
||||
group.blocksRaycasts = inputEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/MainMenuWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/MainMenuWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2251b1b163bc42aeaeaeba8a4442363c
|
||||
timeCreated: 1769784245
|
||||
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// New Save modal window:
|
||||
/// - Opens over SelectSaveWindow
|
||||
/// - User enters a save name
|
||||
/// - Create -> SaveManager.CreateNewSaveGame(name) (and typically sets CurrentSave)
|
||||
/// - Then immediately LoadGameData(name) (optional but robust), and raises OnSaveCreated
|
||||
/// </summary>
|
||||
public class NewSaveWindow : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Input")]
|
||||
[SerializeField]
|
||||
private TMP_InputField _nameInput;
|
||||
|
||||
[SerializeField]
|
||||
private Button _createButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _cancelButton;
|
||||
|
||||
[Header("Error UI")]
|
||||
[SerializeField]
|
||||
private GameObject _errorBox;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _errorText;
|
||||
|
||||
[Header("Validation")]
|
||||
[SerializeField]
|
||||
private int _minNameLength = 1;
|
||||
|
||||
[SerializeField]
|
||||
private int _maxNameLength = 24;
|
||||
|
||||
[SerializeField]
|
||||
private bool _trimWhitespace = true;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _isBusy;
|
||||
|
||||
private bool _isOpen;
|
||||
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private Sequence _seq;
|
||||
|
||||
private bool _tutorialsEnabled;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_createButton != null) _createButton.onClick.AddListener(OnCreateClicked);
|
||||
if (_cancelButton != null) _cancelButton.onClick.AddListener(Close);
|
||||
|
||||
HideError();
|
||||
CloseImmediate();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_createButton != null) _createButton.onClick.RemoveListener(OnCreateClicked);
|
||||
if (_cancelButton != null) _cancelButton.onClick.RemoveListener(Close);
|
||||
|
||||
StopTween();
|
||||
}
|
||||
|
||||
public event Action OnCloseWindow;
|
||||
public event Action<string> OnSaveCreated;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_isOpen)
|
||||
return;
|
||||
|
||||
Debug.Log($"Opening {nameof(NewSaveWindow)}");
|
||||
OpenInternal().Forget();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!_isOpen || _isBusy) return;
|
||||
CloseInternal().Forget();
|
||||
}
|
||||
|
||||
public void CloseImmediate()
|
||||
{
|
||||
_isOpen = false;
|
||||
_isBusy = false;
|
||||
|
||||
if (_nameInput != null) _nameInput.text = string.Empty;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
HideError();
|
||||
}
|
||||
|
||||
private async UniTask OpenInternal()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
_isOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("Opening Internal...");
|
||||
Debug.Log($"{gameObject} is {gameObject.activeSelf}");
|
||||
|
||||
ResetCtsAndCancelRunning();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
Debug.Log($"{gameObject} is now {gameObject.activeSelf}");
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
|
||||
_isOpen = true;
|
||||
_isBusy = true;
|
||||
|
||||
if (_nameInput != null) _nameInput.text = string.Empty;
|
||||
HideError();
|
||||
|
||||
Debug.Log("Opening - Creating Sequence");
|
||||
|
||||
_seq = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log("Opening - Sequence Running.");
|
||||
await _seq.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log($"Opening - Sequence Error: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Debug.Log("Opening - Sequence over.");
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
|
||||
_canvasGroup.alpha = 1f;
|
||||
_canvasGroup.interactable = true;
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
|
||||
_isBusy = false;
|
||||
FocusInput();
|
||||
}
|
||||
|
||||
private async UniTask CloseInternal()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
CloseImmediate();
|
||||
OnCloseWindow?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
ResetCtsAndCancelRunning();
|
||||
|
||||
_isBusy = true;
|
||||
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
|
||||
_seq = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _seq.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_isOpen = false;
|
||||
_isBusy = false;
|
||||
|
||||
gameObject.SetActive(false);
|
||||
OnCloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
private void FocusInput()
|
||||
{
|
||||
if (_nameInput == null) return;
|
||||
|
||||
_nameInput.ActivateInputField();
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(_nameInput.gameObject);
|
||||
}
|
||||
|
||||
|
||||
private void OnCreateClicked()
|
||||
{
|
||||
if (_isBusy) return;
|
||||
CreateSave().Forget();
|
||||
}
|
||||
|
||||
private async UniTask CreateSave()
|
||||
{
|
||||
HideError();
|
||||
|
||||
if (_saveManager == null)
|
||||
{
|
||||
ShowError("Save system not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var raw = _nameInput != null ? _nameInput.text : string.Empty;
|
||||
var name = _trimWhitespace ? (raw ?? string.Empty).Trim() : raw ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ShowError("Please enter a save name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.Length < _minNameLength)
|
||||
{
|
||||
ShowError($"Save name must be at least {_minNameLength} character(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_maxNameLength > 0 && name.Length > _maxNameLength)
|
||||
{
|
||||
ShowError($"Save name must be {_maxNameLength} characters or fewer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContainsIllegalFileNameChars(name, out var illegalChars))
|
||||
{
|
||||
ShowError(illegalChars.Length == 1
|
||||
? $"That name contains an illegal character: '{illegalChars[0]}'."
|
||||
: $"That name contains illegal characters: {string.Join(" ", illegalChars.Select(c => $"'{c}'"))}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsWindowsReservedFileName(name))
|
||||
{
|
||||
ShowError("That name is reserved by the operating system. Please choose a different name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_saveManager.DoesSaveExist(name))
|
||||
{
|
||||
ShowError("A save with that name already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
_isBusy = true;
|
||||
SetButtonsInteractable(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _saveManager.CreateNewSaveGame(name);
|
||||
|
||||
// Tell SelectSaveWindow to start game.
|
||||
OnSaveCreated?.Invoke(name);
|
||||
|
||||
// Close ourselves immediately (caller will close SelectSaveWindow)
|
||||
CloseImmediate();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ShowError("Failed to create save. Please try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBusy = false;
|
||||
SetButtonsInteractable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetButtonsInteractable(bool interactable)
|
||||
{
|
||||
if (_createButton != null) _createButton.interactable = interactable;
|
||||
if (_cancelButton != null) _cancelButton.interactable = interactable;
|
||||
if (_nameInput != null) _nameInput.interactable = interactable;
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
if (_errorText != null) _errorText.text = message;
|
||||
if (_errorBox != null) _errorBox.SetActive(true);
|
||||
}
|
||||
|
||||
private void HideError()
|
||||
{
|
||||
if (_errorBox != null) _errorBox.SetActive(false);
|
||||
if (_errorText != null) _errorText.text = string.Empty;
|
||||
}
|
||||
|
||||
private static bool ContainsIllegalFileNameChars(string name, out char[] illegalChars)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
illegalChars = name.Where(c => invalid.Contains(c)).Distinct().ToArray();
|
||||
return illegalChars.Length > 0;
|
||||
}
|
||||
|
||||
private static bool IsWindowsReservedFileName(string name)
|
||||
{
|
||||
var trimmed = (name ?? string.Empty).Trim().TrimEnd('.', ' ');
|
||||
if (string.IsNullOrEmpty(trimmed)) return true;
|
||||
|
||||
var upper = trimmed.ToUpperInvariant();
|
||||
|
||||
if (upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL") return true;
|
||||
|
||||
if (upper.Length == 4)
|
||||
{
|
||||
if (upper.StartsWith("COM") && char.IsDigit(upper[3]) && upper[3] != '0') return true;
|
||||
if (upper.StartsWith("LPT") && char.IsDigit(upper[3]) && upper[3] != '0') return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResetCtsAndCancelRunning()
|
||||
{
|
||||
if (_seq.isAlive)
|
||||
{
|
||||
_seq.Stop();
|
||||
_seq = default;
|
||||
}
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
{
|
||||
if (_seq.isAlive) _seq.Stop();
|
||||
_seq = default;
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/NewSaveWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/NewSaveWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98d490ab66c54c3d8bafdfeae4945734
|
||||
timeCreated: 1770232259
|
||||
308
Assets/Scripts/UI/Menus/PauseMenuWindow.cs
Normal file
308
Assets/Scripts/UI/Menus/PauseMenuWindow.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
public class PauseMenuWindow : MonoBehaviour, IUIWindow
|
||||
{
|
||||
[Header("Root UI")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _windowRect;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _resumeButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _saveButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _settingsButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _exitButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _quitToDesktopButton;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.OutQuad,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Scale")]
|
||||
[SerializeField]
|
||||
private float _hiddenScale = 0.85f;
|
||||
|
||||
[Header("Internal")]
|
||||
[SerializeField]
|
||||
private GraphicRaycaster _graphicRaycaster;
|
||||
|
||||
private AudioManager _audioManager;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private GameService _gameService;
|
||||
private InteractManager _interactManager;
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
public bool IsModal => true;
|
||||
|
||||
public WindowType WindowType => WindowType.PauseMenuWindow;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Start hidden by default
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
if (_windowRect != null)
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_resumeButton != null) _resumeButton.onClick.AddListener(OnResumeButtonClick);
|
||||
if (_saveButton != null) _saveButton.onClick.AddListener(OnSaveButtonClick);
|
||||
if (_settingsButton != null) _settingsButton.onClick.AddListener(OnSettingsButtonClick);
|
||||
if (_exitButton != null) _exitButton.onClick.AddListener(OnExitButtonClick);
|
||||
if (_quitToDesktopButton != null) _quitToDesktopButton.onClick.AddListener(OnQuitToDesktopButtonClick);
|
||||
|
||||
_interactManager.AddUIRaycaster(_graphicRaycaster);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_resumeButton != null) _resumeButton.onClick.RemoveListener(OnResumeButtonClick);
|
||||
if (_saveButton != null) _saveButton.onClick.RemoveListener(OnSaveButtonClick);
|
||||
if (_settingsButton != null) _settingsButton.onClick.RemoveListener(OnSettingsButtonClick);
|
||||
if (_exitButton != null) _exitButton.onClick.RemoveListener(OnExitButtonClick);
|
||||
if (_quitToDesktopButton != null) _quitToDesktopButton.onClick.RemoveListener(OnQuitToDesktopButtonClick);
|
||||
|
||||
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopAndResetCancellation();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_canvasGroup == null || _windowRect == null)
|
||||
{
|
||||
Debug.LogError("[PauseMenuWindow] Missing CanvasGroup or WindowRect reference.");
|
||||
gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
StopAndResetCancellation();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
|
||||
var alpha = new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var scale = new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = Vector3.one * _hiddenScale,
|
||||
endValue = Vector3.one,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, alpha))
|
||||
.Group(Tween.Scale(_windowRect, scale));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 1f;
|
||||
_windowRect.localScale = Vector3.one;
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_canvasGroup == null || _windowRect == null)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StopAndResetCancellation();
|
||||
|
||||
// Block clicks immediately
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
var alpha = new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var scale = new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _windowRect.localScale,
|
||||
endValue = Vector3.one * _hiddenScale,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, alpha))
|
||||
.Group(Tween.Scale(_windowRect, scale));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sequence = default;
|
||||
}
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator, SaveManager saveManager, GameService gameService,
|
||||
InteractManager interactManager, AudioManager audioManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_saveManager = saveManager;
|
||||
_gameService = gameService;
|
||||
_interactManager = interactManager;
|
||||
_audioManager = audioManager;
|
||||
}
|
||||
|
||||
private void StopAndResetCancellation()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void OnResumeButtonClick()
|
||||
{
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
private void OnSaveButtonClick()
|
||||
{
|
||||
SaveGame().Forget();
|
||||
}
|
||||
|
||||
private void OnSettingsButtonClick()
|
||||
{
|
||||
_eventCoordinator.Publish(new UIToggleSettingsWindow(true));
|
||||
}
|
||||
|
||||
private void OnExitButtonClick()
|
||||
{
|
||||
_eventCoordinator.Publish(new FadeEvent(false, 1f));
|
||||
ExitButtonInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask ExitButtonInternal()
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(1));
|
||||
_audioManager.StopAllAudio();
|
||||
await SaveGame();
|
||||
_eventCoordinator.Publish(new PauseButtonClickedEvent());
|
||||
await _gameService.LoadMainMenu();
|
||||
}
|
||||
|
||||
private void OnQuitToDesktopButtonClick()
|
||||
{
|
||||
QuitButtonInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask QuitButtonInternal()
|
||||
{
|
||||
_eventCoordinator.Publish(new FadeEvent(false, 1f));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(1));
|
||||
await SaveGame();
|
||||
_gameService.QuitGame();
|
||||
}
|
||||
|
||||
private async UniTask SaveGame()
|
||||
{
|
||||
if (_saveManager == null) return;
|
||||
await _saveManager.SaveGameDataLatest();
|
||||
// TODO: Saved feedback popup/toast
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/PauseMenuWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/PauseMenuWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19b9bc67046d4238ac544f6fe36a6066
|
||||
timeCreated: 1769707615
|
||||
355
Assets/Scripts/UI/Menus/SelectSaveWindow.cs
Normal file
355
Assets/Scripts/UI/Menus/SelectSaveWindow.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// Select Save Window:
|
||||
/// - Shows EXACTLY 3 slots (filled first, then empty).
|
||||
/// - Clicking a slot:
|
||||
/// - Filled slot => loads that save, then StartGame
|
||||
/// - Empty slot => opens NewSaveWindow (create + load), then StartGame
|
||||
/// - Delete is still supported (per-slot delete button + confirm modal).
|
||||
/// - Back closes this window (handled by MainMenuWindow).
|
||||
/// </summary>
|
||||
public class SelectSaveWindow : MonoBehaviour
|
||||
{
|
||||
private const int MAX_SLOTS = 3;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private RectTransform _listContentParent;
|
||||
|
||||
[SerializeField]
|
||||
private SaveSlotUI _saveSlotPrefab;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _backButton;
|
||||
|
||||
[Header("New Save Window")]
|
||||
[SerializeField]
|
||||
private NewSaveWindow _newSaveWindow;
|
||||
|
||||
[Header("Confirm Delete Window (optional but recommended)")]
|
||||
[SerializeField]
|
||||
private ConfirmDeleteWindow _confirmDeleteWindow;
|
||||
|
||||
private readonly List<SaveSlotUI> _instantiatedSlots = new();
|
||||
|
||||
private int _currentSelectionIndex;
|
||||
private GameService _gameService;
|
||||
private bool _isBusy;
|
||||
|
||||
private SaveManager _saveManager;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow += HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated += HandleSaveCreatedAndStartGame;
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete += HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel += HandleCancelDelete;
|
||||
_confirmDeleteWindow.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
{
|
||||
_newSaveWindow.OnCloseWindow -= HandleNewSaveClosed;
|
||||
_newSaveWindow.OnSaveCreated -= HandleSaveCreatedAndStartGame;
|
||||
}
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
{
|
||||
_confirmDeleteWindow.OnConfirmDelete -= HandleConfirmDelete;
|
||||
_confirmDeleteWindow.OnCancel -= HandleCancelDelete;
|
||||
}
|
||||
|
||||
ClearSlots();
|
||||
}
|
||||
|
||||
public event Action OnCloseWindow;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager, GameService gameService)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
_gameService = gameService;
|
||||
}
|
||||
|
||||
/// <summary>Called by MainMenuWindow after enabling this GO.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (_newSaveWindow != null)
|
||||
_newSaveWindow.CloseImmediate();
|
||||
|
||||
if (_confirmDeleteWindow != null)
|
||||
_confirmDeleteWindow.Close();
|
||||
|
||||
EnsureThreeSlotsExist();
|
||||
RefreshSlotsData();
|
||||
SetBusy(false);
|
||||
}
|
||||
|
||||
private void OnBackClicked()
|
||||
{
|
||||
if (_isBusy)
|
||||
return;
|
||||
|
||||
// Check if Confirm Delete or New Save Window is open.
|
||||
|
||||
OnCloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleNewSaveClosed()
|
||||
{
|
||||
// NewSaveWindow was closed (cancel/back) => return control to the list.
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
RefreshSlotsData();
|
||||
}
|
||||
|
||||
private void HandleSaveCreatedAndStartGame(string _)
|
||||
{
|
||||
// NewSaveWindow already created + loaded the save.
|
||||
OnCloseWindow?.Invoke();
|
||||
_gameService?.StartGame().Forget();
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
Debug.Log("[SelectSaveWindow] SetBusy: " + busy);
|
||||
_isBusy = busy;
|
||||
|
||||
if (_backButton != null)
|
||||
_backButton.interactable = !busy;
|
||||
|
||||
foreach (var slot in _instantiatedSlots)
|
||||
if (slot != null)
|
||||
slot.SetInteractable(!busy);
|
||||
|
||||
Debug.Log($"[SelectSaveWindow] Finished set busy: {busy}");
|
||||
}
|
||||
|
||||
private void EnsureThreeSlotsExist()
|
||||
{
|
||||
if (_listContentParent == null || _saveSlotPrefab == null)
|
||||
return;
|
||||
|
||||
if (_instantiatedSlots.Count == MAX_SLOTS)
|
||||
return;
|
||||
|
||||
ClearSlots();
|
||||
|
||||
for (var i = 0; i < MAX_SLOTS; i++)
|
||||
{
|
||||
var slot = Instantiate(_saveSlotPrefab, _listContentParent);
|
||||
_instantiatedSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshSlotsData()
|
||||
{
|
||||
// Always show 3 slots; if save system is missing, they’ll all appear empty/disabled.
|
||||
if (_saveManager == null)
|
||||
{
|
||||
for (var i = 0; i < _instantiatedSlots.Count; i++)
|
||||
_instantiatedSlots[i]?.SetEmpty(OnEmptySlotClicked);
|
||||
|
||||
SelectBackButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Newest first, cap at 3
|
||||
var saveFiles = _saveManager.GetAvailableSaves();
|
||||
var infos = (saveFiles ?? new List<(string, DateTime)>())
|
||||
.Select(sf => new SaveFileInfo(sf.FileName, sf.LastModified))
|
||||
.OrderByDescending(i => i.LastModified)
|
||||
.Take(MAX_SLOTS)
|
||||
.ToArray();
|
||||
|
||||
for (var i = 0; i < MAX_SLOTS; i++)
|
||||
{
|
||||
var slot = _instantiatedSlots.ElementAtOrDefault(i);
|
||||
if (slot == null) continue;
|
||||
|
||||
if (i < infos.Length)
|
||||
slot.SetFilled(infos[i], OnFilledSlotClicked, OnSlotDeleteClicked);
|
||||
else
|
||||
slot.SetEmpty(OnEmptySlotClicked);
|
||||
}
|
||||
|
||||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||||
SelectSlot(_currentSelectionIndex);
|
||||
}
|
||||
|
||||
private void ClearSlots()
|
||||
{
|
||||
foreach (var slot in _instantiatedSlots)
|
||||
if (slot != null)
|
||||
Destroy(slot.gameObject);
|
||||
|
||||
_instantiatedSlots.Clear();
|
||||
_currentSelectionIndex = 0;
|
||||
}
|
||||
|
||||
private void OnFilledSlotClicked(SaveFileInfo saveInfo)
|
||||
{
|
||||
if (_isBusy) return;
|
||||
if (_saveManager == null) return;
|
||||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||||
|
||||
if (!_saveManager.DoesSaveExist(saveInfo.FileName))
|
||||
{
|
||||
RefreshSlotsData();
|
||||
return;
|
||||
}
|
||||
|
||||
LoadAndStartGame(saveInfo.FileName).Forget();
|
||||
}
|
||||
|
||||
private void OnEmptySlotClicked()
|
||||
{
|
||||
Debug.Log("[SelectSaveWindow] Empty slot clicked.");
|
||||
if (_isBusy)
|
||||
return;
|
||||
|
||||
if (_newSaveWindow == null)
|
||||
{
|
||||
Debug.LogWarning("[SelectSaveWindow] NewSaveWindow reference not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_newSaveWindow.Open();
|
||||
}
|
||||
|
||||
private async UniTask LoadAndStartGame(string profileName)
|
||||
{
|
||||
SetBusy(true);
|
||||
|
||||
try
|
||||
{
|
||||
await _saveManager.LoadGameData(profileName);
|
||||
OnCloseWindow?.Invoke();
|
||||
_gameService?.StartGame().Forget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SelectSaveWindow] Failed to load profile '{profileName}': {ex}");
|
||||
SetBusy(false);
|
||||
RefreshSlotsData();
|
||||
RestoreSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotDeleteClicked(SaveFileInfo saveInfo)
|
||||
{
|
||||
if (_isBusy) return;
|
||||
if (_saveManager == null) return;
|
||||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||||
|
||||
// No confirm window wired? Do a direct delete.
|
||||
if (_confirmDeleteWindow == null)
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
return;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
_confirmDeleteWindow.Open(saveInfo);
|
||||
}
|
||||
|
||||
private void HandleConfirmDelete(SaveFileInfo saveInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
TryDeleteAndRefresh(saveInfo.FileName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCancelDelete()
|
||||
{
|
||||
SetBusy(false);
|
||||
RestoreSelection();
|
||||
}
|
||||
|
||||
private void TryDeleteAndRefresh(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return;
|
||||
|
||||
if (!_saveManager.DoesSaveExist(fileName))
|
||||
{
|
||||
RefreshSlotsData();
|
||||
return;
|
||||
}
|
||||
|
||||
var deleted = _saveManager.Delete(fileName);
|
||||
if (!deleted)
|
||||
Debug.LogWarning($"[SelectSaveWindow] Failed to delete save '{fileName}'.");
|
||||
|
||||
RefreshSlotsData();
|
||||
}
|
||||
|
||||
private void RestoreSelection()
|
||||
{
|
||||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||||
SelectSlot(_currentSelectionIndex);
|
||||
}
|
||||
|
||||
private void SelectBackButton()
|
||||
{
|
||||
if (_backButton == null) return;
|
||||
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(_backButton.gameObject);
|
||||
else
|
||||
_backButton.Select();
|
||||
}
|
||||
|
||||
private void SelectSlot(int index)
|
||||
{
|
||||
if (_instantiatedSlots.Count == 0)
|
||||
{
|
||||
SelectBackButton();
|
||||
return;
|
||||
}
|
||||
|
||||
index = Mathf.Clamp(index, 0, _instantiatedSlots.Count - 1);
|
||||
_currentSelectionIndex = index;
|
||||
|
||||
var go = _instantiatedSlots[index]?.GetSelectableGameObject();
|
||||
if (go == null) return;
|
||||
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/SelectSaveWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/SelectSaveWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06df6456fa0544518f3c8d3a6802fc5c
|
||||
timeCreated: 1770231671
|
||||
726
Assets/Scripts/UI/Menus/SettingsWindow.cs
Normal file
726
Assets/Scripts/UI/Menus/SettingsWindow.cs
Normal file
@@ -0,0 +1,726 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using BriarQueen.Framework.Services.Settings.Data;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
using AudioSettings = BriarQueen.Framework.Services.Settings.Data.AudioSettings;
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
public class SettingsWindow : MonoBehaviour, IUIWindow
|
||||
{
|
||||
[Header("UI Elements")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _windowRect;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField]
|
||||
private Button _applyButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button _backButton;
|
||||
|
||||
[Header("Game")]
|
||||
[SerializeField]
|
||||
private Slider _popupDisplayDurationSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _popupDisplayDurationText;
|
||||
|
||||
[SerializeField]
|
||||
private Toggle _tutorialsEnabledToggle;
|
||||
|
||||
[SerializeField]
|
||||
private Toggle _tooltipsEnabledToggle;
|
||||
|
||||
[SerializeField]
|
||||
private Toggle _autoUseToolsToggle;
|
||||
|
||||
[Header("Visual")]
|
||||
[SerializeField]
|
||||
private Toggle _fullscreenToggle;
|
||||
|
||||
[Tooltip("0 = VSync Off, 1 = Every V-Blank, 2 = Every 2nd V-Blank")]
|
||||
[SerializeField]
|
||||
private Slider _vsyncSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _vsyncValueText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _maxFramerateSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _maxFramerateValueText;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField]
|
||||
private Slider _masterVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _masterVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _musicVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _musicVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _sfxVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _sfxVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _voiceVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _voiceVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _ambienceVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _ambienceVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Slider _uiVolumeSlider;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _uiVolumeText;
|
||||
|
||||
[SerializeField]
|
||||
private Toggle _muteWhenUnfocusedToggle;
|
||||
|
||||
[Header("Unsaved Changes Warning")]
|
||||
[SerializeField]
|
||||
private TMP_Text _pendingChangesText;
|
||||
|
||||
[SerializeField]
|
||||
private string _pendingChangesMessage = "You have unsaved changes.";
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.25f,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[Header("Scale")]
|
||||
[SerializeField]
|
||||
private float _hiddenScale = 0.85f;
|
||||
|
||||
[Header("Selection")]
|
||||
[SerializeField]
|
||||
private Selectable _firstSelectedOnOpen;
|
||||
|
||||
[Header("Game Slider Ranges")]
|
||||
[SerializeField]
|
||||
private float _popupDisplayDurationMin = 1f;
|
||||
|
||||
[SerializeField]
|
||||
private float _popupDisplayDurationMax = 10f;
|
||||
|
||||
[Header("Visual Slider Ranges")]
|
||||
[SerializeField]
|
||||
private int _maxFramerateMin = 30;
|
||||
|
||||
[SerializeField]
|
||||
private int _maxFramerateMax = 240;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
private AudioSettings _draftAudio;
|
||||
private GameSettings _draftGameSettings;
|
||||
private VisualSettings _draftVisual;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
|
||||
private bool _ignoreUiCallbacks;
|
||||
|
||||
private AudioSettings _loadedAudio;
|
||||
private GameSettings _loadedGameSettings;
|
||||
private VisualSettings _loadedVisual;
|
||||
|
||||
private Sequence _sequence;
|
||||
|
||||
private SettingsService _settingsService;
|
||||
private bool _waitingChanges;
|
||||
|
||||
public bool IsModal => true;
|
||||
public WindowType WindowType => WindowType.SettingsWindow;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_applyButton != null) _applyButton.onClick.AddListener(OnApplyClicked);
|
||||
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
|
||||
|
||||
HookSlider(_masterVolumeSlider, _masterVolumeText, v => _draftAudio.MasterVolume = v);
|
||||
HookSlider(_musicVolumeSlider, _musicVolumeText, v => _draftAudio.MusicVolume = v);
|
||||
HookSlider(_sfxVolumeSlider, _sfxVolumeText, v => _draftAudio.SfxVolume = v);
|
||||
HookSlider(_voiceVolumeSlider, _voiceVolumeText, v => _draftAudio.VoiceVolume = v);
|
||||
HookSlider(_ambienceVolumeSlider, _ambienceVolumeText, v => _draftAudio.AmbienceVolume = v);
|
||||
HookSlider(_uiVolumeSlider, _uiVolumeText, v => _draftAudio.UIVolume = v);
|
||||
|
||||
SetupGameSliders();
|
||||
|
||||
if (_popupDisplayDurationSlider != null)
|
||||
_popupDisplayDurationSlider.onValueChanged.AddListener(OnPopupDisplayDurationChanged);
|
||||
|
||||
if (_tutorialsEnabledToggle != null)
|
||||
_tutorialsEnabledToggle.onValueChanged.AddListener(OnTutorialsToggleChanged);
|
||||
|
||||
if (_tooltipsEnabledToggle != null)
|
||||
_tooltipsEnabledToggle.onValueChanged.AddListener(OnTooltipsToggleChanged);
|
||||
|
||||
if(_autoUseToolsToggle != null)
|
||||
_autoUseToolsToggle.onValueChanged.AddListener(OnAutoToolsToggleChanged);
|
||||
|
||||
if (_muteWhenUnfocusedToggle != null)
|
||||
_muteWhenUnfocusedToggle.onValueChanged.AddListener(OnMuteWhenUnfocusedChanged);
|
||||
|
||||
if (_fullscreenToggle != null)
|
||||
_fullscreenToggle.onValueChanged.AddListener(OnFullscreenToggleChanged);
|
||||
|
||||
SetupVsyncSlider();
|
||||
|
||||
if (_vsyncSlider != null)
|
||||
_vsyncSlider.onValueChanged.AddListener(OnVsyncSliderChanged);
|
||||
|
||||
SetupMaxFramerateSlider();
|
||||
|
||||
if (_maxFramerateSlider != null)
|
||||
_maxFramerateSlider.onValueChanged.AddListener(OnMaxFramerateSliderChanged);
|
||||
|
||||
SetPendingChangesVisible(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopAndResetCancellation();
|
||||
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
if (_applyButton != null) _applyButton.onClick.RemoveListener(OnApplyClicked);
|
||||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||||
|
||||
if (_popupDisplayDurationSlider != null)
|
||||
_popupDisplayDurationSlider.onValueChanged.RemoveListener(OnPopupDisplayDurationChanged);
|
||||
|
||||
if (_tutorialsEnabledToggle != null)
|
||||
_tutorialsEnabledToggle.onValueChanged.RemoveListener(OnTutorialsToggleChanged);
|
||||
|
||||
if (_tooltipsEnabledToggle != null)
|
||||
_tooltipsEnabledToggle.onValueChanged.RemoveListener(OnTooltipsToggleChanged);
|
||||
|
||||
if (_muteWhenUnfocusedToggle != null)
|
||||
_muteWhenUnfocusedToggle.onValueChanged.RemoveListener(OnMuteWhenUnfocusedChanged);
|
||||
|
||||
if (_fullscreenToggle != null)
|
||||
_fullscreenToggle.onValueChanged.RemoveListener(OnFullscreenToggleChanged);
|
||||
|
||||
if (_vsyncSlider != null)
|
||||
_vsyncSlider.onValueChanged.RemoveListener(OnVsyncSliderChanged);
|
||||
|
||||
if (_maxFramerateSlider != null)
|
||||
_maxFramerateSlider.onValueChanged.RemoveListener(OnMaxFramerateSliderChanged);
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_settingsService != null)
|
||||
LoadSettings(_settingsService.Audio, _settingsService.Visual, _settingsService.Game);
|
||||
|
||||
StopAndResetCancellation();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
_windowRect.localScale = Vector3.one * _hiddenScale;
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
var alpha = new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var scale = new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = Vector3.one * _hiddenScale,
|
||||
endValue = Vector3.one,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, alpha))
|
||||
.Group(Tween.Scale(_windowRect, scale));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
|
||||
SelectDefault();
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
StopAndResetCancellation();
|
||||
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
var alpha = new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var scale = new TweenSettings<Vector3>
|
||||
{
|
||||
startValue = _windowRect.localScale,
|
||||
endValue = Vector3.one * _hiddenScale,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, alpha))
|
||||
.Group(Tween.Scale(_windowRect, scale));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(SettingsService settingsService, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
private void OnTutorialsToggleChanged(bool value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_draftGameSettings.TutorialsEnabled = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnTooltipsToggleChanged(bool value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_draftGameSettings.TooltipsEnabled = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnAutoToolsToggleChanged(bool value)
|
||||
{
|
||||
if (_ignoreUiCallbacks)
|
||||
return;
|
||||
|
||||
_draftGameSettings.AutoUseTools = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnPopupDisplayDurationChanged(float value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_draftGameSettings.PopupDisplayDuration =
|
||||
Mathf.Clamp(value, _popupDisplayDurationMin, _popupDisplayDurationMax);
|
||||
|
||||
UpdateSecondsLabel(_popupDisplayDurationText, _draftGameSettings.PopupDisplayDuration);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnMuteWhenUnfocusedChanged(bool value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_draftAudio.MuteWhenUnfocused = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnFullscreenToggleChanged(bool isFullscreen)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_draftVisual.FullScreenMode = isFullscreen
|
||||
? FullScreenMode.FullScreenWindow
|
||||
: FullScreenMode.Windowed;
|
||||
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnVsyncSliderChanged(float value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
var v = Mathf.Clamp(Mathf.RoundToInt(value), 0, 2);
|
||||
_draftVisual.VSyncCount = v;
|
||||
|
||||
UpdateVsyncLabel(v);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnMaxFramerateSliderChanged(float value)
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
var fps = Mathf.Clamp(Mathf.RoundToInt(value), _maxFramerateMin, _maxFramerateMax);
|
||||
_draftVisual.MaxFramerate = fps;
|
||||
|
||||
UpdateMaxFramerateLabel(fps);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnApplyClicked()
|
||||
{
|
||||
_loadedAudio = CloneAudio(_draftAudio);
|
||||
_loadedVisual = CloneVisual(_draftVisual);
|
||||
_loadedGameSettings = CloneGameSettings(_draftGameSettings);
|
||||
|
||||
_settingsService.Apply(_loadedAudio, _loadedVisual, _loadedGameSettings);
|
||||
|
||||
_waitingChanges = false;
|
||||
SetPendingChangesVisible(false);
|
||||
}
|
||||
|
||||
private void OnBackClicked()
|
||||
{
|
||||
if (HasUnappliedChanges())
|
||||
{
|
||||
_draftAudio = CloneAudio(_loadedAudio);
|
||||
_draftVisual = CloneVisual(_loadedVisual);
|
||||
_draftGameSettings = CloneGameSettings(_loadedGameSettings);
|
||||
|
||||
ApplySettingsToUI();
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new UIToggleSettingsWindow(false));
|
||||
}
|
||||
|
||||
private void StopAndResetCancellation()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
if (_cts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void SelectDefault()
|
||||
{
|
||||
if (EventSystem.current == null) return;
|
||||
|
||||
if (_firstSelectedOnOpen != null)
|
||||
EventSystem.current.SetSelectedGameObject(_firstSelectedOnOpen.gameObject);
|
||||
else if (_applyButton != null)
|
||||
EventSystem.current.SetSelectedGameObject(_applyButton.gameObject);
|
||||
}
|
||||
|
||||
private void SetupGameSliders()
|
||||
{
|
||||
if (_popupDisplayDurationSlider == null) return;
|
||||
|
||||
_popupDisplayDurationSlider.minValue = _popupDisplayDurationMin;
|
||||
_popupDisplayDurationSlider.maxValue = _popupDisplayDurationMax;
|
||||
_popupDisplayDurationSlider.wholeNumbers = false;
|
||||
}
|
||||
|
||||
private void SetupVsyncSlider()
|
||||
{
|
||||
if (_vsyncSlider == null) return;
|
||||
|
||||
_vsyncSlider.minValue = 0;
|
||||
_vsyncSlider.maxValue = 2;
|
||||
_vsyncSlider.wholeNumbers = true;
|
||||
|
||||
UpdateVsyncLabel((int)_vsyncSlider.value);
|
||||
}
|
||||
|
||||
private void SetupMaxFramerateSlider()
|
||||
{
|
||||
if (_maxFramerateSlider == null) return;
|
||||
|
||||
_maxFramerateSlider.minValue = _maxFramerateMin;
|
||||
_maxFramerateSlider.maxValue = _maxFramerateMax;
|
||||
_maxFramerateSlider.wholeNumbers = true;
|
||||
|
||||
UpdateMaxFramerateLabel(Mathf.RoundToInt(_maxFramerateSlider.value));
|
||||
}
|
||||
|
||||
private void HookSlider(Slider slider, TextMeshProUGUI label, Action<float> assignToDraft)
|
||||
{
|
||||
if (slider == null) return;
|
||||
|
||||
slider.minValue = 0f;
|
||||
slider.maxValue = 1f;
|
||||
|
||||
slider.onValueChanged.AddListener(v =>
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
var clamped = Mathf.Clamp01(v);
|
||||
assignToDraft?.Invoke(clamped);
|
||||
UpdatePercentLabel(label, clamped);
|
||||
|
||||
MarkDirty();
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadSettings(AudioSettings audio, VisualSettings visual, GameSettings game)
|
||||
{
|
||||
_loadedAudio = CloneAudio(audio ?? new AudioSettings());
|
||||
_loadedVisual = CloneVisual(visual ?? new VisualSettings());
|
||||
_loadedGameSettings = CloneGameSettings(game ?? new GameSettings());
|
||||
|
||||
_draftAudio = CloneAudio(_loadedAudio);
|
||||
_draftVisual = CloneVisual(_loadedVisual);
|
||||
_draftGameSettings = CloneGameSettings(_loadedGameSettings);
|
||||
|
||||
ApplySettingsToUI();
|
||||
}
|
||||
|
||||
private void ApplySettingsToUI()
|
||||
{
|
||||
_ignoreUiCallbacks = true;
|
||||
|
||||
SetSlider(_masterVolumeSlider, _masterVolumeText, _draftAudio.MasterVolume);
|
||||
SetSlider(_musicVolumeSlider, _musicVolumeText, _draftAudio.MusicVolume);
|
||||
SetSlider(_sfxVolumeSlider, _sfxVolumeText, _draftAudio.SfxVolume);
|
||||
SetSlider(_voiceVolumeSlider, _voiceVolumeText, _draftAudio.VoiceVolume);
|
||||
SetSlider(_ambienceVolumeSlider, _ambienceVolumeText, _draftAudio.AmbienceVolume);
|
||||
SetSlider(_uiVolumeSlider, _uiVolumeText, _draftAudio.UIVolume);
|
||||
|
||||
if (_tutorialsEnabledToggle != null)
|
||||
_tutorialsEnabledToggle.isOn = _draftGameSettings.TutorialsEnabled;
|
||||
|
||||
if (_tooltipsEnabledToggle != null)
|
||||
_tooltipsEnabledToggle.isOn = _draftGameSettings.TooltipsEnabled;
|
||||
|
||||
if(_autoUseToolsToggle != null)
|
||||
_autoUseToolsToggle.isOn = _draftGameSettings.AutoUseTools;
|
||||
|
||||
if (_muteWhenUnfocusedToggle != null)
|
||||
_muteWhenUnfocusedToggle.isOn = _draftAudio.MuteWhenUnfocused;
|
||||
|
||||
if (_fullscreenToggle != null)
|
||||
_fullscreenToggle.isOn = _draftVisual.FullScreenMode != FullScreenMode.Windowed;
|
||||
|
||||
if (_vsyncSlider != null)
|
||||
{
|
||||
_vsyncSlider.value = Mathf.Clamp(_draftVisual.VSyncCount, 0, 2);
|
||||
UpdateVsyncLabel(_draftVisual.VSyncCount);
|
||||
}
|
||||
|
||||
if (_maxFramerateSlider != null)
|
||||
{
|
||||
var fps = Mathf.Clamp(_draftVisual.MaxFramerate, _maxFramerateMin, _maxFramerateMax);
|
||||
_maxFramerateSlider.value = fps;
|
||||
UpdateMaxFramerateLabel(fps);
|
||||
}
|
||||
|
||||
if (_popupDisplayDurationSlider != null)
|
||||
{
|
||||
_popupDisplayDurationSlider.value = _draftGameSettings.PopupDisplayDuration;
|
||||
UpdateSecondsLabel(_popupDisplayDurationText, _draftGameSettings.PopupDisplayDuration);
|
||||
}
|
||||
|
||||
_ignoreUiCallbacks = false;
|
||||
|
||||
_waitingChanges = HasUnappliedChanges();
|
||||
SetPendingChangesVisible(_waitingChanges);
|
||||
}
|
||||
|
||||
private void SetSlider(Slider slider, TextMeshProUGUI label, float value)
|
||||
{
|
||||
if (slider != null)
|
||||
slider.value = Mathf.Clamp01(value);
|
||||
|
||||
UpdatePercentLabel(label, value);
|
||||
}
|
||||
|
||||
private void UpdatePercentLabel(TextMeshProUGUI label, float value)
|
||||
{
|
||||
if (label == null) return;
|
||||
label.text = $"{Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
private void UpdateSecondsLabel(TextMeshProUGUI label, float value)
|
||||
{
|
||||
if (label == null) return;
|
||||
label.text = $"{value:0.0}s";
|
||||
}
|
||||
|
||||
private void UpdateVsyncLabel(int v)
|
||||
{
|
||||
if (_vsyncValueText == null) return;
|
||||
|
||||
_vsyncValueText.text = v switch
|
||||
{
|
||||
0 => "Off",
|
||||
1 => "On",
|
||||
2 => "Half",
|
||||
_ => "On"
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateMaxFramerateLabel(int fps)
|
||||
{
|
||||
if (_maxFramerateValueText == null) return;
|
||||
_maxFramerateValueText.text = $"{fps} FPS";
|
||||
}
|
||||
|
||||
private void MarkDirty()
|
||||
{
|
||||
if (_ignoreUiCallbacks) return;
|
||||
|
||||
_waitingChanges = HasUnappliedChanges();
|
||||
SetPendingChangesVisible(_waitingChanges);
|
||||
}
|
||||
|
||||
private bool HasUnappliedChanges()
|
||||
{
|
||||
return !AudioEquals(_draftAudio, _loadedAudio) ||
|
||||
!VisualEquals(_draftVisual, _loadedVisual) ||
|
||||
!GameEquals(_draftGameSettings, _loadedGameSettings);
|
||||
}
|
||||
|
||||
private void SetPendingChangesVisible(bool visible)
|
||||
{
|
||||
if (_pendingChangesText == null) return;
|
||||
|
||||
_pendingChangesText.gameObject.SetActive(visible);
|
||||
if (visible)
|
||||
_pendingChangesText.text = _pendingChangesMessage;
|
||||
}
|
||||
|
||||
private static bool AudioEquals(AudioSettings a, AudioSettings b)
|
||||
{
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
const float eps = 0.0001f;
|
||||
|
||||
return Mathf.Abs(a.MasterVolume - b.MasterVolume) < eps &&
|
||||
Mathf.Abs(a.MusicVolume - b.MusicVolume) < eps &&
|
||||
Mathf.Abs(a.SfxVolume - b.SfxVolume) < eps &&
|
||||
Mathf.Abs(a.VoiceVolume - b.VoiceVolume) < eps &&
|
||||
Mathf.Abs(a.AmbienceVolume - b.AmbienceVolume) < eps &&
|
||||
Mathf.Abs(a.UIVolume - b.UIVolume) < eps &&
|
||||
a.MuteWhenUnfocused == b.MuteWhenUnfocused;
|
||||
}
|
||||
|
||||
private static bool VisualEquals(VisualSettings a, VisualSettings b)
|
||||
{
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
return a.FullScreenMode == b.FullScreenMode &&
|
||||
a.VSyncCount == b.VSyncCount &&
|
||||
a.MaxFramerate == b.MaxFramerate;
|
||||
}
|
||||
|
||||
private static bool GameEquals(GameSettings a, GameSettings b)
|
||||
{
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
const float eps = 0.0001f;
|
||||
|
||||
return Mathf.Abs(a.PopupDisplayDuration - b.PopupDisplayDuration) < eps &&
|
||||
a.TutorialsEnabled == b.TutorialsEnabled &&
|
||||
a.TooltipsEnabled == b.TooltipsEnabled &&
|
||||
a.AutoUseTools == b.AutoUseTools;
|
||||
}
|
||||
|
||||
private static AudioSettings CloneAudio(AudioSettings a)
|
||||
{
|
||||
if (a == null) a = new AudioSettings();
|
||||
|
||||
return new AudioSettings
|
||||
{
|
||||
MasterVolume = a.MasterVolume,
|
||||
MusicVolume = a.MusicVolume,
|
||||
SfxVolume = a.SfxVolume,
|
||||
VoiceVolume = a.VoiceVolume,
|
||||
AmbienceVolume = a.AmbienceVolume,
|
||||
UIVolume = a.UIVolume,
|
||||
MuteWhenUnfocused = a.MuteWhenUnfocused
|
||||
};
|
||||
}
|
||||
|
||||
private static VisualSettings CloneVisual(VisualSettings v)
|
||||
{
|
||||
if (v == null) v = new VisualSettings();
|
||||
|
||||
return new VisualSettings
|
||||
{
|
||||
FullScreenMode = v.FullScreenMode,
|
||||
VSyncCount = v.VSyncCount,
|
||||
MaxFramerate = v.MaxFramerate <= 0 ? 120 : v.MaxFramerate
|
||||
};
|
||||
}
|
||||
|
||||
private static GameSettings CloneGameSettings(GameSettings g)
|
||||
{
|
||||
if (g == null) g = new GameSettings();
|
||||
|
||||
return new GameSettings
|
||||
{
|
||||
PopupDisplayDuration = g.PopupDisplayDuration,
|
||||
TutorialsEnabled = g.TutorialsEnabled,
|
||||
TooltipsEnabled = g.TooltipsEnabled,
|
||||
AutoUseTools = g.AutoUseTools,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Menus/SettingsWindow.cs.meta
Normal file
3
Assets/Scripts/UI/Menus/SettingsWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37445792122d444baf3ac50efe28744f
|
||||
timeCreated: 1769787064
|
||||
364
Assets/Scripts/UI/Menus/VerticalScrollbar.cs
Normal file
364
Assets/Scripts/UI/Menus/VerticalScrollbar.cs
Normal file
@@ -0,0 +1,364 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
namespace BriarQueen.UI.Menus
|
||||
{
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class VerticalScrollbar : MonoBehaviour, IDragHandler, IPointerDownHandler
|
||||
{
|
||||
private static readonly Vector3[] Corners = new Vector3[4];
|
||||
|
||||
[Header("Hierarchy")]
|
||||
[SerializeField]
|
||||
private RectTransform _viewport;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _content;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _trackRect;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _handleRect;
|
||||
|
||||
[Header("Scroll Settings")]
|
||||
[SerializeField]
|
||||
private float _wheelPixels = 80f;
|
||||
|
||||
[SerializeField]
|
||||
private float _padSpeed = 900f;
|
||||
|
||||
[SerializeField]
|
||||
private float _inputSystemWheelScale = 0.05f;
|
||||
|
||||
[Header("Handle")]
|
||||
[SerializeField]
|
||||
private bool _useCustomHandleSizing;
|
||||
|
||||
[SerializeField]
|
||||
private float _minHandleHeight = 24f;
|
||||
|
||||
[Header("Alignment")]
|
||||
[SerializeField]
|
||||
private bool _centerContentWhenNotScrollable = true;
|
||||
|
||||
[SerializeField]
|
||||
private float _topInset = 6f;
|
||||
|
||||
[SerializeField]
|
||||
private float _bottomInset = 6f;
|
||||
|
||||
[Header("Track")]
|
||||
[SerializeField]
|
||||
private bool _hideTrackWhenNotScrollable = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Header("Editor Debug")]
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _debugNormalized;
|
||||
#endif
|
||||
|
||||
private bool _isScrollable;
|
||||
private float _scrollRange;
|
||||
private Camera _uiCamera;
|
||||
|
||||
public float Normalized { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
_uiCamera = canvas.worldCamera;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleMouseWheel();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_viewport == null || _content == null || _trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
if (!TryGetContentBounds(out var top, out var bottom))
|
||||
return;
|
||||
|
||||
var contentHeight = top - bottom;
|
||||
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
||||
|
||||
_isScrollable = contentHeight > viewportHeight;
|
||||
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
||||
Normalized = Mathf.Clamp01(_debugNormalized);
|
||||
|
||||
if (_centerContentWhenNotScrollable && !_isScrollable)
|
||||
{
|
||||
CenterContent(top, bottom);
|
||||
Normalized = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
||||
SetContentY(offset);
|
||||
|
||||
if (Normalized <= 0.0001f)
|
||||
AlignFirstChildToTop();
|
||||
}
|
||||
|
||||
UpdateTrackVisibility();
|
||||
UpdateHandle();
|
||||
}
|
||||
#endif
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
DragHandle(eventData);
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
DragHandle(eventData);
|
||||
}
|
||||
|
||||
public void Rebuild()
|
||||
{
|
||||
if (_viewport == null || _content == null)
|
||||
return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||
|
||||
if (!TryGetContentBounds(out var top, out var bottom))
|
||||
return;
|
||||
|
||||
var contentHeight = top - bottom;
|
||||
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
||||
|
||||
_isScrollable = contentHeight > viewportHeight;
|
||||
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
||||
|
||||
if (_centerContentWhenNotScrollable && !_isScrollable)
|
||||
{
|
||||
CenterContent(top, bottom);
|
||||
Normalized = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetNormalized(Normalized);
|
||||
}
|
||||
|
||||
UpdateTrackVisibility();
|
||||
UpdateHandle();
|
||||
}
|
||||
|
||||
public void SetNormalized(float normalized)
|
||||
{
|
||||
Normalized = Mathf.Clamp01(normalized);
|
||||
|
||||
if (!_isScrollable)
|
||||
return;
|
||||
|
||||
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
||||
SetContentY(offset);
|
||||
|
||||
if (Normalized <= 0.0001f)
|
||||
AlignFirstChildToTop();
|
||||
|
||||
UpdateHandle();
|
||||
}
|
||||
|
||||
private void CenterContent(float top, float bottom)
|
||||
{
|
||||
var contentCenter = (top + bottom) * 0.5f;
|
||||
var viewportCenter = (_viewport.rect.yMin + _viewport.rect.yMax) * 0.5f;
|
||||
|
||||
var delta = viewportCenter - contentCenter;
|
||||
|
||||
var position = _content.anchoredPosition;
|
||||
position.y += delta;
|
||||
_content.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void AlignFirstChildToTop()
|
||||
{
|
||||
RectTransform first = null;
|
||||
|
||||
for (var i = 0; i < _content.childCount; i++)
|
||||
{
|
||||
var child = _content.GetChild(i) as RectTransform;
|
||||
if (child != null && child.gameObject.activeSelf)
|
||||
{
|
||||
first = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first == null)
|
||||
return;
|
||||
|
||||
first.GetWorldCorners(Corners);
|
||||
|
||||
var childTop = _viewport.InverseTransformPoint(Corners[1]).y;
|
||||
var targetTop = _viewport.rect.yMax - _topInset;
|
||||
|
||||
var delta = targetTop - childTop;
|
||||
|
||||
var position = _content.anchoredPosition;
|
||||
position.y += delta;
|
||||
_content.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void ScrollByPixels(float pixels)
|
||||
{
|
||||
if (!_isScrollable)
|
||||
return;
|
||||
|
||||
var current = Normalized * _scrollRange;
|
||||
var next = Mathf.Clamp(current + pixels, 0f, _scrollRange);
|
||||
|
||||
Normalized = _scrollRange > 0f ? next / _scrollRange : 0f;
|
||||
SetNormalized(Normalized);
|
||||
}
|
||||
|
||||
private void DragHandle(PointerEventData eventData)
|
||||
{
|
||||
if (!_isScrollable || _trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_trackRect, eventData.position, _uiCamera,
|
||||
out var localPoint))
|
||||
return;
|
||||
|
||||
var halfHandleHeight = _handleRect.rect.height * 0.5f;
|
||||
|
||||
var min = _trackRect.rect.yMin + halfHandleHeight;
|
||||
var max = _trackRect.rect.yMax - halfHandleHeight;
|
||||
|
||||
var y = Mathf.Clamp(localPoint.y, min, max);
|
||||
var normalized = 1f - Mathf.InverseLerp(min, max, y);
|
||||
|
||||
SetNormalized(normalized);
|
||||
}
|
||||
|
||||
private void HandleMouseWheel()
|
||||
{
|
||||
var wheel = ReadMouseWheelDelta();
|
||||
if (Mathf.Abs(wheel) > 0.01f)
|
||||
ScrollByPixels(-wheel * _wheelPixels);
|
||||
}
|
||||
|
||||
private float ReadMouseWheelDelta()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
if (Mouse.current != null)
|
||||
return Mouse.current.scroll.ReadValue().y * _inputSystemWheelScale;
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
return Input.mouseScrollDelta.y;
|
||||
#endif
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void SetContentY(float y)
|
||||
{
|
||||
var position = _content.anchoredPosition;
|
||||
position.y = y;
|
||||
_content.anchoredPosition = position;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateHandle()
|
||||
{
|
||||
if (_trackRect == null || _handleRect == null)
|
||||
return;
|
||||
|
||||
if (!_isScrollable)
|
||||
{
|
||||
_handleRect.anchoredPosition = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCustomHandleSizing)
|
||||
{
|
||||
var ratio = Mathf.Clamp01(_viewport.rect.height / (_scrollRange + _viewport.rect.height));
|
||||
var height = Mathf.Max(_trackRect.rect.height * ratio, _minHandleHeight);
|
||||
_handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
}
|
||||
|
||||
var half = _handleRect.rect.height * 0.5f;
|
||||
|
||||
var min = _trackRect.rect.yMin + half;
|
||||
var max = _trackRect.rect.yMax - half;
|
||||
|
||||
var y = Mathf.Lerp(max, min, Normalized);
|
||||
|
||||
var position = _handleRect.anchoredPosition;
|
||||
position.y = y;
|
||||
_handleRect.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void UpdateTrackVisibility()
|
||||
{
|
||||
if (_trackRect == null)
|
||||
return;
|
||||
|
||||
if (_hideTrackWhenNotScrollable)
|
||||
_trackRect.gameObject.SetActive(_isScrollable);
|
||||
}
|
||||
|
||||
private bool TryGetContentBounds(out float top, out float bottom)
|
||||
{
|
||||
top = float.MinValue;
|
||||
bottom = float.MaxValue;
|
||||
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < _content.childCount; i++)
|
||||
{
|
||||
var child = _content.GetChild(i) as RectTransform;
|
||||
if (child == null || !child.gameObject.activeSelf)
|
||||
continue;
|
||||
|
||||
child.GetWorldCorners(Corners);
|
||||
|
||||
for (var c = 0; c < 4; c++)
|
||||
{
|
||||
var local = _viewport.InverseTransformPoint(Corners[c]);
|
||||
top = Mathf.Max(top, local.y);
|
||||
bottom = Mathf.Min(bottom, local.y);
|
||||
}
|
||||
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/Menus/VerticalScrollbar.cs.meta
Normal file
2
Assets/Scripts/UI/Menus/VerticalScrollbar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d868730b4606b4100910e781df521d52
|
||||
3
Assets/Scripts/UI/Scopes.meta
Normal file
3
Assets/Scripts/UI/Scopes.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74dc4af3d5024f99a5c29e4c3e2d542f
|
||||
timeCreated: 1773786938
|
||||
31
Assets/Scripts/UI/Scopes/MainMenuLifetimeScope.cs
Normal file
31
Assets/Scripts/UI/Scopes/MainMenuLifetimeScope.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using BriarQueen.UI.Menus;
|
||||
using BriarQueen.UI.Menus.Components;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace BriarQueen.UI.Scopes
|
||||
{
|
||||
public class MainMenuLifetimeScope : LifetimeScope
|
||||
{
|
||||
[SerializeField] private MainMenuWindow _mainMenuWindow;
|
||||
[SerializeField] private SelectSaveWindow _saveWindow;
|
||||
[SerializeField] private SaveSlotUI _saveSlotUI;
|
||||
[SerializeField] private NewSaveWindow _newSaveWindow;
|
||||
|
||||
protected override void Configure(IContainerBuilder builder)
|
||||
{
|
||||
if (_mainMenuWindow != null)
|
||||
builder.RegisterComponent(_mainMenuWindow);
|
||||
|
||||
if (_saveWindow != null)
|
||||
builder.RegisterComponent(_saveWindow);
|
||||
|
||||
if (_saveSlotUI != null)
|
||||
builder.RegisterComponent(_saveSlotUI);
|
||||
|
||||
if (_newSaveWindow != null)
|
||||
builder.RegisterComponent(_newSaveWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Scopes/MainMenuLifetimeScope.cs.meta
Normal file
3
Assets/Scripts/UI/Scopes/MainMenuLifetimeScope.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fe9705cca9c4e78b764174b3ba75697
|
||||
timeCreated: 1770242616
|
||||
104
Assets/Scripts/UI/Scopes/UISceneLifetimeScope.cs
Normal file
104
Assets/Scripts/UI/Scopes/UISceneLifetimeScope.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.UI.Codex;
|
||||
using BriarQueen.UI.HUD;
|
||||
using BriarQueen.UI.Menus;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace BriarQueen.UI.Scopes
|
||||
{
|
||||
public class UISceneLifetimeScope : LifetimeScope
|
||||
{
|
||||
[Header("Windows")]
|
||||
[SerializeField]
|
||||
private PauseMenuWindow _pauseMenuWindow;
|
||||
|
||||
[SerializeField]
|
||||
private SettingsWindow _settingsWindow;
|
||||
|
||||
[SerializeField]
|
||||
private CodexWindow _codexWindow;
|
||||
|
||||
[Header("Popups")]
|
||||
[SerializeField]
|
||||
private TutorialPopup _tutorialPopupWindow;
|
||||
|
||||
[SerializeField]
|
||||
private InfoPopup _infoPopup;
|
||||
|
||||
[Header("UI Objects")]
|
||||
[SerializeField]
|
||||
private ScreenFader _screenFader;
|
||||
|
||||
[Header("HUD")]
|
||||
[SerializeField]
|
||||
private HUDContainer _hudContainer;
|
||||
|
||||
[SerializeField]
|
||||
private CursorTooltip _cursorTooltip;
|
||||
|
||||
[SerializeField]
|
||||
private InventoryBar _inventoryBar;
|
||||
|
||||
|
||||
protected override void Configure(IContainerBuilder builder)
|
||||
{
|
||||
if (_pauseMenuWindow != null)
|
||||
builder.RegisterComponent(_pauseMenuWindow);
|
||||
|
||||
if (_settingsWindow != null)
|
||||
builder.RegisterComponent(_settingsWindow);
|
||||
|
||||
if (_codexWindow != null)
|
||||
builder.RegisterComponent(_codexWindow);
|
||||
|
||||
if (_tutorialPopupWindow != null)
|
||||
builder.RegisterComponent(_tutorialPopupWindow);
|
||||
|
||||
if (_infoPopup != null)
|
||||
builder.RegisterComponent(_infoPopup);
|
||||
|
||||
if (_screenFader != null)
|
||||
builder.RegisterComponent(_screenFader);
|
||||
|
||||
if (_hudContainer != null)
|
||||
builder.RegisterComponent(_hudContainer);
|
||||
|
||||
if (_cursorTooltip != null)
|
||||
builder.RegisterComponent(_cursorTooltip);
|
||||
|
||||
if (_inventoryBar != null)
|
||||
builder.RegisterComponent(_inventoryBar);
|
||||
|
||||
|
||||
builder.RegisterBuildCallback(container =>
|
||||
{
|
||||
var uiManager = container.Resolve<UIManager>();
|
||||
|
||||
if (_pauseMenuWindow != null)
|
||||
uiManager.RegisterWindow(_pauseMenuWindow);
|
||||
|
||||
if (_settingsWindow != null)
|
||||
uiManager.RegisterWindow(_settingsWindow);
|
||||
|
||||
if (_codexWindow != null)
|
||||
uiManager.RegisterWindow(_codexWindow);
|
||||
|
||||
if (_tutorialPopupWindow != null)
|
||||
uiManager.RegisterTutorialPopup(_tutorialPopupWindow);
|
||||
|
||||
if (_infoPopup != null)
|
||||
uiManager.RegisterInfoPopup(_infoPopup);
|
||||
|
||||
if (_screenFader != null)
|
||||
uiManager.RegisterScreenFader(_screenFader);
|
||||
|
||||
if (_hudContainer != null)
|
||||
uiManager.RegisterHUD(_hudContainer);
|
||||
|
||||
Debug.Log($"[UISceneLifetimeScope] UI registered with {uiManager}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/Scopes/UISceneLifetimeScope.cs.meta
Normal file
3
Assets/Scripts/UI/Scopes/UISceneLifetimeScope.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2fcd047574341aa800c79051b738e60
|
||||
timeCreated: 1769710416
|
||||
181
Assets/Scripts/UI/ScreenFader.cs
Normal file
181
Assets/Scripts/UI/ScreenFader.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI
|
||||
{
|
||||
public class ScreenFader : MonoBehaviour, IScreenFader
|
||||
{
|
||||
private const float ALPHA_EPSILON = 0.001f;
|
||||
|
||||
[Header("UI Elements")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Tooltip("Used for black / solid-color fades.")]
|
||||
[SerializeField]
|
||||
private Image _solidImage;
|
||||
|
||||
private Sequence _currentFadeSequence;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private CancellationTokenSource _fadeCts;
|
||||
|
||||
public bool IsModal => true;
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
Debug.LogError($"{nameof(ScreenFader)} on {name} is missing a CanvasGroup reference.", this);
|
||||
|
||||
SetInteractionState(_canvasGroup != null && _canvasGroup.alpha > ALPHA_EPSILON);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelAndDisposeFadeToken();
|
||||
StopCurrentSequence();
|
||||
}
|
||||
|
||||
// Window Stubs - Fader is Non-Interactive
|
||||
public async UniTask Show()
|
||||
{
|
||||
await FadeToAsync( 1f);
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
await FadeFromAsync(1f);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator)
|
||||
{
|
||||
Debug.Log("ScreenFader constructed");
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public async UniTask FadeToAsync(float duration)
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
return;
|
||||
|
||||
_solidImage.color = Color.black;
|
||||
|
||||
BeginNewFade();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
SetInteractionState(true);
|
||||
|
||||
await FadeScreen(1f, duration, _fadeCts.Token);
|
||||
}
|
||||
|
||||
public async UniTask FadeFromAsync(float duration)
|
||||
{
|
||||
if (_canvasGroup == null) return;
|
||||
|
||||
BeginNewFade();
|
||||
|
||||
await FadeScreen(0f, duration, _fadeCts.Token);
|
||||
|
||||
if (!_fadeCts.IsCancellationRequested)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
SetInteractionState(false);
|
||||
}
|
||||
}
|
||||
|
||||
public UniTask FadeToAsync(FadeStyle style, System.Drawing.Color tint, float duration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ApplyTint(FadeStyle style, Color tint)
|
||||
{
|
||||
if (_solidImage != null)
|
||||
{
|
||||
_solidImage.enabled = true;
|
||||
_solidImage.color = Color.black;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask FadeScreen(float targetAlpha, float duration, CancellationToken token)
|
||||
{
|
||||
targetAlpha = Mathf.Clamp01(targetAlpha);
|
||||
duration = Mathf.Max(0f, duration);
|
||||
|
||||
var currentAlpha = _canvasGroup.alpha;
|
||||
|
||||
if (Mathf.Abs(currentAlpha - targetAlpha) <= ALPHA_EPSILON)
|
||||
{
|
||||
_canvasGroup.alpha = targetAlpha;
|
||||
SetInteractionState(targetAlpha > ALPHA_EPSILON);
|
||||
return;
|
||||
}
|
||||
|
||||
StopCurrentSequence();
|
||||
|
||||
if (duration <= 0f)
|
||||
{
|
||||
_canvasGroup.alpha = targetAlpha;
|
||||
SetInteractionState(targetAlpha > ALPHA_EPSILON);
|
||||
|
||||
_eventCoordinator?.PublishImmediate(new FadeCompletedEvent());
|
||||
return;
|
||||
}
|
||||
|
||||
_currentFadeSequence = Sequence.Create()
|
||||
.Group(Tween.Alpha(_canvasGroup, targetAlpha, duration, Ease.InOutSine));
|
||||
|
||||
try
|
||||
{
|
||||
await _currentFadeSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_canvasGroup.alpha = targetAlpha;
|
||||
SetInteractionState(targetAlpha > ALPHA_EPSILON);
|
||||
|
||||
_eventCoordinator?.PublishImmediate(new FadeCompletedEvent());
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Another fade interrupted this one. Intentionally ignore completion.
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginNewFade()
|
||||
{
|
||||
CancelAndDisposeFadeToken();
|
||||
StopCurrentSequence();
|
||||
|
||||
_fadeCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void CancelAndDisposeFadeToken()
|
||||
{
|
||||
if (_fadeCts == null) return;
|
||||
|
||||
if (!_fadeCts.IsCancellationRequested) _fadeCts.Cancel();
|
||||
|
||||
_fadeCts.Dispose();
|
||||
_fadeCts = null;
|
||||
}
|
||||
|
||||
private void StopCurrentSequence()
|
||||
{
|
||||
if (_currentFadeSequence.isAlive) _currentFadeSequence.Stop();
|
||||
}
|
||||
|
||||
private void SetInteractionState(bool isBlocking)
|
||||
{
|
||||
_canvasGroup.interactable = isBlocking;
|
||||
_canvasGroup.blocksRaycasts = isBlocking;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/ScreenFader.cs.meta
Normal file
3
Assets/Scripts/UI/ScreenFader.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3af562a2d3fb4b4bb261dc75d159b993
|
||||
timeCreated: 1769712302
|
||||
Reference in New Issue
Block a user