diff --git a/src/engine/bbox.rs b/src/engine/bbox.rs new file mode 100644 index 0000000..1fb393b --- /dev/null +++ b/src/engine/bbox.rs @@ -0,0 +1,22 @@ +//! This module helps us to deal with bounding boxes. + +use crate::engine::vector::Vector; + +/// This struct represents a bounding box. +pub struct Bbox { + /// The position of the top right corner of the box. + pub position: Vector, + + /// The size of the box. + pub size: Vector, +} + +impl Bbox { + /// Creates a bounding box from x, y, width and height. + pub fn new(x: f64, y: f64, w: f64, h: f64) -> Bbox { + Bbox { + position: Vector::new(x, y), + size: Vector::new(w, h), + } + } +} diff --git a/src/engine/character.rs b/src/engine/character.rs new file mode 100644 index 0000000..1818cff --- /dev/null +++ b/src/engine/character.rs @@ -0,0 +1,231 @@ +//! This module helps us dealing with characters. + +use std::time::{Duration, Instant}; + +use crate::engine::bbox::Bbox; +use crate::engine::physics; +use crate::engine::scene::Updatable; +use crate::engine::vector::Vector; + +use crate::engine::controls::Controls; +use crate::engine::math::{clamp, duration_as_f64, duration_as_frame}; +// use crate::engine::physics; +// use crate::engine::renderer::Drawable; +// use crate::engine::scene::Updatable; +// use crate::engine::texture::Texture; + +/// The different sides a character can face. +pub enum Side { + /// The character looks to the left. + Left, + + /// The character looks to the right. + Right, +} + +impl Side { + /// Returns the side corresponding to the force. + /// + /// Returns None if the force is null. + pub fn from_force(force: Vector) -> Option { + if force.x > 0.0 { + Some(Side::Right) + } else if force.x < 0.0 { + Some(Side::Left) + } else { + None + } + } + + /// Returns the offset in the texture. + pub fn offset(&self) -> i32 { + match *self { + Side::Left => 32, + Side::Right => 0, + } + } +} + +/// A character, enemy or controllable. +pub struct Character { + /// The position of the character. + pub position: Vector, + + /// The speed of the character. + pub speed: Vector, + + /// If the player is controlling a character. + controls: Option, + + /// The side of the character. + side: Side, + + /// The counter of jumps. + /// + /// When it's 0, the character can no longer jump. + jump_counter: usize, + + /// The maximum number of jumps a character can do. + /// + /// It's reset when the character hits the ground. + max_jump: usize, + + /// The timer of the character's animation. + animation_timer: Instant, + + /// Whether the character is walking or not. + walking: bool, + + /// Indicates that the player has released the jump button. + can_jump: bool, +} + +impl Character { + /// Creates a character in (0, 0). + fn generic(controls: Option) -> Character { + Character { + position: Vector::new(0.0, 0.0), + speed: Vector::new(0.0, 0.0), + controls, + side: Side::Right, + jump_counter: 1, + max_jump: 1, + animation_timer: Instant::now(), + can_jump: true, + walking: false, + } + } + + /// Creates a character in (0, 0). + pub fn new() -> Character { + Character::generic(None) + } + + /// Creates a character with the specified controls. + pub fn with_controls(controls: Controls) -> Character { + Character::generic(Some(controls)) + } + + /// Makes the character jump. + pub fn jump(&mut self) { + if self.can_jump && self.jump_counter > 0 { + self.jump_counter -= 1; + self.speed.y = physics::JUMP_SPEED.y; + } + } + + /// Resets the jump counter. + pub fn ground_collision(&mut self) { + self.jump_counter = self.max_jump; + } + + /// Make the player fall. + pub fn fall_off(&mut self) { + if self.jump_counter == self.max_jump { + self.jump_counter -= 1; + } + } + + /// Makes the player die. + pub fn die(&self) { + panic!(); + } + + /// Returns a reference to the controls. + pub fn controls(&self) -> &Option { + &self.controls + } + + /// Returns a view that looks at the character. + pub fn view(&self) -> Bbox { + Bbox::new(self.position.x, self.position.y, 24.0 * 16.0, 24.0 * 9.0) + } + + /// Returns the collision bounding box of the character. + pub fn bbox(&self) -> Bbox { + Bbox::new(self.position.x + 8.0, self.position.y, 16.0, 32.0) + } +} + +impl Updatable for Character { + fn update(&mut self, duration: &Duration) { + let mut force = Vector::new(0.0, 0.0); + + if let Some(ref controls) = self.controls { + force += controls.direction(); + } + + if let Some(side) = Side::from_force(force) { + if !self.walking { + self.animation_timer = Instant::now(); + } + self.walking = true; + self.side = side; + } else { + if self.walking { + self.animation_timer = Instant::now(); + } + self.walking = false; + } + + let duration = duration_as_f64(duration); + + // Compute acceleration + let accel = physics::G + force * 64.0 * 64.0; + + // Compute speed + self.speed.x *= physics::GROUND_FRICTION.x; + self.speed += accel * duration; + + if self.speed.y > physics::MAXIMUM_VERTICAL_SPEED { + self.speed.y = physics::MAXIMUM_VERTICAL_SPEED; + } + + let limit = match self.controls { + Some(controls) if controls.is_running() => physics::MAXIMUM_RUNNING_SPEED, + _ => physics::MAXIMUM_WALKING_SPEED, + }; + + self.speed.x = clamp(self.speed.x, -limit, limit); + + // Compute position + self.position += self.speed * duration; + } + + // fn manage_event(&mut self, event: &Event) { + // let action = if let Some(ref controls) = self.controls { + // controls.convert(event) + // } else { + // None + // }; + + // match action { + // Some(Action::Jump(true)) => { + // self.jump(); + // self.can_jump = false; + // } + + // Some(Action::Jump(false)) => { + // self.can_jump = true; + // } + + // _ => (), + // } + // } +} + +// impl Drawable for Character { +// fn texture(&self) -> Texture { +// Texture::Rusty +// } +// +// fn texture_rect(&self) -> IntRect { +// let frame = duration_as_frame(&Instant::now().duration_since(self.animation_timer), 4); +// let offset = if self.walking { 64 } else { 0 }; +// IntRect::new(self.side.offset() + offset, frame * 32, 32, 32) +// } +// +// fn position(&self) -> Vector { +// self.position +// } +// } diff --git a/src/engine/controls.rs b/src/engine/controls.rs new file mode 100644 index 0000000..d7a2ba2 --- /dev/null +++ b/src/engine/controls.rs @@ -0,0 +1,121 @@ +//! This module helps to deal with controls. + +use crate::engine::event::{Event, Key}; +use crate::engine::vector::Vector; + +/// The different actions that a user can do. +pub enum Action { + /// The jump button. + /// + /// A bool at true means that the button was pressed, + /// A bool at false means that the button was released. + Jump(bool), +} + +pub enum Controls { + /// A keyboard controller. + Keyboard(KeyboardMap), +} + +impl Controls { + /// Returns the default keyboard controls. + pub fn default_keyboard() -> Controls { + Controls::Keyboard(KeyboardMap::default()) + } + + // /// Returns the default gamepad controls from id. + // /// + // /// Returns None if the gamepad corresponding to the id is not connected. + // pub fn default_gamepad_from_id(id: u32) -> Option { + // match GamepadMap::from_id(id) { + // Some(map) => Some(Controls::Gamepad(map)), + // None => None, + // } + // } + + // /// Returns the default gamepad controls from id. + // /// + // /// Returns None if the gamepad corresponding to the id is not connected. + // pub fn all_gamepads() -> Vec { + // GamepadMap::all() + // .into_iter() + // .map(|x| Controls::Gamepad(x)) + // .collect() + // } + + /// Converts an event and depending on the config, returns the corresponding action. + pub fn convert(&self, event: &Event) -> Option { + match self { + Controls::Keyboard(ref map) => map.convert(event), + // Controls::Gamepad(ref map) => map.convert(event), + } + } + + /// Returns the direction of the controls. + pub fn direction(&self) -> Vector { + match self { + Controls::Keyboard(ref map) => map.direction(), + // Controls::Gamepad(ref map) => map.direction(), + } + } + + /// Returns whether the running key is pressed. + pub fn is_running(&self) -> bool { + false + // match self { + // Controls::Keyboard(ref map) => map.is_running(), + // Controls::Gamepad(ref map) => map.is_running(), + // } + } +} + +/// A map between keyboard keys and actions. +#[derive(Copy, Clone)] +pub struct KeyboardMap { + /// The key corresponding to the jump button. + jump_key: Key, + + /// The key corresponding to the run button. + run_key: Key, + + /// The key corresponding to the left button. + left_key: Key, + + /// The key corresponding to the right button. + right_key: Key, +} + +impl KeyboardMap { + /// Creates the default keyboard config. + pub fn default() -> KeyboardMap { + KeyboardMap { + jump_key: Key::Space, + run_key: Key::ArrowUp, + left_key: Key::ArrowLeft, + right_key: Key::ArrowRight, + } + } + + /// Converts an event and depending on the config, returns the corresponding action. + pub fn convert(&self, event: &Event) -> Option { + match event { + Event::KeyPressed(code) if *code == self.jump_key => Some(Action::Jump(true)), + Event::KeyReleased(code) if *code == self.jump_key => Some(Action::Jump(false)), + _ => None, + } + } + + /// Returns the direction of the keys. + pub fn direction(&self) -> Vector { + let mut ret = Vector::new(0.0, 0.0); + const RIGHT: Vector = Vector { x: 1.0, y: 0.0 }; + + if self.right_key.is_pressed() { + ret += RIGHT; + } + if self.left_key.is_pressed() { + ret -= RIGHT; + } + ret + } +} diff --git a/src/engine/event.rs b/src/engine/event.rs index bc2b8c9..5b31547 100644 --- a/src/engine/event.rs +++ b/src/engine/event.rs @@ -117,6 +117,9 @@ pub enum Key { /// The bottom arrow key. ArrowDown, + + /// The space key. + Space, } impl Key { @@ -127,6 +130,7 @@ impl Key { "ArrowRight" => Some(Key::ArrowRight), "ArrowUp" => Some(Key::ArrowUp), "ArrowDown" => Some(Key::ArrowDown), + "Space" => Some(Key::Space), _ => None, } } diff --git a/src/engine/map.rs b/src/engine/map.rs new file mode 100644 index 0000000..6afad40 --- /dev/null +++ b/src/engine/map.rs @@ -0,0 +1,526 @@ +//! This module contains everything related to maps. + +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use sfml::graphics::{FloatRect, IntRect}; +use sfml::system::Vector2; + +use crate::engine::character::Damage; +use crate::engine::math::{clamp, Matrix}; +use crate::engine::renderer::Drawable; +use crate::engine::texture::{byte_to_index, Texture, SPRITE_SIZE_F32, SPRITE_SIZE_I32}; +use crate::{Error, Result}; + +/// This enum represents if the collision happens on the X axis or the Y axis. +#[derive(Copy, Clone)] +pub enum CollisionAxis { + /// The X axis. + X, + + /// The Y axis. + Y, + + /// Both axis simultaneously + Both, +} + +impl CollisionAxis { + /// Returns true if the collision occured on X axis. + pub fn is_x(self) -> bool { + match self { + CollisionAxis::Y => false, + _ => true, + } + } + + /// Returns true if the collision occured on Y axis. + pub fn is_y(self) -> bool { + match self { + CollisionAxis::X => false, + _ => true, + } + } +} + +/// This struct represents the different sides from which a collision can occur. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct CollisionTile { + /// If the character comes from the top, it will collide if this bool is true. + pub from_top: bool, + + /// If the character comes from the left, it will collide if this bool is true. + pub from_left: bool, + + /// If the character comes from the right, it will collide if this bool is true. + pub from_right: bool, + + /// If the character comes from the bottom, it will collide if this bool is true. + pub from_bottom: bool, +} + +impl CollisionTile { + /// Creates a collision tile that does not collide. + pub fn empty() -> CollisionTile { + CollisionTile { + from_top: false, + from_left: false, + from_right: false, + from_bottom: false, + } + } + + /// Creates a collision tile that collides from every side. + pub fn full() -> CollisionTile { + CollisionTile { + from_top: true, + from_left: true, + from_right: true, + from_bottom: true, + } + } + + /// Tests whether a collision tile is full or not. + pub fn is_full(self) -> bool { + self.from_top && self.from_left && self.from_right && self.from_bottom + } + + /// Tests whether a collision tile is empty or not. + pub fn is_empty(self) -> bool { + !self.from_top && !self.from_left && !self.from_right && !self.from_bottom + } +} + +/// This struct represents a renderable tile linking to its part in the tileset texture. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct GraphicTile(Option); + +impl GraphicTile { + /// Creates the correct graphic tile depending on the neighbours. + /// + /// A none will be considered solid. + pub fn from_neighbour_options(tiles: &[Option; 8]) -> GraphicTile { + GraphicTile::from_neighbours(&[ + tiles[0].unwrap_or_else(CollisionTile::full), + tiles[1].unwrap_or_else(CollisionTile::full), + tiles[2].unwrap_or_else(CollisionTile::full), + tiles[3].unwrap_or_else(CollisionTile::full), + tiles[4].unwrap_or_else(CollisionTile::full), + tiles[5].unwrap_or_else(CollisionTile::full), + tiles[6].unwrap_or_else(CollisionTile::full), + tiles[7].unwrap_or_else(CollisionTile::full), + ]) + } + + /// Creates the correct graphic tile depending on the neighbours. + pub fn from_neighbours(tiles: &[CollisionTile; 8]) -> GraphicTile { + let mut byte = 0; + + if !tiles[7].is_full() || !tiles[0].is_full() || !tiles[1].is_full() { + byte += 1; + } + + if !tiles[1].is_full() { + byte += 2; + } + + if !tiles[1].is_full() || !tiles[2].is_full() || !tiles[3].is_full() { + byte += 4; + } + + if !tiles[3].is_full() { + byte += 8; + } + + if !tiles[3].is_full() || !tiles[4].is_full() || !tiles[5].is_full() { + byte += 16; + } + + if !tiles[5].is_full() { + byte += 32; + } + + if !tiles[5].is_full() || !tiles[6].is_full() || !tiles[7].is_full() { + byte += 64; + } + + if !tiles[7].is_full() { + byte += 128; + } + + GraphicTile(Some(byte_to_index(byte))) + } + + /// Returns the offset to the corresponding graphic tile in the texture. + pub fn offset(self) -> (i32, i32) { + match self.0 { + None => (0, 0), + Some(v) => (32 * v, 0), + } + } + + /// Returns true if the tile is visible. + pub fn is_visible(&self) -> bool { + self.0.is_some() + } +} + +/// A tile and its position. +pub struct PositionedTile { + /// The graphic representation of the positioned tile. + pub graphic: GraphicTile, + + /// The collision representation of the positioned tile. + pub collision: CollisionTile, + + /// The position of the positioned tile. + pub position: (f32, f32), +} + +impl Drawable for PositionedTile { + fn texture(&self) -> Texture { + Texture::Overworld + } + + fn texture_rect(&self) -> IntRect { + let offset = self.graphic.offset(); + IntRect::new(offset.0, offset.1, SPRITE_SIZE_I32, SPRITE_SIZE_I32) + } + + fn position(&self) -> Vector2 { + self.position.into() + } +} + +/// The map represents the tiles contained in a level. +#[derive(Clone)] +pub struct Map { + /// The entrace point of the character in the map. + entrance: (usize, usize), + + /// The collision tiles contained in the level. + collision_tiles: Matrix, + + /// The graphic tiles contained in the level. + graphic_tiles: Matrix, +} + +impl Map { + /// Creates a map full of nothing, with a ground at the bottom. + pub fn new(rows: usize, cols: usize) -> Map { + let mut tiles = Matrix::from_size(rows, cols, CollisionTile::empty()); + let rows = tiles.rows(); + + for i in 0..tiles.cols() { + tiles[(rows - 1, i)] = CollisionTile::full(); + } + + Map::from_collision_tiles(tiles) + } + + /// Loads a map from a file. + pub fn from_file>(path: P) -> Result { + let mut file = File::open(path.as_ref()).map_err(Error::Load)?; + let mut s = String::new(); + file.read_to_string(&mut s).map_err(Error::Load)?; + Map::from_str(&s) + } + + /// Loads a map from a string. + pub fn from_str(text: &str) -> Result { + let split = text.split('\n').collect::>(); + + // First two usize are the size of the map + let size = split[0] + .split_whitespace() + .map(|x| x.parse::().unwrap()) + .collect::>(); + + let mut tiles = Matrix::from_size(size[0], size[1], CollisionTile::empty()); + + for (row, line) in split.iter().skip(1).enumerate() { + for (col, tile) in line.split_whitespace().enumerate() { + let num = tile.parse::().unwrap(); + match num { + 0 => (), + 1 => tiles[(row, col)] = CollisionTile::full(), + _ => panic!("Expecting 0 or 1 in level files"), + } + } + } + + Ok(Map::from_collision_tiles(tiles)) + } + + // /// Encodes the map into a binary format. + // pub fn encode(&self) -> Result> { + // serialize(&self.save_map()).map_err(Error::Encoding) + // } + + // /// Decodes a map from bytes. + // pub fn decode(content: &[u8]) -> Result { + // deserialize(content) + // .map(SaveMap::to_map) + // .map_err(Error::Decoding) + // } + + // /// Saves the map to a file. + // pub fn save>(&self, path: P) -> Result<()> { + // let file = File::create(path.as_ref()).map_err(Error::Save)?; + // let mut writer = BufWriter::new(file); + // serialize_into(&mut writer, &self.save_map()).map_err(Error::Encoding)?; + // Ok(()) + // } + + // /// Loads a map from a file. + // pub fn load>(path: P) -> Result { + // let file = File::open(path.as_ref()).map_err(Error::Load)?; + // let mut reader = BufReader::new(file); + // Ok(deserialize_from(&mut reader) + // .map(SaveMap::to_map) + // .map_err(Error::Decoding)?) + // } + + /// Creates a map from its entrance and collision tiles. + pub fn from_entrance_and_collision_tiles(e: (usize, usize), t: Matrix) -> Map { + let rows = t.rows(); + let cols = t.cols(); + + let graphic_tiles = Matrix::from_size(rows, cols, GraphicTile(None)); + + let mut map = Map { + entrance: e, + collision_tiles: t, + graphic_tiles, + }; + + for i in 0..rows { + for j in 0..cols { + map.graphic_tiles[(i, j)] = map.graphic_tile(i, j); + } + } + + map + } + + /// Creates a map from its tiles. + pub fn from_collision_tiles(collision_tiles: Matrix) -> Map { + let entrance = Map::find_entrance(&collision_tiles); + Map::from_entrance_and_collision_tiles(entrance, collision_tiles) + } + + /// Creates the neighbours of a tile. + pub fn neighbours(&self, i: usize, j: usize) -> [Option; 8] { + [ + if i > 0 && j > 0 { + self.collision_tiles.get(i - 1, j - 1).cloned() + } else { + None + }, + if i > 0 { + self.collision_tiles.get(i - 1, j).cloned() + } else { + None + }, + if i > 0 { + self.collision_tiles.get(i - 1, j + 1).cloned() + } else { + None + }, + self.collision_tiles.get(i, j + 1).cloned(), + self.collision_tiles.get(i + 1, j + 1).cloned(), + self.collision_tiles.get(i + 1, j).cloned(), + if j > 0 { + self.collision_tiles.get(i + 1, j - 1).cloned() + } else { + None + }, + if j > 0 { + self.collision_tiles.get(i, j - 1).cloned() + } else { + None + }, + ] + } + + /// Returns the graphic tile corresponding to the collision tiles. + pub fn graphic_tile(&self, i: usize, j: usize) -> GraphicTile { + if self.collision_tiles[(i, j)].is_full() { + GraphicTile::from_neighbour_options(&self.neighbours(i, j)) + } else { + GraphicTile(None) + } + } + + /// Returns a tile of the map. + pub fn collision_tile(&self, i: usize, j: usize) -> Option { + self.collision_tiles.get(i, j).cloned() + } + + /// Changes a tile of the map. + pub fn set_tile(&mut self, i: usize, j: usize, tile: CollisionTile) { + // Change the collision tile + self.collision_tiles[(i, j)] = tile; + + // Refresh the current graphic tile and their neighbours + use std::cmp::max; + for i in max(i, 1) - 1..=(i + 1) { + for j in max(j, 1) - 1..=(j + 1) { + let new_tile = self.graphic_tile(i, j); + if let Some(tile) = self.graphic_tiles.get_mut(i, j) { + *tile = new_tile; + } + } + } + } + + /// Finds a possible entrance. + pub fn find_entrance(tiles: &Matrix) -> (usize, usize) { + (tiles.rows() - 5, 1) + } + + /// Returns the entrance of the map. + pub fn entrance(&self) -> (usize, usize) { + self.entrance + } + + /// Returns an iterator to the positioned tiles. + pub fn at(&self, row: usize, col: usize) -> PositionedTile { + PositionedTile { + collision: self.collision_tiles[(row, col)], + graphic: self.graphic_tiles[(row, col)], + position: (col as f32 * SPRITE_SIZE_F32, row as f32 * SPRITE_SIZE_F32), + } + } + + /// Returns the number of rows of the map. + pub fn rows(&self) -> usize { + self.collision_tiles.rows() + } + + /// Returns the number of columns of the map. + pub fn cols(&self) -> usize { + self.collision_tiles.cols() + } + + /// Checks whether the bounding box collides with elements of the map. + /// + /// Returns the new correct position. + pub fn collides_bbox( + &self, + old: FloatRect, + new: FloatRect, + ) -> Option<(CollisionAxis, Vector2, Damage)> { + let mut damage = Damage::None; + + let cols = self.collision_tiles.cols() - 1; + let rows = self.collision_tiles.rows() - 1; + + let min_col = clamp(new.left / SPRITE_SIZE_F32, 0.0, cols as f32) as usize; + let min_row = clamp(new.top / SPRITE_SIZE_F32, 0.0, rows as f32) as usize; + + let max_col = clamp((new.left + new.width) / SPRITE_SIZE_F32, 0.0, cols as f32) as usize; + let max_row = clamp((new.top + new.height) / SPRITE_SIZE_F32, 0.0, rows as f32) as usize; + + let mut collision_x = false; + let mut collision_y = false; + let mut new = new; + + for col in min_col..=max_col { + for row in min_row..=max_row { + let tile_left = col as f32 * SPRITE_SIZE_F32; + let tile_top = row as f32 * SPRITE_SIZE_F32; + + let tile = FloatRect::new(tile_left, tile_top, SPRITE_SIZE_F32, SPRITE_SIZE_F32); + + if !overlap(new, tile) { + continue; + } + + // Collisions between feet and ground + if self.collision_tiles[(row, col)].from_top + && old.top + old.height <= tile_top + && new.top + new.height >= tile_top + { + collision_y = true; + new.top = tile_top - new.height; + } + + if !overlap(new, tile) { + continue; + } + + // Collisions between right and right wall + if self.collision_tiles[(row, col)].from_left + && old.left + old.width <= tile_left + && new.left + new.width >= tile_left + { + collision_x = true; + new.left = tile_left - new.width; + } + + if !overlap(new, tile) { + continue; + } + + // Collisions between left and left wall + if self.collision_tiles[(row, col)].from_right + && old.left >= tile_left + SPRITE_SIZE_F32 + && new.left <= tile_left + SPRITE_SIZE_F32 + { + collision_x = true; + new.left = tile_left + SPRITE_SIZE_F32; + } + + if !overlap(new, tile) { + continue; + } + + // Collisions between head and roof + if self.collision_tiles[(row, col)].from_bottom + && old.top >= tile_top + SPRITE_SIZE_F32 + && new.top <= tile_top + SPRITE_SIZE_F32 + { + collision_y = true; + new.top = tile_top + SPRITE_SIZE_F32; + } + } + } + + // Collision between the player and left border of the level + if new.left < 0.0 { + new.left = 0.0; + collision_x = true; + } + + // Collision between the player and right border of the level + if new.left > cols as f32 * SPRITE_SIZE_F32 { + new.left = cols as f32 * SPRITE_SIZE_F32; + collision_x = true; + } + + // Collision between the player and the void + if new.top > self.collision_tiles.rows() as f32 * SPRITE_SIZE_F32 { + new.top = self.collision_tiles.rows() as f32 * SPRITE_SIZE_F32; + collision_y = true; + damage = Damage::Death; + } + + let new_pos = Vector2::new(new.left, new.top); + match (collision_x, collision_y) { + (true, true) => Some((CollisionAxis::Both, new_pos, damage)), + (true, false) => Some((CollisionAxis::X, new_pos, damage)), + (false, true) => Some((CollisionAxis::Y, new_pos, damage)), + (false, false) => None, + } + } +} + +/// Checks if two boxes overlap. +pub fn overlap(box1: FloatRect, box2: FloatRect) -> bool { + box2.left < box1.left + box1.width + && box2.left + box2.width > box1.left + && box2.top < box1.top + box1.height + && box2.top + box2.height > box1.top +} diff --git a/src/engine/math.rs b/src/engine/math.rs new file mode 100644 index 0000000..c579a7f --- /dev/null +++ b/src/engine/math.rs @@ -0,0 +1,105 @@ +//! This module contains useful math tools. + +use std::ops::{Index, IndexMut}; +use std::time::Duration; + +/// Clamp a number between two boundaries. +pub fn clamp(number: f64, min: f64, max: f64) -> f64 { + if number < min { + min + } else if number > max { + max + } else { + number + } +} + +/// Converts a duration into an animation frame number. +pub fn duration_as_frame(duration: &Duration, total: usize) -> i32 { + let secs = duration_as_f64(duration); + + (secs * 10.0) as i32 % total as i32 +} + +/// Converts a duration into its number of seconds. +pub fn duration_as_f64(duration: &Duration) -> f64 { + duration.as_secs() as f64 + duration.subsec_nanos() as f64 / 1_000_000_000.0 +} + +/// A generic matrix type, useful for levels. +#[derive(Clone)] +pub struct Matrix { + /// The number of rows of the matrix. + rows: usize, + + /// The number of cols of the matrix. + cols: usize, + + /// The contained data in the matrix. + data: Vec, +} + +impl Matrix +where + T: Clone, +{ + /// Creates a matrix from an element and duplicates it. + pub fn from_size(rows: usize, cols: usize, element: T) -> Matrix { + Matrix { + rows, + cols, + data: vec![element; rows * cols], + } + } +} + +impl Matrix { + /// Converts an a pair corresponding to the row and columns into an integer. + pub fn to_usize(&self, (row, col): (usize, usize)) -> usize { + self.rows * col + row + } + + /// Returns the number of rows. + pub fn rows(&self) -> usize { + self.rows + } + + /// Returns the number of columns. + pub fn cols(&self) -> usize { + self.cols + } + + /// Returns the tile if any, none otherwise. + pub fn get(&self, row: usize, col: usize) -> Option<&T> { + if row < self.rows && col < self.cols { + Some(&self[(row, col)]) + } else { + None + } + } + + /// Returns a mutable reference to the tile if any. + pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> { + if row < self.rows && col < self.cols { + Some(&mut self[(row, col)]) + } else { + None + } + } +} + +impl Index<(usize, usize)> for Matrix { + type Output = T; + + fn index(&self, index: (usize, usize)) -> &T { + let index = self.to_usize(index); + &self.data[index] + } +} + +impl IndexMut<(usize, usize)> for Matrix { + fn index_mut(&mut self, index: (usize, usize)) -> &mut T { + let index = self.to_usize(index); + &mut self.data[index] + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 06aea12..0f2ee65 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,7 +1,15 @@ //! This module contains the whole engine that manages everything. +pub mod bbox; +pub mod character; +pub mod controls; +pub mod controls; pub mod event; pub mod image; +pub mod math; +pub mod physics; +pub mod scene; +pub mod vector; use std::cell::RefCell; use std::rc::Rc; @@ -165,7 +173,7 @@ impl Engine { self.context.clear_rect(0.0, 0.0, 1920.0, 1080.0); inner .textures - .test + .rusty .render(inner.x, inner.y, &self.context)?; Ok(()) @@ -203,13 +211,13 @@ impl InnerEngine { /// /// It holds all our resources. pub struct TextureManager { - test: Image, + rusty: Image, } impl TextureManager { /// Creates and start the loading of all our textures. fn new() -> Result { - let test = Image::new("static/image.png")?; - Ok(TextureManager { test }) + let rusty = Image::new("static/textures/rusty.png")?; + Ok(TextureManager { rusty }) } } diff --git a/src/engine/physics.rs b/src/engine/physics.rs new file mode 100644 index 0000000..f6edaf6 --- /dev/null +++ b/src/engine/physics.rs @@ -0,0 +1,27 @@ +//! This module helps us deal with physics. + +use crate::engine::vector::Vector; + +/// The gravity force. +pub const G: Vector = Vector { + x: 0.0, + y: 25.0 * 32.0, +}; + +/// The friction with the ground. +pub const GROUND_FRICTION: Vector = Vector { x: 0.5, y: 1.0 }; + +/// The speed of a jump. +pub const JUMP_SPEED: Vector = Vector { + x: 0.0, + y: -12.5 * 32.0, +}; + +/// The maximum vertical speed. +pub const MAXIMUM_VERTICAL_SPEED: f64 = 10.0 * 32.0; + +/// The maximul walking speed. +pub const MAXIMUM_WALKING_SPEED: f64 = 2.5 * 32.0; + +/// The maximum running speed. +pub const MAXIMUM_RUNNING_SPEED: f64 = 5.0 * 32.0; diff --git a/src/engine/scene.rs b/src/engine/scene.rs new file mode 100644 index 0000000..c153500 --- /dev/null +++ b/src/engine/scene.rs @@ -0,0 +1,150 @@ +//! This module contains the scene struct which holds everything needed during a game. + +use std::time::Duration; + +use crate::engine::character::Character; +use crate::engine::map::Map; +use crate::engine::texture::SPRITE_SIZE_F32; + +/// Contains everything needed to play. +pub struct Scene { + /// The characters contained in the scene. + characters: Vec, + + /// The map of the scene. + map: Map, +} + +/// The type used to represent whether a a scene is running or finished. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum State { + /// The scene is running. + Running, + + /// The scene is finished. + Finished, +} + +impl Scene { + /// Creates a scene from a map level. + pub fn from_map(map: Map) -> Scene { + Scene { + characters: vec![], + map, + } + } + + /// Adds a character to the scene. + pub fn add(&mut self, character: Character) { + let mut character = character; + character.position.x = self.map.entrance().1 as f32 * SPRITE_SIZE_F32; + character.position.y = self.map.entrance().0 as f32 * SPRITE_SIZE_F32; + + self.characters.push(character); + } + + /// Returns the controlable. + fn controlable(&self) -> Option<&Character> { + for character in &self.characters { + if character.controls().is_some() { + return Some(&character); + } + } + None + } + + // /// Returns the right view. + // pub fn view(&self) -> Option { + // let view = self.controlable()?.view(); + // let mut center = view.center(); + // let size = view.size(); + + // // Clamp center so that the view doesn't show things outside the level. + // if center.x - size.x / 2.0 < 0.0 { + // center.x = size.x / 2.0; + // } + + // if center.y - size.y / 2.0 < 0.0 { + // center.y = size.y / 2.0; + // } + + // let right_limit = self.map.cols() as f32 * SPRITE_SIZE_F32; + // let bottom_limit = self.map.rows() as f32 * SPRITE_SIZE_F32; + + // if center.x + size.x / 2.0 > right_limit { + // center.x = right_limit - size.x / 2.0; + // } + + // if center.y + size.y / 2.0 > bottom_limit { + // center.y = bottom_limit - size.y / 2.0; + // } + + // Some(View::new(center, size)) + // } + + /// Updates the whole scene. + pub fn update(&mut self, duration: &Duration) -> State { + let mut state = State::Finished; + + for c in &mut self.characters { + // Don't need to update if the character is dead + // if c.is_alive() { + if true { + let old = c.bbox(); + + // Compute the offset between position and bbox + let offset = old.position - c.position; + + c.update(duration); + + if let Some((axis, position, damage)) = self.map.collides_bbox(old, c.bbox()) { + c.position = position - offset; + if axis.is_x() { + c.speed.x = 0.0; + } + if axis.is_y() { + c.speed.y = 0.0; + c.ground_collision(); + } + + c.die(); + } else { + c.fall_off(); + } + + // If a character is alive and still have controls, the game should continue + if c.controls().is_some() { + state = State::Running; + } + } + } + + state + } + + // /// Transfers an event to the elements contained in the scene that should receive events. + // pub fn manage_event(&mut self, event: &Event) { + // for c in &mut self.characters { + // c.manage_event(event); + // } + // } + + /// Returns a reference to the characters of the scene. + pub fn characters(&self) -> &Vec { + &self.characters + } + + /// Returns a reference to the map. + pub fn map(&self) -> &Map { + &self.map + } +} + +/// Trait that needs to be implemented for everything that can be updatable. +pub trait Updatable { + /// Updates the thing depending on the duration since last frame. + fn update(&mut self, duration: &Duration); + + // /// Called when an event arrives. + // fn manage_event(&mut self, event: &Event); +} diff --git a/src/engine/vector.rs b/src/engine/vector.rs new file mode 100644 index 0000000..e4f2375 --- /dev/null +++ b/src/engine/vector.rs @@ -0,0 +1,81 @@ +//! A module for basic maths. + +use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +/// A 2 dimensional vector. +#[derive(Copy, Clone)] +pub struct Vector { + /// The X coordinate. + pub x: f64, + + /// The Y coordinate. + pub y: f64, +} + +impl Vector { + /// Creates a new vector from its coordinates. + pub fn new(x: f64, y: f64) -> Vector { + Vector { x, y } + } +} + +impl Add for Vector { + type Output = Vector; + fn add(self, rhs: Vector) -> Vector { + Vector { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl AddAssign for Vector { + fn add_assign(&mut self, rhs: Vector) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl Sub for Vector { + type Output = Vector; + fn sub(self, rhs: Vector) -> Vector { + Vector { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl SubAssign for Vector { + fn sub_assign(&mut self, rhs: Vector) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl Mul for Vector { + type Output = Vector; + fn mul(self, rhs: f64) -> Vector { + Vector { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl MulAssign for Vector { + fn mul_assign(&mut self, rhs: f64) { + self.x *= rhs; + self.y *= rhs; + } +} + +impl Mul for f64 { + type Output = Vector; + fn mul(self, rhs: Vector) -> Vector { + Vector { + x: rhs.x * self, + y: rhs.y * self, + } + } +} diff --git a/static/textures/rusty.png b/static/textures/rusty.png new file mode 100644 index 0000000..6805f4f Binary files /dev/null and b/static/textures/rusty.png differ