From 64f825465de9fa30c4dfe2707067efdb96110db8 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Thu, 23 Oct 2025 21:59:37 -0700 Subject: Init --- .../liz/ecs/animation/AnimationSystemTest.kt | 238 +++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt (limited to 'composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt') diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt new file mode 100644 index 0000000..bb7298e --- /dev/null +++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt @@ -0,0 +1,238 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.DAGWorld +import kotlin.test.* + +class AnimationSystemTest { + + private fun createTestSpriteSheet(): SpriteSheet { + return SpriteSheet( + imagePath = "test.png", + frames = mapOf( + FrameName("idle_0") to Rect(0, 0, 32, 32), + FrameName("idle_1") to Rect(32, 0, 32, 32), + FrameName("walk_0") to Rect(0, 32, 32, 32), + FrameName("walk_1") to Rect(32, 32, 32, 32), + FrameName("walk_2") to Rect(64, 32, 32, 32) + ) + ) + } + + private fun createTestAnimator(): Animator { + return Animator( + clips = mapOf( + AnimationName("idle") to AnimationClip( + frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")), + frameDuration = 0.1f, + loopMode = LoopMode.LOOP + ), + AnimationName("walk") to AnimationClip( + frameNames = listOf( + FrameName("walk_0"), + FrameName("walk_1"), + FrameName("walk_2") + ), + frameDuration = 0.15f, + loopMode = LoopMode.LOOP + ) + ), + currentClip = AnimationName("idle") + ) + } + + @Test + fun `animation advances frame after frame duration`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + assertEquals(0, animator.frameIndex) + assertEquals(0f, animator.elapsed) + + // Update with less than frame duration - should not advance + world.update(0.05f) + assertEquals(0, animator.frameIndex) + assertEquals(0.05f, animator.elapsed) + + // Update to exceed frame duration - should advance to frame 1 + world.update(0.06f) + assertEquals(1, animator.frameIndex) + assertTrue(animator.elapsed < 0.1f) + } + + @Test + fun `LOOP mode wraps to beginning`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Idle animation has 2 frames at 0.1s each + world.update(0.1f) // Frame 1 + assertEquals(1, animator.frameIndex) + + world.update(0.1f) // Should wrap to Frame 0 + assertEquals(0, animator.frameIndex) + assertTrue(animator.playing) // Still playing + } + + @Test + fun `ONCE mode stops at last frame`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = Animator( + clips = mapOf( + AnimationName("once") to AnimationClip( + frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")), + frameDuration = 0.1f, + loopMode = LoopMode.ONCE + ) + ), + currentClip = AnimationName("once") + ) + entity.add(createTestSpriteSheet()) + entity.add(animator) + + world.update(0.1f) // Frame 1 + assertEquals(1, animator.frameIndex) + assertTrue(animator.playing) + + world.update(0.1f) // Should stop at frame 1 + assertEquals(1, animator.frameIndex) + assertFalse(animator.playing) + + // Further updates should not change anything + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertFalse(animator.playing) + } + + @Test + fun `PING_PONG mode bounces back and forth`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = Animator( + clips = mapOf( + AnimationName("pingpong") to AnimationClip( + frameNames = listOf( + FrameName("walk_0"), + FrameName("walk_1"), + FrameName("walk_2") + ), + frameDuration = 0.1f, + loopMode = LoopMode.PING_PONG + ) + ), + currentClip = AnimationName("pingpong") + ) + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Forward: 0 -> 1 -> 2 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(1, animator.direction) + + world.update(0.1f) + assertEquals(2, animator.frameIndex) + assertEquals(1, animator.direction) + + // Hit end, should reverse: 2 -> 1 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(-1, animator.direction) + + // Continue backward: 1 -> 0 + world.update(0.1f) + assertEquals(0, animator.frameIndex) + assertEquals(-1, animator.direction) + + // Hit beginning, should reverse: 0 -> 1 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(1, animator.direction) + } + + @Test + fun `play() switches animation and resets state`() { + val animator = createTestAnimator() + assertEquals(AnimationName("idle"), animator.currentClip) + assertEquals(0, animator.frameIndex) + + // Manually advance + animator.frameIndex = 1 + animator.elapsed = 0.05f + + // Switch to walk animation + animator.play(AnimationName("walk")) + assertEquals(AnimationName("walk"), animator.currentClip) + assertEquals(0, animator.frameIndex) + assertEquals(0f, animator.elapsed) + assertEquals(1, animator.direction) + assertTrue(animator.playing) + } + + @Test + fun `pause() and resume() control playback`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + animator.pause() + assertFalse(animator.playing) + + // Update should not advance animation + world.update(0.1f) + assertEquals(0, animator.frameIndex) + + animator.resume() + assertTrue(animator.playing) + + // Now it should advance + world.update(0.1f) + assertEquals(1, animator.frameIndex) + } + + @Test + fun `getCurrentFrameName returns correct frame`() { + val animator = createTestAnimator() + + assertEquals(FrameName("idle_0"), animator.getCurrentFrameName()) + + animator.frameIndex = 1 + assertEquals(FrameName("idle_1"), animator.getCurrentFrameName()) + + animator.play(AnimationName("walk")) + assertEquals(FrameName("walk_0"), animator.getCurrentFrameName()) + } + + @Test + fun `multiple updates in single frame work correctly`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Large delta time should advance multiple frames + world.update(0.25f) // Should advance 2 frames and have 0.05s remainder + assertEquals(0, animator.frameIndex) // Wrapped back to 0 (LOOP mode) + assertTrue(animator.elapsed >= 0.04f && animator.elapsed <= 0.06f) + } + + @Test + fun `entities without SpriteSheet are skipped`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(animator) // No SpriteSheet + + // Should not crash + world.update(0.1f) + } +} -- cgit v1.2.3-70-g09d2