using System; using System.Collections.Generic; using Cthangover.Core.UI.Event; using Cthangover.Core.Utils; using Godot; namespace Cthangover.Mods.Effects.Animation { /// /// Frame-by-frame sprite animation via crossfade between two TextureRect nodes. /// Rather than using a spritesheet, it keeps a "current" and "next" texture and /// lerps their modulate between transparent and opaque, creating smooth transitions. /// A built-in wait/waitPercent mechanism holds the "next" frame fully visible for /// a configurable interval before advancing, producing a paced flip-book effect. /// Exposes NextFrame/NextCycle events so external systems can sequence or react /// to animation milestones without polling. /// public partial class AnimationController : Control, IOnUpdateEvent { [Export] private TextureRect current; [Export] private TextureRect next; [Export] private Texture2D empty; [Export] private bool isLoop = true; [Export] private float speed = 1f; [Export] private float nextFrameSpeed = 0.2f; /// Fires when the current frame transition completes and the next frame texture is assigned. public event Action NextFrame; /// Fires when the animation wraps around (looping) or reaches the final frame (non-looping). public event Action NextCycle; [Export] private Color clearColor = new(1f, 1f, 1f, 0f); [Export] private Color completeColor = new(1f, 1f, 1f, 1f); private int currentFrame; private double timestamp; private float percent; private bool wait; private float waitPercent; private bool isStarted; /// When true, the animation wraps from the last frame back to the first. When false, playback stops at the final frame. public bool IsLoop { get => isLoop; set => isLoop = value; } /// Crossfade speed multiplier between frames. Higher values produce faster transitions. public float Speed { get => speed; set => speed = value; } /// Hold duration multiplier for the "wait" phase after a frame transition completes. public float NextFrameSpeed { get => nextFrameSpeed; set => nextFrameSpeed = value; } /// Whether playback is currently active. False on startup and after or . public bool IsStarted => isStarted; /// Ordered list of textures to cycle through. Set externally before calling . public List FrameSet { get; set; } /// Resets to idle state: stops playback, clears frame set, and blanks both texture nodes to transparent. public void Clear() { isStarted = false; FrameSet = null; current.Texture = empty; current.Modulate = Colors.Transparent; next.Texture = empty; next.Modulate = Colors.Transparent; } /// Stops playback without clearing textures, freezing on the current frame. public void Pause() { isStarted = false; } /// Starts or restarts playback from the first frame in . public void Play() { currentFrame = 0; isStarted = true; timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0; current.Texture = empty; next.Modulate = clearColor; next.Texture = FrameSet[currentFrame]; } /// Update priority. Lower numbers run earlier in the frame. Default 1. public int Priority => 1; /// Advances the crossfade each frame. Handles both the transition phase (lerping modulate) and the wait phase (holding the next frame visible). public void OnUpdate() { if(!isStarted) return; if (Lists.IsEmpty(FrameSet)) { Clear(); return; } double now = Godot.Time.GetTicksUsec() / 1_000_000.0; if (!wait) { float delta = (float)(now - timestamp) * speed; percent = Mathf.Min(percent + delta, 1f); current.Modulate = completeColor.Lerp(Colors.Transparent, percent); next.Modulate = Colors.Transparent.Lerp(completeColor, percent); } else { float delta = (float)(now - timestamp) * nextFrameSpeed; waitPercent = Mathf.Min(waitPercent + delta, 1f); } timestamp = now; if (percent >= 1f && !wait) { wait = true; SwitchNextFrame(); percent = 0f; } if (waitPercent >= 1f && wait) { wait = false; waitPercent = 0f; } } private void SwitchNextFrame() { NextFrame?.Invoke(); if (Lists.IsEmpty(FrameSet)) { Clear(); return; } var needNextCycle = false; currentFrame++; if (FrameSet.Count <= currentFrame) { if (isLoop) currentFrame = 0; else { currentFrame--; Pause(); } needNextCycle = true; } current.Modulate = completeColor; current.Texture = next.Texture; next.Modulate = clearColor; next.Texture = FrameSet[currentFrame]; if(needNextCycle) NextCycle?.Invoke(); } } }