diff --git a/src/caret.rs b/src/caret.rs index 43593cc..fa71952 100644 --- a/src/caret.rs +++ b/src/caret.rs @@ -196,11 +196,11 @@ impl Caret { } match self.direction { - Direction::Left => self.x -= 0x400, // 2.0fix9 + Direction::Left => self.x -= 0x400, Direction::Up => self.y -= 0x400, Direction::Right => self.x += 0x400, Direction::Bottom => self.y += 0x400, - Direction::FacingPlayer => {} + Direction::FacingPlayer => (), } } CaretType::DrownedQuote => { @@ -302,8 +302,8 @@ impl Caret { if self.anim_num == 0 { match self.direction { Direction::Left => { - self.vel_x = rng.range(-0x600..0x600) as i32; // -3.0fix9..3.0fix9 - self.vel_y = rng.range(-0x200..0x200) as i32; // -1.0fix9..1.0fix9 + self.vel_x = rng.range(-0x600..0x600) as i32; + self.vel_y = rng.range(-0x200..0x200) as i32; } Direction::Up => { self.vel_y = rng.range(-3..-1) as i32 * 0x200; diff --git a/src/common.rs b/src/common.rs index 50a22c7..14c2b0c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -530,3 +530,29 @@ impl From for [f32; 4] { [color.r, color.g, color.b, color.a] } } + +pub trait SliceExt { + type Item; + + fn get_two_mut(&mut self, a: usize, b: usize) -> Option<(&mut Self::Item, &mut Self::Item)>; +} + +impl SliceExt for [T] { + type Item = T; + + fn get_two_mut(&mut self, a: usize, b: usize) -> Option<(&mut Self::Item, &mut Self::Item)> { + if a == b { + None + } else { + if a >= self.len() || b >= self.len() { + None + } else { + unsafe { + let ar = &mut *(self.get_unchecked_mut(a) as *mut _); + let br = &mut *(self.get_unchecked_mut(b) as *mut _); + Some((ar, br)) + } + } + } + } +} diff --git a/src/components/falling_island.rs b/src/components/falling_island.rs new file mode 100644 index 0000000..b097a56 --- /dev/null +++ b/src/components/falling_island.rs @@ -0,0 +1,56 @@ +use crate::common::{Color, Rect}; +use crate::entity::GameEntity; +use crate::frame::Frame; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::graphics; +use crate::scripting::tsc::text_script::TextScriptExecutionState; +use crate::shared_game_state::SharedGameState; + +pub struct FallingIsland {} + +impl FallingIsland { + pub fn new() -> FallingIsland { + FallingIsland {} + } +} + +impl GameEntity<()> for FallingIsland { + fn tick(&mut self, state: &mut SharedGameState, custom: ()) -> GameResult { + Ok(()) + } + + fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult { + let (pos_x, pos_y) = + if let TextScriptExecutionState::FallingIsland(_, _, pos_x, pos_y, _, _) = state.textscript_vm.state { + (pos_x, pos_y) + } else { + return Ok(()); + }; + + let off_x = (state.canvas_size.0 - 320.0) * 0.5; + let clip_rect: Rect = Rect::new_size( + ((off_x + 80.0) * state.scale) as _, + (80.0 * state.scale) as _, + (160.0 * state.scale) as _, + (80.0 * state.scale) as _, + ); + + graphics::clear(ctx, Color::from_rgb(0, 0, 32)); + graphics::set_clip_rect(ctx, Some(clip_rect))?; + + static RECT_BG: Rect = Rect { left: 0, top: 0, right: 160, bottom: 80 }; + static RECT_ISLAND: Rect = Rect { left: 160, top: 0, right: 200, bottom: 24 }; + static RECT_TERRAIN: Rect = Rect { left: 160, top: 48, right: 320, bottom: 80 }; + + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &state.npc_table.tex_npc1_name)?; + batch.add_rect(off_x + 80.0, 80.0, &RECT_BG); + batch.add_rect(off_x + (pos_x as f32 / 512.0) - 20.0, (pos_y as f32 / 512.0) - 12.0, &RECT_ISLAND); + batch.add_rect(off_x + 80.0, 128.0, &RECT_TERRAIN); + batch.draw(ctx)?; + + graphics::set_clip_rect(ctx, None)?; + + Ok(()) + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 9e48a63..7061d40 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,6 +1,7 @@ pub mod boss_life_bar; pub mod credits; pub mod draw_common; +pub mod falling_island; pub mod flash; pub mod hud; pub mod inventory; diff --git a/src/engine_constants/npcs.rs b/src/engine_constants/npcs.rs index eb484c3..e98568d 100644 --- a/src/engine_constants/npcs.rs +++ b/src/engine_constants/npcs.rs @@ -5,7 +5,6 @@ use crate::common::Rect; #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct NPCConsts { // pub n000_null: () // Defined in code - #[serde(default = "default_n001_experience")] pub n001_experience: [Rect; 6], @@ -13,7 +12,6 @@ pub struct NPCConsts { pub n002_behemoth: [Rect; 14], // pub n003_dead_enemy: () // Defined in code - #[serde(default = "default_n004_smoke")] pub n004_smoke: [Rect; 16], @@ -141,7 +139,6 @@ pub struct NPCConsts { pub n045_baby: [Rect; 3], // pub n046_hv_trigger: () // Defined in code - #[serde(default = "default_n047_sandcroc")] pub n047_sandcroc: [Rect; 5], @@ -230,7 +227,6 @@ pub struct NPCConsts { pub n075_kanpachi: [Rect; 2], // pub n076_flowers: () // Defined in code - #[serde(default = "default_n077_yamashita")] pub n077_yamashita: [Rect; 3], @@ -319,7 +315,6 @@ pub struct NPCConsts { pub n105_hey_bubble_low: [Rect; 2], // pub n106_hey_bubble_high: () // Defined in code - #[serde(default = "default_n107_malco_broken")] pub n107_malco_broken: [Rect; 10], @@ -456,7 +451,6 @@ pub struct NPCConsts { pub n151_blue_robot_standing: [Rect; 4], // pub n152_shutter_stuck: () // Defined in code - #[serde(default = "default_n153_gaudi")] pub n153_gaudi: [Rect; 14], @@ -572,7 +566,6 @@ pub struct NPCConsts { pub n190_broken_robot: [Rect; 2], // pub n191_water_level: () // Defined in code - #[serde(default = "default_n192_scooter")] pub n192_scooter: [Rect; 4], @@ -655,7 +648,6 @@ pub struct NPCConsts { pub n218_core_giant_ball: [Rect; 2], // pub n219_smoke_generator: () // Defined in code - #[serde(default = "default_n220_shovel_brigade")] pub n220_shovel_brigade: [Rect; 4], @@ -695,8 +687,8 @@ pub struct NPCConsts { #[serde(default = "default_n232_orangebell")] pub n232_orangebell: [Rect; 6], - #[serde(default = "default_n233_orangebell_hat")] - pub n233_orangebell_hat: [Rect; 8], + #[serde(default = "default_n233_orangebell_bat")] + pub n233_orangebell_bat: [Rect; 8], #[serde(default = "default_n234_red_flowers_picked")] pub n234_red_flowers_picked: [Rect; 2], @@ -726,7 +718,6 @@ pub struct NPCConsts { pub n242_bat_last_cave: [Rect; 8], // pub n243_bat_generator: () // Defined in code - #[serde(default = "default_n244_lava_drop")] pub n244_lava_drop: Rect, @@ -742,8 +733,8 @@ pub struct NPCConsts { #[serde(default = "default_n248_misery_boss_vanishing")] pub n248_misery_boss_vanishing: [Rect; 3], - #[serde(default = "default_n249_misery_boss_energy_shot")] - pub n249_misery_boss_energy_shot: [Rect; 2], + #[serde(default = "default_n249_misery_boss_appearing")] + pub n249_misery_boss_appearing: [Rect; 2], #[serde(default = "default_n250_misery_boss_lighting_ball")] pub n250_misery_boss_lighting_ball: [Rect; 3], @@ -787,14 +778,14 @@ pub struct NPCConsts { #[serde(default = "default_n263_doctor_boss")] pub n263_doctor_boss: [Rect; 18], - #[serde(default = "default_n264_doctor_boss_red_wave_projectile")] - pub n264_doctor_boss_red_wave_projectile: Rect, + #[serde(default = "default_n264_doctor_boss_red_projectile")] + pub n264_doctor_boss_red_projectile: Rect, - #[serde(default = "default_n265_doctor_boss_red_ball_projectile")] - pub n265_doctor_boss_red_ball_projectile: [Rect; 3], + #[serde(default = "default_n265_doctor_boss_red_projectile_trail")] + pub n265_doctor_boss_red_projectile_trail: [Rect; 3], - #[serde(default = "default_n266_doctor_boss_red_ball_projectile_bouncing")] - pub n266_doctor_boss_red_ball_projectile_bouncing: [Rect; 2], + #[serde(default = "default_n266_doctor_boss_red_projectile_bouncing")] + pub n266_doctor_boss_red_projectile_bouncing: [Rect; 2], #[serde(default = "default_n267_muscle_doctor")] pub n267_muscle_doctor: [Rect; 20], @@ -811,7 +802,6 @@ pub struct NPCConsts { // pub n271_ironhead_block: () // Defined in code // pub n272_ironhead_block_generator: () // Defined in code - #[serde(default = "default_n273_droll_projectile")] pub n273_droll_projectile: [Rect; 3], @@ -837,7 +827,6 @@ pub struct NPCConsts { pub n280_sue_teleported: [Rect; 4], // pub n281_doctor_energy_form: () // Defined in code - #[serde(default = "default_n282_mini_undead_core_active")] pub n282_mini_undead_core_active: [Rect; 3], @@ -869,17 +858,14 @@ pub struct NPCConsts { pub n291_mini_undead_core_inactive: [Rect; 2], // pub n292_quake: () // Defined in code - #[serde(default = "default_n293_undead_core_energy_shot")] pub n293_undead_core_energy_shot: [Rect; 2], // pub n294_quake_falling_block_generator: () // Defined in code - #[serde(default = "default_n295_cloud")] pub n295_cloud: [Rect; 4], // pub n296_cloud_generator: () // Defined in code - #[serde(default = "default_n297_sue_dragon_mouth")] pub n297_sue_dragon_mouth: Rect, @@ -896,7 +882,6 @@ pub struct NPCConsts { pub n301_misery_fish_missile: [Rect; 8], // pub n302_camera_focus_marker: () // Defined in code - #[serde(default = "default_n303_curly_machine_gun")] pub n303_curly_machine_gun: [Rect; 4], @@ -961,7 +946,6 @@ pub struct NPCConsts { pub n323_bute_spinning: [Rect; 4], // pub n324_bute_generator: () // Defined in code - #[serde(default = "default_n325_heavy_press_lighting")] pub n325_heavy_press_lighting: [Rect; 7], @@ -996,7 +980,6 @@ pub struct NPCConsts { pub n335_ikachan: [Rect; 3], // pub n336_ikachan_generator: () // Defined in code - #[serde(default = "default_n337_numahachi")] pub n337_numahachi: [Rect; 2], @@ -1004,7 +987,6 @@ pub struct NPCConsts { pub n338_green_devil: [Rect; 4], // pub n339_green_devil_generator: () // Defined in code - #[serde(default = "default_n340_ballos")] pub n340_ballos: [Rect; 22], @@ -1048,7 +1030,6 @@ pub struct NPCConsts { pub n353_bute_sword_flying: [Rect; 8], // pub n354_invisible_deathtrap_wall: () // Defined in code - #[serde(default = "default_n355_quote_and_curly_on_balrog")] pub n355_quote_and_curly_on_balrog: [Rect; 4], @@ -1062,7 +1043,6 @@ pub struct NPCConsts { pub n358_misery_credits: [Rect; 5], // pub n359_water_droplet_generator: () // Defined in code - #[serde(default = "default_n360_credits_thank_you")] pub n360_credits_thank_you: Rect, @@ -1077,6 +1057,15 @@ pub struct NPCConsts { #[serde(default = "default_b04_core")] pub b04_core: [Rect; 10], + + #[serde(default = "default_b05_ironhead")] + pub b05_ironhead: [Rect; 18], + + #[serde(default = "default_b06_sisters")] + pub b06_sisters: [Rect; 14], + + #[serde(default = "default_b07_undead_core")] + pub b07_undead_core: [Rect; 15], } fn default_n001_experience() -> [Rect; 6] { @@ -1280,17 +1269,11 @@ fn default_n016_save_point() -> [Rect; 8] { } fn default_n017_health_refill() -> [Rect; 2] { - [ - Rect { left: 288, top: 0, right: 304, bottom: 16 }, - Rect { left: 304, top: 0, right: 320, bottom: 16 }, - ] + [Rect { left: 288, top: 0, right: 304, bottom: 16 }, Rect { left: 304, top: 0, right: 320, bottom: 16 }] } fn default_n018_door() -> [Rect; 2] { - [ - Rect { left: 224, top: 16, right: 240, bottom: 40 }, - Rect { left: 192, top: 112, right: 208, bottom: 136 }, - ] + [Rect { left: 224, top: 16, right: 240, bottom: 40 }, Rect { left: 192, top: 112, right: 208, bottom: 136 }] } fn default_n019_balrog_bust_in() -> [Rect; 8] { @@ -1320,10 +1303,7 @@ fn default_n021_chest_open() -> Rect { } fn default_n022_teleporter() -> [Rect; 2] { - [ - Rect { left: 240, top: 16, right: 264, bottom: 48 }, - Rect { left: 248, top: 152, right: 272, bottom: 184 }, - ] + [Rect { left: 240, top: 16, right: 264, bottom: 48 }, Rect { left: 248, top: 152, right: 272, bottom: 184 }] } fn default_n023_teleporter_lights() -> [Rect; 8] { @@ -1357,10 +1337,7 @@ fn default_n024_power_critter() -> [Rect; 12] { } fn default_n025_lift() -> [Rect; 2] { - [ - Rect { left: 256, top: 64, right: 288, bottom: 80 }, - Rect { left: 256, top: 80, right: 288, bottom: 96 }, - ] + [Rect { left: 256, top: 64, right: 288, bottom: 80 }, Rect { left: 256, top: 80, right: 288, bottom: 96 }] } fn default_n026_bat_flying() -> [Rect; 8] { @@ -1430,24 +1407,15 @@ fn default_n031_bat_hanging() -> [Rect; 10] { } fn default_n032_life_capsule() -> [Rect; 2] { - [ - Rect { left: 32, top: 96, right: 48, bottom: 112 }, - Rect { left: 48, top: 96, right: 64, bottom: 112 }, - ] + [Rect { left: 32, top: 96, right: 48, bottom: 112 }, Rect { left: 48, top: 96, right: 64, bottom: 112 }] } fn default_n033_balrog_bouncing_projectile() -> [Rect; 2] { - [ - Rect { left: 240, top: 64, right: 256, bottom: 80 }, - Rect { left: 240, top: 80, right: 256, bottom: 96 }, - ] + [Rect { left: 240, top: 64, right: 256, bottom: 80 }, Rect { left: 240, top: 80, right: 256, bottom: 96 }] } fn default_n034_bed() -> [Rect; 2] { - [ - Rect { left: 192, top: 48, right: 224, bottom: 64 }, - Rect { left: 192, top: 184, right: 224, bottom: 200 }, - ] + [Rect { left: 192, top: 48, right: 224, bottom: 64 }, Rect { left: 192, top: 184, right: 224, bottom: 200 }] } fn default_n035_mannan() -> [Rect; 8] { @@ -1481,10 +1449,7 @@ fn default_n036_balrog_hover() -> [Rect; 12] { } fn default_n037_sign() -> [Rect; 2] { - [ - Rect { left: 192, top: 64, right: 208, bottom: 80 }, - Rect { left: 208, top: 64, right: 224, bottom: 80 }, - ] + [Rect { left: 192, top: 64, right: 208, bottom: 80 }, Rect { left: 208, top: 64, right: 224, bottom: 80 }] } fn default_n038_fireplace() -> [Rect; 4] { @@ -1497,10 +1462,7 @@ fn default_n038_fireplace() -> [Rect; 4] { } fn default_n039_save_sign() -> [Rect; 2] { - [ - Rect { left: 224, top: 64, right: 240, bottom: 80 }, - Rect { left: 240, top: 64, right: 256, bottom: 80 }, - ] + [Rect { left: 224, top: 64, right: 240, bottom: 80 }, Rect { left: 240, top: 64, right: 256, bottom: 80 }] } fn default_n040_santa() -> [Rect; 14] { @@ -1558,10 +1520,7 @@ fn default_n042_sue() -> [Rect; 26] { } fn default_n043_chalkboard() -> [Rect; 2] { - [ - Rect { left: 128, top: 80, right: 168, bottom: 112 }, - Rect { left: 168, top: 80, right: 208, bottom: 112 }, - ] + [Rect { left: 128, top: 80, right: 168, bottom: 112 }, Rect { left: 168, top: 80, right: 208, bottom: 112 }] } fn default_n044_polish() -> [Rect; 6] { @@ -1912,10 +1871,7 @@ fn default_n071_chinfish() -> [Rect; 6] { } fn default_n072_sprinkler() -> [Rect; 2] { - [ - Rect { left: 224, top: 48, right: 240, bottom: 64 }, - Rect { left: 240, top: 48, right: 256, bottom: 64 }, - ] + [Rect { left: 224, top: 48, right: 240, bottom: 64 }, Rect { left: 240, top: 48, right: 256, bottom: 64 }] } fn default_n073_water_droplet() -> [Rect; 5] { @@ -1946,10 +1902,7 @@ fn default_n074_jack() -> [Rect; 12] { } fn default_n075_kanpachi() -> [Rect; 2] { - [ - Rect { left: 272, top: 32, right: 296, bottom: 56 }, - Rect { left: 296, top: 32, right: 320, bottom: 56 }, - ] + [Rect { left: 272, top: 32, right: 296, bottom: 56 }, Rect { left: 296, top: 32, right: 320, bottom: 56 }] } fn default_n077_yamashita() -> [Rect; 3] { @@ -1961,10 +1914,7 @@ fn default_n077_yamashita() -> [Rect; 3] { } fn default_n078_pot() -> [Rect; 2] { - [ - Rect { left: 160, top: 48, right: 176, bottom: 64 }, - Rect { left: 176, top: 48, right: 192, bottom: 64 }, - ] + [Rect { left: 160, top: 48, right: 176, bottom: 64 }, Rect { left: 176, top: 48, right: 192, bottom: 64 }] } fn default_n079_mahin() -> [Rect; 6] { @@ -2231,10 +2181,7 @@ fn default_n099_fan_down() -> [Rect; 3] { } fn default_n100_grate() -> [Rect; 2] { - [ - Rect { left: 272, top: 48, right: 288, bottom: 64 }, - Rect { left: 272, top: 48, right: 288, bottom: 64 }, - ] + [Rect { left: 272, top: 48, right: 288, bottom: 64 }, Rect { left: 272, top: 48, right: 288, bottom: 64 }] } fn default_n101_malco_screen() -> [Rect; 3] { @@ -2277,10 +2224,7 @@ fn default_n104_frog() -> [Rect; 6] { } fn default_n105_hey_bubble_low() -> [Rect; 2] { - [ - Rect { left: 128, top: 32, right: 144, bottom: 48 }, - Rect { left: 128, top: 32, right: 128, bottom: 32 }, - ] + [Rect { left: 128, top: 32, right: 144, bottom: 48 }, Rect { left: 128, top: 32, right: 128, bottom: 32 }] } fn default_n107_malco_broken() -> [Rect; 10] { @@ -2445,10 +2389,7 @@ fn default_n119_table_chair() -> Rect { } fn default_n120_colon_a() -> [Rect; 2] { - [ - Rect { left: 64, top: 0, right: 80, bottom: 16 }, - Rect { left: 64, top: 16, right: 80, bottom: 32 }, - ] + [Rect { left: 64, top: 0, right: 80, bottom: 16 }, Rect { left: 64, top: 16, right: 80, bottom: 32 }] } fn default_n121_colon_b() -> [Rect; 3] { @@ -2494,17 +2435,11 @@ fn default_n123_curly_boss_bullet() -> [Rect; 4] { } fn default_n124_sunstone() -> [Rect; 2] { - [ - Rect { left: 160, top: 0, right: 192, bottom: 32 }, - Rect { left: 192, top: 0, right: 224, bottom: 32 }, - ] + [Rect { left: 160, top: 0, right: 192, bottom: 32 }, Rect { left: 192, top: 0, right: 224, bottom: 32 }] } fn default_n125_hidden_item() -> [Rect; 2] { - [ - Rect { left: 0, top: 96, right: 16, bottom: 112 }, - Rect { left: 16, top: 96, right: 32, bottom: 112 }, - ] + [Rect { left: 0, top: 96, right: 16, bottom: 112 }, Rect { left: 16, top: 96, right: 32, bottom: 112 }] } fn default_n126_puppy_running() -> [Rect; 12] { @@ -2597,10 +2532,7 @@ fn default_n130_puppy_sitting() -> [Rect; 8] { } fn default_n131_puppy_sleeping() -> [Rect; 2] { - [ - Rect { left: 144, top: 144, right: 160, bottom: 160 }, - Rect { left: 144, top: 160, right: 160, bottom: 176 }, - ] + [Rect { left: 144, top: 144, right: 160, bottom: 160 }, Rect { left: 144, top: 160, right: 160, bottom: 176 }] } fn default_n132_puppy_barking() -> [Rect; 10] { @@ -2661,10 +2593,7 @@ fn default_n137_large_door_frame() -> Rect { } fn default_n138_large_door() -> [Rect; 2] { - [ - Rect { left: 96, top: 112, right: 112, bottom: 136 }, - Rect { left: 112, top: 112, right: 128, bottom: 136 }, - ] + [Rect { left: 96, top: 112, right: 112, bottom: 136 }, Rect { left: 112, top: 112, right: 128, bottom: 136 }] } fn default_n139_doctor() -> [Rect; 6] { @@ -2712,10 +2641,7 @@ fn default_n140_toroko_frenzied() -> [Rect; 28] { } fn default_n141_toroko_block_projectile() -> [Rect; 2] { - [ - Rect { left: 288, top: 32, right: 304, bottom: 48 }, - Rect { left: 304, top: 32, right: 320, bottom: 48 }, - ] + [Rect { left: 288, top: 32, right: 304, bottom: 48 }, Rect { left: 304, top: 32, right: 320, bottom: 48 }] } fn default_n142_flower_cub() -> [Rect; 5] { @@ -2729,10 +2655,7 @@ fn default_n142_flower_cub() -> [Rect; 5] { } fn default_n143_jenka_collapsed() -> [Rect; 2] { - [ - Rect { left: 208, top: 32, right: 224, bottom: 48 }, - Rect { left: 208, top: 48, right: 224, bottom: 64 }, - ] + [Rect { left: 208, top: 32, right: 224, bottom: 48 }, Rect { left: 208, top: 48, right: 224, bottom: 64 }] } fn default_n144_toroko_teleporting_in() -> [Rect; 10] { @@ -2751,10 +2674,7 @@ fn default_n144_toroko_teleporting_in() -> [Rect; 10] { } fn default_n145_king_sword() -> [Rect; 2] { - [ - Rect { left: 96, top: 32, right: 112, bottom: 48 }, - Rect { left: 112, top: 32, right: 128, bottom: 48 }, - ] + [Rect { left: 96, top: 32, right: 112, bottom: 48 }, Rect { left: 112, top: 32, right: 128, bottom: 48 }] } fn default_n146_lighting() -> [Rect; 5] { @@ -2785,10 +2705,7 @@ fn default_n147_critter_purple() -> [Rect; 12] { } fn default_n148_critter_purple_projectile() -> [Rect; 2] { - [ - Rect { left: 96, top: 96, right: 104, bottom: 104 }, - Rect { left: 104, top: 96, right: 112, bottom: 104 }, - ] + [Rect { left: 96, top: 96, right: 104, bottom: 104 }, Rect { left: 104, top: 96, right: 112, bottom: 104 }] } fn default_n149_horizontal_moving_block() -> Rect { @@ -2829,19 +2746,19 @@ 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: 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: 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 ] } @@ -2859,14 +2776,14 @@ 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: 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 + Rect { left: 24, top: 72, right: 48, bottom: 96 }, // 3 19 ] } @@ -2955,10 +2872,7 @@ fn default_n165_curly_collapsed() -> [Rect; 3] { } fn default_n166_chaba() -> [Rect; 2] { - [ - Rect { left: 144, top: 104, right: 184, bottom: 128 }, - Rect { left: 184, top: 104, right: 224, bottom: 128 }, - ] + [Rect { left: 144, top: 104, right: 184, bottom: 128 }, Rect { left: 184, top: 104, right: 224, bottom: 128 }] } fn default_n167_booster_falling() -> [Rect; 3] { @@ -3064,10 +2978,7 @@ fn default_n176_buyo_buyo_base() -> [Rect; 6] { } fn default_n177_buyo_buyo() -> [Rect; 2] { - [ - Rect { left: 192, top: 128, right: 208, bottom: 144 }, - Rect { left: 208, top: 128, right: 224, bottom: 144 }, - ] + [Rect { left: 192, top: 128, right: 208, bottom: 144 }, Rect { left: 208, top: 128, right: 224, bottom: 144 }] } fn default_n178_core_blade_projectile() -> [Rect; 3] { @@ -3132,10 +3043,7 @@ fn default_n182_curly_ai_polar_star() -> [Rect; 4] { } fn default_n183_curly_air_tank_bubble() -> [Rect; 2] { - [ - Rect { left: 56, top: 96, right: 80, bottom: 120 }, - Rect { left: 80, top: 96, right: 104, bottom: 120 }, - ] + [Rect { left: 56, top: 96, right: 80, bottom: 120 }, Rect { left: 80, top: 96, right: 104, bottom: 120 }] } fn default_n184_shutter() -> [Rect; 4] { @@ -3187,10 +3095,7 @@ fn default_n189_homing_flame() -> [Rect; 3] { } fn default_n190_broken_robot() -> [Rect; 2] { - [ - Rect { left: 192, top: 32, right: 208, bottom: 48 }, - Rect { left: 208, top: 32, right: 224, bottom: 48 }, - ] + [Rect { left: 192, top: 32, right: 208, bottom: 48 }, Rect { left: 208, top: 32, right: 224, bottom: 48 }] } fn default_n192_scooter() -> [Rect; 4] { @@ -3215,10 +3120,7 @@ fn default_n195_background_grate() -> Rect { } fn default_n196_ironhead_wall() -> [Rect; 2] { - [ - Rect { left: 112, top: 64, right: 144, bottom: 80 }, - Rect { left: 112, top: 80, right: 144, bottom: 96 }, - ] + [Rect { left: 112, top: 64, right: 144, bottom: 80 }, Rect { left: 112, top: 80, right: 144, bottom: 96 }] } fn default_n197_porcupine_fish() -> [Rect; 4] { @@ -3266,10 +3168,7 @@ fn default_n200_zombie_dragon() -> [Rect; 12] { } fn default_n201_zombie_dragon_dead() -> [Rect; 2] { - [ - Rect { left: 200, top: 0, right: 240, bottom: 40 }, - Rect { left: 200, top: 40, right: 240, bottom: 80 }, - ] + [Rect { left: 200, top: 0, right: 240, bottom: 40 }, Rect { left: 200, top: 40, right: 240, bottom: 80 }] } fn default_n202_zombie_dragon_projectile() -> [Rect; 3] { @@ -3292,17 +3191,11 @@ fn default_n203_critter_destroyed_egg_corridor() -> [Rect; 6] { } fn default_n204_small_falling_spike() -> [Rect; 2] { - [ - Rect { left: 240, top: 80, right: 256, bottom: 96 }, - Rect { left: 240, top: 144, right: 256, bottom: 160 }, - ] + [Rect { left: 240, top: 80, right: 256, bottom: 96 }, Rect { left: 240, top: 144, right: 256, bottom: 160 }] } fn default_n205_large_falling_spike() -> [Rect; 2] { - [ - Rect { left: 112, top: 80, right: 128, bottom: 112 }, - Rect { left: 128, top: 80, right: 144, bottom: 112 }, - ] + [Rect { left: 112, top: 80, right: 128, bottom: 112 }, Rect { left: 128, top: 80, right: 144, bottom: 112 }] } fn default_n206_counter_bomb() -> [Rect; 3] { @@ -3421,10 +3314,7 @@ fn default_n217_itoh() -> [Rect; 8] { } fn default_n218_core_giant_ball() -> [Rect; 2] { - [ - Rect { left: 256, top: 120, right: 288, bottom: 152 }, - Rect { left: 288, top: 120, right: 320, bottom: 152 }, - ] + [Rect { left: 256, top: 120, right: 288, bottom: 152 }, Rect { left: 288, top: 120, right: 320, bottom: 152 }] } fn default_n220_shovel_brigade() -> [Rect; 4] { @@ -3516,24 +3406,15 @@ fn default_n228_droll() -> [Rect; 8] { } fn default_n229_red_flowers_sprouts() -> [Rect; 2] { - [ - Rect { left: 0, top: 96, right: 48, bottom: 112 }, - Rect { left: 0, top: 112, right: 48, bottom: 128 }, - ] + [Rect { left: 0, top: 96, right: 48, bottom: 112 }, Rect { left: 0, top: 112, right: 48, bottom: 128 }] } fn default_n230_red_flowers_blooming() -> [Rect; 2] { - [ - Rect { left: 48, top: 96, right: 96, bottom: 128 }, - Rect { left: 96, top: 96, right: 144, bottom: 128 }, - ] + [Rect { left: 48, top: 96, right: 96, bottom: 128 }, Rect { left: 96, top: 96, right: 144, bottom: 128 }] } fn default_n231_rocket() -> [Rect; 2] { - [ - Rect { left: 176, top: 32, right: 208, bottom: 48 }, - Rect { left: 176, top: 48, right: 208, bottom: 64 }, - ] + [Rect { left: 176, top: 32, right: 208, bottom: 48 }, Rect { left: 176, top: 48, right: 208, bottom: 64 }] } fn default_n232_orangebell() -> [Rect; 6] { @@ -3547,7 +3428,7 @@ fn default_n232_orangebell() -> [Rect; 6] { ] } -fn default_n233_orangebell_hat() -> [Rect; 8] { +fn default_n233_orangebell_bat() -> [Rect; 8] { [ Rect { left: 256, top: 0, right: 272, bottom: 16 }, Rect { left: 272, top: 0, right: 288, bottom: 16 }, @@ -3561,10 +3442,7 @@ fn default_n233_orangebell_hat() -> [Rect; 8] { } fn default_n234_red_flowers_picked() -> [Rect; 2] { - [ - Rect { left: 144, top: 96, right: 192, bottom: 112 }, - Rect { left: 144, top: 112, right: 192, bottom: 128 }, - ] + [Rect { left: 144, top: 96, right: 192, bottom: 112 }, Rect { left: 144, top: 112, right: 192, bottom: 128 }] } fn default_n235_midorin() -> [Rect; 8] { @@ -3610,10 +3488,7 @@ fn default_n238_press_sideways() -> [Rect; 3] { } fn default_n239_cage_bars() -> [Rect; 2] { - [ - Rect { left: 192, top: 48, right: 256, bottom: 80 }, - Rect { left: 96, top: 112, right: 144, bottom: 144 }, - ] + [Rect { left: 192, top: 48, right: 256, bottom: 80 }, Rect { left: 96, top: 112, right: 144, bottom: 144 }] } fn default_n240_mimiga_jailed() -> [Rect; 12] { @@ -3709,11 +3584,8 @@ fn default_n248_misery_boss_vanishing() -> [Rect; 3] { ] } -fn default_n249_misery_boss_energy_shot() -> [Rect; 2] { - [ - Rect { left: 48, top: 48, right: 64, bottom: 64 }, - Rect { left: 64, top: 48, right: 80, bottom: 64 }, - ] +fn default_n249_misery_boss_appearing() -> [Rect; 2] { + [Rect { left: 48, top: 48, right: 64, bottom: 64 }, Rect { left: 64, top: 48, right: 80, bottom: 64 }] } fn default_n250_misery_boss_lighting_ball() -> [Rect; 3] { @@ -3725,10 +3597,7 @@ fn default_n250_misery_boss_lighting_ball() -> [Rect; 3] { } fn default_n251_misery_boss_lighting() -> [Rect; 2] { - [ - Rect { left: 80, top: 32, right: 96, bottom: 64 }, - Rect { left: 96, top: 32, right: 112, bottom: 64 }, - ] + [Rect { left: 80, top: 32, right: 96, bottom: 64 }, Rect { left: 96, top: 32, right: 112, bottom: 64 }] } fn default_n252_misery_boss_bats() -> [Rect; 8] { @@ -3745,17 +3614,11 @@ fn default_n252_misery_boss_bats() -> [Rect; 8] { } fn default_n253_experience_capsule() -> [Rect; 2] { - [ - Rect { left: 0, top: 64, right: 16, bottom: 80 }, - Rect { left: 16, top: 64, right: 32, bottom: 80 }, - ] + [Rect { left: 0, top: 64, right: 16, bottom: 80 }, Rect { left: 16, top: 64, right: 32, bottom: 80 }] } fn default_n254_helicopter() -> [Rect; 2] { - [ - Rect { left: 0, top: 0, right: 128, bottom: 64 }, - Rect { left: 0, top: 64, right: 128, bottom: 128 }, - ] + [Rect { left: 0, top: 0, right: 128, bottom: 64 }, Rect { left: 0, top: 64, right: 128, bottom: 128 }] } fn default_n255_helicopter_blades() -> [Rect; 8] { @@ -3795,10 +3658,7 @@ fn default_n258_mimiga_sleeping() -> Rect { } fn default_n259_curly_unconscious() -> [Rect; 2] { - [ - Rect { left: 224, top: 96, right: 240, bottom: 112 }, - Rect { left: 224, top: 112, right: 240, bottom: 128 }, - ] + [Rect { left: 224, top: 96, right: 240, bottom: 112 }, Rect { left: 224, top: 112, right: 240, bottom: 128 }] } fn default_n260_shovel_brigade_caged() -> [Rect; 6] { @@ -3853,11 +3713,11 @@ fn default_n263_doctor_boss() -> [Rect; 18] { ] } -fn default_n264_doctor_boss_red_wave_projectile() -> Rect { +fn default_n264_doctor_boss_red_projectile() -> Rect { Rect { left: 288, top: 0, right: 304, bottom: 16 } } -fn default_n265_doctor_boss_red_ball_projectile() -> [Rect; 3] { +fn default_n265_doctor_boss_red_projectile_trail() -> [Rect; 3] { [ Rect { left: 288, top: 16, right: 304, bottom: 32 }, Rect { left: 288, top: 32, right: 304, bottom: 48 }, @@ -3865,11 +3725,8 @@ fn default_n265_doctor_boss_red_ball_projectile() -> [Rect; 3] { ] } -fn default_n266_doctor_boss_red_ball_projectile_bouncing() -> [Rect; 2] { - [ - Rect { left: 304, top: 16, right: 320, bottom: 32 }, - Rect { left: 304, top: 32, right: 320, bottom: 48 }, - ] +fn default_n266_doctor_boss_red_projectile_bouncing() -> [Rect; 2] { + [Rect { left: 304, top: 16, right: 320, bottom: 32 }, Rect { left: 304, top: 32, right: 320, bottom: 48 }] } fn default_n267_muscle_doctor() -> [Rect; 20] { @@ -3934,10 +3791,7 @@ fn default_n269_red_bat_bouncing() -> [Rect; 6] { } fn default_n270_doctor_red_energy() -> [Rect; 2] { - [ - Rect { left: 170, top: 34, right: 174, bottom: 38 }, - Rect { left: 170, top: 42, right: 174, bottom: 46 }, - ] + [Rect { left: 170, top: 34, right: 174, bottom: 38 }, Rect { left: 170, top: 42, right: 174, bottom: 46 }] } fn default_n273_droll_projectile() -> [Rect; 3] { @@ -4017,10 +3871,7 @@ fn default_n278_little_family() -> [Rect; 6] { } fn default_n279_large_falling_block() -> [Rect; 2] { - [ - Rect { left: 0, top: 16, right: 32, bottom: 48 }, - Rect { left: 16, top: 0, right: 32, bottom: 16 }, - ] + [Rect { left: 0, top: 16, right: 32, bottom: 48 }, Rect { left: 16, top: 0, right: 32, bottom: 16 }] } fn default_n280_sue_teleported() -> [Rect; 4] { @@ -4155,17 +4006,11 @@ fn default_n290_bat_misery() -> [Rect; 6] { } fn default_n291_mini_undead_core_inactive() -> [Rect; 2] { - [ - Rect { left: 256, top: 80, right: 320, bottom: 120 }, - Rect { left: 256, top: 0, right: 320, bottom: 40 }, - ] + [Rect { left: 256, top: 80, right: 320, bottom: 120 }, Rect { left: 256, top: 0, right: 320, bottom: 40 }] } fn default_n293_undead_core_energy_shot() -> [Rect; 2] { - [ - Rect { left: 240, top: 200, right: 280, bottom: 240 }, - Rect { left: 280, top: 200, right: 320, bottom: 240 }, - ] + [Rect { left: 240, top: 200, right: 280, bottom: 240 }, Rect { left: 280, top: 200, right: 320, bottom: 240 }] } fn default_n295_cloud() -> [Rect; 4] { @@ -4195,10 +4040,7 @@ fn default_n298_intro_doctor() -> [Rect; 8] { } fn default_n299_intro_balrog_misery() -> [Rect; 2] { - [ - Rect { left: 0, top: 0, right: 48, bottom: 48 }, - Rect { left: 48, top: 0, right: 96, bottom: 48 }, - ] + [Rect { left: 0, top: 0, right: 48, bottom: 48 }, Rect { left: 48, top: 0, right: 96, bottom: 48 }] } fn default_n300_intro_demon_crown() -> Rect { @@ -4500,10 +4342,7 @@ fn default_n326_sue_itoh_human_transition() -> [Rect; 16] { } fn default_n327_sneeze() -> [Rect; 2] { - [ - Rect { left: 240, top: 80, right: 256, bottom: 96 }, - Rect { left: 256, top: 80, right: 272, bottom: 96 }, - ] + [Rect { left: 240, top: 80, right: 256, bottom: 96 }, Rect { left: 256, top: 80, right: 272, bottom: 96 }] } fn default_n328_human_transform_machine() -> Rect { @@ -4511,10 +4350,7 @@ fn default_n328_human_transform_machine() -> Rect { } fn default_n329_laboratory_fan() -> [Rect; 2] { - [ - Rect { left: 48, top: 0, right: 64, bottom: 16 }, - Rect { left: 64, top: 0, right: 80, bottom: 16 }, - ] + [Rect { left: 48, top: 0, right: 64, bottom: 16 }, Rect { left: 64, top: 0, right: 80, bottom: 16 }] } fn default_n330_rolling() -> [Rect; 3] { @@ -4543,10 +4379,7 @@ fn default_n332_ballos_shockwave() -> [Rect; 3] { } fn default_n333_ballos_lighting() -> [Rect; 2] { - [ - Rect { left: 80, top: 120, right: 104, bottom: 144 }, - Rect { left: 104, top: 120, right: 128, bottom: 144 }, - ] + [Rect { left: 80, top: 120, right: 104, bottom: 144 }, Rect { left: 104, top: 120, right: 128, bottom: 144 }] } fn default_n334_sweat() -> [Rect; 4] { @@ -4567,10 +4400,7 @@ fn default_n335_ikachan() -> [Rect; 3] { } fn default_n337_numahachi() -> [Rect; 2] { - [ - Rect { left: 256, top: 112, right: 288, bottom: 152 }, - Rect { left: 288, top: 112, right: 320, bottom: 152 }, - ] + [Rect { left: 256, top: 112, right: 288, bottom: 152 }, Rect { left: 288, top: 112, right: 320, bottom: 152 }] } fn default_n338_green_devil() -> [Rect; 4] { @@ -4630,10 +4460,7 @@ fn default_n343_ballos_2_cutscene() -> Rect { } fn default_n344_ballos_2_eyes() -> [Rect; 2] { - [ - Rect { left: 272, top: 0, right: 296, bottom: 16 }, - Rect { left: 296, top: 0, right: 320, bottom: 16 }, - ] + [Rect { left: 272, top: 0, right: 296, bottom: 16 }, Rect { left: 296, top: 0, right: 320, bottom: 16 }] } fn default_n345_ballos_skull_projectile() -> [Rect; 4] { @@ -4659,10 +4486,7 @@ fn default_n347_hoppy() -> [Rect; 4] { } fn default_n348_ballos_4_spikes() -> [Rect; 2] { - [ - Rect { left: 128, top: 152, right: 160, bottom: 176 }, - Rect { left: 160, top: 152, right: 192, bottom: 176 }, - ] + [Rect { left: 128, top: 152, right: 160, bottom: 176 }, Rect { left: 160, top: 152, right: 192, bottom: 176 }] } fn default_n349_statue() -> Rect { @@ -4758,10 +4582,7 @@ fn default_n355_quote_and_curly_on_balrog() -> [Rect; 4] { } fn default_n356_balrog_rescuing() -> [Rect; 2] { - [ - Rect { left: 240, top: 128, right: 280, bottom: 152 }, - Rect { left: 240, top: 152, right: 280, bottom: 176 }, - ] + [Rect { left: 240, top: 128, right: 280, bottom: 152 }, Rect { left: 240, top: 152, right: 280, bottom: 176 }] } fn default_n357_puppy_ghost() -> Rect { @@ -4822,28 +4643,35 @@ fn default_b02_balfrog() -> [Rect; 18] { fn default_b03_monster_x() -> [Rect; 29] { [ - Rect { left: 216, top: 0, right: 320, bottom: 48 }, // face + // face + Rect { left: 216, top: 0, right: 320, bottom: 48 }, 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 + // tracks up + Rect { left: 0, top: 0, right: 72, bottom: 32 }, 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 + // tracks down + Rect { left: 0, top: 64, right: 72, bottom: 96 }, 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 + // frame + Rect { left: 0, top: 128, right: 72, bottom: 160 }, 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 + // shield left + Rect { left: 216, top: 96, right: 264, bottom: 144 }, + // shield right + Rect { left: 264, top: 96, right: 312, bottom: 144 }, + // part 4 + Rect { left: 0, top: 192, right: 16, bottom: 208 }, 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 }, @@ -4856,16 +4684,87 @@ fn default_b03_monster_x() -> [Rect; 29] { fn default_b04_core() -> [Rect; 10] { [ - Rect { left: 0, top: 0, right: 72, bottom: 112 }, // face + // face + Rect { left: 0, top: 0, right: 72, bottom: 112 }, Rect { left: 0, top: 112, right: 72, bottom: 224 }, Rect { left: 160, top: 0, right: 232, bottom: 112 }, Rect { left: 0, top: 0, right: 0, bottom: 0 }, - - Rect { left: 72, top: 0, right: 160, bottom: 112 }, // tail + // tail + Rect { left: 72, top: 0, right: 160, bottom: 112 }, Rect { left: 72, top: 112, right: 160, bottom: 224 }, Rect { left: 0, top: 0, right: 0, bottom: 0 }, - - Rect { left: 256, top: 0, right: 320, bottom: 40 }, // small head + // small head + Rect { left: 256, top: 0, right: 320, bottom: 40 }, + Rect { left: 256, top: 40, right: 320, bottom: 80 }, + Rect { left: 256, top: 80, right: 320, bottom: 120 }, + ] +} + +fn default_b05_ironhead() -> [Rect; 18] { + [ + // set 1 + Rect { left: 0, top: 0, right: 64, bottom: 24 }, + Rect { left: 64, top: 0, right: 128, bottom: 24 }, + Rect { left: 128, top: 0, right: 192, bottom: 24 }, + Rect { left: 64, top: 0, right: 128, bottom: 24 }, + Rect { left: 0, top: 0, right: 64, bottom: 24 }, + Rect { left: 192, top: 0, right: 256, bottom: 24 }, + Rect { left: 256, top: 0, right: 320, bottom: 24 }, + Rect { left: 192, top: 0, right: 256, bottom: 24 }, + Rect { left: 256, top: 48, right: 320, bottom: 72 }, + // set 2 + Rect { left: 0, top: 24, right: 64, bottom: 48 }, + Rect { left: 64, top: 24, right: 128, bottom: 48 }, + Rect { left: 128, top: 24, right: 192, bottom: 48 }, + Rect { left: 64, top: 24, right: 128, bottom: 48 }, + Rect { left: 0, top: 24, right: 64, bottom: 48 }, + Rect { left: 192, top: 24, right: 256, bottom: 48 }, + Rect { left: 256, top: 24, right: 320, bottom: 48 }, + Rect { left: 192, top: 24, right: 256, bottom: 48 }, + Rect { left: 256, top: 48, right: 320, bottom: 72 }, + ] +} + +fn default_b06_sisters() -> [Rect; 14] { + [ + // head + Rect { left: 0, top: 80, right: 40, bottom: 112 }, + Rect { left: 40, top: 80, right: 80, bottom: 112 }, + Rect { left: 80, top: 80, right: 120, bottom: 112 }, + Rect { left: 120, top: 80, right: 160, bottom: 112 }, + Rect { left: 0, top: 112, right: 40, bottom: 144 }, + Rect { left: 40, top: 112, right: 80, bottom: 144 }, + Rect { left: 80, top: 112, right: 120, bottom: 144 }, + Rect { left: 120, top: 112, right: 160, bottom: 144 }, + // body + Rect { left: 0, top: 0, right: 40, bottom: 40 }, + Rect { left: 40, top: 0, right: 80, bottom: 40 }, + Rect { left: 80, top: 0, right: 120, bottom: 40 }, + Rect { left: 0, top: 40, right: 40, bottom: 80 }, + Rect { left: 40, top: 40, right: 80, bottom: 80 }, + Rect { left: 80, top: 40, right: 120, bottom: 80 }, + ] +} + +fn default_b07_undead_core() -> [Rect; 15] { + [ + // face + Rect { left: 0, top: 0, right: 0, bottom: 0 }, + Rect { left: 160, top: 112, right: 232, bottom: 152 }, + Rect { left: 160, top: 152, right: 232, bottom: 192 }, + Rect { left: 160, top: 192, right: 232, bottom: 232 }, + Rect { left: 248, top: 160, right: 320, bottom: 200 }, + // head + Rect { left: 0, top: 0, right: 72, bottom: 112 }, + Rect { left: 0, top: 112, right: 72, bottom: 224 }, + Rect { left: 160, top: 0, right: 232, bottom: 112 }, + Rect { left: 0, top: 0, right: 0, bottom: 0 }, + // tail + Rect { left: 72, top: 0, right: 160, bottom: 112 }, + Rect { left: 72, top: 112, right: 160, bottom: 224 }, + Rect { left: 0, top: 0, right: 0, bottom: 0 }, + // small head + Rect { left: 256, top: 0, right: 320, bottom: 40 }, Rect { left: 256, top: 40, right: 320, bottom: 80 }, Rect { left: 256, top: 80, right: 320, bottom: 120 }, ] diff --git a/src/framework/backend.rs b/src/framework/backend.rs index bcce639..72521f9 100644 --- a/src/framework/backend.rs +++ b/src/framework/backend.rs @@ -53,6 +53,8 @@ pub trait BackendRenderer { fn draw_outline_rect(&mut self, rect: Rect, line_width: usize, color: Color) -> GameResult; + fn set_clip_rect(&mut self, rect: Option) -> GameResult; + fn imgui(&self) -> GameResult<&mut imgui::Context>; fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult; diff --git a/src/framework/backend_null.rs b/src/framework/backend_null.rs index df8fdfe..eb5f71e 100644 --- a/src/framework/backend_null.rs +++ b/src/framework/backend_null.rs @@ -118,6 +118,10 @@ impl BackendRenderer for NullRenderer { Ok(()) } + fn set_clip_rect(&mut self, rect: Option) -> GameResult { + Ok(()) + } + fn imgui(&self) -> GameResult<&mut imgui::Context> { unsafe { Ok(&mut *self.0.as_ptr()) } } diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index 87b4ba3..c99eee4 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -5,8 +5,9 @@ use std::ffi::c_void; use std::rc::Rc; use std::time::Duration; -use imgui::internal::RawWrapper; use imgui::{ConfigFlags, DrawCmd, DrawData, ImString, Key, MouseCursor, TextureId}; +use imgui::internal::RawWrapper; +use sdl2::{EventPump, keyboard, pixels, Sdl, VideoSubsystem}; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Scancode; use sdl2::mouse::{Cursor, SystemCursor}; @@ -14,7 +15,6 @@ use sdl2::pixels::PixelFormatEnum; use sdl2::render::{Texture, TextureCreator, WindowCanvas}; use sdl2::video::GLProfile; use sdl2::video::WindowContext; -use sdl2::{keyboard, pixels, EventPump, Sdl, VideoSubsystem}; use crate::common::{Color, Rect}; use crate::framework::backend::{ @@ -570,6 +570,23 @@ impl BackendRenderer for SDL2Renderer { Ok(()) } + fn set_clip_rect(&mut self, rect: Option) -> GameResult { + let mut refs = self.refs.borrow_mut(); + + if let Some(rect) = &rect { + refs.canvas.set_clip_rect(Some(sdl2::rect::Rect::new( + rect.left as i32, + rect.top as i32, + rect.width() as u32, + rect.height() as u32, + ))); + } else { + refs.canvas.set_clip_rect(None); + } + + Ok(()) + } + fn imgui(&self) -> GameResult<&mut imgui::Context> { unsafe { Ok(&mut *self.imgui.as_ptr()) } } diff --git a/src/framework/graphics.rs b/src/framework/graphics.rs index 997c2c1..a6a2927 100644 --- a/src/framework/graphics.rs +++ b/src/framework/graphics.rs @@ -103,6 +103,15 @@ pub fn draw_outline_rect(ctx: &mut Context, rect: Rect, line_width: usize, color Ok(()) } +pub fn set_clip_rect(ctx: &mut Context, rect: Option) -> GameResult { + if let Some(renderer) = ctx.renderer.as_mut() { + return renderer.set_clip_rect(rect); + } + + Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string())) +} + + pub fn imgui_context(ctx: &Context) -> GameResult<&mut imgui::Context> { if let Some(renderer) = ctx.renderer.as_ref() { return renderer.imgui(); diff --git a/src/framework/render_opengl.rs b/src/framework/render_opengl.rs index c7516a7..4f4c0a5 100644 --- a/src/framework/render_opengl.rs +++ b/src/framework/render_opengl.rs @@ -1159,6 +1159,23 @@ impl BackendRenderer for OpenGLRenderer { fn supports_vertex_draw(&self) -> bool { true } + + fn set_clip_rect(&mut self, rect: Option) -> GameResult { + if let Some((_, gl)) = self.get_context() { + unsafe { + if let Some(rect) = &rect { + gl.gl.Enable(gl::SCISSOR_TEST); + gl.gl.Scissor(rect.left as GLint, rect.top as GLint, rect.width() as GLint, rect.height() as GLint); + } else { + gl.gl.Disable(gl::SCISSOR_TEST); + } + } + + Ok(()) + } else { + Err(RenderError("No OpenGL context available!".to_string())) + } + } } impl OpenGLRenderer { diff --git a/src/lib.rs b/src/lib.rs index c88b091..3408c18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ use crate::framework::ui::UI; use crate::framework::vfs::PhysicalFS; use crate::scene::loading_scene::LoadingScene; use crate::scene::Scene; +use crate::scripting::tsc::text_script::ScriptMode; use crate::shared_game_state::{SharedGameState, TimingMode}; use crate::texture_set::{G_MAG, I_MAG}; @@ -99,16 +100,22 @@ impl Game { if let Some(scene) = self.scene.as_mut() { let state_ref = unsafe { &mut *self.state.get() }; + let speed = if state_ref.textscript_vm.mode == ScriptMode::Map && state_ref.textscript_vm.flags.cutscene_skip() { + 4.0 * state_ref.settings.speed + } else { + 1.0 * state_ref.settings.speed + }; + match state_ref.settings.timing_mode { TimingMode::_50Hz | TimingMode::_60Hz => { let last_tick = self.next_tick; while self.start_time.elapsed().as_nanos() >= self.next_tick && self.loops < 10 { - if (state_ref.settings.speed - 1.0).abs() < 0.01 { + if (speed - 1.0).abs() < 0.01 { self.next_tick += state_ref.settings.timing_mode.get_delta() as u128; } else { self.next_tick += - (state_ref.settings.timing_mode.get_delta() as f64 / state_ref.settings.speed) as u128; + (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; } self.loops += 1; } @@ -117,7 +124,7 @@ impl Game { log::warn!("Frame skip is way too high, a long system lag occurred?"); self.last_tick = self.start_time.elapsed().as_nanos(); self.next_tick = self.last_tick - + (state_ref.settings.timing_mode.get_delta() as f64 / state_ref.settings.speed) as u128; + + (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; self.loops = 0; } diff --git a/src/npc/ai/balcony.rs b/src/npc/ai/balcony.rs new file mode 100644 index 0000000..6f5a269 --- /dev/null +++ b/src/npc/ai/balcony.rs @@ -0,0 +1,252 @@ +use crate::common::Direction; +use crate::npc::list::NPCList; +use crate::npc::NPC; +use crate::player::Player; +use crate::rng::RNG; +use crate::{GameResult, SharedGameState}; + +impl NPC { + pub(crate) fn tick_n254_helicopter(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + match self.action_num { + 0 => { + self.action_num = 1; + + // blades + let mut npc = NPC::create(255, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x + 0x2400; + npc.y = self.y - 0x7200; + npc.parent_id = self.id; + + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.x = self.x - 0x4000; + npc.y = self.y - 0x6800; + npc.direction = Direction::Right; + + let _ = npc_list.spawn(0x100, npc); + } + 20 => { + self.action_num = 21; + self.action_counter = 0; + self.action_counter2 = 60; + } + 30 => { + self.action_num = 21; + + // momorin + let mut npc = NPC::create(223, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x - 0x1600; + npc.y = self.y - 0x1c00; + + let _ = npc_list.spawn(0x100, npc); + } + 40 => { + self.action_num = 21; + + // momorin + let mut npc = NPC::create(223, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x - 0x1200; + npc.y = self.y - 0x1c00; + + let _ = npc_list.spawn(0x100, npc); + + // santa + let mut npc = NPC::create(40, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x - 0x2c00; + npc.y = self.y - 0x1c00; + + let _ = npc_list.spawn(0x100, npc); + + // chaco + let mut npc = NPC::create(223, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x - 0x4600; + npc.y = self.y - 0x1c00; + + let _ = npc_list.spawn(0x100, npc); + } + _ => (), + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 1 }; + + self.anim_rect = state.constants.npc.n254_helicopter[dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n255_helicopter_blades( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + if self.direction == Direction::Left { + self.display_bounds.left = 0x7000; + self.display_bounds.right = 0x7000; + } else { + self.display_bounds.left = 0x5000; + self.display_bounds.right = 0x5000; + } + } + + if let Some(parent) = self.get_parent_ref_mut(npc_list) { + if parent.action_num >= 20 { + self.action_num = 10; + } + } + } + 10 | 11 => { + self.action_num = 11; + + self.anim_num += 1; + if self.anim_num > 3 { + self.anim_num = 0; + } + } + _ => (), + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n255_helicopter_blades[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n260_shovel_brigade_caged( + &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.x += 0x200; + self.y -= 0x400; + self.action_num = 1; + self.anim_num = 0; + self.anim_counter = 0; + } + + if self.rng.range(0..160) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 12 { + self.action_num = 1; + self.anim_num = 0; + } + } + 10 => { + self.action_num = 11; + self.anim_num = 2; + + // create heart + let mut npc = NPC::create(87, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y - 0x2000; + + let _ = npc_list.spawn(0x100, npc); + } + _ => (), + } + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + + self.anim_rect = state.constants.npc.n260_shovel_brigade_caged[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n261_chie_caged(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.x -= 0x200; + self.y -= 0x400; + self.action_num = 1; + self.action_counter = 0; + self.anim_num = 0; + } + + if self.rng.range(0..160) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 12 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => (), + } + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n261_chie_caged[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n262_chaco_caged(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.x -= 0x200; + self.y -= 0x400; + self.action_num = 1; + self.action_counter = 0; + self.anim_num = 0; + } + + if self.rng.range(0..160) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 12 { + self.action_num = 1; + self.anim_num = 0; + } + } + _ => (), + } + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n262_chaco_caged[self.anim_num as usize + dir_offset]; + + Ok(()) + } +} diff --git a/src/npc/ai/balrog.rs b/src/npc/ai/balrog.rs index 7cfddd2..462a7a5 100644 --- a/src/npc/ai/balrog.rs +++ b/src/npc/ai/balrog.rs @@ -1212,7 +1212,6 @@ impl NPC { self.anim_rect = state.constants.npc.n169_balrog_shooting_missiles[self.anim_num as usize + dir_offset]; - Ok(()) } @@ -1280,4 +1279,37 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n306_balrog_nurse(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + self.anim_num = 0; + self.y += 0x800; + } + + 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.n306_balrog_nurse[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/ai/characters.rs b/src/npc/ai/characters.rs index 8ab8b50..769d298 100644 --- a/src/npc/ai/characters.rs +++ b/src/npc/ai/characters.rs @@ -566,4 +566,248 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n278_little_family(&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.vel_x = 0; + } + + if self.rng.range(0..60) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + if self.rng.range(0..60) == 1 { + self.action_num = 10; + 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; + } + } + 10 | 11 => { + if self.action_counter == 10 { + self.action_num = 11; + self.action_counter = self.rng.range(0..16) as u16; + self.anim_num = 0; + self.direction = if self.rng.range(0..9) % 2 != 0 { Direction::Left } else { Direction::Right }; + } + + 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.vel_x = self.direction.vector_x() * 0x100; + + self.animate(4, 0, 1); + self.action_counter += 1; + if self.action_counter > 32 { + self.action_num = 0; + } + } + _ => (), + } + + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + let offset = match self.event_num { + 200 => 0, + 210 => 2, + _ => 4, + }; + + self.anim_rect = state.constants.npc.n278_little_family[self.anim_num as usize + offset]; + + Ok(()) + } + + pub(crate) fn tick_n305_small_puppy(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x2000; + self.anim_counter = self.rng.range(0..6) as u16; + } + + if self.action_num == 1 { + self.animate(6, 0, 1); + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n305_small_puppy[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n326_sue_itoh_human_transition( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 0; + self.x += 0x2000; + self.y -= 0x1000; + } + + self.action_counter += 1; + if self.action_counter > 80 { + self.action_num = 10; + self.action_counter = 0; + } else { + if self.direction != Direction::Left { + if self.action_counter == 50 { + self.anim_num = 1; + } + if self.action_counter == 60 { + self.anim_num = 0; + } + } else { + if self.action_counter == 30 { + self.anim_num = 1; + } + if self.action_counter == 40 { + self.anim_num = 0; + } + } + } + } + 10 => { + self.action_counter += 1; + if self.action_counter <= 50 { + self.anim_num = if self.action_counter & 2 != 0 { 2 } else { 3 }; + } else { + self.action_num = 15; + self.anim_num = 4; + + let actr: &mut i16 = unsafe { std::mem::transmute(&mut self.action_counter) }; + if self.direction == Direction::Left { + *actr = -20; + } else { + *actr = 0; + } + } + } + 15 => { + let actr: &mut i16 = unsafe { std::mem::transmute(&mut self.action_counter) }; + *actr += 1; + if *actr > 40 { + *actr = 0; + self.action_num = 20; + } + } + 20 => { + self.vel_y += 0x40; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 30; + self.action_counter = 0; + self.anim_num = 6; + + let mut npc = NPC::create(327, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = if self.direction == Direction::Left { self.y - 0x1000 } else { self.y - 0x2000 }; + npc.parent_id = self.id; + + let _ = npc_list.spawn(0x100, npc); + } + } + 30 => { + self.action_counter += 1; + if self.action_counter == 30 { + self.anim_num = 7; + } + if self.action_counter == 40 { + self.action_num = 40; + } + } + 40 | 41 => { + if self.action_num == 40 { + self.action_num = 41; + self.action_counter = 0; + self.anim_num = 0; + } + + self.action_counter += 1; + if self.action_counter == 30 { + self.anim_num = 1; + } + if self.action_counter == 40 { + self.anim_num = 0; + } + } + _ => (), + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 8 }; + + self.anim_rect = state.constants.npc.n326_sue_itoh_human_transition[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n327_sneeze(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + self.action_counter += 1; + + if self.action_num == 0 { + if self.action_counter < 4 { + self.y -= 0x400; + } + + if let Some(parent) = self.get_parent_ref_mut(npc_list) { + if parent.anim_num == 7 { + self.action_num = 1; + self.anim_num = 1; + self.target_x = self.x; + self.target_y = self.y; + } + } + } else if self.action_num == 1 { + if self.action_counter >= 48 { + self.x = self.target_x; + self.y = self.target_y; + } else { + self.x += self.target_x + self.rng.range(-1..1) * 0x200; + self.y += self.target_y + self.rng.range(-1..1) * 0x200; + } + } + + if self.action_counter > 70 { + self.cond.set_alive(false); + } + + self.anim_rect = state.constants.npc.n327_sneeze[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/curly.rs b/src/npc/ai/curly.rs index 4061934..dab1335 100644 --- a/src/npc/ai/curly.rs +++ b/src/npc/ai/curly.rs @@ -786,7 +786,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n259_curly_unconcious( + pub(crate) fn tick_n259_curly_unconscious( &mut self, state: &mut SharedGameState, players: [&mut Player; 2], diff --git a/src/npc/ai/doctor.rs b/src/npc/ai/doctor.rs index e5d9e99..87a9f6e 100644 --- a/src/npc/ai/doctor.rs +++ b/src/npc/ai/doctor.rs @@ -1,7 +1,11 @@ -use crate::common::Direction; +use crate::common::{Direction, Rect, 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; +use crate::stage::Stage; impl NPC { pub(crate) fn tick_n139_doctor(&mut self, state: &mut SharedGameState) -> GameResult { @@ -112,6 +116,98 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n256_doctor_facing_away( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x1000; + + state.npc_super_pos.0 = 0; + } + + self.anim_num = 0; + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 0; + self.anim_counter = 0; + self.action_counter2 = 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; + + self.action_counter2 += 1; + if self.action_counter2 > 5 { + self.action_num = 1; + } + } + } + } + 20 | 21 => { + self.action_num = 21; + self.anim_num = 2; + } + 40 | 41 => { + if self.action_num == 40 { + self.action_num = 41; + + let mut npc = NPC::create(257, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x - 0x1c00; + npc.y = self.y - 0x2000; + + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.direction = Direction::Right; + let _ = npc_list.spawn(0xaa, npc); + } + + self.anim_num = 4; + } + 50 | 51 => { + if self.action_num == 50 { + self.action_num = 51; + self.anim_num = 4; + self.anim_counter = 0; + self.action_counter2 = 0; + } + + self.anim_counter += 1; + if self.anim_counter > 5 { + self.anim_counter = 0; + + self.anim_num += 1; + if self.anim_num > 5 { + self.anim_num = 4; + + self.action_counter2 += 1; + if self.action_counter2 > 5 { + self.action_num = 41; + } + } + } + } + _ => (), + } + + self.anim_rect = state.constants.npc.n256_doctor_facing_away[self.anim_num as usize]; + + Ok(()) + } + pub(crate) fn tick_n257_red_crystal(&mut self, state: &mut SharedGameState) -> GameResult { if self.action_num == 0 { self.action_num = 1; @@ -155,4 +251,916 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n263_doctor_boss( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 => { + self.action_num = 1; + self.anim_num = 3; + self.y += 0x1000; + } + 2 => { + self.action_counter += 1; + self.anim_num = if (self.action_counter & 2) != 0 { 0 } else { 3 }; + + if self.action_counter > 50 { + self.action_num = 10; + } + } + 10 => { + self.vel_y += 0x80; + self.npc_flags.set_shootable(true); + self.damage = 3; + + if self.flags.hit_bottom_wall() { + self.action_num = 20; + self.action_counter = 0; + self.anim_num = 0; + self.action_counter3 = self.life; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 20 => { + self.action_counter += 1; + if self.action_counter < 50 && (20 + self.life) < self.action_counter3 { + self.action_counter = 50; + } + + if self.action_counter == 50 { + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + self.anim_num = 4; + } + + if self.action_counter == 80 { + self.anim_num = 5; + state.sound_manager.play_sfx(25); + + let mut npc = NPC::create(264, &state.npc_table); + npc.cond.set_alive(true); + npc.y = self.y; + + if self.direction == Direction::Left { + npc.x = self.x - 0x2000; + npc.tsc_direction = 0x0; + + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 0x400; + let _ = npc_list.spawn(0x100, npc.clone()); + } else { + npc.x = self.x + 0x2000; + npc.tsc_direction = 0x2; + + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 0x402; + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + + if self.action_counter == 120 { + self.anim_num = 0; + } + + if self.action_counter > 130 && (50 + self.life) < self.action_counter3 { + self.action_counter = 161; + } + + if self.action_counter > 160 { + self.action_num = 100; + self.anim_num = 0; + } + } + 30 | 31 => { + if self.action_num == 30 { + self.action_num = 31; + self.action_counter = 0; + self.anim_num = 6; + self.target_x = self.x; + self.npc_flags.set_shootable(true); + } + + self.action_counter += 1; + self.x = if (self.action_counter & 2) != 0 { self.target_x } else { self.target_x + 0x200 }; + + if self.action_counter > 50 { + self.action_num = 32; + self.action_counter = 0; + self.anim_num = 7; + + state.sound_manager.play_sfx(101); + + let mut npc = NPC::create(266, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + for i in (8..256).step_by(16) { + npc.vel_x = ((i as f64 * CDEG_RAD).cos() * -1024.0) as i32; + npc.vel_y = ((i as f64 * CDEG_RAD).sin() * -1024.0) as i32; + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + } + 32 => { + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 100; + self.anim_num = 0; + } + } + 100 | 101 => { + if self.action_num == 100 { + self.action_num = 101; + self.npc_flags.set_shootable(false); + self.damage = 0; + self.action_counter = 0; + + state.sound_manager.play_sfx(29); + } + + self.action_counter += 2; + if self.action_counter > 16 { + self.action_num = 102; + self.action_counter = 0; + self.anim_num = 3; + self.target_x = self.rng.range(5..35) * 0x2000; + self.target_y = self.rng.range(5..7) * 0x2000; + } + } + 102 => { + self.action_counter += 1; + if self.action_counter > 40 { + self.action_num = 103; + self.action_counter = 16; + self.anim_num = 2; + self.vel_y = 0; + self.x = self.target_x; + self.y = self.target_y; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 103 => { + self.action_counter = self.action_counter.saturating_sub(2); + if self.action_counter == 0 { + self.npc_flags.set_shootable(true); + self.damage = 3; + if self.action_counter2 < 4 { + self.action_num = 10; + self.action_counter2 += 1; + } else { + self.action_num = 30; + self.action_counter2 = 0; + } + } + } + 500 => { + self.npc_flags.set_shootable(false); + self.anim_num = 6; + self.vel_y += 0x10; + + if self.flags.hit_bottom_wall() { + self.action_num = 501; + self.action_counter = 0; + self.target_x = self.x; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 501 => { + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + self.anim_num = 8; + self.action_counter += 1; + self.x = if (self.action_counter & 2) != 0 { self.target_x } else { self.target_x + 0x200 }; + } + _ => (), + } + + if self.action_num == 102 { + state.npc_super_pos = (self.target_x, self.target_y); + } else { + state.npc_super_pos = (self.x, self.y); + } + + 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 { 9 }; + + self.anim_rect = state.constants.npc.n263_doctor_boss[self.anim_num as usize + dir_offset]; + + if self.action_num == 101 || self.action_num == 103 { + self.anim_rect.top += self.action_counter; + self.anim_rect.bottom -= self.action_counter; + self.display_bounds.top = (16 - self.action_counter as u32) * 0x200; + } else { + self.display_bounds.top = 0x2000; + } + + Ok(()) + } + + pub(crate) fn tick_n264_doctor_boss_red_projectile( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + stage: &mut Stage, + ) -> GameResult { + if self.x < 0 || self.x > stage.map.width as i32 * state.tile_size.as_int() * 0x200 { + self.vanish(state); + return Ok(()); + } + + if self.action_num == 0 { + self.action_num = 1; + self.target_x = self.x; + self.target_y = self.y; + self.action_counter2 = self.tsc_direction / 8; + self.tsc_direction %= 8; + } + + if self.action_num == 1 { + self.action_counter2 += 6; + self.action_counter2 &= 0xff; + + if self.action_counter < 128 { + self.action_counter += 1; + } + + self.vel_x += if self.tsc_direction != 0 { 0x15 } else { -0x15 }; + self.target_x += self.vel_x; + + let angle = self.action_counter2 as f64 * CDEG_RAD; + self.x = self.target_x + self.action_counter as i32 * (angle.cos() * -512.0) as i32 / 8; + self.y = self.target_y + self.action_counter as i32 * (angle.sin() * -512.0) as i32 / 2; + + let mut npc = NPC::create(265, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc); + } + + self.anim_rect = state.constants.npc.n264_doctor_boss_red_projectile; + + Ok(()) + } + + pub(crate) fn tick_n265_doctor_boss_red_projectile_trail(&mut self, state: &mut SharedGameState) -> GameResult { + self.anim_counter += 1; + if self.anim_counter > 3 { + self.anim_counter = 0; + self.anim_num += 1; + } + + if self.anim_num > 2 { + self.cond.set_alive(false); + } else { + self.anim_rect = state.constants.npc.n265_doctor_boss_red_projectile_trail[self.anim_num as usize]; + } + + Ok(()) + } + + pub(crate) fn tick_n266_doctor_boss_red_projectile_bouncing( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + if self.flags.hit_left_wall() { + self.vel_x = -self.vel_x; + } + + if self.flags.hit_right_wall() { + self.vel_x = -self.vel_x; + } + + if self.flags.hit_top_wall() { + self.vel_y = 0x200; + } + + if self.flags.hit_bottom_wall() { + self.vel_y = -0x200; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + + self.anim_rect = state.constants.npc.n266_doctor_boss_red_projectile_bouncing[self.anim_num as usize]; + + self.action_counter += 1; + if self.action_counter % 4 == 1 { + let mut npc = NPC::create(265, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc); + } + + if self.action_counter > 250 { + self.vanish(state); + } + + Ok(()) + } + + pub(crate) fn tick_n267_muscle_doctor( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 | 2 => { + if self.action_num == 0 { + let player = self.get_closest_player_ref(&players); + self.direction = if state.npc_super_pos.0 > player.x { Direction::Left } else { Direction::Right }; + + self.x = state.npc_super_pos.0 + self.direction.vector_x() * 0xc00; + self.y = state.npc_super_pos.1; + } + + if self.action_num == 1 { + self.action_num = 2; + } + + self.vel_y += 0x80; + self.action_counter += 1; + self.anim_num = if (self.action_counter & 2) != 0 { 0 } else { 3 }; + } + 5 | 6 => { + if self.action_num == 5 { + self.action_num = 6; + self.anim_num = 1; + self.anim_counter = 0; + } + + self.vel_y += 0x80; + self.animate(40, 1, 2); + } + 7 | 8 => { + if self.action_num == 7 { + self.action_num = 8; + self.action_counter = 0; + self.anim_num = 3; + } + + self.vel_y += 0x40; + self.action_counter += 1; + if self.action_counter > 40 { + self.action_num = 10; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.npc_flags.set_invulnerable(true); + self.vel_x = 0; + self.action_num = 11; + self.action_counter = 0; + self.anim_num = 1; + self.anim_counter = 0; + self.action_counter3 = self.life; + } + + self.vel_y += 0x80; + let player = self.get_closest_player_mut(players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + if self.flags.hit_bottom_wall() { + if self.life + 20 > self.action_counter3 { + self.animate(10, 1, 2); + } else if player.flags.hit_bottom_wall() && player.x > self.x - 0x6000 && player.x < self.x + 0x6000 + { + self.anim_num = 6; + state.quake_counter = 10; + state.sound_manager.play_sfx(26); + + player.damage(5, state, npc_list); + player.vel_y = -0x400; + player.vel_x = if self.x > player.x { -0x5FF } else { 0x5FF }; + + let mut npc = NPC::create(270, &state.npc_table); + npc.cond.set_alive(true); + npc.direction = Direction::Bottom; + + for _ in 0..100 { + npc.x = self.x + (self.rng.range(-16..16) as i32) * 0x200; + npc.y = self.y + (self.rng.range(-16..16) as i32) * 0x200; + npc.vel_x = 3 * self.rng.range(-512..512); + npc.vel_y = 3 * self.rng.range(-512..512); + + let _ = npc_list.spawn(0xaa, npc.clone()); + } + } + } else { + self.anim_num = 4; + } + + self.action_counter += 1; + if self.action_counter > 30 || self.life + 20 < self.action_counter3 { + self.action_counter2 += 1; + if self.action_counter2 > 10 { + self.action_counter2 = 0; + } + + match self.action_counter2 { + 1 | 9 => { + self.action_num = 40; + } + 2 | 7 => { + self.action_num = 100; + } + 3 | 6 => { + self.action_num = 30; + } + 8 => { + self.action_num = 20; + } + _ => { + self.action_num = 15; + self.action_counter = 0; + } + } + } + } + 15 => { + self.anim_num = 3; + self.action_counter += 1; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + if self.action_counter > 20 { + self.action_num = 16; + self.anim_num = 4; + self.anim_counter = 0; + + self.vel_x = self.direction.vector_x() * 0x400; + self.vel_y = -0x600; + } + } + 16 => { + self.vel_y += 0x40; + self.animate(1, 4, 5); + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + if self.vel_y > 0 && self.flags.hit_bottom_wall() { + self.action_num = 17; + } + } + 17 | 18 => { + if self.action_num == 17 { + self.action_num = 18; + self.action_counter = 0; + + state.quake_counter = 10; + state.sound_manager.play_sfx(26); + } + + self.anim_num = 3; + self.action_counter += 1; + self.vel_x = 7 * self.vel_x / 8; + self.vel_y += 0x80; + + if self.action_counter > 10 { + self.action_num = 10; + } + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.action_counter = 0; + } + + self.anim_num = 6; + self.action_counter += 1; + if self.action_counter > 20 && self.action_counter % 3 == 1 { + let mut npc = NPC::create(269, &state.npc_table); + + npc.cond.set_alive(true); + npc.x = self.x + if self.direction == Direction::Left { -0x1000 } else { 0x1000 }; + npc.y = self.y - 0x800; + npc.vel_x = 4 * self.rng.range(256..512) * self.direction.vector_x(); + npc.vel_y = self.rng.range(-512..512); + npc.direction = self.direction; + + let _ = npc_list.spawn(0x100, npc); + state.sound_manager.play_sfx(39); + } + + if self.action_counter > 90 { + self.action_num = 10; + } + } + 30 | 31 => { + if self.action_num == 30 { + self.action_num = 31; + self.action_counter = 0; + + self.npc_flags.set_solid_soft(true); + self.npc_flags.set_shootable(false); + } + + self.anim_num = 3; + self.action_counter += 1; + if self.action_counter > 20 { + self.action_num = 32; + self.action_counter = 0; + self.anim_num = 7; + + self.npc_flags.set_rear_and_top_not_hurt(true); + self.damage = 10; + self.vel_x = self.direction.vector_x() * 0x5FF; + + state.sound_manager.play_sfx(25); + } + } + 32 => { + self.action_counter += 1; + self.vel_y = 0; + + self.anim_num = if self.action_counter & 2 != 0 { 7 } else { 8 }; + + if self.action_counter > 30 { + self.action_num = 18; + self.action_counter = 0; + self.damage = 5; + + self.npc_flags.set_rear_and_top_not_hurt(false); + self.npc_flags.set_solid_soft(false); + self.npc_flags.set_shootable(true); + } + + if self.flags.hit_left_wall() || self.flags.hit_right_wall() { + self.action_num = 15; + self.action_counter = 0; + self.damage = 5; + + self.npc_flags.set_rear_and_top_not_hurt(false); + self.npc_flags.set_solid_soft(false); + self.npc_flags.set_shootable(true); + } + } + 40 => { + self.anim_num = 3; + self.action_counter += 1; + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + + if self.action_counter > 20 { + self.action_num = 41; + self.action_counter = 0; + self.anim_num = 4; + self.vel_y = -0x800; + self.vel_x = self.direction.vector_x() * 0x400; + } + } + 41 => { + self.vel_y += 0x40; + self.animate(1, 4, 5); + + let player = self.get_closest_player_ref(&players); + if player.y > self.y && player.x > self.x - 0x1000 && player.x < self.x + 0x1000 { + self.action_num = 16; + self.vel_y = 0x5FF; + self.vel_x = 0; + } + + if self.vel_y > 0 && self.flags.hit_bottom_wall() { + self.action_num = 17; + } + } + 100 | 101 => { + if self.action_num == 100 { + self.action_num = 101; + self.action_counter = 0; + self.npc_flags.set_shootable(false); + self.npc_flags.set_invulnerable(false); + self.damage = 0; + state.sound_manager.play_sfx(29); + } + + self.action_counter += 2; + if self.action_counter > 28 { + self.action_num = 102; + self.action_counter = 0; + self.anim_num = 0; + + let player = self.get_closest_player_ref(&players); + self.target_x = player.x; + self.target_y = player.y - 0x4000; + + self.target_x = self.target_x.clamp(0x8000, 0x48000); + if self.target_y < 0x8000 { + self.target_y = 0x8000; + } + } + } + 102 => { + self.action_counter += 1; + if self.action_counter > 40 { + self.action_num = 103; + self.action_counter = 28; + self.anim_num = 4; + self.vel_y = 0; + self.x = self.target_x; + self.y = self.target_y; + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + } + } + 103 => { + if self.action_counter > 0 { + self.action_counter -= 2; + } else { + self.action_num = 16; + self.vel_x = 0; + self.vel_y = -0x200; + + self.npc_flags.set_shootable(true); + self.npc_flags.set_invulnerable(true); + self.damage = 5; + } + } + 500 => { + npc_list.kill_npcs_by_type(269, true, state); + + self.npc_flags.set_shootable(false); + self.anim_num = 4; + self.vel_y += 0x20; + self.vel_x = 0; + + if self.flags.hit_bottom_wall() { + self.action_num = 501; + self.action_counter = 0; + self.target_x = self.x; + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + } + } + 510 | 511 => { + if self.action_num == 510 { + self.action_num = 511; + self.action_counter = 0; + self.anim_num = 9; + self.target_x = self.x; + self.y += 0x2000; + self.npc_flags.set_ignore_solidity(true); + } + + state.quake_counter = 2; + self.action_counter += 1; + if self.action_counter % 6 == 3 { + state.sound_manager.play_sfx(25); + } + + self.x = if self.action_counter & 2 != 0 { self.target_x } else { self.target_x + 512 }; + + if self.action_counter > 352 { + self.action_num = 512; + self.anim_num = 0; + } + } + 520 => { + self.damage = 0; + state.npc_super_pos.1 = -0x4000; + } + _ => (), + } + + if self.action_num > 10 && self.action_num <= 500 { + if self.action_num == 102 { + state.npc_super_pos = (self.target_x, self.target_y); + } else { + state.npc_super_pos = (self.x, self.y); + } + } + + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.action_num < 512 { + if self.action_num >= 510 { + let mut npc = NPC::create(270, &state.npc_table); + npc.cond.set_alive(true); + + for i in 0..4 { + npc.x = self.x + self.rng.range(-0x10..0x10) * 0x200; + npc.y = self.y - ((0x150 - self.action_counter as i32) / 8) * 0x200; + npc.vel_x = 2 * self.rng.range(-0x200..0); + npc.vel_y = if i >= 2 { 0 } else { self.rng.range(-0x200..0x200) }; + npc.direction = Direction::Bottom; + let _ = npc_list.spawn(0xAA, npc.clone()); + } + } else if self.action_num != 102 && self.action_num != 103 && self.rng.range(0..3) == 2 { + let mut npc = NPC::create(270, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x + self.rng.range(-0x10..0x10) * 0x200; + npc.y = self.y + self.rng.range(-8..4) * 0x200; + npc.vel_x = self.vel_x; + npc.vel_y = 0; + npc.direction = Direction::Bottom; + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 10 }; + + self.anim_rect = state.constants.npc.n267_muscle_doctor[self.anim_num as usize + dir_offset]; + + if self.action_num == 511 { + self.anim_rect.top += self.action_counter / 8; + self.display_bounds.top = (44u32).saturating_sub(self.action_counter as u32 / 8) * 0x200; + self.display_bounds.bottom = 0x800; + } else if self.action_num == 101 || self.action_num == 103 { + self.anim_rect.top += self.action_counter; + self.anim_rect.bottom -= self.action_counter; + self.display_bounds.top = (28u32).saturating_sub(self.action_counter as u32) * 0x200; + } else { + self.display_bounds.top = 0x3800; + } + + Ok(()) + } + + pub(crate) fn tick_n269_red_bat_bouncing(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.vel_x2 = self.vel_x; + self.vel_y2 = self.vel_y; + } + + if self.action_num == 1 { + if self.vel_x2 < 0 && self.flags.hit_left_wall() { + self.direction = Direction::Right; + self.vel_x2 = -self.vel_x2; + } else if self.vel_x2 > 0 && self.flags.hit_right_wall() { + self.direction = Direction::Left; + self.vel_x2 = -self.vel_x2; + } else if self.vel_y2 < 0 && self.flags.hit_top_wall() { + self.vel_y2 = -self.vel_y2; + } else if self.vel_y2 > 0 && self.flags.hit_bottom_wall() { + self.vel_y2 = -self.vel_y2; + } + + self.x += self.vel_x2; + self.y += self.vel_y2; + self.animate(2, 0, 2); + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 3 }; + self.anim_rect = state.constants.npc.n269_red_bat_bouncing[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n270_doctor_red_energy( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + if self.direction == Direction::Bottom || self.direction == Direction::Up { + self.vel_y += self.direction.vector_y() * 0x40; + + self.action_counter += 1; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.x += self.vel_x; + self.y += self.vel_y; + if self.action_counter > 50 || self.flags.any_flag() { + self.cond.set_alive(false) + } + } else if self.direction == Direction::Right { + if self.action_num == 0 { + self.action_num = 1; + self.npc_flags.set_ignore_solidity(true); + self.vel_x = 3 * self.rng.range(-0x200..0x200); + self.vel_y = 3 * self.rng.range(-0x200..0x200); + self.action_counter2 = self.rng.range(0x10..0x33) as u16; + self.action_counter3 = self.rng.range(0x80..0x100) as u16; + } + + if let Some(parent) = self.get_parent_ref_mut(npc_list) { + if self.x < parent.x { + self.vel_x += 0x200 / self.action_counter2 as i32; + } + if self.x > parent.x { + self.vel_x -= 0x200 / self.action_counter2 as i32; + } + if self.y < parent.y { + self.vel_y += 0x200 / self.action_counter2 as i32; + } + if self.y > parent.y { + self.vel_y -= 0x200 / self.action_counter2 as i32; + } + if self.vel_x > 2 * self.action_counter3 as i32 { + self.vel_x = 2 * self.action_counter3 as i32; + } + if self.vel_x < -2 * self.action_counter3 as i32 { + self.vel_x = -2 * self.action_counter3 as i32; + } + if self.vel_y > 3 * self.action_counter3 as i32 { + self.vel_y = 3 * self.action_counter3 as i32; + } + if self.vel_y < -3 * self.action_counter3 as i32 { + self.vel_y = -3 * self.action_counter3 as i32; + } + self.x += self.vel_x; + self.y += self.vel_y; + } + } + + self.anim_rect = state.constants.npc.n270_doctor_red_energy[self.rng.range(0..1) as usize]; + + Ok(()) + } + + pub(crate) fn tick_n281_doctor_energy_form( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 => { + self.action_num = 1; + self.action_counter = 0; + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 0; + } + + self.action_counter += 1; + + let mut npc = NPC::create(270, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y + 0x10000; + npc.direction = Direction::Right; + npc.parent_id = self.id; + + let _ = npc_list.spawn(0x100, npc); + + if self.action_counter > 150 { + self.action_num = 12; + } + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.action_counter = 0; + } + + self.action_counter += 1; + if self.action_counter > 250 { + self.action_num = 22; + npc_list.remove_by_type(270, state); + } + } + _ => (), + } + + self.anim_rect = Rect::new(0, 0, 0, 0); + + Ok(()) + } } diff --git a/src/npc/ai/hell.rs b/src/npc/ai/hell.rs new file mode 100644 index 0000000..f0245c9 --- /dev/null +++ b/src/npc/ai/hell.rs @@ -0,0 +1,75 @@ +use crate::{GameResult, SharedGameState}; +use crate::caret::CaretType; +use crate::common::Direction; +use crate::npc::NPC; +use crate::rng::RNG; + +impl NPC { + pub(crate) fn tick_n337_numahachi(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x1000; + } + + if self.action_num == 1 { + self.action_num = 2; + self.anim_num = 0; + self.vel_x = 0; + } + + if self.action_num == 2{ + self.animate(50, 0, 1); + } + + self.vel_y += 0x40; + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n337_numahachi[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n357_puppy_ghost(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 => { + self.anim_rect = state.constants.npc.n357_puppy_ghost; + self.action_counter += 1; + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 0; + state.sound_manager.play_sfx(29); + } + + self.action_counter += 1; + self.anim_rect = state.constants.npc.n357_puppy_ghost; + + if self.action_counter & 2 != 0 { + self.anim_rect.right = self.anim_rect.left; + } + + if self.action_counter > 50 { + self.cond.set_alive(false); + } + } + _ => (), + } + + if self.action_counter & 8 == 1 { + state.create_caret( + self.x + self.rng.range(-8..8) * 0x200, + self.y + 0x1000, + CaretType::LittleParticles, + Direction::Up, + ); + } + + Ok(()) + } +} diff --git a/src/npc/ai/igor.rs b/src/npc/ai/igor.rs index 10fbeb9..fb053ce 100644 --- a/src/npc/ai/igor.rs +++ b/src/npc/ai/igor.rs @@ -1,4 +1,4 @@ -use crate::common::{CDEG_RAD, Direction}; +use crate::common::{Direction, CDEG_RAD}; use crate::framework::error::GameResult; use crate::npc::list::NPCList; use crate::npc::NPC; @@ -88,7 +88,12 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n088_igor_boss(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], npc_list: &NPCList) -> GameResult { + pub(crate) fn tick_n088_igor_boss( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -98,14 +103,7 @@ impl NPC { self.vel_x = 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; - } - } + self.animate(5, 0, 1); self.action_counter += 1; if self.action_counter > 50 { @@ -131,14 +129,7 @@ impl NPC { } self.action_counter += 1; - 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.animate(3, 2, 5); self.vel_x = self.direction.vector_x() * 0x200; @@ -155,8 +146,9 @@ impl NPC { self.damage = 2; self.vel_x = (self.vel_x * 3) / 2; self.vel_y = -0x400; - } else if (self.direction == Direction::Left && self.x - 24 * 0x200 < player.x) - || (self.direction == Direction::Right && self.x + 24 * 0x200 > player.x) { + } else if (self.direction == Direction::Left && self.x - 0x3000 < player.x) + || (self.direction == Direction::Right && self.x + 0x3000 > player.x) + { self.action_num = 4; } } @@ -174,7 +166,7 @@ impl NPC { self.action_counter = 0; self.anim_num = 7; self.damage = 5; - self.hit_bounds.left = 24 * 0x200; + self.hit_bounds.left = 0x3000; self.hit_bounds.top = 1; state.sound_manager.play_sfx(70); @@ -227,13 +219,15 @@ impl NPC { self.action_num = 10; self.action_counter = 0; - let player = self.get_closest_player_mut(players); + let player = self.get_closest_player_ref(&players); self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; } self.action_counter += 1; if self.action_counter > 100 && self.action_counter % 6 == 1 { - let deg = (if self.direction == Direction::Left { 0x88 } else { 0xf8 } + self.rng.range(-16..16)) as f64 * CDEG_RAD; + let deg = (if self.direction == Direction::Left { 0x88 } else { 0xf8 } + self.rng.range(-16..16)) + as f64 + * CDEG_RAD; let vel_x = (deg.cos() * 1536.0) as i32; let vel_y = (deg.sin() * 1536.0) as i32; @@ -270,12 +264,18 @@ impl NPC { self.y += self.vel_y; let dir_offset = if self.direction == Direction::Left { 0 } else { 12 }; + self.anim_rect = state.constants.npc.n088_igor_boss[self.anim_num as usize + dir_offset]; Ok(()) } - pub(crate) fn tick_n089_igor_dead(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], npc_list: &NPCList) -> GameResult { + pub(crate) fn tick_n089_igor_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 { @@ -327,13 +327,13 @@ impl NPC { self.action_counter += 1; if (self.action_counter & 0x02) != 0 && self.action_counter < 100 { self.anim_num = 0; - self.display_bounds.left = 20 * 0x200; - self.display_bounds.right = 20 * 0x200; - self.display_bounds.top = 20 * 0x200; + self.display_bounds.left = 0x2800; + self.display_bounds.right = 0x2800; + self.display_bounds.top = 0x2800; } else { self.anim_num = 1; - self.display_bounds.left = 12 * 0x200; - self.display_bounds.right = 12 * 0x200; + self.display_bounds.left = 0x1800; + self.display_bounds.right = 0x1800; self.display_bounds.top = 0x1000; } @@ -378,4 +378,158 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n268_igor_enemy( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + let player = self.get_closest_player_ref(&players); + + if self.x < player.x - 0x28000 + || self.x > player.x + 0x28000 + || self.y < player.y - 0x1E000 + || self.y > player.y + 0x1E000 + { + self.action_num = 1; + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.y += 0x1000; + } + + self.animate(20, 0, 1); + + if self.x < player.x + 0xE000 + && self.x > player.x - 0xE000 + && self.x < player.x + 0x6000 + && self.x > player.x - 0xE000 + { + self.action_num = 10; + } + + if self.shock != 0 { + self.action_num = 10; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 0; + self.anim_num = 0; + self.action_counter = 0; + self.direction = if self.x < player.x { Direction::Right } else { Direction::Left }; + } + + self.vel_x = self.direction.vector_x() * 0x200; + if self.x < player.x + 0x8000 && self.x > player.x - 0x8000 { + self.action_num = 20; + self.action_counter = 0; + } + + if self.vel_x < 0 && self.flags.hit_left_wall() { + self.action_num = 20; + self.action_counter = 0; + } + + if self.vel_x > 0 && self.flags.hit_right_wall() { + self.action_num = 20; + self.action_counter = 0; + } + + self.animate(4, 2, 5); + } + 20 => { + self.vel_x = 0; + self.anim_num = 6; + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 30; + + self.vel_x = self.direction.vector_x() * 0x200; + self.vel_y = -0x5FF; + + state.sound_manager.play_sfx(108); + } + } + 30 => { + self.anim_num = 7; + if self.flags.hit_bottom_wall() { + self.action_num = 40; + self.action_counter = 0; + state.quake_counter = 20; + state.sound_manager.play_sfx(26); + } + } + 40 => { + self.vel_x = 0; + self.anim_num = 6; + self.action_counter += 1; + if self.action_counter > 30 { + self.action_num = 50; + } + } + 50 | 51 => { + if self.action_num == 50 { + self.action_num = 51; + self.action_counter = 0; + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + if self.action_counter > 30 && self.action_counter % 4 == 1 { + let deg = (if self.direction == Direction::Left { 0x88 } else { 0xf8 } + self.rng.range(-16..16)) + as f64 + * CDEG_RAD; + let vel_x = (deg.cos() * 1536.0) as i32; + let vel_y = (deg.sin() * 1536.0) as i32; + + let mut npc = NPC::create(11, &state.npc_table); + + npc.cond.set_alive(true); + npc.direction = Direction::Left; + npc.x = self.x; + npc.y = self.y + 0x800; + npc.vel_x = vel_x; + npc.vel_y = vel_y; + + let _ = npc_list.spawn(0x100, npc); + state.sound_manager.play_sfx(12); + } + + if self.action_counter < 50 && self.action_counter & 2 != 0 { + self.anim_num = 9; + } else { + self.anim_num = 8; + } + + if self.action_counter > 82 { + self.action_num = 10; + self.direction = if player.x < self.x { Direction::Left } else { Direction::Right }; + } + } + _ => (), + } + + self.vel_y += 0x33; + 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 { 10 }; + + self.anim_rect = state.constants.npc.n268_igor_enemy[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/ai/last_cave.rs b/src/npc/ai/last_cave.rs index 875c2ea..edbd72e 100644 --- a/src/npc/ai/last_cave.rs +++ b/src/npc/ai/last_cave.rs @@ -1,7 +1,10 @@ +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; impl NPC { @@ -18,7 +21,7 @@ impl NPC { self.anim_num = 0; } - let player = self.get_closest_player_mut(players); + let player = self.get_closest_player_ref(&players); if self.x > player.x { self.direction = Direction::Left; } else { @@ -106,4 +109,371 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n242_bat_last_cave(&mut self, state: &mut SharedGameState) -> GameResult { + loop { + match self.action_num { + 0 => { + self.action_num = 1; + self.target_x = self.x; + self.target_y = self.y; + self.action_counter = self.rng.range(0..50) as u16; + continue; + } + 1 if self.action_counter > 0 => { + self.action_counter -= 1; + } + 1 => { + self.action_num = 2; + self.vel_y = 0x400; + continue; + } + 2 => { + self.vel_x = self.direction.vector_x() * 0x100; + self.vel_y += (self.target_y - self.y).signum() * 0x10; + self.vel_y = self.vel_y.clamp(-0x300, 0x300); + } + _ => (), + } + break; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.animate(1, 0, 2); + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n242_bat_last_cave[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n243_bat_generator(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = self.rng.range(0..500) as u16; + } + + if self.action_counter > 0 { + self.action_counter -= 1; + } else { + self.action_num = 0; + + let mut npc = NPC::create(242, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y + self.rng.range(-32..32) * 0x200; + npc.direction = self.direction; + + let _ = npc_list.spawn(0x100, npc); + } + } + _ => return Ok(()), + } + + Ok(()) + } + + pub(crate) fn tick_n244_lava_drop(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + self.vel_y += 0x40; + + // idfk why was that there in original code but I'll leave it there in case + let mut hit = self.flags.hit_anything(); + + if self.action_counter > 10 && self.flags.in_water() { + hit = true; + } + + if hit { + for _ in 0..3 { + state.create_caret(self.x, self.y, CaretType::Bubble, Direction::Right); + } + + let player = self.get_closest_player_ref(&players); + if self.x > player.x - 0x20000 + && self.x < player.x + 0x20000 + && self.y > player.y - 0x14000 + && self.y < player.y + 0x14000 + { + state.sound_manager.play_sfx(21); + } + + self.cond.set_alive(false); + return Ok(()); + } + + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + self.y += self.vel_y; + self.anim_rect = state.constants.npc.n244_lava_drop; + + Ok(()) + } + + pub(crate) fn tick_n245_lava_drop_generator( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.target_x = self.x; + self.action_counter = self.event_num; + } + + self.anim_num = 0; + if self.anim_counter > 0 { + self.anim_counter -= 1; + return Ok(()); + } + + self.action_num = 10; + self.anim_counter = 0; + } + 10 => { + self.anim_counter += 1; + if self.anim_counter > 10 { + self.anim_counter = 0; + self.anim_num += 1; + + if self.anim_num > 3 { + self.anim_num = 0; + self.action_num = 1; + self.action_counter = self.flag_num; + + let mut npc = NPC::create(244, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc); + } + } + } + _ => (), + } + + self.x = if (self.anim_counter & 2) != 0 { self.target_x } else { self.target_x + 0x200 }; + self.anim_rect = state.constants.npc.n245_lava_drop_generator[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n276_red_demon( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 | 2 => { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x1000; + } + + if self.action_num == 1 { + self.vel_x = 0; + self.action_num = 2; + self.anim_num = 0; + } + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + self.animate(20, 0, 1); + + if self.shock > 0 { + self.action_num = 10; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 2; + self.action_counter = 0; + self.npc_flags.set_shootable(true); + } + + self.action_counter += 1; + match self.action_counter { + 30 | 40 | 50 => { + let player = self.get_closest_player_ref(&players); + + let mut npc = NPC::create(277, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + let angle = f64::atan2((player.y - 0x1400 - self.y) as f64, (player.x - self.x) as f64); + npc.vel_x = (2048.0 * angle.cos()) as i32; + npc.vel_y = (2048.0 * angle.sin()) as i32; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(39); + } + 34 | 44 | 54 => { + self.anim_num = 3; + } + _ => (), + } + + if self.action_counter > 60 { + self.action_num = 20; + self.action_counter = 0; + self.anim_num = 2; + } + } + 20 => { + self.action_counter += 1; + if self.action_counter > 20 { + self.action_num = 21; + self.action_counter = 0; + self.anim_num = 5; + + self.vel_y = -0x5ff; + + let player = self.get_closest_player_ref(&players); + self.vel_x = if self.x < player.x { 0x100 } else { -0x100 }; + } + } + 21 => { + self.action_counter += 1; + match self.action_counter { + 30 | 40 | 50 => { + let player = self.get_closest_player_ref(&players); + + let mut npc = NPC::create(277, &state.npc_table); + npc.cond.set_alive(true); + + let angle = f64::atan2((player.y - 0x1400 - self.y) as f64, (player.x - self.x) as f64); + npc.vel_x = (2048.0 * angle.cos()) as i32; + npc.vel_y = (2048.0 * angle.sin()) as i32; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(39); + } + 34 | 44 => { + self.anim_num = 5; + } + _ => (), + } + + if self.action_counter > 53 { + self.anim_num = 7; + } + + if self.flags.hit_bottom_wall() { + self.action_num = 22; + self.action_counter = 0; + self.anim_num = 2; + state.quake_counter = 10; + state.sound_manager.play_sfx(26); + } + } + 22 => { + self.vel_x /= 2; + + self.action_counter += 1; + if self.action_counter > 22 { + self.action_num = 10; + } + } + 50 => { + self.npc_flags.set_shootable(false); + self.damage = 0; + + if self.flags.hit_bottom_wall() { + self.action_num = 51; + self.action_counter = 0; + self.anim_num = 2; + state.quake_counter = 10; + state.sound_manager.play_sfx(72); + + npc_list.create_death_smoke( + self.x, + self.y, + self.display_bounds.right as usize, + 8, + state, + &self.rng, + ); + } + } + 51 => { + self.vel_x = 7 * self.vel_x / 8; + self.anim_num = 8; + } + _ => (), + } + + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.action_num < 50 { + let player = self.get_closest_player_ref(&players); + self.direction = if self.x < player.x { Direction::Right } else { Direction::Left }; + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 9 }; + + self.anim_rect = state.constants.npc.n276_red_demon[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n277_red_demon_projectile(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + } + + if self.action_num == 1 { + self.x += self.vel_x; + self.y += self.vel_y; + + if self.flags.any_flag() { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + for _ in 0..3 { + let _ = npc_list.spawn(0x100, npc.clone()); + } + + self.vanish(state); + + return Ok(()); + } + } + + self.action_counter += 1; + if self.action_counter % 5 == 0 { + state.sound_manager.play_sfx(110); + } + + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + + self.anim_rect = state.constants.npc.n277_red_demon_projectile[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/maze.rs b/src/npc/ai/maze.rs index 941e7d0..9164a77 100644 --- a/src/npc/ai/maze.rs +++ b/src/npc/ai/maze.rs @@ -1596,4 +1596,33 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n304_gaudi_hospital(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.y += 0x1400; + } + + self.anim_num = 0; + } + 10 => { + self.anim_num = 1; + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.anim_num = 2; + } + + self.animate(10, 2, 3); + } + _ => (), + } + + self.anim_rect = state.constants.npc.n304_gaudi_hospital[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/misc.rs b/src/npc/ai/misc.rs index ca47eb4..4caa131 100644 --- a/src/npc/ai/misc.rs +++ b/src/npc/ai/misc.rs @@ -1,11 +1,11 @@ -use num_traits::{abs, clamp}; +use std::hint::unreachable_unchecked; use crate::caret::CaretType; use crate::common::{Direction, Rect}; use crate::components::flash::Flash; use crate::framework::error::GameResult; -use crate::npc::{NPC, NPCLayer}; use crate::npc::list::NPCList; +use crate::npc::{NPCLayer, NPC}; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; @@ -610,7 +610,7 @@ impl NPC { self.cond.set_alive(false); } - if self.y > stage.map.height as i32 * 0x2000 { + if self.y > stage.map.height as i32 * state.tile_size.as_int() * 0x200 { // out of map self.cond.set_alive(false); } @@ -651,7 +651,7 @@ impl NPC { self.anim_num = 0; let player = self.get_closest_player_mut(players); - if abs(player.x - self.x) < 0x1000 && player.y < self.y + 0x1000 && player.y > self.y - 0x2000 { + if (player.x - self.x).abs() < 0x1000 && player.y < self.y + 0x1000 && player.y > self.y - 0x2000 { state.sound_manager.play_sfx(43); self.action_num = 1; } @@ -666,6 +666,7 @@ impl NPC { } 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(()) @@ -706,8 +707,8 @@ impl NPC { { let i = self.get_closest_player_idx_mut(&players); - if abs(players[i].x - self.x) < 480 * 0x200 - && abs(players[i].y - self.y) < 240 * 0x200 + if (players[i].x - self.x).abs() < 480 * 0x200 + && (players[i].y - self.y).abs() < 240 * 0x200 && self.rng.range(0..5) == 1 { let mut particle = NPC::create(199, &state.npc_table); @@ -724,7 +725,7 @@ impl NPC { continue; } - if abs(player.y - self.y) < 0x1000 && player.x < self.x && player.x > self.x - 96 * 0x200 { + if (player.y - self.y).abs() < 0x1000 && player.x < self.x && player.x > self.x - 96 * 0x200 { player.vel_x -= 0x88; player.cond.set_increase_acceleration(true); } @@ -766,8 +767,8 @@ impl NPC { { let i = self.get_closest_player_idx_mut(&players); - if abs(players[i].x - self.x) < 480 * 0x200 - && abs(players[i].y - self.y) < 240 * 0x200 + if (players[i].x - self.x).abs() < 480 * 0x200 + && (players[i].y - self.y).abs() < 240 * 0x200 && self.rng.range(0..5) == 1 { let mut particle = NPC::create(199, &state.npc_table); @@ -784,7 +785,7 @@ impl NPC { continue; } - if abs(player.x - self.x) < 0x1000 && player.y < self.y && player.y > self.y - 96 * 0x200 { + if (player.x - self.x).abs() < 0x1000 && player.y < self.y && player.y > self.y - 96 * 0x200 { player.vel_y -= 0x88; } } @@ -825,8 +826,8 @@ impl NPC { { let i = self.get_closest_player_idx_mut(&players); - if abs(players[i].x - self.x) < 480 * 0x200 - && abs(players[i].y - self.y) < 240 * 0x200 + if (players[i].x - self.x).abs() < 480 * 0x200 + && (players[i].y - self.y).abs() < 240 * 0x200 && self.rng.range(0..5) == 1 { let mut particle = NPC::create(199, &state.npc_table); @@ -839,7 +840,7 @@ impl NPC { } for player in players.iter_mut() { - if abs(player.y - self.y) < 0x1000 && player.x > self.x && player.x < self.x + 96 * 0x200 { + if (player.y - self.y).abs() < 0x1000 && player.x > self.x && player.x < self.x + 96 * 0x200 { player.vel_x += 0x88; player.cond.set_increase_acceleration(true); } @@ -881,8 +882,8 @@ impl NPC { { let i = self.get_closest_player_idx_mut(&players); - if abs(players[i].x - self.x) < 480 * 0x200 - && abs(players[i].y - self.y) < 240 * 0x200 + if (players[i].x - self.x).abs() < 480 * 0x200 + && (players[i].y - self.y).abs() < 240 * 0x200 && self.rng.range(0..5) == 1 { let mut particle = NPC::create(199, &state.npc_table); @@ -895,7 +896,7 @@ impl NPC { } for player in players.iter_mut() { - if abs(player.x - self.x) < 0x1000 && player.y > self.y && player.y < self.y + 96 * 0x200 { + if (player.x - self.x).abs() < 0x1000 && player.y > self.y && player.y < self.y + 96 * 0x200 { player.vel_y += 0x88; } } @@ -959,14 +960,7 @@ impl NPC { } } 10 => { - self.anim_counter += 1; - if self.anim_counter > 2 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 2; - } - } + self.animate(2, 2, 2); for player in players.iter() { if player.y > self.y { @@ -1246,7 +1240,7 @@ impl NPC { _ => (), } - self.vel_x = clamp(self.vel_x, -0x200, 0x200); + self.vel_x = self.vel_x.clamp(-0x200, 0x200); self.x += self.vel_x; if self.anim_num != 149 { @@ -1390,7 +1384,7 @@ impl NPC { _ => (), } - self.vel_y = clamp(self.vel_y, -0x200, 0x200); + self.vel_y = self.vel_y.clamp(-0x200, 0x200); self.y += self.vel_y; if self.anim_num != 149 { @@ -1798,6 +1792,91 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n246_press_proximity( + &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.action_num = 1; + self.y -= 0x800; + } + + for player in players.iter() { + if player.x < self.x + 0x1000 + && player.x > self.x - 0x1000 + && player.y > self.y + 0x1000 + && player.y < self.y + 0x10000 + { + self.action_num = 5; + } + } + } + 5 => { + if self.flags.hit_bottom_wall() { + self.action_num = 10; + self.anim_counter = 0; + self.anim_num = 1; + } + } + 10 => { + self.animate(2, 2, 2); + + for player in players.iter() { + if player.y > self.y { + self.npc_flags.set_solid_hard(false); + self.damage = 127; + } else { + self.npc_flags.set_solid_hard(true); + self.damage = 0; + } + } + + if self.flags.hit_bottom_wall() { + if self.anim_num > 1 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + for _ in 0..4 { + 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()); + } + + state.quake_counter = 10; + state.sound_manager.play_sfx(26); + } + + self.action_num = 1; + self.anim_num = 0; + self.damage = 0; + self.npc_flags.set_solid_hard(true); + } + } + _ => (), + } + + if self.action_num >= 5 { + self.vel_y += 0x80; + + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + self.y += self.vel_y; + } + + self.anim_rect = state.constants.npc.n246_press_proximity[self.anim_num as usize]; + + Ok(()) + } + pub(crate) fn tick_n253_experience_capsule( &mut self, state: &mut SharedGameState, @@ -1832,12 +1911,368 @@ impl NPC { Ok(()) } + pub(crate) fn tick_n271_ironhead_block(&mut self, state: &mut SharedGameState, stage: &mut Stage) -> GameResult { + if self.vel_x < 0 && self.x < -0x2000 + || self.vel_x > 0 && self.x > stage.map.width as i32 * state.tile_size.as_int() * 0x200 + 0x2000 + { + self.cond.set_alive(false); + return Ok(()); + } + + if self.action_num == 0 { + self.action_num = 1; + if self.rng.range(0..9) == 9 { + self.anim_rect = Rect::new(0, 64, 32, 96); + self.display_bounds = Rect::new(0x2000, 0x2000, 0x2000, 0x2000); + self.hit_bounds = Rect::new(0x1800, 0x1800, 0x1800, 0x1800); + } else { + self.anim_rect = + Rect::new_size(16 * (self.rng.range(0..3) as u16), 16 * (self.rng.range(0..3) as u16 / 3), 16, 16); + } + + self.vel_x = self.direction.vector_x() * 2 * self.rng.range(256..512); + self.vel_y = self.rng.range(-512..512); + } + + if self.vel_y < 0 && self.y - (self.hit_bounds.top as i32) < 0x1000 { + self.vel_y = -self.vel_y; + state.create_caret(self.x, self.y - 0x1000, CaretType::LittleParticles, Direction::Left); + state.create_caret(self.x, self.y - 0x1000, CaretType::LittleParticles, Direction::Left); + } + + if self.vel_y > 0 && (self.hit_bounds.bottom as i32) + self.y > 0x1D000 { + self.vel_y = -self.vel_y; + state.create_caret(self.x, self.y + 0x1000, CaretType::LittleParticles, Direction::Left); + state.create_caret(self.x, self.y + 0x1000, CaretType::LittleParticles, Direction::Left); + } + + self.x += self.vel_x; + self.y += self.vel_y; + + Ok(()) + } + + pub(crate) fn tick_n272_ironhead_block_generator( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = self.rng.range(0..200) as u16; + } + + if self.action_num == 1 { + if self.action_counter > 0 { + self.action_counter -= 1; + } else { + self.action_num = 0; + + let mut npc = NPC::create(271, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y + self.rng.range(-32..32) * 0x200; + npc.direction = self.direction; + + let _ = npc_list.spawn(0x100, npc); + } + } + + Ok(()) + } + + pub(crate) fn tick_n279_large_falling_block( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + stage: &mut Stage, + ) -> GameResult { + match self.action_num { + 0 => { + if self.action_num == 0 { + match self.direction { + Direction::Left => { + self.action_num = 100; + self.npc_flags.set_invulnerable(true); + self.anim_num = 0; + } + Direction::Up => { + self.anim_num = 0; + self.action_num = 10; + } + Direction::Right => { + self.action_num = 100; + self.npc_flags.set_invulnerable(true); + self.anim_num = 1; + self.display_bounds = Rect::new(0x1000, 0x1000, 0x1000, 0x1000); + self.hit_bounds = Rect::new(0x1000, 0x1000, 0x1000, 0x1000); + } + _ => (), + } + } + + if self.direction == Direction::Up { + self.action_num = 11; + self.action_counter = 16; + + if self.action_counter > 0 { + self.action_counter = self.action_counter.saturating_sub(2); + } else { + self.action_num = 100; + self.npc_flags.set_invulnerable(true); + } + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 16; + } + + if self.action_counter > 0 { + self.action_counter = self.action_counter.saturating_sub(2); + } else { + self.action_num = 100; + self.npc_flags.set_invulnerable(true); + } + } + 100 => { + self.vel_y += 0x40; + if self.vel_y > 0x700 { + self.vel_y = 0x700; + } + + if self.y > 0x10000 { + self.npc_flags.set_ignore_solidity(false); + } + + if self.flags.hit_bottom_wall() { + self.vel_y = -0x200; + self.action_num = 110; + self.npc_flags.set_ignore_solidity(true); + state.sound_manager.play_sfx(26); + state.quake_counter = 10; + + 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 + 0x2000; + npc.vel_x = self.rng.range(-0x155..0x155); + npc.vel_y = self.rng.range(-0x600..0); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + } + 110 => { + self.vel_y += 0x40; + if self.y > stage.map.height as i32 * state.tile_size.as_int() * 0x200 + 0x4000 { + self.cond.set_alive(false); + return Ok(()); + } + } + _ => (), + } + + let player = self.get_closest_player_ref(&players); + self.damage = if player.y > self.y { 10 } else { 0 }; + + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n279_large_falling_block[self.anim_num as usize]; + + if self.action_num == 11 { + self.anim_rect.top += self.action_counter; + self.anim_rect.bottom += self.action_counter; + self.display_bounds.top = (16u32).saturating_sub(self.action_counter as u32) * 0x200; + } + + Ok(()) + } + pub(crate) fn tick_n292_quake(&mut self, state: &mut SharedGameState) -> GameResult { state.quake_counter = 10; Ok(()) } + pub(crate) fn tick_n294_quake_falling_block_generator( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + stage: &mut Stage, + ) -> GameResult { + match self.action_num { + 0 => { + if self.x < (stage.map.width as i32 - 6) * state.tile_size.as_int() * 0x200 { + self.action_num = 1; + self.action_counter = 0; + } + } + 1 => { + self.action_counter += 1; + let player = &players[0]; + if player.equip.has_booster_2_0() { + self.x = player.x + 0x8000; + if self.x < 0x34000 { + self.x = 0x34000; + } + } else { + self.x = player.x + 0xC000; + if self.x < 0x2E000 { + self.x = 0x2E000; + } + } + + let map_start = (stage.map.width as i32 - 10) * state.tile_size.as_int() * 0x200; + if self.x > map_start { + self.x = map_start; + } + + if self.action_counter > 24 { + let mut npc = NPC::create(279, &state.npc_table); + + npc.cond.set_alive(true); + npc.x = self.x + + if player.equip.has_booster_2_0() { + self.rng.range(-14..14) + } else { + self.rng.range(-11..11) + } * state.tile_size.as_int() + * 0x200; + npc.y = player.y - 0x1C000; + npc.direction = if self.rng.range(0..10) & 1 != 0 { Direction::Left } else { Direction::Right }; + + let _ = npc_list.spawn(0x100, npc); + + self.action_counter = self.rng.range(0..15) as u16; + } + } + _ => (), + } + + Ok(()) + } + + pub(crate) fn tick_n295_cloud(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = self.tsc_direction % 4; + + match self.tsc_direction { + 0 => { + self.vel_y = -0x1000; + self.display_bounds.right = 0xD000; + self.display_bounds.left = 0xD000; + } + 1 => { + self.vel_y = -0x800; + self.display_bounds.right = 0x7000; + self.display_bounds.left = 0x7000; + } + 2 => { + self.vel_y = -0x400; + self.display_bounds.right = 0x4000; + self.display_bounds.left = 0x4000; + } + 3 => { + self.vel_y = -0x200; + self.display_bounds.right = 0x2800; + self.display_bounds.left = 0x2800; + } + 4 => { + self.vel_x = -0x400; + self.display_bounds.right = 0xD000; + self.display_bounds.left = 0xD000; + } + 5 => { + self.vel_x = -0x200; + self.display_bounds.right = 0x7000; + self.display_bounds.left = 0x7000; + } + 6 => { + self.vel_x = -0x100; + self.display_bounds.right = 0x4000; + self.display_bounds.left = 0x4000; + } + 7 => { + self.vel_x = -0x80; + self.display_bounds.right = 0x2800; + self.display_bounds.left = 0x2800; + } + _ => (), + } + } + + if self.action_num == 1 { + self.x += self.vel_x; + self.y += self.vel_y; + + if self.x < -0x8000 { + self.cond.set_alive(false); + } + + if self.y < -0x4000 { + self.cond.set_alive(false); + } + } + + self.anim_rect = state.constants.npc.n295_cloud[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n296_cloud_generator(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + self.action_counter += 1; + if self.action_counter <= 16 { + return Ok(()); + } + + self.action_counter = self.rng.range(0..16) as u16; + let dir = self.rng.range(0..100) as u16 & 3; + let mut npc = NPC::create(295, &state.npc_table); + npc.cond.set_alive(true); + + if self.direction != Direction::Left { + let layer = match dir { + 0 => 128, + 1 => 85, + 2 => 64, + 3 => 0, + _ => unsafe { + unreachable_unchecked(); + }, + }; + + npc.x = self.x; + npc.y = self.y + self.rng.range(-7..7) * state.tile_size.as_int() * 0x200; + npc.tsc_direction = dir + 4; + + let _ = npc_list.spawn(layer, npc); + } else { + let layer = match dir { + 0 => 384, + 1 => 128, + 2 => 64, + 3 => 0, + _ => unsafe { unreachable_unchecked() }, + }; + + npc.x = self.x + self.rng.range(-10..10) * state.tile_size.as_int() * 0x200; + npc.y = self.y; + npc.tsc_direction = dir; + + let _ = npc_list.spawn(layer, npc); + } + + Ok(()) + } + pub(crate) fn tick_n297_sue_dragon_mouth(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { if let Some(npc) = self.get_parent_ref_mut(npc_list) { self.x = npc.x + 0x2000; @@ -2102,7 +2537,7 @@ impl NPC { npc_list: &NPCList, ) -> GameResult { let player = self.get_closest_player_mut(players); - if abs(player.x - self.x) < 320 * 0x200 + if (player.x - self.x).abs() < 320 * 0x200 && player.y < self.y + 320 * 0x200 && player.y > self.y - 160 * 0x200 && self.rng.range(0..100) == 2 @@ -2117,4 +2552,16 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n360_credits_thank_you(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + self.x -= 0x1000; + self.y -= 0x1000; + } + + self.anim_rect = state.constants.npc.n360_credits_thank_you; + + Ok(()) + } } diff --git a/src/npc/ai/misery.rs b/src/npc/ai/misery.rs index 8b36ada..e2795a3 100644 --- a/src/npc/ai/misery.rs +++ b/src/npc/ai/misery.rs @@ -6,11 +6,14 @@ use crate::caret::CaretType; use crate::common::{Direction, CDEG_RAD}; use crate::components::flash::Flash; use crate::framework::error::GameResult; +use crate::npc::boss::BossNPC; use crate::npc::list::NPCList; use crate::npc::NPC; +use crate::physics::PhysicalEntity; use crate::player::Player; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; +use crate::stage::Stage; impl NPC { pub(crate) fn tick_n066_misery_bubble(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { @@ -573,7 +576,7 @@ impl NPC { npc.y = self.target_y; let _ = npc_list.spawn(0x100, npc.clone()); - npc.x = self.x - 0x2000; + npc.x = self.target_x - 0x2000; npc.direction = Direction::Right; let _ = npc_list.spawn(0x100, npc); } @@ -716,7 +719,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n249_misery_boss_energy_shot(&mut self, state: &mut SharedGameState) -> GameResult { + pub(crate) fn tick_n249_misery_boss_appearing(&mut self, state: &mut SharedGameState) -> GameResult { self.action_counter2 += 1; if self.action_counter2 > 8 { self.cond.set_alive(false); @@ -724,12 +727,616 @@ impl NPC { if self.direction == Direction::Left { self.x -= 0x400; - self.anim_rect = state.constants.npc.n249_misery_boss_energy_shot[0]; + self.anim_rect = state.constants.npc.n249_misery_boss_appearing[0]; } else { self.x += 0x400; - self.anim_rect = state.constants.npc.n249_misery_boss_energy_shot[1]; + self.anim_rect = state.constants.npc.n249_misery_boss_appearing[1]; } Ok(()) } + + pub(crate) fn tick_n250_misery_boss_lighting_ball( + &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.action_num = 1; + self.target_y = self.y; + self.vel_x = 0; + self.vel_y = -0x200; + } + + let player = self.get_closest_player_ref(&players); + + self.vel_x += if self.x < player.x { 0x10 } else { -0x10 }; + self.vel_y += if self.y < self.target_y { 0x20 } else { -0x20 }; + + 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; + + self.animate(2, 0, 1); + + if player.x > self.x - 0x1000 && player.x < self.x + 0x1000 && player.y > self.y { + self.action_num = 10; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 0; + } + + self.action_counter += 1; + if self.action_counter > 10 { + let mut npc = NPC::create(251, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(101); + self.cond.set_alive(false); + + return Ok(()); + } + + self.anim_num = if (self.action_counter & 2) != 0 { 2 } else { 1 }; + } + _ => (), + } + + self.anim_rect = state.constants.npc.n250_misery_boss_lighting_ball[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n251_misery_boss_lighting( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + } + + if self.action_num == 1 { + self.anim_num = (self.anim_num + 1) & 1; + self.y += 0x1000; + + if self.flags.any_flag() { + npc_list.create_death_smoke(self.x, self.y, self.display_bounds.right as usize, 3, state, &self.rng); + self.cond.set_alive(false); + } + } + + self.anim_rect = state.constants.npc.n251_misery_boss_lighting[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n252_misery_boss_bats( + &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.action_num = 1; + self.action_counter = 0; + self.action_counter2 = self.tsc_direction; + } + + self.action_counter2 += 2; + self.action_counter2 &= 0xff; + + if self.action_counter < 192 { + self.action_counter += 1; + } + + if let Some(parent) = self.get_parent_ref_mut(npc_list) { + self.x = parent.x + + self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).cos() * -512.0) as i32 + / 4; + self.y = parent.y + + self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).sin() * -512.0) as i32 + / 4; + + if parent.action_num == 151 { + self.action_num = 10; + self.anim_num = 0; + } + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.npc_flags.set_shootable(true); + self.npc_flags.set_invulnerable(false); + self.npc_flags.set_ignore_solidity(false); + + let player = self.get_closest_player_ref(&players); + let deg = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64); + let deg = deg + self.rng.range(-3..3) as f64 * CDEG_RAD; + + self.vel_x = (deg.cos() * -512.0) as i32; + self.vel_y = (deg.sin() * -512.0) as i32; + + self.anim_num = 1; + self.anim_counter = 0; + + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.flags.hit_anything() { + let mut npc = NPC::create(4, &state.npc_table); + + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc.clone()); + let _ = npc_list.spawn(0x100, npc.clone()); + let _ = npc_list.spawn(0x100, npc); + + self.cond.set_alive(false); + } + + self.animate(4, 1, 3); + } + _ => (), + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n252_misery_boss_bats[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n283_misery_possessed( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + stage: &mut Stage, + boss: &mut BossNPC, + ) -> GameResult { + if self.action_num < 100 && (!boss.parts[0].cond.alive() || self.life < 400) { + self.action_num = 100; + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 9 { + self.action_num = 1; + self.y -= 0x1000; + state.sound_manager.play_sfx(29); + } + + self.action_counter += 1; + self.anim_num = if self.action_counter & 2 != 0 { 9 } else { 0 }; + } + 10 => { + self.action_num = 11; + self.anim_num = 9; + } + 20 | 21 => { + if self.action_num == 21 { + self.action_num = 21; + self.action_counter = 0; + self.anim_num = 0; + self.anim_counter = 0; + + state.npc_super_pos.0 = 0; + } + + self.vel_x = 7 * self.vel_x / 8; + self.vel_y = 7 * self.vel_y / 8; + + self.animate(20, 0, 1); + + self.action_counter += 1; + if self.action_counter > 100 { + self.action_num = 30; + } + + let player = self.get_closest_player_ref(&players); + + self.direction = if player.x > self.x { Direction::Left } else { Direction::Right }; + } + 30 | 31 => { + if self.action_num == 31 { + self.action_num = 31; + self.action_counter = 0; + self.anim_num = 2; + self.action_counter3 = self.life; + } + + self.animate(1, 2, 3); + + if self.flags.hit_bottom_wall() { + self.vel_y = -0x200; + } + + let player = self.get_closest_player_ref(&players); + + self.vel_x += if self.x > boss.parts[0].x { -0x20 } else { 0x20 }; + self.vel_y += if self.y > player.y { -0x10 } else { 0x10 }; + + self.vel_x = self.vel_x.clamp(-0x200, 0x200); + self.vel_y = self.vel_y.clamp(-0x200, 0x200); + + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + self.action_counter += 1; + if self.action_counter > 150 && (self.life + 20 < self.action_counter3 || state.npc_super_pos.0 != 0) { + state.npc_super_pos.0 = 0; + self.action_num = 40; + } + + if boss.parts[0].anim_num != 0 && self.action_counter > 250 { + self.action_num = 50; + } + } + 40 | 41 => { + if self.action_num == 40 { + self.action_num = 41; + self.action_counter = 0; + self.vel_x = 0; + self.vel_y = 0; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + self.action_counter3 = if player.y >= 0x14000 { 289 } else { 290 }; + } + + self.action_counter += 1; + self.anim_num = if self.action_counter & 2 != 0 { 4 } else { 5 }; + + if self.action_counter % 6 == 1 { + state.sound_manager.play_sfx(39); + + let mut npc = NPC::create(self.action_counter3, &state.npc_table); + npc.cond.set_alive(true); + + if self.action_counter3 == 289 { + npc.x = self.x + self.rng.range(-64..64) * 0x200; + npc.y = self.y + self.rng.range(-32..32) * 0x200; + } else { + npc.x = self.x + self.rng.range(-32..32) * 0x200; + npc.y = self.y + self.rng.range(-64..64) * 0x200; + } + + npc.x = npc.x.clamp( + 2 * state.tile_size.as_int() * 0x200, + (stage.map.width as i32 - 2) * state.tile_size.as_int() * 0x200, + ); + npc.y = npc.y.clamp( + 2 * state.tile_size.as_int() * 0x200, + (stage.map.height as i32 - 2) * state.tile_size.as_int() * 0x200, + ); + + let _ = npc_list.spawn(0x100, npc); + } + + if self.action_counter > 50 { + self.action_num = 42; + self.action_counter = 0; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 42 => { + self.action_counter += 1; + self.anim_num = 6; + if self.action_counter > 50 { + self.action_num = 30; + self.vel_y = -0x200; + self.vel_x = self.direction.vector_x() * 0x200; + } + } + 50 | 51 => { + let player = self.get_closest_player_ref(&players); + + if self.action_num == 50 { + self.action_num = 51; + self.action_counter = 0; + self.vel_x = 0; + self.vel_y = 0; + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + state.sound_manager.play_sfx(103); + } + + self.action_counter += 1; + self.anim_num = if self.action_counter & 2 != 0 { 4 } else { 5 }; + + let period = if player.equip.has_booster_2_0() { 10 } else { 24 }; + if self.action_counter % period == 1 { + state.sound_manager.play_sfx(39); + + let mut npc = NPC::create(301, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x + 0x1400 * self.direction.vector_x(); + npc.y = self.y; + npc.tsc_direction = match ((self.action_counter / 6) & 3, self.direction) { + (0, Direction::Left) => 0x58, + (1, Direction::Left) => 0x6C, + (2, Direction::Left) => 0x94, + (3, Direction::Left) => 0xA8, + (0, _) => 0xD8, + (1, _) => 0xEC, + (2, _) => 0x14, + (3, _) => 0x28, + _ => unsafe { + unreachable_unchecked(); + }, + }; + + let _ = npc_list.spawn(0x100, npc); + } + + if self.action_counter > 50 { + self.action_num = 42; + self.action_counter = 0; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + } + 99 => { + self.vel_x = 0; + self.vel_y = 0; + self.anim_num = 9; + self.npc_flags.set_shootable(false); + } + 100 | 101 => { + if self.action_num == 100 { + self.action_num = 101; + self.anim_num = 9; + self.damage = 0; + self.npc_flags.set_shootable(false); + self.npc_flags.set_ignore_solidity(true); + self.vel_y = -0x200; + self.shock += 50; + self.hit_bounds.bottom = 0x1800; + boss.parts[0].anim_num += 1; + } + + self.vel_y += 0x20; + if self.y > 0x1B000 - self.hit_bounds.bottom as i32 { + self.y = 0x1B000 - self.hit_bounds.bottom as i32; + self.action_num = 102; + self.anim_num = 10; + self.vel_x = 0; + self.vel_y = 0; + } + } + _ => (), + } + + self.x += if self.shock > 0 { self.vel_x / 2 } else { self.vel_x }; + self.y += self.vel_y; + + let dir_offset = if self.direction == Direction::Left { 0 } else { 11 }; + + self.anim_rect = state.constants.npc.n283_misery_possessed[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n289_critter_orange( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + stage: &mut Stage, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 2; + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x > self.x { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + if self.action_counter > 16 { + self.action_num = 10; + self.display_bounds.top = 0x1000; + self.display_bounds.bottom = 0x1000; + self.damage = 2; + self.npc_flags.set_shootable(true); + } + } + 10 => { + if self.flags.hit_bottom_wall() { + self.action_num = 11; + self.anim_num = 0; + self.action_counter = 0; + self.vel_x = 0; + + let player = self.get_closest_player_ref(&players); + self.direction = if player.x > self.x { Direction::Left } else { Direction::Right }; + } + } + 11 => { + self.action_counter += 1; + if self.action_counter > 10 { + self.anim_num = 2; + self.action_counter2 += 1; + self.action_num = if self.action_counter2 > 4 { 12 } else { 10 }; + + state.sound_manager.play_sfx(30); + self.vel_x = self.direction.vector_x() * 0x200; + self.vel_y = -0x600; + } + } + 12 => { + self.npc_flags.set_ignore_solidity(true); + + if self.y > stage.map.height as i32 * state.tile_size.as_int() * 0x200 { + self.vanish(state); + } + } + _ => (), + } + + if self.action_num >= 10 { + 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 { 3 }; + + self.anim_rect = state.constants.npc.n289_critter_orange[self.anim_num as usize + dir_offset]; + + if self.action_num == 1 { + self.anim_rect.top += 8 - self.action_counter / 2; + self.anim_rect.bottom -= 8 + self.action_counter / 2; + self.display_bounds.top = (self.action_counter as u32 * 0x200) / 2; + self.display_bounds.bottom = (self.action_counter as u32 * 0x200) / 2; + } + + Ok(()) + } + + pub(crate) fn tick_n290_bat_misery( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + stage: &mut Stage, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.anim_num = 2; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + if self.action_counter > 16 { + self.action_num = 10; + self.display_bounds.top = 0x1000; + self.display_bounds.bottom = 0x1000; + self.damage = 2; + self.npc_flags.set_shootable(true); + self.target_y = self.y; + self.vel_y = 0x400; + } + } + 10 => { + self.animate(2, 0, 2); + + self.vel_y += if self.y >= self.target_y { -0x40 } else { 0x40 }; + self.vel_x += if self.direction == Direction::Left { -0x10 } else { 0x10 }; + + if self.x < 0 + || self.y < 0 + || self.x > (stage.map.width as i32 * state.tile_size.as_int()) + || self.y > (stage.map.height as i32 * state.tile_size.as_int()) + { + self.vanish(state); + return Ok(()); + } + } + _ => (), + } + + 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.n290_bat_misery[self.anim_num as usize + dir_offset]; + + if self.action_num == 1 { + self.anim_rect.top += 8 - self.action_counter / 2; + self.anim_rect.bottom -= 8 + self.action_counter / 2; + self.display_bounds.top = (self.action_counter as u32 * 0x200) / 2; + self.display_bounds.bottom = (self.action_counter as u32 * 0x200) / 2; + } + + Ok(()) + } + + pub(crate) fn tick_n301_misery_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 = self.tsc_direction; + } + + let radians = self.action_counter2 as f64 * CDEG_RAD; + self.vel_x = 2 * (radians.cos() * 512.0) as i32; + self.vel_y = 2 * (radians.sin() * 512.0) as i32; + 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.n301_misery_fish_missile[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/mod.rs b/src/npc/ai/mod.rs index 1ec1799..a32d1cf 100644 --- a/src/npc/ai/mod.rs +++ b/src/npc/ai/mod.rs @@ -1,25 +1,27 @@ -pub(in super) mod balrog; -pub(in super) mod booster; -pub(in super) mod chaco; -pub(in super) mod characters; -pub(in super) mod curly; -pub(in super) mod doctor; -pub(in super) mod egg_corridor; -pub(in super) mod first_cave; -pub(in super) mod grasstown; -pub(in super) mod igor; -pub(in super) mod intro; -pub(in super) mod last_cave; -pub(in super) mod maze; -pub(in super) mod mimiga_village; -pub(in super) mod misc; -pub(in super) mod misery; -pub(in super) mod outer_wall; -pub(in super) mod pickups; -pub(in super) mod plantation; -pub(in super) mod quote; -pub(in super) mod sand_zone; -pub(in super) mod santa; -pub(in super) mod sue; -pub(in super) mod toroko; -pub(in super) mod weapon_trail; +pub(super) mod balcony; +pub(super) mod balrog; +pub(super) mod booster; +pub(super) mod chaco; +pub(super) mod characters; +pub(super) mod curly; +pub(super) mod doctor; +pub(super) mod egg_corridor; +pub(super) mod first_cave; +pub(super) mod grasstown; +pub(super) mod hell; +pub(super) mod igor; +pub(super) mod intro; +pub(super) mod last_cave; +pub(super) mod maze; +pub(super) mod mimiga_village; +pub(super) mod misc; +pub(super) mod misery; +pub(super) mod outer_wall; +pub(super) mod pickups; +pub(super) mod plantation; +pub(super) mod quote; +pub(super) mod sand_zone; +pub(super) mod santa; +pub(super) mod sue; +pub(super) mod toroko; +pub(super) mod weapon_trail; diff --git a/src/npc/ai/outer_wall.rs b/src/npc/ai/outer_wall.rs index 85282c6..0dc9611 100644 --- a/src/npc/ai/outer_wall.rs +++ b/src/npc/ai/outer_wall.rs @@ -301,4 +301,77 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n347_hoppy(&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; + let player = self.get_closest_player_ref(&players); + if player.y < self.y + 0x10000 && player.y > self.y - 0x10000 { + self.action_num = 10; + self.action_counter = 0; + self.anim_num = 1; + } + } + 10 => { + self.action_counter += 1; + if self.action_counter == 4 { + self.anim_num = 2; + } + + if self.action_counter > 12 { + self.action_num = 12; + self.anim_num = 3; + self.vel_x = 0x700; + state.sound_manager.play_sfx(6); + } + } + 12 => { + let player = self.get_closest_player_ref(&players); + if player.y < self.y { + self.vel_y = -0xAA; + } else { + self.vel_y = 0xAA; + } + + if self.flags.hit_left_wall() { + self.action_num = 13; + self.action_counter = 0; + self.anim_num = 2; + self.vel_x = 0; + self.vel_y = 0; + } else { + self.vel_x -= 0x2A; + if self.vel_x < -0x5FF { + self.vel_x = -0x5FF; + } + self.x += self.vel_x; + self.y += self.vel_y; + } + } + 13 => { + self.action_counter += 1; + if self.action_counter == 2 { + self.anim_num = 1; + } + + if self.action_counter == 6 { + self.anim_num = 0; + } + + if self.action_counter > 16 { + self.action_num = 1; + } + } + _ => (), + } + + self.anim_rect = state.constants.npc.n347_hoppy[self.anim_num as usize]; + + Ok(()) + } } diff --git a/src/npc/ai/plantation.rs b/src/npc/ai/plantation.rs index a41d06c..7441ad5 100644 --- a/src/npc/ai/plantation.rs +++ b/src/npc/ai/plantation.rs @@ -1,5 +1,5 @@ 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; @@ -481,4 +481,733 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n232_orangebell(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.target_x = self.x; + self.target_y = self.y; + self.vel_y = 0x200; + + for _ in 0..8 { + let mut npc = NPC::create(233, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y; + npc.direction = self.direction; + npc.parent_id = self.id; + + let _ = npc_list.spawn(0x100, npc); + } + } + + if self.vel_x < 0 && self.flags.hit_left_wall() { + self.direction = Direction::Right; + } + + if self.vel_x > 0 && self.flags.hit_right_wall() { + self.direction = Direction::Left; + } + + self.vel_x = self.direction.vector_x() * 0x100; + + self.vel_y += if self.y < self.target_y { 8 } else { -8 }; + + self.vel_y = self.vel_y.clamp(-0x200, 0x200); + + self.animate(5, 0, 2); + } + _ => (), + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n232_orangebell[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n233_orangebell_bat( + &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.action_num = 1; + self.vel_x = ((self.rng.range(0..255) as f64 * CDEG_RAD).cos() * -512.0) as i32; + self.vel_y = ((self.rng.range(0..255) as f64 * CDEG_RAD).sin() * -512.0) as i32; + + self.action_counter2 = 120; + self.vel_y2 = self.rng.range(-32..32) * 0x200; + } + + if let Some(parent) = self.get_parent_ref_mut(npc_list) { + if parent.npc_type == 232 { + self.target_x = parent.x; + self.target_y = parent.y; + self.direction = parent.direction; + } + } + + self.vel_x += (self.target_x - self.x).signum() * 8; + self.vel_y += ((self.target_y + self.vel_y2) - self.y).signum() * 0x20; + + self.vel_x = self.vel_x.clamp(-0x400, 0x400); + self.vel_y = self.vel_y.clamp(-0x400, 0x400); + + if self.action_counter2 >= 120 { + let player = self.get_closest_player_ref(&players); + + if self.x - 0x1000 < player.x + && self.x + 0x1000 > player.x + && self.y < player.y + && self.y + 0x16000 > player.y + { + self.vel_x /= 4; + self.vel_y = 0; + self.action_num = 3; + self.npc_flags.set_ignore_solidity(false); + } + } else { + self.action_counter2 += 1; + } + } + 3 => { + self.vel_y += 0x40; + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + if self.flags.hit_bottom_wall() { + self.vel_y = 0; + self.vel_x *= 2; + self.action_counter2 = 0; + self.action_num = 1; + self.npc_flags.set_ignore_solidity(true); + } + } + _ => (), + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.action_num == 3 { + self.anim_num = 3; + } else { + self.animate(1, 0, 2); + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n233_orangebell_bat[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n235_midorin(&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.vel_x = 0; + } + + if self.rng.range(0..30) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + if self.rng.range(0..30) == 1 { + self.action_num = 10; + 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; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = self.rng.range(0..16) as u16; + self.anim_num = 2; + self.anim_counter = 0; + + self.direction = if self.rng.range(0..9) & 1 != 0 { Direction::Left } else { Direction::Right }; + } + + 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.vel_x = self.direction.vector_x() * 0x400; + + self.animate(1, 2, 3); + + self.action_counter += 1; + if self.action_counter > 64 { + self.action_num = 0; + } + } + _ => (), + } + + self.vel_y += 0x20; + if self.vel_y > 0x5ff { + self.vel_y = 0x5ff; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + if self.anim_num == 2 { + self.hit_bounds.top = 0xa00; + } else { + self.hit_bounds.top = 0x800; + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; + + self.anim_rect = state.constants.npc.n235_midorin[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n236_gunfish( + &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.action_num = 1; + self.action_counter = self.rng.range(0..50) as u16; + self.target_x = self.x; + self.target_y = self.y; + self.vel_y = 0; + } + + if self.action_counter > 0 { + self.action_counter -= 1; + } else { + self.action_num = 2; + self.vel_y = 0x200; + } + } + 2 => { + let player = self.get_closest_player_ref(&players); + if self.x >= player.x { + self.direction = Direction::Left; + } else { + self.direction = Direction::Right; + } + + if player.x - 0x10000 < self.x + && player.x + 0x10000 > self.x + && player.y - 0x4000 < self.y + && player.y + 0x14000 > self.y + { + self.action_counter += 1; + } + + if self.action_counter > 80 { + self.action_num = 10; + self.action_counter = 0; + } + + self.animate(1, 0, 1); + } + 10 => { + self.action_counter += 1; + if self.action_counter > 20 { + self.action_num = 20; + self.action_counter = 0; + } + + self.animate(1, 2, 3); + } + 20 => { + self.action_counter += 1; + if self.action_counter > 60 { + self.action_num = 2; + self.action_counter = 0; + } + + if self.action_counter % 10 == 3 { + state.sound_manager.play_sfx(39); + + let mut npc = NPC::create(237, &state.npc_table); + npc.cond.set_alive(true); + + if self.direction == Direction::Left { + npc.x = self.x - 0x1000; + npc.y = self.y - 0x1000; + npc.vel_x = -0x400; + npc.vel_y = -0x400; + } else { + npc.x = self.x + 0x1000; + npc.y = self.y - 0x1000; + npc.vel_x = 0x400; + npc.vel_y = -0x400; + } + + let _ = npc_list.spawn(0x100, npc); + } + + self.animate(1, 4, 5); + } + _ => (), + } + + if self.y < self.target_y { + self.vel_y += 0x10; + } else { + self.vel_y -= 0x10; + } + + self.vel_y = self.vel_y.clamp(-0x100, 0x100); + + let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; + + self.anim_rect = state.constants.npc.n236_gunfish[dir_offset + self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n237_gunfish_projectile(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + } + + if self.action_num == 1 { + self.action_counter += 1; + + let hit = self.flags.hit_anything() || (self.action_counter > 10 && self.flags.in_water()); + + if hit { + for _ in 0..5 { + state.create_caret(self.x, self.y, CaretType::Bubble, Direction::Left); + } + + state.sound_manager.play_sfx(21); + self.cond.set_alive(false); + return Ok(()); + } + } + + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n237_gunfish_projectile; + + Ok(()) + } + + pub(crate) fn tick_n240_mimiga_jailed(&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.vel_x = 0; + } + + if self.rng.range(0..60) == 1 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + + if self.rng.range(0..60) == 1 { + self.action_num = 10; + 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; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = self.rng.range(0..16) as u16; + self.anim_num = 2; + self.anim_counter = 0; + + self.direction = if self.rng.range(0..9) & 1 != 0 { Direction::Left } else { Direction::Right }; + } + + 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.vel_x = self.direction.vector_x() * 0x200; + + self.animate(4, 2, 5); + + self.action_counter += 1; + if self.action_counter > 32 { + self.action_num = 0; + } + } + _ => (), + } + + 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.n240_mimiga_jailed[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n273_droll_projectile(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + } + + if self.action_num == 1 { + self.x += self.vel_x; + self.y += self.vel_y; + + if self.flags.any_flag() { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + for _ in 0..3 { + let _ = npc_list.spawn(0x100, npc.clone()); + } + + self.vanish(state); + return Ok(()); + } + } + + self.action_counter += 1; + if self.action_counter % 5 != 0 { + state.sound_manager.play_sfx(110); + } + + self.anim_num += 1; + if self.anim_num > 2 { + self.anim_num = 0; + } + + self.anim_rect = state.constants.npc.n273_droll_projectile[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n274_droll( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { + match self.action_num { + 0 | 1 | 2 => { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x1000; + self.target_x = self.x; + } + + if self.action_num == 1 { + self.vel_x = 0; + self.action_num = 2; + self.anim_num = 0; + } + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + + self.animate(40, 0, 1); + + if self.shock > 0 { + self.action_num = 10; + } + } + 10 | 11 => { + if self.action_num == 10 { + self.action_num = 11; + self.anim_num = 2; + self.action_counter = 0; + } + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 12; + self.anim_num = 3; + self.action_counter2 = 0; + + self.vel_y = -0x600; + self.vel_x = self.direction.vector_x() * 0x200; + } + } + 12 => { + if self.vel_y > 0 { + self.anim_num = 4; + if self.action_counter2 == 0 { + self.action_counter2 += 1; + + let player = self.get_closest_player_ref(&players); + + let mut npc = NPC::create(273, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + let angle = f64::atan2((player.y - 0x1400 - self.y) as f64, (player.x - self.x) as f64); + npc.vel_x = (2048.0 * angle.cos()) as i32; + npc.vel_y = (2048.0 * angle.sin()) as i32; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(39); + } + } + + if self.vel_y > 0x200 { + self.anim_num = 5; + } + + if self.flags.hit_bottom_wall() { + self.action_num = 13; + self.anim_num = 2; + self.action_counter = 0; + self.vel_x = 0; + } + } + 13 => { + self.vel_x /= 2; + + self.action_counter += 1; + if self.action_counter > 10 { + self.action_num = 1; + } + } + _ => (), + } + + self.vel_y += 0x55; + 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.n274_droll[self.anim_num as usize + dir_offset]; + + Ok(()) + } + + pub(crate) fn tick_n275_puppy_plantation( + &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.action_counter = 0; + } + + 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_ref(&players); + + if self.x - 0x8000 < player.x + && self.x + 0x8000 > player.x + && self.y - 0x4000 < player.y + && self.y + 0x2000 > player.y + { + self.animate(3, 2, 3); + } + } + 2 => { + 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; + + self.anim_rect = state.constants.npc.n275_puppy_plantation[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n308_stumpy(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + match self.action_num { + 0 | 1 => { + self.action_num = 1; + + let player = self.get_closest_player_ref(&players); + + if player.x < self.x + 0x1e000 + && player.x > self.x - 0x1e000 + && player.y < self.y + 0x18000 + && player.y > self.y - 0x18000 + { + self.action_num = 10; + } + } + 10 | 11 => { + let player = self.get_closest_player_ref(&players); + + if self.action_num == 10 { + self.action_num = 11; + self.action_counter = 0; + self.vel_x2 = 0; + self.vel_y2 = 0; + + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 20; + } + + self.anim_counter += 1; + // this is a typo in original exe + if self.action_counter > 1 { + self.anim_counter = 0; + self.anim_num += 1; + + if self.anim_num > 1 { + self.anim_num = 0; + } + } + + if player.x > self.x + 0x28000 + && player.x < self.x - 0x28000 + && player.y > self.y + 0x1e000 + && player.y < self.y - 0x1e000 + { + self.action_num = 0; + } + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.action_counter = 0; + + let player = self.get_closest_player_ref(&players); + let deg = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64); + let deg = deg + self.rng.range(-3..3) as f64 * CDEG_RAD; + + self.vel_x2 = (deg.cos() * -1024.0) as i32; + self.vel_y2 = (deg.sin() * -1024.0) as i32; + + self.direction = if self.vel_x2 < 0 { Direction::Left } else { Direction::Right }; + } + + if self.vel_x2 < 0 && self.flags.hit_left_wall() { + self.direction = Direction::Right; + self.vel_x2 = -self.vel_x2; + } + + if self.vel_x2 > 0 && self.flags.hit_right_wall() { + self.direction = Direction::Left; + self.vel_x2 = -self.vel_x2; + } + + if self.vel_y2 < 0 && self.flags.hit_top_wall() { + self.vel_y2 = -self.vel_y2; + } + + if self.vel_y2 > 0 && self.flags.hit_bottom_wall() { + self.vel_y2 = -self.vel_y2; + } + + if self.flags.in_water() { + self.vel_y2 = -0x200; + } + + self.x += self.vel_x2; + self.y += self.vel_y2; + + self.action_counter += 1; + if self.action_counter > 50 { + self.action_num = 10; + } + + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + } + _ => (), + } + + let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; + + self.anim_rect = state.constants.npc.n308_stumpy[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/ai/santa.rs b/src/npc/ai/santa.rs index ed42ac3..ed6a569 100644 --- a/src/npc/ai/santa.rs +++ b/src/npc/ai/santa.rs @@ -1,7 +1,7 @@ -use crate::framework::error::GameResult; use num_traits::abs; use crate::common::Direction; +use crate::framework::error::GameResult; use crate::npc::NPC; use crate::player::Player; use crate::rng::RNG; @@ -24,13 +24,8 @@ impl NPC { } let player = self.get_closest_player_mut(players); - if abs(self.x - player.x) < 32 * 0x200 - && self.y - 32 * 0x200 < player.y && self.y + 0x2000 > player.y { - self.direction = if self.x > player.x { - Direction::Left - } else { - Direction::Right - }; + if abs(self.x - player.x) < 32 * 0x200 && self.y - 32 * 0x200 < player.y && self.y + 0x2000 > player.y { + self.direction = if self.x > player.x { Direction::Left } else { Direction::Right }; } } 2 => { @@ -71,4 +66,38 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n307_santa_caged(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + self.anim_num = 0; + self.x += 0x200; + self.y -= 0x400; + } + + if self.rng.range(0..160) == 10 { + self.action_num = 2; + self.action_counter = 0; + self.anim_num = 1; + } + } + 2 => { + self.action_counter += 1; + if self.action_counter > 12 { + 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.n307_santa_caged[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/ai/sue.rs b/src/npc/ai/sue.rs index cd64023..5663a72 100644 --- a/src/npc/ai/sue.rs +++ b/src/npc/ai/sue.rs @@ -1,14 +1,20 @@ -use crate::framework::error::GameResult; - use crate::common::Direction; +use crate::framework::error::GameResult; +use crate::npc::boss::BossNPC; 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::stage::Stage; impl NPC { - pub fn tick_n042_sue(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], npc_list: &NPCList) -> GameResult { + pub fn tick_n042_sue( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -111,7 +117,8 @@ impl NPC { self.vel_y = 0; self.action_num = 14; - self.parent_id = npc_list.iter_alive() + self.parent_id = npc_list + .iter_alive() .find_map(|npc| if npc.event_num == 501 { Some(npc.id) } else { None }) .unwrap_or(0); } @@ -143,19 +150,13 @@ impl NPC { let _ = npc_list.spawn(0x80, npc); } - state.npc_super_pos = ( - self.x - 24 * 0x200, - self.y - 0x1000 - ); + state.npc_super_pos = (self.x - 24 * 0x200, self.y - 0x1000); } 17 => { self.vel_x = 0; self.anim_num = 12; - state.npc_super_pos = ( - self.x, - self.y - 0x1000 - ); + state.npc_super_pos = (self.x, self.y - 0x1000); } 20 | 21 => { if self.action_num == 20 { @@ -282,4 +283,312 @@ impl NPC { Ok(()) } + + pub(crate) fn tick_n280_sue_teleported(&mut self, state: &mut SharedGameState) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.action_counter = 0; + self.anim_counter = 0; + self.x += 0xc00; + self.target_x = self.x; + } + + self.action_counter += 1; + if self.action_counter > 64 { + self.action_num = 2; + self.action_counter = 0; + } + } + 2 => { + self.anim_num = 0; + if self.flags.hit_bottom_wall() { + self.action_num = 4; + self.action_counter = 0; + self.anim_num = 1; + state.sound_manager.play_sfx(23); + } + } + _ => (), + } + + 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 { 2 }; + + self.anim_rect = state.constants.npc.n280_sue_teleported[self.anim_num as usize + dir_offset]; + + if self.action_num == 1 { + self.anim_rect.bottom = self.anim_rect.top + self.action_counter / 4; + + self.x = if self.action_counter & 2 != 0 { self.target_x } else { self.target_x + 0x200 }; + } + + Ok(()) + } + + pub(crate) fn tick_n284_sue_possessed( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + stage: &mut Stage, + boss: &mut BossNPC, + ) -> GameResult { + if self.action_num < 100 && (!boss.parts[0].cond.alive() || self.life < 400) { + self.action_num = 100; + } + + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.y -= 0x800; + self.action_counter3 = self.life; + + state.sound_manager.play_sfx(29); + } + + self.action_counter += 1; + if self.action_counter & 2 != 0 { + self.display_bounds.top = 0x2000; + self.display_bounds.right = 0x2000; + self.display_bounds.left = 0x2000; + self.anim_num = 11; + } else { + self.display_bounds.top = 0x600; + self.display_bounds.right = 0x1000; + self.display_bounds.left = 0x1000; + self.anim_num = 12; + } + + if self.action_counter > 50 { + self.action_num = 10; + } + } + 10 => { + self.action_num = 11; + self.anim_num = 11; + self.display_bounds.top = 0x2000; + self.display_bounds.right = 0x2000; + self.display_bounds.left = 0x2000; + + npc_list.kill_npcs_by_type(257, true, state); + } + 20 | 21 => { + if self.action_num == 20 { + self.action_num = 21; + self.action_counter = 0; + self.anim_num = 0; + self.anim_counter = 0; + self.damage = 0; + self.npc_flags.set_shootable(true); + self.npc_flags.set_ignore_solidity(false); + } + + self.vel_x = 7 * self.vel_x / 8; + self.vel_y = 7 * self.vel_y / 8; + + self.animate(20, 0, 1); + + self.action_counter += 1; + if self.action_counter > 80 { + self.action_num = 30; + } + + let player = self.get_closest_player_ref(&players); + + self.direction = if player.x > self.x { Direction::Left } else { Direction::Right }; + + if self.life + 50 < self.action_counter3 { + self.action_counter3 = self.life; + state.npc_super_pos.0 = 10; + } + } + 30 | 31 => { + if self.action_num == 30 { + self.action_num = 31; + self.action_counter = 0; + self.anim_num = 2; + self.vel_x = 0; + self.vel_y = 0; + } + + self.action_counter += 1; + if self.action_counter > 16 { + self.action_counter2 += 1; + self.action_counter2 &= 3; + + self.action_num = match self.action_counter2 { + 0 | 2 => 32, + 1 | 3 => 34, + _ => self.action_num, + }; + } + } + 32 | 33 => { + let player = self.get_closest_player_ref(&players); + + if self.action_num == 32 { + self.action_num = 33; + self.action_counter = 0; + self.npc_flags.set_shootable(false); + self.npc_flags.set_ignore_solidity(false); + self.target_x = if player.x >= self.x { player.x + 0x14000 } else { player.x - 0x14000 }; + self.target_y = player.y; + + let angle = f64::atan2((self.y - self.target_y) as f64, (self.x - self.target_x) as f64); + + self.vel_x = (-1536.0 * angle.cos()) as i32; + self.vel_y = (-1536.0 * angle.sin()) as i32; + + let half_w = stage.map.width as i32 * state.tile_size.as_int() * 0x200 / 2; + let half_h = stage.map.height as i32 * state.tile_size.as_int() * 0x200 / 2; + + if ((self.x < half_w && self.vel_x > 0) || (self.x > half_w && self.vel_x < 0)) + || ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0)) + { + self.npc_flags.set_ignore_solidity(true); + } + + self.direction = if self.vel_x <= 0 { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + self.anim_num = if self.action_counter & 2 != 0 { 3 } else { 8 }; + + if self.action_counter > 50 || (self.flags.hit_right_wall() || self.flags.hit_left_wall()) { + self.action_num = 20; + } + } + 34 | 35 => { + let player = self.get_closest_player_ref(&players); + + if self.action_num == 34 { + self.action_num = 35; + self.action_counter = 0; + self.damage = 4; + self.npc_flags.set_ignore_solidity(false); + self.target_x = player.x; + self.target_y = player.y; + + let angle = f64::atan2((self.y - self.target_y) as f64, (self.x - self.target_x) as f64); + + self.vel_x = (-1536.0 * angle.cos()) as i32; + self.vel_y = (-1536.0 * angle.sin()) as i32; + + let half_w = stage.map.width as i32 * state.tile_size.as_int() * 0x200 / 2; + let half_h = stage.map.height as i32 * state.tile_size.as_int() * 0x200 / 2; + + if ((self.x < half_w && self.vel_x > 0) || (self.x > half_w && self.vel_x < 0)) + || ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0)) + { + self.npc_flags.set_ignore_solidity(true); + } + + self.direction = if self.vel_x <= 0 { Direction::Left } else { Direction::Right }; + } + + self.action_counter += 1; + if self.action_counter > 20 && self.shock != 0 { + self.action_num = 40; + } else if self.action_counter > 50 || (self.flags.hit_right_wall() || self.flags.hit_left_wall()) { + self.action_num = 20; + } + + self.animate(1, 4, 7); + + if self.action_counter % 5 == 1 { + state.sound_manager.play_sfx(109); + } + } + 40 | 41 => { + if self.action_num == 40 { + self.action_num = 41; + self.action_counter = 0; + self.anim_num = 2; + self.damage = 0; + self.npc_flags.set_ignore_solidity(false); + } + + self.vel_x = 7 * self.vel_x / 8; + self.vel_y = 7 * self.vel_y / 8; + + self.action_counter += 1; + if self.action_counter > 6 { + self.action_num = 42; + self.action_counter = 0; + + self.vel_x = self.direction.vector_x() * 0x200; + self.vel_y = -0x200; + } + } + 42 => { + self.anim_num = 9; + if self.flags.hit_bottom_wall() { + self.action_num = 43; + self.action_counter = 0; + self.anim_num = 2; + + let player = self.get_closest_player_ref(&players); + self.direction = if self.x < player.x { Direction::Right } else { Direction::Left }; + } + + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + } + 43 => { + self.action_counter += 1; + if self.action_counter > 16 { + self.action_num = 20; + } + } + 99 => { + self.vel_x = 0; + self.vel_y = 0; + self.anim_num = 9; + self.npc_flags.set_shootable(false); + } + 100 | 101 => { + if self.action_num == 100 { + self.action_num = 101; + self.anim_num = 9; + self.damage = 0; + self.npc_flags.set_shootable(false); + self.npc_flags.set_ignore_solidity(true); + self.shock += 50; + boss.parts[0].anim_num += 1; + } + + self.vel_y += 0x20; + if self.y > 0x1B000 - self.hit_bounds.bottom as i32 { + self.y = 0x1B000 - self.hit_bounds.bottom as i32; + self.action_num = 102; + self.anim_num = 10; + self.vel_x = 0; + self.vel_y = 0; + } + } + _ => (), + } + + self.x += if self.shock > 0 { self.vel_x / 2 } else { 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.n284_sue_possessed[self.anim_num as usize + dir_offset]; + + Ok(()) + } } diff --git a/src/npc/boss/balfrog.rs b/src/npc/boss/balfrog.rs index c55cfa8..cdda25d 100644 --- a/src/npc/boss/balfrog.rs +++ b/src/npc/boss/balfrog.rs @@ -20,14 +20,7 @@ impl NPC { self.y += self.vel_y; self.action_counter += 1; - self.anim_counter += 1; - if self.anim_counter > 1 { - self.anim_counter = 0; - self.anim_num += 1; - if self.anim_num > 2 { - self.anim_num = 0; - } - } + self.animate(1, 0, 2); self.anim_rect = state.constants.npc.n108_balfrog_projectile[self.anim_num as usize]; diff --git a/src/npc/boss/ironhead.rs b/src/npc/boss/ironhead.rs index 579bb2a..c385600 100644 --- a/src/npc/boss/ironhead.rs +++ b/src/npc/boss/ironhead.rs @@ -1,9 +1,11 @@ -use crate::common::Direction; +use crate::common::{Direction, Rect}; use crate::framework::error::GameResult; use crate::npc::boss::BossNPC; +use crate::npc::list::NPCList; use crate::npc::NPC; -use crate::shared_game_state::SharedGameState; +use crate::player::Player; use crate::rng::RNG; +use crate::shared_game_state::SharedGameState; impl NPC { pub(crate) fn tick_n196_ironhead_wall(&mut self, state: &mut SharedGameState) -> GameResult { @@ -96,5 +98,174 @@ impl NPC { } impl BossNPC { - pub(crate) fn tick_b05_ironhead(&mut self) {} + pub(crate) fn tick_b05_ironhead( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + ) { + match self.parts[0].action_num { + 0 => { + self.parts[0].cond.set_alive(true); + self.parts[0].exp = 1; + self.parts[0].direction = Direction::Right; + self.parts[0].action_num = 100; + self.parts[0].x = 0x14000; + self.parts[0].y = 0x10000; + self.hurt_sound[0] = 54; + self.parts[0].npc_flags.set_event_when_killed(true); + self.parts[0].npc_flags.set_shootable(true); + self.parts[0].npc_flags.set_ignore_solidity(true); + self.parts[0].npc_flags.set_show_damage(true); + self.parts[0].size = 3; + self.parts[0].damage = 10; + self.parts[0].event_num = 1000; + self.parts[0].life = 400; + self.parts[0].display_bounds = Rect::new(0x5000, 0x1800, 0x3000, 0x1800); + self.parts[0].hit_bounds = Rect::new(0x2000, 0x1400, 0x2000, 0x1400); + } + 100 | 101 => { + if self.parts[0].action_num == 100 { + self.parts[0].action_num = 101; + self.parts[0].action_counter = 0; + self.parts[0].npc_flags.set_shootable(false); + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 50 { + self.parts[0].action_num = 250; + self.parts[0].action_counter = 0; + } + + if self.parts[0].action_counter % 4 == 0 { + let mut npc = NPC::create(197, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].rng.range(15..18) * state.tile_size.as_int() * 0x200; + npc.y = self.parts[0].rng.range(2..13) * state.tile_size.as_int() * 0x200; + + let _ = npc_list.spawn(0x100, npc); + } + } + 250 | 251 => { + if self.parts[0].action_num == 250 { + self.parts[0].action_num = 251; + if self.parts[0].direction == Direction::Right { + let player = self.parts[0].get_closest_player_ref(&players); + + self.parts[0].x = 0x1E000; + self.parts[0].y = player.y; + } else { + self.parts[0].x = 0x5A000; + self.parts[0].y = self.parts[0].rng.range(2..13) * state.tile_size.as_int() * 0x200; + } + + self.parts[0].target_x = self.parts[0].x; + self.parts[0].target_y = self.parts[0].y; + self.parts[0].vel_y = self.parts[0].rng.range(-0x200..0x200); + self.parts[0].vel_x = self.parts[0].rng.range(-0x200..0x200); + self.parts[0].npc_flags.set_shootable(true); + } + + if self.parts[0].direction == Direction::Right { + self.parts[0].target_x += 0x400; + } else { + self.parts[0].target_x -= 0x200; + if self.parts[0].target_y >= self.parts[0].y { + self.parts[0].target_y -= 0x200; + } else { + self.parts[0].target_y += 0x200; + } + } + + self.parts[0].vel_x += if self.parts[0].x >= self.parts[0].target_x { -8 } else { 8 }; + self.parts[0].vel_y += if self.parts[0].y >= self.parts[0].target_y { -8 } else { 8 }; + self.parts[0].vel_y = self.parts[0].vel_y.clamp(-0x200, 0x200); + self.parts[0].x += self.parts[0].vel_x; + self.parts[0].y += self.parts[0].vel_y; + + if self.parts[0].direction == Direction::Right { + if self.parts[0].x > 0x5A000 { + self.parts[0].direction = Direction::Left; + self.parts[0].action_num = 100; + } + } else if self.parts[0].x < 0x22000 { + self.parts[0].direction = Direction::Right; + self.parts[0].action_num = 100; + } + + self.parts[0].action_counter += 1; + if [300, 310, 320].contains(&self.parts[0].action_counter) { + state.sound_manager.play_sfx(39); + + let mut npc = NPC::create(198, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + 0x1400; + npc.y = self.parts[0].y + 0x200; + npc.vel_x = self.parts[0].rng.range(-3..0) * 0x200; + npc.vel_y = self.parts[0].rng.range(-3..3) * 0x200; + npc.direction = Direction::Right; + + let _ = npc_list.spawn(0x100, npc); + } + + self.parts[0].animate(2, 0, 7); + } + 1000 | 1001 => { + if self.parts[0].action_num == 1000 { + self.parts[0].action_num = 1001; + self.parts[0].anim_num = 8; + self.parts[0].npc_flags.set_shootable(false); + self.parts[0].damage = 0; + self.parts[0].target_x = self.parts[0].x; + self.parts[0].target_y = self.parts[0].y; + state.quake_counter = 20; + for _ in 0..32 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-0x40..0x40) * 0x200; + npc.vel_x = self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.vel_y = self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.direction = Direction::Left; + + let _ = npc_list.spawn(0x100, npc); + } + + npc_list.kill_npcs_by_type(197, true, state); + npc_list.kill_npcs_by_type(271, true, state); + npc_list.kill_npcs_by_type(272, true, state); + } + + self.parts[0].target_x -= 0x200; + self.parts[0].x = self.parts[0].target_x + self.parts[0].rng.range(-1..1) * 0x200; + self.parts[0].y = self.parts[0].target_y + self.parts[0].rng.range(-1..1) * 0x200; + + if self.parts[0].action_counter % 4 == 0 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-0x40..0x40) * 0x200; + npc.vel_x = self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.vel_y = self.parts[0].rng.range(-0x80..0x80) * 0x200; + npc.direction = Direction::Left; + + let _ = npc_list.spawn(0x100, npc); + } + } + _ => (), + } + + let offset = if self.parts[0].shock != 0 { + self.parts[19].action_counter += 1; + if self.parts[19].action_counter & 2 != 0 { + 0 + } else { + 9 + } + } else { + 0 + }; + + self.parts[0].anim_rect = state.constants.npc.b05_ironhead[self.parts[0].anim_num as usize + offset]; + } } diff --git a/src/npc/boss/mod.rs b/src/npc/boss/mod.rs index 3d629f6..c7c1a2c 100644 --- a/src/npc/boss/mod.rs +++ b/src/npc/boss/mod.rs @@ -20,7 +20,7 @@ pub mod ironhead; pub mod monster_x; pub mod omega; pub mod press; -pub mod twins; +pub mod sisters; pub mod undead_core; pub struct BossNPC { @@ -82,9 +82,9 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Fl 2 => self.tick_b02_balfrog(state, players, npc_list), 3 => self.tick_b03_monster_x(state, players, npc_list, flash), 4 => self.tick_b04_core(state, players, npc_list, stage), - 5 => self.tick_b05_ironhead(), - 6 => self.tick_b06_twins(), - 7 => self.tick_b07_undead_core(), + 5 => self.tick_b05_ironhead(state, players, npc_list), + 6 => self.tick_b06_sisters(state, players, npc_list, flash), + 7 => self.tick_b07_undead_core(state, npc_list, stage, flash), 8 => self.tick_b08_press(), 9 => self.tick_b09_ballos(), _ => {} diff --git a/src/npc/boss/sisters.rs b/src/npc/boss/sisters.rs new file mode 100644 index 0000000..257b7ec --- /dev/null +++ b/src/npc/boss/sisters.rs @@ -0,0 +1,464 @@ +use crate::common::{Direction, Rect, SliceExt, CDEG_RAD}; +use crate::components::flash::Flash; +use crate::npc::boss::BossNPC; +use crate::npc::list::NPCList; +use crate::npc::NPC; +use crate::player::Player; +use crate::rng::RNG; +use crate::SharedGameState; + +impl BossNPC { + pub(crate) fn tick_b06_sisters( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + flash: &mut Flash, + ) { + match self.parts[0].action_num { + 0 => { + self.parts[0].cond.set_alive(true); + self.parts[0].direction = Direction::Left; + self.parts[0].action_num = 10; + self.parts[0].exp = 0; + self.parts[0].x = 0x14000; + self.parts[0].y = 0x10000; + self.parts[0].display_bounds = Rect::new(0x1000, 0x1000, 0x10000, 0x1000); + self.parts[0].hit_bounds = Rect::new(0x1000, 0x1000, 0x1000, 0x1000); + self.hurt_sound[0] = 54; + self.parts[0].npc_flags.set_ignore_solidity(true); + self.parts[0].npc_flags.set_event_when_killed(true); + self.parts[0].size = 3; + self.parts[0].damage = 0; + self.parts[0].event_num = 1000; + self.parts[0].life = 500; + self.parts[0].action_counter3 = self.parts[0].rng.range(700..1200) as u16; + self.parts[0].target_x = 0xB4; + self.parts[0].target_y = 0x3D; + + self.parts[2].display_bounds = Rect::new(0x2800, 0x2000, 0x2800, 0x2000); + self.parts[2].hit_bounds = Rect::new(0x1800, 0x1400, 0x1800, 0x1400); + self.parts[2].npc_flags.set_ignore_solidity(true); + self.parts[2].npc_flags.set_invulnerable(true); + self.parts[2].parent_id = 3; + self.parts[2].cond.set_alive(true); + self.parts[2].cond.set_damage_boss(true); + self.parts[2].damage = 10; + + self.parts[3].cond.set_alive(true); + self.parts[3].display_bounds = Rect::new(0x2800, 0x2800, 0x2800, 0x2800); + self.parts[3].hit_bounds = Rect::new(0x1800, 0x400, 0x1800, 0x2000); + self.parts[3].npc_flags.set_ignore_solidity(true); + self.parts[3].parent_id = 0; + self.parts[3].damage = 10; + + self.parts[4] = self.parts[2].clone(); + self.parts[4].id = 4; + self.parts[4].parent_id = 5; + + self.parts[5] = self.parts[3].clone(); + self.parts[5].id = 5; + self.parts[5].action_counter2 = 128; + } + 20 => { + self.parts[0].target_x -= 1; + if self.parts[0].target_x <= 112 { + self.parts[0].action_num = 100; + self.parts[0].action_counter = 0; + self.parts[2].action_num = 100; + self.parts[4].action_num = 100; + self.parts[3].action_num = 100; + self.parts[5].action_num = 100; + } + } + 100 => { + let actr2: &mut i16 = unsafe { std::mem::transmute(&mut self.parts[0].action_counter2) }; + let mut b = true; + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter < 100 { + *actr2 += 1; + } else if self.parts[0].action_counter < 120 { + *actr2 += 2; + } else if self.parts[0].action_counter < self.parts[0].action_counter3 { + *actr2 += 4; + } else if self.parts[0].action_counter < self.parts[0].action_counter3 + 40 { + *actr2 += 2; + } else if self.parts[0].action_counter < self.parts[0].action_counter3 + 60 { + *actr2 += 1; + } else { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 110; + self.parts[0].action_counter3 = self.parts[0].rng.range(400..700) as u16; + b = false; + } + + if b && *actr2 >= 0x400 { + *actr2 -= 0x400; + } + } + 110 => { + let actr2: &mut i16 = unsafe { std::mem::transmute(&mut self.parts[0].action_counter2) }; + let mut b = true; + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter < 20 { + *actr2 -= 1; + } else if self.parts[0].action_counter < 60 { + *actr2 -= 2; + } else if self.parts[0].action_counter < self.parts[0].action_counter2 { + *actr2 -= 4; + } else if self.parts[0].action_counter < self.parts[0].action_counter2 + 40 { + *actr2 -= 2; + } else if self.parts[0].action_counter < self.parts[0].action_counter2 + 60 { + *actr2 -= 1; + } else { + if self.parts[0].life >= 300 { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 100; + *actr2 = self.parts[0].rng.range(400..700) as i16; + } else { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 400; + self.parts[2].action_num = 400; + self.parts[4].action_num = 400; + b = false; + } + } + + if b && *actr2 <= 0 { + *actr2 += 0x400; + } + } + 400 => { + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 100 { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 401; + } + } + 401 => { + let actr2: &mut i16 = unsafe { std::mem::transmute(&mut self.parts[0].action_counter2) }; + let mut b = true; + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter < 100 { + *actr2 += 1; + } else if self.parts[0].action_counter < 120 { + *actr2 += 2; + } else if self.parts[0].action_counter < 500 { + *actr2 += 2; + } else if self.parts[0].action_counter < 540 { + *actr2 += 4; + } else if self.parts[0].action_counter < 560 { + *actr2 += 2; + } else { + self.parts[0].action_num = 100; + self.parts[0].action_counter = 0; + self.parts[2].action_num = 100; + self.parts[4].action_num = 100; + b = false; + } + + if b && *actr2 >= 0x400 { + *actr2 -= 0x400; + } + } + 1000 | 1001 => { + if self.parts[0].action_num == 1000 { + self.parts[0].action_num = 1001; + self.parts[0].action_counter = 0; + self.parts[2].action_num = 1000; + self.parts[3].action_num = 1000; + self.parts[4].action_num = 1000; + self.parts[5].action_num = 1000; + + npc_list.create_death_smoke( + self.parts[0].x, + self.parts[0].y, + self.parts[0].display_bounds.right as usize, + 40, + state, + &self.parts[0].rng, + ); + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 100 { + self.parts[0].action_num = 1010; + } + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + self.parts[0].rng.range(-128..128) * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-70..70) * 0x200; + + let _ = npc_list.spawn(0x100, npc); + } + 1010 => { + let actr2: &mut i16 = unsafe { std::mem::transmute(&mut self.parts[0].action_counter2) }; + *actr2 += 4; + + if *actr2 >= 0x400 { + *actr2 -= 0x400; + } + + if self.parts[0].target_x > 8 { + self.parts[0].target_x -= 1; + } + if self.parts[0].target_y > 0 { + self.parts[0].target_y -= 1; + } + if self.parts[0].target_x < -8 { + self.parts[0].target_x += 1; + } + if self.parts[0].target_y < 0 { + self.parts[0].target_y += 1; + } + + if self.parts[0].target_y == 0 { + self.parts[0].action_num = 1020; + self.parts[0].action_counter = 0; + + flash.set_cross(self.parts[0].x, self.parts[0].y); + state.sound_manager.play_sfx(35); + } + } + 1020 => { + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 50 { + npc_list.kill_npcs_by_type(211, true, state); + + self.parts[0].cond.set_alive(false); + self.parts[1].cond.set_alive(false); + self.parts[2].cond.set_alive(false); + self.parts[3].cond.set_alive(false); + self.parts[4].cond.set_alive(false); + self.parts[5].cond.set_alive(false); + + self.parts[0].action_num = 0; + } + } + _ => (), + } + + self.tick_b06_sisters_dragon_head(2, state, &players, npc_list); + self.tick_b06_sisters_dragon_body(3, state, &players); + self.tick_b06_sisters_dragon_head(4, state, &players, npc_list); + self.tick_b06_sisters_dragon_body(5, state, &players); + + self.parts[0].anim_rect = Rect::new(0, 0, 0, 0); + } + + fn tick_b06_sisters_dragon_head( + &mut self, + i: usize, + state: &mut SharedGameState, + players: &[&mut Player; 2], + npc_list: &NPCList, + ) { + let parent = self.parts[i].parent_id as usize; + let (base, part) = if let Some(x) = self.parts.get_two_mut(parent, i) { + x + } else { + return; + }; + + match part.action_num { + 0 => { + part.action_num = 1; + } + 100 | 200 | 201 => { + if part.action_num == 100 { + part.action_num = 200; + } + + if part.action_num == 200 { + part.action_num = 201; + part.anim_num = 0; + part.hit_bounds.left = 0x2000; + part.npc_flags.set_shootable(false); + part.action_counter2 = part.rng.range(100..200) as u16; + } + + if part.action_counter2 > 0 { + part.action_counter2 -= 1; + } else { + part.action_num = 210; + part.action_counter = 0; + part.action_counter3 = 0; + } + } + 210 => { + part.action_counter += 1; + if part.action_counter == 3 { + part.anim_num = 1; + } + + if part.action_counter == 6 { + part.anim_num = 2; + part.hit_bounds.left = 0x1000; + part.npc_flags.set_shootable(true); + part.action_counter3 = 0; + } + + if part.action_counter > 150 { + part.action_num = 220; + part.action_counter = 0; + } + + if part.shock != 0 { + part.action_counter3 += 1; + } + + if part.action_counter3 > 10 { + part.action_num = 300; + part.action_counter = 0; + part.anim_num = 3; + part.hit_bounds.left = 0x2000; + + state.sound_manager.play_sfx(51); + npc_list.remove_by_type(211, state); + } + } + 220 => { + part.action_counter += 1; + if part.action_counter % 8 == 1 { + let mut npc = NPC::create(202, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = part.x + 0x1000 * part.direction.vector_x(); + npc.y = part.y; + + let player = part.get_closest_player_ref(players); + let angle = f64::atan2((player.y - npc.y) as f64, (player.x - npc.x) as f64) + + (part.rng.range(-6..6) as f64 * CDEG_RAD); + + npc.vel_x = (angle.cos() * 512.0) as i32; + npc.vel_y = (angle.sin() * 512.0) as i32; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(33); + } + + if part.action_counter > 50 { + part.action_num = 200; + } + } + 300 => { + part.action_counter += 1; + if part.action_counter > 100 { + part.action_num = 200; + } + } + 400 | 401 => { + if part.action_num == 400 { + part.action_num = 401; + part.action_counter = 0; + part.anim_num = 0; + part.hit_bounds.left = 0x2000; + part.npc_flags.set_shootable(false); + } + + part.action_counter += 1; + if part.action_counter == 3 { + part.anim_num = 1; + } + + if part.action_counter == 6 { + part.anim_num = 2; + part.hit_bounds.left = 0x1000; + part.npc_flags.set_shootable(true); + part.action_counter3 = 0; + } + + if part.action_counter > 20 && part.action_counter % 32 == 1 { + let mut npc = NPC::create(202, &state.npc_table); + npc.cond.set_alive(true); + + let player = part.get_closest_player_ref(players); + let angle = f64::atan2((player.y - npc.y) as f64, (player.x - npc.x) as f64) + + (part.rng.range(-6..6) as f64 * CDEG_RAD); + + npc.x = part.x + 0x1000 * part.direction.vector_x(); + npc.y = part.y; + + npc.vel_x = (angle.cos() * 512.0) as i32; + npc.vel_y = (angle.sin() * 512.0) as i32; + + let _ = npc_list.spawn(0x100, npc); + + state.sound_manager.play_sfx(33); + } + } + _ => (), + } + + part.direction = base.direction; + part.x = base.x + 0x800 * part.direction.vector_x(); + part.y = base.y - 0x1000; + + let dir_offset = if part.direction == Direction::Left { 0 } else { 4 }; + + part.anim_rect = state.constants.npc.b06_sisters[part.anim_num as usize + dir_offset]; + } + + fn tick_b06_sisters_dragon_body(&mut self, i: usize, state: &mut SharedGameState, players: &[&mut Player; 2]) { + let parent = self.parts[i].parent_id as usize; + let (base, part) = if let Some(x) = self.parts.get_two_mut(parent, i) { + x + } else { + return; + }; + + match part.action_num { + 0 | 10 => { + if part.action_num == 0 { + part.action_num = 10; + let angle = + ((part.action_counter2) as u8).wrapping_add((base.action_counter2 / 4) as u8) as f64 * CDEG_RAD; + part.x += base.x + base.target_x as i32 * (angle.cos() * -512.0) as i32; + part.y += base.y + base.target_y as i32 * (angle.sin() * -512.0) as i32; + } + + let player = part.get_closest_player_ref(players); + part.direction = if part.x > player.x { Direction::Left } else { Direction::Right }; + } + 100 => { + let angle = + ((part.action_counter2) as u8).wrapping_add((base.action_counter2 / 4) as u8) as f64 * CDEG_RAD; + part.target_x = base.x + base.target_x as i32 * (angle.cos() * -512.0) as i32; + part.target_y = base.y + base.target_y as i32 * (angle.sin() * -512.0) as i32; + + part.x += (part.target_x - part.x) / 8; + part.y += (part.target_y - part.y) / 8; + + let player = part.get_closest_player_ref(players); + part.direction = if part.x > player.x { Direction::Left } else { Direction::Right }; + } + 1000 | 1001 => { + if part.action_num == 1000 { + part.action_num = 1001; + part.npc_flags.set_shootable(false); + } + + let angle = + ((part.action_counter2) as u8).wrapping_add((base.action_counter2 / 4) as u8) as f64 * CDEG_RAD; + part.target_x = base.x + base.target_x as i32 * (angle.cos() * -512.0) as i32; + part.target_y = base.y + base.target_y as i32 * (angle.sin() * -512.0) as i32; + + part.x += (part.target_x - part.x) / 8; + part.y += (part.target_y - part.y) / 8; + + part.direction = if part.x > base.x { Direction::Left } else { Direction::Right }; + } + _ => (), + } + + part.animate(2, 0, 2); + + let dir_offset = if part.direction == Direction::Left { 0 } else { 3 }; + part.anim_rect = state.constants.npc.b06_sisters[part.anim_num as usize + dir_offset + 8]; + } +} diff --git a/src/npc/boss/twins.rs b/src/npc/boss/twins.rs deleted file mode 100644 index 5077467..0000000 --- a/src/npc/boss/twins.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::npc::boss::BossNPC; - -impl BossNPC { - pub(crate) fn tick_b06_twins(&mut self) { - - } -} diff --git a/src/npc/boss/undead_core.rs b/src/npc/boss/undead_core.rs index 09604b8..42d8f5b 100644 --- a/src/npc/boss/undead_core.rs +++ b/src/npc/boss/undead_core.rs @@ -1,7 +1,1084 @@ +use std::hint::unreachable_unchecked; + +use crate::common::{Direction, Rect, SliceExt, CDEG_RAD}; +use crate::components::flash::Flash; use crate::npc::boss::BossNPC; +use crate::npc::list::NPCList; +use crate::npc::NPC; +use crate::player::Player; +use crate::rng::RNG; +use crate::stage::Stage; +use crate::{GameResult, SharedGameState}; -impl BossNPC { - pub(crate) fn tick_b07_undead_core(&mut self) { +impl NPC { + pub(crate) fn tick_n282_mini_undead_core_active( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + ) -> GameResult { + if self.action_num == 0 { + self.action_num = 20; + self.target_y = self.y; + self.vel_y = if self.rng.range(0..100) & 1 == 0 { -0x100 } else { 0x100 }; + } + if self.action_num == 20 { + self.vel_x = -0x200; + if self.x < -0x8000 { + self.cond.set_alive(false); + } + + if self.target_y < self.y { + self.vel_y -= 0x10; + } + if self.target_y > self.y { + self.vel_y += 0x10; + } + + self.vel_y = self.vel_y.clamp(-0x100, 0x100); + + let player = self.get_closest_player_ref(&players); + if player.flags.hit_bottom_wall() + && player.y < self.y - 0x800 + && player.x > self.x - 0x3000 + && player.x < self.x + 0x3000 + { + self.target_y = 0x12000; + self.anim_num = 2; + } else if self.anim_num != 1 { + self.anim_num = 0; + } + + if player.flags.hit_left_wall() + && player.x < self.x - self.hit_bounds.right as i32 + && player.x > self.x - self.hit_bounds.right as i32 - 0x1000 + && player.hit_bounds.bottom as i32 + player.y > self.y - self.hit_bounds.top as i32 + && (player.y - player.hit_bounds.top as i32) < self.hit_bounds.bottom as i32 + self.y + { + self.npc_flags.set_solid_hard(false); + self.anim_num = 1; + } else if player.flags.hit_right_wall() + && player.x > self.hit_bounds.right as i32 + self.x + && player.x < self.x + self.hit_bounds.right as i32 + 0x1000 + && player.hit_bounds.bottom as i32 + player.y > self.y - self.hit_bounds.top as i32 + && (player.y - player.hit_bounds.top as i32) < self.hit_bounds.bottom as i32 + self.y + { + self.npc_flags.set_solid_hard(false); + self.anim_num = 1; + } else if player.flags.hit_top_wall() + && player.y < self.y - self.hit_bounds.top as i32 + && player.y > self.y - self.hit_bounds.top as i32 - 0x1000 + && player.hit_bounds.left as i32 + player.x > self.x - self.hit_bounds.right as i32 + && (player.x - player.hit_bounds.right as i32) < self.hit_bounds.left as i32 + self.x + { + self.npc_flags.set_solid_hard(false); + self.anim_num = 1; + } else if player.flags.hit_bottom_wall() + && player.y > self.hit_bounds.bottom as i32 + self.y - 0x800 + && player.y < self.y + self.hit_bounds.bottom as i32 + 0x1800 + && player.hit_bounds.left as i32 + player.x > self.x - self.hit_bounds.right as i32 - 0x800 + && (player.x - player.hit_bounds.right as i32) < self.x + self.hit_bounds.left as i32 + 0x800 + { + self.npc_flags.set_solid_hard(false); + self.anim_num = 1; + } + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n282_mini_undead_core_active[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n291_mini_undead_core_inactive(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.action_num = 20; + if self.direction == Direction::Right { + self.npc_flags.set_solid_hard(false); + self.anim_num = 1; + } + } + + self.anim_rect = state.constants.npc.n291_mini_undead_core_inactive[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n293_undead_core_energy_shot( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + ) -> GameResult { + if self.action_num == 0 { + self.action_num = 1; + } + + if self.action_num == 1 { + self.anim_num += 1; + if self.anim_num > 1 { + self.anim_num = 0; + } + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x + self.rng.range(0..16) * 0x200; + npc.y = self.y + self.rng.range(-16..16) * 0x200; + + let _ = npc_list.spawn(0x100, npc); + + self.x -= 0x1000; + if self.x < -0x4000 { + self.cond.set_alive(false); + } + } + + self.anim_rect = state.constants.npc.n293_undead_core_energy_shot[self.anim_num as usize]; + + Ok(()) + } + + pub(crate) fn tick_n285_undead_core_spiral_projectile( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + stage: &mut Stage, + ) -> GameResult { + if self.x < 0 || self.x > stage.map.width as i32 * state.tile_size.as_int() * 0x200 { + self.vanish(state); + return Ok(()); + } + + if self.action_num == 0 { + self.action_num = 1; + self.target_x = self.x; + self.target_y = self.y; + self.action_counter2 = self.tsc_direction / 8; + self.tsc_direction &= 7; + } + + if self.action_num == 1 { + self.action_counter2 += 24; + self.action_counter2 &= 0xFF; + + if self.action_counter < 128 { + self.action_counter += 1; + } + + self.vel_x += self.direction.vector_x() * 0x15; + self.target_x += self.vel_x; + + self.x = self.target_x + 4 * ((self.action_counter2 as f64).cos() * -512.0) as i32; + self.y = self.target_y + 6 * ((self.action_counter2 as f64).sin() * -512.0) as i32; + + let mut npc = NPC::create(286, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.x; + npc.y = self.y; + + let _ = npc_list.spawn(0x100, npc); + } + + self.anim_rect = state.constants.npc.n285_undead_core_spiral_projectile; + + Ok(()) + } + + pub(crate) fn tick_n286_undead_core_spiral_projectile_trail(&mut self, state: &mut SharedGameState) -> GameResult { + self.anim_counter += 1; + if self.anim_counter > 0 { + self.anim_counter = 0; + self.anim_num += 1; + } + + if self.anim_num < 3 { + self.anim_rect = state.constants.npc.n286_undead_core_spiral_projectile_trail[self.anim_num as usize]; + } else { + self.cond.set_alive(false); + } + + Ok(()) + } + + pub(crate) fn tick_n287_orange_smoke(&mut self, state: &mut SharedGameState) -> GameResult { + if self.action_num == 0 { + self.vel_x = self.rng.range(-4..4) * 0x200; + self.action_num = 1; + } else if self.action_num == 1 { + self.vel_x = 20 * self.vel_x / 21; + self.vel_y = 20 * self.vel_y / 21; + self.x += self.vel_x; + self.y += self.vel_y; + } + + self.anim_counter += 1; + if self.anim_counter > 1 { + self.anim_counter = 0; + self.anim_num += 1; + } + + if self.anim_num < 7 { + self.anim_rect = state.constants.npc.n287_orange_smoke[self.anim_num as usize]; + } else { + self.cond.set_alive(false); + } + + Ok(()) + } + + pub(crate) fn tick_n288_undead_core_exploding_rock( + &mut self, + state: &mut SharedGameState, + players: [&mut Player; 2], + npc_list: &NPCList, + stage: &mut Stage, + ) -> GameResult { + match self.action_num { + 0 | 1 => { + if self.action_num == 0 { + self.action_num = 1; + self.vel_x = -0x200; + } + + match self.direction { + Direction::Up => { + self.vel_y -= 0x20; + if self.vel_y < -0x5FF { + self.vel_y = -0x5FF; + } + + if self.flags.hit_top_wall() { + self.action_num = 2; + } + } + Direction::Bottom => { + self.vel_y += 0x20; + if self.vel_y > 0x5FF { + self.vel_y = 0x5FF; + } + + if self.flags.hit_bottom_wall() { + self.action_num = 2; + } + } + _ => (), + } + + self.animate(3, 0, 1); + } + 2 | 3 => { + if self.action_num == 2 { + let player = self.get_closest_player_ref(&players); + + self.action_num = 3; + self.action_counter = 0; + self.npc_flags.set_ignore_solidity(true); + self.vel_x = if self.x > player.x { -0x400 } else { 0x400 }; + self.vel_y = 0; + + self.display_bounds = Rect::new(0x1800, 0x1800, 0x1800, 0x1800); + state.sound_manager.play_sfx(44); + } + + self.anim_num += 1; + if self.anim_num > 4 { + self.anim_num = 2; + } + + self.action_counter += 1; + if self.action_counter & 3 == 1 { + let mut npc = NPC::create(287, &state.npc_table); + npc.cond.set_alive(true); + + npc.x = self.x; + npc.y = self.y; + npc.vel_y = self.direction.vector_y() * 0x400; + + let _ = npc_list.spawn(0x100, npc); + } + + if self.x < 0x2000 || self.x > (stage.map.width as i32 + 1) * state.tile_size.as_int() * 0x200 { + self.cond.set_alive(false); + } + } + _ => (), + } + + self.x += self.vel_x; + self.y += self.vel_y; + + self.anim_rect = state.constants.npc.n288_undead_core_exploding_rock[self.anim_num as usize]; + + Ok(()) + } +} + +impl BossNPC { + pub(crate) fn tick_b07_undead_core( + &mut self, + state: &mut SharedGameState, + npc_list: &NPCList, + stage: &mut Stage, + flash: &mut Flash, + ) { + let mut v19 = false; + + match self.parts[0].action_num { + 1 => { + self.parts[0].action_num = 10; + self.parts[0].exp = 1; + self.parts[0].cond.set_alive(true); + self.parts[0].npc_flags.set_ignore_solidity(true); + self.parts[0].npc_flags.set_invulnerable(true); + self.parts[0].npc_flags.set_show_damage(true); + self.parts[0].life = 700; + self.hurt_sound[0] = 114; + self.parts[0].x = 0x4a000; + self.parts[0].y = 0xf000; + self.parts[0].vel_x = 0; + self.parts[0].vel_y = 0; + self.parts[0].event_num = 1000; + self.parts[0].npc_flags.set_event_when_killed(true); + + self.parts[3].cond.set_alive(true); + self.parts[3].action_num = 0; + + self.parts[4].cond.set_alive(true); + self.parts[4].action_num = 10; + + self.parts[5].cond.set_alive(true); + self.parts[5].action_num = 10; + + self.parts[8].cond.set_alive(true); + self.parts[8].npc_flags.set_ignore_solidity(true); + self.parts[8].display_bounds.left = 0; + self.parts[8].display_bounds.top = 0; + self.parts[8].hit_bounds.right = 0x5000; + self.parts[8].hit_bounds.top = 0x2000; + self.parts[8].hit_bounds.bottom = 0x2000; + self.parts[8].action_counter2 = 0; + + self.parts[9] = self.parts[8].clone(); + self.parts[9].hit_bounds.right = 0x4800; + self.parts[9].hit_bounds.top = 0x3000; + self.parts[9].hit_bounds.bottom = 0x3000; + self.parts[9].action_counter2 = 1; + + self.parts[10] = self.parts[8].clone(); + self.parts[10].hit_bounds.right = 0x5800; + self.parts[10].hit_bounds.top = 0x1000; + self.parts[10].hit_bounds.bottom = 0x1000; + self.parts[10].action_counter2 = 2; + + self.parts[11] = self.parts[8].clone(); + self.parts[11].cond.set_damage_boss(true); + self.parts[11].hit_bounds.right = 0x2800; + self.parts[11].hit_bounds.top = 0x2800; + self.parts[11].hit_bounds.bottom = 0x2800; + self.parts[11].action_counter2 = 3; + + self.parts[1].cond.set_alive(true); + self.parts[1].action_num = 0; + self.parts[1].npc_flags.set_shootable(true); + self.parts[1].npc_flags.set_ignore_solidity(true); + self.parts[1].life = 1000; + self.hurt_sound[1] = 54; + self.parts[1].hit_bounds.right = 0x3000; + self.parts[1].hit_bounds.top = 0x2000; + self.parts[1].hit_bounds.bottom = 0x2000; + self.parts[1].display_bounds.left = 0x4000; + self.parts[1].display_bounds.top = 0x2800; + self.parts[1].parent_id = 0; + + self.parts[2] = self.parts[1].clone(); + self.parts[2].action_counter3 = 128; + + self.parts[6] = self.parts[1].clone(); + self.parts[6].action_counter3 = 1; + + self.parts[7] = self.parts[1].clone(); + self.parts[7].action_counter2 = 1; + self.parts[7].action_counter3 = 128; + + self.parts[19].action_counter = self.parts[0].life; + + for i in 0u16..20 { + self.parts[i as usize].id = i; + } + } + 15 => { + self.parts[0].action_num = 16; + self.parts[0].direction = Direction::Left; + self.parts[3].action_num = 10; + self.parts[4].anim_num = 0; + v19 = true; + } + 20 => { + self.parts[0].action_num = 210; + self.parts[0].direction = Direction::Left; + self.parts[1].action_num = 5; + self.parts[2].action_num = 5; + self.parts[6].action_num = 5; + self.parts[7].action_num = 5; + v19 = true; + } + 200 | 201 => { + if self.parts[0].action_num == 200 { + self.parts[0].action_num = 201; + self.parts[0].action_counter = 0; + self.parts[3].action_num = 0; + self.parts[4].anim_num = 2; + self.parts[5].anim_num = 0; + self.parts[8].npc_flags.set_invulnerable(false); + self.parts[9].npc_flags.set_invulnerable(false); + self.parts[10].npc_flags.set_invulnerable(false); + self.parts[11].npc_flags.set_shootable(false); + + state.npc_super_pos.1 = 0; + state.sound_manager.stop_sfx(40); + state.sound_manager.stop_sfx(41); + state.sound_manager.stop_sfx(58); + + v19 = true; + } + + self.parts[0].action_counter += 1; + if (self.parts[0].direction == Direction::Right + || self.parts[0].anim_num > 0 + || self.parts[0].life < 200) + && self.parts[0].action_counter > 200 + { + self.parts[0].action_counter2 += 1; + state.sound_manager.play_sfx(115); + + if self.parts[0].life >= 200 { + self.parts[0].action_num = if self.parts[0].action_counter2 <= 2 { 210 } else { 220 }; + } else { + self.parts[0].action_num = 230; + } + } + } + 210 | 211 => { + if self.parts[0].action_num == 210 { + self.parts[0].action_num = 211; + self.parts[0].action_counter = 0; + self.parts[3].action_num = 10; + self.parts[8].npc_flags.set_invulnerable(true); + self.parts[9].npc_flags.set_invulnerable(true); + self.parts[10].npc_flags.set_invulnerable(true); + self.parts[11].npc_flags.set_shootable(true); + self.parts[19].action_counter = self.parts[0].life; + v19 = true; + } + + self.parts[19].action_counter2 += 1; + if self.parts[0].shock != 0 && (self.parts[19].action_counter2 & 2) != 0 { + self.parts[4].anim_num = 1; + self.parts[5].anim_num = 1; + } else { + self.parts[4].anim_num = 0; + self.parts[5].anim_num = 0; + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 100 == 1 { + state.npc_curly_counter = self.parts[0].rng.range(80..100) as u16; + state.npc_curly_target = (self.parts[11].x, self.parts[11].y); + } + + if self.parts[0].action_counter < 300 { + if self.parts[0].action_counter % 120 == 1 { + let mut npc = NPC::create(288, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x - 0x4000; + npc.y = self.parts[0].y - 0x2000; + npc.direction = Direction::Up; + + let _ = npc_list.spawn(0x20, npc); + } + + if self.parts[0].action_counter % 120 == 61 { + let mut npc = NPC::create(288, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x - 0x4000; + npc.y = self.parts[0].y + 0x2000; + npc.direction = Direction::Bottom; + + let _ = npc_list.spawn(0x20, npc); + } + } + + if self.parts[0].life + 50 < self.parts[19].action_counter || self.parts[0].action_counter > 400 { + self.parts[0].action_num = 200; + } + } + 220 | 221 => { + if self.parts[0].action_num == 220 { + state.npc_super_pos.1 = 1; + + self.parts[0].action_num = 221; + self.parts[0].action_counter = 0; + self.parts[0].action_counter2 = 0; + self.parts[3].action_num = 20; + self.parts[8].npc_flags.set_invulnerable(true); + self.parts[9].npc_flags.set_invulnerable(true); + self.parts[10].npc_flags.set_invulnerable(true); + self.parts[11].npc_flags.set_shootable(true); + self.parts[19].action_counter = self.parts[0].life; + + state.quake_counter = 100; + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 40 == 1 { + let (x, y) = match self.parts[0].rng.range(0..3) { + 0 => (self.parts[1].x, self.parts[1].y), + 1 => (self.parts[2].x, self.parts[2].y), + 2 => (self.parts[6].x, self.parts[6].y), + 3 => (self.parts[7].x, self.parts[7].y), + _ => unsafe { + unreachable_unchecked(); + }, + }; + + state.sound_manager.play_sfx(25); + + let mut npc = NPC::create(285, &state.npc_table); + npc.cond.set_alive(true); + npc.x = x - 0x2000; + npc.y = y; + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 1024; + let _ = npc_list.spawn(0x100, npc); + } + + self.parts[19].action_counter2 += 1; + + if self.parts[0].shock != 0 && (self.parts[19].action_counter2 & 2) != 0 { + self.parts[4].anim_num = 1; + self.parts[5].anim_num = 1; + } else { + self.parts[4].anim_num = 0; + self.parts[5].anim_num = 0; + } + + if self.parts[0].life + 150 < self.parts[0].action_counter + || self.parts[0].action_counter > 400 + || self.parts[0].life < 200 + { + self.parts[0].action_num = 200; + } + } + 230 | 231 => { + if self.parts[0].action_num == 230 { + self.parts[0].action_num = 231; + self.parts[0].action_counter = 0; + self.parts[3].action_num = 30; + self.parts[8].npc_flags.set_invulnerable(true); + self.parts[9].npc_flags.set_invulnerable(true); + self.parts[10].npc_flags.set_invulnerable(true); + self.parts[11].npc_flags.set_shootable(true); + + state.sound_manager.play_sfx(25); + + let mut npc = NPC::create(285, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[3].x - 0x2000; + npc.y = self.parts[3].y; + + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 1024; + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 0; + npc.x = self.parts[3].x; + npc.y = self.parts[3].y - 0x2000; + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 1024; + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 0; + npc.x = self.parts[3].x; + npc.y = self.parts[3].y + 0x2000; + let _ = npc_list.spawn(0x100, npc.clone()); + + npc.tsc_direction = 1024; + let _ = npc_list.spawn(0x100, npc); + + self.parts[19].action_counter = self.parts[0].life; + v19 = true; + } + + self.parts[19].action_counter2 += 1; + if self.parts[0].shock != 0 && (self.parts[19].action_counter2 & 2) != 0 { + self.parts[4].anim_num = 1; + self.parts[5].anim_num = 1; + } else { + self.parts[4].anim_num = 0; + self.parts[5].anim_num = 0; + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 100 == 1 { + state.npc_curly_counter = self.parts[0].rng.range(80..100) as u16; + state.npc_curly_target = (self.parts[11].x, self.parts[11].y); + } + + if self.parts[0].action_counter % 120 == 1 { + let mut npc = NPC::create(288, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x - 0x4000; + npc.y = self.parts[0].y - 0x2000; + npc.direction = Direction::Up; + + let _ = npc_list.spawn(0x20, npc.clone()); + } + + if self.parts[0].action_counter % 120 == 61 { + let mut npc = NPC::create(288, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x - 0x4000; + npc.y = self.parts[0].y + 0x2000; + npc.direction = Direction::Bottom; + + let _ = npc_list.spawn(0x20, npc.clone()); + } + } + 500 | 501 => { + if self.parts[0].action_num == 500 { + state.sound_manager.stop_sfx(40); + state.sound_manager.stop_sfx(41); + state.sound_manager.stop_sfx(58); + + self.parts[0].action_num = 501; + self.parts[0].action_counter = 0; + self.parts[0].vel_x = 0; + self.parts[0].vel_y = 0; + self.parts[3].action_num = 0; + self.parts[4].anim_num = 2; + self.parts[5].anim_num = 0; + self.parts[1].action_num = 5; + self.parts[2].action_num = 5; + self.parts[6].action_num = 5; + self.parts[7].action_num = 5; + + state.quake_counter = 20; + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + + for _ in 0..100 { + npc.x = self.parts[0].x + self.parts[0].rng.range(-128..128) * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-64..64) * 0x200; + npc.vel_x = self.parts[0].rng.range(-128..128) * 0x200; + npc.vel_y = self.parts[0].rng.range(-128..128) * 0x200; + + let _ = npc_list.spawn(0, npc.clone()); + } + + npc_list.kill_npcs_by_type(282, true, state); + + self.parts[11].npc_flags.set_shootable(false); + + for i in 0..12 { + self.parts[i].npc_flags.set_invulnerable(false); + } + } + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 16 != 0 { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = self.parts[0].x + self.parts[0].rng.range(-64..64) * 0x200; + npc.y = self.parts[0].y + self.parts[0].rng.range(-32..32) * 0x200; + npc.vel_x = self.parts[0].rng.range(-128..128) * 0x200; + npc.vel_y = self.parts[0].rng.range(-128..128) * 0x200; + + let _ = npc_list.spawn(0x100, npc); + } + + self.parts[0].x += 0x40; + self.parts[0].y += 0x80; + + if self.parts[0].action_counter > 200 { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 1000; + } + } + 1000 => { + state.quake_counter = 100; + + self.parts[0].action_counter += 1; + if self.parts[0].action_counter % 8 == 0 { + state.sound_manager.play_sfx(44); + } + + npc_list.create_death_smoke( + self.parts[0].x + self.parts[0].rng.range(-72..72) * 0x200, + self.parts[0].y + self.parts[0].rng.range(-64..64) * 0x200, + 1, + 1, + state, + &self.parts[0].rng, + ); + + if self.parts[0].action_counter > 100 { + self.parts[0].action_counter = 0; + self.parts[0].action_num = 1001; + + state.sound_manager.play_sfx(35); + flash.set_cross(self.parts[0].x, self.parts[0].y); + } + } + 1001 => { + state.quake_counter = 40; + self.parts[0].action_counter += 1; + if self.parts[0].action_counter > 50 { + for i in 0..20 { + self.parts[i].cond.set_alive(false); + } + + npc_list.kill_npcs_by_type(158, true, state); + npc_list.kill_npcs_by_type(301, true, state); + } + } + _ => (), + } + + if v19 { + state.quake_counter = 20; + state.sound_manager.play_sfx(26); + + if self.parts[0].action_num == 201 { + self.parts[7].action_num = 10; + self.parts[6].action_num = 10; + self.parts[2].action_num = 10; + self.parts[1].action_num = 10; + } + + if self.parts[0].action_num == 221 { + self.parts[7].action_num = 20; + self.parts[6].action_num = 20; + self.parts[2].action_num = 20; + self.parts[1].action_num = 20; + } + + if self.parts[0].action_num == 231 { + self.parts[7].action_num = 30; + self.parts[6].action_num = 30; + self.parts[2].action_num = 30; + self.parts[1].action_num = 30; + } + + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + + for _ in 0..8 { + npc.x = self.parts[4].x + self.parts[0].rng.range(-32..16) * 0x200; + npc.y = self.parts[4].y; + npc.vel_x = self.parts[0].rng.range(-0x200..0x200); + npc.vel_y = self.parts[0].rng.range(-0x100..0x100); + + let _ = npc_list.spawn(0x100, npc.clone()); + } + } + + if self.parts[0].action_num >= 200 && self.parts[0].action_num < 300 { + if self.parts[0].x < 0x18000 { + self.parts[0].direction = Direction::Right; + } + if self.parts[0].x > (stage.map.width as i32 - 4) * state.tile_size.as_int() * 0x200 { + self.parts[0].direction = Direction::Left; + } + + self.parts[0].vel_x += self.parts[0].direction.vector_x() * 4; + } + + if [201, 211, 221, 231].contains(&self.parts[0].action_num) { + self.parts[0].action_counter3 += 1; + if self.parts[0].action_counter3 == 150 { + self.parts[0].action_counter3 = 0; + + let mut npc = NPC::create(282, &state.npc_table); + npc.cond.set_alive(true); + npc.x = stage.map.width as i32 * state.tile_size.as_int() * 0x200 + 0x40; + npc.y = (self.parts[0].rng.range(-1..3) + 10) * state.tile_size.as_int() * 0x200; + + let _ = npc_list.spawn(0x30, npc); + } else if self.parts[0].action_counter3 == 75 { + let mut npc = NPC::create(282, &state.npc_table); + npc.cond.set_alive(true); + npc.x = stage.map.width as i32 * state.tile_size.as_int() * 0x200 + 0x40; + npc.y = (self.parts[0].rng.range(-3..0) + 3) * state.tile_size.as_int() * 0x200; + + let _ = npc_list.spawn(0x30, npc); + } + } + + self.parts[0].vel_x = self.parts[0].vel_x.clamp(-0x80, 0x80); + self.parts[0].vel_y = self.parts[0].vel_y.clamp(-0x80, 0x80); + + self.parts[0].x += self.parts[0].vel_x; + self.parts[0].y += self.parts[0].vel_y; + + self.tick_b07_undead_core_face(3, state, npc_list); + self.tick_b07_undead_core_head(4, state); + self.tick_b07_undead_core_tail(5, state); + self.tick_b07_undead_core_small_head(1, state, stage); + self.tick_b07_undead_core_small_head(2, state, stage); + self.tick_b07_undead_core_small_head(6, state, stage); + self.tick_b07_undead_core_small_head(7, state, stage); + self.tick_b07_undead_core_hitbox(8); + self.tick_b07_undead_core_hitbox(9); + self.tick_b07_undead_core_hitbox(10); + self.tick_b07_undead_core_hitbox(11); + } + + fn tick_b07_undead_core_face(&mut self, i: usize, state: &mut SharedGameState, npc_list: &NPCList) { + let (head, tail) = self.parts.split_at_mut(i); + let base = &mut head[0]; + let part = &mut tail[0]; + + match part.action_num { + 0 => { + part.anim_num = 0; + } + 10 => { + part.anim_num = 1; + } + 20 => { + part.anim_num = 2; + } + 30 | 31 => { + if part.action_num == 30 { + part.action_num = 31; + part.anim_num = 3; + part.action_counter = 100; + } + + part.action_counter += 1; + if part.action_counter > 300 { + part.action_counter = 0; + } + + if part.action_counter > 250 && part.action_counter % 16 == 1 { + state.sound_manager.play_sfx(26); + } + + if part.action_counter > 250 && part.action_counter % 16 == 7 { + state.sound_manager.play_sfx(101); + + let mut npc = NPC::create(293, &state.npc_table); + npc.cond.set_alive(true); + npc.x = part.x; + npc.y = part.y; + + let _ = npc_list.spawn(0x80, npc); + } + + if part.action_counter == 200 { + state.sound_manager.play_sfx(116); + } + + part.anim_num = if part.action_counter > 200 && part.action_counter & 1 != 0 { 4 } else { 3 }; + } + _ => (), + } + + part.display_bounds.right = 0x4800; + part.display_bounds.left = 0x4800; + part.display_bounds.top = 0x2800; + part.x = base.x - 0x4800; + part.y = base.y + 0x800; + part.npc_flags.set_ignore_solidity(true); + + part.anim_rect = state.constants.npc.b07_undead_core[part.anim_num as usize]; + } + + fn tick_b07_undead_core_head(&mut self, i: usize, state: &mut SharedGameState) { + let (head, tail) = self.parts.split_at_mut(i); + let base = &mut head[0]; + let part = &mut tail[0]; + + match part.action_num { + 10 | 11 => { + if part.action_num == 10 { + part.action_num = 11; + part.anim_num = 2; + part.npc_flags.set_ignore_solidity(true); + part.display_bounds.left = 0x4800; + part.display_bounds.top = 0x7000; + } + + part.x = base.x - 0x4800; + part.y = base.y; + } + 50 | 51 => { + if part.action_num == 50 { + part.action_num = 51; + part.action_counter = 112; + } + + part.action_counter = part.action_counter.saturating_sub(1); + if part.action_counter == 0 { + part.action_num = 100; + part.anim_num = 3; + } + } + 100 => { + part.anim_num = 3; + } + _ => (), + } + + part.anim_rect = state.constants.npc.b07_undead_core[part.anim_num as usize + 5]; + + if part.action_num == 51 { + part.anim_rect.bottom = part.action_counter + part.anim_rect.top; + } + } + + fn tick_b07_undead_core_tail(&mut self, i: usize, state: &mut SharedGameState) { + let (head, tail) = self.parts.split_at_mut(i); + let base = &mut head[0]; + let part = &mut tail[0]; + + match part.action_num { + 10 | 11 => { + if part.action_num == 10 { + part.action_num = 11; + part.anim_num = 0; + part.npc_flags.set_ignore_solidity(true); + part.display_bounds.left = 0x5800; + part.display_bounds.top = 0x7000; + } + + part.x = base.x + 0x5800; + part.y = base.y; + } + 50 | 51 => { + if part.action_num == 50 { + part.action_num = 51; + part.action_counter = 112; + } + + part.action_counter = part.action_counter.saturating_sub(1); + if part.action_counter == 0 { + part.action_num = 100; + part.anim_num = 2; + } + } + 100 => { + part.anim_num = 2; + } + _ => (), + } + + part.anim_rect = state.constants.npc.b07_undead_core[part.anim_num as usize + 9]; + + if part.action_num == 51 { + part.anim_rect.bottom = part.action_counter + part.anim_rect.top; + } + } + + fn tick_b07_undead_core_small_head(&mut self, i: usize, state: &mut SharedGameState, stage: &mut Stage) { + let parent = self.parts[i].parent_id as usize; + let (base, part) = if let Some(x) = self.parts.get_two_mut(parent, i) { + x + } else { + return; + }; + + if !part.cond.alive() { + return; + } + + part.life = 1000; + match part.action_num { + 0 => { + part.npc_flags.set_shootable(false); + } + 5 => { + part.anim_num = 0; + part.npc_flags.set_shootable(false); + part.action_counter3 += 1; + part.action_counter3 &= 0xff; + } + 10 => { + part.anim_num = 0; + part.npc_flags.set_shootable(false); + part.action_counter3 += 2; + part.action_counter3 &= 0xff; + } + 20 => { + part.anim_num = 1; + part.npc_flags.set_shootable(false); + part.action_counter3 += 2; + part.action_counter3 &= 0xff; + } + 30 => { + part.anim_num = 0; + part.npc_flags.set_shootable(false); + part.action_counter3 += 4; + part.action_counter3 &= 0xff; + } + 200 | 201 => { + if part.action_num == 200 { + part.action_num = 201; + part.anim_num = 2; + part.vel_x = 0; + part.vel_y = 0; + } + + part.vel_x += 0x20; + part.x += part.vel_x; + if part.x > (stage.map.width as i32) * state.tile_size.as_int() * 0x200 + 0x4000 { + part.cond.set_alive(false); + } + } + _ => (), + } + + if part.action_num < 50 { + let angle = + if part.action_counter2 != 0 { part.action_counter3 + 0x80 } else { part.action_counter3 + 0x180 }; + + let angle = ((angle / 2) as f64 * CDEG_RAD); + + part.x = base.x + 0x30 * (angle.cos() * -512.0) as i32 - 0x1000; + part.y = base.y + 0x50 * (angle.sin() * -512.0) as i32; + } + + part.anim_rect = state.constants.npc.b07_undead_core[part.anim_num as usize + 12]; + } + + fn tick_b07_undead_core_hitbox(&mut self, i: usize) { + let (head, tail) = self.parts.split_at_mut(i); + let base = &mut head[0]; + let part = &mut tail[0]; + + match part.action_counter2 { + 0 => { + part.x = base.x; + part.y = base.y - 0x4000; + } + 1 => { + part.x = base.x + 0x3800; + part.y = base.y; + } + 2 => { + part.x = base.x + 0x800; + part.y = base.y + 0x4000; + } + 3 => { + part.x = base.x - 0x3800; + part.y = base.y + 0x800; + } + _ => (), + } } } diff --git a/src/npc/mod.rs b/src/npc/mod.rs index 8a52266..7103460 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -2,7 +2,6 @@ use std::io; use std::io::Cursor; use byteorder::{ReadBytesExt, LE}; -use num_traits::abs; use crate::bitfield; use crate::common::Direction; @@ -14,6 +13,7 @@ use crate::entity::GameEntity; use crate::frame::Frame; use crate::framework::context::Context; use crate::framework::error::GameResult; +use crate::npc::boss::BossNPC; use crate::npc::list::NPCList; use crate::physics::PhysicalEntity; use crate::player::Player; @@ -192,7 +192,8 @@ impl NPC { if let Some(batch) = state.texture_set.get_or_load_batch(ctx, &state.constants, texture)?.glow() { let off_x = - if self.direction == Direction::Left { self.display_bounds.left } else { self.display_bounds.right } as i32; + if self.direction == Direction::Left { self.display_bounds.left } else { self.display_bounds.right } + as i32; let shock = if self.shock > 0 { (2 * ((self.shock as i32 / 2) % 2) - 1) as f32 } else { 0.0 }; let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time); @@ -214,16 +215,17 @@ impl NPC { } } -impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mut Flash)> for NPC { +impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mut Flash, &mut BossNPC)> for NPC { fn tick( &mut self, state: &mut SharedGameState, - (players, npc_list, stage, bullet_manager, flash): ( + (players, npc_list, stage, bullet_manager, flash, boss): ( [&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mut Flash, + &mut BossNPC, ), ) -> GameResult { #[allow(unused_assignments)] @@ -467,31 +469,96 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu 229 => self.tick_n229_red_flowers_sprouts(state), 230 => self.tick_n230_red_flowers_blooming(state), 231 => self.tick_n231_rocket(state, players, npc_list), + 232 => self.tick_n232_orangebell(state, npc_list), + 233 => self.tick_n233_orangebell_bat(state, players, npc_list), 234 => self.tick_n234_red_flowers_picked(state), + 235 => self.tick_n235_midorin(state), + 236 => self.tick_n236_gunfish(state, players, npc_list), + 237 => self.tick_n237_gunfish_projectile(state), 238 => self.tick_n238_press_sideways(state, players, npc_list), 239 => self.tick_n239_cage_bars(state), + 240 => self.tick_n240_mimiga_jailed(state), 241 => self.tick_n241_critter_red(state, players), + 242 => self.tick_n242_bat_last_cave(state), + 243 => self.tick_n243_bat_generator(state, npc_list), + 244 => self.tick_n244_lava_drop(state, players), + 245 => self.tick_n245_lava_drop_generator(state, npc_list), + 246 => self.tick_n246_press_proximity(state, players, npc_list), 247 => self.tick_n247_misery_boss(state, players, npc_list), 248 => self.tick_n248_misery_boss_vanishing(state), - 249 => self.tick_n249_misery_boss_energy_shot(state), + 249 => self.tick_n249_misery_boss_appearing(state), + 250 => self.tick_n250_misery_boss_lighting_ball(state, players, npc_list), + 251 => self.tick_n251_misery_boss_lighting(state, npc_list), + 252 => self.tick_n252_misery_boss_bats(state, players, npc_list), 253 => self.tick_n253_experience_capsule(state, npc_list), + 254 => self.tick_n254_helicopter(state, npc_list), + 255 => self.tick_n255_helicopter_blades(state, npc_list), + 256 => self.tick_n256_doctor_facing_away(state, npc_list), 257 => self.tick_n257_red_crystal(state), 258 => self.tick_n258_mimiga_sleeping(state), - 259 => self.tick_n259_curly_unconcious(state, players, npc_list), + 259 => self.tick_n259_curly_unconscious(state, players, npc_list), + 260 => self.tick_n260_shovel_brigade_caged(state, players, npc_list), + 261 => self.tick_n261_chie_caged(state, players), + 262 => self.tick_n262_chaco_caged(state, players), + 263 => self.tick_n263_doctor_boss(state, players, npc_list), + 264 => self.tick_n264_doctor_boss_red_projectile(state, npc_list, stage), + 265 => self.tick_n265_doctor_boss_red_projectile_trail(state), + 266 => self.tick_n266_doctor_boss_red_projectile_bouncing(state, npc_list), + 267 => self.tick_n267_muscle_doctor(state, players, npc_list), + 268 => self.tick_n268_igor_enemy(state, players, npc_list), + 269 => self.tick_n269_red_bat_bouncing(state), + 270 => self.tick_n270_doctor_red_energy(state, npc_list), + 271 => self.tick_n271_ironhead_block(state, stage), + 272 => self.tick_n272_ironhead_block_generator(state, npc_list), + 273 => self.tick_n273_droll_projectile(state, npc_list), + 274 => self.tick_n274_droll(state, players, npc_list), + 275 => self.tick_n275_puppy_plantation(state, players), + 276 => self.tick_n276_red_demon(state, players, npc_list), + 277 => self.tick_n277_red_demon_projectile(state, npc_list), + 278 => self.tick_n278_little_family(state), + 279 => self.tick_n279_large_falling_block(state, players, npc_list, stage), + 280 => self.tick_n280_sue_teleported(state), + 281 => self.tick_n281_doctor_energy_form(state, npc_list), + 282 => self.tick_n282_mini_undead_core_active(state, players), + 283 => self.tick_n283_misery_possessed(state, players, npc_list, stage, boss), + 284 => self.tick_n284_sue_possessed(state, players, npc_list, stage, boss), + 285 => self.tick_n285_undead_core_spiral_projectile(state, npc_list, stage), + 286 => self.tick_n286_undead_core_spiral_projectile_trail(state), + 287 => self.tick_n287_orange_smoke(state), + 288 => self.tick_n288_undead_core_exploding_rock(state, players, npc_list, stage), + 289 => self.tick_n289_critter_orange(state, players, stage), + 290 => self.tick_n290_bat_misery(state, players, stage), + 291 => self.tick_n291_mini_undead_core_inactive(state), 292 => self.tick_n292_quake(state), + 293 => self.tick_n293_undead_core_energy_shot(state, npc_list), + 294 => self.tick_n294_quake_falling_block_generator(state, players, npc_list, stage), + 295 => self.tick_n295_cloud(state), + 296 => self.tick_n296_cloud_generator(state, npc_list), 297 => self.tick_n297_sue_dragon_mouth(state, npc_list), 298 => self.tick_n298_intro_doctor(state), 299 => self.tick_n299_intro_balrog_misery(state), 300 => self.tick_n300_intro_demon_crown(state), + 301 => self.tick_n301_misery_fish_missile(state, players), 302 => self.tick_n302_camera_focus_marker(state, players, npc_list), + 304 => self.tick_n304_gaudi_hospital(state), + 305 => self.tick_n305_small_puppy(state), + 306 => self.tick_n306_balrog_nurse(state), + 307 => self.tick_n307_santa_caged(state), + 308 => self.tick_n308_stumpy(state, players), + 326 => self.tick_n326_sue_itoh_human_transition(state, npc_list), + 327 => self.tick_n327_sneeze(state, npc_list), 328 => self.tick_n328_human_transform_machine(state), 329 => self.tick_n329_laboratory_fan(state), + 337 => self.tick_n337_numahachi(state), + 347 => self.tick_n347_hoppy(state, players), 349 => self.tick_n349_statue(state), 351 => self.tick_n351_statue_shootable(state, npc_list), 352 => self.tick_n352_ending_characters(state, npc_list), 355 => self.tick_n355_quote_and_curly_on_balrog(state, npc_list), + 357 => self.tick_n357_puppy_ghost(state), 358 => self.tick_n358_misery_credits(state), 359 => self.tick_n359_water_droplet_generator(state, players, npc_list), + 360 => self.tick_n360_credits_thank_you(state), _ => { #[cfg(feature = "hooks")] { @@ -509,11 +576,11 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu self.shock -= 1; } - if abs(self.prev_x - self.x) > 0x1000 { + if (self.prev_x - self.x).abs() > 0x1000 { self.prev_x = self.x; } - if abs(self.prev_y - self.y) > 0x1000 { + if (self.prev_y - self.y).abs() > 0x1000 { self.prev_y = self.y; } diff --git a/src/npc/utils.rs b/src/npc/utils.rs index 9eb4f63..bf68864 100644 --- a/src/npc/utils.rs +++ b/src/npc/utils.rs @@ -1,46 +1,48 @@ ///! Various utility functions for NPC-related objects - use num_traits::abs; -use crate::weapon::bullet::Bullet; use crate::caret::CaretType; use crate::common::{Condition, Direction, Flag, Rect}; -use crate::map::NPCData; -use crate::npc::{NPC, NPCFlag, NPCTable, NPCLayer}; -use crate::npc::list::NPCList; -use crate::player::Player; -use crate::rng::{RNG, Xoroshiro32PlusPlus}; -use crate::shared_game_state::{SharedGameState, TileSize}; use crate::components::number_popup::NumberPopup; +use crate::map::NPCData; +use crate::npc::list::NPCList; +use crate::npc::{NPCFlag, NPCLayer, NPCTable, NPC}; +use crate::player::Player; +use crate::rng::{Xoroshiro32PlusPlus, RNG}; +use crate::shared_game_state::{SharedGameState, TileSize}; +use crate::weapon::bullet::Bullet; impl NPC { /// Initializes the RNG. Called when the [NPC] is being added to an [NPCList]. pub(crate) fn init_rng(&mut self) { - self.rng = Xoroshiro32PlusPlus::new((self.id as u32) - .wrapping_sub(self.npc_type as u32) - .wrapping_add(self.flag_num as u32) - .wrapping_mul(214013) - .wrapping_add(2531011) >> 5); + self.rng = Xoroshiro32PlusPlus::new( + (self.id as u32) + .wrapping_sub(self.npc_type as u32) + .rotate_right(5) + .wrapping_sub(self.flag_num as u32) + .rotate_right((self.event_num % 13) as u32) + .wrapping_mul(214013) + .rotate_right(13) + .wrapping_add(2531011) + .rotate_right(5), + ); } /// Creates a new NPC object with properties that have been populated with data from given NPC data table. pub fn create(npc_type: u16, table: &NPCTable) -> NPC { let display_bounds = table.get_display_bounds(npc_type); let hit_bounds = table.get_hit_bounds(npc_type); - let (size, life, damage, flags, exp, spritesheet_id) = - match table.get_entry(npc_type) { - Some(entry) => { - ( - entry.size, - entry.life, - entry.damage as u16, - entry.npc_flags, - entry.experience as u16, - entry.spritesheet_id as u16 - ) - } - None => { (2, 0, 0, NPCFlag(0), 0, 0) } - }; + let (size, life, damage, flags, exp, spritesheet_id) = match table.get_entry(npc_type) { + Some(entry) => ( + entry.size, + entry.life, + entry.damage as u16, + entry.npc_flags, + entry.experience as u16, + entry.spritesheet_id as u16, + ), + None => (2, 0, 0, NPCFlag(0), 0, 0), + }; let npc_flags = NPCFlag(flags.0); NPC { @@ -161,19 +163,16 @@ impl NPC { /// Returns true if the [NPC] collides with a [Bullet]. pub fn collides_with_bullet(&self, bullet: &Bullet) -> bool { - ( - self.npc_flags.shootable() - && (self.x - self.hit_bounds.right as i32) < (bullet.x + bullet.enemy_hit_width as i32) - && (self.x + self.hit_bounds.right as i32) > (bullet.x - bullet.enemy_hit_width as i32) - && (self.y - self.hit_bounds.top as i32) < (bullet.y + bullet.enemy_hit_height as i32) - && (self.y + self.hit_bounds.bottom as i32) > (bullet.y - bullet.enemy_hit_height as i32) - ) || ( - self.npc_flags.invulnerable() + (self.npc_flags.shootable() + && (self.x - self.hit_bounds.right as i32) < (bullet.x + bullet.enemy_hit_width as i32) + && (self.x + self.hit_bounds.right as i32) > (bullet.x - bullet.enemy_hit_width as i32) + && (self.y - self.hit_bounds.top as i32) < (bullet.y + bullet.enemy_hit_height as i32) + && (self.y + self.hit_bounds.bottom as i32) > (bullet.y - bullet.enemy_hit_height as i32)) + || (self.npc_flags.invulnerable() && (self.x - self.hit_bounds.right as i32) < (bullet.x + bullet.hit_bounds.right as i32) && (self.x + self.hit_bounds.right as i32) > (bullet.x - bullet.hit_bounds.left as i32) && (self.y - self.hit_bounds.top as i32) < (bullet.y + bullet.hit_bounds.bottom as i32) - && (self.y + self.hit_bounds.bottom as i32) > (bullet.y - bullet.hit_bounds.top as i32) - ) + && (self.y + self.hit_bounds.bottom as i32) > (bullet.y - bullet.hit_bounds.top as i32)) } /// Creates experience drop for this NPC. @@ -247,9 +246,15 @@ impl NPCList { } match npc.size { - 1 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng); } - 2 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng); } - 3 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng); } + 1 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng); + } + 2 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng); + } + 3 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng); + } _ => {} }; } @@ -264,9 +269,15 @@ impl NPCList { } match npc.size { - 1 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng); } - 2 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng); } - 3 => { self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng); } + 1 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng); + } + 2 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng); + } + 3 => { + self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng); + } _ => {} }; @@ -344,18 +355,43 @@ impl NPCList { /// Creates NPC death smoke diffusing in random directions. #[inline] - pub fn create_death_smoke(&self, x: i32, y: i32, radius: usize, amount: usize, state: &mut SharedGameState, rng: &dyn RNG) { + pub fn create_death_smoke( + &self, + x: i32, + y: i32, + radius: usize, + amount: usize, + state: &mut SharedGameState, + rng: &dyn RNG, + ) { self.create_death_smoke_common(x, y, radius, amount, Direction::Left, state, rng) } /// Creates NPC death smoke diffusing upwards. #[inline] - pub fn create_death_smoke_up(&self, x: i32, y: i32, radius: usize, amount: usize, state: &mut SharedGameState, rng: &dyn RNG) { + pub fn create_death_smoke_up( + &self, + x: i32, + y: i32, + radius: usize, + amount: usize, + state: &mut SharedGameState, + rng: &dyn RNG, + ) { self.create_death_smoke_common(x, y, radius, amount, Direction::Up, state, rng) } #[allow(clippy::too_many_arguments)] - fn create_death_smoke_common(&self, x: i32, y: i32, radius: usize, amount: usize, direction: Direction, state: &mut SharedGameState, rng: &dyn RNG) { + fn create_death_smoke_common( + &self, + x: i32, + y: i32, + radius: usize, + amount: usize, + direction: Direction, + state: &mut SharedGameState, + rng: &dyn RNG, + ) { let radius = (radius / 0x200) as i32; let mut npc = NPC::create(4, &state.npc_table); diff --git a/src/player/mod.rs b/src/player/mod.rs index 48257c7..c38c8aa 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -472,8 +472,8 @@ impl Player { state.constants.player.air_physics.max_move }; - self.vel_x = clamp(self.vel_x, -max_move, max_move); - self.vel_y = clamp(self.vel_y, -max_move, max_move); + self.vel_x = self.vel_x.clamp(-max_move, max_move); + self.vel_y = self.vel_y.clamp(-max_move, max_move); if !self.splash && self.flags.in_water() { let vertical_splash = !self.flags.hit_bottom_wall() && self.vel_y > 0x200; @@ -551,8 +551,100 @@ impl Player { Ok(()) } - fn tick_ironhead(&mut self, _state: &mut SharedGameState) -> GameResult { - // todo ironhead boss controls + fn tick_ironhead(&mut self, state: &mut SharedGameState) -> GameResult { + self.up = false; + self.down = false; + + if state.control_flags.control_enabled() { + if self.controller.move_left() || self.controller.move_right() { + if self.controller.move_left() { + self.vel_x -= 0x100; + } + if self.controller.move_right() { + self.vel_x += 0x100; + } + } else if self.vel_x > 0x7f || self.vel_x < -0x7f { + self.vel_x += 0x80 * -self.vel_x.signum(); + } else { + self.vel_x = 0; + } + + if self.controller.move_up() || self.controller.move_down() { + if self.controller.move_up() { + self.vel_y -= 0x100; + } + if self.controller.move_down() { + self.vel_y += 0x100; + } + } else if self.vel_y > 0x7f || self.vel_y < -0x7f { + self.vel_y += 0x80 * -self.vel_y.signum(); + } else { + self.vel_y = 0; + } + } else { + if self.vel_x > 0x7f || self.vel_x < -0x7f { + self.vel_x += 0x80 * -self.vel_x.signum(); + } else { + self.vel_x = 0; + } + + if self.vel_y > 0x7f || self.vel_y < -0x7f { + self.vel_y += 0x80 * -self.vel_y.signum(); + } else { + self.vel_y = 0; + } + } + + if self.vel_y < -0x200 && self.flags.hit_top_wall() { + state.create_caret(self.x, self.y - self.hit_bounds.top as i32, CaretType::LittleParticles, Direction::FacingPlayer); + } + + if self.vel_y > 0x200 && self.flags.hit_bottom_wall() { + state.create_caret(self.x, self.y + self.hit_bounds.bottom as i32, CaretType::LittleParticles, Direction::FacingPlayer); + } + + self.vel_x = self.vel_x.clamp(-0x400, 0x400); + self.vel_y = self.vel_y.clamp(-0x400, 0x400); + + if self.controller.move_left() && self.controller.move_up() { + if self.vel_x < -0x30c { + self.vel_x = -0x30c; + } + if self.vel_y < -0x30c { + self.vel_y = -0x30c; + } + } + + if self.controller.move_right() && self.controller.move_up() { + if self.vel_x > 0x30c { + self.vel_x = 0x30c; + } + if self.vel_y < -0x30c { + self.vel_y = -0x30c; + } + } + + if self.controller.move_left() && self.controller.move_down() { + if self.vel_x < -0x30c { + self.vel_x = -0x30c; + } + if self.vel_y > 0x30c { + self.vel_y = 0x30c; + } + } + + if self.controller.move_right() && self.controller.move_down() { + if self.vel_x > 0x30c { + self.vel_x = 0x30c; + } + if self.vel_y > 0x30c { + self.vel_y = 0x30c; + } + } + + self.x += self.vel_x; + self.y += self.vel_y; + Ok(()) } diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index b505acd..911a6ce 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -7,6 +7,7 @@ use crate::common::{interpolate_fix9_scale, Color, Direction, FadeDirection, Fad use crate::components::boss_life_bar::BossLifeBar; use crate::components::credits::Credits; use crate::components::draw_common::Alignment; +use crate::components::falling_island::FallingIsland; use crate::components::flash::Flash; use crate::components::hud::HUD; use crate::components::inventory::InventoryUI; @@ -32,9 +33,11 @@ use crate::rng::XorShift; use crate::scene::title_scene::TitleScene; use crate::scene::Scene; use crate::scripting::tsc::credit_script::CreditScriptVM; +use crate::scripting::tsc::text_script::{ + ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM, +}; use crate::shared_game_state::{SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage}; -use crate::scripting::tsc::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM}; use crate::texture_set::SpriteBatch; use crate::weapon::bullet::BulletManager; use crate::weapon::{Weapon, WeaponType}; @@ -48,6 +51,7 @@ pub struct GameScene { pub stage_select: StageSelect, pub flash: Flash, pub credits: Credits, + pub falling_island: FallingIsland, pub inventory_ui: InventoryUI, pub hud_player1: HUD, pub hud_player2: HUD, @@ -136,6 +140,7 @@ impl GameScene { stage_select: StageSelect::new(), flash: Flash::new(), credits: Credits::new(), + falling_island: FallingIsland::new(), inventory_ui: InventoryUI::new(), hud_player1: HUD::new(Alignment::Left), hud_player2: HUD::new(Alignment::Right), @@ -398,7 +403,11 @@ impl GameScene { for y in 0..(state.canvas_size.1 as i32 / 16 + 1) { if direction == FadeDirection::Left { - batch.add_rect(state.canvas_size.0 - x as f32 * 16.0 - 16.0, y as f32 * 16.0, &rect); + batch.add_rect( + state.canvas_size.0 - x as f32 * 16.0 - 16.0, + y as f32 * 16.0, + &rect, + ); } else { batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect); } @@ -813,14 +822,14 @@ impl GameScene { for npc in self.npc_list.iter_alive() { if npc.x < (self.frame.x - 128 * 0x200 - npc.display_bounds.width() as i32 * 0x200) || npc.x - > (self.frame.x - + 128 * 0x200 - + (state.canvas_size.0 as i32 + npc.display_bounds.width() as i32) * 0x200) - && npc.y < (self.frame.y - 128 * 0x200 - npc.display_bounds.height() as i32 * 0x200) + > (self.frame.x + + 128 * 0x200 + + (state.canvas_size.0 as i32 + npc.display_bounds.width() as i32) * 0x200) + && npc.y < (self.frame.y - 128 * 0x200 - npc.display_bounds.height() as i32 * 0x200) || npc.y - > (self.frame.y - + 128 * 0x200 - + (state.canvas_size.1 as i32 + npc.display_bounds.height() as i32) * 0x200) + > (self.frame.y + + 128 * 0x200 + + (state.canvas_size.1 as i32 + npc.display_bounds.height() as i32) * 0x200) { continue; } @@ -1209,6 +1218,27 @@ impl GameScene { (10, 50, 255), batch, ), + 270 => self.draw_light( + interpolate_fix9_scale(npc.prev_x - self.frame.prev_x, npc.x - self.frame.x, state.frame_time), + interpolate_fix9_scale(npc.prev_y - self.frame.prev_y, npc.y - self.frame.y, state.frame_time), + 0.4, + (192, 0, 0), + batch, + ), + 285 | 287 => self.draw_light( + interpolate_fix9_scale(npc.prev_x - self.frame.prev_x, npc.x - self.frame.x, state.frame_time), + interpolate_fix9_scale(npc.prev_y - self.frame.prev_y, npc.y - self.frame.y, state.frame_time), + 1.0, + (150, 90, 0), + batch, + ), + 293 => self.draw_light( + interpolate_fix9_scale(npc.prev_x - self.frame.prev_x, npc.x - self.frame.x, state.frame_time), + interpolate_fix9_scale(npc.prev_y - self.frame.prev_y, npc.y - self.frame.y, state.frame_time), + 4.0, + (255, 255, 255), + batch, + ), _ => {} } } @@ -1227,7 +1257,6 @@ impl GameScene { canvas.add(SpriteBatchCommand::DrawRect(rect, rect)); canvas.draw()?; - graphics::set_render_target(ctx, Some(canvas))?; graphics::draw_rect( ctx, @@ -1620,6 +1649,7 @@ impl GameScene { &mut self.stage, &mut self.bullet_manager, &mut self.flash, + &mut self.boss, ), )?; } @@ -1950,6 +1980,7 @@ impl Scene for GameScene { | TextScriptExecutionState::WaitTicks(_, _, _) | TextScriptExecutionState::WaitInput(_, _, _) | TextScriptExecutionState::Msg(_, _, _, _) + | TextScriptExecutionState::FallingIsland(_, _, _, _, _, _) if !state.control_flags.control_enabled() && !state.textscript_vm.flags.cutscene_skip() => { if self.player1.controller.inventory() { @@ -1966,61 +1997,59 @@ impl Scene for GameScene { } } - let mut ticks = 1; - if state.textscript_vm.mode == ScriptMode::Map && state.textscript_vm.flags.cutscene_skip() { - ticks = 4; - } + match state.textscript_vm.mode { + ScriptMode::Map => { + TextScriptVM::run(state, self, ctx)?; - for _ in 0..ticks { - match state.textscript_vm.mode { - ScriptMode::Map => { - TextScriptVM::run(state, self, ctx)?; - - if state.control_flags.tick_world() { - self.tick_world(state)?; + match state.textscript_vm.state { + TextScriptExecutionState::FallingIsland(_, _, _, _, _, _) => (), + _ => { + if state.control_flags.tick_world() { + self.tick_world(state)?; + } } } - ScriptMode::StageSelect => { - self.stage_select.tick(state, (ctx, &self.player1, &self.player2))?; - - TextScriptVM::run(state, self, ctx)?; - } - ScriptMode::Inventory => { - self.inventory_ui.tick(state, (ctx, &mut self.player1, &mut self.inventory_player1))?; - - TextScriptVM::run(state, self, ctx)?; - } } + ScriptMode::StageSelect => { + self.stage_select.tick(state, (ctx, &self.player1, &self.player2))?; - if state.control_flags.credits_running() { - self.skip_counter = 0; - CreditScriptVM::run(state, ctx)?; + TextScriptVM::run(state, self, ctx)?; } + ScriptMode::Inventory => { + self.inventory_ui.tick(state, (ctx, &mut self.player1, &mut self.inventory_player1))?; - match state.fade_state { - FadeState::FadeOut(tick, direction) if tick < 15 => { - state.fade_state = FadeState::FadeOut(tick + 1, direction); - } - FadeState::FadeOut(tick, _) if tick == 15 => { - state.fade_state = FadeState::Hidden; - } - FadeState::FadeIn(tick, direction) if tick > -15 => { - state.fade_state = FadeState::FadeIn(tick - 1, direction); - } - FadeState::FadeIn(tick, _) if tick == -15 => { - state.fade_state = FadeState::Visible; - } - _ => {} + TextScriptVM::run(state, self, ctx)?; } + } - self.flash.tick(state, ())?; + if state.control_flags.credits_running() { + self.skip_counter = 0; + CreditScriptVM::run(state, ctx)?; + } - #[cfg(feature = "scripting-lua")] - state.lua.scene_tick(); - - if state.control_flags.tick_world() { - self.tick = self.tick.wrapping_add(1); + match state.fade_state { + FadeState::FadeOut(tick, direction) if tick < 15 => { + state.fade_state = FadeState::FadeOut(tick + 1, direction); } + FadeState::FadeOut(tick, _) if tick == 15 => { + state.fade_state = FadeState::Hidden; + } + FadeState::FadeIn(tick, direction) if tick > -15 => { + state.fade_state = FadeState::FadeIn(tick - 1, direction); + } + FadeState::FadeIn(tick, _) if tick == -15 => { + state.fade_state = FadeState::Visible; + } + _ => {} + } + + self.flash.tick(state, ())?; + + #[cfg(feature = "scripting-lua")] + state.lua.scene_tick(); + + if state.control_flags.tick_world() { + self.tick = self.tick.wrapping_add(1); } Ok(()) @@ -2105,7 +2134,10 @@ impl Scene for GameScene { self.player1.popup.draw(state, ctx, &self.frame)?; self.player2.popup.draw(state, ctx, &self.frame)?; - if state.settings.shader_effects && self.lighting_mode == LightingMode::Ambient { + if !state.control_flags.credits_running() + && state.settings.shader_effects + && self.lighting_mode == LightingMode::Ambient + { self.draw_light_map(state, ctx)?; } self.flash.draw(state, ctx, &self.frame)?; @@ -2206,6 +2238,7 @@ impl Scene for GameScene { self.credits.draw(state, ctx, &self.frame)?; } + self.falling_island.draw(state, ctx, &self.frame)?; self.draw_text_boxes(state, ctx)?; if self.skip_counter > 0 { diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index 6ca861c..766046b 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -92,6 +92,7 @@ pub enum TextScriptExecutionState { WaitStanding(u16, u32), WaitConfirmation(u16, u32, u16, u8, ConfirmSelection), WaitFade(u16, u32), + FallingIsland(u16, u32, i32, i32, u16, bool), SaveProfile(u16, u32), LoadProfile, Reset, @@ -517,7 +518,31 @@ impl TextScriptVM { } break; } + TextScriptExecutionState::FallingIsland(event, ip, pos_x, mut pos_y, mut tick, mode) => { + if tick == 900 { + state.textscript_vm.state = TextScriptExecutionState::Running(event, ip); + break; + } + tick += 1; + + if mode { + if tick < 350 { + pos_y += 0x33; + } else if tick < 500 { + pos_y += 0x19; + } else if tick < 600 { + pos_y += 0xC; + } else if tick == 750 { + tick = 900; + } + } else { + pos_y += 0x33; + } + + state.textscript_vm.state = TextScriptExecutionState::FallingIsland(event, ip, pos_x, pos_y, tick, mode); + break; + } TextScriptExecutionState::SaveProfile(event, ip) => { state.save_game(game_scene, ctx)?; state.textscript_vm.state = TextScriptExecutionState::Running(event, ip); @@ -1326,6 +1351,7 @@ impl TextScriptVM { &mut game_scene.stage, &mut game_scene.bullet_manager, &mut game_scene.flash, + &mut game_scene.boss, ), )?; } @@ -1561,22 +1587,45 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } + TSCOpCode::CPS => { + state.sound_manager.stop_sfx(58); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SPS => { + state.sound_manager.loop_sfx(58); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CSS => { + state.sound_manager.stop_sfx(40); + state.sound_manager.stop_sfx(41); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SSS => { + let _freq = read_cur_varint(&mut cursor)?; + + // todo change freq + state.sound_manager.loop_sfx(40); + state.sound_manager.loop_sfx(41); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::XX1 => { + let mode = read_cur_varint(&mut cursor)?; + + exec_state = TextScriptExecutionState::FallingIsland(event, cursor.position() as u32, 0x15000, 0x8000, 0, mode != 0); + } // unimplemented opcodes // Zero operands - TSCOpCode::CPS - | TSCOpCode::KE2 - | TSCOpCode::CSS - | TSCOpCode::MLP - | TSCOpCode::SPS - | TSCOpCode::FR2 - | TSCOpCode::STC - | TSCOpCode::HM2 => { + TSCOpCode::KE2 | TSCOpCode::MLP | TSCOpCode::FR2 | TSCOpCode::STC | TSCOpCode::HM2 => { log::warn!("unimplemented opcode: {:?}", op); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // One operand codes - TSCOpCode::UNJ | TSCOpCode::XX1 | TSCOpCode::SSS | TSCOpCode::ACH => { + TSCOpCode::UNJ | TSCOpCode::ACH => { let par_a = read_cur_varint(&mut cursor)?; log::warn!("unimplemented opcode: {:?} {}", op, par_a); diff --git a/src/weapon/spur.rs b/src/weapon/spur.rs index 3d506b0..b6ddf17 100644 --- a/src/weapon/spur.rs +++ b/src/weapon/spur.rs @@ -13,7 +13,7 @@ impl Weapon { bullet_manager: &mut BulletManager, state: &mut SharedGameState, ) { - const BULLETS: [u16; 6] = [44, 45, 46, 47, 48, 49]; + const BULLETS: [u16; 6] = [37, 38, 39, 40, 41, 42]; let mut shoot = false; let btype; @@ -22,7 +22,7 @@ impl Weapon { self.add_xp(if player.equip.has_turbocharge() { 3 } else { 2 }, player, state); self.counter1 += 1; - if (self.counter1 / 2 % 2) != 0 { + if self.counter1 & 2 != 0 { match self.level { WeaponLevel::Level1 => { state.sound_manager.play_sfx(59);