//! Tile map of the shared world. use crate::codec::{DecodeError, Reader, Writer}; /// Longest accepted map edge, in tiles. pub const MAX_MAP_SIZE: u32 = 1024; /// Tile coordinate. `x` grows to the right, `y` grows downwards. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Pos { pub x: i32, pub y: i32, } impl Pos { pub const fn new(x: i32, y: i32) -> Self { Self { x, y } } } /// What a tile is made of. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum Terrain { /// Open ground that can be walked on. Ground = 0, /// Rock that a stone pickaxe can break. SoftRock = 1, /// Rock that needs a copper pickaxe. HardRock = 2, /// Indestructible rock at the edge of the map. Bedrock = 3, } impl Terrain { pub fn from_u8(value: u8) -> Option { match value { 0 => Some(Terrain::Ground), 1 => Some(Terrain::SoftRock), 2 => Some(Terrain::HardRock), 3 => Some(Terrain::Bedrock), _ => None, } } pub fn is_walkable(self) -> bool { matches!(self, Terrain::Ground) } /// Number of hits that break this terrain, or `None` if it cannot be dug. pub fn hits_to_break(self) -> Option { match self { Terrain::SoftRock => Some(3), Terrain::HardRock => Some(6), Terrain::Ground | Terrain::Bedrock => None, } } /// Lowest pickaxe tier that can dig this terrain, or `None` if it cannot /// be dug. pub fn required_tool_tier(self) -> Option { match self { Terrain::SoftRock => Some(1), Terrain::HardRock => Some(2), Terrain::Ground | Terrain::Bedrock => None, } } } /// One cell of the map. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Tile { pub terrain: Terrain, /// Hits this tile has taken so far. Always below the number of hits that /// break its terrain, and zero for terrain that cannot be dug. pub damage: u8, } impl Tile { pub const fn new(terrain: Terrain) -> Self { Self { terrain, damage: 0 } } } /// Rectangular grid of tiles. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Map { width: u32, height: u32, tiles: Vec, } impl Map { /// Creates a map in which every tile has the given terrain. /// /// # Panics /// Panics if an edge is zero or longer than [`MAX_MAP_SIZE`]. pub fn filled(width: u32, height: u32, terrain: Terrain) -> Self { assert!(width > 0 && height > 0, "map must not be empty"); assert!( width <= MAX_MAP_SIZE && height <= MAX_MAP_SIZE, "map is too large" ); let count = (width as usize) * (height as usize); Self { width, height, tiles: vec![Tile::new(terrain); count], } } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } fn index(&self, pos: Pos) -> Option { if pos.x < 0 || pos.y < 0 { return None; } let (x, y) = (pos.x as u32, pos.y as u32); if x >= self.width || y >= self.height { return None; } Some((y as usize) * (self.width as usize) + (x as usize)) } pub fn in_bounds(&self, pos: Pos) -> bool { self.index(pos).is_some() } pub fn get(&self, pos: Pos) -> Option { self.index(pos).map(|index| self.tiles[index]) } /// Replaces the tile at `pos`. Returns `false` if `pos` is outside the /// map. pub fn set(&mut self, pos: Pos, tile: Tile) -> bool { match self.index(pos) { Some(index) => { self.tiles[index] = tile; true } None => false, } } /// All tiles, row by row: the tile at `pos` has index `y * width + x`. pub fn tiles(&self) -> &[Tile] { &self.tiles } pub fn encode(&self, w: &mut Writer) { w.u32(self.width); w.u32(self.height); for tile in &self.tiles { w.u8(tile.terrain as u8); w.u8(tile.damage); } } pub fn decode(r: &mut Reader<'_>) -> Result { let width = r.u32()?; let height = r.u32()?; if width == 0 || height == 0 || width > MAX_MAP_SIZE || height > MAX_MAP_SIZE { return Err(DecodeError::InvalidValue("map size")); } let count = (width as usize) * (height as usize); if r.remaining() < count * 2 { return Err(DecodeError::UnexpectedEnd); } let mut tiles = Vec::with_capacity(count); for _ in 0..count { let terrain = Terrain::from_u8(r.u8()?).ok_or(DecodeError::InvalidValue("terrain"))?; let damage = r.u8()?; if damage > 0 && damage >= terrain.hits_to_break().unwrap_or(0) { return Err(DecodeError::InvalidValue("tile damage")); } tiles.push(Tile { terrain, damage }); } Ok(Self { width, height, tiles, }) } }