1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-12-02 01:18:37 +00:00

make some of the NPCs aware of second player

This commit is contained in:
Alula 2020-11-30 14:31:40 +01:00
parent d6d2df53f7
commit 87b2df2ade
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
15 changed files with 258 additions and 119 deletions

View file

@ -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;
}

View file

@ -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)?;
}

View file

@ -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<u16, RefCell<NPC>>, stage: &mut Stage) -> GameResult {
pub(crate) fn tick_n012_balrog_cutscene(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], map: &BTreeMap<u16, RefCell<NPC>>, 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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}
}
_ => {}

View file

@ -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<u16, RefCell<NPC>>, &mut Stage)> for NPC {
fn tick(&mut self, state: &mut SharedGameState, (player, map, stage): (&mut Player, &BTreeMap<u16, RefCell<NPC>>, &mut Stage)) -> GameResult {
impl GameEntity<([&mut Player; 2], &BTreeMap<u16, RefCell<NPC>>, &mut Stage)> for NPC {
fn tick(&mut self, state: &mut SharedGameState, (players, map, stage): ([&mut Player; 2], &BTreeMap<u16, RefCell<NPC>>, &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<u16, RefCell<NPC>>, &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<u16, RefCell<NPC>>, &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(()),
}?;

28
src/npc/npc_utils.rs Normal file
View file

@ -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]
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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))?;
}
}