From 87b2df2ade91610a6171dbfe32d64eef03106458 Mon Sep 17 00:00:00 2001 From: Alula Date: Mon, 30 Nov 2020 14:31:40 +0100 Subject: [PATCH] make some of the NPCs aware of second player --- src/common.rs | 1 + src/components/hud.rs | 14 +++++- src/npc/balrog.rs | 24 ++++++++-- src/npc/characters.rs | 4 +- src/npc/egg_corridor.rs | 19 ++++++-- src/npc/grasstown.rs | 28 +++++++++--- src/npc/mimiga_village.rs | 10 +++-- src/npc/misc.rs | 40 +++++++++++------ src/npc/mod.rs | 95 ++++++++++++++++++++------------------- src/npc/npc_utils.rs | 28 ++++++++++++ src/npc/quote.rs | 18 ++++---- src/npc/santa.rs | 3 +- src/player/player_hit.rs | 12 ++--- src/scene/game_scene.rs | 35 +++++++++++---- src/text_script.rs | 46 ++++++++++++++----- 15 files changed, 258 insertions(+), 119 deletions(-) create mode 100644 src/npc/npc_utils.rs diff --git a/src/common.rs b/src/common.rs index 69e95c3..e595022 100644 --- a/src/common.rs +++ b/src/common.rs @@ -92,6 +92,7 @@ bitfield! { pub credits_running, set_credits_running: 3; // 0x08 // engine specific flags + pub friendly_fire, set_friendly_fire: 14; pub wind, set_wind: 15; } diff --git a/src/components/hud.rs b/src/components/hud.rs index d1858ff..609bce3 100644 --- a/src/components/hud.rs +++ b/src/components/hud.rs @@ -12,6 +12,7 @@ pub struct HUD { pub alignment: Alignment, pub weapon_x_pos: usize, pub visible: bool, + pub has_player2: bool, ammo: u16, max_ammo: u16, xp: u16, @@ -35,6 +36,7 @@ impl HUD { alignment, weapon_x_pos: 16, visible: false, + has_player2: false, ammo: 0, max_ammo: 0, xp: 0, @@ -114,6 +116,14 @@ impl GameEntity<(&Player, &Inventory)> for HUD { Alignment::Left => (0.0, 0.0, 0.0), Alignment::Right => (state.canvas_size.0 - 112.0, state.canvas_size.0 - 48.0, state.canvas_size.0 - 104.0), }; + let air_offset = if self.has_player2 { + 50.0 * match self.alignment { + Alignment::Left => -1.0, + Alignment::Right => 1.0, + } + } else { + 0.0 + }; if self.max_ammo == 0 { batch.add_rect(bar_offset + weap_x + 48.0, 16.0, @@ -165,7 +175,7 @@ impl GameEntity<(&Player, &Inventory)> for HUD { Rect::new_size(112, 80, 32, 8) }; - batch.add_rect((state.canvas_size.0 / 2.0).floor() - 40.0, + batch.add_rect((state.canvas_size.0 / 2.0).floor() - 40.0 + air_offset, (state.canvas_size.1 / 2.0).floor(), &rect); } @@ -203,7 +213,7 @@ impl GameEntity<(&Player, &Inventory)> for HUD { batch.draw(ctx)?; if self.air_counter > 0 && self.air_counter % 6 < 4 { - draw_number((state.canvas_size.0 / 2.0).floor() + 8.0, + draw_number((state.canvas_size.0 / 2.0).floor() + 8.0 + air_offset, (state.canvas_size.1 / 2.0).floor(), (self.air / 10) as usize, Alignment::Left, state, ctx)?; } diff --git a/src/npc/balrog.rs b/src/npc/balrog.rs index dbda3d1..2e09df5 100644 --- a/src/npc/balrog.rs +++ b/src/npc/balrog.rs @@ -74,7 +74,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n010_balrog_shooting(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n010_balrog_shooting(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -201,11 +203,13 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n012_balrog_cutscene(&mut self, state: &mut SharedGameState, player: &Player, map: &BTreeMap>, stage: &mut Stage) -> GameResult { + pub(crate) fn tick_n012_balrog_cutscene(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], map: &BTreeMap>, stage: &mut Stage) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { if self.direction == Direction::FacingPlayer { + let player = self.get_closest_player_mut(players); + if self.x <= player.x { self.direction = Direction::Right; } else { @@ -233,6 +237,8 @@ impl NPC { 10 | 11 => { if self.action_num == 10 { if self.direction == Direction::FacingPlayer { + let player = self.get_closest_player_mut(players); + if self.x <= player.x { self.direction = Direction::Right; } else { @@ -269,6 +275,8 @@ impl NPC { 20 | 21 => { if self.action_num == 20 { if self.direction == Direction::FacingPlayer { + let player = self.get_closest_player_mut(players); + if self.x <= player.x { self.direction = Direction::Right; } else { @@ -332,6 +340,8 @@ impl NPC { 40 | 41 => { if self.action_num == 40 { if self.direction == Direction::FacingPlayer { + let player = self.get_closest_player_mut(players); + if self.x <= player.x { self.direction = Direction::Right; } else { @@ -354,6 +364,8 @@ impl NPC { 42 | 43 => { if self.action_num == 42 { if self.direction == Direction::FacingPlayer { + let player = self.get_closest_player_mut(players); + if self.x <= player.x { self.direction = Direction::Right; } else { @@ -645,7 +657,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n036_balrog_hover(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n036_balrog_hover(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -799,7 +813,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n068_balrog_running(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + pub(crate) fn tick_n068_balrog_running(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { diff --git a/src/npc/characters.rs b/src/npc/characters.rs index bb03f5c..d517057 100644 --- a/src/npc/characters.rs +++ b/src/npc/characters.rs @@ -7,13 +7,15 @@ use crate::player::Player; use crate::shared_game_state::SharedGameState; impl NPC { - pub(crate) fn tick_n029_cthulhu(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n029_cthulhu(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { if self.action_num == 0 { self.action_num = 1; self.anim_num = 0; self.anim_counter = 0; } + let player = self.get_closest_player_mut(players); + if abs(self.x - player.x) < 48 * 0x200 && self.y - 48 * 0x200 < player.y && self.y + 16 * 0x200 > player.y { self.anim_num = 1; } else { diff --git a/src/npc/egg_corridor.rs b/src/npc/egg_corridor.rs index 82cf541..3828b28 100644 --- a/src/npc/egg_corridor.rs +++ b/src/npc/egg_corridor.rs @@ -95,7 +95,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n005_green_critter(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n005_green_critter(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -105,6 +105,8 @@ impl NPC { self.anim_rect = state.constants.npc.n005_green_critter[self.anim_num as usize + if self.direction == Direction::Right { 3 } else { 0 }]; } + let player = self.get_closest_player_mut(players); + if self.x > player.x { self.direction = Direction::Left; } else { @@ -307,9 +309,10 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n007_basil(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n007_basil(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { + let player = self.get_closest_player_mut(players); self.x = player.x; if self.direction == Direction::Left { @@ -321,6 +324,7 @@ impl NPC { 1 => { self.vel_x -= 0x40; + let player = self.get_closest_player_mut(players); if self.x < (player.x - 192 * 0x200) { self.action_num = 2; } @@ -333,6 +337,7 @@ impl NPC { 2 => { self.vel_x += 0x40; + let player = self.get_closest_player_mut(players); if self.x > (player.x + 192 * 0x200) { self.action_num = 1; } @@ -368,9 +373,11 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n008_blue_beetle(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n008_blue_beetle(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { + let player = self.get_closest_player_mut(players); + if player.x < self.x + 16 * 0x200 && player.x > self.x - 16 * 0x200 { self.npc_flags.set_shootable(true); self.vel_y = -0x100; @@ -401,6 +408,8 @@ impl NPC { } } 1 => { + let player = self.get_closest_player_mut(players); + if self.x > player.x { self.direction = Direction::Left; self.vel_x -= 0x10; @@ -535,7 +544,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n058_basu(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n058_basu(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 => { if player.x < self.x + 16 * 0x200 && player.x > self.x - 16 * 0x200 { diff --git a/src/npc/grasstown.rs b/src/npc/grasstown.rs index 1b13427..d545f58 100644 --- a/src/npc/grasstown.rs +++ b/src/npc/grasstown.rs @@ -10,7 +10,9 @@ use crate::player::Player; use crate::shared_game_state::SharedGameState; impl NPC { - pub(crate) fn tick_n024_power_critter(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n024_power_critter(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -140,7 +142,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n026_bat_flying(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n026_bat_flying(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -215,7 +219,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n028_flying_critter(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n028_flying_critter(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -349,7 +355,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n031_bat_hanging(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n031_bat_hanging(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -779,7 +787,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n104_frog(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n104_frog(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 => { self.action_num = 1; @@ -1068,7 +1078,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n109_malco_powered_on(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n109_malco_powered_on(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -1088,6 +1098,8 @@ impl NPC { self.anim_num = 1; } + let player = self.get_closest_player_mut(players); + if abs(self.x - player.x) < 32 * 0x200 && self.y - 32 * 0x200 < player.y && self.y + 16 * 0x200 > player.y { @@ -1134,7 +1146,9 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n110_puchi(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n110_puchi(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { + let player = self.get_closest_player_mut(players); + match self.action_num { 0 => { self.action_num = 1; diff --git a/src/npc/mimiga_village.rs b/src/npc/mimiga_village.rs index 90eb7ac..6040bae 100644 --- a/src/npc/mimiga_village.rs +++ b/src/npc/mimiga_village.rs @@ -209,7 +209,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n079_mahin(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n079_mahin(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { self.action_num = 1; @@ -224,6 +224,7 @@ impl NPC { self.anim_num = 1; } + let player = self.get_closest_player_mut(players); if (self.x - (32 * 0x200) < player.x) && (self.x + (32 * 0x200) > player.x) && (self.y - (32 * 0x200) < player.y) && (self.y + (16 * 0x200) > player.y) { if self.x > player.x { @@ -258,7 +259,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n080_gravekeeper(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n080_gravekeeper(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 { @@ -270,6 +271,7 @@ impl NPC { self.anim_num = 0; + let player = self.get_closest_player_mut(players); if abs(player.x - self.x) < 128 * 0x200 && self.y - 48 * 0x200 < player.y && self.y + 32 * 0x200 > player.y { self.anim_counter = 0; @@ -295,6 +297,7 @@ impl NPC { } } + let player = self.get_closest_player_mut(players); if abs(player.x - self.x) < 16 * 0x200 { self.hit_bounds.left = 18 * 0x200; self.action_counter = 0; @@ -356,7 +359,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n081_giant_pignon(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n081_giant_pignon(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { let dir_offset = if self.direction == Direction::Left { 0 } else { 6 }; match self.action_num { @@ -441,6 +444,7 @@ impl NPC { } if self.shock > 0 && [1, 2, 4].contains(&self.action_num) { + let player = self.get_closest_player_mut(players); self.vel_x = if self.x < player.x { 0x100 } else { -0x100 }; self.vel_y = -0x200; self.anim_num = 5; diff --git a/src/npc/misc.rs b/src/npc/misc.rs index 3cfbee7..f2ff665 100644 --- a/src/npc/misc.rs +++ b/src/npc/misc.rs @@ -528,9 +528,11 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n046_hv_trigger(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n046_hv_trigger(&mut self, players: [&mut Player; 2]) -> GameResult { self.npc_flags.set_event_when_touched(true); + let player = self.get_closest_player_mut(players); + if self.direction == Direction::Left { if self.x < player.x { self.x += 0x5ff; @@ -643,10 +645,12 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n085_terminal(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n085_terminal(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { self.anim_num = 0; + let player = self.get_closest_player_mut(players); + if abs(player.x - self.x) < 8 * 0x200 && player.y < self.y + 8 * 0x200 && player.y > self.y - 16 * 0x200 { state.sound_manager.play_sfx(43); self.action_num = 1; @@ -667,7 +671,7 @@ impl NPC { Ok(()) } - pub(crate) fn tick_n096_fan_left(&mut self, state: &mut SharedGameState, player: &mut Player) -> GameResult { + pub(crate) fn tick_n096_fan_left(&mut self, state: &mut SharedGameState, mut players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 => { if self.action_num == 0 && self.direction == Direction::Right { @@ -686,19 +690,27 @@ impl NPC { } } - if abs(player.x - self.x) < 480 * 0x200 && abs(player.y - self.y) < 240 * 0x200 - && state.game_rng.range(0..5) == 1 { - let mut particle = NPCMap::create_npc(199, &state.npc_table); - particle.cond.set_alive(true); - particle.direction = Direction::Left; - particle.x = self.x; - particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; - state.new_npcs.push(particle); + { + if abs(players[0].x - self.x) < 480 * 0x200 && abs(players[0].y - self.y) < 240 * 0x200 + && state.game_rng.range(0..5) == 1 { + let mut particle = NPCMap::create_npc(199, &state.npc_table); + particle.cond.set_alive(true); + particle.direction = Direction::Left; + particle.x = self.x; + particle.y = self.y + (state.game_rng.range(-8..8) * 0x200) as isize; + state.new_npcs.push(particle); + } } - if abs(player.y - self.y) < 8 * 0x200 && player.x < self.x && player.x > self.x - 96 * 0x200 { - player.vel_x -= 0x88; - player.cond.set_increase_acceleration(true); + for player in players.iter_mut() { + if !player.cond.alive() || player.cond.hidden() { + continue; + } + + if abs(player.y - self.y) < 8 * 0x200 && player.x < self.x && player.x > self.x - 96 * 0x200 { + player.vel_x -= 0x88; + player.cond.set_increase_acceleration(true); + } } } _ => {} diff --git a/src/npc/mod.rs b/src/npc/mod.rs index 6ac0991..76b99f8 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -11,7 +11,7 @@ use num_traits::abs; use crate::bitfield; use crate::caret::CaretType; -use crate::common::{Condition, fix9_scale, interpolate_fix9_scale, Rect}; +use crate::common::{Condition, interpolate_fix9_scale, Rect}; use crate::common::Direction; use crate::common::Flag; use crate::entity::GameEntity; @@ -37,6 +37,7 @@ pub mod maze; pub mod mimiga_village; pub mod misc; pub mod misery; +pub mod npc_utils; pub mod pickups; pub mod quote; pub mod sand_zone; @@ -163,22 +164,22 @@ impl NPC { } } -impl GameEntity<(&mut Player, &BTreeMap>, &mut Stage)> for NPC { - fn tick(&mut self, state: &mut SharedGameState, (player, map, stage): (&mut Player, &BTreeMap>, &mut Stage)) -> GameResult { +impl GameEntity<([&mut Player; 2], &BTreeMap>, &mut Stage)> for NPC { + fn tick(&mut self, state: &mut SharedGameState, (players, map, stage): ([&mut Player; 2], &BTreeMap>, &mut Stage)) -> GameResult { match self.npc_type { 0 => self.tick_n000_null(), 1 => self.tick_n001_experience(state), 2 => self.tick_n002_behemoth(state), 3 => self.tick_n003_dead_enemy(), 4 => self.tick_n004_smoke(state), - 5 => self.tick_n005_green_critter(state, player), + 5 => self.tick_n005_green_critter(state, players), 6 => self.tick_n006_green_beetle(state), - 7 => self.tick_n007_basil(state, player), - 8 => self.tick_n008_blue_beetle(state, player), + 7 => self.tick_n007_basil(state, players), + 8 => self.tick_n008_blue_beetle(state, players), 9 => self.tick_n009_balrog_falling_in(state), - 10 => self.tick_n010_balrog_shooting(state, player), + 10 => self.tick_n010_balrog_shooting(state, players), 11 => self.tick_n011_balrogs_projectile(state), - 12 => self.tick_n012_balrog_cutscene(state, player, map, stage), + 12 => self.tick_n012_balrog_cutscene(state, players, map, stage), 13 => self.tick_n013_forcefield(state), 14 => self.tick_n014_key(state), 15 => self.tick_n015_chest_closed(state), @@ -190,89 +191,89 @@ impl GameEntity<(&mut Player, &BTreeMap>, &mut Stage)> for NPC 21 => self.tick_n021_chest_open(state), 22 => self.tick_n022_teleporter(state), 23 => self.tick_n023_teleporter_lights(state), - 24 => self.tick_n024_power_critter(state, player), + 24 => self.tick_n024_power_critter(state, players), 25 => self.tick_n025_lift(state), - 26 => self.tick_n026_bat_flying(state, player), + 26 => self.tick_n026_bat_flying(state, players), 27 => self.tick_n027_death_trap(state), - 28 => self.tick_n028_flying_critter(state, player), - 29 => self.tick_n029_cthulhu(state, player), + 28 => self.tick_n028_flying_critter(state, players), + 29 => self.tick_n029_cthulhu(state, players), 30 => self.tick_n030_gunsmith(state), - 31 => self.tick_n031_bat_hanging(state, player), + 31 => self.tick_n031_bat_hanging(state, players), 32 => self.tick_n032_life_capsule(state), 33 => self.tick_n033_balrog_bouncing_projectile(state), 34 => self.tick_n034_bed(state), 35 => self.tick_n035_mannan(state), - 36 => self.tick_n036_balrog_hover(state, player), + 36 => self.tick_n036_balrog_hover(state, players), 37 => self.tick_n037_sign(state), 38 => self.tick_n038_fireplace(state), 39 => self.tick_n039_save_sign(state), - 40 => self.tick_n040_santa(state, player), + 40 => self.tick_n040_santa(state, players), 41 => self.tick_n041_busted_door(state), - 42 => self.tick_n042_sue(state, player, map), + 42 => self.tick_n042_sue(state, players[0], map), 43 => self.tick_n043_chalkboard(state), - 46 => self.tick_n046_hv_trigger(state, player), + 46 => self.tick_n046_hv_trigger(players), 52 => self.tick_n052_sitting_blue_robot(state), 55 => self.tick_n055_kazuma(state), - 58 => self.tick_n058_basu(state, player), - 59 => self.tick_n059_eye_door(state, player), - 60 => self.tick_n060_toroko(state, player), + 58 => self.tick_n058_basu(state, players), + 59 => self.tick_n059_eye_door(state, players[0]), + 60 => self.tick_n060_toroko(state, players[0]), 61 => self.tick_n061_king(state), 62 => self.tick_n062_kazuma_computer(state), 63 => self.tick_n063_toroko_stick(state), - 64 => self.tick_n064_first_cave_critter(state, player), - 65 => self.tick_n065_first_cave_bat(state, player), + 64 => self.tick_n064_first_cave_critter(state, players[0]), + 65 => self.tick_n065_first_cave_bat(state, players[0]), 66 => self.tick_n066_misery_bubble(state, map), 67 => self.tick_n067_misery_floating(state), - 68 => self.tick_n068_balrog_running(state, player), + 68 => self.tick_n068_balrog_running(state, players), 69 => self.tick_n069_pignon(state), 70 => self.tick_n070_sparkle(state), 71 => self.tick_n071_chinfish(state), - 72 => self.tick_n072_sprinkler(state, player), + 72 => self.tick_n072_sprinkler(state, players[0]), 73 => self.tick_n073_water_droplet(state, stage), 74 => self.tick_n074_jack(state), - 75 => self.tick_n075_kanpachi(state, player), + 75 => self.tick_n075_kanpachi(state, players[0]), 76 => self.tick_n076_flowers(), 77 => self.tick_n077_yamashita(state), 78 => self.tick_n078_pot(state), - 79 => self.tick_n079_mahin(state, player), - 80 => self.tick_n080_gravekeeper(state, player), - 81 => self.tick_n081_giant_pignon(state, player), + 79 => self.tick_n079_mahin(state, players), + 80 => self.tick_n080_gravekeeper(state, players), + 81 => self.tick_n081_giant_pignon(state, players), 82 => self.tick_n082_misery_standing(state), 83 => self.tick_n083_igor_cutscene(state), 84 => self.tick_n084_basu_projectile(state), - 85 => self.tick_n085_terminal(state, player), + 85 => self.tick_n085_terminal(state, players), 86 => self.tick_n086_missile_pickup(state), 87 => self.tick_n087_heart_pickup(state), - 88 => self.tick_n088_igor_boss(state, player), - 89 => self.tick_n089_igor_dead(state, player), + 88 => self.tick_n088_igor_boss(state, players[0]), + 89 => self.tick_n089_igor_dead(state, players[0]), 91 => self.tick_n091_mimiga_cage(state), 92 => self.tick_n092_sue_at_pc(state), - 93 => self.tick_n093_chaco(state, player), - 94 => self.tick_n094_kulala(state, player), + 93 => self.tick_n093_chaco(state, players[0]), + 94 => self.tick_n094_kulala(state, players[0]), 95 => self.tick_n095_jelly(state), - 96 => self.tick_n096_fan_left(state, player), - 97 => self.tick_n097_fan_up(state, player), - 98 => self.tick_n098_fan_right(state, player), - 99 => self.tick_n099_fan_down(state, player), + 96 => self.tick_n096_fan_left(state, players), + 97 => self.tick_n097_fan_up(state, players[0]), + 98 => self.tick_n098_fan_right(state, players[0]), + 99 => self.tick_n099_fan_down(state, players[0]), 100 => self.tick_n100_grate(state), 101 => self.tick_n101_malco_screen(state), 102 => self.tick_n102_malco_computer_wave(state), 103 => self.tick_n103_mannan_projectile(state), - 104 => self.tick_n104_frog(state, player), + 104 => self.tick_n104_frog(state, players), 105 => self.tick_n105_hey_bubble_low(state), 106 => self.tick_n106_hey_bubble_high(state), 107 => self.tick_n107_malco_broken(state), 108 => self.tick_n108_balfrog_projectile(state), - 109 => self.tick_n109_malco_powered_on(state, player), - 110 => self.tick_n110_puchi(state, player), - 111 => self.tick_n111_quote_teleport_out(state, player), - 112 => self.tick_n112_quote_teleport_in(state, player), - 114 => self.tick_n114_press(state, player), + 109 => self.tick_n109_malco_powered_on(state, players), + 110 => self.tick_n110_puchi(state, players), + 111 => self.tick_n111_quote_teleport_out(state, players), + 112 => self.tick_n112_quote_teleport_in(state, players), + 114 => self.tick_n114_press(state, players[0]), 129 => self.tick_n129_fireball_snake_trail(state), - 149 => self.tick_n149_horizontal_moving_block(state, player), - 150 => self.tick_n150_quote(state, player), + 149 => self.tick_n149_horizontal_moving_block(state, players[0]), + 150 => self.tick_n150_quote(state, players[0]), 154 => self.tick_n154_gaudi_dead(state), - 157 => self.tick_n157_vertical_moving_block(state, player), + 157 => self.tick_n157_vertical_moving_block(state, players[0]), 192 => self.tick_n192_scooter(state), 193 => self.tick_n193_broken_scooter(state), 194 => self.tick_n194_broken_blue_robot(state), @@ -281,7 +282,7 @@ impl GameEntity<(&mut Player, &BTreeMap>, &mut Stage)> for NPC 298 => self.tick_n298_intro_doctor(state), 299 => self.tick_n299_intro_balrog_misery(state), 300 => self.tick_n300_intro_demon_crown(state), - 361 => self.tick_n361_gaudi_dashing(state, player), + 361 => self.tick_n361_gaudi_dashing(state, players[0]), _ => Ok(()), }?; diff --git a/src/npc/npc_utils.rs b/src/npc/npc_utils.rs new file mode 100644 index 0000000..9aea4a0 --- /dev/null +++ b/src/npc/npc_utils.rs @@ -0,0 +1,28 @@ +use num_traits::abs; + +use crate::npc::NPC; +use crate::player::Player; + +impl NPC { + pub fn get_closest_player_mut<'a>(&self, players: [&'a mut Player; 2]) -> &'a mut Player { + let mut max_dist = f64::MAX; + let mut player_idx = 0; + + for (idx, player) in players.iter().enumerate() { + if !player.cond.alive() || player.cond.hidden() { + continue; + } + + let dist_x = abs(self.x - player.x) as f64; + let dist_y = abs(self.y - player.y) as f64; + let dist = (dist_x * dist_x + dist_y * dist_y).sqrt(); + + if dist < max_dist { + max_dist = dist; + player_idx = idx; + } + } + + players[player_idx] + } +} diff --git a/src/npc/quote.rs b/src/npc/quote.rs index b85ac45..2c46443 100644 --- a/src/npc/quote.rs +++ b/src/npc/quote.rs @@ -5,7 +5,7 @@ use crate::player::Player; use crate::shared_game_state::SharedGameState; impl NPC { - pub fn tick_n111_quote_teleport_out(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub fn tick_n111_quote_teleport_out(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { self.action_num = 1; @@ -58,10 +58,9 @@ impl NPC { let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; self.anim_rect = state.constants.npc.n111_quote_teleport_out[self.anim_num as usize + dir_offset]; - if player.equip.has_mimiga_mask() { - self.anim_rect.top += 32; - self.anim_rect.bottom += 32; - } + let offset = players[state.textscript_vm.executor_player.index()].get_texture_offset(); + self.anim_rect.top += offset; + self.anim_rect.bottom += offset; if self.action_num == 4 { self.anim_rect.bottom = self.anim_rect.top + self.action_counter / 4; @@ -74,7 +73,7 @@ impl NPC { Ok(()) } - pub fn tick_n112_quote_teleport_in(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub fn tick_n112_quote_teleport_in(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 => { self.action_num = 1; @@ -116,10 +115,9 @@ impl NPC { let dir_offset = if self.direction == Direction::Left { 0 } else { 2 }; self.anim_rect = state.constants.npc.n111_quote_teleport_out[self.anim_num as usize + dir_offset]; - if player.equip.has_mimiga_mask() { - self.anim_rect.top += 32; - self.anim_rect.bottom += 32; - } + let offset = players[state.textscript_vm.executor_player.index()].get_texture_offset(); + self.anim_rect.top += offset; + self.anim_rect.bottom += offset; if self.action_num == 1 { self.anim_rect.bottom = self.anim_rect.top + self.action_counter / 4; diff --git a/src/npc/santa.rs b/src/npc/santa.rs index 3968db8..4a795df 100644 --- a/src/npc/santa.rs +++ b/src/npc/santa.rs @@ -7,7 +7,7 @@ use num_traits::abs; use crate::common::Direction; impl NPC { - pub(crate) fn tick_n040_santa(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult { + pub(crate) fn tick_n040_santa(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { match self.action_num { 0 | 1 =>{ if self.action_num == 0 { @@ -22,6 +22,7 @@ impl NPC { self.anim_num = 1; } + let player = self.get_closest_player_mut(players); if abs(self.x - player.x) < 32 * 0x200 && self.y - 32 * 0x200 < player.y && self.y + 16 * 0x200 > player.y { self.direction = if self.x > player.x { diff --git a/src/player/player_hit.rs b/src/player/player_hit.rs index 01f95c2..6569eec 100644 --- a/src/player/player_hit.rs +++ b/src/player/player_hit.rs @@ -7,7 +7,7 @@ use crate::common::{Condition, Direction, Flag, Rect}; use crate::inventory::{AddExperienceResult, Inventory}; use crate::npc::{NPC, NPCMap}; use crate::physics::PhysicalEntity; -use crate::player::{ControlMode, Player}; +use crate::player::{ControlMode, Player, TargetPlayer}; use crate::shared_game_state::SharedGameState; impl PhysicalEntity for Player { @@ -236,7 +236,7 @@ impl Player { flags } - fn tick_npc_collision(&mut self, state: &mut SharedGameState, npc: &mut NPC, inventory: &mut Inventory) { + fn tick_npc_collision(&mut self, id: TargetPlayer, state: &mut SharedGameState, npc: &mut NPC, inventory: &mut Inventory) { let flags: Flag; if npc.npc_flags.solid_soft() { @@ -289,6 +289,7 @@ impl Player { if npc.npc_flags.interactable() && !state.control_flags.interactions_disabled() && flags.0 != 0 && self.cond.interacted() { state.control_flags.set_tick_world(true); state.control_flags.set_interactions_disabled(true); + state.textscript_vm.executor_player = id; state.textscript_vm.start_script(npc.event_num); self.cond.set_interacted(false); self.vel_x = 0; @@ -298,6 +299,7 @@ impl Player { if npc.npc_flags.event_when_touched() && !state.control_flags.interactions_disabled() && flags.0 != 0 { state.control_flags.set_tick_world(true); state.control_flags.set_interactions_disabled(true); + state.textscript_vm.executor_player = id; state.textscript_vm.start_script(npc.event_num); } @@ -315,17 +317,17 @@ impl Player { } } - pub fn tick_npc_collisions(&mut self, state: &mut SharedGameState, npc_map: &mut NPCMap, inventory: &mut Inventory) { + pub fn tick_npc_collisions(&mut self, id: TargetPlayer, state: &mut SharedGameState, npc_map: &mut NPCMap, inventory: &mut Inventory) { for npc_cell in npc_map.npcs.values() { let mut npc = npc_cell.borrow_mut(); if !npc.cond.alive() { continue; } - self.tick_npc_collision(state, npc.borrow_mut(), inventory); + self.tick_npc_collision(id, state, npc.borrow_mut(), inventory); } for boss_npc in npc_map.boss_map.parts.iter_mut() { if boss_npc.cond.alive() { - self.tick_npc_collision(state, boss_npc, inventory); + self.tick_npc_collision(id, state, boss_npc, inventory); } } diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 00a9872..09c614d 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -18,7 +18,7 @@ use crate::frame::{Frame, UpdateTarget}; use crate::inventory::{Inventory, TakeExperienceResult}; use crate::npc::{NPC, NPCMap}; use crate::physics::PhysicalEntity; -use crate::player::{Player, PlayerAppearance}; +use crate::player::{Player, PlayerAppearance, TargetPlayer}; use crate::rng::RNG; use crate::scene::Scene; use crate::scene::title_scene::TitleScene; @@ -962,6 +962,9 @@ impl GameScene { } fn tick_world(&mut self, state: &mut SharedGameState) -> GameResult { + self.hud_player1.has_player2 = !self.player2.cond.hidden(); + self.hud_player2.has_player2 = self.hud_player1.has_player2; + self.player1.current_weapon = { if let Some(weapon) = self.inventory_player1.get_current_weapon_mut() { weapon.wtype as u8 @@ -969,6 +972,13 @@ impl GameScene { 0 } }; + self.player2.current_weapon = { + if let Some(weapon) = self.inventory_player2.get_current_weapon_mut() { + weapon.wtype as u8 + } else { + 0 + } + }; self.player1.tick(state, ())?; self.player2.tick(state, ())?; @@ -996,11 +1006,13 @@ impl GameScene { self.player2.damage = 0; } - for npc_cell in self.npc_map.npcs.values() { - let mut npc = npc_cell.borrow_mut(); + { + for npc_cell in self.npc_map.npcs.values() { + let mut npc = npc_cell.borrow_mut(); - if npc.cond.alive() { - npc.tick(state, (&mut self.player1, &self.npc_map.npcs, &mut self.stage))?; + if npc.cond.alive() { + npc.tick(state, ([&mut self.player1, &mut self.player2], &self.npc_map.npcs, &mut self.stage))?; + } } } self.npc_map.boss_map.tick(state, (&mut self.player1, &self.npc_map.npcs, &mut self.stage))?; @@ -1008,10 +1020,10 @@ impl GameScene { self.npc_map.garbage_collect(); self.player1.tick_map_collisions(state, &mut self.stage); - self.player1.tick_npc_collisions(state, &mut self.npc_map, &mut self.inventory_player1); + self.player1.tick_npc_collisions(TargetPlayer::Player1, state, &mut self.npc_map, &mut self.inventory_player1); self.player2.tick_map_collisions(state, &mut self.stage); - self.player2.tick_npc_collisions(state, &mut self.npc_map, &mut self.inventory_player2); + self.player2.tick_npc_collisions(TargetPlayer::Player2, state, &mut self.npc_map, &mut self.inventory_player2); for npc_cell in self.npc_map.npcs.values() { let mut npc = npc_cell.borrow_mut(); @@ -1036,8 +1048,13 @@ impl GameScene { match self.frame.update_target { UpdateTarget::Player => { - self.frame.target_x = self.player1.target_x; - self.frame.target_y = self.player1.target_y; + if !self.player2.cond.hidden() { + self.frame.target_x = (self.player1.target_x + self.player2.target_x) / 2; + self.frame.target_y = (self.player1.target_y + self.player2.target_y) / 2; + } else { + self.frame.target_x = self.player1.target_x; + self.frame.target_y = self.player1.target_y; + } } UpdateTarget::NPC(npc_id) => { if let Some(npc_cell) = self.npc_map.npcs.get(&npc_id) { diff --git a/src/text_script.rs b/src/text_script.rs index 6d5bbdf..347290d 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -366,7 +366,7 @@ pub struct TextScriptVM { pub flags: TextScriptFlags, pub mode: ScriptMode, /// The player who triggered the event. - pub trigger_player: TargetPlayer, + pub executor_player: TargetPlayer, /// Toggle for non-strict TSC parsing because English versions of CS+ (both AG and Nicalis release) /// modified the events carelessly and since original Pixel's engine hasn't enforced constraints /// while parsing no one noticed them. @@ -451,7 +451,7 @@ impl TextScriptVM { stack: Vec::with_capacity(6), flags: TextScriptFlags(0), mode: ScriptMode::Map, - trigger_player: TargetPlayer::Player1, + executor_player: TargetPlayer::Player1, strict_mode: false, suspend: true, face: 0, @@ -805,11 +805,13 @@ impl TextScriptVM { } OpCode::SMC => { game_scene.player1.cond.set_hidden(false); + game_scene.player2.cond.set_hidden(false); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::HMC => { game_scene.player1.cond.set_hidden(true); + game_scene.player2.cond.set_hidden(true); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1063,23 +1065,29 @@ impl TextScriptVM { let pos_x = read_cur_varint(&mut cursor)? as isize * 16 * 0x200; let pos_y = read_cur_varint(&mut cursor)? as isize * 16 * 0x200; - game_scene.player1.vel_x = 0; - game_scene.player1.vel_y = 0; - game_scene.player1.x = pos_x; - game_scene.player1.y = pos_y; + for player in [&mut game_scene.player1, &mut game_scene.player2].iter_mut() { + player.vel_x = 0; + player.vel_y = 0; + player.x = pos_x; + player.y = pos_y; + } exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::S2MV => { let param = read_cur_varint(&mut cursor)? as usize; + let (executor, partner) = match state.textscript_vm.executor_player { + TargetPlayer::Player1 => (&game_scene.player1, &mut game_scene.player2), + TargetPlayer::Player2 => (&game_scene.player2, &mut game_scene.player1), + }; + match param { 0 | 1 => { - game_scene.player2.vel_x = 0; - game_scene.player2.vel_y = 0; - game_scene.player2.x = game_scene.player1.x - + if param == 0 { -16 * 0x200 } else { 16 * 0x200 }; - game_scene.player2.y = game_scene.player1.y; + partner.vel_x = 0; + partner.vel_y = 0; + partner.x = executor.x + if param == 0 { -16 * 0x200 } else { 16 * 0x200 }; + partner.y = executor.y; } _ => { log::warn!("stub: <2MV unknown param"); @@ -1368,6 +1376,7 @@ impl TextScriptVM { let life = read_cur_varint(&mut cursor)? as u16; game_scene.player1.life = clamp(game_scene.player1.life + life, 0, game_scene.player1.max_life); + game_scene.player2.life = clamp(game_scene.player2.life + life, 0, game_scene.player2.max_life); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1378,6 +1387,10 @@ impl TextScriptVM { game_scene.inventory_player1.add_item(item_id); } + if !game_scene.inventory_player2.has_item(item_id) { + game_scene.inventory_player2.add_item(item_id); + } + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::IpN => { @@ -1388,12 +1401,17 @@ impl TextScriptVM { game_scene.inventory_player1.add_item(item_id); } + if game_scene.inventory_player2.has_item_amount(item_id, Ordering::Less, amount) { + game_scene.inventory_player2.add_item(item_id); + } + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::ITm => { let item_id = read_cur_varint(&mut cursor)? as u16; game_scene.inventory_player1.consume_item(item_id); + game_scene.inventory_player2.consume_item(item_id); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1404,6 +1422,7 @@ impl TextScriptVM { if let Some(wtype) = weapon_type { game_scene.inventory_player1.add_weapon(wtype, max_ammo); + game_scene.inventory_player2.add_weapon(wtype, max_ammo); } exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); @@ -1414,17 +1433,20 @@ impl TextScriptVM { if let Some(wtype) = weapon_type { game_scene.inventory_player1.remove_weapon(wtype); + game_scene.inventory_player2.remove_weapon(wtype); } exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::AEp => { game_scene.inventory_player1.refill_all_ammo(); + game_scene.inventory_player2.refill_all_ammo(); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::ZAM => { game_scene.inventory_player1.reset_all_weapon_xp(); + game_scene.inventory_player2.reset_all_weapon_xp(); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1512,7 +1534,7 @@ impl TextScriptVM { if tick_npc != 0 { if let Some(npc) = game_scene.npc_map.npcs.get(&tick_npc) { - npc.borrow_mut().tick(state, (&mut game_scene.player1, &game_scene.npc_map.npcs, &mut game_scene.stage))?; + npc.borrow_mut().tick(state, ([&mut game_scene.player1, &mut game_scene.player2], &game_scene.npc_map.npcs, &mut game_scene.stage))?; } }