using System.Collections.Generic; using Ichni.Story.Dialogue; using TMPro; using UnityEngine; using UnityEngine.Localization; using UnityEngine.UI; namespace Ichni.Story.UI { /// /// 文本块视图。点击后通过 Yarn Spinner 进入对应节点的对话。 /// public class TextBlockView : StoryBlockView { [Header("Text Block")] public TMP_Text storyIdText; public TMP_Text titleText; [Tooltip("各状态对应的背景预制体。未配置 Forbidden 时会自动回退到 Locked 背景。")] public Dictionary blockVisuals = new Dictionary(); /// 该文本块对应的 Yarn 节点名。 public string YarnNodeName { get; private set; } // 当前已实例化的状态背景对象;状态切换时销毁并替换 private GameObject _currentVisual; // 直接持有 StoryData 的 LocalizedString,并订阅其 StringChanged 回调。 // Unity Localization 会在首次订阅和语言切换时自行异步刷新文本。 private LocalizedString _titleReference; private bool _isTitleLocalizationSubscribed; private void OnEnable() { StartTitleLocalization(); } private void OnDisable() { StopTitleLocalization(); } public override void Initialize(StoryBlockDefinition def, Vector2 position, StoryBlockState blockState) { // 注意:base.Initialize 末尾会调用 ApplyState(state),届时已生成对应背景 YarnNodeName = def.yarnNodeName; base.Initialize(def, position, blockState); if (storyIdText != null) storyIdText.text = def.blockId; StopTitleLocalization(); _titleReference = def.title; StartTitleLocalization(); } /// /// 订阅当前 TextBlock 的 LocalizedString。无需手写 Table / Key 读取或监听全局 Locale, /// LocalizedString 会遵循 Unity 官方生命周期异步加载,并在语言变化后触发 StringChanged。 /// private void StartTitleLocalization() { if (_isTitleLocalizationSubscribed || titleText == null) return; if (_titleReference == null || _titleReference.IsEmpty) { titleText.text = blockId; return; } _titleReference.StringChanged += ApplyLocalizedTitle; _isTitleLocalizationSubscribed = true; } private void StopTitleLocalization() { if (!_isTitleLocalizationSubscribed || _titleReference == null) return; _titleReference.StringChanged -= ApplyLocalizedTitle; _isTitleLocalizationSubscribed = false; } private void ApplyLocalizedTitle(string localizedTitle) { if (titleText != null) titleText.text = string.IsNullOrEmpty(localizedTitle) ? blockId : localizedTitle; } /// /// 应用状态:在基类逻辑(按钮可交互性)之外,切换到对应状态的背景视觉。 /// public override void ApplyState(StoryBlockState newState) { base.ApplyState(newState); UpdateVisual(newState); } /// /// 按当前状态实例化对应的背景预制体,作为第一个子物体(位于文本/端口/按钮之下), /// 并拉伸铺满整个 block。状态切换时先销毁旧背景。 /// private void UpdateVisual(StoryBlockState newState) { if (_currentVisual != null) { // 旧背景中的 Button 即将被销毁,先主动解绑,防止基类继续持有失效引用。 BindInteractionButton(null); Destroy(_currentVisual); _currentVisual = null; } if (blockVisuals == null) { Debug.LogWarning($"[TextBlockView] block '{blockId}' 缺少状态 {newState} 的背景预制体(blockVisuals 未配置)。"); return; } // Forbidden 专用美术尚未配置前沿用 Locked 背景,保证互斥路线 Block 始终可见且不可点击。 // 后续只需在 Inspector 中补充 Forbidden 键,即可自动覆盖此回退表现。 if ((!blockVisuals.TryGetValue(newState, out GameObject prefab) || prefab == null) && newState == StoryBlockState.Forbidden) { blockVisuals.TryGetValue(StoryBlockState.Locked, out prefab); } if (prefab == null) { Debug.LogWarning($"[TextBlockView] block '{blockId}' 缺少状态 {newState} 的背景预制体。"); return; } _currentVisual = Instantiate(prefab, blockRect); _currentVisual.transform.SetAsFirstSibling(); // 状态视觉可自由调整层级;不再依赖“第一个子物体就是 Button”的脆弱约定。 // 通过基类重新绑定,确保每一次状态背景替换后 TextBlock 仍能响应点击。 Button interactionButton = _currentVisual.GetComponentInChildren