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 _characterDefinitions; private readonly Dictionary _loadedCharacters = new(); private readonly Dictionary> _loadingCharacters = new(); private readonly Dictionary> _loadedVideos = new(); private readonly Dictionary> _loadingVideos = new(); private readonly HashSet _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 LoadedCharacterIndices => _loadedCharacters.Keys; public IReadOnlyCollection 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 GetPrizes(int characterIndex) { List sets = PrizeSystemManager.PrizeContainer?.Sets; if (sets == null || characterIndex < 0 || characterIndex >= sets.Count) return Array.Empty(); return sets[characterIndex]?.Prizes ?? (IReadOnlyList)Array.Empty(); } public bool HasConfiguredPrize(int characterIndex) => GetPrizes(characterIndex).Any(prize => prize is not EmptyPrize); public bool TryFetchCurrentPrizes(out List 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(out T prize) where T : Prize { prize = null; if (CharacterIndex < 0 || CharacterIndex >= _prizeAvailable.Length || !_prizeAvailable[CharacterIndex]) return false; prize = GetPrizes(CharacterIndex).OfType().FirstOrDefault(); return prize != null; } public void RefreshPrizes() { foreach (KeyValuePair 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 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 characterTask = EnsureCharacterLoadedAsync(centerIndex); Task 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 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 EnsureCharacterLoadedAsync(int characterIndex) { if (_loadedCharacters.TryGetValue(characterIndex, out LoadedCharacter loadedCharacter)) return loadedCharacter.Character; if (_loadingCharacters.TryGetValue(characterIndex, out Task loadingTask)) return await loadingTask; Task newLoadingTask = LoadCharacterAsync(characterIndex); _loadingCharacters[characterIndex] = newLoadingTask; try { return await newLoadingTask; } finally { _loadingCharacters.Remove(characterIndex); } } private async Task LoadCharacterAsync(int characterIndex) { CharacterDefinition definition = _characterDefinitions[characterIndex]; AsyncOperationHandle 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(); 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 EnsureVideoLoadedAsync(int characterIndex) { if (_loadedVideos.TryGetValue(characterIndex, out AsyncOperationHandle loadedHandle)) return loadedHandle.Result; if (_loadingVideos.TryGetValue(characterIndex, out Task loadingTask)) return await loadingTask; Task newLoadingTask = LoadVideoAsync(characterIndex); _loadingVideos[characterIndex] = newLoadingTask; try { return await newLoadingTask; } finally { _loadingVideos.Remove(characterIndex); } } private async Task LoadVideoAsync(int characterIndex) { CharacterDefinition definition = _characterDefinitions[characterIndex]; AsyncOperationHandle handle = Addressables.LoadAssetAsync( 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 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 handle)) throw new InvalidOperationException($"Видеофон с индексом {characterIndex} не загружен."); return handle.Result; } private void SetCharacterPrizes(int characterIndex, Character character) { List 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 handle) { Character = character; Handle = handle; } public Character Character { get; } public AsyncOperationHandle Handle { get; } } } }