using System.Collections; using UnityEngine; using UnityEngine.UI; using TMPro; public class EnemyCardController : MonoBehaviour { [Header("UI Elements")] public Image avatarImage; public Animator avatarAnimator; public TMP_Text nicknameText; public TMP_Text dialogueText; public RectTransform dialoguePanel; [Header("Settings")] public float typingSpeed = 0.05f; public float panelExpandWidth = 300f; public float panelExpandSpeed = 5f; [Header("Animation names")] public string idleAnimationName = "Idle_bot_1"; public string transitionIn = "forvard_bot"; public string transitionOut = "back_bot"; private Coroutine panelAnimCoroutine; private Coroutine textTypingCoroutine; private Vector2 dialogueOriginalPos; private WaitForSeconds cachedTypingWait; private System.Collections.Generic.HashSet availableStates = new System.Collections.Generic.HashSet(); private void Awake() { if (dialoguePanel != null) dialogueOriginalPos = dialoguePanel.anchoredPosition; cachedTypingWait = new WaitForSeconds(typingSpeed); ValidateReferences(); } private void OnValidate() { ValidateReferences(); } private void ValidateReferences() { if (avatarImage == null) Debug.LogWarning($"[{name}] avatarImage не назначен!", this); if (nicknameText == null) Debug.LogWarning($"[{name}] nicknameText не назначен!", this); if (dialogueText == null) Debug.LogWarning($"[{name}] dialogueText не назначен!", this); if (dialoguePanel == null) Debug.LogWarning($"[{name}] dialoguePanel не назначен!", this); } private void OnDisable() { StopAllAnimations(); ResetUIState(); } public void SetEnemy(EnemyData enemy) { if (enemy == null) { Debug.LogWarning($"[{name}] SetEnemy: enemy is null!"); return; } if (nicknameText != null) nicknameText.text = enemy.nickname; SetupAnimatorOrSprite(enemy); ResetUIState(); // Устанавливаем порядок панели (только когда объект активен) if (dialoguePanel != null && gameObject.activeInHierarchy) dialoguePanel.SetSiblingIndex(0); } private void SetupAnimatorOrSprite(EnemyData enemy) { availableStates.Clear(); if (enemy.animatorOverride != null && avatarAnimator != null) { avatarAnimator.runtimeAnimatorController = enemy.animatorOverride; avatarAnimator.enabled = true; // Кэшируем доступные клипы один раз var overrides = new System.Collections.Generic.List>(); enemy.animatorOverride.GetOverrides(overrides); foreach (var pair in overrides) { if (pair.Value != null) // Есть переопределённый клип availableStates.Add(pair.Key.name); } SetEmotion(idleAnimationName); } else { if (avatarAnimator != null) avatarAnimator.enabled = false; if (enemy.characterSprite != null && avatarImage != null) avatarImage.sprite = enemy.characterSprite; } } private void ResetUIState() { if (dialogueText != null) dialogueText.text = ""; if (dialoguePanel != null) { dialoguePanel.anchoredPosition = dialogueOriginalPos; } } private void StopAllAnimations() { if (panelAnimCoroutine != null) { StopCoroutine(panelAnimCoroutine); panelAnimCoroutine = null; } if (textTypingCoroutine != null) { StopCoroutine(textTypingCoroutine); textTypingCoroutine = null; } } /// /// Открывает карточку с анимацией, вызывает callback когда открылась /// public void ShowCard(string emotionState, System.Action onShown = null) { if (panelAnimCoroutine != null) StopCoroutine(panelAnimCoroutine); panelAnimCoroutine = StartCoroutine(ShowCardCoroutine(emotionState, onShown)); } /// /// Закрывает карточку с анимацией, вызывает callback когда закрылась /// public void HideCard(System.Action onHidden = null) { if (panelAnimCoroutine != null) StopCoroutine(panelAnimCoroutine); panelAnimCoroutine = StartCoroutine(HideCardCoroutine(onHidden)); } /// /// Печатает текст посимвольно, вызывает callback по окончании /// public void TypeText(string text, System.Action onComplete = null, bool skipSizePreparation = false) { if (textTypingCoroutine != null) StopCoroutine(textTypingCoroutine); textTypingCoroutine = StartCoroutine(TypeTextCoroutine(text, onComplete, skipSizePreparation)); } /// /// Мгновенно устанавливает текст без анимации печати /// public void SetTextImmediate(string text) { if (textTypingCoroutine != null) { StopCoroutine(textTypingCoroutine); textTypingCoroutine = null; } if (dialogueText != null) dialogueText.text = text; } /// /// Подготавливает размер текста заранее (чтобы избежать "блика" при открытии панели) /// public IEnumerator PrepareTextSizeAsync(string text) { yield return PrepareTextSize(text); } private IEnumerator ShowCardCoroutine(string emotionState, System.Action onShown) { if (dialoguePanel == null) yield break; SetEmotion(transitionIn); Vector2 hiddenPos = dialogueOriginalPos; Vector2 shownPos = hiddenPos + new Vector2(panelExpandWidth, 0); float t = 0f; while (t < 1f) { t += Time.deltaTime * panelExpandSpeed; dialoguePanel.anchoredPosition = Vector2.Lerp(hiddenPos, shownPos, t); yield return null; } dialoguePanel.anchoredPosition = shownPos; SetEmotion(emotionState); onShown?.Invoke(); } private IEnumerator HideCardCoroutine(System.Action onHidden) { if (dialoguePanel == null) yield break; SetEmotion(transitionOut); if (dialogueText != null) dialogueText.text = ""; Vector2 hiddenPos = dialogueOriginalPos; Vector2 shownPos = hiddenPos + new Vector2(panelExpandWidth, 0); float t = 0f; while (t < 1f) { t += Time.deltaTime * panelExpandSpeed; dialoguePanel.anchoredPosition = Vector2.Lerp(shownPos, hiddenPos, t); yield return null; } dialoguePanel.anchoredPosition = hiddenPos; SetEmotion(idleAnimationName); onHidden?.Invoke(); } private IEnumerator TypeTextCoroutine(string text, System.Action onComplete, bool skipSizePreparation) { if (dialogueText == null) yield break; // Подготовка размера шрифта (пропускаем если уже сделано) if (!skipSizePreparation) { yield return PrepareTextSize(text); } // Печать по символам через maxVisibleCharacters (оптимизация) dialogueText.text = text; dialogueText.ForceMeshUpdate(); int totalChars = dialogueText.textInfo.characterCount; dialogueText.maxVisibleCharacters = 0; for (int i = 0; i <= totalChars; i++) { dialogueText.maxVisibleCharacters = i; yield return cachedTypingWait; } onComplete?.Invoke(); } private IEnumerator PrepareTextSize(string text) { if (dialogueText == null) yield break; dialogueText.enableWordWrapping = true; dialogueText.overflowMode = TextOverflowModes.Overflow; dialogueText.text = text; dialogueText.enableAutoSizing = true; dialogueText.fontSizeMin = 5; yield return null; // Ждём кадр для расчёта float finalFontSize = dialogueText.fontSize; dialogueText.enableAutoSizing = false; dialogueText.fontSize = finalFontSize; dialogueText.text = ""; } public void SetEmotion(string stateName) { if (avatarAnimator == null || !avatarAnimator.enabled || string.IsNullOrEmpty(stateName)) return; if (availableStates.Contains(stateName)) avatarAnimator.Play(stateName); } }