Files
Cielonos/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs
SoulliesOfficial 8186f54e90 新场景,剧情
2026-06-02 12:55:39 -04:00

226 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UI;
using Yarn.Unity;
using Yarn.Markup;
namespace SLSUtilities.Narrative.UI
{
/// <summary>
/// 高级台词展现层,继承自官方 LinePresenter。
/// 在保留所有原生功能打字机、淡入淡出、LineAdvancer 状态机)的基础上,
/// 扩展了角色立绘/头像切换、关键词高亮与悬停百科等功能。
/// </summary>
public class AdvancedLinePresenter : LinePresenter
{
[TitleGroup("立绘系统 (Portrait System)", Alignment = TitleAlignments.Centered)]
[BoxGroup("立绘系统 (Portrait System)/UI 引用")]
[Tooltip("用于显示角色立绘/头像的 Image 组件")]
[SerializeField] private Image portraitImage;
[BoxGroup("立绘系统 (Portrait System)/UI 引用")]
[Tooltip("立绘的容器节点。当没有立绘时整体隐藏,避免空白区域占位")]
[SerializeField] private GameObject portraitContainer;
[TitleGroup("关键词系统 (Keyword System)", Alignment = TitleAlignments.Centered)]
[BoxGroup("关键词系统 (Keyword System)/设置")]
[LabelText("启用关键词高亮")]
[Tooltip("是否在台词中自动识别并高亮关键词")]
[SerializeField] private bool enableKeywordHighlight = true;
[BoxGroup("关键词系统 (Keyword System)/设置")]
[ShowIf(nameof(enableKeywordHighlight))]
[LabelText("高亮颜色 (Highlight Color)")]
[Tooltip("关键词文本的高亮颜色")]
[SerializeField] private Color keywordHighlightColor = new Color(0.67f, 0.87f, 1f); // #AADDFF
// 缓存是否已构建的标记,避免每句台词重复构建
private bool _keywordCacheBuilt = false;
// 当前台词的 Markup 解析结果,保存到 PostProcessDisplayText 使用
private MarkupParseResult _currentLineMarkup;
// ---------------------------------------------------------------
// 生命周期
// ---------------------------------------------------------------
public override YarnTask OnDialogueStartedAsync()
{
// 在对话开始时构建关键词缓存
if (enableKeywordHighlight && StorySystem.Database != null)
{
KeywordProcessor.BuildCache(StorySystem.Database.keywords);
_keywordCacheBuilt = true;
}
return base.OnDialogueStartedAsync();
}
public override YarnTask OnDialogueCompleteAsync()
{
_keywordCacheBuilt = false;
return base.OnDialogueCompleteAsync();
}
// ---------------------------------------------------------------
// 台词处理
// ---------------------------------------------------------------
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// 确保打字机已通过 IAsyncTypewriter 装饰器包裹,防止其在开始打字时抹除关键词富文本标签。
// 基类 Awake 必然已实例化 Typewriter在此将其替换为我们的富文本打字机包装器。
if (Typewriter != null && !(Typewriter is KeywordTypewriterWrapper))
{
Typewriter = new KeywordTypewriterWrapper(Typewriter, markup => KeywordProcessor.ProcessWithMarkup(markup));
}
// 保存当前台词的 Markup供 PostProcessDisplayText 使用
_currentLineMarkup = line.TextWithoutCharacterName;
// 在台词展示之前更新立绘
UpdatePortrait(line);
// 调用父类处理所有其余逻辑(文本设置、打字机、淡入淡出、等待输入)
// 父类会在 Typewriter.PrepareForContent 之后调用 PostProcessDisplayText()
await base.RunLineAsync(line, token);
}
/// <summary>
/// 由父类 LinePresenter 在 Typewriter.PrepareForContent 之后调用。
/// 此时文本已完整设置到 TMP 组件上,但打字机尚未开始逐字展示。
/// 我们在此注入关键词的 link 标签。
/// </summary>
protected override void PostProcessDisplayText()
{
if (!enableKeywordHighlight || !_keywordCacheBuilt) return;
if (lineText == null) return;
// 使用基于 [kw] Markup 标签的处理,替代正则自动扫描
lineText.text = KeywordProcessor.ProcessWithMarkup(_currentLineMarkup);
}
// ---------------------------------------------------------------
// 立绘系统
// ---------------------------------------------------------------
private void UpdatePortrait(LocalizedLine line)
{
if (portraitImage == null) return;
string characterName = line.CharacterName;
if (string.IsNullOrWhiteSpace(characterName) || StorySystem.Database == null)
{
SetPortraitVisible(false);
return;
}
CharacterData charData = null;
foreach (var c in StorySystem.Database.characters)
{
if (c != null && string.Equals(c.nameKey, characterName, StringComparison.OrdinalIgnoreCase))
{
charData = c;
break;
}
}
if (charData == null)
{
SetPortraitVisible(false);
return;
}
YarnTagParser.Parse(line.Metadata, out var kvTags, out _);
kvTags.TryGetValue("mood", out string mood);
Sprite targetSprite = ResolvePortraitSprite(charData, mood);
if (targetSprite == null)
{
SetPortraitVisible(false);
return;
}
portraitImage.sprite = targetSprite;
SetPortraitVisible(true);
}
private static Sprite ResolvePortraitSprite(CharacterData charData, string mood)
{
if (!string.IsNullOrEmpty(mood) &&
charData.expressions != null &&
charData.expressions.TryGetValue(mood, out Sprite moodSprite) &&
moodSprite != null)
{
return moodSprite;
}
return charData.defaultPortrait;
}
private void SetPortraitVisible(bool visible)
{
if (portraitContainer != null)
portraitContainer.SetActive(visible);
else if (portraitImage != null)
portraitImage.enabled = visible;
}
}
/// <summary>
/// 关键词打字机包装器。
/// 解决官方打字机在 PrepareForContent 和 RunTypewriter 时,
/// 会强行用 plainText 覆盖 text 组件,导致我们注入的富文本高亮标签被抹除的问题。
/// </summary>
public class KeywordTypewriterWrapper : IAsyncTypewriter
{
private readonly IAsyncTypewriter _inner;
private readonly Func<MarkupParseResult, string> _processMarkupFunc;
public KeywordTypewriterWrapper(IAsyncTypewriter inner, Func<MarkupParseResult, string> processMarkupFunc)
{
_inner = inner;
_processMarkupFunc = processMarkupFunc;
}
public TMPro.TMP_Text? TextElement
{
get => _inner.TextElement;
set => _inner.TextElement = value;
}
public List<IActionMarkupHandler> ActionMarkupHandlers => _inner.ActionMarkupHandlers;
public void PrepareForContent(MarkupParseResult line)
{
_inner.PrepareForContent(line);
if (TextElement != null)
{
TextElement.text = _processMarkupFunc(line);
}
}
public async YarnTask RunTypewriter(MarkupParseResult line, System.Threading.CancellationToken cancellationToken)
{
var task = _inner.RunTypewriter(line, cancellationToken);
if (TextElement != null)
{
TextElement.text = _processMarkupFunc(line);
}
await task;
if (TextElement != null)
{
TextElement.text = _processMarkupFunc(line);
}
}
public void ContentWillDismiss() => _inner.ContentWillDismiss();
public void ContentDidDismiss() => _inner.ContentDidDismiss();
}
}