First commit for private source control. Older commits available on Github.
This commit is contained in:
BIN
Assets/Scripts/Framework/Managers/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/Achievements.meta
Normal file
3
Assets/Scripts/Framework/Managers/Achievements.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bcf66bc161f4b768faf5d95e4d786f3
|
||||
timeCreated: 1773830070
|
||||
BIN
Assets/Scripts/Framework/Managers/Achievements/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Achievements/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/Achievements/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Achievements/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4956f4efd45455b907a6183ed3d06f1
|
||||
timeCreated: 1773830079
|
||||
@@ -0,0 +1,18 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Achievements.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "New Achievement", menuName = "Briar Queen/Achievements/New Achievement")]
|
||||
public class AchievementSo : ScriptableObject
|
||||
{
|
||||
[Header("Achievement Details")]
|
||||
public AchievementID Achievement;
|
||||
|
||||
public string Name;
|
||||
public string Description;
|
||||
|
||||
[Header("Steam Integration")]
|
||||
public string SteamUniqueIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef4d39885de444d9840717db9173c5b5
|
||||
timeCreated: 1772721525
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f220a47c48b444abc68a41e0aa5cf9f
|
||||
timeCreated: 1772728857
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Progression;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Achievements.Steam
|
||||
{
|
||||
public class SteamManager : IDisposable, IManager
|
||||
{
|
||||
private const uint STEAM_APP_ID = 0;
|
||||
private readonly AchievementRegistry _achievementRegistry;
|
||||
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
[Inject]
|
||||
public SteamManager(EventCoordinator eventCoordinator, AchievementRegistry achievementRegistry)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_achievementRegistry = achievementRegistry;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<UnlockAchievementEvent>(OnUnlockAchievement);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
SteamClient.Init(STEAM_APP_ID);
|
||||
Initialized = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log($"[Steam Manager] Steam Failed to Init {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
_eventCoordinator.Subscribe<UnlockAchievementEvent>(OnUnlockAchievement);
|
||||
}
|
||||
|
||||
private void OnUnlockAchievement(UnlockAchievementEvent evt)
|
||||
{
|
||||
if (!_achievementRegistry.TryGetAchievement(evt.AchievementID, out var achievement))
|
||||
return;
|
||||
|
||||
var steamAch =
|
||||
SteamUserStats.Achievements.FirstOrDefault(x => x.Identifier == achievement.SteamUniqueIdentifier);
|
||||
|
||||
if (string.IsNullOrEmpty(steamAch.Identifier))
|
||||
{
|
||||
Debug.LogWarning($"Steam achievement '{evt.AchievementID}' not found in Steamworks.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (steamAch.State) return;
|
||||
|
||||
steamAch.Trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91a1d462420f49cb8cb6428d41848983
|
||||
timeCreated: 1772728857
|
||||
3
Assets/Scripts/Framework/Managers/Audio.meta
Normal file
3
Assets/Scripts/Framework/Managers/Audio.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90e810a6ab61441b947dc8c2add38f83
|
||||
timeCreated: 1769802028
|
||||
BIN
Assets/Scripts/Framework/Managers/Audio/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Audio/.DS_Store
vendored
Normal file
Binary file not shown.
779
Assets/Scripts/Framework/Managers/Audio/AudioManager.cs
Normal file
779
Assets/Scripts/Framework/Managers/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,779 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Audio;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Audio.Data;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using VContainer;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// AudioManager (modifier-based mixer control)
|
||||
/// Key idea:
|
||||
/// - 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
|
||||
/// </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 float DEFAULT_VOICE_DUCK_TARGET_DB = -20f;
|
||||
|
||||
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 AudioSource _musicSourceA;
|
||||
private AudioSource _musicSourceB;
|
||||
private AudioSource _voiceSource;
|
||||
private AudioSource _uiSource;
|
||||
|
||||
private string _activeVoiceSubtitleId;
|
||||
private AudioFileSo _currentMusicTrack;
|
||||
|
||||
private CancellationTokenSource _musicDuckCts;
|
||||
private CancellationTokenSource _musicFadeCts;
|
||||
private CancellationTokenSource _voiceCts;
|
||||
|
||||
private float _musicDuckDbCurrent;
|
||||
private float _pauseDuckDbCurrent;
|
||||
|
||||
private Sequence _musicDuckSequence;
|
||||
private Sequence _pauseDuckSequence;
|
||||
|
||||
private bool _voiceFinishedPublished;
|
||||
private bool _disposed;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
[Inject]
|
||||
public AudioManager(AudioMixer mainMixer, AudioRegistry audioRegistry, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_audioMixer = mainMixer;
|
||||
_audioRegistry = audioRegistry;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(AudioManager)}] Initialize called after Dispose.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Initialized)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(AudioManager)}] Initialize called more than once.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[{nameof(AudioManager)}] Initializing...");
|
||||
|
||||
CreateSources();
|
||||
PrimeMixerBaseValues();
|
||||
|
||||
_pauseDuckDbCurrent = 0f;
|
||||
_musicDuckDbCurrent = 0f;
|
||||
|
||||
ApplyAllEffectiveVolumes();
|
||||
|
||||
_eventCoordinator.Subscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
|
||||
Initialized = true;
|
||||
Debug.Log($"[{nameof(AudioManager)}] Initialized.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (Initialized)
|
||||
_eventCoordinator.Unsubscribe<UIStackChangedEvent>(OnUIStackChanged);
|
||||
|
||||
StopAndDispose(ref _musicDuckCts);
|
||||
StopAndDispose(ref _musicFadeCts);
|
||||
StopAndDispose(ref _voiceCts);
|
||||
|
||||
if (_pauseDuckSequence.isAlive)
|
||||
{
|
||||
_pauseDuckSequence.Stop();
|
||||
_pauseDuckSequence = default;
|
||||
}
|
||||
|
||||
if (_musicDuckSequence.isAlive)
|
||||
{
|
||||
_musicDuckSequence.Stop();
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
foreach (var go in _createdAudioObjects)
|
||||
{
|
||||
if (go != null)
|
||||
Object.Destroy(go);
|
||||
}
|
||||
|
||||
_createdAudioObjects.Clear();
|
||||
_ambienceSources.Clear();
|
||||
_sfxChannels.Clear();
|
||||
_currentAmbienceTracks.Clear();
|
||||
_baseDb.Clear();
|
||||
|
||||
_musicSourceA = null;
|
||||
_musicSourceB = null;
|
||||
_voiceSource = null;
|
||||
_uiSource = null;
|
||||
_currentMusicTrack = null;
|
||||
_activeVoiceSubtitleId = null;
|
||||
_voiceFinishedPublished = false;
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(AudioManager)}] SetVolume called before Initialize.");
|
||||
return;
|
||||
}
|
||||
|
||||
var linear = Mathf.Clamp01(value01);
|
||||
var db = Linear01ToDb(linear);
|
||||
|
||||
_baseDb[parameter] = db;
|
||||
ApplyEffectiveVolume(parameter);
|
||||
}
|
||||
|
||||
private static float Linear01ToDb(float linear01)
|
||||
{
|
||||
var lin = Mathf.Max(linear01, 0.0001f);
|
||||
return Mathf.Log10(lin) * 20f;
|
||||
}
|
||||
|
||||
private void PrimeBaseFromMixer(string parameter)
|
||||
{
|
||||
if (_audioMixer != null && _audioMixer.GetFloat(parameter, out var db))
|
||||
_baseDb[parameter] = db;
|
||||
else
|
||||
_baseDb[parameter] = 0f;
|
||||
}
|
||||
|
||||
private void ApplyAllEffectiveVolumes()
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (_audioMixer == null || string.IsNullOrWhiteSpace(parameter))
|
||||
return;
|
||||
|
||||
if (!_baseDb.TryGetValue(parameter, out var baseDb))
|
||||
baseDb = 0f;
|
||||
|
||||
var effective = baseDb;
|
||||
|
||||
if (parameter == AudioMixerParameters.MUSIC_VOLUME ||
|
||||
parameter == AudioMixerParameters.SFX_VOLUME ||
|
||||
parameter == AudioMixerParameters.AMBIENCE_VOLUME)
|
||||
{
|
||||
effective += _pauseDuckDbCurrent;
|
||||
}
|
||||
|
||||
if (parameter == AudioMixerParameters.MUSIC_VOLUME)
|
||||
effective += _musicDuckDbCurrent;
|
||||
|
||||
_audioMixer.SetFloat(parameter, effective);
|
||||
}
|
||||
|
||||
private void OnUIStackChanged(UIStackChangedEvent e)
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
if (e.AnyUIOpen)
|
||||
OnGamePausedInternal().Forget();
|
||||
else
|
||||
OnGameUnpausedInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask OnGamePausedInternal()
|
||||
{
|
||||
PauseVoiceSource(true);
|
||||
await TweenPauseDuckTo(PAUSE_DUCK_TARGET_DB, PAUSE_DUCK_FADE_SECONDS);
|
||||
}
|
||||
|
||||
private async UniTask OnGameUnpausedInternal()
|
||||
{
|
||||
await TweenPauseDuckTo(0f, PAUSE_DUCK_FADE_SECONDS);
|
||||
PauseVoiceSource(false);
|
||||
}
|
||||
|
||||
private async UniTask TweenPauseDuckTo(float targetDb, float seconds)
|
||||
{
|
||||
if (_pauseDuckSequence.isAlive)
|
||||
{
|
||||
_pauseDuckSequence.Stop();
|
||||
_pauseDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _pauseDuckDbCurrent;
|
||||
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
v =>
|
||||
{
|
||||
_pauseDuckDbCurrent = v;
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
||||
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
||||
},
|
||||
Ease.OutCubic,
|
||||
useUnscaledTime: true));
|
||||
|
||||
await _pauseDuckSequence.ToUniTask();
|
||||
_pauseDuckSequence = default;
|
||||
}
|
||||
|
||||
public void PauseVoiceSource(bool paused)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null)
|
||||
return;
|
||||
|
||||
if (paused)
|
||||
_voiceSource.Pause();
|
||||
else
|
||||
_voiceSource.UnPause();
|
||||
}
|
||||
|
||||
public void Play(string audioName)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(AudioManager)}] Play called before Initialize.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_audioRegistry.TryGetAudio(audioName, out var audioFile))
|
||||
{
|
||||
Debug.LogWarning($"[AudioManager] Audio '{audioName}' not found in registry!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFile is AudioFileSo audio)
|
||||
Play(audio);
|
||||
else
|
||||
Debug.LogWarning($"[AudioManager] Audio '{audioName}' is not a valid AudioSO!");
|
||||
}
|
||||
|
||||
public void Play(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || !audioData || !audioData.Clip)
|
||||
return;
|
||||
|
||||
switch (audioData.Type)
|
||||
{
|
||||
case TrackType.Music:
|
||||
CrossfadeMusic(audioData, audioData.FadeTime > 0f ? audioData.FadeTime : 1.0f).Forget();
|
||||
break;
|
||||
|
||||
case TrackType.Ambience:
|
||||
if (!_currentAmbienceTracks.Contains(audioData))
|
||||
{
|
||||
_currentAmbienceTracks.Add(audioData);
|
||||
PlayOnAvailableAmbienceSource(audioData);
|
||||
}
|
||||
break;
|
||||
|
||||
case TrackType.Sfx:
|
||||
PlaySfx(audioData);
|
||||
break;
|
||||
|
||||
case TrackType.UIFX:
|
||||
PlayOneShotAsync(_uiSource, audioData).Forget();
|
||||
break;
|
||||
|
||||
case TrackType.Voice:
|
||||
PlayVoiceLine(audioData).Forget();
|
||||
break;
|
||||
}
|
||||
|
||||
if (audioData.DuckMusic)
|
||||
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid PlayVoiceLine(AudioFileSo audioData)
|
||||
{
|
||||
if (!Initialized || _voiceSource == null || audioData?.Clip == null)
|
||||
return;
|
||||
|
||||
StopAndDispose(ref _voiceCts);
|
||||
_voiceCts = new CancellationTokenSource();
|
||||
var token = _voiceCts.Token;
|
||||
|
||||
_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.priority = audioData.Priority;
|
||||
_voiceSource.Play();
|
||||
|
||||
try
|
||||
{
|
||||
await UniTask.WaitUntil(() => !_voiceSource.isPlaying, cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
PublishVoiceFinishedIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishVoiceFinishedIfNeeded()
|
||||
{
|
||||
if (_voiceFinishedPublished)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
|
||||
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
|
||||
|
||||
_voiceFinishedPublished = true;
|
||||
_activeVoiceSubtitleId = null;
|
||||
}
|
||||
|
||||
private async UniTask DuckMusicAsync(float clipLengthSeconds, float fadeTimeSeconds)
|
||||
{
|
||||
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);
|
||||
|
||||
var hold = clipLengthSeconds - fadeTimeSeconds * 2f;
|
||||
if (hold > 0.01f)
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(hold), cancellationToken: token);
|
||||
|
||||
await TweenMusicDuckTo(0f, fadeTimeSeconds, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await TweenMusicDuckTo(0f, 0.35f, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask TweenMusicDuckTo(float targetDb, float seconds, CancellationToken token)
|
||||
{
|
||||
if (_musicDuckSequence.isAlive)
|
||||
{
|
||||
_musicDuckSequence.Stop();
|
||||
_musicDuckSequence = default;
|
||||
}
|
||||
|
||||
seconds = Mathf.Max(0f, seconds);
|
||||
|
||||
var from = _musicDuckDbCurrent;
|
||||
|
||||
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(
|
||||
from,
|
||||
targetDb,
|
||||
seconds,
|
||||
v =>
|
||||
{
|
||||
_musicDuckDbCurrent = v;
|
||||
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
||||
},
|
||||
Ease.OutCubic,
|
||||
useUnscaledTime: true));
|
||||
|
||||
await _musicDuckSequence.ToUniTask(cancellationToken: token);
|
||||
_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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
StopMusic();
|
||||
StopVoice();
|
||||
StopAllSfx();
|
||||
StopAllAmbience();
|
||||
|
||||
if (_uiSource != null)
|
||||
_uiSource.Stop();
|
||||
}
|
||||
|
||||
private static void StopAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
|
||||
private AudioSource CreateAudioSource(string name, string groupName)
|
||||
{
|
||||
var obj = new GameObject(name);
|
||||
Object.DontDestroyOnLoad(obj);
|
||||
_createdAudioObjects.Add(obj);
|
||||
|
||||
var src = obj.AddComponent<AudioSource>();
|
||||
var group = _audioMixer.FindMatchingGroups(groupName);
|
||||
|
||||
if (group != null && group.Length > 0)
|
||||
src.outputAudioMixerGroup = group[0];
|
||||
else
|
||||
Debug.LogWarning($"[AudioManager] Mixer Group '{groupName}' not found for {name}");
|
||||
|
||||
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;
|
||||
|
||||
source.clip = audioData.Clip;
|
||||
source.loop = audioData.Loopable;
|
||||
source.volume = audioData.Volume;
|
||||
source.pitch = audioData.Pitch;
|
||||
source.priority = audioData.Priority;
|
||||
source.Play();
|
||||
}
|
||||
|
||||
private struct SfxChannel
|
||||
{
|
||||
public AudioSource Source;
|
||||
public float StartedAtUnscaled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a9a5c68541441fead61270b45705435
|
||||
timeCreated: 1769802028
|
||||
3
Assets/Scripts/Framework/Managers/Audio/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Audio/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2419f0c722d945eb8899b181c7f37012
|
||||
timeCreated: 1773830009
|
||||
129
Assets/Scripts/Framework/Managers/Audio/Data/AudioFileSO.cs
Normal file
129
Assets/Scripts/Framework/Managers/Audio/Data/AudioFileSO.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Audio.Data
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Briar Queen/Audio/New Audio File", fileName = "New Audio File")]
|
||||
public class AudioFileSo : ScriptableObject
|
||||
{
|
||||
[Header("Audio Type")]
|
||||
[SerializeField]
|
||||
private TrackType _type;
|
||||
|
||||
[Header("Audio ID")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsMusic))]
|
||||
private MusicKey _musicKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsSfx))]
|
||||
private SFXKey _sfxKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsUiFx))]
|
||||
private UIFXKey _uiFxKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsAmbience))]
|
||||
private AmbienceKey _ambienceKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsVoice))]
|
||||
private VoiceKey _voiceKey;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("A globally unique identifier used for linking with subtitles or dialogue events.")]
|
||||
private SubtitleKey _matchingSubtitleID;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("A readable display name (optional, for debugging or editor UI).")]
|
||||
private string _displayName;
|
||||
|
||||
[Header("Audio Settings")]
|
||||
[SerializeField]
|
||||
private AudioClip _clip;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _volume = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0.5f, 2f)]
|
||||
private float _pitch = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0, 256)]
|
||||
private int _priority = 128;
|
||||
|
||||
[SerializeField]
|
||||
private bool _loopable;
|
||||
|
||||
[Header("Mixing & Behavior")]
|
||||
[SerializeField]
|
||||
[Tooltip("If true, music volume will duck during playback.")]
|
||||
private bool _duckMusic;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Seconds to fade music back in after ducking.")]
|
||||
[Min(0.1f)]
|
||||
private float _fadeTime = 0.5f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Optional narrative event name (for triggers, debugging, or cutscenes).")]
|
||||
private string _narrativeEvent;
|
||||
|
||||
public TrackType Type => _type;
|
||||
|
||||
public bool IsMusic => _type == TrackType.Music;
|
||||
public bool IsSfx => _type == TrackType.Sfx;
|
||||
public bool IsUiFx => _type == TrackType.UIFX;
|
||||
public bool IsAmbience => _type == TrackType.Ambience;
|
||||
public bool IsVoice => _type == TrackType.Voice;
|
||||
|
||||
public MusicKey MusicKey => _musicKey;
|
||||
public SFXKey SfxKey => _sfxKey;
|
||||
public UIFXKey UiFxKey => _uiFxKey;
|
||||
public AmbienceKey AmbienceKey => _ambienceKey;
|
||||
public VoiceKey VoiceKey => _voiceKey;
|
||||
|
||||
public SubtitleKey MatchingSubtitleID => _matchingSubtitleID;
|
||||
public string DisplayName => _displayName;
|
||||
|
||||
public AudioClip Clip => _clip;
|
||||
public float Volume => _volume;
|
||||
public float Pitch => _pitch;
|
||||
public int Priority => _priority;
|
||||
public bool Loopable => _loopable;
|
||||
|
||||
public bool DuckMusic => _duckMusic;
|
||||
public float FadeTime => _fadeTime;
|
||||
public string NarrativeEvent => _narrativeEvent;
|
||||
|
||||
public string UniqueID
|
||||
{
|
||||
get
|
||||
{
|
||||
return _type switch
|
||||
{
|
||||
TrackType.Music when _musicKey != MusicKey.None =>
|
||||
AudioNameIdentifiers.Get(_musicKey),
|
||||
|
||||
TrackType.Sfx when _sfxKey != SFXKey.None =>
|
||||
AudioNameIdentifiers.Get(_sfxKey),
|
||||
|
||||
TrackType.UIFX when _uiFxKey != UIFXKey.None =>
|
||||
AudioNameIdentifiers.Get(_uiFxKey),
|
||||
|
||||
TrackType.Ambience when _ambienceKey != AmbienceKey.None =>
|
||||
AudioNameIdentifiers.Get(_ambienceKey),
|
||||
|
||||
TrackType.Voice when _voiceKey != VoiceKey.None =>
|
||||
AudioNameIdentifiers.Get(_voiceKey),
|
||||
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e10f120272164594b27c0055a5d34c1b
|
||||
timeCreated: 1769802228
|
||||
34
Assets/Scripts/Framework/Managers/DebugManager.cs
Normal file
34
Assets/Scripts/Framework/Managers/DebugManager.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
public class DebugManager : MonoBehaviour
|
||||
{
|
||||
private SaveManager _saveManager;
|
||||
|
||||
[Header("Current Loaded Save")]
|
||||
[SerializeField, ReadOnly]
|
||||
private SaveGame _currentSave;
|
||||
|
||||
[Inject]
|
||||
public void Construct(SaveManager saveManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_saveManager.OnSaveGameLoaded += AttachSaveGame;
|
||||
}
|
||||
|
||||
private void AttachSaveGame(SaveGame save)
|
||||
{
|
||||
_currentSave = save;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/DebugManager.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/DebugManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82bf9e8d9ecf4c329adc84acf1d572e1
|
||||
timeCreated: 1773948069
|
||||
3
Assets/Scripts/Framework/Managers/Hints.meta
Normal file
3
Assets/Scripts/Framework/Managers/Hints.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db9965a5157f46b4aaa3abe51d9d3ddc
|
||||
timeCreated: 1769720397
|
||||
BIN
Assets/Scripts/Framework/Managers/Hints/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Hints/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/Hints/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Hints/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 005cc9eaed8f467aa2902f25d7f3f11c
|
||||
timeCreated: 1773830283
|
||||
9
Assets/Scripts/Framework/Managers/Hints/Data/BaseHint.cs
Normal file
9
Assets/Scripts/Framework/Managers/Hints/Data/BaseHint.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Hints.Data
|
||||
{
|
||||
public abstract class BaseHint
|
||||
{
|
||||
public abstract UniTask Activate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f85579f4af9471b8cbe5ddcff8f5702
|
||||
timeCreated: 1769724499
|
||||
161
Assets/Scripts/Framework/Managers/Hints/HintManager.cs
Normal file
161
Assets/Scripts/Framework/Managers/Hints/HintManager.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
// ==============================
|
||||
// HintManager.cs (fallback to nearest lower stage)
|
||||
// ==============================
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Progression;
|
||||
using BriarQueen.Framework.Managers.Hints.Data;
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Hints
|
||||
{
|
||||
public class HintManager : IDisposable, IManager
|
||||
{
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
|
||||
private readonly Dictionary<int, BaseHint> _hints = new();
|
||||
private CancellationTokenSource _activeHintCts;
|
||||
|
||||
private BaseLevel _currentLevel;
|
||||
|
||||
private CancellationTokenSource _levelCts;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
[Inject]
|
||||
public HintManager(EventCoordinator eventCoordinator)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<int, BaseHint> Hints => _hints;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<LevelChangedEvent>(OnLevelChanged);
|
||||
_eventCoordinator.Unsubscribe<RequestHintEvent>(OnHintRequested);
|
||||
|
||||
CancelAndDispose(ref _activeHintCts);
|
||||
CancelAndDispose(ref _levelCts);
|
||||
|
||||
_currentLevel = null;
|
||||
_hints.Clear();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_eventCoordinator.Subscribe<LevelChangedEvent>(OnLevelChanged);
|
||||
_eventCoordinator.Subscribe<RequestHintEvent>(OnHintRequested);
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
private void OnLevelChanged(LevelChangedEvent evt)
|
||||
{
|
||||
CancelAndDispose(ref _activeHintCts);
|
||||
CancelAndDispose(ref _levelCts);
|
||||
_levelCts = new CancellationTokenSource();
|
||||
|
||||
_hints.Clear();
|
||||
_currentLevel = evt?.Level;
|
||||
|
||||
if (_currentLevel?.Hints == null) return;
|
||||
|
||||
foreach (var kvp in _currentLevel.Hints) _hints[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
private void OnHintRequested(RequestHintEvent evt)
|
||||
{
|
||||
RequestHint().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the hint for the current hint stage.
|
||||
/// If no hint exists at that stage, it falls back to the nearest lower stage that has a hint.
|
||||
/// </summary>
|
||||
public async UniTask RequestHint()
|
||||
{
|
||||
if (_currentLevel == null || _hints.Count == 0) return;
|
||||
|
||||
var stage = Mathf.Max(0, _currentLevel.CurrentLevelHintStage);
|
||||
|
||||
if (!TryGetHintForStageOrFallback(stage, out var resolvedStage, out var hint) || hint == null)
|
||||
// Optional: later show a “No hints available” toast
|
||||
return;
|
||||
|
||||
// Cancel any in-progress hint playback (spam-proof).
|
||||
CancelAndDispose(ref _activeHintCts);
|
||||
_activeHintCts = new CancellationTokenSource();
|
||||
|
||||
var levelToken = _levelCts?.Token ?? CancellationToken.None;
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(levelToken, _activeHintCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await hint.Activate().AttachExternalCancellation(linked.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Fine: new request or level changed.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[HintManager] Hint activation threw at requested stage {stage} (resolved {resolvedStage}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetHintForStageOrFallback(int requestedStage, out int resolvedStage, out BaseHint hint)
|
||||
{
|
||||
// Exact stage
|
||||
if (_hints.TryGetValue(requestedStage, out hint) && hint != null)
|
||||
{
|
||||
resolvedStage = requestedStage;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ Fallback: nearest lower stage that exists
|
||||
// (Designers can skip stages; hint still works.)
|
||||
var best = -1;
|
||||
|
||||
// If you prefer performance, you can cache/sort keys once per level.
|
||||
foreach (var k in _hints.Keys)
|
||||
if (k <= requestedStage && k > best)
|
||||
best = k;
|
||||
|
||||
if (best >= 0 && _hints.TryGetValue(best, out hint) && hint != null)
|
||||
{
|
||||
resolvedStage = best;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedStage = -1;
|
||||
hint = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void CancelAndDispose(ref CancellationTokenSource cts)
|
||||
{
|
||||
if (cts == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7556af28e404ff29c4c88500afceadb
|
||||
timeCreated: 1769720397
|
||||
8
Assets/Scripts/Framework/Managers/IManager.cs
Normal file
8
Assets/Scripts/Framework/Managers/IManager.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace BriarQueen.Framework.Managers
|
||||
{
|
||||
public interface IManager
|
||||
{
|
||||
bool Initialized { get; }
|
||||
void Initialize();
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/IManager.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/IManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8516f899cfe140c78c9eacfee4cb63b2
|
||||
timeCreated: 1773842107
|
||||
3
Assets/Scripts/Framework/Managers/IO.meta
Normal file
3
Assets/Scripts/Framework/Managers/IO.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aaea558f2146422cbbe33cf3836c9ef0
|
||||
timeCreated: 1769701587
|
||||
BIN
Assets/Scripts/Framework/Managers/IO/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/IO/.DS_Store
vendored
Normal file
Binary file not shown.
422
Assets/Scripts/Framework/Managers/IO/SaveManager.cs
Normal file
422
Assets/Scripts/Framework/Managers/IO/SaveManager.cs
Normal file
@@ -0,0 +1,422 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Extensions;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using MemoryPack;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using SaveGame = BriarQueen.Data.IO.Saves.SaveGame;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.IO
|
||||
{
|
||||
public class SaveManager : IDisposable, IManager
|
||||
{
|
||||
private const int MAX_RETRY_COUNT = 3;
|
||||
private const int RETRY_DELAY_MS = 100;
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly object _saveLock = new();
|
||||
|
||||
private CancellationTokenSource _currentSaveCts;
|
||||
private DateTime _lastSaveTime;
|
||||
|
||||
[Inject]
|
||||
public SaveManager(EventCoordinator eventCoordinator)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public bool IsGameLoaded { get; private set; }
|
||||
public SaveGame CurrentSave { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<RequestGameSaveEvent>(OnRequestedSave);
|
||||
_currentSaveCts?.Cancel();
|
||||
}
|
||||
|
||||
public event Action<SaveGame> OnSaveGameLoaded;
|
||||
public event Action OnSaveGameSaved;
|
||||
|
||||
// Existing synchronous hook
|
||||
public event Action<SaveGame> OnSaveRequested;
|
||||
|
||||
// New async hook that runs BEFORE cloning/serialization
|
||||
public event Func<UniTask> OnBeforeSaveRequestedAsync;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_eventCoordinator.Subscribe<RequestGameSaveEvent>(OnRequestedSave);
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
private void OnRequestedSave(RequestGameSaveEvent evt)
|
||||
{
|
||||
SaveGameDataLatest().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the latest save and cancels any in-progress save.
|
||||
/// </summary>
|
||||
public async UniTask SaveGameDataLatest()
|
||||
{
|
||||
CancellationTokenSource oldCts = null;
|
||||
CancellationTokenSource myCts;
|
||||
|
||||
lock (_saveLock)
|
||||
{
|
||||
if (_currentSaveCts != null)
|
||||
{
|
||||
oldCts = _currentSaveCts;
|
||||
_currentSaveCts.Cancel();
|
||||
}
|
||||
|
||||
myCts = new CancellationTokenSource();
|
||||
_currentSaveCts = myCts;
|
||||
}
|
||||
|
||||
oldCts?.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
await SaveGameDataInternal(myCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// A newer save was requested; ignore.
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_saveLock)
|
||||
{
|
||||
if (ReferenceEquals(_currentSaveCts, myCts))
|
||||
{
|
||||
_currentSaveCts.Dispose();
|
||||
_currentSaveCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
myCts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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" };
|
||||
|
||||
// NEW: let systems write into CurrentSave before the clone is made
|
||||
await InvokeBeforeSaveRequestedAsync();
|
||||
|
||||
var saveClone = CurrentSave.DeepClone();
|
||||
OnSaveRequested?.Invoke(saveClone);
|
||||
Debug.Log($"[SaveManager] After OnSaveRequested, clone inventory count = {saveClone.InventoryData?.Count ?? 0}");
|
||||
|
||||
byte[] binaryData = null;
|
||||
await UniTask.SwitchToThreadPool();
|
||||
try
|
||||
{
|
||||
binaryData = MemoryPackSerializer.Serialize(saveClone);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await UniTask.SwitchToMainThread(ct);
|
||||
}
|
||||
|
||||
if (binaryData == null || binaryData.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[SaveManager] Empty serialized data, skipping save.");
|
||||
return;
|
||||
}
|
||||
|
||||
var saveDir = FilePaths.SaveDataFolder;
|
||||
var backupDir = FilePaths.SaveBackupDataFolder;
|
||||
Directory.CreateDirectory(saveDir);
|
||||
Directory.CreateDirectory(backupDir);
|
||||
|
||||
var saveFileName = CurrentSave.SaveFileName + ".sav";
|
||||
var saveFilePath = Path.Combine(saveDir, saveFileName);
|
||||
var backupFilePath = Path.Combine(backupDir, saveFileName);
|
||||
var tempSavePath = saveFilePath + ".tmp";
|
||||
|
||||
var attempt = 0;
|
||||
var success = false;
|
||||
while (attempt < MAX_RETRY_COUNT && !success)
|
||||
{
|
||||
attempt++;
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempSavePath, binaryData, ct);
|
||||
|
||||
if (File.Exists(saveFilePath))
|
||||
File.Replace(tempSavePath, saveFilePath, backupFilePath);
|
||||
else
|
||||
File.Move(tempSavePath, saveFilePath);
|
||||
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Save attempt {attempt} failed: {ex}");
|
||||
await UniTask.Delay(RETRY_DELAY_MS, cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Debug.LogError("[SaveManager] All save attempts failed, save aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentSave = saveClone;
|
||||
IsGameLoaded = true;
|
||||
_lastSaveTime = DateTime.UtcNow;
|
||||
|
||||
OnSaveGameSaved?.Invoke();
|
||||
Debug.Log($"[SaveManager] Save complete: {CurrentSave.SaveFileName}");
|
||||
}
|
||||
|
||||
private async UniTask InvokeBeforeSaveRequestedAsync()
|
||||
{
|
||||
var handlers = OnBeforeSaveRequestedAsync;
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
foreach (Func<UniTask> handler in handlers.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] OnBeforeSaveRequestedAsync handler failed: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask CreateNewSaveGame(string saveFileName)
|
||||
{
|
||||
CurrentSave = new SaveGame
|
||||
{
|
||||
SaveFileName = saveFileName,
|
||||
SaveVersion = "0.0.1a",
|
||||
OpeningCinematicPlayed = false
|
||||
};
|
||||
IsGameLoaded = true;
|
||||
OnSaveGameLoaded?.Invoke(CurrentSave);
|
||||
await SaveGameDataLatest();
|
||||
}
|
||||
|
||||
public List<(string FileName, DateTime LastModified)> GetAvailableSaves()
|
||||
{
|
||||
var saveDir = FilePaths.SaveDataFolder;
|
||||
if (!Directory.Exists(saveDir)) return new List<(string, DateTime)>();
|
||||
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(saveDir, "*.sav")
|
||||
.Select(file => (Path.GetFileNameWithoutExtension(file),
|
||||
File.GetLastWriteTimeUtc(file))
|
||||
)
|
||||
.OrderByDescending(x => x.Item2)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to enumerate saves: {e.Message}");
|
||||
return new List<(string, DateTime)>();
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask LoadLatestSave()
|
||||
{
|
||||
var available = GetAvailableSaves();
|
||||
if (available.Count > 0)
|
||||
{
|
||||
await LoadGameData(available[0].FileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[SaveManager] No save files found. Creating new game.");
|
||||
await CreateNewSaveGame("NewGame");
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask LoadGameData(string saveFileName)
|
||||
{
|
||||
var mainPath = Path.Combine(FilePaths.SaveDataFolder, saveFileName + ".sav");
|
||||
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, saveFileName + ".sav");
|
||||
|
||||
var loadedSave = await LoadFromFileAsync(mainPath);
|
||||
if (loadedSave == null)
|
||||
{
|
||||
Debug.LogWarning($"[SaveManager] Main save load failed, trying backup: {saveFileName}");
|
||||
loadedSave = await LoadFromFileAsync(backupPath);
|
||||
|
||||
if (loadedSave != null)
|
||||
{
|
||||
CurrentSave = loadedSave;
|
||||
await SaveGameDataLatest();
|
||||
Debug.Log("[SaveManager] Restored save from backup.");
|
||||
}
|
||||
}
|
||||
|
||||
CurrentSave = loadedSave ?? new SaveGame { SaveFileName = saveFileName, SaveVersion = "0.0.1-Pre-Alpha" };
|
||||
|
||||
Debug.Log($"[SaveManager] Loading save {loadedSave?.SaveFileName}");
|
||||
IsGameLoaded = true;
|
||||
OnSaveGameLoaded?.Invoke(CurrentSave);
|
||||
}
|
||||
|
||||
private async UniTask<SaveGame> LoadFromFileAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(path);
|
||||
if (bytes == null || bytes.Length == 0) return null;
|
||||
|
||||
await UniTask.SwitchToThreadPool();
|
||||
SaveGame result = null;
|
||||
try
|
||||
{
|
||||
result = MemoryPackSerializer.Deserialize<SaveGame>(bytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await UniTask.SwitchToMainThread();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to load or deserialize '{path}': {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Delete(string saveFileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saveFileName))
|
||||
return false;
|
||||
|
||||
var normalizedName = Path.GetFileNameWithoutExtension(saveFileName);
|
||||
|
||||
var mainPath = Path.Combine(FilePaths.SaveDataFolder, normalizedName + ".sav");
|
||||
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, normalizedName + ".sav");
|
||||
|
||||
var deletedAny = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(mainPath))
|
||||
{
|
||||
File.Delete(mainPath);
|
||||
deletedAny = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to delete main save '{mainPath}': {ex}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
File.Delete(backupPath);
|
||||
deletedAny = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Failed to delete backup save '{backupPath}': {ex}");
|
||||
}
|
||||
|
||||
if (CurrentSave != null &&
|
||||
string.Equals(CurrentSave.SaveFileName, normalizedName, StringComparison.Ordinal))
|
||||
{
|
||||
CurrentSave = null;
|
||||
IsGameLoaded = false;
|
||||
}
|
||||
|
||||
return deletedAny;
|
||||
}
|
||||
|
||||
public bool DoesSaveExist(string saveInfoFileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saveInfoFileName))
|
||||
return false;
|
||||
|
||||
var mainPath = Path.Combine(FilePaths.SaveDataFolder, saveInfoFileName + ".sav");
|
||||
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, saveInfoFileName + ".sav");
|
||||
|
||||
try
|
||||
{
|
||||
return File.Exists(mainPath) || File.Exists(backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] Error checking save existence for '{saveInfoFileName}': {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLevelFlag(LevelFlag levelFlag, bool value, bool requestSave = true)
|
||||
{
|
||||
if (CurrentSave?.PersistentVariables?.Game == null)
|
||||
{
|
||||
Debug.LogWarning($"[SaveManager] Could not set level flag '{levelFlag}' because CurrentSave or PersistentVariables.Game is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentSave.PersistentVariables.Game.SetLevelFlag(levelFlag, value);
|
||||
|
||||
if (requestSave)
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
public void SetPuzzleCompleted(PuzzleKey puzzleKey, bool value, bool requestSave = true)
|
||||
{
|
||||
if (CurrentSave?.PersistentVariables?.Game == null)
|
||||
{
|
||||
Debug.LogWarning($"[SaveManager] Could not set level flag '{puzzleKey}' because CurrentSave or PersistentVariables.Game is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentSave.PersistentVariables.Game.SetPuzzleCompleted(puzzleKey, value);
|
||||
|
||||
if (requestSave)
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
public bool GetLevelFlag(LevelFlag levelFlag)
|
||||
{
|
||||
if (CurrentSave?.PersistentVariables?.Game == null)
|
||||
{
|
||||
Debug.LogWarning($"[SaveManager] Could not get level flag '{levelFlag}' because CurrentSave or PersistentVariables.Game is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return CurrentSave.PersistentVariables.Game.GetLevelFlag(levelFlag);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/IO/SaveManager.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/IO/SaveManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f1f9b48ff6442b0ab094380f3d83f0b
|
||||
timeCreated: 1769701587
|
||||
3
Assets/Scripts/Framework/Managers/Input.meta
Normal file
3
Assets/Scripts/Framework/Managers/Input.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4502fb509ddf4731bcc567b0f3b45259
|
||||
timeCreated: 1770055219
|
||||
BIN
Assets/Scripts/Framework/Managers/Input/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Input/.DS_Store
vendored
Normal file
Binary file not shown.
560
Assets/Scripts/Framework/Managers/Input/InputManager.cs
Normal file
560
Assets/Scripts/Framework/Managers/Input/InputManager.cs
Normal file
@@ -0,0 +1,560 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Input;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
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
|
||||
{
|
||||
public enum DeviceInputType
|
||||
{
|
||||
KeyboardAndMouse,
|
||||
XboxController,
|
||||
PlaystationController,
|
||||
SwitchProController
|
||||
}
|
||||
|
||||
public class InputManager : MonoBehaviour, IDisposable, IManager
|
||||
{
|
||||
[Header("Input")]
|
||||
[SerializeField]
|
||||
private PlayerInput _playerInput;
|
||||
|
||||
[Header("Virtual Cursor")]
|
||||
[SerializeField]
|
||||
private float _controllerCursorSpeed = 1200f;
|
||||
|
||||
[SerializeField]
|
||||
private bool _clampCursorToScreen = true;
|
||||
|
||||
private readonly Dictionary<string, InputAction> _cachedByName = new();
|
||||
|
||||
private InputAction _clickAction;
|
||||
private InputAction _codexAction;
|
||||
private bool _codexShown;
|
||||
private bool _toolScreenShown;
|
||||
|
||||
private GameService _gameService;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private InputAction _hideHudAction;
|
||||
private bool _hudHidden;
|
||||
|
||||
private bool _initialized;
|
||||
private bool _isPaused;
|
||||
private InputAction _pauseAction;
|
||||
|
||||
private InputAction _pointAction;
|
||||
private InputAction _rightClickAction;
|
||||
private InputAction _openToolsAction;
|
||||
private InputAction _nextToolAction;
|
||||
private InputAction _previousToolAction;
|
||||
private InputAction _nextItemAction;
|
||||
private InputAction _previousItemAction;
|
||||
private InputAction _virtualMouseAction;
|
||||
private InputAction _submitAction;
|
||||
|
||||
private UICursorService _uiCursorService;
|
||||
|
||||
private Vector2 _controllerCursorInput;
|
||||
|
||||
public Vector2 PointerPosition { get; private set; }
|
||||
|
||||
public DeviceInputType DeviceInputType { get; private set; }
|
||||
|
||||
public string ActiveActionMap => _playerInput != null && _playerInput.currentActionMap != null
|
||||
? _playerInput.currentActionMap.name
|
||||
: string.Empty;
|
||||
|
||||
public bool Initialized => _initialized;
|
||||
|
||||
public bool IsPaused => _isPaused;
|
||||
|
||||
public bool UsingControllerCursor => DeviceInputType != DeviceInputType.KeyboardAndMouse;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
PointerPosition = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_initialized)
|
||||
return;
|
||||
|
||||
UpdateControllerCursor();
|
||||
|
||||
if (_uiCursorService != null && UsingControllerCursor)
|
||||
_uiCursorService.SetVirtualCursorPosition(PointerPosition);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
return;
|
||||
|
||||
if (_eventCoordinator != null)
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(OnCodexStateChanged);
|
||||
_eventCoordinator.Unsubscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
|
||||
}
|
||||
|
||||
UnbindCoreInputs();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(
|
||||
EventCoordinator eventCoordinator,
|
||||
UICursorService uiCursorService,
|
||||
GameService gameService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_uiCursorService = uiCursorService;
|
||||
_gameService = gameService;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Debug.Log("[InputManager] Initialize called");
|
||||
|
||||
if (_initialized)
|
||||
{
|
||||
Debug.Log("[InputManager] Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_eventCoordinator == null)
|
||||
{
|
||||
Debug.LogWarning("[InputManager] EventCoordinator is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerInput == null)
|
||||
{
|
||||
Debug.LogWarning("[InputManager] PlayerInput is null");
|
||||
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;
|
||||
|
||||
Debug.Log("[InputManager] Initialization complete");
|
||||
}
|
||||
|
||||
private void CacheActions()
|
||||
{
|
||||
_cachedByName.Clear();
|
||||
|
||||
_pointAction = CacheAction("Point");
|
||||
_virtualMouseAction = CacheAction("Virtual_Mouse");
|
||||
_pauseAction = CacheAction("Pause");
|
||||
_clickAction = CacheAction("Click");
|
||||
_rightClickAction = CacheAction("Right_Click");
|
||||
_hideHudAction = CacheAction("Hide_HUD");
|
||||
_codexAction = CacheAction("Codex");
|
||||
_openToolsAction = CacheAction("Show_Tools");
|
||||
_nextToolAction = CacheAction("Next_Tool");
|
||||
_previousToolAction = CacheAction("Previous_Tool");
|
||||
_nextItemAction = CacheAction("Next_Item");
|
||||
_previousItemAction = CacheAction("Previous_Item");
|
||||
_submitAction = CacheAction("Submit");
|
||||
}
|
||||
|
||||
private InputAction CacheAction(string actionName)
|
||||
{
|
||||
var action = GetAction(actionName);
|
||||
|
||||
if (action != null && !string.IsNullOrWhiteSpace(action.name))
|
||||
_cachedByName[action.name] = action;
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private void BindCoreInputs()
|
||||
{
|
||||
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)
|
||||
{
|
||||
_virtualMouseAction.performed += OnVirtualMouse;
|
||||
_virtualMouseAction.canceled += OnVirtualMouse;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void UnbindCoreInputs()
|
||||
{
|
||||
if (_pointAction != null)
|
||||
{
|
||||
_pointAction.performed -= OnPoint;
|
||||
_pointAction.canceled -= OnPoint;
|
||||
}
|
||||
|
||||
if (_virtualMouseAction != null)
|
||||
{
|
||||
_virtualMouseAction.performed -= OnVirtualMouse;
|
||||
_virtualMouseAction.canceled -= OnVirtualMouse;
|
||||
}
|
||||
|
||||
if (_pauseAction != null)
|
||||
_pauseAction.performed -= OnPause;
|
||||
|
||||
if (_clickAction != null)
|
||||
_clickAction.performed -= OnClick;
|
||||
|
||||
if (_rightClickAction != null)
|
||||
_rightClickAction.performed -= OnRightClick;
|
||||
|
||||
if (_hideHudAction != null)
|
||||
_hideHudAction.performed -= OnHideHUD;
|
||||
|
||||
if (_codexAction != null)
|
||||
_codexAction.performed -= OnCodex;
|
||||
|
||||
if (_openToolsAction != null)
|
||||
_openToolsAction.performed -= OnOpenTools;
|
||||
|
||||
if (_nextToolAction != null)
|
||||
_nextToolAction.performed -= OnNextToolClicked;
|
||||
|
||||
if (_previousToolAction != null)
|
||||
_previousToolAction.performed -= OnPreviousToolClicked;
|
||||
|
||||
if (_nextItemAction != null)
|
||||
_nextItemAction.performed -= OnNextItemClicked;
|
||||
|
||||
if (_previousItemAction != null)
|
||||
_previousItemAction.performed -= OnPreviousItemClicked;
|
||||
|
||||
if (_playerInput != null)
|
||||
_playerInput.onControlsChanged -= OnControlsChanged;
|
||||
}
|
||||
|
||||
private void UpdateControllerCursor()
|
||||
{
|
||||
if (!UsingControllerCursor)
|
||||
return;
|
||||
|
||||
if (_controllerCursorInput.sqrMagnitude <= 0.0001f)
|
||||
return;
|
||||
|
||||
PointerPosition += _controllerCursorInput * (_controllerCursorSpeed * Time.unscaledDeltaTime);
|
||||
|
||||
if (_clampCursorToScreen)
|
||||
{
|
||||
PointerPosition = new Vector2(
|
||||
Mathf.Clamp(PointerPosition.x, 0f, Screen.width),
|
||||
Mathf.Clamp(PointerPosition.y, 0f, Screen.height)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnControlsChanged(PlayerInput playerInput)
|
||||
{ Debug.Log($"Controls changed. Scheme: {playerInput.currentControlScheme}");
|
||||
DeviceInputType = GetDeviceInputType(playerInput);
|
||||
ApplyCursorModeForCurrentScheme();
|
||||
}
|
||||
|
||||
private void ApplyCursorModeForCurrentScheme()
|
||||
{
|
||||
if (_uiCursorService == null)
|
||||
return;
|
||||
|
||||
var useVirtualCursor = UsingControllerCursor;
|
||||
_uiCursorService.SetUseVirtualCursor(useVirtualCursor);
|
||||
|
||||
if (useVirtualCursor)
|
||||
_uiCursorService.SetVirtualCursorPosition(PointerPosition);
|
||||
}
|
||||
|
||||
public void BindPauseForSkip(Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (_pauseAction == null || callback == null)
|
||||
return;
|
||||
|
||||
_pauseAction.performed -= OnPause;
|
||||
_pauseAction.performed += callback;
|
||||
}
|
||||
|
||||
public void ResetPauseBind(Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (_pauseAction == null || callback == null)
|
||||
return;
|
||||
|
||||
_pauseAction.performed -= callback;
|
||||
_pauseAction.performed -= OnPause;
|
||||
_pauseAction.performed += OnPause;
|
||||
}
|
||||
|
||||
public void BindSubmitForStart(Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (_submitAction == null || callback == null)
|
||||
return;
|
||||
|
||||
_submitAction.performed += callback;
|
||||
}
|
||||
|
||||
public void ResetSubmitBind(Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (_submitAction == null || callback == null)
|
||||
return;
|
||||
|
||||
_submitAction.performed -= callback;
|
||||
}
|
||||
|
||||
public void BindAction(string actionName, Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (callback == null)
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
action.performed += callback;
|
||||
}
|
||||
|
||||
public void UnbindAction(string actionName, Action<InputAction.CallbackContext> callback)
|
||||
{
|
||||
if (callback == null)
|
||||
return;
|
||||
|
||||
var action = GetCachedAction(actionName);
|
||||
if (action == null)
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.performed -= callback;
|
||||
}
|
||||
|
||||
private InputAction GetCachedAction(string actionName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actionName))
|
||||
return null;
|
||||
|
||||
if (_cachedByName.TryGetValue(actionName, out var cached))
|
||||
return cached;
|
||||
|
||||
var action = GetAction(actionName);
|
||||
if (action != null)
|
||||
_cachedByName[actionName] = action;
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private static DeviceInputType GetDeviceInputType(PlayerInput input)
|
||||
{
|
||||
if (input == null)
|
||||
return DeviceInputType.KeyboardAndMouse;
|
||||
|
||||
return input.currentControlScheme switch
|
||||
{
|
||||
"Keyboard&Mouse" => DeviceInputType.KeyboardAndMouse,
|
||||
"Xbox" => DeviceInputType.XboxController,
|
||||
"Playstation" => DeviceInputType.PlaystationController,
|
||||
"Switch Pro" => DeviceInputType.SwitchProController,
|
||||
_ => DeviceInputType.KeyboardAndMouse
|
||||
};
|
||||
}
|
||||
|
||||
private void OnHudStateChanged(UIToggleHudEvent evt)
|
||||
{
|
||||
_hudHidden = !evt.Show;
|
||||
}
|
||||
|
||||
private void OnCodexStateChanged(ToggleCodexEvent evt)
|
||||
{
|
||||
_codexShown = evt.Shown;
|
||||
}
|
||||
|
||||
private void OnToolScreenStateChanged(ToggleToolScreenEvent evt)
|
||||
{
|
||||
_toolScreenShown = evt.Shown;
|
||||
}
|
||||
|
||||
private void OnHideHUD(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_hudHidden = !_hudHidden;
|
||||
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(_hudHidden));
|
||||
}
|
||||
|
||||
private void OnPause(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if(_gameService.IsMainMenuSceneLoaded)
|
||||
return;
|
||||
|
||||
_isPaused = !_isPaused;
|
||||
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
|
||||
}
|
||||
|
||||
private void OnCodex(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if(_gameService.IsMainMenuSceneLoaded)
|
||||
return;
|
||||
_codexShown = !_codexShown;
|
||||
_eventCoordinator?.Publish(new ToggleCodexEvent(_codexShown));
|
||||
}
|
||||
|
||||
private void OnClick(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new OnClickEvent(ctx));
|
||||
}
|
||||
|
||||
private void OnRightClick(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.PublishImmediate(new OnRightClickEvent(ctx));
|
||||
}
|
||||
|
||||
private void OnPoint(InputAction.CallbackContext ctx)
|
||||
{
|
||||
PointerPosition = ctx.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
private void OnVirtualMouse(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_controllerCursorInput = ctx.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
private void OnOpenTools(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if(_gameService.IsMainMenuSceneLoaded)
|
||||
return;
|
||||
_toolScreenShown = !_toolScreenShown;
|
||||
_eventCoordinator?.Publish(new ToggleToolScreenEvent(_toolScreenShown));
|
||||
}
|
||||
|
||||
private void OnNextToolClicked(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.Publish(new OnNextToolChangedEvent());
|
||||
}
|
||||
|
||||
private void OnPreviousToolClicked(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.Publish(new OnPreviousToolChangedEvent());
|
||||
}
|
||||
|
||||
private void OnNextItemClicked(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.Publish(new OnNextItemClickedEvent());
|
||||
}
|
||||
|
||||
private void OnPreviousItemClicked(InputAction.CallbackContext ctx)
|
||||
{
|
||||
_eventCoordinator?.Publish(new OnPreviousItemClickedEvent());
|
||||
}
|
||||
|
||||
public InputAction GetAction(string actionName)
|
||||
{
|
||||
if (_playerInput == null || _playerInput.actions == null || string.IsNullOrWhiteSpace(actionName))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return _playerInput.actions.FindAction(actionName, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd6a30bb8344415299513beb6108b2ad
|
||||
timeCreated: 1770055219
|
||||
3
Assets/Scripts/Framework/Managers/Interaction.meta
Normal file
3
Assets/Scripts/Framework/Managers/Interaction.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccf6b34d51dc4839b574a41a9ef8f834
|
||||
timeCreated: 1769719788
|
||||
BIN
Assets/Scripts/Framework/Managers/Interaction/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Interaction/.DS_Store
vendored
Normal file
Binary file not shown.
8
Assets/Scripts/Framework/Managers/Interaction/Data.meta
Normal file
8
Assets/Scripts/Framework/Managers/Interaction/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9707e08d4c234e9ea90b934d4760382
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Scripts/Framework/Managers/Interaction/Data/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Interaction/Data/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Interaction.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Inventory-safe per-item logic. Serialized INSIDE ItemDataSo via SerializeReference.
|
||||
/// No scene objects required.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class BaseInteraction
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when player uses 'self' on 'other' from the inventory UI.
|
||||
/// Return true if the interaction was handled (success OR "can't" message shown).
|
||||
/// Return false if this interaction doesn't apply, so caller may try other routes/fallback.
|
||||
/// </summary>
|
||||
public abstract UniTask<bool> TryUseWith(
|
||||
ItemDataSo self,
|
||||
ItemDataSo other,
|
||||
PlayerManager playerManager,
|
||||
EventCoordinator eventCoordinator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default "does nothing" interaction.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class NoOpItemInteraction : BaseInteraction
|
||||
{
|
||||
public override UniTask<bool> TryUseWith(
|
||||
ItemDataSo self,
|
||||
ItemDataSo other,
|
||||
PlayerManager playerManager,
|
||||
EventCoordinator eventCoordinator)
|
||||
{
|
||||
return UniTask.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ab49b601a434a30a12dd1e3fe82a282
|
||||
timeCreated: 1771171646
|
||||
@@ -0,0 +1,18 @@
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Interaction.Data
|
||||
{
|
||||
public interface IInteractable
|
||||
{
|
||||
UICursorService.CursorStyle ApplicableCursorStyle { get; }
|
||||
|
||||
string InteractableName { get; }
|
||||
|
||||
UniTask OnInteract(ItemDataSo item = null);
|
||||
|
||||
UniTask EnterHover();
|
||||
UniTask ExitHover();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aaad7fee39064af5aa7bd0096e87065a
|
||||
timeCreated: 1769717587
|
||||
452
Assets/Scripts/Framework/Managers/Interaction/InteractManager.cs
Normal file
452
Assets/Scripts/Framework/Managers/Interaction/InteractManager.cs
Normal file
@@ -0,0 +1,452 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Input;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified interaction service (World + UI):
|
||||
/// - One hover loop
|
||||
/// - One interactable interface
|
||||
/// - UI hover takes priority over world hover
|
||||
/// - Any topmost UI blocks lower UI/world interaction
|
||||
/// - Supports modal/exclusive UI raycasters without disabling canvases
|
||||
/// </summary>
|
||||
public sealed class InteractManager : IDisposable, IManager
|
||||
{
|
||||
private const float HOVER_INTERVAL_SECONDS = 0.05f; // 20Hz
|
||||
private const float POINTER_MOVE_THRESHOLD_SQR = 4f; // pixels^2
|
||||
private const float MAX_RAY_DISTANCE = 250f;
|
||||
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly EventSystem _eventSystem;
|
||||
private readonly InputManager _inputManager;
|
||||
|
||||
// World raycasting
|
||||
private readonly LayerMask _layerMask = ~0;
|
||||
|
||||
private readonly PointerEventData _uiPointerEventData;
|
||||
|
||||
// Scene-bound UI raycasters
|
||||
private readonly List<GraphicRaycaster> _uiRaycasters = new(8);
|
||||
private readonly List<RaycastResult> _uiResults = new(32);
|
||||
private readonly RaycastHit[] _worldHits = new RaycastHit[8];
|
||||
|
||||
private IInteractable _currentHovered;
|
||||
private bool _disposed;
|
||||
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, only this raycaster is allowed to receive UI interaction.
|
||||
/// Rendering is unaffected; this is input-only.
|
||||
/// </summary>
|
||||
private GraphicRaycaster _exclusiveRaycaster;
|
||||
|
||||
private CancellationTokenSource _hoverCts;
|
||||
private Vector2 _lastPointerPos;
|
||||
private ItemDataSo _selectedItem;
|
||||
|
||||
[Inject]
|
||||
public InteractManager(
|
||||
EventCoordinator eventCoordinator,
|
||||
InputManager inputManager,
|
||||
EventSystem eventSystem)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
_eventSystem = eventSystem;
|
||||
|
||||
_uiPointerEventData = _eventSystem != null
|
||||
? new PointerEventData(_eventSystem)
|
||||
: null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
_eventCoordinator.Unsubscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Unsubscribe<OnClickEvent>(OnClickReceived);
|
||||
_eventCoordinator.Unsubscribe<OnRightClickEvent>(OnRightClickReceived);
|
||||
|
||||
_hoverCts?.Cancel();
|
||||
_hoverCts?.Dispose();
|
||||
|
||||
_currentHovered = null;
|
||||
_exclusiveRaycaster = null;
|
||||
|
||||
_eventCoordinator.Publish(new HoverInteractableChangedEvent(null));
|
||||
_eventCoordinator.Publish(
|
||||
new CursorStyleChangeEvent(
|
||||
UICursorService.CursorStyle.Default));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assign the active UI GraphicRaycaster (legacy compatibility).
|
||||
/// This replaces the full registered list.
|
||||
/// </summary>
|
||||
public void SetUIRaycaster(GraphicRaycaster raycaster)
|
||||
{
|
||||
_uiRaycasters.Clear();
|
||||
_uiResults.Clear();
|
||||
|
||||
if (raycaster != null)
|
||||
_uiRaycasters.Add(raycaster);
|
||||
|
||||
if (_exclusiveRaycaster != null && _exclusiveRaycaster != raycaster)
|
||||
_exclusiveRaycaster = null;
|
||||
|
||||
if (_uiRaycasters.Count == 0 && _currentHovered != null)
|
||||
ClearHover().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add another UI GraphicRaycaster.
|
||||
/// Safe to call multiple times; duplicates are ignored.
|
||||
/// </summary>
|
||||
public void AddUIRaycaster(GraphicRaycaster raycaster)
|
||||
{
|
||||
if (raycaster == null)
|
||||
return;
|
||||
|
||||
if (!_uiRaycasters.Contains(raycaster))
|
||||
_uiRaycasters.Add(raycaster);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a UI GraphicRaycaster.
|
||||
/// </summary>
|
||||
public void RemoveUIRaycaster(GraphicRaycaster raycaster)
|
||||
{
|
||||
if (raycaster == null)
|
||||
return;
|
||||
|
||||
_uiRaycasters.Remove(raycaster);
|
||||
_uiResults.Clear();
|
||||
|
||||
if (_exclusiveRaycaster == raycaster)
|
||||
_exclusiveRaycaster = null;
|
||||
|
||||
if (_uiRaycasters.Count == 0 && _currentHovered != null)
|
||||
ClearHover().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restrict UI interaction to a single raycaster.
|
||||
/// Useful for modal windows that should block all lower UI without disabling canvases.
|
||||
/// </summary>
|
||||
public void SetExclusiveRaycaster(GraphicRaycaster raycaster)
|
||||
{
|
||||
_exclusiveRaycaster = raycaster;
|
||||
|
||||
if (_currentHovered != null)
|
||||
ClearHover().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear exclusive mode and return to using all registered raycasters.
|
||||
/// </summary>
|
||||
public void ClearExclusiveRaycaster()
|
||||
{
|
||||
_exclusiveRaycaster = null;
|
||||
|
||||
if (_currentHovered != null)
|
||||
ClearHover().Forget();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Debug.Log("[InteractManager] Initializing...");
|
||||
_eventCoordinator.Subscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Subscribe<OnClickEvent>(OnClickReceived);
|
||||
_eventCoordinator.Subscribe<OnRightClickEvent>(OnRightClickReceived);
|
||||
|
||||
StartHoverLoop();
|
||||
Debug.Log("[InteractManager] Initialized.");
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
private void StartHoverLoop()
|
||||
{
|
||||
_hoverCts?.Cancel();
|
||||
_hoverCts?.Dispose();
|
||||
|
||||
_hoverCts = new CancellationTokenSource();
|
||||
_lastPointerPos = _inputManager.PointerPosition;
|
||||
|
||||
HoverLoop(_hoverCts.Token).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid HoverLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(HOVER_INTERVAL_SECONDS),
|
||||
cancellationToken: token);
|
||||
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
var pointer = _inputManager.PointerPosition;
|
||||
var delta = pointer - _lastPointerPos;
|
||||
|
||||
if (delta.sqrMagnitude < POINTER_MOVE_THRESHOLD_SQR && _currentHovered != null)
|
||||
continue;
|
||||
|
||||
_lastPointerPos = pointer;
|
||||
|
||||
// Topmost UI always wins. If it is not interactable, it still blocks lower UI/world.
|
||||
if (TryRaycastTopUI(pointer, out var uiHit))
|
||||
{
|
||||
if (uiHit != null)
|
||||
await SetHovered(uiHit);
|
||||
else
|
||||
await ClearHover();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// World fallback only if no UI at all was hit.
|
||||
var worldHit = RaycastWorld(pointer);
|
||||
await SetHovered(worldHit);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[InteractManager] Hover loop error: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRaycastTopUI(Vector2 pointer, out IInteractable interactable)
|
||||
{
|
||||
|
||||
interactable = null;
|
||||
|
||||
if (_uiPointerEventData == null)
|
||||
return false;
|
||||
|
||||
_uiPointerEventData.Reset();
|
||||
_uiPointerEventData.position = pointer;
|
||||
|
||||
_uiResults.Clear();
|
||||
|
||||
if (_exclusiveRaycaster != null)
|
||||
{
|
||||
if (!_exclusiveRaycaster.isActiveAndEnabled)
|
||||
return false;
|
||||
|
||||
_exclusiveRaycaster.Raycast(_uiPointerEventData, _uiResults);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiRaycasters.Count == 0)
|
||||
return false;
|
||||
|
||||
for (var r = 0; r < _uiRaycasters.Count; r++)
|
||||
{
|
||||
var raycaster = _uiRaycasters[r];
|
||||
if (raycaster == null || !raycaster.isActiveAndEnabled)
|
||||
continue;
|
||||
|
||||
raycaster.Raycast(_uiPointerEventData, _uiResults);
|
||||
}
|
||||
}
|
||||
|
||||
if (_uiResults.Count == 0)
|
||||
return false;
|
||||
|
||||
_uiResults.Sort((a, b) =>
|
||||
{
|
||||
if (a.sortingLayer != b.sortingLayer)
|
||||
return b.sortingLayer.CompareTo(a.sortingLayer);
|
||||
|
||||
if (a.sortingOrder != b.sortingOrder)
|
||||
return b.sortingOrder.CompareTo(a.sortingOrder);
|
||||
|
||||
if (a.depth != b.depth)
|
||||
return b.depth.CompareTo(a.depth);
|
||||
|
||||
if (!Mathf.Approximately(a.distance, b.distance))
|
||||
return a.distance.CompareTo(b.distance);
|
||||
|
||||
return a.index.CompareTo(b.index);
|
||||
});
|
||||
|
||||
foreach (var rr in _uiResults)
|
||||
{
|
||||
if (rr.gameObject == null)
|
||||
continue;
|
||||
|
||||
var candidate = rr.gameObject.GetComponentInParent<IInteractable>();
|
||||
if (candidate != null)
|
||||
{
|
||||
interactable = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // UI was hit, but nothing interactable was found, so UI still blocks world
|
||||
}
|
||||
|
||||
private static bool IsRaycastResultHigherPriority(RaycastResult candidate, RaycastResult currentBest)
|
||||
{
|
||||
if (candidate.sortingLayer != currentBest.sortingLayer)
|
||||
return candidate.sortingLayer > currentBest.sortingLayer;
|
||||
|
||||
if (candidate.sortingOrder != currentBest.sortingOrder)
|
||||
return candidate.sortingOrder > currentBest.sortingOrder;
|
||||
|
||||
if (candidate.depth != currentBest.depth)
|
||||
return candidate.depth > currentBest.depth;
|
||||
|
||||
if (!Mathf.Approximately(candidate.distance, currentBest.distance))
|
||||
return candidate.distance < currentBest.distance;
|
||||
|
||||
return candidate.index < currentBest.index;
|
||||
}
|
||||
|
||||
private IInteractable RaycastWorld(Vector2 pointer)
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam == null)
|
||||
return null;
|
||||
|
||||
var ray = cam.ScreenPointToRay(pointer);
|
||||
var count = Physics.RaycastNonAlloc(
|
||||
ray,
|
||||
_worldHits,
|
||||
MAX_RAY_DISTANCE,
|
||||
_layerMask,
|
||||
QueryTriggerInteraction.Collide);
|
||||
|
||||
if (count <= 0)
|
||||
return null;
|
||||
|
||||
var bestDist = float.PositiveInfinity;
|
||||
IInteractable best = null;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var hit = _worldHits[i];
|
||||
if (hit.collider == null)
|
||||
continue;
|
||||
|
||||
var candidate = hit.collider.GetComponentInParent<IInteractable>();
|
||||
if (candidate == null)
|
||||
continue;
|
||||
|
||||
if (hit.distance < bestDist)
|
||||
{
|
||||
bestDist = hit.distance;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private async UniTask SetHovered(IInteractable next)
|
||||
{
|
||||
if (ReferenceEquals(_currentHovered, next))
|
||||
return;
|
||||
|
||||
if (_currentHovered != null)
|
||||
try
|
||||
{
|
||||
await _currentHovered.ExitHover();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_currentHovered = next;
|
||||
|
||||
_eventCoordinator.Publish(
|
||||
new HoverInteractableChangedEvent(_currentHovered));
|
||||
|
||||
var cursor =
|
||||
_currentHovered?.ApplicableCursorStyle
|
||||
?? UICursorService.CursorStyle.Default;
|
||||
|
||||
_eventCoordinator.Publish(
|
||||
new CursorStyleChangeEvent(cursor));
|
||||
|
||||
if (_currentHovered != null)
|
||||
try
|
||||
{
|
||||
await _currentHovered.EnterHover();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask ClearHover()
|
||||
{
|
||||
if (_currentHovered == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await _currentHovered.ExitHover();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_currentHovered = null;
|
||||
|
||||
_eventCoordinator.Publish(
|
||||
new HoverInteractableChangedEvent(null));
|
||||
|
||||
_eventCoordinator.Publish(
|
||||
new CursorStyleChangeEvent(
|
||||
UICursorService.CursorStyle.Default));
|
||||
}
|
||||
|
||||
private void OnClickReceived(OnClickEvent evt)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_currentHovered == null)
|
||||
return;
|
||||
|
||||
_currentHovered.OnInteract(_selectedItem).Forget();
|
||||
}
|
||||
|
||||
private void OnRightClickReceived(OnRightClickEvent obj)
|
||||
{
|
||||
_eventCoordinator.Publish(new SelectedItemChangedEvent(null));
|
||||
}
|
||||
|
||||
private void OnSelectedItemChanged(SelectedItemChangedEvent evt)
|
||||
{
|
||||
_selectedItem = evt.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e536e3741ed64dbca4c9523a72211140
|
||||
timeCreated: 1769719788
|
||||
3
Assets/Scripts/Framework/Managers/Levels.meta
Normal file
3
Assets/Scripts/Framework/Managers/Levels.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f2a02cfae2940a4bbc18a73887c4ba5
|
||||
timeCreated: 1769778109
|
||||
BIN
Assets/Scripts/Framework/Managers/Levels/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Levels/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/Levels/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Levels/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe1cec78ab0b4a05bdafb735afcfab5a
|
||||
timeCreated: 1773830332
|
||||
243
Assets/Scripts/Framework/Managers/Levels/Data/BaseItem.cs
Normal file
243
Assets/Scripts/Framework/Managers/Levels/Data/BaseItem.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
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.Audio;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Destruction;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
public class BaseItem : MonoBehaviour, IInteractable
|
||||
{
|
||||
[Header("Item Details")]
|
||||
[SerializeField]
|
||||
private ItemDataSo _itemData;
|
||||
|
||||
[Tooltip("Used for custom tooltip. Defaults to Item Name")]
|
||||
[SerializeField]
|
||||
private string _interactableTooltip = string.Empty;
|
||||
|
||||
[Header("Object Setup")]
|
||||
[SerializeField]
|
||||
protected CanvasGroup _canvasGroup;
|
||||
|
||||
protected AddressableManager AddressableManager;
|
||||
protected AssetRegistry AssetRegistry;
|
||||
protected DestructionService DestructionService;
|
||||
|
||||
protected EventCoordinator EventCoordinator;
|
||||
protected CancellationTokenSource PickupCts;
|
||||
|
||||
protected Sequence PickupSequence;
|
||||
protected PlayerManager PlayerManager;
|
||||
protected SaveManager SaveManager;
|
||||
protected SettingsService SettingsService;
|
||||
|
||||
protected TutorialService TutorialService;
|
||||
protected AudioManager AudioManager;
|
||||
|
||||
public ItemDataSo ItemData => _itemData;
|
||||
|
||||
public CanvasGroup CanvasGroup => _canvasGroup;
|
||||
|
||||
protected string InteractableTooltip => _interactableTooltip;
|
||||
|
||||
public GameObject GameObject => gameObject;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
public virtual UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Pickup;
|
||||
|
||||
public virtual string InteractableName =>
|
||||
!string.IsNullOrWhiteSpace(_interactableTooltip) ? _interactableTooltip : _itemData.ItemName;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the item is interacted with. Defaults to Pickup.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public virtual async UniTask OnInteract(ItemDataSo item = null)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
|
||||
return;
|
||||
}
|
||||
|
||||
CheckCyclingTutorial();
|
||||
|
||||
if (!CheckEmptyHands())
|
||||
return;
|
||||
|
||||
await Pickup();
|
||||
await OnInteracted();
|
||||
}
|
||||
|
||||
public virtual UniTask EnterHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual UniTask ExitHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when item is interacted with.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual UniTask OnInteracted()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual bool CheckEmptyHands()
|
||||
{
|
||||
if (PlayerManager.GetEquippedTool() != ToolID.None)
|
||||
{
|
||||
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.EmptyHands)));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CheckCyclingTutorial()
|
||||
{
|
||||
TutorialService.DisplayTutorial(TutorialPopupID.ItemCycling);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator, SaveManager saveManager, PlayerManager playerManager,
|
||||
DestructionService destructionService, SettingsService settingsService,
|
||||
AddressableManager addressableManager, AssetRegistry assetRegistry, TutorialService tutorialService,
|
||||
AudioManager audioManager)
|
||||
{
|
||||
EventCoordinator = eventCoordinator;
|
||||
SaveManager = saveManager;
|
||||
PlayerManager = playerManager;
|
||||
DestructionService = destructionService;
|
||||
SettingsService = settingsService;
|
||||
AddressableManager = addressableManager;
|
||||
AssetRegistry = assetRegistry;
|
||||
TutorialService = tutorialService;
|
||||
AudioManager = audioManager;
|
||||
}
|
||||
|
||||
protected virtual async UniTask Remove()
|
||||
{
|
||||
// TODO - Play Cut Vines SFX
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (PickupSequence.isAlive)
|
||||
{
|
||||
PickupSequence.Complete();
|
||||
PickupCts?.Cancel();
|
||||
PickupCts?.Dispose();
|
||||
}
|
||||
|
||||
PickupCts = new CancellationTokenSource();
|
||||
|
||||
PickupSequence = Sequence.Create().Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = 0.5f
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await PickupSequence.ToUniTask(cancellationToken: PickupCts.Token);
|
||||
await OnRemoved();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateSaveGameOnRemoval();
|
||||
await DestructionService.Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private 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 = _itemData.UniqueID
|
||||
});
|
||||
|
||||
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
protected virtual UniTask OnRemoved()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual async UniTask Pickup()
|
||||
{
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (PickupSequence.isAlive)
|
||||
{
|
||||
PickupSequence.Complete();
|
||||
PickupCts?.Cancel();
|
||||
PickupCts?.Dispose();
|
||||
}
|
||||
|
||||
PickupCts = new CancellationTokenSource();
|
||||
|
||||
PickupSequence = Sequence.Create().Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = 0.5f
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await PickupSequence.ToUniTask(cancellationToken: PickupCts.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlayerManager.CollectItem(_itemData);
|
||||
await DestructionService.Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 529a2a922f6f48a982159e2dbc41c542
|
||||
timeCreated: 1770918505
|
||||
116
Assets/Scripts/Framework/Managers/Levels/Data/BaseLevel.cs
Normal file
116
Assets/Scripts/Framework/Managers/Levels/Data/BaseLevel.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Hints.Data;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
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.Settings;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Levels.Data
|
||||
{
|
||||
public class BaseLevel : MonoBehaviour
|
||||
{
|
||||
[Header("General")]
|
||||
[SerializeField]
|
||||
private LevelKey _levelKey;
|
||||
[SerializeField]
|
||||
private string _levelName;
|
||||
[Header("Items")]
|
||||
[SerializeField]
|
||||
public List<BaseItem> Pickups;
|
||||
|
||||
public List<CodexTrigger> CodexTriggers;
|
||||
|
||||
[Header("Setup")]
|
||||
[SerializeField]
|
||||
protected GraphicRaycaster _raycaster;
|
||||
|
||||
protected DestructionService DestructionService;
|
||||
|
||||
protected EventCoordinator EventCoordinator;
|
||||
protected InteractManager InteractManager;
|
||||
protected PlayerManager PlayerManager;
|
||||
protected SaveManager SaveManager;
|
||||
protected SettingsService SettingsService;
|
||||
protected TutorialService TutorialService;
|
||||
public virtual string SceneID => AssetKeyIdentifiers.Get(SceneKey.GameScene);
|
||||
public string LevelID => AssetKeyIdentifiers.Get(_levelKey);
|
||||
|
||||
public virtual string LevelName => _levelName;
|
||||
|
||||
public virtual bool IsPuzzleLevel { get; }
|
||||
|
||||
public virtual int CurrentLevelHintStage { get; set; }
|
||||
|
||||
public virtual Dictionary<int, BaseHint> Hints { get; }
|
||||
|
||||
[Inject]
|
||||
public void Construct(EventCoordinator eventCoordinator, InteractManager interactManager, SaveManager saveManager,
|
||||
DestructionService destructionService, SettingsService settingsService, PlayerManager playerManager,
|
||||
TutorialService tutorialService)
|
||||
{
|
||||
EventCoordinator = eventCoordinator;
|
||||
InteractManager = interactManager;
|
||||
SaveManager = saveManager;
|
||||
DestructionService = destructionService;
|
||||
SettingsService = settingsService;
|
||||
PlayerManager = playerManager;
|
||||
TutorialService = tutorialService;
|
||||
}
|
||||
|
||||
public async UniTask PostLoad()
|
||||
{
|
||||
InteractManager.AddUIRaycaster(_raycaster);
|
||||
EventCoordinator.Publish(new UIToggleHudEvent(true));
|
||||
|
||||
await PostLoadInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after Level Load, but before activating. Override for Implementations
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual UniTask PostLoadInternal()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async UniTask PostActivate()
|
||||
{
|
||||
await PostActivateInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after a level is activated. Override for implementations.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual UniTask PostActivateInternal()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async UniTask PreUnload()
|
||||
{
|
||||
InteractManager.RemoveUIRaycaster(_raycaster);
|
||||
await PreUnloadInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before the level is destroyed. Override for cleanup, etc.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual UniTask PreUnloadInternal()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ccd49cd2f5c4e8b9cfd394f7844bdf8
|
||||
timeCreated: 1769725044
|
||||
301
Assets/Scripts/Framework/Managers/Levels/LevelManager.cs
Normal file
301
Assets/Scripts/Framework/Managers/Levels/LevelManager.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
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.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;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Levels
|
||||
{
|
||||
public class LevelManager : IDisposable, IManager
|
||||
{
|
||||
private const float LEVEL_FADE_DURATION = 1f;
|
||||
|
||||
private readonly AddressableManager _addressableManager;
|
||||
private readonly AssetRegistry _assetRegistry;
|
||||
private readonly DestructionService _destructionService;
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly object _lock = new();
|
||||
private readonly PuzzleService _puzzleService;
|
||||
private readonly SaveManager _saveManager;
|
||||
|
||||
private UniTask<bool> _activeLoadTask = UniTask.FromResult(false);
|
||||
private BaseLevel _currentLevel;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
[Inject]
|
||||
public LevelManager(
|
||||
AddressableManager addressableManager,
|
||||
AssetRegistry assetRegistry,
|
||||
DestructionService destructionService,
|
||||
EventCoordinator eventCoordinator,
|
||||
SaveManager saveManager,
|
||||
PuzzleService puzzleService)
|
||||
{
|
||||
_addressableManager = addressableManager;
|
||||
_assetRegistry = assetRegistry;
|
||||
_destructionService = destructionService;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_saveManager = saveManager;
|
||||
_puzzleService = puzzleService;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
return;
|
||||
|
||||
Debug.Log($"[{nameof(LevelManager)}] Initializing...");
|
||||
_saveManager.OnSaveRequested += OnSaveGameRequested;
|
||||
_eventCoordinator.Subscribe<UpdateHintProgressEvent>(OnHintStageUpdated);
|
||||
Debug.Log($"[{nameof(LevelManager)}] Initialized.");
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
_saveManager.OnSaveRequested -= OnSaveGameRequested;
|
||||
_eventCoordinator.Unsubscribe<UpdateHintProgressEvent>(OnHintStageUpdated);
|
||||
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (evt.Force)
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = incoming;
|
||||
return;
|
||||
}
|
||||
|
||||
var current = Mathf.Max(0, _currentLevel.CurrentLevelHintStage);
|
||||
_currentLevel.CurrentLevelHintStage = Mathf.Max(current, incoming);
|
||||
}
|
||||
|
||||
private void OnSaveGameRequested(SaveGame saveGame)
|
||||
{
|
||||
if (saveGame == null || _currentLevel == null)
|
||||
return;
|
||||
|
||||
saveGame.CurrentLevelID = _currentLevel.LevelID;
|
||||
saveGame.CurrentSceneID = _currentLevel.SceneID;
|
||||
|
||||
saveGame.LevelHintStages ??= new Dictionary<string, int>();
|
||||
saveGame.LevelHintStages[_currentLevel.LevelID] = Mathf.Max(0, _currentLevel.CurrentLevelHintStage);
|
||||
}
|
||||
|
||||
public UniTask<bool> LoadLevel(string levelAssetID)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(levelAssetID))
|
||||
{
|
||||
Debug.LogError("[LevelManager] LoadLevel called with null/empty levelAssetID.");
|
||||
return UniTask.FromResult(false);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_activeLoadTask = LoadLevelInternal(levelAssetID);
|
||||
return _activeLoadTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadLevelInternal(string levelAssetID)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_assetRegistry == null)
|
||||
{
|
||||
Debug.LogError("[LevelManager] AssetRegistry is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_assetRegistry.TryGetReference(levelAssetID, out var levelRef) || levelRef == null)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] No level reference found for id '{levelAssetID}'.");
|
||||
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)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Failed to instantiate level '{levelAssetID}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var level = levelObj.GetComponent<BaseLevel>();
|
||||
if (level == null)
|
||||
{
|
||||
Debug.LogError($"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
|
||||
await _destructionService.Destroy(levelObj);
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentLevel = level;
|
||||
|
||||
RestoreHintStageForCurrentLevel();
|
||||
await RestoreItemStateForCurrentLevel();
|
||||
|
||||
await _currentLevel.PostLoad();
|
||||
|
||||
if (_currentLevel is BasePuzzle puzzle)
|
||||
await _puzzleService.LoadPuzzle(puzzle);
|
||||
|
||||
_eventCoordinator.Publish(new LevelChangedEvent(_currentLevel));
|
||||
|
||||
_eventCoordinator.PublishImmediate(new FadeEvent(true, LEVEL_FADE_DURATION));
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
|
||||
|
||||
if (_currentLevel != null)
|
||||
await _currentLevel.PostActivate();
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreHintStageForCurrentLevel()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
return;
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save?.LevelHintStages == null)
|
||||
{
|
||||
_currentLevel.CurrentLevelHintStage = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (interactable.ItemData == null)
|
||||
{
|
||||
Debug.Log($"[LevelManager] No Item Data for {interactable.InteractableName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
foreach (var trigger in codexTriggers)
|
||||
{
|
||||
if (save.DiscoveredCodexEntries.Any(x => x.UniqueIdentifier == trigger.Entry.UniqueID))
|
||||
{
|
||||
if (trigger.RemoveTrigger)
|
||||
await _destructionService.Destroy(trigger.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UniTask UnloadLevel()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_activeLoadTask.Status == UniTaskStatus.Pending)
|
||||
return _activeLoadTask.ContinueWith(_ => UnloadLevelInternal());
|
||||
|
||||
return UnloadLevelInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask UnloadLevelInternal()
|
||||
{
|
||||
if (_currentLevel == null)
|
||||
return;
|
||||
|
||||
var level = _currentLevel;
|
||||
_currentLevel = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (level is BasePuzzle puzzle)
|
||||
await _puzzleService.SavePuzzle(puzzle);
|
||||
|
||||
_eventCoordinator.Publish(new RequestGameSaveEvent());
|
||||
await level.PreUnload();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LevelManager] Exception in PreUnload of {level.name}: {ex}");
|
||||
}
|
||||
|
||||
await _destructionService.Destroy(level.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21e9be484b29431886214bcd47848292
|
||||
timeCreated: 1769778109
|
||||
3
Assets/Scripts/Framework/Managers/Player.meta
Normal file
3
Assets/Scripts/Framework/Managers/Player.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84087611dabd4fb286680a7ed6b586d5
|
||||
timeCreated: 1769703840
|
||||
BIN
Assets/Scripts/Framework/Managers/Player/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Player/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/Player/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Player/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 650e923c83a4463ba1eebe0b4c85277c
|
||||
timeCreated: 1773830133
|
||||
3
Assets/Scripts/Framework/Managers/Player/Data/Codex.meta
Normal file
3
Assets/Scripts/Framework/Managers/Player/Data/Codex.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58a00e94414846c6b914db0fbc38837f
|
||||
timeCreated: 1773682720
|
||||
87
Assets/Scripts/Framework/Managers/Player/Data/Codex/Codex.cs
Normal file
87
Assets/Scripts/Framework/Managers/Player/Data/Codex/Codex.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
public class Codex
|
||||
{
|
||||
private readonly List<CodexEntrySo> _entries = new();
|
||||
|
||||
public IReadOnlyList<CodexEntrySo> Entries => _entries;
|
||||
|
||||
public void AddEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (_entries.Any(x => x.UniqueID == entry.UniqueID))
|
||||
return;
|
||||
|
||||
Debug.Log($"Adding codex entry {entry.UniqueID}");
|
||||
|
||||
_entries.Add(entry);
|
||||
}
|
||||
|
||||
public void RemoveEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (_entries.All(x => x.UniqueID != entry.UniqueID))
|
||||
return;
|
||||
|
||||
_entries.Remove(entry);
|
||||
}
|
||||
|
||||
public void RemoveEntry(string uniqueID)
|
||||
{
|
||||
var entry = _entries.FirstOrDefault(x => x.UniqueID == uniqueID);
|
||||
|
||||
if (entry)
|
||||
_entries.Remove(entry);
|
||||
}
|
||||
|
||||
public bool HasEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return false;
|
||||
|
||||
return _entries.Any(x => x.UniqueID == entry.UniqueID);
|
||||
}
|
||||
|
||||
public bool HasEntry(string uniqueID)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uniqueID))
|
||||
return false;
|
||||
|
||||
return _entries.Any(x => x.UniqueID == uniqueID);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetEntriesByType(CodexType codexType)
|
||||
{
|
||||
return _entries.Where(x => x.EntryType == codexType);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetBookEntries()
|
||||
{
|
||||
return GetEntriesByType(CodexType.BookEntry);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetPuzzleClues()
|
||||
{
|
||||
return GetEntriesByType(CodexType.PuzzleClue);
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetPhotoEntries()
|
||||
{
|
||||
return GetEntriesByType(CodexType.Photo);
|
||||
}
|
||||
|
||||
public void ClearEntries()
|
||||
{
|
||||
_entries.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15993ce7dcfd437186874d9d0822ad2d
|
||||
timeCreated: 1773682720
|
||||
@@ -0,0 +1,40 @@
|
||||
using BriarQueen.Framework.Managers.Levels.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
public class CodexTrigger : BaseItem, ICodexTrigger
|
||||
{
|
||||
[Header("Codex")]
|
||||
[SerializeField]
|
||||
private CodexEntrySo _codexEntry;
|
||||
|
||||
[SerializeField]
|
||||
private bool _removeTrigger;
|
||||
|
||||
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
|
||||
|
||||
public override string InteractableName =>
|
||||
!string.IsNullOrWhiteSpace(InteractableTooltip) ? InteractableTooltip : _codexEntry.Title;
|
||||
|
||||
public GameObject TriggerObject => GameObject;
|
||||
public CodexEntrySo Entry => _codexEntry;
|
||||
|
||||
public bool RemoveTrigger => _removeTrigger;
|
||||
|
||||
public override async UniTask OnInteract(ItemDataSo item = null)
|
||||
{
|
||||
if (!CheckEmptyHands())
|
||||
return;
|
||||
|
||||
PlayerManager.UnlockCodexEntry(_codexEntry);
|
||||
|
||||
if (_removeTrigger)
|
||||
{
|
||||
await Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b26357202d94390a9d04c7cd3194b40
|
||||
timeCreated: 1773861326
|
||||
@@ -0,0 +1,13 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Codex
|
||||
{
|
||||
public interface ICodexTrigger
|
||||
{
|
||||
GameObject TriggerObject { get; }
|
||||
|
||||
CodexEntrySo Entry { get; }
|
||||
bool RemoveTrigger { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f81e1754251482a9e9bc183c5e1bd5b
|
||||
timeCreated: 1773868877
|
||||
109
Assets/Scripts/Framework/Managers/Player/Data/CodexEntrySO.cs
Normal file
109
Assets/Scripts/Framework/Managers/Player/Data/CodexEntrySO.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Briar Queen/Codex/New Codex Entry", fileName = "New Codex Entry")]
|
||||
public class CodexEntrySo : ScriptableObject
|
||||
{
|
||||
|
||||
[Header("Codex Type")]
|
||||
[SerializeField]
|
||||
private CodexType _codexType;
|
||||
|
||||
[Header("Codex ID")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsBookEntry))]
|
||||
private BookEntryID _bookEntryID;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPuzzleClue))]
|
||||
private ClueEntryID _clueEntryID;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPhoto))]
|
||||
private PhotoEntryID _photoEntryID;
|
||||
|
||||
|
||||
[Header("Display")]
|
||||
[SerializeField]
|
||||
private string _title;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(EConditionOperator.Or, nameof(IsPhoto), nameof(_isPhotoOverride))]
|
||||
private Sprite _displayImage;
|
||||
|
||||
[TextArea(6, 20)]
|
||||
[SerializeField]
|
||||
[ShowIf(EConditionOperator.Or, nameof(IsBookEntry), nameof(_bodyTextOverride))]
|
||||
private string _bodyText;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(EConditionOperator.Or, nameof(IsPhoto), nameof(_isPhotoOverride))]
|
||||
private string _photoDescription;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(EConditionOperator.Or, nameof(IsPhoto), nameof(_isPhotoOverride))]
|
||||
private string _polaroidWriting;
|
||||
|
||||
[Header("Behaviour")]
|
||||
[SerializeField]
|
||||
private bool _startsUnlocked;
|
||||
|
||||
[SerializeField]
|
||||
private bool _hiddenUntilUnlocked = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _isPhotoOverride;
|
||||
|
||||
[SerializeField]
|
||||
private bool _bodyTextOverride;
|
||||
|
||||
[SerializeField]
|
||||
private Location _location;
|
||||
|
||||
public CodexType EntryType => _codexType;
|
||||
public Location Location => _location;
|
||||
|
||||
public bool IsBookEntry => _codexType == CodexType.BookEntry;
|
||||
public bool IsPuzzleClue => _codexType == CodexType.PuzzleClue;
|
||||
public bool IsPhoto => _codexType == CodexType.Photo;
|
||||
|
||||
public BookEntryID BookEntryID => _bookEntryID;
|
||||
public ClueEntryID ClueEntryID => _clueEntryID;
|
||||
public PhotoEntryID PhotoEntryID => _photoEntryID;
|
||||
|
||||
public string Title => _title;
|
||||
public Sprite DisplayImage => _displayImage;
|
||||
public string BodyText => _bodyText;
|
||||
public string PhotoDescription => _photoDescription;
|
||||
public string PolaroidWriting => _polaroidWriting;
|
||||
|
||||
public bool StartsUnlocked => _startsUnlocked;
|
||||
public bool HiddenUntilUnlocked => _hiddenUntilUnlocked;
|
||||
public bool IsPhotoOverride => _isPhotoOverride;
|
||||
public bool IsBodyTextOverride => _bodyTextOverride;
|
||||
|
||||
|
||||
public string UniqueID
|
||||
{
|
||||
get
|
||||
{
|
||||
return _codexType switch
|
||||
{
|
||||
CodexType.BookEntry when _bookEntryID != BookEntryID.None =>
|
||||
CodexEntryIDs.Get(_bookEntryID),
|
||||
|
||||
CodexType.PuzzleClue when _clueEntryID != ClueEntryID.None =>
|
||||
CodexEntryIDs.Get(_clueEntryID),
|
||||
|
||||
CodexType.Photo when _photoEntryID != PhotoEntryID.None =>
|
||||
CodexEntryIDs.Get(_photoEntryID),
|
||||
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5bdd4c819cc240c1b89ef5a0e33145ef
|
||||
timeCreated: 1773682436
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a795285ea8e94689952d456af9369c92
|
||||
timeCreated: 1769700038
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Inventory
|
||||
{
|
||||
public class Backpack
|
||||
{
|
||||
private readonly List<ItemDataSo> _items = new();
|
||||
|
||||
public IReadOnlyList<ItemDataSo> Items => _items;
|
||||
|
||||
|
||||
public void AddItem(ItemDataSo item)
|
||||
{
|
||||
if (_items.Any(x => x.UniqueID == item.UniqueID))
|
||||
return;
|
||||
|
||||
Debug.Log($"Adding item {item.UniqueID}");
|
||||
|
||||
_items.Add(item);
|
||||
}
|
||||
|
||||
public void RemoveItem(ItemDataSo item)
|
||||
{
|
||||
if (_items.All(x => x.UniqueID != item.UniqueID)) return;
|
||||
|
||||
_items.Remove(item);
|
||||
}
|
||||
|
||||
public void RemoveItem(string uniqueID)
|
||||
{
|
||||
var item = _items.FirstOrDefault(x => x.UniqueID == uniqueID);
|
||||
|
||||
if (item) _items.Remove(item);
|
||||
}
|
||||
|
||||
public void ClearItems()
|
||||
{
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 855a4eee12334e29938a4b6c564aa157
|
||||
timeCreated: 1769701500
|
||||
82
Assets/Scripts/Framework/Managers/Player/Data/ItemDataSO.cs
Normal file
82
Assets/Scripts/Framework/Managers/Player/Data/ItemDataSO.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Briar Queen/Items/New Item Data", fileName = "New Item Data")]
|
||||
public class ItemDataSo : ScriptableObject
|
||||
{
|
||||
public enum ItemIdType
|
||||
{
|
||||
None = 0,
|
||||
Pickup = 1,
|
||||
Environment = 2,
|
||||
PuzzleSlot = 3
|
||||
}
|
||||
|
||||
[Header("Item Type")]
|
||||
[SerializeField]
|
||||
private ItemIdType _idType;
|
||||
|
||||
[Header("Item Details")]
|
||||
[Tooltip("Pickup key used for inventory items.")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPickup))]
|
||||
private ItemKey _itemKey;
|
||||
|
||||
[Tooltip("Environment key used for world interactables.")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsEnvironment))]
|
||||
private EnvironmentKey _environmentKey;
|
||||
|
||||
[Tooltip("Puzzle slot key used for puzzle slot interactables.")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsPuzzleSlot))]
|
||||
private PuzzleSlotKey _puzzleSlotKey;
|
||||
|
||||
[SerializeField]
|
||||
private Sprite _icon;
|
||||
|
||||
[SerializeField]
|
||||
private string _itemName;
|
||||
|
||||
[Header("Inventory Interaction (serialized inline, no extra SO assets)")]
|
||||
[SerializeReference]
|
||||
private BaseInteraction _interaction = new NoOpItemInteraction();
|
||||
|
||||
public Sprite Icon => _icon;
|
||||
public string ItemName => _itemName;
|
||||
public BaseInteraction Interaction => _interaction;
|
||||
|
||||
public bool IsPickup => _idType == ItemIdType.Pickup;
|
||||
public bool IsEnvironment => _idType == ItemIdType.Environment;
|
||||
public bool IsPuzzleSlot => _idType == ItemIdType.PuzzleSlot;
|
||||
|
||||
public ItemIdType IdType => _idType;
|
||||
public ItemKey ItemKey => _itemKey;
|
||||
public EnvironmentKey EnvironmentKey => _environmentKey;
|
||||
public PuzzleSlotKey PuzzleSlotKey => _puzzleSlotKey;
|
||||
|
||||
public string UniqueID
|
||||
{
|
||||
get
|
||||
{
|
||||
return _idType switch
|
||||
{
|
||||
ItemIdType.Pickup when _itemKey != ItemKey.None =>
|
||||
ItemIDs.Get(_itemKey),
|
||||
|
||||
ItemIdType.Environment when _environmentKey != EnvironmentKey.None =>
|
||||
ItemIDs.Get(_environmentKey),
|
||||
|
||||
ItemIdType.PuzzleSlot when _puzzleSlotKey != PuzzleSlotKey.None =>
|
||||
ItemIDs.Get(_puzzleSlotKey),
|
||||
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 668f0266fce84811b2efce4a48adff09
|
||||
timeCreated: 1769700109
|
||||
3
Assets/Scripts/Framework/Managers/Player/Data/Tools.meta
Normal file
3
Assets/Scripts/Framework/Managers/Player/Data/Tools.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfd9e5206a504db9a0aa92f0e934776f
|
||||
timeCreated: 1773954968
|
||||
201
Assets/Scripts/Framework/Managers/Player/Data/Tools/Toolbelt.cs
Normal file
201
Assets/Scripts/Framework/Managers/Player/Data/Tools/Toolbelt.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player.Data.Tools
|
||||
{
|
||||
public class Toolbelt : IDisposable
|
||||
{
|
||||
private readonly Dictionary<ToolID, bool> _unlockedTools = new();
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly TutorialService _tutorialService;
|
||||
|
||||
public IReadOnlyDictionary<ToolID, bool> UnlockedTools => _unlockedTools;
|
||||
|
||||
public ToolID CurrentTool { get; private set; } = ToolID.None;
|
||||
|
||||
public Toolbelt(EventCoordinator eventCoordinator, TutorialService tutorialService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_tutorialService = tutorialService;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_eventCoordinator.Subscribe<OnNextToolChangedEvent>(OnNextToolChanged);
|
||||
_eventCoordinator.Subscribe<OnPreviousToolChangedEvent>(OnPreviousToolChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<OnNextToolChangedEvent>(OnNextToolChanged);
|
||||
_eventCoordinator.Unsubscribe<OnPreviousToolChangedEvent>(OnPreviousToolChanged);
|
||||
}
|
||||
|
||||
public void Unlock(ToolID id)
|
||||
{
|
||||
if (id == ToolID.None)
|
||||
return;
|
||||
|
||||
_unlockedTools[id] = true;
|
||||
Debug.Log($"{this} Tool {id} has been unlocked");
|
||||
}
|
||||
|
||||
public void Lock(ToolID id)
|
||||
{
|
||||
if (id == ToolID.None)
|
||||
return;
|
||||
|
||||
if (_unlockedTools.ContainsKey(id))
|
||||
_unlockedTools[id] = false;
|
||||
|
||||
if (CurrentTool == id)
|
||||
SelectNextTool();
|
||||
}
|
||||
|
||||
public bool HasAccess(ToolID id)
|
||||
{
|
||||
if (id == ToolID.None)
|
||||
return true;
|
||||
|
||||
return _unlockedTools.TryGetValue(id, out var unlocked) && unlocked;
|
||||
}
|
||||
|
||||
public bool IsKnown(ToolID id)
|
||||
{
|
||||
if (id == ToolID.None)
|
||||
return true;
|
||||
|
||||
return _unlockedTools.ContainsKey(id);
|
||||
}
|
||||
|
||||
public void SelectTool(ToolID id)
|
||||
{
|
||||
if (!HasAccess(id))
|
||||
return;
|
||||
|
||||
SetCurrentTool(id, 0);
|
||||
}
|
||||
|
||||
public void SelectEmptyHands()
|
||||
{
|
||||
SetCurrentTool(ToolID.None, 0);
|
||||
}
|
||||
|
||||
public void SelectNextTool()
|
||||
{
|
||||
Debug.Log($"SelectNextTool: {CurrentTool}");
|
||||
CycleTool(1);
|
||||
}
|
||||
|
||||
public void SelectPreviousTool()
|
||||
{
|
||||
CycleTool(-1);
|
||||
}
|
||||
|
||||
private void OnNextToolChanged(OnNextToolChangedEvent e)
|
||||
{
|
||||
SelectNextTool();
|
||||
}
|
||||
|
||||
private void OnPreviousToolChanged(OnPreviousToolChangedEvent e)
|
||||
{
|
||||
SelectPreviousTool();
|
||||
}
|
||||
|
||||
private void CycleTool(int direction)
|
||||
{
|
||||
var selectableTools = GetSelectableTools();
|
||||
|
||||
Debug.Log($"CycleTool: {selectableTools.Count}");
|
||||
|
||||
foreach (var selectableTool in selectableTools)
|
||||
{
|
||||
Debug.Log($"CycleTool: {selectableTool}");
|
||||
}
|
||||
|
||||
if (selectableTools.Count == 0)
|
||||
{
|
||||
SetCurrentTool(ToolID.None, direction);
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = selectableTools.IndexOf(CurrentTool);
|
||||
|
||||
if (currentIndex < 0)
|
||||
{
|
||||
int fallbackIndex = direction > 0 ? 0 : selectableTools.Count - 1;
|
||||
SetCurrentTool(selectableTools[fallbackIndex], direction);
|
||||
return;
|
||||
}
|
||||
|
||||
int nextIndex = currentIndex + direction;
|
||||
|
||||
if (nextIndex < 0)
|
||||
nextIndex = selectableTools.Count - 1;
|
||||
else if (nextIndex >= selectableTools.Count)
|
||||
nextIndex = 0;
|
||||
|
||||
Debug.Log($"CycleTool: Setting tool to {selectableTools[nextIndex]}");
|
||||
|
||||
SetCurrentTool(selectableTools[nextIndex], direction);
|
||||
}
|
||||
|
||||
private void CheckToolsCycling()
|
||||
{
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.ToolCycling);
|
||||
}
|
||||
|
||||
private void SetCurrentTool(ToolID newTool, int direction)
|
||||
{
|
||||
if (CurrentTool == newTool)
|
||||
return;
|
||||
|
||||
CurrentTool = newTool;
|
||||
CheckToolsCycling();
|
||||
|
||||
_eventCoordinator.Publish(new SelectedToolChangedEvent(CurrentTool, direction));
|
||||
|
||||
if (newTool == ToolID.None)
|
||||
{
|
||||
_eventCoordinator.Publish(
|
||||
new OverrideCursorStyleChangeEvent(UICursorService.CursorStyle.Default));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Enum.TryParse(newTool.ToString(), out UICursorService.CursorStyle style))
|
||||
{
|
||||
_eventCoordinator.Publish(new OverrideCursorStyleChangeEvent(style));
|
||||
}
|
||||
else
|
||||
{
|
||||
_eventCoordinator.Publish(
|
||||
new OverrideCursorStyleChangeEvent(UICursorService.CursorStyle.Default));
|
||||
}
|
||||
}
|
||||
|
||||
private List<ToolID> GetSelectableTools()
|
||||
{
|
||||
var tools = _unlockedTools
|
||||
.Where(x => x.Value && x.Key != ToolID.None)
|
||||
.Select(x => x.Key)
|
||||
.OrderBy(x => (int)x)
|
||||
.ToList();
|
||||
|
||||
Debug.Log($"{_unlockedTools.Count} tools have been unlocked");
|
||||
|
||||
tools.Insert(0, ToolID.None);
|
||||
|
||||
Debug.Log($"[Get Selectable Tools: {tools.Count}]");
|
||||
|
||||
return tools;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 565885be67c447bbbb3a64e39ae1f734
|
||||
timeCreated: 1773954972
|
||||
449
Assets/Scripts/Framework/Managers/Player/PlayerManager.cs
Normal file
449
Assets/Scripts/Framework/Managers/Player/PlayerManager.cs
Normal file
@@ -0,0 +1,449 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Data.IO.Saves;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Audio;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.Player.Data.Codex;
|
||||
using BriarQueen.Framework.Managers.Player.Data.Inventory;
|
||||
using BriarQueen.Framework.Managers.Player.Data.Tools;
|
||||
using BriarQueen.Framework.Registries;
|
||||
using BriarQueen.Framework.Services.Tutorials;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// Owns player state (inventory + codex + tools) and keeps it in sync with save data.
|
||||
/// </summary>
|
||||
public class PlayerManager : IDisposable, IManager
|
||||
{
|
||||
private readonly CodexRegistry _codexRegistry;
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly ItemRegistry _itemRegistry;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly AudioManager _audioManager;
|
||||
private readonly TutorialService _tutorialService;
|
||||
|
||||
private Backpack _backpack;
|
||||
private Codex _codex;
|
||||
private Toolbelt _toolbelt;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
[Inject]
|
||||
public PlayerManager(
|
||||
SaveManager saveManager,
|
||||
ItemRegistry itemRegistry,
|
||||
CodexRegistry codexRegistry,
|
||||
EventCoordinator eventCoordinator,
|
||||
TutorialService tutorialService,
|
||||
AudioManager audioManager)
|
||||
{
|
||||
_saveManager = saveManager;
|
||||
_itemRegistry = itemRegistry;
|
||||
_codexRegistry = codexRegistry;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_tutorialService = tutorialService;
|
||||
_audioManager = audioManager;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
return;
|
||||
|
||||
_saveManager.OnSaveGameLoaded += LoadFromSave;
|
||||
_saveManager.OnSaveRequested += UpdateSaveGame;
|
||||
|
||||
_backpack ??= new Backpack();
|
||||
_codex ??= new Codex();
|
||||
|
||||
EnsureToolbelt();
|
||||
|
||||
if (_saveManager.IsGameLoaded && _saveManager.CurrentSave != null)
|
||||
LoadFromSave(_saveManager.CurrentSave);
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
_saveManager.OnSaveGameLoaded -= LoadFromSave;
|
||||
_saveManager.OnSaveRequested -= UpdateSaveGame;
|
||||
|
||||
_toolbelt?.Dispose();
|
||||
_toolbelt = null;
|
||||
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ItemDataSo> GetInventoryItems()
|
||||
{
|
||||
return _backpack?.Items ?? Array.Empty<ItemDataSo>();
|
||||
}
|
||||
|
||||
public IReadOnlyList<ItemDataSo> GetAllCollectedItems()
|
||||
{
|
||||
var collectedItems = new List<ItemDataSo>();
|
||||
var saveCollected = _saveManager.CurrentSave?.CollectedItems;
|
||||
|
||||
if (saveCollected == null)
|
||||
return collectedItems;
|
||||
|
||||
foreach (var item in saveCollected)
|
||||
{
|
||||
var template = _itemRegistry.FindItemTemplateByID(item.UniqueIdentifier);
|
||||
if (template != null)
|
||||
collectedItems.Add(template);
|
||||
}
|
||||
|
||||
return collectedItems;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CodexEntrySo> GetDiscoveredCodexEntries()
|
||||
{
|
||||
return _codex?.Entries ?? Array.Empty<CodexEntrySo>();
|
||||
}
|
||||
|
||||
public IEnumerable<CodexEntrySo> GetDiscoveredCodexEntriesByType(CodexType codexType)
|
||||
{
|
||||
return _codex?.GetEntriesByType(codexType) ?? Enumerable.Empty<CodexEntrySo>();
|
||||
}
|
||||
|
||||
public bool HasCodexEntry(string uniqueIdentifier)
|
||||
{
|
||||
return _codex != null && _codex.HasEntry(uniqueIdentifier);
|
||||
}
|
||||
|
||||
public bool HasCodexEntry(CodexEntrySo entry)
|
||||
{
|
||||
return _codex != null && _codex.HasEntry(entry);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ToolID, bool> GetTools()
|
||||
{
|
||||
EnsureToolbelt();
|
||||
return _toolbelt.UnlockedTools;
|
||||
}
|
||||
|
||||
public bool HasAccessToTool(ToolID id)
|
||||
{
|
||||
return _toolbelt != null && _toolbelt.HasAccess(id);
|
||||
}
|
||||
|
||||
#region Inventory / Selection
|
||||
|
||||
public void CollectItem(string uniqueIdentifier)
|
||||
{
|
||||
var item = _itemRegistry.FindItemTemplateByID(uniqueIdentifier);
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
CollectItem(item);
|
||||
}
|
||||
|
||||
public void CollectItem(ItemDataSo item)
|
||||
{
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
_backpack ??= new Backpack();
|
||||
_backpack.AddItem(item);
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save != null)
|
||||
{
|
||||
save.CollectedItems ??= new List<ItemSaveData>();
|
||||
|
||||
if (save.CollectedItems.All(x => x.UniqueIdentifier != item.UniqueID))
|
||||
{
|
||||
save.CollectedItems.Add(new ItemSaveData
|
||||
{
|
||||
UniqueIdentifier = item.UniqueID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemCollected));
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new InventoryChangedEvent());
|
||||
}
|
||||
|
||||
public void RemoveItem(string uniqueIdentifier)
|
||||
{
|
||||
var item = _backpack?.Items.FirstOrDefault(x => x.UniqueID == uniqueIdentifier);
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
RemoveItem(item);
|
||||
}
|
||||
|
||||
public void RemoveItem(ItemDataSo item)
|
||||
{
|
||||
if (item == null || _backpack == null)
|
||||
return;
|
||||
|
||||
_backpack.RemoveItem(item);
|
||||
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new InventoryChangedEvent());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Codex
|
||||
|
||||
public void UnlockCodexEntry(string uniqueIdentifier)
|
||||
{
|
||||
var entry = _codexRegistry.FindEntryByID(uniqueIdentifier);
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
UnlockCodexEntry(entry);
|
||||
}
|
||||
|
||||
public void UnlockCodexEntry(CodexEntrySo entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
_codex ??= new Codex();
|
||||
|
||||
if (_codex.HasEntry(entry))
|
||||
return;
|
||||
|
||||
_codex.AddEntry(entry);
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save != null)
|
||||
{
|
||||
save.DiscoveredCodexEntries ??= new List<CodexSaveData>();
|
||||
|
||||
if (save.DiscoveredCodexEntries.All(x => x.UniqueIdentifier != entry.UniqueID))
|
||||
{
|
||||
save.DiscoveredCodexEntries.Add(new CodexSaveData
|
||||
{
|
||||
UniqueIdentifier = entry.UniqueID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_tutorialService.DisplayTutorial(TutorialPopupID.Codex);
|
||||
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
_eventCoordinator.Publish(new CodexChangedEvent(entry.EntryType));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tools
|
||||
|
||||
public ToolID GetEquippedTool()
|
||||
{
|
||||
EnsureToolbelt();
|
||||
return _toolbelt.CurrentTool;
|
||||
}
|
||||
|
||||
public void UnlockTool(ToolID id)
|
||||
{
|
||||
EnsureToolbelt();
|
||||
|
||||
if (_toolbelt.HasAccess(id))
|
||||
return;
|
||||
|
||||
_toolbelt.Unlock(id);
|
||||
Debug.Log($"Tool {id} has been unlocked");
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save != null)
|
||||
{
|
||||
save.Tools ??= new Dictionary<ToolID, bool>();
|
||||
save.Tools[id] = true;
|
||||
}
|
||||
|
||||
Debug.Log($"Tool {id} access after unlock = {_toolbelt.HasAccess(id)}");
|
||||
|
||||
_eventCoordinator.Publish(new ToolbeltChangedEvent(id, false));
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
public void LockTool(ToolID id)
|
||||
{
|
||||
EnsureToolbelt();
|
||||
|
||||
if (!_toolbelt.UnlockedTools.TryGetValue(id, out var unlocked) || !unlocked)
|
||||
return;
|
||||
|
||||
_toolbelt.Lock(id);
|
||||
|
||||
var save = _saveManager.CurrentSave;
|
||||
if (save != null)
|
||||
{
|
||||
save.Tools ??= new Dictionary<ToolID, bool>();
|
||||
save.Tools[id] = false;
|
||||
}
|
||||
|
||||
_eventCoordinator.Publish(new ToolbeltChangedEvent(id, true));
|
||||
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
|
||||
}
|
||||
|
||||
private void LoadToolsFromSave(SaveGame save)
|
||||
{
|
||||
_toolbelt?.Dispose();
|
||||
_toolbelt = new Toolbelt(_eventCoordinator, _tutorialService);
|
||||
_toolbelt.Initialize();
|
||||
|
||||
if (save.Tools == null)
|
||||
return;
|
||||
|
||||
foreach (var kvp in save.Tools)
|
||||
{
|
||||
if (kvp.Value)
|
||||
_toolbelt.Unlock(kvp.Key);
|
||||
else
|
||||
_toolbelt.Lock(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureToolbelt()
|
||||
{
|
||||
if (_toolbelt != null)
|
||||
return;
|
||||
|
||||
_toolbelt = new Toolbelt(_eventCoordinator, _tutorialService);
|
||||
_toolbelt.Initialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Saving / Loading
|
||||
|
||||
private void LoadFromSave(SaveGame save)
|
||||
{
|
||||
if (save == null)
|
||||
return;
|
||||
|
||||
LoadItemsFromSave(save);
|
||||
LoadCodexFromSave(save);
|
||||
LoadToolsFromSave(save);
|
||||
}
|
||||
|
||||
private void LoadItemsFromSave(SaveGame save)
|
||||
{
|
||||
_backpack = new Backpack();
|
||||
|
||||
Debug.Log($"[PlayerManager] LoadItemsFromSave InventoryData count = {save.InventoryData?.Count ?? 0}");
|
||||
|
||||
if (save.InventoryData != null)
|
||||
{
|
||||
foreach (var item in save.InventoryData)
|
||||
{
|
||||
Debug.Log($"[PlayerManager] Loading InventoryData item '{item.UniqueIdentifier}'");
|
||||
|
||||
var itemData = _itemRegistry.FindItemTemplateByID(item.UniqueIdentifier);
|
||||
if (!itemData)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerManager] Missing item template '{item.UniqueIdentifier}', skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_backpack.AddItem(itemData);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[PlayerManager] Backpack count after load = {_backpack.Items?.Count ?? 0}");
|
||||
|
||||
_eventCoordinator.Publish(new SelectedItemChangedEvent(null));
|
||||
_eventCoordinator.Publish(new InventoryChangedEvent());
|
||||
}
|
||||
|
||||
private void LoadCodexFromSave(SaveGame save)
|
||||
{
|
||||
_codex = new Codex();
|
||||
|
||||
if (save.DiscoveredCodexEntries != null)
|
||||
{
|
||||
foreach (var codexEntry in save.DiscoveredCodexEntries)
|
||||
{
|
||||
var entryData = _codexRegistry.FindEntryByID(codexEntry.UniqueIdentifier);
|
||||
if (!entryData)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerManager] Missing codex entry '{codexEntry.UniqueIdentifier}', skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_codex.AddEntry(entryData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSaveGame(SaveGame save)
|
||||
{
|
||||
if (save == null)
|
||||
return;
|
||||
|
||||
Debug.Log($"[PlayerManager] UpdateSaveGame backpack count = {_backpack?.Items?.Count ?? 0}");
|
||||
|
||||
save.InventoryData = new List<ItemSaveData>();
|
||||
|
||||
if (_backpack?.Items != null)
|
||||
{
|
||||
foreach (var item in _backpack.Items)
|
||||
{
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
Debug.Log($"[PlayerManager] Writing InventoryData item '{item.UniqueID}'");
|
||||
|
||||
save.InventoryData.Add(new ItemSaveData
|
||||
{
|
||||
UniqueIdentifier = item.UniqueID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[PlayerManager] Final InventoryData count = {save.InventoryData.Count}");
|
||||
|
||||
save.DiscoveredCodexEntries ??= new List<CodexSaveData>();
|
||||
save.DiscoveredCodexEntries.Clear();
|
||||
|
||||
if (_codex?.Entries != null)
|
||||
{
|
||||
foreach (var entry in _codex.Entries)
|
||||
{
|
||||
if (!entry)
|
||||
continue;
|
||||
|
||||
save.DiscoveredCodexEntries.Add(new CodexSaveData
|
||||
{
|
||||
UniqueIdentifier = entry.UniqueID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
save.Tools ??= new Dictionary<ToolID, bool>();
|
||||
save.Tools.Clear();
|
||||
|
||||
if (_toolbelt?.UnlockedTools != null)
|
||||
{
|
||||
foreach (var kvp in _toolbelt.UnlockedTools)
|
||||
{
|
||||
save.Tools[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ec45c587ec54a4eb6ffbdedf10eed57
|
||||
timeCreated: 1769703840
|
||||
3
Assets/Scripts/Framework/Managers/UI.meta
Normal file
3
Assets/Scripts/Framework/Managers/UI.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d5ba8eaeca4785ae5052c314f42104
|
||||
timeCreated: 1769706518
|
||||
BIN
Assets/Scripts/Framework/Managers/UI/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/UI/.DS_Store
vendored
Normal file
Binary file not shown.
3
Assets/Scripts/Framework/Managers/UI/Base.meta
Normal file
3
Assets/Scripts/Framework/Managers/UI/Base.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdeb86849b3447b08f904cbe2af60572
|
||||
timeCreated: 1773830601
|
||||
15
Assets/Scripts/Framework/Managers/UI/Base/IHud.cs
Normal file
15
Assets/Scripts/Framework/Managers/UI/Base/IHud.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IHud
|
||||
{
|
||||
UniTask Hide();
|
||||
UniTask Show();
|
||||
|
||||
UniTask DisplayInteractText(string text);
|
||||
|
||||
GraphicRaycaster Raycaster { get; }
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/UI/Base/IHud.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/UI/Base/IHud.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69ce1a09c8ba468184443377cb160251
|
||||
timeCreated: 1773830812
|
||||
15
Assets/Scripts/Framework/Managers/UI/Base/IPopup.cs
Normal file
15
Assets/Scripts/Framework/Managers/UI/Base/IPopup.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IPopup
|
||||
{
|
||||
UniTask Show();
|
||||
UniTask Hide();
|
||||
|
||||
UniTask Play(string text, float duration);
|
||||
|
||||
GameObject GameObject { get; }
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/UI/Base/IPopup.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/UI/Base/IPopup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6c40bdc6e55460e859a7d0d76422e5b
|
||||
timeCreated: 1773830634
|
||||
12
Assets/Scripts/Framework/Managers/UI/Base/IScreenFader.cs
Normal file
12
Assets/Scripts/Framework/Managers/UI/Base/IScreenFader.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IScreenFader
|
||||
{
|
||||
UniTask FadeFromAsync(float duration);
|
||||
UniTask FadeToAsync(float duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 486cc0093cdf41db9db9d4a4af124a6f
|
||||
timeCreated: 1773831975
|
||||
12
Assets/Scripts/Framework/Managers/UI/Base/IUIWindow.cs
Normal file
12
Assets/Scripts/Framework/Managers/UI/Base/IUIWindow.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public interface IUIWindow
|
||||
{
|
||||
UniTask Show();
|
||||
UniTask Hide();
|
||||
|
||||
WindowType WindowType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27aca68a2b694025b3ed9c26b2d7d5b2
|
||||
timeCreated: 1769707713
|
||||
9
Assets/Scripts/Framework/Managers/UI/Base/WindowType.cs
Normal file
9
Assets/Scripts/Framework/Managers/UI/Base/WindowType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace BriarQueen.Framework.Managers.UI.Base
|
||||
{
|
||||
public enum WindowType
|
||||
{
|
||||
PauseMenuWindow,
|
||||
SettingsWindow,
|
||||
CodexWindow
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 616cfa2de3124c308c9706f50b7ab339
|
||||
timeCreated: 1773832262
|
||||
8
Assets/Scripts/Framework/Managers/UI/Events.meta
Normal file
8
Assets/Scripts/Framework/Managers/UI/Events.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75c1e9dba297a499998f45e608b4b71a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Scripts/Framework/Managers/UI/Events/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/UI/Events/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
using BriarQueen.Framework.Events.System;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI.Events
|
||||
{
|
||||
public record UIToggleSettingsWindow(bool Show) : IEvent;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf239878f7aa941c2abbef0ebb2e41bd
|
||||
285
Assets/Scripts/Framework/Managers/UI/UICursorService.cs
Normal file
285
Assets/Scripts/Framework/Managers/UI/UICursorService.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
public sealed class UICursorService : MonoBehaviour
|
||||
{
|
||||
public enum CursorStyle
|
||||
{
|
||||
Default,
|
||||
Interact,
|
||||
Talk,
|
||||
Pickup,
|
||||
Inspect,
|
||||
UseItem,
|
||||
Travel,
|
||||
Knife,
|
||||
}
|
||||
|
||||
[Header("System Cursor")]
|
||||
[SerializeField]
|
||||
private bool _enableCustomCursor = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool _restoreDefaultCursorOnDisable = true;
|
||||
|
||||
[Header("Software Cursor")]
|
||||
[SerializeField]
|
||||
private RectTransform _virtualCursorTransform;
|
||||
|
||||
[SerializeField]
|
||||
private Image _virtualCursorImage;
|
||||
|
||||
[Header("Styles")]
|
||||
[SerializeField]
|
||||
private CursorStyle _startingStyle = CursorStyle.Default;
|
||||
|
||||
[SerializeField]
|
||||
private List<CursorStyleEntry> _styles = new();
|
||||
|
||||
private readonly Dictionary<CursorStyle, CursorStyleEntry> _styleMap = new();
|
||||
|
||||
private EventCoordinator _eventCoordinator;
|
||||
|
||||
private CursorStyle _currentStyle;
|
||||
private CursorStyle _currentStyleOverride = CursorStyle.Default;
|
||||
private bool _isStyleOverridden;
|
||||
|
||||
private Texture2D _currentTexture;
|
||||
private Vector2 _currentHotspot;
|
||||
private CursorMode _currentMode;
|
||||
|
||||
private bool _useVirtualCursor;
|
||||
|
||||
public CursorStyleEntry CurrentStyleEntry => _styleMap[_currentStyle];
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BuildStyleMap();
|
||||
_currentStyle = _startingStyle;
|
||||
_currentStyleOverride = CursorStyle.Default;
|
||||
_isStyleOverridden = false;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_eventCoordinator?.Subscribe<CursorStyleChangeEvent>(OnCursorStyleChangeEvent);
|
||||
_eventCoordinator?.Subscribe<OverrideCursorStyleChangeEvent>(OnOverrideCursorStyleChangeEvent);
|
||||
|
||||
ApplyCurrentEffectiveStyle();
|
||||
ApplyCursorModeVisuals();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_eventCoordinator?.Unsubscribe<CursorStyleChangeEvent>(OnCursorStyleChangeEvent);
|
||||
_eventCoordinator?.Unsubscribe<OverrideCursorStyleChangeEvent>(OnOverrideCursorStyleChangeEvent);
|
||||
|
||||
if (_restoreDefaultCursorOnDisable)
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
BuildStyleMap();
|
||||
}
|
||||
|
||||
public void SetUseVirtualCursor(bool useVirtualCursor)
|
||||
{
|
||||
if (_useVirtualCursor == useVirtualCursor)
|
||||
return;
|
||||
|
||||
_useVirtualCursor = useVirtualCursor;
|
||||
ApplyCursorModeVisuals();
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
public void SetVirtualCursorPosition(Vector2 screenPosition)
|
||||
{
|
||||
if (!_useVirtualCursor || _virtualCursorTransform == null)
|
||||
return;
|
||||
|
||||
_virtualCursorTransform.position = screenPosition;
|
||||
}
|
||||
|
||||
public void SetStyle(CursorStyle style)
|
||||
{
|
||||
_currentStyle = style;
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
public void SetOverrideStyle(CursorStyle style)
|
||||
{
|
||||
if (style == CursorStyle.Default)
|
||||
{
|
||||
ClearOverrideStyle();
|
||||
return;
|
||||
}
|
||||
|
||||
_isStyleOverridden = true;
|
||||
_currentStyleOverride = style;
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
public void ClearOverrideStyle()
|
||||
{
|
||||
_isStyleOverridden = false;
|
||||
_currentStyleOverride = CursorStyle.Default;
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
public void ResetToDefault()
|
||||
{
|
||||
_currentStyle = CursorStyle.Default;
|
||||
ClearOverrideStyle();
|
||||
}
|
||||
|
||||
private void OnCursorStyleChangeEvent(CursorStyleChangeEvent evt)
|
||||
{
|
||||
_currentStyle = evt.Style;
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
private void OnOverrideCursorStyleChangeEvent(OverrideCursorStyleChangeEvent evt)
|
||||
{
|
||||
if (evt.Style == CursorStyle.Default)
|
||||
{
|
||||
ClearOverrideStyle();
|
||||
return;
|
||||
}
|
||||
|
||||
_isStyleOverridden = true;
|
||||
_currentStyleOverride = evt.Style;
|
||||
ApplyCurrentEffectiveStyle();
|
||||
}
|
||||
|
||||
private void ApplyCursorModeVisuals()
|
||||
{
|
||||
Cursor.visible = !_useVirtualCursor;
|
||||
|
||||
if (_virtualCursorImage != null)
|
||||
_virtualCursorImage.enabled = _useVirtualCursor;
|
||||
}
|
||||
|
||||
private void ApplyCurrentEffectiveStyle()
|
||||
{
|
||||
var style = GetEffectiveStyle();
|
||||
|
||||
if (_useVirtualCursor)
|
||||
{
|
||||
ApplyVirtualCursorStyle(style);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplySystemCursorStyle(style);
|
||||
}
|
||||
}
|
||||
|
||||
private CursorStyle GetEffectiveStyle()
|
||||
{
|
||||
return _isStyleOverridden ? _currentStyleOverride : _currentStyle;
|
||||
}
|
||||
|
||||
private void ApplyVirtualCursorStyle(CursorStyle style)
|
||||
{
|
||||
if (_virtualCursorImage == null)
|
||||
return;
|
||||
|
||||
if (!_styleMap.TryGetValue(style, out var entry) || entry.Texture == null)
|
||||
{
|
||||
if (!_styleMap.TryGetValue(CursorStyle.Default, out entry) || entry.Texture == null)
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = new Rect(0, 0, entry.Texture.width, entry.Texture.height);
|
||||
var pivot = new Vector2(0.5f, 0.5f);
|
||||
_virtualCursorImage.sprite = Sprite.Create(entry.Texture, rect, pivot);
|
||||
}
|
||||
|
||||
private void ApplySystemCursorStyle(CursorStyle style)
|
||||
{
|
||||
if (!_enableCustomCursor)
|
||||
return;
|
||||
|
||||
if (!_styleMap.TryGetValue(style, out var entry) || entry.Texture == null)
|
||||
{
|
||||
if (_styleMap.TryGetValue(CursorStyle.Default, out var defaultEntry) && defaultEntry.Texture != null)
|
||||
{
|
||||
entry = defaultEntry;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
CacheApplied(null, Vector2.zero, CursorMode.Auto);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var hotspot = entry.Hotspot;
|
||||
|
||||
if (entry.Texture != null && entry.HotspotIsNormalized)
|
||||
{
|
||||
hotspot = new Vector2(
|
||||
hotspot.x * entry.Texture.width,
|
||||
hotspot.y * entry.Texture.height);
|
||||
}
|
||||
|
||||
if (IsSameAsCurrent(entry.Texture, hotspot, entry.Mode))
|
||||
return;
|
||||
|
||||
Cursor.SetCursor(entry.Texture, hotspot, entry.Mode);
|
||||
CacheApplied(entry.Texture, hotspot, entry.Mode);
|
||||
}
|
||||
|
||||
private bool IsSameAsCurrent(Texture2D texture, Vector2 hotspot, CursorMode mode)
|
||||
{
|
||||
return _currentTexture == texture &&
|
||||
_currentHotspot == hotspot &&
|
||||
_currentMode == mode;
|
||||
}
|
||||
|
||||
private void CacheApplied(Texture2D texture, Vector2 hotspot, CursorMode mode)
|
||||
{
|
||||
_currentTexture = texture;
|
||||
_currentHotspot = hotspot;
|
||||
_currentMode = mode;
|
||||
}
|
||||
|
||||
private void BuildStyleMap()
|
||||
{
|
||||
_styleMap.Clear();
|
||||
|
||||
if (_styles == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _styles.Count; i++)
|
||||
{
|
||||
var entry = _styles[i];
|
||||
_styleMap[entry.Style] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct CursorStyleEntry
|
||||
{
|
||||
public CursorStyle Style;
|
||||
public Texture2D Texture;
|
||||
public Vector2 Hotspot;
|
||||
public CursorMode Mode;
|
||||
public bool HotspotIsNormalized;
|
||||
public Vector2 TooltipOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 515a74faf67c4af9912b8b8eed4d8ad7
|
||||
timeCreated: 1769716152
|
||||
434
Assets/Scripts/Framework/Managers/UI/UIManager.cs
Normal file
434
Assets/Scripts/Framework/Managers/UI/UIManager.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Interaction;
|
||||
using BriarQueen.Framework.Managers.IO;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using BriarQueen.Framework.Managers.UI.Events;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UIManager:
|
||||
/// - Modal windows use the window stack
|
||||
/// - Non-modal UI (popups / fader) does not use the stack
|
||||
/// - HUD visibility is event-driven
|
||||
/// - Concrete UI implementations are hidden behind interfaces
|
||||
/// </summary>
|
||||
public class UIManager : IDisposable, IManager
|
||||
{
|
||||
private readonly EventCoordinator _eventCoordinator;
|
||||
private readonly InteractManager _interactManager;
|
||||
private readonly SaveManager _saveManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
private readonly Dictionary<WindowType, IUIWindow> _windows = new();
|
||||
private readonly Stack<IUIWindow> _windowStack = new();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
private IHud _hudContainer;
|
||||
private IPopup _infoPopup;
|
||||
private IPopup _tutorialPopup;
|
||||
private IScreenFader _screenFader;
|
||||
|
||||
[Inject]
|
||||
public UIManager(
|
||||
EventCoordinator eventCoordinator,
|
||||
InteractManager interactManager,
|
||||
SettingsService settingsService,
|
||||
SaveManager saveManager)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_interactManager = interactManager;
|
||||
_settingsService = settingsService;
|
||||
_saveManager = saveManager;
|
||||
}
|
||||
|
||||
private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null;
|
||||
|
||||
public bool IsAnyUIOpen => _windowStack.Count > 0;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
return;
|
||||
|
||||
_disposed = false;
|
||||
SubscribeToEvents();
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Initialized || _disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
UnsubscribeFromEvents();
|
||||
ResetUIStateHard();
|
||||
|
||||
Initialized = false;
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
_eventCoordinator.Subscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Subscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Subscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Subscribe<FadeEvent>(OnFadeEvent);
|
||||
_eventCoordinator.Subscribe<DisplayInteractEvent>(OnDisplayInteractText);
|
||||
_eventCoordinator.Subscribe<DisplayTutorialPopupEvent>(OnTutorialDisplayPopup);
|
||||
_eventCoordinator.Subscribe<CodexChangedEvent>(OnCodexChangedEvent);
|
||||
_eventCoordinator.Subscribe<UIToggleHudEvent>(OnHudToggleEvent);
|
||||
_eventCoordinator.Subscribe<ToolbeltChangedEvent>(OnToolbeltChangedEvent);
|
||||
}
|
||||
|
||||
private void UnsubscribeFromEvents()
|
||||
{
|
||||
_eventCoordinator.Unsubscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
||||
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
||||
_eventCoordinator.Unsubscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
||||
_eventCoordinator.Unsubscribe<FadeEvent>(OnFadeEvent);
|
||||
_eventCoordinator.Unsubscribe<DisplayInteractEvent>(OnDisplayInteractText);
|
||||
_eventCoordinator.Unsubscribe<DisplayTutorialPopupEvent>(OnTutorialDisplayPopup);
|
||||
_eventCoordinator.Unsubscribe<CodexChangedEvent>(OnCodexChangedEvent);
|
||||
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudToggleEvent);
|
||||
_eventCoordinator.Unsubscribe<ToolbeltChangedEvent>(OnToolbeltChangedEvent);
|
||||
}
|
||||
|
||||
public void RegisterWindow(IUIWindow window)
|
||||
{
|
||||
if (window == null)
|
||||
return;
|
||||
|
||||
_windows[window.WindowType] = window;
|
||||
window.Hide().Forget();
|
||||
}
|
||||
|
||||
public void RegisterHUD(IHud hudContainer)
|
||||
{
|
||||
_hudContainer = hudContainer;
|
||||
|
||||
if (_hudContainer != null)
|
||||
{
|
||||
_hudContainer.Hide().Forget();
|
||||
_interactManager.AddUIRaycaster(_hudContainer.Raycaster);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterInfoPopup(IPopup infoPopup)
|
||||
{
|
||||
_infoPopup = infoPopup;
|
||||
|
||||
if (_infoPopup != null)
|
||||
_infoPopup.Hide().Forget();
|
||||
}
|
||||
|
||||
public void RegisterTutorialPopup(IPopup tutorialPopup)
|
||||
{
|
||||
_tutorialPopup = tutorialPopup;
|
||||
|
||||
if (_tutorialPopup != null)
|
||||
_tutorialPopup.Hide().Forget();
|
||||
}
|
||||
|
||||
public void RegisterScreenFader(IScreenFader screenFader)
|
||||
{
|
||||
_screenFader = screenFader;
|
||||
}
|
||||
|
||||
private IUIWindow GetWindow(WindowType windowType)
|
||||
{
|
||||
return _windows.TryGetValue(windowType, out var window) ? window : null;
|
||||
}
|
||||
|
||||
private async UniTask ApplyHudVisibility(bool visible)
|
||||
{
|
||||
if (_disposed || _hudContainer == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (visible)
|
||||
await _hudContainer.Show();
|
||||
else
|
||||
await _hudContainer.Hide();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[UIManager] ApplyHudVisibility error: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPauseClickReceived(PauseButtonClickedEvent _)
|
||||
{
|
||||
if (_windowStack.Count > 0)
|
||||
{
|
||||
CloseTopWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenWindow(WindowType.PauseMenuWindow);
|
||||
}
|
||||
|
||||
private void ToggleSettingsWindow(UIToggleSettingsWindow eventData)
|
||||
{
|
||||
if (eventData.Show)
|
||||
OpenWindow(WindowType.SettingsWindow);
|
||||
else
|
||||
CloseWindow(WindowType.SettingsWindow);
|
||||
}
|
||||
|
||||
private void ToggleCodexWindow(ToggleCodexEvent eventData)
|
||||
{
|
||||
if (eventData.Shown)
|
||||
OpenWindow(WindowType.CodexWindow);
|
||||
else
|
||||
CloseWindow(WindowType.CodexWindow);
|
||||
}
|
||||
|
||||
private void OnCodexChangedEvent(CodexChangedEvent eventData)
|
||||
{
|
||||
if (_infoPopup == null)
|
||||
return;
|
||||
|
||||
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
||||
var codexText = GetCodexTextForEntry(eventData.EntryType);
|
||||
|
||||
_infoPopup.Play(codexText, duration).Forget();
|
||||
}
|
||||
|
||||
private void OnToolbeltChangedEvent(ToolbeltChangedEvent eventData)
|
||||
{
|
||||
if (_infoPopup == null)
|
||||
return;
|
||||
|
||||
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
||||
var toolText = GetToolbeltTextForEntry(eventData.ToolID, eventData.Lost);
|
||||
|
||||
_infoPopup.Play(toolText, duration).Forget();
|
||||
}
|
||||
|
||||
private string GetToolbeltTextForEntry(ToolID toolID, bool lost)
|
||||
{
|
||||
if (lost)
|
||||
return $"You lost the {toolID.ToString()}.";
|
||||
else
|
||||
return $"You gained the {toolID.ToString()}.";
|
||||
}
|
||||
|
||||
private string GetCodexTextForEntry(CodexType codexType)
|
||||
{
|
||||
return codexType switch
|
||||
{
|
||||
CodexType.BookEntry => "You've acquired a new book entry.",
|
||||
CodexType.PuzzleClue => "You've acquired a new puzzle clue.",
|
||||
CodexType.Photo => "You've acquired a new photo.",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private void OnTutorialDisplayPopup(DisplayTutorialPopupEvent eventData)
|
||||
{
|
||||
if (_tutorialPopup == null)
|
||||
return;
|
||||
|
||||
if (!_settingsService.AreTutorialsEnabled())
|
||||
return;
|
||||
|
||||
var duration = 3f;
|
||||
var tutorialText = TutorialPopupTexts.AllPopups[eventData.TutorialID];
|
||||
|
||||
_tutorialPopup.Play(tutorialText, duration).Forget();
|
||||
}
|
||||
|
||||
private void OnDisplayInteractText(DisplayInteractEvent eventData)
|
||||
{
|
||||
if (_hudContainer == null)
|
||||
return;
|
||||
|
||||
_hudContainer.DisplayInteractText(eventData.Message).Forget();
|
||||
}
|
||||
|
||||
private void OnFadeEvent(FadeEvent eventData)
|
||||
{
|
||||
if (_screenFader == null)
|
||||
return;
|
||||
|
||||
if (eventData.Hidden)
|
||||
_screenFader.FadeFromAsync(eventData.Duration).Forget();
|
||||
else
|
||||
_screenFader.FadeToAsync(eventData.Duration).Forget();
|
||||
}
|
||||
|
||||
private void OnHudToggleEvent(UIToggleHudEvent eventData)
|
||||
{
|
||||
ApplyHudVisibility(eventData.Show).Forget();
|
||||
}
|
||||
|
||||
public void OpenWindow(WindowType windowType)
|
||||
{
|
||||
OpenWindowInternal(windowType).Forget();
|
||||
}
|
||||
|
||||
private async UniTask OpenWindowInternal(WindowType windowType)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
var window = GetWindow(windowType);
|
||||
if (window == null)
|
||||
{
|
||||
Debug.LogError($"[UIManager] Window of type {windowType} not registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveWindow == window)
|
||||
return;
|
||||
|
||||
if (ActiveWindow != null)
|
||||
await ActiveWindow.Hide();
|
||||
|
||||
_windowStack.Push(window);
|
||||
await window.Show();
|
||||
|
||||
NotifyUIStackChanged();
|
||||
}
|
||||
|
||||
public void CloseWindow(WindowType windowType)
|
||||
{
|
||||
CloseWindowInternal(windowType).Forget();
|
||||
}
|
||||
|
||||
private async UniTask CloseWindowInternal(WindowType windowType)
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
return;
|
||||
|
||||
var target = GetWindow(windowType);
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
while (_windowStack.Count > 0)
|
||||
{
|
||||
var current = _windowStack.Pop();
|
||||
if (current != null)
|
||||
await current.Hide();
|
||||
|
||||
if (current == target)
|
||||
break;
|
||||
}
|
||||
|
||||
if (ActiveWindow != null)
|
||||
await ActiveWindow.Show();
|
||||
|
||||
NotifyUIStackChanged();
|
||||
}
|
||||
|
||||
public void CloseTopWindow()
|
||||
{
|
||||
CloseTopWindowInternal().Forget();
|
||||
}
|
||||
|
||||
private async UniTask CloseTopWindowInternal()
|
||||
{
|
||||
if (_disposed || _windowStack.Count == 0)
|
||||
return;
|
||||
|
||||
var top = _windowStack.Pop();
|
||||
|
||||
if (top != null)
|
||||
await top.Hide();
|
||||
|
||||
if (ActiveWindow != null)
|
||||
await ActiveWindow.Show();
|
||||
|
||||
NotifyUIStackChanged();
|
||||
}
|
||||
|
||||
public void ResetUIState()
|
||||
{
|
||||
ResetUIStateAsync().Forget();
|
||||
}
|
||||
|
||||
public async UniTask ResetUIStateAsync()
|
||||
{
|
||||
while (_windowStack.Count > 0)
|
||||
{
|
||||
var window = _windowStack.Pop();
|
||||
if (window != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await window.Hide();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_tutorialPopup != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _tutorialPopup.Hide();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (_infoPopup != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _infoPopup.Hide();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
NotifyUIStackChanged();
|
||||
}
|
||||
|
||||
private void ResetUIStateHard()
|
||||
{
|
||||
foreach (var kv in _windows)
|
||||
{
|
||||
if (kv.Value is Component component && component != null)
|
||||
component.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_tutorialPopup?.GameObject != null)
|
||||
_tutorialPopup.GameObject.SetActive(false);
|
||||
|
||||
if (_infoPopup?.GameObject != null)
|
||||
_infoPopup.GameObject.SetActive(false);
|
||||
|
||||
if (_hudContainer is Component hudComponent && hudComponent != null)
|
||||
hudComponent.gameObject.SetActive(false);
|
||||
|
||||
if (_screenFader is Component faderComponent && faderComponent != null)
|
||||
faderComponent.gameObject.SetActive(false);
|
||||
|
||||
_windowStack.Clear();
|
||||
}
|
||||
|
||||
private void NotifyUIStackChanged()
|
||||
{
|
||||
_eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Framework/Managers/UI/UIManager.cs.meta
Normal file
3
Assets/Scripts/Framework/Managers/UI/UIManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 877f1720bf604f91a1b277e25a03dcbe
|
||||
timeCreated: 1769706518
|
||||
Reference in New Issue
Block a user