1use crate::palette::{GbColor, Palette};
15use crate::sprite::{SpriteLayer, SpriteOamEntry, OAM_X_FLIP};
16use crate::tile::{TileSet, TILE_PIXELS};
17use crate::indexed_framebuffer::RgbaIndexedFrameBuffer;
18use crate::{Rgba, TILE_SIZE};
19
20use super::types::ANIM_BASE_TILE_ID;
21use super::AnimEffect;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MonSide {
28 Player,
29 Enemy,
30}
31
32impl MonSide {
33 pub fn other(self) -> Self {
35 match self {
36 Self::Player => Self::Enemy,
37 Self::Enemy => Self::Player,
38 }
39 }
40
41 fn index(self) -> usize {
42 match self {
43 Self::Player => 0,
44 Self::Enemy => 1,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy)]
51pub struct MonRect {
52 pub x: i32,
53 pub y: i32,
54}
55
56pub const WAVY_LINE_OFFSETS: [i8; 32] = [
59 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1,
61];
62
63pub const WAVY_SCREEN_FRAMES: u16 = 255;
65
66pub const SPIRAL_BALL_COORDS: [(u8, u8); 21] = [
70 (0x38, 0x28), (0x40, 0x18), (0x50, 0x10), (0x60, 0x18), (0x68, 0x28),
71 (0x60, 0x38), (0x50, 0x40), (0x40, 0x38), (0x40, 0x28), (0x46, 0x1E),
72 (0x50, 0x18), (0x5B, 0x1E), (0x60, 0x28), (0x5B, 0x32), (0x50, 0x38),
73 (0x46, 0x32), (0x48, 0x28), (0x50, 0x20), (0x58, 0x28), (0x50, 0x30),
74 (0x50, 0x28),
75];
76
77pub const UPWARD_BALLS_X_PLAYER: [u8; 6] = [0x10, 0x40, 0x28, 0x18, 0x38, 0x30];
79pub const UPWARD_BALLS_X_ENEMY: [u8; 6] = [0x60, 0x90, 0x78, 0x68, 0x88, 0x80];
81
82pub const FALLING_INITIAL_X: [u8; 20] = [
84 0x38, 0x40, 0x50, 0x60, 0x70, 0x88, 0x90, 0x56, 0x67, 0x4A, 0x77, 0x84, 0x98, 0x32, 0x22, 0x5C, 0x6C, 0x7D, 0x8E, 0x99,
86];
87
88pub const FALLING_INITIAL_MOVEMENT: [u8; 20] = [
90 0x00, 0x84, 0x06, 0x81, 0x02, 0x88, 0x01, 0x83, 0x05, 0x89, 0x09, 0x80, 0x07, 0x87, 0x03, 0x82, 0x04, 0x85, 0x08, 0x86,
92];
93
94pub const FALLING_DELTA_XS: [u8; 9] = [0, 1, 3, 5, 7, 9, 11, 13, 15];
100const FALLING_DELTA_XS_OVERFLOW: u8 = 0xFA;
101
102pub const BALL_TILE: u8 = 0x7A;
105pub const DROPLET_TILE: u8 = 0x71;
107pub const PETAL_TILE: u8 = 0x71;
109pub const LEAF_TILE: u8 = 0x37;
111
112pub const FLASH_SCREEN_LONG_PALETTE: [[u8; 4]; 12] = [
115 [3, 3, 2, 1],
116 [3, 3, 3, 2],
117 [3, 3, 3, 3],
118 [3, 3, 3, 2],
119 [3, 3, 2, 1],
120 [3, 2, 1, 0],
121 [2, 1, 0, 0],
122 [1, 0, 0, 0],
123 [0, 0, 0, 0],
124 [1, 0, 0, 0],
125 [2, 1, 0, 0],
126 [3, 2, 1, 0],
127];
128
129pub const MINIMIZED_BLOB: [[u8; 8]; 5] = [
133 [0, 0, 0, 1, 1, 0, 0, 0],
134 [0, 0, 1, 1, 1, 1, 0, 0],
135 [0, 1, 1, 1, 1, 1, 1, 0],
136 [0, 0, 1, 1, 1, 1, 0, 0],
137 [0, 0, 1, 0, 0, 1, 0, 0],
138];
139
140const FLASH_SHORT_FRAMES: u8 = 4; const FLASH_LONG_FRAMES: u8 = 48; const BLINK_FRAMES: u8 = 60; const SQUISH_FRAMES: u8 = 26; const SHAKE_BACK_AND_FORTH_FRAMES: u8 = 96; const SPIRAL_BALLS_FRAMES: u8 = 105; const FALLING_FRAMES: u16 = 156; const DROPLET_FRAMES: u8 = 64; const HUD_SHAKE_FRAMES: u8 = 32; const MINIMIZE_FRAMES: u8 = 6; const SUBSTITUTE_FRAMES: u8 = 3; const BOUNCE_FRAMES: u8 = 5 * 7 * 3 + 3;
156const SLIDE_DOWN_HIDE_FRAMES: u8 = 16;
158const TRANSFORM_FRAMES: u8 = 12;
165const TRANSFORM_HIDDEN_FRAMES: u8 = 6;
167
168const HUD_SHAKE_HEIGHT: u32 = 7 * TILE_SIZE;
171const SCREEN_SHAKE_HEIGHT: u32 = 12 * TILE_SIZE;
174
175fn remap_shades(fb: &mut RgbaIndexedFrameBuffer, map: &[u8; 4]) {
181 fb.remap_shades(map);
182}
183
184fn shift_rows_h(fb: &mut RgbaIndexedFrameBuffer, y_end: u32, dx: i32) {
189 if dx == 0 {
190 return;
191 }
192 let w = fb.width() as i32;
193 let src = fb.indexed().clone();
194 for y in 0..y_end.min(fb.height()) {
195 for x in 0..w {
196 let sx = x - dx;
197 let color = if sx >= 0 && sx < w {
198 src.get_pixel(sx as u32, y).unwrap_or(GbColor::White)
199 } else {
200 GbColor::White
201 };
202 fb.set_pixel_index(x as u32, y, color);
203 }
204 }
205}
206
207fn shift_rows_v(fb: &mut RgbaIndexedFrameBuffer, y_end: u32, dy: i32) {
209 if dy == 0 {
210 return;
211 }
212 let src = fb.indexed().clone();
213 for y in 0..y_end.min(fb.height()) as i32 {
214 let sy = y - dy;
215 for x in 0..fb.width() as i32 {
216 let color = if sy >= 0 && (sy as u32) < y_end.min(fb.height()) {
217 src.get_pixel(x as u32, sy as u32).unwrap_or(GbColor::White)
218 } else {
219 GbColor::White
220 };
221 fb.set_pixel_index(x as u32, y as u32, color);
222 }
223 }
224}
225
226#[derive(Debug, Clone, Copy)]
231enum Flash {
232 Short { frame: u8 },
233 Long { frame: u8 },
234}
235
236#[derive(Debug, Clone, Copy)]
242struct Shake {
243 horizontal: bool,
244 vertical: bool,
245 pixels: i32,
246 decay: bool,
247 frame: u16,
248 total: u16,
249}
250
251impl Shake {
252 fn offset(&self) -> (i32, i32) {
253 let amp = if self.decay {
254 let step = self.frame / 9;
256 let within = self.frame % 9;
257 let amp = (self.pixels - step as i32).max(1);
258 if within < 4 {
259 amp
260 } else {
261 0
262 }
263 } else {
264 if self.frame % 2 == 0 {
266 self.pixels
267 } else {
268 -self.pixels
269 }
270 };
271 (
272 if self.horizontal { amp } else { 0 },
273 if self.vertical { amp } else { 0 },
274 )
275 }
276}
277
278#[derive(Debug, Clone, Copy)]
282enum Tint {
283 Dark,
284 Light,
285 DarkenMon,
286}
287
288impl Tint {
289 fn map(&self) -> [u8; 4] {
290 match self {
291 Tint::Dark => [3, 3, 2, 1],
293 Tint::Light => [0, 0, 1, 2],
295 Tint::DarkenMon => [1, 2, 3, 3],
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
303struct Blink {
304 side: MonSide,
305 frame: u8,
306}
307
308#[derive(Debug, Clone, Copy)]
312struct Squish {
313 side: MonSide,
314 frame: u8,
315}
316
317impl Squish {
318 fn params(&self) -> (u32, bool) {
320 let pass = (self.frame / 3).min(7);
321 let width = 7u32.saturating_sub(pass as u32 + 1);
322 (width, pass % 2 == 1)
323 }
324}
325
326#[derive(Debug, Clone, Copy)]
330struct Transform {
331 side: MonSide,
332 frame: u8,
333}
334
335#[derive(Debug, Clone, Copy)]
338struct ShakeBackAndForth {
339 side: MonSide,
340 frame: u8,
341}
342
343#[derive(Debug, Clone, Copy)]
346struct HudShake {
347 frame: u8,
348}
349
350#[derive(Debug, Clone, Copy)]
354struct Bounce {
355 side: MonSide,
356 frame: u8,
357}
358
359#[derive(Debug, Clone, Copy)]
363struct SlideDownHide {
364 side: MonSide,
365 frame: u8,
366}
367
368#[derive(Debug, Clone)]
370enum Objects {
371 None,
372 SpiralBalls { side: MonSide, frame: u8 },
375 ShootBalls { pillars: Vec<Pillar>, active: usize, frame: u8 },
378 Falling { tile: u8, frame: u16, tick: u8, objects: Vec<FallingObject> },
380 Droplets { iter: u8, half: u8, base_x: i32 },
383}
384
385#[derive(Debug, Clone)]
386struct Pillar {
387 base_y: i32, base_x: i32, balls: Vec<i32>,
391}
392
393#[derive(Debug, Clone, Copy)]
394struct FallingObject {
395 x: u8, y: u8, movement: u8,
398}
399
400#[derive(Debug, Clone)]
406pub struct BattleEffects {
407 flash: Option<Flash>,
408 shake: Option<Shake>,
409 tint: Option<Tint>,
410 wave_frame: u16,
411 hud_shake: Option<HudShake>,
412 blink: Option<Blink>,
413 squish: Option<Squish>,
414 shake_bnf: Option<ShakeBackAndForth>,
415 bounce: Option<Bounce>,
416 slide_down_hide: Option<SlideDownHide>,
417 transform: Option<Transform>,
420 forced_hidden: [bool; 2],
423 minimized: [bool; 2],
426 substitute: [bool; 2],
430 objects: Objects,
431}
432
433impl Default for BattleEffects {
434 fn default() -> Self {
435 Self::new()
436 }
437}
438
439impl BattleEffects {
440 pub fn new() -> Self {
441 Self {
442 flash: None,
443 shake: None,
444 tint: None,
445 wave_frame: 0,
446 hud_shake: None,
447 blink: None,
448 squish: None,
449 shake_bnf: None,
450 bounce: None,
451 slide_down_hide: None,
452 transform: None,
453 forced_hidden: [false; 2],
454 minimized: [false; 2],
455 substitute: [false; 2],
456 objects: Objects::None,
457 }
458 }
459
460 pub fn clear_side(&mut self, side: MonSide) {
462 let i = side.index();
463 self.forced_hidden[i] = false;
464 self.minimized[i] = false;
465 self.substitute[i] = false;
466 if self.transform.is_some_and(|t| t.side == side) {
467 self.transform = None;
468 }
469 }
470
471 pub fn set_substitute(&mut self, side: MonSide, up: bool) {
473 self.substitute[side.index()] = up;
474 }
475
476 pub fn show_mon(&mut self, side: MonSide) {
479 self.forced_hidden[side.index()] = false;
480 }
481
482 pub fn apply(&mut self, effect: &AnimEffect, attacker: MonSide) -> u8 {
488 match *effect {
489 AnimEffect::None => 0,
490 AnimEffect::FlashScreen { frames } => {
491 if frames as usize >= FLASH_LONG_FRAMES as usize {
492 self.flash = Some(Flash::Long { frame: 0 });
493 FLASH_LONG_FRAMES
494 } else {
495 self.flash = Some(Flash::Short { frame: 0 });
496 FLASH_SHORT_FRAMES
497 }
498 }
499 AnimEffect::ShakeScreenH { pixels, frames } => {
500 self.start_shake(true, false, pixels, frames);
501 self.shake_duration()
502 }
503 AnimEffect::ShakeScreenV { pixels, frames } => {
504 self.start_shake(false, true, pixels, frames);
505 self.shake_duration()
506 }
507 AnimEffect::ShakeScreenHV { pixels, frames } => {
508 self.start_shake(true, true, pixels, frames);
509 self.shake_duration()
510 }
511 AnimEffect::DarkScreenPalette => {
512 self.tint = Some(Tint::Dark);
513 0
514 }
515 AnimEffect::LightScreenPalette => {
516 self.tint = Some(Tint::Light);
517 0
518 }
519 AnimEffect::DarkenMonPalette => {
520 self.tint = Some(Tint::DarkenMon);
521 0
522 }
523 AnimEffect::ResetScreenPalette => {
524 self.tint = None;
525 0
526 }
527 AnimEffect::WavyScreen => {
528 self.wave_frame = self.wave_frame.max(1);
529 WAVY_SCREEN_FRAMES as u8
530 }
531 AnimEffect::SubstituteMon => {
532 self.substitute[attacker.index()] = true;
535 self.forced_hidden[attacker.index()] = false;
536 SUBSTITUTE_FRAMES
537 }
538 AnimEffect::MinimizeMon => {
539 self.minimized[attacker.index()] = true;
542 self.forced_hidden[attacker.index()] = false;
543 MINIMIZE_FRAMES
544 }
545 AnimEffect::TransformMon => {
546 self.transform = Some(Transform {
554 side: attacker,
555 frame: 0,
556 });
557 self.forced_hidden[attacker.index()] = true;
558 TRANSFORM_FRAMES
559 }
560 AnimEffect::SquishMonPic => {
561 self.squish = Some(Squish {
562 side: attacker,
563 frame: 0,
564 });
565 SQUISH_FRAMES
566 }
567 AnimEffect::ShakeBackAndForth => {
568 self.shake_bnf = Some(ShakeBackAndForth {
569 side: attacker,
570 frame: 0,
571 });
572 SHAKE_BACK_AND_FORTH_FRAMES
573 }
574 AnimEffect::BounceUpAndDown => {
575 self.bounce = Some(Bounce {
576 side: attacker,
577 frame: 0,
578 });
579 BOUNCE_FRAMES
580 }
581 AnimEffect::SlidePlayerMonDownAndHide => {
582 self.slide_down_hide = Some(SlideDownHide {
583 side: attacker,
584 frame: 0,
585 });
586 SLIDE_DOWN_HIDE_FRAMES
587 }
588 AnimEffect::BlinkPlayerMon { .. } | AnimEffect::FlashPlayerMonPic => {
591 self.blink = Some(Blink {
592 side: attacker,
593 frame: 0,
594 });
595 BLINK_FRAMES
596 }
597 AnimEffect::BlinkEnemyMon { .. } | AnimEffect::FlashEnemyMonPic => {
598 self.blink = Some(Blink {
599 side: attacker.other(),
600 frame: 0,
601 });
602 BLINK_FRAMES
603 }
604 AnimEffect::ShowPlayerMon => {
605 self.show_mon(attacker);
606 0
607 }
608 AnimEffect::ShowEnemyMon => {
609 self.show_mon(attacker.other());
610 0
611 }
612 AnimEffect::HidePlayerMon => {
613 self.forced_hidden[attacker.index()] = true;
614 0
615 }
616 AnimEffect::HideEnemyMon => {
617 self.forced_hidden[attacker.other().index()] = true;
618 0
619 }
620 AnimEffect::ShakeEnemyHud { .. } => {
621 self.hud_shake = Some(HudShake { frame: 0 });
624 HUD_SHAKE_FRAMES
625 }
626 AnimEffect::SpiralBallsInward => {
627 self.objects = Objects::SpiralBalls {
628 side: attacker,
629 frame: 0,
630 };
631 SPIRAL_BALLS_FRAMES
632 }
633 AnimEffect::ShootBallsUpward { many } => {
634 self.objects = Self::new_shoot_balls(attacker, many);
635 self.shoot_balls_frames() as u8
636 }
637 AnimEffect::PetalsFalling => {
638 self.objects = Self::new_falling(PETAL_TILE, 20);
639 FALLING_FRAMES as u8
640 }
641 AnimEffect::LeavesFalling => {
642 self.objects = Self::new_falling(LEAF_TILE, 3);
643 FALLING_FRAMES as u8
644 }
645 AnimEffect::WaterDroplets => {
646 self.objects = Objects::Droplets {
647 iter: 0,
648 half: 0,
649 base_x: -16,
650 };
651 DROPLET_FRAMES
652 }
653 _ => 0,
656 }
657 }
658
659 fn start_shake(&mut self, h: bool, v: bool, pixels: i8, frames: u8) {
660 let pixels = pixels.unsigned_abs() as i32;
661 let decay = pixels > 1;
664 let total = if decay {
665 (pixels as u16) * 9
666 } else {
667 frames.max(1) as u16
668 };
669 self.shake = Some(Shake {
670 horizontal: h,
671 vertical: v,
672 pixels,
673 decay,
674 frame: 0,
675 total,
676 });
677 }
678
679 fn shake_duration(&self) -> u8 {
680 self.shake.map_or(0, |s| s.total.min(255) as u8)
681 }
682
683 fn new_shoot_balls(side: MonSide, many: bool) -> Objects {
684 let make_pillar = |base_y: i32, base_x: i32, count: usize| Pillar {
687 base_y,
688 base_x,
689 balls: (1..=count).map(|i| base_y + 8 * i as i32).collect(),
690 };
691 let pillars = if many {
692 let (xs, y) = match side {
694 MonSide::Player => (&UPWARD_BALLS_X_PLAYER[..], 0x50),
695 MonSide::Enemy => (&UPWARD_BALLS_X_ENEMY[..], 0x28),
696 };
697 xs.iter()
698 .map(|&x| make_pillar(y as i32, x as i32, 4))
699 .collect()
700 } else {
701 let (y, x) = match side {
703 MonSide::Player => (6 * 8, 5 * 8),
704 MonSide::Enemy => (0, 16 * 8),
705 };
706 vec![make_pillar(y, x, 5)]
707 };
708 Objects::ShootBalls {
709 pillars,
710 active: 0,
711 frame: 0,
712 }
713 }
714
715 fn shoot_balls_frames(&self) -> u16 {
716 match &self.objects {
717 Objects::ShootBalls { pillars, .. } => pillars
720 .iter()
721 .map(|p| 2 * (p.balls.len() as u16).saturating_sub(1))
722 .sum(),
723 _ => 0,
724 }
725 }
726
727 fn new_falling(tile: u8, count: usize) -> Objects {
728 let objects = (0..count)
729 .map(|i| FallingObject {
730 y: if i == 0 { 0 } else { 8 * (i as u8 + 1) },
733 x: FALLING_INITIAL_X[i],
734 movement: FALLING_INITIAL_MOVEMENT[i],
735 })
736 .collect();
737 Objects::Falling {
738 tile,
739 frame: 0,
740 tick: 0,
741 objects,
742 }
743 }
744
745 pub fn tick(&mut self) {
747 if let Some(flash) = &mut self.flash {
748 let done = match flash {
749 Flash::Short { frame } => {
750 *frame += 1;
751 *frame >= FLASH_SHORT_FRAMES
752 }
753 Flash::Long { frame } => {
754 *frame += 1;
755 *frame >= FLASH_LONG_FRAMES
756 }
757 };
758 if done {
759 self.flash = None;
760 }
761 }
762
763 if let Some(shake) = &mut self.shake {
764 shake.frame += 1;
765 if shake.frame >= shake.total {
766 self.shake = None;
767 }
768 }
769
770 if self.wave_frame > 0 {
771 self.wave_frame += 1;
772 if self.wave_frame > WAVY_SCREEN_FRAMES {
773 self.wave_frame = 0;
774 }
775 }
776
777 if let Some(hud) = &mut self.hud_shake {
778 hud.frame += 1;
779 if hud.frame >= HUD_SHAKE_FRAMES {
780 self.hud_shake = None;
781 }
782 }
783
784 if let Some(blink) = &mut self.blink {
785 blink.frame += 1;
786 if blink.frame >= BLINK_FRAMES {
787 self.blink = None;
788 }
789 }
790
791 if let Some(squish) = &mut self.squish {
792 squish.frame += 1;
793 if squish.frame >= SQUISH_FRAMES {
794 self.forced_hidden[squish.side.index()] = true;
796 self.squish = None;
797 }
798 }
799
800 if let Some(bnf) = &mut self.shake_bnf {
801 bnf.frame += 1;
802 if bnf.frame >= SHAKE_BACK_AND_FORTH_FRAMES {
803 self.forced_hidden[bnf.side.index()] = true;
805 self.shake_bnf = None;
806 }
807 }
808
809 if let Some(bounce) = &mut self.bounce {
810 bounce.frame += 1;
811 if bounce.frame >= BOUNCE_FRAMES {
812 self.bounce = None;
814 }
815 }
816
817 if let Some(sdh) = &mut self.slide_down_hide {
818 sdh.frame += 1;
819 if sdh.frame >= SLIDE_DOWN_HIDE_FRAMES {
820 self.forced_hidden[sdh.side.index()] = true;
823 self.slide_down_hide = None;
824 }
825 }
826
827 if let Some(tf) = &mut self.transform {
828 tf.frame += 1;
829 if tf.frame == TRANSFORM_HIDDEN_FRAMES {
830 self.forced_hidden[tf.side.index()] = false;
833 }
834 if tf.frame >= TRANSFORM_FRAMES {
835 self.transform = None;
836 }
837 }
838
839 match &mut self.objects {
840 Objects::None => {}
841 Objects::SpiralBalls { frame, .. } => {
842 *frame += 1;
843 if *frame >= SPIRAL_BALLS_FRAMES {
844 self.objects = Objects::None;
845 self.flash = Some(Flash::Short { frame: 0 });
847 }
848 }
849 Objects::ShootBalls {
850 pillars,
851 active,
852 frame,
853 } => {
854 *frame += 1;
855 let pillar = &mut pillars[*active];
856 let top = pillar.base_y + 8;
857 for ball in &mut pillar.balls {
858 if *ball != top {
859 *ball = ball.wrapping_sub(4);
862 }
863 }
864 pillar.balls.retain(|b| *b != top);
866 if pillar.balls.is_empty() {
867 if *active + 1 < pillars.len() {
868 *active += 1;
869 *frame = 0;
870 } else {
871 self.objects = Objects::None;
872 }
873 }
874 }
875 Objects::Falling {
876 frame,
877 tick,
878 objects,
879 ..
880 } => {
881 *frame += 1;
882 if *frame % 3 == 0 {
884 *tick += 1;
885 for obj in objects.iter_mut() {
886 let next = obj.movement.wrapping_add(1);
888 obj.movement = if next & 0x7f == 9 {
889 (next & 0x80) ^ 0x80
890 } else {
891 next
892 };
893 obj.y = obj.y.wrapping_add(2);
897 if obj.y >= 112 {
898 obj.y = 160; }
900 let idx = (obj.movement & 0x7f) as usize;
901 let delta = FALLING_DELTA_XS
902 .get(idx)
903 .copied()
904 .unwrap_or(FALLING_DELTA_XS_OVERFLOW);
905 if obj.movement & 0x80 != 0 {
906 obj.x = obj.x.wrapping_sub(delta);
907 } else {
908 obj.x = obj.x.wrapping_add(delta);
909 }
910 }
911 }
912 if *frame >= FALLING_FRAMES {
914 self.objects = Objects::None;
915 }
916 }
917 Objects::Droplets { iter, half, base_x } => {
918 *base_x = Self::droplet_grid_end_x(*base_x, if *half == 0 { 16 } else { 24 });
923 *half += 1;
924 if *half >= 2 {
925 *half = 0;
926 *iter += 1;
927 if *iter >= 32 {
928 self.objects = Objects::None;
929 }
930 }
931 }
932 }
933 }
934
935 fn droplet_grid_end_x(mut x: i32, base_y: i32) -> i32 {
938 let mut y = base_y;
939 loop {
940 x += 27;
941 if x >= 144 {
942 x -= 168;
943 y += 16;
944 if y >= 112 {
945 return x;
946 }
947 }
948 }
949 }
950
951 pub fn mon_hidden(&self, side: MonSide) -> bool {
956 if self.forced_hidden[side.index()] {
957 return true;
958 }
959 if let Some(blink) = self.blink {
960 if blink.side == side && blink.frame % 10 < 5 {
961 return true;
962 }
963 }
964 false
965 }
966
967 pub fn mon_dx(&self, side: MonSide) -> i32 {
971 if let Some(bnf) = self.shake_bnf {
972 if bnf.side == side {
973 return if (bnf.frame / 3) % 2 == 0 { -8 } else { 8 };
974 }
975 }
976 0
977 }
978
979 pub fn mon_dy(&self, side: MonSide) -> i32 {
984 if let Some(bounce) = self.bounce {
985 if bounce.side == side {
986 let frame = bounce.frame as i32;
987 if frame >= 5 * 21 {
988 return 0;
989 }
990 let within = frame % 21;
991 return (within / 3 + 1) * 8;
992 }
993 }
994 0
995 }
996
997 pub fn slide_down_hide_params(&self, side: MonSide) -> Option<(u32, i32)> {
1001 match self.slide_down_hide {
1002 Some(s) if s.side == side => {
1003 if s.frame < 8 {
1004 Some((5, 16))
1005 } else {
1006 Some((3, 32))
1007 }
1008 }
1009 _ => None,
1010 }
1011 }
1012
1013 pub fn squish_params(&self, side: MonSide) -> Option<(u32, bool)> {
1015 match self.squish {
1016 Some(s) if s.side == side => Some(s.params()),
1017 _ => None,
1018 }
1019 }
1020
1021 pub fn is_minimized(&self, side: MonSide) -> bool {
1023 self.minimized[side.index()]
1024 }
1025
1026 pub fn is_substitute(&self, side: MonSide) -> bool {
1028 self.substitute[side.index()]
1029 }
1030
1031 pub fn objects_active(&self) -> bool {
1033 !matches!(self.objects, Objects::None)
1034 }
1035
1036 pub fn enemy_hud_shake_offset(&self) -> i32 {
1038 match self.hud_shake {
1039 Some(hud) => {
1040 if (hud.frame / 2) % 2 == 0 {
1041 2
1042 } else {
1043 -2
1044 }
1045 }
1046 None => 0,
1047 }
1048 }
1049
1050 pub fn apply_enemy_hud_shake(&self, fb: &mut RgbaIndexedFrameBuffer) {
1057 let dx = self.enemy_hud_shake_offset();
1058 if dx != 0 {
1059 shift_rows_h(fb, HUD_SHAKE_HEIGHT, dx);
1060 }
1061 }
1062
1063 pub fn apply_screen_effects(&self, fb: &mut RgbaIndexedFrameBuffer) {
1066 if let Some(shake) = self.shake {
1067 let (dx, dy) = shake.offset();
1068 shift_rows_h(fb, SCREEN_SHAKE_HEIGHT, dx);
1069 shift_rows_v(fb, SCREEN_SHAKE_HEIGHT, dy);
1070 }
1071
1072 if self.wave_frame > 0 {
1073 let w = fb.width() as usize;
1076 let src = fb.indexed().clone();
1077 let start = (self.wave_frame - 1) as usize;
1078 for y in 0..fb.height() as usize {
1079 let shift = WAVY_LINE_OFFSETS[(start + y) % 32] as i32;
1080 if shift == 0 {
1081 continue;
1082 }
1083 for x in 0..w as i32 {
1084 let sx = (x + shift).clamp(0, w as i32 - 1) as usize;
1085 let color = src
1086 .get_pixel(sx as u32, y as u32)
1087 .unwrap_or(GbColor::White);
1088 fb.set_pixel_index(x as u32, y as u32, color);
1089 }
1090 }
1091 }
1092
1093 if let Some(tint) = self.tint {
1094 remap_shades(fb, &tint.map());
1095 }
1096
1097 match self.flash {
1098 Some(Flash::Short { frame }) => {
1099 if frame < 2 {
1100 remap_shades(fb, &[3, 2, 1, 0]);
1102 } else {
1103 fb.clear(Rgba::WHITE);
1105 }
1106 }
1107 Some(Flash::Long { frame }) => {
1108 let step = if frame < 24 {
1111 (frame / 2) as usize
1112 } else {
1113 (frame - 24) as usize % 12
1114 };
1115 remap_shades(fb, &FLASH_SCREEN_LONG_PALETTE[step]);
1116 }
1117 None => {}
1118 }
1119 }
1120
1121 pub fn render_objects(
1127 &self,
1128 fb: &mut RgbaIndexedFrameBuffer,
1129 ts0: &TileSet,
1130 ts1: &TileSet,
1131 pal: &Palette,
1132 ) {
1133 let mut layer = SpriteLayer::new();
1134 match &self.objects {
1135 Objects::None => return,
1136 Objects::SpiralBalls { side, frame } => {
1137 let step = (*frame / 5) as usize;
1138 let (base_y, base_x) = match side {
1140 MonSide::Player => (0i32, 0i32),
1141 MonSide::Enemy => (-40, 80),
1142 };
1143 for k in 0..3 {
1144 let Some(&(y, x)) = SPIRAL_BALL_COORDS.get(step + k) else {
1145 break;
1146 };
1147 layer.add(Self::oam(
1148 base_y + y as i32,
1149 base_x + x as i32,
1150 BALL_TILE,
1151 0,
1152 ));
1153 }
1154 layer.render(fb, ts0, pal, pal, None);
1155 }
1156 Objects::ShootBalls {
1157 pillars, active, ..
1158 } => {
1159 let pillar = &pillars[*active];
1160 for &y in &pillar.balls {
1161 layer.add(Self::oam(y, pillar.base_x, BALL_TILE, 0));
1162 }
1163 layer.render(fb, ts0, pal, pal, None);
1164 }
1165 Objects::Falling { tile, objects, .. } => {
1166 for obj in objects {
1167 let attr = if obj.movement & 0x80 != 0 {
1168 OAM_X_FLIP
1169 } else {
1170 0
1171 };
1172 layer.add(Self::oam(obj.y as i32, obj.x as i32, *tile, attr));
1173 }
1174 layer.render(fb, ts1, pal, pal, None);
1175 }
1176 Objects::Droplets { half, base_x, .. } => {
1177 let base_y = if *half == 0 { 16 } else { 24 };
1178 let mut x = *base_x;
1179 let mut y = base_y;
1180 loop {
1181 x += 27;
1182 if x >= 144 {
1183 x -= 168;
1184 y += 16;
1185 if y >= 112 {
1186 break;
1187 }
1188 }
1189 layer.add(Self::oam(y, x, DROPLET_TILE, 0));
1190 }
1191 layer.render(fb, ts0, pal, pal, None);
1192 }
1193 }
1194 }
1195
1196 fn oam(oam_y: i32, oam_x: i32, tile: u8, attr: u8) -> SpriteOamEntry {
1199 SpriteOamEntry::new(
1200 oam_y - 16,
1201 oam_x - 8,
1202 tile.wrapping_sub(ANIM_BASE_TILE_ID),
1203 attr,
1204 )
1205 }
1206
1207 pub fn draw_substitute(fb: &mut RgbaIndexedFrameBuffer, rect: MonRect, doll: &TileSet, pal: &Palette, side: MonSide) {
1217 let (base_tile, dx, dy) = match side {
1218 MonSide::Enemy => (0usize, 2 * TILE_SIZE, 4 * TILE_SIZE),
1219 MonSide::Player => (4, 3 * TILE_SIZE, 4 * TILE_SIZE),
1220 };
1221 let origin_x = rect.x + dx as i32;
1222 let origin_y = rect.y + dy as i32;
1223 for t in 0..4usize {
1224 let tile = doll.get(base_tile + t);
1225 let tx = origin_x + (t % 2) as i32 * TILE_PIXELS as i32;
1226 let ty = origin_y + (t / 2) as i32 * TILE_PIXELS as i32;
1227 Self::draw_tile_opaque(fb, tile, tx, ty, pal);
1228 }
1229 }
1230
1231 pub fn draw_minimized(fb: &mut RgbaIndexedFrameBuffer, rect: MonRect, pal: &Palette) {
1235 let color = pal.color(GbColor::from_u8(3));
1236 let ox = rect.x + 3 * TILE_SIZE as i32;
1237 let oy = rect.y + 4 * TILE_SIZE as i32 + 2;
1238 for (row, bits) in MINIMIZED_BLOB.iter().enumerate() {
1239 for (col, &on) in bits.iter().enumerate() {
1240 if on != 0 {
1241 let x = ox + col as i32;
1242 let y = oy + row as i32;
1243 if x >= 0 && y >= 0 {
1244 fb.set_pixel(x as u32, y as u32, color);
1245 }
1246 }
1247 }
1248 }
1249 }
1250
1251 pub fn draw_squished(
1256 fb: &mut RgbaIndexedFrameBuffer,
1257 ts: &TileSet,
1258 x: i32,
1259 y: i32,
1260 tiles_per_row: u32,
1261 pal: &Palette,
1262 width_tiles: u32,
1263 anchor_right: bool,
1264 ) {
1265 if width_tiles == 0 {
1266 return;
1267 }
1268 let full_w = tiles_per_row * TILE_SIZE;
1269 let out_w = width_tiles * TILE_SIZE;
1270 let ox = if anchor_right {
1271 x + (full_w - out_w) as i32
1272 } else {
1273 x
1274 };
1275 let total_rows = ts.len() as u32 / tiles_per_row;
1276 for dy in 0..total_rows * TILE_SIZE {
1277 let ty = dy / TILE_SIZE;
1278 for dx in 0..out_w {
1279 let sx = dx * full_w / out_w;
1281 let tx = sx / TILE_SIZE;
1282 let tile = ts.get((ty * tiles_per_row + tx) as usize);
1283 let c = tile.get((dy % TILE_SIZE) as usize, (sx % TILE_SIZE) as usize);
1284 if c == 0 {
1285 continue;
1286 }
1287 let px = ox + dx as i32;
1288 let py = y + dy as i32;
1289 if px >= 0 && py >= 0 {
1290 fb.set_pixel(px as u32, py as u32, pal.color(GbColor::from_u8(c)));
1291 }
1292 }
1293 }
1294 }
1295
1296 pub fn draw_mon_rows(
1301 fb: &mut RgbaIndexedFrameBuffer,
1302 ts: &TileSet,
1303 x: i32,
1304 y: i32,
1305 tiles_per_row: u32,
1306 pal: &Palette,
1307 rows: u32,
1308 ) {
1309 let max_tiles = (rows * tiles_per_row) as usize;
1310 for idx in 0..max_tiles.min(ts.len()) {
1311 let tile = ts.get(idx);
1312 let tx = idx as u32 % tiles_per_row;
1313 let ty = idx as u32 / tiles_per_row;
1314 for row in 0..TILE_PIXELS {
1315 for col in 0..TILE_PIXELS {
1316 let c = tile.get(row, col);
1317 if c == 0 {
1318 continue;
1319 }
1320 let px = x + (tx * TILE_SIZE) as i32 + col as i32;
1321 let py = y + (ty * TILE_SIZE) as i32 + row as i32;
1322 if px >= 0 && py >= 0 {
1323 fb.set_pixel(px as u32, py as u32, pal.color(GbColor::from_u8(c)));
1324 }
1325 }
1326 }
1327 }
1328 }
1329
1330 fn draw_tile_opaque(
1333 fb: &mut RgbaIndexedFrameBuffer,
1334 tile: &crate::tile::Tile,
1335 x: i32,
1336 y: i32,
1337 pal: &Palette,
1338 ) {
1339 for row in 0..TILE_PIXELS {
1340 for col in 0..TILE_PIXELS {
1341 let c = tile.get(row, col);
1342 let px = x + col as i32;
1343 let py = y + row as i32;
1344 if px >= 0 && py >= 0 {
1345 fb.set_pixel(px as u32, py as u32, pal.color(GbColor::from_u8(c)));
1346 }
1347 }
1348 }
1349 }
1350}
1351
1352#[cfg(test)]
1355mod tests {
1356 use super::*;
1357 use crate::palette::GRAYSCALE_PALETTE;
1358 use crate::tile::Tile;
1359 use crate::RenderConfig;
1360
1361 fn fb() -> RgbaIndexedFrameBuffer {
1362 RgbaIndexedFrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE)
1363 }
1364
1365 fn apply(fx: &mut BattleEffects, effect: AnimEffect) -> u8 {
1366 fx.apply(&effect, MonSide::Player)
1367 }
1368
1369 #[test]
1372 fn squish_narrows_one_tile_per_pass_alternating_anchor() {
1373 let mut fx = BattleEffects::new();
1374 let wait = apply(&mut fx, AnimEffect::SquishMonPic);
1375 assert_eq!(wait, 26);
1376 let expected_widths = [6, 5, 4, 3, 2, 1, 0, 0];
1378 for (pass, &w) in expected_widths.iter().enumerate() {
1379 let (width, anchor_right) = fx.squish_params(MonSide::Player).unwrap();
1380 assert_eq!(width, w, "pass {}", pass);
1381 assert_eq!(anchor_right, pass % 2 == 1, "pass {}", pass);
1382 for _ in 0..3 {
1383 fx.tick();
1384 }
1385 }
1386 fx.tick();
1388 fx.tick();
1389 assert!(fx.squish_params(MonSide::Player).is_none());
1390 assert!(fx.mon_hidden(MonSide::Player));
1392 fx.apply(&AnimEffect::ShowPlayerMon, MonSide::Player);
1394 assert!(!fx.mon_hidden(MonSide::Player));
1395 }
1396
1397 #[test]
1398 fn squish_targets_attacker_side_only() {
1399 let mut fx = BattleEffects::new();
1400 fx.apply(&AnimEffect::SquishMonPic, MonSide::Enemy);
1401 assert!(fx.squish_params(MonSide::Enemy).is_some());
1402 assert!(fx.squish_params(MonSide::Player).is_none());
1403 }
1404
1405 #[test]
1408 fn transform_blocks_stream_and_reveals_after_beat() {
1409 let mut fx = BattleEffects::new();
1410 let wait = apply(&mut fx, AnimEffect::TransformMon);
1412 assert_eq!(wait, 12);
1413 assert!(fx.mon_hidden(MonSide::Player));
1415 assert!(!fx.mon_hidden(MonSide::Enemy));
1416 for _ in 0..5 {
1418 fx.tick();
1419 }
1420 assert!(fx.mon_hidden(MonSide::Player));
1421 fx.tick();
1424 assert!(!fx.mon_hidden(MonSide::Player));
1425 for _ in 0..6 {
1426 fx.tick();
1427 }
1428 assert!(!fx.mon_hidden(MonSide::Player));
1430 }
1431
1432 #[test]
1433 fn transform_targets_attacker_side() {
1434 let mut fx = BattleEffects::new();
1435 fx.apply(&AnimEffect::TransformMon, MonSide::Enemy);
1436 assert!(fx.mon_hidden(MonSide::Enemy));
1437 assert!(!fx.mon_hidden(MonSide::Player));
1438 fx.clear_side(MonSide::Enemy);
1440 fx.tick();
1441 assert!(!fx.mon_hidden(MonSide::Enemy));
1442 }
1443
1444 #[test]
1447 fn shake_back_and_forth_offsets_and_duration() {
1448 let mut fx = BattleEffects::new();
1449 let wait = apply(&mut fx, AnimEffect::ShakeBackAndForth);
1450 assert_eq!(wait, 96);
1451 for i in 0..32u8 {
1452 let want = if i % 2 == 0 { -8 } else { 8 };
1453 for _ in 0..3 {
1454 assert_eq!(fx.mon_dx(MonSide::Player), want);
1455 fx.tick();
1456 }
1457 }
1458 assert_eq!(fx.mon_dx(MonSide::Player), 0);
1459 assert!(fx.mon_hidden(MonSide::Player));
1461 }
1462
1463 #[test]
1466 fn blink_hides_first_five_of_ten() {
1467 let mut fx = BattleEffects::new();
1468 let wait = apply(&mut fx, AnimEffect::BlinkPlayerMon { times: 6 });
1469 assert_eq!(wait, 60);
1470 for cycle in 0..6 {
1471 for _ in 0..5 {
1472 assert!(fx.mon_hidden(MonSide::Player), "cycle {}", cycle);
1473 fx.tick();
1474 }
1475 for _ in 0..5 {
1476 assert!(!fx.mon_hidden(MonSide::Player), "cycle {}", cycle);
1477 fx.tick();
1478 }
1479 }
1480 }
1481
1482 #[test]
1483 fn blink_enemy_mon_blinks_defender() {
1484 let mut fx = BattleEffects::new();
1487 fx.apply(&AnimEffect::BlinkEnemyMon { times: 6 }, MonSide::Enemy);
1488 assert!(fx.mon_hidden(MonSide::Player));
1489 assert!(!fx.mon_hidden(MonSide::Enemy));
1490 }
1491
1492 #[test]
1495 fn spiral_balls_follow_coordinate_sequence() {
1496 let mut fx = BattleEffects::new();
1497 let wait = apply(&mut fx, AnimEffect::SpiralBallsInward);
1498 assert_eq!(wait, 105);
1499 let ts = TileSet::blank(80);
1500 let mut buf = fb();
1502 fx.render_objects(&mut buf, &ts, &ts, &GRAYSCALE_PALETTE);
1503 if let Objects::SpiralBalls { frame, .. } = &fx.objects {
1504 assert_eq!(*frame, 0);
1505 } else {
1506 panic!("expected spiral balls");
1507 }
1508 for _ in 0..105 {
1510 fx.tick();
1511 }
1512 assert!(!fx.objects_active());
1513 assert!(matches!(fx.flash, Some(Flash::Short { .. })));
1514 }
1515
1516 #[test]
1517 fn spiral_balls_enemy_base_offset() {
1518 let mut fx = BattleEffects::new();
1519 fx.apply(&AnimEffect::SpiralBallsInward, MonSide::Enemy);
1520 if let Objects::SpiralBalls { side, .. } = &fx.objects {
1522 assert_eq!(*side, MonSide::Enemy);
1523 } else {
1524 panic!("expected spiral balls");
1525 }
1526 }
1527
1528 #[test]
1531 fn shoot_balls_rise_and_pop_at_top() {
1532 let mut fx = BattleEffects::new();
1533 let wait = apply(&mut fx, AnimEffect::ShootBallsUpward { many: false });
1534 assert_eq!(wait, 8);
1536 if let Objects::ShootBalls { pillars, .. } = &fx.objects {
1537 assert_eq!(pillars[0].base_y, 48);
1539 assert_eq!(pillars[0].base_x, 40);
1540 assert_eq!(pillars[0].balls, vec![56, 64, 72, 80, 88]);
1541 } else {
1542 panic!("expected shoot balls");
1543 }
1544 fx.tick();
1546 if let Objects::ShootBalls { pillars, .. } = &fx.objects {
1547 assert_eq!(pillars[0].balls, vec![60, 68, 76, 84]);
1548 }
1549 for _ in 0..7 {
1550 fx.tick();
1551 }
1552 assert!(!fx.objects_active());
1553 }
1554
1555 #[test]
1556 fn shoot_many_balls_runs_six_pillars() {
1557 let mut fx = BattleEffects::new();
1558 fx.apply(&AnimEffect::ShootBallsUpward { many: true }, MonSide::Player);
1559 if let Objects::ShootBalls { pillars, .. } = &fx.objects {
1560 assert_eq!(pillars.len(), 6);
1561 assert_eq!(pillars[0].base_x, 0x10);
1562 assert_eq!(pillars[0].base_y, 0x50);
1563 assert_eq!(pillars[0].balls.len(), 4);
1564 } else {
1565 panic!("expected shoot balls");
1566 }
1567 for _ in 0..100 {
1569 fx.tick();
1570 }
1571 assert!(!fx.objects_active());
1572 }
1573
1574 #[test]
1577 fn petals_fall_until_first_reaches_104() {
1578 let mut fx = BattleEffects::new();
1579 let wait = apply(&mut fx, AnimEffect::PetalsFalling);
1580 assert_eq!(wait, 156);
1581 if let Objects::Falling { tile, objects, .. } = &fx.objects {
1582 assert_eq!(*tile, PETAL_TILE);
1583 assert_eq!(objects.len(), 20);
1584 assert_eq!(objects[0].y, 0);
1586 assert_eq!(objects[1].y, 16);
1587 assert_eq!(objects[0].x, FALLING_INITIAL_X[0]);
1588 } else {
1589 panic!("expected falling petals");
1590 }
1591 fx.tick();
1593 fx.tick();
1594 fx.tick();
1595 if let Objects::Falling { objects, tick, .. } = &fx.objects {
1596 assert_eq!(*tick, 1);
1597 assert_eq!(objects[0].y, 2);
1598 }
1599 for _ in 0..153 {
1600 fx.tick();
1601 }
1602 assert!(!fx.objects_active());
1603 }
1604
1605 #[test]
1606 fn leaves_use_leaf_tile_and_three_objects() {
1607 let mut fx = BattleEffects::new();
1608 fx.apply(&AnimEffect::LeavesFalling, MonSide::Player);
1609 if let Objects::Falling { tile, objects, .. } = &fx.objects {
1610 assert_eq!(*tile, LEAF_TILE);
1611 assert_eq!(objects.len(), 3);
1612 } else {
1613 panic!("expected falling leaves");
1614 }
1615 }
1616
1617 #[test]
1618 fn falling_movement_byte_wraps_and_flips_direction() {
1619 let mut fx = BattleEffects::new();
1622 fx.apply(&AnimEffect::LeavesFalling, MonSide::Player);
1623 let x0 = FALLING_INITIAL_X[0];
1624 let mut expected_x = x0;
1626 for (i, delta) in [1u8, 3, 5, 7, 9, 11, 13, 15].iter().enumerate() {
1627 for _ in 0..3 {
1628 fx.tick();
1629 }
1630 expected_x = expected_x.wrapping_add(*delta);
1631 if let Objects::Falling { objects, .. } = &fx.objects {
1632 assert_eq!(objects[0].x, expected_x, "after tick {}", i + 1);
1633 }
1634 }
1635 for _ in 0..3 {
1638 fx.tick();
1639 }
1640 if let Objects::Falling { objects, .. } = &fx.objects {
1641 assert_eq!(objects[0].x, expected_x);
1642 assert_eq!(objects[0].movement, 0x80);
1643 }
1644 }
1645
1646 #[test]
1649 fn droplets_run_64_frames_and_scroll() {
1650 let mut fx = BattleEffects::new();
1651 let wait = apply(&mut fx, AnimEffect::WaterDroplets);
1652 assert_eq!(wait, 64);
1653 let x_start = match &fx.objects {
1654 Objects::Droplets { base_x, .. } => *base_x,
1655 _ => panic!("expected droplets"),
1656 };
1657 assert_eq!(x_start, -16);
1658 fx.tick();
1659 let x_after = match &fx.objects {
1661 Objects::Droplets { base_x, .. } => *base_x,
1662 _ => panic!("expected droplets"),
1663 };
1664 assert_ne!(x_after, x_start);
1665 for _ in 0..63 {
1666 fx.tick();
1667 }
1668 assert!(!fx.objects_active());
1669 }
1670
1671 #[test]
1674 fn hud_shake_alternates_plus_minus_two() {
1675 let mut fx = BattleEffects::new();
1676 let wait = apply(&mut fx, AnimEffect::ShakeEnemyHud { variant: 1 });
1677 assert_eq!(wait, 32);
1678 let pattern = [2, 2, -2, -2];
1679 for i in 0..32 {
1680 assert_eq!(fx.enemy_hud_shake_offset(), pattern[i % 4], "frame {}", i);
1681 fx.tick();
1682 }
1683 assert_eq!(fx.enemy_hud_shake_offset(), 0);
1684 }
1685
1686 #[test]
1689 fn wavy_screen_lasts_255_frames() {
1690 let mut fx = BattleEffects::new();
1691 let wait = apply(&mut fx, AnimEffect::WavyScreen);
1692 assert_eq!(wait, 255);
1693 for _ in 0..255 {
1694 fx.tick();
1695 }
1696 assert_eq!(fx.wave_frame, 0);
1697 }
1698
1699 #[test]
1702 fn shake_screen_decays_amplitude() {
1703 let mut fx = BattleEffects::new();
1706 let wait = apply(&mut fx, AnimEffect::ShakeScreenH {
1707 pixels: 8,
1708 frames: 72,
1709 });
1710 assert_eq!(wait, 72);
1711 let offsets: Vec<i32> = (0..9).map(|_| {
1712 let o = fx.shake.unwrap().offset().0;
1713 fx.tick();
1714 o
1715 }).collect();
1716 assert_eq!(offsets, vec![8, 8, 8, 8, 0, 0, 0, 0, 0]);
1717 assert_eq!(fx.shake.unwrap().offset().0, 7);
1719 for _ in 0..63 {
1720 fx.tick();
1721 }
1722 assert!(fx.shake.is_none());
1723 }
1724
1725 #[test]
1726 fn small_shake_alternates() {
1727 let mut fx = BattleEffects::new();
1728 apply(&mut fx, AnimEffect::ShakeScreenV {
1729 pixels: 1,
1730 frames: 4,
1731 });
1732 let offsets: Vec<i32> = (0..4).map(|_| {
1733 let o = fx.shake.unwrap().offset().1;
1734 fx.tick();
1735 o
1736 }).collect();
1737 assert_eq!(offsets, vec![1, -1, 1, -1]);
1738 assert!(fx.shake.is_none());
1739 }
1740
1741 #[test]
1744 fn flash_short_and_long_durations() {
1745 let mut fx = BattleEffects::new();
1746 assert_eq!(apply(&mut fx, AnimEffect::FlashScreen { frames: 4 }), 4);
1747 for _ in 0..4 {
1748 fx.tick();
1749 }
1750 assert!(fx.flash.is_none());
1751 assert_eq!(apply(&mut fx, AnimEffect::FlashScreen { frames: 48 }), 48);
1752 for _ in 0..48 {
1753 fx.tick();
1754 }
1755 assert!(fx.flash.is_none());
1756 }
1757
1758 #[test]
1759 fn flash_long_palette_step_timing() {
1760 let step_of = |frame: u8| {
1763 if frame < 24 {
1764 (frame / 2) as usize
1765 } else {
1766 (frame - 24) as usize % 12
1767 }
1768 };
1769 assert_eq!(step_of(0), 0);
1770 assert_eq!(step_of(1), 0);
1771 assert_eq!(step_of(2), 1);
1772 assert_eq!(step_of(23), 11);
1773 assert_eq!(step_of(24), 0);
1774 assert_eq!(step_of(35), 11);
1775 assert_eq!(step_of(36), 0);
1776 assert_eq!(step_of(47), 11);
1777 }
1778
1779 #[test]
1782 fn bounce_slides_down_five_times_then_shows() {
1783 let mut fx = BattleEffects::new();
1784 let wait = apply(&mut fx, AnimEffect::BounceUpAndDown);
1786 assert_eq!(wait, 5 * 21 + 3);
1787 for cycle in 0..5 {
1788 for step in 0..7 {
1790 for _ in 0..3 {
1791 assert_eq!(
1792 fx.mon_dy(MonSide::Player),
1793 (step + 1) * 8,
1794 "cycle {} step {}",
1795 cycle,
1796 step
1797 );
1798 fx.tick();
1799 }
1800 }
1801 }
1802 assert_eq!(fx.mon_dy(MonSide::Player), 0);
1804 assert!(!fx.mon_hidden(MonSide::Player));
1805 }
1806
1807 #[test]
1810 fn slide_down_hide_shrinks_rows_then_hides() {
1811 let mut fx = BattleEffects::new();
1812 let wait = apply(&mut fx, AnimEffect::SlidePlayerMonDownAndHide);
1813 assert_eq!(wait, 16);
1814 for _ in 0..8 {
1816 assert_eq!(fx.slide_down_hide_params(MonSide::Player), Some((5, 16)));
1817 fx.tick();
1818 }
1819 for _ in 0..8 {
1821 assert_eq!(fx.slide_down_hide_params(MonSide::Player), Some((3, 32)));
1822 fx.tick();
1823 }
1824 assert_eq!(fx.slide_down_hide_params(MonSide::Player), None);
1825 assert!(fx.mon_hidden(MonSide::Player));
1827 fx.apply(&AnimEffect::ShowPlayerMon, MonSide::Player);
1828 assert!(!fx.mon_hidden(MonSide::Player));
1829 }
1830
1831 #[test]
1832 fn draw_mon_rows_crops_bottom_rows() {
1833 let mut ts = TileSet::blank(49);
1835 for i in 0..49 {
1836 let mut t = Tile::blank();
1837 for row in 0..8 {
1838 for col in 0..8 {
1839 t.pixels[row][col] = (i % 4) as u8;
1840 }
1841 }
1842 ts.set(i, t);
1843 }
1844 let mut buf = fb();
1845 buf.clear(Rgba::WHITE);
1846 BattleEffects::draw_mon_rows(&mut buf, &ts, 8, 40, 7, &GRAYSCALE_PALETTE, 5);
1847 let below = buf.get_pixel(8, 40 + 5 * 8).unwrap();
1849 assert_eq!((below.r, below.g, below.b), (255, 255, 255));
1850 let c1 = buf.get_pixel(8 + 8, 40).unwrap();
1852 assert_eq!((c1.r, c1.g, c1.b), (170, 170, 170));
1853 }
1854
1855 #[test]
1858 fn substitute_and_minimize_latch_until_cleared() {
1859 let mut fx = BattleEffects::new();
1860 apply(&mut fx, AnimEffect::SubstituteMon);
1861 assert!(fx.is_substitute(MonSide::Player));
1862 assert!(!fx.is_substitute(MonSide::Enemy));
1863 fx.clear_side(MonSide::Player);
1864 assert!(!fx.is_substitute(MonSide::Player));
1865
1866 apply(&mut fx, AnimEffect::MinimizeMon);
1867 assert!(fx.is_minimized(MonSide::Player));
1868 fx.clear_side(MonSide::Player);
1869 assert!(!fx.is_minimized(MonSide::Player));
1870 }
1871
1872 #[test]
1875 fn draw_substitute_places_doll_tiles() {
1876 let mut doll = TileSet::blank(24);
1878 for i in 0..24 {
1879 let mut t = Tile::blank();
1880 for row in 0..8 {
1881 for col in 0..8 {
1882 t.pixels[row][col] = (i % 4) as u8;
1883 }
1884 }
1885 doll.set(i, t);
1886 }
1887 let mut buf = fb();
1888 let rect = MonRect { x: 96, y: 0 };
1889 BattleEffects::draw_substitute(&mut buf, rect, &doll, &GRAYSCALE_PALETTE, MonSide::Enemy);
1890 assert!(buf.get_pixel(96 + 16, 32).is_some());
1892 let white = buf.get_pixel(96 + 16, 32).unwrap();
1894 assert_eq!((white.r, white.g, white.b), (255, 255, 255));
1895 let c1 = buf.get_pixel(96 + 16 + 8, 32).unwrap();
1896 assert_eq!((c1.r, c1.g, c1.b), (170, 170, 170));
1897 let below = buf.get_pixel(96 + 16, 32 + 16).unwrap();
1901 assert_eq!((below.r, below.g, below.b), (255, 255, 255));
1902 }
1903
1904 #[test]
1905 fn draw_minimized_blob_at_pic_offset() {
1906 let mut buf = fb();
1907 buf.clear(Rgba::WHITE);
1908 BattleEffects::draw_minimized(&mut buf, MonRect { x: 8, y: 40 }, &GRAYSCALE_PALETTE);
1909 assert_eq!(buf.get_pixel(32 + 3, 74).unwrap().r, 0);
1911 assert_eq!(buf.get_pixel(32 + 4, 74).unwrap().r, 0);
1912 assert_eq!(buf.get_pixel(32 + 2, 74).unwrap().r, 255);
1913 assert_eq!(buf.get_pixel(32 + 2, 74 + 4).unwrap().r, 0);
1915 assert_eq!(buf.get_pixel(32 + 5, 74 + 4).unwrap().r, 0);
1916 }
1917
1918 #[test]
1919 fn draw_squished_scales_horizontally() {
1920 let mut ts = TileSet::blank(7);
1922 for i in 0..7 {
1923 let mut t = Tile::blank();
1924 for row in 0..8 {
1925 for col in 0..8 {
1926 t.pixels[row][col] = (i % 4) as u8;
1927 }
1928 }
1929 ts.set(i, t);
1930 }
1931 let mut buf = fb();
1932 BattleEffects::draw_squished(&mut buf, &ts, 0, 0, 7, &GRAYSCALE_PALETTE, 4, false);
1933 assert!(buf.get_pixel(0, 4).is_some());
1936 let right = buf.get_pixel(33, 4).unwrap();
1938 assert_eq!((right.r, right.g, right.b), (255, 255, 255));
1939 }
1940}