From a6d37bf0dcc438b8c717535f19b12436ea8ab143 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Wed, 15 Dec 2021 10:08:05 -0500 Subject: [PATCH] Refactor voronoi map builder to use Rust builder pattern --- src/map_builders.rs | 38 ++++-- src/map_builders/area_starting_points.rs | 79 ++++++++++++ src/map_builders/bsp_dungeon.rs | 14 ++- src/map_builders/bsp_interior.rs | 20 +-- src/map_builders/cellular_automata.rs | 149 ++++++----------------- src/map_builders/cull_unreachable.rs | 46 +++++++ src/map_builders/distant_exit.rs | 55 +++++++++ src/map_builders/voronoi_spawning.rs | 56 +++++++++ 8 files changed, 321 insertions(+), 136 deletions(-) create mode 100644 src/map_builders/area_starting_points.rs create mode 100644 src/map_builders/cull_unreachable.rs create mode 100644 src/map_builders/distant_exit.rs create mode 100644 src/map_builders/voronoi_spawning.rs diff --git a/src/map_builders.rs b/src/map_builders.rs index 249450b..75f6411 100644 --- a/src/map_builders.rs +++ b/src/map_builders.rs @@ -1,7 +1,10 @@ +mod area_starting_points; mod bsp_dungeon; mod bsp_interior; mod cellular_automata; mod common; +mod cull_unreachable; +mod distant_exit; mod dla; mod drunkard; mod maze; @@ -11,14 +14,17 @@ mod room_based_stairs; mod room_based_starting_position; mod simple_map; mod voronoi; +mod voronoi_spawning; mod waveform_collapse; -use crate::{spawner, Map, Position, Rect, SHOW_MAPGEN_VISUALIZER}; use ::rltk::RandomNumberGenerator; +use area_starting_points::{AreaStartingPosition, XStart, YStart}; use bsp_dungeon::BspDungeonBuilder; use bsp_interior::BspInteriorBuilder; use cellular_automata::CellularAutomataBuilder; use common::*; +use cull_unreachable::CullUnreachable; +use distant_exit::DistantExit; use dla::DLABuilder; use drunkard::DrunkardsWalkBuilder; use maze::MazeBuilder; @@ -29,8 +35,11 @@ use room_based_starting_position::RoomBasedStartingPosition; use simple_map::SimpleMapBuilder; use specs::prelude::*; use voronoi::VoronoiCellBuilder; +use voronoi_spawning::VoronoiSpawning; use waveform_collapse::WaveformCollapseBuilder; +use crate::{spawner, Map, Position, Rect, SHOW_MAPGEN_VISUALIZER}; + pub struct BuilderMap { pub spawn_list: Vec<(usize, String)>, pub map: Map, @@ -40,6 +49,16 @@ pub struct BuilderMap { } impl BuilderMap { + fn new(new_depth: i32) -> BuilderMap { + BuilderMap { + spawn_list: Vec::new(), + map: Map::new(new_depth), + starting_position: None, + rooms: None, + history: Vec::new(), + } + } + fn take_snapshot(&mut self) { if SHOW_MAPGEN_VISUALIZER { let mut snapshot = self.map.clone(); @@ -62,13 +81,7 @@ impl BuilderChain { BuilderChain { starter: None, builders: Vec::new(), - build_data: BuilderMap { - spawn_list: Vec::new(), - map: Map::new(new_depth), - starting_position: None, - rooms: None, - history: Vec::new(), - }, + build_data: BuilderMap::new(new_depth), } } @@ -155,10 +168,11 @@ pub trait MapBuilder { pub fn random_builder(new_depth: i32, rng: &mut RandomNumberGenerator) -> BuilderChain { let mut builder = BuilderChain::new(new_depth); builder - .start_with(SimpleMapBuilder::new()) - .with(RoomBasedSpawner::new()) - .with(RoomBasedStartingPosition::new()) - .with(RoomBasedStairs::new()); + .start_with(CellularAutomataBuilder::new()) + .with(AreaStartingPosition::new(XStart::Center, YStart::Center)) + .with(CullUnreachable::new()) + .with(VoronoiSpawning::new()) + .with(DistantExit::new()); builder } diff --git a/src/map_builders/area_starting_points.rs b/src/map_builders/area_starting_points.rs new file mode 100644 index 0000000..a34a2fd --- /dev/null +++ b/src/map_builders/area_starting_points.rs @@ -0,0 +1,79 @@ +use rltk::RandomNumberGenerator; + +use crate::map_builders::{BuilderMap, MetaMapBuilder}; +use crate::{Position, TileType}; + +#[allow(dead_code)] +pub enum XStart { + Left, + Center, + Right, +} + +#[allow(dead_code)] +pub enum YStart { + Top, + Center, + Bottom, +} + +pub struct AreaStartingPosition { + x: XStart, + y: YStart, +} + +impl MetaMapBuilder for AreaStartingPosition { + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl AreaStartingPosition { + #[allow(dead_code)] + pub fn new(x: XStart, y: YStart) -> Box { + Box::new(AreaStartingPosition { x, y }) + } + + fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let seed_x = match self.x { + XStart::Left => 1, + XStart::Center => build_data.map.width / 2, + XStart::Right => build_data.map.width - 2, + }; + + let seed_y = match self.y { + YStart::Top => 1, + YStart::Center => build_data.map.height / 2, + YStart::Bottom => build_data.map.height - 2, + }; + + let mut available_floors: Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { + if *tiletype == TileType::Floor { + available_floors.push(( + idx, + rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new( + idx as i32 % build_data.map.width, + idx as i32 / build_data.map.width, + ), + rltk::Point::new(seed_x, seed_y), + ), + )); + } + } + if available_floors.is_empty() { + panic!("No valid floors to start on"); + } + + available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); + + let start_x = available_floors[0].0 as i32 % build_data.map.width; + let start_y = available_floors[0].0 as i32 / build_data.map.width; + + build_data.starting_position = Some(Position { + x: start_x, + y: start_y, + }); + } +} diff --git a/src/map_builders/bsp_dungeon.rs b/src/map_builders/bsp_dungeon.rs index 73fa0f1..cbc4a98 100644 --- a/src/map_builders/bsp_dungeon.rs +++ b/src/map_builders/bsp_dungeon.rs @@ -2,8 +2,8 @@ use rltk::RandomNumberGenerator; use crate::components::Position; use crate::map_builders::common::{apply_room_to_map, draw_corridor}; -use crate::{spawner, Map, Rect, TileType, SHOW_MAPGEN_VISUALIZER}; use crate::map_builders::{BuilderMap, InitialMapBuilder}; +use crate::{spawner, Map, Rect, TileType, SHOW_MAPGEN_VISUALIZER}; pub struct BspDungeonBuilder { rects: Vec, @@ -18,9 +18,7 @@ impl InitialMapBuilder for BspDungeonBuilder { impl BspDungeonBuilder { pub fn new() -> Box { - Box::new(BspDungeonBuilder { - rects: Vec::new(), - }) + Box::new(BspDungeonBuilder { rects: Vec::new() }) } fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { @@ -28,8 +26,12 @@ impl BspDungeonBuilder { self.rects.clear(); // Start with a single map-sized rectangle - self.rects - .push(Rect::new(2, 2, build_data.map.width - 5, build_data.map.height - 5)); + self.rects.push(Rect::new( + 2, + 2, + build_data.map.width - 5, + build_data.map.height - 5, + )); let first_room = self.rects[0]; self.add_subrects(first_room); // Divide the first room diff --git a/src/map_builders/bsp_interior.rs b/src/map_builders/bsp_interior.rs index b995f7a..9264f57 100644 --- a/src/map_builders/bsp_interior.rs +++ b/src/map_builders/bsp_interior.rs @@ -2,9 +2,9 @@ use rltk::RandomNumberGenerator; use super::MapBuilder; use crate::components::Position; -use crate::{spawner, Map, Rect, TileType, SHOW_MAPGEN_VISUALIZER}; -use crate::map_builders::{BuilderMap, InitialMapBuilder}; use crate::map_builders::common::draw_corridor; +use crate::map_builders::{BuilderMap, InitialMapBuilder}; +use crate::{spawner, Map, Rect, TileType, SHOW_MAPGEN_VISUALIZER}; const MIN_ROOM_SIZE: i32 = 8; @@ -21,9 +21,7 @@ impl InitialMapBuilder for BspInteriorBuilder { impl BspInteriorBuilder { pub fn new() -> Box { - Box::new(BspInteriorBuilder { - rects: Vec::new(), - }) + Box::new(BspInteriorBuilder { rects: Vec::new() }) } fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { @@ -31,8 +29,12 @@ impl BspInteriorBuilder { self.rects.clear(); // Start with a single map-sized rectangle - self.rects - .push(Rect::new(1, 1, build_data.map.width - 2, build_data.map.height - 2)); + self.rects.push(Rect::new( + 1, + 1, + build_data.map.width - 2, + build_data.map.height - 2, + )); let first_room = self.rects[0]; self.add_subrects(first_room, rng); // Divide the first room @@ -45,7 +47,9 @@ impl BspInteriorBuilder { for y in room.y1..room.y2 { for x in room.x1..room.x2 { let idx = build_data.map.xy_idx(x, y); - if idx > 0 && idx < ((build_data.map.width * build_data.map.height) - 1) as usize { + if idx > 0 + && idx < ((build_data.map.width * build_data.map.height) - 1) as usize + { build_data.map.tiles[idx] = TileType::Floor; } } diff --git a/src/map_builders/cellular_automata.rs b/src/map_builders/cellular_automata.rs index 52b63d0..e0809e4 100644 --- a/src/map_builders/cellular_automata.rs +++ b/src/map_builders/cellular_automata.rs @@ -2,117 +2,83 @@ use std::collections::HashMap; use rltk::RandomNumberGenerator; -use super::common::{ +use crate::components::Position; +use crate::map_builders::common::{ generate_voronoi_spawn_regions, remove_unreachable_areas_returning_most_distant, }; -use super::MapBuilder; -use crate::components::Position; +use crate::map_builders::{BuilderMap, InitialMapBuilder}; use crate::{spawner, Map, TileType, SHOW_MAPGEN_VISUALIZER}; -pub struct CellularAutomataBuilder { - map: Map, - starting_position: Position, - depth: i32, - history: Vec, - noise_areas: HashMap>, - spawn_list: Vec<(usize, String)>, -} +pub struct CellularAutomataBuilder {} -impl MapBuilder for CellularAutomataBuilder { - fn get_map(&self) -> Map { - self.map.clone() - } - - fn get_starting_position(&self) -> Position { - self.starting_position - } - - fn get_snapshot_history(&self) -> Vec { - self.history.clone() - } - - fn build_map(&mut self) { - self.build(); - } - - fn take_snapshot(&mut self) { - if SHOW_MAPGEN_VISUALIZER { - let mut snapshot = self.map.clone(); - for v in snapshot.revealed_tiles.iter_mut() { - *v = true; - } - - self.history.push(snapshot); - } - } - - fn get_spawn_list(&self) -> &Vec<(usize, String)> { - &self.spawn_list +impl InitialMapBuilder for CellularAutomataBuilder { + #[allow(dead_code)] + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); } } impl CellularAutomataBuilder { - pub fn new(new_depth: i32) -> CellularAutomataBuilder { - CellularAutomataBuilder { - map: Map::new(new_depth), - starting_position: Position::default(), - depth: new_depth, - history: Vec::new(), - noise_areas: HashMap::new(), - spawn_list: Vec::new(), - } + pub fn new() -> Box { + Box::new(CellularAutomataBuilder {}) } - fn build(&mut self) { - let mut rng = RandomNumberGenerator::new(); - + fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { // First we completely randomize the map, setting 55% of it to be floor. - for y in 1..self.map.height - 1 { - for x in 1..self.map.width - 1 { + for y in 1..build_data.map.height - 1 { + for x in 1..build_data.map.width - 1 { let roll = rng.roll_dice(1, 100); - let idx = self.map.xy_idx(x, y); + let idx = build_data.map.xy_idx(x, y); if roll > 55 { - self.map.tiles[idx] = TileType::Floor + build_data.map.tiles[idx] = TileType::Floor } else { - self.map.tiles[idx] = TileType::Wall + build_data.map.tiles[idx] = TileType::Wall } } } - self.take_snapshot(); + build_data.take_snapshot(); // Now we iteratively apply cellular automata rules for _i in 0..15 { - let mut newtiles = self.map.tiles.clone(); + let mut newtiles = build_data.map.tiles.clone(); - for y in 1..self.map.height - 1 { - for x in 1..self.map.width - 1 { - let idx = self.map.xy_idx(x, y); + for y in 1..build_data.map.height - 1 { + for x in 1..build_data.map.width - 1 { + let idx = build_data.map.xy_idx(x, y); let mut neighbors = 0; - if self.map.tiles[idx - 1] == TileType::Wall { + if build_data.map.tiles[idx - 1] == TileType::Wall { neighbors += 1; } - if self.map.tiles[idx + 1] == TileType::Wall { + if build_data.map.tiles[idx + 1] == TileType::Wall { neighbors += 1; } - if self.map.tiles[idx - self.map.width as usize] == TileType::Wall { + if build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall { neighbors += 1; } - if self.map.tiles[idx + self.map.width as usize] == TileType::Wall { + if build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall { neighbors += 1; } - if self.map.tiles[idx - (self.map.width as usize - 1)] == TileType::Wall { + if build_data.map.tiles[idx - (build_data.map.width as usize - 1)] + == TileType::Wall + { neighbors += 1; } - if self.map.tiles[idx - (self.map.width as usize + 1)] == TileType::Wall { + if build_data.map.tiles[idx - (build_data.map.width as usize + 1)] + == TileType::Wall + { neighbors += 1; } - if self.map.tiles[idx + (self.map.width as usize - 1)] == TileType::Wall { + if build_data.map.tiles[idx + (build_data.map.width as usize - 1)] + == TileType::Wall + { neighbors += 1; } - if self.map.tiles[idx + (self.map.width as usize + 1)] == TileType::Wall { + if build_data.map.tiles[idx + (build_data.map.width as usize + 1)] + == TileType::Wall + { neighbors += 1; } @@ -124,45 +90,8 @@ impl CellularAutomataBuilder { } } - self.map.tiles = newtiles.clone(); - self.take_snapshot(); - } - - // Find a starting point; start at the middle and walk left until we find an open tile - self.starting_position = Position { - x: self.map.width / 2, - y: self.map.height / 2, - }; - let mut start_idx = self - .map - .xy_idx(self.starting_position.x, self.starting_position.y); - while self.map.tiles[start_idx] != TileType::Floor { - self.starting_position.x -= 1; - start_idx = self - .map - .xy_idx(self.starting_position.x, self.starting_position.y) - } - - // Find all tiles we can reach from the starting point - let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx); - self.take_snapshot(); - - // Place the stairs - self.map.tiles[exit_tile] = TileType::DownStairs; - self.take_snapshot(); - - // Now we build a noise map for use in spawning entities later - self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng); - - // Spawn the entities - for area in self.noise_areas.iter() { - spawner::spawn_region( - &self.map, - &mut rng, - area.1, - self.depth, - &mut self.spawn_list, - ); + build_data.map.tiles = newtiles.clone(); + build_data.take_snapshot(); } } } diff --git a/src/map_builders/cull_unreachable.rs b/src/map_builders/cull_unreachable.rs new file mode 100644 index 0000000..9342e54 --- /dev/null +++ b/src/map_builders/cull_unreachable.rs @@ -0,0 +1,46 @@ +use rltk::RandomNumberGenerator; + +use crate::map_builders::{BuilderMap, MetaMapBuilder}; +use crate::TileType; + +pub struct CullUnreachable {} + +impl MetaMapBuilder for CullUnreachable { + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl CullUnreachable { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CullUnreachable {}) + } + + fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); + let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); + + build_data.map.populate_blocked(); + + let map_starts: Vec = vec![start_idx]; + let dijkstra_map = rltk::DijkstraMap::new( + build_data.map.width as usize, + build_data.map.height as usize, + &map_starts, + &build_data.map, + 1000.0, + ); + + for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = dijkstra_map.map[i]; + + // We can't go to this tile - so we'll make it a wall + if distance_to_start == f32::MAX { + *tile = TileType::Wall; + } + } + } + } +} diff --git a/src/map_builders/distant_exit.rs b/src/map_builders/distant_exit.rs new file mode 100644 index 0000000..47a72c7 --- /dev/null +++ b/src/map_builders/distant_exit.rs @@ -0,0 +1,55 @@ +use rltk::RandomNumberGenerator; + +use crate::map_builders::{BuilderMap, MetaMapBuilder}; +use crate::TileType; + +pub struct DistantExit {} + +impl MetaMapBuilder for DistantExit { + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl DistantExit { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DistantExit {}) + } + + fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); + let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); + + build_data.map.populate_blocked(); + + let map_starts: Vec = vec![start_idx]; + let dijkstra_map = rltk::DijkstraMap::new( + build_data.map.width as usize, + build_data.map.height as usize, + &map_starts, + &build_data.map, + 1000.0, + ); + let mut exit_tile = (0, 0.0f32); + for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = dijkstra_map.map[i]; + + // We can't get to this tile - so we'll make it a wall + if distance_to_start != f32::MAX { + // If it is further away than our current exit candidate, move the exit + if distance_to_start > exit_tile.1 { + exit_tile.0 = i; + exit_tile.1 = distance_to_start; + } + } + } + } + + // Place a staircase + let stairs_idx = exit_tile.0; + build_data.map.tiles[stairs_idx] = TileType::DownStairs; + build_data.take_snapshot(); + } +} diff --git a/src/map_builders/voronoi_spawning.rs b/src/map_builders/voronoi_spawning.rs new file mode 100644 index 0000000..531317c --- /dev/null +++ b/src/map_builders/voronoi_spawning.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use rltk::RandomNumberGenerator; + +use crate::map_builders::{BuilderMap, MetaMapBuilder}; +use crate::{spawner, TileType}; + +pub struct VoronoiSpawning {} + +impl MetaMapBuilder for VoronoiSpawning { + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl VoronoiSpawning { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(VoronoiSpawning {}) + } + + fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let mut noise_areas: HashMap> = HashMap::new(); + let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as u64); + noise.set_noise_type(rltk::NoiseType::Cellular); + noise.set_frequency(0.08); + noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan); + + for y in 1..build_data.map.height - 1 { + for x in 1..build_data.map.width - 1 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::Floor { + let cell_value_f = noise.get_noise(x as f32, y as f32) * 10240.0; + let cell_value = cell_value_f as i32; + + if noise_areas.contains_key(&cell_value) { + noise_areas.get_mut(&cell_value).unwrap().push(idx); + } else { + noise_areas.insert(cell_value, vec![idx]); + } + } + } + } + + // Spawn the entities + for area in noise_areas.iter() { + spawner::spawn_region( + &build_data.map, + rng, + area.1, + build_data.map.depth, + &mut build_data.spawn_list, + ); + } + } +}