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]>
174 lines
5.4 KiB
C#
174 lines
5.4 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
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>
|
|
/// Presents Yarn options as rows at the bottom of the dialogue stream,
|
|
/// reusing the stock OptionItem component from the Yarn package.
|
|
/// </summary>
|
|
public sealed class DialogueStreamOptionsPresenter : DialoguePresenterBase
|
|
{
|
|
[SerializeField] OptionItem optionViewPrefab;
|
|
[SerializeField] Transform optionsContainer;
|
|
[SerializeField] CanvasGroup canvasGroup;
|
|
[SerializeField] bool showUnavailableOptions;
|
|
|
|
readonly List<OptionItem> optionViews = new List<OptionItem>();
|
|
|
|
public override YarnTask OnDialogueStartedAsync() => YarnTask.CompletedTask;
|
|
|
|
public override YarnTask OnDialogueCompleteAsync()
|
|
{
|
|
HideAllOptions();
|
|
return YarnTask.CompletedTask;
|
|
}
|
|
|
|
public override YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
|
|
{
|
|
// Lines are handled by DialogueStreamPresenter.
|
|
return YarnTask.CompletedTask;
|
|
}
|
|
|
|
public override async YarnTask<DialogueOption?> RunOptionsAsync(
|
|
DialogueOption[] dialogueOptions,
|
|
LineCancellationToken cancellationToken)
|
|
{
|
|
var anyAvailable = false;
|
|
foreach (var option in dialogueOptions)
|
|
{
|
|
if (option.IsAvailable)
|
|
{
|
|
anyAvailable = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!anyAvailable)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
while (dialogueOptions.Length > optionViews.Count)
|
|
{
|
|
optionViews.Add(CreateNewOptionView());
|
|
}
|
|
|
|
var selectedOptionCompletionSource = new YarnTaskCompletionSource<DialogueOption?>();
|
|
var completionCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
|
|
cancellationToken.NextContentToken);
|
|
|
|
async YarnTask CancelSourceWhenDialogueCancelled()
|
|
{
|
|
await YarnTask.WaitUntilCanceled(completionCancellationSource.Token);
|
|
|
|
if (cancellationToken.IsNextContentRequested)
|
|
{
|
|
selectedOptionCompletionSource.TrySetResult(null);
|
|
}
|
|
}
|
|
|
|
CancelSourceWhenDialogueCancelled().Forget();
|
|
|
|
for (var i = 0; i < dialogueOptions.Length; i++)
|
|
{
|
|
var optionView = optionViews[i];
|
|
var option = dialogueOptions[i];
|
|
|
|
if (!option.IsAvailable && !showUnavailableOptions)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
optionView.gameObject.SetActive(true);
|
|
optionView.Option = option;
|
|
optionView.OnOptionSelected = selectedOptionCompletionSource;
|
|
optionView.completionToken = completionCancellationSource.Token;
|
|
}
|
|
|
|
var optionIndexToSelect = -1;
|
|
for (var i = 0; i < optionViews.Count; i++)
|
|
{
|
|
var view = optionViews[i];
|
|
if (!view.isActiveAndEnabled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (view.IsHighlighted)
|
|
{
|
|
optionIndexToSelect = i;
|
|
break;
|
|
}
|
|
|
|
if (optionIndexToSelect == -1)
|
|
{
|
|
optionIndexToSelect = i;
|
|
}
|
|
}
|
|
|
|
if (optionIndexToSelect > -1)
|
|
{
|
|
optionViews[optionIndexToSelect].Select();
|
|
}
|
|
|
|
if (canvasGroup != null)
|
|
{
|
|
canvasGroup.alpha = 1f;
|
|
canvasGroup.interactable = true;
|
|
canvasGroup.blocksRaycasts = true;
|
|
}
|
|
|
|
var completedTask = await selectedOptionCompletionSource.Task;
|
|
completionCancellationSource.Cancel();
|
|
|
|
if (canvasGroup != null)
|
|
{
|
|
canvasGroup.interactable = false;
|
|
canvasGroup.blocksRaycasts = false;
|
|
canvasGroup.alpha = 0f;
|
|
}
|
|
|
|
HideAllOptions();
|
|
await YarnTask.Yield();
|
|
|
|
if (cancellationToken.NextContentToken.IsCancellationRequested)
|
|
{
|
|
return await DialogueRunner.NoOptionSelected;
|
|
}
|
|
|
|
return completedTask;
|
|
}
|
|
|
|
OptionItem CreateNewOptionView()
|
|
{
|
|
var parent = optionsContainer != null
|
|
? optionsContainer
|
|
: (canvasGroup != null ? canvasGroup.transform : transform);
|
|
|
|
var optionView = Instantiate(optionViewPrefab, parent, false);
|
|
optionView.transform.SetAsLastSibling();
|
|
optionView.gameObject.SetActive(false);
|
|
return optionView;
|
|
}
|
|
|
|
void HideAllOptions()
|
|
{
|
|
foreach (var optionView in optionViews)
|
|
{
|
|
if (optionView != null)
|
|
{
|
|
optionView.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|