add UI layer: dialogue stream, HUD, player menu, and skills panel
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]>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user