Files
test/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs
T
2026-04-09 17:05:39 +07:00

152 lines
8.0 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;
}
string spriteAssetName = set.TmpSpriteAsset != null ? set.TmpSpriteAsset.name : string.Empty;
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)
{
// <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
{
SpriteAssetName = spriteAssetName,
UseSpriteName = true,
SpriteName = FormatCellSpriteName(startCell),
IsAnimated = false
};
return true;
}
info = new EmotionSpriteInfo
{
SpriteAssetName = spriteAssetName,
SpriteIndex = startCell,
IsAnimated = false
};
return true;
}
// ── Multi-frame (animated) ────────────────────────────────────────
int endCell = startCell + frameCount - 1;
int fps = EstimateFps(entry.FrameTicks, frameCount);
info = new EmotionSpriteInfo
{
SpriteAssetName = spriteAssetName,
SpriteIndex = startCell,
IsAnimated = true,
AnimEndFrame = endCell,
AnimFPS = fps > 0 ? fps : DefaultAnimFps
};
return true;
}
// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Tên ô giống tool atlas: "cell_XXXX" (4 chữ số). Không đọc Unity <c>Sprite.name</c>.
/// Same cell naming as atlas tool: "cell_XXXX" (4 digits). Does not read Unity Sprite.name.
/// </summary>
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));
}
}
}