1use super::types::Direction;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TeleportSpinPhase {
30 SpinInPlace,
34 SpinUp,
37 Delay,
39 Done,
41}
42
43pub const SPIN_IN_PLACE_FRAMES: u16 = 136;
45pub const SPIN_UP_STEP_DELAY: u16 = 3;
47pub const SPIN_UP_STEPS: u16 = 5;
49pub const SPIN_POST_DELAY_FRAMES: u16 = 10;
51pub const SPIN_UP_STEP_PIXELS: i32 = 16;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TeleportSpinSfx {
57 SpinLoop,
59 Rise,
61}
62
63#[derive(Debug, Clone, Copy)]
73pub struct TeleportSpinState {
74 spin_order: [Direction; 4],
76 start_index: usize,
79 frame: u16,
81 phase: TeleportSpinPhase,
82}
83
84impl TeleportSpinState {
85 pub fn new(current_facing: Direction, spin_order: [Direction; 4]) -> Self {
86 let start_index = spin_order
89 .iter()
90 .position(|&d| d == current_facing)
91 .unwrap_or(0);
92 Self {
93 spin_order,
94 start_index,
95 frame: 0,
96 phase: TeleportSpinPhase::SpinInPlace,
97 }
98 }
99
100 fn spin_in_place_index(frame: u16) -> Option<usize> {
103 let mut start = 0u16;
104 for i in 0..16u16 {
105 let dur = 16 - i;
106 if frame < start + dur {
107 return Some(i as usize);
108 }
109 start += dur;
110 }
111 None
112 }
113
114 pub fn tick(&mut self) -> Option<TeleportSpinSfx> {
118 if self.phase == TeleportSpinPhase::Done {
119 return None;
120 }
121 let mut sfx = None;
122 match self.phase {
123 TeleportSpinPhase::SpinInPlace => {
124 if self.frame == 0
126 || Self::spin_in_place_index(self.frame)
127 != Self::spin_in_place_index(self.frame.wrapping_sub(1))
128 {
129 if let Some(i) = Self::spin_in_place_index(self.frame) {
130 if (16 - i) % 4 == 0 {
131 sfx = Some(TeleportSpinSfx::SpinLoop);
132 }
133 }
134 }
135 if self.frame + 1 >= SPIN_IN_PLACE_FRAMES {
136 self.phase = TeleportSpinPhase::SpinUp;
137 }
138 }
139 TeleportSpinPhase::SpinUp => {
140 if self.frame == SPIN_IN_PLACE_FRAMES {
141 sfx = Some(TeleportSpinSfx::Rise);
142 }
143 let spin_up_frames = (SPIN_UP_STEPS - 1) * (1 + SPIN_UP_STEP_DELAY) + 1;
145 if self.frame + 1 >= SPIN_IN_PLACE_FRAMES + spin_up_frames {
146 self.phase = TeleportSpinPhase::Delay;
147 }
148 }
149 TeleportSpinPhase::Delay => {
150 let spin_up_frames = (SPIN_UP_STEPS - 1) * (1 + SPIN_UP_STEP_DELAY) + 1;
151 if self.frame + 1 >= SPIN_IN_PLACE_FRAMES + spin_up_frames + SPIN_POST_DELAY_FRAMES
152 {
153 self.phase = TeleportSpinPhase::Done;
154 }
155 }
156 TeleportSpinPhase::Done => {}
157 }
158 self.frame += 1;
159 sfx
160 }
161
162 pub fn is_done(&self) -> bool {
163 self.phase == TeleportSpinPhase::Done
164 }
165
166 pub fn phase(&self) -> TeleportSpinPhase {
167 self.phase
168 }
169
170 pub fn facing(&self) -> Direction {
172 let step = match self.phase {
173 TeleportSpinPhase::SpinInPlace => {
174 Self::spin_in_place_index(self.frame).unwrap_or(15)
175 }
176 TeleportSpinPhase::SpinUp => {
177 let f = self.frame - SPIN_IN_PLACE_FRAMES;
178 ((f / (1 + SPIN_UP_STEP_DELAY)) as usize).min((SPIN_UP_STEPS - 1) as usize) + 16
179 }
180 TeleportSpinPhase::Delay | TeleportSpinPhase::Done => 16 + (SPIN_UP_STEPS - 1) as usize,
181 };
182 self.spin_order[(self.start_index + step) % 4]
183 }
184
185 pub fn player_y_offset(&self) -> i32 {
188 match self.phase {
189 TeleportSpinPhase::SpinInPlace => 0,
190 TeleportSpinPhase::SpinUp => {
191 let f = self.frame - SPIN_IN_PLACE_FRAMES;
192 let step = ((f / (1 + SPIN_UP_STEP_DELAY)) as i32 + 1).min(SPIN_UP_STEPS as i32);
193 -step * SPIN_UP_STEP_PIXELS
194 }
195 TeleportSpinPhase::Delay | TeleportSpinPhase::Done => {
196 -(SPIN_UP_STEPS as i32) * SPIN_UP_STEP_PIXELS
197 }
198 }
199 }
200
201 pub fn player_visible(&self) -> bool {
204 self.player_y_offset() > -(SPIN_UP_STEPS as i32) * SPIN_UP_STEP_PIXELS
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum EnterMapSpinPhase {
215 SpinDown,
219 SpinInPlace,
222 Done,
224}
225
226pub const ENTER_MAP_SPIN_DOWN_FRAMES: u16 =
229 (ENTER_MAP_SPIN_DOWN_STEPS - 1) * (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY) + 1;
230pub const ENTER_MAP_SPIN_DOWN_STEPS: u16 = 5;
233pub const ENTER_MAP_SPIN_DOWN_STEP_DELAY: u16 = 3;
235pub const ENTER_MAP_SPIN_DOWN_STEP_PIXELS: i32 = 16;
237pub const ENTER_MAP_SPIN_IN_PLACE_FRAMES: u16 = 36;
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum EnterMapSpinSfx {
244 Descend,
246 Land,
248}
249
250#[derive(Debug, Clone, Copy)]
258pub struct EnterMapSpinState {
259 spin_order: [Direction; 4],
261 start_index: usize,
264 frame: u16,
266 phase: EnterMapSpinPhase,
267 spin_in_place: bool,
268}
269
270impl EnterMapSpinState {
271 pub fn new(current_facing: Direction, spin_order: [Direction; 4], spin_in_place: bool) -> Self {
272 let start_index = spin_order
273 .iter()
274 .position(|&d| d == current_facing)
275 .unwrap_or(0);
276 Self {
277 spin_order,
278 start_index,
279 frame: 0,
280 phase: EnterMapSpinPhase::SpinDown,
281 spin_in_place,
282 }
283 }
284
285 pub fn tick(&mut self) -> Option<EnterMapSpinSfx> {
289 if self.phase == EnterMapSpinPhase::Done {
290 return None;
291 }
292 let mut sfx = None;
293 match self.phase {
294 EnterMapSpinPhase::SpinDown => {
295 if self.frame == 0 {
296 sfx = Some(EnterMapSpinSfx::Descend);
297 }
298 if self.frame + 1 >= ENTER_MAP_SPIN_DOWN_FRAMES {
299 sfx = Some(EnterMapSpinSfx::Land);
302 self.phase = if self.spin_in_place {
303 EnterMapSpinPhase::SpinInPlace
304 } else {
305 EnterMapSpinPhase::Done
306 };
307 }
308 }
309 EnterMapSpinPhase::SpinInPlace => {
310 if self.frame + 1
311 >= ENTER_MAP_SPIN_DOWN_FRAMES + ENTER_MAP_SPIN_IN_PLACE_FRAMES
312 {
313 self.phase = EnterMapSpinPhase::Done;
314 }
315 }
316 EnterMapSpinPhase::Done => {}
317 }
318 self.frame += 1;
319 sfx
320 }
321
322 pub fn is_done(&self) -> bool {
323 self.phase == EnterMapSpinPhase::Done
324 }
325
326 pub fn phase(&self) -> EnterMapSpinPhase {
327 self.phase
328 }
329
330 pub fn facing(&self) -> Direction {
332 let step = match self.phase {
333 EnterMapSpinPhase::SpinDown => {
334 (self.frame / (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY)) as usize
335 }
336 EnterMapSpinPhase::SpinInPlace => {
337 let f = self.frame - ENTER_MAP_SPIN_DOWN_FRAMES;
338 let spin = (f as usize).min(ENTER_MAP_SPIN_IN_PLACE_FRAMES as usize - 1);
340 let mut start = 0usize;
341 let mut idx = 0usize;
342 for i in 0..8usize {
343 let dur = i + 1;
344 if spin < start + dur {
345 idx = i;
346 break;
347 }
348 start += dur;
349 }
350 ENTER_MAP_SPIN_DOWN_STEPS as usize + idx
351 }
352 EnterMapSpinPhase::Done => 0,
354 };
355 self.spin_order[(self.start_index + step) % 4]
356 }
357
358 pub fn player_y_offset(&self) -> i32 {
361 match self.phase {
362 EnterMapSpinPhase::SpinDown => {
363 if self.frame == 0 {
367 -(ENTER_MAP_SPIN_DOWN_STEPS as i32) * ENTER_MAP_SPIN_DOWN_STEP_PIXELS
368 } else {
369 let moves = ((self.frame - 1) / (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY) + 1)
370 .min(ENTER_MAP_SPIN_DOWN_STEPS);
371 -((ENTER_MAP_SPIN_DOWN_STEPS - moves) as i32)
372 * ENTER_MAP_SPIN_DOWN_STEP_PIXELS
373 }
374 }
375 EnterMapSpinPhase::SpinInPlace | EnterMapSpinPhase::Done => 0,
376 }
377 }
378
379 pub fn player_visible(&self) -> bool {
381 self.player_y_offset() > -(ENTER_MAP_SPIN_DOWN_STEPS as i32)
382 * ENTER_MAP_SPIN_DOWN_STEP_PIXELS
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub struct ElevatorShakeParams {
392 pub iterations: u8,
394 pub pixel_offset: u8,
396}
397
398impl ElevatorShakeParams {
399 pub const fn total_frames(&self) -> u16 {
401 self.iterations as u16 * 2
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum ElevatorShakeSfx {
408 Rattle,
410 Arrive,
412}
413
414#[derive(Debug, Clone, Copy)]
418pub struct ElevatorShakeState {
419 frame: u16,
421 params: ElevatorShakeParams,
422}
423
424impl ElevatorShakeState {
425 pub fn new(params: ElevatorShakeParams) -> Self {
426 Self { frame: 0, params }
427 }
428
429 pub fn params(&self) -> ElevatorShakeParams {
430 self.params
431 }
432
433 pub fn total_frames(&self) -> u16 {
435 self.params.total_frames()
436 }
437
438 pub fn tick(&mut self) -> Option<ElevatorShakeSfx> {
442 if self.is_done() {
443 return None;
444 }
445 let sfx = if self.frame + 1 >= self.total_frames() {
446 Some(ElevatorShakeSfx::Arrive)
447 } else if self.frame % 2 == 0 {
448 Some(ElevatorShakeSfx::Rattle)
449 } else {
450 None
451 };
452 self.frame += 1;
453 sfx
454 }
455
456 pub fn is_done(&self) -> bool {
457 self.frame >= self.total_frames()
458 }
459
460 pub fn offset_y(&self) -> i32 {
463 if self.is_done() {
464 return 0;
465 }
466 let iteration = self.frame / 2;
467 let px = self.params.pixel_offset as i32;
468 if iteration % 2 == 0 {
469 -px
470 } else {
471 px
472 }
473 }
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum TileAnimKind {
482 None,
484 Water,
486 WaterFlower,
488}
489
490#[derive(Debug, Clone, Copy)]
500pub struct TileAnimState {
501 counter1: u8,
502 counter2: u8,
503 water_shift: i8,
505 flower_frame: Option<u8>,
507 kind: TileAnimKind,
509}
510
511impl TileAnimState {
512 pub fn new() -> Self {
513 Self {
514 counter1: 0,
515 counter2: 0,
516 water_shift: 0,
517 flower_frame: None,
518 kind: TileAnimKind::None,
519 }
520 }
521
522 pub fn set_tileset(&mut self, kind: TileAnimKind) {
526 self.kind = kind;
527 self.counter1 = 0;
528 }
529
530 pub fn tick(&mut self) {
532 if self.kind == TileAnimKind::None {
533 return;
534 }
535 self.counter1 = self.counter1.wrapping_add(1);
536 if self.counter1 < 20 {
537 return;
538 }
539 if self.counter1 == 21 {
540 self.counter1 = 0;
542 self.flower_frame = Some(match self.counter2 & 3 {
543 0 | 1 => 1,
544 2 => 2,
545 _ => 3,
546 });
547 return;
548 }
549 self.counter2 = (self.counter2 + 1) & 7;
551 self.water_shift += if self.counter2 & 4 == 0 { 1 } else { -1 };
554 if self.kind == TileAnimKind::Water {
557 self.counter1 = 0;
558 }
559 }
560
561 pub fn water_shift(&self) -> i8 {
564 self.water_shift
565 }
566
567 pub fn flower_frame(&self) -> Option<u8> {
570 self.flower_frame
571 }
572
573 pub fn kind(&self) -> TileAnimKind {
575 self.kind
576 }
577}
578
579impl Default for TileAnimState {
580 fn default() -> Self {
581 Self::new()
582 }
583}
584
585pub const FISHING_CAST_DELAY_FRAMES: u16 = 10;
589pub const FISHING_ROD_OUT_FRAMES: u16 = 100;
591pub const FISHING_SHAKE_ITERATIONS: u16 = 10;
593pub const FISHING_SHAKE_STEP_FRAMES: u16 = 3;
595pub const FISHING_BUBBLE_FRAMES: u16 = 60;
597
598pub const FISHING_ANIM_FRAMES: u16 = FISHING_CAST_DELAY_FRAMES
600 + FISHING_ROD_OUT_FRAMES
601 + FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES
602 + FISHING_BUBBLE_FRAMES;
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
606pub enum FishingAnimPhase {
607 CastDelay,
610 RodOut,
613 Shake,
616 Bubble,
620 Done,
622}
623
624#[derive(Debug, Clone, Copy)]
629pub struct FishingAnimState {
630 frame: u16,
632 facing: Direction,
634 bite: bool,
636 phase: FishingAnimPhase,
637}
638
639impl FishingAnimState {
640 pub fn new(facing: Direction, bite: bool) -> Self {
641 Self {
642 frame: 0,
643 facing,
644 bite,
645 phase: FishingAnimPhase::CastDelay,
646 }
647 }
648
649 fn phase_for(frame: u16, bite: bool) -> FishingAnimPhase {
650 let rod_end = FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES;
651 if frame < FISHING_CAST_DELAY_FRAMES {
652 FishingAnimPhase::CastDelay
653 } else if frame < rod_end {
654 FishingAnimPhase::RodOut
655 } else if !bite {
656 FishingAnimPhase::Done
657 } else if frame < rod_end + FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES {
658 FishingAnimPhase::Shake
659 } else if frame < rod_end
660 + FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES
661 + FISHING_BUBBLE_FRAMES
662 {
663 FishingAnimPhase::Bubble
664 } else {
665 FishingAnimPhase::Done
666 }
667 }
668
669 pub fn tick(&mut self) {
671 self.frame = self.frame.saturating_add(1);
672 self.phase = Self::phase_for(self.frame, self.bite);
673 }
674
675 pub fn phase(&self) -> FishingAnimPhase {
676 self.phase
677 }
678
679 pub fn is_done(&self) -> bool {
680 self.phase == FishingAnimPhase::Done
681 }
682
683 pub fn facing(&self) -> Direction {
685 self.facing
686 }
687
688 pub fn pose_active(&self) -> bool {
691 matches!(
692 self.phase,
693 FishingAnimPhase::RodOut | FishingAnimPhase::Shake | FishingAnimPhase::Bubble
694 )
695 }
696
697 pub fn rod_visible(&self) -> bool {
701 self.pose_active()
702 && !(self.phase == FishingAnimPhase::Bubble && self.facing == Direction::Up)
703 }
704
705 pub fn bubble_active(&self) -> bool {
707 self.phase == FishingAnimPhase::Bubble
708 }
709
710 pub fn player_shake_offset(&self) -> i32 {
713 if self.phase != FishingAnimPhase::Shake {
714 return 0;
715 }
716 let shake_start = FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES;
717 let iteration = (self.frame - shake_start) / FISHING_SHAKE_STEP_FRAMES;
718 if iteration % 2 == 0 { 1 } else { 0 }
719 }
720
721 pub fn rod_piece(facing: Direction) -> (i32, i32, u8, bool) {
726 match facing {
727 Direction::Down => (20, 35, 0, false),
728 Direction::Up => (20, -12, 0, false),
729 Direction::Left => (0, 16, 1, false),
730 Direction::Right => (48, 16, 1, true),
731 }
732 }
733}
734
735pub const BOULDER_DUST_STEPS: u8 = 8;
739pub const BOULDER_DUST_STEP_FRAMES: u8 = 3;
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub struct BoulderDustState {
751 facing: Direction,
754 anchor_x: u16,
756 anchor_y: u16,
757 step: u8,
759 frame: u8,
761}
762
763impl BoulderDustState {
764 pub const fn inactive() -> Self {
766 Self {
767 facing: Direction::Down,
768 anchor_x: 0,
769 anchor_y: 0,
770 step: BOULDER_DUST_STEPS,
771 frame: 0,
772 }
773 }
774
775 pub const fn new(facing: Direction, anchor_x: u16, anchor_y: u16) -> Self {
778 Self {
779 facing,
780 anchor_x,
781 anchor_y,
782 step: 0,
783 frame: 0,
784 }
785 }
786
787 pub fn tick(&mut self) {
789 if self.step >= BOULDER_DUST_STEPS {
790 return;
791 }
792 self.frame += 1;
793 if self.frame >= BOULDER_DUST_STEP_FRAMES {
794 self.frame = 0;
795 self.step += 1;
796 }
797 }
798
799 pub fn is_active(&self) -> bool {
801 self.step < BOULDER_DUST_STEPS
802 }
803
804 pub fn facing(&self) -> Direction {
806 self.facing
807 }
808
809 pub fn anchor(&self) -> (u16, u16) {
811 (self.anchor_x, self.anchor_y)
812 }
813
814 pub fn step(&self) -> u8 {
816 self.step.min(BOULDER_DUST_STEPS - 1)
817 }
818
819 pub fn base_offset(&self) -> (i32, i32) {
823 match self.facing {
824 Direction::Down => (8, 52),
825 Direction::Up => (8, -12),
826 Direction::Left => (-24, 20),
827 Direction::Right => (40, 20),
828 }
829 }
830
831 pub fn drift_px(&self) -> (i32, i32) {
834 match self.facing {
835 Direction::Down => (0, -1),
836 Direction::Up => (0, 1),
837 Direction::Left => (1, 0),
838 Direction::Right => (-1, 0),
839 }
840 }
841
842 pub fn tile_drifts(&self) -> [(i32, i32); 4] {
848 let (dx, dy) = self.drift_px();
849 match self.facing {
850 Direction::Left | Direction::Right => [(0, 0), (dx, 0), (dx, 0), (dx, 0)],
851 _ => [(dx, dy); 4],
852 }
853 }
854
855 pub fn palette_flipped(&self) -> bool {
858 self.step % 2 == 1
859 }
860}
861
862pub const FLASH_WHITE_FRAMES: u8 = 3;
866
867pub const SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES: u16 = 120;
872pub const SHIP_DEPARTURE_WATER_FILL_FRAMES: u16 = 3;
876pub const SHIP_DEPARTURE_SCROLL_ITERATIONS: u16 = 8;
878pub const SHIP_DEPARTURE_ITERATION_FRAMES: u16 = 16 * 8;
880pub const SHIP_DEPARTURE_ERASE_FRAMES: u16 = 120;
882pub const SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION: i32 = 2 * 8;
884pub const SHIP_DEPARTURE_PUFF_SPACING_PX: i32 = 16;
887pub const SHIP_DEPARTURE_PUFF_DRIFT_PX_PER_SUBSTEP: i32 = 2;
889pub const SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION: u16 = 16;
891pub const SHIP_DEPARTURE_SUBSTEP_FRAMES: u16 = 8;
893pub const SHIP_DEPARTURE_SMOKESTACK_TILE_X: u16 = 16;
895pub const SHIP_DEPARTURE_SMOKESTACK_TILE_Y: f32 = 10.5;
896pub const SHIP_DEPARTURE_PUFF_START_SCREEN_X: i32 = 88;
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum ShipDeparturePhase {
902 InitialPause,
904 WaterFill,
906 Scroll,
909 Erase,
912 Done,
914}
915
916pub const SHIP_DEPARTURE_TOTAL_FRAMES: u16 = SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
918 + SHIP_DEPARTURE_WATER_FILL_FRAMES
919 + SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES
920 + SHIP_DEPARTURE_ERASE_FRAMES;
921
922#[derive(Debug, Clone, Copy, PartialEq, Eq)]
924pub enum ShipDepartureSfx {
925 Horn,
928}
929
930#[derive(Debug, Clone, Copy)]
936pub struct ShipDepartureState {
937 frame: u16,
939 phase: ShipDeparturePhase,
940}
941
942impl ShipDepartureState {
943 pub fn new() -> Self {
944 Self {
945 frame: 0,
946 phase: ShipDeparturePhase::InitialPause,
947 }
948 }
949
950 fn phase_for(frame: u16) -> ShipDeparturePhase {
951 let scroll_end = SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
952 + SHIP_DEPARTURE_WATER_FILL_FRAMES
953 + SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES;
954 if frame < SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES {
955 ShipDeparturePhase::InitialPause
956 } else if frame < SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
957 ShipDeparturePhase::WaterFill
958 } else if frame < scroll_end {
959 ShipDeparturePhase::Scroll
960 } else if frame < SHIP_DEPARTURE_TOTAL_FRAMES {
961 ShipDeparturePhase::Erase
962 } else {
963 ShipDeparturePhase::Done
964 }
965 }
966
967 pub fn tick(&mut self) -> Option<ShipDepartureSfx> {
971 if self.phase == ShipDeparturePhase::Done {
972 return None;
973 }
974 let mut sfx = None;
975 let next = self.frame + 1;
976 let next_phase = Self::phase_for(next);
977 if next_phase != self.phase {
978 match next_phase {
979 ShipDeparturePhase::Scroll => {
980 sfx = Some(ShipDepartureSfx::Horn);
981 }
982 ShipDeparturePhase::Erase => {
983 sfx = Some(ShipDepartureSfx::Horn);
984 }
985 _ => {}
986 }
987 }
988 self.frame = next;
989 self.phase = next_phase;
990 sfx
991 }
992
993 pub fn is_done(&self) -> bool {
994 self.phase == ShipDeparturePhase::Done
995 }
996
997 pub fn phase(&self) -> ShipDeparturePhase {
998 self.phase
999 }
1000
1001 pub fn frame(&self) -> u16 {
1003 self.frame
1004 }
1005
1006 pub fn ship_erased(&self) -> bool {
1011 matches!(
1012 self.phase,
1013 ShipDeparturePhase::Erase | ShipDeparturePhase::Done
1014 )
1015 }
1016
1017 pub fn scroll_iteration(&self) -> u16 {
1020 match self.phase {
1021 ShipDeparturePhase::Scroll => {
1022 (self.frame
1023 - SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
1024 - SHIP_DEPARTURE_WATER_FILL_FRAMES)
1025 / SHIP_DEPARTURE_ITERATION_FRAMES
1026 }
1027 ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
1028 SHIP_DEPARTURE_SCROLL_ITERATIONS - 1
1029 }
1030 _ => 0,
1031 }
1032 }
1033
1034 pub fn scroll_substep(&self) -> u16 {
1038 match self.phase {
1039 ShipDeparturePhase::Scroll => {
1040 (self.frame
1041 - SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
1042 - SHIP_DEPARTURE_WATER_FILL_FRAMES)
1043 / SHIP_DEPARTURE_SUBSTEP_FRAMES
1044 }
1045 ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
1046 SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION - 1
1047 }
1048 _ => 0,
1049 }
1050 }
1051
1052 pub fn scroll_px(&self) -> i32 {
1057 match self.phase {
1058 ShipDeparturePhase::Scroll => {
1059 self.scroll_iteration() as i32 * SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION
1060 + (self.scroll_substep() as i32
1061 % SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION as i32)
1062 + 1
1063 }
1064 ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
1065 SHIP_DEPARTURE_SCROLL_ITERATIONS as i32
1066 * SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION
1067 }
1068 _ => 0,
1069 }
1070 }
1071
1072 pub fn puff_count(&self) -> usize {
1076 match self.phase {
1077 ShipDeparturePhase::InitialPause | ShipDeparturePhase::WaterFill => 0,
1078 ShipDeparturePhase::Scroll | ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
1079 (self.scroll_iteration() + 1) as usize
1080 }
1081 }
1082 }
1083
1084 pub fn puff_x_offset(&self, i: usize) -> i32 {
1090 let i = i as i32;
1091 let spawn = SHIP_DEPARTURE_PUFF_START_SCREEN_X - SHIP_DEPARTURE_PUFF_SPACING_PX * i;
1092 let s = self.scroll_substep() as i32;
1093 let substeps_live = (s - SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION as i32 * i).max(0);
1094 spawn + SHIP_DEPARTURE_PUFF_DRIFT_PX_PER_SUBSTEP * (substeps_live + 1)
1095 }
1096
1097 pub fn puff_screen_y(&self) -> i32 {
1100 (SHIP_DEPARTURE_SMOKESTACK_TILE_Y * 8.0) as i32
1101 }
1102}
1103
1104impl Default for ShipDepartureState {
1105 fn default() -> Self {
1106 Self::new()
1107 }
1108}
1109
1110#[cfg(test)]
1113mod tests {
1114 use super::*;
1115
1116 const SPIN_ORDER: [Direction; 4] = [
1118 Direction::Down,
1119 Direction::Left,
1120 Direction::Up,
1121 Direction::Right,
1122 ];
1123
1124 #[test]
1127 fn spin_starts_on_current_facing_then_cycles() {
1128 for (start, order) in [
1131 (
1132 Direction::Down,
1133 [
1134 Direction::Down,
1135 Direction::Left,
1136 Direction::Up,
1137 Direction::Right,
1138 ],
1139 ),
1140 (
1141 Direction::Up,
1142 [
1143 Direction::Up,
1144 Direction::Right,
1145 Direction::Down,
1146 Direction::Left,
1147 ],
1148 ),
1149 ] {
1150 let mut spin = TeleportSpinState::new(start, SPIN_ORDER);
1151 for (i, want) in order.iter().enumerate() {
1152 assert_eq!(&spin.facing(), want, "start {:?} spin {}", start, i);
1153 for _ in 0..(16 - i) {
1155 spin.tick();
1156 }
1157 }
1158 }
1159 }
1160
1161 #[test]
1162 fn spin_honors_a_custom_facing_cycle() {
1163 const REVERSED: [Direction; 4] = [
1165 Direction::Down,
1166 Direction::Right,
1167 Direction::Up,
1168 Direction::Left,
1169 ];
1170 let mut spin = TeleportSpinState::new(Direction::Down, REVERSED);
1171 assert_eq!(spin.facing(), Direction::Down);
1172 for _ in 0..16 {
1173 spin.tick();
1174 }
1175 assert_eq!(spin.facing(), Direction::Right, "second spin of the custom cycle");
1176 let spin = TeleportSpinState::new(Direction::Up, [Direction::Down; 4]);
1178 assert_eq!(spin.facing(), Direction::Down);
1179 }
1180
1181 #[test]
1182 fn spin_sfx_schedule() {
1183 let mut spin = TeleportSpinState::new(Direction::Down, SPIN_ORDER);
1186 let mut loops = 0;
1187 let mut rises = 0;
1188 let mut frames = 0;
1189 while !spin.is_done() {
1190 match spin.tick() {
1191 Some(TeleportSpinSfx::SpinLoop) => loops += 1,
1192 Some(TeleportSpinSfx::Rise) => {
1193 rises += 1;
1194 assert_eq!(frames, SPIN_IN_PLACE_FRAMES, "rise at spin-up start");
1195 }
1196 None => {}
1197 }
1198 frames += 1;
1199 assert!(frames < 1000, "spin must terminate");
1200 }
1201 assert_eq!(loops, 4);
1202 assert_eq!(rises, 1);
1203 assert_eq!(frames, 136 + 17 + 10);
1205 }
1206
1207 #[test]
1208 fn spin_rises_off_screen_and_hides() {
1209 let mut spin = TeleportSpinState::new(Direction::Down, SPIN_ORDER);
1210 assert_eq!(spin.player_y_offset(), 0);
1211 assert!(spin.player_visible());
1212 for _ in 0..SPIN_IN_PLACE_FRAMES - 1 {
1213 spin.tick();
1214 }
1215 assert_eq!(spin.player_y_offset(), 0, "still grounded on the last spin frame");
1216 spin.tick(); assert_eq!(spin.player_y_offset(), -16);
1218 for _ in 0..16 {
1220 spin.tick();
1221 }
1222 assert_eq!(spin.player_y_offset(), -80);
1223 assert!(!spin.player_visible(), "sprite fully above the screen");
1224 }
1225
1226 #[test]
1229 fn enter_map_spin_starts_hidden_and_descends_in_five_steps() {
1230 let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, true);
1231 assert_eq!(anim.phase(), EnterMapSpinPhase::SpinDown);
1234 assert!(!anim.player_visible(), "hidden until the spin-down descends");
1235 assert_eq!(anim.player_y_offset(), -80);
1236
1237 let mut last = -80;
1240 for frame in 1..=17 {
1241 anim.tick();
1242 if frame % 4 == 1 {
1243 assert!(anim.player_y_offset() > last, "move {frame} descends");
1244 last = anim.player_y_offset();
1245 }
1246 }
1247 assert_eq!(anim.player_y_offset(), 0, "standing position after the 5th move");
1248 assert!(anim.player_visible());
1249 }
1250
1251 #[test]
1252 fn enter_map_spin_timing_and_sfx() {
1253 let mut anim = EnterMapSpinState::new(Direction::Left, SPIN_ORDER, true);
1254 assert_eq!(anim.tick(), Some(EnterMapSpinSfx::Descend));
1256 for _ in 1..ENTER_MAP_SPIN_DOWN_FRAMES - 1 {
1258 assert_eq!(anim.tick(), None);
1259 }
1260 assert_eq!(
1261 anim.tick(),
1262 Some(EnterMapSpinSfx::Land),
1263 "land when the spin-down completes"
1264 );
1265 assert_eq!(anim.phase(), EnterMapSpinPhase::SpinInPlace);
1266
1267 let mut ticks = 0;
1269 while !anim.is_done() {
1270 anim.tick();
1271 ticks += 1;
1272 assert!(ticks <= ENTER_MAP_SPIN_IN_PLACE_FRAMES, "bounded");
1273 }
1274 assert_eq!(ticks, ENTER_MAP_SPIN_IN_PLACE_FRAMES);
1275 assert!(anim.is_done());
1276 }
1277
1278 #[test]
1279 fn enter_map_spin_skips_spin_in_place_on_warp_pad() {
1280 let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, false);
1282 let mut ticks = 0;
1283 while !anim.is_done() && ticks < 200 {
1284 anim.tick();
1285 ticks += 1;
1286 }
1287 assert_eq!(ticks, ENTER_MAP_SPIN_DOWN_FRAMES, "no spin-in-place phase");
1288 assert!(anim.is_done());
1289 }
1290
1291 #[test]
1292 fn enter_map_spin_facing_cycles_and_restores() {
1293 let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, true);
1296 anim.tick(); assert_eq!(anim.facing(), Direction::Down);
1298 for _ in 0..4 {
1299 anim.tick(); }
1301 assert_eq!(anim.facing(), Direction::Left);
1302 for _ in 0..ENTER_MAP_SPIN_DOWN_FRAMES + 5 {
1304 anim.tick();
1305 }
1306 assert_eq!(anim.phase(), EnterMapSpinPhase::SpinInPlace);
1307 assert_eq!(anim.facing(), Direction::Left);
1310 }
1311
1312 const SHAKE: ElevatorShakeParams = ElevatorShakeParams {
1315 iterations: 100,
1316 pixel_offset: 1,
1317 };
1318
1319 #[test]
1320 fn elevator_shake_alternates_offset_each_iteration() {
1321 let mut shake = ElevatorShakeState::new(SHAKE);
1322 assert_eq!(shake.offset_y(), -1);
1324 shake.tick();
1325 assert_eq!(shake.offset_y(), -1, "one iteration lasts 2 frames");
1326 shake.tick();
1327 assert_eq!(shake.offset_y(), 1);
1328 shake.tick();
1329 assert_eq!(shake.offset_y(), 1);
1330 shake.tick();
1331 assert_eq!(shake.offset_y(), -1);
1332 }
1333
1334 #[test]
1335 fn elevator_shake_sfx_and_duration() {
1336 let mut shake = ElevatorShakeState::new(SHAKE);
1337 let mut rattles = 0;
1338 let mut dings = 0;
1339 let mut frames = 0;
1340 while !shake.is_done() {
1341 match shake.tick() {
1342 Some(ElevatorShakeSfx::Rattle) => rattles += 1,
1343 Some(ElevatorShakeSfx::Arrive) => dings += 1,
1344 None => {}
1345 }
1346 frames += 1;
1347 }
1348 assert_eq!(frames, SHAKE.total_frames());
1349 assert_eq!(rattles, 100, "rattle once per iteration");
1350 assert_eq!(dings, 1, "arrival ding at the end");
1351 assert_eq!(shake.offset_y(), 0, "scroll restored after the shake");
1352 }
1353
1354 #[test]
1355 fn elevator_shake_honors_custom_params() {
1356 let params = ElevatorShakeParams {
1357 iterations: 4,
1358 pixel_offset: 2,
1359 };
1360 let mut shake = ElevatorShakeState::new(params);
1361 assert_eq!(shake.total_frames(), 8);
1362 assert_eq!(shake.offset_y(), -2);
1363 shake.tick();
1364 shake.tick();
1365 assert_eq!(shake.offset_y(), 2);
1366 let mut frames = 2;
1367 while !shake.is_done() {
1368 shake.tick();
1369 frames += 1;
1370 }
1371 assert_eq!(frames, 8);
1372 }
1373
1374 #[test]
1377 fn tile_anim_disabled_for_none_tilesets() {
1378 let mut anim = TileAnimState::new();
1379 anim.set_tileset(TileAnimKind::None);
1380 for _ in 0..100 {
1381 anim.tick();
1382 }
1383 assert_eq!(anim.water_shift(), 0);
1384 assert_eq!(anim.flower_frame(), None);
1385 }
1386
1387 #[test]
1388 fn tile_anim_water_rotates_every_20_frames() {
1389 let mut anim = TileAnimState::new();
1390 anim.set_tileset(TileAnimKind::Water);
1391 for _ in 0..19 {
1392 anim.tick();
1393 }
1394 assert_eq!(anim.water_shift(), 0, "no update before counter1 hits 20");
1395 anim.tick();
1396 assert_eq!(anim.water_shift(), 1, "first update shifts right one pixel");
1397 for _ in 0..20 {
1398 anim.tick();
1399 }
1400 assert_eq!(anim.water_shift(), 2);
1401 }
1402
1403 #[test]
1404 fn tile_anim_water_direction_follows_counter2_bit2() {
1405 let mut anim = TileAnimState::new();
1409 anim.set_tileset(TileAnimKind::Water);
1410 let expected = [1, 2, 3, 2, 1, 0, -1, 0];
1411 for want in expected {
1412 for _ in 0..20 {
1413 anim.tick();
1414 }
1415 assert_eq!(anim.water_shift(), want);
1416 }
1417 }
1418
1419 #[test]
1420 fn tile_anim_flower_frames_cycle_1_2_3_1() {
1421 let mut anim = TileAnimState::new();
1424 anim.set_tileset(TileAnimKind::WaterFlower);
1425 assert_eq!(anim.flower_frame(), None, "base tile until the first update");
1426 let expected = [1, 2, 3, 1, 1, 2];
1427 for want in expected {
1428 for _ in 0..21 {
1429 anim.tick();
1430 }
1431 assert_eq!(anim.flower_frame(), Some(want));
1432 }
1433 }
1434
1435 #[test]
1436 fn tile_anim_map_load_resets_counter_but_keeps_water_phase() {
1437 let mut anim = TileAnimState::new();
1439 anim.set_tileset(TileAnimKind::WaterFlower);
1440 for _ in 0..10 {
1441 anim.tick();
1442 }
1443 anim.set_tileset(TileAnimKind::Water);
1444 for _ in 0..19 {
1445 anim.tick();
1446 }
1447 assert_eq!(anim.water_shift(), 0);
1448 anim.tick();
1449 assert_eq!(anim.water_shift(), 1);
1450 assert_eq!(anim.flower_frame(), None, "water-only tilesets never flower");
1451 }
1452
1453 fn tick_n(anim: &mut FishingAnimState, n: u16) {
1457 for _ in 0..n {
1458 anim.tick();
1459 }
1460 }
1461
1462 #[test]
1463 fn fishing_anim_no_bite_finishes_after_rod_out() {
1464 for facing in [Direction::Down, Direction::Up, Direction::Left, Direction::Right] {
1466 let mut anim = FishingAnimState::new(facing, false);
1467 assert_eq!(anim.phase(), FishingAnimPhase::CastDelay);
1468 for _ in 1..FISHING_CAST_DELAY_FRAMES {
1471 anim.tick();
1472 assert_eq!(anim.phase(), FishingAnimPhase::CastDelay);
1473 }
1474 anim.tick();
1475 assert_eq!(anim.phase(), FishingAnimPhase::RodOut, "rod appears at frame 10");
1476 for _ in 0..FISHING_ROD_OUT_FRAMES - 1 {
1477 anim.tick();
1478 assert_eq!(anim.phase(), FishingAnimPhase::RodOut);
1479 }
1480 anim.tick();
1481 assert_eq!(anim.phase(), FishingAnimPhase::Done, "no bite → straight to text");
1482 assert!(anim.is_done());
1483 }
1484 }
1485
1486 #[test]
1487 fn fishing_anim_bite_plays_shake_then_bubble_then_done() {
1488 let mut anim = FishingAnimState::new(Direction::Down, true);
1489 tick_n(&mut anim, FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES);
1490 assert_eq!(anim.phase(), FishingAnimPhase::Shake, "bite starts the shake");
1491 assert!(!anim.bubble_active());
1492
1493 for i in 0..FISHING_SHAKE_ITERATIONS {
1495 assert_eq!(
1496 anim.phase(),
1497 FishingAnimPhase::Shake,
1498 "shake iteration {i} still active"
1499 );
1500 assert_eq!(anim.player_shake_offset(), if i % 2 == 0 { 1 } else { 0 });
1502 tick_n(&mut anim, FISHING_SHAKE_STEP_FRAMES);
1503 }
1504 assert_eq!(anim.phase(), FishingAnimPhase::Bubble, "shake ends → bubble");
1505 assert!(anim.bubble_active());
1506 assert_eq!(anim.player_shake_offset(), 0, "no shake during the bubble");
1507
1508 for f in 1..FISHING_BUBBLE_FRAMES {
1509 anim.tick();
1510 assert_eq!(anim.phase(), FishingAnimPhase::Bubble, "bubble frame {f}");
1511 assert!(anim.bubble_active());
1512 }
1513 anim.tick();
1514 assert_eq!(anim.phase(), FishingAnimPhase::Done, "bubble ends → result text");
1515 assert!(anim.is_done());
1516 assert!(!anim.bubble_active());
1517 }
1518
1519 #[test]
1520 fn fishing_anim_total_duration() {
1521 let mut no_bite = FishingAnimState::new(Direction::Left, false);
1522 let mut bite = FishingAnimState::new(Direction::Left, true);
1523 for _ in 0..FISHING_ANIM_FRAMES {
1524 no_bite.tick();
1525 bite.tick();
1526 }
1527 assert!(no_bite.is_done(), "no-bite finishes at 10 + 100 = 110 frames");
1529 assert!(bite.is_done(), "bite finishes at 10 + 100 + 30 + 60 = 200 frames");
1530 let mut late = FishingAnimState::new(Direction::Down, true);
1531 for _ in 0..FISHING_ANIM_FRAMES - 1 {
1532 late.tick();
1533 }
1534 assert_eq!(late.phase(), FishingAnimPhase::Bubble, "frame 199 is still the bubble");
1535 }
1536
1537 #[test]
1538 fn fishing_anim_pose_and_rod_visibility_follow_phase_and_facing() {
1539 let mut anim = FishingAnimState::new(Direction::Up, true);
1541 assert!(!anim.pose_active());
1542 assert!(!anim.rod_visible());
1543 tick_n(&mut anim, FISHING_CAST_DELAY_FRAMES);
1544 assert!(anim.pose_active(), "fishing pose while the rod is out");
1545 assert!(anim.rod_visible());
1546 assert_eq!(anim.facing(), Direction::Up);
1547
1548 let shake_bubble = FISHING_CAST_DELAY_FRAMES
1551 + FISHING_ROD_OUT_FRAMES
1552 + FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES;
1553 tick_n(&mut anim, shake_bubble);
1554 assert_eq!(anim.phase(), FishingAnimPhase::Bubble);
1555 assert!(!anim.rod_visible(), "up-facing rod hidden under the bubble");
1556 assert!(anim.pose_active());
1557 tick_n(&mut anim, FISHING_BUBBLE_FRAMES);
1558 assert!(anim.is_done());
1559 assert!(!anim.pose_active(), "pose restored after the anim");
1560
1561 let mut left = FishingAnimState::new(Direction::Left, true);
1563 tick_n(&mut left, shake_bubble);
1564 assert_eq!(left.phase(), FishingAnimPhase::Bubble);
1565 assert!(left.rod_visible());
1566 }
1567
1568 #[test]
1569 fn fishing_anim_rod_piece_offsets() {
1570 assert_eq!(FishingAnimState::rod_piece(Direction::Down), (20, 35, 0, false));
1571 assert_eq!(FishingAnimState::rod_piece(Direction::Up), (20, -12, 0, false));
1572 assert_eq!(FishingAnimState::rod_piece(Direction::Left), (0, 16, 1, false));
1573 assert_eq!(FishingAnimState::rod_piece(Direction::Right), (48, 16, 1, true));
1574 }
1575
1576 #[test]
1579 fn boulder_dust_inactive_constant_is_inert() {
1580 let mut dust = BoulderDustState::inactive();
1581 assert!(!dust.is_active());
1582 dust.tick();
1583 assert!(!dust.is_active());
1584 }
1585
1586 #[test]
1587 fn boulder_dust_base_offsets() {
1588 for (facing, expected) in [
1589 (Direction::Down, (8, 52)),
1590 (Direction::Up, (8, -12)),
1591 (Direction::Left, (-24, 20)),
1592 (Direction::Right, (40, 20)),
1593 ] {
1594 let dust = BoulderDustState::new(facing, 5, 5);
1595 assert_eq!(dust.base_offset(), expected, "facing {:?}", facing);
1596 }
1597 }
1598
1599 #[test]
1600 fn boulder_dust_drifts_against_the_push_direction() {
1601 for (facing, expected) in [
1602 (Direction::Down, (0, -1)),
1603 (Direction::Up, (0, 1)),
1604 (Direction::Left, (1, 0)),
1605 (Direction::Right, (-1, 0)),
1606 ] {
1607 let dust = BoulderDustState::new(facing, 5, 5);
1608 assert_eq!(dust.drift_px(), expected, "facing {:?}", facing);
1609 }
1610 }
1611
1612 #[test]
1613 fn boulder_dust_horizontal_push_moves_three_of_four_tiles() {
1614 let dust = BoulderDustState::new(Direction::Left, 5, 5);
1616 assert_eq!(dust.tile_drifts(), [(0, 0), (1, 0), (1, 0), (1, 0)]);
1617 let dust = BoulderDustState::new(Direction::Down, 5, 5);
1618 assert_eq!(dust.tile_drifts(), [(0, -1); 4], "vertical pushes move all tiles");
1619 }
1620
1621 #[test]
1622 fn boulder_dust_runs_8_steps_of_3_frames_then_ends() {
1623 let mut dust = BoulderDustState::new(Direction::Down, 5, 5);
1624 assert!(dust.is_active());
1625 assert_eq!(dust.step(), 0);
1626 for tick in 1..=24 {
1627 dust.tick();
1628 if tick == 24 {
1629 assert!(!dust.is_active(), "tick 24 completes the animation");
1630 continue;
1631 }
1632 assert!(dust.is_active(), "tick {}", tick);
1633 if tick % 3 == 0 {
1634 assert_eq!(dust.step(), tick / 3, "step advances every 3rd tick");
1635 } else {
1636 assert_eq!(dust.step(), (tick - 1) / 3, "tick {}", tick);
1637 }
1638 }
1639 assert!(!dust.is_active(), "8 steps × 3 frames = 24 frames");
1640 dust.tick();
1642 assert!(!dust.is_active());
1643 }
1644
1645 #[test]
1646 fn boulder_dust_palette_flashes_every_step() {
1647 let mut dust = BoulderDustState::new(Direction::Right, 5, 5);
1648 let mut seen = Vec::new();
1649 for _ in 0..24 {
1650 seen.push(dust.palette_flipped());
1651 dust.tick();
1652 }
1653 let expected: Vec<bool> = (0..8).map(|s| s % 2 == 1).flat_map(|f| [f, f, f]).collect();
1654 assert_eq!(seen, expected, "palette toggles on each odd step");
1655 }
1656
1657 #[test]
1658 fn boulder_dust_keeps_its_world_anchor() {
1659 let mut dust = BoulderDustState::new(Direction::Up, 12, 7);
1660 assert_eq!(dust.anchor(), (12, 7));
1661 for _ in 0..24 {
1662 dust.tick();
1663 }
1664 assert_eq!(dust.anchor(), (12, 7), "anchor survives the animation");
1665 }
1666
1667 #[test]
1670 fn ship_departure_phase_boundaries() {
1671 let mut dep = ShipDepartureState::new();
1672 assert_eq!(dep.phase(), ShipDeparturePhase::InitialPause);
1673 for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES {
1674 dep.tick();
1675 }
1676 assert_eq!(dep.phase(), ShipDeparturePhase::WaterFill);
1677 for _ in 0..SHIP_DEPARTURE_WATER_FILL_FRAMES {
1678 dep.tick();
1679 }
1680 assert_eq!(dep.phase(), ShipDeparturePhase::Scroll);
1681 for _ in 0..SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES {
1682 dep.tick();
1683 }
1684 assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
1685 for _ in 0..SHIP_DEPARTURE_ERASE_FRAMES {
1686 dep.tick();
1687 }
1688 assert_eq!(dep.phase(), ShipDeparturePhase::Done);
1689 assert!(dep.is_done());
1690 assert_eq!(dep.frame(), SHIP_DEPARTURE_TOTAL_FRAMES);
1691 assert_eq!(dep.tick(), None);
1693 assert_eq!(dep.frame(), SHIP_DEPARTURE_TOTAL_FRAMES);
1694 }
1695
1696 #[test]
1697 fn ship_departure_horn_timing() {
1698 let mut dep = ShipDepartureState::new();
1701 let mut horns = Vec::new();
1702 for _ in 0..SHIP_DEPARTURE_TOTAL_FRAMES {
1703 if let Some(sfx) = dep.tick() {
1704 horns.push((dep.frame(), sfx));
1705 }
1706 }
1707 assert_eq!(
1708 horns,
1709 vec![
1710 (
1711 SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES,
1712 ShipDepartureSfx::Horn
1713 ),
1714 (
1715 SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
1716 + SHIP_DEPARTURE_WATER_FILL_FRAMES
1717 + SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES,
1718 ShipDepartureSfx::Horn
1719 ),
1720 ]
1721 );
1722 }
1723
1724 #[test]
1725 fn ship_departure_scroll_px_ramps() {
1726 let mut dep = ShipDepartureState::new();
1730 for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
1731 dep.tick();
1732 }
1733 let mut seen = Vec::new();
1734 for _ in 0..SHIP_DEPARTURE_ITERATION_FRAMES {
1735 seen.push(dep.scroll_px());
1736 dep.tick();
1737 }
1738 let expected: Vec<i32> = (0..16).flat_map(|d| core::iter::repeat(d + 1).take(8)).collect();
1739 assert_eq!(seen, expected, "scroll ramps 1..=16 across iteration 0");
1740
1741 for _ in 0..(40 * 8 - SHIP_DEPARTURE_ITERATION_FRAMES) {
1744 dep.tick();
1745 }
1746 assert_eq!(dep.scroll_iteration(), 2);
1747 assert_eq!(dep.scroll_substep(), 40);
1748 assert_eq!(dep.scroll_px(), 41);
1749
1750 for _ in 0..(SHIP_DEPARTURE_TOTAL_FRAMES - 1 - (123 + 40 * 8)) {
1753 dep.tick();
1754 }
1755 assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
1756 assert_eq!(dep.scroll_px(), 128);
1757 assert_eq!(dep.scroll_iteration(), SHIP_DEPARTURE_SCROLL_ITERATIONS - 1);
1758 assert_eq!(dep.scroll_substep(), 127);
1759 assert!(dep.ship_erased());
1760 }
1761
1762 #[test]
1763 fn ship_departure_puff_positions() {
1764 let mut dep = ShipDepartureState::new();
1766 for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
1767 dep.tick();
1768 }
1769 for _ in 0..16 * 8 {
1773 dep.tick();
1774 }
1775 assert_eq!(dep.puff_count(), 2);
1776 assert_eq!(dep.puff_x_offset(0), 88 + 2 * (16 + 1));
1777 assert_eq!(dep.puff_x_offset(1), 72 + 2 * (16 - 16 + 1));
1778
1779 for _ in 0..SHIP_DEPARTURE_ITERATION_FRAMES {
1781 dep.tick();
1782 }
1783 assert_eq!(dep.puff_count(), 3);
1784 assert_eq!(dep.puff_x_offset(0), 88 + 2 * (32 + 1));
1785 assert_eq!(dep.puff_x_offset(1), 72 + 2 * (32 - 16 + 1));
1786 assert_eq!(dep.puff_x_offset(2), 56 + 2 * (32 - 32 + 1));
1787
1788 for _ in 0..6 * SHIP_DEPARTURE_ITERATION_FRAMES {
1790 dep.tick();
1791 }
1792 assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
1793 assert_eq!(dep.puff_count(), 8);
1794 assert_eq!(dep.puff_x_offset(0), 88 + 2 * 128);
1795 assert_eq!(dep.puff_x_offset(7), 88 - 16 * 7 + 2 * (127 - 16 * 7 + 1));
1796 assert_eq!(dep.puff_screen_y(), 84);
1798 }
1799
1800 #[test]
1801 fn ship_departure_erase_flag_only_in_erase_phase() {
1802 let mut dep = ShipDepartureState::new();
1803 assert!(!dep.ship_erased());
1804 for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
1805 dep.tick();
1806 }
1807 assert!(!dep.ship_erased(), "hull still visible during the scroll");
1808 for _ in 0..SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES {
1809 dep.tick();
1810 }
1811 assert!(dep.ship_erased(), "erase phase shows the ship as water");
1812 }
1813}