Make MapBuilder modular with MapArchitect trait
This commit is contained in:
parent
9cd0af9679
commit
b807418601
94
src/map_builder/automata.rs
Normal file
94
src/map_builder/automata.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use super::MapArchitect;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct CellularAutomataArchitect {}
|
||||||
|
|
||||||
|
impl MapArchitect for CellularAutomataArchitect {
|
||||||
|
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
|
||||||
|
let mut mb = MapBuilder {
|
||||||
|
map: Map::new(),
|
||||||
|
rooms: Vec::new(),
|
||||||
|
monster_spawns: Vec::new(),
|
||||||
|
player_start: Point::zero(),
|
||||||
|
amulet_start: Point::zero(),
|
||||||
|
theme: super::themes::DungeonTheme::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.random_noise_map(rng, &mut mb.map);
|
||||||
|
for _ in 0..10 {
|
||||||
|
self.iteration(&mut mb.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = self.find_start(&mb.map);
|
||||||
|
mb.monster_spawns = mb.spawn_monsters(&start, rng);
|
||||||
|
mb.player_start = start;
|
||||||
|
mb.amulet_start = mb.find_most_distant(&mb.player_start);
|
||||||
|
|
||||||
|
mb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellularAutomataArchitect {
|
||||||
|
fn random_noise_map(&mut self, rng: &mut RandomNumberGenerator, map: &mut Map) {
|
||||||
|
map.tiles.iter_mut().for_each(|t| {
|
||||||
|
let roll = rng.range(0, 100);
|
||||||
|
if roll > 55 {
|
||||||
|
*t = TileType::Floor;
|
||||||
|
} else {
|
||||||
|
*t = TileType::Wall;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_neighbors(&self, x: i32, y: i32, map: &Map) -> usize {
|
||||||
|
let mut neighbors = 0;
|
||||||
|
|
||||||
|
for iy in -1..=1 {
|
||||||
|
for ix in -1..1 {
|
||||||
|
if !(ix == 0 && iy == 0) && map.tiles[map_idx(x + ix, y + iy)] == TileType::Wall {
|
||||||
|
neighbors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
neighbors
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iteration(&mut self, map: &mut Map) {
|
||||||
|
let mut new_tiles = map.tiles.clone();
|
||||||
|
|
||||||
|
for y in 1..SCREEN_HEIGHT - 1 {
|
||||||
|
for x in 1..SCREEN_WIDTH - 1 {
|
||||||
|
let neighbors = self.count_neighbors(x, y, map);
|
||||||
|
let idx = map_idx(x, y);
|
||||||
|
if neighbors > 4 || neighbors == 0 {
|
||||||
|
new_tiles[idx] = TileType::Wall;
|
||||||
|
} else {
|
||||||
|
new_tiles[idx] = TileType::Floor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.tiles = new_tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_start(&self, map: &Map) -> Point {
|
||||||
|
let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
|
||||||
|
let closest_point = map
|
||||||
|
.tiles
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, t)| **t == TileType::Floor)
|
||||||
|
.map(|(idx, _)| {
|
||||||
|
(
|
||||||
|
idx,
|
||||||
|
DistanceAlg::Pythagoras.distance2d(center, map.index_to_point2d(idx)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.min_by(|(_, distance), (_, distance2)| distance.partial_cmp(&distance2).unwrap())
|
||||||
|
.map(|(idx, _)| idx)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
map.index_to_point2d(closest_point)
|
||||||
|
}
|
||||||
|
}
|
87
src/map_builder/drunkard.rs
Normal file
87
src/map_builder/drunkard.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use super::MapArchitect;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
const STAGGER_DISTANCE: usize = 400;
|
||||||
|
const NUM_TILES: usize = (SCREEN_WIDTH * SCREEN_HEIGHT) as usize;
|
||||||
|
const DESIRED_FLOOR: usize = NUM_TILES / 3;
|
||||||
|
|
||||||
|
pub struct DrunkardsWalkArchitect {}
|
||||||
|
|
||||||
|
impl MapArchitect for DrunkardsWalkArchitect {
|
||||||
|
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
|
||||||
|
let mut mb = MapBuilder {
|
||||||
|
map: Map::new(),
|
||||||
|
rooms: Vec::new(),
|
||||||
|
monster_spawns: Vec::new(),
|
||||||
|
player_start: Point::zero(),
|
||||||
|
amulet_start: Point::zero(),
|
||||||
|
theme: super::themes::DungeonTheme::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mb.fill(TileType::Wall);
|
||||||
|
let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
|
||||||
|
self.drunkard(¢er, rng, &mut mb.map);
|
||||||
|
|
||||||
|
while mb
|
||||||
|
.map
|
||||||
|
.tiles
|
||||||
|
.iter()
|
||||||
|
.filter(|t| **t == TileType::Floor)
|
||||||
|
.count()
|
||||||
|
< DESIRED_FLOOR
|
||||||
|
{
|
||||||
|
self.drunkard(
|
||||||
|
&Point::new(rng.range(0, SCREEN_WIDTH), rng.range(0, SCREEN_HEIGHT)),
|
||||||
|
rng,
|
||||||
|
&mut mb.map,
|
||||||
|
);
|
||||||
|
|
||||||
|
let dijkstra_map = DijkstraMap::new(
|
||||||
|
SCREEN_WIDTH,
|
||||||
|
SCREEN_HEIGHT,
|
||||||
|
&[mb.map.point2d_to_index(center)],
|
||||||
|
&mb.map,
|
||||||
|
1024.0,
|
||||||
|
);
|
||||||
|
dijkstra_map
|
||||||
|
.map
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, distance)| *distance > &2000.0)
|
||||||
|
.for_each(|(idx, _)| mb.map.tiles[idx] = TileType::Wall);
|
||||||
|
}
|
||||||
|
|
||||||
|
mb.monster_spawns = mb.spawn_monsters(¢er, rng);
|
||||||
|
mb.player_start = center;
|
||||||
|
mb.amulet_start = mb.find_most_distant(&mb.player_start);
|
||||||
|
|
||||||
|
mb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrunkardsWalkArchitect {
|
||||||
|
fn drunkard(&mut self, start: &Point, rng: &mut RandomNumberGenerator, map: &mut Map) {
|
||||||
|
let mut drunkard_pos = start.clone();
|
||||||
|
let mut distance_staggered = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let drunk_idx = map.point2d_to_index(drunkard_pos);
|
||||||
|
map.tiles[drunk_idx] = TileType::Floor;
|
||||||
|
|
||||||
|
match rng.range(0, 4) {
|
||||||
|
0 => drunkard_pos.x -= 1,
|
||||||
|
1 => drunkard_pos.x += 1,
|
||||||
|
2 => drunkard_pos.y -= 1,
|
||||||
|
_ => drunkard_pos.y += 1,
|
||||||
|
}
|
||||||
|
if !map.in_bounds(drunkard_pos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
distance_staggered += 1;
|
||||||
|
if distance_staggered > STAGGER_DISTANCE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/map_builder/empty.rs
Normal file
27
src/map_builder/empty.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use super::MapArchitect;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct EmptyArchitect {}
|
||||||
|
|
||||||
|
impl MapArchitect for EmptyArchitect {
|
||||||
|
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
|
||||||
|
let mut mb = MapBuilder {
|
||||||
|
map: Map::new(),
|
||||||
|
rooms: Vec::new(),
|
||||||
|
monster_spawns: Vec::new(),
|
||||||
|
player_start: Point::zero(),
|
||||||
|
amulet_start: Point::zero(),
|
||||||
|
theme: super::themes::DungeonTheme::new(),
|
||||||
|
};
|
||||||
|
mb.fill(TileType::Floor);
|
||||||
|
mb.player_start = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
|
||||||
|
mb.amulet_start = mb.find_most_distant(&mb.player_start);
|
||||||
|
for _ in 0..50 {
|
||||||
|
mb.monster_spawns.push(Point::new(
|
||||||
|
rng.range(1, SCREEN_WIDTH),
|
||||||
|
rng.range(1, SCREEN_HEIGHT),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
mb
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +1,72 @@
|
|||||||
|
mod automata;
|
||||||
|
mod drunkard;
|
||||||
|
mod empty;
|
||||||
|
mod prefab;
|
||||||
|
mod rooms;
|
||||||
|
mod themes;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use automata::CellularAutomataArchitect;
|
||||||
|
use drunkard::DrunkardsWalkArchitect;
|
||||||
|
use empty::EmptyArchitect;
|
||||||
|
use prefab::apply_prefab;
|
||||||
|
use rooms::RoomsArchitect;
|
||||||
|
use themes::*;
|
||||||
|
|
||||||
const NUM_ROOMS: usize = 20;
|
const NUM_ROOMS: usize = 20;
|
||||||
|
|
||||||
|
trait MapArchitect {
|
||||||
|
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MapTheme: Sync + Send {
|
||||||
|
fn tile_to_render(&self, tile_type: TileType) -> FontCharType;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MapBuilder {
|
pub struct MapBuilder {
|
||||||
pub map: Map,
|
pub map: Map,
|
||||||
pub rooms: Vec<Rect>,
|
pub rooms: Vec<Rect>,
|
||||||
|
pub monster_spawns: Vec<Point>,
|
||||||
pub player_start: Point,
|
pub player_start: Point,
|
||||||
pub amulet_start: Point,
|
pub amulet_start: Point,
|
||||||
|
pub theme: Box<dyn MapTheme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapBuilder {
|
impl MapBuilder {
|
||||||
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
|
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
|
||||||
let mut mb = MapBuilder {
|
let mut architect: Box<dyn MapArchitect> = match rng.range(0, 3) {
|
||||||
map: Map::new(),
|
0 => Box::new(DrunkardsWalkArchitect {}),
|
||||||
rooms: Vec::new(),
|
1 => Box::new(RoomsArchitect {}),
|
||||||
player_start: Point::zero(),
|
2 => Box::new(CellularAutomataArchitect {}),
|
||||||
amulet_start: Point::zero(),
|
_ => Box::new(EmptyArchitect {}),
|
||||||
};
|
};
|
||||||
mb.fill(TileType::Wall);
|
|
||||||
mb.build_random_rooms(rng);
|
let mut mb = architect.new(rng);
|
||||||
mb.build_corridors(rng);
|
apply_prefab(&mut mb, rng);
|
||||||
mb.player_start = mb.rooms[0].center();
|
|
||||||
|
mb.theme = match rng.range(0, 2) {
|
||||||
|
0 => DungeonTheme::new(),
|
||||||
|
_ => ForestTheme::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mb
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill(&mut self, tile: TileType) {
|
||||||
|
self.map.tiles.iter_mut().for_each(|t| *t = tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_most_distant(&self, point: &Point) -> Point {
|
||||||
let dijkstra_map = DijkstraMap::new(
|
let dijkstra_map = DijkstraMap::new(
|
||||||
SCREEN_WIDTH,
|
SCREEN_WIDTH,
|
||||||
SCREEN_HEIGHT,
|
SCREEN_HEIGHT,
|
||||||
&[mb.map.point2d_to_index(mb.player_start)],
|
&[self.map.point2d_to_index(*point)],
|
||||||
&mb.map,
|
&self.map,
|
||||||
1024.0,
|
1024.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const UNREACHABLE: &f32 = &f32::MAX;
|
const UNREACHABLE: &f32 = &f32::MAX;
|
||||||
mb.amulet_start = mb.map.index_to_point2d(
|
self.map.index_to_point2d(
|
||||||
dijkstra_map
|
dijkstra_map
|
||||||
.map
|
.map
|
||||||
.iter()
|
.iter()
|
||||||
@ -38,12 +75,7 @@ impl MapBuilder {
|
|||||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0,
|
.0,
|
||||||
);
|
)
|
||||||
mb
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fill(&mut self, tile: TileType) {
|
|
||||||
self.map.tiles.iter_mut().for_each(|t| *t = tile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_random_rooms(&mut self, rng: &mut RandomNumberGenerator) {
|
fn build_random_rooms(&mut self, rng: &mut RandomNumberGenerator) {
|
||||||
@ -110,4 +142,28 @@ impl MapBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_monsters(&self, start: &Point, rng: &mut RandomNumberGenerator) -> Vec<Point> {
|
||||||
|
const NUM_MONSTERS: usize = 50;
|
||||||
|
let mut spawnable_tiles: Vec<Point> = self
|
||||||
|
.map
|
||||||
|
.tiles
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(idx, t)| {
|
||||||
|
**t == TileType::Floor
|
||||||
|
&& DistanceAlg::Pythagoras.distance2d(*start, self.map.index_to_point2d(*idx))
|
||||||
|
> 10.0
|
||||||
|
})
|
||||||
|
.map(|(idx, _)| self.map.index_to_point2d(idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut spawns = Vec::new();
|
||||||
|
for _ in 0..NUM_MONSTERS {
|
||||||
|
let target_index = rng.random_slice_index(&spawnable_tiles).unwrap();
|
||||||
|
spawns.push(spawnable_tiles[target_index]);
|
||||||
|
spawnable_tiles.remove(target_index);
|
||||||
|
}
|
||||||
|
spawns
|
||||||
|
}
|
||||||
}
|
}
|
84
src/map_builder/prefab.rs
Normal file
84
src/map_builder/prefab.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
const FORTRESS: (&str, i32, i32) = (
|
||||||
|
"
|
||||||
|
------------
|
||||||
|
---######---
|
||||||
|
---#----#---
|
||||||
|
---#-M--#---
|
||||||
|
-###----###-
|
||||||
|
--M------M--
|
||||||
|
-###----###-
|
||||||
|
---#----#---
|
||||||
|
---#----#---
|
||||||
|
---######---
|
||||||
|
------------
|
||||||
|
",
|
||||||
|
12,
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn apply_prefab(mb: &mut MapBuilder, rng: &mut RandomNumberGenerator) {
|
||||||
|
let mut placement = None;
|
||||||
|
|
||||||
|
let dijkstra_map = DijkstraMap::new(
|
||||||
|
SCREEN_WIDTH,
|
||||||
|
SCREEN_HEIGHT,
|
||||||
|
&[mb.map.point2d_to_index(mb.player_start)],
|
||||||
|
&mb.map,
|
||||||
|
1024.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut attempts = 0;
|
||||||
|
while placement.is_none() && attempts < 10 {
|
||||||
|
let dimensions = Rect::with_size(
|
||||||
|
rng.range(0, SCREEN_WIDTH - FORTRESS.1),
|
||||||
|
rng.range(0, SCREEN_HEIGHT - FORTRESS.2),
|
||||||
|
FORTRESS.1,
|
||||||
|
FORTRESS.2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut can_place = false;
|
||||||
|
dimensions.for_each(|pt| {
|
||||||
|
let idx = mb.map.point2d_to_index(pt);
|
||||||
|
let distance = dijkstra_map.map[idx];
|
||||||
|
if distance < 2000.0 && distance > 20.0 && mb.amulet_start != pt {
|
||||||
|
can_place = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if can_place {
|
||||||
|
placement = Some(Point::new(dimensions.x1, dimensions.y1));
|
||||||
|
let points = dimensions.point_set();
|
||||||
|
mb.monster_spawns.retain(|pt| !points.contains(pt));
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(placement) = placement {
|
||||||
|
let string_vec: Vec<char> = FORTRESS
|
||||||
|
.0
|
||||||
|
.chars()
|
||||||
|
.filter(|a| *a != '\r' && *a != '\n')
|
||||||
|
.collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
for ty in placement.y..placement.y + FORTRESS.2 {
|
||||||
|
for tx in placement.x..placement.x + FORTRESS.1 {
|
||||||
|
let idx = map_idx(tx, ty);
|
||||||
|
let c = string_vec[i];
|
||||||
|
match c {
|
||||||
|
'M' => {
|
||||||
|
mb.map.tiles[idx] = TileType::Floor;
|
||||||
|
mb.monster_spawns.push(Point::new(tx, ty));
|
||||||
|
}
|
||||||
|
'-' => mb.map.tiles[idx] = TileType::Floor,
|
||||||
|
'#' => mb.map.tiles[idx] = TileType::Wall,
|
||||||
|
_ => println!("No idea what to do with [{}]", c),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/map_builder/rooms.rs
Normal file
28
src/map_builder/rooms.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use super::MapArchitect;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct RoomsArchitect {}
|
||||||
|
|
||||||
|
impl MapArchitect for RoomsArchitect {
|
||||||
|
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
|
||||||
|
let mut mb = MapBuilder {
|
||||||
|
map: Map::new(),
|
||||||
|
rooms: Vec::new(),
|
||||||
|
monster_spawns: Vec::new(),
|
||||||
|
player_start: Point::zero(),
|
||||||
|
amulet_start: Point::zero(),
|
||||||
|
theme: super::themes::DungeonTheme::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mb.fill(TileType::Wall);
|
||||||
|
mb.build_random_rooms(rng);
|
||||||
|
mb.build_corridors(rng);
|
||||||
|
mb.player_start = mb.rooms[0].center();
|
||||||
|
mb.amulet_start = mb.find_most_distant(&mb.player_start);
|
||||||
|
for room in mb.rooms.iter().skip(1) {
|
||||||
|
mb.monster_spawns.push(room.center());
|
||||||
|
}
|
||||||
|
|
||||||
|
mb
|
||||||
|
}
|
||||||
|
}
|
35
src/map_builder/themes.rs
Normal file
35
src/map_builder/themes.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use super::MapTheme;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct DungeonTheme {}
|
||||||
|
pub struct ForestTheme {}
|
||||||
|
|
||||||
|
impl DungeonTheme {
|
||||||
|
pub fn new() -> Box<dyn MapTheme> {
|
||||||
|
Box::new(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapTheme for DungeonTheme {
|
||||||
|
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
|
||||||
|
match tile_type {
|
||||||
|
TileType::Floor => to_cp437('.'),
|
||||||
|
TileType::Wall => to_cp437('#'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForestTheme {
|
||||||
|
pub fn new() -> Box<dyn MapTheme> {
|
||||||
|
Box::new(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapTheme for ForestTheme {
|
||||||
|
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
|
||||||
|
match tile_type {
|
||||||
|
TileType::Floor => to_cp437(';'),
|
||||||
|
TileType::Wall => to_cp437('"'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user