using System; using UnityEngine; using BumpCombat.Constants; namespace BumpCombat.Defense { [DisallowMultipleComponent] public sealed class DefenseSpriteSequence : MonoBehaviour { private SpriteRenderer target; private Sprite[] frames; private string key; private float durationScale = 1f; private bool useUnscaledTime; private float frameRemaining; private int frame; private Sprite[] nextFrames; private string nextKey; private bool destroyWhenFinished; private float delayRemaining; private bool animationFinished; private Vector2 travelStart; private System.Func travelStartProvider; private Vector2 travelEnd; private float travelDuration; private float travelElapsed; private Action travelArrival; private System.Func travelEndProvider; private System.Func travelCanAdvance; private bool travelArrivalInvoked; private bool hideRendererAtArrival; private System.Action completionCallback; private bool finished; private bool fitsLightningBeam; private Vector2 beamStart; private Vector2 beamEnd; private float beamThickness; private bool centersImpactWave; private Vector2 impactWaveCenter; private float impactWaveScale; private bool fitsLightningStormGround; private Vector2 lightningStormGroundCenter; private float lightningStormGroundRadius; public void Play(SpriteRenderer renderer, string sequenceKey, Sprite[] sequenceFrames, bool destroyOnFinish, string followingKey = null, Sprite[] followingFrames = null, float startDelay = 0f) { enabled = true; completionCallback = null; finished = false; animationFinished = false; durationScale = 1f; useUnscaledTime = false; target = renderer; key = sequenceKey; frames = sequenceFrames; destroyWhenFinished = destroyOnFinish; nextKey = followingKey; nextFrames = followingFrames; delayRemaining = Mathf.Max(0f, startDelay); travelStartProvider = null; travelEndProvider = null; travelCanAdvance = null; travelArrival = null; travelDuration = 0f; travelElapsed = 0f; travelArrivalInvoked = false; hideRendererAtArrival = false; if (target == null || frames == null || frames.Length == 0) { FinishSequence(); return; } frame = 0; target.sprite = frames[frame]; target.enabled = delayRemaining <= 0f; frameRemaining = DefenseTowerArt.FrameSeconds(key, frame) * durationScale; UpdateVisualAlignment(); } public void FitLightningBeam(Vector2 start, Vector2 end, float thickness) { fitsLightningBeam = true; beamStart = start; beamEnd = end; beamThickness = thickness; UpdateVisualAlignment(); } public void CenterImpactWave(Vector2 center, float uniformScale) { centersImpactWave = true; impactWaveCenter = center; impactWaveScale = uniformScale; UpdateVisualAlignment(); } public void FitLightningStormGround(Vector2 center, float radius) { fitsLightningStormGround = true; lightningStormGroundCenter = center; lightningStormGroundRadius = Mathf.Max(0f, radius); UpdateVisualAlignment(); } public bool IsFinished => finished; public void SetPlaybackTiming(float newDurationScale, bool useUnscaledTime) { if (float.IsNaN(newDurationScale) || float.IsInfinity(newDurationScale) || newDurationScale <= 0f) throw new ArgumentOutOfRangeException(nameof(newDurationScale), "Playback duration scale must be positive and finite."); float previousScale = durationScale; durationScale = newDurationScale; this.useUnscaledTime = useUnscaledTime; if (!finished && frames != null && frames.Length > 0) frameRemaining *= durationScale / previousScale; } public void SetCompletionCallback(System.Action callback) { if (callback == null) return; if (finished) callback(); else completionCallback += callback; } public void SetCanAdvance(System.Func canAdvance) { travelCanAdvance = canAdvance; } public void Travel(Vector2 start, Vector2 end, float duration, Action onArrival = null, System.Func endProvider = null, System.Func canAdvance = null, bool hideAtArrival = false, System.Func startProvider = null) { travelStart = start; travelStartProvider = startProvider; travelEnd = end; travelDuration = Mathf.Max(0.001f, duration); travelElapsed = 0f; travelArrival = onArrival; travelEndProvider = endProvider; travelCanAdvance = canAdvance ?? travelCanAdvance; travelArrivalInvoked = false; hideRendererAtArrival = hideAtArrival; if (delayRemaining <= 0f) ResolveTravelStart(); else transform.position = start; } private void ResolveTravelStart() { if (travelStartProvider != null) { travelStart = travelStartProvider(); travelStartProvider = null; } transform.position = travelStart; } private void Update() { if (target == null || frames == null || frames.Length == 0) { FinishSequence(); return; } if (travelCanAdvance != null && !travelCanAdvance()) return; float deltaTime = useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; if (delayRemaining > 0f) { delayRemaining -= deltaTime; if (delayRemaining > 0f) return; if (travelDuration > 0f || travelStartProvider != null) ResolveTravelStart(); target.enabled = true; } if (travelDuration > 0f && travelElapsed < travelDuration) { travelElapsed += deltaTime; Vector2 currentEnd = travelEndProvider != null ? travelEndProvider() : travelEnd; transform.position = Vector2.Lerp( travelStart, currentEnd, Mathf.Clamp01(travelElapsed / travelDuration)); if (travelElapsed >= travelDuration && !travelArrivalInvoked) { travelArrivalInvoked = true; if (hideRendererAtArrival && target != null) target.enabled = false; travelArrival?.Invoke(); } } if (animationFinished) { if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence(); return; } frameRemaining -= deltaTime; while (frameRemaining <= 0f) { frame++; if (frame >= frames.Length) { if (nextFrames != null && nextFrames.Length > 0) { frames = nextFrames; key = nextKey; nextFrames = null; frame = 0; } else { animationFinished = true; if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence(); return; } } target.sprite = frames[frame]; UpdateVisualAlignment(); frameRemaining += DefenseTowerArt.FrameSeconds(key, frame) * durationScale; } } private void UpdateVisualAlignment() { if (target == null || target.sprite == null) return; if (fitsLightningBeam) { Vector2 direction = beamEnd - beamStart; float distance = direction.magnitude; Vector2Int bounds = DefenseTowerArtBounds.LightningBeamXBounds(frame); float pixelsPerUnit = target.sprite.pixelsPerUnit; float visibleWidth = (bounds.y - bounds.x) / pixelsPerUnit; float scaleX = distance <= 0f || visibleWidth <= 0f ? 0f : distance / visibleWidth; float localStart = (bounds.x - target.sprite.pivot.x) / pixelsPerUnit * scaleX; transform.rotation = Quaternion.Euler(0f, 0f, distance > 0f ? Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg : 0f); transform.position = beamStart - (Vector2)(transform.right * localStart); transform.localScale = new Vector3(scaleX, beamThickness, 1f); } if (fitsLightningStormGround) { float scale = lightningStormGroundRadius * target.sprite.pixelsPerUnit / DefenseTowerArtBounds.LightningStormGroundRadiusPixels; transform.localScale = Vector3.one * scale; Vector2 localCenter = (DefenseTowerArtBounds.LightningStormGroundCenterPixels - target.sprite.pivot) / target.sprite.pixelsPerUnit; transform.position = lightningStormGroundCenter - (Vector2)transform.TransformVector(localCenter); } if (centersImpactWave) { transform.localScale = Vector3.one * impactWaveScale; Vector2 localCenter = (DefenseTowerArtBounds.ImpactWaveCenterPixels(frame, target.sprite) - target.sprite.pivot) / target.sprite.pixelsPerUnit; transform.position = impactWaveCenter - (Vector2)transform.TransformVector(localCenter); } } private void FinishSequence() { if (finished) return; finished = true; if (destroyWhenFinished) { Destroy(gameObject); } else { if (target != null) target.enabled = false; enabled = false; } System.Action callback = completionCallback; completionCallback = null; callback?.Invoke(); } } }