PJ/Assets/scripts/dotfs_scripts/MANAGER_Dialogues.cs

408 lines
13 KiB
C#
Raw Normal View History

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;
}
}