From 091892b3398c20bd61839831632168ba31962698 Mon Sep 17 00:00:00 2001 From: Alula Date: Tue, 15 Dec 2020 23:09:14 +0100 Subject: [PATCH] add Monster X and some tweaks --- README.md | 11 +- src/bullet.rs | 4 +- src/caret.rs | 16 +- src/engine_constants/npcs.rs | 36 ++ src/lib.rs | 2 +- src/npc/boss/balfrog.rs | 1 - src/npc/boss/mod.rs | 16 +- src/npc/boss/monster_x.rs | 837 ++++++++++++++++++++++++++++++++++- src/npc/boss/omega.rs | 9 +- src/npc/maze.rs | 17 + src/npc/mod.rs | 2 + src/scene/game_scene.rs | 2 +- src/scene/mod.rs | 9 +- src/texture_set.rs | 16 +- 14 files changed, 931 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index a251d04..6ea504b 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,19 @@ Because methods used to extract game data from cartridge vary, you have to find - [x] PixTone SFX - [ ] NPCs/entities - [x] Initial implementation - - [ ] Miscellaneous entities (~30% done) - - [ ] Bosses (~20% done) + - [ ] Miscellaneous entities (~40% done) + - [ ] Bosses (~30% done) + - [x] Omega + - [x] Balfrog + - [x] Monster X - [x] First Cave - [x] Mimiga Village - [x] Egg Corridor - [x] Grasstown - - [ ] Sand Zone (~30% done) + - [ ] Sand Zone (~50% done) - [ ] Labirynth (~10% done) + - [x] Waterway + - [ ] Egg Corridor? (~20% done) - [ ] Outer Wall - [ ] Plantation - [ ] Last Cave diff --git a/src/bullet.rs b/src/bullet.rs index 2808e62..400791f 100644 --- a/src/bullet.rs +++ b/src/bullet.rs @@ -43,8 +43,8 @@ impl BulletManager { self.bullets.iter().filter(|b| b.owner == player_id && b.btype == btype).count() } - pub fn count_bullets_all(&self, btype: u16) -> usize { - self.bullets.iter().filter(|b| b.btype == btype).count() + pub fn count_bullets_type_idx_all(&self, type_idx: u16) -> usize { + self.bullets.iter().filter(|b| (b.btype.saturating_sub(2) / 3) == type_idx).count() } pub fn count_bullets_multi(&self, btypes: [u16; 3], player_id: TargetPlayer) -> usize { diff --git a/src/caret.rs b/src/caret.rs index bf95e69..0b1e973 100644 --- a/src/caret.rs +++ b/src/caret.rs @@ -1,7 +1,7 @@ use std::fs::read_to_string; use crate::bitfield; -use crate::common::{Condition, Direction, Rect, CDEG_RAD}; +use crate::common::{CDEG_RAD, Condition, Direction, Rect}; use crate::engine_constants::EngineConstants; use crate::rng::RNG; @@ -173,11 +173,11 @@ impl Caret { } match self.direction { - Direction::Left => self.x -= 0x400, // 2.0fix9 - Direction::Up => self.y -= 0x400, - Direction::Right => self.x += 0x400, - Direction::Bottom => self.y += 0x400, - Direction::FacingPlayer => unreachable!(), + Direction::Left => self.x -= 0x400, // 2.0fix9 + Direction::Up => self.y -= 0x400, + Direction::Right => self.x += 0x400, + Direction::Bottom => self.y += 0x400, + Direction::FacingPlayer => {}, } } CaretType::DrownedQuote => { @@ -185,8 +185,8 @@ impl Caret { self.anim_counter = 1; match self.direction { - Direction::Left => self.anim_rect = constants.caret.drowned_quote_left_rect, - Direction::Right => self.anim_rect = constants.caret.drowned_quote_right_rect, + Direction::Left => self.anim_rect = constants.caret.drowned_quote_left_rect, + Direction::Right => self.anim_rect = constants.caret.drowned_quote_right_rect, Direction::FacingPlayer => unreachable!(), _ => {} } diff --git a/src/engine_constants/npcs.rs b/src/engine_constants/npcs.rs index 3749180..b36481e 100644 --- a/src/engine_constants/npcs.rs +++ b/src/engine_constants/npcs.rs @@ -1071,6 +1071,8 @@ pub struct NPCConsts { #[serde(default = "default_b02_balfrog")] pub b02_balfrog: [Rect; 18], + #[serde(default = "default_b03_monster_x")] + pub b03_monster_x: [Rect; 29], } fn default_n001_experience() -> [Rect; 6] { @@ -4788,3 +4790,37 @@ fn default_b02_balfrog() -> [Rect; 18] { Rect { left: 120, top: 24, right: 160, bottom: 48 }, ] } + +fn default_b03_monster_x() -> [Rect; 29] { + [ + Rect { left: 216, top: 0, right: 320, bottom: 48 }, // face + Rect { left: 216, top: 48, right: 320, bottom: 96 }, + Rect { left: 216, top: 144, right: 320, bottom: 192 }, + Rect { left: 0, top: 0, right: 72, bottom: 32 }, // tracks up + Rect { left: 0, top: 32, right: 72, bottom: 64 }, + Rect { left: 72, top: 0, right: 144, bottom: 32 }, + Rect { left: 144, top: 0, right: 216, bottom: 32 }, + Rect { left: 72, top: 32, right: 144, bottom: 64 }, + Rect { left: 144, top: 32, right: 216, bottom: 64 }, + Rect { left: 0, top: 64, right: 72, bottom: 96 }, // tracks down + Rect { left: 0, top: 96, right: 72, bottom: 128 }, + Rect { left: 72, top: 64, right: 144, bottom: 96 }, + Rect { left: 144, top: 64, right: 216, bottom: 96 }, + Rect { left: 72, top: 96, right: 144, bottom: 128 }, + Rect { left: 144, top: 96, right: 216, bottom: 128 }, + Rect { left: 0, top: 128, right: 72, bottom: 160 }, // frame + Rect { left: 72, top: 128, right: 144, bottom: 160 }, + Rect { left: 0, top: 160, right: 72, bottom: 192 }, + Rect { left: 72, top: 160, right: 144, bottom: 192 }, + Rect { left: 216, top: 96, right: 264, bottom: 144 }, // shield left + Rect { left: 264, top: 96, right: 312, bottom: 144 }, // shield right + Rect { left: 0, top: 192, right: 16, bottom: 208 }, // part 4 + Rect { left: 16, top: 192, right: 32, bottom: 208 }, + Rect { left: 32, top: 192, right: 48, bottom: 208 }, + Rect { left: 48, top: 192, right: 64, bottom: 208 }, + Rect { left: 0, top: 208, right: 16, bottom: 224 }, + Rect { left: 16, top: 208, right: 32, bottom: 224 }, + Rect { left: 32, top: 208, right: 48, bottom: 224 }, + Rect { left: 48, top: 208, right: 64, bottom: 224 }, + ] +} diff --git a/src/lib.rs b/src/lib.rs index 4b8dd1e..646a59b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,7 +115,7 @@ impl Game { } if self.loops != 0 { - scene.draw_tick(&mut self.state, ctx)?; + scene.draw_tick(&mut self.state)?; self.last_tick = last_tick; } diff --git a/src/npc/boss/balfrog.rs b/src/npc/boss/balfrog.rs index b35ab39..26a2d80 100644 --- a/src/npc/boss/balfrog.rs +++ b/src/npc/boss/balfrog.rs @@ -242,7 +242,6 @@ impl BossNPC { let deg = f64::atan2(py as f64, px as f64) + self.parts[0].rng.range(-16..16) as f64 * CDEG_RAD; - // todo rand let mut npc = NPCMap::create_npc(108, &state.npc_table); npc.cond.set_alive(true); diff --git a/src/npc/boss/mod.rs b/src/npc/boss/mod.rs index 1f8891c..ba0389d 100644 --- a/src/npc/boss/mod.rs +++ b/src/npc/boss/mod.rs @@ -24,9 +24,9 @@ pub mod undead_core; pub struct BossNPC { pub boss_type: u16, - pub parts: [NPC; 16], - pub hurt_sound: [u8; 16], - pub death_sound: [u8; 16], + pub parts: [NPC; 20], + pub hurt_sound: [u8; 20], + pub death_sound: [u8; 20], } impl BossNPC { @@ -35,7 +35,7 @@ impl BossNPC { let mut part = NPC::empty(); part.cond.set_drs_boss(true); part - }; 16]; + }; 20]; parts[0].cond.set_alive(true); for (i, part) in parts.iter_mut().enumerate() { part.rng.load_state((i @@ -48,8 +48,8 @@ impl BossNPC { BossNPC { boss_type: 0, parts, - hurt_sound: [0; 16], - death_sound: [0; 16], + hurt_sound: [0; 20], + death_sound: [0; 20], } } } @@ -63,7 +63,7 @@ impl GameEntity<([&mut Player; 2], &BTreeMap>, &mut Stage, &Bu match self.boss_type { 1 => self.tick_b01_omega(state, players, map, bullet_manager), 2 => self.tick_b02_balfrog(state, players), - 3 => self.tick_b03_monster_x(), + 3 => self.tick_b03_monster_x(state, players, map), 4 => self.tick_b04_core(), 5 => self.tick_b05_ironhead(), 6 => self.tick_b06_twins(), @@ -83,7 +83,7 @@ impl GameEntity<([&mut Player; 2], &BTreeMap>, &mut Stage, &Bu fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult { let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, state.npc_table.tex_npc2_name.as_str())?; - for npc in self.parts.iter() { + for npc in self.parts.iter().rev() { if !npc.cond.alive() || npc.cond.hidden() { continue; } diff --git a/src/npc/boss/monster_x.rs b/src/npc/boss/monster_x.rs index 98d10ef..648e80b 100644 --- a/src/npc/boss/monster_x.rs +++ b/src/npc/boss/monster_x.rs @@ -1,7 +1,840 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; + +use ggez::GameResult; +use num_traits::{abs, clamp}; + +use crate::caret::CaretType; +use crate::common::{CDEG_RAD, Direction, Rect}; +use crate::npc::{NPC, NPCMap}; use crate::npc::boss::BossNPC; +use crate::player::Player; +use crate::shared_game_state::SharedGameState; -impl BossNPC { - pub(crate) fn tick_b03_monster_x(&mut self) { +impl NPC { + pub(crate) fn tick_n158_fish_missile(&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.action_counter2 = match self.direction { + Direction::Left => 0xa0, + Direction::Up => 0xe0, + Direction::Right => 0x20, + Direction::Bottom => 0x60, + Direction::FacingPlayer => 0, + }; + } + + let radians = self.action_counter2 as f64 * CDEG_RAD; + self.vel_x = 2 * (radians.cos() * 512.0) as isize; + self.vel_y = 2 * (radians.sin() * 512.0) as isize; + self.x += self.vel_x; + self.y += self.vel_y; + + let player = self.get_closest_player_mut(players); + let direction = f64::atan2(-(self.y - player.y) as f64, -(self.x - player.x) as f64); + + if direction < radians { + if radians - direction < std::f64::consts::PI { + self.action_counter2 = self.action_counter2.wrapping_sub(1) & 0xff; + } else { + self.action_counter2 = (self.action_counter2 + 1) & 0xff; + } + } else { + if direction - radians < std::f64::consts::PI { + self.action_counter2 = (self.action_counter2 + 1) & 0xff; + } else { + self.action_counter2 = self.action_counter2.wrapping_sub(1) & 0xff; + } + } + } + _ => {} + } + + self.anim_counter += 1; + if self.anim_counter > 2 { + self.anim_counter = 0; + state.create_caret(self.x, self.y, CaretType::Exhaust, Direction::FacingPlayer); + } + + self.anim_num = (self.action_counter2 + 0x10) / 0x20; + + if self.anim_num > 7 { + self.anim_num = 7; + } + + self.anim_rect = state.constants.npc.n158_fish_missile[self.anim_num as usize]; + + Ok(()) + } +} + +impl BossNPC { + pub(crate) fn tick_b03_monster_x(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], npc_map: &BTreeMap>) { + match self.parts[0].action_num { + 0 => { + self.parts[0].life = 1; + self.parts[0].x = -320 * 0x200; + } + 1 => { + self.parts[0].life = 700; + self.parts[0].exp = 1; + self.parts[0].action_num = 2; + self.parts[0].anim_num = 0; + self.parts[0].x = 2048 * 0x200; + self.parts[0].y = 200 * 0x200; + self.parts[0].size = 3; + self.parts[0].event_num = 1000; + self.parts[0].hit_bounds = Rect { + left: 24 * 0x200, + top: 24 * 0x200, + right: 24 * 0x200, + bottom: 24 * 0x200, + }; + self.parts[0].npc_flags.set_ignore_solidity(true); + self.parts[0].npc_flags.set_event_when_killed(true); + self.parts[0].npc_flags.set_show_damage(true); + self.hurt_sound[0] = 54; + + self.parts[1].cond.set_alive(true); + self.parts[1].size = 3; + self.parts[1].direction = Direction::Left; + self.parts[1].display_bounds = Rect { + left: 24 * 0x200, + top: 24 * 0x200, + right: 24 * 0x200, + bottom: 24 * 0x200, + }; + self.parts[1].npc_flags.set_ignore_solidity(true); + + self.parts[2] = self.parts[1]; + self.parts[2].direction = Direction::Right; + + self.parts[3].cond.set_alive(true); + self.parts[3].life = 60; + self.parts[3].size = 2; + self.parts[3].target_x = 0; + self.parts[3].display_bounds = Rect { + left: 8 * 0x200, + top: 8 * 0x200, + right: 8 * 0x200, + bottom: 8 * 0x200, + }; + self.parts[3].hit_bounds = Rect { + left: 5 * 0x200, + top: 5 * 0x200, + right: 5 * 0x200, + bottom: 5 * 0x200, + }; + self.parts[3].npc_flags.set_ignore_solidity(true); + self.hurt_sound[3] = 54; + self.death_sound[3] = 71; + + self.parts[4] = self.parts[3]; + self.parts[3].target_x = 1; + + self.parts[5] = self.parts[3]; + self.parts[6] = self.parts[3]; + self.parts[5].target_x = 2; + self.parts[6].target_x = 3; + self.parts[5].life = 100; + self.parts[6].life = 100; + + self.parts[7].cond.set_alive(true); + self.parts[7].x = self.parts[0].x; + self.parts[7].y = self.parts[0].y; + self.parts[7].size = 3; + self.parts[7].anim_num = 0; + self.parts[7].display_bounds = Rect { + left: 52 * 0x200, + top: 24 * 0x200, + right: 52 * 0x200, + bottom: 24 * 0x200, + }; + self.parts[7].hit_bounds = Rect { + left: 8 * 0x200, + top: 24 * 0x200, + right: 8 * 0x200, + bottom: 16 * 0x200, + }; + self.parts[7].npc_flags.set_ignore_solidity(true); + + self.parts[9].cond.set_alive(true); + self.parts[9].x = self.parts[0].x - 64 * 0x200; + self.parts[9].y = self.parts[0].y - 56 * 0x200; + self.parts[9].size = 3; + self.parts[9].action_num = 0; + self.parts[9].direction = Direction::Up; + self.parts[9].display_bounds = Rect { + left: 36 * 0x200, + top: 8 * 0x200, + right: 36 * 0x200, + bottom: 24 * 0x200, + }; + self.parts[9].hit_bounds = Rect { + left: 28 * 0x200, + top: 8 * 0x200, + right: 28 * 0x200, + bottom: 16 * 0x200, + }; + self.hurt_sound[9] = 52; + self.parts[9].npc_flags.set_rear_and_top_not_hurt(true); + self.parts[9].npc_flags.set_ignore_solidity(true); + self.parts[9].npc_flags.set_invulnerable(true); + self.parts[9].npc_flags.set_solid_soft(true); + + self.parts[10] = self.parts[9]; + self.parts[10].x = self.parts[0].x + 64 * 0x200; + + self.parts[11] = self.parts[9]; + self.parts[11].x = self.parts[0].x - 64 * 0x200; + self.parts[11].y = self.parts[0].y + 56 * 0x200; + self.parts[11].direction = Direction::Bottom; + self.parts[11].display_bounds.top = 24 * 0x200; + self.parts[11].display_bounds.bottom = 8 * 0x200; + self.parts[11].hit_bounds.top = 16 * 0x200; + self.parts[11].hit_bounds.bottom = 8 * 0x200; + + self.parts[12] = self.parts[11]; + self.parts[12].x = self.parts[0].x + 64 * 0x200; + + self.parts[13] = self.parts[9]; + self.parts[13].display_bounds = Rect { + left: 30 * 0x200, + top: 16 * 0x200, + right: 42 * 0x200, + bottom: 16 * 0x200, + }; + self.parts[13].action_counter2 = 9; + self.parts[13].anim_num = 0; + self.parts[13].npc_flags.0 = 0; + self.parts[13].npc_flags.set_ignore_solidity(true); + + self.parts[14] = self.parts[13]; + self.parts[14].action_counter2 = 10; + self.parts[14].anim_num = 1; + self.parts[14].display_bounds.left = 42 * 0x200; + self.parts[14].display_bounds.right = 30 * 0x200; + + self.parts[15] = self.parts[13]; + self.parts[15].action_counter2 = 11; + self.parts[15].anim_num = 2; + self.parts[15].display_bounds.top = 16 * 0x200; + self.parts[15].display_bounds.bottom = 16 * 0x200; + + self.parts[16] = self.parts[15]; + self.parts[16].action_counter2 = 12; + self.parts[16].anim_num = 3; + self.parts[16].display_bounds.left = 42 * 0x200; + self.parts[16].display_bounds.right = 30 * 0x200; + } + 10 | 11 => { + if self.parts[0].action_num == 10 { + self.parts[0].action_num = 11; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 = 0; + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 100 { + self.parts[0].action_counter = 0; + + let player_idx = self.parts[0].get_closest_player_idx_mut(&players); + self.parts[0].action_num = if self.parts[0].x > players[player_idx].x { 100 } else { 200 }; + } + } + 100 | 101 => { + if self.parts[0].action_num == 100 { + self.parts[0].action_num = 101; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 += 1; + } + + self.parts[0].action_counter += 1; + + match self.parts[0].action_counter { + 4 => self.parts[9].action_num = 100, + 8 => self.parts[10].action_num = 100, + 10 => self.parts[11].action_num = 100, + 12 => self.parts[12].action_num = 100, + _ => {} + } + + if self.parts[0].action_counter > 120 && self.parts[0].action_counter2 > 2 { + self.parts[0].action_num = 300; + } + + let player_idx = self.parts[0].get_closest_player_idx_mut(&players); + if self.parts[0].action_counter > 121 && players[player_idx].x > self.parts[0].x { + self.parts[0].action_num = 200; + } + } + 200 | 201 => { + if self.parts[0].action_num == 200 { + self.parts[0].action_num = 201; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 += 1; + } + + self.parts[0].action_counter += 1; + + match self.parts[0].action_counter { + 4 => self.parts[9].action_num = 200, + 8 => self.parts[10].action_num = 200, + 10 => self.parts[11].action_num = 200, + 12 => self.parts[12].action_num = 200, + _ => {} + } + + if self.parts[0].action_counter > 120 && self.parts[0].action_counter2 > 2 { + self.parts[0].action_num = 400; + } + + let player_idx = self.parts[0].get_closest_player_idx_mut(&players); + if self.parts[0].action_counter > 121 && players[player_idx].x < self.parts[0].x { + self.parts[0].action_num = 100; + } + } + 300 | 301 => { + if self.parts[0].action_num == 300 { + self.parts[0].action_num = 301; + self.parts[0].action_counter = 0; + } + + self.parts[0].action_counter += 1; + + match self.parts[0].action_counter { + 4 => self.parts[9].action_num = 300, + 8 => self.parts[10].action_num = 300, + 10 => self.parts[11].action_num = 300, + 12 => self.parts[12].action_num = 300, + _ => {} + } + + if self.parts[0].action_counter > 50 { + if !self.parts[3].cond.alive() && !self.parts[4].cond.alive() + && !self.parts[5].cond.alive() && !self.parts[6].cond.alive() { + self.parts[0].action_num = 600; + } else { + self.parts[0].action_num = 500; + } + } + } + 400 | 401 => { + if self.parts[0].action_num == 400 { + self.parts[0].action_num = 401; + self.parts[0].action_counter = 0; + } + + self.parts[0].action_counter += 1; + + match self.parts[0].action_counter { + 4 => self.parts[9].action_num = 400, + 8 => self.parts[10].action_num = 400, + 10 => self.parts[11].action_num = 400, + 12 => self.parts[12].action_num = 400, + _ => {} + } + + if self.parts[0].action_counter > 50 { + if !self.parts[3].cond.alive() && !self.parts[4].cond.alive() + && !self.parts[5].cond.alive() && !self.parts[6].cond.alive() { + self.parts[0].action_num = 600; + } else { + self.parts[0].action_num = 500; + } + } + } + 500 | 501 => { + if self.parts[0].action_num == 500 { + self.parts[0].action_num = 501; + self.parts[0].action_counter = 0; + self.parts[1].action_num = 10; + self.parts[2].action_num = 10; + } + + self.parts[0].action_counter += 1; + + if self.parts[0].action_counter > 300 { + self.parts[0].action_num = 502; + self.parts[0].action_counter = 0; + } + + if !self.parts[3].cond.alive() && !self.parts[4].cond.alive() + && !self.parts[5].cond.alive() && !self.parts[6].cond.alive() { + self.parts[0].action_num = 502; + self.parts[0].action_counter = 0; + } + } + 502 | 503 => { + if self.parts[0].action_num == 502 { + self.parts[0].action_num = 503; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 = 0; + self.parts[1].action_num = 20; + self.parts[2].action_num = 20; + } + + self.parts[0].action_counter += 1; + + if self.parts[0].action_counter > 50 { + let player_idx = self.parts[0].get_closest_player_idx_mut(&players); + self.parts[0].action_num = if self.parts[0].x > players[player_idx].x { 100 } else { 200 }; + } + } + 600 | 601 => { + if self.parts[0].action_num == 600 { + self.parts[0].action_num = 601; + self.parts[0].action_counter = 0; + self.parts[0].vel_y2 = self.parts[0].life as isize; + self.parts[1].action_num = 30; + self.parts[2].action_num = 30; + } + + self.parts[0].action_counter += 1; + if (self.parts[0].life as isize) < self.parts[0].vel_y2.saturating_sub(200) + || self.parts[0].action_counter > 300 { + self.parts[0].action_num = 602; + self.parts[0].action_counter = 0; + } + } + 602 | 603 => { + if self.parts[0].action_num == 602 { + self.parts[0].action_num = 603; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 = 0; + self.parts[1].action_num = 40; + self.parts[2].action_num = 40; + } + + self.parts[0].action_counter += 1; + + if self.parts[0].action_counter > 50 { + let player_idx = self.parts[0].get_closest_player_idx_mut(&players); + self.parts[0].action_num = if self.parts[0].x > players[player_idx].x { 100 } else { 200 }; + } + } + 1000 => { + state.quake_counter = 2; + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 8 == 0 { + state.sound_manager.play_sfx(52); + } + + let mut npc = NPCMap::create_npc(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + self.parts[0].rng.range(-72..72) as isize * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-64..64) as isize * 0x200; + state.new_npcs.push(npc); + + if self.parts[0].action_counter > 100 { + self.parts[0].action_num = 1001; + self.parts[0].action_counter = 0; + // todo flash + state.sound_manager.play_sfx(35); + } + } + 1001 => { + state.quake_counter = 40; + self.parts[0].action_counter += 1; + + if self.parts[0].action_counter > 50 { + for part in self.parts.iter_mut() { + part.cond.set_alive(false); + } + + for npc_cell in npc_map.values() { + let npc = npc_cell.borrow_mut(); + if npc.cond.alive() && npc.npc_type == 158 { + npc.cond.set_alive(false); + } + } + + let mut npc = NPCMap::create_npc(159, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x; + npc.y = self.parts[1].y - 24 * 0x200; + + state.new_npcs.push(npc); + } + } + _ => {} + } + + self.tick_b03_monster_x_track(9, state, &players); + self.tick_b03_monster_x_track(10, state, &players); + self.tick_b03_monster_x_track(11, state, &players); + self.tick_b03_monster_x_track(12, state, &players); + + self.parts[0].x += (((self.parts[9].x + self.parts[10].x + self.parts[11].x + self.parts[12].x) / 4) - self.parts[0].x) / 16; + self.tick_b03_monster_x_face(7, state); + + self.tick_b03_monster_x_frame(13, state); + self.tick_b03_monster_x_frame(14, state); + self.tick_b03_monster_x_frame(15, state); + self.tick_b03_monster_x_frame(16, state); + + self.tick_b03_monster_x_shield(1, state); + self.tick_b03_monster_x_shield(2, state); + + if self.parts[3].cond.alive() { self.tick_b03_monster_x_eye(3, state, &players); } + if self.parts[4].cond.alive() { self.tick_b03_monster_x_eye(4, state, &players); } + if self.parts[5].cond.alive() { self.tick_b03_monster_x_eye(5, state, &players); } + if self.parts[6].cond.alive() { self.tick_b03_monster_x_eye(6, state, &players); } + + if self.parts[0].life == 0 && self.parts[0].action_num < 1000 { + self.parts[0].action_num = 1000; + self.parts[0].action_counter = 0; + self.parts[0].shock = 150; + + self.parts[9].action_num = 300; + self.parts[10].action_num = 300; + self.parts[11].action_num = 300; + self.parts[12].action_num = 300; + } + } + + fn tick_b03_monster_x_face(&mut self, i: usize, state: &mut SharedGameState) { + match self.parts[i].action_num { + 0 => { + self.parts[0].npc_flags.set_shootable(false); + self.parts[i].anim_num = 2; + } + 10 | 11 => { + if self.parts[i].action_num == 10 { + self.parts[i].action_num = 11; + self.parts[i].action_counter = (self.parts[i].target_x * 10 + 40) as u16; + self.parts[i].anim_num = 2; + self.parts[0].npc_flags.set_shootable(true); + } + + if self.parts[0].shock > 0 { + self.parts[i].action_counter2 += 1; + if self.parts[i].action_counter2 / 2 % 2 != 0 { + self.parts[i].anim_num = 1; + } else { + self.parts[i].anim_num = 0; + } + } else { + self.parts[i].anim_num = 0; + } + } + _ => {} + } + + self.parts[7].x = self.parts[0].x; + self.parts[7].y = self.parts[0].y; + + self.parts[i].anim_rect = state.constants.npc.b03_monster_x[self.parts[i].anim_num as usize]; + } + + fn tick_b03_monster_x_track(&mut self, i: usize, state: &mut SharedGameState, players: &[&mut Player; 2]) { + match self.parts[i].action_num { + 10 => { + self.parts[i].anim_num = 0; + self.parts[i].npc_flags.set_bouncy(false); + } + 100 | 101 => { + if self.parts[i].action_num == 100 { + self.parts[i].action_num = 101; + self.parts[i].action_counter = 0; + self.parts[i].anim_num = 2; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(true); + } + + self.parts[i].action_counter += 1; + if self.parts[i].action_counter > 30 { + self.parts[i].action_num = 102; + } + + self.parts[i].animate(0, 2, 3); + self.parts[i].vel_x -= 0x20; + } + 102 | 103 => { + if self.parts[i].action_num == 102 { + self.parts[i].action_num = 103; + self.parts[i].anim_num = 0; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(false); + } + + self.parts[i].action_counter += 1; + self.parts[i].animate(1, 0, 1); + self.parts[i].vel_x -= 0x20; + } + 200 | 201 => { + if self.parts[i].action_num == 200 { + self.parts[i].action_num = 201; + self.parts[i].action_counter = 0; + self.parts[i].anim_num = 4; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(true); + self.parts[i].npc_flags.set_rear_and_top_not_hurt(true); + } + + self.parts[i].action_counter += 1; + if self.parts[i].action_counter > 30 { + self.parts[i].action_num = 202; + } + + self.parts[i].animate(0, 4, 5); + self.parts[i].vel_x += 0x20; + } + 202 | 203 => { + if self.parts[i].action_num == 202 { + self.parts[i].action_num = 203; + self.parts[i].anim_num = 0; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(false); + } + + self.parts[i].action_counter += 1; + + self.parts[i].animate(1, 0, 1); + self.parts[i].vel_x += 0x20; + } + 300 | 301 => { + if self.parts[i].action_num == 300 { + self.parts[i].action_num = 301; + self.parts[i].anim_num = 4; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(true); + } + + self.parts[i].animate(0, 4, 5); + + self.parts[i].vel_x += 0x20; + if self.parts[i].vel_x > 0 { + self.parts[i].vel_x = 0; + self.parts[i].action_num = 10; + } + } + 400 | 401 => { + if self.parts[i].action_num == 400 { + self.parts[i].action_num = 401; + self.parts[i].anim_num = 2; + self.parts[i].anim_counter = 0; + self.parts[i].npc_flags.set_bouncy(true); + } + + self.parts[i].animate(0, 2, 3); + + self.parts[i].vel_x -= 0x20; + if self.parts[i].vel_x < 0 { + self.parts[i].vel_x = 0; + self.parts[i].action_num = 10; + } + } + _ => {} + } + + if self.parts[i].action_counter % 2 == 1 && [101, 201, 301, 401].contains(&self.parts[i].action_num) { + state.sound_manager.play_sfx(112); + } + + if self.parts[i].action_counter % 4 == 1 && [103, 203].contains(&self.parts[i].action_num) { + state.sound_manager.play_sfx(111); + } + + let player_idx = self.parts[i].get_closest_player_idx_mut(players); + if self.parts[i].action_num >= 100 && abs(players[player_idx].y - self.parts[i].y) < 4 * 0x200 { + self.parts[i].damage = 10; + self.parts[i].npc_flags.set_rear_and_top_not_hurt(true); + } else { + self.parts[i].damage = 0; + self.parts[i].npc_flags.set_rear_and_top_not_hurt(false); + } + + self.parts[i].vel_x = clamp(self.parts[i].vel_x, -0x400, 0x400); + self.parts[i].x += self.parts[i].vel_x; + + let dir_offset = if self.parts[i].direction == Direction::Up { 3 } else { 9 }; + + self.parts[i].anim_rect = state.constants.npc.b03_monster_x[self.parts[i].anim_num as usize + dir_offset]; + } + + fn tick_b03_monster_x_frame(&mut self, i: usize, state: &mut SharedGameState) { + match self.parts[i].action_num { + 10 | 11 => { + if self.parts[i].action_num == 10 { + self.parts[i].action_num = 11; + self.parts[i].action_counter = (self.parts[i].anim_num * 30 + 30) as u16; + } + + if self.parts[i].action_counter > 0 { + self.parts[i].action_counter -= 1; + } else { + self.parts[i].action_counter = 120; + state.sound_manager.play_sfx(39); + + let mut npc = NPCMap::create_npc(158, &state.npc_table); + npc.cond.set_alive(true); + + match self.parts[i].anim_num { + 0 => { + npc.direction = Direction::Bottom; + npc.x = self.parts[i].x + -30 * 0x200; + npc.y = self.parts[i].y + 6 * 0x200; + } + 1 => { + npc.direction = Direction::Right; + npc.x = self.parts[i].x + 30 * 0x200; + npc.y = self.parts[i].y + 6 * 0x200; + } + 2 => { + npc.direction = Direction::Left; + npc.x = self.parts[i].x - 30 * 0x200; + npc.y = self.parts[i].y - 6 * 0x200; + } + 3 => { + npc.direction = Direction::Up; + npc.x = self.parts[i].x + 30 * 0x200; + npc.y = self.parts[i].y - 6 * 0x200; + } + _ => {} + } + + state.new_npcs.push(npc); + } + } + _ => {} + } + + self.parts[i].x = (self.parts[0].x + self.parts[self.parts[i].action_counter2 as usize].x) / 2; + self.parts[i].y = (self.parts[0].y + self.parts[self.parts[i].action_counter2 as usize].y) / 2; + + self.parts[i].anim_rect = state.constants.npc.b03_monster_x[15 + self.parts[i].anim_num as usize]; + } + + fn tick_b03_monster_x_shield(&mut self, i: usize, state: &mut SharedGameState) { + match self.parts[i].action_num { + 10 => { + self.parts[i].target_x += 0x200; + + if self.parts[i].target_x > 32 * 0x200 { + self.parts[i].target_x = 32 * 0x200; + self.parts[i].action_num = 0; + self.parts[3].action_num = 10; + self.parts[4].action_num = 10; + self.parts[5].action_num = 10; + self.parts[6].action_num = 10; + } + } + 20 => { + self.parts[i].target_x -= 0x200; + + if self.parts[i].target_x < 0 { + self.parts[i].target_x = 0; + self.parts[i].action_num = 0; + self.parts[3].action_num = 0; + self.parts[4].action_num = 0; + self.parts[5].action_num = 0; + self.parts[6].action_num = 0; + } + } + 30 => { + self.parts[i].target_x += 0x200; + + if self.parts[i].target_x > 20 * 0x200 { + self.parts[i].target_x = 20 * 0x200; + self.parts[i].action_num = 0; + self.parts[7].action_num = 10; + self.parts[13].action_num = 10; + self.parts[14].action_num = 10; + self.parts[15].action_num = 10; + self.parts[16].action_num = 10; + } + } + 40 => { + self.parts[i].target_x -= 0x200; + + if self.parts[i].target_x < 0 { + self.parts[i].target_x = 0; + self.parts[i].action_num = 0; + self.parts[7].action_num = 0; + self.parts[13].action_num = 0; + self.parts[14].action_num = 0; + self.parts[15].action_num = 0; + self.parts[16].action_num = 0; + } + } + _ => {} + } + + self.parts[i].x = self.parts[0].x + self.parts[i].direction.vector_x() * (24 * 0x200 + self.parts[i].target_x); + self.parts[i].y = self.parts[0].y; + + let dir_offset = if self.parts[i].direction == Direction::Left { 19 } else { 20 }; + + self.parts[i].anim_rect = state.constants.npc.b03_monster_x[dir_offset]; + } + + fn tick_b03_monster_x_eye(&mut self, i: usize, state: &mut SharedGameState, players: &[&mut Player; 2]) { + match self.parts[i].action_num { + 0 => { + self.parts[i].npc_flags.set_shootable(false); + self.parts[i].anim_num = 0; + } + 10 | 11 => { + if self.parts[i].action_num == 10 { + self.parts[i].action_num = 11; + self.parts[i].action_counter = (self.parts[i].target_x * 10 + 40) as u16; + self.parts[i].npc_flags.set_shootable(true); + } + + self.parts[i].anim_num = if self.parts[i].action_counter < 16 + && self.parts[i].action_counter / 2 % 2 != 0 { 1 } else { 0 }; + + if self.parts[i].action_counter > 0 { + self.parts[i].action_counter -= 1; + } else { + let player_idx = self.parts[i].get_closest_player_idx_mut(players); + let px = self.parts[i].x - players[player_idx].x; + let py = self.parts[i].y - players[player_idx].y; + + let deg = f64::atan2(py as f64, px as f64) + + self.parts[i].rng.range(-2..2) as f64 * CDEG_RAD; + + let mut npc = NPCMap::create_npc(156, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[i].x; + npc.y = self.parts[i].y; + npc.vel_x = (deg.cos() * -1536.0) as isize; + npc.vel_y = (deg.sin() * -1536.0) as isize; + + state.new_npcs.push(npc); + + state.sound_manager.play_sfx(39); + self.parts[i].action_counter = 40; + } + } + _ => {} + } + + match self.parts[i].target_x { + 0 => { + self.parts[i].x = self.parts[0].x - 22 * 0x200; + self.parts[i].y = self.parts[0].y - 16 * 0x200; + } + 1 => { + self.parts[i].x = self.parts[0].x + 28 * 0x200; + self.parts[i].y = self.parts[0].y - 16 * 0x200; + } + 2 => { + self.parts[i].x = self.parts[0].x - 15 * 0x200; + self.parts[i].y = self.parts[0].y + 14 * 0x200; + } + 3 => { + self.parts[i].x = self.parts[0].x + 17 * 0x200; + self.parts[i].y = self.parts[0].y + 14 * 0x200; + } + _ => {} + } + + self.parts[i].anim_rect = state.constants.npc.b03_monster_x[21 + self.parts[i].target_x as usize + 4 * self.parts[i].anim_num as usize]; } } diff --git a/src/npc/boss/omega.rs b/src/npc/boss/omega.rs index c41180d..952e4eb 100644 --- a/src/npc/boss/omega.rs +++ b/src/npc/boss/omega.rs @@ -13,9 +13,8 @@ use crate::shared_game_state::SharedGameState; impl NPC { pub(crate) fn tick_n048_omega_projectiles(&mut self, state: &mut SharedGameState) -> GameResult { - if self.flags.hit_left_wall() && self.vel_x < 0 { - self.vel_x = -self.vel_x; - } else if self.flags.hit_right_wall() && self.vel_x > 0 { + if (self.flags.hit_left_wall() && self.vel_x < 0) + || (self.flags.hit_right_wall() && self.vel_x > 0) { self.vel_x = -self.vel_x; } else if self.flags.hit_bottom_wall() { self.action_counter2 += 1; @@ -202,7 +201,7 @@ impl BossNPC { state.sound_manager.play_sfx(39); } - if self.parts[0].action_counter == 200 || bullet_manager.count_bullets_all(6) > 0 { + if self.parts[0].action_counter == 200 || bullet_manager.count_bullets_type_idx_all(6) > 0 { self.parts[0].action_num = 70; self.parts[0].anim_counter = 0; @@ -275,7 +274,7 @@ impl BossNPC { } 120 => { self.parts[0].action_counter += 1; - if self.parts[0].action_counter == 50 || bullet_manager.count_bullets_all(6) > 0 { + if self.parts[0].action_counter == 50 || bullet_manager.count_bullets_type_idx_all(6) > 0 { self.parts[0].action_num = 130; self.parts[0].action_counter = 0; self.parts[0].anim_counter = 0; diff --git a/src/npc/maze.rs b/src/npc/maze.rs index 85949b5..67f6459 100644 --- a/src/npc/maze.rs +++ b/src/npc/maze.rs @@ -5,6 +5,7 @@ use crate::common::Direction; use crate::npc::NPC; use crate::player::Player; use crate::shared_game_state::SharedGameState; +use crate::caret::CaretType; impl NPC { pub(crate) fn tick_n154_gaudi_dead(&mut self, state: &mut SharedGameState) -> GameResult { @@ -14,6 +15,22 @@ impl NPC { 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); + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_num = (self.anim_num + 1) % 3; + + self.anim_rect = state.constants.npc.n156_gaudi_projectile[self.anim_num as usize]; + + Ok(()) + } + pub(crate) fn tick_n361_gaudi_dashing(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 => { diff --git a/src/npc/mod.rs b/src/npc/mod.rs index d088ecf..0fe9525 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -285,7 +285,9 @@ impl GameEntity<([&mut Player; 2], &BTreeMap>, &mut Stage, &Bu 150 => self.tick_n150_quote(state, players), 151 => self.tick_n151_blue_robot_standing(state), 154 => self.tick_n154_gaudi_dead(state), + 156 => self.tick_n156_gaudi_projectile(state), 157 => self.tick_n157_vertical_moving_block(state, players), + 158 => self.tick_n158_fish_missile(state, players), 192 => self.tick_n192_scooter(state), 193 => self.tick_n193_broken_scooter(state), 194 => self.tick_n194_broken_blue_robot(state), diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 719acd0..d6a82b4 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -1301,7 +1301,7 @@ impl Scene for GameScene { Ok(()) } - fn draw_tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + fn draw_tick(&mut self, state: &mut SharedGameState) -> GameResult { self.frame.prev_x = self.frame.x; self.frame.prev_y = self.frame.y; self.player1.prev_x = self.player1.x; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 07dba72..036b8c3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7,14 +7,21 @@ pub mod game_scene; pub mod loading_scene; pub mod title_scene; +/// Implement this trait on any object that represents an interactive game screen. pub trait Scene { + /// Called when the scene is shown. fn init(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + /// Called at game tick. Perform any game state updates there. fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } - fn draw_tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + /// Called before draws between two ticks to update previous positions used for interpolation. + /// DO NOT perform updates of the game state there. + fn draw_tick(&mut self, _state: &mut SharedGameState) -> GameResult { Ok(()) } + /// Called during frame rendering operation. fn draw(&self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + /// Independent draw meant for debug overlay, that lets you mutate the game state. fn debug_overlay_draw(&mut self, _game_ui: &mut Components, _state: &mut SharedGameState, _ctx: &mut Context, _frame: &mut imgui::Ui) -> GameResult { Ok(()) } } diff --git a/src/texture_set.rs b/src/texture_set.rs index 8c86664..e52d6a3 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -4,7 +4,7 @@ use std::io::{BufReader, Read, Seek, SeekFrom}; use ggez; use ggez::{Context, GameError, GameResult, graphics}; use ggez::filesystem; -use ggez::graphics::{Color, Drawable, DrawMode, DrawParam, FilterMode, Image, Mesh, mint, Rect}; +use ggez::graphics::{Drawable, DrawMode, DrawParam, FilterMode, Image, Mesh, mint, Rect}; use ggez::graphics::spritebatch::SpriteBatch; use ggez::nalgebra::{Point2, Vector2}; use image::RgbaImage; @@ -182,20 +182,6 @@ impl TextureSet { Image::from_rgba8(ctx, width as u16, height as u16, img.as_ref()) } - fn load_image_from_buf(&self, ctx: &mut Context, buf: &[u8]) -> GameResult { - let img = { - let image = image::load_from_memory(buf)?; - let mut rgba = image.to_rgba(); - if image.color().channel_count() != 4 { - TextureSet::make_transparent(&mut rgba); - } - rgba - }; - let (width, height) = img.dimensions(); - - Image::from_rgba8(ctx, width as u16, height as u16, img.as_ref()) - } - pub fn load_texture(&self, ctx: &mut Context, constants: &EngineConstants, name: &str) -> GameResult { let path = self.paths.iter().find_map(|s| FILE_TYPES .iter()