Files
test/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs
T
2026-04-08 17:42:02 +07:00

169 lines
8.4 KiB
C#

using TMPro;
using UnityEngine;
namespace BrewMonster.Scripts.Chat.EmotionData
{
/// <summary>
/// Ánh xạ (emotionSet, emotionIndex) từ <see cref="EmotionLibrarySO"/> sang tag TMP.
/// Maps (emotionSet, emotionIndex) from EmotionLibrarySO to TMP rich-text tag.
///
/// TMP_SpriteAsset của mỗi bộ giờ nằm trực tiếp trong EmotionSetSnapshot.TmpSpriteAsset —
/// chỉ cần gán Library vào SO này là đủ.
/// Each set's TMP_SpriteAsset now lives directly in EmotionSetSnapshot.TmpSpriteAsset —
/// only the Library reference is needed here.
///
/// Cách dùng:
/// 1) Tạo EmotionLibrarySO bằng Emotion Atlas Converter.
/// 2) Tạo TMP_SpriteAsset cho từng atlas, gán vào EmotionSetSnapshot.TmpSpriteAsset trong Library.
/// 3) Kéo SO này vào field trên GameObject có CECUIManager (Awake gọi SetEmotionSpriteMap).
/// </summary>
[CreateAssetMenu(fileName = "EmotionLibrarySpriteMap", menuName = "Perfect World/Chat/Emotion Library Sprite Map", order = 2)]
public class EmotionLibrarySpriteMap : ScriptableObject, IEmotionSpriteMap
{
[Tooltip("Dữ liệu emotion đã convert — TmpSpriteAsset của từng bộ nằm trong EmotionSetSnapshot. " +
"Converted emotion data — each set's TmpSpriteAsset lives inside EmotionSetSnapshot.")]
public EmotionLibrarySO Library;
[Tooltip("Nếu true và emoji chỉ 1 frame: dùng <sprite name=\"…\"> (không cần tra index). " +
"Emoji động (nhiều frame) vẫn cần TmpSpriteAsset trong EmotionSetSnapshot.")]
public bool PreferSpriteNameTag = true;
[Tooltip("FPS mặc định cho <sprite anim> khi không suy ra được từ FrameTicks.")]
public int DefaultAnimFps = 10;
// ─────────────────────────────────────────────────────────────────────
// IEmotionSpriteMap
// ─────────────────────────────────────────────────────────────────────
public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info)
{
info = default;
if (Library == null)
{
Debug.LogWarning($"[Cuong] 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($"[Cuong] 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<string>())}]");
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($"[Cuong] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: entry.FrameSprites null/empty. Chạy lại Emotion Atlas Converter để populate FrameSprites.");
return false;
}
if (frames.Length == 1)
{
Sprite s0 = frames[0];
if (s0 == null)
{
Debug.LogWarning($"[Cuong] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frames[0] == null.");
return false;
}
if (PreferSpriteNameTag && !string.IsNullOrEmpty(s0.name))
{
info = new EmotionSpriteInfo { UseSpriteName = true, SpriteName = s0.name, IsAnimated = false };
return true;
}
// PreferSpriteNameTag=false → tra index trong TmpSpriteAsset của set
int idx = FindSpriteIndex(set.TmpSpriteAsset, s0.name);
if (idx < 0)
{
Debug.LogWarning($"[Cuong] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: không tìm thấy sprite \"{s0.name}\" trong TMP asset " +
(set.TmpSpriteAsset == null ? "(null — gán TmpSpriteAsset vào EmotionSetSnapshot trong Library)." : $"\"{set.TmpSpriteAsset.name}\"."));
return false;
}
info = new EmotionSpriteInfo { SpriteIndex = idx, IsAnimated = false };
return true;
}
// Multi-frame (animated) — bắt buộc phải có TmpSpriteAsset trong set để tra index
TMP_SpriteAsset animAsset = set.TmpSpriteAsset;
if (animAsset == null)
{
Debug.LogWarning($"[Cuong] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: EmotionSetSnapshot[{emotionSet}].TmpSpriteAsset == null " +
$"(emoji động {frames.Length} frames cần TmpSpriteAsset để tra index). Gán TmpSpriteAsset vào EmotionSetSnapshot trong Library.");
return false;
}
int start = FindSpriteIndex(animAsset, frames[0]?.name);
int end = FindSpriteIndex(animAsset, frames[frames.Length - 1]?.name);
if (start < 0 || end < 0)
{
Debug.LogWarning($"[Cuong] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frame không tìm thấy trong \"{animAsset.name}\". " +
$"start=\"{frames[0]?.name}\"→{start}, end=\"{frames[frames.Length - 1]?.name}\"→{end}.");
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;
}
// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Tra tên sprite trong spriteCharacterTable của asset, trả về index hoặc -1.
/// Look up sprite name in the asset's spriteCharacterTable; returns index or -1.
/// </summary>
private static int FindSpriteIndex(TMP_SpriteAsset asset, string spriteName)
{
if (asset == null || string.IsNullOrEmpty(spriteName))
return -1;
// TMP 3.x: dùng spriteCharacterTable (spriteInfoList đã deprecated)
// TMP 3.x: use spriteCharacterTable (spriteInfoList is deprecated)
var charTable = asset.spriteCharacterTable;
if (charTable == null)
{
BMLogger.LogError("[Cuong] charTable == null");
return -1;
}
BMLogger.Log($"[Cuong] {spriteName}");
for (int i = 0; i < charTable.Count; i++)
{
if (charTable[i] != null && charTable[i].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 span = frameTicks[numFrames - 1] - frameTicks[0];
if (span <= 0)
return 0;
return Mathf.Max(1, Mathf.RoundToInt((numFrames - 1) * 60f / span));
}
}
}