400 lines
14 KiB
C#
400 lines
14 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using BITKit;
|
|
using BITKit.Entities;
|
|
using BITKit.Mod;
|
|
using BITKit.UX;
|
|
using Cysharp.Threading.Tasks;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Net.BITKit.Chat;
|
|
using Net.BITKit.Localization;
|
|
using Net.Project.B.Dialogue;
|
|
using Project.B.Player;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Net.Project.B.UX
|
|
{
|
|
public class UXDialogue<THud>:UIToolkitSubPanel<THud>, IUXDialogue where THud:IUXPanel
|
|
{
|
|
private readonly ILocalizationService _localizationService;
|
|
private readonly IMainTicker _ticker;
|
|
private readonly ILogger<UXDialogue<THud>> _logger;
|
|
private readonly IDialogueService _dialogueService;
|
|
private readonly IChatService _chatService;
|
|
private readonly ValidHandle _isBusy = new();
|
|
private VisualElement _rootVisualElement;
|
|
|
|
private readonly IEntitiesService _entitiesService;
|
|
|
|
[UXBindPath("dialogue-container")]
|
|
private VisualElement _dialogueContainer;
|
|
[UXBindPath("dialogue-choose-container")]
|
|
private VisualElement _dialogueChooseContainer;
|
|
[UXBindPath("comic-container")]
|
|
private VisualElement _comicContainer;
|
|
[UXBindPath("tips-foldout")]
|
|
private Foldout _tipsFoldout;
|
|
|
|
private readonly ValidHandle _allowTips = new();
|
|
|
|
|
|
private VisualElement _floatDialogueContainer;
|
|
|
|
private VisualTreeAsset _dialogueTemplate;
|
|
private VisualTreeAsset _dialogueChooseTemplate;
|
|
|
|
private readonly ConcurrentDictionary<int, VisualElement> _subtitles = new();
|
|
|
|
private UniTaskCompletionSource<int> _chooseTask;
|
|
|
|
private readonly ValidHandle _isVisible = new();
|
|
|
|
private readonly ValidHandle _allowComic = new();
|
|
|
|
private readonly ValidHandle _inDialogue = new();
|
|
|
|
private CancellationTokenSource _newDialogueCancellationTokenSource;
|
|
private readonly IntervalUpdate _skipInterval = new (0.1f);
|
|
|
|
private readonly ConcurrentDictionary<int, IDialogueData> _dialogues = new();
|
|
|
|
private readonly Dictionary<int, Label> _floatDialogues=new();
|
|
private readonly Dictionary<int, ValidHandle> _floatDialoguesGc = new();
|
|
|
|
private readonly IUXKeyMap<InputAction> _uxKeyMap;
|
|
private readonly IPlayerKeyMap<InputAction> _playerKeyMap;
|
|
|
|
public UXDialogue(IServiceProvider serviceProvider, IDialogueService dialogueService, ILogger<UXDialogue<THud>> logger, IEntitiesService entitiesService, IMainTicker ticker, ILocalizationService localizationService, IUXKeyMap<InputAction> uxKeyMap, IPlayerKeyMap<InputAction> playerKeyMap, IChatService chatService) : base(serviceProvider)
|
|
{
|
|
_dialogueService = dialogueService;
|
|
_logger = logger;
|
|
_entitiesService = entitiesService;
|
|
_ticker = ticker;
|
|
_localizationService = localizationService;
|
|
_uxKeyMap = uxKeyMap;
|
|
_playerKeyMap = playerKeyMap;
|
|
_chatService = chatService;
|
|
|
|
_dialogueService.OnDialogueStart += OnDialogueStart;
|
|
_dialogueService.OnDialogueEnd+=OnDialogueEnd;
|
|
|
|
_dialogueService.OnDialogueChoose += OnDialogueChoose;
|
|
|
|
_chatService.OnMessageReceived += OnMessageReceived;
|
|
|
|
ticker.Add(Tick);
|
|
}
|
|
|
|
private void OnMessageReceived(ChatMessage obj)
|
|
{
|
|
if (obj.FromUserId == Environment.UserName)
|
|
{
|
|
_allowTips.Clear();
|
|
}
|
|
}
|
|
|
|
private void Tick(float obj)
|
|
{
|
|
foreach (var (id,visualElement) in _floatDialogues)
|
|
{
|
|
if(_entitiesService.TryGetEntity(id,out var entity) is false)continue;
|
|
|
|
if (entity.ServiceProvider.QueryComponents(out Collider collider))
|
|
{
|
|
visualElement.SetPosition(collider.bounds.center + Vector3.up * (collider.bounds.extents.y * 0.32f));
|
|
}
|
|
else if (entity.ServiceProvider.QueryComponents(out Transform transform))
|
|
{
|
|
visualElement.SetPosition(transform.position);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override async UniTask OnInitiatedAsync()
|
|
{
|
|
_logger.LogInformation("UX对话初始化中...");
|
|
using var _ = _isBusy.GetHandle();
|
|
|
|
_logger.LogInformation($"{typeof(THud).Name}已初始化完成,正在加载Template...");
|
|
|
|
_rootVisualElement = Panel.Root as VisualElement;
|
|
|
|
UXUtils.Inject(this, _rootVisualElement);
|
|
|
|
_dialogueContainer.Clear();
|
|
_dialogueChooseContainer.Clear();
|
|
|
|
_dialogueTemplate = await ModService.LoadAsset<VisualTreeAsset>("ux_dialogue_template");
|
|
_dialogueChooseTemplate = await ModService.LoadAsset<VisualTreeAsset>("ux_dialogue_choose_template");
|
|
_logger.LogInformation("UX对话初始化完成");
|
|
|
|
_dialogueContainer.SetActive(false);
|
|
|
|
_isVisible.AddListener(_dialogueContainer.SetActive);
|
|
|
|
_allowComic.AddListener(_comicContainer.SetActive);
|
|
|
|
_allowComic?.Invoke();
|
|
|
|
if (Panel is UIToolKitPanel panel)
|
|
{
|
|
panel.InputActionGroup.RegisterCallback(_playerKeyMap.RunKey, OnConfirm);
|
|
|
|
_floatDialogueContainer = panel.RootVisualElement.Create<VisualElement>();
|
|
_floatDialogueContainer.pickingMode = PickingMode.Ignore;
|
|
_floatDialogueContainer.style.position = new StyleEnum<Position>(Position.Absolute);
|
|
_floatDialogueContainer.style.top = 0;
|
|
_floatDialogueContainer.style.left = 0;
|
|
_floatDialogueContainer.style.right = 0;
|
|
_floatDialogueContainer.style.bottom = 0;
|
|
|
|
_floatDialogueContainer.AddToClassList("ui_hud_dialogue_container");
|
|
|
|
_floatDialogueContainer.SendToBack();
|
|
}
|
|
|
|
_allowTips.AddListener(x =>
|
|
{
|
|
_tipsFoldout.Blur();
|
|
|
|
_tipsFoldout.schedule.Execute(() =>
|
|
{
|
|
|
|
|
|
if (x)
|
|
{
|
|
_tipsFoldout.value = false;
|
|
_tipsFoldout.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
_tipsFoldout.SetActive(false);
|
|
_tipsFoldout.Clear();
|
|
}
|
|
});
|
|
});
|
|
|
|
_tipsFoldout.Q<Toggle>().focusable = false;
|
|
|
|
_allowTips.Invoke();
|
|
}
|
|
|
|
private void OnConfirm(InputAction.CallbackContext obj)
|
|
{
|
|
if(_dialogues.Count is 0)return;
|
|
if (obj.JustPressed() is false) return;
|
|
if (Panel.Enabled is false) return;
|
|
if (_skipInterval.AllowUpdateWithoutReset is false) return;
|
|
|
|
foreach (var dialogue in _dialogues.Values.ToArray())
|
|
{
|
|
dialogue.CancellationTokenSource.Cancel();
|
|
}
|
|
|
|
throw new OperationCanceledException();
|
|
}
|
|
|
|
private UniTask<int> OnDialogueChoose(IDialogueData arg1, IReadOnlyCollection<IDialogueChoice> arg2)
|
|
{
|
|
BITAppForUnity.AllowCursor.AddElement(this);
|
|
|
|
_chooseTask?.TrySetCanceled();
|
|
|
|
_chooseTask = new UniTaskCompletionSource<int>();
|
|
|
|
_dialogueChooseContainer.Clear();
|
|
|
|
for (var i = 0; i < arg2.Count; i++)
|
|
{
|
|
var choice = arg2.ElementAt(i);
|
|
|
|
var container = _dialogueChooseContainer.Create(_dialogueChooseTemplate);
|
|
container.Get<Label>().text = i.ToString();
|
|
container.Get<Label>(1).text = choice.Text;
|
|
var i1 = i;
|
|
container.Get<Button>().clicked += () =>
|
|
{
|
|
_chooseTask.TrySetResult(i1);
|
|
BITAppForUnity.AllowCursor.RemoveElement(this);
|
|
_dialogueChooseContainer.Clear();
|
|
};
|
|
}
|
|
return _chooseTask.Task;
|
|
}
|
|
|
|
private async void OnDialogueEnd(IDialogueData obj)
|
|
{
|
|
_dialogues.TryRemove(obj.Identity);
|
|
_inDialogue.RemoveElement(obj.Identity);
|
|
|
|
_allowComic.RemoveElement(obj.Identity);
|
|
|
|
if (_subtitles.TryGetValue(obj.Identity, out var subtitle) is false) return;
|
|
|
|
subtitle.SetEnabled(false);
|
|
|
|
try
|
|
{
|
|
_newDialogueCancellationTokenSource?.Cancel();
|
|
_newDialogueCancellationTokenSource = new();
|
|
|
|
|
|
await UniTask.Delay(5000).AttachExternalCancellation(_newDialogueCancellationTokenSource.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
|
|
}
|
|
|
|
if (_dialogueChooseContainer.visible is false) return;
|
|
|
|
subtitle.RemoveFromHierarchy();
|
|
|
|
_isVisible.RemoveElement(obj.Identity);
|
|
|
|
if (_floatDialoguesGc.TryGetValue(obj.ActorIdentity, out var gc))
|
|
{
|
|
gc.RemoveElement(obj.Identity);
|
|
}
|
|
}
|
|
|
|
private async UniTask OnDialogueStart(IDialogueData arg)
|
|
{
|
|
_skipInterval.Reset();
|
|
|
|
_newDialogueCancellationTokenSource?.Cancel();
|
|
|
|
await _isBusy;
|
|
|
|
_dialogues.TryAdd(arg.Identity, arg);
|
|
|
|
_inDialogue.AddElement(arg.Identity);
|
|
|
|
switch (arg.MetaObject)
|
|
{
|
|
case Texture2D texture2D:
|
|
{
|
|
_allowComic.AddElement(arg.Identity);
|
|
_comicContainer.style.backgroundImage = new(texture2D);
|
|
if (string.IsNullOrEmpty(arg.Text)) return;
|
|
}
|
|
break;
|
|
case Sprite sprite:
|
|
{
|
|
_allowComic.AddElement(arg.Identity);
|
|
_comicContainer.style.backgroundImage = new(sprite);
|
|
if (string.IsNullOrEmpty(arg.Text)) return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
_isVisible.AddElement(arg.Identity);
|
|
|
|
var container = _dialogueContainer.Create(_dialogueTemplate);
|
|
|
|
if (arg.ActorIdentity is not 0 && _entitiesService.TryGetEntity(arg.ActorIdentity, out var entity) &&
|
|
entity.ServiceProvider.GetService<IdComponent>() is var id)
|
|
{
|
|
container.Get<Label>().text = string.IsNullOrEmpty(id.Name) ? string.Empty : _localizationService.GetLocalizedString( id.Name) + ":";
|
|
}
|
|
else
|
|
{
|
|
var players = _entitiesService.QueryComponents<IdComponent, LocalPlayerComponent>().ToArray();
|
|
container.Get<Label>().text =_localizationService.GetLocalizedString( players.Any() ? players[0].Item1.Name : "#Narrator") + ":";
|
|
}
|
|
|
|
var text = arg.Text;
|
|
|
|
foreach (var func in OnSubtitle.CastAsFunc())
|
|
{
|
|
text = func.Invoke(text);
|
|
}
|
|
|
|
var currentText = _localizationService.GetLocalizedString(text);
|
|
;
|
|
container.Get<Label>(1).text = currentText;
|
|
if (container.Get<Label>(2) is { } label)
|
|
{
|
|
if (SubtitleLanguages is { Length: > 0 })
|
|
{
|
|
var stringBuilder = new StringBuilder();
|
|
foreach (var lang in SubtitleLanguages)
|
|
{
|
|
if (lang == _localizationService.CurrentLanguage) continue;
|
|
|
|
var newText = _localizationService.GetLocalizedString(text, lang);
|
|
|
|
if (newText.Equals(currentText, StringComparison.CurrentCultureIgnoreCase)) continue;
|
|
|
|
stringBuilder.AppendLine(newText);
|
|
}
|
|
|
|
label.text = stringBuilder.ToString();
|
|
label.SetActive(string.IsNullOrEmpty(label.text) is false);
|
|
}
|
|
else
|
|
{
|
|
label.SetActive(false);
|
|
}
|
|
}
|
|
|
|
|
|
_subtitles.TryAdd(arg.Identity, container);
|
|
|
|
if (_dialogueContainer is ScrollView scrollView)
|
|
{
|
|
scrollView.ScrollToBottom();
|
|
}
|
|
|
|
if (_entitiesService.Entities.ContainsKey(arg.ActorIdentity))
|
|
{
|
|
if (_floatDialogues.TryGetValue(arg.ActorIdentity, out var floatLabel) is false)
|
|
{
|
|
floatLabel = _floatDialogues[arg.ActorIdentity] = _floatDialogueContainer.Create<Label>();
|
|
var gc =_floatDialoguesGc[arg.ActorIdentity] = new();
|
|
gc.AddListener(x =>
|
|
{
|
|
if (!x)
|
|
{
|
|
_floatDialogues.Remove(arg.ActorIdentity);
|
|
_floatDialoguesGc.Remove(arg.ActorIdentity);
|
|
gc.Dispose();
|
|
floatLabel.RemoveFromHierarchy();
|
|
}
|
|
});
|
|
gc.AddElement(arg.Identity);
|
|
}
|
|
|
|
floatLabel.text = _localizationService.GetLocalizedString( arg.Text);
|
|
}
|
|
|
|
if (arg.MetaObject is string tips)
|
|
{
|
|
_tipsFoldout.Clear();
|
|
_tipsFoldout.Create<Label>().text = tips;
|
|
|
|
_allowTips.Invoke(true);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_dialogueService.OnDialogueStart -= OnDialogueStart;
|
|
_dialogueService.OnDialogueEnd-=OnDialogueEnd;
|
|
|
|
_ticker.Remove(Tick);
|
|
}
|
|
|
|
public event Func<string, string> OnSubtitle;
|
|
public string[] SubtitleLanguages { get; set; }
|
|
}
|
|
}
|