using Ichni; using Ichni.Menu; using TMPro; using UnityEngine; using UnityEngine.UI; namespace Ichni.Story.UI { /// /// 剧情树中的歌曲 Block 视图。 /// 它只保存 这一个稳定歌曲标识;歌曲名称、作曲者与插画 /// 均从当前章节的 读取,避免 StoryData 与选曲资料出现两份可漂移的数据。 /// 点击后切换到选曲并自动选中对应歌曲的流程将在后续阶段接入。 /// public class SongBlockView : StoryBlockView { [Header("Song Block")] [Tooltip("Mask 下的 16:9 插画 Image。Sprite 由当前章节的 SongItemData.illustration 提供。")] public Image illustrationImage; [Tooltip("illustrationImage 对应的 RectTransform。仅修改其 anchoredPosition.y 以完成纵向裁切。")] public RectTransform illustrationRect; public TMP_Text songNameText; public TMP_Text composerText; [Tooltip("歌曲或剧情 Block 不可进入时显示的遮罩节点。")] public GameObject lockOverlay; private ChapterSelectionUnit _chapter; private SongItemData _song; private bool _canEnterSong; public override void Initialize(StoryBlockDefinition def, Vector2 position, StoryBlockState blockState) { base.Initialize(def, position, blockState); if (TryResolveSong(out _chapter, out _song)) { illustrationImage.sprite = _song.illustration; songNameText.text = _song.displaySongName; composerText.text = _song.composer; } else { // 配置错误时保留稳定 ID 以便定位问题,同时锁定 Block,避免从剧情页绕过内容授权。 illustrationImage.sprite = null; songNameText.text = def.songName; composerText.text = string.Empty; } // 数据存储的是最终 local Y,而非叠加在 Prefab 默认位置上的偏移量。 // Clamp 仍保留作为运行时防线:即使旧资产或外部工具写入越界值,也不会在 Mask 边缘露出空白。 Vector2 illustrationPosition = illustrationRect.anchoredPosition; illustrationPosition.y = Mathf.Clamp(def.illustrationLocalY, -74.25f, 74.25f); illustrationRect.anchoredPosition = illustrationPosition; ApplyState(blockState); } /// /// StoryBlock 的剧情状态和歌曲的内容解锁状态必须同时满足,才能真正点击进入歌曲。 /// 复用选曲页面的统一授权判断,防止 Story SongBlock 成为绕过入口。 /// public override void ApplyState(StoryBlockState newState) { base.ApplyState(newState); bool storyAllowsInteraction = state != StoryBlockState.Locked && state != StoryBlockState.Forbidden; _canEnterSong = _chapter != null && _song != null && GameSaveManager.instance != null && GameSaveManager.instance.UnlockSaveModule != null && GameSaveManager.instance.UnlockSaveModule.CanEnterSong(_chapter, _song); bool canInteract = storyAllowsInteraction && _canEnterSong; button.interactable = canInteract; lockOverlay.SetActive(!canInteract); } /// /// 根据正在构建的剧情章节索引定位歌曲,而不是盲目信任 currentChapter。 /// 这样即使未来从其它页面直接打开 StoryData,也不会把同名歌曲解析到上一次残留的章节中。 /// private bool TryResolveSong(out ChapterSelectionUnit chapter, out SongItemData song) { chapter = null; song = null; string chapterIndex = StoryManager.instance?.treeController?.ActiveChapterIndex; ChapterSelectionManager chapterManager = ChapterSelectionManager.instance; if (string.IsNullOrEmpty(chapterIndex) || chapterManager == null) { Debug.LogWarning($"[SongBlockView] 无法解析歌曲 Block '{blockId}':当前章节或 ChapterSelectionManager 不可用。"); return false; } foreach (ChapterSelectionUnit candidate in chapterManager.chapters) { if (candidate != null && candidate.chapterIndex == chapterIndex) { chapter = candidate; break; } } if (chapter == null) { Debug.LogWarning($"[SongBlockView] 无法解析歌曲 Block '{blockId}':找不到章节 '{chapterIndex}' 的 ChapterSelectionUnit。"); return false; } foreach (SongItemData candidate in chapter.songs) { if (candidate != null && candidate.songName == definition.songName) { song = candidate; return true; } } Debug.LogWarning($"[SongBlockView] 无法解析歌曲 Block '{blockId}':章节 '{chapterIndex}' 中不存在歌曲 ID '{definition.songName}'。"); return false; } protected override void OnClicked() { if (!_canEnterSong) { return; } // 占位:后续接入曲目选择流程 Debug.Log($"[SongBlockView] 点击音乐块 '{blockId}' (song='{definition.songName}')。占位实现,曲目选择将在后续阶段接入。"); } } }