use super::types::Direction;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TeleportSpinPhase {
SpinInPlace,
SpinUp,
Delay,
Done,
}
pub const SPIN_IN_PLACE_FRAMES: u16 = 136;
pub const SPIN_UP_STEP_DELAY: u16 = 3;
pub const SPIN_UP_STEPS: u16 = 5;
pub const SPIN_POST_DELAY_FRAMES: u16 = 10;
pub const SPIN_UP_STEP_PIXELS: i32 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TeleportSpinSfx {
SpinLoop,
Rise,
}
#[derive(Debug, Clone, Copy)]
pub struct TeleportSpinState {
spin_order: [Direction; 4],
start_index: usize,
frame: u16,
phase: TeleportSpinPhase,
}
impl TeleportSpinState {
pub fn new(current_facing: Direction, spin_order: [Direction; 4]) -> Self {
let start_index = spin_order
.iter()
.position(|&d| d == current_facing)
.unwrap_or(0);
Self {
spin_order,
start_index,
frame: 0,
phase: TeleportSpinPhase::SpinInPlace,
}
}
fn spin_in_place_index(frame: u16) -> Option<usize> {
let mut start = 0u16;
for i in 0..16u16 {
let dur = 16 - i;
if frame < start + dur {
return Some(i as usize);
}
start += dur;
}
None
}
pub fn tick(&mut self) -> Option<TeleportSpinSfx> {
if self.phase == TeleportSpinPhase::Done {
return None;
}
let mut sfx = None;
match self.phase {
TeleportSpinPhase::SpinInPlace => {
if self.frame == 0
|| Self::spin_in_place_index(self.frame)
!= Self::spin_in_place_index(self.frame.wrapping_sub(1))
{
if let Some(i) = Self::spin_in_place_index(self.frame) {
if (16 - i) % 4 == 0 {
sfx = Some(TeleportSpinSfx::SpinLoop);
}
}
}
if self.frame + 1 >= SPIN_IN_PLACE_FRAMES {
self.phase = TeleportSpinPhase::SpinUp;
}
}
TeleportSpinPhase::SpinUp => {
if self.frame == SPIN_IN_PLACE_FRAMES {
sfx = Some(TeleportSpinSfx::Rise);
}
let spin_up_frames = (SPIN_UP_STEPS - 1) * (1 + SPIN_UP_STEP_DELAY) + 1;
if self.frame + 1 >= SPIN_IN_PLACE_FRAMES + spin_up_frames {
self.phase = TeleportSpinPhase::Delay;
}
}
TeleportSpinPhase::Delay => {
let spin_up_frames = (SPIN_UP_STEPS - 1) * (1 + SPIN_UP_STEP_DELAY) + 1;
if self.frame + 1 >= SPIN_IN_PLACE_FRAMES + spin_up_frames + SPIN_POST_DELAY_FRAMES
{
self.phase = TeleportSpinPhase::Done;
}
}
TeleportSpinPhase::Done => {}
}
self.frame += 1;
sfx
}
pub fn is_done(&self) -> bool {
self.phase == TeleportSpinPhase::Done
}
pub fn phase(&self) -> TeleportSpinPhase {
self.phase
}
pub fn facing(&self) -> Direction {
let step = match self.phase {
TeleportSpinPhase::SpinInPlace => {
Self::spin_in_place_index(self.frame).unwrap_or(15)
}
TeleportSpinPhase::SpinUp => {
let f = self.frame - SPIN_IN_PLACE_FRAMES;
((f / (1 + SPIN_UP_STEP_DELAY)) as usize).min((SPIN_UP_STEPS - 1) as usize) + 16
}
TeleportSpinPhase::Delay | TeleportSpinPhase::Done => 16 + (SPIN_UP_STEPS - 1) as usize,
};
self.spin_order[(self.start_index + step) % 4]
}
pub fn player_y_offset(&self) -> i32 {
match self.phase {
TeleportSpinPhase::SpinInPlace => 0,
TeleportSpinPhase::SpinUp => {
let f = self.frame - SPIN_IN_PLACE_FRAMES;
let step = ((f / (1 + SPIN_UP_STEP_DELAY)) as i32 + 1).min(SPIN_UP_STEPS as i32);
-step * SPIN_UP_STEP_PIXELS
}
TeleportSpinPhase::Delay | TeleportSpinPhase::Done => {
-(SPIN_UP_STEPS as i32) * SPIN_UP_STEP_PIXELS
}
}
}
pub fn player_visible(&self) -> bool {
self.player_y_offset() > -(SPIN_UP_STEPS as i32) * SPIN_UP_STEP_PIXELS
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnterMapSpinPhase {
SpinDown,
SpinInPlace,
Done,
}
pub const ENTER_MAP_SPIN_DOWN_FRAMES: u16 =
(ENTER_MAP_SPIN_DOWN_STEPS - 1) * (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY) + 1;
pub const ENTER_MAP_SPIN_DOWN_STEPS: u16 = 5;
pub const ENTER_MAP_SPIN_DOWN_STEP_DELAY: u16 = 3;
pub const ENTER_MAP_SPIN_DOWN_STEP_PIXELS: i32 = 16;
pub const ENTER_MAP_SPIN_IN_PLACE_FRAMES: u16 = 36;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnterMapSpinSfx {
Descend,
Land,
}
#[derive(Debug, Clone, Copy)]
pub struct EnterMapSpinState {
spin_order: [Direction; 4],
start_index: usize,
frame: u16,
phase: EnterMapSpinPhase,
spin_in_place: bool,
}
impl EnterMapSpinState {
pub fn new(current_facing: Direction, spin_order: [Direction; 4], spin_in_place: bool) -> Self {
let start_index = spin_order
.iter()
.position(|&d| d == current_facing)
.unwrap_or(0);
Self {
spin_order,
start_index,
frame: 0,
phase: EnterMapSpinPhase::SpinDown,
spin_in_place,
}
}
pub fn tick(&mut self) -> Option<EnterMapSpinSfx> {
if self.phase == EnterMapSpinPhase::Done {
return None;
}
let mut sfx = None;
match self.phase {
EnterMapSpinPhase::SpinDown => {
if self.frame == 0 {
sfx = Some(EnterMapSpinSfx::Descend);
}
if self.frame + 1 >= ENTER_MAP_SPIN_DOWN_FRAMES {
sfx = Some(EnterMapSpinSfx::Land);
self.phase = if self.spin_in_place {
EnterMapSpinPhase::SpinInPlace
} else {
EnterMapSpinPhase::Done
};
}
}
EnterMapSpinPhase::SpinInPlace => {
if self.frame + 1
>= ENTER_MAP_SPIN_DOWN_FRAMES + ENTER_MAP_SPIN_IN_PLACE_FRAMES
{
self.phase = EnterMapSpinPhase::Done;
}
}
EnterMapSpinPhase::Done => {}
}
self.frame += 1;
sfx
}
pub fn is_done(&self) -> bool {
self.phase == EnterMapSpinPhase::Done
}
pub fn phase(&self) -> EnterMapSpinPhase {
self.phase
}
pub fn facing(&self) -> Direction {
let step = match self.phase {
EnterMapSpinPhase::SpinDown => {
(self.frame / (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY)) as usize
}
EnterMapSpinPhase::SpinInPlace => {
let f = self.frame - ENTER_MAP_SPIN_DOWN_FRAMES;
let spin = (f as usize).min(ENTER_MAP_SPIN_IN_PLACE_FRAMES as usize - 1);
let mut start = 0usize;
let mut idx = 0usize;
for i in 0..8usize {
let dur = i + 1;
if spin < start + dur {
idx = i;
break;
}
start += dur;
}
ENTER_MAP_SPIN_DOWN_STEPS as usize + idx
}
EnterMapSpinPhase::Done => 0,
};
self.spin_order[(self.start_index + step) % 4]
}
pub fn player_y_offset(&self) -> i32 {
match self.phase {
EnterMapSpinPhase::SpinDown => {
if self.frame == 0 {
-(ENTER_MAP_SPIN_DOWN_STEPS as i32) * ENTER_MAP_SPIN_DOWN_STEP_PIXELS
} else {
let moves = ((self.frame - 1) / (1 + ENTER_MAP_SPIN_DOWN_STEP_DELAY) + 1)
.min(ENTER_MAP_SPIN_DOWN_STEPS);
-((ENTER_MAP_SPIN_DOWN_STEPS - moves) as i32)
* ENTER_MAP_SPIN_DOWN_STEP_PIXELS
}
}
EnterMapSpinPhase::SpinInPlace | EnterMapSpinPhase::Done => 0,
}
}
pub fn player_visible(&self) -> bool {
self.player_y_offset() > -(ENTER_MAP_SPIN_DOWN_STEPS as i32)
* ENTER_MAP_SPIN_DOWN_STEP_PIXELS
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElevatorShakeParams {
pub iterations: u8,
pub pixel_offset: u8,
}
impl ElevatorShakeParams {
pub const fn total_frames(&self) -> u16 {
self.iterations as u16 * 2
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElevatorShakeSfx {
Rattle,
Arrive,
}
#[derive(Debug, Clone, Copy)]
pub struct ElevatorShakeState {
frame: u16,
params: ElevatorShakeParams,
}
impl ElevatorShakeState {
pub fn new(params: ElevatorShakeParams) -> Self {
Self { frame: 0, params }
}
pub fn params(&self) -> ElevatorShakeParams {
self.params
}
pub fn total_frames(&self) -> u16 {
self.params.total_frames()
}
pub fn tick(&mut self) -> Option<ElevatorShakeSfx> {
if self.is_done() {
return None;
}
let sfx = if self.frame + 1 >= self.total_frames() {
Some(ElevatorShakeSfx::Arrive)
} else if self.frame % 2 == 0 {
Some(ElevatorShakeSfx::Rattle)
} else {
None
};
self.frame += 1;
sfx
}
pub fn is_done(&self) -> bool {
self.frame >= self.total_frames()
}
pub fn offset_y(&self) -> i32 {
if self.is_done() {
return 0;
}
let iteration = self.frame / 2;
let px = self.params.pixel_offset as i32;
if iteration % 2 == 0 {
-px
} else {
px
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TileAnimKind {
None,
Water,
WaterFlower,
}
#[derive(Debug, Clone, Copy)]
pub struct TileAnimState {
counter1: u8,
counter2: u8,
water_shift: i8,
flower_frame: Option<u8>,
kind: TileAnimKind,
}
impl TileAnimState {
pub fn new() -> Self {
Self {
counter1: 0,
counter2: 0,
water_shift: 0,
flower_frame: None,
kind: TileAnimKind::None,
}
}
pub fn set_tileset(&mut self, kind: TileAnimKind) {
self.kind = kind;
self.counter1 = 0;
}
pub fn tick(&mut self) {
if self.kind == TileAnimKind::None {
return;
}
self.counter1 = self.counter1.wrapping_add(1);
if self.counter1 < 20 {
return;
}
if self.counter1 == 21 {
self.counter1 = 0;
self.flower_frame = Some(match self.counter2 & 3 {
0 | 1 => 1,
2 => 2,
_ => 3,
});
return;
}
self.counter2 = (self.counter2 + 1) & 7;
self.water_shift += if self.counter2 & 4 == 0 { 1 } else { -1 };
if self.kind == TileAnimKind::Water {
self.counter1 = 0;
}
}
pub fn water_shift(&self) -> i8 {
self.water_shift
}
pub fn flower_frame(&self) -> Option<u8> {
self.flower_frame
}
pub fn kind(&self) -> TileAnimKind {
self.kind
}
}
impl Default for TileAnimState {
fn default() -> Self {
Self::new()
}
}
pub const FISHING_CAST_DELAY_FRAMES: u16 = 10;
pub const FISHING_ROD_OUT_FRAMES: u16 = 100;
pub const FISHING_SHAKE_ITERATIONS: u16 = 10;
pub const FISHING_SHAKE_STEP_FRAMES: u16 = 3;
pub const FISHING_BUBBLE_FRAMES: u16 = 60;
pub const FISHING_ANIM_FRAMES: u16 = FISHING_CAST_DELAY_FRAMES
+ FISHING_ROD_OUT_FRAMES
+ FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES
+ FISHING_BUBBLE_FRAMES;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FishingAnimPhase {
CastDelay,
RodOut,
Shake,
Bubble,
Done,
}
#[derive(Debug, Clone, Copy)]
pub struct FishingAnimState {
frame: u16,
facing: Direction,
bite: bool,
phase: FishingAnimPhase,
}
impl FishingAnimState {
pub fn new(facing: Direction, bite: bool) -> Self {
Self {
frame: 0,
facing,
bite,
phase: FishingAnimPhase::CastDelay,
}
}
fn phase_for(frame: u16, bite: bool) -> FishingAnimPhase {
let rod_end = FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES;
if frame < FISHING_CAST_DELAY_FRAMES {
FishingAnimPhase::CastDelay
} else if frame < rod_end {
FishingAnimPhase::RodOut
} else if !bite {
FishingAnimPhase::Done
} else if frame < rod_end + FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES {
FishingAnimPhase::Shake
} else if frame < rod_end
+ FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES
+ FISHING_BUBBLE_FRAMES
{
FishingAnimPhase::Bubble
} else {
FishingAnimPhase::Done
}
}
pub fn tick(&mut self) {
self.frame = self.frame.saturating_add(1);
self.phase = Self::phase_for(self.frame, self.bite);
}
pub fn phase(&self) -> FishingAnimPhase {
self.phase
}
pub fn is_done(&self) -> bool {
self.phase == FishingAnimPhase::Done
}
pub fn facing(&self) -> Direction {
self.facing
}
pub fn pose_active(&self) -> bool {
matches!(
self.phase,
FishingAnimPhase::RodOut | FishingAnimPhase::Shake | FishingAnimPhase::Bubble
)
}
pub fn rod_visible(&self) -> bool {
self.pose_active()
&& !(self.phase == FishingAnimPhase::Bubble && self.facing == Direction::Up)
}
pub fn bubble_active(&self) -> bool {
self.phase == FishingAnimPhase::Bubble
}
pub fn player_shake_offset(&self) -> i32 {
if self.phase != FishingAnimPhase::Shake {
return 0;
}
let shake_start = FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES;
let iteration = (self.frame - shake_start) / FISHING_SHAKE_STEP_FRAMES;
if iteration % 2 == 0 { 1 } else { 0 }
}
pub fn rod_piece(facing: Direction) -> (i32, i32, u8, bool) {
match facing {
Direction::Down => (20, 35, 0, false),
Direction::Up => (20, -12, 0, false),
Direction::Left => (0, 16, 1, false),
Direction::Right => (48, 16, 1, true),
}
}
}
pub const BOULDER_DUST_STEPS: u8 = 8;
pub const BOULDER_DUST_STEP_FRAMES: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoulderDustState {
facing: Direction,
anchor_x: u16,
anchor_y: u16,
step: u8,
frame: u8,
}
impl BoulderDustState {
pub const fn inactive() -> Self {
Self {
facing: Direction::Down,
anchor_x: 0,
anchor_y: 0,
step: BOULDER_DUST_STEPS,
frame: 0,
}
}
pub const fn new(facing: Direction, anchor_x: u16, anchor_y: u16) -> Self {
Self {
facing,
anchor_x,
anchor_y,
step: 0,
frame: 0,
}
}
pub fn tick(&mut self) {
if self.step >= BOULDER_DUST_STEPS {
return;
}
self.frame += 1;
if self.frame >= BOULDER_DUST_STEP_FRAMES {
self.frame = 0;
self.step += 1;
}
}
pub fn is_active(&self) -> bool {
self.step < BOULDER_DUST_STEPS
}
pub fn facing(&self) -> Direction {
self.facing
}
pub fn anchor(&self) -> (u16, u16) {
(self.anchor_x, self.anchor_y)
}
pub fn step(&self) -> u8 {
self.step.min(BOULDER_DUST_STEPS - 1)
}
pub fn base_offset(&self) -> (i32, i32) {
match self.facing {
Direction::Down => (8, 52),
Direction::Up => (8, -12),
Direction::Left => (-24, 20),
Direction::Right => (40, 20),
}
}
pub fn drift_px(&self) -> (i32, i32) {
match self.facing {
Direction::Down => (0, -1),
Direction::Up => (0, 1),
Direction::Left => (1, 0),
Direction::Right => (-1, 0),
}
}
pub fn tile_drifts(&self) -> [(i32, i32); 4] {
let (dx, dy) = self.drift_px();
match self.facing {
Direction::Left | Direction::Right => [(0, 0), (dx, 0), (dx, 0), (dx, 0)],
_ => [(dx, dy); 4],
}
}
pub fn palette_flipped(&self) -> bool {
self.step % 2 == 1
}
}
pub const FLASH_WHITE_FRAMES: u8 = 3;
pub const SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES: u16 = 120;
pub const SHIP_DEPARTURE_WATER_FILL_FRAMES: u16 = 3;
pub const SHIP_DEPARTURE_SCROLL_ITERATIONS: u16 = 8;
pub const SHIP_DEPARTURE_ITERATION_FRAMES: u16 = 16 * 8;
pub const SHIP_DEPARTURE_ERASE_FRAMES: u16 = 120;
pub const SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION: i32 = 2 * 8;
pub const SHIP_DEPARTURE_PUFF_SPACING_PX: i32 = 16;
pub const SHIP_DEPARTURE_PUFF_DRIFT_PX_PER_SUBSTEP: i32 = 2;
pub const SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION: u16 = 16;
pub const SHIP_DEPARTURE_SUBSTEP_FRAMES: u16 = 8;
pub const SHIP_DEPARTURE_SMOKESTACK_TILE_X: u16 = 16;
pub const SHIP_DEPARTURE_SMOKESTACK_TILE_Y: f32 = 10.5;
pub const SHIP_DEPARTURE_PUFF_START_SCREEN_X: i32 = 88;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShipDeparturePhase {
InitialPause,
WaterFill,
Scroll,
Erase,
Done,
}
pub const SHIP_DEPARTURE_TOTAL_FRAMES: u16 = SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
+ SHIP_DEPARTURE_WATER_FILL_FRAMES
+ SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES
+ SHIP_DEPARTURE_ERASE_FRAMES;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShipDepartureSfx {
Horn,
}
#[derive(Debug, Clone, Copy)]
pub struct ShipDepartureState {
frame: u16,
phase: ShipDeparturePhase,
}
impl ShipDepartureState {
pub fn new() -> Self {
Self {
frame: 0,
phase: ShipDeparturePhase::InitialPause,
}
}
fn phase_for(frame: u16) -> ShipDeparturePhase {
let scroll_end = SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
+ SHIP_DEPARTURE_WATER_FILL_FRAMES
+ SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES;
if frame < SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES {
ShipDeparturePhase::InitialPause
} else if frame < SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
ShipDeparturePhase::WaterFill
} else if frame < scroll_end {
ShipDeparturePhase::Scroll
} else if frame < SHIP_DEPARTURE_TOTAL_FRAMES {
ShipDeparturePhase::Erase
} else {
ShipDeparturePhase::Done
}
}
pub fn tick(&mut self) -> Option<ShipDepartureSfx> {
if self.phase == ShipDeparturePhase::Done {
return None;
}
let mut sfx = None;
let next = self.frame + 1;
let next_phase = Self::phase_for(next);
if next_phase != self.phase {
match next_phase {
ShipDeparturePhase::Scroll => {
sfx = Some(ShipDepartureSfx::Horn);
}
ShipDeparturePhase::Erase => {
sfx = Some(ShipDepartureSfx::Horn);
}
_ => {}
}
}
self.frame = next;
self.phase = next_phase;
sfx
}
pub fn is_done(&self) -> bool {
self.phase == ShipDeparturePhase::Done
}
pub fn phase(&self) -> ShipDeparturePhase {
self.phase
}
pub fn frame(&self) -> u16 {
self.frame
}
pub fn ship_erased(&self) -> bool {
matches!(
self.phase,
ShipDeparturePhase::Erase | ShipDeparturePhase::Done
)
}
pub fn scroll_iteration(&self) -> u16 {
match self.phase {
ShipDeparturePhase::Scroll => {
(self.frame
- SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
- SHIP_DEPARTURE_WATER_FILL_FRAMES)
/ SHIP_DEPARTURE_ITERATION_FRAMES
}
ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
SHIP_DEPARTURE_SCROLL_ITERATIONS - 1
}
_ => 0,
}
}
pub fn scroll_substep(&self) -> u16 {
match self.phase {
ShipDeparturePhase::Scroll => {
(self.frame
- SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
- SHIP_DEPARTURE_WATER_FILL_FRAMES)
/ SHIP_DEPARTURE_SUBSTEP_FRAMES
}
ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION - 1
}
_ => 0,
}
}
pub fn scroll_px(&self) -> i32 {
match self.phase {
ShipDeparturePhase::Scroll => {
self.scroll_iteration() as i32 * SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION
+ (self.scroll_substep() as i32
% SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION as i32)
+ 1
}
ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
SHIP_DEPARTURE_SCROLL_ITERATIONS as i32
* SHIP_DEPARTURE_SCROLL_PX_PER_ITERATION
}
_ => 0,
}
}
pub fn puff_count(&self) -> usize {
match self.phase {
ShipDeparturePhase::InitialPause | ShipDeparturePhase::WaterFill => 0,
ShipDeparturePhase::Scroll | ShipDeparturePhase::Erase | ShipDeparturePhase::Done => {
(self.scroll_iteration() + 1) as usize
}
}
}
pub fn puff_x_offset(&self, i: usize) -> i32 {
let i = i as i32;
let spawn = SHIP_DEPARTURE_PUFF_START_SCREEN_X - SHIP_DEPARTURE_PUFF_SPACING_PX * i;
let s = self.scroll_substep() as i32;
let substeps_live = (s - SHIP_DEPARTURE_SUBSTEPS_PER_ITERATION as i32 * i).max(0);
spawn + SHIP_DEPARTURE_PUFF_DRIFT_PX_PER_SUBSTEP * (substeps_live + 1)
}
pub fn puff_screen_y(&self) -> i32 {
(SHIP_DEPARTURE_SMOKESTACK_TILE_Y * 8.0) as i32
}
}
impl Default for ShipDepartureState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SPIN_ORDER: [Direction; 4] = [
Direction::Down,
Direction::Left,
Direction::Up,
Direction::Right,
];
#[test]
fn spin_starts_on_current_facing_then_cycles() {
for (start, order) in [
(
Direction::Down,
[
Direction::Down,
Direction::Left,
Direction::Up,
Direction::Right,
],
),
(
Direction::Up,
[
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
],
),
] {
let mut spin = TeleportSpinState::new(start, SPIN_ORDER);
for (i, want) in order.iter().enumerate() {
assert_eq!(&spin.facing(), want, "start {:?} spin {}", start, i);
for _ in 0..(16 - i) {
spin.tick();
}
}
}
}
#[test]
fn spin_honors_a_custom_facing_cycle() {
const REVERSED: [Direction; 4] = [
Direction::Down,
Direction::Right,
Direction::Up,
Direction::Left,
];
let mut spin = TeleportSpinState::new(Direction::Down, REVERSED);
assert_eq!(spin.facing(), Direction::Down);
for _ in 0..16 {
spin.tick();
}
assert_eq!(spin.facing(), Direction::Right, "second spin of the custom cycle");
let spin = TeleportSpinState::new(Direction::Up, [Direction::Down; 4]);
assert_eq!(spin.facing(), Direction::Down);
}
#[test]
fn spin_sfx_schedule() {
let mut spin = TeleportSpinState::new(Direction::Down, SPIN_ORDER);
let mut loops = 0;
let mut rises = 0;
let mut frames = 0;
while !spin.is_done() {
match spin.tick() {
Some(TeleportSpinSfx::SpinLoop) => loops += 1,
Some(TeleportSpinSfx::Rise) => {
rises += 1;
assert_eq!(frames, SPIN_IN_PLACE_FRAMES, "rise at spin-up start");
}
None => {}
}
frames += 1;
assert!(frames < 1000, "spin must terminate");
}
assert_eq!(loops, 4);
assert_eq!(rises, 1);
assert_eq!(frames, 136 + 17 + 10);
}
#[test]
fn spin_rises_off_screen_and_hides() {
let mut spin = TeleportSpinState::new(Direction::Down, SPIN_ORDER);
assert_eq!(spin.player_y_offset(), 0);
assert!(spin.player_visible());
for _ in 0..SPIN_IN_PLACE_FRAMES - 1 {
spin.tick();
}
assert_eq!(spin.player_y_offset(), 0, "still grounded on the last spin frame");
spin.tick(); assert_eq!(spin.player_y_offset(), -16);
for _ in 0..16 {
spin.tick();
}
assert_eq!(spin.player_y_offset(), -80);
assert!(!spin.player_visible(), "sprite fully above the screen");
}
#[test]
fn enter_map_spin_starts_hidden_and_descends_in_five_steps() {
let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, true);
assert_eq!(anim.phase(), EnterMapSpinPhase::SpinDown);
assert!(!anim.player_visible(), "hidden until the spin-down descends");
assert_eq!(anim.player_y_offset(), -80);
let mut last = -80;
for frame in 1..=17 {
anim.tick();
if frame % 4 == 1 {
assert!(anim.player_y_offset() > last, "move {frame} descends");
last = anim.player_y_offset();
}
}
assert_eq!(anim.player_y_offset(), 0, "standing position after the 5th move");
assert!(anim.player_visible());
}
#[test]
fn enter_map_spin_timing_and_sfx() {
let mut anim = EnterMapSpinState::new(Direction::Left, SPIN_ORDER, true);
assert_eq!(anim.tick(), Some(EnterMapSpinSfx::Descend));
for _ in 1..ENTER_MAP_SPIN_DOWN_FRAMES - 1 {
assert_eq!(anim.tick(), None);
}
assert_eq!(
anim.tick(),
Some(EnterMapSpinSfx::Land),
"land when the spin-down completes"
);
assert_eq!(anim.phase(), EnterMapSpinPhase::SpinInPlace);
let mut ticks = 0;
while !anim.is_done() {
anim.tick();
ticks += 1;
assert!(ticks <= ENTER_MAP_SPIN_IN_PLACE_FRAMES, "bounded");
}
assert_eq!(ticks, ENTER_MAP_SPIN_IN_PLACE_FRAMES);
assert!(anim.is_done());
}
#[test]
fn enter_map_spin_skips_spin_in_place_on_warp_pad() {
let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, false);
let mut ticks = 0;
while !anim.is_done() && ticks < 200 {
anim.tick();
ticks += 1;
}
assert_eq!(ticks, ENTER_MAP_SPIN_DOWN_FRAMES, "no spin-in-place phase");
assert!(anim.is_done());
}
#[test]
fn enter_map_spin_facing_cycles_and_restores() {
let mut anim = EnterMapSpinState::new(Direction::Down, SPIN_ORDER, true);
anim.tick(); assert_eq!(anim.facing(), Direction::Down);
for _ in 0..4 {
anim.tick(); }
assert_eq!(anim.facing(), Direction::Left);
for _ in 0..ENTER_MAP_SPIN_DOWN_FRAMES + 5 {
anim.tick();
}
assert_eq!(anim.phase(), EnterMapSpinPhase::SpinInPlace);
assert_eq!(anim.facing(), Direction::Left);
}
const SHAKE: ElevatorShakeParams = ElevatorShakeParams {
iterations: 100,
pixel_offset: 1,
};
#[test]
fn elevator_shake_alternates_offset_each_iteration() {
let mut shake = ElevatorShakeState::new(SHAKE);
assert_eq!(shake.offset_y(), -1);
shake.tick();
assert_eq!(shake.offset_y(), -1, "one iteration lasts 2 frames");
shake.tick();
assert_eq!(shake.offset_y(), 1);
shake.tick();
assert_eq!(shake.offset_y(), 1);
shake.tick();
assert_eq!(shake.offset_y(), -1);
}
#[test]
fn elevator_shake_sfx_and_duration() {
let mut shake = ElevatorShakeState::new(SHAKE);
let mut rattles = 0;
let mut dings = 0;
let mut frames = 0;
while !shake.is_done() {
match shake.tick() {
Some(ElevatorShakeSfx::Rattle) => rattles += 1,
Some(ElevatorShakeSfx::Arrive) => dings += 1,
None => {}
}
frames += 1;
}
assert_eq!(frames, SHAKE.total_frames());
assert_eq!(rattles, 100, "rattle once per iteration");
assert_eq!(dings, 1, "arrival ding at the end");
assert_eq!(shake.offset_y(), 0, "scroll restored after the shake");
}
#[test]
fn elevator_shake_honors_custom_params() {
let params = ElevatorShakeParams {
iterations: 4,
pixel_offset: 2,
};
let mut shake = ElevatorShakeState::new(params);
assert_eq!(shake.total_frames(), 8);
assert_eq!(shake.offset_y(), -2);
shake.tick();
shake.tick();
assert_eq!(shake.offset_y(), 2);
let mut frames = 2;
while !shake.is_done() {
shake.tick();
frames += 1;
}
assert_eq!(frames, 8);
}
#[test]
fn tile_anim_disabled_for_none_tilesets() {
let mut anim = TileAnimState::new();
anim.set_tileset(TileAnimKind::None);
for _ in 0..100 {
anim.tick();
}
assert_eq!(anim.water_shift(), 0);
assert_eq!(anim.flower_frame(), None);
}
#[test]
fn tile_anim_water_rotates_every_20_frames() {
let mut anim = TileAnimState::new();
anim.set_tileset(TileAnimKind::Water);
for _ in 0..19 {
anim.tick();
}
assert_eq!(anim.water_shift(), 0, "no update before counter1 hits 20");
anim.tick();
assert_eq!(anim.water_shift(), 1, "first update shifts right one pixel");
for _ in 0..20 {
anim.tick();
}
assert_eq!(anim.water_shift(), 2);
}
#[test]
fn tile_anim_water_direction_follows_counter2_bit2() {
let mut anim = TileAnimState::new();
anim.set_tileset(TileAnimKind::Water);
let expected = [1, 2, 3, 2, 1, 0, -1, 0];
for want in expected {
for _ in 0..20 {
anim.tick();
}
assert_eq!(anim.water_shift(), want);
}
}
#[test]
fn tile_anim_flower_frames_cycle_1_2_3_1() {
let mut anim = TileAnimState::new();
anim.set_tileset(TileAnimKind::WaterFlower);
assert_eq!(anim.flower_frame(), None, "base tile until the first update");
let expected = [1, 2, 3, 1, 1, 2];
for want in expected {
for _ in 0..21 {
anim.tick();
}
assert_eq!(anim.flower_frame(), Some(want));
}
}
#[test]
fn tile_anim_map_load_resets_counter_but_keeps_water_phase() {
let mut anim = TileAnimState::new();
anim.set_tileset(TileAnimKind::WaterFlower);
for _ in 0..10 {
anim.tick();
}
anim.set_tileset(TileAnimKind::Water);
for _ in 0..19 {
anim.tick();
}
assert_eq!(anim.water_shift(), 0);
anim.tick();
assert_eq!(anim.water_shift(), 1);
assert_eq!(anim.flower_frame(), None, "water-only tilesets never flower");
}
fn tick_n(anim: &mut FishingAnimState, n: u16) {
for _ in 0..n {
anim.tick();
}
}
#[test]
fn fishing_anim_no_bite_finishes_after_rod_out() {
for facing in [Direction::Down, Direction::Up, Direction::Left, Direction::Right] {
let mut anim = FishingAnimState::new(facing, false);
assert_eq!(anim.phase(), FishingAnimPhase::CastDelay);
for _ in 1..FISHING_CAST_DELAY_FRAMES {
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::CastDelay);
}
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::RodOut, "rod appears at frame 10");
for _ in 0..FISHING_ROD_OUT_FRAMES - 1 {
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::RodOut);
}
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::Done, "no bite → straight to text");
assert!(anim.is_done());
}
}
#[test]
fn fishing_anim_bite_plays_shake_then_bubble_then_done() {
let mut anim = FishingAnimState::new(Direction::Down, true);
tick_n(&mut anim, FISHING_CAST_DELAY_FRAMES + FISHING_ROD_OUT_FRAMES);
assert_eq!(anim.phase(), FishingAnimPhase::Shake, "bite starts the shake");
assert!(!anim.bubble_active());
for i in 0..FISHING_SHAKE_ITERATIONS {
assert_eq!(
anim.phase(),
FishingAnimPhase::Shake,
"shake iteration {i} still active"
);
assert_eq!(anim.player_shake_offset(), if i % 2 == 0 { 1 } else { 0 });
tick_n(&mut anim, FISHING_SHAKE_STEP_FRAMES);
}
assert_eq!(anim.phase(), FishingAnimPhase::Bubble, "shake ends → bubble");
assert!(anim.bubble_active());
assert_eq!(anim.player_shake_offset(), 0, "no shake during the bubble");
for f in 1..FISHING_BUBBLE_FRAMES {
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::Bubble, "bubble frame {f}");
assert!(anim.bubble_active());
}
anim.tick();
assert_eq!(anim.phase(), FishingAnimPhase::Done, "bubble ends → result text");
assert!(anim.is_done());
assert!(!anim.bubble_active());
}
#[test]
fn fishing_anim_total_duration() {
let mut no_bite = FishingAnimState::new(Direction::Left, false);
let mut bite = FishingAnimState::new(Direction::Left, true);
for _ in 0..FISHING_ANIM_FRAMES {
no_bite.tick();
bite.tick();
}
assert!(no_bite.is_done(), "no-bite finishes at 10 + 100 = 110 frames");
assert!(bite.is_done(), "bite finishes at 10 + 100 + 30 + 60 = 200 frames");
let mut late = FishingAnimState::new(Direction::Down, true);
for _ in 0..FISHING_ANIM_FRAMES - 1 {
late.tick();
}
assert_eq!(late.phase(), FishingAnimPhase::Bubble, "frame 199 is still the bubble");
}
#[test]
fn fishing_anim_pose_and_rod_visibility_follow_phase_and_facing() {
let mut anim = FishingAnimState::new(Direction::Up, true);
assert!(!anim.pose_active());
assert!(!anim.rod_visible());
tick_n(&mut anim, FISHING_CAST_DELAY_FRAMES);
assert!(anim.pose_active(), "fishing pose while the rod is out");
assert!(anim.rod_visible());
assert_eq!(anim.facing(), Direction::Up);
let shake_bubble = FISHING_CAST_DELAY_FRAMES
+ FISHING_ROD_OUT_FRAMES
+ FISHING_SHAKE_ITERATIONS * FISHING_SHAKE_STEP_FRAMES;
tick_n(&mut anim, shake_bubble);
assert_eq!(anim.phase(), FishingAnimPhase::Bubble);
assert!(!anim.rod_visible(), "up-facing rod hidden under the bubble");
assert!(anim.pose_active());
tick_n(&mut anim, FISHING_BUBBLE_FRAMES);
assert!(anim.is_done());
assert!(!anim.pose_active(), "pose restored after the anim");
let mut left = FishingAnimState::new(Direction::Left, true);
tick_n(&mut left, shake_bubble);
assert_eq!(left.phase(), FishingAnimPhase::Bubble);
assert!(left.rod_visible());
}
#[test]
fn fishing_anim_rod_piece_offsets() {
assert_eq!(FishingAnimState::rod_piece(Direction::Down), (20, 35, 0, false));
assert_eq!(FishingAnimState::rod_piece(Direction::Up), (20, -12, 0, false));
assert_eq!(FishingAnimState::rod_piece(Direction::Left), (0, 16, 1, false));
assert_eq!(FishingAnimState::rod_piece(Direction::Right), (48, 16, 1, true));
}
#[test]
fn boulder_dust_inactive_constant_is_inert() {
let mut dust = BoulderDustState::inactive();
assert!(!dust.is_active());
dust.tick();
assert!(!dust.is_active());
}
#[test]
fn boulder_dust_base_offsets() {
for (facing, expected) in [
(Direction::Down, (8, 52)),
(Direction::Up, (8, -12)),
(Direction::Left, (-24, 20)),
(Direction::Right, (40, 20)),
] {
let dust = BoulderDustState::new(facing, 5, 5);
assert_eq!(dust.base_offset(), expected, "facing {:?}", facing);
}
}
#[test]
fn boulder_dust_drifts_against_the_push_direction() {
for (facing, expected) in [
(Direction::Down, (0, -1)),
(Direction::Up, (0, 1)),
(Direction::Left, (1, 0)),
(Direction::Right, (-1, 0)),
] {
let dust = BoulderDustState::new(facing, 5, 5);
assert_eq!(dust.drift_px(), expected, "facing {:?}", facing);
}
}
#[test]
fn boulder_dust_horizontal_push_moves_three_of_four_tiles() {
let dust = BoulderDustState::new(Direction::Left, 5, 5);
assert_eq!(dust.tile_drifts(), [(0, 0), (1, 0), (1, 0), (1, 0)]);
let dust = BoulderDustState::new(Direction::Down, 5, 5);
assert_eq!(dust.tile_drifts(), [(0, -1); 4], "vertical pushes move all tiles");
}
#[test]
fn boulder_dust_runs_8_steps_of_3_frames_then_ends() {
let mut dust = BoulderDustState::new(Direction::Down, 5, 5);
assert!(dust.is_active());
assert_eq!(dust.step(), 0);
for tick in 1..=24 {
dust.tick();
if tick == 24 {
assert!(!dust.is_active(), "tick 24 completes the animation");
continue;
}
assert!(dust.is_active(), "tick {}", tick);
if tick % 3 == 0 {
assert_eq!(dust.step(), tick / 3, "step advances every 3rd tick");
} else {
assert_eq!(dust.step(), (tick - 1) / 3, "tick {}", tick);
}
}
assert!(!dust.is_active(), "8 steps × 3 frames = 24 frames");
dust.tick();
assert!(!dust.is_active());
}
#[test]
fn boulder_dust_palette_flashes_every_step() {
let mut dust = BoulderDustState::new(Direction::Right, 5, 5);
let mut seen = Vec::new();
for _ in 0..24 {
seen.push(dust.palette_flipped());
dust.tick();
}
let expected: Vec<bool> = (0..8).map(|s| s % 2 == 1).flat_map(|f| [f, f, f]).collect();
assert_eq!(seen, expected, "palette toggles on each odd step");
}
#[test]
fn boulder_dust_keeps_its_world_anchor() {
let mut dust = BoulderDustState::new(Direction::Up, 12, 7);
assert_eq!(dust.anchor(), (12, 7));
for _ in 0..24 {
dust.tick();
}
assert_eq!(dust.anchor(), (12, 7), "anchor survives the animation");
}
#[test]
fn ship_departure_phase_boundaries() {
let mut dep = ShipDepartureState::new();
assert_eq!(dep.phase(), ShipDeparturePhase::InitialPause);
for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::WaterFill);
for _ in 0..SHIP_DEPARTURE_WATER_FILL_FRAMES {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::Scroll);
for _ in 0..SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
for _ in 0..SHIP_DEPARTURE_ERASE_FRAMES {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::Done);
assert!(dep.is_done());
assert_eq!(dep.frame(), SHIP_DEPARTURE_TOTAL_FRAMES);
assert_eq!(dep.tick(), None);
assert_eq!(dep.frame(), SHIP_DEPARTURE_TOTAL_FRAMES);
}
#[test]
fn ship_departure_horn_timing() {
let mut dep = ShipDepartureState::new();
let mut horns = Vec::new();
for _ in 0..SHIP_DEPARTURE_TOTAL_FRAMES {
if let Some(sfx) = dep.tick() {
horns.push((dep.frame(), sfx));
}
}
assert_eq!(
horns,
vec![
(
SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES,
ShipDepartureSfx::Horn
),
(
SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES
+ SHIP_DEPARTURE_WATER_FILL_FRAMES
+ SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES,
ShipDepartureSfx::Horn
),
]
);
}
#[test]
fn ship_departure_scroll_px_ramps() {
let mut dep = ShipDepartureState::new();
for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
dep.tick();
}
let mut seen = Vec::new();
for _ in 0..SHIP_DEPARTURE_ITERATION_FRAMES {
seen.push(dep.scroll_px());
dep.tick();
}
let expected: Vec<i32> = (0..16).flat_map(|d| std::iter::repeat(d + 1).take(8)).collect();
assert_eq!(seen, expected, "scroll ramps 1..=16 across iteration 0");
for _ in 0..(40 * 8 - SHIP_DEPARTURE_ITERATION_FRAMES) {
dep.tick();
}
assert_eq!(dep.scroll_iteration(), 2);
assert_eq!(dep.scroll_substep(), 40);
assert_eq!(dep.scroll_px(), 41);
for _ in 0..(SHIP_DEPARTURE_TOTAL_FRAMES - 1 - (123 + 40 * 8)) {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
assert_eq!(dep.scroll_px(), 128);
assert_eq!(dep.scroll_iteration(), SHIP_DEPARTURE_SCROLL_ITERATIONS - 1);
assert_eq!(dep.scroll_substep(), 127);
assert!(dep.ship_erased());
}
#[test]
fn ship_departure_puff_positions() {
let mut dep = ShipDepartureState::new();
for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
dep.tick();
}
for _ in 0..16 * 8 {
dep.tick();
}
assert_eq!(dep.puff_count(), 2);
assert_eq!(dep.puff_x_offset(0), 88 + 2 * (16 + 1));
assert_eq!(dep.puff_x_offset(1), 72 + 2 * (16 - 16 + 1));
for _ in 0..SHIP_DEPARTURE_ITERATION_FRAMES {
dep.tick();
}
assert_eq!(dep.puff_count(), 3);
assert_eq!(dep.puff_x_offset(0), 88 + 2 * (32 + 1));
assert_eq!(dep.puff_x_offset(1), 72 + 2 * (32 - 16 + 1));
assert_eq!(dep.puff_x_offset(2), 56 + 2 * (32 - 32 + 1));
for _ in 0..6 * SHIP_DEPARTURE_ITERATION_FRAMES {
dep.tick();
}
assert_eq!(dep.phase(), ShipDeparturePhase::Erase);
assert_eq!(dep.puff_count(), 8);
assert_eq!(dep.puff_x_offset(0), 88 + 2 * 128);
assert_eq!(dep.puff_x_offset(7), 88 - 16 * 7 + 2 * (127 - 16 * 7 + 1));
assert_eq!(dep.puff_screen_y(), 84);
}
#[test]
fn ship_departure_erase_flag_only_in_erase_phase() {
let mut dep = ShipDepartureState::new();
assert!(!dep.ship_erased());
for _ in 0..SHIP_DEPARTURE_INITIAL_PAUSE_FRAMES + SHIP_DEPARTURE_WATER_FILL_FRAMES {
dep.tick();
}
assert!(!dep.ship_erased(), "hull still visible during the scroll");
for _ in 0..SHIP_DEPARTURE_SCROLL_ITERATIONS * SHIP_DEPARTURE_ITERATION_FRAMES {
dep.tick();
}
assert!(dep.ship_erased(), "erase phase shows the ship as water");
}
}