1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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)
}
}
|