add carets and fix booster

This commit is contained in:
Alula 2020-08-20 20:31:47 +02:00
parent 1993720f34
commit ef9ca2e89a
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
13 changed files with 544 additions and 223 deletions

View File

@ -1,6 +1,6 @@
# doukutsu-rs
A re-implementation of Cave Story (Doukutsu Monogatari) engine written in [Rust](https://www.rust-lang.org/), aiming for accuracy and cleaner code.
A re-implementation of Cave Story (Doukutsu Monogatari) engine written in [Rust](https://www.rust-lang.org/), aiming for behavior accuracy and cleaner code.
Later plans might involve turning it into a fully-featured modding tool with live debugging and stuff.
**The project is still in a very early state and nowhere near being playable. Expect lots of breaking changes and bugs**
@ -26,7 +26,7 @@ The engine should work fine with [CSE2-Enhanced](https://github.com/Clownacy/CSE
The project is a result of me wanting to build something in a new programming language for memes.
I had an idea of writing my own CS engine long time before and I would've very likely picked C++17/20+SDL2, but after
I had an idea of writing my own CS engine long time before and I would've very likely picked C++17/20 and SDL2, but after
all I've picked Rust instead because it seemed quite interesting for me.
Would 90% of end-users running this thing care about the programming language software was written in? After all who tf cares if the performance is the same (and maybe a slightly better), but you also get a lot of various benefits?

132
src/caret.rs Normal file
View File

@ -0,0 +1,132 @@
use crate::bitfield;
use crate::common::{Direction, Rect};
use crate::engine_constants::EngineConstants;
bitfield! {
pub struct Cond(u16);
impl Debug;
pub visible, set_visible: 7;
}
#[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)]
pub enum CaretType {
None,
Bubble,
ProjectileDissipation,
Shoot,
SnakeAfterimage,
Zzz,
SnakeAfterimage2,
Exhaust,
DrownedQuote,
QuestionMark,
LevelUp,
HurtParticles,
Explosion,
SmallParticles,
Unknown,
SmallProjectileDissipation,
Empty,
PushJumpKey,
}
pub struct Caret {
pub ctype: CaretType,
pub x: isize,
pub y: isize,
pub vel_x: isize,
pub vel_y: isize,
pub offset_x: isize,
pub offset_y: isize,
pub cond: Cond,
pub direct: Direction,
pub anim_rect: Rect<usize>,
anim_num: usize,
anim_wait: isize,
}
impl Caret {
pub fn new(x: isize, y: isize, ctype: CaretType, direct: Direction, constants: &EngineConstants) -> Self {
let (offset_x, offset_y) = constants.caret.offsets[ctype as usize];
Self {
ctype,
x,
y,
vel_x: 0,
vel_y: 0,
offset_x,
offset_y,
cond: Cond(0x80),
direct,
anim_rect: Rect::<usize>::new(0, 0, 0, 0),
anim_num: 0,
anim_wait: 0,
}
}
pub fn tick(&mut self, constants: &EngineConstants) {
match self.ctype {
CaretType::None => {}
CaretType::Bubble => {}
CaretType::ProjectileDissipation => {}
CaretType::Shoot => {}
CaretType::SnakeAfterimage | CaretType::SnakeAfterimage2 => { // dupe, unused
}
CaretType::Zzz => {}
CaretType::Exhaust => {
self.anim_wait += 1;
if self.anim_wait > 1 {
self.anim_wait = 0;
self.anim_num += 1;
}
if self.anim_num >= constants.caret.exhaust_rects.len() {
self.cond.set_visible(false);
return;
}
self.anim_rect = constants.caret.exhaust_rects[self.anim_num];
match self.direct {
Direction::Left => { self.x -= 0x400; }
Direction::Up => { self.y -= 0x400; }
Direction::Right => { self.x += 0x400; }
Direction::Bottom => { self.y += 0x400; }
}
}
CaretType::DrownedQuote => {}
CaretType::QuestionMark => {
self.anim_wait += 1;
if self.anim_wait < 5 {
self.y -= 0x800;
}
if self.anim_wait == 32 {
self.cond.set_visible(false);
}
self.anim_rect = match self.direct {
Direction::Left => { constants.caret.question_left_rect }
Direction::Right => { constants.caret.question_right_rect }
_ => { self.anim_rect }
}
}
CaretType::LevelUp => {}
CaretType::HurtParticles => {}
CaretType::Explosion => {}
CaretType::SmallParticles => {}
CaretType::Unknown => {
// not implemented because it was apparently broken in og game?
self.cond.set_visible(false);
}
CaretType::SmallProjectileDissipation => {}
CaretType::Empty => {}
CaretType::PushJumpKey => {}
}
}
pub fn is_dead(&self) -> bool {
!self.cond.visible()
}
}

View File

@ -25,6 +25,15 @@ impl Direction {
_ => { None }
}
}
pub fn opposite(&self) -> Direction {
match self {
Direction::Left => { Direction::Right }
Direction::Up => { Direction::Bottom }
Direction::Right => { Direction::Left }
Direction::Bottom => { Direction::Up }
}
}
}
#[derive(Debug, Clone, Copy)]

View File

@ -1,6 +1,6 @@
use log::info;
use std::collections::HashMap;
use log::info;
use maplit::hashmap;
use crate::common::{Direction, Rect};
@ -21,6 +21,7 @@ pub struct PhysicsConsts {
#[derive(Debug, Copy, Clone)]
pub struct BoosterConsts {
pub fuel: usize,
pub b2_0_up: isize,
pub b2_0_up_nokey: isize,
pub b2_0_down: isize,
@ -36,8 +37,8 @@ pub struct MyCharConsts {
pub direction: Direction,
pub view: Rect<usize>,
pub hit: Rect<usize>,
pub life: u16,
pub max_life: u16,
pub life: usize,
pub max_life: usize,
pub unit: u8,
pub air_physics: PhysicsConsts,
pub water_physics: PhysicsConsts,
@ -45,20 +46,45 @@ pub struct MyCharConsts {
pub animations_right: [Rect<usize>; 12],
}
#[derive(Debug)]
pub struct CaretConsts {
pub offsets: [(isize, isize); 18],
pub bubble_left_rects: Vec<Rect<usize>>,
pub bubble_right_rects: Vec<Rect<usize>>,
pub exhaust_rects: Vec<Rect<usize>>,
pub question_left_rect: Rect<usize>,
pub question_right_rect: Rect<usize>,
}
impl Clone for CaretConsts {
fn clone(&self) -> Self {
Self {
offsets: self.offsets,
bubble_left_rects: self.bubble_left_rects.clone(),
bubble_right_rects: self.bubble_right_rects.clone(),
exhaust_rects: self.exhaust_rects.clone(),
question_left_rect: self.question_left_rect,
question_right_rect: self.question_right_rect,
}
}
}
#[derive(Debug)]
pub struct EngineConstants {
pub is_cs_plus: bool,
pub my_char: MyCharConsts,
pub booster: BoosterConsts,
pub caret: CaretConsts,
pub tex_sizes: HashMap<String, (usize, usize)>,
}
impl Clone for EngineConstants {
fn clone(&self) -> Self {
EngineConstants {
Self {
is_cs_plus: self.is_cs_plus,
my_char: self.my_char,
booster: self.booster,
caret: self.caret.clone(),
tex_sizes: self.tex_sizes.clone(),
}
}
@ -128,11 +154,57 @@ impl EngineConstants {
],
},
booster: BoosterConsts {
fuel: 50,
b2_0_up: -0x5ff,
b2_0_up_nokey: -0x5ff,
b2_0_down: 0x5ff,
b2_0_left: -0x5ff,
b2_0_right: 0x5ff
b2_0_right: 0x5ff,
},
caret: CaretConsts {
offsets: [
(0, 0),
(4 * 0x200, 4 * 0x200),
(8 * 0x200, 8 * 0x200),
(8 * 0x200, 8 * 0x200),
(8 * 0x200, 8 * 0x200),
(4 * 0x200, 4 * 0x200),
(8 * 0x200, 8 * 0x200),
(4 * 0x200, 4 * 0x200),
(8 * 0x200, 8 * 0x200),
(8 * 0x200, 8 * 0x200),
(28 * 0x200, 8 * 0x200),
(4 * 0x200, 4 * 0x200),
(16 * 0x200, 16 * 0x200),
(4 * 0x200, 4 * 0x200),
(20 * 0x200, 20 * 0x200),
(4 * 0x200, 4 * 0x200),
(20 * 0x200, 4 * 0x200),
(52 * 0x200, 4 * 0x200),
],
bubble_left_rects: vec![
Rect { left: 0, top: 64, right: 8, bottom: 72 },
Rect { left: 8, top: 64, right: 16, bottom: 72 },
Rect { left: 16, top: 64, right: 24, bottom: 72 },
Rect { left: 24, top: 64, right: 32, bottom: 72 },
],
bubble_right_rects: vec![
Rect { left: 64, top: 24, right: 72, bottom: 32 },
Rect { left: 72, top: 24, right: 80, bottom: 32 },
Rect { left: 80, top: 24, right: 88, bottom: 32 },
Rect { left: 88, top: 24, right: 96, bottom: 32 },
],
exhaust_rects: vec![
Rect { left: 56, top: 0, right: 64, bottom: 8 },
Rect { left: 64, top: 0, right: 72, bottom: 8 },
Rect { left: 72, top: 0, right: 80, bottom: 8 },
Rect { left: 80, top: 0, right: 88, bottom: 8 },
Rect { left: 88, top: 0, right: 96, bottom: 8 },
Rect { left: 96, top: 0, right: 104, bottom: 8 },
Rect { left: 104, top: 0, right: 112, bottom: 8 },
],
question_left_rect: Rect { left: 0, top: 80, right: 16, bottom: 96 },
question_right_rect: Rect { left: 48, top: 64, right: 64, bottom: 80 },
},
tex_sizes: hashmap! {
str!("ArmsImage") => (256, 16),
@ -257,6 +329,7 @@ impl EngineConstants {
info!("Applying Cave Story+ constants patches...");
self.is_cs_plus = true;
self.tex_sizes.insert(str!("Caret"), (320, 320));
self.tex_sizes.insert(str!("MyChar"), (200, 384));
}
}

View File

@ -79,6 +79,7 @@
//!
//! ```
#![allow(dead_code)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]

View File

@ -1,12 +1,13 @@
use crate::ggez::{Context, GameResult};
use imgui::{Condition, im_str, ImStr, ImString, Window};
use itertools::Itertools;
use crate::ggez::{Context, GameResult};
use crate::scene::game_scene::GameScene;
use crate::SharedGameState;
pub struct LiveDebugger {
selected_item: i32,
map_selector_visible: bool,
stages: Vec<ImString>,
error: Option<ImString>,
}
@ -15,21 +16,37 @@ impl LiveDebugger {
pub fn new() -> Self {
Self {
selected_item: -1,
map_selector_visible: false,
stages: vec![],
error: None,
}
}
pub fn run_ingame(&mut self, game_scene: &mut GameScene, state: &mut SharedGameState, ctx: &mut Context, ui: &mut imgui::Ui) -> GameResult {
/*Window::new(im_str!("Live Debugger"))
Window::new(im_str!("Live Debugger"))
.position([5.0, 5.0], Condition::FirstUseEver)
.size([300.0, 100.0], Condition::FirstUseEver)
.build(ui, || {
ui.text(format!(
"Player position: ({:.1},{:.1})",
state.player.x as f32 / 512.0,
state.player.y as f32 / 512.0,
game_scene.player.x as f32 / 512.0,
game_scene.player.y as f32 / 512.0,
));
});*/
ui.text(format!(
"Player velocity: ({:.1},{:.1})",
game_scene.player.vel_x as f32 / 512.0,
game_scene.player.vel_y as f32 / 512.0,
));
ui.text(format!(
"Booster fuel: ({})", game_scene.player.booster_fuel
));
if ui.button(im_str!("Map Selector"), [0.0, 0.0]) {
self.map_selector_visible = true;
}
});
if self.error.is_some() {
Window::new(im_str!("Error!"))
@ -47,39 +64,41 @@ impl LiveDebugger {
});
}
Window::new(im_str!("Map selector"))
.resizable(false)
.collapsed(true, Condition::FirstUseEver)
.size([240.0, 280.0], Condition::FirstUseEver)
.build(ui, || {
if self.stages.is_empty() {
for s in state.stages.iter() {
self.stages.push(ImString::new(s.name.to_owned()));
}
self.selected_item = match state.stages.iter().find_position(|s| s.name == game_scene.stage.data.name) {
Some((pos, _)) => { pos as i32 }
_ => { -1 }
};
}
let stages: Vec<&ImStr> = self.stages.iter().map(|e| e.as_ref()).collect();
ui.push_item_width(-1.0);
ui.list_box(im_str!(""), &mut self.selected_item, &stages, 10);
if ui.button(im_str!("Load"), [0.0, 0.0]) {
match GameScene::new(state, ctx, self.selected_item as usize) {
Ok(mut scene) => {
scene.player.x = (scene.stage.map.width / 2 * 16 * 0x200) as isize;
scene.player.y = (scene.stage.map.height / 2 * 16 * 0x200) as isize;
state.next_scene = Some(Box::new(scene));
if self.map_selector_visible {
Window::new(im_str!("Map selector"))
.resizable(false)
.position([5.0, 35.0], Condition::FirstUseEver)
.size([240.0, 280.0], Condition::FirstUseEver)
.build(ui, || {
if self.stages.is_empty() {
for s in state.stages.iter() {
self.stages.push(ImString::new(s.name.to_owned()));
}
Err(e) => {
self.error = Some(ImString::new(e.to_string()));
self.selected_item = match state.stages.iter().find_position(|s| s.name == game_scene.stage.data.name) {
Some((pos, _)) => { pos as i32 }
_ => { -1 }
};
}
let stages: Vec<&ImStr> = self.stages.iter().map(|e| e.as_ref()).collect();
ui.push_item_width(-1.0);
ui.list_box(im_str!(""), &mut self.selected_item, &stages, 10);
if ui.button(im_str!("Load"), [0.0, 0.0]) {
match GameScene::new(state, ctx, self.selected_item as usize) {
Ok(mut scene) => {
scene.player.x = (scene.stage.map.width / 2 * 16 * 0x200) as isize;
scene.player.y = (scene.stage.map.height / 2 * 16 * 0x200) as isize;
state.next_scene = Some(Box::new(scene));
}
Err(e) => {
self.error = Some(ImString::new(e.to_string()));
}
}
}
}
});
});
}
Ok(())
}

View File

@ -35,7 +35,10 @@ use crate::sound::SoundManager;
use crate::stage::StageData;
use crate::texture_set::TextureSet;
use crate::ui::UI;
use crate::caret::{Caret, CaretType};
use crate::common::Direction;
mod caret;
mod common;
mod engine_constants;
mod entity;
@ -46,12 +49,14 @@ mod live_debugger;
mod map;
mod player;
mod player_hit;
mod rng;
mod scene;
mod stage;
mod sound;
mod text_script;
mod texture_set;
mod ui;
mod weapon;
bitfield! {
pub struct KeyState(u16);
@ -86,6 +91,7 @@ struct Game {
pub struct SharedGameState {
pub flags: GameFlags,
pub carets: Vec<Caret>,
pub key_state: KeyState,
pub key_trigger: KeyState,
pub texture_set: TextureSet,
@ -107,6 +113,18 @@ impl SharedGameState {
self.key_old = self.key_state.0;
self.key_trigger = KeyState(trigger);
}
pub fn tick_carets(&mut self) {
for caret in self.carets.iter_mut() {
caret.tick(&self.constants);
}
self.carets.retain(|c| !c.is_dead());
}
pub fn create_caret(&mut self, x: isize, y: isize, ctype: CaretType, direct: Direction) {
self.carets.push(Caret::new(x, y, ctype, direct, &self.constants));
}
}
impl Game {
@ -137,6 +155,7 @@ impl Game {
def_matrix: DrawParam::new().to_matrix(),
state: SharedGameState {
flags: GameFlags(0),
carets: Vec::new(),
key_state: KeyState(0),
key_trigger: KeyState(0),
texture_set: TextureSet::new(base_path),

View File

@ -1,11 +1,11 @@
use crate::ggez::{Context, GameResult};
use num_traits::clamp;
use crate::bitfield;
use crate::caret::CaretType;
use crate::common::{Direction, Rect};
use crate::engine_constants::PhysicsConsts;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::ggez::{Context, GameResult};
use crate::SharedGameState;
use crate::str;
@ -69,31 +69,30 @@ bitfield! {
pub struct Player {
pub x: isize,
pub y: isize,
pub xm: isize,
pub ym: isize,
pub vel_x: isize,
pub vel_y: isize,
pub target_x: isize,
pub target_y: isize,
pub life: usize,
pub max_life: usize,
pub cond: Cond,
pub flags: Flags,
pub equip: Equip,
pub direction: Direction,
pub view: Rect<usize>,
pub hit: Rect<usize>,
pub life: u16,
pub max_life: u16,
pub unit: u8,
pub air_physics: PhysicsConsts,
pub water_physics: PhysicsConsts,
pub question: bool,
pub booster_fuel: usize,
index_x: isize,
index_y: isize,
sprash: bool,
ques: bool,
up: bool,
down: bool,
shock: u8,
shock_counter: u8,
booster_switch: u8,
star: u8,
bubble: u8,
boost_sw: u8,
boost_cnt: isize,
exp_wait: isize,
exp_count: isize,
anim_num: usize,
@ -111,31 +110,30 @@ impl Player {
Ok(Player {
x: 0,
y: 0,
xm: 0,
ym: 0,
vel_x: 0,
vel_y: 0,
target_x: 0,
target_y: 0,
life: constants.my_char.life,
max_life: constants.my_char.max_life,
cond: Cond(constants.my_char.cond),
flags: Flags(constants.my_char.flags),
equip: Equip(constants.my_char.equip),
direction: constants.my_char.direction.clone(),
view: constants.my_char.view,
hit: constants.my_char.hit,
life: constants.my_char.life,
max_life: constants.my_char.max_life,
unit: constants.my_char.unit,
air_physics: constants.my_char.air_physics,
water_physics: constants.my_char.water_physics,
question: false,
booster_fuel: 0,
index_x: 0,
index_y: 0,
sprash: false,
ques: false,
up: false,
down: false,
shock: 0,
shock_counter: 0,
booster_switch: 0,
star: 0,
bubble: 0,
boost_sw: 0,
boost_cnt: 0,
exp_wait: 0,
exp_count: 0,
anim_num: 0,
@ -145,42 +143,42 @@ impl Player {
})
}
fn tick_normal(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<()> {
fn tick_normal(&mut self, state: &mut SharedGameState) -> GameResult {
if self.cond.cond_x02() {
return Ok(());
}
let physics = if self.flags.underwater() { &self.water_physics } else { &self.air_physics };
let physics = if self.flags.underwater() { state.constants.my_char.water_physics } else { state.constants.my_char.air_physics };
self.ques = false;
self.question = false;
if !state.flags.control_enabled() {
self.boost_sw = 0;
self.booster_switch = 0;
}
// todo: split those into separate procedures and refactor (try to not break the logic!)
// ground movement
if self.flags.flag_x08() || self.flags.flag_x10() || self.flags.flag_x20() {
self.boost_sw = 0;
self.booster_switch = 0;
if self.equip.has_booster_0_8() || self.equip.has_booster_2_0() {
self.boost_cnt = 50;
self.booster_fuel = state.constants.booster.fuel;
} else {
self.boost_cnt = 0;
self.booster_fuel = 0;
}
if state.flags.control_enabled() {
if state.key_trigger.only_down() && state.key_state.only_down() && !self.cond.cond_x01() && state.flags.flag_x04() {
if state.key_trigger.only_down() && state.key_state.only_down() && !self.cond.cond_x01() && !state.flags.flag_x04() {
self.cond.set_cond_x01(true);
self.ques = true;
self.question = true;
} else {
if state.key_state.left() && self.xm > -physics.max_dash {
self.xm -= physics.dash_ground;
if state.key_state.left() && self.vel_x > -physics.max_dash {
self.vel_x -= physics.dash_ground;
}
if state.key_state.right() && self.xm < physics.max_dash {
self.xm += physics.dash_ground;
if state.key_state.right() && self.vel_x < physics.max_dash {
self.vel_x += physics.dash_ground;
}
if state.key_state.left() {
@ -194,62 +192,61 @@ impl Player {
}
if !self.cond.cond_x20() {
if self.xm < 0 {
if self.xm > -physics.resist {
self.xm = 0;
if self.vel_x < 0 {
if self.vel_x > -physics.resist {
self.vel_x = 0;
} else {
self.xm += physics.resist;
self.vel_x += physics.resist;
}
} else if self.xm > 0 {
if self.xm < physics.resist {
self.xm = 0;
}
if self.vel_x > 0 {
if self.vel_x < physics.resist {
self.vel_x = 0;
} else {
self.xm -= physics.resist;
self.vel_x -= physics.resist;
}
}
}
} else { // air movement
if state.flags.control_enabled() {
if (self.equip.has_booster_0_8() || self.equip.has_booster_2_0()) && state.key_trigger.jump() && self.boost_cnt != 0 {
if state.key_trigger.jump() && self.booster_fuel != 0 {
if self.equip.has_booster_0_8() {
self.boost_sw = 1;
self.booster_switch = 1;
if self.ym > 0x100 { // 0.5fix9
self.ym /= 2;
if self.vel_y > 0x100 { // 0.5fix9
self.vel_y /= 2;
}
}
if self.equip.has_booster_2_0() {
} else if self.equip.has_booster_2_0() {
if state.key_state.up() {
self.boost_sw = 2;
self.xm = 0;
self.ym = state.constants.booster.b2_0_up;
self.booster_switch = 2;
self.vel_x = 0;
self.vel_y = state.constants.booster.b2_0_up;
} else if state.key_state.left() {
self.boost_sw = 2;
self.xm = 0;
self.ym = state.constants.booster.b2_0_left;
self.booster_switch = 1;
self.vel_x = state.constants.booster.b2_0_left;
self.vel_y = 0;
} else if state.key_state.right() {
self.boost_sw = 2;
self.xm = 0;
self.ym = state.constants.booster.b2_0_right;
self.booster_switch = 1;
self.vel_x = state.constants.booster.b2_0_right;
self.vel_y = 0;
} else if state.key_state.down() {
self.boost_sw = 2;
self.xm = 0;
self.ym = state.constants.booster.b2_0_down;
self.booster_switch = 3;
self.vel_x = 0;
self.vel_y = state.constants.booster.b2_0_down;
} else {
self.boost_sw = 2;
self.xm = 0;
self.ym = state.constants.booster.b2_0_up_nokey;
self.booster_switch = 2;
self.vel_x = 0;
self.vel_y = state.constants.booster.b2_0_up_nokey;
}
}
}
if state.key_state.left() && self.xm > -physics.max_dash {
self.xm -= physics.dash_air;
if state.key_state.left() && self.vel_x > -physics.max_dash {
self.vel_x -= physics.dash_air;
}
if state.key_state.right() && self.xm < physics.max_dash {
self.xm += physics.dash_air;
if state.key_state.right() && self.vel_x < physics.max_dash {
self.vel_x += physics.dash_air;
}
if state.key_state.left() {
@ -261,16 +258,16 @@ impl Player {
}
}
if self.equip.has_booster_2_0() && self.boost_sw != 0 && !state.key_state.jump() || self.boost_cnt == 0 {
match self.boost_sw {
1 => { self.xm /= 2 }
2 => { self.ym /= 2 }
if self.equip.has_booster_2_0() && self.booster_switch != 0 && (!state.key_state.jump() || self.booster_fuel == 0) {
match self.booster_switch {
1 => { self.vel_x /= 2 }
2 => { self.vel_y /= 2 }
_ => {}
}
}
if self.boost_cnt == 0 || !state.key_state.jump() {
self.boost_cnt = 0;
if self.booster_fuel == 0 || !state.key_state.jump() {
self.booster_switch = 0;
}
}
@ -281,7 +278,7 @@ impl Player {
if state.key_trigger.jump() && (self.flags.flag_x08() || self.flags.flag_x10() || self.flags.flag_x20()) {
if !self.flags.force_up() {
self.ym = -physics.jump;
self.vel_y = -physics.jump;
// todo: PlaySoundObject(15, SOUND_MODE_PLAY);
}
}
@ -293,110 +290,107 @@ impl Player {
}
// booster losing fuel
if self.boost_sw != 0 && self.boost_cnt != 0 {
self.boost_cnt -= 1;
if self.booster_switch != 0 && self.booster_fuel != 0 {
self.booster_fuel -= 1;
}
// wind / current forces
if self.flags.force_left() {
self.xm -= 0x88;
self.vel_x -= 0x88;
}
if self.flags.force_up() {
self.ym -= 0x80;
self.vel_y -= 0x80;
}
if self.flags.force_right() {
self.xm += 0x80;
self.vel_x += 0x80;
}
if self.flags.force_down() {
self.ym += 0x55;
self.vel_y += 0x55;
}
if self.equip.has_booster_2_0() && self.boost_sw != 0 {
match self.boost_sw {
if self.equip.has_booster_2_0() && self.booster_switch != 0 {
match self.booster_switch {
1 => {
if self.flags.flag_x01() || self.flags.flag_x04() {
self.ym = -0x100; // -0.5fix9
self.vel_y = -0x100; // -0.5fix9
}
if self.direction == Direction::Left {
self.xm -= 0x20; // 0.1fix9
self.vel_x -= 0x20; // 0.1fix9
}
if self.direction == Direction::Right {
self.xm += 0x20; // 0.1fix9
self.vel_x += 0x20; // 0.1fix9
}
// todo: particles and sound
if state.key_trigger.jump() || self.boost_cnt % 3 == 1 {
if self.direction == Direction::Left {
// SetCaret(self.x + 2 * 0x200, self.y + 2 * 0x200, 7, 2);
}
if self.direction == Direction::Right {
// SetCaret(self.x + 2 * 0x200, self.y + 2 * 0x200, 7, 0);
// todo: sound
if state.key_trigger.jump() || self.booster_fuel % 3 == 1 {
if self.direction == Direction::Left || self.direction == Direction::Right {
state.create_caret(self.x + 0x400, self.y + 0x400, CaretType::Exhaust, self.direction.opposite());
}
// PlaySoundObject(113, SOUND_MODE_PLAY);
}
}
2 => {
self.ym -= 0x20;
self.vel_y -= 0x20;
// todo: particles and sound
if state.key_trigger.jump() || self.boost_cnt % 3 == 1 {
// SetCaret(self.x, self.y + 6 * 0x200, 7, 3);
// todo: sound
if state.key_trigger.jump() || self.booster_fuel % 3 == 1 {
state.create_caret(self.x, self.y + 6 * 0x200, CaretType::Exhaust, Direction::Bottom);
// PlaySoundObject(113, SOUND_MODE_PLAY);
}
}
// todo: particles and sound
3 if state.key_trigger.jump() || self.boost_cnt % 3 == 1 => {
// SetCaret(self.x, self.y + 6 * 0x200, 7, 1);
// todo: sound
3 if state.key_trigger.jump() || self.booster_fuel % 3 == 1 => {
state.create_caret(self.x, self.y + 6 * 0x200, CaretType::Exhaust, Direction::Up);
// PlaySoundObject(113, SOUND_MODE_PLAY);
}
_ => {}
}
} else if self.flags.force_up() {
self.ym += physics.gravity_air;
} else if self.equip.has_booster_0_8() && self.boost_sw != 0 && self.ym > -0x400 {
self.ym -= 0x20;
self.vel_y += physics.gravity_air;
} else if self.equip.has_booster_0_8() && self.booster_switch != 0 && self.vel_y > -0x400 {
self.vel_y -= 0x20;
if self.boost_cnt % 3 == 0 {
// SetCaret(self.x, self.y + self.hit.bottom as isize / 2, 7, 3);
if self.booster_fuel % 3 == 0 {
state.create_caret(self.x, self.y + self.hit.bottom as isize / 2, CaretType::Exhaust, Direction::Bottom);
// PlaySoundObject(113, SOUND_MODE_PLAY);
}
// bounce off of ceiling
if self.flags.flag_x02() {
self.ym = 0x200; // 1.0fix9
self.vel_y = 0x200; // 1.0fix9
}
} else if self.ym < 0 && state.flags.control_enabled() && state.key_state.jump() {
self.ym += physics.gravity_air;
} else if self.vel_y < 0 && state.flags.control_enabled() && state.key_state.jump() {
self.vel_y += physics.gravity_air;
} else {
self.ym += physics.gravity_ground;
self.vel_y += physics.gravity_ground;
}
if !state.flags.control_enabled() || !state.key_trigger.jump() {
if self.flags.flag_x10() && self.xm < 0 {
self.ym = -self.xm;
if self.flags.flag_x10() && self.vel_x < 0 {
self.vel_y = -self.vel_x;
}
if self.flags.flag_x20() && self.xm > 0 {
self.ym = self.xm;
if self.flags.flag_x20() && self.vel_x > 0 {
self.vel_y = self.vel_x;
}
if (self.flags.flag_x08() && self.flags.flag_x80000() && self.xm < 0)
|| (self.flags.flag_x08() && self.flags.flag_x10000() && self.xm > 0)
if (self.flags.flag_x08() && self.flags.flag_x80000() && self.vel_x < 0)
|| (self.flags.flag_x08() && self.flags.flag_x10000() && self.vel_x > 0)
|| (self.flags.flag_x08() && self.flags.flag_x20000() && self.flags.flag_x40000()) {
self.ym = 0x400; // 2.0fix9
self.vel_y = 0x400; // 2.0fix9
}
}
let max_move = if self.flags.underwater() && !(self.flags.force_left() || self.flags.force_up() || self.flags.force_right() || self.flags.force_down()) {
self.water_physics.max_move
physics.max_move
} else {
self.air_physics.max_move
physics.max_move
};
self.xm = clamp(self.xm, -max_move, max_move);
self.ym = clamp(self.ym, -max_move, max_move);
self.vel_x = clamp(self.vel_x, -max_move, max_move);
self.vel_y = clamp(self.vel_y, -max_move, max_move);
// todo: water splashing
@ -406,7 +400,7 @@ impl Player {
// spike damage
if self.flags.flag_x400() {
//self.damage(10); // todo: borrow checker yells at me
self.damage(10);
}
// camera
@ -423,14 +417,14 @@ impl Player {
}
if state.flags.control_enabled() && state.key_state.up() {
self.index_x -= 0x200; // 1.0fix9
if self.index_x < -0x8000 { // -64.0fix9
self.index_x = -0x8000;
self.index_y -= 0x200; // 1.0fix9
if self.index_y < -0x8000 { // -64.0fix9
self.index_y = -0x8000;
}
} else if state.flags.control_enabled() && state.key_state.down() {
self.index_x += 0x200; // 1.0fix9
if self.index_x > 0x8000 { // -64.0fix9
self.index_x = 0x8000;
self.index_y += 0x200; // 1.0fix9
if self.index_y > 0x8000 { // -64.0fix9
self.index_y = 0x8000;
}
} else {
if self.index_y > 0x200 { // 1.0fix9
@ -445,16 +439,16 @@ impl Player {
self.target_x = self.x + self.index_x;
self.target_y = self.y + self.index_y;
if self.xm > physics.resist || self.xm < -physics.resist {
self.x += self.xm;
if self.vel_x > physics.resist || self.vel_x < -physics.resist {
self.x += self.vel_x;
}
self.y += self.ym;
self.y += self.vel_y;
Ok(())
}
fn tick_stream(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<()> {
fn tick_stream(&mut self, state: &mut SharedGameState) -> GameResult {
Ok(())
}
@ -518,7 +512,7 @@ impl Player {
} else if state.key_state.down() {
self.anim_num = 10;
} else {
self.anim_num = if self.ym > 0 { 1 } else { 3 };
self.anim_num = if self.vel_y > 0 { 1 } else { 3 };
}
match self.direction {
@ -532,11 +526,29 @@ impl Player {
}
}
pub fn damage(&mut self, hp: isize) {}
pub fn damage(&mut self, hp: isize) {
if self.shock_counter > 0 {
return;
}
// PlaySoundObject(16, SOUND_MODE_PLAY); // todo: damage sound
self.shock_counter = 128;
self.cond.set_cond_x01(false);
if self.unit != 1 {
self.vel_y = -0x400; // -2.0fix9
}
self.life = if hp >= self.life as isize { 0 } else { (self.life as isize - hp) as usize };
if self.equip.has_whimsical_star() && self.star > 0 {
self.star -= 1;
}
}
}
impl GameEntity for Player {
fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<()> {
fn tick(&mut self, state: &mut SharedGameState, _ctx: &mut Context) -> GameResult {
if !self.cond.visible() {
return Ok(());
}
@ -545,8 +557,8 @@ impl GameEntity for Player {
self.exp_wait -= 1;
}
if self.shock != 0 {
self.shock -= 1;
if self.shock_counter != 0 {
self.shock_counter -= 1;
} else if self.exp_count != 0 {
// SetValueView(&self.x, &self.y, self.exp_count); // todo: damage popup
self.exp_count = 0;
@ -558,10 +570,10 @@ impl GameEntity for Player {
// AirProcess(); // todo
}
self.tick_normal(state, ctx)?;
self.tick_normal(state)?;
}
1 => {
self.tick_stream(state, ctx)?;
self.tick_stream(state)?;
}
_ => {}
}

View File

@ -3,6 +3,8 @@ use num_traits::clamp;
use crate::player::Player;
use crate::stage::Stage;
use crate::SharedGameState;
use crate::caret::CaretType;
use crate::common::Direction;
const OFF_X: &[isize; 4] = &[0, 1, 0, 1];
const OFF_Y: &[isize; 4] = &[0, 0, 1, 1];
@ -16,12 +18,12 @@ impl Player {
&& (self.x - self.hit.right as isize) > x * 0x10 * 0x200 {
self.x = ((x * 0x10 + 8) * 0x200) + self.hit.right as isize;
if self.xm < -0x180 {
self.xm = -0x180;
if self.vel_x < -0x180 {
self.vel_x = -0x180;
}
if !state.key_state.left() && self.xm < 0 {
self.xm = 0;
if !state.key_state.left() && self.vel_x < 0 {
self.vel_x = 0;
}
self.flags.set_flag_x01(true);
@ -34,12 +36,12 @@ impl Player {
&& (self.x + self.hit.right as isize) < x * 0x10 * 0x200 {
self.x = ((x * 0x10 - 8) * 0x200) - self.hit.right as isize;
if self.xm > 0x180 {
self.xm = 0x180;
if self.vel_x > 0x180 {
self.vel_x = 0x180;
}
if !state.key_state.right() && self.xm > 0 {
self.xm = 0;
if !state.key_state.right() && self.vel_x > 0 {
self.vel_x = 0;
}
self.flags.set_flag_x04(true);
@ -52,12 +54,12 @@ impl Player {
&& (self.y - self.hit.top as isize) > y * 0x10 * 0x200 {
self.y = ((y * 0x10 + 8) * 0x200) + self.hit.top as isize;
if !self.cond.cond_x02() && self.ym < -0x200 {
if !self.cond.cond_x02() && self.vel_y < -0x200 {
// PutLittleStar(); todo
}
if self.ym < 0 {
self.ym = 0;
if self.vel_y < 0 {
self.vel_y = 0;
}
self.flags.set_flag_x02(true);
@ -70,12 +72,12 @@ impl Player {
&& ((self.y + self.hit.bottom as isize) < y * 0x10 * 0x200) {
self.y = ((y * 0x10 - 8) * 0x200) - self.hit.bottom as isize;
if self.ym > 0x400 {
if self.vel_y > 0x400 {
// PlaySoundObject(23, SOUND_MODE_PLAY); todo
}
if self.ym > 0 {
self.ym = 0;
if self.vel_y > 0 {
self.vel_y = 0;
}
self.flags.set_flag_x08(true);
@ -89,12 +91,12 @@ impl Player {
&& (self.y + self.hit.bottom as isize) > (y * 0x10 - 8) * 0x200 {
self.y = (y * 0x10 * 0x200) - ((self.x - x * 0x10 * 0x200) / 2) + 0x800 + self.hit.top as isize;
if !self.cond.cond_x02() && self.ym < -0x200 {
if !self.cond.cond_x02() && self.vel_y < -0x200 {
// PutLittleStar(); todo
}
if self.ym < 0 {
self.ym = 0;
if self.vel_y < 0 {
self.vel_y = 0;
}
self.flags.set_flag_x02(true);
@ -108,12 +110,12 @@ impl Player {
&& (self.y + self.hit.bottom as isize) > (y * 0x10 - 8) * 0x200 {
self.y = (y * 0x10 * 0x200) - ((self.x - x * 0x10 * 0x200) / 2) - 0x800 + self.hit.top as isize;
if !self.cond.cond_x02() && self.ym < -0x200 {
if !self.cond.cond_x02() && self.vel_y < -0x200 {
// PutLittleStar(); todo
}
if self.ym < 0 {
self.ym = 0;
if self.vel_y < 0 {
self.vel_y = 0;
}
self.flags.set_flag_x02(true);
@ -127,12 +129,12 @@ impl Player {
&& (self.y + self.hit.bottom as isize) > (y * 0x10 - 8) * 0x200 {
self.y = (y * 0x10 * 0x200) + ((self.x - x * 0x10 * 0x200) / 2) - 0x800 + self.hit.top as isize;
if !self.cond.cond_x02() && self.ym < -0x200 {
if !self.cond.cond_x02() && self.vel_y < -0x200 {
// PutLittleStar(); todo
}
if self.ym < 0 {
self.ym = 0;
if self.vel_y < 0 {
self.vel_y = 0;
}
self.flags.set_flag_x02(true);
@ -146,12 +148,12 @@ impl Player {
&& (self.y + self.hit.bottom as isize) > (y * 0x10 - 8) * 0x200 {
self.y = (y * 0x10 * 0x200) + ((self.x - x * 0x10 * 0x200) / 2) + 0x800 + self.hit.top as isize;
if !self.cond.cond_x02() && self.ym < -0x200 {
if !self.cond.cond_x02() && self.vel_y < -0x200 {
// PutLittleStar(); todo
}
if self.ym < 0 {
self.ym = 0;
if self.vel_y < 0 {
self.vel_y = 0;
}
self.flags.set_flag_x02(true);
@ -167,12 +169,12 @@ impl Player {
&& (self.y - self.hit.top as isize) < (y * 0x10 + 8) * 0x200 {
self.y = (y * 0x10 * 0x200) + ((self.x - x * 0x10 * 0x200) / 2) - 0x800 - self.hit.bottom as isize;
if self.ym > 0x400 {
if self.vel_y > 0x400 {
// PlaySoundObject(23, SOUND_MODE_PLAY); todo
}
if self.ym > 0 {
self.ym = 0;
if self.vel_y > 0 {
self.vel_y = 0;
}
self.flags.set_flag_x20(true);
@ -189,12 +191,12 @@ impl Player {
&& (self.y - self.hit.top as isize) < (y * 0x10 + 8) * 0x200 {
self.y = (y * 0x10 * 0x200) + ((self.x - x * 0x10 * 0x200) / 2) + 0x800 - self.hit.bottom as isize;
if self.ym > 0x400 {
if self.vel_y > 0x400 {
// PlaySoundObject(23, SOUND_MODE_PLAY); todo
}
if self.ym > 0 {
self.ym = 0;
if self.vel_y > 0 {
self.vel_y = 0;
}
self.flags.set_flag_x20(true);
@ -211,12 +213,12 @@ impl Player {
&& (self.y - self.hit.top as isize) < (y * 0x10 + 8) * 0x200 {
self.y = (y * 0x10 * 0x200) - ((self.x - x * 0x10 * 0x200) / 2) + 0x800 - self.hit.bottom as isize;
if self.ym > 0x400 {
if self.vel_y > 0x400 {
// PlaySoundObject(23, SOUND_MODE_PLAY); todo
}
if self.ym > 0 {
self.ym = 0;
if self.vel_y > 0 {
self.vel_y = 0;
}
self.flags.set_flag_x10(true);
@ -233,12 +235,12 @@ impl Player {
&& (self.y - self.hit.top as isize) < (y * 0x10 + 8) * 0x200 {
self.y = (y * 0x10 * 0x200) - ((self.x - x * 0x10 * 0x200) / 2) - 0x800 - self.hit.bottom as isize;
if self.ym > 0x400 {
if self.vel_y > 0x400 {
// PlaySoundObject(23, SOUND_MODE_PLAY); todo
}
if self.ym > 0 {
self.ym = 0;
if self.vel_y > 0 {
self.vel_y = 0;
}
self.flags.set_flag_x10(true);
@ -309,4 +311,10 @@ impl Player {
}
}
}
pub fn tick_npc_collisions(&mut self, state: &mut SharedGameState, stage: &Stage) {
if self.question {
state.create_caret(self.x, self.y, CaretType::QuestionMark, Direction::Left);
}
}
}

22
src/rng.rs Normal file
View File

@ -0,0 +1,22 @@
/// Stateful RNG
pub struct RNG {
pub seed: u32,
}
impl RNG {
pub fn new() -> Self {
Self {
seed: 0,
}
}
pub fn next(&mut self) -> u32 {
// MSVC LCG values
self.seed = self.seed.wrapping_mul(214013).wrapping_add(2531011);
self.seed
}
pub fn range(&mut self, start: i32, end: i32) -> i32 {
start + (self.next() % (end - start) as u32) as i32
}
}

View File

@ -17,9 +17,10 @@ pub struct GameScene {
pub stage: Stage,
pub frame: Frame,
pub player: Player,
tex_tileset_name: String,
tex_background_name: String,
tex_caret_name: String,
tex_hud_name: String,
tex_tileset_name: String,
life_bar: usize,
life_bar_count: usize,
}
@ -43,9 +44,10 @@ impl GameScene {
info!("Loaded stage: {}", stage.data.name);
info!("Map size: {}x{}", stage.map.width, stage.map.height);
let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join("");
let tex_background_name = stage.data.background.filename();
let tex_caret_name = str!("Caret");
let tex_hud_name = str!("TextBox");
let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join("");
Ok(Self {
tick: 0,
@ -56,9 +58,10 @@ impl GameScene {
y: 0,
wait: 16,
},
tex_tileset_name,
tex_background_name,
tex_caret_name,
tex_hud_name,
tex_tileset_name,
life_bar: 3,
life_bar_count: 0,
})
@ -176,6 +179,19 @@ impl GameScene {
Ok(())
}
fn draw_carets(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_caret_name)?;
for caret in state.carets.iter() {
batch.add_rect((((caret.x - caret.offset_x) / 0x200) - (self.frame.x / 0x200)) as f32,
(((caret.y - caret.offset_y) / 0x200) - (self.frame.y / 0x200)) as f32,
&caret.anim_rect);
}
batch.draw(ctx)?;
Ok(())
}
fn draw_tiles(&self, state: &mut SharedGameState, ctx: &mut Context, layer: TileLayer) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_tileset_name)?;
let mut rect = Rect::<usize>::new(0, 0, 16, 16);
@ -226,7 +242,7 @@ impl Scene for GameScene {
state.sound_manager.play_song(ctx)?;
//self.player.x = 700 * 0x200;
//self.player.y = 1000 * 0x200;
//self.player.equip.set_booster_2_0(true);
self.player.equip.set_booster_2_0(true);
state.flags.set_flag_x01(true);
state.flags.set_control_enabled(true);
Ok(())
@ -239,7 +255,9 @@ impl Scene for GameScene {
self.player.tick(state, ctx)?;
self.player.flags.0 = 0;
state.tick_carets();
self.player.tick_map_collisions(state, &self.stage);
self.player.tick_npc_collisions(state, &self.stage);
self.frame.update(state, &self.player, &self.stage);
}
@ -269,6 +287,7 @@ impl Scene for GameScene {
self.draw_tiles(state, ctx, TileLayer::Background)?;
self.player.draw(state, ctx, &self.frame)?;
self.draw_tiles(state, ctx, TileLayer::Foreground)?;
self.draw_carets(state, ctx)?;
self.draw_hud(state, ctx)?;
self.draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?;

View File

@ -57,6 +57,10 @@ impl SizedBatch {
}
pub fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect<usize>) {
if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 {
return;
}
let param = DrawParam::new()
.src(Rect::new(rect.left as f32 / self.width as f32,
rect.top as f32 / self.height as f32,

3
src/weapon.rs Normal file
View File

@ -0,0 +1,3 @@
pub struct Weapon {
}