1use crate::sprite::SpriteOamEntry;
2
3use super::*;
4
5#[derive(Debug, Clone)]
9struct SubAnimState {
10 subanim_id: u8,
12 transform: SubAnimTransform,
14 frame_index: usize,
16 num_frames: usize,
18 delay: u8,
20 waiting_for_delay: bool,
22 tileset: u8,
24 sound_id: u8,
26 sound_pending: bool,
29}
30
31#[derive(Debug, Clone)]
43pub struct AnimationPlayer {
44 move_id: usize,
46 player_is_attacker: bool,
48 command_index: usize,
50 num_commands: usize,
52 subanim_state: Option<SubAnimState>,
54 oam_buffer: Vec<SpriteOamEntry>,
56 finished: bool,
58}
59
60impl AnimationPlayer {
61 pub fn new() -> Self {
63 Self {
64 move_id: 0,
65 player_is_attacker: true,
66 command_index: 0,
67 num_commands: 0,
68 subanim_state: None,
69 oam_buffer: Vec::with_capacity(40),
70 finished: true,
71 }
72 }
73
74 pub fn start(&mut self, move_id: usize, player_is_attacker: bool) {
78 let move_id = if !player_is_attacker {
82 match move_id + 1 {
83 id if id == AMNESIA as usize => CONF_ANIM as usize - 1,
84 id if id == REST as usize => SLP_ANIM as usize - 1,
85 _ => move_id,
86 }
87 } else {
88 move_id
89 };
90
91 self.move_id = move_id;
92 self.player_is_attacker = player_is_attacker;
93 self.command_index = 0;
94 self.subanim_state = None;
95 self.oam_buffer.clear();
96 self.finished = false;
97
98 if move_id < NUM_MOVE_ANIMS {
99 self.num_commands = MOVE_ANIM_DATA[move_id].len();
100 } else {
101 self.num_commands = 0;
102 self.finished = true;
103 }
104 }
105
106 pub fn is_finished(&self) -> bool {
108 self.finished
109 }
110
111 pub fn oam_entries(&self) -> &[SpriteOamEntry] {
113 &self.oam_buffer
114 }
115
116 pub fn current_tileset(&self) -> Option<u8> {
118 self.subanim_state.as_ref().map(|s| s.tileset)
119 }
120
121 pub fn decode_command(raw: &(u8, u8, u8, u8)) -> AnimCommand {
123 let (kind, sound_val, id_val, packed) = *raw;
124 if kind == 0 {
125 AnimCommand::SubAnim {
127 sound_id: sound_val,
128 subanim_id: id_val,
129 tileset: packed >> 6,
130 delay: packed & 0x3F,
131 }
132 } else {
133 AnimCommand::Effect {
135 sound_id: sound_val,
136 effect: SpecialEffect::from_u8(id_val).unwrap_or(SpecialEffect::WavyScreen),
137 }
138 }
139 }
140
141 pub fn resolve_transform(&self, raw_transform: SubAnimTransform) -> SubAnimTransform {
144 match raw_transform {
145 SubAnimTransform::Enemy => {
146 if self.player_is_attacker {
147 SubAnimTransform::HFlip
149 } else {
150 SubAnimTransform::Normal
152 }
153 }
154 other => {
155 if self.player_is_attacker {
160 SubAnimTransform::Normal
161 } else {
162 other
163 }
164 }
165 }
166 }
167}
168
169impl AnimationPlayer {
172 pub fn render_frame_block(
179 frame_block_id: usize,
180 base_coord_id: usize,
181 transform: SubAnimTransform,
182 dest: &mut Vec<SpriteOamEntry>,
183 ) {
184 if frame_block_id >= NUM_FRAMEBLOCKS || base_coord_id >= NUM_BASECOORDS {
185 return;
186 }
187
188 let fb_data = FRAME_BLOCK_DATA[frame_block_id];
189 let (base_y, base_x) = BASE_COORDS[base_coord_id];
190
191 for &(x_off, y_off, raw_tile, flags) in fb_data {
194 let (screen_y, screen_x, tile_id, oam_flags) = match transform {
195 SubAnimTransform::Normal | SubAnimTransform::Reverse => {
196 let y = base_y as i32 + y_off as i32;
200 let x = base_x as i32 + x_off as i32;
201 let tile = raw_tile.wrapping_add(ANIM_BASE_TILE_ID);
202 (y, x, tile, flags)
203 }
204
205 SubAnimTransform::HvFlip => {
206 let y = 136i32 - (base_y as i32 + y_off as i32);
210 let x = 168i32 - (base_x as i32 + x_off as i32);
211 let tile = raw_tile.wrapping_add(ANIM_BASE_TILE_ID);
212 let new_flags = match flags & 0x60 {
214 0x00 => (flags & !0x60) | 0x60,
215 0x20 => (flags & !0x60) | 0x40,
216 0x40 => (flags & !0x60) | 0x20,
217 0x60 => flags & !0x60,
218 _ => flags, };
220 (y, x, tile, new_flags)
221 }
222
223 SubAnimTransform::HFlip => {
224 let y = base_y as i32 + y_off as i32 + 40;
228 let x = 168i32 - (base_x as i32 + x_off as i32);
229 let tile = raw_tile.wrapping_add(ANIM_BASE_TILE_ID);
230 let new_flags = flags ^ OAM_XFLIP;
232 (y, x, tile, new_flags)
233 }
234
235 SubAnimTransform::CoordFlip => {
236 let y = (136i32 - base_y as i32) + y_off as i32;
240 let x = (168i32 - base_x as i32) + x_off as i32;
241 let tile = raw_tile.wrapping_add(ANIM_BASE_TILE_ID);
242 (y, x, tile, flags)
243 }
244
245 SubAnimTransform::Enemy => {
246 let y = base_y as i32 + y_off as i32;
248 let x = base_x as i32 + x_off as i32;
249 let tile = raw_tile.wrapping_add(ANIM_BASE_TILE_ID);
250 (y, x, tile, flags)
251 }
252 };
253
254 dest.push(SpriteOamEntry::new(screen_y, screen_x, tile_id, oam_flags));
255 }
256 }
257}
258
259impl AnimationPlayer {
262 pub fn tick(&mut self) -> AnimTickResult {
273 if self.finished {
274 return AnimTickResult::Done;
275 }
276
277 if let Some(ref mut state) = self.subanim_state {
279 if state.waiting_for_delay {
281 state.waiting_for_delay = false;
282 }
284
285 let subanim = get_subanimation(state.subanim_id as usize);
287
288 if state.frame_index < state.num_frames {
289 let frame_idx = if state.transform == SubAnimTransform::Reverse {
290 state.num_frames - 1 - state.frame_index
292 } else {
293 state.frame_index
294 };
295
296 let frame = &subanim.frames[frame_idx];
297 let mode = frame.mode;
298
299 if mode.cleans_oam() {
301 self.oam_buffer.clear();
302 }
303
304 Self::render_frame_block(
306 frame.frame_block_id as usize,
307 frame.base_coord_id as usize,
308 state.transform,
309 &mut self.oam_buffer,
310 );
311
312 state.frame_index += 1;
313
314 let frame_hook = get_frame_hook(self.move_id as u8 + 1);
318 let counter = (state.num_frames - state.frame_index) as u8 + 1;
319 let hook = frame_hook.and_then(|h| h.effect_for_counter(counter));
320
321 if frame_hook == Some(FrameHook::Growl) {
324 let copies: Vec<SpriteOamEntry> =
325 self.oam_buffer.iter().take(4).cloned().collect();
326 for (i, entry) in copies.into_iter().enumerate() {
327 if self.oam_buffer.len() > 4 + i {
328 self.oam_buffer[4 + i] = entry;
329 } else {
330 self.oam_buffer.push(entry);
331 }
332 }
333 }
334
335 let sound = if state.sound_pending {
338 state.sound_pending = false;
339 (state.sound_id != 0).then_some(state.sound_id)
340 } else {
341 None
342 };
343
344 if mode.has_delay() && state.delay > 0 {
348 state.waiting_for_delay = true;
349 return AnimTickResult::WaitDelay {
350 frames: state.delay,
351 sound,
352 hook,
353 };
354 }
355
356 return AnimTickResult::Playing { sound, hook };
358 }
359
360 self.subanim_state = None;
362 self.oam_buffer.clear();
363 }
364
365 if self.command_index >= self.num_commands {
367 self.finished = true;
368 return AnimTickResult::Done;
369 }
370
371 let raw = &MOVE_ANIM_DATA[self.move_id][self.command_index];
372 self.command_index += 1;
373 let cmd = Self::decode_command(raw);
374
375 match cmd {
376 AnimCommand::SubAnim {
377 sound_id,
378 subanim_id,
379 tileset,
380 delay,
381 } => {
382 let subanim = get_subanimation(subanim_id as usize);
383 let raw_transform = subanim.transform;
384 let resolved = self.resolve_transform(raw_transform);
385 let num_frames = subanim.frames.len();
386
387 self.subanim_state = Some(SubAnimState {
388 subanim_id,
389 transform: resolved,
390 frame_index: 0,
391 num_frames,
392 delay,
393 waiting_for_delay: false,
394 tileset,
395 sound_id,
396 sound_pending: true,
397 });
398
399 self.tick()
401 }
402
403 AnimCommand::Effect { sound_id, effect } => AnimTickResult::Effect {
404 sound: (sound_id != 0).then_some(sound_id),
405 effect,
406 },
407 }
408 }
409}
410
411impl FrameHook {
414 pub fn effect_for_counter(self, counter: u8) -> Option<AnimEffect> {
420 const FLASH: AnimEffect = AnimEffect::FlashScreen { frames: 4 };
422 match self {
423 FrameHook::FlashScreen => Some(FLASH),
424 FrameHook::FlashScreenEveryFour => (counter % 4 == 0).then_some(FLASH),
425 FrameHook::FlashScreenEveryEight => (counter % 8 == 0).then_some(FLASH),
426 FrameHook::BlizzardFlash => matches!(counter, 13 | 9 | 5 | 1).then_some(FLASH),
427 FrameHook::Explode => {
428 if counter == 1 {
429 Some(AnimEffect::HidePlayerMon)
432 } else {
433 (counter % 4 == 0).then_some(FLASH)
434 }
435 }
436 FrameHook::RockSlide => match counter {
437 8..=11 => Some(AnimEffect::ShakeScreenHV {
440 pixels: 1,
441 frames: 9,
442 }),
443 1 => Some(FLASH),
444 _ => None,
445 },
446 FrameHook::Growl
447 | FrameHook::TailWhipUnused
448 | FrameHook::BallToss
449 | FrameHook::BallShake
450 | FrameHook::BallPoof
451 | FrameHook::TradeHideMonster
452 | FrameHook::TradeShakeBall
453 | FrameHook::TradeJumpBall => None,
454 }
455 }
456}
457
458impl AnimationPlayer {
461 pub fn apply_effect(effect: SpecialEffect) -> AnimEffect {
466 match effect {
467 SpecialEffect::WavyScreen => AnimEffect::WavyScreen,
468 SpecialEffect::SubstituteMon => AnimEffect::SubstituteMon,
469 SpecialEffect::ShakeBackAndForth => AnimEffect::ShakeBackAndForth,
470 SpecialEffect::SlideEnemyMonOff => AnimEffect::SlideEnemyMonOff,
471 SpecialEffect::ShowEnemyMonPic => AnimEffect::ShowEnemyMon,
472 SpecialEffect::ShowMonPic => AnimEffect::ShowPlayerMon,
473 SpecialEffect::BlinkEnemyMon => AnimEffect::BlinkEnemyMon { times: 6 },
474 SpecialEffect::HideEnemyMonPic => AnimEffect::HideEnemyMon,
475 SpecialEffect::FlashEnemyMonPic => AnimEffect::FlashEnemyMonPic,
476 SpecialEffect::DelayAnimation10 => AnimEffect::Delay10,
477 SpecialEffect::SpiralBallsInward => AnimEffect::SpiralBallsInward,
478 SpecialEffect::ShakeEnemyHud2 => AnimEffect::ShakeEnemyHud { variant: 2 },
479 SpecialEffect::ShakeEnemyHud => AnimEffect::ShakeEnemyHud { variant: 1 },
480 SpecialEffect::SlideMonHalfOff => AnimEffect::SlidePlayerMonHalfOff,
481 SpecialEffect::PetalsFalling => AnimEffect::PetalsFalling,
482 SpecialEffect::LeavesFalling => AnimEffect::LeavesFalling,
483 SpecialEffect::TransformMon => AnimEffect::TransformMon,
484 SpecialEffect::SlideMonDownAndHide => AnimEffect::SlidePlayerMonDownAndHide,
485 SpecialEffect::MinimizeMon => AnimEffect::MinimizeMon,
486 SpecialEffect::BounceUpAndDown => AnimEffect::BounceUpAndDown,
487 SpecialEffect::ShootManyBallsUpward => AnimEffect::ShootBallsUpward { many: true },
488 SpecialEffect::ShootBallsUpward => AnimEffect::ShootBallsUpward { many: false },
489 SpecialEffect::SquishMonPic => AnimEffect::SquishMonPic,
490 SpecialEffect::HideMonPic => AnimEffect::HidePlayerMon,
491 SpecialEffect::LightScreenPalette => AnimEffect::LightScreenPalette,
492 SpecialEffect::ResetMonPosition => AnimEffect::ResetPlayerMonPosition,
493 SpecialEffect::MoveMonHorizontally => AnimEffect::MovePlayerMonH,
494 SpecialEffect::BlinkMon => AnimEffect::BlinkPlayerMon { times: 6 },
495 SpecialEffect::SlideMonOff => AnimEffect::SlidePlayerMonOff,
498 SpecialEffect::FlashMonPic => AnimEffect::FlashPlayerMonPic,
499 SpecialEffect::SlideMonDown => AnimEffect::SlidePlayerMonDown,
500 SpecialEffect::SlideMonUp => AnimEffect::SlidePlayerMonUp,
501 SpecialEffect::FlashScreenLong => AnimEffect::FlashScreen { frames: 48 },
504 SpecialEffect::DarkenMonPalette => AnimEffect::DarkenMonPalette,
505 SpecialEffect::WaterDropletsEverywhere => AnimEffect::WaterDroplets,
506 SpecialEffect::ShakeScreen => AnimEffect::ShakeScreenH {
509 pixels: 8,
510 frames: 72,
511 },
512 SpecialEffect::ResetScreenPalette => AnimEffect::ResetScreenPalette,
513 SpecialEffect::DarkScreenPalette => AnimEffect::DarkScreenPalette,
514 SpecialEffect::DarkScreenFlash => AnimEffect::FlashScreen { frames: 4 },
516 }
517 }
518}
519
520impl Default for AnimationPlayer {
521 fn default() -> Self {
522 Self::new()
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 fn run_animation(move_id: usize) -> Vec<AnimTickResult> {
531 run_animation_as(move_id, true)
532 }
533
534 fn run_animation_as(move_id: usize, player_is_attacker: bool) -> Vec<AnimTickResult> {
535 let mut player = AnimationPlayer::new();
536 player.start(move_id, player_is_attacker);
537 let mut results = Vec::new();
538 let mut iterations = 0;
539 const MAX_ITERATIONS: usize = 10000;
540
541 loop {
542 if iterations >= MAX_ITERATIONS {
543 break;
544 }
545 iterations += 1;
546
547 let result = player.tick();
548 results.push(result.clone());
549
550 match result {
551 AnimTickResult::Done => break,
552 AnimTickResult::WaitDelay { frames: n, .. } => {
553 for _ in 0..n {
554 let delay_result = player.tick();
555 results.push(delay_result.clone());
556 iterations += 1;
557 }
558 }
559 _ => {}
560 }
561 }
562 results
563 }
564
565 #[test]
566 fn pound_animation_has_frames() {
567 let results = run_animation(0x00);
568 let has_frames = results.iter().any(|r| matches!(r, AnimTickResult::Playing { .. } | AnimTickResult::WaitDelay { .. }));
569 assert!(has_frames, "Pound should have animation frames");
570 }
571
572 #[test]
573 fn earthquake_has_screen_shake() {
574 let results = run_animation(0x58);
575 let shake_count = results.iter().filter(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::ShakeScreen, .. })).count();
576 assert_eq!(shake_count, 2, "Earthquake should have 2 screen shakes");
577 }
578
579 #[test]
580 fn thunder_punch_has_palette_effects() {
581 let results = run_animation(0x08);
582 let dark_palette = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::DarkScreenPalette, .. }));
583 let reset_palette = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::ResetScreenPalette, .. }));
584 assert!(dark_palette, "ThunderPunch should have dark screen palette");
585 assert!(reset_palette, "ThunderPunch should have reset screen palette");
586 }
587
588 #[test]
589 fn selfdestruct_has_explosion() {
590 let results = run_animation(0x77);
591 let has_frames = results.iter().any(|r| matches!(r, AnimTickResult::Playing { .. } | AnimTickResult::WaitDelay { .. }));
592 assert!(has_frames, "Selfdestruct should have explosion frames");
593 }
594
595 #[test]
596 fn splash_has_bounce() {
597 let results = run_animation(0x95);
598 let bounce = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::BounceUpAndDown, .. }));
599 assert!(bounce, "Splash should have bounce effect");
600 }
601
602 #[test]
603 fn teleport_has_squish_and_balls() {
604 let results = run_animation(0x63);
605 let squish = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SquishMonPic, .. }));
606 let balls = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::ShootBallsUpward, .. }));
607 assert!(squish, "Teleport should have squish effect");
608 assert!(balls, "Teleport should have shoot balls upward");
609 }
610
611 #[test]
612 fn acid_armor_slides_down() {
613 let results = run_animation(0x96);
614 let slide = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SlideMonDownAndHide, .. }));
615 assert!(slide, "Acid Armor should have slide down and hide");
616 }
617
618 #[test]
619 fn minimize_has_minimize_effect() {
620 let results = run_animation(0x6A);
621 let minimize = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::MinimizeMon, .. }));
622 assert!(minimize, "Minimize should have minimize effect");
623 }
624
625 #[test]
626 fn transform_has_transform_effect() {
627 let results = run_animation(0x8F);
628 let transform = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::TransformMon, .. }));
629 assert!(transform, "Transform should have transform effect");
630 }
631
632 #[test]
633 fn substitute_has_substitute_effect() {
634 let results = run_animation(0xA3);
635 let substitute = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SubstituteMon, .. }));
636 assert!(substitute, "Substitute should have substitute effect");
637 }
638
639 #[test]
640 fn double_team_has_shake() {
641 let results = run_animation(0x67);
642 let shake = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::ShakeBackAndForth, .. }));
643 assert!(shake, "Double Team should have shake back and forth");
644 }
645
646 #[test]
647 fn recover_has_blink() {
648 let results = run_animation(0x68);
649 let blink = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::BlinkMon, .. }));
650 assert!(blink, "Recover should have blink effect");
651 }
652
653 #[test]
654 fn whirlwind_slides_enemy_off() {
655 let results = run_animation(0x11);
656 let slide = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SlideEnemyMonOff, .. }));
657 assert!(slide, "Whirlwind should slide enemy off");
658 }
659
660 #[test]
661 fn dig_slides_mon_up() {
662 let results = run_animation(0x5A);
663 let slide = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SlideMonUp, .. }));
664 assert!(slide, "Dig should slide mon up");
665 }
666
667 #[test]
668 fn confusion_has_flash() {
669 let results = run_animation(0x5C);
670 let flash = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::FlashScreenLong, .. }));
671 assert!(flash, "Confusion should have screen flash");
672 }
673
674 #[test]
675 fn psychic_has_flash_and_wavy() {
676 let results = run_animation(0x5D);
677 let flash = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::FlashScreenLong, .. }));
678 let wavy = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::WavyScreen, .. }));
679 assert!(flash, "Psychic should have screen flash");
680 assert!(wavy, "Psychic should have wavy screen");
681 }
682
683 #[test]
684 fn hyper_beam_has_complex_sequence() {
685 let results = run_animation(0x3E);
686 let dark_palette = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::DarkScreenPalette, .. }));
687 let spiral = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::SpiralBallsInward, .. }));
688 let reset = results.iter().any(|r| matches!(r, AnimTickResult::Effect { effect: SpecialEffect::ResetScreenPalette, .. }));
689 assert!(dark_palette, "Hyper Beam should have dark palette");
690 assert!(spiral, "Hyper Beam should have spiral balls");
691 assert!(reset, "Hyper Beam should have reset palette");
692 }
693
694 #[test]
695 fn slide_mon_off_is_full_slide() {
696 assert_eq!(
699 AnimationPlayer::apply_effect(SpecialEffect::SlideMonOff),
700 AnimEffect::SlidePlayerMonOff
701 );
702 assert_eq!(
703 AnimationPlayer::apply_effect(SpecialEffect::SlideMonHalfOff),
704 AnimEffect::SlidePlayerMonHalfOff
705 );
706 }
707
708 #[test]
709 fn flash_screen_long_and_shake_screen_params() {
710 assert_eq!(
713 AnimationPlayer::apply_effect(SpecialEffect::FlashScreenLong),
714 AnimEffect::FlashScreen { frames: 48 }
715 );
716 assert_eq!(
718 AnimationPlayer::apply_effect(SpecialEffect::ShakeScreen),
719 AnimEffect::ShakeScreenH {
720 pixels: 8,
721 frames: 72
722 }
723 );
724 }
725
726 #[test]
727 fn share_move_animations_enemy_turn() {
728 assert_eq!(
731 run_animation_as(AMNESIA as usize - 1, false),
732 run_animation_as(CONF_ANIM as usize - 1, false)
733 );
734 assert_eq!(
735 run_animation_as(REST as usize - 1, false),
736 run_animation_as(SLP_ANIM as usize - 1, false)
737 );
738 }
739
740 #[test]
741 fn share_move_animations_player_turn_unchanged() {
742 let mut player = AnimationPlayer::new();
746 player.start(AMNESIA as usize - 1, true);
747 player.tick();
748 let player_turn_oam: Vec<(i32, i32)> =
750 player.oam_entries().iter().map(|e| (e.y, e.x)).collect();
751
752 let mut enemy = AnimationPlayer::new();
753 enemy.start(AMNESIA as usize - 1, false);
754 enemy.tick();
755 let enemy_turn_oam: Vec<(i32, i32)> =
756 enemy.oam_entries().iter().map(|e| (e.y, e.x)).collect();
757 assert_ne!(player_turn_oam, enemy_turn_oam);
758 }
759
760 fn hook_of(r: &AnimTickResult) -> Option<&AnimEffect> {
762 match r {
763 AnimTickResult::Playing { hook, .. } | AnimTickResult::WaitDelay { hook, .. } => {
764 hook.as_ref()
765 }
766 _ => None,
767 }
768 }
769
770 #[test]
771 fn hyper_beam_hook_flashes_every_four_frame_blocks() {
772 let results = run_animation(0x3E);
776 let flashes = results
777 .iter()
778 .filter(|r| matches!(hook_of(r), Some(AnimEffect::FlashScreen { .. })))
779 .count();
780 assert_eq!(flashes, 3, "Hyper Beam should flash 3 times via the hook");
781 }
782
783 #[test]
784 fn selfdestruct_hook_flashes_and_hides_mon() {
785 let results = run_animation(0x77);
789 let flashes = results
790 .iter()
791 .filter(|r| matches!(hook_of(r), Some(AnimEffect::FlashScreen { .. })))
792 .count();
793 assert_eq!(flashes, 5, "Selfdestruct should flash 5 times via the hook");
794 let hides = results
795 .iter()
796 .filter(|r| matches!(hook_of(r), Some(AnimEffect::HidePlayerMon)))
797 .count();
798 assert_eq!(hides, 1, "Selfdestruct should hide the attacking mon once");
799 }
800
801 #[test]
802 fn rock_slide_hook_shakes_and_flashes() {
803 let results = run_animation(0x9C);
808 let shakes = results
809 .iter()
810 .filter(|r| matches!(hook_of(r), Some(AnimEffect::ShakeScreenHV { .. })))
811 .count();
812 assert_eq!(shakes, 4, "Rock Slide should shake 4 times via the hook");
813 let flashes = results
814 .iter()
815 .filter(|r| matches!(hook_of(r), Some(AnimEffect::FlashScreen { .. })))
816 .count();
817 assert_eq!(flashes, 3, "Rock Slide should flash 3 times via the hook");
818 }
819
820 #[test]
821 fn growl_hook_duplicates_oam_entries() {
822 let mut player = AnimationPlayer::new();
825 player.start(0x2C, true); player.tick();
827 assert_eq!(player.oam_entries().len(), 8);
828 }
829
830 #[test]
831 fn sound_id_reported_on_first_frame_only() {
832 let mut player = AnimationPlayer::new();
835 player.start(0x00, true);
836 match player.tick() {
837 AnimTickResult::Playing { sound, .. } | AnimTickResult::WaitDelay { sound, .. } => {
838 assert_eq!(sound, Some(1));
839 }
840 other => panic!("Expected Playing/WaitDelay, got {:?}", other),
841 }
842 match player.tick() {
843 AnimTickResult::Playing { sound, .. } | AnimTickResult::WaitDelay { sound, .. } => {
844 assert_eq!(sound, None);
845 }
846 other => panic!("Expected Playing/WaitDelay, got {:?}", other),
847 }
848 }
849
850 #[test]
851 fn effect_command_reports_sound() {
852 let mut player = AnimationPlayer::new();
855 player.start(0x20, true);
856 match player.tick() {
857 AnimTickResult::Effect { sound, .. } => {
858 assert_eq!(sound, Some(73));
859 }
860 other => panic!("Expected Effect, got {:?}", other),
861 }
862 }
863
864 #[test]
865 fn all_moves_produce_animations() {
866 for move_id in 0..203 {
867 let mut player = AnimationPlayer::new();
868 player.start(move_id, true);
869 let mut has_frames = false;
870 let mut iterations = 0;
871
872 for _ in 0..1000 {
873 if iterations >= 1000 {
874 break;
875 }
876 iterations += 1;
877
878 match player.tick() {
879 AnimTickResult::Done => break,
880 AnimTickResult::Playing { .. } => has_frames = true,
881 AnimTickResult::Effect { .. } => has_frames = true,
882 AnimTickResult::WaitDelay { frames: n, .. } => {
883 has_frames = true;
884 for _ in 0..n {
885 player.tick();
886 iterations += 1;
887 }
888 }
889 }
890 }
891 assert!(has_frames, "Move 0x{:02X} should produce animation frames", move_id);
892 }
893 }
894}