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

155 lines
8.4 KiB
C#

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.
///
/// 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).
/// </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. Emotion data converted by the atlas tool.")]
public EmotionLibrarySO Library;
[Tooltip("Nếu true và emoji chỉ 1 frame: dùng <sprite name=\"cell_XXXX\"> thay vì <sprite index=N>. " +
"If true and single-frame: use <sprite name=\"cell_XXXX\"> instead of <sprite index=N>.")]
public bool PreferSpriteNameTag = true;
[Tooltip("FPS mặc định cho <sprite anim> khi không suy ra được từ FrameTicks. " +
"Default FPS for <sprite anim> 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<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($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: entry.FrameSprites null/empty. Chạy lại Emotion Atlas Converter để populate FrameSprites.");
return false;
}
// ── Single frame ──────────────────────────────────────────────────
if (frames.Length == 1)
{
Sprite s0 = frames[0];
if (s0 == null)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frames[0] == null.");
return false;
}
if (PreferSpriteNameTag)
{
// <sprite name="cell_XXXX"> — TMP tra tên trong SpriteAsset được gán trên component.
// <sprite name="cell_XXXX"> — TMP looks up the name in the SpriteAsset on the component.
info = new EmotionSpriteInfo { UseSpriteName = true, SpriteName = s0.name, IsAnimated = false };
return true;
}
// PreferSpriteNameTag=false → dùng <sprite index=N>, parse N từ tên cell_XXXX.
// PreferSpriteNameTag=false → use <sprite index=N>, parse N from cell_XXXX name.
int idx = ParseCellIndex(s0.name);
if (idx < 0)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: không parse được index từ tên sprite \"{s0.name}\". Tên phải có dạng cell_XXXX.");
return false;
}
info = new EmotionSpriteInfo { SpriteIndex = idx, IsAnimated = false };
return true;
}
// ── Multi-frame (animated) ────────────────────────────────────────
// Parse start/end index trực tiếp từ tên cell_XXXX — không cần tra spriteCharacterTable.
// Parse start/end index directly from cell_XXXX name — no spriteCharacterTable lookup.
int start = ParseCellIndex(frames[0]?.name);
int end = ParseCellIndex(frames[frames.Length - 1]?.name);
if (start < 0 || end < 0)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: không parse được index từ tên frame. " +
$"start=\"{frames[0]?.name}\"→{start}, end=\"{frames[frames.Length - 1]?.name}\"→{end}. Tên phải có dạng cell_XXXX.");
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>
/// Parse linear index từ tên dạng "cell_XXXX" (atlas cắt trái→phải, trên→dưới).
/// Parse linear index from a "cell_XXXX" sprite name (atlas sliced left-to-right, top-to-bottom).
/// Trả về -1 nếu tên không hợp lệ. Returns -1 if the name is invalid.
/// </summary>
private static int ParseCellIndex(string spriteName)
{
const string prefix = "cell_";
if (!string.IsNullOrEmpty(spriteName)
&& spriteName.StartsWith(prefix)
&& int.TryParse(spriteName.Substring(prefix.Length), out int idx))
return idx;
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));
}
}
}