doukutsu-rs/src/npc/utils.rs

343 lines
12 KiB
Rust

///! 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};
use crate::npc::list::NPCList;
use crate::player::Player;
use crate::rng::{RNG, Xoroshiro32PlusPlus};
use crate::shared_game_state::SharedGameState;
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);
}
/// 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 npc_flags = NPCFlag(flags.0);
NPC {
id: 0,
npc_type,
x: 0,
y: 0,
vel_x: 0,
vel_y: 0,
vel_x2: 0,
vel_y2: 0,
target_x: 0,
target_y: 0,
prev_x: 0,
prev_y: 0,
action_num: 0,
anim_num: 0,
flag_num: 0,
event_num: 0,
shock: 0,
exp,
size,
life,
damage,
spritesheet_id,
cond: Condition(0x00),
flags: Flag(0),
direction: if npc_flags.spawn_facing_right() { Direction::Right } else { Direction::Left },
tsc_direction: 0,
npc_flags,
display_bounds,
hit_bounds,
parent_id: 0,
action_counter: 0,
action_counter2: 0,
action_counter3: 0,
anim_counter: 0,
anim_rect: Rect::new(0, 0, 0, 0),
rng: Xoroshiro32PlusPlus::new(0),
}
}
pub fn create_from_data(data: &NPCData, table: &NPCTable) -> NPC {
let mut npc = NPC::create(data.npc_type, table);
npc.id = data.id;
npc.x = data.x as i32 * 16 * 0x200;
npc.y = data.y as i32 * 16 * 0x200;
npc.flag_num = data.flag_num;
npc.event_num = data.event_num;
npc.npc_flags = NPCFlag(data.flags | npc.npc_flags.0);
npc.direction = if npc.npc_flags.spawn_facing_right() { Direction::Right } else { Direction::Left };
npc
}
/// Returns a reference to parent NPC (if present).
pub fn get_parent_ref_mut<'a: 'b, 'b>(&self, npc_list: &'a NPCList) -> Option<&'b mut NPC> {
match self.parent_id {
0 => None,
id if id == self.id => None,
id => npc_list.get_npc(id as usize),
}
}
/// Cycles animation frames in given range and speed.
pub fn animate(&mut self, ticks_between_frames: u16, start_frame: u16, end_frame: u16) {
self.anim_counter += 1;
if self.anim_counter > ticks_between_frames {
self.anim_counter = 0;
self.anim_num += 1;
if self.anim_num > end_frame {
self.anim_num = start_frame;
}
}
}
/// Returns index of player that's closest to the current NPC.
pub fn get_closest_player_idx_mut<'a>(&self, players: &[&'a mut Player; 2]) -> usize {
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;
}
}
player_idx
}
/// Returns a reference to closest player.
pub fn get_closest_player_mut<'a>(&self, players: [&'a mut Player; 2]) -> &'a mut Player {
let idx = self.get_closest_player_idx_mut(&players);
players[idx]
}
/// Returns a reference to closest player.
pub fn get_closest_player_ref<'a, 'b: 'a>(&self, players: &'a [&'a mut Player; 2]) -> &'b &'a mut Player {
let idx = self.get_closest_player_idx_mut(&players);
&players[idx]
}
/// 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.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)
)
}
/// Creates experience drop for this NPC.
pub fn create_xp_drop(&self, state: &SharedGameState, npc_list: &NPCList) {
let mut exp = self.exp;
let mut xp_npc = NPC::create(1, &state.npc_table);
xp_npc.cond.set_alive(true);
xp_npc.direction = Direction::Left;
xp_npc.x = self.x;
xp_npc.y = self.y;
while exp > 0 {
let exp_piece = if exp >= 20 {
exp -= 20;
20
} else if exp >= 5 {
exp -= 5;
5
} else {
exp -= 1;
1
};
xp_npc.exp = exp_piece;
let _ = npc_list.spawn(0x100, xp_npc.clone());
}
}
/// Makes the NPC disappear and turns it into damage value holder.
pub fn vanish(&mut self, state: &SharedGameState) {
let mut npc = NPC::create(3, &state.npc_table);
npc.cond.set_alive(true);
npc.x = self.x;
npc.y = self.y;
*self = npc;
}
}
#[allow(dead_code)]
impl NPCList {
/// Returns true if at least one NPC with specified type is alive.
#[inline]
pub fn is_alive_by_type(&self, npc_type: u16) -> bool {
self.iter_alive().any(|npc| npc.npc_type == npc_type)
}
/// Returns true if at least one NPC with specified event is alive.
#[inline]
pub fn is_alive_by_event(&self, event_num: u16) -> bool {
self.iter_alive().any(|npc| npc.event_num == event_num)
}
/// Called once NPC is killed, creates smoke and drops.
pub fn kill_npc(&self, id: usize, vanish: bool, can_drop_missile: bool, state: &mut SharedGameState) {
if let Some(npc) = self.get_npc(id) {
if let Some(table_entry) = state.npc_table.get_entry(npc.npc_type) {
state.sound_manager.play_sfx(table_entry.death_sound);
}
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); }
_ => {}
};
if npc.exp != 0 {
let rng = npc.rng.range(0..4);
match rng {
0 => {
let mut heart_pick = NPC::create(87, &state.npc_table);
heart_pick.cond.set_alive(true);
heart_pick.direction = Direction::Left;
heart_pick.x = npc.x;
heart_pick.y = npc.y;
heart_pick.exp = if npc.exp > 6 { 6 } else { 2 };
let _ = self.spawn(0x100, heart_pick);
}
1 if can_drop_missile => {
let mut missile_pick = NPC::create(86, &state.npc_table);
missile_pick.cond.set_alive(true);
missile_pick.direction = Direction::Left;
missile_pick.x = npc.x;
missile_pick.y = npc.y;
missile_pick.exp = if npc.exp > 6 { 3 } else { 1 };
let _ = self.spawn(0x100, missile_pick);
}
_ => {
npc.create_xp_drop(state, self);
}
}
}
state.game_flags.set(npc.flag_num as usize, true);
if npc.npc_flags.show_damage() {
// todo show damage
if vanish {
npc.vanish(state);
}
} else {
npc.cond.set_alive(false);
}
}
}
/// Removes NPCs whose event number matches the provided one.
pub fn remove_by_event(&mut self, event_num: u16, state: &mut SharedGameState) {
for npc in self.iter_alive() {
if npc.event_num == event_num {
npc.cond.set_alive(false);
state.game_flags.set(npc.flag_num as usize, true);
}
}
}
/// Removes NPCs (and creates a smoke effect) whose type IDs match the provided one.
pub fn remove_by_type(&mut self, npc_type: u16, state: &mut SharedGameState) {
for npc in self.iter_alive() {
if npc.npc_type == npc_type {
npc.cond.set_alive(false);
state.game_flags.set(npc.flag_num as usize, true);
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),
_ => {}
};
}
}
}
/// 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) {
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) {
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) {
let radius = (radius / 0x200) as i32;
let mut npc = NPC::create(4, &state.npc_table);
npc.cond.set_alive(true);
npc.direction = direction;
for _ in 0..amount {
let off_x = rng.range(-radius..radius) as i32 * 0x200;
let off_y = rng.range(-radius..radius) as i32 * 0x200;
npc.x = x + off_x;
npc.y = y + off_y;
let _ = self.spawn(0x100, npc.clone());
}
state.create_caret(x, y, CaretType::Explosion, Direction::Left);
}
}