1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-09-20 11:13:41 +00:00

Compare commits

..

5 commits

Author SHA1 Message Date
Alula 41307ccbc7
Merge branch 'master' of github.com:alula/doukutsu-rs 2021-01-16 14:52:13 +01:00
Alula 49d990bc38
android sdk changes 2021-01-16 14:52:03 +01:00
Alula 8e810bf026
add spur and other stuff 2021-01-16 14:51:52 +01:00
Alula 2563c09b69
fix some rng bugs 2021-01-16 14:51:12 +01:00
Alula 9f70bf6fd0
new npcs 2021-01-16 14:50:46 +01:00
15 changed files with 646 additions and 115 deletions

View file

@ -8,7 +8,7 @@ version = "0.1.0"
crate-type = ["lib", "cdylib"] crate-type = ["lib", "cdylib"]
[package.metadata.android] [package.metadata.android]
android_version = 28 android_version = 29
target_sdk_version = 28 target_sdk_version = 28
min_sdk_version = 26 min_sdk_version = 26
build_targets = ["aarch64-linux-android"] build_targets = ["aarch64-linux-android"]
@ -18,7 +18,6 @@ opengles_version = [3, 1]
fullscreen = true fullscreen = true
orientation = "sensorLandscape" orientation = "sensorLandscape"
permission = [ permission = [
{name = "android.permission.MANAGE_EXTERNAL_STORAGE"},
{name = "android.permission.READ_EXTERNAL_STORAGE"}, {name = "android.permission.READ_EXTERNAL_STORAGE"},
{name = "android.permission.WRITE_EXTERNAL_STORAGE"} {name = "android.permission.WRITE_EXTERNAL_STORAGE"}
] ]

View file

