using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using TMPro;
/// <summary>
/// GAME 1303 Tier 6 — Plays a DialogueData conversation: typewriter text, speaker name,
/// portrait, and choice buttons. One of these in the scene serves every conversation.
///
/// SETUP
/// 1. Canvas -> a "DialoguePanel" (a box at the bottom of the screen) containing:
/// - TMP text for the line -> Line Text
/// - TMP text for the speaker name -> Speaker Text
/// - an Image for the portrait -> Portrait Image
/// - a "ContinueIndicator" (a small blinking arrow) -> Continue Indicator
/// - an empty "ChoiceContainer" with a Vertical Layout Group -> Choice Container
/// Add this script to the panel's parent (or anywhere) and drag the panel in.
/// 2. Choice Button Prefab: a UI Button with a TMP text child.
/// 3. Start a conversation from any Interactable: OnInteract -> DialogueRunner.StartDialogue,
/// then drag the DialogueData asset into the event's object slot.
/// 4. Advance: the player's InteractionPrompt forwards the Interact key here
/// automatically. Pressing it while text is still typing shows the whole line at once
/// (a small thing that players notice immediately).
/// 5. Pause Player: the runner sets PlayerController2D / TopDownMover InputEnabled off
/// while a conversation with Pause Player runs.
///
/// Flags: lines and choices read and write Tier 4's ChoiceFlagManager, so dialogue and
/// world state share one memory.
/// </summary>
public class DialogueRunner : MonoBehaviour
{
public static DialogueRunner Active { get; private set; }
public static bool IsAnyOpen => Active != null && Active.IsOpen;
[Header("UI")]
public GameObject panel;
public TMP_Text lineText;
public TMP_Text speakerText;
public Image portraitImage;
public GameObject continueIndicator;
public Transform choiceContainer;
public Button choiceButtonPrefab;
[Header("Typewriter")]
public float charactersPerSecond = 40f;
public bool typewriter = true;
[Tooltip("Blip sound played every few characters. Leave blank for silence.")]
public string typeSfx = "";
public int blipEveryNChars = 3;
[Header("Events")]
public UnityEvent OnDialogueStarted;
public UnityEvent OnDialogueEnded;
public UnityEvent<string> OnLineShown; // the line's text
public bool IsOpen { get; private set; }
private DialogueData data;
private int index;
private bool typing;
private bool awaitingChoice;
private int pendingJump = -1;
private Coroutine typeRoutine;
private readonly List<Button> spawnedChoices = new List<Button>();
private MonoBehaviour playerMover;
private static readonly HashSet<DialogueData> played = new HashSet<DialogueData>();
void Awake()
{
if (panel != null) panel.SetActive(false);
}
/// <summary>Wire an Interactable's OnInteract here and drag in the DialogueData asset.</summary>
public void StartDialogue(DialogueData dialogue)
{
if (dialogue == null || dialogue.lines.Count == 0) return;
if (dialogue.oneShot && played.Contains(dialogue)) return;
if (IsOpen) return;
data = dialogue;
played.Add(dialogue);
Active = this;
IsOpen = true;
index = -1;
pendingJump = -1;
if (panel != null) panel.SetActive(true);
if (data.pausePlayer) SetPlayerFrozen(true);
OnDialogueStarted?.Invoke();
Advance();
}
/// <summary>Next line, or finish the typewriter if it's still running.</summary>
public void Advance()
{
if (!IsOpen || awaitingChoice) return;
if (typing) // skip the typewriter
{
if (typeRoutine != null) StopCoroutine(typeRoutine);
typing = false;
lineText.text = CurrentLine != null ? CurrentLine.text : lineText.text;
ShowContinueIndicator(true);
MaybeShowChoices();
return;
}
// Where do we go? A pending jump (from a choice) wins, then the line's own
// goToLine, otherwise just the next line down the list.
int next;
if (pendingJump >= 0) { next = pendingJump; pendingJump = -1; }
else if (index >= 0 && index < data.lines.Count && data.lines[index].goToLine >= 0)
next = data.lines[index].goToLine;
else next = index + 1;
// Skip any line whose flag test fails
while (next < data.lines.Count && !FlagsOk(data.lines[next].requireFlags)) next++;
if (next >= data.lines.Count) { EndDialogue(); return; }
index = next;
ShowLine(data.lines[index]);
}
DialogueData.Line CurrentLine =>
(data != null && index >= 0 && index < data.lines.Count) ? data.lines[index] : null;
void ShowLine(DialogueData.Line line)
{
ClearChoices();
ShowContinueIndicator(false);
if (speakerText != null)
{
speakerText.text = line.speaker;
speakerText.gameObject.SetActive(!string.IsNullOrEmpty(line.speaker));
}
if (portraitImage != null)
{
portraitImage.sprite = line.portrait;
portraitImage.gameObject.SetActive(line.portrait != null);
}
if (!string.IsNullOrEmpty(line.setFlag)) ChoiceFlagManager.Instance?.SetFlag(line.setFlag);
if (line.sound != null) AudioManager.Instance?.PlaySFX(line.sound);
OnLineShown?.Invoke(line.text);
if (typewriter && charactersPerSecond > 0f)
typeRoutine = StartCoroutine(Type(line.text));
else
{
lineText.text = line.text;
ShowContinueIndicator(true);
MaybeShowChoices();
}
}
IEnumerator Type(string text)
{
typing = true;
lineText.text = "";
float delay = 1f / charactersPerSecond;
for (int i = 0; i < text.Length; i++)
{
lineText.text += text[i];
if (!string.IsNullOrEmpty(typeSfx) && i % blipEveryNChars == 0 && text[i] != ' ')
AudioManager.Instance?.PlaySFX(typeSfx, 0.3f);
yield return new WaitForSecondsRealtime(delay);
}
typing = false;
ShowContinueIndicator(true);
MaybeShowChoices();
}
void MaybeShowChoices()
{
var line = CurrentLine;
if (line == null || line.choices == null || line.choices.Count == 0) return;
if (choiceContainer == null || choiceButtonPrefab == null)
{
Debug.LogWarning("DialogueRunner: this line has choices but no Choice Container / Button Prefab is set.");
return;
}
awaitingChoice = true;
ShowContinueIndicator(false);
foreach (var choice in line.choices)
{
if (!FlagsOk(choice.requireFlags)) continue;
var btn = Instantiate(choiceButtonPrefab, choiceContainer);
var label = btn.GetComponentInChildren<TMP_Text>();
if (label != null) label.text = choice.text;
var captured = choice;
btn.onClick.AddListener(() => PickChoice(captured));
spawnedChoices.Add(btn);
}
// Every choice was filtered out — just carry on
if (spawnedChoices.Count == 0) { awaitingChoice = false; ShowContinueIndicator(true); }
}
void PickChoice(DialogueData.Choice choice)
{
if (!string.IsNullOrEmpty(choice.setFlag)) ChoiceFlagManager.Instance?.SetFlag(choice.setFlag);
ClearChoices();
awaitingChoice = false;
if (choice.goToLine < 0) { EndDialogue(); return; }
pendingJump = choice.goToLine;
Advance();
}
void ClearChoices()
{
foreach (var b in spawnedChoices) if (b != null) Destroy(b.gameObject);
spawnedChoices.Clear();
}
void ShowContinueIndicator(bool on)
{
if (continueIndicator != null) continueIndicator.SetActive(on);
}
public void EndDialogue()
{
if (!IsOpen) return;
IsOpen = false;
awaitingChoice = false;
typing = false;
ClearChoices();
if (panel != null) panel.SetActive(false);
if (data != null && data.pausePlayer) SetPlayerFrozen(false);
if (data != null && !string.IsNullOrEmpty(data.setFlagOnComplete))
ChoiceFlagManager.Instance?.SetFlag(data.setFlagOnComplete);
Active = null;
data = null;
OnDialogueEnded?.Invoke();
}
bool FlagsOk(string flags)
{
if (string.IsNullOrWhiteSpace(flags)) return true;
return ChoiceFlagManager.Instance == null || ChoiceFlagManager.Instance.HasAll(flags);
}
void SetPlayerFrozen(bool frozen)
{
if (playerMover == null)
{
var p = GameObject.FindGameObjectWithTag("Player");
if (p == null) return;
var pc2d = p.GetComponent<PlayerController2D>();
if (pc2d != null) playerMover = pc2d;
else
{
var td2d = p.GetComponent<TopDownMover>();
if (td2d != null) playerMover = td2d;
}
if (playerMover == null) return;
}
if (playerMover is PlayerController2D pc) pc.InputEnabled = !frozen;
else if (playerMover is TopDownMover td) td.InputEnabled = !frozen;
}
/// <summary>Let one-shot conversations play again (new game / testing).</summary>
public static void ResetPlayedHistory() => played.Clear();
}