use std::path::Path; use std::fs::File; use std::io::{BufWriter, BufReader}; use serde::{Serialize, Deserialize}; use bincode::{serialize, deserialize, serialize_into, deserialize_from}; use sfml::graphics::{FloatRect, IntRect}; use sfml::system::Vector2; use crate::{Error, Result}; use crate::engine::math::{clamp, Matrix}; use crate::engine::renderer::Drawable; use crate::engine::texture::Texture; use crate::engine::character::Damage; /// 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, Serialize, Deserialize)] 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, } } } /// This struct represents a renderable tile linking to its part in the tileset texture. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum GraphicTile { /// There is nothing to draw. Hidden, /// Top left corner of a solid tile. TopLeft, /// Top of a solid tile. Top, /// Top right corner of a solid tile. TopRight, /// Left of a solid tile. Left, /// Center of a solid tile. Center, /// Right of a solid tile. Right, /// Bottom left corner of a solid tile. BottomLeft, /// Bottom of a solid tile. Bottom, /// Bottom right corner of a solid tile. BottomRight, } impl GraphicTile { /// Returns a graphic tile from a collision tile. /// /// This might be ugly for the moment. pub fn from_collision_tile(collision: CollisionTile) -> GraphicTile { if collision == CollisionTile::empty() { GraphicTile::Hidden } else { GraphicTile::Center } } /// Checks if a graphic tile has a top border. pub fn is_top(self) -> bool { match self { GraphicTile::TopLeft | GraphicTile::Top | GraphicTile::TopRight => true, _ => false, } } /// Checks if a graphic tile has a left border. pub fn is_left(self) -> bool { match self { GraphicTile::TopLeft | GraphicTile::Left | GraphicTile::BottomLeft => true, _ => false, } } /// Checks if a graphic tile has a right border. pub fn is_right(self) -> bool { match self { GraphicTile::TopRight | GraphicTile::Right | GraphicTile::BottomRight => true, _ => false, } } /// Checks if a graphic tile has a bottom border. pub fn is_bottom(self) -> bool { match self { GraphicTile::BottomLeft | GraphicTile::Bottom | GraphicTile::BottomRight => true, _ => false, } } /// Creates a vec containing all the non hidden graphic tiles. pub fn all() -> Vec { vec![ GraphicTile::TopLeft, GraphicTile::Top, GraphicTile::TopRight, GraphicTile::Left, GraphicTile::Center, GraphicTile::Right, GraphicTile::BottomLeft, GraphicTile::Bottom, GraphicTile::BottomRight, ] } /// Creates the correct graphic tile depending on the neighbours. /// /// A none will be considered solid. pub fn from_neighbour_options( top: Option, left: Option, right: Option, bottom: Option, ) -> GraphicTile { GraphicTile::from_neighbours( top.unwrap_or_else(CollisionTile::full), left.unwrap_or_else(CollisionTile::full), right.unwrap_or_else(CollisionTile::full), bottom.unwrap_or_else(CollisionTile::full), ) } /// Creates the correct graphic tile depending on the neighbours. pub fn from_neighbours( top: CollisionTile, left: CollisionTile, right: CollisionTile, bottom: CollisionTile, ) -> GraphicTile { let mut all = GraphicTile::all() .into_iter() .map(|x| (x, true)) .collect::>(); for (ref mut tile, ref mut possible) in &mut all { if tile.is_top() == (top == CollisionTile::full()) { *possible = false; } if tile.is_left() == (left == CollisionTile::full()) { *possible = false; } if tile.is_right() == (right == CollisionTile::full()) { *possible = false; } if tile.is_bottom() == (bottom == CollisionTile::full()) { *possible = false; } } for (tile, possible) in all { if possible { return tile; } } GraphicTile::Center } /// Returns the offset to the corresponding graphic tile in the texture. pub fn offset(self) -> (i32, i32) { let vertical = match self { GraphicTile::TopLeft | GraphicTile::Top | GraphicTile::TopRight => 0, GraphicTile::Left | GraphicTile::Center | GraphicTile::Right => 16, GraphicTile::BottomLeft | GraphicTile::Bottom | GraphicTile::BottomRight => 32, _ => 0, }; let horizontal = match self { GraphicTile::TopLeft | GraphicTile::Left | GraphicTile::BottomLeft => 0, GraphicTile::Top | GraphicTile::Center | GraphicTile::Bottom => 16, GraphicTile::TopRight | GraphicTile::Right | GraphicTile::BottomRight => 32, _ => 0, }; // (horizontal, vertical) (vertical, horizontal) } } /// 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, 16, 16) } fn position(&self) -> Vector2 { self.position.into() } } /// The map represents the tiles contained in a level. #[derive(Serialize, Deserialize)] pub struct Map { /// The entrace point of the character in the map. entrance: (usize, usize), /// The tiles contained in the level. tiles: Matrix<(CollisionTile, GraphicTile)>, } 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) } /// Encodes the map into a binary format. pub fn encode(&self) -> Result> { serialize(&self).map_err(Error::Encoding) } /// Decodes a map from bytes. pub fn decode(content: &[u8]) -> Result { deserialize(content).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).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_err(Error::Decoding)?) } /// Creates a map from its tiles. pub fn from_collision_tiles(tiles: Matrix) -> Map { let rows = tiles.rows(); let cols = tiles.cols(); let mut matrix = Matrix::from_size(rows, cols, (CollisionTile::empty(), GraphicTile::Hidden)); for i in 0..rows { for j in 0..cols { let graphic = if tiles[(i, j)] == CollisionTile::full() { // TODO This is uggly // If there is an overflow, we should give None instead let (i, j) = (i as isize, j as isize); GraphicTile::from_neighbour_options( tiles.get(((i) as usize, (j - 1) as usize)).cloned(), tiles.get(((i - 1) as usize, (j) as usize)).cloned(), tiles.get(((i + 1) as usize, (j) as usize)).cloned(), tiles.get(((i) as usize, (j + 1) as usize)).cloned(), ) } else { GraphicTile::Hidden }; matrix[(i, j)] = (tiles[(i, j)], graphic); } } Map { entrance: Map::find_entrance(&matrix), tiles: matrix, } } /// Returns a tile of the map. pub fn tile(&self, i: usize, j: usize) -> Option { self.tiles.get((i, j)).map(|x| x.0) } /// Changes a tile of the map. pub fn set_tile(&mut self, i: usize, j: usize, tile: CollisionTile) { self.tiles[(i, j)] = (tile, GraphicTile::from_collision_tile(tile)); } /// Finds a possible entrance. pub fn find_entrance(tiles: &Matrix<(CollisionTile, GraphicTile)>) -> (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.tiles[(row, col)].0, graphic: self.tiles[(row, col)].1, position: (col as f32 * 16.0, row as f32 * 16.0), } } /// Returns the number of rows of the map. pub fn rows(&self) -> usize { self.tiles.rows() } /// Returns the number of columns of the map. pub fn cols(&self) -> usize { self.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.tiles.cols() - 1; let rows = self.tiles.rows() - 1; let min_col = clamp(new.left / 16.0, 0.0, cols as f32) as usize; let min_row = clamp(new.top / 16.0, 0.0, rows as f32) as usize; let max_col = clamp((new.left + new.width) / 16.0, 0.0, cols as f32) as usize; let max_row = clamp((new.top + new.height) / 16.0, 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 * 16.0; let tile_top = row as f32 * 16.0; let tile = FloatRect::new(tile_left, tile_top, 16.0, 16.0); if !overlap(new, tile) { continue; } // Collisions between feet and ground if self.tiles[(row, col)].0.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.tiles[(row, col)].0.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.tiles[(row, col)].0.from_right && old.left >= tile_left + 16.0 && new.left <= tile_left + 16.0 { collision_x = true; new.left = tile_left + 16.0; } if !overlap(new, tile) { continue; } // Collisions between head and roof if self.tiles[(row, col)].0.from_bottom && old.top >= tile_top + 16.0 && new.top <= tile_top + 16.0 { collision_y = true; new.top = tile_top + 16.0; } } } // 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 * 16.0 { new.left = cols as f32 * 16.0; collision_x = true; } // Collision between the player and the void if new.top > self.tiles.rows() as f32 * 16.0 { new.top = self.tiles.rows() as f32 * 16.0; 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 }