1use crate::{FrameBuffer, TILE_SIZE};
2
3const BLACK_TILE: u8 = 1;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BattleTransitionKind {
8 DoubleCircle,
9 Spiral { outward: bool },
10 Circle,
11 SpiralTrainerStronger,
12 HorizontalStripes,
13 Shrink,
14 VerticalStripes,
15 Split,
16}
17
18const CIRCLE_DATA_1: &[u8] = &[2, 3, 5, 4, 9, 0xFF];
25const CIRCLE_DATA_2: &[u8] = &[1, 1, 2, 2, 4, 2, 4, 2, 3, 0xFF];
26const CIRCLE_DATA_3: &[u8] = &[2, 1, 3, 1, 4, 1, 4, 1, 4, 1, 3, 1, 2, 1, 1, 1, 1, 0xFF];
27const CIRCLE_DATA_4: &[u8] = &[4, 1, 4, 0, 3, 1, 3, 0, 2, 1, 2, 0, 1, 0xFF];
28const CIRCLE_DATA_5: &[u8] = &[4, 0, 3, 0, 3, 0, 2, 0, 2, 0, 1, 0, 1, 0, 1, 0xFF];
29
30const CIRCLE_LEFT: u8 = 0;
32const CIRCLE_RIGHT: u8 = 1;
33
34struct HalfCircleEntry {
36 quadrant_x: u8,
37 data: &'static [u8],
38 x: i16,
39 y: i16,
40}
41
42const HALF_CIRCLE_1: [HalfCircleEntry; 10] = [
44 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_1, x: 18, y: 6 },
45 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_2, x: 19, y: 3 },
46 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_3, x: 18, y: 0 },
47 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_4, x: 14, y: 0 },
48 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_5, x: 10, y: 0 },
49 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_5, x: 9, y: 0 },
50 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_4, x: 5, y: 0 },
51 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_3, x: 1, y: 0 },
52 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_2, x: 0, y: 3 },
53 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_1, x: 1, y: 6 },
54];
55
56const HALF_CIRCLE_2: [HalfCircleEntry; 10] = [
58 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_1, x: 1, y: 11 },
59 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_2, x: 0, y: 14 },
60 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_3, x: 1, y: 17 },
61 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_4, x: 5, y: 17 },
62 HalfCircleEntry { quadrant_x: CIRCLE_LEFT, data: CIRCLE_DATA_5, x: 9, y: 17 },
63 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_5, x: 10, y: 17 },
64 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_4, x: 14, y: 17 },
65 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_3, x: 18, y: 17 },
66 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_2, x: 19, y: 14 },
67 HalfCircleEntry { quadrant_x: CIRCLE_RIGHT, data: CIRCLE_DATA_1, x: 18, y: 11 },
68];
69
70#[derive(Debug, Clone)]
79pub struct BattleTransitionState {
80 kind: BattleTransitionKind,
81 tiles: Vec<u8>,
84 src: Vec<(u8, u8)>,
88 width_tiles: usize,
89 height_tiles: usize,
90 done: bool,
91 frame: u8,
93 counter: u8,
95 x: i16,
97 y: i16,
98 dir: u8,
99 hc_step: usize,
101 hc_half: u8,
102 spiral_dir: u8,
104 spiral_x: i16,
105 spiral_y: i16,
106}
107
108pub trait TransitionFb {
114 fn size(&self) -> (usize, usize);
116 fn tile_black(&mut self, tx: usize, ty: usize);
119 fn tile_copy(&mut self, tx: usize, ty: usize, src: &Self, stx: usize, sty: usize);
122}
123
124impl TransitionFb for FrameBuffer {
125 fn size(&self) -> (usize, usize) {
126 (self.width as usize, self.height as usize)
127 }
128 fn tile_black(&mut self, tx: usize, ty: usize) {
129 let (w, h) = self.size();
130 let px = tx * TILE_SIZE as usize;
131 let py = ty * TILE_SIZE as usize;
132 for dy in 0..TILE_SIZE as usize {
133 let y = py + dy;
134 if y >= h {
135 break;
136 }
137 for dx in 0..TILE_SIZE as usize {
138 let x = px + dx;
139 if x >= w {
140 break;
141 }
142 let off = (y * w + x) * 4;
143 self.data[off] = 0;
144 self.data[off + 1] = 0;
145 self.data[off + 2] = 0;
146 }
147 }
148 }
149 fn tile_copy(&mut self, tx: usize, ty: usize, src: &Self, stx: usize, sty: usize) {
150 let (w, h) = self.size();
151 let px = tx * TILE_SIZE as usize;
152 let py = ty * TILE_SIZE as usize;
153 let spx = stx * TILE_SIZE as usize;
154 let spy = sty * TILE_SIZE as usize;
155 for dy in 0..TILE_SIZE as usize {
156 let y = py + dy;
157 let sy = spy + dy;
158 if y >= h || sy >= h {
159 break;
160 }
161 for dx in 0..TILE_SIZE as usize {
162 let x = px + dx;
163 let sx = spx + dx;
164 if x >= w || sx >= w {
165 break;
166 }
167 let off = (y * w + x) * 4;
168 let soff = (sy * w + sx) * 4;
169 self.data[off..off + 4].copy_from_slice(&src.data[soff..soff + 4]);
170 }
171 }
172 }
173}
174
175impl<C: crate::palette::ColorIndex> TransitionFb for crate::IndexedFrameBuffer<C> {
176 fn size(&self) -> (usize, usize) {
177 (self.width(), self.height())
178 }
179 fn tile_black(&mut self, tx: usize, ty: usize) {
180 self.fill_rect(
183 (tx * TILE_SIZE as usize) as u32,
184 (ty * TILE_SIZE as usize) as u32,
185 TILE_SIZE,
186 TILE_SIZE,
187 C::from_u8(3),
188 );
189 }
190 fn tile_copy(&mut self, tx: usize, ty: usize, src: &Self, stx: usize, sty: usize) {
191 let bits = crate::index_bits::<C>();
192 let gpr = (self.width() + 7) / 8;
193 let (w, h) = self.size();
194 let px = tx * TILE_SIZE as usize;
195 let py = ty * TILE_SIZE as usize;
196 let spx = stx * TILE_SIZE as usize;
197 let spy = sty * TILE_SIZE as usize;
198 for dy in 0..TILE_SIZE as usize {
199 let y = py + dy;
200 let sy = spy + dy;
201 if y >= h || sy >= h {
202 break;
203 }
204 for dx in 0..TILE_SIZE as usize {
205 let x = px + dx;
206 let sx = spx + dx;
207 if x >= w || sx >= w {
208 break;
209 }
210 let doff = (y * gpr + x / 8) * bits;
213 let soff = (sy * gpr + sx / 8) * bits;
214 let bit_shift = 7 - (x % 8);
215 let sbit_shift = 7 - (sx % 8);
216 let dst = self.packed_mut();
217 for plane in 0..bits {
218 let sb = (src.packed()[soff + plane] >> sbit_shift) & 1;
219 dst[doff + plane] = (dst[doff + plane] & !(1 << bit_shift)) | (sb << bit_shift);
220 }
221 }
222 }
223 }
224}
225
226impl<C: crate::palette::ColorIndex> TransitionFb for crate::RgbaIndexedFrameBuffer<C> {
227 fn size(&self) -> (usize, usize) {
228 (self.width() as usize, self.height() as usize)
229 }
230 fn tile_black(&mut self, tx: usize, ty: usize) {
231 self.indexed_mut().tile_black(tx, ty);
232 }
233 fn tile_copy(&mut self, tx: usize, ty: usize, src: &Self, stx: usize, sty: usize) {
234 self.indexed_mut().tile_copy(tx, ty, src.indexed(), stx, sty);
235 }
236}
237
238impl BattleTransitionState {
239 pub fn new(kind: BattleTransitionKind, width_tiles: usize, height_tiles: usize) -> Self {
240 let w = width_tiles;
241 let h = height_tiles;
242 let mut src = Vec::with_capacity(w * h);
243 for ty in 0..h {
244 for tx in 0..w {
245 src.push((tx as u8, ty as u8));
246 }
247 }
248 Self {
249 kind,
250 tiles: vec![0u8; w * h],
251 src,
252 width_tiles: w,
253 height_tiles: h,
254 done: false,
255 frame: 0,
256 counter: 0,
257 x: 0,
258 y: 0,
259 dir: 0,
260 hc_step: 0,
261 hc_half: 0,
262 spiral_dir: 3,
263 spiral_x: 10,
264 spiral_y: 10,
265 }
266 }
267
268 pub fn is_done(&self) -> bool {
269 self.done
270 }
271
272 pub fn tick(&mut self) {
273 if self.done {
274 return;
275 }
276 match self.kind {
277 BattleTransitionKind::DoubleCircle => self.tick_double_circle(),
278 BattleTransitionKind::Spiral { outward } => {
279 if outward {
280 self.tick_spiral_outward()
281 } else {
282 self.tick_spiral_inward()
283 }
284 }
285 BattleTransitionKind::Circle => self.tick_circle(),
286 BattleTransitionKind::SpiralTrainerStronger => self.tick_spiral_outward(),
287 BattleTransitionKind::HorizontalStripes => self.tick_horizontal_stripes(),
288 BattleTransitionKind::Shrink => self.tick_shrink(),
289 BattleTransitionKind::VerticalStripes => self.tick_vertical_stripes(),
290 BattleTransitionKind::Split => self.tick_split(),
291 }
292 }
293
294 pub fn render<F: TransitionFb>(&self, source_fb: &F, dest_fb: &mut F) -> bool {
302 for ty in 0..self.height_tiles {
303 for tx in 0..self.width_tiles {
304 let idx = ty * self.width_tiles + tx;
305 let black = self.tiles[idx] != 0;
306 let (stx, sty) = self.src[idx];
309 if black {
310 dest_fb.tile_black(tx, ty);
311 } else {
312 dest_fb.tile_copy(tx, ty, source_fb, stx as usize, sty as usize);
313 }
314 }
315 }
316 self.all_black()
317 }
318
319 fn all_black(&self) -> bool {
320 for &t in &self.tiles {
321 if t == 0 {
322 return false;
323 }
324 }
325 true
326 }
327
328 fn fill_black(&mut self) {
329 self.tiles.fill(BLACK_TILE);
330 }
331
332 fn tick_spiral_inward(&mut self) {
335 const TILES_PER_TICK: u8 = 3;
345 const DIRECTIONS: [(i16, i16); 4] = [
347 (0, 1), (1, 0), (0, -1), (-1, 0), ];
352
353 let w = self.width_tiles as i16;
354 let h = self.height_tiles as i16;
355
356 if self.frame == 0 {
358 self.x = 0;
359 self.y = 0;
360 self.dir = 0;
361 self.frame = 1;
362 }
363
364 for _ in 0..TILES_PER_TICK {
365 if self.x >= 0 && self.y >= 0 {
367 self.set_tile(self.x as usize, self.y as usize);
368 }
369
370 let mut moved = false;
374 for _ in 0..4 {
375 let (dx, dy) = DIRECTIONS[self.dir as usize];
376 let nx = self.x + dx;
377 let ny = self.y + dy;
378 let in_bounds = nx >= 0 && nx < w && ny >= 0 && ny < h;
379 if in_bounds && self.tiles[ny as usize * self.width_tiles + nx as usize] == 0 {
380 self.x = nx;
381 self.y = ny;
382 moved = true;
383 break;
384 }
385 self.dir = (self.dir + 1) % 4;
386 }
387
388 if !moved {
389 self.fill_black();
390 self.done = true;
391 return;
392 }
393 }
394 }
395
396 fn tick_spiral_outward(&mut self) {
397 const DIRECTIONS: [(i16, i16); 4] = [(0, -1), (-1, 0), (0, 1), (1, 0)];
401
402 let w = self.width_tiles as i16;
403 let h = self.height_tiles as i16;
404
405 if self.spiral_x < 0 || self.spiral_x >= w || self.spiral_y < 0 || self.spiral_y >= h {
406 self.fill_black();
407 self.done = true;
408 return;
409 }
410
411 for _ in 0..3 {
413 if self.spiral_x >= 0 && self.spiral_x < w && self.spiral_y >= 0 && self.spiral_y < h {
414 self.set_tile(self.spiral_x as usize, self.spiral_y as usize);
415 }
416
417 let (dx, dy) = DIRECTIONS[self.spiral_dir as usize];
418 let nx = self.spiral_x + dx;
419 let ny = self.spiral_y + dy;
420
421 if nx >= 0
423 && nx < w
424 && ny >= 0
425 && ny < h
426 && self.tiles[ny as usize * self.width_tiles + nx as usize] == 0
427 {
428 self.spiral_x = nx;
429 self.spiral_y = ny;
430 } else {
431 self.spiral_dir = (self.spiral_dir + 1) % 4;
433 let (ndx, ndy) = DIRECTIONS[self.spiral_dir as usize];
434 self.spiral_x += ndx;
435 self.spiral_y += ndy;
436 }
437 }
438
439 if self.spiral_x < 0 || self.spiral_x >= w || self.spiral_y < 0 || self.spiral_y >= h {
440 self.fill_black();
441 self.done = true;
442 }
443 }
444
445 fn draw_arc(&mut self, entry: &HalfCircleEntry, quadrant_y: u8) {
451 let fill_dir: i16 = if entry.quadrant_x == CIRCLE_RIGHT { 1 } else { -1 };
452 let row_dir: i16 = if quadrant_y == 0 { 1 } else { -1 };
453 let mut x = entry.x;
454 let mut y = entry.y;
455 let mut i = 0;
456 loop {
457 let fill = entry.data[i];
461 i += 1;
462 let row_start_x = x;
463 for _ in 0..fill {
464 if x >= 0 && y >= 0 {
465 self.set_tile(x as usize, y as usize);
466 }
467 x += fill_dir;
468 }
469 x = row_start_x;
470 y += row_dir;
472 let skip = entry.data[i];
473 i += 1;
474 if skip == 0xFF {
475 break; }
477 for _ in 0..skip {
479 x -= fill_dir;
480 }
481 }
482 }
483
484 fn tick_circle(&mut self) {
485 if self.counter > 0 {
489 self.counter -= 1;
490 return;
491 }
492 let entry = if self.hc_half == 0 {
493 &HALF_CIRCLE_1[self.hc_step]
494 } else {
495 &HALF_CIRCLE_2[self.hc_step]
496 };
497 self.draw_arc(entry, self.hc_half);
498 self.hc_step += 1;
499 if self.hc_step >= 10 {
500 if self.hc_half == 0 {
501 self.hc_half = 1;
502 self.hc_step = 0;
503 self.counter = 2; } else {
505 self.fill_black();
506 self.done = true;
507 }
508 } else {
509 self.counter = 2; }
511 }
512
513 fn tick_double_circle(&mut self) {
514 if self.counter > 0 {
517 self.counter -= 1;
518 return;
519 }
520 self.draw_arc(&HALF_CIRCLE_1[self.hc_step], 0);
521 self.draw_arc(&HALF_CIRCLE_2[self.hc_step], 1);
522 self.hc_step += 1;
523 if self.hc_step >= 10 {
524 self.fill_black();
525 self.done = true;
526 } else {
527 self.counter = 2; }
529 }
530
531 fn tick_horizontal_stripes(&mut self) {
534 if self.counter > 0 {
539 self.counter -= 1;
540 return;
541 }
542 let col = self.frame as usize;
543 if col >= self.width_tiles {
544 self.fill_black();
545 self.done = true;
546 return;
547 }
548
549 for row in (0..self.height_tiles).step_by(2) {
551 self.set_tile(col, row);
552 }
553 let rcol = self.width_tiles - 1 - col;
555 for row in (1..self.height_tiles).step_by(2) {
556 self.set_tile(rcol, row);
557 }
558
559 self.frame += 1;
560 self.counter = 3; }
562
563 fn tick_vertical_stripes(&mut self) {
566 if self.counter > 0 {
570 self.counter -= 1;
571 return;
572 }
573 let row = self.frame as usize;
574 if row >= self.height_tiles {
575 self.fill_black();
576 self.done = true;
577 return;
578 }
579
580 for col in (0..self.width_tiles).step_by(2) {
582 self.set_tile(col, row);
583 }
584 let brow = self.height_tiles - 1 - row;
586 for col in (0..self.width_tiles).step_by(2) {
587 self.set_tile(col, brow);
588 }
589
590 self.frame += 1;
591 self.counter = 3;
592 }
593
594 fn copy_tile(&mut self, sx: usize, sy: usize, dx: usize, dy: usize) {
599 let s = sy * self.width_tiles + sx;
600 let d = dy * self.width_tiles + dx;
601 self.tiles[d] = self.tiles[s];
602 self.src[d] = self.src[s];
603 }
604
605 fn tick_shrink(&mut self) {
606 if self.counter > 0 {
611 self.counter -= 1;
612 return;
613 }
614 let step = self.frame as usize;
615 let w = self.width_tiles;
616 let h = self.height_tiles;
617 if step >= h / 2 {
618 self.fill_black();
619 self.done = true;
620 return;
621 }
622
623 let rmid = h / 2; let cmid = w / 2; for y in (1..rmid).rev() {
630 for x in 0..w {
631 self.copy_tile(x, y - 1, x, y);
632 }
633 }
634 for y in rmid..(h - 1) {
635 for x in 0..w {
636 self.copy_tile(x, y + 1, x, y);
637 }
638 }
639 for x in 0..w {
641 self.set_tile(x, 0);
642 self.set_tile(x, h - 1);
643 }
644
645 for x in (1..cmid).rev() {
649 for y in 0..h {
650 self.copy_tile(x - 1, y, x, y);
651 }
652 }
653 for x in cmid..(w - 1) {
654 for y in 0..h {
655 self.copy_tile(x + 1, y, x, y);
656 }
657 }
658 for y in 0..h {
660 self.set_tile(0, y);
661 self.set_tile(w - 1, y);
662 }
663
664 self.frame += 1;
665 self.counter = 5; }
667
668 fn tick_split(&mut self) {
671 if self.counter > 0 {
676 self.counter -= 1;
677 return;
678 }
679 let step = self.frame as usize;
680 let w = self.width_tiles;
681 let h = self.height_tiles;
682 if step >= h / 2 {
683 self.fill_black();
684 self.done = true;
685 return;
686 }
687
688 let rmid = h / 2; let cmid = w / 2; for y in ((rmid + 1)..h).rev() {
695 for x in 0..w {
696 self.copy_tile(x, y - 1, x, y);
697 }
698 }
699 for y in 0..(rmid - 1) {
700 for x in 0..w {
701 self.copy_tile(x, y + 1, x, y);
702 }
703 }
704 for x in 0..w {
706 self.set_tile(x, rmid - 1);
707 self.set_tile(x, rmid);
708 }
709
710 for x in ((cmid + 1)..w).rev() {
714 for y in 0..h {
715 self.copy_tile(x - 1, y, x, y);
716 }
717 }
718 for x in 0..(cmid - 1) {
719 for y in 0..h {
720 self.copy_tile(x + 1, y, x, y);
721 }
722 }
723 for y in 0..h {
725 self.set_tile(cmid - 1, y);
726 self.set_tile(cmid, y);
727 }
728
729 self.frame += 1;
730 self.counter = 5; }
732
733 fn set_tile(&mut self, x: usize, y: usize) {
736 if x < self.width_tiles && y < self.height_tiles {
737 self.tiles[y * self.width_tiles + x] = BLACK_TILE;
738 }
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use crate::Rgba;
746 use dotzuki_engine::render_config::RenderConfig;
747
748 fn tile_is_black(state: &BattleTransitionState, x: usize, y: usize) -> bool {
749 state.tiles[y * state.width_tiles + x] != 0
750 }
751
752 fn src_of(state: &BattleTransitionState, x: usize, y: usize) -> (u8, u8) {
753 state.src[y * state.width_tiles + x]
754 }
755
756 #[test]
757 fn test_battle_transition_state_new() {
758 let state = BattleTransitionState::new(BattleTransitionKind::Circle, 20, 18);
759 assert_eq!(state.tiles.len(), 360); assert_eq!(state.width_tiles, 20);
761 assert_eq!(state.height_tiles, 18);
762 assert!(!state.is_done());
763 assert_eq!(src_of(&state, 5, 7), (5, 7));
765 }
766
767 #[test]
768 fn test_battle_transition_state_set_tile() {
769 let mut state = BattleTransitionState::new(BattleTransitionKind::Circle, 20, 18);
770 state.set_tile(0, 0);
771 assert_eq!(state.tiles[0], 1); }
773
774 #[test]
777 fn test_circle_first_arc_entry() {
778 let mut state = BattleTransitionState::new(BattleTransitionKind::Circle, 20, 18);
782 state.tick();
783 assert!(tile_is_black(&state, 18, 6));
785 assert!(tile_is_black(&state, 19, 6));
786 assert!(!tile_is_black(&state, 17, 6));
787 assert!(tile_is_black(&state, 15, 7));
789 assert!(tile_is_black(&state, 19, 7));
790 assert!(!tile_is_black(&state, 14, 7));
791 assert!(tile_is_black(&state, 11, 8));
793 assert!(tile_is_black(&state, 19, 8));
794 assert!(!tile_is_black(&state, 10, 8));
795 assert!(!tile_is_black(&state, 0, 0));
797 assert!(!tile_is_black(&state, 10, 10));
798 assert!(!state.is_done());
799 }
800
801 #[test]
802 fn test_double_circle_first_tick_draws_both_halves() {
803 let mut state = BattleTransitionState::new(BattleTransitionKind::DoubleCircle, 20, 18);
805 state.tick();
806 assert!(tile_is_black(&state, 18, 6));
808 assert!(tile_is_black(&state, 15, 7));
809 assert!(tile_is_black(&state, 11, 8));
810 assert!(tile_is_black(&state, 1, 11));
813 assert!(tile_is_black(&state, 0, 11));
814 assert!(!tile_is_black(&state, 2, 11));
815 assert!(tile_is_black(&state, 4, 10));
818 assert!(tile_is_black(&state, 0, 10));
819 assert!(!tile_is_black(&state, 5, 10));
820 assert!(tile_is_black(&state, 8, 9));
822 assert!(tile_is_black(&state, 0, 9));
823 assert!(!tile_is_black(&state, 9, 9));
824 }
825
826 #[test]
827 fn test_circle_completes_fully_black() {
828 let mut state = BattleTransitionState::new(BattleTransitionKind::Circle, 20, 18);
830 let mut ticks = 0;
831 while !state.is_done() && ticks < 120 {
832 state.tick();
833 ticks += 1;
834 }
835 assert!(state.is_done());
836 assert!(ticks <= 20 * 3, "took {ticks} ticks");
837 assert!(state.all_black());
838 }
839
840 #[test]
841 fn test_double_circle_completes_fully_black() {
842 let mut state = BattleTransitionState::new(BattleTransitionKind::DoubleCircle, 20, 18);
844 let mut ticks = 0;
845 while !state.is_done() && ticks < 60 {
846 state.tick();
847 ticks += 1;
848 }
849 assert!(state.is_done());
850 assert!(ticks <= 10 * 3, "took {ticks} ticks");
851 assert!(state.all_black());
852 }
853
854 #[test]
857 fn test_shrink_first_step_compresses_toward_center() {
858 let mut state = BattleTransitionState::new(BattleTransitionKind::Shrink, 20, 18);
859 state.tick();
860 for x in 0..20 {
862 assert!(tile_is_black(&state, x, 0), "row 0, col {x}");
863 assert!(tile_is_black(&state, x, 17), "row 17, col {x}");
864 }
865 for y in 0..18 {
866 assert!(tile_is_black(&state, 0, y), "col 0, row {y}");
867 assert!(tile_is_black(&state, 19, y), "col 19, row {y}");
868 }
869 assert_eq!(src_of(&state, 5, 1), (4, 0));
874 assert_eq!(src_of(&state, 5, 8), (4, 7));
875 assert_eq!(src_of(&state, 5, 9), (4, 10));
876 assert_eq!(src_of(&state, 5, 16), (4, 17));
877 assert_eq!(src_of(&state, 1, 5), (0, 4));
878 assert_eq!(src_of(&state, 9, 5), (8, 4));
879 assert_eq!(src_of(&state, 10, 5), (11, 4));
880 assert_eq!(src_of(&state, 18, 5), (19, 4));
881 assert!(!state.is_done());
882 }
883
884 #[test]
885 fn test_split_first_step_pushes_away_from_center() {
886 let mut state = BattleTransitionState::new(BattleTransitionKind::Split, 20, 18);
887 state.tick();
888 for x in 0..20 {
890 assert!(tile_is_black(&state, x, 8), "row 8, col {x}");
891 assert!(tile_is_black(&state, x, 9), "row 9, col {x}");
892 }
893 for y in 0..18 {
894 assert!(tile_is_black(&state, 9, y), "col 9, row {y}");
895 assert!(tile_is_black(&state, 10, y), "col 10, row {y}");
896 }
897 assert_eq!(src_of(&state, 5, 0), (6, 1));
902 assert_eq!(src_of(&state, 5, 7), (6, 8));
903 assert_eq!(src_of(&state, 5, 10), (6, 9));
904 assert_eq!(src_of(&state, 5, 17), (6, 16));
905 assert_eq!(src_of(&state, 0, 5), (1, 6));
906 assert_eq!(src_of(&state, 8, 5), (9, 6));
907 assert_eq!(src_of(&state, 11, 5), (10, 6));
908 assert_eq!(src_of(&state, 19, 5), (18, 6));
909 assert!(!state.is_done());
910 }
911
912 #[test]
913 fn test_shrink_completes_fully_black() {
914 let mut state = BattleTransitionState::new(BattleTransitionKind::Shrink, 20, 18);
916 let mut ticks = 0;
917 while !state.is_done() && ticks < 120 {
918 state.tick();
919 ticks += 1;
920 }
921 assert!(state.is_done());
922 assert!(ticks <= 9 * 6 + 1, "took {ticks} ticks");
923 assert!(state.all_black());
924 }
925
926 #[test]
927 fn test_split_completes_fully_black() {
928 let mut state = BattleTransitionState::new(BattleTransitionKind::Split, 20, 18);
929 let mut ticks = 0;
930 while !state.is_done() && ticks < 120 {
931 state.tick();
932 ticks += 1;
933 }
934 assert!(state.is_done());
935 assert!(ticks <= 9 * 6 + 1, "took {ticks} ticks");
936 assert!(state.all_black());
937 }
938
939 #[test]
940 fn test_shrink_render_copies_source_pixels() {
941 let mut state = BattleTransitionState::new(BattleTransitionKind::Shrink, 20, 18);
943 let mut source = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::BLACK);
944 for ty in 0..18usize {
946 for tx in 0..20usize {
947 let v = (tx + ty * 20) as u8;
948 for dy in 0..8usize {
949 for dx in 0..8usize {
950 let off = ((ty * 8 + dy) * 160 + (tx * 8 + dx)) * 4;
951 source.data[off] = v;
952 source.data[off + 1] = v;
953 source.data[off + 2] = v;
954 source.data[off + 3] = 255;
955 }
956 }
957 }
958 }
959 state.tick(); let mut dest = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::BLACK);
961 state.render(&source, &mut dest);
962 let off = ((1 * 8 + 3) * 160 + (5 * 8 + 3)) * 4;
964 assert_eq!(dest.data[off], 4);
965 assert_eq!(dest.data[0], 0);
967 let off2 = ((10 * 8 + 3) * 160 + (5 * 8 + 3)) * 4;
969 assert_eq!(dest.data[off2], (4 + 11 * 20) as u8);
970 }
971}