using UnityEngine; namespace BrewMonster.Scripts.Chat.EmotionData { /// /// Ánh xạ (emotionSet, emotionIndex) từ sang tag TMP. /// Maps (emotionSet, emotionIndex) from EmotionLibrarySO to TMP rich-text tag. /// /// Atlas được cắt trái→phải, trên→dưới, sub-sprite đặt tên cell_XXXX. /// Số XXXX trong tên chính là index tuyến tính → không cần tra spriteCharacterTable. /// Atlas is sliced left-to-right, top-to-bottom, named cell_XXXX. /// The number XXXX in the name IS the linear index → no spriteCharacterTable lookup needed. /// /// Cách dùng: /// 1) Tạo EmotionLibrarySO bằng Emotion Atlas Converter. /// 2) Gán TMP_SpriteAsset cho từng set vào EmotionSetSnapshot.TmpSpriteAsset trong Library /// (chỉ cần cho emoji động nhiều frame hoặc khi PreferSpriteNameTag = false). /// 3) Kéo SO này vào field trên GameObject có CECUIManager (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. Emotion data converted by the atlas tool.")] public EmotionLibrarySO Library; [Tooltip("Nếu true và emoji chỉ 1 frame: dùng thay vì . " + "If true and single-frame: use instead of .")] public bool PreferSpriteNameTag = true; [Tooltip("FPS mặc định cho khi không suy ra được từ FrameTicks. " + "Default FPS for when it cannot be derived from FrameTicks.")] public int DefaultAnimFps = 10; // ───────────────────────────────────────────────────────────────────── // IEmotionSpriteMap // ───────────────────────────────────────────────────────────────────── public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info) { info = default; if (Library == null) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Library == null. Gán EmotionLibrarySO vào field Library."); return false; } EmotionSetSnapshot set = Library.GetSetOrNull(emotionSet); if (set == null) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Không tìm thấy set index={emotionSet}. " + $"Sets hiện có: [{string.Join(", ", Library.Sets?.ConvertAll(s => s?.EmotionSetIndex.ToString() ?? "null") ?? new System.Collections.Generic.List())}]"); return false; } if (emotionIndex < 0 || emotionIndex >= set.Entries.Count) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: emotionIndex={emotionIndex} ngoài range [0, {set.Entries.Count - 1}]."); return false; } EmotionEntryData entry = set.Entries[emotionIndex]; Sprite[] frames = entry.FrameSprites; if (frames == null || frames.Length == 0) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: entry.FrameSprites null/empty. Chạy lại Emotion Atlas Converter để populate FrameSprites."); return false; } // Dùng StartPos/NumFrames từ txt (khớp EmotionAtlasConverterCore: cellIndex = StartPos + f). // Use StartPos/NumFrames from txt (matches EmotionAtlasConverterCore: cellIndex = StartPos + f). // Không đọc Sprite.name — GetName() chỉ được gọi trên main thread (vd. gói chat từ network thread). // Do not read Sprite.name — GetName() is main-thread-only (e.g. chat packets on network thread). int startCell = entry.StartPos; int frameCount = frames.Length; if (startCell < 0 || frameCount < 1) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: StartPos={startCell} hoặc không có frame."); return false; } // ── Single frame ────────────────────────────────────────────────── if (frameCount == 1) { if (frames[0] == null) { Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frames[0] == null."); return false; } if (PreferSpriteNameTag) { // — TMP tra tên trong SpriteAsset được gán trên component. // — TMP looks up the name in the SpriteAsset on the component. info = new EmotionSpriteInfo { UseSpriteName = true, SpriteName = FormatCellSpriteName(startCell), IsAnimated = false }; return true; } info = new EmotionSpriteInfo { SpriteIndex = startCell, IsAnimated = false }; return true; } // ── Multi-frame (animated) ──────────────────────────────────────── int endCell = startCell + frameCount - 1; int fps = EstimateFps(entry.FrameTicks, frameCount); info = new EmotionSpriteInfo { SpriteIndex = startCell, IsAnimated = true, AnimEndFrame = endCell, AnimFPS = fps > 0 ? fps : DefaultAnimFps }; return true; } // ───────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────── /// /// Tên ô giống tool atlas: "cell_XXXX" (4 chữ số). Không đọc Unity Sprite.name. /// Same cell naming as atlas tool: "cell_XXXX" (4 digits). Does not read Unity Sprite.name. /// private static string FormatCellSpriteName(int cellIndex) => $"cell_{cellIndex:D4}"; private static int EstimateFps(int[] frameTicks, int numFrames) { if (frameTicks == null || numFrames < 2 || frameTicks.Length < numFrames) return 0; int span = frameTicks[numFrames - 1] - frameTicks[0]; if (span <= 0) return 0; return Mathf.Max(1, Mathf.RoundToInt((numFrames - 1) * 60f / span)); } } }