Restructured for new direction.
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
"GUID:593a5b492d29ac6448b1ebf7f035ef33",
|
||||
"GUID:84651a3751eca9349aac36a66bba901b",
|
||||
"GUID:75469ad4d38634e559750d17036d5f7c",
|
||||
"GUID:776d03a35f1b52c4a9aed9f56d7b4229"
|
||||
"GUID:776d03a35f1b52c4a9aed9f56d7b4229",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
8
Assets/Scripts/Framework/Effects.meta
Normal file
8
Assets/Scripts/Framework/Effects.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eca349fcd212e4ac18902c56b71847e0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
499
Assets/Scripts/Framework/Effects/UIDissolveImage.cs
Normal file
499
Assets/Scripts/Framework/Effects/UIDissolveImage.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Effects
|
||||
{
|
||||
[ExecuteAlways]
|
||||
public class UIDissolveImage : MonoBehaviour
|
||||
{
|
||||
private static readonly int _dissolveAmountId = Shader.PropertyToID("_DissolveAmount");
|
||||
private static readonly int _reverseDirectionId = Shader.PropertyToID("_ReverseDirection");
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField]
|
||||
private Image _image;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private Material _dissolveMaterialTemplate;
|
||||
|
||||
[Header("Targeting")]
|
||||
[SerializeField]
|
||||
private bool _dissolveChildGraphics;
|
||||
|
||||
[SerializeField]
|
||||
private bool _includeInactiveChildGraphics = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _includeTextGraphics;
|
||||
|
||||
[Header("Tween")]
|
||||
[SerializeField]
|
||||
private float _duration = 0.75f;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _ease = Ease.InOutSine;
|
||||
|
||||
[SerializeField]
|
||||
private bool _useUnscaledTime = true;
|
||||
|
||||
[Header("Direction")]
|
||||
[SerializeField]
|
||||
private bool _reverseDirection;
|
||||
|
||||
[Header("Destruction")]
|
||||
[SerializeField]
|
||||
private bool _destroyWhenFullyDissolved = true;
|
||||
|
||||
[Header("Editor Preview")]
|
||||
[SerializeField]
|
||||
private bool _previewInEditMode;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _previewDissolveAmount;
|
||||
|
||||
private readonly List<Graphic> _targetGraphics = new();
|
||||
private readonly List<Material> _originalMaterials = new();
|
||||
|
||||
private CancellationTokenSource _dissolveCts;
|
||||
private DestructionService _destructionService;
|
||||
private bool _hasOriginalMaterials;
|
||||
private bool _isPreviewMaterial;
|
||||
private Material _runtimeMaterial;
|
||||
private Sequence _dissolveSequence;
|
||||
|
||||
public float DissolveAmount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return _runtimeMaterial.GetFloat(_dissolveAmountId);
|
||||
}
|
||||
set => SetDissolveAmount(value);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(DestructionService destructionService)
|
||||
{
|
||||
_destructionService = destructionService;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
CreateRuntimeMaterial(false);
|
||||
SetDissolveAmount(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
RefreshEditModePreview();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
RestoreEditModeMaterial();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelDissolve();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DestroyRuntimeMaterial(false);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
SetReverseDirection(_reverseDirection);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
public UniTask DissolveIn()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
return TweenDissolve(1f, 0f, _duration, false);
|
||||
}
|
||||
|
||||
public UniTask DissolveOut()
|
||||
{
|
||||
return TweenDissolve(0f, 1f, _duration, _destroyWhenFullyDissolved);
|
||||
}
|
||||
|
||||
public UniTask DissolveOut(bool destroyWhenComplete)
|
||||
{
|
||||
return TweenDissolve(0f, 1f, _duration, destroyWhenComplete);
|
||||
}
|
||||
|
||||
public UniTask DissolveOutAndDestroy()
|
||||
{
|
||||
return TweenDissolve(0f, 1f, _duration, true);
|
||||
}
|
||||
|
||||
public UniTask TweenDissolve(float from, float to, float duration)
|
||||
{
|
||||
return TweenDissolve(from, to, duration, false);
|
||||
}
|
||||
|
||||
public async UniTask TweenDissolve(float from, float to, float duration, bool destroyWhenComplete)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
CreateRuntimeMaterial(false);
|
||||
}
|
||||
|
||||
CancelDissolve();
|
||||
|
||||
_dissolveCts = new CancellationTokenSource();
|
||||
|
||||
SetDissolveAmount(from);
|
||||
|
||||
_dissolveSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
to,
|
||||
Mathf.Max(0f, duration),
|
||||
SetDissolveAmount,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
try
|
||||
{
|
||||
await _dissolveSequence.ToUniTask(cancellationToken: _dissolveCts.Token);
|
||||
SetDissolveAmount(to);
|
||||
|
||||
if (destroyWhenComplete && Mathf.Approximately(to, 1f))
|
||||
{
|
||||
await DestroyAfterDissolve();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another dissolve request or object destruction.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dissolveSequence = default;
|
||||
|
||||
if (_dissolveCts != null)
|
||||
{
|
||||
_dissolveCts.Dispose();
|
||||
_dissolveCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDissolveAmount(float amount)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial.SetFloat(_dissolveAmountId, Mathf.Clamp01(amount));
|
||||
SetTargetsMaterialDirty();
|
||||
}
|
||||
|
||||
public void SetReverseDirection(bool reverseDirection)
|
||||
{
|
||||
_reverseDirection = reverseDirection;
|
||||
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial.SetFloat(_reverseDirectionId, _reverseDirection ? 1f : 0f);
|
||||
SetTargetsMaterialDirty();
|
||||
}
|
||||
|
||||
public void CancelDissolve()
|
||||
{
|
||||
if (_dissolveSequence.isAlive)
|
||||
{
|
||||
_dissolveSequence.Stop();
|
||||
_dissolveSequence = default;
|
||||
}
|
||||
|
||||
if (_dissolveCts != null)
|
||||
{
|
||||
_dissolveCts.Cancel();
|
||||
_dissolveCts.Dispose();
|
||||
_dissolveCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask DestroyAfterDissolve()
|
||||
{
|
||||
if (_destructionService != null)
|
||||
{
|
||||
await _destructionService.Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[{nameof(UIDissolveImage)}] Missing {nameof(DestructionService)}. Destroying directly.");
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_image == null)
|
||||
{
|
||||
_image = GetComponent<Image>();
|
||||
}
|
||||
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveTargetGraphics()
|
||||
{
|
||||
ResolveReferences();
|
||||
_targetGraphics.Clear();
|
||||
|
||||
if (_dissolveChildGraphics)
|
||||
{
|
||||
var root = _canvasGroup != null ? _canvasGroup.transform : transform;
|
||||
var graphics = root.GetComponentsInChildren<Graphic>(_includeInactiveChildGraphics);
|
||||
|
||||
foreach (var graphic in graphics)
|
||||
{
|
||||
if (IsValidTargetGraphic(graphic))
|
||||
{
|
||||
_targetGraphics.Add(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_image != null)
|
||||
{
|
||||
_targetGraphics.Add(_image);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidTargetGraphic(Graphic graphic)
|
||||
{
|
||||
if (graphic == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TMP needs a TMP-compatible dissolve shader; replacing its SDF material breaks text rendering.
|
||||
if (!_includeTextGraphics && graphic is TMP_Text)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CreateRuntimeMaterial(bool isPreviewMaterial)
|
||||
{
|
||||
ResolveTargetGraphics();
|
||||
|
||||
if (_targetGraphics.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceMaterial = _dissolveMaterialTemplate != null
|
||||
? _dissolveMaterialTemplate
|
||||
: _targetGraphics[0].material;
|
||||
|
||||
if (sourceMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_runtimeMaterial != null)
|
||||
{
|
||||
ApplyRuntimeMaterialToTargets();
|
||||
return;
|
||||
}
|
||||
|
||||
SaveOriginalMaterials();
|
||||
|
||||
_runtimeMaterial = Instantiate(sourceMaterial);
|
||||
_runtimeMaterial.name = isPreviewMaterial
|
||||
? $"{nameof(UIDissolveImage)} Preview Material"
|
||||
: $"{nameof(UIDissolveImage)} Runtime Material";
|
||||
_isPreviewMaterial = isPreviewMaterial;
|
||||
|
||||
if (isPreviewMaterial)
|
||||
{
|
||||
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
|
||||
}
|
||||
|
||||
ApplyRuntimeMaterialToTargets();
|
||||
SetReverseDirection(_reverseDirection);
|
||||
}
|
||||
|
||||
private void ApplyRuntimeMaterialToTargets()
|
||||
{
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
if (graphic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
graphic.material = _runtimeMaterial;
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveOriginalMaterials()
|
||||
{
|
||||
_originalMaterials.Clear();
|
||||
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
_originalMaterials.Add(graphic != null ? graphic.material : null);
|
||||
}
|
||||
|
||||
_hasOriginalMaterials = true;
|
||||
}
|
||||
|
||||
private void RestoreOriginalMaterials()
|
||||
{
|
||||
if (!_hasOriginalMaterials)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var graphic = _targetGraphics[i];
|
||||
if (graphic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
graphic.material = _originalMaterials[i];
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
|
||||
_originalMaterials.Clear();
|
||||
_hasOriginalMaterials = false;
|
||||
}
|
||||
|
||||
private void SetTargetsMaterialDirty()
|
||||
{
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
if (graphic != null)
|
||||
{
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyRuntimeMaterial(bool immediate)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreOriginalMaterials();
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
DestroyImmediate(_runtimeMaterial);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(_runtimeMaterial);
|
||||
}
|
||||
|
||||
_runtimeMaterial = null;
|
||||
_isPreviewMaterial = false;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void RefreshEditModePreview()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_previewInEditMode)
|
||||
{
|
||||
RestoreEditModeMaterial();
|
||||
return;
|
||||
}
|
||||
|
||||
CreateRuntimeMaterial(true);
|
||||
SetReverseDirection(_reverseDirection);
|
||||
SetDissolveAmount(_previewDissolveAmount);
|
||||
}
|
||||
|
||||
private void RestoreEditModeMaterial()
|
||||
{
|
||||
if (!_isPreviewMaterial)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyRuntimeMaterial(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Framework/Effects/UIDissolveImage.cs.meta
Normal file
2
Assets/Scripts/Framework/Effects/UIDissolveImage.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1ca11eb5d39d4da19232afd2e808c96
|
||||
502
Assets/Scripts/Framework/Effects/UIEdgeDarken.cs
Normal file
502
Assets/Scripts/Framework/Effects/UIEdgeDarken.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.Framework.Effects
|
||||
{
|
||||
[ExecuteAlways]
|
||||
public class UIEdgeDarken : MonoBehaviour
|
||||
{
|
||||
private static readonly int _amountId = Shader.PropertyToID("_Amount");
|
||||
private static readonly int _centerDarknessId = Shader.PropertyToID("_CenterDarkness");
|
||||
private static readonly int _colorId = Shader.PropertyToID("_Color");
|
||||
private static readonly int _edgeDarknessId = Shader.PropertyToID("_EdgeDarkness");
|
||||
private static readonly int _edgeWidthId = Shader.PropertyToID("_EdgeWidth");
|
||||
private static readonly int _rectPivotId = Shader.PropertyToID("_RectPivot");
|
||||
private static readonly int _rectSizeId = Shader.PropertyToID("_RectSize");
|
||||
private static readonly int _softnessId = Shader.PropertyToID("_Softness");
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField]
|
||||
private Graphic _graphic;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField]
|
||||
private Material _edgeDarkenMaterialTemplate;
|
||||
|
||||
[Header("Targeting")]
|
||||
[SerializeField]
|
||||
private bool _targetChildGraphics;
|
||||
|
||||
[SerializeField]
|
||||
private bool _includeInactiveChildGraphics = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _includeTextGraphics;
|
||||
|
||||
[Header("Darken")]
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _amount = 1f;
|
||||
|
||||
[SerializeField]
|
||||
private Color _color = new(0f, 0f, 0f, 0.65f);
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _edgeDarkness = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _centerDarkness;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0.001f, 0.5f)]
|
||||
private float _edgeWidth = 0.22f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0.001f, 0.5f)]
|
||||
private float _softness = 0.18f;
|
||||
|
||||
[Header("Tween")]
|
||||
[SerializeField]
|
||||
private float _duration = 0.35f;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _ease = Ease.InOutSine;
|
||||
|
||||
[SerializeField]
|
||||
private bool _useUnscaledTime = true;
|
||||
|
||||
[Header("Editor Preview")]
|
||||
[SerializeField]
|
||||
private bool _previewInEditMode;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _previewAmount = 1f;
|
||||
|
||||
private readonly List<Graphic> _targetGraphics = new();
|
||||
private readonly List<Material> _originalMaterials = new();
|
||||
private readonly List<Material> _runtimeMaterials = new();
|
||||
|
||||
private CancellationTokenSource _darkenCts;
|
||||
private bool _hasOriginalMaterials;
|
||||
private bool _isPreviewMaterial;
|
||||
private Sequence _darkenSequence;
|
||||
|
||||
public float Amount
|
||||
{
|
||||
get => _amount;
|
||||
set => SetAmount(value);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
CreateRuntimeMaterial(false);
|
||||
ApplyMaterialProperties(_amount);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
RefreshEditModePreview();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
RestoreEditModeMaterial();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTween();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DestroyRuntimeMaterial(false);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
ApplyMaterialProperties(_amount);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
public UniTask DarkenIn()
|
||||
{
|
||||
return TweenAmount(0f, 1f, _duration);
|
||||
}
|
||||
|
||||
public UniTask DarkenOut()
|
||||
{
|
||||
return TweenAmount(1f, 0f, _duration);
|
||||
}
|
||||
|
||||
public async UniTask TweenAmount(float from, float to, float duration)
|
||||
{
|
||||
if (_runtimeMaterials.Count == 0)
|
||||
{
|
||||
CreateRuntimeMaterial(false);
|
||||
}
|
||||
|
||||
CancelTween();
|
||||
_darkenCts = new CancellationTokenSource();
|
||||
|
||||
SetAmount(from);
|
||||
|
||||
_darkenSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
to,
|
||||
Mathf.Max(0f, duration),
|
||||
SetAmount,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
try
|
||||
{
|
||||
await _darkenSequence.ToUniTask(cancellationToken: _darkenCts.Token);
|
||||
SetAmount(to);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another darken request or object destruction.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_darkenSequence = default;
|
||||
|
||||
if (_darkenCts != null)
|
||||
{
|
||||
_darkenCts.Dispose();
|
||||
_darkenCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAmount(float amount)
|
||||
{
|
||||
_amount = Mathf.Clamp01(amount);
|
||||
ApplyMaterialProperties(_amount);
|
||||
}
|
||||
|
||||
public void CancelTween()
|
||||
{
|
||||
if (_darkenSequence.isAlive)
|
||||
{
|
||||
_darkenSequence.Stop();
|
||||
_darkenSequence = default;
|
||||
}
|
||||
|
||||
if (_darkenCts != null)
|
||||
{
|
||||
_darkenCts.Cancel();
|
||||
_darkenCts.Dispose();
|
||||
_darkenCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_graphic == null)
|
||||
{
|
||||
_graphic = GetComponent<Graphic>();
|
||||
}
|
||||
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveTargetGraphics()
|
||||
{
|
||||
ResolveReferences();
|
||||
_targetGraphics.Clear();
|
||||
|
||||
if (_targetChildGraphics)
|
||||
{
|
||||
var root = _canvasGroup != null ? _canvasGroup.transform : transform;
|
||||
var graphics = root.GetComponentsInChildren<Graphic>(_includeInactiveChildGraphics);
|
||||
|
||||
foreach (var graphic in graphics)
|
||||
{
|
||||
if (IsValidTargetGraphic(graphic))
|
||||
{
|
||||
_targetGraphics.Add(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_graphic != null)
|
||||
{
|
||||
_targetGraphics.Add(_graphic);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidTargetGraphic(Graphic graphic)
|
||||
{
|
||||
if (graphic == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TMP needs a TMP-compatible shader; replacing its SDF material breaks text rendering.
|
||||
if (!_includeTextGraphics && graphic is TMP_Text)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CreateRuntimeMaterial(bool isPreviewMaterial)
|
||||
{
|
||||
ResolveTargetGraphics();
|
||||
|
||||
if (_targetGraphics.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceMaterial = _edgeDarkenMaterialTemplate != null
|
||||
? _edgeDarkenMaterialTemplate
|
||||
: _targetGraphics[0].material;
|
||||
|
||||
if (sourceMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_runtimeMaterials.Count > 0)
|
||||
{
|
||||
ApplyRuntimeMaterialToTargets();
|
||||
return;
|
||||
}
|
||||
|
||||
SaveOriginalMaterials();
|
||||
_isPreviewMaterial = isPreviewMaterial;
|
||||
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
if (graphic == null)
|
||||
{
|
||||
_runtimeMaterials.Add(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
var runtimeMaterial = Instantiate(sourceMaterial);
|
||||
runtimeMaterial.name = isPreviewMaterial
|
||||
? $"{nameof(UIEdgeDarken)} Preview Material"
|
||||
: $"{nameof(UIEdgeDarken)} Runtime Material";
|
||||
|
||||
if (isPreviewMaterial)
|
||||
{
|
||||
runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
|
||||
}
|
||||
|
||||
_runtimeMaterials.Add(runtimeMaterial);
|
||||
}
|
||||
|
||||
ApplyRuntimeMaterialToTargets();
|
||||
}
|
||||
|
||||
private void ApplyRuntimeMaterialToTargets()
|
||||
{
|
||||
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var graphic = _targetGraphics[i];
|
||||
if (graphic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
graphic.material = _runtimeMaterials[i];
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMaterialProperties(float amount)
|
||||
{
|
||||
if (_runtimeMaterials.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var material = _runtimeMaterials[i];
|
||||
var graphic = _targetGraphics[i];
|
||||
if (material == null || graphic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rectTransform = graphic.rectTransform;
|
||||
material.SetFloat(_amountId, Mathf.Clamp01(amount));
|
||||
material.SetColor(_colorId, _color);
|
||||
material.SetFloat(_edgeDarknessId, _edgeDarkness);
|
||||
material.SetFloat(_centerDarknessId, _centerDarkness);
|
||||
material.SetFloat(_edgeWidthId, _edgeWidth);
|
||||
material.SetFloat(_softnessId, _softness);
|
||||
material.SetVector(_rectSizeId, rectTransform.rect.size);
|
||||
material.SetVector(_rectPivotId, rectTransform.pivot);
|
||||
}
|
||||
|
||||
SetTargetsMaterialDirty();
|
||||
}
|
||||
|
||||
private void SaveOriginalMaterials()
|
||||
{
|
||||
_originalMaterials.Clear();
|
||||
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
_originalMaterials.Add(graphic != null ? graphic.material : null);
|
||||
}
|
||||
|
||||
_hasOriginalMaterials = true;
|
||||
}
|
||||
|
||||
private void RestoreOriginalMaterials()
|
||||
{
|
||||
if (!_hasOriginalMaterials)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var graphic = _targetGraphics[i];
|
||||
if (graphic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
graphic.material = _originalMaterials[i];
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
|
||||
_originalMaterials.Clear();
|
||||
_hasOriginalMaterials = false;
|
||||
}
|
||||
|
||||
private void SetTargetsMaterialDirty()
|
||||
{
|
||||
foreach (var graphic in _targetGraphics)
|
||||
{
|
||||
if (graphic != null)
|
||||
{
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyRuntimeMaterial(bool immediate)
|
||||
{
|
||||
if (_runtimeMaterials.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreOriginalMaterials();
|
||||
|
||||
foreach (var runtimeMaterial in _runtimeMaterials)
|
||||
{
|
||||
if (runtimeMaterial == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
DestroyImmediate(runtimeMaterial);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(runtimeMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
_runtimeMaterials.Clear();
|
||||
_isPreviewMaterial = false;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void RefreshEditModePreview()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_previewInEditMode)
|
||||
{
|
||||
RestoreEditModeMaterial();
|
||||
return;
|
||||
}
|
||||
|
||||
CreateRuntimeMaterial(true);
|
||||
ApplyMaterialProperties(_previewAmount);
|
||||
}
|
||||
|
||||
private void RestoreEditModeMaterial()
|
||||
{
|
||||
if (!_isPreviewMaterial)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyRuntimeMaterial(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Effects/UIEdgeDarken.cs.meta
Normal file
3
Assets/Scripts/Framework/Effects/UIEdgeDarken.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cb0728fb51e41b2aa93ec51993d9150
|
||||
timeCreated: 1770379821
|
||||
399
Assets/Scripts/Framework/Effects/UIFogReveal.cs
Normal file
399
Assets/Scripts/Framework/Effects/UIFogReveal.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Game.Effects
|
||||
{
|
||||
[ExecuteAlways]
|
||||
public class UIFogReveal : MonoBehaviour
|
||||
{
|
||||
// ── Shader property IDs ───────────────────────────────────────
|
||||
private static readonly int _fogAmountId = Shader.PropertyToID("_FogAmount");
|
||||
private static readonly int _fogColorId = Shader.PropertyToID("_FogColor");
|
||||
private static readonly int _fogIntensityId = Shader.PropertyToID("_FogIntensity");
|
||||
private static readonly int _edgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
|
||||
private static readonly int _noiseScaleId = Shader.PropertyToID("_NoiseScale");
|
||||
private static readonly int _driftSpeedId = Shader.PropertyToID("_DriftSpeed");
|
||||
private static readonly int _densityVariationId = Shader.PropertyToID("_DensityVariation");
|
||||
private static readonly int _aspectRatioId = Shader.PropertyToID("_AspectRatio");
|
||||
private static readonly int _fogMotionId = Shader.PropertyToID("_FogMotion");
|
||||
private static readonly int _useSpriteId = Shader.PropertyToID("_UseSprite");
|
||||
|
||||
// ── Inspector ─────────────────────────────────────────────────
|
||||
[Header("References")]
|
||||
[SerializeField] private Image _image;
|
||||
[SerializeField] private Material _fogMaterialTemplate;
|
||||
|
||||
[Header("Fog Settings")]
|
||||
[SerializeField] private Color _fogColor = new(0.18f, 0.20f, 0.26f, 1f);
|
||||
[SerializeField][Range(0f, 1f)] private float _fogIntensity = 0.95f;
|
||||
[SerializeField][Range(0.05f, 1f)] private float _edgeSoftness = 0.55f;
|
||||
[SerializeField][Range(1f, 20f)] private float _noiseScale = 5f;
|
||||
[SerializeField][Range(0f, 0.5f)] private float _driftSpeed = 0.04f;
|
||||
[SerializeField][Range(0f, 1f)] private float _densityVariation = 0.28f;
|
||||
[SerializeField] private bool _fogMotion = true;
|
||||
[SerializeField] private bool _useSprite = false;
|
||||
|
||||
[Header("Fog Range")]
|
||||
[SerializeField][Range(0f, 1f)] private float _startFog = 0f;
|
||||
[SerializeField][Range(0f, 1f)] private float _maxFog = 0.6f;
|
||||
|
||||
[Header("Screen")]
|
||||
[SerializeField] private bool _autoAspectRatio = true;
|
||||
[SerializeField] private float _aspectRatio = 1.777f;
|
||||
|
||||
[Header("Tween")]
|
||||
[SerializeField] private float _duration = 1.5f;
|
||||
[SerializeField] private Ease _ease = Ease.InOutSine;
|
||||
[SerializeField] private bool _useUnscaledTime = true;
|
||||
|
||||
[Header("Delay")]
|
||||
[SerializeField][Range(0f, 5f)] private float _fogInDelay = 0f;
|
||||
[SerializeField][Range(0f, 5f)] private float _fogOutDelay = 0f;
|
||||
|
||||
[Header("Editor Preview")]
|
||||
[SerializeField] private bool _previewInEditMode;
|
||||
[SerializeField][Range(0f, 1f)] private float _previewFogAmount;
|
||||
|
||||
// ── Runtime state ─────────────────────────────────────────────
|
||||
private Material _runtimeMaterial;
|
||||
private bool _isPreviewMaterial;
|
||||
private Sequence _fogSequence;
|
||||
private CancellationTokenSource _fogCts;
|
||||
private DestructionService _destructionService;
|
||||
|
||||
// ── Public properties ─────────────────────────────────────────
|
||||
public float FogAmount
|
||||
{
|
||||
get => _runtimeMaterial != null ? _runtimeMaterial.GetFloat(_fogAmountId) : 0f;
|
||||
set => SetFogAmount(value);
|
||||
}
|
||||
|
||||
public float FogInDuration => _fogInDelay + _duration;
|
||||
public float FogOutDuration => _fogOutDelay + _duration;
|
||||
|
||||
public bool FogMotion
|
||||
{
|
||||
get => _fogMotion;
|
||||
set
|
||||
{
|
||||
_fogMotion = value;
|
||||
_runtimeMaterial?.SetFloat(_fogMotionId, value ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseSprite
|
||||
{
|
||||
get => _useSprite;
|
||||
set
|
||||
{
|
||||
_useSprite = value;
|
||||
_runtimeMaterial?.SetFloat(_useSpriteId, value ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public float MaxFog
|
||||
{
|
||||
get => _maxFog;
|
||||
set => _maxFog = value;
|
||||
}
|
||||
|
||||
// ── DI ────────────────────────────────────────────────────────
|
||||
[Inject]
|
||||
public void Construct(DestructionService destructionService)
|
||||
{
|
||||
_destructionService = destructionService;
|
||||
}
|
||||
|
||||
// ── Unity lifecycle ───────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
CreateRuntimeMaterial(false);
|
||||
SetFogAmount(_startFog);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
RestoreEditModeMaterial();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!_autoAspectRatio) return;
|
||||
|
||||
var canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas == null) return;
|
||||
|
||||
var rt = canvas.GetComponent<RectTransform>();
|
||||
var ratio = rt.rect.width / Mathf.Max(rt.rect.height, 0.001f);
|
||||
_runtimeMaterial?.SetFloat(_aspectRatioId, ratio);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelFog();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DestroyRuntimeMaterial(false);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
ApplyShaderSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
RestoreEditModeMaterial();
|
||||
RefreshEditModePreview();
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Animate fog in (startFog → maxFog). Respects _fogInDelay.</summary>
|
||||
public UniTask FogIn() => TweenFogWithDelay(_startFog, _maxFog, _duration, _fogInDelay);
|
||||
|
||||
/// <summary>Animate fog in over a custom duration, no delay.</summary>
|
||||
public UniTask FogIn(float duration) => TweenFog(_startFog, _maxFog, duration);
|
||||
|
||||
/// <summary>Animate fog out (maxFog → startFog). Respects _fogOutDelay.</summary>
|
||||
public UniTask FogOut() => TweenFogWithDelay(_maxFog, _startFog, _duration, _fogOutDelay);
|
||||
|
||||
/// <summary>Animate fog out over a custom duration, no delay.</summary>
|
||||
public UniTask FogOut(float duration) => TweenFog(_maxFog, _startFog, duration);
|
||||
|
||||
/// <summary>Animate fog to an arbitrary target amount, no delay.</summary>
|
||||
public UniTask FogTo(float target, float duration) => TweenFog(FogAmount, target, duration);
|
||||
|
||||
/// <summary>Snap fog to a value immediately, cancels any running tween.</summary>
|
||||
public void FogSet(float amount)
|
||||
{
|
||||
CancelFog();
|
||||
SetFogAmount(amount);
|
||||
}
|
||||
|
||||
/// <summary>Snap fog to startFog immediately.</summary>
|
||||
public void FogReset()
|
||||
{
|
||||
CancelFog();
|
||||
SetFogAmount(_startFog);
|
||||
}
|
||||
|
||||
public void CancelFog()
|
||||
{
|
||||
if (_fogSequence.isAlive)
|
||||
{
|
||||
_fogSequence.Stop();
|
||||
_fogSequence = default;
|
||||
}
|
||||
|
||||
if (_fogCts != null)
|
||||
{
|
||||
_fogCts.Cancel();
|
||||
_fogCts.Dispose();
|
||||
_fogCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask TweenFog(float from, float to, float duration)
|
||||
{
|
||||
await TweenFogWithDelay(from, to, duration, 0f);
|
||||
}
|
||||
|
||||
public async UniTask TweenFogWithDelay(float from, float to, float duration, float delay)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
CreateRuntimeMaterial(false);
|
||||
|
||||
CancelFog();
|
||||
|
||||
_fogCts = new CancellationTokenSource();
|
||||
SetFogAmount(from);
|
||||
|
||||
try
|
||||
{
|
||||
if (delay > 0f)
|
||||
{
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(delay),
|
||||
ignoreTimeScale: _useUnscaledTime,
|
||||
cancellationToken: _fogCts.Token);
|
||||
}
|
||||
|
||||
_fogSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
to,
|
||||
Mathf.Max(0f, duration),
|
||||
SetFogAmount,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
await _fogSequence.ToUniTask(cancellationToken: _fogCts.Token);
|
||||
SetFogAmount(to);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted — normal, swallow it.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fogSequence = default;
|
||||
|
||||
if (_fogCts != null)
|
||||
{
|
||||
_fogCts.Dispose();
|
||||
_fogCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private void SetFogAmount(float amount)
|
||||
{
|
||||
if (_runtimeMaterial == null) return;
|
||||
_runtimeMaterial.SetFloat(_fogAmountId, Mathf.Clamp01(amount));
|
||||
_image?.SetMaterialDirty();
|
||||
}
|
||||
|
||||
private void ApplyShaderSettings()
|
||||
{
|
||||
if (_runtimeMaterial == null) return;
|
||||
|
||||
_runtimeMaterial.SetColor(_fogColorId, _fogColor);
|
||||
_runtimeMaterial.SetFloat(_fogIntensityId, _fogIntensity);
|
||||
_runtimeMaterial.SetFloat(_edgeSoftnessId, _edgeSoftness);
|
||||
_runtimeMaterial.SetFloat(_noiseScaleId, _noiseScale);
|
||||
_runtimeMaterial.SetFloat(_driftSpeedId, _driftSpeed);
|
||||
_runtimeMaterial.SetFloat(_densityVariationId, _densityVariation);
|
||||
_runtimeMaterial.SetFloat(_fogMotionId, _fogMotion ? 1f : 0f);
|
||||
_runtimeMaterial.SetFloat(_useSpriteId, _useSprite ? 1f : 0f);
|
||||
|
||||
if (!_autoAspectRatio)
|
||||
_runtimeMaterial.SetFloat(_aspectRatioId, _aspectRatio);
|
||||
|
||||
_image?.SetMaterialDirty();
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_image == null)
|
||||
_image = GetComponent<Image>();
|
||||
}
|
||||
|
||||
private void CreateRuntimeMaterial(bool isPreviewMaterial)
|
||||
{
|
||||
if (_image == null) return;
|
||||
|
||||
var source = _fogMaterialTemplate != null
|
||||
? _fogMaterialTemplate
|
||||
: _image.material;
|
||||
|
||||
if (source == null) return;
|
||||
|
||||
if (_runtimeMaterial != null)
|
||||
{
|
||||
_image.material = _runtimeMaterial;
|
||||
_image.SetMaterialDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial = Instantiate(source);
|
||||
_runtimeMaterial.name = isPreviewMaterial
|
||||
? $"{nameof(UIFogReveal)} Preview Material"
|
||||
: $"{nameof(UIFogReveal)} Runtime Material";
|
||||
|
||||
_isPreviewMaterial = isPreviewMaterial;
|
||||
|
||||
if (isPreviewMaterial)
|
||||
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
|
||||
|
||||
_image.material = _runtimeMaterial;
|
||||
_image.SetMaterialDirty();
|
||||
|
||||
ApplyShaderSettings();
|
||||
}
|
||||
|
||||
private void DestroyRuntimeMaterial(bool immediate)
|
||||
{
|
||||
if (_runtimeMaterial == null) return;
|
||||
|
||||
if (_image != null)
|
||||
{
|
||||
_image.material = null;
|
||||
_image.SetMaterialDirty();
|
||||
}
|
||||
|
||||
if (immediate)
|
||||
DestroyImmediate(_runtimeMaterial);
|
||||
else
|
||||
Destroy(_runtimeMaterial);
|
||||
|
||||
_runtimeMaterial = null;
|
||||
_isPreviewMaterial = false;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void RefreshEditModePreview()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
if (!_previewInEditMode)
|
||||
{
|
||||
RestoreEditModeMaterial();
|
||||
return;
|
||||
}
|
||||
|
||||
CreateRuntimeMaterial(true);
|
||||
ApplyShaderSettings();
|
||||
SetFogAmount(_previewFogAmount);
|
||||
}
|
||||
|
||||
private void RestoreEditModeMaterial()
|
||||
{
|
||||
if (!_isPreviewMaterial) return;
|
||||
DestroyRuntimeMaterial(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Effects/UIFogReveal.cs.meta
Normal file
3
Assets/Scripts/Framework/Effects/UIFogReveal.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 912c8bc1a5f84113848a078f0581c8ce
|
||||
timeCreated: 1778334335
|
||||
249
Assets/Scripts/Framework/Effects/UILightGlow.cs
Normal file
249
Assets/Scripts/Framework/Effects/UILightGlow.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.Framework.Effects
|
||||
{
|
||||
[RequireComponent(typeof(Image))]
|
||||
public class UILightGlow : MonoBehaviour
|
||||
{
|
||||
private const string _lightShaderName = "BriarQueen/UI/Light Glow";
|
||||
|
||||
private static readonly int _lightColorId = Shader.PropertyToID("_LightColor");
|
||||
private static readonly int _intensityId = Shader.PropertyToID("_Intensity");
|
||||
private static readonly int _flickerOffsetId = Shader.PropertyToID("_FlickerOffset");
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField]
|
||||
private Image _image;
|
||||
|
||||
[SerializeField]
|
||||
private Material _lightMaterialTemplate;
|
||||
|
||||
[Header("Light")]
|
||||
[SerializeField]
|
||||
private Color _startingColor = new(1f, 0.78f, 0.35f, 1f);
|
||||
|
||||
[SerializeField]
|
||||
private float _startingIntensity = 1.5f;
|
||||
|
||||
[SerializeField]
|
||||
private bool _randomizeFlickerOffset = true;
|
||||
|
||||
[Header("Tween")]
|
||||
[SerializeField]
|
||||
private float _defaultTweenDuration = 0.35f;
|
||||
|
||||
[SerializeField]
|
||||
private Ease _ease = Ease.InOutSine;
|
||||
|
||||
[SerializeField]
|
||||
private bool _useUnscaledTime = true;
|
||||
|
||||
private Material _runtimeMaterial;
|
||||
private Sequence _lightSequence;
|
||||
private CancellationTokenSource _lightCts;
|
||||
|
||||
public Color LightColor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return _startingColor;
|
||||
}
|
||||
|
||||
return _runtimeMaterial.GetColor(_lightColorId);
|
||||
}
|
||||
}
|
||||
|
||||
public float Intensity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return _startingIntensity;
|
||||
}
|
||||
|
||||
return _runtimeMaterial.GetFloat(_intensityId);
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_image == null)
|
||||
{
|
||||
_image = GetComponent<Image>();
|
||||
}
|
||||
|
||||
CreateRuntimeMaterial();
|
||||
SetLightColor(_startingColor);
|
||||
SetIntensity(_startingIntensity);
|
||||
|
||||
if (_randomizeFlickerOffset && _runtimeMaterial != null)
|
||||
{
|
||||
_runtimeMaterial.SetFloat(_flickerOffsetId, UnityEngine.Random.Range(0f, 100f));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTween();
|
||||
|
||||
if (_runtimeMaterial != null)
|
||||
{
|
||||
Destroy(_runtimeMaterial);
|
||||
_runtimeMaterial = null;
|
||||
}
|
||||
}
|
||||
|
||||
public UniTask ChangeColor(Color targetColor)
|
||||
{
|
||||
return ChangeColor(targetColor, _defaultTweenDuration);
|
||||
}
|
||||
|
||||
public UniTask ChangeColor(Color targetColor, float duration)
|
||||
{
|
||||
return TweenTo(targetColor, Intensity, duration);
|
||||
}
|
||||
|
||||
public UniTask ChangeIntensity(float targetIntensity)
|
||||
{
|
||||
return ChangeIntensity(targetIntensity, _defaultTweenDuration);
|
||||
}
|
||||
|
||||
public UniTask ChangeIntensity(float targetIntensity, float duration)
|
||||
{
|
||||
return TweenTo(LightColor, targetIntensity, duration);
|
||||
}
|
||||
|
||||
public UniTask TurnOff()
|
||||
{
|
||||
return ChangeIntensity(0f, _defaultTweenDuration);
|
||||
}
|
||||
|
||||
public UniTask TurnOn()
|
||||
{
|
||||
return ChangeIntensity(_startingIntensity, _defaultTweenDuration);
|
||||
}
|
||||
|
||||
public async UniTask TweenTo(Color targetColor, float targetIntensity, float duration)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
CreateRuntimeMaterial();
|
||||
}
|
||||
|
||||
CancelTween();
|
||||
|
||||
var fromColor = LightColor;
|
||||
var fromIntensity = Intensity;
|
||||
var safeDuration = Mathf.Max(0f, duration);
|
||||
|
||||
_lightCts = new CancellationTokenSource();
|
||||
|
||||
_lightSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
0f,
|
||||
1f,
|
||||
safeDuration,
|
||||
progress =>
|
||||
{
|
||||
SetLightColor(Color.LerpUnclamped(fromColor, targetColor, progress));
|
||||
SetIntensity(Mathf.LerpUnclamped(fromIntensity, targetIntensity, progress));
|
||||
},
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
try
|
||||
{
|
||||
await _lightSequence.ToUniTask(cancellationToken: _lightCts.Token);
|
||||
SetLightColor(targetColor);
|
||||
SetIntensity(targetIntensity);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another light tween or object destruction.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lightSequence = default;
|
||||
|
||||
if (_lightCts != null)
|
||||
{
|
||||
_lightCts.Dispose();
|
||||
_lightCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLightColor(Color color)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial.SetColor(_lightColorId, color);
|
||||
}
|
||||
|
||||
public void SetIntensity(float intensity)
|
||||
{
|
||||
if (_runtimeMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial.SetFloat(_intensityId, Mathf.Max(0f, intensity));
|
||||
}
|
||||
|
||||
public void CancelTween()
|
||||
{
|
||||
if (_lightSequence.isAlive)
|
||||
{
|
||||
_lightSequence.Stop();
|
||||
_lightSequence = default;
|
||||
}
|
||||
|
||||
if (_lightCts != null)
|
||||
{
|
||||
_lightCts.Cancel();
|
||||
_lightCts.Dispose();
|
||||
_lightCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRuntimeMaterial()
|
||||
{
|
||||
if (_image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lightMaterialTemplate != null)
|
||||
{
|
||||
_runtimeMaterial = Instantiate(_lightMaterialTemplate);
|
||||
_image.material = _runtimeMaterial;
|
||||
return;
|
||||
}
|
||||
|
||||
var shader = Shader.Find(_lightShaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(UILightGlow)}] Could not find shader '{_lightShaderName}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeMaterial = new Material(shader)
|
||||
{
|
||||
name = $"{nameof(UILightGlow)} Runtime Material"
|
||||
};
|
||||
|
||||
_image.material = _runtimeMaterial;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Framework/Effects/UILightGlow.cs.meta
Normal file
2
Assets/Scripts/Framework/Effects/UILightGlow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3e14c91f3c942fc84e800d2fb583fb0
|
||||
@@ -3,5 +3,5 @@ using BriarQueen.Framework.Events.System;
|
||||
|
||||
namespace BriarQueen.Framework.Events.UI
|
||||
{
|
||||
public record DisplayTutorialPopupEvent(TutorialPopupID TutorialID) : IEvent;
|
||||
public record DisplayTutorialPopupEvent(TutorialPopupID TutorialID, string ResolvedText) : IEvent;
|
||||
}
|
||||
@@ -8,9 +8,5 @@ namespace BriarQueen.Framework.Events.UI
|
||||
SolidColor = 0, // Left for Compat.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publish with color != Color.clear to fade in.
|
||||
/// Publish with color == Color.clear to fade out (uses last active style).
|
||||
/// </summary>
|
||||
public record FadeEvent(bool Hidden, float Duration = 0.25f) : IEvent;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using BriarQueen.Framework.Events.System;
|
||||
|
||||
namespace BriarQueen.Framework.Events.UI
|
||||
{
|
||||
public record UIBackRequestedEvent : IEvent;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cf9029b5f1a4c4f8740cb7394c1b5f8
|
||||
timeCreated: 1778300000
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Assets.Components;
|
||||
using BriarQueen.Framework.Managers.Assets.Components;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
@@ -11,7 +11,7 @@ using UnityEngine.SceneManagement;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace BriarQueen.Framework.Assets
|
||||
namespace BriarQueen.Framework.Managers.Assets
|
||||
{
|
||||
public class AddressableManager : IDisposable
|
||||
{
|
||||
@@ -31,14 +31,18 @@ namespace BriarQueen.Framework.Assets
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var handle in _instanceHandles.Values)
|
||||
{
|
||||
if (handle.IsValid())
|
||||
Addressables.ReleaseInstance(handle);
|
||||
}
|
||||
|
||||
_instanceHandles.Clear();
|
||||
|
||||
foreach (var handle in _assetHandles.Values)
|
||||
{
|
||||
if (handle.IsValid())
|
||||
Addressables.Release(handle);
|
||||
}
|
||||
|
||||
_assetHandles.Clear();
|
||||
}
|
||||
@@ -67,7 +71,8 @@ namespace BriarQueen.Framework.Assets
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (handle.IsValid()) Addressables.Release(handle);
|
||||
if (handle.IsValid())
|
||||
Addressables.Release(handle);
|
||||
|
||||
throw;
|
||||
}
|
||||
@@ -81,7 +86,8 @@ namespace BriarQueen.Framework.Assets
|
||||
{
|
||||
if (_assetHandles.TryGetValue(asset, out var handle))
|
||||
{
|
||||
if (handle.IsValid()) Addressables.Release(handle);
|
||||
if (handle.IsValid())
|
||||
Addressables.Release(handle);
|
||||
|
||||
_assetHandles.Remove(asset);
|
||||
}
|
||||
@@ -97,8 +103,20 @@ namespace BriarQueen.Framework.Assets
|
||||
)
|
||||
{
|
||||
var handle = Addressables.LoadSceneAsync(assetReference, loadSceneMode, autoLoad);
|
||||
await handle.ToUniTask(progress, cancellationToken: cancellationToken);
|
||||
return handle;
|
||||
|
||||
try
|
||||
{
|
||||
await handle.ToUniTask(progress, cancellationToken: cancellationToken);
|
||||
return handle;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (handle.IsValid())
|
||||
Addressables.Release(handle);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async UniTask UnloadSceneAsync(AsyncOperationHandle<SceneInstance> sceneHandle)
|
||||
@@ -116,6 +134,9 @@ namespace BriarQueen.Framework.Assets
|
||||
)
|
||||
{
|
||||
var handle = Addressables.InstantiateAsync(reference, position, rotation, parent);
|
||||
GameObject go = null;
|
||||
NotifyOnDestruction notify = null;
|
||||
Action onInstanceDestroyed = null;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -127,33 +148,32 @@ namespace BriarQueen.Framework.Assets
|
||||
return null;
|
||||
}
|
||||
|
||||
var go = handle.Result;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_instanceHandles[go] = handle;
|
||||
}
|
||||
go = handle.Result;
|
||||
|
||||
var prefabScope = go.GetComponent<LifetimeScope>();
|
||||
var injectionScope = scope ?? prefabScope?.Container ?? _lifetimeContainer;
|
||||
|
||||
injectionScope.InjectGameObject(go);
|
||||
|
||||
var notify = go.GetComponent<NotifyOnDestruction>();
|
||||
notify = go.GetComponent<NotifyOnDestruction>();
|
||||
|
||||
if (!notify)
|
||||
{
|
||||
notify = go.AddComponent<NotifyOnDestruction>();
|
||||
injectionScope.Inject(notify);
|
||||
}
|
||||
|
||||
void OnInstanceDestroyed()
|
||||
onInstanceDestroyed = () =>
|
||||
{
|
||||
TryReleaseInstance(go);
|
||||
notify.OnDestroyedCalled -= OnInstanceDestroyed;
|
||||
}
|
||||
notify.OnDestroyedCalled -= onInstanceDestroyed;
|
||||
};
|
||||
|
||||
notify.OnDestroyedCalled += OnInstanceDestroyed;
|
||||
notify.OnDestroyedCalled += onInstanceDestroyed;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_instanceHandles[go] = handle;
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
@@ -161,6 +181,17 @@ namespace BriarQueen.Framework.Assets
|
||||
{
|
||||
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (notify != null && onInstanceDestroyed != null)
|
||||
{
|
||||
notify.OnDestroyedCalled -= onInstanceDestroyed;
|
||||
}
|
||||
|
||||
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -201,4 +232,4 @@ namespace BriarQueen.Framework.Assets
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,42 @@ using System;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Assets.Components
|
||||
namespace BriarQueen.Framework.Managers.Assets.Components
|
||||
{
|
||||
public class NotifyOnDestruction : MonoBehaviour, IDestructible
|
||||
{
|
||||
private DestructionService _destructionService;
|
||||
private bool _destroyedNotified;
|
||||
|
||||
public UniTask OnPreDestroy()
|
||||
{
|
||||
OnPreDestroyCalled?.Invoke();
|
||||
return UniTask.CompletedTask; // No async operation needed, just a notification
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public UniTask OnDestroyed()
|
||||
{
|
||||
OnDestroyedCalled?.Invoke();
|
||||
return UniTask.CompletedTask; // No async operation needed, just a notification
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(DestructionService destructionService)
|
||||
{
|
||||
_destructionService = destructionService;
|
||||
RaiseDestroyedOnce();
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public event Action OnPreDestroyCalled;
|
||||
public event Action OnDestroyedCalled;
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RaiseDestroyedOnce();
|
||||
}
|
||||
|
||||
private void RaiseDestroyedOnce()
|
||||
{
|
||||
if (_destroyedNotified)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_destroyedNotified = true;
|
||||
OnDestroyedCalled?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace BriarQueen.Framework.Assets
|
||||
namespace BriarQueen.Framework.Managers.Assets
|
||||
{
|
||||
public interface IAssetProvider
|
||||
{
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
@@ -23,41 +22,38 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
/// - Settings set "base" volumes (in dB) per mixer parameter.
|
||||
/// - Runtime states (Pause duck, Voice duck) apply "modifiers" (extra dB offsets).
|
||||
/// - Effective mixer value is always: effectiveDb = baseDb + modifiersDb
|
||||
/// - UI and Ambience route through SFX channel/group.
|
||||
/// - SFX pool is transient — new channels are spawned on demand and
|
||||
/// finished channels are reaped before each play.
|
||||
/// </summary>
|
||||
public class AudioManager : IDisposable, IManager
|
||||
{
|
||||
private const int INITIAL_AMBIENCE_SOURCES = 3;
|
||||
private const int INITIAL_SFX_SOURCES = 6;
|
||||
|
||||
private const float PAUSE_DUCK_TARGET_DB = -18f;
|
||||
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
|
||||
private const int INITIAL_SFX_SOURCES = 8;
|
||||
private const float PAUSE_DUCK_TARGET_DB = -18f;
|
||||
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
|
||||
private const float DEFAULT_VOICE_DUCK_TARGET_DB = -20f;
|
||||
|
||||
private readonly AudioMixer _audioMixer;
|
||||
private readonly AudioRegistry _audioRegistry;
|
||||
private readonly AudioMixer _audioMixer;
|
||||
private readonly AudioRegistry _audioRegistry;
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
|
||||
private readonly Dictionary<string, float> _baseDb = new();
|
||||
private readonly List<GameObject> _createdAudioObjects = new();
|
||||
private readonly List<AudioSource> _ambienceSources = new();
|
||||
private readonly List<AudioFileSo> _currentAmbienceTracks = new();
|
||||
private readonly List<SfxChannel> _sfxChannels = new();
|
||||
private readonly Dictionary<string, float> _baseDb = new();
|
||||
private readonly List<GameObject> _createdAudioObjects = new();
|
||||
private readonly List<SfxChannel> _sfxChannels = new();
|
||||
|
||||
private AudioSource _musicSourceA;
|
||||
private AudioSource _musicSourceB;
|
||||
private AudioSource _voiceSource;
|
||||
private AudioSource _uiSource;
|
||||
|
||||
private string _activeVoiceSubtitleId;
|
||||
private string _activeVoiceSubtitleId;
|
||||
private AudioFileSo _currentMusicTrack;
|
||||
|
||||
private CancellationTokenSource _musicDuckCts;
|
||||
private CancellationTokenSource _musicFadeCts;
|
||||
private CancellationTokenSource _voiceCts;
|
||||
|
||||
private float _musicDuckDbCurrent;
|
||||
private float _pauseDuckDbCurrent;
|
||||
|
||||
private float _musicDuckDbCurrent;
|
||||
private float _pauseDuckDbCurrent;
|
||||
private Sequence _musicDuckSequence;
|
||||
private Sequence _pauseDuckSequence;
|
||||
|
||||
@@ -69,8 +65,8 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
[Inject]
|
||||
public AudioManager(AudioMixer mainMixer, AudioRegistry audioRegistry, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_audioMixer = mainMixer;
|
||||
_audioRegistry = audioRegistry;
|
||||
_audioMixer = mainMixer;
|
||||
_audioRegistry = audioRegistry;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
@@ -137,53 +133,41 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
}
|
||||
|
||||
_createdAudioObjects.Clear();
|
||||
_ambienceSources.Clear();
|
||||
_sfxChannels.Clear();
|
||||
_currentAmbienceTracks.Clear();
|
||||
_baseDb.Clear();
|
||||
|
||||
_musicSourceA = null;
|
||||
_musicSourceB = null;
|
||||
_voiceSource = null;
|
||||
_uiSource = null;
|
||||
_currentMusicTrack = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_musicSourceA = null;
|
||||
_musicSourceB = null;
|
||||
_voiceSource = null;
|
||||
_currentMusicTrack = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_voiceFinishedPublished = false;
|
||||
Initialized = false;
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
// ── Source creation ───────────────────────────────────────────
|
||||
|
||||
private void CreateSources()
|
||||
{
|
||||
_musicSourceA = CreateAudioSource("Music_Source_A", AudioMixerGroups.MUSIC_GROUP);
|
||||
_musicSourceB = CreateAudioSource("Music_Source_B", AudioMixerGroups.MUSIC_GROUP);
|
||||
|
||||
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
|
||||
_uiSource = CreateAudioSource("UI_Source", AudioMixerGroups.UI_GROUP);
|
||||
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
|
||||
|
||||
for (var i = 0; i < INITIAL_SFX_SOURCES; i++)
|
||||
{
|
||||
var src = CreateAudioSource($"SFX_Source_{i}", AudioMixerGroups.SFX_GROUP);
|
||||
_sfxChannels.Add(new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = -999f
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < INITIAL_AMBIENCE_SOURCES; i++)
|
||||
{
|
||||
_ambienceSources.Add(CreateAudioSource($"Ambience_Source_{i}", AudioMixerGroups.AMBIENCE_GROUP));
|
||||
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Volume ────────────────────────────────────────────────────
|
||||
|
||||
private void PrimeMixerBaseValues()
|
||||
{
|
||||
PrimeBaseFromMixer(AudioMixerParameters.MASTER_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.MUSIC_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.SFX_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.VOICE_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.UI_VOLUME);
|
||||
}
|
||||
|
||||
public void SetVolume(string parameter, float value01)
|
||||
@@ -194,25 +178,18 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
return;
|
||||
}
|
||||
|
||||
var linear = Mathf.Clamp01(value01);
|
||||
var db = Linear01ToDb(linear);
|
||||
|
||||
_baseDb[parameter] = db;
|
||||
_baseDb[parameter] = Linear01ToDb(Mathf.Clamp01(value01));
|
||||
ApplyEffectiveVolume(parameter);
|
||||
}
|
||||
|
||||
private static float Linear01ToDb(float linear01)
|
||||
{
|
||||
var lin = Mathf.Max(linear01, 0.0001f);
|
||||
return Mathf.Log10(lin) * 20f;
|
||||
return Mathf.Log10(Mathf.Max(linear01, 0.0001f)) * 20f;
|
||||
}
|
||||
|
||||
private void PrimeBaseFromMixer(string parameter)
|
||||
{
|
||||
if (_audioMixer != null && _audioMixer.GetFloat(parameter, out var db))
|
||||
_baseDb[parameter] = db;
|
||||
else
|
||||
_baseDb[parameter] = 0f;
|
||||
_baseDb[parameter] = _audioMixer != null && _audioMixer.GetFloat(parameter, out var db) ? db : 0f;
|
||||
}
|
||||
|
||||
private void ApplyAllEffectiveVolumes()
|
||||
@@ -220,9 +197,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MASTER_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.VOICE_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.UI_VOLUME);
|
||||
}
|
||||
|
||||
private void ApplyEffectiveVolume(string parameter)
|
||||
@@ -236,8 +211,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
var effective = baseDb;
|
||||
|
||||
if (parameter == AudioMixerParameters.MUSIC_VOLUME ||
|
||||
parameter == AudioMixerParameters.SFX_VOLUME ||
|
||||
parameter == AudioMixerParameters.AMBIENCE_VOLUME)
|
||||
parameter == AudioMixerParameters.SFX_VOLUME)
|
||||
{
|
||||
effective += _pauseDuckDbCurrent;
|
||||
}
|
||||
@@ -248,10 +222,11 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_audioMixer.SetFloat(parameter, effective);
|
||||
}
|
||||
|
||||
// ── UI stack / pause duck ─────────────────────────────────────
|
||||
|
||||
private void OnUIStackChanged(UIStackChangedEvent e)
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
if (!Initialized) return;
|
||||
|
||||
if (e.AnyUIOpen)
|
||||
OnGamePausedInternal().Forget();
|
||||
@@ -279,20 +254,18 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_pauseDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _pauseDuckDbCurrent;
|
||||
|
||||
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
Mathf.Max(0f, seconds),
|
||||
v =>
|
||||
{
|
||||
_pauseDuckDbCurrent = v;
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
},
|
||||
Ease.OutCubic,
|
||||
useUnscaledTime: true));
|
||||
@@ -303,15 +276,13 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
|
||||
public void PauseVoiceSource(bool paused)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null)
|
||||
return;
|
||||
|
||||
if (paused)
|
||||
_voiceSource.Pause();
|
||||
else
|
||||
_voiceSource.UnPause();
|
||||
if (!Initialized || _voiceSource == null) return;
|
||||
if (paused) _voiceSource.Pause();
|
||||
else _voiceSource.UnPause();
|
||||
}
|
||||
|
||||
// ── Play ──────────────────────────────────────────────────────
|
||||
|
||||
public void Play(string audioName)
|
||||
{
|
||||
if (!Initialized)
|
||||
@@ -344,21 +315,11 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
break;
|
||||
|
||||
case TrackType.Ambience:
|
||||
if (!_currentAmbienceTracks.Contains(audioData))
|
||||
{
|
||||
_currentAmbienceTracks.Add(audioData);
|
||||
PlayOnAvailableAmbienceSource(audioData);
|
||||
}
|
||||
break;
|
||||
|
||||
case TrackType.UIFX:
|
||||
case TrackType.Sfx:
|
||||
PlaySfx(audioData);
|
||||
break;
|
||||
|
||||
case TrackType.UIFX:
|
||||
PlayOneShotAsync(_uiSource, audioData).Forget();
|
||||
break;
|
||||
|
||||
case TrackType.Voice:
|
||||
PlayVoiceLine(audioData).Forget();
|
||||
break;
|
||||
@@ -368,6 +329,8 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
|
||||
}
|
||||
|
||||
// ── Voice ─────────────────────────────────────────────────────
|
||||
|
||||
private async UniTaskVoid PlayVoiceLine(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null || audioData?.Clip == null)
|
||||
@@ -377,15 +340,15 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_voiceCts = new CancellationTokenSource();
|
||||
var token = _voiceCts.Token;
|
||||
|
||||
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
|
||||
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
|
||||
_voiceFinishedPublished = false;
|
||||
|
||||
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
|
||||
|
||||
_voiceSource.clip = audioData.Clip;
|
||||
_voiceSource.pitch = audioData.Pitch;
|
||||
_voiceSource.volume = audioData.Volume;
|
||||
_voiceSource.loop = false;
|
||||
_voiceSource.clip = audioData.Clip;
|
||||
_voiceSource.pitch = audioData.Pitch;
|
||||
_voiceSource.volume = audioData.Volume;
|
||||
_voiceSource.loop = false;
|
||||
_voiceSource.priority = audioData.Priority;
|
||||
_voiceSource.Play();
|
||||
|
||||
@@ -404,31 +367,210 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
|
||||
private void PublishVoiceFinishedIfNeeded()
|
||||
{
|
||||
if (_voiceFinishedPublished)
|
||||
return;
|
||||
if (_voiceFinishedPublished) return;
|
||||
|
||||
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
|
||||
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
|
||||
|
||||
_voiceFinishedPublished = true;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
}
|
||||
|
||||
public void StopVoice()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _voiceCts);
|
||||
|
||||
if (_voiceSource != null && _voiceSource.isPlaying)
|
||||
_voiceSource.Stop();
|
||||
|
||||
PublishVoiceFinishedIfNeeded();
|
||||
}
|
||||
|
||||
// ── SFX (transient pool) ──────────────────────────────────────
|
||||
|
||||
private void PlaySfx(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
// Reap finished channels first so we don't accumulate stale entries
|
||||
ReapFinishedSfxChannels();
|
||||
|
||||
// Try to find a free channel from the existing pool
|
||||
AudioSource src = null;
|
||||
var channelIndex = -1;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var s = _sfxChannels[i].Source;
|
||||
if (s != null && !s.isPlaying)
|
||||
{
|
||||
src = s;
|
||||
channelIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No free channel — spawn a transient one
|
||||
if (src == null)
|
||||
{
|
||||
src = CreateAudioSource($"SFX_Source_Transient_{_sfxChannels.Count}", AudioMixerGroups.SFX_GROUP);
|
||||
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
|
||||
channelIndex = _sfxChannels.Count - 1;
|
||||
|
||||
Debug.Log($"[AudioManager] SFX pool expanded to {_sfxChannels.Count} channels.");
|
||||
}
|
||||
|
||||
src.priority = audioData.Priority;
|
||||
src.pitch = audioData.Pitch;
|
||||
src.loop = audioData.Loopable;
|
||||
src.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
_sfxChannels[channelIndex] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = Time.unscaledTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes finished transient channels from the pool to prevent unbounded growth.
|
||||
/// Preserves the initial pool channels even when idle.
|
||||
/// </summary>
|
||||
private void ReapFinishedSfxChannels()
|
||||
{
|
||||
for (var i = _sfxChannels.Count - 1; i >= INITIAL_SFX_SOURCES; i--)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null || src.isPlaying)
|
||||
continue;
|
||||
|
||||
// Destroy the transient GameObject and remove from pool
|
||||
if (src.gameObject != null)
|
||||
{
|
||||
_createdAudioObjects.Remove(src.gameObject);
|
||||
Object.Destroy(src.gameObject);
|
||||
}
|
||||
|
||||
_sfxChannels.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllSfx()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
for (var i = _sfxChannels.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null) continue;
|
||||
|
||||
src.Stop();
|
||||
|
||||
// Destroy transient channels, reset initial ones
|
||||
if (i >= INITIAL_SFX_SOURCES)
|
||||
{
|
||||
if (src.gameObject != null)
|
||||
{
|
||||
_createdAudioObjects.Remove(src.gameObject);
|
||||
Object.Destroy(src.gameObject);
|
||||
}
|
||||
|
||||
_sfxChannels.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sfxChannels[i] = new SfxChannel { Source = src, StartedAtUnscaled = -999f };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Music ─────────────────────────────────────────────────────
|
||||
|
||||
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
|
||||
{
|
||||
if (!Initialized || !newTrack || !newTrack.Clip) return;
|
||||
if (_currentMusicTrack == newTrack) return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
_musicFadeCts = new CancellationTokenSource();
|
||||
var token = _musicFadeCts.Token;
|
||||
|
||||
var activeSource = _musicSourceA.isPlaying ? _musicSourceA
|
||||
: _musicSourceB.isPlaying ? _musicSourceB
|
||||
: null;
|
||||
|
||||
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
|
||||
|
||||
PlayOnSource(inactiveSource, newTrack);
|
||||
|
||||
if (activeSource == null)
|
||||
{
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
return;
|
||||
}
|
||||
|
||||
duration = Mathf.Max(0.0001f, duration);
|
||||
|
||||
var elapsed = 0f;
|
||||
var startVolume = activeSource.volume;
|
||||
|
||||
try
|
||||
{
|
||||
while (elapsed < duration)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var t = elapsed / duration;
|
||||
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
||||
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
||||
}
|
||||
|
||||
activeSource.volume = 0f;
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activeSource.Stop();
|
||||
activeSource.volume = startVolume;
|
||||
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
}
|
||||
|
||||
public void StopMusic()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
|
||||
if (_musicSourceA != null) { _musicSourceA.Stop(); _musicSourceA.clip = null; _musicSourceA.volume = 0f; }
|
||||
if (_musicSourceB != null) { _musicSourceB.Stop(); _musicSourceB.clip = null; _musicSourceB.volume = 0f; }
|
||||
|
||||
_currentMusicTrack = null;
|
||||
}
|
||||
|
||||
private async UniTask DuckMusicAsync(float clipLengthSeconds, float fadeTimeSeconds)
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _musicDuckCts);
|
||||
_musicDuckCts = new CancellationTokenSource();
|
||||
var token = _musicDuckCts.Token;
|
||||
|
||||
fadeTimeSeconds = Mathf.Max(0.0001f, fadeTimeSeconds);
|
||||
var duckTarget = DEFAULT_VOICE_DUCK_TARGET_DB;
|
||||
|
||||
try
|
||||
{
|
||||
await TweenMusicDuckTo(duckTarget, fadeTimeSeconds, token);
|
||||
await TweenMusicDuckTo(DEFAULT_VOICE_DUCK_TARGET_DB, fadeTimeSeconds, token);
|
||||
|
||||
var hold = clipLengthSeconds - fadeTimeSeconds * 2f;
|
||||
if (hold > 0.01f)
|
||||
@@ -450,15 +592,13 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _musicDuckDbCurrent;
|
||||
|
||||
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
Mathf.Max(0f, seconds),
|
||||
v =>
|
||||
{
|
||||
_musicDuckDbCurrent = v;
|
||||
@@ -471,258 +611,22 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
|
||||
{
|
||||
if (!Initialized || !newTrack || !newTrack.Clip)
|
||||
return;
|
||||
|
||||
if (_currentMusicTrack == newTrack)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
_musicFadeCts = new CancellationTokenSource();
|
||||
var token = _musicFadeCts.Token;
|
||||
|
||||
var activeSource = _musicSourceA.isPlaying
|
||||
? _musicSourceA
|
||||
: _musicSourceB.isPlaying
|
||||
? _musicSourceB
|
||||
: null;
|
||||
|
||||
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
|
||||
|
||||
PlayOnSource(inactiveSource, newTrack);
|
||||
|
||||
if (activeSource == null)
|
||||
{
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
return;
|
||||
}
|
||||
|
||||
duration = Mathf.Max(0.0001f, duration);
|
||||
|
||||
var elapsed = 0f;
|
||||
var startVolume = activeSource.volume;
|
||||
|
||||
try
|
||||
{
|
||||
while (elapsed < duration)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var t = elapsed / duration;
|
||||
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
||||
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
|
||||
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
||||
}
|
||||
|
||||
activeSource.volume = 0f;
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activeSource.Stop();
|
||||
activeSource.volume = startVolume;
|
||||
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
}
|
||||
|
||||
private void PlaySfx(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
var channelIndex = GetBestSfxChannelIndex(audioData.Priority);
|
||||
if (channelIndex < 0 || channelIndex >= _sfxChannels.Count)
|
||||
return;
|
||||
|
||||
var src = _sfxChannels[channelIndex].Source;
|
||||
if (src == null)
|
||||
return;
|
||||
|
||||
if (src.isPlaying)
|
||||
src.Stop();
|
||||
|
||||
src.priority = audioData.Priority;
|
||||
src.pitch = audioData.Pitch;
|
||||
src.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
_sfxChannels[channelIndex] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = Time.unscaledTime
|
||||
};
|
||||
}
|
||||
|
||||
private int GetBestSfxChannelIndex(int incomingPriority)
|
||||
{
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
if (!src.isPlaying)
|
||||
return i;
|
||||
}
|
||||
|
||||
var bestIndex = -1;
|
||||
var worstPriority = int.MinValue;
|
||||
var oldestStart = float.MaxValue;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
var p = src.priority;
|
||||
var started = _sfxChannels[i].StartedAtUnscaled;
|
||||
|
||||
if (p > worstPriority || (p == worstPriority && started < oldestStart))
|
||||
{
|
||||
worstPriority = p;
|
||||
oldestStart = started;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
public void StopAmbience(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || !audioData || !audioData.Clip || audioData.Type != TrackType.Ambience)
|
||||
return;
|
||||
|
||||
if (_currentAmbienceTracks.Remove(audioData))
|
||||
{
|
||||
foreach (var source in _ambienceSources.Where(s => s != null && s.clip == audioData.Clip))
|
||||
source.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllAmbience()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
foreach (var s in _ambienceSources)
|
||||
{
|
||||
if (s != null)
|
||||
s.Stop();
|
||||
}
|
||||
|
||||
_currentAmbienceTracks.Clear();
|
||||
}
|
||||
|
||||
private void PlayOnAvailableAmbienceSource(AudioFileSo audioData)
|
||||
{
|
||||
var source = _ambienceSources.FirstOrDefault(s => s != null && !s.isPlaying);
|
||||
if (source == null)
|
||||
{
|
||||
source = CreateAudioSource(
|
||||
$"Ambience_Source_{_ambienceSources.Count}",
|
||||
AudioMixerGroups.AMBIENCE_GROUP);
|
||||
|
||||
_ambienceSources.Add(source);
|
||||
}
|
||||
|
||||
PlayOnSource(source, audioData);
|
||||
}
|
||||
|
||||
public void StopMusic()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
|
||||
if (_musicSourceA != null)
|
||||
{
|
||||
_musicSourceA.Stop();
|
||||
_musicSourceA.clip = null;
|
||||
_musicSourceA.volume = 0f;
|
||||
}
|
||||
|
||||
if (_musicSourceB != null)
|
||||
{
|
||||
_musicSourceB.Stop();
|
||||
_musicSourceB.clip = null;
|
||||
_musicSourceB.volume = 0f;
|
||||
}
|
||||
|
||||
_currentMusicTrack = null;
|
||||
}
|
||||
|
||||
public void StopVoice()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _voiceCts);
|
||||
|
||||
if (_voiceSource != null && _voiceSource.isPlaying)
|
||||
_voiceSource.Stop();
|
||||
|
||||
PublishVoiceFinishedIfNeeded();
|
||||
}
|
||||
|
||||
public void StopAllSfx()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
src.Stop();
|
||||
_sfxChannels[i] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = -999f
|
||||
};
|
||||
}
|
||||
}
|
||||
// ── Stop all ──────────────────────────────────────────────────
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
if (!Initialized) return;
|
||||
StopMusic();
|
||||
StopVoice();
|
||||
StopAllSfx();
|
||||
StopAllAmbience();
|
||||
|
||||
if (_uiSource != null)
|
||||
_uiSource.Stop();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
|
||||
private static void StopAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (cts == null) return;
|
||||
try { cts.Cancel(); } catch { }
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
@@ -733,7 +637,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
Object.DontDestroyOnLoad(obj);
|
||||
_createdAudioObjects.Add(obj);
|
||||
|
||||
var src = obj.AddComponent<AudioSource>();
|
||||
var src = obj.AddComponent<AudioSource>();
|
||||
var group = _audioMixer.FindMatchingGroups(groupName);
|
||||
|
||||
if (group != null && group.Length > 0)
|
||||
@@ -744,28 +648,14 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
return src;
|
||||
}
|
||||
|
||||
private async UniTaskVoid PlayOneShotAsync(AudioSource source, AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || source == null || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
source.priority = audioData.Priority;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
var seconds = audioData.Clip.length / Mathf.Max(audioData.Pitch, 0.0001f);
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
private void PlayOnSource(AudioSource source, AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || source == null || audioData == null)
|
||||
return;
|
||||
if (!Initialized || source == null || audioData == null) return;
|
||||
|
||||
source.clip = audioData.Clip;
|
||||
source.loop = audioData.Loopable;
|
||||
source.volume = audioData.Volume;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.clip = audioData.Clip;
|
||||
source.loop = audioData.Loopable;
|
||||
source.volume = audioData.Volume;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.priority = audioData.Priority;
|
||||
source.Play();
|
||||
}
|
||||
@@ -773,7 +663,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
private struct SfxChannel
|
||||
{
|
||||
public AudioSource Source;
|
||||
public float StartedAtUnscaled;
|
||||
public float StartedAtUnscaled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers
|
||||
@@ -13,6 +16,7 @@ namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
private SaveManager _saveManager;
|
||||
private PlayerManager _playerManager;
|
||||
private InputManager _inputManager;
|
||||
|
||||
[Header("Current Loaded Save")]
|
||||
[SerializeField, ReadOnly]
|
||||
@@ -23,10 +27,11 @@ namespace BriarQueen.Framework.Managers
|
||||
private ItemKey _itemToGive;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager, PlayerManager playerManager)
|
||||
public void Construct(SaveManager saveManager, PlayerManager playerManager, InputManager inputManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
_playerManager = playerManager;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
@@ -38,6 +43,7 @@ namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
_currentSave = save;
|
||||
}
|
||||
|
||||
|
||||
[Button]
|
||||
private void GiveItem()
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
private readonly object _saveLock = new();
|
||||
|
||||
private CancellationTokenSource _currentSaveCts;
|
||||
private DateTime _lastSaveTime;
|
||||
|
||||
[Inject]
|
||||
public SaveManager(EventCoordinator eventCoordinator)
|
||||
@@ -112,12 +111,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
private async UniTask SaveGameDataInternal(CancellationToken ct)
|
||||
{
|
||||
if ((DateTime.UtcNow - _lastSaveTime).TotalMilliseconds < 250)
|
||||
{
|
||||
Debug.Log("[SaveManager] Last save within 250ms, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentSave == null)
|
||||
CurrentSave = new SaveGame { SaveFileName = "NewGame" };
|
||||
|
||||
@@ -186,7 +179,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
CurrentSave = saveClone;
|
||||
IsGameLoaded = true;
|
||||
_lastSaveTime = DateTime.UtcNow;
|
||||
|
||||
OnSaveGameSaved?.Invoke();
|
||||
Debug.Log($"[SaveManager] Save complete: {CurrentSave.SaveFileName}");
|
||||
@@ -272,8 +264,7 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
if (loadedSave != null)
|
||||
{
|
||||
CurrentSave = loadedSave;
|
||||
await SaveGameDataLatest();
|
||||
RestoreBackupToMain(mainPath, backupPath);
|
||||
Debug.Log("[SaveManager] Restored save from backup.");
|
||||
}
|
||||
}
|
||||
@@ -285,6 +276,41 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
OnSaveGameLoaded?.Invoke(CurrentSave);
|
||||
}
|
||||
|
||||
private void RestoreBackupToMain(string mainPath, string backupPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mainPath) || string.IsNullOrWhiteSpace(backupPath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var mainDirectory = Path.GetDirectoryName(mainPath);
|
||||
if (!string.IsNullOrWhiteSpace(mainDirectory))
|
||||
Directory.CreateDirectory(mainDirectory);
|
||||
|
||||
var tempRestorePath = mainPath + ".restoretmp";
|
||||
|
||||
if (File.Exists(tempRestorePath))
|
||||
File.Delete(tempRestorePath);
|
||||
|
||||
File.Copy(backupPath, tempRestorePath, overwrite: true);
|
||||
|
||||
if (File.Exists(mainPath))
|
||||
File.Replace(tempRestorePath, mainPath, null, ignoreMetadataErrors: true);
|
||||
else
|
||||
File.Move(tempRestorePath, mainPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to restore backup '{backupPath}' to '{mainPath}': {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var tempRestorePath = mainPath + ".restoretmp";
|
||||
if (File.Exists(tempRestorePath))
|
||||
File.Delete(tempRestorePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<SaveGame> LoadFromFileAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
|
||||
@@ -432,4 +458,4 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
return collected.Any(x => x.UniqueIdentifier == uniqueIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Input
|
||||
@@ -48,6 +47,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
|
||||
private bool _initialized;
|
||||
private bool _isPaused;
|
||||
private bool _isAnyUIOpen;
|
||||
private InputAction _pauseAction;
|
||||
|
||||
private InputAction _pointAction;
|
||||
@@ -58,7 +58,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
private InputAction _nextItemAction;
|
||||
private InputAction _previousItemAction;
|
||||
private InputAction _virtualMouseAction;
|
||||
private InputAction _submitAction;
|
||||
private InputAction _submitAction;
|
||||
|
||||
private UICursorService _uiCursorService;
|
||||
|
||||
@@ -77,6 +77,8 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
public bool IsPaused => _isPaused;
|
||||
|
||||
public bool UsingControllerCursor => DeviceInputType != DeviceInputType.KeyboardAndMouse;
|
||||
|
||||
public string CurrentControlScheme => _playerInput?.currentControlScheme ?? string.Empty;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -109,6 +111,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(OnCodexStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
|
||||
_eventCoordinator.Unsubscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
}
|
||||
|
||||
UnbindCoreInputs();
|
||||
@@ -148,28 +151,18 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerInput.actions == null)
|
||||
{
|
||||
Debug.LogWarning("[InputManager] PlayerInput.actions is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[InputManager] Current map before cache: {ActiveActionMap}");
|
||||
|
||||
CacheActions();
|
||||
|
||||
Debug.Log($"[InputManager] Point action: {_pointAction}");
|
||||
Debug.Log($"[InputManager] Click action: {_clickAction}");
|
||||
Debug.Log($"[InputManager] Virtual_Mouse action: {_virtualMouseAction}");
|
||||
|
||||
BindCoreInputs();
|
||||
|
||||
DeviceInputType = GetDeviceInputType(_playerInput);
|
||||
ApplyCursorModeForCurrentScheme();
|
||||
|
||||
_initialized = true;
|
||||
_eventCoordinator.Subscribe<UIToggleHudEvent>(OnHudStateChanged);
|
||||
_eventCoordinator.Subscribe<ToggleCodexEvent>(OnCodexStateChanged);
|
||||
_eventCoordinator.Subscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
|
||||
_eventCoordinator.Subscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
|
||||
Debug.Log("[InputManager] Initialization complete");
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private void CacheActions()
|
||||
@@ -205,14 +198,12 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
{
|
||||
if (_pointAction != null)
|
||||
{
|
||||
Debug.Log("[InputManager] Binding Point");
|
||||
|
||||
|
||||
_pointAction.performed += OnPoint;
|
||||
_pointAction.canceled += OnPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[InputManager] Required action 'Point' not found.");
|
||||
}
|
||||
|
||||
if (_virtualMouseAction != null)
|
||||
@@ -223,53 +214,33 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
|
||||
if (_pauseAction != null)
|
||||
_pauseAction.performed += OnPause;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Pause' not found.");
|
||||
|
||||
if (_clickAction != null)
|
||||
_clickAction.performed += OnClick;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Click' not found.");
|
||||
|
||||
if (_rightClickAction != null)
|
||||
_rightClickAction.performed += OnRightClick;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Right_Click' not found.");
|
||||
|
||||
if (_hideHudAction != null)
|
||||
_hideHudAction.performed += OnHideHUD;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Hide_HUD' not found.");
|
||||
|
||||
if (_codexAction != null)
|
||||
_codexAction.performed += OnCodex;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Codex' not found.");
|
||||
|
||||
if (_openToolsAction != null)
|
||||
_openToolsAction.performed += OnOpenTools;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Show_Tools' not found.");
|
||||
|
||||
if (_nextToolAction != null)
|
||||
_nextToolAction.performed += OnNextToolClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Next_Tool' not found.");
|
||||
|
||||
if (_previousToolAction != null)
|
||||
_previousToolAction.performed += OnPreviousToolClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Previous_Tool' not found.");
|
||||
|
||||
if (_nextItemAction != null)
|
||||
_nextItemAction.performed += OnNextItemClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Next_Item' not found.");
|
||||
|
||||
if (_previousItemAction != null)
|
||||
_previousItemAction.performed += OnPreviousItemClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Previous_Item' not found.");
|
||||
|
||||
if (_playerInput != null)
|
||||
_playerInput.onControlsChanged += OnControlsChanged;
|
||||
@@ -343,7 +314,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
}
|
||||
|
||||
private void OnControlsChanged(PlayerInput playerInput)
|
||||
{ Debug.Log($"Controls changed. Scheme: {playerInput.currentControlScheme}");
|
||||
{
|
||||
DeviceInputType = GetDeviceInputType(playerInput);
|
||||
ApplyCursorModeForCurrentScheme();
|
||||
}
|
||||
@@ -391,7 +362,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
{
|
||||
if (_submitAction == null || callback == null)
|
||||
return;
|
||||
|
||||
|
||||
_submitAction.performed -= callback;
|
||||
}
|
||||
|
||||
@@ -401,11 +372,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
action.performed += callback;
|
||||
@@ -417,11 +383,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
}
|
||||
@@ -471,18 +432,28 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
_toolScreenShown = evt.Shown;
|
||||
}
|
||||
|
||||
private void OnUIStackChanged(UIStackChangedEvent evt)
|
||||
{
|
||||
_isAnyUIOpen = evt.AnyUIOpen;
|
||||
_isPaused = evt.AnyUIOpen && _gameService != null && !_gameService.IsMainMenuSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnHideHUD(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_hudHidden = !_hudHidden;
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(_hudHidden));
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(!_hudHidden));
|
||||
}
|
||||
|
||||
private void OnPause(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if(_gameService.IsMainMenuSceneLoaded)
|
||||
var isMainMenu = _gameService != null && _gameService.IsMainMenuSceneLoaded;
|
||||
if (isMainMenu || _isAnyUIOpen)
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new UIBackRequestedEvent());
|
||||
return;
|
||||
|
||||
_isPaused = !_isPaused;
|
||||
}
|
||||
|
||||
_isPaused = true;
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
@@ -556,5 +527,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
@@ -31,11 +32,16 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
[Tooltip("Used for custom tooltip. Defaults to Item Name")]
|
||||
[SerializeField]
|
||||
private string _interactableTooltip = string.Empty;
|
||||
protected string _interactableTooltip = string.Empty;
|
||||
[Tooltip("Optional. Used for custom interaction.")]
|
||||
[SerializeField]
|
||||
private string _pickupText = string.Empty;
|
||||
|
||||
[Header("Object Setup")]
|
||||
[SerializeField]
|
||||
protected CanvasGroup _canvasGroup;
|
||||
|
||||
protected bool _isLocked;
|
||||
|
||||
protected AddressableManager AddressableManager;
|
||||
protected AssetRegistry AssetRegistry;
|
||||
@@ -68,8 +74,23 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public virtual UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Pickup;
|
||||
|
||||
public virtual string InteractableName =>
|
||||
!string.IsNullOrWhiteSpace(_interactableTooltip) ? _interactableTooltip : _itemData.ItemName;
|
||||
public virtual string InteractableName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_interactableTooltip))
|
||||
{
|
||||
return _interactableTooltip;
|
||||
}
|
||||
|
||||
if (_itemData != null && !string.IsNullOrWhiteSpace(_itemData.ItemName))
|
||||
{
|
||||
return _itemData.ItemName;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -91,6 +112,15 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
await Pickup();
|
||||
await OnInteracted();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_pickupText))
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(_pickupText));
|
||||
}
|
||||
else
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.LooksImportant)));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual UniTask EnterHover()
|
||||
@@ -147,8 +177,8 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
protected virtual async UniTask Remove()
|
||||
{
|
||||
// TODO - Play Cut Vines SFX
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (PickupSequence.isAlive)
|
||||
{
|
||||
@@ -184,7 +214,7 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSaveGameOnRemoval()
|
||||
protected virtual void UpdateSaveGameOnRemoval()
|
||||
{
|
||||
var save = SaveManager.CurrentSave;
|
||||
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
|
||||
@@ -240,5 +270,35 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
await DestructionService.Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
_isLocked = true;
|
||||
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
_isLocked = false;
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
public void OnValidate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
var canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (!canvasGroup)
|
||||
{
|
||||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
_canvasGroup = canvasGroup;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
@@ -8,12 +10,19 @@ using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data.Codex;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Puzzles.Base;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
using SettingsService = BriarQueen.Framework.Services.Settings.SettingsService;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
@@ -30,6 +39,10 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public List<CodexTrigger> CodexTriggers;
|
||||
|
||||
[Header("Puzzles")]
|
||||
[SerializeField]
|
||||
public List<BasePuzzle> Puzzles;
|
||||
|
||||
[Header("Setup")]
|
||||
[SerializeField]
|
||||
protected GraphicRaycaster _raycaster;
|
||||
@@ -47,8 +60,6 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public virtual string LevelName => _levelName;
|
||||
|
||||
public virtual bool IsPuzzleLevel { get; }
|
||||
|
||||
public virtual int CurrentLevelHintStage { get; set; }
|
||||
|
||||
public virtual Dictionary<int, BaseHint> Hints { get; }
|
||||
@@ -112,5 +123,72 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Button("Discover Level References")]
|
||||
private void DiscoverLevelReferences()
|
||||
{
|
||||
Undo.RecordObject(this, "Discover Level References");
|
||||
|
||||
var discoveredCodexTriggers = GetComponentsInChildren<CodexTrigger>(true)
|
||||
.Where(trigger => trigger != null)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var discoveredPickups = GetComponentsInChildren<BaseItem>(true)
|
||||
.Where(item => item != null && item is not CodexTrigger)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var discoveredPuzzles = GetComponentsInChildren<BasePuzzle>(true)
|
||||
.Where(puzzle => puzzle != null)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Pickups = discoveredPickups;
|
||||
CodexTriggers = discoveredCodexTriggers;
|
||||
Puzzles = discoveredPuzzles;
|
||||
|
||||
EditorUtility.SetDirty(this);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
|
||||
|
||||
Debug.Log(
|
||||
$"[BaseLevel] Discovery complete for '{name}'. Pickups: {Pickups.Count}, CodexTriggers: {CodexTriggers.Count}, Puzzles: {Puzzles.Count}",
|
||||
this);
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(Component component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var names = new Stack<string>();
|
||||
var current = component.transform;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
names.Push(current.name);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return string.Join("/", names);
|
||||
}
|
||||
#endif
|
||||
|
||||
public void OnValidate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
CanvasScaler scaler = GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
scaler.referenceResolution = new Vector2(1920, 1200);
|
||||
|
||||
GraphicRaycaster raycaster = GetComponent<GraphicRaycaster>();
|
||||
_raycaster = raycaster;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Progression;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Puzzles;
|
||||
using BriarQueen.Framework.Services.Puzzles.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
@@ -33,6 +32,7 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private readonly SaveManager _saveManager;
|
||||
|
||||
private UniTask<bool> _activeLoadTask = UniTask.FromResult(false);
|
||||
private bool _isLoadInProgress;
|
||||
private BaseLevel _currentLevel;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
@@ -57,7 +57,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[{nameof(LevelManager)}] Initializing...");
|
||||
_saveManager.OnSaveRequested += OnSaveGameRequested;
|
||||
@@ -70,7 +72,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_saveManager.OnSaveRequested -= OnSaveGameRequested;
|
||||
_eventCoordinator.Unsubscribe<UpdateHintProgressEvent>(OnHintStageUpdated);
|
||||
@@ -81,10 +85,14 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private void OnHintStageUpdated(UpdateHintProgressEvent evt)
|
||||
{
|
||||
if (_currentLevel == null || evt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(evt.LevelID, _currentLevel.LevelID, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var incoming = Mathf.Max(0, evt.Stage);
|
||||
|
||||
@@ -101,7 +109,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private void OnSaveGameRequested(SaveGame saveGame)
|
||||
{
|
||||
if (saveGame == null || _currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
saveGame.CurrentLevelID = _currentLevel.LevelID;
|
||||
saveGame.CurrentSceneID = _currentLevel.SceneID;
|
||||
@@ -120,12 +130,73 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_activeLoadTask = LoadLevelInternal(levelAssetID);
|
||||
if (_isLoadInProgress)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] LoadLevel('{levelAssetID}') requested while another level load is already in progress. Returning the active load task.");
|
||||
return _activeLoadTask;
|
||||
}
|
||||
|
||||
_isLoadInProgress = true;
|
||||
_activeLoadTask = LoadLevelTracked(levelAssetID);
|
||||
return _activeLoadTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadLevelTracked(string levelAssetID)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await LoadLevelInternal(levelAssetID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isLoadInProgress = false;
|
||||
_activeLoadTask = UniTask.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadLevelInternal(string levelAssetID)
|
||||
{
|
||||
var previousLevelId = _currentLevel != null ? _currentLevel.LevelID : null;
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
await UnloadLevelInternal();
|
||||
}
|
||||
|
||||
if (await TryLoadLevelCore(levelAssetID))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(previousLevelId) &&
|
||||
!string.Equals(previousLevelId, levelAssetID, StringComparison.Ordinal))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] Failed to load '{levelAssetID}'. Attempting recovery by reloading previous level '{previousLevelId}'.");
|
||||
|
||||
if (await TryLoadLevelCore(previousLevelId))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] Recovery succeeded by reloading '{previousLevelId}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(true, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTask<bool> TryLoadLevelCore(string levelAssetID)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -141,12 +212,6 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
return false;
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
await UnloadLevelInternal();
|
||||
|
||||
var levelObj = await _addressableManager.InstantiateAsync(levelRef);
|
||||
if (levelObj == null)
|
||||
{
|
||||
@@ -157,7 +222,8 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
var level = levelObj.GetComponent<BaseLevel>();
|
||||
if (level == null)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
|
||||
Debug.LogError(
|
||||
$"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
|
||||
await _destructionService.Destroy(levelObj);
|
||||
return false;
|
||||
}
|
||||
@@ -169,8 +235,7 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
|
||||
await _currentLevel.PostLoad();
|
||||
|
||||
if (_currentLevel is BasePuzzle puzzle)
|
||||
await _puzzleService.LoadPuzzle(puzzle);
|
||||
await _puzzleService.LoadPuzzles(_currentLevel.Puzzles);
|
||||
|
||||
_eventCoordinator.Publish(new LevelChangedEvent(_currentLevel));
|
||||
|
||||
@@ -178,7 +243,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
await _currentLevel.PostActivate();
|
||||
}
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
return true;
|
||||
@@ -186,29 +253,38 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Exception while loading '{levelAssetID}': {ex}");
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _destructionService.Destroy(_currentLevel.gameObject);
|
||||
}
|
||||
catch (Exception destroyEx)
|
||||
{
|
||||
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
|
||||
}
|
||||
|
||||
_currentLevel = null;
|
||||
}
|
||||
|
||||
await CleanupFailedCurrentLevel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask CleanupFailedCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _destructionService.Destroy(_currentLevel.gameObject);
|
||||
}
|
||||
catch (Exception destroyEx)
|
||||
{
|
||||
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentLevel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreHintStageForCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save?.LevelHintStages == null)
|
||||
@@ -218,23 +294,33 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
}
|
||||
|
||||
if (save.LevelHintStages.TryGetValue(_currentLevel.LevelID, out var stage))
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = Mathf.Max(0, stage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask RestoreItemStateForCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var interactables = _currentLevel.Pickups;
|
||||
if (interactables == null || interactables.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var interactable in interactables)
|
||||
{
|
||||
@@ -245,10 +331,14 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
}
|
||||
|
||||
if (save.CollectedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
|
||||
{
|
||||
await _destructionService.Destroy(interactable.gameObject);
|
||||
}
|
||||
|
||||
if (save.RemovedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
|
||||
{
|
||||
await _destructionService.Destroy(interactable.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
var codexTriggers = _currentLevel.CodexTriggers;
|
||||
@@ -258,7 +348,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
if (save.DiscoveredCodexEntries.Any(x => x.UniqueIdentifier == trigger.Entry.UniqueID))
|
||||
{
|
||||
if (trigger.RemoveTrigger)
|
||||
{
|
||||
await _destructionService.Destroy(trigger.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +360,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
lock (_lock)
|
||||
{
|
||||
if (_activeLoadTask.Status == UniTaskStatus.Pending)
|
||||
{
|
||||
return _activeLoadTask.ContinueWith(_ => UnloadLevelInternal());
|
||||
}
|
||||
|
||||
return UnloadLevelInternal();
|
||||
}
|
||||
@@ -277,15 +371,16 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private async UniTask UnloadLevelInternal()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var level = _currentLevel;
|
||||
_currentLevel = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (level is BasePuzzle puzzle)
|
||||
await _puzzleService.SavePuzzle(puzzle);
|
||||
await _puzzleService.SavePuzzles(level.Puzzles);
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
await level.PreUnload();
|
||||
@@ -298,4 +393,4 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
await _destructionService.Destroy(level.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,22 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
public class Codex
|
||||
{
|
||||
public Codex(bool unlocked = false)
|
||||
{
|
||||
CodexUnlocked = unlocked;
|
||||
}
|
||||
|
||||
public bool CodexUnlocked { get; private set; }
|
||||
|
||||
private readonly List<CodexEntrySo> _entries = new();
|
||||
|
||||
public IReadOnlyList<CodexEntrySo> Entries => _entries;
|
||||
|
||||
public void UnlockCodex()
|
||||
{
|
||||
CodexUnlocked = true;
|
||||
}
|
||||
|
||||
public void AddEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
@@ -66,7 +78,7 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetBookEntries()
|
||||
{
|
||||
return GetEntriesByType(CodexType.BookEntry);
|
||||
return GetEntriesByType(CodexType.DocumentEntry);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetPuzzleClues()
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
@@ -28,6 +33,12 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
if (!CheckEmptyHands())
|
||||
return;
|
||||
|
||||
if (!PlayerManager.CodexUnlocked())
|
||||
{
|
||||
EventCoordinator.PublishImmediate(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CodexLocked)));
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerManager.UnlockCodexEntry(_codexEntry);
|
||||
|
||||
@@ -36,5 +47,20 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
await Remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateSaveGameOnRemoval()
|
||||
{
|
||||
var save = SaveManager.CurrentSave;
|
||||
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
|
||||
|
||||
save.RemovedItems ??= new List<ItemSaveData>();
|
||||
|
||||
save.RemovedItems.Add(new ItemSaveData
|
||||
{
|
||||
UniqueIdentifier = _codexEntry.UniqueID
|
||||
});
|
||||
|
||||
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
[Header("Codex ID")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsBookEntry))]
|
||||
private BookEntryID _bookEntryID;
|
||||
private DocumentEntryID _documentEntryID;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPuzzleClue))]
|
||||
@@ -66,11 +66,11 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
public CodexType EntryType => _codexType;
|
||||
public Location Location => _location;
|
||||
|
||||
public bool IsBookEntry => _codexType == CodexType.BookEntry;
|
||||
public bool IsBookEntry => _codexType == CodexType.DocumentEntry;
|
||||
public bool IsPuzzleClue => _codexType == CodexType.PuzzleClue;
|
||||
public bool IsPhoto => _codexType == CodexType.Photo;
|
||||
|
||||
public BookEntryID BookEntryID => _bookEntryID;
|
||||
public DocumentEntryID DocumentEntryID => _documentEntryID;
|
||||
public ClueEntryID ClueEntryID => _clueEntryID;
|
||||
public PhotoEntryID PhotoEntryID => _photoEntryID;
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
{
|
||||
return _codexType switch
|
||||
{
|
||||
CodexType.BookEntry when _bookEntryID != BookEntryID.None =>
|
||||
CodexEntryIDs.Get(_bookEntryID),
|
||||
CodexType.DocumentEntry when _documentEntryID != DocumentEntryID.None =>
|
||||
CodexEntryIDs.Get(_documentEntryID),
|
||||
|
||||
CodexType.PuzzleClue when _clueEntryID != ClueEntryID.None =>
|
||||
CodexEntryIDs.Get(_clueEntryID),
|
||||
|
||||
@@ -7,7 +7,6 @@ using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Tools
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
}
|
||||
}
|
||||
|
||||
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemCollected));
|
||||
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemPickup));
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new InventoryChangedEvent());
|
||||
}
|
||||
@@ -208,11 +208,21 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
#region Codex
|
||||
|
||||
public void UnlockCodex() => _codex.UnlockCodex();
|
||||
|
||||
public bool CodexUnlocked()
|
||||
{
|
||||
return _codex is { CodexUnlocked: true };
|
||||
}
|
||||
|
||||
public void UnlockCodexEntry(string uniqueIdentifier)
|
||||
{
|
||||
var entry = _codexRegistry.FindEntryByID(uniqueIdentifier);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerManager] Could not unlock codex entry '{uniqueIdentifier}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
UnlockCodexEntry(entry);
|
||||
}
|
||||
@@ -243,7 +253,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
}
|
||||
}
|
||||
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.Codex);
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.CodexKeyboard);
|
||||
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new CodexChangedEvent(entry.EntryType));
|
||||
@@ -382,7 +392,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
private void LoadCodexFromSave(SaveGame save)
|
||||
{
|
||||
_codex = new Codex();
|
||||
_codex = new Codex(save.CodexUnlocked);
|
||||
|
||||
if (save.DiscoveredCodexEntries != null)
|
||||
{
|
||||
@@ -458,4 +468,4 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IUIBackHandler
|
||||
{
|
||||
bool HandleBackRequest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18b01f6ab9d3468ba9a99360acbe4e5c
|
||||
timeCreated: 1778300000
|
||||
@@ -57,7 +57,18 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
|
||||
private bool _useVirtualCursor;
|
||||
|
||||
public CursorStyleEntry CurrentStyleEntry => _styleMap[_currentStyle];
|
||||
public CursorStyleEntry CurrentStyleEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetStyleEntry(GetEffectiveStyle(), out var entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator)
|
||||
@@ -192,6 +203,22 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
return _isStyleOverridden ? _currentStyleOverride : _currentStyle;
|
||||
}
|
||||
|
||||
private bool TryGetStyleEntry(CursorStyle style, out CursorStyleEntry entry)
|
||||
{
|
||||
if (_styleMap.TryGetValue(style, out entry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_styleMap.TryGetValue(CursorStyle.Default, out entry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyVirtualCursorStyle(CursorStyle style)
|
||||
{
|
||||
if (_virtualCursorImage == null)
|
||||
@@ -282,4 +309,4 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
public Vector2 TooltipOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Extensions;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
@@ -28,6 +29,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private readonly InteractManager _interactManager;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly PlayerManager _playerManager;
|
||||
|
||||
private readonly Dictionary<WindowType, IUIWindow> _windows = new();
|
||||
private readonly Stack<IUIWindow> _windowStack = new();
|
||||
@@ -46,12 +48,14 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
EventCoordinator eventCoordinator,
|
||||
InteractManager interactManager,
|
||||
SettingsService settingsService,
|
||||
SaveManager saveManager)
|
||||
SaveManager saveManager,
|
||||
PlayerManager playerManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_interactManager = interactManager;
|
||||
_settingsService = settingsService;
|
||||
_saveManager = saveManager;
|
||||
_playerManager = playerManager;
|
||||
}
|
||||
|
||||
private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null;
|
||||
@@ -84,6 +88,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
_eventCoordinator.Subscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Subscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
_eventCoordinator.Subscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Subscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Subscribe<FadeEvent>(OnFadeEvent);
|
||||
@@ -97,6 +102,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private void UnsubscribeFromEvents()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Unsubscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Unsubscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Unsubscribe<FadeEvent>(OnFadeEvent);
|
||||
@@ -175,13 +181,18 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
if (_windowStack.Count > 0)
|
||||
{
|
||||
CloseTopWindow();
|
||||
TryHandleBackRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenWindow(WindowType.PauseMenuWindow);
|
||||
}
|
||||
|
||||
private void OnBackRequested(UIBackRequestedEvent _)
|
||||
{
|
||||
TryHandleBackRequest();
|
||||
}
|
||||
|
||||
private void ToggleSettingsWindow(UIToggleSettingsWindow eventData)
|
||||
{
|
||||
if (eventData.Show)
|
||||
@@ -192,6 +203,9 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
|
||||
private void ToggleCodexWindow(ToggleCodexEvent eventData)
|
||||
{
|
||||
if(!_playerManager.CodexUnlocked())
|
||||
return;
|
||||
|
||||
if (eventData.Shown)
|
||||
OpenWindow(WindowType.CodexWindow);
|
||||
else
|
||||
@@ -231,7 +245,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
return codexType switch
|
||||
{
|
||||
CodexType.BookEntry => "You've acquired a new document.",
|
||||
CodexType.DocumentEntry => "You've acquired a new document.",
|
||||
CodexType.PuzzleClue => "You've acquired a new puzzle clue.",
|
||||
CodexType.Photo => "You've acquired a new photo.",
|
||||
_ => string.Empty
|
||||
@@ -246,10 +260,14 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
if (!_settingsService.AreTutorialsEnabled())
|
||||
return;
|
||||
|
||||
var duration = 3f;
|
||||
var tutorialText = TutorialPopupTexts.AllPopups[eventData.TutorialID];
|
||||
if (string.IsNullOrWhiteSpace(eventData.ResolvedText))
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] Empty resolved text for tutorial '{eventData.TutorialID}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_tutorialPopup.Play(tutorialText, duration).Forget();
|
||||
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
||||
_tutorialPopup.Play(eventData.ResolvedText, duration).Forget();
|
||||
}
|
||||
|
||||
private void OnDisplayInteractText(DisplayInteractEvent eventData)
|
||||
@@ -340,6 +358,21 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
CloseTopWindowInternal().Forget();
|
||||
}
|
||||
|
||||
private void TryHandleBackRequest()
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveWindow is IUIBackHandler backHandler && backHandler.HandleBackRequest())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseTopWindow();
|
||||
}
|
||||
|
||||
private async UniTask CloseTopWindowInternal()
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
@@ -431,4 +464,4 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
_eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Managers.Achievements.Data;
|
||||
using UnityEngine;
|
||||
@@ -19,8 +18,22 @@ namespace BriarQueen.Framework.Registries
|
||||
if (_achievementDictionary != null)
|
||||
return;
|
||||
|
||||
_achievementDictionary = _achievementSos.ToDictionary(achievement => achievement.Achievement,
|
||||
achievement => achievement);
|
||||
RebuildLookup();
|
||||
}
|
||||
|
||||
private void RebuildLookup()
|
||||
{
|
||||
_achievementDictionary = new Dictionary<AchievementID, AchievementSo>();
|
||||
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_achievementDictionary,
|
||||
_achievementSos,
|
||||
this,
|
||||
nameof(AchievementRegistry),
|
||||
"Achievements",
|
||||
nameof(AchievementSo.Achievement),
|
||||
entry => entry.Achievement,
|
||||
entry => entry);
|
||||
}
|
||||
|
||||
public bool TryGetAchievement(AchievementID identifier, out AchievementSo achievement)
|
||||
@@ -35,4 +48,4 @@ namespace BriarQueen.Framework.Registries
|
||||
return _achievementDictionary.Values;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,47 +45,57 @@ namespace BriarQueen.Framework.Registries
|
||||
{
|
||||
_assetDictionary = new Dictionary<string, AssetReference>();
|
||||
|
||||
AddEntries(_sceneReferences, "Scenes");
|
||||
AddEntries(_levelReferences, "Levels");
|
||||
AddEntries(_itemReferences, "Items");
|
||||
AddEntries(_uiReferences, "UI");
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_assetDictionary,
|
||||
_sceneReferences,
|
||||
this,
|
||||
nameof(AssetRegistry),
|
||||
"Scenes",
|
||||
"AssetKey",
|
||||
entry => entry.AssetKey,
|
||||
entry => entry.Asset,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
|
||||
entry => entry.Asset != null,
|
||||
"AssetReference is null");
|
||||
|
||||
private void AddEntries(List<AssetEntry> entries, string category)
|
||||
{
|
||||
if (entries == null)
|
||||
return;
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_assetDictionary,
|
||||
_levelReferences,
|
||||
this,
|
||||
nameof(AssetRegistry),
|
||||
"Levels",
|
||||
"AssetKey",
|
||||
entry => entry.AssetKey,
|
||||
entry => entry.Asset,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
|
||||
entry => entry.Asset != null,
|
||||
"AssetReference is null");
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_assetDictionary,
|
||||
_itemReferences,
|
||||
this,
|
||||
nameof(AssetRegistry),
|
||||
"Items",
|
||||
"AssetKey",
|
||||
entry => entry.AssetKey,
|
||||
entry => entry.Asset,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
|
||||
entry => entry.Asset != null,
|
||||
"AssetReference is null");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.AssetKey))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[AssetRegistry] Skipping {category} entry '{entry.name}' because AssetKey is empty.", this);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Asset == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[AssetRegistry] Skipping {category} entry '{entry.name}' because AssetReference is null.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_assetDictionary.ContainsKey(entry.AssetKey))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[AssetRegistry] Duplicate AssetKey detected: '{entry.AssetKey}' from entry '{entry.name}'.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
_assetDictionary.Add(entry.AssetKey, entry.Asset);
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_assetDictionary,
|
||||
_uiReferences,
|
||||
this,
|
||||
nameof(AssetRegistry),
|
||||
"UI",
|
||||
"AssetKey",
|
||||
entry => entry.AssetKey,
|
||||
entry => entry.Asset,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
|
||||
entry => entry.Asset != null,
|
||||
"AssetReference is null");
|
||||
}
|
||||
|
||||
public bool TryGetReference(string key, out AssetReference reference)
|
||||
@@ -116,4 +126,4 @@ namespace BriarQueen.Framework.Registries
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Framework.Managers.Audio.Data;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
@@ -17,15 +16,32 @@ namespace BriarQueen.Framework.Registries
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_audioFileDict == null)
|
||||
_audioFileDict = _audioFiles.ToDictionary(entry => entry.UniqueID, entry => entry);
|
||||
if (_audioFileDict != null)
|
||||
return;
|
||||
|
||||
RebuildLookup();
|
||||
}
|
||||
|
||||
private void RebuildLookup()
|
||||
{
|
||||
_audioFileDict = new Dictionary<string, AudioFileSo>();
|
||||
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_audioFileDict,
|
||||
_audioFiles,
|
||||
this,
|
||||
nameof(AudioRegistry),
|
||||
"Audio Files",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
}
|
||||
|
||||
public bool TryGetAudio(string audioName, out AudioFileSo audioFile)
|
||||
{
|
||||
if (_audioFileDict == null) EnsureInitialized();
|
||||
|
||||
return _audioFileDict!.TryGetValue(audioName, out audioFile);
|
||||
EnsureInitialized();
|
||||
return _audioFileDict.TryGetValue(audioName, out audioFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,39 +35,38 @@ namespace BriarQueen.Framework.Registries
|
||||
{
|
||||
_entryLookup = new Dictionary<string, CodexEntrySo>();
|
||||
|
||||
AddEntries(_bookEntries, "Book Entries");
|
||||
AddEntries(_puzzleClues, "Puzzle Clues");
|
||||
AddEntries(_photoEntries, "Photo Entries");
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_bookEntries,
|
||||
this,
|
||||
nameof(CodexRegistry),
|
||||
"Book Entries",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
|
||||
private void AddEntries(List<CodexEntrySo> entries, string category)
|
||||
{
|
||||
if (entries == null)
|
||||
return;
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_puzzleClues,
|
||||
this,
|
||||
nameof(CodexRegistry),
|
||||
"Puzzle Clues",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.UniqueID))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[CodexRegistry] Skipping {category} entry '{entry.name}' because UniqueID is empty.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_entryLookup.ContainsKey(entry.UniqueID))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[CodexRegistry] Duplicate UniqueID detected: '{entry.UniqueID}' from entry '{entry.name}'.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
_entryLookup.Add(entry.UniqueID, entry);
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_photoEntries,
|
||||
this,
|
||||
nameof(CodexRegistry),
|
||||
"Photo Entries",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
}
|
||||
|
||||
public bool TryGetEntry(string entryID, out CodexEntrySo entry)
|
||||
@@ -130,4 +129,4 @@ namespace BriarQueen.Framework.Registries
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,39 +35,38 @@ namespace BriarQueen.Framework.Registries
|
||||
{
|
||||
_entryLookup = new Dictionary<string, ItemDataSo>();
|
||||
|
||||
AddEntries(_puzzleSlots, "Puzzle Slots");
|
||||
AddEntries(_pickupItems, "Pickup Items");
|
||||
AddEntries(_environmentInteractables, "Environment Interactables");
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_puzzleSlots,
|
||||
this,
|
||||
nameof(ItemRegistry),
|
||||
"Puzzle Slots",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
|
||||
private void AddEntries(List<ItemDataSo> entries, string category)
|
||||
{
|
||||
if (entries == null)
|
||||
return;
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_pickupItems,
|
||||
this,
|
||||
nameof(ItemRegistry),
|
||||
"Pickup Items",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.UniqueID))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[ItemRegistry] Skipping {category} entry '{entry.name}' because UniqueID is empty.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_entryLookup.ContainsKey(entry.UniqueID))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[ItemRegistry] Duplicate UniqueID detected: '{entry.UniqueID}' from entry '{entry.name}'.",
|
||||
this);
|
||||
continue;
|
||||
}
|
||||
|
||||
_entryLookup.Add(entry.UniqueID, entry);
|
||||
}
|
||||
RegistryLookupBuilder.AddEntries(
|
||||
_entryLookup,
|
||||
_environmentInteractables,
|
||||
this,
|
||||
nameof(ItemRegistry),
|
||||
"Environment Interactables",
|
||||
"UniqueID",
|
||||
entry => entry.UniqueID,
|
||||
entry => entry,
|
||||
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
|
||||
}
|
||||
|
||||
public bool TryGetEntry(string itemID, out ItemDataSo entry)
|
||||
@@ -130,4 +129,4 @@ namespace BriarQueen.Framework.Registries
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
69
Assets/Scripts/Framework/Registries/RegistryLookupBuilder.cs
Normal file
69
Assets/Scripts/Framework/Registries/RegistryLookupBuilder.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Registries
|
||||
{
|
||||
internal static class RegistryLookupBuilder
|
||||
{
|
||||
public static void AddEntries<TKey, TEntry, TValue>(
|
||||
Dictionary<TKey, TValue> lookup,
|
||||
IEnumerable<TEntry> entries,
|
||||
UnityEngine.Object context,
|
||||
string registryName,
|
||||
string category,
|
||||
string keyLabel,
|
||||
Func<TEntry, TKey> keySelector,
|
||||
Func<TEntry, TValue> valueSelector,
|
||||
Func<TEntry, bool> isKeyValid = null,
|
||||
Func<TEntry, bool> isEntryValid = null,
|
||||
string invalidEntryReason = null)
|
||||
where TEntry : UnityEngine.Object
|
||||
{
|
||||
if (lookup == null)
|
||||
throw new ArgumentNullException(nameof(lookup));
|
||||
|
||||
if (entries == null)
|
||||
return;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry)
|
||||
continue;
|
||||
|
||||
if (isKeyValid != null && !isKeyValid(entry))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[{registryName}] Skipping {category} entry '{entry.name}' because {keyLabel} is invalid.",
|
||||
context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEntryValid != null && !isEntryValid(entry))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[{registryName}] Skipping {category} entry '{entry.name}' because {invalidEntryReason}.",
|
||||
context);
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = keySelector(entry);
|
||||
|
||||
if (lookup.ContainsKey(key))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{registryName}] Duplicate {keyLabel} detected: '{key}' from entry '{entry.name}'.",
|
||||
context);
|
||||
continue;
|
||||
}
|
||||
|
||||
lookup.Add(key, valueSelector(entry));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasNonEmptyKey(string key)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be2d54c81fb1f49eb974c7b2b0a91f47
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
using System;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Levels;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
using VContainer;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace BriarQueen.Framework.Services.Game
|
||||
{
|
||||
public class GameService
|
||||
@@ -75,7 +78,7 @@ namespace BriarQueen.Framework.Services.Game
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(false, fadeDuration));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(fadeDuration));
|
||||
|
||||
await UnloadGameSceneIfLoaded();
|
||||
await PrepareToLeaveGameplayScene();
|
||||
|
||||
if (_assetRegistry == null ||
|
||||
!_assetRegistry.TryGetReference(
|
||||
@@ -193,6 +196,12 @@ namespace BriarQueen.Framework.Services.Game
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask PrepareToLeaveGameplayScene()
|
||||
{
|
||||
await _levelManager.UnloadLevel();
|
||||
await UnloadGameSceneIfLoaded();
|
||||
}
|
||||
|
||||
public async UniTask SwapGameSceneHandle(AsyncOperationHandle<SceneInstance> nextSceneHandle)
|
||||
{
|
||||
if (!nextSceneHandle.IsValid())
|
||||
@@ -208,7 +217,10 @@ namespace BriarQueen.Framework.Services.Game
|
||||
}
|
||||
|
||||
if (_gameSceneHandle.IsValid())
|
||||
{
|
||||
await _levelManager.UnloadLevel();
|
||||
await _addressableManager.UnloadSceneAsync(_gameSceneHandle);
|
||||
}
|
||||
|
||||
_gameSceneHandle = nextSceneHandle;
|
||||
SceneManager.SetActiveScene(nextSceneHandle.Result.Scene);
|
||||
@@ -223,4 +235,4 @@ namespace BriarQueen.Framework.Services.Game
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
// ==============================
|
||||
// PuzzleBase.cs (updated)
|
||||
// ==============================
|
||||
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.Hints.Data;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Services.Puzzles.Base
|
||||
{
|
||||
public abstract class BasePuzzle : BaseLevel
|
||||
public abstract class BasePuzzle : MonoBehaviour
|
||||
{
|
||||
protected AddressableManager AddressableManager;
|
||||
protected AssetRegistry AssetRegistry;
|
||||
protected AudioManager AudioManager;
|
||||
protected DestructionService DestructionService;
|
||||
protected EventCoordinator EventCoordinator;
|
||||
protected ItemRegistry ItemRegistry;
|
||||
protected PuzzleService PuzzleService;
|
||||
protected SaveManager SaveManager;
|
||||
|
||||
public abstract string PuzzleID { get; }
|
||||
|
||||
public override bool IsPuzzleLevel => true;
|
||||
|
||||
// BaseLevel still requires these.
|
||||
public abstract override string LevelName { get; }
|
||||
public abstract override Dictionary<int, BaseHint> Hints { get; }
|
||||
|
||||
public abstract UniTask CompletePuzzle();
|
||||
|
||||
public virtual UniTask PostLoad()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual UniTask PreUnload()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator, AudioManager audioManager,
|
||||
SaveManager saveManager, ItemRegistry itemRegistry, AddressableManager addressableManager,
|
||||
AssetRegistry assetRegistry, PuzzleService puzzleService)
|
||||
AssetRegistry assetRegistry, PuzzleService puzzleService, DestructionService destructionService)
|
||||
{
|
||||
EventCoordinator = eventCoordinator;
|
||||
AudioManager = audioManager;
|
||||
@@ -45,6 +47,7 @@ namespace BriarQueen.Framework.Services.Puzzles.Base
|
||||
AddressableManager = addressableManager;
|
||||
AssetRegistry = assetRegistry;
|
||||
PuzzleService = puzzleService;
|
||||
DestructionService = destructionService;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace BriarQueen.Framework.Services.Puzzles
|
||||
{
|
||||
private readonly SaveManager _saveManager;
|
||||
|
||||
private BasePuzzle _currentBasePuzzle;
|
||||
private readonly Dictionary<string, BasePuzzle> _activePuzzles = new();
|
||||
private bool _isWritingState;
|
||||
|
||||
[Inject]
|
||||
@@ -31,40 +31,98 @@ namespace BriarQueen.Framework.Services.Puzzles
|
||||
|
||||
public async UniTask LoadPuzzle(BasePuzzle basePuzzle)
|
||||
{
|
||||
_currentBasePuzzle = basePuzzle;
|
||||
if (_currentBasePuzzle == null)
|
||||
if (basePuzzle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await TryRestorePuzzleState(_currentBasePuzzle);
|
||||
if (string.IsNullOrWhiteSpace(basePuzzle.PuzzleID))
|
||||
{
|
||||
Debug.LogWarning($"[PuzzleService] Cannot load puzzle '{basePuzzle.name}' with null/empty PuzzleID.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activePuzzles[basePuzzle.PuzzleID] = basePuzzle;
|
||||
|
||||
await basePuzzle.PostLoad();
|
||||
await TryRestorePuzzleState(basePuzzle);
|
||||
}
|
||||
|
||||
public async UniTask LoadPuzzles(IEnumerable<BasePuzzle> basePuzzles)
|
||||
{
|
||||
_activePuzzles.Clear();
|
||||
|
||||
if (basePuzzles == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var basePuzzle in basePuzzles)
|
||||
{
|
||||
await LoadPuzzle(basePuzzle);
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask SavePuzzle(BasePuzzle basePuzzle)
|
||||
{
|
||||
if (basePuzzle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentBasePuzzle != null && basePuzzle != _currentBasePuzzle)
|
||||
if (!string.IsNullOrWhiteSpace(basePuzzle.PuzzleID) &&
|
||||
_activePuzzles.TryGetValue(basePuzzle.PuzzleID, out var activePuzzle) &&
|
||||
activePuzzle != basePuzzle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SavePuzzleState(basePuzzle, flushToDisk: true);
|
||||
_currentBasePuzzle = null;
|
||||
await basePuzzle.PreUnload();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(basePuzzle.PuzzleID))
|
||||
{
|
||||
_activePuzzles.Remove(basePuzzle.PuzzleID);
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask SavePuzzles(IEnumerable<BasePuzzle> basePuzzles)
|
||||
{
|
||||
if (basePuzzles != null)
|
||||
{
|
||||
foreach (var basePuzzle in basePuzzles)
|
||||
{
|
||||
await SavePuzzle(basePuzzle);
|
||||
}
|
||||
}
|
||||
|
||||
_activePuzzles.Clear();
|
||||
}
|
||||
|
||||
private async UniTask OnBeforeSaveRequestedAsync()
|
||||
{
|
||||
if (_currentBasePuzzle == null)
|
||||
if (_activePuzzles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SavePuzzleState(_currentBasePuzzle, flushToDisk: false);
|
||||
foreach (var basePuzzle in _activePuzzles.Values.ToList())
|
||||
{
|
||||
await SavePuzzleState(basePuzzle, flushToDisk: false);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask TryRestorePuzzleState(BasePuzzle basePuzzle)
|
||||
{
|
||||
if (basePuzzle == null || _saveManager.CurrentSave == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (basePuzzle is not IPuzzleStateful stateful)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
var entry = save.PuzzleStates?.FirstOrDefault(x => x != null && x.PuzzleID == basePuzzle.PuzzleID);
|
||||
@@ -83,20 +141,28 @@ namespace BriarQueen.Framework.Services.Puzzles
|
||||
private async UniTask SavePuzzleState(BasePuzzle basePuzzle, bool flushToDisk)
|
||||
{
|
||||
if (basePuzzle == null || _saveManager.CurrentSave == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (basePuzzle is not IPuzzleStateful stateful)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isWritingState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isWritingState = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (basePuzzle is IPuzzleWorldStateSync worldStateSync)
|
||||
{
|
||||
worldStateSync.SyncWorldStateToSave();
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
save.PuzzleStates ??= new List<PuzzleStateSaveData>();
|
||||
@@ -123,7 +189,9 @@ namespace BriarQueen.Framework.Services.Puzzles
|
||||
existing.Completed = stateful.IsCompleted;
|
||||
|
||||
if (flushToDisk)
|
||||
{
|
||||
await _saveManager.SaveGameDataLatest();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -131,4 +199,4 @@ namespace BriarQueen.Framework.Services.Puzzles
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ namespace BriarQueen.Framework.Services.Settings.Data
|
||||
public float MusicVolume;
|
||||
public float SfxVolume;
|
||||
public float VoiceVolume;
|
||||
public float AmbienceVolume;
|
||||
public float UIVolume;
|
||||
public bool MuteWhenUnfocused;
|
||||
|
||||
public AudioSettings()
|
||||
{
|
||||
@@ -19,9 +16,6 @@ namespace BriarQueen.Framework.Services.Settings.Data
|
||||
MusicVolume = 0.75f; // 75%
|
||||
SfxVolume = 0.75f; // 75%
|
||||
VoiceVolume = 1.0f; // 100%
|
||||
AmbienceVolume = 0.75f; // 75%
|
||||
UIVolume = 0.5f; // 50%
|
||||
MuteWhenUnfocused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@ namespace BriarQueen.Framework.Services.Settings
|
||||
AudioMixerGroups.MASTER_GROUP => Audio.MasterVolume,
|
||||
AudioMixerGroups.MUSIC_GROUP => Audio.MusicVolume,
|
||||
AudioMixerGroups.SFX_GROUP => Audio.SfxVolume,
|
||||
AudioMixerGroups.UI_GROUP => Audio.UIVolume,
|
||||
AudioMixerGroups.VOICE_GROUP => Audio.VoiceVolume,
|
||||
_ => Audio.MasterVolume
|
||||
};
|
||||
@@ -111,10 +110,6 @@ namespace BriarQueen.Framework.Services.Settings
|
||||
_audioManager.SetVolume(AudioMixerParameters.MUSIC_VOLUME, a.MusicVolume);
|
||||
_audioManager.SetVolume(AudioMixerParameters.SFX_VOLUME, a.SfxVolume);
|
||||
_audioManager.SetVolume(AudioMixerParameters.VOICE_VOLUME, a.VoiceVolume);
|
||||
_audioManager.SetVolume(AudioMixerParameters.AMBIENCE_VOLUME, a.AmbienceVolume);
|
||||
_audioManager.SetVolume(AudioMixerParameters.UI_VOLUME, a.UIVolume);
|
||||
|
||||
Application.runInBackground = !a.MuteWhenUnfocused;
|
||||
}
|
||||
|
||||
private void ApplyVisual(VisualSettings v)
|
||||
|
||||
@@ -2,8 +2,11 @@ using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine.InputSystem;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Services.Tutorials
|
||||
@@ -11,23 +14,26 @@ namespace BriarQueen.Framework.Services.Tutorials
|
||||
public class TutorialService
|
||||
{
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly InputManager _inputManager;
|
||||
|
||||
[Inject]
|
||||
public TutorialService(
|
||||
EventCoordinator eventCoordinator,
|
||||
SettingsService settingsService,
|
||||
SaveManager saveManager)
|
||||
SettingsService settingsService,
|
||||
SaveManager saveManager,
|
||||
InputManager inputManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_settingsService = settingsService;
|
||||
_saveManager = saveManager;
|
||||
_settingsService = settingsService;
|
||||
_saveManager = saveManager;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
public void DisplayTutorial(TutorialPopupID tutorialPopupID)
|
||||
{
|
||||
var save = _saveManager.CurrentSave;
|
||||
var save = _saveManager.CurrentSave;
|
||||
var tutorialVars = save?.PersistentVariables?.TutorialPopupVariables;
|
||||
|
||||
if (tutorialVars == null)
|
||||
@@ -39,9 +45,60 @@ namespace BriarQueen.Framework.Services.Tutorials
|
||||
tutorialVars.MarkDisplayed(tutorialPopupID);
|
||||
|
||||
if (_settingsService.AreTutorialsEnabled())
|
||||
_eventCoordinator.Publish(new DisplayTutorialPopupEvent(tutorialPopupID));
|
||||
{
|
||||
var resolvedText = ResolveText(tutorialPopupID);
|
||||
_eventCoordinator.Publish(new DisplayTutorialPopupEvent(tutorialPopupID, resolvedText));
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
// ── Text resolution ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Resolves {ActionName} tokens in the tutorial text for the given ID
|
||||
/// to the current binding display string for that action.
|
||||
/// Hotswap-safe — reads the current control scheme at call time.
|
||||
/// </summary>
|
||||
public string ResolveText(TutorialPopupID id)
|
||||
{
|
||||
if (!TutorialPopupTexts.AllPopups.TryGetValue(id, out var template))
|
||||
return string.Empty;
|
||||
|
||||
return ResolveText(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves {ActionName} tokens in an arbitrary string to the current
|
||||
/// binding display string for that action.
|
||||
/// </summary>
|
||||
public string ResolveText(string template)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return template;
|
||||
|
||||
return Regex.Replace(template, @"\{(\w+)\}", match =>
|
||||
{
|
||||
var actionName = match.Groups[1].Value;
|
||||
var binding = GetBindingDisplayString(actionName);
|
||||
return string.IsNullOrWhiteSpace(binding) ? match.Value : binding;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the display string for a named action's current binding,
|
||||
/// matched to the active control scheme.
|
||||
/// </summary>
|
||||
public string GetBindingDisplayString(string actionName)
|
||||
{
|
||||
if (_inputManager == null) return string.Empty;
|
||||
|
||||
var action = _inputManager.GetAction(actionName);
|
||||
if (action == null) return string.Empty;
|
||||
|
||||
var displayString = action.GetBindingDisplayString(group: _inputManager.CurrentControlScheme);
|
||||
|
||||
return string.IsNullOrWhiteSpace(displayString) ? string.Empty : displayString;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user