diff --git a/Cargo.toml b/Cargo.toml index a94d2ec..ff44a43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mapgen" -version = "0.2.0" +version = "0.2.1" authors = ["Krzysztof Langner "] description = "Map generator for games (dungeons, worlds etc.)" keywords = ["game", "map", "map-generator"] diff --git a/README.md b/README.md index 049681b..a3ee855 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Generate procedural maps for games. [Try it in the browser](https://klangner.git * Map generators * [ ] BSP Interior - * [ ] BSP Room + * [x] BSP Rooms * [x] Cellular automata * [ ] Diffusion-Limited Aggregation (DLA) * [ ] Drunkard's walk @@ -35,7 +35,7 @@ Generate procedural maps for games. [Try it in the browser](https://klangner.git Add dependency to your project ``` -mapgen = "0.1" +mapgen = "0.2" ``` Using single map generator: diff --git a/demo/src/lib.rs b/demo/src/lib.rs index c9fb11d..3e25a78 100644 --- a/demo/src/lib.rs +++ b/demo/src/lib.rs @@ -6,6 +6,7 @@ use mapgen::dungeon::{ map::TileType, cellular_automata::CellularAutomataGen, simple_rooms::SimpleRoomsGen, + bsp_rooms::BspRoomsGen, starting_point::{AreaStartingPosition, XStart, YStart}, cull_unreachable::CullUnreachable, distant_exit::DistantExit, @@ -63,12 +64,30 @@ impl World { tiles } } + pub fn new_bsp_rooms(width: u32, height: u32, seed: u32) -> World { + World::print_map_info(format!("BSP Rooms with the seed: {}", seed)); + let mut rng = StdRng::seed_from_u64(seed as u64); + let map = MapBuilder::new(BspRoomsGen::new()) + .with(NearestCorridors::new()) + .build_map_with_rng(width as usize, height as usize, &mut rng); + let tiles = (0..map.tiles.len()) + .map(|i| if map.tiles[i] == TileType::Floor {Cell::Floor} else {Cell::Wall}) + .collect(); + World { + width, + height, + tiles } + } + pub fn new_random(width: u32, height: u32, seed: u32) -> World { let mut rng = rand::thread_rng(); - if rng.gen::() < 0.5 { + let px = rng.gen::(); + if px < 0.3333 { World::new_cellular_automata(width, height, seed) - } else { + } else if px < 0.6666 { World::new_simple_rooms(width, height, seed) + } else { + World::new_bsp_rooms(width, height, seed) } } diff --git a/demo/www/index.html b/demo/www/index.html index a4e1734..0b9200a 100644 --- a/demo/www/index.html +++ b/demo/www/index.html @@ -37,6 +37,7 @@ diff --git a/demo/www/index.js b/demo/www/index.js index e3cd163..d1ff6f2 100644 --- a/demo/www/index.js +++ b/demo/www/index.js @@ -32,6 +32,12 @@ function newSimpleRooms() { requestAnimationFrame(renderLoop); } +function newBspRooms() { + var seed = Date.now(); + world = World.new_bsp_rooms(width, height, seed); + requestAnimationFrame(renderLoop); +} + function newRandomGen() { var seed = Date.now(); world = World.new_random(width, height, seed); @@ -101,4 +107,5 @@ newRandomGen(); // Connect UI element document.getElementById('cellular-automata-option').addEventListener('click', newCellularAutomata); document.getElementById('simple-rooms-option').addEventListener('click', newSimpleRooms); +document.getElementById('bsp-rooms-option').addEventListener('click', newBspRooms); document.getElementById('random-option').addEventListener('click', newRandomGen); diff --git a/src/common/random.rs b/src/common/random.rs index c77c70c..047d01f 100644 --- a/src/common/random.rs +++ b/src/common/random.rs @@ -6,7 +6,11 @@ use rand::prelude::*; /// Generate random number between start (inclusive) and end (exclusive). pub fn random_range(rng: &mut StdRng, start: usize, end: usize) -> usize { let max = (end - start) as u32; - ((rng.next_u32() % max) + start as u32) as usize + if max == 0 { + start + } else { + ((rng.next_u32() % max) + start as u32) as usize + } } /// ------------------------------------------------------------------------------------------------ diff --git a/src/dungeon/bsp_rooms.rs b/src/dungeon/bsp_rooms.rs new file mode 100644 index 0000000..042ced9 --- /dev/null +++ b/src/dungeon/bsp_rooms.rs @@ -0,0 +1,138 @@ +//! Random rooms map generator. +//! +//! Try to generate rooms of different size to fill the map area. +//! Rooms will not overlap. +//! +//! Example generator usage: +//! ``` +//! use rand::prelude::*; +//! use mapgen::dungeon::{ +//! MapGenerator, +//! bsp_rooms::BspRoomsGen +//! }; +//! +//! let mut rng = StdRng::seed_from_u64(100); +//! let gen = BspRoomsGen::new(); +//! let map = gen.generate_map(80, 50, &mut rng); +//! +//! assert_eq!(map.width, 80); +//! assert_eq!(map.height, 50); +//! ``` +//! + +use rand::prelude::*; +use super::MapGenerator; +use crate::common::geometry::Rect; +use crate::common::random; +use super::map::{Map, TileType}; + + +pub struct BspRoomsGen { + max_split: usize, +} + +impl MapGenerator for BspRoomsGen { + fn generate_map(&self, width: usize, height: usize, rng : &mut StdRng) -> Map { + self.build_rooms(width, height, rng) + } +} + +impl BspRoomsGen { + pub fn new() -> Box { + Box::new(BspRoomsGen { + max_split: 240, + }) + } + + fn build_rooms(&self, width: usize, height: usize, rng : &mut StdRng) -> Map { + let mut map = Map::new(width, height); + let mut rects: Vec = Vec::new(); + // Start with a single map-sized rectangle + rects.push( Rect::new(2, 2, (width-5) as i32, (height-5) as i32) ); + let first_room = rects[0]; + rects.append(&mut self.split_into_subrects(first_room)); // Divide the first room + + // Up to max_split times, we get a random rectangle and divide it. If its possible to squeeze a + // room in there, we place it and add it to the rooms list. + let mut n_rooms = 0; + while n_rooms < self.max_split { + let rect = self.get_random_rect(rng, &rects); + let candidate = self.get_random_sub_rect(rect, rng); + + if self.is_possible(candidate, &map) { + map.add_room(candidate); + rects.append(&mut self.split_into_subrects(rect)); + } + n_rooms += 1; + } + + map + } + + fn split_into_subrects(&self, rect: Rect) -> Vec { + let mut rects: Vec = Vec::new(); + let width = i32::abs(rect.x1 - rect.x2); + let height = i32::abs(rect.y1 - rect.y2); + let half_width = i32::max(width / 2, 1); + let half_height = i32::max(height / 2, 1); + + rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height )); + rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width, half_height )); + rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width, half_height )); + rects.push(Rect::new( rect.x1 + half_width, rect.y1 + half_height, half_width, half_height )); + + rects + } + + fn get_random_rect(&self, rng : &mut StdRng, rects: &Vec) -> Rect { + if rects.len() == 1 { return rects[0]; } + let idx = random::random_range(rng, 0, rects.len()); + rects[idx] + } + + fn get_random_sub_rect(&self, rect: Rect, rng: &mut StdRng) -> Rect { + let mut result = rect; + let rect_width = i32::abs(rect.x1 - rect.x2); + let rect_height = i32::abs(rect.y1 - rect.y2); + + let w = usize::max(3, random::random_range(rng, 1, usize::min(rect_width as usize, 20))) + 1; + let h = usize::max(3, random::random_range(rng, 1, usize::min(rect_height as usize, 20))) + 1; + + result.x1 += random::random_range(rng, 0, 6) as i32; + result.y1 += random::random_range(rng, 0, 6) as i32; + result.x2 = result.x1 + w as i32; + result.y2 = result.y1 + h as i32; + + result + } + + fn is_possible(&self, rect: Rect, map: &Map) -> bool { + let mut expanded = rect; + expanded.x1 -= 2; + expanded.x2 += 2; + expanded.y1 -= 2; + expanded.y2 += 2; + + let mut can_build = true; + + for r in map.rooms.iter() { + if r.intersect(&rect) { can_build = false; } + } + + for y in expanded.y1 ..= expanded.y2 { + for x in expanded.x1 ..= expanded.x2 { + if x > map.width as i32 -2 { can_build = false; } + if y > map.height as i32 -2 { can_build = false; } + if x < 1 { can_build = false; } + if y < 1 { can_build = false; } + if can_build { + if map.at(x as usize, y as usize) != TileType::Wall { + can_build = false; + } + } + } + } + + can_build + } +} \ No newline at end of file diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs index 277ea98..1ccd797 100644 --- a/src/dungeon/mod.rs +++ b/src/dungeon/mod.rs @@ -29,6 +29,7 @@ pub mod map; pub mod cellular_automata; pub mod cull_unreachable; +pub mod bsp_rooms; pub mod distant_exit; pub mod simple_rooms; pub mod rooms_corridors_nearest;