summaryrefslogtreecommitdiff
path: root/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
diff options
context:
space:
mode:
Diffstat (limited to 'composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt')
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt238
1 files changed, 238 insertions, 0 deletions
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)
+ }
+}