1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-07-23 04:50:51 +00:00

Compare commits

...

6 commits

Author SHA1 Message Date
Alula d1d188ac77
intro scene/accuracy bugfixes 2021-03-23 02:53:01 +01:00
Alula 4043830e56
sand zone npcs 2021-03-23 02:52:27 +01:00
Alula c4b32f28ae
remove log spam 2021-03-23 02:52:15 +01:00
Alula f6c9a03126
add multiselect to menu 2021-03-23 02:49:55 +01:00
Alula a0c8cfa26d
minor fixes 2021-03-23 02:49:18 +01:00
Alula b49cc83a5b
reworked player animation system a bit 2021-03-23 02:48:46 +01:00
16 changed files with 1031 additions and 110 deletions

View file

@ -240,7 +240,7 @@ fn test_builtin_fs() {
FSNode::Directory("memes", vec![
FSNode::File("nothing.txt", &[]),
FSNode::Directory("secret stuff", vec![
FSNode::File("passwords.txt", &b"12345678"),
FSNode::File("passwords.txt", b"12345678"),
]),
]),
FSNode::File("test2.txt", &[]),

View file

@ -44,8 +44,9 @@ impl BackendEventLoop for NullEventLoop {
game.loops = 0;
state_ref.frame_time = 0.0;
}
std::thread::sleep(std::time::Duration::from_millis(10));
game.draw(ctx).unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
//game.draw(ctx).unwrap();
}
}

View file

