using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using TMPro; using DG.Tweening; // cần DOTween using BrewMonster.Scripts.UI.GamePlay; using BrewMonster.Scripts; public enum ImageResType { IMG_POPUPNUM = 0, IMG_HITMISSED, IMG_FACTION, IMG_PATEQUEST, IMG_LEVELUP, IMG_GOTEXP, IMG_GOTMONEY, IMG_DEADLYSTRIKE, IMG_GOTSP, IMG_INVALIDHIT, IMG_TEAMLEADER, IMG_BOOTHBAR, IMG_HPWARN, IMG_MPWARN, IMG_RETORT, IMG_IMMUNE, IMG_TEAMMATE, IMG_PKSTATE, IMG_GMFLAG, IMG_ATTACKLOSE, IMG_SUCCESS, IMG_REBOUND, IMG_BEAT_BACK, IMG_ADD, IMG_DODGE_DEBUFF, IMG_KING, NUM_IMAGE, } public class FLoatingTextManager : MonoBehaviour { public static FLoatingTextManager Instance { get; private set; } [Header("Prefab")] [SerializeField] private AUIFloatTextIcon floatTextIconPrefab; [Header("Settings")] [SerializeField] private int poolSize = 20; [SerializeField] private Vector3 offset = new Vector3(0, 2f, 0); [SerializeField] private float riseDistance = 1.5f; [SerializeField] private float riseDuration = 0.8f; /// /// Minimum spacing between floating texts for the same source object (seconds). /// 同一来源物体连续飘字的最小间隔(秒)。 /// [SerializeField] private float staggerIntervalSeconds = 0.3f; private readonly Queue pool = new(); /// /// Per-source next allowed show time (Unity instance ID → Time.time). /// private readonly Dictionary _nextFloatingTextTimeBySourceId = new(); [Header("Sprite List")] [SerializeField] private Dictionary imageDic = new Dictionary(); /// /// Keeps Addressables handles alive so sprites in imageDic are not released. /// 保留 Addressables 句柄,避免已加载的 Sprite 被卸载。 /// private readonly List> _spriteLoadHandles = new List>(); private void Awake() { // Singleton if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // Tạo sẵn pool for (int i = 0; i < poolSize; i++) { var textObj = Instantiate(floatTextIconPrefab, transform); textObj.gameObject.SetActive(false); pool.Enqueue(textObj); } LoadAllImages(); } private void OnDestroy() { foreach (var h in _spriteLoadHandles) { if (h.IsValid()) Addressables.Release(h); } _spriteLoadHandles.Clear(); } /// /// Gọi để spawn text damage /// /// If set, multiple calls for this object are spaced by . public void ShowText(Vector3 worldPos, int damage, Color color = default, float scale = 1f, ImageResType imageResType = ImageResType.NUM_IMAGE, UnityEngine.Object sourceForStagger = null) { float delay = 0f; UnityEngine.Object staggerSource = sourceForStagger; if (staggerSource != null) { int key = staggerSource.GetInstanceID(); float now = Time.time; if (!_nextFloatingTextTimeBySourceId.TryGetValue(key, out float nextSlot)) nextSlot = now; float showAt = Mathf.Max(now, nextSlot); delay = showAt - now; _nextFloatingTextTimeBySourceId[key] = showAt + staggerIntervalSeconds; } void DoShow() { if (staggerSource != null && !staggerSource) return; var text = GetFromPool(); var imageShow = imageResType == ImageResType.NUM_IMAGE ? null : imageDic[imageResType]; if (damage > 0) text.Show(worldPos, damage.ToString(), color, scale, riseDistance, riseDuration, imageShow, () => ReturnToPool(text)); else text.Show(worldPos, "", color, scale, riseDistance, riseDuration, imageShow, () => ReturnToPool(text)); } if (delay <= 0f) DoShow(); else DOVirtual.DelayedCall(delay, DoShow, false).SetTarget(this); } private AUIFloatTextIcon GetFromPool() { if (pool.Count > 0) { return pool.Dequeue(); } // Nếu hết pool, tạo thêm var text = Instantiate(floatTextIconPrefab, transform); text.gameObject.SetActive(false); return text; } private void ReturnToPool(AUIFloatTextIcon text) { pool.Enqueue(text); } public async Task LoadAllImages() { await LoadImage(ImageResType.IMG_HITMISSED, "InGame/未命中.tga"); await LoadImage(ImageResType.IMG_LEVELUP, "InGame/升级了.tga"); await LoadImage(ImageResType.IMG_GOTEXP, "InGame/经验.tga"); await LoadImage(ImageResType.IMG_GOTMONEY, "InGame/金钱.tga"); await LoadImage(ImageResType.IMG_DEADLYSTRIKE, "InGame/爆击.tga"); await LoadImage(ImageResType.IMG_GOTSP, "InGame/元神.tga"); await LoadImage(ImageResType.IMG_INVALIDHIT, "InGame/无效.tga"); //LoadImage(ImageResType.IMG_TEAMLEADER, "Window/LeaderMark.tga"); await LoadImage(ImageResType.IMG_HPWARN, "InGame/hp_warn.tga"); await LoadImage(ImageResType.IMG_MPWARN, "InGame/mp_warn.tga"); await LoadImage(ImageResType.IMG_RETORT, "InGame/反震.tga"); await LoadImage(ImageResType.IMG_IMMUNE, "InGame/免疫.tga"); //LoadImage(ImageResType.IMG_TEAMMATE, "Window/Teammate.tga"); await LoadImage(ImageResType.IMG_PKSTATE, "InGame/PK状态标记.tga"); await LoadImage(ImageResType.IMG_GMFLAG, "InGame/GM标志.dds"); await LoadImage(ImageResType.IMG_ATTACKLOSE, "InGame/失败.tga"); await LoadImage(ImageResType.IMG_SUCCESS, "InGame/成功.tga"); await LoadImage(ImageResType.IMG_REBOUND, "InGame/复仇惩戒.tga"); await LoadImage(ImageResType.IMG_BEAT_BACK, "InGame/复仇镜像.tga"); await LoadImage(ImageResType.IMG_ADD, "InGame/吸血.tga"); await LoadImage(ImageResType.IMG_DODGE_DEBUFF, "InGame/状态闪避.tga"); //LoadImage(ImageResType.IMG_KING, "King/皇冠图标.tga"); return false; } private async Task LoadImage(ImageResType type, string path) { if (string.IsNullOrEmpty(path)) return false; // Same normalization as skill/gfx paths (PC backslashes → Addressables-style slashes). // 与技能 gfx 路径一致:反斜杠转为斜杠,便于与 Addressables 地址对齐。 string normalized = path.Replace('\\', '/'); IReadOnlyList candidates = BuildSpriteAddressCandidates(normalized); foreach (string address in candidates) { // TODO: use AddressableManager to load the sprite. var sprite = await AddressableManager.Instance.LoadSpriteAsync(address); if (sprite != null) { imageDic[type] = sprite; return true; } } Debug.Log($"[FLoatingTextManager] Sprite load failed for {type}. Addressables keys must match the catalog exactly; " + $"tried: {string.Join("; ", candidates)}. "); return false; } /// /// Addressables uses exact string addresses. Imported UI art may drop .tga/.dds in the key; try those variants. /// Addressables 为精确字符串地址;导入后地址可能没有 .tga/.dds 等后缀,依次尝试这些候选。 /// private static List BuildSpriteAddressCandidates(string path) { var list = new List { path }; void addUnique(string s) { if (!string.IsNullOrEmpty(s) && !list.Contains(s)) list.Add(s); } string[] sourceExtensions = { ".tga", ".dds", ".bmp", ".png", ".jpg", ".jpeg" }; foreach (string ext in sourceExtensions) { if (path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) { addUnique(path.Substring(0, path.Length - ext.Length)); break; } } return list; } }