Restructured for new direction.
This commit is contained in:
3
Assets/Scripts/Framework/Managers/Assets.meta
Normal file
3
Assets/Scripts/Framework/Managers/Assets.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff2ed8f2f4124c45aad87cacd37abf0d
|
||||
timeCreated: 1769705501
|
||||
235
Assets/Scripts/Framework/Managers/Assets/AddressableManager.cs
Normal file
235
Assets/Scripts/Framework/Managers/Assets/AddressableManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1db17fcf6194082baad6949fa448ca0
|
||||
timeCreated: 1769705501
|
||||
3
Assets/Scripts/Framework/Managers/Assets/Components.meta
Normal file
3
Assets/Scripts/Framework/Managers/Assets/Components.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff405febbf4c423ea26f3a0313bef4d5
|
||||
timeCreated: 1773836297
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2aae9ec81f949ffbc5de664b65b46b3
|
||||
timeCreated: 1769705628
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace BriarQueen.Framework.Managers.Assets
|
||||
{
|
||||
public interface IAssetProvider
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84550fd141464293b5d6680bdb50bf93
|
||||
timeCreated: 1769705539
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
@@ -23,41 +22,38 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
/// - Settings set "base" volumes (in dB) per mixer parameter.
|
||||
/// - Runtime states (Pause duck, Voice duck) apply "modifiers" (extra dB offsets).
|
||||
/// - Effective mixer value is always: effectiveDb = baseDb + modifiersDb
|
||||
/// - UI and Ambience route through SFX channel/group.
|
||||
/// - SFX pool is transient — new channels are spawned on demand and
|
||||
/// finished channels are reaped before each play.
|
||||
/// </summary>
|
||||
public class AudioManager : IDisposable, IManager
|
||||
{
|
||||
private const int INITIAL_AMBIENCE_SOURCES = 3;
|
||||
private const int INITIAL_SFX_SOURCES = 6;
|
||||
|
||||
private const float PAUSE_DUCK_TARGET_DB = -18f;
|
||||
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
|
||||
private const int INITIAL_SFX_SOURCES = 8;
|
||||
private const float PAUSE_DUCK_TARGET_DB = -18f;
|
||||
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
|
||||
private const float DEFAULT_VOICE_DUCK_TARGET_DB = -20f;
|
||||
|
||||
private readonly AudioMixer _audioMixer;
|
||||
private readonly AudioRegistry _audioRegistry;
|
||||
private readonly AudioMixer _audioMixer;
|
||||
private readonly AudioRegistry _audioRegistry;
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
|
||||
private readonly Dictionary<string, float> _baseDb = new();
|
||||
private readonly List<GameObject> _createdAudioObjects = new();
|
||||
private readonly List<AudioSource> _ambienceSources = new();
|
||||
private readonly List<AudioFileSo> _currentAmbienceTracks = new();
|
||||
private readonly List<SfxChannel> _sfxChannels = new();
|
||||
private readonly Dictionary<string, float> _baseDb = new();
|
||||
private readonly List<GameObject> _createdAudioObjects = new();
|
||||
private readonly List<SfxChannel> _sfxChannels = new();
|
||||
|
||||
private AudioSource _musicSourceA;
|
||||
private AudioSource _musicSourceB;
|
||||
private AudioSource _voiceSource;
|
||||
private AudioSource _uiSource;
|
||||
|
||||
private string _activeVoiceSubtitleId;
|
||||
private string _activeVoiceSubtitleId;
|
||||
private AudioFileSo _currentMusicTrack;
|
||||
|
||||
private CancellationTokenSource _musicDuckCts;
|
||||
private CancellationTokenSource _musicFadeCts;
|
||||
private CancellationTokenSource _voiceCts;
|
||||
|
||||
private float _musicDuckDbCurrent;
|
||||
private float _pauseDuckDbCurrent;
|
||||
|
||||
private float _musicDuckDbCurrent;
|
||||
private float _pauseDuckDbCurrent;
|
||||
private Sequence _musicDuckSequence;
|
||||
private Sequence _pauseDuckSequence;
|
||||
|
||||
@@ -69,8 +65,8 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
[Inject]
|
||||
public AudioManager(AudioMixer mainMixer, AudioRegistry audioRegistry, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_audioMixer = mainMixer;
|
||||
_audioRegistry = audioRegistry;
|
||||
_audioMixer = mainMixer;
|
||||
_audioRegistry = audioRegistry;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
@@ -137,53 +133,41 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
}
|
||||
|
||||
_createdAudioObjects.Clear();
|
||||
_ambienceSources.Clear();
|
||||
_sfxChannels.Clear();
|
||||
_currentAmbienceTracks.Clear();
|
||||
_baseDb.Clear();
|
||||
|
||||
_musicSourceA = null;
|
||||
_musicSourceB = null;
|
||||
_voiceSource = null;
|
||||
_uiSource = null;
|
||||
_currentMusicTrack = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_musicSourceA = null;
|
||||
_musicSourceB = null;
|
||||
_voiceSource = null;
|
||||
_currentMusicTrack = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_voiceFinishedPublished = false;
|
||||
Initialized = false;
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
// ── Source creation ───────────────────────────────────────────
|
||||
|
||||
private void CreateSources()
|
||||
{
|
||||
_musicSourceA = CreateAudioSource("Music_Source_A", AudioMixerGroups.MUSIC_GROUP);
|
||||
_musicSourceB = CreateAudioSource("Music_Source_B", AudioMixerGroups.MUSIC_GROUP);
|
||||
|
||||
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
|
||||
_uiSource = CreateAudioSource("UI_Source", AudioMixerGroups.UI_GROUP);
|
||||
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
|
||||
|
||||
for (var i = 0; i < INITIAL_SFX_SOURCES; i++)
|
||||
{
|
||||
var src = CreateAudioSource($"SFX_Source_{i}", AudioMixerGroups.SFX_GROUP);
|
||||
_sfxChannels.Add(new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = -999f
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < INITIAL_AMBIENCE_SOURCES; i++)
|
||||
{
|
||||
_ambienceSources.Add(CreateAudioSource($"Ambience_Source_{i}", AudioMixerGroups.AMBIENCE_GROUP));
|
||||
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Volume ────────────────────────────────────────────────────
|
||||
|
||||
private void PrimeMixerBaseValues()
|
||||
{
|
||||
PrimeBaseFromMixer(AudioMixerParameters.MASTER_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.MUSIC_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.SFX_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.VOICE_VOLUME);
|
||||
PrimeBaseFromMixer(AudioMixerParameters.UI_VOLUME);
|
||||
}
|
||||
|
||||
public void SetVolume(string parameter, float value01)
|
||||
@@ -194,25 +178,18 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
return;
|
||||
}
|
||||
|
||||
var linear = Mathf.Clamp01(value01);
|
||||
var db = Linear01ToDb(linear);
|
||||
|
||||
_baseDb[parameter] = db;
|
||||
_baseDb[parameter] = Linear01ToDb(Mathf.Clamp01(value01));
|
||||
ApplyEffectiveVolume(parameter);
|
||||
}
|
||||
|
||||
private static float Linear01ToDb(float linear01)
|
||||
{
|
||||
var lin = Mathf.Max(linear01, 0.0001f);
|
||||
return Mathf.Log10(lin) * 20f;
|
||||
return Mathf.Log10(Mathf.Max(linear01, 0.0001f)) * 20f;
|
||||
}
|
||||
|
||||
private void PrimeBaseFromMixer(string parameter)
|
||||
{
|
||||
if (_audioMixer != null && _audioMixer.GetFloat(parameter, out var db))
|
||||
_baseDb[parameter] = db;
|
||||
else
|
||||
_baseDb[parameter] = 0f;
|
||||
_baseDb[parameter] = _audioMixer != null && _audioMixer.GetFloat(parameter, out var db) ? db : 0f;
|
||||
}
|
||||
|
||||
private void ApplyAllEffectiveVolumes()
|
||||
@@ -220,9 +197,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MASTER_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.VOICE_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.UI_VOLUME);
|
||||
}
|
||||
|
||||
private void ApplyEffectiveVolume(string parameter)
|
||||
@@ -236,8 +211,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
var effective = baseDb;
|
||||
|
||||
if (parameter == AudioMixerParameters.MUSIC_VOLUME ||
|
||||
parameter == AudioMixerParameters.SFX_VOLUME ||
|
||||
parameter == AudioMixerParameters.AMBIENCE_VOLUME)
|
||||
parameter == AudioMixerParameters.SFX_VOLUME)
|
||||
{
|
||||
effective += _pauseDuckDbCurrent;
|
||||
}
|
||||
@@ -248,10 +222,11 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_audioMixer.SetFloat(parameter, effective);
|
||||
}
|
||||
|
||||
// ── UI stack / pause duck ─────────────────────────────────────
|
||||
|
||||
private void OnUIStackChanged(UIStackChangedEvent e)
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
if (!Initialized) return;
|
||||
|
||||
if (e.AnyUIOpen)
|
||||
OnGamePausedInternal().Forget();
|
||||
@@ -279,20 +254,18 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_pauseDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _pauseDuckDbCurrent;
|
||||
|
||||
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
Mathf.Max(0f, seconds),
|
||||
v =>
|
||||
{
|
||||
_pauseDuckDbCurrent = v;
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
},
|
||||
Ease.OutCubic,
|
||||
useUnscaledTime: true));
|
||||
@@ -303,15 +276,13 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
|
||||
public void PauseVoiceSource(bool paused)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null)
|
||||
return;
|
||||
|
||||
if (paused)
|
||||
_voiceSource.Pause();
|
||||
else
|
||||
_voiceSource.UnPause();
|
||||
if (!Initialized || _voiceSource == null) return;
|
||||
if (paused) _voiceSource.Pause();
|
||||
else _voiceSource.UnPause();
|
||||
}
|
||||
|
||||
// ── Play ──────────────────────────────────────────────────────
|
||||
|
||||
public void Play(string audioName)
|
||||
{
|
||||
if (!Initialized)
|
||||
@@ -344,21 +315,11 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
break;
|
||||
|
||||
case TrackType.Ambience:
|
||||
if (!_currentAmbienceTracks.Contains(audioData))
|
||||
{
|
||||
_currentAmbienceTracks.Add(audioData);
|
||||
PlayOnAvailableAmbienceSource(audioData);
|
||||
}
|
||||
break;
|
||||
|
||||
case TrackType.UIFX:
|
||||
case TrackType.Sfx:
|
||||
PlaySfx(audioData);
|
||||
break;
|
||||
|
||||
case TrackType.UIFX:
|
||||
PlayOneShotAsync(_uiSource, audioData).Forget();
|
||||
break;
|
||||
|
||||
case TrackType.Voice:
|
||||
PlayVoiceLine(audioData).Forget();
|
||||
break;
|
||||
@@ -368,6 +329,8 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
|
||||
}
|
||||
|
||||
// ── Voice ─────────────────────────────────────────────────────
|
||||
|
||||
private async UniTaskVoid PlayVoiceLine(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null || audioData?.Clip == null)
|
||||
@@ -377,15 +340,15 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_voiceCts = new CancellationTokenSource();
|
||||
var token = _voiceCts.Token;
|
||||
|
||||
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
|
||||
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
|
||||
_voiceFinishedPublished = false;
|
||||
|
||||
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
|
||||
|
||||
_voiceSource.clip = audioData.Clip;
|
||||
_voiceSource.pitch = audioData.Pitch;
|
||||
_voiceSource.volume = audioData.Volume;
|
||||
_voiceSource.loop = false;
|
||||
_voiceSource.clip = audioData.Clip;
|
||||
_voiceSource.pitch = audioData.Pitch;
|
||||
_voiceSource.volume = audioData.Volume;
|
||||
_voiceSource.loop = false;
|
||||
_voiceSource.priority = audioData.Priority;
|
||||
_voiceSource.Play();
|
||||
|
||||
@@ -404,31 +367,210 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
|
||||
private void PublishVoiceFinishedIfNeeded()
|
||||
{
|
||||
if (_voiceFinishedPublished)
|
||||
return;
|
||||
if (_voiceFinishedPublished) return;
|
||||
|
||||
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
|
||||
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
|
||||
|
||||
_voiceFinishedPublished = true;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
}
|
||||
|
||||
public void StopVoice()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _voiceCts);
|
||||
|
||||
if (_voiceSource != null && _voiceSource.isPlaying)
|
||||
_voiceSource.Stop();
|
||||
|
||||
PublishVoiceFinishedIfNeeded();
|
||||
}
|
||||
|
||||
// ── SFX (transient pool) ──────────────────────────────────────
|
||||
|
||||
private void PlaySfx(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
// Reap finished channels first so we don't accumulate stale entries
|
||||
ReapFinishedSfxChannels();
|
||||
|
||||
// Try to find a free channel from the existing pool
|
||||
AudioSource src = null;
|
||||
var channelIndex = -1;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var s = _sfxChannels[i].Source;
|
||||
if (s != null && !s.isPlaying)
|
||||
{
|
||||
src = s;
|
||||
channelIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No free channel — spawn a transient one
|
||||
if (src == null)
|
||||
{
|
||||
src = CreateAudioSource($"SFX_Source_Transient_{_sfxChannels.Count}", AudioMixerGroups.SFX_GROUP);
|
||||
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
|
||||
channelIndex = _sfxChannels.Count - 1;
|
||||
|
||||
Debug.Log($"[AudioManager] SFX pool expanded to {_sfxChannels.Count} channels.");
|
||||
}
|
||||
|
||||
src.priority = audioData.Priority;
|
||||
src.pitch = audioData.Pitch;
|
||||
src.loop = audioData.Loopable;
|
||||
src.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
_sfxChannels[channelIndex] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = Time.unscaledTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes finished transient channels from the pool to prevent unbounded growth.
|
||||
/// Preserves the initial pool channels even when idle.
|
||||
/// </summary>
|
||||
private void ReapFinishedSfxChannels()
|
||||
{
|
||||
for (var i = _sfxChannels.Count - 1; i >= INITIAL_SFX_SOURCES; i--)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null || src.isPlaying)
|
||||
continue;
|
||||
|
||||
// Destroy the transient GameObject and remove from pool
|
||||
if (src.gameObject != null)
|
||||
{
|
||||
_createdAudioObjects.Remove(src.gameObject);
|
||||
Object.Destroy(src.gameObject);
|
||||
}
|
||||
|
||||
_sfxChannels.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllSfx()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
for (var i = _sfxChannels.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null) continue;
|
||||
|
||||
src.Stop();
|
||||
|
||||
// Destroy transient channels, reset initial ones
|
||||
if (i >= INITIAL_SFX_SOURCES)
|
||||
{
|
||||
if (src.gameObject != null)
|
||||
{
|
||||
_createdAudioObjects.Remove(src.gameObject);
|
||||
Object.Destroy(src.gameObject);
|
||||
}
|
||||
|
||||
_sfxChannels.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sfxChannels[i] = new SfxChannel { Source = src, StartedAtUnscaled = -999f };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Music ─────────────────────────────────────────────────────
|
||||
|
||||
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
|
||||
{
|
||||
if (!Initialized || !newTrack || !newTrack.Clip) return;
|
||||
if (_currentMusicTrack == newTrack) return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
_musicFadeCts = new CancellationTokenSource();
|
||||
var token = _musicFadeCts.Token;
|
||||
|
||||
var activeSource = _musicSourceA.isPlaying ? _musicSourceA
|
||||
: _musicSourceB.isPlaying ? _musicSourceB
|
||||
: null;
|
||||
|
||||
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
|
||||
|
||||
PlayOnSource(inactiveSource, newTrack);
|
||||
|
||||
if (activeSource == null)
|
||||
{
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
return;
|
||||
}
|
||||
|
||||
duration = Mathf.Max(0.0001f, duration);
|
||||
|
||||
var elapsed = 0f;
|
||||
var startVolume = activeSource.volume;
|
||||
|
||||
try
|
||||
{
|
||||
while (elapsed < duration)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var t = elapsed / duration;
|
||||
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
||||
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
||||
}
|
||||
|
||||
activeSource.volume = 0f;
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activeSource.Stop();
|
||||
activeSource.volume = startVolume;
|
||||
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
}
|
||||
|
||||
public void StopMusic()
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
|
||||
if (_musicSourceA != null) { _musicSourceA.Stop(); _musicSourceA.clip = null; _musicSourceA.volume = 0f; }
|
||||
if (_musicSourceB != null) { _musicSourceB.Stop(); _musicSourceB.clip = null; _musicSourceB.volume = 0f; }
|
||||
|
||||
_currentMusicTrack = null;
|
||||
}
|
||||
|
||||
private async UniTask DuckMusicAsync(float clipLengthSeconds, float fadeTimeSeconds)
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
if (!Initialized) return;
|
||||
|
||||
StopAndDispose(ref _musicDuckCts);
|
||||
_musicDuckCts = new CancellationTokenSource();
|
||||
var token = _musicDuckCts.Token;
|
||||
|
||||
fadeTimeSeconds = Mathf.Max(0.0001f, fadeTimeSeconds);
|
||||
var duckTarget = DEFAULT_VOICE_DUCK_TARGET_DB;
|
||||
|
||||
try
|
||||
{
|
||||
await TweenMusicDuckTo(duckTarget, fadeTimeSeconds, token);
|
||||
await TweenMusicDuckTo(DEFAULT_VOICE_DUCK_TARGET_DB, fadeTimeSeconds, token);
|
||||
|
||||
var hold = clipLengthSeconds - fadeTimeSeconds * 2f;
|
||||
if (hold > 0.01f)
|
||||
@@ -450,15 +592,13 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _musicDuckDbCurrent;
|
||||
|
||||
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
Mathf.Max(0f, seconds),
|
||||
v =>
|
||||
{
|
||||
_musicDuckDbCurrent = v;
|
||||
@@ -471,258 +611,22 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
|
||||
{
|
||||
if (!Initialized || !newTrack || !newTrack.Clip)
|
||||
return;
|
||||
|
||||
if (_currentMusicTrack == newTrack)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
_musicFadeCts = new CancellationTokenSource();
|
||||
var token = _musicFadeCts.Token;
|
||||
|
||||
var activeSource = _musicSourceA.isPlaying
|
||||
? _musicSourceA
|
||||
: _musicSourceB.isPlaying
|
||||
? _musicSourceB
|
||||
: null;
|
||||
|
||||
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
|
||||
|
||||
PlayOnSource(inactiveSource, newTrack);
|
||||
|
||||
if (activeSource == null)
|
||||
{
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
return;
|
||||
}
|
||||
|
||||
duration = Mathf.Max(0.0001f, duration);
|
||||
|
||||
var elapsed = 0f;
|
||||
var startVolume = activeSource.volume;
|
||||
|
||||
try
|
||||
{
|
||||
while (elapsed < duration)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var t = elapsed / duration;
|
||||
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
||||
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
|
||||
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
||||
}
|
||||
|
||||
activeSource.volume = 0f;
|
||||
inactiveSource.volume = newTrack.Volume;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activeSource.Stop();
|
||||
activeSource.volume = startVolume;
|
||||
|
||||
_currentMusicTrack = newTrack;
|
||||
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
||||
}
|
||||
|
||||
private void PlaySfx(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
var channelIndex = GetBestSfxChannelIndex(audioData.Priority);
|
||||
if (channelIndex < 0 || channelIndex >= _sfxChannels.Count)
|
||||
return;
|
||||
|
||||
var src = _sfxChannels[channelIndex].Source;
|
||||
if (src == null)
|
||||
return;
|
||||
|
||||
if (src.isPlaying)
|
||||
src.Stop();
|
||||
|
||||
src.priority = audioData.Priority;
|
||||
src.pitch = audioData.Pitch;
|
||||
src.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
_sfxChannels[channelIndex] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = Time.unscaledTime
|
||||
};
|
||||
}
|
||||
|
||||
private int GetBestSfxChannelIndex(int incomingPriority)
|
||||
{
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
if (!src.isPlaying)
|
||||
return i;
|
||||
}
|
||||
|
||||
var bestIndex = -1;
|
||||
var worstPriority = int.MinValue;
|
||||
var oldestStart = float.MaxValue;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
var p = src.priority;
|
||||
var started = _sfxChannels[i].StartedAtUnscaled;
|
||||
|
||||
if (p > worstPriority || (p == worstPriority && started < oldestStart))
|
||||
{
|
||||
worstPriority = p;
|
||||
oldestStart = started;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
public void StopAmbience(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || !audioData || !audioData.Clip || audioData.Type != TrackType.Ambience)
|
||||
return;
|
||||
|
||||
if (_currentAmbienceTracks.Remove(audioData))
|
||||
{
|
||||
foreach (var source in _ambienceSources.Where(s => s != null && s.clip == audioData.Clip))
|
||||
source.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllAmbience()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
foreach (var s in _ambienceSources)
|
||||
{
|
||||
if (s != null)
|
||||
s.Stop();
|
||||
}
|
||||
|
||||
_currentAmbienceTracks.Clear();
|
||||
}
|
||||
|
||||
private void PlayOnAvailableAmbienceSource(AudioFileSo audioData)
|
||||
{
|
||||
var source = _ambienceSources.FirstOrDefault(s => s != null && !s.isPlaying);
|
||||
if (source == null)
|
||||
{
|
||||
source = CreateAudioSource(
|
||||
$"Ambience_Source_{_ambienceSources.Count}",
|
||||
AudioMixerGroups.AMBIENCE_GROUP);
|
||||
|
||||
_ambienceSources.Add(source);
|
||||
}
|
||||
|
||||
PlayOnSource(source, audioData);
|
||||
}
|
||||
|
||||
public void StopMusic()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
|
||||
if (_musicSourceA != null)
|
||||
{
|
||||
_musicSourceA.Stop();
|
||||
_musicSourceA.clip = null;
|
||||
_musicSourceA.volume = 0f;
|
||||
}
|
||||
|
||||
if (_musicSourceB != null)
|
||||
{
|
||||
_musicSourceB.Stop();
|
||||
_musicSourceB.clip = null;
|
||||
_musicSourceB.volume = 0f;
|
||||
}
|
||||
|
||||
_currentMusicTrack = null;
|
||||
}
|
||||
|
||||
public void StopVoice()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _voiceCts);
|
||||
|
||||
if (_voiceSource != null && _voiceSource.isPlaying)
|
||||
_voiceSource.Stop();
|
||||
|
||||
PublishVoiceFinishedIfNeeded();
|
||||
}
|
||||
|
||||
public void StopAllSfx()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _sfxChannels.Count; i++)
|
||||
{
|
||||
var src = _sfxChannels[i].Source;
|
||||
if (src == null)
|
||||
continue;
|
||||
|
||||
src.Stop();
|
||||
_sfxChannels[i] = new SfxChannel
|
||||
{
|
||||
Source = src,
|
||||
StartedAtUnscaled = -999f
|
||||
};
|
||||
}
|
||||
}
|
||||
// ── Stop all ──────────────────────────────────────────────────
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
if (!Initialized) return;
|
||||
StopMusic();
|
||||
StopVoice();
|
||||
StopAllSfx();
|
||||
StopAllAmbience();
|
||||
|
||||
if (_uiSource != null)
|
||||
_uiSource.Stop();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
|
||||
private static void StopAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (cts == null) return;
|
||||
try { cts.Cancel(); } catch { }
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
@@ -733,7 +637,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
Object.DontDestroyOnLoad(obj);
|
||||
_createdAudioObjects.Add(obj);
|
||||
|
||||
var src = obj.AddComponent<AudioSource>();
|
||||
var src = obj.AddComponent<AudioSource>();
|
||||
var group = _audioMixer.FindMatchingGroups(groupName);
|
||||
|
||||
if (group != null && group.Length > 0)
|
||||
@@ -744,28 +648,14 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
return src;
|
||||
}
|
||||
|
||||
private async UniTaskVoid PlayOneShotAsync(AudioSource source, AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || source == null || audioData == null || audioData.Clip == null)
|
||||
return;
|
||||
|
||||
source.priority = audioData.Priority;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.PlayOneShot(audioData.Clip, audioData.Volume);
|
||||
|
||||
var seconds = audioData.Clip.length / Mathf.Max(audioData.Pitch, 0.0001f);
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
private void PlayOnSource(AudioSource source, AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || source == null || audioData == null)
|
||||
return;
|
||||
if (!Initialized || source == null || audioData == null) return;
|
||||
|
||||
source.clip = audioData.Clip;
|
||||
source.loop = audioData.Loopable;
|
||||
source.volume = audioData.Volume;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.clip = audioData.Clip;
|
||||
source.loop = audioData.Loopable;
|
||||
source.volume = audioData.Volume;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.priority = audioData.Priority;
|
||||
source.Play();
|
||||
}
|
||||
@@ -773,7 +663,7 @@ namespace BriarQueen.Framework.Managers.Audio
|
||||
private struct SfxChannel
|
||||
{
|
||||
public AudioSource Source;
|
||||
public float StartedAtUnscaled;
|
||||
public float StartedAtUnscaled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers
|
||||
@@ -13,6 +16,7 @@ namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
private SaveManager _saveManager;
|
||||
private PlayerManager _playerManager;
|
||||
private InputManager _inputManager;
|
||||
|
||||
[Header("Current Loaded Save")]
|
||||
[SerializeField, ReadOnly]
|
||||
@@ -23,10 +27,11 @@ namespace BriarQueen.Framework.Managers
|
||||
private ItemKey _itemToGive;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager, PlayerManager playerManager)
|
||||
public void Construct(SaveManager saveManager, PlayerManager playerManager, InputManager inputManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
_playerManager = playerManager;
|
||||
_inputManager = inputManager;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
@@ -38,6 +43,7 @@ namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
_currentSave = save;
|
||||
}
|
||||
|
||||
|
||||
[Button]
|
||||
private void GiveItem()
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
private readonly object _saveLock = new();
|
||||
|
||||
private CancellationTokenSource _currentSaveCts;
|
||||
private DateTime _lastSaveTime;
|
||||
|
||||
[Inject]
|
||||
public SaveManager(EventCoordinator eventCoordinator)
|
||||
@@ -112,12 +111,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
private async UniTask SaveGameDataInternal(CancellationToken ct)
|
||||
{
|
||||
if ((DateTime.UtcNow - _lastSaveTime).TotalMilliseconds < 250)
|
||||
{
|
||||
Debug.Log("[SaveManager] Last save within 250ms, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentSave == null)
|
||||
CurrentSave = new SaveGame { SaveFileName = "NewGame" };
|
||||
|
||||
@@ -186,7 +179,6 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
CurrentSave = saveClone;
|
||||
IsGameLoaded = true;
|
||||
_lastSaveTime = DateTime.UtcNow;
|
||||
|
||||
OnSaveGameSaved?.Invoke();
|
||||
Debug.Log($"[SaveManager] Save complete: {CurrentSave.SaveFileName}");
|
||||
@@ -272,8 +264,7 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
|
||||
if (loadedSave != null)
|
||||
{
|
||||
CurrentSave = loadedSave;
|
||||
await SaveGameDataLatest();
|
||||
RestoreBackupToMain(mainPath, backupPath);
|
||||
Debug.Log("[SaveManager] Restored save from backup.");
|
||||
}
|
||||
}
|
||||
@@ -285,6 +276,41 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
OnSaveGameLoaded?.Invoke(CurrentSave);
|
||||
}
|
||||
|
||||
private void RestoreBackupToMain(string mainPath, string backupPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mainPath) || string.IsNullOrWhiteSpace(backupPath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var mainDirectory = Path.GetDirectoryName(mainPath);
|
||||
if (!string.IsNullOrWhiteSpace(mainDirectory))
|
||||
Directory.CreateDirectory(mainDirectory);
|
||||
|
||||
var tempRestorePath = mainPath + ".restoretmp";
|
||||
|
||||
if (File.Exists(tempRestorePath))
|
||||
File.Delete(tempRestorePath);
|
||||
|
||||
File.Copy(backupPath, tempRestorePath, overwrite: true);
|
||||
|
||||
if (File.Exists(mainPath))
|
||||
File.Replace(tempRestorePath, mainPath, null, ignoreMetadataErrors: true);
|
||||
else
|
||||
File.Move(tempRestorePath, mainPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to restore backup '{backupPath}' to '{mainPath}': {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var tempRestorePath = mainPath + ".restoretmp";
|
||||
if (File.Exists(tempRestorePath))
|
||||
File.Delete(tempRestorePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<SaveGame> LoadFromFileAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
|
||||
@@ -432,4 +458,4 @@ namespace BriarQueen.Framework.Managers.IO
|
||||
return collected.Any(x => x.UniqueIdentifier == uniqueIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Game;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Input
|
||||
@@ -48,6 +47,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
|
||||
private bool _initialized;
|
||||
private bool _isPaused;
|
||||
private bool _isAnyUIOpen;
|
||||
private InputAction _pauseAction;
|
||||
|
||||
private InputAction _pointAction;
|
||||
@@ -58,7 +58,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
private InputAction _nextItemAction;
|
||||
private InputAction _previousItemAction;
|
||||
private InputAction _virtualMouseAction;
|
||||
private InputAction _submitAction;
|
||||
private InputAction _submitAction;
|
||||
|
||||
private UICursorService _uiCursorService;
|
||||
|
||||
@@ -77,6 +77,8 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
public bool IsPaused => _isPaused;
|
||||
|
||||
public bool UsingControllerCursor => DeviceInputType != DeviceInputType.KeyboardAndMouse;
|
||||
|
||||
public string CurrentControlScheme => _playerInput?.currentControlScheme ?? string.Empty;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -109,6 +111,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(OnCodexStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
|
||||
_eventCoordinator.Unsubscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
}
|
||||
|
||||
UnbindCoreInputs();
|
||||
@@ -148,28 +151,18 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerInput.actions == null)
|
||||
{
|
||||
Debug.LogWarning("[InputManager] PlayerInput.actions is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[InputManager] Current map before cache: {ActiveActionMap}");
|
||||
|
||||
CacheActions();
|
||||
|
||||
Debug.Log($"[InputManager] Point action: {_pointAction}");
|
||||
Debug.Log($"[InputManager] Click action: {_clickAction}");
|
||||
Debug.Log($"[InputManager] Virtual_Mouse action: {_virtualMouseAction}");
|
||||
|
||||
BindCoreInputs();
|
||||
|
||||
DeviceInputType = GetDeviceInputType(_playerInput);
|
||||
ApplyCursorModeForCurrentScheme();
|
||||
|
||||
_initialized = true;
|
||||
_eventCoordinator.Subscribe<UIToggleHudEvent>(OnHudStateChanged);
|
||||
_eventCoordinator.Subscribe<ToggleCodexEvent>(OnCodexStateChanged);
|
||||
_eventCoordinator.Subscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
|
||||
_eventCoordinator.Subscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
|
||||
Debug.Log("[InputManager] Initialization complete");
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private void CacheActions()
|
||||
@@ -205,14 +198,12 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
{
|
||||
if (_pointAction != null)
|
||||
{
|
||||
Debug.Log("[InputManager] Binding Point");
|
||||
|
||||
|
||||
_pointAction.performed += OnPoint;
|
||||
_pointAction.canceled += OnPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[InputManager] Required action 'Point' not found.");
|
||||
}
|
||||
|
||||
if (_virtualMouseAction != null)
|
||||
@@ -223,53 +214,33 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
|
||||
if (_pauseAction != null)
|
||||
_pauseAction.performed += OnPause;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Pause' not found.");
|
||||
|
||||
if (_clickAction != null)
|
||||
_clickAction.performed += OnClick;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Click' not found.");
|
||||
|
||||
if (_rightClickAction != null)
|
||||
_rightClickAction.performed += OnRightClick;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Right_Click' not found.");
|
||||
|
||||
if (_hideHudAction != null)
|
||||
_hideHudAction.performed += OnHideHUD;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Hide_HUD' not found.");
|
||||
|
||||
if (_codexAction != null)
|
||||
_codexAction.performed += OnCodex;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Codex' not found.");
|
||||
|
||||
if (_openToolsAction != null)
|
||||
_openToolsAction.performed += OnOpenTools;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Show_Tools' not found.");
|
||||
|
||||
if (_nextToolAction != null)
|
||||
_nextToolAction.performed += OnNextToolClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Next_Tool' not found.");
|
||||
|
||||
if (_previousToolAction != null)
|
||||
_previousToolAction.performed += OnPreviousToolClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Previous_Tool' not found.");
|
||||
|
||||
if (_nextItemAction != null)
|
||||
_nextItemAction.performed += OnNextItemClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Next_Item' not found.");
|
||||
|
||||
if (_previousItemAction != null)
|
||||
_previousItemAction.performed += OnPreviousItemClicked;
|
||||
else
|
||||
Debug.LogWarning("[InputManager] Action 'Previous_Item' not found.");
|
||||
|
||||
if (_playerInput != null)
|
||||
_playerInput.onControlsChanged += OnControlsChanged;
|
||||
@@ -343,7 +314,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
}
|
||||
|
||||
private void OnControlsChanged(PlayerInput playerInput)
|
||||
{ Debug.Log($"Controls changed. Scheme: {playerInput.currentControlScheme}");
|
||||
{
|
||||
DeviceInputType = GetDeviceInputType(playerInput);
|
||||
ApplyCursorModeForCurrentScheme();
|
||||
}
|
||||
@@ -391,7 +362,7 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
{
|
||||
if (_submitAction == null || callback == null)
|
||||
return;
|
||||
|
||||
|
||||
_submitAction.performed -= callback;
|
||||
}
|
||||
|
||||
@@ -401,11 +372,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
action.performed += callback;
|
||||
@@ -417,11 +383,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
}
|
||||
@@ -471,18 +432,28 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
_toolScreenShown = evt.Shown;
|
||||
}
|
||||
|
||||
private void OnUIStackChanged(UIStackChangedEvent evt)
|
||||
{
|
||||
_isAnyUIOpen = evt.AnyUIOpen;
|
||||
_isPaused = evt.AnyUIOpen && _gameService != null && !_gameService.IsMainMenuSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnHideHUD(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_hudHidden = !_hudHidden;
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(_hudHidden));
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(!_hudHidden));
|
||||
}
|
||||
|
||||
private void OnPause(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if(_gameService.IsMainMenuSceneLoaded)
|
||||
var isMainMenu = _gameService != null && _gameService.IsMainMenuSceneLoaded;
|
||||
if (isMainMenu || _isAnyUIOpen)
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new UIBackRequestedEvent());
|
||||
return;
|
||||
|
||||
_isPaused = !_isPaused;
|
||||
}
|
||||
|
||||
_isPaused = true;
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
@@ -556,5 +527,6 @@ namespace BriarQueen.Framework.Managers.Input
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
@@ -31,11 +32,16 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
[Tooltip("Used for custom tooltip. Defaults to Item Name")]
|
||||
[SerializeField]
|
||||
private string _interactableTooltip = string.Empty;
|
||||
protected string _interactableTooltip = string.Empty;
|
||||
[Tooltip("Optional. Used for custom interaction.")]
|
||||
[SerializeField]
|
||||
private string _pickupText = string.Empty;
|
||||
|
||||
[Header("Object Setup")]
|
||||
[SerializeField]
|
||||
protected CanvasGroup _canvasGroup;
|
||||
|
||||
protected bool _isLocked;
|
||||
|
||||
protected AddressableManager AddressableManager;
|
||||
protected AssetRegistry AssetRegistry;
|
||||
@@ -68,8 +74,23 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public virtual UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Pickup;
|
||||
|
||||
public virtual string InteractableName =>
|
||||
!string.IsNullOrWhiteSpace(_interactableTooltip) ? _interactableTooltip : _itemData.ItemName;
|
||||
public virtual string InteractableName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_interactableTooltip))
|
||||
{
|
||||
return _interactableTooltip;
|
||||
}
|
||||
|
||||
if (_itemData != null && !string.IsNullOrWhiteSpace(_itemData.ItemName))
|
||||
{
|
||||
return _itemData.ItemName;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -91,6 +112,15 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
await Pickup();
|
||||
await OnInteracted();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_pickupText))
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(_pickupText));
|
||||
}
|
||||
else
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.LooksImportant)));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual UniTask EnterHover()
|
||||
@@ -147,8 +177,8 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
protected virtual async UniTask Remove()
|
||||
{
|
||||
// TODO - Play Cut Vines SFX
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (PickupSequence.isAlive)
|
||||
{
|
||||
@@ -184,7 +214,7 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSaveGameOnRemoval()
|
||||
protected virtual void UpdateSaveGameOnRemoval()
|
||||
{
|
||||
var save = SaveManager.CurrentSave;
|
||||
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
|
||||
@@ -240,5 +270,35 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
await DestructionService.Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
_isLocked = true;
|
||||
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
_isLocked = false;
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
_canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
public void OnValidate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
var canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (!canvasGroup)
|
||||
{
|
||||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
_canvasGroup = canvasGroup;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
@@ -8,12 +10,19 @@ using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data.Codex;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Puzzles.Base;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
using SettingsService = BriarQueen.Framework.Services.Settings.SettingsService;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
@@ -30,6 +39,10 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public List<CodexTrigger> CodexTriggers;
|
||||
|
||||
[Header("Puzzles")]
|
||||
[SerializeField]
|
||||
public List<BasePuzzle> Puzzles;
|
||||
|
||||
[Header("Setup")]
|
||||
[SerializeField]
|
||||
protected GraphicRaycaster _raycaster;
|
||||
@@ -47,8 +60,6 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
|
||||
public virtual string LevelName => _levelName;
|
||||
|
||||
public virtual bool IsPuzzleLevel { get; }
|
||||
|
||||
public virtual int CurrentLevelHintStage { get; set; }
|
||||
|
||||
public virtual Dictionary<int, BaseHint> Hints { get; }
|
||||
@@ -112,5 +123,72 @@ namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Button("Discover Level References")]
|
||||
private void DiscoverLevelReferences()
|
||||
{
|
||||
Undo.RecordObject(this, "Discover Level References");
|
||||
|
||||
var discoveredCodexTriggers = GetComponentsInChildren<CodexTrigger>(true)
|
||||
.Where(trigger => trigger != null)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var discoveredPickups = GetComponentsInChildren<BaseItem>(true)
|
||||
.Where(item => item != null && item is not CodexTrigger)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var discoveredPuzzles = GetComponentsInChildren<BasePuzzle>(true)
|
||||
.Where(puzzle => puzzle != null)
|
||||
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Pickups = discoveredPickups;
|
||||
CodexTriggers = discoveredCodexTriggers;
|
||||
Puzzles = discoveredPuzzles;
|
||||
|
||||
EditorUtility.SetDirty(this);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
|
||||
|
||||
Debug.Log(
|
||||
$"[BaseLevel] Discovery complete for '{name}'. Pickups: {Pickups.Count}, CodexTriggers: {CodexTriggers.Count}, Puzzles: {Puzzles.Count}",
|
||||
this);
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(Component component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var names = new Stack<string>();
|
||||
var current = component.transform;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
names.Push(current.name);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return string.Join("/", names);
|
||||
}
|
||||
#endif
|
||||
|
||||
public void OnValidate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
CanvasScaler scaler = GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
scaler.referenceResolution = new Vector2(1920, 1200);
|
||||
|
||||
GraphicRaycaster raycaster = GetComponent<GraphicRaycaster>();
|
||||
_raycaster = raycaster;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Assets;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Progression;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Assets;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Puzzles;
|
||||
using BriarQueen.Framework.Services.Puzzles.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
@@ -33,6 +32,7 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private readonly SaveManager _saveManager;
|
||||
|
||||
private UniTask<bool> _activeLoadTask = UniTask.FromResult(false);
|
||||
private bool _isLoadInProgress;
|
||||
private BaseLevel _currentLevel;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
@@ -57,7 +57,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[{nameof(LevelManager)}] Initializing...");
|
||||
_saveManager.OnSaveRequested += OnSaveGameRequested;
|
||||
@@ -70,7 +72,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_saveManager.OnSaveRequested -= OnSaveGameRequested;
|
||||
_eventCoordinator.Unsubscribe<UpdateHintProgressEvent>(OnHintStageUpdated);
|
||||
@@ -81,10 +85,14 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private void OnHintStageUpdated(UpdateHintProgressEvent evt)
|
||||
{
|
||||
if (_currentLevel == null || evt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(evt.LevelID, _currentLevel.LevelID, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var incoming = Mathf.Max(0, evt.Stage);
|
||||
|
||||
@@ -101,7 +109,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private void OnSaveGameRequested(SaveGame saveGame)
|
||||
{
|
||||
if (saveGame == null || _currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
saveGame.CurrentLevelID = _currentLevel.LevelID;
|
||||
saveGame.CurrentSceneID = _currentLevel.SceneID;
|
||||
@@ -120,12 +130,73 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_activeLoadTask = LoadLevelInternal(levelAssetID);
|
||||
if (_isLoadInProgress)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] LoadLevel('{levelAssetID}') requested while another level load is already in progress. Returning the active load task.");
|
||||
return _activeLoadTask;
|
||||
}
|
||||
|
||||
_isLoadInProgress = true;
|
||||
_activeLoadTask = LoadLevelTracked(levelAssetID);
|
||||
return _activeLoadTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadLevelTracked(string levelAssetID)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await LoadLevelInternal(levelAssetID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isLoadInProgress = false;
|
||||
_activeLoadTask = UniTask.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadLevelInternal(string levelAssetID)
|
||||
{
|
||||
var previousLevelId = _currentLevel != null ? _currentLevel.LevelID : null;
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
await UnloadLevelInternal();
|
||||
}
|
||||
|
||||
if (await TryLoadLevelCore(levelAssetID))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(previousLevelId) &&
|
||||
!string.Equals(previousLevelId, levelAssetID, StringComparison.Ordinal))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] Failed to load '{levelAssetID}'. Attempting recovery by reloading previous level '{previousLevelId}'.");
|
||||
|
||||
if (await TryLoadLevelCore(previousLevelId))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LevelManager] Recovery succeeded by reloading '{previousLevelId}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(true, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTask<bool> TryLoadLevelCore(string levelAssetID)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -141,12 +212,6 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
return false;
|
||||
}
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
await UnloadLevelInternal();
|
||||
|
||||
var levelObj = await _addressableManager.InstantiateAsync(levelRef);
|
||||
if (levelObj == null)
|
||||
{
|
||||
@@ -157,7 +222,8 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
var level = levelObj.GetComponent<BaseLevel>();
|
||||
if (level == null)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
|
||||
Debug.LogError(
|
||||
$"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
|
||||
await _destructionService.Destroy(levelObj);
|
||||
return false;
|
||||
}
|
||||
@@ -169,8 +235,7 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
|
||||
await _currentLevel.PostLoad();
|
||||
|
||||
if (_currentLevel is BasePuzzle puzzle)
|
||||
await _puzzleService.LoadPuzzle(puzzle);
|
||||
await _puzzleService.LoadPuzzles(_currentLevel.Puzzles);
|
||||
|
||||
_eventCoordinator.Publish(new LevelChangedEvent(_currentLevel));
|
||||
|
||||
@@ -178,7 +243,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
await _currentLevel.PostActivate();
|
||||
}
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
return true;
|
||||
@@ -186,29 +253,38 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Exception while loading '{levelAssetID}': {ex}");
|
||||
|
||||
if (_currentLevel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _destructionService.Destroy(_currentLevel.gameObject);
|
||||
}
|
||||
catch (Exception destroyEx)
|
||||
{
|
||||
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
|
||||
}
|
||||
|
||||
_currentLevel = null;
|
||||
}
|
||||
|
||||
await CleanupFailedCurrentLevel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask CleanupFailedCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _destructionService.Destroy(_currentLevel.gameObject);
|
||||
}
|
||||
catch (Exception destroyEx)
|
||||
{
|
||||
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentLevel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreHintStageForCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save?.LevelHintStages == null)
|
||||
@@ -218,23 +294,33 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
}
|
||||
|
||||
if (save.LevelHintStages.TryGetValue(_currentLevel.LevelID, out var stage))
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = Mathf.Max(0, stage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask RestoreItemStateForCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var interactables = _currentLevel.Pickups;
|
||||
if (interactables == null || interactables.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var interactable in interactables)
|
||||
{
|
||||
@@ -245,10 +331,14 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
}
|
||||
|
||||
if (save.CollectedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
|
||||
{
|
||||
await _destructionService.Destroy(interactable.gameObject);
|
||||
}
|
||||
|
||||
if (save.RemovedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
|
||||
{
|
||||
await _destructionService.Destroy(interactable.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
var codexTriggers = _currentLevel.CodexTriggers;
|
||||
@@ -258,7 +348,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
if (save.DiscoveredCodexEntries.Any(x => x.UniqueIdentifier == trigger.Entry.UniqueID))
|
||||
{
|
||||
if (trigger.RemoveTrigger)
|
||||
{
|
||||
await _destructionService.Destroy(trigger.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +360,9 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
lock (_lock)
|
||||
{
|
||||
if (_activeLoadTask.Status == UniTaskStatus.Pending)
|
||||
{
|
||||
return _activeLoadTask.ContinueWith(_ => UnloadLevelInternal());
|
||||
}
|
||||
|
||||
return UnloadLevelInternal();
|
||||
}
|
||||
@@ -277,15 +371,16 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
private async UniTask UnloadLevelInternal()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var level = _currentLevel;
|
||||
_currentLevel = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (level is BasePuzzle puzzle)
|
||||
await _puzzleService.SavePuzzle(puzzle);
|
||||
await _puzzleService.SavePuzzles(level.Puzzles);
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
await level.PreUnload();
|
||||
@@ -298,4 +393,4 @@ namespace BriarQueen.Framework.Managers.Levels
|
||||
await _destructionService.Destroy(level.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,22 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
public class Codex
|
||||
{
|
||||
public Codex(bool unlocked = false)
|
||||
{
|
||||
CodexUnlocked = unlocked;
|
||||
}
|
||||
|
||||
public bool CodexUnlocked { get; private set; }
|
||||
|
||||
private readonly List<CodexEntrySo> _entries = new();
|
||||
|
||||
public IReadOnlyList<CodexEntrySo> Entries => _entries;
|
||||
|
||||
public void UnlockCodex()
|
||||
{
|
||||
CodexUnlocked = true;
|
||||
}
|
||||
|
||||
public void AddEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
@@ -66,7 +78,7 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetBookEntries()
|
||||
{
|
||||
return GetEntriesByType(CodexType.BookEntry);
|
||||
return GetEntriesByType(CodexType.DocumentEntry);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetPuzzleClues()
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
@@ -28,6 +33,12 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
if (!CheckEmptyHands())
|
||||
return;
|
||||
|
||||
if (!PlayerManager.CodexUnlocked())
|
||||
{
|
||||
EventCoordinator.PublishImmediate(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CodexLocked)));
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerManager.UnlockCodexEntry(_codexEntry);
|
||||
|
||||
@@ -36,5 +47,20 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
await Remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateSaveGameOnRemoval()
|
||||
{
|
||||
var save = SaveManager.CurrentSave;
|
||||
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
|
||||
|
||||
save.RemovedItems ??= new List<ItemSaveData>();
|
||||
|
||||
save.RemovedItems.Add(new ItemSaveData
|
||||
{
|
||||
UniqueIdentifier = _codexEntry.UniqueID
|
||||
});
|
||||
|
||||
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
[Header("Codex ID")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsBookEntry))]
|
||||
private BookEntryID _bookEntryID;
|
||||
private DocumentEntryID _documentEntryID;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPuzzleClue))]
|
||||
@@ -66,11 +66,11 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
public CodexType EntryType => _codexType;
|
||||
public Location Location => _location;
|
||||
|
||||
public bool IsBookEntry => _codexType == CodexType.BookEntry;
|
||||
public bool IsBookEntry => _codexType == CodexType.DocumentEntry;
|
||||
public bool IsPuzzleClue => _codexType == CodexType.PuzzleClue;
|
||||
public bool IsPhoto => _codexType == CodexType.Photo;
|
||||
|
||||
public BookEntryID BookEntryID => _bookEntryID;
|
||||
public DocumentEntryID DocumentEntryID => _documentEntryID;
|
||||
public ClueEntryID ClueEntryID => _clueEntryID;
|
||||
public PhotoEntryID PhotoEntryID => _photoEntryID;
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace BriarQueen.Framework.Managers.Player.Data
|
||||
{
|
||||
return _codexType switch
|
||||
{
|
||||
CodexType.BookEntry when _bookEntryID != BookEntryID.None =>
|
||||
CodexEntryIDs.Get(_bookEntryID),
|
||||
CodexType.DocumentEntry when _documentEntryID != DocumentEntryID.None =>
|
||||
CodexEntryIDs.Get(_documentEntryID),
|
||||
|
||||
CodexType.PuzzleClue when _clueEntryID != ClueEntryID.None =>
|
||||
CodexEntryIDs.Get(_clueEntryID),
|
||||
|
||||
@@ -7,7 +7,6 @@ using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Tools
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
}
|
||||
}
|
||||
|
||||
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemCollected));
|
||||
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemPickup));
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new InventoryChangedEvent());
|
||||
}
|
||||
@@ -208,11 +208,21 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
#region Codex
|
||||
|
||||
public void UnlockCodex() => _codex.UnlockCodex();
|
||||
|
||||
public bool CodexUnlocked()
|
||||
{
|
||||
return _codex is { CodexUnlocked: true };
|
||||
}
|
||||
|
||||
public void UnlockCodexEntry(string uniqueIdentifier)
|
||||
{
|
||||
var entry = _codexRegistry.FindEntryByID(uniqueIdentifier);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerManager] Could not unlock codex entry '{uniqueIdentifier}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
UnlockCodexEntry(entry);
|
||||
}
|
||||
@@ -243,7 +253,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
}
|
||||
}
|
||||
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.Codex);
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.CodexKeyboard);
|
||||
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new CodexChangedEvent(entry.EntryType));
|
||||
@@ -382,7 +392,7 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
private void LoadCodexFromSave(SaveGame save)
|
||||
{
|
||||
_codex = new Codex();
|
||||
_codex = new Codex(save.CodexUnlocked);
|
||||
|
||||
if (save.DiscoveredCodexEntries != null)
|
||||
{
|
||||
@@ -458,4 +468,4 @@ namespace BriarQueen.Framework.Managers.Player
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IUIBackHandler
|
||||
{
|
||||
bool HandleBackRequest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18b01f6ab9d3468ba9a99360acbe4e5c
|
||||
timeCreated: 1778300000
|
||||
@@ -57,7 +57,18 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
|
||||
private bool _useVirtualCursor;
|
||||
|
||||
public CursorStyleEntry CurrentStyleEntry => _styleMap[_currentStyle];
|
||||
public CursorStyleEntry CurrentStyleEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetStyleEntry(GetEffectiveStyle(), out var entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator)
|
||||
@@ -192,6 +203,22 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
return _isStyleOverridden ? _currentStyleOverride : _currentStyle;
|
||||
}
|
||||
|
||||
private bool TryGetStyleEntry(CursorStyle style, out CursorStyleEntry entry)
|
||||
{
|
||||
if (_styleMap.TryGetValue(style, out entry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_styleMap.TryGetValue(CursorStyle.Default, out entry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyVirtualCursorStyle(CursorStyle style)
|
||||
{
|
||||
if (_virtualCursorImage == null)
|
||||
@@ -282,4 +309,4 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
public Vector2 TooltipOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Extensions;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
@@ -28,6 +29,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private readonly InteractManager _interactManager;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly PlayerManager _playerManager;
|
||||
|
||||
private readonly Dictionary<WindowType, IUIWindow> _windows = new();
|
||||
private readonly Stack<IUIWindow> _windowStack = new();
|
||||
@@ -46,12 +48,14 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
EventCoordinator eventCoordinator,
|
||||
InteractManager interactManager,
|
||||
SettingsService settingsService,
|
||||
SaveManager saveManager)
|
||||
SaveManager saveManager,
|
||||
PlayerManager playerManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_interactManager = interactManager;
|
||||
_settingsService = settingsService;
|
||||
_saveManager = saveManager;
|
||||
_playerManager = playerManager;
|
||||
}
|
||||
|
||||
private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null;
|
||||
@@ -84,6 +88,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
_eventCoordinator.Subscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Subscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
_eventCoordinator.Subscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Subscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Subscribe<FadeEvent>(OnFadeEvent);
|
||||
@@ -97,6 +102,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
private void UnsubscribeFromEvents()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Unsubscribe<UIBackRequestedEvent>(OnBackRequested);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Unsubscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Unsubscribe<FadeEvent>(OnFadeEvent);
|
||||
@@ -175,13 +181,18 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
if (_windowStack.Count > 0)
|
||||
{
|
||||
CloseTopWindow();
|
||||
TryHandleBackRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenWindow(WindowType.PauseMenuWindow);
|
||||
}
|
||||
|
||||
private void OnBackRequested(UIBackRequestedEvent _)
|
||||
{
|
||||
TryHandleBackRequest();
|
||||
}
|
||||
|
||||
private void ToggleSettingsWindow(UIToggleSettingsWindow eventData)
|
||||
{
|
||||
if (eventData.Show)
|
||||
@@ -192,6 +203,9 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
|
||||
private void ToggleCodexWindow(ToggleCodexEvent eventData)
|
||||
{
|
||||
if(!_playerManager.CodexUnlocked())
|
||||
return;
|
||||
|
||||
if (eventData.Shown)
|
||||
OpenWindow(WindowType.CodexWindow);
|
||||
else
|
||||
@@ -231,7 +245,7 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
return codexType switch
|
||||
{
|
||||
CodexType.BookEntry => "You've acquired a new document.",
|
||||
CodexType.DocumentEntry => "You've acquired a new document.",
|
||||
CodexType.PuzzleClue => "You've acquired a new puzzle clue.",
|
||||
CodexType.Photo => "You've acquired a new photo.",
|
||||
_ => string.Empty
|
||||
@@ -246,10 +260,14 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
if (!_settingsService.AreTutorialsEnabled())
|
||||
return;
|
||||
|
||||
var duration = 3f;
|
||||
var tutorialText = TutorialPopupTexts.AllPopups[eventData.TutorialID];
|
||||
if (string.IsNullOrWhiteSpace(eventData.ResolvedText))
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] Empty resolved text for tutorial '{eventData.TutorialID}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_tutorialPopup.Play(tutorialText, duration).Forget();
|
||||
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
||||
_tutorialPopup.Play(eventData.ResolvedText, duration).Forget();
|
||||
}
|
||||
|
||||
private void OnDisplayInteractText(DisplayInteractEvent eventData)
|
||||
@@ -340,6 +358,21 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
CloseTopWindowInternal().Forget();
|
||||
}
|
||||
|
||||
private void TryHandleBackRequest()
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveWindow is IUIBackHandler backHandler && backHandler.HandleBackRequest())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseTopWindow();
|
||||
}
|
||||
|
||||
private async UniTask CloseTopWindowInternal()
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
@@ -431,4 +464,4 @@ namespace BriarQueen.Framework.Managers.UI
|
||||
_eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user