@ -52,6 +52,26 @@ impl CombinedMenuController {
false
}
pub fn trigger_left(&self) -> bool {
for cont in self.controllers.iter() {
if cont.trigger_left() {
return true;
}
}
false
}
pub fn trigger_right(&self) -> bool {
for cont in self.controllers.iter() {
if cont.trigger_right() {
return true;
}
}
false
}
pub fn trigger_ok(&self) -> bool {
for cont in self.controllers.iter() {
if cont.trigger_menu_ok() {

View file

@ -1,7 +1,6 @@
use crate::common::Rect;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::shared_game_state::SharedGameState;
@ -35,6 +34,8 @@ pub enum MenuSelectionResult<'a> {
None,
Canceled,
Selected(usize, &'a mut MenuEntry),
Left(usize, &'a mut MenuEntry),
Right(usize, &'a mut MenuEntry),
}
pub struct Menu {
@ -53,17 +54,7 @@ static QUOTE_FRAMES: [u16; 4] = [0, 1, 0, 2];
impl Menu {
pub fn new(x: isize, y: isize, width: u16, height: u16) -> Menu {
Menu {
x,
y,
width,
height,
selected: 0,
entry_y: 0,
anim_num: 0,
anim_wait: 0,
entries: Vec::new(),
}
Menu { x, y, width, height, selected: 0, entry_y: 0, anim_num: 0, anim_wait: 0, entries: Vec::new() }
}
pub fn push_entry(&mut self, entry: MenuEntry) {
@ -181,9 +172,7 @@ impl Menu {
rect.right = rect.left + 16;
rect.bottom = rect.top + 16;
batch.add_rect(self.x as f32,
self.y as f32 + 2.0 + self.entry_y as f32,
&rect);
batch.add_rect(self.x as f32, self.y as f32 + 2.0 + self.entry_y as f32, &rect);
batch.draw(ctx)?;
@ -191,18 +180,47 @@ impl Menu {
for entry in self.entries.iter() {
match entry {
MenuEntry::Active(name) => {
state.font.draw_text(name.chars(), self.x as f32 + 20.0, y, &state.constants, &mut state.texture_set, ctx)?;
state.font.draw_text(
name.chars(),
self.x as f32 + 20.0,
y,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
MenuEntry::Disabled(name) => {
state.font.draw_colored_text(name.chars(), self.x as f32 + 20.0, y, (0xa0, 0xa0, 0xff, 0xff), &state.constants, &mut state.texture_set, ctx)?;
state.font.draw_colored_text(
name.chars(),
self.x as f32 + 20.0,
y,
(0xa0, 0xa0, 0xff, 0xff),
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
MenuEntry::Toggle(name, value) => {
let value_text = if *value { "ON" } else { "OFF" };
let val_text_len = state.font.text_width(value_text.chars(), &state.constants);
state.font.draw_text(name.chars(), self.x as f32 + 20.0, y, &state.constants, &mut state.texture_set, ctx)?;
state.font.draw_text(
name.chars(),
self.x as f32 + 20.0,
y,
&state.constants,
&mut state.texture_set,
ctx,
)?;
state.font.draw_text(value_text.chars(), self.x as f32 + self.width as f32 - val_text_len, y, &state.constants, &mut state.texture_set, ctx)?;
state.font.draw_text(
value_text.chars(),
self.x as f32 + self.width as f32 - val_text_len,
y,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
MenuEntry::Hidden => {}
_ => {}
@ -214,7 +232,11 @@ impl Menu {
Ok(())
}
pub fn tick(&mut self, controller: &mut CombinedMenuController, state: &mut SharedGameState) -> MenuSelectionResult {
pub fn tick(
&mut self,
controller: &mut CombinedMenuController,
state: &mut SharedGameState,
) -> MenuSelectionResult {
if controller.trigger_back() {
state.sound_manager.play_sfx(5);
return MenuSelectionResult::Canceled;
@ -237,8 +259,12 @@ impl Menu {
if let Some(entry) = self.entries.get(self.selected) {
match entry {
MenuEntry::Active(_) => { break; }
MenuEntry::Toggle(_, _) => { break; }
MenuEntry::Active(_) => {
break;
}
MenuEntry::Toggle(_, _) => {
break;
}
_ => {}
}
} else {
@ -248,11 +274,7 @@ impl Menu {
}
if !self.entries.is_empty() {
self.entry_y = self.entries[0..(self.selected)]
.iter()
.map(|e| e.height())
.sum::<f64>()
.max(0.0) as u16;
self.entry_y = self.entries[0..(self.selected)].iter().map(|e| e.height()).sum::<f64>().max(0.0) as u16;
}
let mut y = self.y as f32 + 6.0;
@ -260,17 +282,22 @@ impl Menu {
let entry_bounds = Rect::new_size(self.x, y as isize, self.width as isize, entry.height() as isize);
y += entry.height() as f32;
if !((controller.trigger_ok() && self.selected == idx)
|| state.touch_controls.consume_click_in(entry_bounds)) {
continue;
}
match entry {
MenuEntry::Active(_) | MenuEntry::Toggle(_, _) => {
self.selected = idx;
MenuEntry::Active(_) | MenuEntry::Toggle(_, _)
if (self.selected == idx && controller.trigger_ok())
|| state.touch_controls.consume_click_in(entry_bounds) =>
{
state.sound_manager.play_sfx(18);
return MenuSelectionResult::Selected(idx, entry);
}
MenuEntry::Options(_, _, _) if controller.trigger_left() => {
state.sound_manager.play_sfx(1);
return MenuSelectionResult::Left(self.selected, entry);
}
MenuEntry::Options(_, _, _) if controller.trigger_right() => {
state.sound_manager.play_sfx(1);
return MenuSelectionResult::Right(self.selected, entry);
}
_ => {}
}
}

View file

@ -91,6 +91,8 @@ impl NPC {
self.vel_y = 0x5ff;
}
self.y += self.vel_y;
Ok(())
}

View file

@ -8,6 +8,7 @@ use crate::player::Player;
use crate::rng::RNG;
use crate::shared_game_state::SharedGameState;
use crate::weapon::bullet::BulletManager;
use crate::caret::CaretType;
impl NPC {
pub(crate) fn tick_n117_curly(
@ -271,4 +272,53 @@ impl NPC {
Ok(())
}
pub(crate) fn tick_n123_curly_boss_bullet(&mut self, state: &mut SharedGameState) -> GameResult {
if self.action_num == 0 {
self.action_num = 1;
state.create_caret(self.x, self.y, CaretType::Shoot, Direction::Left);
state.sound_manager.play_sfx(32);
match self.direction {
Direction::Left => {
self.vel_x = -0x1000;
self.vel_y = self.rng.range(-0x80..0x80);
}
Direction::Up => {
self.vel_x = self.rng.range(-0x80..0x80);
self.vel_y = -0x1000;
}
Direction::Right => {
self.vel_x = 0x1000;
self.vel_y = self.rng.range(-0x80..0x80);
}
Direction::Bottom => {
self.vel_x = self.rng.range(-0x80..0x80);
self.vel_y = 0x1000;
}
Direction::FacingPlayer => unreachable!(),
}
self.anim_rect = state.constants.npc.n123_curly_boss_bullet[self.direction as usize];
}
if match self.direction {
Direction::Left if self.flags.hit_left_wall() => true,
Direction::Right if self.flags.hit_right_wall() => true,
Direction::Up if self.flags.hit_top_wall() => true,
Direction::Bottom if self.flags.hit_bottom_wall() => true,
_ => false,
} {
state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Right);
state.sound_manager.play_sfx(28);
self.cond.set_alive(false);
return Ok(());
}
self.x += self.vel_x;
self.y += self.vel_y;
Ok(())
}
}

View file

@ -688,6 +688,166 @@ impl NPC {
Ok(())
}
pub(crate) fn tick_n056_tan_beetle(
&mut self,
state: &mut SharedGameState,
players: [&mut Player; 2],
) -> GameResult {
match self.action_num {
0 => {
self.action_num = if self.direction == Direction::Left { 1 } else { 3 };
}
1 => {
self.vel_x -= 0x10;
if self.vel_x < -0x400 {
self.vel_x = -0x400;
}
self.x += if self.shock != 0 { self.vel_x / 2 } else { self.vel_x };
self.animate(1, 1, 2);
if self.flags.hit_left_wall() {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 0;
self.vel_x = 0;
self.direction = Direction::Right;
}
}
2 => {
let player = self.get_closest_player_mut(players);
if self.x < player.x && self.x > player.x - 0x20000 && (self.y - player.y).abs() < 0x1000 {
self.action_num = 3;
self.anim_num = 1;
self.anim_counter = 0;
}
}
3 => {
self.vel_x += 0x10;
if self.vel_x > 0x400 {
self.vel_x = 0x400;
}
self.x += if self.shock != 0 { self.vel_x / 2 } else { self.vel_x };
self.animate(1, 1, 2);
if self.flags.hit_right_wall() {
self.action_num = 4;
self.action_counter = 0;
self.anim_num = 0;
self.vel_x = 0;
self.direction = Direction::Left;
}
}
4 => {
let player = self.get_closest_player_mut(players);
if self.x > player.x && self.x < player.x + 0x20000 && (self.y - player.y).abs() < 0x1000 {
self.action_num = 1;
self.anim_num = 1;
self.anim_counter = 0;
}
}
_ => {}
}
let dir_offset = if self.direction == Direction::Left { 0 } else { 3 };
self.anim_rect = state.constants.npc.n056_tan_beetle[self.anim_num as usize + dir_offset];
Ok(())
}
pub(crate) fn tick_n057_crow(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult {
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = self.rng.range(0..1) as u16;
self.anim_counter = self.rng.range(0..4) as u16;
self.action_counter2 = 120;
let mut angle = self.rng.range(0..255);
self.vel_x = ((angle as f64 * CDEG_RAD).cos() * -512.0) as i32;
angle += 0x40;
self.target_x = self.x + 8 * ((angle as f64 * CDEG_RAD).cos() * -512.0) as i32;
self.vel_y = ((angle as f64 * CDEG_RAD).sin() * -512.0) as i32;
angle += 0x40;
self.target_y = self.y + 8 * ((angle as f64 * CDEG_RAD).sin() * -512.0) as i32;
}
let player = self.get_closest_player_mut(players);
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
self.vel_x += ((self.target_x - self.x).signum() * 0x10).clamp(-0x200, 0x200);
self.vel_y += ((self.target_y - self.y).signum() * 0x10).clamp(-0x200, 0x200);
if self.shock != 0 {
self.action_num = 2;
self.action_counter = 0;
self.vel_x = self.direction.opposite().vector_x() * 0x200;
self.vel_y = 0;
}
}
2 => {
let player = self.get_closest_player_mut(players);
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
self.vel_x += if self.y <= player.y + 0x4000 {
(player.x - self.x).signum() * 0x10
} else {
(self.x - player.x).signum() * 0x10
};
self.vel_y += (player.y - self.y).signum() * 0x10;
if self.shock > 0 {
self.vel_x = 0;
self.vel_y += 0x20;
}
if self.vel_x < 0 && self.flags.hit_left_wall() {
self.vel_x = 0x200;
}
if self.vel_x > 0 && self.flags.hit_right_wall() {
self.vel_x = -0x200;
}
if self.vel_y < 0 && self.flags.hit_top_wall() {
self.vel_y = 0x200;
}
if self.vel_y > 0 && self.flags.hit_bottom_wall() {
self.vel_y = -0x200;
}
self.vel_x = clamp(self.vel_x, -0x5ff, 0x5ff);
self.vel_y = clamp(self.vel_y, -0x5ff, 0x5ff);
}
_ => {}
}
self.x += self.vel_x;
self.y += self.vel_y;
if self.shock > 0 {
self.anim_num = 4;
} else {
self.animate(1, 0, 1);
}
let dir_offset = if self.direction == Direction::Left { 0 } else { 5 };
self.anim_rect = state.constants.npc.n057_crow[self.anim_num as usize + dir_offset];
Ok(())
}
pub(crate) fn tick_n120_colon_a(&mut self, state: &mut SharedGameState) -> GameResult {
let anim = if self.direction == Direction::Left { 0 } else { 1 };
@ -696,6 +856,48 @@ impl NPC {
Ok(())
}
pub(crate) fn tick_n121_colon_b(&mut self, state: &mut SharedGameState) -> GameResult {
if self.direction != Direction::Left {
self.anim_rect = state.constants.npc.n121_colon_b[2];
self.action_counter += 1;
if self.action_counter > 100 {
self.action_counter = 0;
state.create_caret(self.x, self.y, CaretType::Zzz, Direction::Left);
}
return Ok(());
}
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = 0;
self.anim_counter = 0;
}
if self.rng.range(0..120) == 10 {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 1;
}
}
2 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 1;
self.anim_num = 0;
}
}
_ => {}
}
self.anim_rect = state.constants.npc.n121_colon_b[self.anim_num as usize];
Ok(())
}
pub(crate) fn tick_n124_sunstone(&mut self, state: &mut SharedGameState) -> GameResult {
match self.action_num {
0 | 1 => {
@ -738,6 +940,105 @@ impl NPC {
Ok(())
}
pub(crate) fn tick_n126_puppy_running(
&mut self,
state: &mut SharedGameState,
players: [&mut Player; 2],
) -> GameResult {
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = 0;
self.anim_counter = 0;
}
if self.rng.range(0..120) == 10 {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 1;
}
let player = self.get_closest_player_ref(&players);
if (self.x - player.x).abs() < 0xc000 && self.y - 0x4000 < player.y && self.y + 0x2000 > player.y {
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
}
if (self.x - player.x).abs() < 0x4000 && self.y - 0x4000 < player.y && self.y + 0x2000 > player.y {
self.action_num = 10;
self.direction = if self.x > player.x { Direction::Right } else { Direction::Left };
}
}
2 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 1;
self.anim_num = 0;
}
}
10 | 11 => {
if self.action_num == 10 {
self.action_num = 11;
self.anim_num = 4;
self.anim_counter = 0;
}
if self.flags.hit_bottom_wall() {
self.animate(2, 4, 5);
} else {
self.anim_num = 5;
self.anim_counter =0;
}
if self.vel_x < 0 && self.flags.hit_left_wall() {
self.vel_x /= -2;
self.direction = Direction::Right;
}
if self.vel_x > 0 && self.flags.hit_right_wall() {
self.vel_x /= -2;
self.direction = Direction::Left;
}
self.vel_x += self.direction.vector_x() * 0x40;
// what the hell pixel?
if self.vel_x > 0x5ff {
self.vel_x = 0x400;
}
if self.vel_x < -0x5ff {
self.vel_x = -0x400;
}
}
_ => {}
}
// why
self.npc_flags.set_interactable(false);
for player in players.iter() {
if player.controller.trigger_down() {
self.npc_flags.set_interactable(true);
}
}
self.vel_y += 0x40;
if self.vel_y > 0x5ff {
self.vel_y = 0x5ff;
}
self.x += self.vel_x;
self.y += self.vel_y;
let dir_offset = if self.direction == Direction::Left { 0 } else { 6 };
self.anim_rect = state.constants.npc.n126_puppy_running[self.anim_num as usize + dir_offset];
Ok(())
}
pub(crate) fn tick_n131_puppy_sleeping(&mut self, state: &mut SharedGameState) -> GameResult {
self.action_counter += 1;
if self.action_counter > 100 {
@ -752,6 +1053,153 @@ impl NPC {
Ok(())
}
pub(crate) fn tick_n132_puppy_barking(
&mut self,
state: &mut SharedGameState,
players: [&mut Player; 2],
) -> GameResult {
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = 0;
self.anim_counter = 0;
}
if self.rng.range(0..120) == 10 {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 1;
}
let player = self.get_closest_player_mut(players);
if (self.x - player.x).abs() < 0x8000 && (self.y - player.y).abs() < 0x2000 {
self.animate(4, 2, 4);
if self.anim_num == 4 && self.anim_counter == 0 {
state.sound_manager.play_sfx(105);
}
}
}
2 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 1;
self.anim_num = 0;
}
}
10 | 11 => {
if self.action_num == 10 {
self.action_num = 11;
self.anim_num = 0;
self.anim_counter = 0;
}
if self.rng.range(0..120) == 10 {
self.action_num = 12;
self.action_counter = 0;
self.anim_num = 1;
}
}
12 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 11;
self.anim_num = 0;
}
}
100 => {
self.action_num = 101;
self.action_counter2 = 0;
}
101 => {
self.anim_counter += 1;
if self.anim_counter > 4 {
self.anim_counter = 0;
self.anim_num += 1;
if self.anim_num > 4 {
if self.action_counter2 > 2 {
self.anim_num = 0;
self.action_counter2 = 0;
} else {
self.anim_num = 2;
self.action_counter2 += 1;
}
}
}
if self.anim_num == 4 && self.anim_counter == 0 {
state.sound_manager.play_sfx(105);
}
}
_ => {}
}
self.vel_y += 0x40;
if self.vel_y > 0x5ff {
self.vel_y = 0x5ff;
}
self.x += self.vel_x;
self.y += self.vel_y;
let dir_offset = if self.direction == Direction::Left { 0 } else { 5 };
self.anim_rect = state.constants.npc.n132_puppy_barking[self.anim_num as usize + dir_offset];
Ok(())
}
pub(crate) fn tick_n136_puppy_carried(
&mut self,
state: &mut SharedGameState,
players: [&mut Player; 2],
) -> GameResult {
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
self.action_num = 1;
self.anim_num = 0;
self.anim_counter = 0;
self.npc_flags.set_interactable(false);
}
if self.rng.range(0..120) == 10 {
self.action_num = 2;
self.action_counter = 0;
self.anim_num = 1;
}
}
2 => {
self.action_counter += 1;
if self.action_counter > 8 {
self.action_num = 1;
self.anim_num = 0;
}
}
_ => {}
}
// todo dog stacking?
let player_index = 0;
let player = &players[player_index];
self.direction = player.direction;
self.y = player.y - 0x1400;
self.x = player.x + 0x800 * self.direction.opposite().vector_x();
let dir_offset = if self.direction == Direction::Left { 0 } else { 2 };
self.anim_rect = state.constants.npc.n136_puppy_carried[self.anim_num as usize + dir_offset];
if (player.anim_num & 1) != 0 {
self.anim_rect.top += 1;
}
Ok(())
}
pub(crate) fn tick_n143_jenka_collapsed(&mut self, state: &mut SharedGameState) -> GameResult {
let anim = if self.direction == Direction::Left { 0 } else { 1 };

View file

@ -198,6 +198,8 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NP
53 => self.tick_n053_skullstep_leg(state, npc_list),
54 => self.tick_n054_skullstep(state, npc_list),
55 => self.tick_n055_kazuma(state),
56 => self.tick_n056_tan_beetle(state, players),
57 => self.tick_n057_crow(state, players),
58 => self.tick_n058_basu(state, players, npc_list),
59 => self.tick_n059_eye_door(state, players),
60 => self.tick_n060_toroko(state, players),
@ -260,12 +262,17 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NP
118 => self.tick_n118_curly_boss(state, players, npc_list, bullet_manager),
119 => self.tick_n119_table_chair(state),
120 => self.tick_n120_colon_a(state),
121 => self.tick_n121_colon_b(state),
123 => self.tick_n123_curly_boss_bullet(state),
124 => self.tick_n124_sunstone(state),
125 => self.tick_n125_hidden_item(state, npc_list),
126 => self.tick_n126_puppy_running(state, players),
127 => self.tick_n127_machine_gun_trail_l2(state),
128 => self.tick_n128_machine_gun_trail_l3(state),
129 => self.tick_n129_fireball_snake_trail(state),
131 => self.tick_n131_puppy_sleeping(state),
132 => self.tick_n132_puppy_barking(state, players),
136 => self.tick_n136_puppy_carried(state, players),
137 => self.tick_n137_large_door_frame(state),
143 => self.tick_n143_jenka_collapsed(state),
149 => self.tick_n149_horizontal_moving_block(state, players, npc_list),

View file

@ -146,6 +146,13 @@ impl NPC {
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 {
(

View file

@ -13,6 +13,8 @@ use crate::input::dummy_player_controller::DummyPlayerController;
use crate::input::player_controller::PlayerController;
use crate::npc::list::NPCList;
use crate::npc::NPC;
use crate::player::skin::basic::BasicPlayerSkin;
use crate::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin};
use crate::rng::RNG;
use crate::shared_game_state::SharedGameState;
@ -26,18 +28,6 @@ pub enum ControlMode {
IronHead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PlayerAppearance {
Quote = 0,
/// Cave Story+ player skins
YellowQuote,
HumanQuote,
HalloweenQuote,
ReindeerQuote,
Curly,
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum TargetPlayer {
Player1,
@ -80,7 +70,7 @@ pub struct Player {
pub damage: u16,
pub air_counter: u16,
pub air: u16,
pub appearance: PlayerAppearance,
pub skin: Box<dyn PlayerSkin>,
pub controller: Box<dyn PlayerController>,
weapon_offset_y: i8,
camera_target_x: i32,
@ -134,7 +124,7 @@ impl Player {
damage: 0,
air_counter: 0,
air: 0,
appearance: PlayerAppearance::Quote,
skin: Box::new(BasicPlayerSkin::new("MyChar".to_string())),
controller: Box::new(DummyPlayerController::new()),
damage_counter: 0,
damage_taken: 0,
@ -146,7 +136,11 @@ impl Player {
}
pub fn get_texture_offset(&self) -> u16 {
(self.appearance as u16 % 6) * 64 + if self.equip.has_mimiga_mask() { 32 } else { 0 }
if self.equip.has_mimiga_mask() {
32
} else {
0
}
}
fn tick_normal(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
@ -555,12 +549,14 @@ impl Player {
if self.flags.hit_bottom_wall() {
if self.cond.interacted() {
self.skin.set_state(PlayerAnimationState::Examining);
self.anim_num = 11;
} else if state.control_flags.control_enabled()
&& self.controller.move_up()
&& (self.controller.move_left() || self.controller.move_right())
{
self.cond.set_fallen(true);
self.skin.set_state(PlayerAnimationState::WalkingUp);
self.anim_counter += 1;
if self.anim_counter > 4 {
@ -579,6 +575,7 @@ impl Player {
&& (self.controller.move_left() || self.controller.move_right())
{
self.cond.set_fallen(true);
self.skin.set_state(PlayerAnimationState::Walking);
self.anim_counter += 1;
if self.anim_counter > 4 {
@ -599,6 +596,7 @@ impl Player {
}
self.cond.set_fallen(false);
self.skin.set_state(PlayerAnimationState::LookingUp);
self.anim_num = 5;
} else {
if self.cond.fallen() {
@ -606,13 +604,21 @@ impl Player {
}
self.cond.set_fallen(false);
self.skin.set_state(PlayerAnimationState::Idle);
self.anim_num = 0;
}
} else if self.controller.look_up() {
self.skin.set_state(PlayerAnimationState::FallingLookingUp);
self.anim_num = 6;
} else if self.controller.look_down() {
self.skin.set_state(PlayerAnimationState::FallingLookingDown);
self.anim_num = 10;
} else {
self.skin.set_state(if self.vel_y > 0 {
PlayerAnimationState::Jumping
} else {
PlayerAnimationState::Falling
});
self.anim_num = if self.vel_y > 0 { 1 } else { 3 };
}
@ -648,9 +654,15 @@ impl Player {
self.weapon_rect.top += 1;
}
let offset = self.get_texture_offset();
self.anim_rect.top += offset;
self.anim_rect.bottom += offset;
self.skin.tick();
self.skin.set_direction(self.direction);
self.skin.set_appearance(if self.equip.has_mimiga_mask() {
PlayerAppearanceState::MimigaMask
} else {
PlayerAppearanceState::Default
});
self.anim_rect = self.skin.animation_frame();
self.tick = self.tick.wrapping_add(1);
}
@ -733,42 +745,44 @@ impl GameEntity<&NPCList> for Player {
if state.constants.is_switch {
let dog_amount = (3000..=3005).filter(|id| state.get_flag(*id as usize)).count();
let vec_x = self.direction.vector_x() * 0x800;
let vec_y = 0x1400;
if dog_amount > 0 {
let vec_x = self.direction.vector_x() * 0x800;
let vec_y = 0x1400;
if let Some(entry) = state.npc_table.get_entry(136) {
let sprite = state.npc_table.get_texture_name(entry.spritesheet_id as u16);
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, sprite)?;
if let Some(entry) = state.npc_table.get_entry(136) {
let sprite = state.npc_table.get_texture_name(entry.spritesheet_id as u16);
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, sprite)?;
let (off_x, frame_id) = if self.direction == Direction::Left {
(entry.display_bounds.right as i32 * 0x200, 0)
} else {
(entry.display_bounds.left as i32 * 0x200, 2)
};
let off_y = entry.display_bounds.top as i32 * 0x200;
let (off_x, frame_id) = if self.direction == Direction::Left {
(entry.display_bounds.right as i32 * 0x200, 0)
} else {
(entry.display_bounds.left as i32 * 0x200, 2)
};
let off_y = entry.display_bounds.top as i32 * 0x200;
for i in 1..=(dog_amount as i32) {
batch.add_rect(
interpolate_fix9_scale(
self.prev_x - frame.prev_x - off_x - vec_x * i,
self.x - frame.x - off_x - vec_x * i,
state.frame_time,
),
interpolate_fix9_scale(
self.prev_y - frame.prev_y - off_y - vec_y * i,
self.y - frame.y - off_y - vec_y * i,
state.frame_time,
),
&state.constants.npc.n136_puppy_carried[frame_id],
);
for i in 1..=(dog_amount as i32) {
batch.add_rect(
interpolate_fix9_scale(
self.prev_x - frame.prev_x - off_x - vec_x * i,
self.x - frame.x - off_x - vec_x * i,
state.frame_time,
),
interpolate_fix9_scale(
self.prev_y - frame.prev_y - off_y - vec_y * i,
self.y - frame.y - off_y - vec_y * i,
state.frame_time,
),
&state.constants.npc.n136_puppy_carried[frame_id],
);
}
batch.draw(ctx)?;
}
batch.draw(ctx)?;
}
}
if self.shock_counter / 2 % 2 != 0 {
return Ok(())
return Ok(());
}
if self.current_weapon != 0 {
@ -791,7 +805,8 @@ impl GameEntity<&NPCList> for Player {
}
{
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "MyChar")?;
let batch =
state.texture_set.get_or_load_batch(ctx, &state.constants, self.skin.get_skin_texture_name())?;
batch.add_rect(
interpolate_fix9_scale(
self.prev_x - self.display_bounds.left as i32 - frame.prev_x,

112
src/player/skin/basic.rs Normal file
View file

@ -0,0 +1,112 @@
use crate::common::{Color, Direction, Rect};
use crate::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin};
#[derive(Clone)]
pub struct BasicPlayerSkin {
texture_name: String,
color: Color,
state: PlayerAnimationState,
appearance: PlayerAppearanceState,
direction: Direction,
tick: u16,
}
impl BasicPlayerSkin {
pub fn new(texture_name: String) -> BasicPlayerSkin {
BasicPlayerSkin {
texture_name,
color: Color::new(1.0, 1.0, 1.0, 1.0),
state: PlayerAnimationState::Idle,
appearance: PlayerAppearanceState::Default,
direction: Direction::Left,
tick: 0,
}
}
}
impl PlayerSkin for BasicPlayerSkin {
fn animation_frame_for(&self, state: PlayerAnimationState, direction: Direction, tick: u16) -> Rect<u16> {
let frame_id = match self.state {
PlayerAnimationState::Idle => 0u16,
PlayerAnimationState::Walking => {
const WALK_INDEXES: [u16; 4] = [1, 0, 2, 0];
WALK_INDEXES[(self.tick as usize / 4) % 4]
}
PlayerAnimationState::WalkingUp => {
const WALK_UP_INDEXES: [u16; 4] = [4, 3, 5, 3];
WALK_UP_INDEXES[(self.tick as usize / 4) % 4]
}
PlayerAnimationState::LookingUp => 3,
PlayerAnimationState::Examining => 7,
PlayerAnimationState::Sitting => 8,
PlayerAnimationState::Collapsed => 9,
PlayerAnimationState::Jumping => 2,
PlayerAnimationState::Falling => 1,
PlayerAnimationState::FallingLookingUp => 4,
PlayerAnimationState::FallingLookingDown => 6,
PlayerAnimationState::FallingUpsideDown => 10,
};
let y_offset = if self.direction == Direction::Left { 0 } else { 16 }
+ match self.appearance {
PlayerAppearanceState::Default => 0,
PlayerAppearanceState::MimigaMask => 32,
PlayerAppearanceState::Custom(i) => (i as u16).saturating_mul(16),
};
Rect::new_size(frame_id * 16, y_offset, 16, 16)
}
fn animation_frame(&self) -> Rect<u16> {
self.animation_frame_for(self.state, self.direction, self.tick)
}
fn tick(&mut self) {
self.tick = self.tick.wrapping_add(1);
}
fn set_state(&mut self, state: PlayerAnimationState) {
if self.state != state {
self.state = state;
self.tick = 0;
}
}
fn get_state(&self) -> PlayerAnimationState {
self.state
}
fn set_appearance(&mut self, appearance: PlayerAppearanceState) {
self.appearance = appearance;
}
fn get_appearance(&mut self) -> PlayerAppearanceState {
self.appearance
}
fn set_color(&mut self, color: Color) {
self.color = color;
}
fn get_color(&self) -> Color {
self.color
}
fn set_direction(&mut self, direction: Direction) {
self.direction = direction;
}
fn get_direction(&self) -> Direction {
self.direction
}
fn get_skin_texture_name(&self) -> &str {
self.texture_name.as_str()
}
fn get_mask_texture_name(&self) -> &str {
""
}
}

98
src/player/skin/mod.rs Normal file
View file

@ -0,0 +1,98 @@
use crate::bitfield;
use crate::common::{Color, Direction, Rect};
pub mod basic;
pub mod pxchar;
bitfield! {
#[derive(Clone, Copy)]
pub struct PlayerSkinFlags(u16);
impl Debug;
pub supports_color, set_supports_color: 0;
}
#[derive(Clone, Copy, PartialEq, Eq)]
/// Represents a player animation state.
pub enum PlayerAnimationState {
Idle,
Walking,
WalkingUp,
LookingUp,
Examining,
Sitting,
Collapsed,
Jumping,
Falling,
FallingLookingUp,
FallingLookingDown,
FallingUpsideDown,
}
/// Represents an alternative appearance of player eg. wearing a Mimiga Mask
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PlayerAppearanceState {
Default,
MimigaMask,
/// Freeware hacks add a <MIMxxxx TSC instruction that sets an offset in player spritesheet to
/// have more than two player appearances.
Custom(u8),
}
/// Represents an interface for implementations of player skin providers.
pub trait PlayerSkin: PlayerSkinClone {
/// Returns animation frame bounds for specified state.
fn animation_frame_for(&self, state: PlayerAnimationState, direction: Direction, tick: u16) -> Rect<u16>;
/// Returns animation frame bounds for current state.
fn animation_frame(&self) -> Rect<u16>;
/// Updates internal animation counters, must be called every game tick.
fn tick(&mut self);
/// Sets the current animation state.
fn set_state(&mut self, state: PlayerAnimationState);
/// Returns current animation state.
fn get_state(&self) -> PlayerAnimationState;
/// Sets the current appearance of skin.
fn set_appearance(&mut self, appearance: PlayerAppearanceState);
/// Returns current appearance of skin.
fn get_appearance(&mut self) -> PlayerAppearanceState;
/// Sets the current color of skin.
fn set_color(&mut self, color: Color);
/// Returns the current color of skin.
fn get_color(&self) -> Color;
/// Sets the current direction;
fn set_direction(&mut self, direction: Direction);
/// Returns the current direction;
fn get_direction(&self) -> Direction;
/// Returns the name of skin spritesheet texture.
fn get_skin_texture_name(&self) -> &str;
/// Returns the name of skin color mask texture.
fn get_mask_texture_name(&self) -> &str;
}
pub trait PlayerSkinClone {
fn clone_box(&self) -> Box<dyn PlayerSkin>;
}
impl<T: 'static + PlayerSkin + Clone> PlayerSkinClone for T {
fn clone_box(&self) -> Box<dyn PlayerSkin> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn PlayerSkin> {
fn clone(&self) -> Box<dyn PlayerSkin> {
self.clone_box()
}
}

141
src/player/skin/pxchar.rs Normal file
View file

@ -0,0 +1,141 @@
use std::io::{Cursor, Read};
use byteorder::{ReadBytesExt, LE};
use num_traits::{AsPrimitive, Num};
use crate::framework::error::GameResult;
use crate::framework::error::GameError::{ResourceLoadError, InvalidValue};
use crate::framework::filesystem;
use crate::framework::context::Context;
struct PxCharReader<T: AsRef<[u8]>> {
cursor: Cursor<T>,
bit_ptr: usize,
}
impl<T: AsRef<[u8]>> PxCharReader<T> {
fn read_integer_shifted<O>(&mut self, bits: usize) -> GameResult<O>
where
O: 'static + Num + Copy,
u128: AsPrimitive<O>,
u64: AsPrimitive<O>,
u32: AsPrimitive<O>,
u16: AsPrimitive<O>,
u8: AsPrimitive<O>,
{
let shift = self.bit_ptr & 7;
self.cursor.set_position((self.bit_ptr / 8) as u64);
self.bit_ptr += bits;
match bits {
// fast paths for aligned bit sizes
0 => Ok((0u8).as_()),
8 if shift == 0 => Ok(self.cursor.read_u8()?.as_()),
16 if shift == 0 => Ok(self.cursor.read_u16::<LE>()?.as_()),
24 if shift == 0 => Ok(self.cursor.read_u24::<LE>()?.as_()),
32 if shift == 0 => Ok(self.cursor.read_u32::<LE>()?.as_()),
48 if shift == 0 => Ok(self.cursor.read_u48::<LE>()?.as_()),
64 if shift == 0 => Ok(self.cursor.read_u64::<LE>()?.as_()),
128 if shift == 0 => Ok(self.cursor.read_u128::<LE>()?.as_()),
// paths for bit shifted numbers
1..=8 => Ok(((self.cursor.read_u16::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
9..=16 => Ok(((self.cursor.read_u24::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
17..=24 => Ok(((self.cursor.read_u32::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
25..=40 => Ok(((self.cursor.read_u48::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
41..=56 => Ok(((self.cursor.read_u64::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
57..=120 => Ok(((self.cursor.read_u128::<LE>()? >> shift) & ((1 << bits) - 1)).as_()),
121..=128 => {
let mut result = self.cursor.read_u128::<LE>()? >> shift;
result |= (self.cursor.read_u8()? as u128) << (128 - shift);
Ok(result.as_())
}
_ => Err(InvalidValue("Cannot read integers bigger than 128 bits.".to_owned())),
}
}
fn read_ranged<O>(&mut self, max_value: u32) -> GameResult<O>
where
O: 'static + Num + Copy,
u128: AsPrimitive<O>,
u64: AsPrimitive<O>,
u32: AsPrimitive<O>,
u16: AsPrimitive<O>,
u8: AsPrimitive<O>,
{
self.read_integer_shifted((32 - max_value.next_power_of_two().leading_zeros()) as usize)
}
fn read_string(&mut self, max_length: u32) -> GameResult<String> {
let mut output = Vec::new();
let length = self.read_ranged::<u32>(max_length)?;
output.reserve(length as usize);
for _ in 0..length {
output.push(self.read_integer_shifted::<u8>(8)?)
}
Ok(String::from_utf8_lossy(&output).to_string())
}
}
impl<T: AsRef<[u8]>> Read for PxCharReader<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// align to byte
if self.bit_ptr & 7 != 0 {
self.bit_ptr = (self.bit_ptr + 7) & !7;
self.cursor.read_u8()?;
}
let result = self.cursor.read(buf);
self.bit_ptr = (self.cursor.position() * 8) as usize;
result
}
}
pub struct PxChar {}
impl PxChar {
pub fn load_pxchar(path: &str, ctx: &mut Context) -> GameResult<PxChar> {
let mut reader = PxCharReader {
cursor: Cursor::new({
let mut stream = filesystem::open(ctx, path)?;
let mut data = Vec::new();
stream.read_to_end(&mut data)?;
data
}),
bit_ptr: 0,
};
let mut magic_buf = [0u8; 6];
reader.read_exact(&mut magic_buf)?;
if &magic_buf != b"PXCHAR" {
return Err(ResourceLoadError("Invalid magic number.".to_string()));
}
let version = reader.read_u8()?;
if version > 5 {
return Err(ResourceLoadError("Unsupported version.".to_string()));
}
let string = reader.read_string(0x100)?;
println!("{}", string);
let description = reader.read_string(0x100)?;
println!("{}", description);
Ok(PxChar {})
}
}
#[test]
fn test() {
use crate::framework::filesystem::mount_vfs;
use crate::framework::vfs::PhysicalFS;
let mut ctx = crate::framework::context::Context::new();
mount_vfs(&mut ctx, Box::new(PhysicalFS::new("data".as_ref(), true)));
println!("lol");
PxChar::load_pxchar("/Player.pxchar", &mut ctx).unwrap();
}

View file

@ -2,9 +2,9 @@ use log::info;
use num_traits::{abs, clamp};
use crate::caret::CaretType;
use crate::common::{fix9_scale, interpolate_fix9_scale, Color, Direction, FadeDirection, FadeState, Rect};
use crate::common::{Color, Direction, FadeDirection, FadeState, fix9_scale, interpolate_fix9_scale, Rect};
use crate::components::boss_life_bar::BossLifeBar;
use crate::components::draw_common::{draw_number, Alignment};
use crate::components::draw_common::{Alignment, draw_number};
use crate::components::flash::Flash;
use crate::components::hud::HUD;
use crate::components::stage_select::StageSelect;
@ -14,7 +14,7 @@ use crate::framework::backend::SpriteBatchCommand;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::framework::graphics::{draw_rect, BlendMode, FilterMode};
use crate::framework::graphics::{BlendMode, draw_rect, FilterMode};
use crate::framework::ui::Components;
use crate::input::touch_controls::TouchControlType;
use crate::inventory::{Inventory, TakeExperienceResult};
@ -22,10 +22,10 @@ use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList;
use crate::npc::NPC;
use crate::physics::PhysicalEntity;
use crate::player::{Player, PlayerAppearance, TargetPlayer};
use crate::player::{Player, TargetPlayer};
use crate::rng::XorShift;
use crate::scene::title_scene::TitleScene;
use crate::scene::Scene;
use crate::scene::title_scene::TitleScene;
use crate::shared_game_state::{Season, SharedGameState};
use crate::stage::{BackgroundType, Stage};
use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM};
@ -120,7 +120,6 @@ impl GameScene {
pub fn add_player2(&mut self) {
self.player2.cond.set_alive(true);
self.player2.cond.set_hidden(self.player1.cond.hidden());
self.player2.appearance = PlayerAppearance::YellowQuote;
self.player2.x = self.player1.x;
self.player2.y = self.player1.y;
self.player2.vel_x = self.player1.vel_x;
@ -1117,6 +1116,8 @@ impl GameScene {
)?;
self.player1.tick_map_collisions(state, &self.npc_list, &mut self.stage);
self.player2.tick_map_collisions(state, &self.npc_list, &mut self.stage);
self.player1.tick_npc_collisions(
TargetPlayer::Player1,
state,
@ -1124,8 +1125,6 @@ impl GameScene {
&mut self.boss,
&mut self.inventory_player1,
);
self.player2.tick_map_collisions(state, &self.npc_list, &mut self.stage);
self.player2.tick_npc_collisions(
TargetPlayer::Player2,
state,
@ -1313,13 +1312,13 @@ impl Scene for GameScene {
state.npc_table.tex_npc1_name = ["Npc/", &self.stage.data.npc1.filename()].join("");
state.npc_table.tex_npc2_name = ["Npc/", &self.stage.data.npc2.filename()].join("");
if state.constants.is_cs_plus {
/*if state.constants.is_cs_plus {
match state.season {
Season::Halloween => self.player1.appearance = PlayerAppearance::HalloweenQuote,
Season::Christmas => self.player1.appearance = PlayerAppearance::ReindeerQuote,
_ => {}
}
}
}*/
self.boss.boss_type = self.stage.data.boss_no as u16;
self.player1.target_x = self.player1.x;
@ -1344,8 +1343,14 @@ impl Scene for GameScene {
state.touch_controls.interact_icon = false;
}
if self.intro_mode && (self.player1.controller.trigger_menu_ok() || self.tick >= 500) {
state.next_scene = Some(Box::new(TitleScene::new()));
if self.intro_mode {
if let TextScriptExecutionState::WaitTicks(_, _, 9999) = state.textscript_vm.state {
state.next_scene = Some(Box::new(TitleScene::new()));
}
if self.player1.controller.trigger_menu_ok() {
state.next_scene = Some(Box::new(TitleScene::new()));
}
}
match state.textscript_vm.mode {

View file

@ -84,10 +84,6 @@ impl TitleScene {
// asset copyright for freeware version
static COPYRIGHT_PIXEL: &str = "2004.12 Studio Pixel";
// asset copyright for Nicalis
static COPYRIGHT_NICALIS: &str = "@2011 NICALIS INC.";
static COPYRIGHT_NICALIS_SWITCH: &str = "@2017 NICALIS INC.";
static DISCORD_LINK: &str = "https://discord.gg/fbRsNNB";
impl Scene for TitleScene {
@ -261,14 +257,7 @@ impl Scene for TitleScene {
}
self.draw_text_centered(VERSION_BANNER.as_str(), state.canvas_size.1 - 15.0, state, ctx)?;
if state.constants.is_switch {
self.draw_text_centered(COPYRIGHT_NICALIS_SWITCH, state.canvas_size.1 - 30.0, state, ctx)?;
} else if state.constants.is_cs_plus {
self.draw_text_centered(COPYRIGHT_NICALIS, state.canvas_size.1 - 30.0, state, ctx)?;
} else {
self.draw_text_centered(COPYRIGHT_PIXEL, state.canvas_size.1 - 30.0, state, ctx)?;
}
self.draw_text_centered(COPYRIGHT_PIXEL, state.canvas_size.1 - 30.0, state, ctx)?;
match self.current_menu {
CurrentMenu::MainMenu => {

View file

@ -731,7 +731,6 @@ impl TextScriptVM {
FromPrimitive::from_i32(read_cur_varint(&mut cursor).unwrap_or_else(|_| OpCode::END as i32));
if let Some(op) = op_maybe {
log::info!("opcode: {:?}", op);
match op {
OpCode::_NOP => {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);