diff --git a/game/assets/levels/level_0/background.png b/game/assets/levels/level_0/background.png new file mode 100644 index 0000000..8fc5cff Binary files /dev/null and b/game/assets/levels/level_0/background.png differ diff --git a/game/assets/levels/level_0/colliders.json b/game/assets/levels/level_0/colliders.json new file mode 100644 index 0000000..21920a4 --- /dev/null +++ b/game/assets/levels/level_0/colliders.json @@ -0,0 +1,32 @@ +[ + { + "x": 322, + "y": 926, + "width": 610, + "height": 73 + }, + { + "x": 852, + "y": 882, + "width": 81, + "height": 178 + }, + { + "x": 852, + "y": 882, + "width": 258, + "height": 33 + }, + { + "x": 1599, + "y": 699, + "width": 918, + "height": 63 + }, + { + "x": 2733, + "y": 789, + "width": 108, + "height": 211 + } +] diff --git a/game/assets/levels/level_0/platforms.png b/game/assets/levels/level_0/platforms.png new file mode 100644 index 0000000..cfa8c08 Binary files /dev/null and b/game/assets/levels/level_0/platforms.png differ diff --git a/game/assets/levels/levels.json b/game/assets/levels/levels.json new file mode 100644 index 0000000..614f9a8 --- /dev/null +++ b/game/assets/levels/levels.json @@ -0,0 +1,3 @@ +[ + "level_0" +] diff --git a/game/src/character/collisions.rs b/game/src/character/collisions.rs index 1c571c7..c802015 100644 --- a/game/src/character/collisions.rs +++ b/game/src/character/collisions.rs @@ -2,11 +2,17 @@ use std::ops::Mul; use raylib::math::{Rectangle, Vector2}; +use crate::scenes::ingame_scene::world::WORLD_LEVEL_X_OFFSET; + use super::{CharacterState, MainCharacter}; pub const GRAVITY_PPS: f32 = 2.0; -pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ()> { +pub fn modify_player_based_on_forces( + player: &mut MainCharacter, + colliders: &Vec, + level_height_offset: f32, +) -> Result<(), ()> { // Modify the player's velocity by the forces player.movement_force += player.base_velocity; player.velocity = player.movement_force; @@ -14,7 +20,7 @@ pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ( // Predict the player's position next frame let predicted_player_position = player.position + player.velocity; - // Calculate a bounding rect around the player + // Calculate a bounding rect around the player both now, and one frame in the future let player_rect = Rectangle::new( predicted_player_position.x - (player.size.x / 2.0), predicted_player_position.y - (player.size.x / 2.0), @@ -25,10 +31,24 @@ pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ( // Calculate a generic "floor" to always collide with let floor_rect = Rectangle::new(f32::MIN, 0.0, f32::MAX, 1.0); + // Check collision conditions + let check_player_colliding_with_floor = || floor_rect.check_collision_recs(&player_rect); + let check_player_colliding_with_floor_next_frame = + || player_rect.y + player_rect.height > floor_rect.y; + let check_player_colliding_with_colliders = || { + colliders.iter().any(|rect| { + let mut translated_rect = rect.clone(); + translated_rect.y += level_height_offset; + translated_rect.x += WORLD_LEVEL_X_OFFSET; + translated_rect.check_collision_recs(&player_rect) + }) + }; + // If the player is colliding, only apply the x force - if (floor_rect.check_collision_recs(&player_rect) - || player_rect.y + player_rect.height > floor_rect.y) - && player.velocity.y > 0.0 + if (check_player_colliding_with_floor() + || check_player_colliding_with_floor_next_frame() + || check_player_colliding_with_colliders()) + && player.velocity.y != 0.0 { player.velocity.y = 0.0; @@ -36,11 +56,17 @@ pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ( if player.current_state == CharacterState::Jumping || player.current_state == CharacterState::Dashing { - player.update_player(Some(CharacterState::Running)); + player.update_player( + Some(CharacterState::Running), + colliders, + level_height_offset, + ); return Ok(()); } } + // Check sideways collisions + // Finally apply the velocity to the player player.position += player.velocity; diff --git a/game/src/character/mod.rs b/game/src/character/mod.rs index 6fe758c..9c1b0d2 100644 --- a/game/src/character/mod.rs +++ b/game/src/character/mod.rs @@ -2,7 +2,10 @@ pub mod collisions; pub mod render; use chrono::{DateTime, Utc}; -use raylib::{math::Vector2, texture::Texture2D}; +use raylib::{ + math::{Rectangle, Vector2}, + texture::Texture2D, +}; use crate::utilities::anim_render::AnimatedSpriteSheet; @@ -48,7 +51,12 @@ impl MainCharacter { } } - pub fn update_player(&mut self, state: Option) { + pub fn update_player( + &mut self, + state: Option, + colliders: &Vec, + level_height_offset: f32, + ) { if let Some(state) = state { // Update the internal state if state != self.current_state { @@ -65,6 +73,6 @@ impl MainCharacter { } // Update the player based on the new velocity - modify_player_based_on_forces(self).unwrap(); + modify_player_based_on_forces(self, colliders, level_height_offset).unwrap(); } } diff --git a/game/src/scenes/ingame_scene/level/loader.rs b/game/src/scenes/ingame_scene/level/loader.rs new file mode 100644 index 0000000..3ca4add --- /dev/null +++ b/game/src/scenes/ingame_scene/level/loader.rs @@ -0,0 +1,53 @@ +use raylib::{RaylibHandle, RaylibThread}; + +use crate::{ + utilities::datastore::{load_texture_from_internal_data, ResourceLoadError}, + StaticGameData, +}; + +use super::Level; + +pub fn load_all_levels( + raylib_handle: &mut RaylibHandle, + thread: &RaylibThread, +) -> Result, ResourceLoadError> { + // Get a listing of all levels we have + let level_names: Vec = serde_json::from_str( + &String::from_utf8( + StaticGameData::get("levels/levels.json") + .expect("Could not load levels.json") + .data + .into(), + ) + .unwrap(), + )?; + + // Build a level list + let mut levels = Vec::new(); + + for level_name in &level_names { + levels.push(Level { + name: level_name.to_string(), + background_tex: load_texture_from_internal_data( + raylib_handle, + thread, + &format!("levels/{}/background.png", level_name), + )?, + platform_tex: load_texture_from_internal_data( + raylib_handle, + thread, + &format!("levels/{}/platforms.png", level_name), + )?, + colliders: serde_json::from_str( + &String::from_utf8( + StaticGameData::get(&format!("levels/{}/colliders.json", level_name)) + .unwrap() + .data + .into(), + ) + .unwrap(), + )?, + }); + } + Ok(levels) +} diff --git a/game/src/scenes/ingame_scene/level/mod.rs b/game/src/scenes/ingame_scene/level/mod.rs new file mode 100644 index 0000000..2119b36 --- /dev/null +++ b/game/src/scenes/ingame_scene/level/mod.rs @@ -0,0 +1,11 @@ +use raylib::{math::Rectangle, texture::Texture2D}; + +pub mod loader; + +#[derive(Debug)] +pub struct Level { + pub name: String, + pub background_tex: Texture2D, + pub platform_tex: Texture2D, + pub colliders: Vec +} diff --git a/game/src/scenes/ingame_scene/mod.rs b/game/src/scenes/ingame_scene/mod.rs index 34d65be..cbcb340 100644 --- a/game/src/scenes/ingame_scene/mod.rs +++ b/game/src/scenes/ingame_scene/mod.rs @@ -1,25 +1,41 @@ use dirty_fsm::{Action, ActionFlag}; use raylib::prelude::*; -use crate::{character::{CharacterState, MainCharacter}, context::GameContext, utilities::{render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender}, world_paint_texture::WorldPaintTexture}}; +use crate::{ + character::{CharacterState, MainCharacter}, + context::GameContext, + utilities::{ + render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender}, + world_paint_texture::WorldPaintTexture, + }, +}; + +use self::level::Level; use super::{Scenes, ScreenError}; use tracing::{debug, trace}; mod hud; +pub mod level; mod update; -mod world; +pub mod world; #[derive(Debug)] pub struct InGameScreen { camera: Camera2D, player: MainCharacter, - world_background: WorldPaintTexture + world_background: WorldPaintTexture, + levels: Vec, + current_level_idx: usize, } impl InGameScreen { /// Construct a new `InGameScreen` - pub fn new(player_sprite_sheet: Texture2D, background_texture: Texture2D) -> Self { + pub fn new( + player_sprite_sheet: Texture2D, + background_texture: Texture2D, + levels: Vec, + ) -> Self { Self { camera: Camera2D { offset: Vector2::zero(), @@ -27,8 +43,10 @@ impl InGameScreen { rotation: 0.0, zoom: 1.0, }, - player: MainCharacter::new(Vector2::new(0.0, -80.0), player_sprite_sheet), - world_background: WorldPaintTexture::new(background_texture) + player: MainCharacter::new(Vector2::new(0.0, -85.0), player_sprite_sheet), + world_background: WorldPaintTexture::new(background_texture), + levels, + current_level_idx: 0, } } } @@ -43,7 +61,12 @@ impl Action for InGameScreen { debug!("Running InGameScreen for the first time"); // Set the player to running - self.player.update_player(Some(CharacterState::Running)); + let cur_level = self.levels.get(self.current_level_idx).unwrap(); + self.player.update_player( + Some(CharacterState::Running), + &cur_level.colliders, + -cur_level.platform_tex.height as f32, + ); Ok(()) } diff --git a/game/src/scenes/ingame_scene/update.rs b/game/src/scenes/ingame_scene/update.rs index 0c98c86..754dbc2 100644 --- a/game/src/scenes/ingame_scene/update.rs +++ b/game/src/scenes/ingame_scene/update.rs @@ -17,6 +17,11 @@ impl FrameUpdate for InGameScreen { config: &GameConfig, ) { puffin::profile_function!(); + + // Get the current level + let cur_level = self.levels.get(self.current_level_idx).unwrap(); + + // Set the camera's offset based on screen size self.camera.offset = raylib.get_screen_size().div(Vector2::new(2.0, 1.05)); self.camera.target = Vector2::new(self.player.position.x, self.camera.target.y); @@ -28,16 +33,20 @@ impl FrameUpdate for InGameScreen { && !(self.player.current_state == CharacterState::Dashing); if is_jump { - self.player.update_player(Some(CharacterState::Jumping)); + self.player.update_player(Some(CharacterState::Jumping), &cur_level.colliders, + -cur_level.platform_tex.height as f32,); } else if is_dash { - self.player.update_player(Some(CharacterState::Dashing)); + self.player.update_player(Some(CharacterState::Dashing), &cur_level.colliders, + -cur_level.platform_tex.height as f32,); } else { if self.player.current_state != CharacterState::Jumping && self.player.current_state != CharacterState::Dashing { - self.player.update_player(Some(CharacterState::Running)); + self.player.update_player(Some(CharacterState::Running), &cur_level.colliders, + -cur_level.platform_tex.height as f32,); } else { - self.player.update_player(None); + self.player.update_player(None, &cur_level.colliders, + -cur_level.platform_tex.height as f32,); } } } diff --git a/game/src/scenes/ingame_scene/world.rs b/game/src/scenes/ingame_scene/world.rs index cf5afdf..4e9fd05 100644 --- a/game/src/scenes/ingame_scene/world.rs +++ b/game/src/scenes/ingame_scene/world.rs @@ -8,6 +8,8 @@ use crate::{ }; use raylib::prelude::*; +pub const WORLD_LEVEL_X_OFFSET: f32 = 200.0; + impl WorldSpaceRender for InGameScreen { fn render_world_space( &self, @@ -16,8 +18,14 @@ impl WorldSpaceRender for InGameScreen { ) { puffin::profile_function!(); + // Get the current level + let cur_level = self.levels.get(self.current_level_idx).unwrap(); + // Render the world background - self.world_background.render(raylib, Vector2::new(0.0, -1080.0), &self.camera); + // self.world_background.render(raylib, Vector2::new(0.0, -1080.0), &self.camera); + + // Render the platform layer + raylib.draw_texture_v(&cur_level.platform_tex, Vector2::new(WORLD_LEVEL_X_OFFSET, -cur_level.platform_tex.height as f32), Color::WHITE); // Render the floor as a line let screen_world_zero = raylib.get_screen_to_world2D(Vector2::zero(), self.camera); diff --git a/game/src/scenes/mod.rs b/game/src/scenes/mod.rs index 7951f15..018934b 100644 --- a/game/src/scenes/mod.rs +++ b/game/src/scenes/mod.rs @@ -1,5 +1,7 @@ use self::{ - fsm_error_screen::FsmErrorScreen, ingame_scene::InGameScreen, loading_screen::LoadingScreen, + fsm_error_screen::FsmErrorScreen, + ingame_scene::{level::loader::load_all_levels, InGameScreen}, + loading_screen::LoadingScreen, main_menu_screen::MainMenuScreen, }; use crate::{ @@ -48,6 +50,7 @@ pub fn build_screen_state_machine( load_texture_from_internal_data(raylib_handle, thread, "character/player_run.png").unwrap(); let world_background = load_texture_from_internal_data(raylib_handle, thread, "default-texture.png").unwrap(); + let levels = load_all_levels(raylib_handle, thread).unwrap(); // Set up the state machine let mut machine = StateMachine::new(); @@ -57,6 +60,9 @@ pub fn build_screen_state_machine( LoadingScreen::new(raylib_handle, thread)?, )?; machine.add_action(Scenes::MainMenuScreen, MainMenuScreen::new())?; - machine.add_action(Scenes::InGameScene, InGameScreen::new(player_sprite_sheet, world_background))?; + machine.add_action( + Scenes::InGameScene, + InGameScreen::new(player_sprite_sheet, world_background, levels), + )?; Ok(machine) } diff --git a/game/src/utilities/datastore.rs b/game/src/utilities/datastore.rs index 506f46d..d181204 100644 --- a/game/src/utilities/datastore.rs +++ b/game/src/utilities/datastore.rs @@ -14,6 +14,8 @@ pub struct StaticGameData; #[derive(Debug, Error)] pub enum ResourceLoadError { + #[error(transparent)] + JsonDeser(#[from] serde_json::Error), #[error(transparent)] Io(#[from] std::io::Error), #[error("Could not load embedded asset: {0}")]