using System; using TMPro; using UnityEngine; namespace BrewMonster.Scripts.Chat.EmotionData { /// /// Ánh xạ (emotionSet, emotionIndex) từ sang tag TMP. /// Gán asset này vào (field emotion) — không phải MonoBehaviour nên không kéo SO trên Inspector được. /// /// Cách dùng: /// 1) Tạo bằng Emotion Atlas Converter. /// 2) Tạo từ cùng atlas (Window → TextMeshPro → Sprite Importer), /// đảm bảo tên sub-sprite khớp (vd. cell_0000) với atlas Multiple từ tool. /// 3) Gán TMP_SpriteAsset vào ô chat (Sprite Asset / Additional). /// 4) Kéo SO này vào field trên GameObject có (Awake gọi SetEmotionSpriteMap). /// [CreateAssetMenu(fileName = "EmotionLibrarySpriteMap", menuName = "Perfect World/Chat/Emotion Library Sprite Map", order = 2)] public class EmotionLibrarySpriteMap : ScriptableObject, IEmotionSpriteMap { [Tooltip("Dữ liệu emotion đã convert (Entries + FrameSprites).")] public EmotionLibrarySO Library; [Tooltip("Sprite Asset dùng cho chat TMP — cùng tên sub-sprite với atlas (cell_XXXX). Bắt buộc cho emoji động (anim) hoặc khi không dùng name tag.")] public TMP_SpriteAsset TmpSpriteAsset; [Tooltip("Nếu true và chỉ 1 frame: thử (không cần tra index). Động (nhiều frame) vẫn cần TmpSpriteAsset để tra index.")] public bool PreferSpriteNameTag = true; [Tooltip("FPS mặc định cho khi không suy ra được từ FrameTicks.")] public int DefaultAnimFps = 10; public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info) { info = default; if (Library == null) return false; EmotionSetSnapshot set = Library.GetSetOrNull(emotionSet); if (set == null || emotionIndex < 0 || emotionIndex >= set.Entries.Count) return false; EmotionEntryData entry = set.Entries[emotionIndex]; Sprite[] frames = entry.FrameSprites; if (frames == null || frames.Length == 0) return false; if (frames.Length == 1) { Sprite s0 = frames[0]; if (s0 == null) return false; if (PreferSpriteNameTag && !string.IsNullOrEmpty(s0.name)) { info = new EmotionSpriteInfo { UseSpriteName = true, SpriteName = s0.name, IsAnimated = false }; return true; } int idx = FindSpriteIndexInTmpAsset(s0.name); if (idx < 0) return false; info = new EmotionSpriteInfo { SpriteIndex = idx, IsAnimated = false }; return true; } if (TmpSpriteAsset == null) return false; int start = FindSpriteIndexInTmpAsset(frames[0]?.name); int end = FindSpriteIndexInTmpAsset(frames[frames.Length - 1]?.name); if (start < 0 || end < 0) return false; int fps = EstimateFps(entry.FrameTicks, frames.Length); info = new EmotionSpriteInfo { SpriteIndex = start, IsAnimated = true, AnimEndFrame = end, AnimFPS = fps > 0 ? fps : DefaultAnimFps }; return true; } private int FindSpriteIndexInTmpAsset(string spriteName) { if (TmpSpriteAsset == null || string.IsNullOrEmpty(spriteName)) return -1; if (TmpSpriteAsset.spriteInfoList != null) { for (int i = 0; i < TmpSpriteAsset.spriteInfoList.Count; i++) { var s = TmpSpriteAsset.spriteInfoList[i]; if (s != null && s.name == spriteName) return i; } } return -1; } private static int EstimateFps(int[] frameTicks, int numFrames) { if (frameTicks == null || numFrames < 2 || frameTicks.Length < numFrames) return 0; int last = frameTicks[numFrames - 1]; int first = frameTicks[0]; int span = last - first; if (span <= 0) return 0; return Mathf.Max(1, Mathf.RoundToInt((numFrames - 1) * 60f / span)); } } }