PJ/Assets/scripts/dotfs_scripts/MANAGER_Dialogues.cs

408 lines
No EOL
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<GameObject> objectsToIgnore = new List<GameObject>();
private List<GameObject> objectsDisabledByDialogue = new List<GameObject>();
[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<string> emotionalPauseSymbols = new List<string> { "?", "!", "...", ",", ":" };
[Header("Shared Choice Buttons")]
public GameObject[] globalChoiceButtons;
private WindowUI activeWindow;
private Queue<Sentence> sentences;
private Queue<UnityEvent> sentenceEvents;
private DATA_Dialogue currentDialogue;
private Sentence currentSentence;
private bool isTyping;
private bool skipTyping;
private bool isWaitingForChoice;
private bool canShowNextSentence = true;
private readonly Dictionary<string, string> _textCache = new Dictionary<string, string>();
void Start()
{
sentences = new Queue<Sentence>();
sentenceEvents = new Queue<UnityEvent>();
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<TextMeshProUGUI>().text = choices[i].choiceText;
Button btn = buttonsToUse[i].GetComponent<Button>();
btn.onClick.RemoveAllListeners();
Choice currentChoice = choices[i];
btn.onClick.AddListener(() => OnChoiceSelected(currentChoice));
}
else
{
buttonsToUse[i].SetActive(false);
}
}
}
private void OnChoiceSelected(Choice choice)
{
if (MANAGER_EventsDialogue.Instance != null) MANAGER_EventsDialogue.Instance.Execute(choice.eventID);
choice.onChoiceSelected?.Invoke();
ClearChoices();
isWaitingForChoice = false;
if (choice.nextDialogue != null) StartDialogue(choice.nextDialogue);
else if (sentences.Count > 0) DisplayNextSentence();
else EndDialogue();
}
private void ClearChoices()
{
foreach (var b in globalChoiceButtons) if(b) b.SetActive(false);
if (activeWindow != null && activeWindow.choiceButtons != null)
{
foreach (var b in activeWindow.choiceButtons) if(b) b.SetActive(false);
}
}
private string ReplaceShortcodes(string rawText)
{
if (string.IsNullOrEmpty(rawText)) return string.Empty;
if (_textCache.TryGetValue(rawText, out string cachedResult))
{
return cachedResult;
}
StringBuilder sb = new StringBuilder(rawText);
sb.Replace("[y]", "<color=#FFFF00>");
sb.Replace("[b]", "<color=#0000FF>");
sb.Replace("[r]", "<color=#FF0000>");
sb.Replace("[o]", "<color=#FFA500>");
sb.Replace("[/c]", "</color>");
string result = sb.ToString();
_textCache[rawText] = result;
return result;
}
void EndDialogue()
{
topWindow.rootObject.SetActive(false);
bottomWindow.rootObject.SetActive(false);
ToggleBackgroundUI(true);
currentDialogue = null;
}
}