//! Turn-based dungeons: the map, field of view, derelict-ship generation, //! and the rules of a crawl. Drawing lives in `scenes::derelict`. pub mod crawl; pub mod fov; pub mod ship; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Pos { pub x: i32, pub y: i32, } impl Pos { pub const fn new(x: i32, y: i32) -> Pos { Pos { x, y } } pub const fn offset(self, dx: i32, dy: i32) -> Pos { Pos::new(self.x + dx, self.y + dy) } /// Steps from here to `other` when diagonal moves are allowed. pub fn steps_to(self, other: Pos) -> i32 { (self.x - other.x).abs().max((self.y - other.y).abs()) } /// The eight cells around this one. pub fn neighbours(self) -> impl Iterator { const DIRS: [(i32, i32); 8] = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]; DIRS.into_iter().map(move |(dx, dy)| self.offset(dx, dy)) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Tile { /// Outside the ship. Void, /// The outer skin of the ship. Hull, /// An inside wall. Wall, Floor, /// Floor strewn with wreckage. Walkable; it just looks different. Debris, Door { open: bool, }, /// The way out: the airlock your own ship is docked to. Airlock, } impl Tile { /// Whether you can step onto it. Closed doors open when bumped instead, /// and stepping into the airlock leaves the ship. pub fn walkable(self) -> bool { matches!(self, Tile::Floor | Tile::Debris | Tile::Door { open: true }) } pub fn blocks_sight(self) -> bool { matches!( self, Tile::Void | Tile::Hull | Tile::Wall | Tile::Door { open: false } | Tile::Airlock ) } } /// A named area of the map, shown to the player while they stand in it. pub struct Room { pub name: &'static str, } pub struct Map { pub width: i32, pub height: i32, tiles: Vec, room_of: Vec>, pub rooms: Vec, } impl Map { pub fn new(width: i32, height: i32, fill: Tile) -> Map { let cells = (width * height) as usize; Map { width, height, tiles: vec![fill; cells], room_of: vec![None; cells], rooms: Vec::new(), } } pub fn contains(&self, p: Pos) -> bool { p.x >= 0 && p.y >= 0 && p.x < self.width && p.y < self.height } /// Index into a per-cell `Vec` laid out like the map. Panics off the map. pub fn index(&self, p: Pos) -> usize { assert!(self.contains(p), "{p:?} is off the map"); (p.y * self.width + p.x) as usize } /// The tile at `p`; everything off the map is `Void`. pub fn tile(&self, p: Pos) -> Tile { if self.contains(p) { self.tiles[self.index(p)] } else { Tile::Void } } pub fn set(&mut self, p: Pos, tile: Tile) { let i = self.index(p); self.tiles[i] = tile; } pub fn room_at(&self, p: Pos) -> Option<&Room> { if !self.contains(p) { return None; } self.room_of[self.index(p)].map(|r| &self.rooms[usize::from(r)]) } pub fn add_room(&mut self, name: &'static str) -> u16 { self.rooms.push(Room { name }); (self.rooms.len() - 1) as u16 } pub fn set_room(&mut self, p: Pos, room: Option) { let i = self.index(p); self.room_of[i] = room; } pub fn positions(&self) -> impl Iterator + use<> { let (w, h) = (self.width, self.height); (0..h).flat_map(move |y| (0..w).map(move |x| Pos::new(x, y))) } /// Steps from `from` to every cell reachable through walkable tiles and /// closed doors, moving diagonally too; `None` where unreachable. pub fn distances_from(&self, from: Pos) -> Vec> { let mut dist = vec![None; self.tiles.len()]; let mut queue = std::collections::VecDeque::from([from]); dist[self.index(from)] = Some(0); while let Some(p) = queue.pop_front() { let d = dist[self.index(p)].unwrap(); for n in p.neighbours() { let tile = self.tile(n); if (tile.walkable() || tile == (Tile::Door { open: false })) && dist[self.index(n)].is_none() { dist[self.index(n)] = Some(d + 1); queue.push_back(n); } } } dist } }