using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyManager : MonoBehaviour { [Header("Список врагов")] public EnemyData[] enemies; [Header("UI контроллер карточки")] public EnemyCardController cardController; [Header("Settings")] public float dialogueHoldTime = 1f; private int currentIndex = 0; private int playerId; private Coroutine currentDialogueCoroutine; private bool isDialoguePlaying = false; // Отслеживание использованных реплик для каждого типа события private Dictionary> usedDialogueIndices = new Dictionary>(); private void Awake() { FetchPlayerId(); } private void FetchPlayerId() { if (playerId != 0) return; // уже установлен var player = gameObject.GetComponentInParent(includeInactive: true); if (player != null) { playerId = player.PlayerId; Debug.Log($"[EnemyManager {name}] FetchPlayerId — playerId = {playerId}"); } else Debug.LogWarning($"[{name}] Player не найден в родителях!"); } public void SetEnemy(int index) { if (enemies == null || index < 0 || index >= enemies.Length) { Debug.LogWarning($"[{name}] SetEnemy: invalid index {index}"); return; } if (!cardController.gameObject.activeSelf) cardController.gameObject.SetActive(true); currentIndex = index; cardController.SetEnemy(enemies[index]); isDialoguePlaying = false; // Сбрасываем историю использованных реплик при смене врага usedDialogueIndices.Clear(); } public void SetEnemyPlayer() { // Гарантируем что playerId установлен — Awake мог не сработать если объект был скрыт FetchPlayerId(); // Профили игроков - это последние 2 элемента в массиве enemies // playerId = 1 -> enemies[Length-2], playerId = 2 -> enemies[Length-1] if (enemies == null || enemies.Length < 2) { Debug.LogWarning($"[{name}] SetEnemyPlayer: недостаточно врагов в массиве для профилей игроков!"); cardController.gameObject.SetActive(false); return; } int playerProfileIndex = enemies.Length - (3 - playerId); Debug.Log($"[EnemyManager {name}] SetEnemyPlayer — playerId={playerId}, enemies.Length={enemies.Length}, playerProfileIndex={playerProfileIndex}"); if (playerProfileIndex < 0 || playerProfileIndex >= enemies.Length) { Debug.LogWarning($"[{name}] SetEnemyPlayer: некорректный индекс профиля игрока {playerProfileIndex}"); cardController.gameObject.SetActive(false); return; } currentIndex = playerProfileIndex; // Скрываем карточку (игрок не показывает UI) cardController.gameObject.SetActive(false); isDialoguePlaying = false; // Сбрасываем историю использованных реплик usedDialogueIndices.Clear(); } /// /// Показывает диалог с карточкой: открывает ? говорит ? закрывает /// Если карточка неактивна - просто играет звук /// public void ShowDialogueWithCard(DialogueLine line) { // Если карточка неактивна - просто играем звук if (cardController == null || !cardController.gameObject.activeInHierarchy) { PlayVoiceOnly(line.voiceClip); return; } if (currentDialogueCoroutine != null) StopCoroutine(currentDialogueCoroutine); currentDialogueCoroutine = StartCoroutine(DialogueWithCardSequence(line)); } /// /// Проигрывает только звук без карточки /// public void PlayVoiceOnly(AudioClip clip) { if (clip == null || SoundManager.Instance == null) return; SoundManager.Instance.PlayVoice(playerId, clip); // Если карточка скрыта (PvP) — isDialoguePlaying сбрасываем по длине клипа, // иначе он остался бы true навсегда и следующая реплика никогда не сыграла if (cardController == null || !cardController.gameObject.activeInHierarchy) { if (gameObject.activeInHierarchy) StartCoroutine(ResetDialogueAfter(clip.length)); else isDialoguePlaying = false; // объект неактивен — сбрасываем флаг сразу } } private IEnumerator ResetDialogueAfter(float duration) { yield return new WaitForSeconds(duration); isDialoguePlaying = false; } /// /// Показывает текст на уже открытой карточке (без анимации открытия/закрытия) /// public void ShowTextOnly(string text, string emotion = null) { if (!string.IsNullOrEmpty(emotion)) cardController.SetEmotion(emotion); cardController.TypeText(text); } private IEnumerator DialogueWithCardSequence(DialogueLine line) { isDialoguePlaying = true; // 1. Подготавливаем размер текста ДО открытия панели (чтобы не было "блика") yield return cardController.PrepareTextSizeAsync(line.text); // 2. Открываем карточку с эмоцией перехода bool cardOpened = false; cardController.ShowCard(line.animationName.ToStateName(), () => cardOpened = true); // Ждём, пока карточка откроется yield return new WaitUntil(() => cardOpened); // 3. Играем звук if (line.voiceClip != null && SoundManager.Instance != null) { SoundManager.Instance.PlayVoice(playerId, line.voiceClip); } // 4. Печатаем текст (размер уже подготовлен) bool textComplete = false; cardController.TypeText(line.text, () => textComplete = true, skipSizePreparation: true); // Ждём, пока текст напечатается yield return new WaitUntil(() => textComplete); // 4. Держим текст немного yield return new WaitForSeconds(dialogueHoldTime); // 5. Закрываем карточку bool cardClosed = false; cardController.HideCard(() => cardClosed = true); yield return new WaitUntil(() => cardClosed); isDialoguePlaying = false; } /// /// Вызывает диалог реакции на событие (с карточкой) /// Выбирает реплику которая ещё не использовалась, пока все не кончатся /// public void TriggerEventReaction(DialogueEventType eventType) { if (enemies == null || currentIndex < 0 || currentIndex >= enemies.Length) return; if(isDialoguePlaying) return; var enemy = enemies[currentIndex]; DialogueLine? line = GetNonRepeatingDialogue(enemy, eventType); if (line.HasValue) { ShowDialogueWithCard(line.Value); } } /// /// Получает случайную реплику которая ещё не использовалась для данного типа события /// private DialogueLine? GetNonRepeatingDialogue(EnemyData enemy, DialogueEventType eventType) { if (enemy == null) return null; // Получаем нужный массив реплик по типу события DialogueLine[] sourceArray = null; switch (eventType) { case DialogueEventType.PlayerHit: sourceArray = enemy.onPlayerHit; break; case DialogueEventType.PlayerMiss: sourceArray = enemy.onPlayerMiss; break; case DialogueEventType.BotHit: sourceArray = enemy.onBotHit; break; case DialogueEventType.BotMiss: sourceArray = enemy.onBotMiss; break; case DialogueEventType.PlayerShipDestroyed: sourceArray = enemy.onPlayerShipDestroyed; break; case DialogueEventType.BotShipDestroyed: sourceArray = enemy.onBotShipDestroyed; break; } if (sourceArray == null || sourceArray.Length == 0) return null; // Инициализируем список использованных реплик для этого типа события если нужно if (!usedDialogueIndices.ContainsKey(eventType)) { usedDialogueIndices[eventType] = new List(); } var usedList = usedDialogueIndices[eventType]; // Создаём список неиспользованных индексов var unusedIndices = new List(); for (int i = 0; i < sourceArray.Length; i++) { if (!usedList.Contains(i)) unusedIndices.Add(i); } // Если все реплики использованы - сбрасываем и начинаем заново if (unusedIndices.Count == 0) { usedList.Clear(); for (int i = 0; i < sourceArray.Length; i++) { unusedIndices.Add(i); } } // Выбираем случайный неиспользованный индекс int randomIndex = unusedIndices[Random.Range(0, unusedIndices.Count)]; usedList.Add(randomIndex); return sourceArray[randomIndex]; } /// /// Реакция на конец игры (только звук, без карточки) /// public void TriggerEndGameReaction(bool playerWon) { if (enemies == null || enemies.Length == 0) return; if (currentIndex < 0 || currentIndex >= enemies.Length) return; isDialoguePlaying = true; var enemy = enemies[currentIndex]; var clips = playerWon ? enemy.playerVictory : enemy.playerDefeat; if (clips == null || clips.Length == 0) return; // Останавливаем голосовой канал текущего игрока, затем проигрываем финальную фразу if (SoundManager.Instance != null) { SoundManager.Instance.StopVoice(playerId); } Debug.Log(clips[0].name); PlayVoiceOnly(clips[Random.Range(0, clips.Length)]); } public EnemyData GetCurrentEnemy() { if (enemies == null || currentIndex < 0 || currentIndex >= enemies.Length) return null; return enemies[currentIndex]; } }