diff --git a/README.md b/README.md index f997b5d..d1a61c3 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Vanilla Cave Story does not work yet because some important data files are embed #### Roadmap +- [x] Checkmarked things = fully implemented +- [ ] Unmarked things = partially or not implemented yet. + - [x] Rendering - [x] Backdrops - [x] Tilemap @@ -49,7 +52,7 @@ Vanilla Cave Story does not work yet because some important data files are embed - [x] HUD - [ ] Text scripts (TSC) - [x] Initial implementation - - [ ] Full implementation of opcodes + - [ ] Full implementation of opcodes (~80% done) - [ ] Credits - [x] Shift-JIS encoding support - [ ] Audio @@ -59,14 +62,14 @@ Vanilla Cave Story does not work yet because some important data files are embed - [x] PixTone SFX - [ ] NPCs/entities - [x] Initial implementation - - [ ] Miscellaneous entities + - [ ] Miscellaneous entities (~30% done) - [ ] Bosses - [x] First Cave - - [ ] Mimiga Village - - [ ] Egg Corridor - - [ ] Grasstown - - [ ] Sand Zone - - [ ] Labirynth + - [x] Mimiga Village + - [ ] Egg Corridor (~70% done) + - [ ] Grasstown (~10% done) + - [ ] Sand Zone (~10% done) + - [ ] Labirynth (~10% done) - [ ] Outer Wall - [ ] Plantation - [ ] Last Cave diff --git a/src/npc/egg_corridor.rs b/src/npc/egg_corridor.rs index 0671ccc..0d8a714 100644 --- a/src/npc/egg_corridor.rs +++ b/src/npc/egg_corridor.rs @@ -5,6 +5,7 @@ use crate::ggez::GameResult; use crate::npc::NPC; use crate::player::Player; use crate::shared_game_state::SharedGameState; +use crate::caret::CaretType; impl NPC { pub(crate) fn tick_n002_behemoth(&mut self, state: &mut SharedGameState) -> GameResult { @@ -440,4 +441,29 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n084_basu_projectile(&mut self, state: &mut SharedGameState) -> GameResult { + self.x += self.vel_x; + self.y += self.vel_y; + + 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 = 0; + } + } + + self.anim_rect = state.constants.npc.n084_basu_projectile[self.anim_num as usize]; + + self.action_counter2 += 1; + if self.flags.0 != 0 || self.action_counter2 > 300 { + state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left); + self.cond.set_alive(false); + } + + Ok(()) + } } diff --git a/src/npc/igor.rs b/src/npc/igor.rs new file mode 100644 index 0000000..ac83922 --- /dev/null +++ b/src/npc/igor.rs @@ -0,0 +1,87 @@ +use crate::common::Direction; +use crate::ggez::GameResult; +use crate::npc::NPC; +use crate::shared_game_state::SharedGameState; + +impl NPC { + pub(crate) fn tick_n083_igor_cutscene(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.vel_x = 0; + self.action_num = 1; + self.anim_num = 0; + self.action_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 5 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + } + } + 2 | 3 => { + if self.action_num == 2 { + self.action_num = 3; + 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; + } + } + + self.vel_x = self.direction.vector_x() * 0x200; + } + 4 | 5 => { + if self.action_num == 4 { + self.vel_x = 0; + self.action_num = 5; + self.action_counter = 0; + self.anim_num = 6; + } + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_counter = 0; + self.action_num = 6; + self.anim_num = 7; + + state.sound_manager.play_sfx(70); + } + } + 6 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 0; + self.anim_num = 0; + } + } + 7 => { + self.action_num = 1; + } + _ => {} + } + + 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 { 8 }; + self.anim_rect = state.constants.npc.n083_igor_cutscene[self.anim_num as usize + dir_offset]; + + Ok(()) + } +} diff --git a/src/npc/mimiga_village.rs b/src/npc/mimiga_village.rs index 42422cd..ea85ddc 100644 --- a/src/npc/mimiga_village.rs +++ b/src/npc/mimiga_village.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use num_traits::clamp; +use num_traits::{abs, clamp}; use crate::common::Direction; use crate::ggez::GameResult; @@ -11,6 +11,7 @@ use crate::shared_game_state::SharedGameState; impl NPC { pub(crate) fn tick_n069_pignon(&mut self, state: &mut SharedGameState) -> GameResult { let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -82,11 +83,7 @@ impl NPC { self.direction = Direction::Left; } - self.vel_x = match self.direction { - Direction::Left => { -0x100 } // -0.5fix9 - Direction::Right => { 0x100 } // 0.5fix9 - _ => { 0 } - }; + self.vel_x = self.direction.vector_x() * 0x100; // 0.5fix9 } 5 => { if self.flags.hit_bottom_wall() { @@ -96,7 +93,7 @@ impl NPC { _ => {} } - if self.shock > 0 && [1,2,4].contains(&self.action_num) { + if self.shock > 0 && [1, 2, 4].contains(&self.action_num) { self.vel_y = -0x200; self.anim_num = 5; self.action_num = 5; @@ -151,11 +148,8 @@ impl NPC { self.anim_num = 2; } - if self.direction == Direction::Left { - self.anim_rect = state.constants.npc.n071_chinfish[self.anim_num as usize]; - } else { - self.anim_rect = state.constants.npc.n071_chinfish[self.anim_num as usize + 3]; - } + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + self.anim_rect = state.constants.npc.n071_chinfish[self.anim_num as usize + dir_offset]; Ok(()) } @@ -215,7 +209,6 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n079_mahin(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { match self.action_num { 0 => { @@ -259,12 +252,223 @@ impl NPC { self.y += self.vel_y; - if self.direction == Direction::Left { - self.anim_rect = state.constants.npc.n079_mahin[self.anim_num as usize]; - } else { - self.anim_rect = state.constants.npc.n079_mahin[self.anim_num as usize + 3]; - } + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + self.anim_rect = state.constants.npc.n079_mahin[self.anim_num as usize + dir_offset]; Ok(()) } + + pub(crate) fn tick_n080_gravekeeper(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.npc_flags.set_shootable(false); + self.damage = 0; + self.action_num = 1; + self.hit_bounds.left = 4 * 0x200; + } + + self.anim_num = 0; + + if abs(player.x - self.x) < 128 * 0x200 + && self.y - 48 * 0x200 < player.y && self.y + 32 * 0x200 > player.y { + self.anim_counter = 0; + self.action_num = 2; + } + + if self.shock > 0 { + self.anim_num = 1; + self.anim_counter = 0; + self.action_num = 2; + self.npc_flags.set_shootable(false); + } + + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 6 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 3 { + self.anim_num = 0; + } + } + + if abs(player.x - self.x) < 16 * 0x200 { + self.hit_bounds.left = 18 * 0x200; + self.action_counter = 0; + self.action_num = 3; + self.npc_flags.set_shootable(true); + + state.sound_manager.play_sfx(34); + } + + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + self.vel_x = self.direction.vector_x() * 0x100; + } + 3 => { + self.vel_x = 0; + + self.action_counter += 1; + if self.action_counter > 40 { + self.action_counter = 0; + self.action_num = 4; + state.sound_manager.play_sfx(106); + } + + self.anim_num = 4; + } + 4 => { + self.damage = 10; + self.action_counter += 1; + if self.action_counter > 2 { + self.action_counter = 0; + self.action_num = 5; + } + + self.anim_num = 5; + } + 5 => { + self.action_counter += 1; + if self.action_counter > 60 { + self.action_num = 0; + } + + self.anim_num = 6; + } + _ => {} + } + + if (self.vel_x < 0 && self.flags.hit_left_wall()) + || (self.vel_x > 0 && self.flags.hit_right_wall()) { + self.vel_x = 0; + } + + self.vel_x = clamp(self.vel_x, -0x400, 0x400); + self.vel_y = clamp(self.vel_y + 0x20, -0x5ff, 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.n080_gravekeeper[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n081_giant_pignon(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + self.vel_x = 0; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + + if state.game_rng.range(0..100) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + + + if state.game_rng.range(0..150) == 1 { + self.action_num = 3; + self.action_counter = 50; + self.anim_num = 0; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 0; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + } + 3 | 4 => { + if self.action_num == 3 { + self.action_num = 4; + self.anim_num = 2; + self.anim_counter = 0; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + + if self.action_counter > 0 { + self.action_counter -= 1; + } else { + self.action_num = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 4 { + self.anim_num = 2; + } + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + + if self.flags.hit_left_wall() { + self.direction = Direction::Right; + } + + if self.flags.hit_right_wall() { + self.direction = Direction::Left; + } + + self.vel_x = self.direction.vector_x() * 0x100; // 0.5fix9 + } + 5 => { + if self.flags.hit_bottom_wall() { + self.action_num = 0; + } + } + _ => {} + } + + if self.shock > 0 && [1, 2, 4].contains(&self.action_num) { + self.vel_x = if self.x < player.x { 0x100 } else { -0x100 }; + self.vel_y = -0x200; + self.anim_num = 5; + self.action_num = 5; + + self.anim_rect = state.constants.npc.n081_giant_pignon[self.anim_num as usize + dir_offset]; + } + + self.vel_y += 0x40; + + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + Ok(()) + } + + pub(crate) fn tick_n091_mimiga_cage(&mut self, state: &SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.y += 16 * 0x200; + self.anim_rect = state.constants.npc.n091_mimiga_cage; + } + + Ok(()) + } } diff --git a/src/npc/misc.rs b/src/npc/misc.rs index 4b1f185..671421e 100644 --- a/src/npc/misc.rs +++ b/src/npc/misc.rs @@ -622,6 +622,204 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n085_terminal(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + match self.action_num { + 0 => { + self.anim_num = 0; + if abs(player.x - self.x) < 8 * 0x200 && player.y < self.y + 8 * 0x200 && player.y > self.y - 16 * 0x200 { + state.sound_manager.play_sfx(43); + self.action_num = 1; + } + } + 1 => { + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 1; + } + } + _ =>{ } + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + self.anim_rect = state.constants.npc.n085_terminal[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n096_fan_left(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 && self.direction == Direction::Right { + self.action_num = 2; + } + + self.anim_num = 1; + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + } + + if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 + && state.game_rng.range(0..5) == 1 { + let mut particle = NPCMap::create_npc(199, &state.npc_table); + particle.cond.set_alive(true); + particle.direction = Direction::Left; + particle.x = self.x; + particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; + state.new_npcs.push(particle); + } + + if abs(player.y - self.y) < 8 * 0x200 && player.x < self.x && player.x > self.x - 96 * 0x200 { + player.vel_x -= 0x88; + player.cond.set_increase_acceleration(true); + } + } + _ => {} + } + + if self.anim_counter == 0 { + self.anim_rect = state.constants.npc.n098_fan_right[self.anim_num as usize]; + } + + Ok(()) + } + + pub(crate) fn tick_n097_fan_up(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 && self.direction == Direction::Right { + self.action_num = 2; + } + + self.anim_num = 1; + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + } + + if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 + && state.game_rng.range(0..5) == 1 { + let mut particle = NPCMap::create_npc(199, &state.npc_table); + particle.cond.set_alive(true); + particle.direction = Direction::Up; + particle.x = self.x + (state.game_rng.range(-8..8) * 0x200) as isize; + particle.y = self.y; + state.new_npcs.push(particle); + } + + if abs(player.x - self.x) < 8 * 0x200 && player.y < self.y && player.y > self.y - 96 * 0x200 { + player.vel_y -= 0x88; + } + } + _ => {} + } + + if self.anim_counter == 0 { + self.anim_rect = state.constants.npc.n097_fan_up[self.anim_num as usize]; + } + + Ok(()) + } + + pub(crate) fn tick_n098_fan_right(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 && self.direction == Direction::Right { + self.action_num = 2; + } + + self.anim_num = 1; + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + } + + if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 + && state.game_rng.range(0..5) == 1 { + let mut particle = NPCMap::create_npc(199, &state.npc_table); + particle.cond.set_alive(true); + particle.direction = Direction::Right; + particle.x = self.x; + particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; + state.new_npcs.push(particle); + } + + if abs(player.y - self.y) < 8 * 0x200 && player.x > self.x && player.x < self.x + 96 * 0x200 { + player.vel_x += 0x88; + player.cond.set_increase_acceleration(true); + } + } + _ => {} + } + + if self.anim_counter == 0 { + self.anim_rect = state.constants.npc.n098_fan_right[self.anim_num as usize]; + } + + Ok(()) + } + + pub(crate) fn tick_n099_fan_down(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 && self.direction == Direction::Right { + self.action_num = 2; + } + + self.anim_num = 1; + } + 2 => { + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + } + + if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 + && state.game_rng.range(0..5) == 1 { + let mut particle = NPCMap::create_npc(199, &state.npc_table); + particle.cond.set_alive(true); + particle.direction = Direction::Bottom; + particle.x = self.x + (state.game_rng.range(-8..8) * 0x200) as isize; + particle.y = self.y; + state.new_npcs.push(particle); + } + + if abs(player.x - self.x) < 8 * 0x200 && player.y > self.y && player.y < self.y + 96 * 0x200 { + player.vel_y -= 0x88; + } + } + _ => {} + } + + if self.anim_counter == 0 { + self.anim_rect = state.constants.npc.n097_fan_up[self.anim_num as usize]; + } + + Ok(()) + } + pub(crate) fn tick_n149_horizontal_moving_block(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { match self.action_num { 0 => { @@ -856,180 +1054,6 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n096_fan_left(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { - match self.action_num { - 0 | 1 => { - if self.action_num == 0 && self.direction == Direction::Right { - self.action_num = 2; - } - - self.anim_num = 1; - } - 2 => { - self.anim_counter += 1; - if self.anim_counter > 0 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 0; - } - } - - if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 - && state.game_rng.range(0..5) == 1 { - let mut particle = NPCMap::create_npc(199, &state.npc_table); - particle.cond.set_alive(true); - particle.direction = Direction::Left; - particle.x = self.x; - particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; - state.new_npcs.push(particle); - } - - if abs(player.y - self.y) < 8 * 0x200 && player.x < self.x && player.x > self.x - 96 * 0x200 { - player.vel_x -= 0x88; - player.cond.set_increase_acceleration(true); - } - } - _ => {} - } - - if self.anim_counter == 0 { - self.anim_rect = state.constants.npc.n098_fan_right[self.anim_num as usize]; - } - - Ok(()) - } - - pub(crate) fn tick_n097_fan_up(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { - match self.action_num { - 0 | 1 => { - if self.action_num == 0 && self.direction == Direction::Right { - self.action_num = 2; - } - - self.anim_num = 1; - } - 2 => { - self.anim_counter += 1; - if self.anim_counter > 0 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 0; - } - } - - if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 - && state.game_rng.range(0..5) == 1 { - let mut particle = NPCMap::create_npc(199, &state.npc_table); - particle.cond.set_alive(true); - particle.direction = Direction::Up; - particle.x = self.x + (state.game_rng.range(-8..8) * 0x200) as isize; - particle.y = self.y; - state.new_npcs.push(particle); - } - - if abs(player.x - self.x) < 8 * 0x200 && player.y < self.y && player.y > self.y - 96 * 0x200 { - player.vel_y -= 0x88; - } - } - _ => {} - } - - if self.anim_counter == 0 { - self.anim_rect = state.constants.npc.n097_fan_up[self.anim_num as usize]; - } - - Ok(()) - } - - pub(crate) fn tick_n098_fan_right(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { - match self.action_num { - 0 | 1 => { - if self.action_num == 0 && self.direction == Direction::Right { - self.action_num = 2; - } - - self.anim_num = 1; - } - 2 => { - self.anim_counter += 1; - if self.anim_counter > 0 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 0; - } - } - - if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 - && state.game_rng.range(0..5) == 1 { - let mut particle = NPCMap::create_npc(199, &state.npc_table); - particle.cond.set_alive(true); - particle.direction = Direction::Right; - particle.x = self.x; - particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; - state.new_npcs.push(particle); - } - - if abs(player.y - self.y) < 8 * 0x200 && player.x > self.x && player.x < self.x + 96 * 0x200 { - player.vel_x += 0x88; - player.cond.set_increase_acceleration(true); - } - } - _ => {} - } - - if self.anim_counter == 0 { - self.anim_rect = state.constants.npc.n098_fan_right[self.anim_num as usize]; - } - - Ok(()) - } - - pub(crate) fn tick_n099_fan_down(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { - match self.action_num { - 0 | 1 => { - if self.action_num == 0 && self.direction == Direction::Right { - self.action_num = 2; - } - - self.anim_num = 1; - } - 2 => { - self.anim_counter += 1; - if self.anim_counter > 0 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 0; - } - } - - if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 - && state.game_rng.range(0..5) == 1 { - let mut particle = NPCMap::create_npc(199, &state.npc_table); - particle.cond.set_alive(true); - particle.direction = Direction::Bottom; - particle.x = self.x + (state.game_rng.range(-8..8) * 0x200) as isize; - particle.y = self.y; - state.new_npcs.push(particle); - } - - if abs(player.x - self.x) < 8 * 0x200 && player.y > self.y && player.y < self.y + 96 * 0x200 { - player.vel_y -= 0x88; - } - } - _ => {} - } - - if self.anim_counter == 0 { - self.anim_rect = state.constants.npc.n097_fan_up[self.anim_num as usize]; - } - - Ok(()) - } - pub(crate) fn tick_n199_wind_particles(&mut self, state: &mut SharedGameState) -> GameResult { if self.action_num == 0 { self.action_num = 1; diff --git a/src/npc/misery.rs b/src/npc/misery.rs index 789aa35..61105d9 100644 --- a/src/npc/misery.rs +++ b/src/npc/misery.rs @@ -24,7 +24,7 @@ impl NPC { self.target_x = npc.x; self.target_y = npc.y; - let angle = ((self.y - self.target_y) as f64 / (self.x - self.target_x) as f64 ).atan(); + let angle = ((self.y - self.target_y) as f64 / (self.x - self.target_x) as f64).atan(); self.vel_x = (angle.cos() * 1024.0) as isize; // 2.0fix9 self.vel_y = (angle.sin() * 1024.0) as isize; @@ -222,11 +222,8 @@ impl NPC { _ => {} } - if self.direction == Direction::Left { - self.anim_rect = state.constants.npc.n067_misery_floating[self.anim_num as usize]; - } else { - self.anim_rect = state.constants.npc.n067_misery_floating[self.anim_num as usize + 8]; - } + let dir_offset = if self.direction == Direction::Left { 0 } else { 8 }; + self.anim_rect = state.constants.npc.n067_misery_floating[self.anim_num as usize + dir_offset]; if self.action_num == 1 && self.anim_counter < 32 { self.anim_counter += 1; @@ -235,5 +232,178 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n082_misery_standing(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 2; + } + + if state.game_rng.range(0..120) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 3; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 8 { + self.action_num = 1; + self.anim_num = 2; + } + } + 15 | 16 => { + if self.action_num == 15 { + self.action_num = 16; + self.action_counter = 0; + self.anim_num = 4; + } + + self.action_counter += 1; + if self.action_counter == 30 { + state.sound_manager.play_sfx(21); + + let mut npc = NPCMap::create_npc(66, &state.npc_table); + npc.x = self.x; + npc.y = self.y - 16 * 0x200; + npc.cond.set_alive(true); + + state.new_npcs.push(npc); + } + + if self.action_counter == 50 { + self.action_num = 14; + } + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.anim_num = 0; + self.vel_y = 0; + self.npc_flags.set_ignore_solidity(true); + } + + self.vel_y -= 0x20; + + if self.y < -8 * 0x200 { + self.cond.set_alive(false); + } + } + 25 | 26 => { + if self.action_num == 25 { + self.action_num = 26; + self.action_counter = 0; + self.anim_num = 5; + self.anim_counter = 0; + } + + self.anim_num += 1; + if self.anim_num > 7 { + self.anim_num = 5; + } + + self.action_counter += 1; + if self.action_counter == 30 { + state.sound_manager.play_sfx(101); + // todo flash + self.action_num = 27; + self.anim_num = 7; + } + } + 27 => { + self.action_counter += 1; + if self.action_counter == 50 { + self.action_num = 14; + } + } + 30 | 31 => { + if self.action_num == 30 { + self.action_num = 31; + self.anim_num = 3; + self.anim_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 10 { + self.action_num = 32; + self.anim_num = 4; + self.anim_counter = 0; + } + } + 32 => { + self.anim_counter += 1; + if self.anim_counter > 100 { + self.action_num = 1; + self.anim_num = 2; + } + } + 40 | 41 => { + if self.action_num == 40 { + self.action_num = 41; + self.action_counter = 0; + } + + self.anim_num = 4; + + self.action_counter += 1; + if self.action_counter == 30 || self.action_counter == 40 || self.action_counter == 50 { + state.sound_manager.play_sfx(33); + + let mut npc = NPCMap::create_npc(11, &state.npc_table); + npc.x = self.x + 8 * 0x200; + npc.y = self.y - 8 * 0x200; + npc.vel_x = 0x600; + npc.vel_y = state.game_rng.range(-0x200..0) as isize; + npc.cond.set_alive(true); + + state.new_npcs.push(npc); + } + } + 50 => { + self.anim_num = 8; + } + _ => {} + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.action_num == 11 + { + if self.anim_counter != 0 + { + self.anim_counter -= 1; + self.anim_num = 1; + } else { + if state.game_rng.range(0..100) == 1 { + self.anim_counter = 30; + } + + self.anim_num = 0; + } + } + + if self.action_num == 14 + { + if self.action_counter != 0 + { + self.action_counter -= 1; + self.anim_num = 3; + } else { + if state.game_rng.range(0..100) == 1 { + self.anim_counter = 30; + } + + self.anim_num = 2; + } + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 9 }; + self.anim_rect = state.constants.npc.n082_misery_standing[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/mod.rs b/src/npc/mod.rs index e0eecf1..e0c6406 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -29,12 +29,14 @@ pub mod characters; pub mod egg_corridor; pub mod first_cave; pub mod grasstown; +pub mod igor; pub mod maze; pub mod mimiga_village; pub mod misc; pub mod misery; pub mod pickups; pub mod sand_zone; +pub mod sue; pub mod toroko; pub mod weapon_trail; @@ -172,6 +174,7 @@ impl GameEntity<(&mut Player, &HashMap>, &mut Stage)> for NPC 38 => self.tick_n038_fireplace(state), 39 => self.tick_n039_save_sign(state), 41 => self.tick_n041_busted_door(state), + 42 => self.tick_n042_sue(state, player, map), 43 => self.tick_n043_chalkboard(state), 46 => self.tick_n046_hv_trigger(state, player), 52 => self.tick_n052_sitting_blue_robot(state), @@ -197,6 +200,13 @@ impl GameEntity<(&mut Player, &HashMap>, &mut Stage)> for NPC 77 => self.tick_n077_yamashita(state), 78 => self.tick_n078_pot(state), 79 => self.tick_n079_mahin(state, player), + 80 => self.tick_n080_gravekeeper(state, player), + 81 => self.tick_n081_giant_pignon(state, player), + 82 => self.tick_n082_misery_standing(state), + 83 => self.tick_n083_igor_cutscene(state), + 84 => self.tick_n084_basu_projectile(state), + 85 => self.tick_n085_terminal(state, player), + 91 => self.tick_n091_mimiga_cage(state), 96 => self.tick_n096_fan_left(state, player), 97 => self.tick_n097_fan_up(state, player), 98 => self.tick_n098_fan_right(state, player), @@ -643,11 +653,11 @@ impl NPCTable { } for npc in table.entries.iter_mut() { - npc.hurt_sound = f.read_u8()?; + npc.death_sound = f.read_u8()?; } for npc in table.entries.iter_mut() { - npc.death_sound = f.read_u8()?; + npc.hurt_sound = f.read_u8()?; } for npc in table.entries.iter_mut() { @@ -713,12 +723,20 @@ impl NPCTable { if let Some(npc) = self.entries.get(npc_type as usize) { match npc.spritesheet_id { 2 => &self.tileset_name, + 6 => "Fade", + 8 => "ItemImage", + 11 => "Arms", + 12 => "ArmsImage", + 14 => "StageImage", + 15 => "Loading", + 16 => "MyChar", 17 => "Bullet", 19 => "Caret", 20 => "Npc/NpcSym", 21 => &self.tex_npc1_name, 22 => &self.tex_npc2_name, 23 => "Npc/NpcRegu", + 26 => "TextBox", _ => "Npc/Npc0" } } else { diff --git a/src/npc/pickups.rs b/src/npc/pickups.rs index 1fb183c..1c3865a 100644 --- a/src/npc/pickups.rs +++ b/src/npc/pickups.rs @@ -143,4 +143,8 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n086_missile_pickup(&mut self, state: &mut SharedGameState) -> GameResult { + Ok(()) + } } diff --git a/src/npc/sue.rs b/src/npc/sue.rs new file mode 100644 index 0000000..72ef4ae --- /dev/null +++ b/src/npc/sue.rs @@ -0,0 +1,233 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use crate::common::Direction; +use crate::ggez::GameResult; +use crate::npc::{NPC, NPCMap}; +use crate::player::Player; +use crate::shared_game_state::SharedGameState; +use num_traits::clamp; + +impl NPC { + pub fn tick_n042_sue(&mut self, state: &mut SharedGameState, player: &Player, map: &HashMap>) -> 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.vel_x = 0; + } + + if state.game_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; + } + } + 3 | 4 => { + if self.action_num == 3 { + self.action_num = 4; + self.anim_num = 2; + self.anim_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 4 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 5 { + self.anim_num = 2; + } + } + + self.vel_x = self.direction.vector_x() * 0x200; + } + 5 => { + self.anim_num = 6; + self.vel_x = 0; + } + 6 | 7 => { + if self.action_num == 6 { + state.sound_manager.play_sfx(50); + self.action_counter = 0; + self.action_num = 7; + self.anim_num = 7; + } + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 0; + } + } + 8 | 9 => { + if self.action_num == 8 { + state.sound_manager.play_sfx(50); + self.action_counter = 0; + self.action_num = 9; + self.anim_num = 7; + self.vel_x = self.direction.vector_x() * -0x400; + self.vel_y = -0x200; + } + + self.action_counter += 1; + if self.action_counter > 3 && self.flags.hit_bottom_wall() { + self.action_num = 10; + self.direction = self.direction.opposite(); + } + } + 10 => { + self.vel_x = 0; + self.anim_num = 8; + } + 11 | 12 => { + if self.action_num == 11 { + self.action_num = 12; + self.action_counter = 0; + self.anim_num = 9; + self.anim_counter = 0; + self.vel_x = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 8 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 10 { + self.anim_num = 9; + } + } + } + 13 | 14 => { + if self.action_num == 13 { + self.anim_num = 11; + self.vel_x = 0; + self.vel_y = 0; + self.action_num = 14; + + if let Some(parent_id) = map.iter() + .filter(|(&id, _)| id != self.id) + .find_map(|(id, npc_cell)| + if npc_cell.borrow().event_num == 501 { Some(*id) } else { None }) { + self.parent_id = parent_id; + } + } + + if let Some(npc_cell) = map.get(&self.parent_id) { + let npc = npc_cell.borrow(); + + self.direction = npc.direction.opposite(); + self.x = npc.x + npc.direction.vector_x() * 6 * 0x200; + self.y = npc.y + 4 * 0x200; + + if npc.anim_num == 2 || npc.anim_num == 4 { + self.y -= 0x200; + } + } + } + 15 | 16 => { + if self.action_num == 15 { + self.action_num = 16; + self.vel_x = 0; + self.anim_num = 0; + + let mut npc = NPCMap::create_npc(257, &state.npc_table); + npc.x = self.x + 128 * 0x200; + npc.y = self.y; + npc.direction = Direction::Left; + npc.cond.set_alive(true); + state.new_npcs.push(npc); + + npc.direction = Direction::Right; + state.new_npcs.push(npc); + } + + state.npc_super_pos = ( + self.x - 24 * 0x200, + self.y - 8 * 0x200 + ); + } + 17 => { + self.vel_x = 0; + self.anim_num = 12; + + state.npc_super_pos = ( + self.x, + self.y - 8 * 0x200 + ); + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.anim_num = 2; + self.anim_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + self.anim_num += 1; + if self.anim_num > 5 { + self.anim_num = 2; + } + } + + self.vel_x = self.direction.vector_x() * 0x400; + + if self.x < player.x - 8 * 0x200 { + self.direction = Direction::Right; + self.action_num = 0; + } + } + 30 | 31 => { + if self.action_num == 30 { + self.action_num = 31; + self.anim_num = 2; + self.anim_counter = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + + self.anim_num += 1; + if self.anim_num > 5{ + self.anim_num = 2; + } + } + + self.vel_x = self.direction.vector_x() * 0x400; + } + 40 => { + self.action_num = 41; + self.anim_num = 9; + self.vel_y = -0x400; + } + _ => {} + } + + if self.action_num != 14 { + self.vel_y += 0x40; + + self.vel_x = clamp(self.vel_x, -0x400, 0x400); + 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 { 13 }; + self.anim_rect = state.constants.npc.n042_sue[self.anim_num as usize + dir_offset]; + + Ok(()) + } +} diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 336f5e3..b142025 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -600,7 +600,7 @@ impl GameScene { 20 if npc.direction == Direction::Right => { self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, ((npc.y - self.frame.y) / 0x200) as f32, - 2.0, (0, 0, 255), batch); + 2.0, (0, 0, 150), batch); if npc.anim_num < 2 { self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, @@ -621,10 +621,10 @@ impl GameScene { 2.0, (255, 0, 0), batch); } 38 => { - let flicker = 200 + state.effect_rng.range(0..55) as u8; + let flicker = (npc.anim_num ^ 5 & 3) as u8 * 15; self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, ((npc.y - self.frame.y) / 0x200) as f32, - 2.0, (flicker, flicker - 80, 0), batch); + 3.5, (130 + flicker, 40 + flicker, 0), batch); } 66 if npc.action_num == 1 && npc.anim_counter % 2 == 0 => self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, @@ -642,6 +642,23 @@ impl GameScene { 75 | 77 => self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, ((npc.y - self.frame.y) / 0x200) as f32, 3.0, (255, 100, 0), batch), + 85 if npc.action_num == 1 => { + let (color, color2) = if npc.direction == Direction::Left { + ((0, 150, 100), (0, 50, 30)) + } else { + ((150, 0, 0), (50, 0, 0)) + }; + + self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, + ((npc.y - self.frame.y) / 0x200) as f32 - 8.0, + 1.5, color, batch); + + if npc.anim_num < 2 { + self.draw_light(((npc.x - self.frame.x) / 0x200) as f32, + ((npc.y - self.frame.y) / 0x200) as f32 - 8.0, + 2.1, color2, batch); + } + } _ => {} } } diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index bec4b0b..8e0bf5b 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -8,6 +8,7 @@ use crate::caret::{Caret, CaretType}; use crate::common::{ControlFlags, Direction, FadeState, KeyState}; use crate::engine_constants::EngineConstants; use crate::ggez::{Context, filesystem, GameResult, graphics}; +use crate::ggez::graphics::Canvas; use crate::npc::{NPC, NPCTable}; use crate::profile::GameProfile; use crate::rng::RNG; @@ -16,9 +17,8 @@ use crate::scene::Scene; use crate::sound::SoundManager; use crate::stage::StageData; use crate::str; -use crate::text_script::{TextScriptExecutionState, TextScriptVM, ScriptMode}; +use crate::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::texture_set::TextureSet; -use crate::ggez::graphics::Canvas; use crate::touch_controls::TouchControls; #[derive(PartialEq, Eq, Copy, Clone)] @@ -52,7 +52,9 @@ pub struct SharedGameState { pub control_flags: ControlFlags, pub game_flags: BitVec, pub fade_state: FadeState, + /// RNG used by game state, using it for anything else might cause unintended side effects and break replays. pub game_rng: RNG, + /// RNG used by graphics effects that aren't dependent on game's state. pub effect_rng: RNG, pub quake_counter: u16, pub teleporter_slots: Vec<(u16, u16)>, @@ -64,6 +66,7 @@ pub struct SharedGameState { pub texture_set: TextureSet, pub base_path: String, pub npc_table: NPCTable, + pub npc_super_pos: (isize, isize), pub stages: Vec, pub sound_manager: SoundManager, pub settings: Settings, @@ -123,6 +126,7 @@ impl SharedGameState { texture_set: TextureSet::new(base_path), base_path: str!(base_path), npc_table: NPCTable::new(), + npc_super_pos: (0, 0), stages: Vec::with_capacity(96), sound_manager: SoundManager::new(ctx)?, settings: Settings { @@ -131,7 +135,7 @@ impl SharedGameState { original_textures: false, enhanced_graphics: true, debug_outlines: false, - touch_controls: cfg!(target_os = "android") + touch_controls: cfg!(target_os = "android"), }, constants, new_npcs: Vec::with_capacity(8),