package coffee.liz.ecs.animation import coffee.liz.ecs.System import coffee.liz.ecs.World /** * System that updates animation playback state for all entities with an Animator component. * * This system: * - Updates elapsed time for playing animations * - Advances frame indices based on frame duration * - Handles loop modes (ONCE, LOOP, PING_PONG) * - Stops animations when they complete (for ONCE mode) */ class AnimationSystem : System { override fun update(world: World, deltaTime: Float) { // Query all entities that have both Animator and SpriteSheet world.query(Animator::class, SpriteSheet::class).forEach { entity -> val animator = entity.get(Animator::class) ?: return@forEach // Skip if animation is not playing if (!animator.playing) return@forEach // Get the current animation clip val clip = animator.clips[animator.currentClip] ?: return@forEach // Accumulate elapsed time animator.elapsed += deltaTime // Advance frames while we have enough elapsed time while (animator.elapsed >= clip.frameDuration) { animator.elapsed -= clip.frameDuration advanceFrame(animator, clip) } } } /** * Advances the animation to the next frame based on loop mode. */ private fun advanceFrame(animator: Animator, clip: AnimationClip) { animator.frameIndex += animator.direction when (clip.loopMode) { LoopMode.ONCE -> { // Play once and stop on last frame if (animator.frameIndex >= clip.frameNames.size) { animator.frameIndex = clip.frameNames.size - 1 animator.playing = false } } LoopMode.LOOP -> { // Loop back to beginning if (animator.frameIndex >= clip.frameNames.size) { animator.frameIndex = 0 } else if (animator.frameIndex < 0) { // Handle negative wrap (shouldn't happen in LOOP but be safe) animator.frameIndex = clip.frameNames.size - 1 } } LoopMode.PING_PONG -> { // Bounce back and forth if (animator.frameIndex >= clip.frameNames.size) { // Hit the end, reverse direction animator.frameIndex = (clip.frameNames.size - 2).coerceAtLeast(0) animator.direction = -1 } else if (animator.frameIndex < 0) { // Hit the beginning, reverse direction animator.frameIndex = 1.coerceAtMost(clip.frameNames.size - 1) animator.direction = 1 } } } } }