diff --git a/src/engine_constants/npcs.rs b/src/engine_constants/npcs.rs index 219211f..eb7e5c6 100644 --- a/src/engine_constants/npcs.rs +++ b/src/engine_constants/npcs.rs @@ -457,9 +457,15 @@ pub struct NPCConsts { // pub n152_shutter_stuck: () // Defined in code + #[serde(default = "default_n153_gaudi")] + pub n153_gaudi: [Rect; 14], + #[serde(default = "default_n154_gaudi_dead")] pub n154_gaudi_dead: [Rect; 6], + #[serde(default = "default_n155_gaudi_flying")] + pub n155_gaudi_flying: [Rect; 8], + #[serde(default = "default_n156_gaudi_projectile")] pub n156_gaudi_projectile: [Rect; 3], @@ -2816,6 +2822,25 @@ fn default_n151_blue_robot_standing() -> [Rect; 4] { ] } +fn default_n153_gaudi() -> [Rect; 14] { + [ + Rect { left: 0, top: 0, right: 24, bottom: 24 }, // 0 0 // left + Rect { left: 24, top: 0, right: 48, bottom: 24 }, // 1 1 + Rect { left: 48, top: 0, right: 72, bottom: 24 }, // 2 2 + Rect { left: 0, top: 0, right: 24, bottom: 24 }, // 3 3 + Rect { left: 72, top: 0, right: 96, bottom: 24 }, // 4 4 + Rect { left: 0, top: 0, right: 24, bottom: 24 }, // 5 5 + Rect { left: 96, top: 48, right: 120, bottom: 72 }, // 6 20 + Rect { left: 0, top: 24, right: 24, bottom: 48 }, // 0 0 // right + Rect { left: 24, top: 24, right: 48, bottom: 48 }, // 1 1 + Rect { left: 48, top: 24, right: 72, bottom: 48 }, // 2 2 + Rect { left: 0, top: 24, right: 24, bottom: 48 }, // 3 3 + Rect { left: 72, top: 24, right: 96, bottom: 48 }, // 4 4 + Rect { left: 0, top: 24, right: 24, bottom: 48 }, // 5 5 + Rect { left: 96, top: 72, right: 120, bottom: 96 }, // 6 20 + ] +} + fn default_n154_gaudi_dead() -> [Rect; 6] { [ Rect { left: 168, top: 24, right: 192, bottom: 48 }, @@ -2827,6 +2852,19 @@ fn default_n154_gaudi_dead() -> [Rect; 6] { ] } +fn default_n155_gaudi_flying() -> [Rect; 8] { + [ + Rect { left: 0, top: 48, right: 24, bottom: 72 }, // 0 14 // left + Rect { left: 24, top: 48, right: 48, bottom: 72 }, // 1 15 + Rect { left: 288, top: 0, right: 312, bottom: 24 }, // 2 18 + Rect { left: 24, top: 48, right: 48, bottom: 72 }, // 3 19 + Rect { left: 0, top: 72, right: 24, bottom: 96 }, // 0 14 // right + Rect { left: 24, top: 72, right: 48, bottom: 96 }, // 1 15 + Rect { left: 288, top: 24, right: 312, bottom: 48 }, // 2 18 + Rect { left: 24, top: 72, right: 48, bottom: 96 }, // 3 19 + ] +} + fn default_n156_gaudi_projectile() -> [Rect; 3] { [ Rect { left: 96, top: 112, right: 112, bottom: 128 }, diff --git a/src/npc/ai/characters.rs b/src/npc/ai/characters.rs index 0b553a2..a9ea7b8 100644 --- a/src/npc/ai/characters.rs +++ b/src/npc/ai/characters.rs @@ -1,7 +1,8 @@ -use crate::framework::error::GameResult; use num_traits::{abs, clamp}; use crate::common::Direction; +use crate::framework::error::GameResult; +use crate::npc::list::NPCList; use crate::npc::NPC; use crate::player::Player; use crate::rng::RNG; @@ -138,7 +139,6 @@ impl NPC { self.vel_x = 0x200; } - if self.action_counter != 0 && self.flags.hit_bottom_wall() { self.action_num = 5; } @@ -198,7 +198,6 @@ impl NPC { _ => {} } - if self.action_num < 30 || self.action_num >= 40 { self.vel_y += 0x40; self.vel_x = clamp(self.vel_x, -0x400, 0x400); @@ -347,6 +346,33 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n145_king_sword(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + if self.action_num == 0 { + let parent = self.get_parent_ref_mut(npc_list); + if let Some(parent) = parent { + if parent.action_counter2 != 0 { + if parent.direction != Direction::Left { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + } else if parent.direction != Direction::Left { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + self.x = if self.direction != Direction::Left { parent.x + 5120 } else { parent.x - 5120 }; + self.y = parent.y; + } + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 1 }; + + self.anim_rect = state.constants.npc.n145_king_sword[self.anim_num as usize + dir_offset]; + + Ok(()) + } + pub(crate) fn tick_n151_blue_robot_standing(&mut self, state: &mut SharedGameState) -> GameResult { match self.action_num { 0 | 1 => { diff --git a/src/npc/ai/curly.rs b/src/npc/ai/curly.rs index 5a16b75..2e57ca5 100644 --- a/src/npc/ai/curly.rs +++ b/src/npc/ai/curly.rs @@ -1,5 +1,6 @@ use num_traits::{abs, clamp}; +use crate::caret::CaretType; use crate::common::Direction; use crate::framework::error::GameResult; use crate::npc::list::NPCList; @@ -8,7 +9,6 @@ use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; use crate::weapon::bullet::BulletManager; -use crate::caret::CaretType; impl NPC { pub(crate) fn tick_n117_curly( @@ -321,4 +321,39 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n165_curly_collapsed( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.y += 0x1400; + } + + let player = self.get_closest_player_mut(players); + self.anim_num = if self.direction == Direction::Right { + if player.x > self.x - 0x4000 + && player.x < self.x + 0x4000 + && player.y > self.y - 0x2000 + && player.y < self.y + 0x2000 + { + 2 + } else { + 1 + } + } else { + 0 + }; + } + _ => {} + } + + self.anim_rect = state.constants.npc.n165_curly_collapsed[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/doctor.rs b/src/npc/ai/doctor.rs new file mode 100644 index 0000000..aa60106 --- /dev/null +++ b/src/npc/ai/doctor.rs @@ -0,0 +1,114 @@ +use crate::common::Direction; +use crate::framework::error::GameResult; +use crate::npc::NPC; +use crate::shared_game_state::SharedGameState; + +impl NPC { + pub(crate) fn tick_n139_doctor(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.vel_x = 0; + self.vel_y = 0; + self.y = self.y + -0x1000; + } + + if !self.flags.hit_bottom_wall() { + self.anim_num = 2; + } else { + self.anim_num = 0; + } + + self.vel_y = self.vel_y + 0x40; + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 0xb; + self.anim_num = 1; + self.anim_counter = 0; + self.action_counter3 = 0; + } + + self.anim_counter = self.anim_counter + 1; + if 6 < self.anim_counter { + self.anim_counter = 0; + self.anim_num = self.anim_num + 1; + } + + if 1 < self.anim_num { + self.anim_num = 0; + self.action_counter3 = self.action_counter3 + 1; + } + + if 8 < self.action_counter3 { + self.anim_num = 0; + self.action_num = 1; + } + } + 0x14 | 0x15 => { + if self.action_num == 0x14 { + self.action_num = 0x15; + self.action_counter = 0; + self.anim_num = 2; + self.target_y = self.y + -0x4000; + } + + if self.y < self.target_y { + self.vel_y = self.vel_y + 0x20; + } else { + self.vel_y = self.vel_y + -0x20; + } + + self.vel_y = self.vel_y.clamp(-0x200, 0x200); + } + 0x1e | 0x1f => { + if self.action_num == 0x1e { + self.action_num = 0x1f; + self.vel_x = 0; + self.vel_y = 0; + self.action_counter = (self.anim_rect.bottom - self.anim_rect.top) * 2; + state.sound_manager.play_sfx(0x1d); + } + + self.action_counter = self.action_counter.saturating_sub(1); + self.anim_num = 0; + + if self.action_counter == 0 { + self.cond.set_alive(false); + } + } + 0x28 | 0x29 => { + if self.action_num == 0x28 { + self.action_num = 0x29; + self.action_counter = 0; + self.vel_x = 0; + self.vel_y = 0; + state.sound_manager.play_sfx(0x1d); + } + self.anim_num = 2; + self.action_counter = self.action_counter + 1; + if 0x3f < self.action_counter { + self.action_num = 0x14; + } + } + _ => {} + } + + self.x += self.vel_x; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + + self.anim_rect = state.constants.npc.n139_doctor[self.anim_num as usize + dir_offset]; + + if self.action_num == 31 || self.action_num == 41 { + self.anim_rect.bottom = self.action_counter / 2 + self.anim_rect.top; + if ((self.action_counter / 2) & 1) != 0 { + self.anim_rect.left += 1; + } + } + + Ok(()) + } +} diff --git a/src/npc/ai/maze.rs b/src/npc/ai/maze.rs index c9ec0a2..b3b241a 100644 --- a/src/npc/ai/maze.rs +++ b/src/npc/ai/maze.rs @@ -1,12 +1,341 @@ use crate::caret::CaretType; -use crate::common::Direction; +use crate::common::{Direction, CDEG_RAD}; use crate::framework::error::GameResult; +use crate::npc::list::NPCList; use crate::npc::NPC; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; impl NPC { + pub(crate) fn tick_n147_critter_purple( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.y += 0x600; + self.action_num = 1; + } + + let player = self.get_closest_player_mut(players); + if self.action_counter <= 7 + || self.x - 0xC000 >= player.x + || self.x + 0xC000 <= player.x + || self.y - 0xC000 >= player.y + || self.y + 0x4000 <= player.y + { + if self.action_counter <= 7 { + self.action_counter += 1; + } + self.anim_num = 0; + } else { + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + self.anim_num = 1; + } + if self.shock != 0 { + self.action_num = 2; + self.anim_num = 0; + self.action_counter = 0; + } + if self.action_counter > 7 + && self.x - 0x6000 < player.x + && self.x + 0x6000 > player.x + && self.y - 0xC000 < player.y + && self.y + 0x4000 > player.y + { + self.action_num = 2; + self.anim_num = 0; + self.action_counter = 0; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 3; + self.anim_num = 2; + self.vel_y = -0x5FF; + state.sound_manager.play_sfx(30); + + let player = self.get_closest_player_mut(players); + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + } + } + 3 => { + if self.vel_y > 0x100 { + self.target_y = self.y; + self.action_num = 4; + self.anim_num = 3; + self.action_counter = 0; + self.action_counter = 0; + } + } + 4 => { + let player = self.get_closest_player_mut(players); + if self.x >= player.x { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + self.action_counter += 1; + if (self.flags.hit_left_wall() || self.flags.hit_right_wall() || self.flags.hit_top_wall()) + || self.action_counter > 60 + { + self.damage = 3; + self.action_num = 5; + self.anim_num = 2; + } else { + if self.action_counter % 4 == 1 { + state.sound_manager.play_sfx(109); + } + if self.flags.hit_bottom_wall() { + self.vel_y = -0x200; + } + if self.action_counter % 30 == 6 { + let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64) + + (self.rng.range(-6..6) as f64 * CDEG_RAD); + + let mut npc = NPC::create(148, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + npc.vel_x = (angle.cos() * -1536.0) as i32; + npc.vel_y = (angle.sin() * -1536.0) as i32; + + let _ = npc_list.spawn(0x100, npc); + state.sound_manager.play_sfx(39); + } + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num > 5 { + self.anim_num = 3; + } + } + } + 5 => { + if self.flags.hit_bottom_wall() { + self.damage = 2; + self.vel_x = 0; + self.action_counter = 0; + self.anim_num = 0; + self.action_num = 1; + state.sound_manager.play_sfx(23); + } + } + _ => {} + } + + if self.action_num == 4 { + self.vel_y = if self.y <= self.target_y { self.vel_y + 0x10 } else { self.vel_y - 0x10 }; + + self.vel_x = self.vel_x.clamp(-0x200, 0x200); + self.vel_y = self.vel_y.clamp(-0x200, 0x200); + } else { + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + } + + self.x += self.vel_x; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; + + self.anim_rect = state.constants.npc.n147_critter_purple[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n148_critter_purple_projectile(&mut self, state: &mut SharedGameState) -> GameResult { + if self.flags.0 != 0 { + state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left); + self.cond.set_alive(false); + } + self.y += self.vel_y; + self.x += self.vel_x; + + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + + self.anim_rect = state.constants.npc.n148_critter_purple_projectile[self.anim_num as usize]; + + self.action_counter3 += 1; + if self.action_counter3 > 300 { + state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left); + self.cond.set_alive(false); + } + + Ok(()) + } + + pub(crate) fn tick_n153_gaudi(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + + if !(self.x <= player.x + 0x28000 + && self.x >= player.x - 0x28000 + && self.y <= player.y + 0x1E000 + && self.y >= player.y - 0x1E000) + { + return Ok(()); + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.vel_x = 0; + self.anim_num = 0; + self.y += 0x600; + } + if self.rng.range(0..100) == 1 { + self.action_num = 2; + self.anim_num = 1; + self.action_counter = 0; + } + if self.rng.range(0..100) == 1 { + if self.direction != Direction::Left { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + } + if self.rng.range(0..100) == 1 { + self.action_num = 10; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 20 { + self.action_num = 1; + self.anim_num = 0; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = self.rng.range(25..100) as u16; + self.anim_num = 2; + self.anim_counter = 0; + } + self.anim_counter += 1; + if self.anim_counter > 3 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num > 5 { + self.anim_num = 2; + } + if self.direction != Direction::Left { + self.vel_x = 0x200; + } else { + self.vel_x = -0x200; + } + + if self.action_counter != 0 { + self.action_counter -= 1; + } else { + self.action_num = 1; + self.anim_num = 0; + self.vel_x = 0; + } + if self.direction != Direction::Left || !self.flags.hit_left_wall() { + if self.direction == Direction::Right && self.flags.hit_right_wall() { + self.anim_num = 2; + self.vel_y = -0x5FF; + self.action_num = 20; + if !player.cond.hidden() { + state.sound_manager.play_sfx(30); + } + } + } else { + self.anim_num = 2; + self.vel_y = -0x5FF; + self.action_num = 20; + if !player.cond.hidden() { + state.sound_manager.play_sfx(30); + } + } + } + 20 => { + if self.direction != Direction::Left || !self.flags.hit_left_wall() { + if self.direction == Direction::Right && self.flags.hit_right_wall() { + self.action_counter3 += 1; + } else { + self.action_counter3 = 0; + } + } else { + self.action_counter3 += 1; + } + + if self.action_counter3 > 10 { + if self.direction != Direction::Left { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + } + if self.direction != Direction::Left { + self.vel_x = 0x100; + } else { + self.vel_x = -0x100; + } + if self.flags.hit_bottom_wall() { + self.action_num = 21; + self.anim_num = 6; + self.action_counter = 0; + self.vel_x = 0; + + if !player.cond.hidden() { + state.sound_manager.play_sfx(23); + } + } + } + 21 => { + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => {} + } + + self.vel_y += 0x40; + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + self.x += self.vel_x; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 7 }; + + self.anim_rect = state.constants.npc.n153_gaudi[self.anim_num as usize + dir_offset]; + + if self.life <= 985 { + self.npc_type = 154; + self.action_num = 0; + } + + Ok(()) + } + pub(crate) fn tick_n154_gaudi_dead(&mut self, state: &mut SharedGameState) -> GameResult { match self.action_num { 0 => { @@ -50,25 +379,125 @@ impl NPC { self.x += self.vel_x; self.y += self.vel_y; - let dir_offset = if self.direction == Direction::Left { - 0 - } else { - 3 - }; + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; self.anim_rect = state.constants.npc.n154_gaudi_dead[self.anim_num as usize + dir_offset]; Ok(()) } + pub(crate) fn tick_n155_gaudi_flying( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + let player = self.get_closest_player_mut(players); + + if self.x > player.x + 0x28000 + || self.x < player.x - 0x28000 + || self.y > player.y + 0x1E000 + || self.y < player.y - 0x1E000 + { + return Ok(()); + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + let deg = self.rng.range(0..255) as f64 * CDEG_RAD; + self.vel_y = (deg.cos() * -512.0) as i32; + self.target_x = self.x + 8 * ((deg + 64.0 * CDEG_RAD).cos() * -512.0) as i32; + + let deg = self.rng.range(0..255) as f64 * CDEG_RAD; + self.vel_y = (deg.sin() * -512.0) as i32; + self.target_y = self.y + 8 * ((deg + 64.0 * CDEG_RAD).sin() * -512.0) as i32; + + self.action_num = 1; + self.action_counter3 = 120; + self.action_counter = self.rng.range(70..150) as u16; + self.anim_num = 0; + } + + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + if self.action_counter != 0 { + self.action_counter -= 1; + } else { + self.action_num = 2; + self.anim_num = 2; + } + } + 2 => { + self.anim_num += 1; + if self.anim_num > 3 { + self.anim_num = 2; + } + self.action_counter += 1; + if self.action_counter > 30 { + let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64) + + (self.rng.range(-6..6) as f64 * CDEG_RAD); + + let mut npc = NPC::create(156, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + npc.vel_x = (angle.cos() * -1536.0) as i32; + npc.vel_y = (angle.sin() * -1536.0) as i32; + + let _ = npc_list.spawn(0x100, npc); + + if !player.cond.hidden() { + state.sound_manager.play_sfx(39); + } + self.action_num = 1; + self.action_counter = self.rng.range(70..150) as u16; + self.anim_num = 0; + self.anim_counter = 0; + } + } + _ => {} + } + if player.x >= self.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + + if self.target_x < self.x { + self.vel_x -= 0x10; + } + if self.target_x > self.x { + self.vel_x += 0x10; + } + if self.target_y < self.y { + self.vel_y -= 0x10; + } + if self.target_y > self.y { + self.vel_y += 0x10; + } + + self.vel_x = self.vel_x.clamp(-0x200, 0x200); + self.vel_y = self.vel_y.clamp(-0x200, 0x200); + + self.x += self.vel_x; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + self.anim_rect = state.constants.npc.n155_gaudi_flying[self.anim_num as usize + dir_offset]; + + if self.life <= 985 { + self.npc_type = 154; + self.action_num = 0; + } + Ok(()) + } + pub(crate) fn tick_n156_gaudi_projectile(&mut self, state: &mut SharedGameState) -> GameResult { if self.action_counter > 300 || (self.flags.0 & 0xff) != 0 { self.cond.set_alive(false); - state.create_caret( - self.x, - self.y, - CaretType::ProjectileDissipation, - Direction::Left, - ); + state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left); } self.x += self.vel_x; @@ -81,6 +510,165 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n160_puu_black( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.npc_flags.set_solid_soft(false); + self.action_num = 1; + } + + let player = self.get_closest_player_mut(players); + + if self.x >= player.x { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + self.vel_y = 2560; + if self.y > 0xFFFF { + self.npc_flags.set_ignore_solidity(false); + self.action_num = 2; + } else { + self.action_counter3 += 1; + } + } + 2 => { + self.vel_y = 0xA00; + if self.flags.hit_bottom_wall() { + npc_list.kill_npcs_by_type(161, true, state); + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.direction = Direction::Left; + + for _ in 0..4 { + npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200; + npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200; + npc.vel_x = self.rng.range(-0x155..0x155) as i32; + npc.vel_y = self.rng.range(-0x600..0) as i32; + + let _ = npc_list.spawn(0x100, npc.clone()); + } + + self.action_num = 3; + self.action_counter = 0; + state.quake_counter = 30; + + state.sound_manager.play_sfx(26); + state.sound_manager.play_sfx(72); + } + + let player = self.get_closest_player_mut(players); + if self.y < player.y && player.flags.hit_bottom_wall() { + self.damage = 20; + } else { + self.damage = 0; + } + } + 3 => { + self.damage = 0; + self.action_counter += 1; + if self.action_counter > 24 { + self.action_num = 4; + self.action_counter3 = 0; + self.action_counter2 = 0; + } + } + 4 => { + state.npc_super_pos = (self.x, self.y); + + if (self.shock & 1) != 0 { + let mut npc = NPC::create(161, &state.npc_table); + npc.cond.set_alive(true); + npc.direction = Direction::Left; + + npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200; + npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200; + npc.vel_x = self.rng.range(-0x600..0x600) as i32; + npc.vel_y = self.rng.range(-0x600..0x600) as i32; + + let _ = npc_list.spawn(0x100, npc); + + self.action_counter3 += 1; + if self.action_counter3 > 30 { + self.action_counter3 = 0; + self.action_num = 5; + self.vel_y = -3072; + self.npc_flags.set_ignore_solidity(true); + } + } + } + 5 => { + state.npc_super_pos = (self.x, self.y); + + self.action_counter3 += 1; + if self.action_counter3 > 60 { + self.action_counter3 = 0; + self.action_num = 6; + } + } + 6 => { + let player = self.get_closest_player_mut(players); + state.npc_super_pos = (player.x, 3276800); + self.action_counter3 += 1; + if self.action_counter3 > 110 { + self.action_counter3 = 10; + self.x = player.x; + self.y = 0; + self.vel_y = 1535; + self.action_num = 1; + } + } + _ => {} + } + self.y += self.vel_y; + + self.anim_num = match self.action_num { + 0 | 1 | 2 | 5 | 6 => 3, + 3 => 2, + 4 => 0, + _ => 0, + }; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + self.anim_rect = state.constants.npc.n160_puu_black[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n161_puu_black_projectile(&mut self, state: &mut SharedGameState) -> GameResult { + self.exp = 0; + + self.vel_x = if self.x >= state.npc_super_pos.0 { self.vel_x - 64 } else { self.vel_x + 64 }; + self.vel_y = if self.y >= state.npc_super_pos.1 { self.vel_y - 64 } else { self.vel_y + 64 }; + + self.vel_x = self.vel_x.clamp(-0x11fd, 0x11fd); + self.vel_y = self.vel_y.clamp(-0x11fd, 0x11fd); + + if self.life <= 99 { + self.npc_flags.set_shootable(false); + self.npc_flags.set_invulnerable(false); + self.damage = 0; + self.anim_num = 2; + } + + self.x += self.vel_x; + self.y += self.vel_y; + if self.anim_num <= 1 { + self.anim_num = if self.rng.range(0..10) != 2 { 1 } else { 0 }; + } + + self.anim_rect = state.constants.npc.n161_puu_black_projectile[self.anim_num as usize]; + + Ok(()) + } + pub(crate) fn tick_n166_chaba(&mut self, state: &mut SharedGameState) -> GameResult { match self.action_num { 0 | 1 => { @@ -111,6 +699,164 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n162_puu_black_dead( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + npc_list.kill_npcs_by_type(161, true, state); + state.sound_manager.play_sfx(72); + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + + for _ in 0..10 { + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y + self.rng.range(-12..12) * 0x200; + npc.vel_x = self.rng.range(-0x600..0x600); + npc.vel_y = self.rng.range(-0x600..0x600); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + + let player = self.get_closest_player_mut(players); + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + + self.anim_num = if self.direction != Direction::Left { 1 } else { 0 }; + self.action_counter3 = 0; + self.action_num = 1; + } + self.action_counter3 += 1; + if (self.action_counter3 & 3) == 0 { + let mut npc = NPC::create(161, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y + self.rng.range(-12..12) * 0x200; + + let _ = npc_list.spawn(0x100, npc); + } + + if self.action_counter3 > 160 { + self.action_counter3 = 0; + self.action_num = 2; + self.target_y = self.y; + } + } + 2 => { + state.quake_counter = 2; + self.action_counter3 += 1; + if self.action_counter3 > 240 { + self.anim_num = 2; + self.action_counter3 = 0; + self.action_num = 3; + } else { + self.anim_num = if self.direction != Direction::Left { 1 } else { 0 }; + + self.anim_rect.top += self.action_counter3 / 8; + self.y = ((self.action_counter3 as i32 / 8) * 0x200) + self.target_y; + self.anim_rect.left -= self.action_counter3 / 2 % 2; + } + if self.action_counter3 % 3 == 2 { + let mut npc = NPC::create(161, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y - 6144; + npc.vel_x = self.rng.range(-512..512); + npc.vel_y = 0x100; + + let _ = npc_list.spawn(0x100, npc); + } + if self.action_counter3 % 4 == 2 { + state.sound_manager.play_sfx(21); + } + } + 3 => { + self.action_counter3 += 1; + if self.action_counter3 > 59 { + npc_list.kill_npcs_by_type(161, true, state); + self.cond.set_alive(false); + } + } + _ => {} + } + + state.npc_super_pos = (self.x, -512000); + + self.anim_rect = state.constants.npc.n162_puu_black_dead[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n163_dr_gero(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + } + + if self.rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => {} + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + self.anim_rect = state.constants.npc.n163_dr_gero[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n164_nurse_hasumi(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + } + + if self.rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => {} + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + self.anim_rect = state.constants.npc.n164_nurse_hasumi[self.anim_num as usize + dir_offset]; + + Ok(()) + } + pub(crate) fn tick_n173_gaudi_armored( &mut self, state: &mut SharedGameState, diff --git a/src/npc/ai/misc.rs b/src/npc/ai/misc.rs index 506fbe2..c296123 100644 --- a/src/npc/ai/misc.rs +++ b/src/npc/ai/misc.rs @@ -2,9 +2,10 @@ use num_traits::{abs, clamp}; use crate::caret::CaretType; use crate::common::{Direction, Rect}; +use crate::components::flash::Flash; use crate::framework::error::GameResult; -use crate::npc::list::NPCList; use crate::npc::{NPC, NPCLayer}; +use crate::npc::list::NPCList; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; @@ -156,7 +157,6 @@ impl NPC { let _ = npc_list.spawn(0x100, npc.clone()); } } - } self.anim_num = 0; @@ -1083,6 +1083,50 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n146_lighting( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + flash: &mut Flash, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + if self.direction == Direction::Right { + flash.set_blink(); + } + } + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 2; + state.sound_manager.play_sfx(101); + } + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num == 2 { + self.damage = 10; + } + if self.anim_num > 4 { + npc_list.create_death_smoke(self.x, self.y, 4096, 8, state, &self.rng); + self.cond.set_alive(false); + return Ok(()); + } + } + _ => {} + } + + self.anim_rect = state.constants.npc.n146_lighting[self.anim_num as usize]; + + Ok(()) + } + pub(crate) fn tick_n149_horizontal_moving_block( &mut self, state: &mut SharedGameState, diff --git a/src/npc/ai/mod.rs b/src/npc/ai/mod.rs index 91cac6d..47819b8 100644 --- a/src/npc/ai/mod.rs +++ b/src/npc/ai/mod.rs @@ -3,6 +3,7 @@ pub mod booster; pub mod chaco; pub mod characters; pub mod curly; +pub mod doctor; pub mod egg_corridor; pub mod first_cave; pub mod grasstown; diff --git a/src/npc/ai/sand_zone.rs b/src/npc/ai/sand_zone.rs index 2b68d21..228f035 100644 --- a/src/npc/ai/sand_zone.rs +++ b/src/npc/ai/sand_zone.rs @@ -8,6 +8,7 @@ use crate::npc::NPC; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; +use crate::weapon::bullet::BulletManager; impl NPC { pub(crate) fn tick_n044_polish(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { @@ -667,7 +668,14 @@ impl NPC { if self.action_counter > 50 { state.sound_manager.play_sfx(25); self.vanish(state); - npc_list.create_death_smoke(self.x, self.y, self.display_bounds.right as usize, 8, state, &self.rng); + npc_list.create_death_smoke( + self.x, + self.y, + self.display_bounds.right as usize, + 8, + state, + &self.rng, + ); } } _ => {} @@ -987,7 +995,7 @@ impl NPC { self.animate(2, 4, 5); } else { self.anim_num = 5; - self.anim_counter =0; + self.anim_counter = 0; } if self.vel_x < 0 && self.flags.hit_left_wall() { @@ -1030,7 +1038,6 @@ impl NPC { self.x += self.vel_x; self.y += self.vel_y; - let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; self.anim_rect = state.constants.npc.n126_puppy_running[self.anim_num as usize + dir_offset]; @@ -1038,6 +1045,78 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n130_puppy_sitting( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + self.npc_flags.set_interactable(true); + } + + if self.rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + let player = self.get_closest_player_mut(players); + if self.x - 0x8000 < player.x + && self.x + 0x8000 > player.x + && self.y - 0x4000 < player.y + && self.y + 0x2000 > player.y + { + self.anim_counter += 1; + if self.anim_counter > 3 { + self.anim_counter = 0; + self.anim_num += 1; + } + + if self.anim_num > 3 { + self.anim_num = 2; + } + } + + if self.x - 0xC000 < player.x + && self.x + 0xC000 > player.x + && self.y - 0x4000 < player.y + && self.y + 0x2000 > player.y + { + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + } + } + _ => {} + } + + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + } + + self.vel_y += 0x40; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + self.x += self.vel_x; + self.y += self.vel_y; + + let anim = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n130_puppy_sitting[anim]; + + Ok(()) + } + pub(crate) fn tick_n131_puppy_sleeping(&mut self, state: &mut SharedGameState) -> GameResult { self.action_counter += 1; if self.action_counter > 100 { @@ -1149,6 +1228,38 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n133_jenka(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + } + + if self.rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => {} + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n133_jenka[self.anim_num as usize + dir_offset]; + + Ok(()) + } + pub(crate) fn tick_n136_puppy_carried( &mut self, state: &mut SharedGameState, @@ -1199,6 +1310,242 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n134_armadillo( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + bullet_manager: &BulletManager, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 2; + self.npc_flags.set_shootable(false); + self.npc_flags.set_invulnerable(true); + } + + let player = self.get_closest_player_mut(players); + if player.x > self.x - 0x28000 + && player.x < self.x + 0x28000 + && player.y > self.y - 0x14000 + && player.y < self.y + 0x8000 + { + self.action_num = 10; + self.npc_flags.set_shootable(true); + self.npc_flags.set_invulnerable(false); + } + } + 10 => { + self.anim_counter += 1; + if self.anim_counter > 4 { + self.anim_counter = 0; + self.anim_num += 1; + } + if (self.anim_num > 1) { + self.anim_num = 0; + } + if (self.direction == Direction::Left && self.flags.hit_left_wall()) { + self.direction = Direction::Right; + } + if (self.direction == Direction::Right && self.flags.hit_right_wall()) { + self.direction = Direction::Left; + } + self.x += self.direction.vector_x() * 0x100; + + if bullet_manager.count_bullets_type_idx_all(6) != 0 { + self.action_num = 20; + self.action_counter = 0; + self.anim_num = 2; + self.npc_flags.set_shootable(false); + self.npc_flags.set_invulnerable(true); + } + } + 20 => { + self.action_counter += 1; + if self.action_counter > 100 { + self.action_num = 10; + self.anim_num = 0; + self.anim_counter = 0; + self.npc_flags.set_shootable(true); + self.npc_flags.set_invulnerable(false); + } + } + _ => {} + } + self.vel_y += 0x40; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + + self.anim_rect = state.constants.npc.n134_armadillo[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n135_skeleton( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + let player = self.get_closest_player_mut(players); + + if player.x < self.x - 0x2C000 + || player.x > self.x + 0x2C000 + || player.y < self.y - 0x14000 + || player.y > self.y + 0x8000 + { + self.action_num = 0; + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.vel_x = 0; + } + + if player.x > self.x - 0x28000 + && player.x < self.x + 0x28000 + && player.y > self.y - 0x14000 + && player.y < self.y + 0x8000 + { + self.action_num = 10; + } + + if self.flags.hit_bottom_wall() { + self.anim_num = 0; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.vel_x = 0; + self.action_num = 11; + self.action_counter = 0; + self.anim_num = 0; + } + + self.action_counter += 1; + if self.action_counter > 4 && self.flags.hit_bottom_wall() { + self.action_num = 20; + self.anim_num = 1; + self.action_counter2 = 0; + self.vel_y = -0x200 * self.rng.range(1..3); + + if self.shock != 0 { + self.vel_x = if self.x >= player.x { self.vel_x + 0x100 } else { self.vel_x - 0x100 }; + } else { + self.vel_x = if self.x >= player.x { self.vel_x - 0x100 } else { self.vel_x + 0x100 } + } + } + } + 20 => { + if self.vel_y > 0 && self.action_counter2 == 0 { + self.action_counter2 += 1; + + let angle = f64::atan2((self.y + 0x800 - player.y) as f64, (self.x - player.x) as f64); + + let mut npc = NPC::create(50, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + npc.vel_x = (angle.cos() * -1024.0) as i32; + npc.vel_y = (angle.sin() * -1024.0) as i32; + + let _ = npc_list.spawn(0x180, npc); + state.sound_manager.play_sfx(39); + } + if self.flags.hit_bottom_wall() { + self.action_num = 10; + self.anim_num = 0; + } + } + _ => {} + } + + if self.action_num > 9 { + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + } + self.vel_y += 0x33; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.vel_x = clamp(self.vel_x, -0x5ff, 0x5ff); + self.y += self.vel_y; + self.x += self.vel_x; + + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n135_skeleton[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n138_large_door(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num != 1 { + if self.action_num > 1 { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 1; + self.action_counter = 0; + self.npc_flags.set_ignore_solidity(true); + } else if self.action_num != 11 { + return Ok(()); + } + + self.action_counter += 1; + if (self.action_counter & 7) == 0 { + state.sound_manager.play_sfx(26); + } + + if self.direction != Direction::Left { + self.x = ((self.action_counter as i32 / 8) * 0x200) + self.target_x; + self.anim_rect.left = 112; + self.anim_rect.top = 112; + self.anim_rect.right = 128; + self.anim_rect.bottom = 136; + self.anim_rect.right -= self.action_counter / 8; + } else { + self.anim_rect.left = 96; + self.anim_rect.top = 112; + self.anim_rect.right = 112; + self.anim_rect.bottom = 136; + self.anim_rect.left += self.action_counter / 8; + } + if self.action_counter == 104 { + self.cond.set_alive(false); + } + } else if self.action_num == 0 { + self.action_num = 1; + if self.direction != Direction::Left { + self.anim_rect.left = 112; + self.anim_rect.top = 112; + self.anim_rect.right = 128; + self.anim_rect.bottom = 136; + self.x -= 4096; + } else { + self.anim_rect.left = 96; + self.anim_rect.top = 112; + self.anim_rect.right = 112; + self.anim_rect.bottom = 136; + self.x += 4096; + } + self.target_x = self.x; + } + } + Ok(()) + } + pub(crate) fn tick_n143_jenka_collapsed(&mut self, state: &mut SharedGameState) -> GameResult { let anim = if self.direction == Direction::Left { 0 } else { 1 }; diff --git a/src/npc/ai/toroko.rs b/src/npc/ai/toroko.rs index 71d6e41..e5d04cc 100644 --- a/src/npc/ai/toroko.rs +++ b/src/npc/ai/toroko.rs @@ -1,10 +1,12 @@ -use crate::framework::error::GameResult; - +use crate::caret::CaretType; use crate::common::Direction; +use crate::framework::error::GameResult; +use crate::npc::list::NPCList; use crate::npc::NPC; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; +use crate::weapon::bullet::BulletManager; impl NPC { pub(crate) fn tick_n060_toroko(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { @@ -24,8 +26,11 @@ impl NPC { } let player = self.get_closest_player_mut(players); - if (self.x - (16 * 0x200) < player.x) && (self.x + (16 * 0x200) > player.x) - && (self.y - (16 * 0x200) < player.y) && (self.y + (16 * 0x200) > player.y) { + if (self.x - (16 * 0x200) < player.x) + && (self.x + (16 * 0x200) > player.x) + && (self.y - (16 * 0x200) < player.y) + && (self.y + (16 * 0x200) > player.y) + { if self.x > player.x { self.direction = Direction::Left; } else { @@ -237,7 +242,6 @@ impl NPC { 4 => { self.vel_x = 0x100 * self.direction.vector_x(); - self.action_counter += 1; if self.action_counter != 0 && self.flags.hit_bottom_wall() { self.action_num = 5; @@ -261,7 +265,6 @@ impl NPC { self.x += self.vel_x; self.y += self.vel_y; - if self.direction == Direction::Left { self.anim_rect = state.constants.npc.n063_toroko_stick[self.anim_num as usize]; } else { @@ -270,4 +273,560 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n140_toroko_frenzied( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + bullet_manager: &BulletManager, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 9; + self.action_counter = 0; + self.npc_flags.set_interactable(false); + } + + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 8; + } + } + 2 => { + self.anim_num += 1; + if self.anim_num > 10 { + self.anim_num = 9; + } + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 3; + self.action_counter = 0; + self.anim_num = 0; + } + } + 3 => { + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 10; + self.npc_flags.set_shootable(true); + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 0; + self.anim_counter = 0; + self.action_counter = self.rng.range(20..130) as u16; + self.vel_x = 0; + } + + let player = self.get_closest_player_mut(players); + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + self.anim_counter += 1; + if self.anim_counter > 4 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num > 1 { + self.anim_num = 0; + } + if bullet_manager.count_bullets_type_idx_all(6) != 0 || bullet_manager.count_bullets_type_idx_all(3) > 3 + { + self.action_num = 20; + } + if self.action_counter != 0 { + self.action_counter -= 1; + } else if (self.rng.range(0..99) & 1) != 0 { + self.action_num = 20; + } else { + self.action_num = 50; + } + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.anim_num = 2; + self.action_counter = 0; + } + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 22; + self.action_counter = 0; + self.anim_num = 3; + self.vel_y = -0x5ff; + if self.direction != Direction::Left { + self.vel_x = 0x200; + } else { + self.vel_x = -0x200; + } + } + } + 22 => { + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 23; + self.action_counter = 0; + self.anim_num = 6; + + let mut npc = NPC::create(141, &state.npc_table); + npc.cond.set_alive(true); + npc.parent_id = self.id; + + let _ = npc_list.spawn(0, npc); + } + } + 23 => { + self.action_counter += 1; + if self.action_counter > 30 { + self.action_num = 24; + self.action_counter = 0; + self.anim_num = 7; + } + + let player = self.get_closest_player_mut(players); + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + } + 24 => { + self.action_counter += 1; + if self.action_counter > 3 { + self.action_num = 25; + self.anim_num = 3; + } + } + 25 => { + if self.flags.hit_bottom_wall() { + self.action_num = 26; + self.action_counter = 0; + self.anim_num = 2; + state.sound_manager.play_sfx(26); + state.quake_counter = 20; + } + } + 26 => { + self.vel_x = 8 * self.vel_x / 9; + self.action_counter += 1; + if self.action_counter > 20 { + self.action_num = 10; + self.anim_num = 0; + } + } + 50 | 51 => { + if self.action_num == 50 { + self.action_num = 51; + self.action_counter = 0; + self.anim_num = 4; + + let mut npc = NPC::create(141, &state.npc_table); + npc.cond.set_alive(true); + npc.parent_id = self.id; + + let _ = npc_list.spawn(0, npc); + } + self.action_counter += 1; + if self.action_counter > 30 { + self.action_num = 52; + self.action_counter = 0; + self.anim_num = 5; + } + + let player = self.get_closest_player_mut(players); + if self.x <= player.x { + self.direction = Direction::Right; + } else { + self.direction = Direction::Left; + } + } + + 52 => { + self.action_counter += 1; + if self.action_counter > 3 { + self.action_num = 10; + self.anim_num = 0; + } + } + 100 => { + self.anim_num = 3; + self.action_num = 101; + self.npc_flags.set_shootable(false); + self.damage = 0; + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + + for _ in 0..8 { + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y + self.rng.range(-12..12) * 0x200; + npc.vel_y = self.rng.range(-0x600..0); + npc.vel_x = self.rng.range(-0x155..0x155); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + 101 => { + if self.flags.hit_bottom_wall() { + self.action_num = 102; + self.action_counter = 0; + self.anim_num = 2; + state.sound_manager.play_sfx(26); + state.quake_counter = 20; + } + } + 102 => { + self.vel_x = 8 * self.vel_x / 9; + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 103; + self.action_counter = 0; + self.anim_num = 10; + } + } + 103 => { + self.action_counter += 1; + if self.action_counter > 50 { + self.anim_num = 9; + self.action_num = 104; + self.action_counter = 0; + } + } + 104 => { + self.anim_num += 1; + if self.anim_num > 10 { + self.anim_num = 9; + } + self.action_counter += 1; + if self.action_counter > 100 { + self.action_counter = 0; + self.anim_num = 9; + self.action_num = 105; + } + } + 105 => { + self.action_counter += 1; + if self.action_counter > 50 { + self.anim_counter = 0; + self.action_num = 106; + self.anim_num = 11; + } + } + 106 => { + self.anim_counter += 1; + if self.anim_counter > 50 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num > 12 { + self.anim_num = 12; + } + } + 140 | 141 => { + if self.action_num == 140 { + self.action_num = 141; + self.action_counter = 0; + self.anim_num = 12; + state.sound_manager.play_sfx(29); + } + + self.anim_num += 1; + if self.anim_num > 13 { + self.anim_num = 12; + } + + self.action_counter += 1; + if self.action_counter > 100 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + for _ in 0..4 { + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y + self.rng.range(-12..12) * 0x200; + npc.vel_y = self.rng.range(-0x600..0); + npc.vel_x = self.rng.range(-0x155..0x155); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + self.cond.set_alive(false); + } + } + _ => {} + } + + if self.action_num > 100 && self.action_num <= 104 && (self.action_counter % 9) == 0 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x + self.rng.range(-12..12) * 0x200; + npc.y = self.y + self.rng.range(-12..12) * 0x200; + npc.vel_y = self.rng.range(-0x600..0); + npc.vel_x = self.rng.range(-0x155..0x155); + + let _ = npc_list.spawn(0x100, npc); + } + + self.vel_y += 0x20; + self.vel_y = self.vel_y.clamp(-0x5ff, 0x5ff); + self.x += self.vel_x; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 14 }; + + self.anim_rect = state.constants.npc.n140_toroko_frenzied[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n141_toroko_block_projectile( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + if self.action_num != 1 { + if 1 < self.action_num { + if self.action_num == 10 { + if (self.flags.0 & 0xf) == 0 { + self.x = self.x + self.vel_x; + self.y = self.y + self.vel_y; + } else { + self.action_num = 0x14; + self.action_counter = 0; + + state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left); + state.sound_manager.play_sfx(0xc); + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + for _ in 0..4 { + npc.x = self.x; + npc.y = self.y; + npc.vel_x = self.rng.range(-0x200..0x200); + npc.vel_y = self.rng.range(-0x200..0x200); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + } else if self.action_num == 0x14 { + self.x = self.x + self.vel_x; + self.y = self.y + self.vel_y; + self.action_counter = self.action_counter + 1; + if 4 < self.action_counter { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + for _ in 0..4 { + npc.x = self.x; + npc.y = self.y; + npc.vel_x = self.rng.range(-0x200..0x200); + npc.vel_y = self.rng.range(-0x200..0x200); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + + self.npc_type = 0x8e; + self.anim_num = 0; + self.action_num = 0x14; + self.vel_x = 0; + self.npc_flags.set_invulnerable(false); + self.npc_flags.set_shootable(true); + self.damage = 1; + } + } + } + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + } + } else { + let parent = self.get_parent_ref_mut(npc_list); + if let Some(parent) = parent { + let player = self.get_closest_player_mut(players); + + if parent.direction == Direction::Left { + self.x = parent.x + 0x1400; + } else { + self.x = parent.x + -0x1400; + } + + self.y = parent.y + -0x1000; + if (parent.action_num == 0x18) || (parent.action_num == 0x34) { + self.action_num = 10; + if parent.direction == Direction::Left { + self.x = parent.x + -0x2000; + } else { + self.x = parent.x + 0x2000; + } + self.y = parent.y; + + let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64); + + self.vel_x = (angle.cos() * -2048.0) as i32; + self.vel_y = (angle.sin() * -2048.0) as i32; + state.sound_manager.play_sfx(0x27); + } + } + } + + self.anim_num = self.anim_num + 1; + if 1 < self.anim_num { + self.anim_num = 0; + } + + self.anim_rect = state.constants.npc.n141_toroko_block_projectile[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n142_flower_cub( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + ) -> GameResult { + match self.action_num { + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 0; + self.action_counter = 0; + } + self.action_counter += 1; + if self.action_counter > 30 { + self.action_num = 12; + self.anim_num = 1; + self.anim_counter = 0; + } + } + 12 => { + self.anim_counter += 1; + if self.anim_counter > 8 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num == 3 { + self.action_num = 20; + self.vel_y = -0x200; + let player = self.get_closest_player_mut(players); + if player.x >= self.x { + self.vel_x = 0x200; + } else { + self.vel_x = -0x200; + } + } + } + 20 => { + if self.vel_y < -127 { + self.anim_num = 3; + } else { + self.anim_num = 4; + } + + if self.flags.hit_bottom_wall() { + self.anim_num = 2; + self.action_num = 21; + self.action_counter = 0; + self.vel_x = 0; + state.sound_manager.play_sfx(23); + } + } + 21 => { + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 10; + self.anim_num = 0; + } + } + _ => {} + } + self.vel_y += 64; + self.vel_y = self.vel_y.clamp(-0x5ff, 0x5ff); + + self.x += self.vel_x; + self.y += self.vel_y; + self.anim_rect = state.constants.npc.n142_flower_cub[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n144_toroko_teleporting_in(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + self.target_x = self.x; + state.sound_manager.play_sfx(29); + } + self.action_counter += 1; + if self.action_counter == 64 { + self.action_num = 2; + self.action_counter = 0; + } + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + self.anim_num += 1; + } + if self.anim_num > 3 { + self.anim_num = 2; + } + if self.flags.hit_bottom_wall() { + self.action_num = 4; + self.action_counter = 0; + self.anim_num = 4; + state.sound_manager.play_sfx(23); + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 0; + self.anim_counter = 0; + } + if self.rng.range(0..120) == 10 { + self.action_num = 12; + self.action_counter = 0; + self.anim_num = 1; + } + } + 12 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 11; + self.anim_num = 0; + } + } + _ => {} + } + + if self.action_num > 1 { + self.vel_y += 0x20; + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + self.y += self.vel_y; + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 5 }; + + self.anim_rect = state.constants.npc.n144_toroko_teleporting_in[self.anim_num as usize + dir_offset]; + + if self.action_num == 1 { + self.anim_rect.bottom = self.action_counter / 4 + self.anim_rect.top; + self.x = if ((self.action_counter / 2) & 1) != 0 { self.target_x } else { self.target_x + 512 } + } + + Ok(()) + } } diff --git a/src/npc/boss/monster_x.rs b/src/npc/boss/monster_x.rs index 66e64de..f90eb4a 100644 --- a/src/npc/boss/monster_x.rs +++ b/src/npc/boss/monster_x.rs @@ -73,6 +73,65 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n159_monster_x_defeated( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.direction = Direction::Left; + + for _ in 0..8 { + npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200; + npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200; + npc.vel_x = self.rng.range(-0x155..0x155) as i32; + npc.vel_y = self.rng.range(-0x600..0) as i32; + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 2; + self.vel_x = -256; + } + self.x = if ((self.action_counter / 2) & 1) != 0 { self.x + 0x200 } else { self.x - 0x200 } + } + 2 => { + self.action_counter += 1; + self.vel_y += 0x40; + if self.y > 0x50000 { + self.cond.set_alive(false); + } + } + _ => {} + } + + self.y += self.vel_y; + self.x += self.vel_x; + + self.anim_rect = state.constants.npc.n159_monster_x_defeated; + + if self.action_counter % 8 == 1 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.direction = Direction::Left; + + npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200; + npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200; + npc.vel_x = self.rng.range(-0x155..0x155) as i32; + npc.vel_y = self.rng.range(-0x600..0) as i32; + + let _ = npc_list.spawn(0x100, npc); + } + Ok(()) + } } impl BossNPC { diff --git a/src/npc/mod.rs b/src/npc/mod.rs index 48cc123..dc15a80 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -20,6 +20,7 @@ use crate::rng::Xoroshiro32PlusPlus; use crate::shared_game_state::SharedGameState; use crate::stage::Stage; use crate::str; +use crate::components::flash::Flash; pub mod ai; pub mod boss; @@ -160,8 +161,8 @@ impl NPC { } } -impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NPC { - fn tick(&mut self, state: &mut SharedGameState, (players, npc_list, stage, bullet_manager): ([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)) -> GameResult { +impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Flash)> for NPC { + fn tick(&mut self, state: &mut SharedGameState, (players, npc_list, stage, bullet_manager, flash): ([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Flash)) -> GameResult { match self.npc_type { 0 => self.tick_n000_null(), 1 => self.tick_n001_experience(state), @@ -292,19 +293,42 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NP 127 => self.tick_n127_machine_gun_trail_l2(state), 128 => self.tick_n128_machine_gun_trail_l3(state), 129 => self.tick_n129_fireball_snake_trail(state), + 130 => self.tick_n130_puppy_sitting(state, players), 131 => self.tick_n131_puppy_sleeping(state), 132 => self.tick_n132_puppy_barking(state, players), + 133 => self.tick_n133_jenka(state), + 134 => self.tick_n134_armadillo(state, players, bullet_manager), + 135 => self.tick_n135_skeleton(state, players, npc_list), 136 => self.tick_n136_puppy_carried(state, players), 137 => self.tick_n137_large_door_frame(state), + 138 => self.tick_n138_large_door(state), + 139 => self.tick_n139_doctor(state), + 140 => self.tick_n140_toroko_frenzied(state, players, npc_list, bullet_manager), + 141 => self.tick_n141_toroko_block_projectile(state, players, npc_list), + 142 => self.tick_n142_flower_cub(state, players), 143 => self.tick_n143_jenka_collapsed(state), + 144 => self.tick_n144_toroko_teleporting_in(state), + 145 => self.tick_n145_king_sword(state, npc_list), + 146 => self.tick_n146_lighting(state, npc_list, flash), + 147 => self.tick_n147_critter_purple(state, players, npc_list), + 148 => self.tick_n148_critter_purple_projectile(state), 149 => self.tick_n149_horizontal_moving_block(state, players, npc_list), 150 => self.tick_n150_quote(state, players, npc_list), 151 => self.tick_n151_blue_robot_standing(state), 152 => self.tick_n152_shutter_stuck(), + 153 => self.tick_n153_gaudi(state, players), 154 => self.tick_n154_gaudi_dead(state), + 155 => self.tick_n155_gaudi_flying(state, players, npc_list), 156 => self.tick_n156_gaudi_projectile(state), 157 => self.tick_n157_vertical_moving_block(state, players, npc_list), 158 => self.tick_n158_fish_missile(state, players), + 159 => self.tick_n159_monster_x_defeated(state, npc_list), + 160 => self.tick_n160_puu_black(state, players, npc_list), + 161 => self.tick_n161_puu_black_projectile(state), + 162 => self.tick_n162_puu_black_dead(state, players, npc_list), + 163 => self.tick_n163_dr_gero(state), + 164 => self.tick_n164_nurse_hasumi(state), + 165 => self.tick_n165_curly_collapsed(state, players), 166 => self.tick_n166_chaba(state), 192 => self.tick_n192_scooter(state), 193 => self.tick_n193_broken_scooter(state), diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 86e2142..df46486 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -1251,7 +1251,7 @@ impl GameScene { for npc in self.npc_list.iter_alive() { npc.tick( state, - ([&mut self.player1, &mut self.player2], &self.npc_list, &mut self.stage, &self.bullet_manager), + ([&mut self.player1, &mut self.player2], &self.npc_list, &mut self.stage, &self.bullet_manager, &mut self.flash), )?; } self.boss.tick( diff --git a/src/text_script.rs b/src/text_script.rs index a5f1a66..deeda42 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -1498,6 +1498,7 @@ impl TextScriptVM { &game_scene.npc_list, &mut game_scene.stage, &game_scene.bullet_manager, + &mut game_scene.flash, ), )?; }