Restructured for new direction.

This commit is contained in:
2026-05-12 12:01:09 +01:00
parent 0439b6c1d2
commit c203f836b1
1134 changed files with 125569 additions and 213519 deletions

View File

@@ -10,7 +10,8 @@
"GUID:593a5b492d29ac6448b1ebf7f035ef33",
"GUID:84651a3751eca9349aac36a66bba901b",
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:776d03a35f1b52c4a9aed9f56d7b4229"
"GUID:776d03a35f1b52c4a9aed9f56d7b4229",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eca349fcd212e4ac18902c56b71847e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e1ca11eb5d39d4da19232afd2e808c96

View 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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7cb0728fb51e41b2aa93ec51993d9150
timeCreated: 1770379821

View 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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 912c8bc1a5f84113848a078f0581c8ce
timeCreated: 1778334335

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d3e14c91f3c942fc84e800d2fb583fb0

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.UI
{
public record UIBackRequestedEvent : IEvent;
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4cf9029b5f1a4c4f8740cb7394c1b5f8
timeCreated: 1778300000

View File

@@ -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
}
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -1,4 +1,4 @@
namespace BriarQueen.Framework.Assets
namespace BriarQueen.Framework.Managers.Assets
{
public interface IAssetProvider
{

View File

@@ -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;
}
}
}

View File

@@ -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()

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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()

View File

@@ -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());
}
}
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,7 @@
namespace BriarQueen.Framework.Managers.UI.Base
{
public interface IUIBackHandler
{
bool HandleBackRequest();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 18b01f6ab9d3468ba9a99360acbe4e5c
timeCreated: 1778300000

View File

@@ -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;
}
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: be2d54c81fb1f49eb974c7b2b0a91f47

View File

@@ -1,5 +1,5 @@
using System;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Managers.Assets;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;

View File

@@ -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
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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;
}
}
}