Runtime UI under Scripts/UI: a UIStateController owning menu/dialogue state, a PlayerControlLock so opening a panel suppresses world input, panel visibility helpers, and the menu input binding. Views: the dialogue stream presenter and its options presenter and message row, the portrait HUD, and the player menu with character, journal, and tab-bar views. Skills gains SkillsPanelView and SkillRowView, which is why the Skills asmdef now references UnityEngine.UI. Prefabs/UI holds the authored canvases (dialogue stream, HUD, player menu) and their rows. UILayerBootstrap remains a hand-added runtime fallback for scenes where those prefabs are not instantiated; it deliberately does not auto-spawn. Editor/UILayerSetup.cs is the one-shot menu that builds and wires those prefabs into a scene, with UILayerSetupAutoRun reminding after a script reload. Art/UI/Placeholder carries the placeholder portrait the HUD falls back to. Co-Authored-By: Claude Opus 5 <[email protected]>
134 lines
4.8 KiB
C#
134 lines
4.8 KiB
C#
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using Yarn.Unity;
|
|
|
|
// Annotations only (not warnings): DialoguePresenterBase declares
|
|
// RunOptionsAsync as YarnTask<DialogueOption?>, so the override needs a
|
|
// nullable annotation context to match it without CS8632.
|
|
#nullable enable annotations
|
|
|
|
namespace NightclubArcadia.UI.Dialogue
|
|
{
|
|
/// <summary>
|
|
/// Yarn presenter that appends each line to a scrolling chat-style stream
|
|
/// rather than replacing a single text box.
|
|
/// </summary>
|
|
public sealed class DialogueStreamPresenter : DialoguePresenterBase
|
|
{
|
|
[SerializeField] DialogueMessageRow messageRowPrefab;
|
|
[SerializeField] RectTransform messageContainer;
|
|
[SerializeField] ScrollRect scrollRect;
|
|
[SerializeField] int maxRows = 100;
|
|
[SerializeField] float lettersPerSecond = 60f;
|
|
|
|
readonly List<DialogueMessageRow> rows = new List<DialogueMessageRow>();
|
|
|
|
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<DialogueOption?> 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();
|
|
}
|
|
}
|
|
}
|