using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using Yarn.Unity; // Annotations only (not warnings): DialoguePresenterBase declares // RunOptionsAsync as YarnTask, so the override needs a // nullable annotation context to match it without CS8632. #nullable enable annotations namespace NightclubArcadia.UI.Dialogue { /// /// Yarn presenter that appends each line to a scrolling chat-style stream /// rather than replacing a single text box. /// public sealed class DialogueStreamPresenter : DialoguePresenterBase { [SerializeField] DialogueMessageRow messageRowPrefab; [SerializeField] RectTransform messageContainer; [SerializeField] ScrollRect scrollRect; [SerializeField] int maxRows = 100; [SerializeField] float lettersPerSecond = 60f; readonly List rows = new List(); void Awake() { // Must exist before LineAdvancer.Start(), which does // presenter.Typewriter?.ActionMarkupHandlers.Add(this); // to learn when a line has finished displaying. Leave Typewriter // null and that is a silent no-op: LineAdvancer never receives // OnLineDisplayComplete, its status never leaves LineBegan, and // pressing the advance key only ever hurries up the line — the // conversation can never move past the first message. Typewriter = new LetterTypewriter { CharactersPerSecond = lettersPerSecond, }; } public override YarnTask OnDialogueStartedAsync() { ClearRows(); return YarnTask.CompletedTask; } public override YarnTask OnDialogueCompleteAsync() { // DialogueTransitionController owns showing/hiding the panel. return YarnTask.CompletedTask; } public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token) { if (messageRowPrefab == null || messageContainer == null) { Debug.LogError($"[{nameof(DialogueStreamPresenter)}] Missing messageRowPrefab or messageContainer.", this); return; } var row = Instantiate(messageRowPrefab, messageContainer); row.gameObject.SetActive(true); rows.Add(row); while (rows.Count > maxRows) { var oldest = rows[0]; rows.RemoveAt(0); if (oldest != null) { Destroy(oldest.gameObject); } } var text = line.TextWithoutCharacterName; row.SetContent(line.CharacterName, text.Text ?? string.Empty); // Delegate the reveal to Yarn's own typewriter rather than // hand-rolling maxVisibleCharacters. It drives the // IActionMarkupHandler lifecycle — including LineAdvancer's // OnLineDisplayComplete, which is what lets the advance key move to // the next line instead of only hurrying up the current one — and // it honours [pause] markup for free. // // TextElement is re-pointed at each new row; the typewriter itself // is reused so LineAdvancer's registration survives. if (Typewriter != null && row.BodyText != null) { Typewriter.TextElement = row.BodyText; Typewriter.PrepareForContent(text); await Typewriter.RunTypewriter(text, token.HurryUpToken).SuppressCancellationThrow(); } // Deliberately no ContentWillDismiss/ContentDidDismiss: rows persist // in the stream, and ContentDidDismiss would blank the row by // resetting maxVisibleCharacters to 0. Canvas.ForceUpdateCanvases(); if (scrollRect != null) { scrollRect.verticalNormalizedPosition = 0f; } if (!token.IsNextContentRequested) { await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow(); } } public override YarnTask RunOptionsAsync( DialogueOption[] dialogueOptions, LineCancellationToken cancellationToken) { // Options are handled by DialogueStreamOptionsPresenter. return DialogueRunner.NoOptionSelected; } void ClearRows() { for (var i = 0; i < rows.Count; i++) { if (rows[i] != null) { Destroy(rows[i].gameObject); } } rows.Clear(); } } }