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

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ff2ed8f2f4124c45aad87cacd37abf0d
timeCreated: 1769705501

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Managers.Assets.Components;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
using VContainer;
using VContainer.Unity;
namespace BriarQueen.Framework.Managers.Assets
{
public class AddressableManager : IDisposable
{
private readonly Dictionary<object, AsyncOperationHandle> _assetHandles = new();
private readonly Dictionary<GameObject, AsyncOperationHandle> _instanceHandles = new();
private readonly IObjectResolver _lifetimeContainer;
private readonly object _lock = new();
[Inject]
public AddressableManager(IObjectResolver lifetimeContainer)
{
_lifetimeContainer = lifetimeContainer;
}
public void Dispose()
{
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();
}
}
public async UniTask<T> LoadAssetAsync<T>(AssetReference reference) where T : class
{
var handle = Addressables.LoadAssetAsync<T>(reference);
try
{
await handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
lock (_lock)
{
_assetHandles[handle.Result] = handle;
}
return handle.Result;
}
Debug.LogError($"[AddressableManager] Failed to load asset: {reference.RuntimeKey}");
return null;
}
catch (OperationCanceledException)
{
if (handle.IsValid())
Addressables.Release(handle);
throw;
}
}
public void ReleaseAsset<T>(T asset) where T : class
{
if (asset == null) return;
lock (_lock)
{
if (_assetHandles.TryGetValue(asset, out var handle))
{
if (handle.IsValid())
Addressables.Release(handle);
_assetHandles.Remove(asset);
}
}
}
public async UniTask<AsyncOperationHandle<SceneInstance>> LoadSceneAsync(
AssetReference assetReference,
LoadSceneMode loadSceneMode = LoadSceneMode.Additive,
CancellationToken cancellationToken = default,
IProgress<float> progress = null,
bool autoLoad = true
)
{
var handle = Addressables.LoadSceneAsync(assetReference, loadSceneMode, autoLoad);
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)
{
if (sceneHandle.IsValid()) await Addressables.UnloadSceneAsync(sceneHandle);
}
public async UniTask<GameObject> InstantiateAsync(
AssetReference reference,
Vector3 position = default,
Quaternion rotation = default,
Transform parent = null,
IObjectResolver scope = null,
CancellationToken cancellationToken = default
)
{
var handle = Addressables.InstantiateAsync(reference, position, rotation, parent);
GameObject go = null;
NotifyOnDestruction notify = null;
Action onInstanceDestroyed = null;
try
{
await handle.WithCancellation(cancellationToken);
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[AddressableManager] Failed to instantiate asset: {reference.RuntimeKey}");
return null;
}
go = handle.Result;
var prefabScope = go.GetComponent<LifetimeScope>();
var injectionScope = scope ?? prefabScope?.Container ?? _lifetimeContainer;
injectionScope.InjectGameObject(go);
notify = go.GetComponent<NotifyOnDestruction>();
if (!notify)
{
notify = go.AddComponent<NotifyOnDestruction>();
}
onInstanceDestroyed = () =>
{
TryReleaseInstance(go);
notify.OnDestroyedCalled -= onInstanceDestroyed;
};
notify.OnDestroyedCalled += onInstanceDestroyed;
lock (_lock)
{
_instanceHandles[go] = handle;
}
return go;
}
catch (OperationCanceledException)
{
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
throw;
}
catch (Exception)
{
if (notify != null && onInstanceDestroyed != null)
{
notify.OnDestroyedCalled -= onInstanceDestroyed;
}
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
throw;
}
}
public bool TryReleaseInstance(GameObject instance)
{
if (instance == null)
return false;
Debug.Log($"[AddressableManager] Trying to release instance: {instance}");
lock (_lock)
{
if (_instanceHandles.TryGetValue(instance, out var handle))
{
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
_instanceHandles.Remove(instance);
return true;
}
}
return false;
}
public void ReleaseInstance(GameObject instance)
{
if (instance == null) return;
lock (_lock)
{
if (_instanceHandles.TryGetValue(instance, out var handle))
{
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
_instanceHandles.Remove(instance);
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e1db17fcf6194082baad6949fa448ca0
timeCreated: 1769705501

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ff405febbf4c423ea26f3a0313bef4d5
timeCreated: 1773836297

View File

@@ -0,0 +1,43 @@
using System;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Framework.Managers.Assets.Components
{
public class NotifyOnDestruction : MonoBehaviour, IDestructible
{
private bool _destroyedNotified;
public UniTask OnPreDestroy()
{
OnPreDestroyCalled?.Invoke();
return UniTask.CompletedTask;
}
public UniTask OnDestroyed()
{
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

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e2aae9ec81f949ffbc5de664b65b46b3
timeCreated: 1769705628

View File

@@ -0,0 +1,6 @@
namespace BriarQueen.Framework.Managers.Assets
{
public interface IAssetProvider
{
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84550fd141464293b5d6680bdb50bf93
timeCreated: 1769705539

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