using System; using System.Collections.Generic; using BumpCombat.Enemies; using UnityEngine; namespace BumpCombat.Combat { [DefaultExecutionOrder(1000)] public sealed class ArtifactStatusVisual : MonoBehaviour { public const int StatusSortingOffset = 1; private const int FrameCount = 6; private const float PixelsPerUnit = 32f; private const string IgniteResourcePath = "Artifacts/ThreeColor-v1/StatusEffects/Ignite-Warlock-v1"; private const string ShockResourcePath = "Artifacts/ThreeColor-v1/StatusEffects/Shock-Warlock-v1"; private static readonly float[] IgniteFrameDurations = { 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, }; private static readonly float[] ShockFrameDurations = { 0.06f, 0.04f, 0.07f, 0.05f, 0.07f, 0.11f, }; private sealed class CachedFrames { public CachedFrames(Sprite[] frames, bool ownsSprites) { Frames = frames; OwnsSprites = ownsSprites; } public Sprite[] Frames { get; } public bool OwnsSprites { get; } } private static readonly Dictionary FrameCache = new(); private sealed class StatusSlot { public StatusSlot( string resourcePath, string objectName, float[] frameDurations) { ResourcePath = resourcePath; ObjectName = objectName; FrameDurations = frameDurations; } public string ResourcePath { get; } public string ObjectName { get; } public float[] FrameDurations { get; } public Sprite[] Frames; public GameObject Root; public SpriteRenderer Renderer; public int FrameIndex; public float FrameElapsed; } private readonly StatusSlot ignite = new( IgniteResourcePath, "Ignite Status Visual", IgniteFrameDurations); private readonly StatusSlot shock = new( ShockResourcePath, "Shock Status Visual", ShockFrameDurations); private EnemyController enemy; private SpriteRenderer sourceRenderer; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetFrameCache() { foreach (CachedFrames cached in FrameCache.Values) { if (!cached.OwnsSprites) { continue; } foreach (Sprite sprite in cached.Frames) { if (sprite == null) { continue; } if (Application.isPlaying) { Destroy(sprite); } else { DestroyImmediate(sprite); } } } FrameCache.Clear(); } private void Awake() { enemy = GetComponent(); sourceRenderer = GetComponent(); } private void OnEnable() { if (enemy == null) { enemy = GetComponent(); } if (sourceRenderer == null) { sourceRenderer = GetComponent(); } if (enemy != null) { enemy.OnDied += HandleEnemyDied; } } private void OnDisable() { if (enemy != null) { enemy.OnDied -= HandleEnemyDied; } ClearSlot(ignite); ClearSlot(shock); } private void LateUpdate() { if (enemy == null || sourceRenderer == null || enemy.IsDead || !gameObject.activeInHierarchy) { ClearSlot(ignite); ClearSlot(shock); return; } UpdateSlot(ignite, enemy.IsIgnited); UpdateSlot(shock, enemy.IsShocked); } private void HandleEnemyDied(EnemyController deadEnemy) { ClearSlot(ignite); ClearSlot(shock); } private void UpdateSlot(StatusSlot slot, bool active) { if (!active) { ClearSlot(slot); return; } if (slot.Root == null) { slot.Frames ??= LoadFrames(slot.ResourcePath); if (slot.Frames.Length == 0) { return; } CreateSlot(slot); } SyncSlot(slot); AdvanceSlot(slot, Time.deltaTime); } private void CreateSlot(StatusSlot slot) { slot.Root = new GameObject(slot.ObjectName); slot.Root.transform.SetParent(transform, false); slot.Root.transform.localPosition = Vector3.zero; slot.Renderer = slot.Root.AddComponent(); slot.Renderer.color = Color.white; slot.FrameIndex = 0; slot.FrameElapsed = 0f; slot.Renderer.sprite = slot.Frames[slot.FrameIndex]; } private void SyncSlot(StatusSlot slot) { if (slot.Root == null || slot.Renderer == null) { return; } Vector2 groundAnchor = enemy.GroundAnchorPosition; slot.Root.transform.SetPositionAndRotation( new Vector3(groundAnchor.x, groundAnchor.y, transform.position.z), Quaternion.identity); slot.Root.transform.localScale = Vector3.one; slot.Renderer.sortingLayerID = sourceRenderer.sortingLayerID; slot.Renderer.sortingOrder = sourceRenderer.sortingOrder + StatusSortingOffset; slot.Renderer.flipX = sourceRenderer.flipX; slot.Renderer.flipY = sourceRenderer.flipY; slot.Renderer.sharedMaterial = sourceRenderer.sharedMaterial; slot.Renderer.enabled = true; } private static void AdvanceSlot(StatusSlot slot, float deltaTime) { if (slot.Renderer == null || slot.Frames == null || slot.Frames.Length == 0 || deltaTime <= 0f) { return; } slot.FrameElapsed += deltaTime; while (slot.FrameElapsed >= slot.FrameDurations[slot.FrameIndex]) { slot.FrameElapsed -= slot.FrameDurations[slot.FrameIndex]; slot.FrameIndex = (slot.FrameIndex + 1) % slot.Frames.Length; slot.Renderer.sprite = slot.Frames[slot.FrameIndex]; } } private static void ClearSlot(StatusSlot slot) { if (slot.Root != null) { slot.Root.SetActive(false); Destroy(slot.Root); } slot.Root = null; slot.Renderer = null; slot.FrameIndex = 0; slot.FrameElapsed = 0f; } private static Sprite[] LoadFrames(string resourcePath) { if (FrameCache.TryGetValue(resourcePath, out CachedFrames cached)) { return cached.Frames; } Sprite[] importedFrames = Resources.LoadAll(resourcePath); if (importedFrames != null && importedFrames.Length >= FrameCount) { Sprite[] frames = new Sprite[FrameCount]; Array.Copy(importedFrames, frames, FrameCount); FrameCache[resourcePath] = new CachedFrames(frames, false); return frames; } Texture2D texture = importedFrames != null && importedFrames.Length > 0 ? importedFrames[0].texture : Resources.Load(resourcePath); if (texture == null || texture.width < FrameCount || texture.width % FrameCount != 0 || texture.height <= 0) { Sprite[] missing = Array.Empty(); FrameCache[resourcePath] = new CachedFrames(missing, false); return missing; } int frameWidth = texture.width / FrameCount; Sprite[] generatedFrames = new Sprite[FrameCount]; for (int i = 0; i < FrameCount; i++) { generatedFrames[i] = Sprite.Create( texture, new Rect(i * frameWidth, 0f, frameWidth, texture.height), new Vector2(0.5f, 0.125f), PixelsPerUnit, 0, SpriteMeshType.FullRect); generatedFrames[i].name = $"{resourcePath} Frame {i + 1}"; generatedFrames[i].hideFlags = HideFlags.HideAndDontSave; } FrameCache[resourcePath] = new CachedFrames(generatedFrames, true); return generatedFrames; } } }