using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.UI
{
/// 屏幕阅读器优先级(与 WCAG / ARIA live-region 等级对齐)。
public enum ScreenReaderPriority
{
/// Polite —— 当前播报结束后再播报,不打断玩家。
Polite = 0,
/// Assertive —— 立即打断当前播报(如战斗死亡、关键提示)。
Assertive = 1
}
///
/// 屏幕阅读器接口(无障碍)。默认无实现;接入第三方 TTS / 平台 API(iOS VoiceOver、
/// Android TalkBack、Steam Overlay TTS)时实现本接口并通过 ServiceLocator 注册。
/// 设计为 string-only 接口而非富文本,避免不同 TTS 引擎对标签解析不一致。
///
public interface IScreenReader
{
void Speak(string text, ScreenReaderPriority priority = ScreenReaderPriority.Polite);
void StopAll();
}
///
/// 字幕管理器(架构 10_UIModule §11 无障碍补完)。
///
/// 职责:与对话气泡(DialogueUI)解耦,专门承担"叙述性字幕 / 关键提示文本 / 战斗播报"。
/// 订阅 (推荐项目内新建 EVT_SubtitleShow)显示底部居中字幕;
/// 若 中存在 实现则同步触发 TTS。
///
/// 队列策略:Polite 串行(FIFO);Assertive 立即清空队列并显示。
///
public class SubtitleManager : MonoBehaviour
{
[Header("UI")]
[SerializeField] private GameObject _root;
[SerializeField] private TMP_Text _label;
[SerializeField] private CanvasGroup _canvasGroup;
[Header("时长")]
[Tooltip("每行字幕显示时间(秒);可被 Show(text, durationOverride) 覆盖。")]
[SerializeField] private float _defaultDuration = 3f;
[SerializeField] private float _fadeDuration = 0.2f;
[Header("事件频道(可选)")]
[SerializeField] private StringEventChannelSO _onSubtitleShow;
private readonly Queue<(string text, float duration, ScreenReaderPriority pri)> _queue = new();
private Coroutine _playCo;
private readonly CompositeDisposable _subs = new();
private void OnEnable()
{
if (_root != null) _root.SetActive(false);
if (_canvasGroup != null) _canvasGroup.alpha = 0f;
_onSubtitleShow?.Subscribe(t => Show(t, _defaultDuration, ScreenReaderPriority.Polite)).AddTo(_subs);
}
private void OnDisable() => _subs.Clear();
/// 显示一行字幕;自动按优先级排队。
public void Show(string text, float duration = -1f,
ScreenReaderPriority priority = ScreenReaderPriority.Polite)
{
if (string.IsNullOrEmpty(text)) return;
float d = duration > 0f ? duration : _defaultDuration;
if (priority == ScreenReaderPriority.Assertive)
{
_queue.Clear();
if (_playCo != null) StopCoroutine(_playCo);
_playCo = null;
}
_queue.Enqueue((text, d, priority));
// 同步触发 TTS
var reader = ServiceLocator.GetOrDefault(null);
reader?.Speak(text, priority);
if (_playCo == null) _playCo = StartCoroutine(PlayLoop());
}
private IEnumerator PlayLoop()
{
while (_queue.Count > 0)
{
var (text, d, _) = _queue.Dequeue();
yield return ShowOne(text, d);
}
_playCo = null;
}
private IEnumerator ShowOne(string text, float duration)
{
if (_label != null) _label.text = text;
if (_root != null) _root.SetActive(true);
yield return Fade(0f, 1f, _fadeDuration);
yield return new WaitForSecondsRealtime(Mathf.Max(0f, duration - _fadeDuration * 2f));
yield return Fade(1f, 0f, _fadeDuration);
if (_root != null) _root.SetActive(false);
}
private IEnumerator Fade(float from, float to, float dur)
{
if (_canvasGroup == null || dur <= 0f)
{
if (_canvasGroup != null) _canvasGroup.alpha = to;
yield break;
}
float t = 0f;
while (t < dur)
{
t += Time.unscaledDeltaTime;
_canvasGroup.alpha = Mathf.Lerp(from, to, t / dur);
yield return null;
}
_canvasGroup.alpha = to;
}
}
}