@ -13,13 +13,15 @@ use crate::stage::Stage;
pub struct BulletManager { pub struct BulletManager {
pub bullets: Vec<Bullet>, pub bullets: Vec<Bullet>,
pub new_bullets: Vec<Bullet>,
} }
impl BulletManager { impl BulletManager {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> BulletManager { pub fn new() -> BulletManager {
BulletManager { BulletManager {
bullets: Vec::with_capacity(32), bullets: Vec::with_capacity(64),
new_bullets: Vec::with_capacity(8),
} }
} }
@ -27,17 +29,29 @@ impl BulletManager {
self.bullets.push(Bullet::new(x, y, btype, owner, direction, constants)); self.bullets.push(Bullet::new(x, y, btype, owner, direction, constants));
} }
pub fn push_bullet(&mut self, bullet: Bullet) {
self.bullets.push(bullet);
}
pub fn tick_bullets(&mut self, state: &mut SharedGameState, players: [&dyn PhysicalEntity; 2], npc_list: &NPCList, stage: &mut Stage) { pub fn tick_bullets(&mut self, state: &mut SharedGameState, players: [&dyn PhysicalEntity; 2], npc_list: &NPCList, stage: &mut Stage) {
for bullet in self.bullets.iter_mut() { let mut i = 0;
while i < self.bullets.len() {
{
let bullet = unsafe { self.bullets.get_unchecked_mut(i) };
i += 1;
if bullet.life < 1 { if bullet.life < 1 {
bullet.cond.set_alive(false); bullet.cond.set_alive(false);
continue; continue;
} }
bullet.tick(state, players, npc_list); bullet.tick(state, players, npc_list, &mut self.new_bullets);
bullet.tick_map_collisions(state, npc_list, stage); bullet.tick_map_collisions(state, npc_list, stage);
} }
self.bullets.append(&mut self.new_bullets);
}
self.bullets.retain(|b| !b.is_dead()); self.bullets.retain(|b| !b.is_dead());
} }
@ -49,7 +63,7 @@ impl BulletManager {
self.bullets.iter().filter(|b| (b.btype.saturating_sub(2) / 3) == type_idx).count() self.bullets.iter().filter(|b| (b.btype.saturating_sub(2) / 3) == type_idx).count()
} }
pub fn count_bullets_multi(&self, btypes: [u16; 3], player_id: TargetPlayer) -> usize { pub fn count_bullets_multi(&self, btypes: &[u16], player_id: TargetPlayer) -> usize {
self.bullets.iter().filter(|b| b.owner == player_id && btypes.contains(&b.btype)).count() self.bullets.iter().filter(|b| b.owner == player_id && btypes.contains(&b.btype)).count()
} }
} }
@ -168,13 +182,83 @@ impl Bullet {
self.y += self.vel_y; self.y += self.vel_y;
} }
self.anim_num = (self.anim_num + 1) % 3; self.anim_num = (self.anim_num + 1) % 4;
let dir_offset = if self.direction == Direction::Left { 0 } else { 4 }; let dir_offset = if self.direction == Direction::Left { 0 } else { 4 };
self.anim_rect = state.constants.weapon.bullet_rects.b001_snake_l1[self.anim_num as usize + dir_offset]; self.anim_rect = state.constants.weapon.bullet_rects.b001_snake_l1[self.anim_num as usize + dir_offset];
} }
fn tick_snake_2(&mut self, state: &mut SharedGameState, npc_list: &NPCList) {
self.action_counter += 1;
if self.action_counter > self.lifetime {
self.cond.set_alive(false);
state.create_caret(self.x, self.y, CaretType::Shoot, Direction::Left);
return;
}
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = state.game_rng.range(0..2) as u16;
match self.direction {
Direction::Left => {
self.vel_x = -0x200;
self.vel_y = if self.target_x & 1 == 0 { -0x400 } else { 0x400 };
}
Direction::Up => {
self.vel_y = -0x200;
self.vel_x = if self.target_x & 1 == 0 { -0x400 } else { 0x400 };
}
Direction::Right => {
self.vel_x = 0x200;
self.vel_y = if self.target_x & 1 == 0 { -0x400 } else { 0x400 };
}
Direction::Bottom => {
self.vel_y = 0x200;
self.vel_x = if self.target_x & 1 == 0 { -0x400 } else { 0x400 };
}
Direction::FacingPlayer => unreachable!(),
};
} else {
match self.direction {
Direction::Left => self.vel_x += -0x80,
Direction::Up => self.vel_y += -0x80,
Direction::Right => self.vel_x += 0x80,
Direction::Bottom => self.vel_y += 0x80,
Direction::FacingPlayer => unreachable!(),
}
if self.action_counter % 5 == 2 {
match self.direction {
Direction::Left | Direction::Right => {
self.vel_y = if self.vel_y < 0 { 0x400 } else { -0x400 };
}
Direction::Up | Direction::Bottom => {
self.vel_x = if self.vel_x < 0 { 0x400 } else { -0x400 };
}
Direction::FacingPlayer => unreachable!(),
}
}
self.x += self.vel_x;
self.y += self.vel_y;
}
self.anim_num = (self.anim_num + 1) % 3;
self.anim_rect = state.constants.weapon.bullet_rects.b002_003_snake_l2_3[self.anim_num as usize];
let mut npc = NPC::create(129, &state.npc_table);
npc.cond.set_alive(true);
npc.x = self.x;
npc.y = self.y;
npc.vel_y = -0x200;
npc.action_counter2 = if self.btype == 3 { self.anim_num + 3 } else { self.anim_num };
let _ = npc_list.spawn(0x100, npc);
}
fn tick_polar_star(&mut self, state: &mut SharedGameState) { fn tick_polar_star(&mut self, state: &mut SharedGameState) {
self.action_counter += 1; self.action_counter += 1;
if self.action_counter > self.lifetime { if self.action_counter > self.lifetime {
@ -374,7 +458,134 @@ impl Bullet {
} }
} }
pub fn tick(&mut self, state: &mut SharedGameState, players: [&dyn PhysicalEntity; 2], npc_list: &NPCList) { fn tick_spur(&mut self, state: &mut SharedGameState, new_bullets: &mut Vec<Bullet>) {
self.action_counter += 1;
if self.action_counter > self.lifetime {
self.cond.set_alive(false);
state.create_caret(self.x, self.y, CaretType::Shoot, Direction::Left);
return;
}
if self.action_num == 0 {
self.action_num = 1;
match self.direction {
Direction::Left => self.vel_x = -0x1000,
Direction::Up => self.vel_y = -0x1000,
Direction::Right => self.vel_x = 0x1000,
Direction::Bottom => self.vel_y = 0x1000,
Direction::FacingPlayer => unreachable!(),
}
match self.btype {
37 => {
match self.direction {
Direction::Left | Direction::Right => self.enemy_hit_height = 0x400,
Direction::Up | Direction::Bottom => self.enemy_hit_width = 0x400,
Direction::FacingPlayer => unreachable!(),
}
}
38 => {
match self.direction {
Direction::Left | Direction::Right => {
self.enemy_hit_height = 0x800;
}
Direction::Up | Direction::Bottom => {
self.enemy_hit_width = 0x800;
}
Direction::FacingPlayer => unreachable!(),
}
}
39 => {
// level 3 uses default values
}
_ => { unreachable!() }
}
} else {
self.x += self.vel_x;
self.y += self.vel_y;
}
match self.btype {
37 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_num = 1;
self.anim_rect = state.constants.weapon.bullet_rects.b037_spur_l1[1];
} else {
self.anim_num = 0;
self.anim_rect = state.constants.weapon.bullet_rects.b037_spur_l1[0];
}
new_bullets.push(Bullet::new(self.x, self.y, 40, self.owner, self.direction, &state.constants));
}
38 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_num = 1;
self.anim_rect = state.constants.weapon.bullet_rects.b038_spur_l2[1];
} else {
self.anim_num = 0;
self.anim_rect = state.constants.weapon.bullet_rects.b038_spur_l2[0];
}
new_bullets.push(Bullet::new(self.x, self.y, 41, self.owner, self.direction, &state.constants));
}
39 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_num = 1;
self.anim_rect = state.constants.weapon.bullet_rects.b039_spur_l3[1];
} else {
self.anim_num = 0;
self.anim_rect = state.constants.weapon.bullet_rects.b039_spur_l3[0];
}
new_bullets.push(Bullet::new(self.x, self.y, 42, self.owner, self.direction, &state.constants));
}
_ => { unreachable!() }
}
}
fn tick_spur_trail(&mut self, state: &mut SharedGameState) {
self.action_counter += 1;
if self.action_counter > 20 {
self.anim_num = self.action_counter.wrapping_sub(20);
if self.anim_num > 2 {
self.cond.set_alive(false);
return;
}
}
if self.damage != 0 && self.life != 100 {
self.damage = 0;
}
match self.btype {
40 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_rect = state.constants.weapon.bullet_rects.b040_spur_trail_l1[self.anim_num as usize + 3];
} else {
self.anim_rect = state.constants.weapon.bullet_rects.b040_spur_trail_l1[self.anim_num as usize];
}
}
41 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_rect = state.constants.weapon.bullet_rects.b041_spur_trail_l2[self.anim_num as usize + 3];
} else {
self.anim_rect = state.constants.weapon.bullet_rects.b041_spur_trail_l2[self.anim_num as usize];
}
}
42 => {
if self.direction == Direction::Up || self.direction == Direction::Bottom {
self.anim_rect = state.constants.weapon.bullet_rects.b042_spur_trail_l3[self.anim_num as usize + 3];
} else {
self.anim_rect = state.constants.weapon.bullet_rects.b042_spur_trail_l3[self.anim_num as usize];
}
}
_ => { unreachable!() }
}
}
pub fn tick(&mut self, state: &mut SharedGameState, players: [&dyn PhysicalEntity; 2], npc_list: &NPCList, new_bullets: &mut Vec<Bullet>) {
if self.lifetime == 0 { if self.lifetime == 0 {
self.cond.set_alive(false); self.cond.set_alive(false);
return; return;
@ -382,8 +593,11 @@ impl Bullet {
match self.btype { match self.btype {
1 => self.tick_snake_1(state), 1 => self.tick_snake_1(state),
2 | 3 => self.tick_snake_2(state, npc_list),
4 | 5 | 6 => self.tick_polar_star(state), 4 | 5 | 6 => self.tick_polar_star(state),
7 | 8 | 9 => self.tick_fireball(state, players, npc_list), 7 | 8 | 9 => self.tick_fireball(state, players, npc_list),
37 | 38 | 39 => self.tick_spur(state, new_bullets),
40 | 41 | 42 => self.tick_spur_trail(state),
_ => self.cond.set_alive(false), _ => self.cond.set_alive(false),
} }
} }

View file

@ -3,6 +3,9 @@ use std::cmp::Ordering;
use crate::engine_constants::EngineConstants; use crate::engine_constants::EngineConstants;
use crate::shared_game_state::SharedGameState; use crate::shared_game_state::SharedGameState;
use crate::weapon::{Weapon, WeaponLevel, WeaponType}; use crate::weapon::{Weapon, WeaponLevel, WeaponType};
use crate::player::Player;
use crate::caret::CaretType;
use crate::common::Direction;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
/// (id, amount) /// (id, amount)
@ -16,13 +19,6 @@ pub struct Inventory {
weapons: Vec<Weapon>, weapons: Vec<Weapon>,
} }
#[derive(Clone, Copy)]
pub enum AddExperienceResult {
None,
LevelUp,
AddStar,
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum TakeExperienceResult { pub enum TakeExperienceResult {
None, None,
@ -102,6 +98,18 @@ impl Inventory {
self.weapons.iter_mut().find(|w| w.wtype == weapon_id).unwrap() self.weapons.iter_mut().find(|w| w.wtype == weapon_id).unwrap()
} }
pub fn trade_weapon(&mut self, old: Option<WeaponType>, new: WeaponType, max_ammo: u16) {
if let Some(wtype) = old {
if let Some(weapon) = self.get_weapon_by_type_mut(wtype) {
*weapon = Weapon::new(new, WeaponLevel::Level1, 0, max_ammo, max_ammo);
} else {
self.add_weapon(new, max_ammo);
}
} else {
self.add_weapon(new, max_ammo);
}
}
pub fn remove_weapon(&mut self, wtype: WeaponType) { pub fn remove_weapon(&mut self, wtype: WeaponType) {
self.weapons.retain(|weapon| weapon.wtype != wtype); self.weapons.retain(|weapon| weapon.wtype != wtype);
} }
@ -151,6 +159,13 @@ impl Inventory {
} }
} }
pub fn reset_current_weapon_xp(&mut self) {
if let Some(weapon) = self.get_current_weapon_mut() {
weapon.level = WeaponLevel::Level1;
weapon.experience = 0;
}
}
pub fn take_xp(&mut self, exp: u16, state: &mut SharedGameState) -> TakeExperienceResult { pub fn take_xp(&mut self, exp: u16, state: &mut SharedGameState) -> TakeExperienceResult {
let mut result = TakeExperienceResult::None; let mut result = TakeExperienceResult::None;
@ -182,9 +197,7 @@ impl Inventory {
result result
} }
pub fn add_xp(&mut self, exp: u16, state: &mut SharedGameState) -> AddExperienceResult { pub fn add_xp(&mut self, exp: u16, player: &mut Player, state: &mut SharedGameState) {
let mut result = AddExperienceResult::None;
if let Some(weapon) = self.get_current_weapon_mut() { if let Some(weapon) = self.get_current_weapon_mut() {
let curr_level_idx = weapon.level as usize - 1; let curr_level_idx = weapon.level as usize - 1;
let lvl_table = state.constants.weapon.level_table[weapon.wtype as usize]; let lvl_table = state.constants.weapon.level_table[weapon.wtype as usize];
@ -194,19 +207,21 @@ impl Inventory {
if weapon.level == WeaponLevel::Level3 { if weapon.level == WeaponLevel::Level3 {
if weapon.experience > lvl_table[2] { if weapon.experience > lvl_table[2] {
weapon.experience = lvl_table[2]; weapon.experience = lvl_table[2];
result = AddExperienceResult::AddStar;
if player.equip.has_whimsical_star() && player.stars < 3 {
player.stars += 1;
}
} }
} else if weapon.experience > lvl_table[curr_level_idx] { } else if weapon.experience > lvl_table[curr_level_idx] {
weapon.level = weapon.level.next(); weapon.level = weapon.level.next();
weapon.experience = 0; weapon.experience = 0;
if weapon.wtype != WeaponType::Spur { if weapon.wtype != WeaponType::Spur {
result = AddExperienceResult::LevelUp; state.sound_manager.play_sfx(27);
state.create_caret(player.x, player.y, CaretType::LevelUp, Direction::Left);
} }
} }
} }
result
} }
/// Get current experience state. Returns a (exp, max exp, max level/exp) tuple. /// Get current experience state. Returns a (exp, max exp, max level/exp) tuple.

View file

@ -257,10 +257,10 @@ pub fn android_main() {
request_perms().expect("Failed to attach to the JVM and request storage permissions."); request_perms().expect("Failed to attach to the JVM and request storage permissions.");
env::set_var("CAVESTORY_DATA_DIR", "/storage/emulated/0/doukutsu"); env::set_var("CAVESTORY_DATA_DIR", "/sdcard/doukutsu");
let _ = std::fs::create_dir("/storage/emulated/0/doukutsu/"); let _ = std::fs::create_dir("/sdcard/doukutsu/");
let _ = std::fs::write("/storage/emulated/0/doukutsu/.nomedia", b""); let _ = std::fs::write("/sdcard/doukutsu/.nomedia", b"");
init().unwrap(); init().unwrap();
} }

View file

@ -67,7 +67,6 @@ impl NPC {
Ok(()) Ok(())
} }
pub(crate) fn tick_n064_first_cave_critter(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult { pub(crate) fn tick_n064_first_cave_critter(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult {
match self.action_num { match self.action_num {
0 | 1 => { 0 | 1 => {

105
src/npc/ai/last_cave.rs Normal file
View file

@ -0,0 +1,105 @@
use ggez::GameResult;
use crate::common::Direction;
use crate::npc::NPC;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
impl NPC {
pub(crate) fn tick_n241_critter_red(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult {
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.y += 3 * 0x200;
self.action_num = 1;
self.anim_num = 0;
}
let player = self.get_closest_player_mut(players);
if self.x > player.x {
self.direction = Direction::Left;
} else {
self.direction = Direction::Right;
}
if self.target_x < 100 {
self.target_x += 1;
}
if self.action_counter >= 8
&& self.x - (144 * 0x200) < player.x
&& self.x + (144 * 0x200) > player.x
&& self.y - (96 * 0x200) < player.y
&& self.y + (96 * 0x200) > player.y {
self.anim_num = 1;
} else {
if self.action_counter < 8 {
self.action_counter += 1;
}
self.anim_num = 0;
}
if self.shock > 0 {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 0;
}
if self.action_counter >= 8
&& self.target_x >= 100
&& self.x - (96 * 0x200) < player.x
&& self.x + (96 * 0x200) > player.x
&& self.y - (80 * 0x200) < player.y
&& self.y + (80 * 0x200) > player.y {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 0;
}
}
2 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 3;
self.anim_num = 2;
self.vel_y = -0x5ff;
state.sound_manager.play_sfx(30);
if self.direction == Direction::Left {
self.vel_x = -0x200;
} else {
self.vel_x = 0x200;
}
}
}
3 => {
if self.flags.hit_bottom_wall() {
self.vel_x = 0;
self.action_counter = 0;
self.action_num = 1;
self.anim_num = 0;
state.sound_manager.play_sfx(23);
}
}
_ => {}
}
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 { 3 };
self.anim_rect = state.constants.npc.n241_critter_red[self.anim_num as usize + dir_offset];
Ok(())
}
}

View file

@ -7,6 +7,7 @@ pub mod first_cave;
pub mod grasstown; pub mod grasstown;
pub mod igor; pub mod igor;
pub mod intro; pub mod intro;
pub mod last_cave;
pub mod maze; pub mod maze;
pub mod mimiga_village; pub mod mimiga_village;
pub mod misc; pub mod misc;

View file

@ -275,6 +275,7 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NP
199 => self.tick_n199_wind_particles(state), 199 => self.tick_n199_wind_particles(state),
211 => self.tick_n211_small_spikes(state), 211 => self.tick_n211_small_spikes(state),
234 => self.tick_n234_red_flowers_picked(state), 234 => self.tick_n234_red_flowers_picked(state),
241 => self.tick_n241_critter_red(state, players),
298 => self.tick_n298_intro_doctor(state), 298 => self.tick_n298_intro_doctor(state),
299 => self.tick_n299_intro_balrog_misery(state), 299 => self.tick_n299_intro_balrog_misery(state),
300 => self.tick_n300_intro_demon_crown(state), 300 => self.tick_n300_intro_demon_crown(state),

View file

@ -4,7 +4,7 @@ use num_traits::abs;
use crate::caret::CaretType; use crate::caret::CaretType;
use crate::common::{Condition, Direction, Flag, Rect}; use crate::common::{Condition, Direction, Flag, Rect};
use crate::inventory::{AddExperienceResult, Inventory}; use crate::inventory::Inventory;
use crate::npc::boss::BossNPC; use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList; use crate::npc::list::NPCList;
use crate::npc::NPC; use crate::npc::NPC;
@ -256,18 +256,7 @@ impl Player {
// experience pickup // experience pickup
1 => { 1 => {
state.sound_manager.play_sfx(14); state.sound_manager.play_sfx(14);
match inventory.add_xp(npc.exp, state) { inventory.add_xp(npc.exp, self, state);
AddExperienceResult::None => {}
AddExperienceResult::LevelUp => {
state.sound_manager.play_sfx(27);
state.create_caret(self.x, self.y, CaretType::LevelUp, Direction::Left);
}
AddExperienceResult::AddStar => {
if self.equip.has_whimsical_star() && self.stars < 3 {
self.stars += 1;
}
}
}
npc.cond.set_alive(false); npc.cond.set_alive(false);
} }
// missile pickup // missile pickup

View file

@ -5,7 +5,7 @@ pub trait RNG {
fn next(&self) -> i32; fn next(&self) -> i32;
fn range(&self, range: Range<i32>) -> i32 { fn range(&self, range: Range<i32>) -> i32 {
range.start.saturating_add((self.next() >> 2) % range.len() as i32) range.start + ((self.next() & 0x7fffffff) % (range.len() as i32))
} }
} }
@ -100,6 +100,6 @@ impl Xoroshiro32PlusPlus {
impl RNG for Xoroshiro32PlusPlus { impl RNG for Xoroshiro32PlusPlus {
fn next(&self) -> i32 { fn next(&self) -> i32 {
(((self.next_u16() as u32) << 16 | self.next_u16() as u32) >> 2) as i32 ((self.next_u16() as u32) << 16 | self.next_u16() as u32) as i32
} }
} }

View file

@ -519,7 +519,7 @@ impl GameScene {
for bullet in self.bullet_manager.bullets.iter() { for bullet in self.bullet_manager.bullets.iter() {
self.draw_light(fix9_scale(bullet.x - self.frame.x, scale), self.draw_light(fix9_scale(bullet.x - self.frame.x, scale),
fix9_scale(bullet.y - self.frame.y, scale), fix9_scale(bullet.y - self.frame.y, scale),
0.7, (200, 200, 200), batch); 0.3, (200, 200, 200), batch);
} }
for caret in state.carets.iter() { for caret in state.carets.iter() {
@ -1055,12 +1055,24 @@ impl GameScene {
self.frame.update(state, &self.stage); self.frame.update(state, &self.stage);
if state.control_flags.control_enabled() { if state.control_flags.control_enabled() {
#[allow(clippy::cast_ref_to_mut)]
let inventory = unsafe { &mut *(&self.inventory_player1 as *const Inventory as *mut Inventory)}; // fuck off
if let Some(weapon) = self.inventory_player1.get_current_weapon_mut() { if let Some(weapon) = self.inventory_player1.get_current_weapon_mut() {
weapon.shoot_bullet(&self.player1, TargetPlayer::Player1, &mut self.bullet_manager, state); weapon.shoot_bullet(&mut self.player1,
TargetPlayer::Player1,
inventory,
&mut self.bullet_manager,
state);
} }
#[allow(clippy::cast_ref_to_mut)]
let inventory = unsafe { &mut *(&self.inventory_player2 as *const Inventory as *mut Inventory)};
if let Some(weapon) = self.inventory_player2.get_current_weapon_mut() { if let Some(weapon) = self.inventory_player2.get_current_weapon_mut() {
weapon.shoot_bullet(&self.player2, TargetPlayer::Player2, &mut self.bullet_manager, state); weapon.shoot_bullet(&mut self.player2,
TargetPlayer::Player2,
inventory,
&mut self.bullet_manager,
state);
} }
self.hud_player1.tick(state, (&self.player1, &mut self.inventory_player1))?; self.hud_player1.tick(state, (&self.player1, &mut self.inventory_player1))?;

37
src/scripting/doukutsu.d.ts vendored Normal file
View file

@ -0,0 +1,37 @@
declare type EventHandler<T> = (this: void, param: T) => void;
declare interface DoukutsuPlayer {
x(): number;
y(): number;
velX(): number;
velY(): number;
};
declare interface DoukutsuScene {
/**
* Returns the tick of current scene.
*/
tick(): number;
/**
* Returns player at specified index.
*/
player(index: number): DoukutsuPlayer | null;
};
declare namespace doukutsu {
/**
* Plays a PixTone sound effect with specified ID.
*/
function playSfx(id: number): void;
/**
* Changes current music to one with specified ID.
* If ID equals 0, the music is stopped.
*/
function playMusic(id: number): void;
function on(event: "tick", handler: EventHandler<DoukutsuScene>): EventHandler<DoukutsuScene>;
function on<T>(event: string, handler: EventHandler<T>): EventHandler<T>;
};

View file

@ -40,45 +40,45 @@ pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
Channel::disabled(), Channel::disabled(),
], ],
}, },
PixToneParameters { // fx2 (CS+) // PixToneParameters { // fx2 (CS+)
channels: [ // channels: [
Channel { // Channel {
enabled: true, // enabled: true,
length: 2000, // length: 2000,
carrier: Waveform { // carrier: Waveform {
waveform_type: 0, // waveform_type: 0,
pitch: 92.000000, // pitch: 92.000000,
level: 32, // level: 32,
offset: 0, // offset: 0,
}, // },
frequency: Waveform { // frequency: Waveform {
waveform_type: 0, // waveform_type: 0,
pitch: 3.000000, // pitch: 3.000000,
level: 44, // level: 44,
offset: 0, // offset: 0,
}, // },
amplitude: Waveform { // amplitude: Waveform {
waveform_type: 0, // waveform_type: 0,
pitch: 0.000000, // pitch: 0.000000,
level: 32, // level: 32,
offset: 0, // offset: 0,
}, // },
envelope: Envelope { // envelope: Envelope {
initial: 7, // initial: 7,
time_a: 2, // time_a: 2,
value_a: 18, // value_a: 18,
time_b: 128, // time_b: 128,
value_b: 0, // value_b: 0,
time_c: 255, // time_c: 255,
value_c: 0, // value_c: 0,
}, // },
}, // },
Channel::disabled(), // Channel::disabled(),
Channel::disabled(), // Channel::disabled(),
Channel::disabled(), // Channel::disabled(),
], // ],
}, // },
/*PixToneParameters { // fx2 PixToneParameters { // fx2
channels: [ channels: [
Channel { Channel {
enabled: true, enabled: true,
@ -115,7 +115,7 @@ pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
Channel::disabled(), Channel::disabled(),
Channel::disabled(), Channel::disabled(),
], ],
},*/ },
PixToneParameters { // fx3 PixToneParameters { // fx3
channels: [ channels: [
Channel { Channel {

View file

@ -373,6 +373,7 @@ pub struct TextScriptVM {
/// while parsing no one noticed them. /// while parsing no one noticed them.
pub strict_mode: bool, pub strict_mode: bool,
pub suspend: bool, pub suspend: bool,
pub numbers: [u16; 4],
pub face: u16, pub face: u16,
pub item: u16, pub item: u16,
pub current_line: TextScriptLine, pub current_line: TextScriptLine,
@ -455,6 +456,7 @@ impl TextScriptVM {
executor_player: TargetPlayer::Player1, executor_player: TargetPlayer::Player1,
strict_mode: false, strict_mode: false,
suspend: true, suspend: true,
numbers: [0; 4],
face: 0, face: 0,
item: 0, item: 0,
current_line: TextScriptLine::Line1, current_line: TextScriptLine::Line1,
@ -715,7 +717,6 @@ impl TextScriptVM {
game_scene.player1.cond.set_interacted(false); game_scene.player1.cond.set_interacted(false);
game_scene.player2.cond.set_interacted(false); game_scene.player2.cond.set_interacted(false);
game_scene.frame.update_target = UpdateTarget::Player;
exec_state = TextScriptExecutionState::Ended; exec_state = TextScriptExecutionState::Ended;
} }
@ -1026,6 +1027,21 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::WaitConfirmation(event, cursor.position() as u32, event_no, 16, ConfirmSelection::Yes); exec_state = TextScriptExecutionState::WaitConfirmation(event, cursor.position() as u32, event_no, 16, ConfirmSelection::Yes);
} }
OpCode::NUM => {
let index = read_cur_varint(&mut cursor)? as usize;
if let Some(num) = state.textscript_vm.numbers.get(index) {
let mut str = num.to_string().chars().collect_vec();
match state.textscript_vm.current_line {
TextScriptLine::Line1 => state.textscript_vm.line_1.append(&mut str),
TextScriptLine::Line2 => state.textscript_vm.line_2.append(&mut str),
TextScriptLine::Line3 => state.textscript_vm.line_3.append(&mut str),
}
}
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
OpCode::GIT => { OpCode::GIT => {
let item = read_cur_varint(&mut cursor)? as u16; let item = read_cur_varint(&mut cursor)? as u16;
state.textscript_vm.item = item; state.textscript_vm.item = item;
@ -1418,6 +1434,8 @@ impl TextScriptVM {
let max_ammo = read_cur_varint(&mut cursor)? as u16; let max_ammo = read_cur_varint(&mut cursor)? as u16;
let weapon_type: Option<WeaponType> = FromPrimitive::from_u8(weapon_id); let weapon_type: Option<WeaponType> = FromPrimitive::from_u8(weapon_id);
state.textscript_vm.numbers[0] = max_ammo;
if let Some(wtype) = weapon_type { if let Some(wtype) = weapon_type {
game_scene.inventory_player1.add_weapon(wtype, max_ammo); game_scene.inventory_player1.add_weapon(wtype, max_ammo);
game_scene.inventory_player2.add_weapon(wtype, max_ammo); game_scene.inventory_player2.add_weapon(wtype, max_ammo);
@ -1442,6 +1460,20 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
} }
OpCode::TAM => {
let old_weapon_id = read_cur_varint(&mut cursor)? as u8;
let new_weapon_id = read_cur_varint(&mut cursor)? as u8;
let max_ammo = read_cur_varint(&mut cursor)? as u16;
let old_weapon_type: Option<WeaponType> = FromPrimitive::from_u8(old_weapon_id);
let new_weapon_type: Option<WeaponType> = FromPrimitive::from_u8(new_weapon_id);
if let Some(wtype) = new_weapon_type {
game_scene.inventory_player1.trade_weapon(old_weapon_type, wtype, max_ammo);
game_scene.inventory_player2.trade_weapon(old_weapon_type, wtype, max_ammo);
}
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
OpCode::ZAM => { OpCode::ZAM => {
game_scene.inventory_player1.reset_all_weapon_xp(); game_scene.inventory_player1.reset_all_weapon_xp();
game_scene.inventory_player2.reset_all_weapon_xp(); game_scene.inventory_player2.reset_all_weapon_xp();
@ -1494,7 +1526,7 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
} }
// One operand codes // One operand codes
OpCode::NUM | OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::MPp | OpCode::SKm | OpCode::SKp |
OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL | OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL |
OpCode::SSS | OpCode::ACH => { OpCode::SSS | OpCode::ACH => {
let par_a = read_cur_varint(&mut cursor)?; let par_a = read_cur_varint(&mut cursor)?;
@ -1510,16 +1542,6 @@ impl TextScriptVM {
log::warn!("unimplemented opcode: {:?} {} {}", op, par_a, par_b); log::warn!("unimplemented opcode: {:?} {} {}", op, par_a, par_b);
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
// Three operand codes
OpCode::TAM => {
let par_a = read_cur_varint(&mut cursor)?;
let par_b = read_cur_varint(&mut cursor)?;
let par_c = read_cur_varint(&mut cursor)?;
log::warn!("unimplemented opcode: {:?} {} {} {}", op, par_a, par_b, par_c);
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
} }
} }

View file

@ -1,8 +1,9 @@
use num_derive::FromPrimitive; use num_derive::FromPrimitive;
use crate::bullet::BulletManager; use crate::bullet::{Bullet, BulletManager};
use crate::caret::CaretType; use crate::caret::CaretType;
use crate::common::Direction; use crate::common::Direction;
use crate::inventory::Inventory;
use crate::player::{Player, TargetPlayer}; use crate::player::{Player, TargetPlayer};
use crate::shared_game_state::SharedGameState; use crate::shared_game_state::SharedGameState;
@ -58,6 +59,8 @@ pub struct Weapon {
pub experience: u16, pub experience: u16,
pub ammo: u16, pub ammo: u16,
pub max_ammo: u16, pub max_ammo: u16,
counter1: u16,
counter2: u16,
} }
impl Weapon { impl Weapon {
@ -68,6 +71,8 @@ impl Weapon {
experience, experience,
ammo, ammo,
max_ammo, max_ammo,
counter1: 0,
counter2: 0,
} }
} }
@ -85,7 +90,7 @@ impl Weapon {
} }
fn shoot_bullet_snake(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) { fn shoot_bullet_snake(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi([1, 2, 3], player_id) < 4 { if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi(&[1, 2, 3], player_id) < 4 {
let btype = match self.level { let btype = match self.level {
WeaponLevel::Level1 => { 1 } WeaponLevel::Level1 => { 1 }
WeaponLevel::Level2 => { 2 } WeaponLevel::Level2 => { 2 }
@ -98,14 +103,20 @@ impl Weapon {
return; return;
} }
self.counter1 = self.counter1.wrapping_add(1);
if player.up { if player.up {
match player.direction { match player.direction {
Direction::Left => { Direction::Left => {
bullet_manager.create_bullet(player.x - 3 * 0x200, player.y - 10 * 0x200, btype, player_id, Direction::Up, &state.constants); let mut bullet = Bullet::new(player.x - 3 * 0x200, player.y - 10 * 0x200, btype, player_id, Direction::Up, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x - 3 * 0x200, player.y - 10 * 0x200, CaretType::Shoot, Direction::Left); state.create_caret(player.x - 3 * 0x200, player.y - 10 * 0x200, CaretType::Shoot, Direction::Left);
} }
Direction::Right => { Direction::Right => {
bullet_manager.create_bullet(player.x + 3 * 0x200, player.y - 10 * 0x200, btype, player_id, Direction::Up, &state.constants); let mut bullet = Bullet::new(player.x + 3 * 0x200, player.y - 10 * 0x200, btype, player_id, Direction::Up, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x + 3 * 0x200, player.y - 10 * 0x200, CaretType::Shoot, Direction::Left); state.create_caret(player.x + 3 * 0x200, player.y - 10 * 0x200, CaretType::Shoot, Direction::Left);
} }
_ => {} _ => {}
@ -113,11 +124,15 @@ impl Weapon {
} else if player.down { } else if player.down {
match player.direction { match player.direction {
Direction::Left => { Direction::Left => {
bullet_manager.create_bullet(player.x - 3 * 0x200, player.y + 10 * 0x200, btype, player_id, Direction::Bottom, &state.constants); let mut bullet = Bullet::new(player.x - 3 * 0x200, player.y + 10 * 0x200, btype, player_id, Direction::Bottom, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x - 3 * 0x200, player.y + 10 * 0x200, CaretType::Shoot, Direction::Left); state.create_caret(player.x - 3 * 0x200, player.y + 10 * 0x200, CaretType::Shoot, Direction::Left);
} }
Direction::Right => { Direction::Right => {
bullet_manager.create_bullet(player.x + 3 * 0x200, player.y + 10 * 0x200, btype, player_id, Direction::Bottom, &state.constants); let mut bullet = Bullet::new(player.x + 3 * 0x200, player.y + 10 * 0x200, btype, player_id, Direction::Bottom, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x + 3 * 0x200, player.y + 10 * 0x200, CaretType::Shoot, Direction::Left); state.create_caret(player.x + 3 * 0x200, player.y + 10 * 0x200, CaretType::Shoot, Direction::Left);
} }
_ => {} _ => {}
@ -125,11 +140,15 @@ impl Weapon {
} else { } else {
match player.direction { match player.direction {
Direction::Left => { Direction::Left => {
bullet_manager.create_bullet(player.x - 6 * 0x200, player.y + 2 * 0x200, btype, player_id, Direction::Left, &state.constants); let mut bullet = Bullet::new(player.x - 6 * 0x200, player.y + 2 * 0x200, btype, player_id, Direction::Left, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x - 12 * 0x200, player.y + 2 * 0x200, CaretType::Shoot, Direction::Left); state.create_caret(player.x - 12 * 0x200, player.y + 2 * 0x200, CaretType::Shoot, Direction::Left);
} }
Direction::Right => { Direction::Right => {
bullet_manager.create_bullet(player.x + 6 * 0x200, player.y + 2 * 0x200, btype, player_id, Direction::Right, &state.constants); let mut bullet = Bullet::new(player.x + 6 * 0x200, player.y + 2 * 0x200, btype, player_id, Direction::Right, &state.constants);
bullet.target_x = self.counter1 as i32;
bullet_manager.push_bullet(bullet);
state.create_caret(player.x + 12 * 0x200, player.y + 2 * 0x200, CaretType::Shoot, Direction::Right); state.create_caret(player.x + 12 * 0x200, player.y + 2 * 0x200, CaretType::Shoot, Direction::Right);
} }
_ => {} _ => {}
@ -141,7 +160,7 @@ impl Weapon {
} }
fn shoot_bullet_polar_star(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) { fn shoot_bullet_polar_star(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi([4, 5, 6], player_id) < 2 { if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi(&[4, 5, 6], player_id) < 2 {
let btype = match self.level { let btype = match self.level {
WeaponLevel::Level1 => { 4 } WeaponLevel::Level1 => { 4 }
WeaponLevel::Level2 => { 5 } WeaponLevel::Level2 => { 5 }
@ -203,7 +222,7 @@ impl Weapon {
fn shoot_bullet_fireball(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) { fn shoot_bullet_fireball(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
let max_bullets = self.level as usize + 1; let max_bullets = self.level as usize + 1;
if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi([7, 8, 9], player_id) < max_bullets { if player.controller.trigger_shoot() && bullet_manager.count_bullets_multi(&[7, 8, 9], player_id) < max_bullets {
let btype = match self.level { let btype = match self.level {
WeaponLevel::Level1 => { 7 } WeaponLevel::Level1 => { 7 }
WeaponLevel::Level2 => { 8 } WeaponLevel::Level2 => { 8 }
@ -258,7 +277,125 @@ impl Weapon {
} }
} }
pub fn shoot_bullet(&mut self, player: &Player, player_id: TargetPlayer, bullet_manager: &mut BulletManager, state: &mut SharedGameState) { fn shoot_bullet_spur(&mut self, player: &mut Player, player_id: TargetPlayer, inventory: &mut Inventory, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
let mut shoot = false;
let mut btype = 0;
if player.controller.shoot() {
inventory.add_xp(if player.equip.has_turbocharge() { 3 } else { 2 }, player, state);
self.counter1 += 1;
if (self.counter1 / 2 % 2) != 0 {
match self.level {
WeaponLevel::Level1 => {
state.sound_manager.play_sfx(59);
}
WeaponLevel::Level2 => {
state.sound_manager.play_sfx(60);
}
WeaponLevel::Level3 => {
if let (_, _, false) = inventory.get_current_max_exp(&state.constants) {
state.sound_manager.play_sfx(61);
}
}
WeaponLevel::None => unreachable!(),
}
}
} else {
if self.counter1 > 0 {
shoot = true;
self.counter1 = 0;
}
}
if let (_, _, true) = inventory.get_current_max_exp(&state.constants) {
if self.counter2 == 0 {
self.counter2 = 1;
state.sound_manager.play_sfx(65);
}
} else {
self.counter2 = 0;
}
let level = self.level;
if !player.controller.shoot() {
inventory.reset_current_weapon_xp();
}
match level {
WeaponLevel::Level1 => {
btype = 6;
shoot = false;
}
WeaponLevel::Level2 => btype = 37,
WeaponLevel::Level3 => {
if self.counter2 == 1 {
btype = 39;
} else {
btype = 38;
}
}
WeaponLevel::None => unreachable!(),
}
const bullets: [u16; 6] = [44, 45, 46, 47, 48, 49];
if bullet_manager.count_bullets_multi(&bullets, player_id) == 0
&& (player.controller.trigger_shoot() || shoot) {
if !self.consume_ammo(1) {
state.sound_manager.play_sfx(37);
} else {
if player.up {
match player.direction {
Direction::Left => {
bullet_manager.create_bullet(player.x - 0x200, player.y - 8 * 0x200, btype, player_id, Direction::Up, &state.constants);
state.create_caret(player.x - 0x200, player.y - 8 * 0x200, CaretType::Shoot, Direction::Left);
}
Direction::Right => {
bullet_manager.create_bullet(player.x + 0x200, player.y - 8 * 0x200, btype, player_id, Direction::Up, &state.constants);
state.create_caret(player.x + 0x200, player.y - 8 * 0x200, CaretType::Shoot, Direction::Left);
}
_ => {}
}
} else if player.down {
match player.direction {
Direction::Left => {
bullet_manager.create_bullet(player.x - 0x200, player.y + 8 * 0x200, btype, player_id, Direction::Bottom, &state.constants);
state.create_caret(player.x - 0x200, player.y + 8 * 0x200, CaretType::Shoot, Direction::Left);
}
Direction::Right => {
bullet_manager.create_bullet(player.x + 0x200, player.y + 8 * 0x200, btype, player_id, Direction::Bottom, &state.constants);
state.create_caret(player.x + 0x200, player.y + 8 * 0x200, CaretType::Shoot, Direction::Left);
}
_ => {}
}
} else {
match player.direction {
Direction::Left => {
bullet_manager.create_bullet(player.x - 6 * 0x200, player.y + 3 * 0x200, btype, player_id, Direction::Left, &state.constants);
state.create_caret(player.x - 6 * 0x200, player.y + 3 * 0x200, CaretType::Shoot, Direction::Left);
}
Direction::Right => {
bullet_manager.create_bullet(player.x + 6 * 0x200, player.y + 3 * 0x200, btype, player_id, Direction::Right, &state.constants);
state.create_caret(player.x + 6 * 0x200, player.y + 3 * 0x200, CaretType::Shoot, Direction::Right);
}
_ => {}
}
}
let sound = match btype {
6 => 49,
37 => 62,
38 => 63,
39 => 64,
_ => 0,
};
state.sound_manager.play_sfx(sound);
}
}
}
pub fn shoot_bullet(&mut self, player: &mut Player, player_id: TargetPlayer, inventory: &mut Inventory, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
if !player.cond.alive() || player.cond.hidden() { if !player.cond.alive() || player.cond.hidden() {
return; return;
} }
@ -274,7 +411,7 @@ impl Weapon {
WeaponType::Blade => {} WeaponType::Blade => {}
WeaponType::SuperMissileLauncher => {} WeaponType::SuperMissileLauncher => {}
WeaponType::Nemesis => {} WeaponType::Nemesis => {}
WeaponType::Spur => {} WeaponType::Spur => self.shoot_bullet_spur(player, player_id, inventory, bullet_manager, state),
} }
} }
} }