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 automata::CellularAutomataArchitect;
|
||||
use drunkard::DrunkardsWalkArchitect;
|
||||
use empty::EmptyArchitect;
|
||||
use prefab::apply_prefab;
|
||||
use rooms::RoomsArchitect;
|
||||
use themes::*;
|
||||
|
||||
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 map: Map,
|
||||
pub rooms: Vec<Rect>,
|
||||
pub monster_spawns: Vec<Point>,
|
||||
pub player_start: Point,
|
||||
pub amulet_start: Point,
|
||||
pub theme: Box<dyn MapTheme>,
|
||||
}
|
||||
|
||||
impl MapBuilder {
|
||||
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
|
||||
let mut mb = MapBuilder {
|
||||
map: Map::new(),
|
||||
rooms: Vec::new(),
|
||||
player_start: Point::zero(),
|
||||
amulet_start: Point::zero(),
|
||||
let mut architect: Box<dyn MapArchitect> = match rng.range(0, 3) {
|
||||
0 => Box::new(DrunkardsWalkArchitect {}),
|
||||
1 => Box::new(RoomsArchitect {}),
|
||||
2 => Box::new(CellularAutomataArchitect {}),
|
||||
_ => Box::new(EmptyArchitect {}),
|
||||
};
|
||||
mb.fill(TileType::Wall);
|
||||
mb.build_random_rooms(rng);
|
||||
mb.build_corridors(rng);
|
||||
mb.player_start = mb.rooms[0].center();
|
||||
|
||||
let mut mb = architect.new(rng);
|
||||
apply_prefab(&mut mb, rng);
|
||||
|
||||
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(
|
||||
SCREEN_WIDTH,
|
||||
SCREEN_HEIGHT,
|
||||
&[mb.map.point2d_to_index(mb.player_start)],
|
||||
&mb.map,
|
||||
&[self.map.point2d_to_index(*point)],
|
||||
&self.map,
|
||||
1024.0,
|
||||
);
|
||||
|
||||
const UNREACHABLE: &f32 = &f32::MAX;
|
||||
mb.amulet_start = mb.map.index_to_point2d(
|
||||
self.map.index_to_point2d(
|
||||
dijkstra_map
|
||||
.map
|
||||
.iter()
|
||||
@ -38,12 +75,7 @@ impl MapBuilder {
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.unwrap()
|
||||
.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) {
|
||||
@ -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