using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using TMPro; using System.Text; [System.Serializable] public class WindowUI { public GameObject rootObject; public TextMeshProUGUI dialogueText; [Tooltip("Родительский объект портрета (Background). Его мы будем включать/выключать.")] public GameObject portraitContainer; [Tooltip("Объект самой иконки (Child), куда будет подставляться спрайт.")] public Image portraitImage; [Tooltip("Если кнопки выбора находятся внутри этого окна, укажите их тут (необязательно)")] public GameObject[] choiceButtons; } public class MANAGER_Dialogues : MonoBehaviour { public static MANAGER_Dialogues Instance { get; private set; } private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } public UnityEvent onSentenceStart; [Header("Windows Configuration")] public WindowUI topWindow; public WindowUI bottomWindow; [Header("UI Focus Settings")] public Transform uiRoot; public List objectsToIgnore = new List(); private List objectsDisabledByDialogue = new List(); [Header("Global Settings")] public AudioSource audioSource; public Camera mainCamera; [Range(0f, 1f)] public float screenThreshold = 0.5f; [Header("Emotional Pauses")] [Tooltip("Длительность паузы в миллисекундах")] public int emotionalPauseDurationMs = 500; [Tooltip("Символы, после которых будет пауза. Многоточие '...' будет считаться за одно срабатывание.")] public List emotionalPauseSymbols = new List { "?", "!", "...", ",", ":" }; [Header("Shared Choice Buttons")] public GameObject[] globalChoiceButtons; private WindowUI activeWindow; private Queue sentences; private Queue sentenceEvents; private DATA_Dialogue currentDialogue; private Sentence currentSentence; private bool isTyping; private bool skipTyping; private bool isWaitingForChoice; private bool canShowNextSentence = true; private readonly Dictionary _textCache = new Dictionary(); void Start() { sentences = new Queue(); sentenceEvents = new Queue(); topWindow.rootObject.SetActive(false); bottomWindow.rootObject.SetActive(false); if (mainCamera == null) mainCamera = Camera.main; if (!objectsToIgnore.Contains(topWindow.rootObject)) objectsToIgnore.Add(topWindow.rootObject); if (!objectsToIgnore.Contains(bottomWindow.rootObject)) objectsToIgnore.Add(bottomWindow.rootObject); } void Update() { bool inputPressed = Input.GetMouseButtonDown(0) || (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began); if (inputPressed && currentDialogue != null && !isWaitingForChoice) { if (isTyping) { if (currentSentence != null && currentSentence.isSkipable) { skipTyping = true; } } else if (canShowNextSentence) { DisplayNextSentence(); } } } public void StartDialogue(DATA_Dialogue dialogue, Transform npcTransform = null, UnityEvent[] events = null) { currentDialogue = dialogue; sentences.Clear(); sentenceEvents.Clear(); isWaitingForChoice = false; ToggleBackgroundUI(false); SelectActiveWindow(npcTransform); foreach (var sentence in dialogue.sentences) sentences.Enqueue(sentence); if (events != null) foreach (var ev in events) sentenceEvents.Enqueue(ev); DisplayNextSentence(); } private void ToggleBackgroundUI(bool show) { if (uiRoot == null) return; if (!show) { objectsDisabledByDialogue.Clear(); foreach (Transform child in uiRoot) { if (child.gameObject.activeSelf && !objectsToIgnore.Contains(child.gameObject)) { child.gameObject.SetActive(false); objectsDisabledByDialogue.Add(child.gameObject); } } } else { foreach (GameObject obj in objectsDisabledByDialogue) { if (obj != null) obj.SetActive(true); } objectsDisabledByDialogue.Clear(); } } private void SelectActiveWindow(Transform npcTransform) { topWindow.rootObject.SetActive(false); bottomWindow.rootObject.SetActive(false); if (npcTransform == null) { activeWindow = bottomWindow; } else { Vector3 viewportPos = mainCamera.WorldToViewportPoint(npcTransform.position); activeWindow = (viewportPos.y < screenThreshold) ? topWindow : bottomWindow; } activeWindow.rootObject.SetActive(true); } public void DisplayNextSentence() { if (sentences.Count == 0) { if (!isTyping && !isWaitingForChoice) EndDialogue(); return; } currentSentence = sentences.Dequeue(); if (MANAGER_EventsDialogue.Instance != null && !string.IsNullOrEmpty(currentSentence.eventID)) { MANAGER_EventsDialogue.Instance.Execute(currentSentence.eventID); } currentSentence.onSentenceStart?.Invoke(); UnityEvent currentEvent = (sentenceEvents.Count > 0) ? sentenceEvents.Dequeue() : null; UpdatePortrait(currentSentence); StopAllCoroutines(); StartCoroutine(TypeSentence(currentSentence, currentEvent)); } private void UpdatePortrait(Sentence sentence) { if (activeWindow.portraitContainer != null && activeWindow.portraitImage != null) { if (sentence.characterSprite != null) { activeWindow.portraitImage.sprite = sentence.characterSprite; activeWindow.portraitImage.gameObject.SetActive(true); activeWindow.portraitContainer.SetActive(true); } else { activeWindow.portraitContainer.SetActive(false); } } } IEnumerator TypeSentence(Sentence sentence, UnityEvent onSentenceEnd) { isTyping = true; skipTyping = false; canShowNextSentence = false; ClearChoices(); string processedText = ReplaceShortcodes(sentence.text); activeWindow.dialogueText.text = processedText; activeWindow.dialogueText.ForceMeshUpdate(); var textInfo = activeWindow.dialogueText.textInfo; int totalVisibleCharacters = textInfo.characterCount; activeWindow.dialogueText.maxVisibleCharacters = 0; WaitForSeconds textDelay = new WaitForSeconds(sentence.speed); WaitForSeconds emotionalPauseDelay = new WaitForSeconds(emotionalPauseDurationMs / 1000f); for (int i = 0; i <= totalVisibleCharacters; i++) { if (skipTyping) { activeWindow.dialogueText.maxVisibleCharacters = totalVisibleCharacters; break; } activeWindow.dialogueText.maxVisibleCharacters = i; if (i > 0) { char currentCharacter = textInfo.characterInfo[i - 1].character; bool isSpecialChar = char.IsWhiteSpace(currentCharacter) || currentCharacter == ',' || currentCharacter == ' '; if (!isSpecialChar) { if (sentence.voiceClip != null && audioSource != null) { audioSource.pitch = Random.Range(sentence.minPitch, sentence.maxPitch); audioSource.PlayOneShot(sentence.voiceClip); } } } if (i > 0 && i < totalVisibleCharacters && ShouldPause(i, textInfo, totalVisibleCharacters)) { yield return emotionalPauseDelay; } else { yield return textDelay; } } isTyping = false; skipTyping = false; yield return null; canShowNextSentence = true; onSentenceEnd?.Invoke(); sentence.onSentenceComplete?.Invoke(); if (sentence.choices != null && sentence.choices.Length > 0) { ShowChoices(sentence.choices); } } private bool ShouldPause(int currentIndex, TMP_TextInfo textInfo, int totalChars) { if (currentIndex <= 0 || currentIndex >= totalChars) return false; int maxMatchLength = 0; foreach (string symbol in emotionalPauseSymbols) { if (string.IsNullOrEmpty(symbol)) continue; int len = symbol.Length; if (currentIndex < len) continue; bool match = true; for (int j = 0; j < len; j++) { if (textInfo.characterInfo[currentIndex - len + j].character != symbol[j]) { match = false; break; } } if (match && len > maxMatchLength) { maxMatchLength = len; } } if (maxMatchLength > 0) { char nextChar = textInfo.characterInfo[currentIndex].character; if (char.IsPunctuation(nextChar)) { return false; } return true; } return false; } private void ShowChoices(Choice[] choices) { isWaitingForChoice = true; GameObject[] buttonsToUse = (activeWindow.choiceButtons != null && activeWindow.choiceButtons.Length > 0) ? activeWindow.choiceButtons : globalChoiceButtons; for (int i = 0; i < buttonsToUse.Length; i++) { if (i < choices.Length) { buttonsToUse[i].SetActive(true); buttonsToUse[i].GetComponentInChildren().text = choices[i].choiceText; Button btn = buttonsToUse[i].GetComponent