diff --git a/README.md b/README.md index 5a4c7f3..61beef5 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If you want to check how the maps look like, then: * [ ] Wave Function Collapse * Map modifiers (filters) * [ ] Area exit point - * [ ] Area starting point + * [x] Area starting point * [x] Cellular automata * [ ] Cull unreachable areas * [ ] Voronoi spawning diff --git a/demo/Cargo.toml b/demo/Cargo.toml index ec41db8..68fd312 100644 --- a/demo/Cargo.toml +++ b/demo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mapgen-demo" -version = "0.1.0" +version = "0.1.1" authors = ["Krzysztof Langner "] edition = "2018" diff --git a/demo/src/main.rs b/demo/src/main.rs index f1b936a..d01c212 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -1,4 +1,3 @@ -use rand::prelude::*; use amethyst::{ assets::{AssetStorage, Loader}, core::{ @@ -14,6 +13,7 @@ use amethyst::{ sprite::{SpriteSheet, SpriteSheetFormat, SpriteSheetHandle}, types::DefaultBackend, RenderFlat2D, RenderToWindow, RenderingBundle, Texture, + palette::Srgba, }, tiles::{MortonEncoder, RenderTiles2D, Tile, TileMap}, utils::application_root_dir, @@ -21,9 +21,10 @@ use amethyst::{ winit, }; use mapgen::dungeon::{ - MapGenerator, - map::{Map, TileType}, + MapBuilder, + map::{Map, Point, TileType}, cellular_automata::CellularAutomataGen, + starting_point::{AreaStartingPosition, XStart, YStart}, }; @@ -33,12 +34,25 @@ struct MapTiles ; impl Tile for MapTiles { fn sprite(&self, p: Point3, world: &World) -> Option { let map = world.read_resource::(); - if map.at(p.x as usize, p.y as usize) == TileType::Wall { + let player_pos = Point::new(p.x as usize, p.y as usize); + if map.starting_point == Some(player_pos) { + Some(64) + } else if map.at(p.x as usize, p.y as usize) == TileType::Wall { Some(35) } else { Some(46) } } + + fn tint(&self, p: Point3, world: &World) -> Srgba { + let map = world.read_resource::(); + let player_pos = Point::new(p.x as usize, p.y as usize); + if map.starting_point == Some(player_pos) { + Srgba::new(1.0, 1.0, 0.0, 1.0) + } else { + Srgba::new(1.0, 1.0, 1.0, 1.0) + } + } } fn load_tiles_sprite_sheet(world: &mut World, png_path: &str, ron_path: &str) -> SpriteSheetHandle { @@ -67,9 +81,9 @@ fn init_camera(world: &mut World, transform: Transform, camera: Camera) -> Entit } fn init_map(world: &mut World) { - let gen = CellularAutomataGen::new(80, 50); - let mut rng = StdRng::seed_from_u64(0); - let map = gen.generate_map(&mut rng); + let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) + .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) + .build_map(); world.insert(map); } diff --git a/src/dungeon/cellular_automata.rs b/src/dungeon/cellular_automata.rs index 046dafa..107b98e 100644 --- a/src/dungeon/cellular_automata.rs +++ b/src/dungeon/cellular_automata.rs @@ -23,7 +23,6 @@ //! ``` //! - use rand::prelude::*; use super::{MapGenerator, MapModifier}; use super::map::{Map, TileType}; diff --git a/src/dungeon/map.rs b/src/dungeon/map.rs index 7b64106..99f13c5 100644 --- a/src/dungeon/map.rs +++ b/src/dungeon/map.rs @@ -9,14 +9,22 @@ /// Position on the map #[derive(PartialEq, Copy, Clone, Debug, Eq, Hash)] -pub struct Position { +pub struct Point { x: usize, y: usize } -impl Position { - pub fn new(x: usize, y: usize) -> Position { - Position {x, y} +impl Point { + /// Create new point + pub fn new(x: usize, y: usize) -> Point { + Point {x, y} + } + + /// Euclidean distance to a given point + pub fn distance_to(self, point: &Point) -> f32 { + let a = (self.x as f32 - point.x as f32).powf(2.0); + let b = (self.y as f32 - point.y as f32).powf(2.0); + (a + b).sqrt() } } @@ -32,8 +40,8 @@ pub struct Map { pub tiles : Vec, pub width : usize, pub height : usize, - pub starting_point: Option, - pub exit_point: Option + pub starting_point: Option, + pub exit_point: Option } impl Map { @@ -70,6 +78,14 @@ impl Map { mod tests { use super::*; + #[test] + fn test_distance() { + let p1 = Point::new(10, 10); + let p2 = Point::new(14, 7); + let distance = p1.distance_to(&p2); + assert_eq!(distance, 5.0); + } + #[test] fn test_new_map() { let map = Map::new(10, 10); diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs index 4966b1b..4a6c9b4 100644 --- a/src/dungeon/mod.rs +++ b/src/dungeon/mod.rs @@ -6,10 +6,30 @@ //! * MapGenerators are use to create initial map. //! * MapModifiers modify existing map. //! +//! Example +//! ``` +//! use mapgen::dungeon::{ +//! MapBuilder, +//! map::{Map, Point, TileType}, +//! cellular_automata::CellularAutomataGen, +//! starting_point::{AreaStartingPosition, XStart, YStart}, +//! }; +//! +//! let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) +//! .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) +//! .build_map(); +//! +//! assert_eq!(map.width, 80); +//! assert_eq!(map.height, 50); +//! assert_eq!(map.starting_point.is_some(), true); +//! ``` +//! pub mod map; pub mod cellular_automata; +pub mod starting_point; +use std::time::{SystemTime, UNIX_EPOCH}; use rand::prelude::*; use map::Map; @@ -34,16 +54,19 @@ pub struct MapBuilder { } impl MapBuilder { + /// Create Map Builder with initial map generator pub fn new(generator : Box) -> MapBuilder { + let system_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Can't access system time"); MapBuilder { generator, modifiers: Vec::new(), - rng: StdRng::seed_from_u64(0) + rng: StdRng::seed_from_u64(system_time.as_secs()) } } - pub fn with(&mut self, modifier : Box) { + pub fn with(&mut self, modifier : Box) -> &mut MapBuilder { self.modifiers.push(modifier); + self } pub fn build_map(&mut self) -> Map { @@ -51,10 +74,31 @@ impl MapBuilder { // Build additional layers in turn for modifier in self.modifiers.iter() { - modifier.modify_map(&mut self.rng, &mut map); + map = modifier.modify_map(&mut self.rng, &map); } map } } +/// ------------------------------------------------------------------------------------------------ +/// Module unit tests +/// ------------------------------------------------------------------------------------------------ +#[cfg(test)] +mod tests { + use super::*; + use cellular_automata::CellularAutomataGen; + use starting_point::{AreaStartingPosition, XStart, YStart}; + + #[test] + fn test_ca_map() { + let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) + .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) + .build_map(); + + assert_eq!(map.width, 80); + assert_eq!(map.height, 50); + assert_eq!(map.starting_point.is_some(), true); + } + +} \ No newline at end of file diff --git a/src/dungeon/starting_point.rs b/src/dungeon/starting_point.rs new file mode 100644 index 0000000..8cb73fe --- /dev/null +++ b/src/dungeon/starting_point.rs @@ -0,0 +1,97 @@ +//! Add starting point to the map +//! +//! This modifier will try to add starting point by finding the floor title closes +//! to the given point. +//! +//! Example generator usage: +//! ``` +//! use rand::prelude::*; +//! use mapgen::dungeon::{ +//! MapModifier, +//! map::{Map, Point, TileType}, +//! starting_point::{AreaStartingPosition, XStart, YStart} +//! }; +//! +//! let mut rng = StdRng::seed_from_u64(100); +//! let mut map = Map::new(80, 50); +//! map.set_tile(10, 10, TileType::Floor); +//! let modifier = AreaStartingPosition::new(XStart::LEFT, YStart::TOP); +//! let new_map = modifier.modify_map(&mut rng, &map); +//! +//! assert_eq!(new_map.starting_point, Some(Point::new(10, 10))); +//! ``` +//! + +use rand::prelude::StdRng; +use super::{MapModifier}; +use super::map::{Map, Point, TileType}; + + +/// Initial x region position +pub enum XStart { LEFT, CENTER, RIGHT } + +/// Initial y region position +pub enum YStart { TOP, CENTER, BOTTOM } + +/// Add starting position to the map +pub struct AreaStartingPosition { + x : XStart, + y : YStart +} + +impl MapModifier for AreaStartingPosition { + fn modify_map(&self, _: &mut StdRng, map: &Map) -> Map { + self.build(map) + } +} + +impl AreaStartingPosition { + /// Create new modifier with given region + pub fn new(x : XStart, y : YStart) -> Box { + Box::new(AreaStartingPosition{ + x, y + }) + } + + fn build(&self, map : &Map) -> Map { + let seed_x; + let seed_y; + + match self.x { + XStart::LEFT => seed_x = 1, + XStart::CENTER => seed_x = map.width / 2, + XStart::RIGHT => seed_x = map.width - 2 + } + + match self.y { + YStart::TOP => seed_y = 1, + YStart::CENTER => seed_y = map.height / 2, + YStart::BOTTOM => seed_y = map.height - 2 + } + + let mut available_floors : Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in map.tiles.iter().enumerate() { + if *tiletype == TileType::Floor { + available_floors.push( + ( + idx, + Point::new(idx % map.width, idx / map.width) + .distance_to(&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 % map.width; + let start_y = available_floors[0].0 / map.width; + + let mut new_map = map.clone(); + new_map.starting_point = Some(Point::new(start_x, start_y)); + new_map + } +} \ No newline at end of file