using System.Collections.Generic; using System.Threading; using UnityEngine; 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 { /// /// Presents Yarn options as rows at the bottom of the dialogue stream, /// reusing the stock OptionItem component from the Yarn package. /// public sealed class DialogueStreamOptionsPresenter : DialoguePresenterBase { [SerializeField] OptionItem optionViewPrefab; [SerializeField] Transform optionsContainer; [SerializeField] CanvasGroup canvasGroup; [SerializeField] bool showUnavailableOptions; readonly List optionViews = new List(); 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 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(); 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); } } } } }