579 lines
20 KiB
C#
579 lines
20 KiB
C#
using DG.Tweening;
|
||
using PrizeSystem;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.AddressableAssets;
|
||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||
using UnityEngine.Serialization;
|
||
using UnityEngine.Video;
|
||
|
||
namespace CharacterSystem
|
||
{
|
||
public class CharactersController : MonoBehaviour
|
||
{
|
||
public static CharactersController I;
|
||
|
||
[SerializeField, FormerlySerializedAs("characterPrefabs")]
|
||
private List<CharacterDefinition> _characterDefinitions;
|
||
|
||
private readonly Dictionary<int, LoadedCharacter> _loadedCharacters = new();
|
||
private readonly Dictionary<int, Task<Character>> _loadingCharacters = new();
|
||
private readonly Dictionary<int, AsyncOperationHandle<VideoClip>> _loadedVideos = new();
|
||
private readonly Dictionary<int, Task<VideoClip>> _loadingVideos = new();
|
||
private readonly HashSet<int> _desiredIndices = new();
|
||
private bool[] _prizeAvailable;
|
||
private int _loadGeneration;
|
||
|
||
public static int CharacterIndex { get; private set; } = -1;
|
||
public static int CharactersAmount => I?._characterDefinitions?.Count ?? 0;
|
||
public static Character CurrentCharacter => I.GetLoadedCharacter(CharacterIndex);
|
||
public static bool HasActiveCharacter
|
||
=> I != null && CharacterIndex >= 0 && I._loadedCharacters.ContainsKey(CharacterIndex);
|
||
public IReadOnlyCollection<int> LoadedCharacterIndices => _loadedCharacters.Keys;
|
||
public IReadOnlyCollection<int> LoadedVideoIndices => _loadedVideos.Keys;
|
||
|
||
private void OnDestroy()
|
||
{
|
||
SettingsManager.OnSettingsChanged -= PullDifficultySettings;
|
||
_loadGeneration++;
|
||
ReleaseAllCharacters();
|
||
ReleaseAllVideos(true);
|
||
|
||
if (I == this)
|
||
I = null;
|
||
}
|
||
|
||
public async Task InitializeAsync()
|
||
{
|
||
I = this;
|
||
|
||
if (_characterDefinitions == null || _characterDefinitions.Count == 0)
|
||
throw new InvalidOperationException("Не заданы определения ботов в контроллере.");
|
||
|
||
ValidateDefinitions();
|
||
_prizeAvailable = new bool[_characterDefinitions.Count];
|
||
SetAllCharactersPrizeAvailable();
|
||
|
||
SettingsManager.OnSettingsChanged -= PullDifficultySettings;
|
||
SettingsManager.OnSettingsChanged += PullDifficultySettings;
|
||
PullDifficultySettings();
|
||
|
||
await PrepareWindowAsync(-1);
|
||
}
|
||
|
||
public void SetAllCharactersPrizeAvailable()
|
||
{
|
||
if (_prizeAvailable == null)
|
||
return;
|
||
|
||
for (int index = 0; index < _prizeAvailable.Length; index++)
|
||
_prizeAvailable[index] = true;
|
||
|
||
foreach (LoadedCharacter loadedCharacter in _loadedCharacters.Values)
|
||
loadedCharacter.Character.prizeModule.ResetModule();
|
||
}
|
||
|
||
public static void ResetAllCharactersDifficulty()
|
||
{
|
||
if (I == null)
|
||
return;
|
||
|
||
foreach (CharacterDefinition definition in I._characterDefinitions)
|
||
definition.DifficultyModule?.ResetPresetIndex();
|
||
}
|
||
|
||
public static void ResetSystem()
|
||
{
|
||
if (I == null)
|
||
return;
|
||
|
||
ResetAllCharactersDifficulty();
|
||
CharacterIndex = -1;
|
||
}
|
||
|
||
public void HideCharacter()
|
||
{
|
||
if (!HasActiveCharacter)
|
||
return;
|
||
|
||
CurrentCharacter.visualModule.GetHideSequence().PlayForward();
|
||
}
|
||
|
||
public void ReturnToMenu()
|
||
{
|
||
int oldIndex = CharacterIndex;
|
||
CharacterIndex = -1;
|
||
ResetAllCharactersDifficulty();
|
||
|
||
if (!_loadedCharacters.TryGetValue(oldIndex, out LoadedCharacter loadedCharacter))
|
||
{
|
||
PreloadMenuCharacter();
|
||
return;
|
||
}
|
||
|
||
Sequence sequence = loadedCharacter.Character.visualModule.GetHideSequence();
|
||
sequence.AppendCallback(PreloadMenuCharacter);
|
||
sequence.PlayForward();
|
||
}
|
||
|
||
public void StartSession(Action onComplete = null)
|
||
{
|
||
ResetSystem();
|
||
SetAllCharactersPrizeAvailable();
|
||
ShowCharacter(0, onComplete);
|
||
}
|
||
|
||
public void ApplyFirstCharacterColorsToBoard()
|
||
{
|
||
if (!_loadedCharacters.TryGetValue(0, out LoadedCharacter loadedCharacter))
|
||
{
|
||
Debug.LogError("Кьюб не подготовлен к запуску PvE.", this);
|
||
return;
|
||
}
|
||
|
||
VisualModule visualModule = loadedCharacter.Character.visualModule;
|
||
Board.Instance.SetColorsImmediately(
|
||
visualModule.InnerColor,
|
||
visualModule.OutlineColor,
|
||
visualModule.CellsColor);
|
||
}
|
||
|
||
public CharacterDefinition[] GetDefinitionsArray()
|
||
=> _characterDefinitions.ToArray();
|
||
|
||
public CharacterDefinition GetDefinition(int characterIndex)
|
||
{
|
||
if (characterIndex < 0 || characterIndex >= _characterDefinitions.Count)
|
||
throw new ArgumentOutOfRangeException(nameof(characterIndex));
|
||
|
||
return _characterDefinitions[characterIndex];
|
||
}
|
||
|
||
public IReadOnlyList<Prize> GetPrizes(int characterIndex)
|
||
{
|
||
List<PrizeSet> sets = PrizeSystemManager.PrizeContainer?.Sets;
|
||
|
||
if (sets == null || characterIndex < 0 || characterIndex >= sets.Count)
|
||
return Array.Empty<Prize>();
|
||
|
||
return sets[characterIndex]?.Prizes ?? (IReadOnlyList<Prize>)Array.Empty<Prize>();
|
||
}
|
||
|
||
public bool HasConfiguredPrize(int characterIndex)
|
||
=> GetPrizes(characterIndex).Any(prize => prize is not EmptyPrize);
|
||
|
||
public bool TryFetchCurrentPrizes(out List<Prize> prizes)
|
||
{
|
||
prizes = null;
|
||
|
||
if (CharacterIndex < 0 || CharacterIndex >= _prizeAvailable.Length || !_prizeAvailable[CharacterIndex])
|
||
return false;
|
||
|
||
_prizeAvailable[CharacterIndex] = false;
|
||
prizes = GetPrizes(CharacterIndex).ToList();
|
||
|
||
if (_loadedCharacters.TryGetValue(CharacterIndex, out LoadedCharacter loadedCharacter))
|
||
loadedCharacter.Character.prizeModule.TryFetchPrize(out _);
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool TryGetCurrentPrize<T>(out T prize) where T : Prize
|
||
{
|
||
prize = null;
|
||
|
||
if (CharacterIndex < 0 || CharacterIndex >= _prizeAvailable.Length || !_prizeAvailable[CharacterIndex])
|
||
return false;
|
||
|
||
prize = GetPrizes(CharacterIndex).OfType<T>().FirstOrDefault();
|
||
return prize != null;
|
||
}
|
||
|
||
public void RefreshPrizes()
|
||
{
|
||
foreach (KeyValuePair<int, LoadedCharacter> entry in _loadedCharacters)
|
||
SetCharacterPrizes(entry.Key, entry.Value.Character);
|
||
}
|
||
|
||
public async void ShowCharacter(int newIndex, Action onComplete = null)
|
||
{
|
||
if (newIndex < 0 || newIndex >= _characterDefinitions.Count)
|
||
throw new ArgumentOutOfRangeException(nameof(newIndex));
|
||
|
||
int oldIndex = CharacterIndex;
|
||
|
||
try
|
||
{
|
||
await PrepareTargetAsync(newIndex, oldIndex);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception, this);
|
||
return;
|
||
}
|
||
|
||
if (!_desiredIndices.Contains(newIndex) || !_loadedCharacters.ContainsKey(newIndex))
|
||
return;
|
||
|
||
CharacterIndex = newIndex;
|
||
|
||
if (newIndex == oldIndex)
|
||
{
|
||
onComplete?.Invoke();
|
||
return;
|
||
}
|
||
|
||
VideoUtility.CrossFadeToCharacter(newIndex, GetLoadedVideo(newIndex));
|
||
Character newCharacter = GetLoadedCharacter(newIndex);
|
||
Sequence sequence = DOTween.Sequence();
|
||
|
||
if (newIndex <= 4)
|
||
LightSystem.SpecialEffect(43 + newIndex, 2);
|
||
|
||
if (_loadedCharacters.TryGetValue(oldIndex, out LoadedCharacter oldCharacter))
|
||
{
|
||
sequence.Append(oldCharacter.Character.visualModule.GetHideSequence());
|
||
sequence.AppendInterval(0.1f);
|
||
}
|
||
|
||
sequence.Append(newCharacter.visualModule.GetShowSequence());
|
||
sequence.AppendCallback(() =>
|
||
{
|
||
SetDesiredWindow(newIndex);
|
||
ReleaseUndesiredCharacters();
|
||
ReleaseUndesiredVideos();
|
||
|
||
if (oldIndex >= 0 && newIndex < oldIndex)
|
||
EventBus.TriggerEvent(GameEvent.Return);
|
||
else
|
||
EventBus.TriggerEvent(GameEvent.Appear);
|
||
|
||
onComplete?.Invoke();
|
||
});
|
||
sequence.PlayForward();
|
||
}
|
||
|
||
public void SuspendForPvp()
|
||
{
|
||
CharacterIndex = -1;
|
||
_loadGeneration++;
|
||
_desiredIndices.Clear();
|
||
ReleaseUndesiredCharacters();
|
||
ReleaseUndesiredVideos();
|
||
}
|
||
|
||
public async void PreloadMenuCharacter()
|
||
{
|
||
try
|
||
{
|
||
await PrepareWindowAsync(-1);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception, this);
|
||
}
|
||
}
|
||
|
||
private async Task PrepareWindowAsync(int centerIndex, int additionalIndex = -1)
|
||
{
|
||
int generation = ++_loadGeneration;
|
||
SetDesiredWindow(centerIndex, additionalIndex);
|
||
int[] indicesToLoad = _desiredIndices.ToArray();
|
||
List<Task> loadingTasks = new();
|
||
|
||
foreach (int characterIndex in indicesToLoad)
|
||
{
|
||
loadingTasks.Add(EnsureCharacterLoadedAsync(characterIndex));
|
||
loadingTasks.Add(EnsureVideoLoadedAsync(characterIndex));
|
||
}
|
||
|
||
await Task.WhenAll(loadingTasks);
|
||
|
||
if (generation == _loadGeneration)
|
||
{
|
||
ReleaseUndesiredCharacters();
|
||
ReleaseUndesiredVideos();
|
||
}
|
||
}
|
||
|
||
private async Task PrepareTargetAsync(int centerIndex, int additionalIndex)
|
||
{
|
||
int generation = ++_loadGeneration;
|
||
SetDesiredWindow(centerIndex, additionalIndex);
|
||
|
||
Task<Character> characterTask = EnsureCharacterLoadedAsync(centerIndex);
|
||
Task<VideoClip> videoTask = EnsureVideoLoadedAsync(centerIndex);
|
||
_ = PreloadWindowRemainderAsync(centerIndex, generation);
|
||
|
||
await Task.WhenAll(characterTask, videoTask);
|
||
}
|
||
|
||
private async Task PreloadWindowRemainderAsync(int centerIndex, int generation)
|
||
{
|
||
int[] indicesToLoad = _desiredIndices
|
||
.Where(index => index != centerIndex)
|
||
.ToArray();
|
||
List<Task> loadingTasks = new();
|
||
|
||
foreach (int characterIndex in indicesToLoad)
|
||
{
|
||
loadingTasks.Add(EnsureCharacterLoadedAsync(characterIndex));
|
||
loadingTasks.Add(EnsureVideoLoadedAsync(characterIndex));
|
||
}
|
||
|
||
try
|
||
{
|
||
await Task.WhenAll(loadingTasks);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception, this);
|
||
}
|
||
|
||
if (generation == _loadGeneration)
|
||
{
|
||
ReleaseUndesiredCharacters();
|
||
ReleaseUndesiredVideos();
|
||
}
|
||
}
|
||
|
||
private async Task<Character> EnsureCharacterLoadedAsync(int characterIndex)
|
||
{
|
||
if (_loadedCharacters.TryGetValue(characterIndex, out LoadedCharacter loadedCharacter))
|
||
return loadedCharacter.Character;
|
||
|
||
if (_loadingCharacters.TryGetValue(characterIndex, out Task<Character> loadingTask))
|
||
return await loadingTask;
|
||
|
||
Task<Character> newLoadingTask = LoadCharacterAsync(characterIndex);
|
||
_loadingCharacters[characterIndex] = newLoadingTask;
|
||
|
||
try
|
||
{
|
||
return await newLoadingTask;
|
||
}
|
||
finally
|
||
{
|
||
_loadingCharacters.Remove(characterIndex);
|
||
}
|
||
}
|
||
|
||
private async Task<Character> LoadCharacterAsync(int characterIndex)
|
||
{
|
||
CharacterDefinition definition = _characterDefinitions[characterIndex];
|
||
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(definition.Prefab.RuntimeKey);
|
||
await handle.Task;
|
||
|
||
if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
|
||
{
|
||
if (handle.IsValid())
|
||
Addressables.Release(handle);
|
||
|
||
throw new InvalidOperationException($"Не удалось загрузить персонажа {definition.CharacterName}.");
|
||
}
|
||
|
||
Character character = handle.Result.GetComponent<Character>();
|
||
if (character == null)
|
||
{
|
||
Addressables.ReleaseInstance(handle);
|
||
throw new InvalidOperationException($"У prefab персонажа {definition.CharacterName} отсутствует Character.");
|
||
}
|
||
|
||
character.difficultyModule = definition.DifficultyModule;
|
||
character.Initialize(characterIndex);
|
||
SetCharacterPrizes(characterIndex, character);
|
||
_loadedCharacters[characterIndex] = new LoadedCharacter(character, handle);
|
||
|
||
if (!_desiredIndices.Contains(characterIndex))
|
||
ReleaseCharacter(characterIndex);
|
||
|
||
return character;
|
||
}
|
||
|
||
private async Task<VideoClip> EnsureVideoLoadedAsync(int characterIndex)
|
||
{
|
||
if (_loadedVideos.TryGetValue(characterIndex, out AsyncOperationHandle<VideoClip> loadedHandle))
|
||
return loadedHandle.Result;
|
||
|
||
if (_loadingVideos.TryGetValue(characterIndex, out Task<VideoClip> loadingTask))
|
||
return await loadingTask;
|
||
|
||
Task<VideoClip> newLoadingTask = LoadVideoAsync(characterIndex);
|
||
_loadingVideos[characterIndex] = newLoadingTask;
|
||
|
||
try
|
||
{
|
||
return await newLoadingTask;
|
||
}
|
||
finally
|
||
{
|
||
_loadingVideos.Remove(characterIndex);
|
||
}
|
||
}
|
||
|
||
private async Task<VideoClip> LoadVideoAsync(int characterIndex)
|
||
{
|
||
CharacterDefinition definition = _characterDefinitions[characterIndex];
|
||
AsyncOperationHandle<VideoClip> handle = Addressables.LoadAssetAsync<VideoClip>(
|
||
definition.BackgroundVideo.RuntimeKey);
|
||
await handle.Task;
|
||
|
||
if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
|
||
{
|
||
if (handle.IsValid())
|
||
Addressables.Release(handle);
|
||
|
||
throw new InvalidOperationException($"Не удалось загрузить видеофон {definition.CharacterName}.");
|
||
}
|
||
|
||
_loadedVideos[characterIndex] = handle;
|
||
|
||
if (!_desiredIndices.Contains(characterIndex))
|
||
ReleaseVideo(characterIndex);
|
||
|
||
return handle.Result;
|
||
}
|
||
|
||
private void SetDesiredWindow(int centerIndex, int additionalIndex = -1)
|
||
{
|
||
_desiredIndices.Clear();
|
||
|
||
if (centerIndex < 0)
|
||
{
|
||
_desiredIndices.Add(0);
|
||
}
|
||
else
|
||
{
|
||
int firstIndex = Mathf.Max(0, centerIndex - 1);
|
||
int lastIndex = Mathf.Min(_characterDefinitions.Count - 1, centerIndex + 1);
|
||
|
||
for (int index = firstIndex; index <= lastIndex; index++)
|
||
_desiredIndices.Add(index);
|
||
}
|
||
|
||
if (additionalIndex >= 0 && additionalIndex < _characterDefinitions.Count)
|
||
_desiredIndices.Add(additionalIndex);
|
||
}
|
||
|
||
private void ReleaseUndesiredCharacters()
|
||
{
|
||
int[] indicesToRelease = _loadedCharacters.Keys
|
||
.Where(index => !_desiredIndices.Contains(index))
|
||
.ToArray();
|
||
|
||
foreach (int characterIndex in indicesToRelease)
|
||
ReleaseCharacter(characterIndex);
|
||
}
|
||
|
||
private void ReleaseUndesiredVideos()
|
||
{
|
||
int[] indicesToRelease = _loadedVideos.Keys
|
||
.Where(index => !_desiredIndices.Contains(index))
|
||
.ToArray();
|
||
|
||
foreach (int characterIndex in indicesToRelease)
|
||
ReleaseVideo(characterIndex);
|
||
}
|
||
|
||
private void ReleaseAllCharacters()
|
||
{
|
||
foreach (int characterIndex in _loadedCharacters.Keys.ToArray())
|
||
ReleaseCharacter(characterIndex);
|
||
}
|
||
|
||
private void ReleaseAllVideos(bool immediately)
|
||
{
|
||
foreach (int characterIndex in _loadedVideos.Keys.ToArray())
|
||
ReleaseVideo(characterIndex, immediately);
|
||
}
|
||
|
||
private void ReleaseCharacter(int characterIndex)
|
||
{
|
||
if (!_loadedCharacters.Remove(characterIndex, out LoadedCharacter loadedCharacter))
|
||
return;
|
||
|
||
if (loadedCharacter.Handle.IsValid())
|
||
Addressables.ReleaseInstance(loadedCharacter.Handle);
|
||
}
|
||
|
||
private void ReleaseVideo(int characterIndex, bool immediately = false)
|
||
{
|
||
if (!_loadedVideos.Remove(characterIndex, out AsyncOperationHandle<VideoClip> handle) ||
|
||
!handle.IsValid())
|
||
return;
|
||
|
||
Action release = () =>
|
||
{
|
||
if (handle.IsValid())
|
||
Addressables.Release(handle);
|
||
};
|
||
|
||
if (immediately)
|
||
release();
|
||
else
|
||
VideoUtility.ReleaseBackgroundWhenUnused(handle.Result, release);
|
||
}
|
||
|
||
private Character GetLoadedCharacter(int characterIndex)
|
||
{
|
||
if (!_loadedCharacters.TryGetValue(characterIndex, out LoadedCharacter loadedCharacter))
|
||
throw new InvalidOperationException($"Персонаж с индексом {characterIndex} не загружен.");
|
||
|
||
return loadedCharacter.Character;
|
||
}
|
||
|
||
private VideoClip GetLoadedVideo(int characterIndex)
|
||
{
|
||
if (!_loadedVideos.TryGetValue(characterIndex, out AsyncOperationHandle<VideoClip> handle))
|
||
throw new InvalidOperationException($"Видеофон с индексом {characterIndex} не загружен.");
|
||
|
||
return handle.Result;
|
||
}
|
||
|
||
private void SetCharacterPrizes(int characterIndex, Character character)
|
||
{
|
||
List<PrizeSet> sets = PrizeSystemManager.PrizeContainer?.Sets;
|
||
if (sets == null || characterIndex < 0 || characterIndex >= sets.Count || sets[characterIndex] == null)
|
||
return;
|
||
|
||
character.prizeModule.SetPrizes(sets[characterIndex]);
|
||
|
||
if (_prizeAvailable != null && _prizeAvailable[characterIndex])
|
||
character.prizeModule.ResetModule();
|
||
}
|
||
|
||
private void PullDifficultySettings()
|
||
{
|
||
foreach (CharacterDefinition definition in _characterDefinitions)
|
||
definition.DifficultyModule?.PullSettings();
|
||
}
|
||
|
||
private void ValidateDefinitions()
|
||
{
|
||
for (int index = 0; index < _characterDefinitions.Count; index++)
|
||
{
|
||
CharacterDefinition definition = _characterDefinitions[index];
|
||
|
||
if (definition == null || definition.CharacterIndex != index || definition.DifficultyModule == null ||
|
||
!definition.Prefab.RuntimeKeyIsValid() || !definition.BackgroundVideo.RuntimeKeyIsValid())
|
||
throw new InvalidOperationException($"Некорректное определение персонажа с индексом {index}.");
|
||
}
|
||
}
|
||
|
||
private sealed class LoadedCharacter
|
||
{
|
||
public LoadedCharacter(Character character, AsyncOperationHandle<GameObject> handle)
|
||
{
|
||
Character = character;
|
||
Handle = handle;
|
||
}
|
||
|
||
public Character Character { get; }
|
||
public AsyncOperationHandle<GameObject> Handle { get; }
|
||
}
|
||
}
|
||
}
|