├── web ├── .gitignore └── index.html ├── renovate.json ├── src ├── gamelog.rs ├── prefabs │ ├── spawn_table_structs.rs │ ├── loot_structs.rs │ ├── prop_structs.rs │ ├── item_structs.rs │ ├── mob_structs.rs │ └── mod.rs ├── map_builders │ ├── waveform_collapse │ │ ├── common.rs │ │ ├── mod.rs │ │ ├── constraints.rs │ │ └── solver.rs │ ├── room_corridor_spawner.rs │ ├── room_based_starting_position.rs │ ├── room_based_stairs.rs │ ├── prefab_builder │ │ ├── prefab_rooms.rs │ │ ├── prefab_sections.rs │ │ └── prefab_levels.rs │ ├── room_based_spawner.rs │ ├── cull_unreachable.rs │ ├── rooms_corridors_bsp.rs │ ├── simple_map.rs │ ├── distant_exit.rs │ ├── rooms_corridors_dogleg.rs │ ├── room_sorter.rs │ ├── voronoi_spawning.rs │ ├── room_corner_rounding.rs │ ├── room_draw.rs │ ├── rooms_corridors_nearest.rs │ ├── area_starting_points.rs │ ├── rooms_corridors_lines.rs │ ├── room_exploder.rs │ ├── door_placement.rs │ ├── cellular_automata.rs │ ├── bsp_interior.rs │ ├── bsp_dungeon.rs │ ├── voronoi.rs │ ├── forest.rs │ ├── common.rs │ ├── maze.rs │ ├── drunkard.rs │ └── dla.rs ├── rex_assets.rs ├── rect.rs ├── map │ ├── dungeon.rs │ ├── astar.rs │ ├── tile_type.rs │ ├── mod.rs │ ├── themes.rs │ └── fov.rs ├── game_system.rs ├── map_indexing_system.rs ├── random_table.rs ├── particle_system.rs ├── hunger_system.rs ├── trigger_system.rs ├── bystander_ai_system.rs ├── monster_ai_system.rs ├── visibility_system.rs ├── camera.rs ├── animal_ai_system.rs ├── spawner.rs ├── damage_system.rs ├── player.rs └── components.rs ├── resources ├── nyan.xp ├── mltest.xp ├── vga8x16.jpg ├── wfc-demo1.xp ├── wfc-demo2.xp ├── terminal8x8.jpg ├── wfc-populated.xp ├── example_tiles.jpg ├── example_tiles.xcf ├── SmallDungeon_80x50.xp ├── backing.fs ├── backing.vs ├── scanlines.vs ├── console_no_bg.fs ├── console_with_bg.fs ├── console_no_bg.vs ├── console_with_bg.vs └── scanlines.fs ├── .gitmodules ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE └── README.md /web/.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm 2 | *.js 3 | *.d.ts 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/gamelog.rs: -------------------------------------------------------------------------------- 1 | pub struct GameLog { 2 | pub entries: Vec, 3 | } 4 | -------------------------------------------------------------------------------- /resources/nyan.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/nyan.xp -------------------------------------------------------------------------------- /resources/mltest.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/mltest.xp -------------------------------------------------------------------------------- /resources/vga8x16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/vga8x16.jpg -------------------------------------------------------------------------------- /resources/wfc-demo1.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/wfc-demo1.xp -------------------------------------------------------------------------------- /resources/wfc-demo2.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/wfc-demo2.xp -------------------------------------------------------------------------------- /resources/terminal8x8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/terminal8x8.jpg -------------------------------------------------------------------------------- /resources/wfc-populated.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/wfc-populated.xp -------------------------------------------------------------------------------- /resources/example_tiles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/example_tiles.jpg -------------------------------------------------------------------------------- /resources/example_tiles.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/example_tiles.xcf -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/legion"] 2 | path = external/legion 3 | url = https://github.com/TomGillen/legion.git 4 | -------------------------------------------------------------------------------- /resources/SmallDungeon_80x50.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smokku/rust_roguelike_tutorial/HEAD/resources/SmallDungeon_80x50.xp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Do not commit savegame 9 | /savegame.json 10 | -------------------------------------------------------------------------------- /resources/backing.fs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec2 TexCoords; 5 | 6 | uniform sampler2D screenTexture; 7 | 8 | void main() 9 | { 10 | vec3 col = texture(screenTexture, TexCoords).rgb; 11 | FragColor = vec4(col, 1.0); 12 | } -------------------------------------------------------------------------------- /resources/backing.vs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec2 aPos; 3 | layout (location = 1) in vec2 aTexCoords; 4 | 5 | out vec2 TexCoords; 6 | 7 | void main() 8 | { 9 | TexCoords = aTexCoords; 10 | gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /resources/scanlines.vs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec2 aPos; 3 | layout (location = 1) in vec2 aTexCoords; 4 | 5 | out vec2 TexCoords; 6 | 7 | void main() 8 | { 9 | TexCoords = aTexCoords; 10 | gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /src/prefabs/spawn_table_structs.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub struct SpawnTableEntry { 5 | pub name: String, 6 | pub weight: i32, 7 | pub min_depth: i32, 8 | pub max_depth: i32, 9 | pub add_map_depth_to_weight: Option, 10 | } 11 | -------------------------------------------------------------------------------- /src/prefabs/loot_structs.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub struct LootTable { 5 | pub name: String, 6 | pub drops: Vec, 7 | } 8 | 9 | #[derive(Deserialize, Debug)] 10 | pub struct LootDrop { 11 | pub name: String, 12 | pub weight: i32, 13 | } 14 | -------------------------------------------------------------------------------- /src/map_builders/waveform_collapse/common.rs: -------------------------------------------------------------------------------- 1 | use super::TileType; 2 | 3 | #[derive(PartialEq, Eq, Hash, Clone, Debug)] 4 | pub struct MapChunk { 5 | pub pattern: Vec, 6 | pub exits: [Vec; 4], 7 | pub has_exits: bool, 8 | pub compatible_with: [Vec; 4], 9 | } 10 | 11 | pub fn tile_idx_in_chunk(chunk_size: i32, x: i32, y: i32) -> usize { 12 | ((y * chunk_size) + x) as usize 13 | } 14 | -------------------------------------------------------------------------------- /resources/console_no_bg.fs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec3 ourColor; 5 | in vec2 TexCoord; 6 | in vec3 ourBackground; 7 | 8 | // texture sampler 9 | uniform sampler2D texture1; 10 | 11 | void main() 12 | { 13 | vec4 original = texture(texture1, TexCoord); 14 | if (original.r < 0.1f || original.g < 0.1f || original.b < 0.1f) discard; 15 | vec4 fg = original * vec4(ourColor, 1.f); 16 | FragColor = fg; 17 | } 18 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/console_with_bg.fs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec3 ourColor; 5 | in vec2 TexCoord; 6 | in vec3 ourBackground; 7 | 8 | // texture sampler 9 | uniform sampler2D texture1; 10 | 11 | void main() 12 | { 13 | vec4 original = texture(texture1, TexCoord); 14 | vec4 fg = original.r > 0.1f || original.g > 0.1f || original.b > 0.1f ? original * vec4(ourColor, 1.f) : vec4(ourBackground, 1.f); 15 | FragColor = fg; 16 | } 17 | -------------------------------------------------------------------------------- /resources/console_no_bg.vs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec3 aColor; 4 | layout (location = 2) in vec3 bColor; 5 | layout (location = 3) in vec2 aTexCoord; 6 | 7 | out vec3 ourColor; 8 | out vec3 ourBackground; 9 | out vec2 TexCoord; 10 | 11 | void main() 12 | { 13 | gl_Position = vec4(aPos, 1.0); 14 | ourColor = aColor; 15 | ourBackground = bColor; 16 | TexCoord = vec2(aTexCoord.x, aTexCoord.y); 17 | } -------------------------------------------------------------------------------- /resources/console_with_bg.vs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec3 aColor; 4 | layout (location = 2) in vec3 bColor; 5 | layout (location = 3) in vec2 aTexCoord; 6 | 7 | out vec3 ourColor; 8 | out vec3 ourBackground; 9 | out vec2 TexCoord; 10 | 11 | void main() 12 | { 13 | gl_Position = vec4(aPos, 1.0); 14 | ourColor = aColor; 15 | ourBackground = bColor; 16 | TexCoord = vec2(aTexCoord.x, aTexCoord.y); 17 | } -------------------------------------------------------------------------------- /src/prefabs/prop_structs.rs: -------------------------------------------------------------------------------- 1 | use super::Renderable; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize, Debug)] 6 | pub struct Prop { 7 | pub name: String, 8 | pub renderable: Option, 9 | pub hidden: Option, 10 | pub blocks_tile: Option, 11 | pub blocks_visibility: Option, 12 | pub door_open: Option, 13 | pub entry_trigger: Option, 14 | } 15 | 16 | #[derive(Deserialize, Debug)] 17 | pub struct EntryTrigger { 18 | pub effects: HashMap, 19 | } 20 | -------------------------------------------------------------------------------- /src/rex_assets.rs: -------------------------------------------------------------------------------- 1 | use rltk::rex::XpFile; 2 | 3 | rltk::embedded_resource!(SMALL_DUNGEON, "../resources/SmallDungeon_80x50.xp"); 4 | rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../resources/wfc-demo1.xp"); 5 | rltk::embedded_resource!(WFC_POPULATED, "../resources/wfc-populated.xp"); 6 | 7 | pub struct RexAssets { 8 | pub menu: XpFile, 9 | } 10 | 11 | impl RexAssets { 12 | pub fn new() -> Self { 13 | rltk::link_resource!(SMALL_DUNGEON, "../resources/SmallDungeon_80x50.xp"); 14 | rltk::link_resource!(WFC_DEMO_IMAGE1, "../resources/wfc-demo1.xp"); 15 | rltk::link_resource!(WFC_POPULATED, "../resources/wfc-populated.xp"); 16 | 17 | RexAssets { 18 | menu: XpFile::from_resource("../resources/SmallDungeon_80x50.xp").unwrap(), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rect.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 4 | pub struct Rect { 5 | pub x1: i32, 6 | pub x2: i32, 7 | pub y1: i32, 8 | pub y2: i32, 9 | } 10 | 11 | impl Rect { 12 | pub fn new(x: i32, y: i32, w: i32, h: i32) -> Rect { 13 | Rect { 14 | x1: x, 15 | y1: y, 16 | x2: x + w, 17 | y2: y + h, 18 | } 19 | } 20 | 21 | // Returns true if this overlaps with other 22 | pub fn intersects(&self, other: &Self) -> bool { 23 | self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1 24 | } 25 | 26 | pub fn center(&self) -> (i32, i32) { 27 | ((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/scanlines.fs: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec2 TexCoords; 5 | 6 | uniform sampler2D screenTexture; 7 | uniform vec3 screenSize; 8 | uniform bool screenBurn; 9 | 10 | void main() 11 | { 12 | vec3 col = texture(screenTexture, TexCoords).rgb; 13 | float scanLine = mod(gl_FragCoord.y, 2.0) * 0.25; 14 | vec3 scanColor = col.rgb - scanLine; 15 | 16 | if (col.r < 0.1f && col.g < 0.1f && col.b < 0.1f) { 17 | if (screenBurn) { 18 | float dist = (1.0 - distance(vec2(gl_FragCoord.x / screenSize.x, gl_FragCoord.y / screenSize.y), vec2(0.5,0.5))) * 0.2; 19 | FragColor = vec4(0.0, dist, dist, 1.0); 20 | } else { 21 | FragColor = vec4(0.0, 0.0, 0.0, 1.0); 22 | } 23 | } else { 24 | FragColor = vec4(scanColor, 1.0); 25 | } 26 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "astar", 4 | "backtrace", 5 | "bindgen", 6 | "bresenham", 7 | "chebyshev", 8 | "dedupe", 9 | "deser", 10 | "equippable", 11 | "gamelog", 12 | "greyscale", 13 | "insectoid", 14 | "longsword", 15 | "mapgen", 16 | "numpad", 17 | "rects", 18 | "renderable", 19 | "rltk", 20 | "roguelike", 21 | "runstate", 22 | "savegame", 23 | "saveload", 24 | "scanlines", 25 | "schedulable", 26 | "structs", 27 | "unequip", 28 | "unserializable", 29 | "uuid", 30 | "viewshed", 31 | "viewsheds", 32 | "voronoi" 33 | ], 34 | "search.exclude": { 35 | "external/**": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/map/dungeon.rs: -------------------------------------------------------------------------------- 1 | use super::Map; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default, Serialize, Deserialize, Clone)] 6 | pub struct MasterDungeonMap { 7 | maps: HashMap, 8 | } 9 | 10 | impl MasterDungeonMap { 11 | pub fn new() -> Self { 12 | MasterDungeonMap { 13 | maps: HashMap::new(), 14 | } 15 | } 16 | 17 | pub fn store_map(&mut self, map: &Map) { 18 | self.maps.insert(map.depth, map.clone()); 19 | } 20 | 21 | pub fn get_map(&self, depth: i32) -> Option { 22 | if self.maps.contains_key(&depth) { 23 | let mut result = self.maps[&depth].clone(); 24 | result.tile_content = vec![Vec::new(); (result.width * result.height) as usize]; 25 | Some(result) 26 | } else { 27 | None 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/prefabs/item_structs.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Deserialize, Debug)] 5 | pub struct Item { 6 | pub name: String, 7 | pub renderable: Option, 8 | pub consumable: Option, 9 | pub weapon: Option, 10 | pub wearable: Option, 11 | } 12 | 13 | #[derive(Deserialize, Debug)] 14 | pub struct Renderable { 15 | pub glyph: char, 16 | pub fg: String, 17 | pub bg: String, 18 | pub order: i32, 19 | } 20 | 21 | #[derive(Deserialize, Debug)] 22 | pub struct Consumable { 23 | pub effects: HashMap, 24 | } 25 | 26 | #[derive(Deserialize, Debug)] 27 | pub struct Weapon { 28 | pub range: String, 29 | pub attribute: String, 30 | pub base_damage: String, 31 | pub hit_bonus: i32, 32 | } 33 | 34 | #[derive(Deserialize, Debug)] 35 | pub struct Wearable { 36 | pub armor_class: f32, 37 | pub slot: String, 38 | } 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Tomasz Sterna "] 3 | edition = "2018" 4 | name = "rust_roguelike_tutorial" 5 | version = "0.1.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | [dependencies] 9 | erased-serde = "0.3" 10 | lazy_static = "1.4.0" 11 | num-rational = "0.3" 12 | pathfinding = "2.0.4" 13 | regex = "1.3.9" 14 | rltk = { version = "0.8.1", features = ["serde"] } 15 | ron = "0.6.0" 16 | serde = { version = "1.0.115", features = ["derive"] } 17 | serde_json = "1.0.57" 18 | type-uuid = "0.1" 19 | uuid = { version = "0.8", features = ["v4"] } 20 | 21 | [dependencies.legion] 22 | default-features = false 23 | features = ["serialize"] 24 | path = "external/legion" 25 | version = "*" 26 | 27 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.legion] 28 | default-features = true 29 | path = "external/legion" 30 | version = "*" 31 | 32 | [target.'cfg(target_arch = "wasm32")'.dependencies] 33 | uuid = { version = "0.8", features = ["wasm-bindgen"] } 34 | -------------------------------------------------------------------------------- /src/map_builders/room_corridor_spawner.rs: -------------------------------------------------------------------------------- 1 | use super::{spawner, BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct CorridorSpawner {} 5 | 6 | impl MetaMapBuilder for CorridorSpawner { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl CorridorSpawner { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(CorridorSpawner {}) 16 | } 17 | 18 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | if let Some(corridors) = &build_data.corridors { 20 | for c in corridors.iter() { 21 | let depth = build_data.map.depth; 22 | spawner::spawn_region(&build_data.map, rng, &c, depth, &mut build_data.spawn_list); 23 | } 24 | } else { 25 | panic!("Corridor Based Spawning only works after corridors have been created"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/game_system.rs: -------------------------------------------------------------------------------- 1 | use super::components::{Skill, Skills}; 2 | 3 | pub fn attr_bonus(value: i32) -> i32 { 4 | (value - 10) / 2 // See: https://roll20.net/compendium/dnd5e/Ability%20Scores#content 5 | } 6 | 7 | pub fn player_hp_per_level(fitness: i32) -> i32 { 8 | 10 + attr_bonus(fitness) 9 | } 10 | 11 | pub fn player_hp_at_level(fitness: i32, level: i32) -> i32 { 12 | 10 + player_hp_per_level(fitness) * level 13 | } 14 | 15 | pub fn npc_hp(fitness: i32, level: i32) -> i32 { 16 | let mut total = 1; 17 | for _i in 0..level { 18 | total += i32::max(1, 8 + attr_bonus(fitness)); 19 | } 20 | total 21 | } 22 | 23 | pub fn mana_per_level(intelligence: i32) -> i32 { 24 | i32::max(1, 4 + attr_bonus(intelligence)) 25 | } 26 | 27 | pub fn mana_at_level(intelligence: i32, level: i32) -> i32 { 28 | mana_per_level(intelligence) * level 29 | } 30 | 31 | pub fn skill_bonus(skill: Skill, skills: &Skills) -> i32 { 32 | if skills.skills.contains_key(&skill) { 33 | skills.skills[&skill] 34 | } else { 35 | -4 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/map_builders/room_based_starting_position.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, Position}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct RoomBasedStartingPosition {} 5 | 6 | impl MetaMapBuilder for RoomBasedStartingPosition { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl RoomBasedStartingPosition { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(RoomBasedStartingPosition {}) 16 | } 17 | 18 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | if let Some(rooms) = &build_data.rooms { 20 | let start_pos = rooms[0].center(); 21 | build_data.starting_position = Some(Position { 22 | x: start_pos.0, 23 | y: start_pos.1, 24 | }); 25 | } else { 26 | panic!("Room Based Staring Position only works after rooms have been created"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/map_builders/room_based_stairs.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct RoomBasedStairs {} 5 | 6 | impl MetaMapBuilder for RoomBasedStairs { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl RoomBasedStairs { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(RoomBasedStairs {}) 16 | } 17 | 18 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | if let Some(rooms) = &build_data.rooms { 20 | let stairs_position = rooms[rooms.len() - 1].center(); 21 | let stairs_idx = build_data.map.xy_idx(stairs_position.0, stairs_position.1); 22 | build_data.map.tiles[stairs_idx] = TileType::DownStairs; 23 | build_data.take_snapshot(); 24 | } else { 25 | panic!("Room Based Stairs only works after rooms have been created"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/map_indexing_system.rs: -------------------------------------------------------------------------------- 1 | use super::{BlocksTile, Map, Position}; 2 | use legion::prelude::*; 3 | 4 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 5 | SystemBuilder::new("map_indexing") 6 | .with_query(Read::::query()) 7 | .write_resource::() 8 | .build(|_, world, map, query| { 9 | map.populate_blocked(); 10 | map.clear_content_index(); 11 | for (entity, position) in query.iter_entities(world) { 12 | let idx = map.xy_idx(position.x, position.y); 13 | 14 | // If they block, update the blocking list 15 | let blocker = world.get_tag::(entity); 16 | if let Some(_blocker) = blocker { 17 | map.blocked[idx] = true; 18 | } 19 | 20 | // Push the entity to the appropriate index slot. 21 | // It's a Copy type, so we don't need to clone it 22 | // (we want to avoid moving it out of the ECS!) 23 | map.tile_content[idx].push(entity); 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/map_builders/prefab_builder/prefab_rooms.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Copy, Clone)] 2 | pub struct PrefabRoom { 3 | pub template: &'static str, 4 | pub width: usize, 5 | pub height: usize, 6 | pub first_depth: i32, 7 | pub last_depth: i32, 8 | } 9 | 10 | pub const TOTALLY_NOT_A_TRAP: PrefabRoom = PrefabRoom { 11 | template: TOTALLY_NOT_A_TRAP_MAP, 12 | width: 5, 13 | height: 5, 14 | first_depth: 0, 15 | last_depth: 100, 16 | }; 17 | 18 | const TOTALLY_NOT_A_TRAP_MAP: &str = " 19 | ^^^ 20 | ^!^ 21 | ^^^ 22 | 23 | "; 24 | 25 | pub const SILLY_SMILE: PrefabRoom = PrefabRoom { 26 | template: SILLY_SMILE_MAP, 27 | width: 6, 28 | height: 6, 29 | first_depth: 0, 30 | last_depth: 100, 31 | }; 32 | 33 | const SILLY_SMILE_MAP: &str = " 34 | ^ ^ 35 | ## 36 | 37 | #### 38 | 39 | "; 40 | 41 | pub const CHECKERBOARD: PrefabRoom = PrefabRoom { 42 | template: CHECKERBOARD_MAP, 43 | width: 6, 44 | height: 6, 45 | first_depth: 0, 46 | last_depth: 100, 47 | }; 48 | 49 | const CHECKERBOARD_MAP: &str = " 50 | #^# 51 | g#%# 52 | #!# 53 | ^# # 54 | 55 | "; 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tomasz Sterna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/map/astar.rs: -------------------------------------------------------------------------------- 1 | use super::Map; 2 | use pathfinding::prelude::astar; 3 | use rltk::{BaseMap, DistanceAlg, Point}; 4 | 5 | pub fn a_star_search(start: Point, end: Point, dist: f32, map: &Map) -> Option<(Vec, i32)> { 6 | astar( 7 | &start, 8 | |p| { 9 | let p_idx = map.xy_idx(p.x, p.y); 10 | map.get_available_exits(p_idx) 11 | .iter() 12 | .map(|(idx, cost)| { 13 | ( 14 | Point::new( 15 | *idx as i32 % map.width as i32, 16 | *idx as i32 / map.width as i32, 17 | ), 18 | (*cost * 256.) as i32, 19 | ) 20 | }) 21 | .collect::>() 22 | }, 23 | |p| (DistanceAlg::PythagorasSquared.distance2d(start, *p) * 256.) as i32, 24 | |p| { 25 | if dist == 0. { 26 | *p == end 27 | } else { 28 | DistanceAlg::PythagorasSquared.distance2d(end, *p) < dist 29 | } 30 | }, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/map_builders/room_based_spawner.rs: -------------------------------------------------------------------------------- 1 | use super::{spawner, BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct RoomBasedSpawner {} 5 | 6 | impl MetaMapBuilder for RoomBasedSpawner { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl RoomBasedSpawner { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(RoomBasedSpawner {}) 16 | } 17 | 18 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | if let Some(rooms) = &build_data.rooms { 20 | // Spawn some entities 21 | for room in rooms.iter().skip(1) { 22 | spawner::spawn_room( 23 | &build_data.map, 24 | rng, 25 | room, 26 | build_data.map.depth, 27 | &mut build_data.spawn_list, 28 | ); 29 | } 30 | } else { 31 | panic!("Room Based Spawning only works after rooms have been created"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/prefabs/mob_structs.rs: -------------------------------------------------------------------------------- 1 | use super::Renderable; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize, Debug)] 6 | pub struct Mob { 7 | pub name: String, 8 | pub renderable: Option, 9 | pub blocks_tile: bool, 10 | pub vision_range: i32, 11 | pub ai: String, 12 | pub quips: Option>, 13 | pub attributes: MobAttributes, 14 | pub skills: Option>, 15 | pub level: Option, 16 | pub hp: Option, 17 | pub mana: Option, 18 | pub equipped: Option>, 19 | pub natural: Option, 20 | pub loot_table: Option, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | pub struct MobAttributes { 25 | pub might: Option, 26 | pub fitness: Option, 27 | pub quickness: Option, 28 | pub intelligence: Option, 29 | } 30 | 31 | #[derive(Deserialize, Debug)] 32 | pub struct MobNatural { 33 | pub armor_class: Option, 34 | pub attacks: Option>, 35 | } 36 | 37 | #[derive(Deserialize, Debug)] 38 | pub struct NaturalAttack { 39 | pub name: String, 40 | pub hit_bonus: i32, 41 | pub damage: String, 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Roguelike Tutorial 2 | 3 | This is a [Roguelike Tutorial - in Rust](https://github.com/thebracket/rustrogueliketutorial) implemented using [Legion](https://github.com/TomGillen/legion) ECS (instead of Specs) and [RON](https://github.com/ron-rs/ron) based prefabs (instead of JSON). 4 | 5 | Still work in progress - as I progress through the tutorial. 6 | 7 | ## Using 8 | 9 | Commits in this repository follow the naming of Herbert's tutorial chapters and subchapters. 10 | If you would like to follow the tutorial, just checkout the commit corresponding to the chapter you are reading. 11 | 12 | ## Running 13 | 14 | Unfortunately, it requires `master` branch of Legion, so you will need to checkout `legion` submodule. 15 | 16 | Either clone all together: 17 | 18 | git clone --recursive https://github.com/smokku/rust_roguelike_tutorial.git 19 | 20 | or after normal clone do: 21 | 22 | git submodule update --init --recursive 23 | 24 | ## Building for Web 25 | 26 | cargo +nightly -Z features=itarget build --release --target wasm32-unknown-unknown 27 | wasm-bindgen target/wasm32-unknown-unknown/release/rust_roguelike_tutorial.wasm --out-dir web --no-modules --no-typescript 28 | 29 | serve web/ 30 | -------------------------------------------------------------------------------- /src/prefabs/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::sync::Mutex; 3 | 4 | mod prefab_master; 5 | pub use prefab_master::*; 6 | mod item_structs; 7 | use item_structs::*; 8 | mod mob_structs; 9 | use mob_structs::*; 10 | mod prop_structs; 11 | use prop_structs::*; 12 | mod spawn_table_structs; 13 | use spawn_table_structs::*; 14 | mod loot_structs; 15 | use loot_structs::*; 16 | 17 | #[derive(Deserialize, Debug)] 18 | pub struct Prefabs { 19 | pub spawn_table: Vec, 20 | pub items: Vec, 21 | pub mobs: Vec, 22 | pub props: Vec, 23 | pub loot_tables: Vec, 24 | } 25 | 26 | lazy_static! { 27 | pub static ref PREFABS: Mutex = Mutex::new(PrefabMaster::empty()); 28 | } 29 | 30 | rltk::embedded_resource!(PREFAB_FILE, "../../prefabs/spawns.ron"); 31 | 32 | pub fn load_prefabs() { 33 | rltk::link_resource!(PREFAB_FILE, "../../prefabs/spawns.ron"); 34 | 35 | // Retrieve the raw data as an array of u8 (8-bit unsigned chars) 36 | let raw_data = rltk::embedding::EMBED 37 | .lock() 38 | .get_resource("../../prefabs/spawns.ron".to_string()) 39 | .unwrap(); 40 | let raw_string = 41 | std::str::from_utf8(&raw_data).expect("Unable to convert to a valid UTF-8 string."); 42 | 43 | let decoder: Prefabs = ron::de::from_str(&raw_string).expect("Unable to parse RON"); 44 | 45 | PREFABS.lock().unwrap().load(decoder); 46 | } 47 | -------------------------------------------------------------------------------- /src/map_builders/prefab_builder/prefab_sections.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | #[derive(PartialEq, Copy, Clone)] 3 | pub enum HorizontalPlacement { 4 | Left, 5 | Center, 6 | Right, 7 | } 8 | 9 | #[allow(dead_code)] 10 | #[derive(PartialEq, Copy, Clone)] 11 | pub enum VerticalPlacement { 12 | Top, 13 | Center, 14 | Bottom, 15 | } 16 | 17 | #[derive(PartialEq, Copy, Clone)] 18 | pub struct PrefabSection { 19 | pub template: &'static str, 20 | pub width: usize, 21 | pub height: usize, 22 | pub placement: (HorizontalPlacement, VerticalPlacement), 23 | } 24 | 25 | pub const UNDERGROUND_FORT: PrefabSection = PrefabSection { 26 | template: RIGHT_FORT, 27 | width: 15, 28 | height: 43, 29 | placement: (HorizontalPlacement::Right, VerticalPlacement::Center), 30 | }; 31 | 32 | const RIGHT_FORT: &str = " # 33 | ####### 34 | # # 35 | # ####### 36 | # g # 37 | # ####### 38 | # # 39 | ### ### 40 | # # 41 | # # 42 | # ## 43 | ^ 44 | ^ 45 | # ## 46 | # # 47 | # # 48 | # # 49 | # # 50 | ### ### 51 | # # 52 | # # 53 | # g # 54 | # # 55 | # # 56 | ### ### 57 | # # 58 | # # 59 | # # 60 | # ## 61 | ^ 62 | ^ 63 | # ## 64 | # # 65 | # # 66 | # # 67 | ### ### 68 | # # 69 | # ####### 70 | # g # 71 | # ####### 72 | # # 73 | ####### 74 | # 75 | "; 76 | -------------------------------------------------------------------------------- /src/random_table.rs: -------------------------------------------------------------------------------- 1 | use rltk::RandomNumberGenerator; 2 | 3 | pub struct RandomEntry { 4 | name: String, 5 | weight: i32, 6 | } 7 | 8 | impl RandomEntry { 9 | pub fn new(name: S, weight: i32) -> Self { 10 | Self { 11 | name: name.to_string(), 12 | weight, 13 | } 14 | } 15 | } 16 | 17 | #[derive(Default)] 18 | pub struct RandomTable { 19 | entries: Vec, 20 | total_weight: i32, 21 | } 22 | 23 | impl RandomTable { 24 | pub fn new() -> Self { 25 | Self { 26 | entries: Vec::new(), 27 | total_weight: 0, 28 | } 29 | } 30 | 31 | pub fn add(mut self, name: S, weight: i32) -> Self { 32 | if weight > 0 { 33 | self.total_weight += weight; 34 | self.entries.push(RandomEntry::new(name, weight)); 35 | } 36 | self 37 | } 38 | 39 | pub fn roll(&self, rng: &mut RandomNumberGenerator) -> Option { 40 | if self.total_weight == 0 { 41 | return None; 42 | } 43 | 44 | let mut roll = rng.roll_dice(1, self.total_weight) - 1; 45 | let mut index: usize = 0; 46 | while roll > 0 { 47 | if roll < self.entries[index].weight { 48 | return Some(self.entries[index].name.clone()); 49 | } 50 | 51 | roll -= self.entries[index].weight; 52 | index += 1; 53 | } 54 | 55 | None 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/map_builders/cull_unreachable.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct CullUnreachable {} 5 | 6 | impl MetaMapBuilder for CullUnreachable { 7 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl CullUnreachable { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(CullUnreachable {}) 16 | } 17 | 18 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | // Find all the tiles we can reach from the starting point 20 | let starting_pos = build_data.starting_position.as_ref().unwrap(); 21 | let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); 22 | build_data.map.populate_blocked(); 23 | let map_starts: Vec = vec![start_idx]; 24 | let dijkstra_map = rltk::DijkstraMap::new( 25 | build_data.map.width, 26 | build_data.map.height, 27 | &map_starts, 28 | &build_data.map, 29 | 1000.0, 30 | ); 31 | for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { 32 | if *tile == TileType::Floor { 33 | let distance_to_start = dijkstra_map.map[i]; 34 | if distance_to_start == std::f32::MAX { 35 | // We can't get to this tile, so we'll make it a wall 36 | *tile = TileType::Wall; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/map_builders/rooms_corridors_bsp.rs: -------------------------------------------------------------------------------- 1 | use super::{draw_corridor, BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct BspCorridors {} 5 | 6 | impl MetaMapBuilder for BspCorridors { 7 | #[allow(dead_code)] 8 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 9 | self.corridors(rng, build_data); 10 | } 11 | } 12 | 13 | impl BspCorridors { 14 | pub fn new() -> Box { 15 | Box::new(BspCorridors {}) 16 | } 17 | 18 | fn corridors(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 20 | rooms_builder.clone() 21 | } else { 22 | panic!("BSP Corridors require a builder with room structures"); 23 | }; 24 | 25 | let mut corridors = Vec::new(); 26 | for i in 0..rooms.len() - 1 { 27 | let room = rooms[i]; 28 | let next_room = rooms[i + 1]; 29 | let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2))); 30 | let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2))); 31 | let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2))); 32 | let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2))); 33 | let corridor = draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); 34 | corridors.push(corridor); 35 | build_data.take_snapshot(); 36 | } 37 | build_data.corridors = Some(corridors); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/map_builders/simple_map.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, InitialMapBuilder, Rect}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct SimpleMapBuilder {} 5 | 6 | impl InitialMapBuilder for SimpleMapBuilder { 7 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build_rooms(rng, build_data); 9 | } 10 | } 11 | 12 | impl SimpleMapBuilder { 13 | pub fn new() -> Box { 14 | Box::new(SimpleMapBuilder {}) 15 | } 16 | 17 | /// Makes a new map using the algorithm from http://rogueliketutorials.com/tutorials/tcod/part-3/ 18 | /// This gives a handful of random rooms and corridors joining them together. 19 | fn build_rooms(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | const MAX_ROOMS: i32 = 30; 21 | const MIN_SIZE: i32 = 6; 22 | const MAX_SIZE: i32 = 10; 23 | let mut rooms = Vec::new(); 24 | 25 | for _i in 0..MAX_ROOMS { 26 | let w = rng.range(MIN_SIZE, MAX_SIZE); 27 | let h = rng.range(MIN_SIZE, MAX_SIZE); 28 | let x = rng.roll_dice(1, build_data.map.width - w - 1) - 1; 29 | let y = rng.roll_dice(1, build_data.map.height - h - 1) - 1; 30 | let new_room = Rect::new(x, y, w, h); 31 | let mut ok = true; 32 | for other_room in rooms.iter() { 33 | if new_room.intersects(other_room) { 34 | ok = false; 35 | break; 36 | } 37 | } 38 | if ok { 39 | rooms.push(new_room); 40 | } 41 | } 42 | 43 | build_data.rooms = Some(rooms); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/map/tile_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 4 | pub enum TileType { 5 | Wall, 6 | Floor, 7 | DownStairs, 8 | Road, 9 | Grass, 10 | ShallowWater, 11 | DeepWater, 12 | WoodFloor, 13 | Bridge, 14 | Gravel, 15 | UpStairs, 16 | } 17 | 18 | pub fn tile_walkable(tt: TileType) -> bool { 19 | match tt { 20 | TileType::Floor 21 | | TileType::DownStairs 22 | | TileType::UpStairs 23 | | TileType::Road 24 | | TileType::Grass 25 | | TileType::ShallowWater 26 | | TileType::WoodFloor 27 | | TileType::Bridge 28 | | TileType::Gravel => true, 29 | TileType::Wall | TileType::DeepWater => false, 30 | } 31 | } 32 | 33 | pub fn tile_opaque(tt: TileType) -> bool { 34 | match tt { 35 | TileType::Wall => true, 36 | TileType::Floor 37 | | TileType::DownStairs 38 | | TileType::UpStairs 39 | | TileType::Road 40 | | TileType::Grass 41 | | TileType::ShallowWater 42 | | TileType::DeepWater 43 | | TileType::WoodFloor 44 | | TileType::Bridge 45 | | TileType::Gravel => false, 46 | } 47 | } 48 | 49 | pub fn tile_cost(tt: TileType) -> f32 { 50 | match tt { 51 | TileType::Road => 0.8, 52 | TileType::Grass => 1.1, 53 | TileType::ShallowWater => 1.2, 54 | TileType::Wall 55 | | TileType::Floor 56 | | TileType::DownStairs 57 | | TileType::UpStairs 58 | | TileType::DeepWater 59 | | TileType::WoodFloor 60 | | TileType::Bridge 61 | | TileType::Gravel => 1.0, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/map_builders/distant_exit.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct DistantExit {} 5 | 6 | impl MetaMapBuilder for DistantExit { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl DistantExit { 13 | #[allow(dead_code)] 14 | pub fn new() -> Box { 15 | Box::new(DistantExit {}) 16 | } 17 | 18 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | // Find all the tiles we can reach from the starting point 20 | let starting_pos = build_data.starting_position.as_ref().unwrap(); 21 | let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); 22 | build_data.map.populate_blocked(); 23 | let map_starts: Vec = vec![start_idx]; 24 | let dijkstra_map = rltk::DijkstraMap::new( 25 | build_data.map.width, 26 | build_data.map.height, 27 | &map_starts, 28 | &build_data.map, 29 | 1000.0, 30 | ); 31 | let mut farthest_tile = 0; 32 | let mut farthest_tile_distance = 0.0f32; 33 | for (i, tile) in build_data.map.tiles.iter().enumerate() { 34 | if *tile == TileType::Floor { 35 | let distance_to_start = dijkstra_map.map[i]; 36 | if distance_to_start != std::f32::MAX { 37 | // If it is further away, move the exit 38 | if distance_to_start > farthest_tile_distance { 39 | farthest_tile = i; 40 | farthest_tile_distance = distance_to_start; 41 | } 42 | } 43 | } 44 | } 45 | 46 | // Place the stairs 47 | build_data.map.tiles[farthest_tile] = TileType::DownStairs; 48 | build_data.take_snapshot(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/map_builders/rooms_corridors_dogleg.rs: -------------------------------------------------------------------------------- 1 | use super::{apply_horizontal_tunnel, apply_vertical_tunnel, BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct DoglegCorridors {} 5 | 6 | impl MetaMapBuilder for DoglegCorridors { 7 | #[allow(dead_code)] 8 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 9 | self.corridors(rng, build_data); 10 | } 11 | } 12 | 13 | impl DoglegCorridors { 14 | pub fn new() -> Box { 15 | Box::new(DoglegCorridors {}) 16 | } 17 | 18 | fn corridors(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 20 | rooms_builder.clone() 21 | } else { 22 | panic!("Dogleg Corridors require a builder with room structures"); 23 | }; 24 | 25 | let mut corridors = Vec::new(); 26 | for (i, room) in rooms.iter().enumerate() { 27 | if i > 0 { 28 | let (new_x, new_y) = room.center(); 29 | let (prev_x, prev_y) = rooms[i - 1].center(); 30 | if rng.range(0, 2) == 1 { 31 | let mut c1 = 32 | apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y); 33 | let mut c2 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x); 34 | c1.append(&mut c2); 35 | corridors.push(c1); 36 | } else { 37 | let mut c1 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x); 38 | let mut c2 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y); 39 | c1.append(&mut c2); 40 | corridors.push(c1); 41 | } 42 | build_data.take_snapshot(); 43 | } 44 | } 45 | build_data.corridors = Some(corridors); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/map_builders/room_sorter.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub enum RoomSort { 5 | LEFTMOST, 6 | RIGHTMOST, 7 | TOPMOST, 8 | BOTTOMMOST, 9 | CENTRAL, 10 | } 11 | pub struct RoomSorter { 12 | sort_by: RoomSort, 13 | } 14 | 15 | impl MetaMapBuilder for RoomSorter { 16 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 17 | self.sorter(rng, build_data); 18 | } 19 | } 20 | 21 | impl RoomSorter { 22 | pub fn new(sort_by: RoomSort) -> Box { 23 | Box::new(RoomSorter { sort_by }) 24 | } 25 | 26 | fn sorter(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 27 | let rooms = build_data.rooms.as_mut().unwrap(); 28 | match self.sort_by { 29 | RoomSort::LEFTMOST => rooms.sort_by(|a, b| a.x1.cmp(&b.x1)), 30 | RoomSort::RIGHTMOST => rooms.sort_by(|a, b| b.x2.cmp(&a.x2)), 31 | RoomSort::TOPMOST => rooms.sort_by(|a, b| a.y1.cmp(&b.y1)), 32 | RoomSort::BOTTOMMOST => rooms.sort_by(|a, b| b.y2.cmp(&a.y2)), 33 | RoomSort::CENTRAL => { 34 | let map_center = 35 | rltk::Point::new(build_data.map.width / 2, build_data.map.height / 2); 36 | rooms.sort_by(|a, b| { 37 | let a_center = a.center(); 38 | let a_center_pt = rltk::Point::new(a_center.0, a_center.1); 39 | let b_center = b.center(); 40 | let b_center_pt = rltk::Point::new(b_center.0, b_center.1); 41 | let distance_a = 42 | rltk::DistanceAlg::Pythagoras.distance2d(a_center_pt, map_center); 43 | let distance_b = 44 | rltk::DistanceAlg::Pythagoras.distance2d(b_center_pt, map_center); 45 | distance_a.partial_cmp(&distance_b).unwrap() 46 | }); 47 | } 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/map_builders/voronoi_spawning.rs: -------------------------------------------------------------------------------- 1 | use super::{spawner, BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | use std::collections::HashMap; 4 | 5 | pub struct VoronoiSpawning {} 6 | 7 | impl MetaMapBuilder for VoronoiSpawning { 8 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 9 | self.build(rng, build_data); 10 | } 11 | } 12 | 13 | impl VoronoiSpawning { 14 | pub fn new() -> Box { 15 | Box::new(VoronoiSpawning {}) 16 | } 17 | 18 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 19 | let mut noise_areas: HashMap> = HashMap::new(); 20 | let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65526) as u64); 21 | noise.set_noise_type(rltk::NoiseType::Cellular); 22 | noise.set_frequency(0.08); 23 | noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan); 24 | 25 | for y in 1..build_data.map.height - 1 { 26 | for x in 1..build_data.map.width - 1 { 27 | let idx = build_data.map.xy_idx(x, y); 28 | if build_data.map.tiles[idx] == TileType::Floor { 29 | let cell_value_f = noise.get_noise(x as f32, y as f32) * 10240.0; 30 | let cell_value = cell_value_f as i32; 31 | 32 | if noise_areas.contains_key(&cell_value) { 33 | noise_areas.get_mut(&cell_value).unwrap().push(idx); 34 | } else { 35 | noise_areas.insert(cell_value, vec![idx]); 36 | } 37 | } 38 | } 39 | } 40 | 41 | // Spawn the entities 42 | for (_id, area) in noise_areas.iter() { 43 | spawner::spawn_region( 44 | &build_data.map, 45 | rng, 46 | area, 47 | build_data.map.depth, 48 | &mut build_data.spawn_list, 49 | ); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/map_builders/room_corner_rounding.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct RoomCornerRounder {} 5 | 6 | impl MetaMapBuilder for RoomCornerRounder { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl RoomCornerRounder { 13 | pub fn new() -> Box { 14 | Box::new(RoomCornerRounder {}) 15 | } 16 | 17 | fn fill_if_corner(&mut self, x: i32, y: i32, build_data: &mut BuilderMap) { 18 | let w = build_data.map.width; 19 | let h = build_data.map.height; 20 | let idx = build_data.map.xy_idx(x, y); 21 | let mut neighbor_walls = 0; 22 | if x > 0 && build_data.map.tiles[idx - 1] == TileType::Wall { 23 | neighbor_walls += 1; 24 | } 25 | if y > 0 && build_data.map.tiles[idx - w as usize] == TileType::Wall { 26 | neighbor_walls += 1; 27 | } 28 | if x < w - 2 && build_data.map.tiles[idx + 1] == TileType::Wall { 29 | neighbor_walls += 1; 30 | } 31 | if y < h - 2 && build_data.map.tiles[idx + w as usize] == TileType::Wall { 32 | neighbor_walls += 1; 33 | } 34 | 35 | if neighbor_walls == 2 { 36 | build_data.map.tiles[idx] = TileType::Wall; 37 | } 38 | } 39 | 40 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 41 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 42 | rooms_builder.clone() 43 | } else { 44 | panic!("Room Rounding require a builder with room structures"); 45 | }; 46 | 47 | for room in rooms.iter() { 48 | self.fill_if_corner(room.x1 + 1, room.y1 + 1, build_data); 49 | self.fill_if_corner(room.x2, room.y1 + 1, build_data); 50 | self.fill_if_corner(room.x1 + 1, room.y2, build_data); 51 | self.fill_if_corner(room.x2, room.y2, build_data); 52 | 53 | build_data.take_snapshot(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/map_builders/room_draw.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, Rect, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct RoomDrawer {} 5 | 6 | impl MetaMapBuilder for RoomDrawer { 7 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl RoomDrawer { 13 | pub fn new() -> Box { 14 | Box::new(RoomDrawer {}) 15 | } 16 | 17 | fn rectangle(&mut self, build_data: &mut BuilderMap, room: &Rect) { 18 | for y in room.y1 + 1..=room.y2 { 19 | for x in room.x1 + 1..=room.x2 { 20 | let idx = build_data.map.xy_idx(x, y); 21 | if idx < build_data.map.width as usize * build_data.map.height as usize { 22 | build_data.map.tiles[idx] = TileType::Floor; 23 | } 24 | } 25 | } 26 | } 27 | 28 | fn circle(&mut self, build_data: &mut BuilderMap, room: &Rect) { 29 | let radius = i32::min(room.x2 - room.x1, room.y2 - room.y1) as f32 / 2.0; 30 | let center = room.center(); 31 | let center_pt = rltk::Point::new(center.0, center.1); 32 | for y in room.y1..=room.y2 { 33 | for x in room.x1..=room.x2 { 34 | let idx = build_data.map.xy_idx(x, y); 35 | let distance = 36 | rltk::DistanceAlg::Pythagoras.distance2d(center_pt, rltk::Point::new(x, y)); 37 | if idx < (build_data.map.width * build_data.map.height) as usize 38 | && distance <= radius 39 | { 40 | build_data.map.tiles[idx] = TileType::Floor; 41 | } 42 | } 43 | } 44 | } 45 | 46 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 47 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 48 | rooms_builder.clone() 49 | } else { 50 | panic!("Room Drawer require a builder with room structures"); 51 | }; 52 | 53 | for room in rooms.iter() { 54 | let room_type = rng.roll_dice(1, 4); 55 | match room_type { 56 | 1 => self.circle(build_data, room), 57 | _ => self.rectangle(build_data, room), 58 | } 59 | build_data.take_snapshot(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/map_builders/rooms_corridors_nearest.rs: -------------------------------------------------------------------------------- 1 | use super::{draw_corridor, BuilderMap, MetaMapBuilder}; 2 | use rltk::RandomNumberGenerator; 3 | use std::collections::HashSet; 4 | 5 | pub struct NearestCorridors {} 6 | 7 | impl MetaMapBuilder for NearestCorridors { 8 | #[allow(dead_code)] 9 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 10 | self.corridors(rng, build_data); 11 | } 12 | } 13 | 14 | impl NearestCorridors { 15 | pub fn new() -> Box { 16 | Box::new(NearestCorridors {}) 17 | } 18 | 19 | fn corridors(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 21 | rooms_builder.clone() 22 | } else { 23 | panic!("Nearest Corridors require a builder with room structures"); 24 | }; 25 | 26 | let mut connected = HashSet::new(); 27 | let mut corridors = Vec::new(); 28 | for (i, room) in rooms.iter().enumerate() { 29 | let mut room_distance = Vec::new(); 30 | let room_center = room.center(); 31 | let room_center_pt = rltk::Point::new(room_center.0, room_center.1); 32 | for (j, other_room) in rooms.iter().enumerate() { 33 | if i != j && !connected.contains(&j) { 34 | let other_center = other_room.center(); 35 | let other_center_pt = rltk::Point::new(other_center.0, other_center.1); 36 | let distance = 37 | rltk::DistanceAlg::Pythagoras.distance2d(room_center_pt, other_center_pt); 38 | room_distance.push((j, distance)); 39 | } 40 | } 41 | 42 | if !room_distance.is_empty() { 43 | room_distance.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 44 | let dest_center = rooms[room_distance[0].0].center(); 45 | let corridor = draw_corridor( 46 | &mut build_data.map, 47 | room_center.0, 48 | room_center.1, 49 | dest_center.0, 50 | dest_center.1, 51 | ); 52 | connected.insert(i); 53 | corridors.push(corridor); 54 | build_data.take_snapshot(); 55 | } 56 | } 57 | build_data.corridors = Some(corridors); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/map_builders/area_starting_points.rs: -------------------------------------------------------------------------------- 1 | use super::{tile_walkable, BuilderMap, MetaMapBuilder, Position}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub enum XStart { 5 | LEFT, 6 | CENTER, 7 | RIGHT, 8 | } 9 | 10 | pub enum YStart { 11 | TOP, 12 | CENTER, 13 | BOTTOM, 14 | } 15 | 16 | pub struct AreaStartingPosition { 17 | x: XStart, 18 | y: YStart, 19 | } 20 | 21 | impl MetaMapBuilder for AreaStartingPosition { 22 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 23 | self.build(rng, build_data); 24 | } 25 | } 26 | 27 | impl AreaStartingPosition { 28 | pub fn new(x: XStart, y: YStart) -> Box { 29 | Box::new(AreaStartingPosition { x, y }) 30 | } 31 | 32 | fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 33 | let seed_x; 34 | let seed_y; 35 | 36 | match self.x { 37 | XStart::LEFT => seed_x = 1, 38 | XStart::CENTER => seed_x = build_data.map.width / 2, 39 | XStart::RIGHT => seed_x = build_data.map.width - 2, 40 | } 41 | match self.y { 42 | YStart::TOP => seed_y = 1, 43 | YStart::CENTER => seed_y = build_data.map.height / 2, 44 | YStart::BOTTOM => seed_y = build_data.map.height - 2, 45 | } 46 | 47 | let mut available_floors = Vec::new(); 48 | for (idx, tile_type) in build_data.map.tiles.iter().enumerate() { 49 | if tile_walkable(*tile_type) { 50 | available_floors.push(( 51 | idx, 52 | rltk::DistanceAlg::PythagorasSquared.distance2d( 53 | rltk::Point::new( 54 | idx as i32 % build_data.map.width, 55 | idx as i32 / build_data.map.width, 56 | ), 57 | rltk::Point::new(seed_x, seed_y), 58 | ), 59 | )); 60 | } 61 | } 62 | if available_floors.is_empty() { 63 | panic!("No valid floors to start on"); 64 | } 65 | 66 | available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 67 | 68 | let starting_idx = available_floors[0].0; 69 | let start_x = starting_idx as i32 % build_data.map.width; 70 | let start_y = starting_idx as i32 / build_data.map.width; 71 | 72 | build_data.starting_position = Some(Position { 73 | x: start_x, 74 | y: start_y, 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/particle_system.rs: -------------------------------------------------------------------------------- 1 | use super::{ParticleLifetime, Position, Renderable, Rltk}; 2 | use legion::prelude::*; 3 | use rltk::{FontCharType, RGB}; 4 | 5 | pub fn cull_dead_particles(world: &mut World, ctx: &Rltk) { 6 | let mut dead_particles = Vec::new(); 7 | let query = Write::::query(); 8 | for (entity, mut particle) in query.iter_entities_mut(world) { 9 | particle.lifetime_ms -= ctx.frame_time_ms; 10 | if particle.lifetime_ms < 0. { 11 | dead_particles.push(entity); 12 | } 13 | } 14 | 15 | for dead in dead_particles.iter() { 16 | world.delete(*dead); 17 | } 18 | } 19 | 20 | struct ParticleRequest { 21 | x: i32, 22 | y: i32, 23 | fg: RGB, 24 | bg: RGB, 25 | glyph: FontCharType, 26 | lifetime: f32, 27 | } 28 | 29 | pub struct ParticleBuilder { 30 | requests: Vec, 31 | } 32 | 33 | impl ParticleBuilder { 34 | pub fn new() -> Self { 35 | ParticleBuilder { 36 | requests: Vec::new(), 37 | } 38 | } 39 | 40 | pub fn request( 41 | &mut self, 42 | x: i32, 43 | y: i32, 44 | fg: RGB, 45 | bg: RGB, 46 | glyph: FontCharType, 47 | lifetime: f32, 48 | ) { 49 | self.requests.push(ParticleRequest { 50 | x, 51 | y, 52 | fg, 53 | bg, 54 | glyph, 55 | lifetime, 56 | }) 57 | } 58 | } 59 | 60 | pub fn particle_spawn() -> Box ()> { 61 | Box::new(|world: &mut World, resources: &mut Resources| { 62 | let mut particle_builder = resources.get_mut::().unwrap(); 63 | let particles: Vec<(Position, Renderable, ParticleLifetime)> = particle_builder 64 | .requests 65 | .iter() 66 | .map(|new_particle| { 67 | ( 68 | Position { 69 | x: new_particle.x, 70 | y: new_particle.y, 71 | }, 72 | Renderable { 73 | fg: new_particle.fg, 74 | bg: new_particle.bg, 75 | glyph: new_particle.glyph, 76 | render_order: -1, 77 | }, 78 | ParticleLifetime { 79 | lifetime_ms: new_particle.lifetime, 80 | }, 81 | ) 82 | }) 83 | .collect(); 84 | particle_builder.requests.clear(); 85 | world.insert((), particles); 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/map_builders/rooms_corridors_lines.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | use std::collections::HashSet; 4 | 5 | pub struct StraightLineCorridors {} 6 | 7 | impl MetaMapBuilder for StraightLineCorridors { 8 | #[allow(dead_code)] 9 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 10 | self.corridors(rng, build_data); 11 | } 12 | } 13 | 14 | impl StraightLineCorridors { 15 | pub fn new() -> Box { 16 | Box::new(StraightLineCorridors {}) 17 | } 18 | 19 | fn corridors(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 21 | rooms_builder.clone() 22 | } else { 23 | panic!("Straight Line Corridors require a builder with room structures"); 24 | }; 25 | 26 | let mut connected = HashSet::new(); 27 | let mut corridors = Vec::new(); 28 | for (i, room) in rooms.iter().enumerate() { 29 | let mut room_distance = Vec::new(); 30 | let room_center = room.center(); 31 | let room_center_pt = rltk::Point::new(room_center.0, room_center.1); 32 | for (j, other_room) in rooms.iter().enumerate() { 33 | if i != j && !connected.contains(&j) { 34 | let other_center = other_room.center(); 35 | let other_center_pt = rltk::Point::new(other_center.0, other_center.1); 36 | let distance = 37 | rltk::DistanceAlg::Pythagoras.distance2d(room_center_pt, other_center_pt); 38 | room_distance.push((j, distance)); 39 | } 40 | } 41 | 42 | if !room_distance.is_empty() { 43 | room_distance.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 44 | let dest_center = rooms[room_distance[0].0].center(); 45 | let line = rltk::line2d( 46 | rltk::LineAlg::Bresenham, 47 | room_center_pt, 48 | rltk::Point::new(dest_center.0, dest_center.1), 49 | ); 50 | let mut corridor = Vec::new(); 51 | for cell in line.iter() { 52 | let idx = build_data.map.xy_idx(cell.x, cell.y); 53 | if build_data.map.tiles[idx] != TileType::Floor { 54 | build_data.map.tiles[idx] = TileType::Floor; 55 | corridor.push(idx); 56 | } 57 | } 58 | connected.insert(i); 59 | corridors.push(corridor); 60 | build_data.take_snapshot(); 61 | } 62 | } 63 | build_data.corridors = Some(corridors); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/hunger_system.rs: -------------------------------------------------------------------------------- 1 | use super::{gamelog::GameLog, HungerClock, HungerState, RunState, SufferDamage}; 2 | use legion::prelude::*; 3 | 4 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 5 | SystemBuilder::new("hunger") 6 | .read_resource::() // The Player 7 | .read_resource::() 8 | .write_resource::() 9 | .with_query(Write::::query()) 10 | .build( 11 | |command_buffer, world, (player_entity, runstate, log), query| { 12 | for (entity, mut clock) in query.iter_entities_mut(world) { 13 | let is_player = entity == **player_entity; 14 | 15 | let proceed = match **runstate { 16 | RunState::PlayerTurn => is_player, 17 | RunState::MonsterTurn => !is_player, 18 | _ => false, 19 | }; 20 | 21 | if proceed { 22 | clock.duration -= 1; 23 | if clock.duration < 1 { 24 | match clock.state { 25 | HungerState::WellFed => { 26 | clock.state = HungerState::Normal; 27 | clock.duration = 200; 28 | if is_player { 29 | log.entries.push("You are no longer well fed.".to_string()); 30 | } 31 | } 32 | HungerState::Normal => { 33 | clock.state = HungerState::Hungry; 34 | clock.duration = 200; 35 | if is_player { 36 | log.entries.push("You are hungry.".to_string()); 37 | } 38 | } 39 | HungerState::Hungry => { 40 | clock.state = HungerState::Starving; 41 | clock.duration = 200; 42 | if is_player { 43 | log.entries.push("You are starving.".to_string()); 44 | } 45 | } 46 | HungerState::Starving => { 47 | // Inflict damage from hunger 48 | if is_player { 49 | log.entries.push( 50 | "Your hunger is getting painful!. You suffer 1 hp damage." 51 | .to_string(), 52 | ); 53 | } 54 | SufferDamage::new_damage(command_buffer, entity, 1, false); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/trigger_system.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | gamelog::GameLog, particle_system::ParticleBuilder, EntryTrigger, Hidden, InflictsDamage, Map, 3 | Name, Position, SingleActivation, SufferDamage, 4 | }; 5 | use legion::prelude::*; 6 | 7 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 8 | SystemBuilder::new("trigger") 9 | .with_query(Read::::query().filter(changed::())) 10 | .read_resource::() 11 | .write_resource::() 12 | .write_resource::() 13 | .read_component::() 14 | .read_component::() 15 | .build( 16 | |command_buffer, world, (map, log, particle_builder), query| { 17 | for (entity, pos) in query.iter_entities(world) { 18 | let idx = map.xy_idx(pos.x, pos.y); 19 | for map_entity in map.tile_content[idx].iter() { 20 | let map_entity = *map_entity; 21 | if entity != map_entity { 22 | // Do not bother to check yourself for being a trap! 23 | if let Some(_trigger) = world.get_tag::(map_entity) { 24 | // entity triggered it 25 | command_buffer.remove_tag::(map_entity); // The trap is no longer hidden 26 | 27 | if let Some(name) = world.get_component::(map_entity) { 28 | log.entries.push(format!("{} triggers!", &name.name)); 29 | } 30 | 31 | // If the trap is damage inflicting, do it 32 | if let Some(damage) = 33 | world.get_component::(map_entity) 34 | { 35 | particle_builder.request( 36 | pos.x, 37 | pos.y, 38 | rltk::RGB::named(rltk::ORANGE), 39 | rltk::RGB::named(rltk::BLACK), 40 | rltk::to_cp437('‼'), 41 | 200.0, 42 | ); 43 | SufferDamage::new_damage( 44 | command_buffer, 45 | entity, 46 | damage.damage, 47 | false, 48 | ); 49 | } 50 | 51 | // If it is single activation, it needs to be removed 52 | if let Some(_sa) = world.get_tag::(map_entity) { 53 | command_buffer.delete(map_entity); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/bystander_ai_system.rs: -------------------------------------------------------------------------------- 1 | use super::{gamelog::GameLog, Bystander, Map, Name, Point, Position, Quips, RunState, Viewshed}; 2 | use legion::prelude::*; 3 | 4 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 5 | SystemBuilder::new("bystander_ai") 6 | .with_query(<(Write, Write)>::query().filter(tag::())) 7 | .write_resource::() 8 | .read_resource::() 9 | .write_resource::() 10 | .read_resource::() 11 | .write_resource::() 12 | .write_component::() 13 | .read_component::() 14 | .build( 15 | |_, world, (map, runstate, rng, player_pos, gamelog), query| unsafe { 16 | if **runstate != RunState::MonsterTurn { 17 | return; 18 | } 19 | 20 | for (entity, (mut viewshed, mut pos)) in query.iter_entities_unchecked(world) { 21 | // Possibly quip 22 | if let Some(mut quips) = world.get_component_mut_unchecked::(entity) { 23 | if !quips.available.is_empty() 24 | && viewshed.visible_tiles.contains(&player_pos) 25 | && rng.roll_dice(1, 6) == 1 26 | { 27 | if let Some(name) = world.get_component::(entity) { 28 | let quip = if quips.available.len() == 1 { 29 | 0 30 | } else { 31 | (rng.roll_dice(1, quips.available.len() as i32) - 1) as usize 32 | }; 33 | gamelog.entries.push(format!( 34 | "{} says \"{}\"", 35 | name.name, quips.available[quip] 36 | )); 37 | quips.available.remove(quip); 38 | } 39 | } 40 | } 41 | 42 | // Try to move randomly 43 | let mut x = pos.x; 44 | let mut y = pos.y; 45 | let move_roll = rng.roll_dice(1, 5); 46 | match move_roll { 47 | 1 => x -= 1, 48 | 2 => x += 1, 49 | 3 => y -= 1, 50 | 4 => y += 1, 51 | _ => {} 52 | } 53 | 54 | if x >= 0 && x < map.width - 1 && y >= 0 && y < map.height - 1 { 55 | let dest_idx = map.xy_idx(x, y); 56 | if !map.blocked[dest_idx] { 57 | let idx = map.xy_idx(pos.x, pos.y); 58 | map.blocked[idx] = false; 59 | pos.x = x; 60 | pos.y = y; 61 | map.blocked[dest_idx] = true; 62 | viewshed.dirty = true; 63 | } 64 | } 65 | } 66 | }, 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/map_builders/room_exploder.rs: -------------------------------------------------------------------------------- 1 | use super::{paint, BuilderMap, MetaMapBuilder, Symmetry, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | pub struct RoomExploder; 4 | 5 | impl MetaMapBuilder for RoomExploder { 6 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 7 | self.build(rng, build_data); 8 | } 9 | } 10 | 11 | impl RoomExploder { 12 | pub fn new() -> Box { 13 | Box::new(RoomExploder {}) 14 | } 15 | 16 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 17 | let rooms = if let Some(rooms_builder) = &build_data.rooms { 18 | rooms_builder.clone() 19 | } else { 20 | panic!("Room Explosions require a builder with room structures"); 21 | }; 22 | 23 | for room in rooms.iter() { 24 | let start = room.center(); 25 | let n_diggers = rng.roll_dice(1, 20) - 5; 26 | if n_diggers > 0 { 27 | for _i in 0..n_diggers { 28 | let mut drunk_x = start.0; 29 | let mut drunk_y = start.1; 30 | 31 | let mut drunk_life = 20; 32 | let mut did_something = false; 33 | 34 | while drunk_life > 0 { 35 | let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y); 36 | if build_data.map.tiles[drunk_idx] == TileType::Wall { 37 | did_something = true; 38 | } 39 | paint(&mut build_data.map, Symmetry::None, 1, drunk_x, drunk_y); 40 | build_data.map.tiles[drunk_idx] = TileType::DownStairs; 41 | 42 | let stagger_direction = rng.roll_dice(1, 4); 43 | match stagger_direction { 44 | 1 => { 45 | if drunk_x > 2 { 46 | drunk_x -= 1; 47 | } 48 | } 49 | 2 => { 50 | if drunk_x < build_data.map.width - 2 { 51 | drunk_x += 1; 52 | } 53 | } 54 | 3 => { 55 | if drunk_y > 2 { 56 | drunk_y -= 1; 57 | } 58 | } 59 | _ => { 60 | if drunk_y < build_data.map.height - 2 { 61 | drunk_y += 1; 62 | } 63 | } 64 | } 65 | 66 | drunk_life -= 1; 67 | } 68 | if did_something { 69 | build_data.take_snapshot(); 70 | } 71 | 72 | for t in build_data.map.tiles.iter_mut() { 73 | if *t == TileType::DownStairs { 74 | *t = TileType::Floor; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/map_builders/door_placement.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct DoorPlacement {} 5 | 6 | impl MetaMapBuilder for DoorPlacement { 7 | #[allow(dead_code)] 8 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 9 | self.doors(rng, build_data); 10 | } 11 | } 12 | 13 | impl DoorPlacement { 14 | #[allow(dead_code)] 15 | pub fn new() -> Box { 16 | Box::new(DoorPlacement {}) 17 | } 18 | 19 | fn doors(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | if let Some(halls_original) = &build_data.corridors { 21 | let halls = halls_original.clone(); // To avoid nested borrowing 22 | for hall in halls.iter() { 23 | // We aren't interested in tiny corridors 24 | if hall.len() > 2 { 25 | if self.door_possible(build_data, hall[0]) { 26 | build_data.spawn_list.push((hall[0], "Door".to_string())); 27 | } 28 | } 29 | } 30 | } else { 31 | // There are no corridors - scan for possible places 32 | let tiles = build_data.map.tiles.clone(); 33 | for (i, tile) in tiles.iter().enumerate() { 34 | if *tile == TileType::Floor 35 | && self.door_possible(build_data, i) 36 | && rng.roll_dice(1, 3) == 1 37 | { 38 | build_data.spawn_list.push((i, "Door".to_string())); 39 | } 40 | } 41 | } 42 | } 43 | 44 | fn door_possible(&mut self, build_data: &mut BuilderMap, idx: usize) -> bool { 45 | for (spawn_idx, _name) in build_data.spawn_list.iter() { 46 | if *spawn_idx == idx { 47 | return false; 48 | } 49 | } 50 | 51 | let x = idx % build_data.map.width as usize; 52 | let y = idx / build_data.map.width as usize; 53 | 54 | // Check for east-west door possibility 55 | if build_data.map.tiles[idx] == TileType::Floor 56 | && x > 1 57 | && build_data.map.tiles[idx - 1] == TileType::Floor 58 | && x < build_data.map.width as usize - 2 59 | && build_data.map.tiles[idx + 1] == TileType::Floor 60 | && y > 1 61 | && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall 62 | && y < build_data.map.height as usize - 2 63 | && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall 64 | { 65 | return true; 66 | } 67 | 68 | // Check for north-south door possibility 69 | if build_data.map.tiles[idx] == TileType::Floor 70 | && x > 1 71 | && build_data.map.tiles[idx - 1] == TileType::Wall 72 | && x < build_data.map.width as usize - 2 73 | && build_data.map.tiles[idx + 1] == TileType::Wall 74 | && y > 1 75 | && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Floor 76 | && y < build_data.map.height as usize - 2 77 | && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Floor 78 | { 79 | return true; 80 | } 81 | 82 | false 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/monster_ai_system.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | a_star_search, particle_system::ParticleBuilder, Confusion, Map, Monster, Point, Position, 3 | RunState, Viewshed, WantsToMelee, 4 | }; 5 | use legion::prelude::*; 6 | 7 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 8 | SystemBuilder::new("monster_ai") 9 | .write_resource::() 10 | .read_resource::() 11 | .read_resource::() 12 | .read_resource::() 13 | .with_query(<(Write, Write)>::query().filter(tag::())) 14 | .write_component::() 15 | .write_resource::() 16 | .build( 17 | |command_buffer, 18 | world, 19 | (map, player_pos, player_entity, runstate, particle_builder), 20 | query| unsafe { 21 | if **runstate != RunState::MonsterTurn { 22 | return; 23 | } 24 | for (entity, (mut viewshed, mut pos)) in query.iter_entities_unchecked(world) { 25 | let mut can_act = true; 26 | 27 | if let Some(mut confused) = 28 | world.get_component_mut_unchecked::(entity) 29 | { 30 | confused.turns -= 1; 31 | if confused.turns < 1 { 32 | command_buffer.remove_component::(entity); 33 | } 34 | can_act = false; 35 | particle_builder.request( 36 | pos.x, 37 | pos.y, 38 | rltk::RGB::named(rltk::MAGENTA), 39 | rltk::RGB::named(rltk::BLACK), 40 | rltk::to_cp437('?'), 41 | 200.0, 42 | ); 43 | } 44 | 45 | if can_act { 46 | let distance = rltk::DistanceAlg::Pythagoras 47 | .distance2d(Point::new(pos.x, pos.y), **player_pos); 48 | if distance < 1.5 { 49 | command_buffer.add_component( 50 | entity, 51 | WantsToMelee { 52 | target: **player_entity, 53 | }, 54 | ); 55 | } else if viewshed.visible_tiles.contains(&**player_pos) { 56 | // Path to the player 57 | if let Some((path, _cost)) = 58 | a_star_search(Point::new(pos.x, pos.y), **player_pos, 2., &**map) 59 | { 60 | if path.len() > 1 { 61 | let mut idx = map.xy_idx(pos.x, pos.y); 62 | map.blocked[idx] = false; 63 | pos.x = path[1].x; 64 | pos.y = path[1].y; 65 | idx = map.xy_idx(pos.x, pos.y); 66 | map.blocked[idx] = true; 67 | viewshed.dirty = true; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/map_builders/cellular_automata.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, InitialMapBuilder, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct CellularAutomataBuilder {} 5 | 6 | impl InitialMapBuilder for CellularAutomataBuilder { 7 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl MetaMapBuilder for CellularAutomataBuilder { 13 | fn build_map(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 14 | self.apply_iteration(build_data); 15 | } 16 | } 17 | 18 | impl CellularAutomataBuilder { 19 | pub fn new() -> Box { 20 | Box::new(CellularAutomataBuilder {}) 21 | } 22 | 23 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 24 | // First we completely randomize the map, setting 55% of it to be floor. 25 | for y in 1..build_data.map.height - 1 { 26 | for x in 1..build_data.map.width - 1 { 27 | let roll = rng.roll_dice(1, 100); 28 | let idx = build_data.map.xy_idx(x, y); 29 | build_data.map.tiles[idx] = if roll > 55 { 30 | TileType::Floor 31 | } else { 32 | TileType::Wall 33 | }; 34 | } 35 | } 36 | build_data.take_snapshot(); 37 | 38 | // Now we iteratively apply cellular automata rules 39 | for _i in 0..15 { 40 | self.apply_iteration(build_data); 41 | } 42 | } 43 | 44 | fn apply_iteration(&mut self, build_data: &mut BuilderMap) { 45 | let mut new_tiles = build_data.map.tiles.clone(); 46 | 47 | for y in 1..build_data.map.height - 1 { 48 | for x in 1..build_data.map.width - 1 { 49 | let idx = build_data.map.xy_idx(x, y); 50 | let mut neighbors = 0; 51 | if build_data.map.tiles[idx - 1] == TileType::Wall { 52 | neighbors += 1; 53 | } 54 | if build_data.map.tiles[idx + 1] == TileType::Wall { 55 | neighbors += 1; 56 | } 57 | if build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall { 58 | neighbors += 1; 59 | } 60 | if build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall { 61 | neighbors += 1; 62 | } 63 | if build_data.map.tiles[idx - (build_data.map.width as usize - 1)] == TileType::Wall 64 | { 65 | neighbors += 1; 66 | } 67 | if build_data.map.tiles[idx - (build_data.map.width as usize + 1)] == TileType::Wall 68 | { 69 | neighbors += 1; 70 | } 71 | if build_data.map.tiles[idx + (build_data.map.width as usize - 1)] == TileType::Wall 72 | { 73 | neighbors += 1; 74 | } 75 | if build_data.map.tiles[idx + (build_data.map.width as usize + 1)] == TileType::Wall 76 | { 77 | neighbors += 1; 78 | } 79 | 80 | new_tiles[idx] = if neighbors > 4 || neighbors == 0 { 81 | TileType::Wall 82 | } else { 83 | TileType::Floor 84 | }; 85 | } 86 | } 87 | 88 | build_data.map.tiles = new_tiles; 89 | build_data.take_snapshot(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/map_builders/waveform_collapse/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, Map, MetaMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | mod common; 4 | use common::*; 5 | mod constraints; 6 | use constraints::*; 7 | mod solver; 8 | use solver::*; 9 | 10 | /// Provides a map builder using the Wave Function Collapse algorithm. 11 | pub struct WaveformCollapseBuilder {} 12 | 13 | impl MetaMapBuilder for WaveformCollapseBuilder { 14 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 15 | self.build(rng, build_data); 16 | } 17 | } 18 | 19 | impl WaveformCollapseBuilder { 20 | /// Constructor for waveform collapse. 21 | pub fn new() -> Box { 22 | Box::new(WaveformCollapseBuilder {}) 23 | } 24 | 25 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 26 | const CHUNK_SIZE: i32 = 8; 27 | 28 | let patterns = build_patterns(&build_data.map, CHUNK_SIZE, true, true); 29 | let constraints = patterns_to_constraints(patterns, CHUNK_SIZE); 30 | self.render_tile_gallery(&constraints, CHUNK_SIZE, build_data); 31 | 32 | let mut tries = 1; 33 | loop { 34 | build_data.map = Map::new( 35 | build_data.map.depth, 36 | build_data.map.width, 37 | build_data.map.height, 38 | build_data.map.name.clone(), 39 | ); 40 | let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE, &build_data.map); 41 | while !solver.iteration(&mut build_data.map, rng) { 42 | build_data.take_snapshot(); 43 | } 44 | build_data.take_snapshot(); 45 | 46 | // If it has hit an impossible condition, try again 47 | if solver.possible { 48 | break; 49 | } 50 | 51 | tries += 1; 52 | } 53 | rltk::console::log(format!("Took {} tries to solve", tries)); 54 | 55 | // We've rewritten whole map, so previous spawn points are invalid 56 | build_data.spawn_list.clear(); 57 | } 58 | 59 | fn render_tile_gallery( 60 | &mut self, 61 | constraints: &[MapChunk], 62 | chunk_size: i32, 63 | build_data: &mut BuilderMap, 64 | ) { 65 | build_data.map = Map::new( 66 | build_data.map.depth, 67 | build_data.map.width, 68 | build_data.map.height, 69 | build_data.map.name.clone(), 70 | ); 71 | let mut counter = 0; 72 | let mut x = 1; 73 | let mut y = 1; 74 | while counter < constraints.len() { 75 | render_pattern_to_map(&mut build_data.map, &constraints[counter], chunk_size, x, y); 76 | 77 | x += chunk_size + 1; 78 | if x + chunk_size > build_data.map.width { 79 | // Move to the next row 80 | x = 1; 81 | y += chunk_size + 1; 82 | 83 | if y + chunk_size > build_data.map.height { 84 | // Move to the next page 85 | build_data.take_snapshot(); 86 | build_data.map = Map::new( 87 | build_data.map.depth, 88 | build_data.map.width, 89 | build_data.map.height, 90 | build_data.map.name.clone(), 91 | ); 92 | 93 | x = 1; 94 | y = 1; 95 | } 96 | } 97 | 98 | counter += 1; 99 | } 100 | build_data.take_snapshot(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/visibility_system.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | field_of_view, gamelog::GameLog, BlocksVisibility, Hidden, Map, Name, Player, Position, 3 | Viewshed, 4 | }; 5 | use legion::prelude::*; 6 | use rltk::Point; 7 | use std::collections::HashSet; 8 | 9 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 10 | SystemBuilder::new("visibility_system") 11 | .write_resource::() 12 | .write_resource::() 13 | .write_resource::() 14 | .with_query(<(Write, Read)>::query()) 15 | .with_query(Read::::query().filter(tag::())) 16 | .read_component::() 17 | .read_component::() 18 | .build( 19 | |command_buffer, world, (map, rng, log), (viewshed_query, view_blocked_query)| { 20 | map.view_blocked.clear(); 21 | for position in view_blocked_query.iter(world) { 22 | let idx = map.xy_idx(position.x, position.y); 23 | map.view_blocked.insert(idx); 24 | } 25 | 26 | let mut seen_tiles: HashSet = HashSet::new(); 27 | for chunk in viewshed_query.iter_chunks_mut(world) { 28 | // Is this the players chunk? 29 | let player_chunk = if let Some(_p) = chunk.tag::() { 30 | // Reset visibility 31 | for t in map.visible_tiles.iter_mut() { 32 | *t = false; 33 | } 34 | true 35 | } else { 36 | false 37 | }; 38 | 39 | let viewsheds = &mut chunk.components_mut::().unwrap(); 40 | let positions = &chunk.components::().unwrap(); 41 | 42 | for (i, pos) in positions.iter().enumerate() { 43 | let viewshed = &mut viewsheds[i]; 44 | if viewshed.dirty { 45 | viewshed.dirty = false; 46 | viewshed.visible_tiles.clear(); 47 | viewshed.visible_tiles = field_of_view( 48 | Point::new(pos.x, pos.y), 49 | viewshed.range as usize, 50 | &**map, 51 | ); 52 | } 53 | 54 | // If this is the player, reveal what they can see 55 | if player_chunk { 56 | for vis in viewshed.visible_tiles.iter() { 57 | let idx = map.xy_idx(vis.x, vis.y); 58 | map.revealed_tiles[idx] = true; 59 | map.visible_tiles[idx] = true; 60 | seen_tiles.insert(idx); 61 | } 62 | } 63 | } 64 | } 65 | 66 | for idx in seen_tiles.iter() { 67 | // Chance to reveal hidden things 68 | for e in map.tile_content[*idx].iter() { 69 | if let Some(_hidden) = world.get_tag::(*e) { 70 | if rng.roll_dice(1, 24) == 1 { 71 | if let Some(name) = world.get_component::(*e) { 72 | log.entries.push(format!("You spotted a {}.", &name.name)); 73 | } 74 | command_buffer.remove_tag::(*e); 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/map_builders/prefab_builder/prefab_levels.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Copy, Clone)] 2 | pub struct PrefabLevel { 3 | pub template: &'static str, 4 | pub width: usize, 5 | pub height: usize, 6 | } 7 | 8 | pub const WFC_POPULATED: PrefabLevel = PrefabLevel { 9 | template: LEVEL_MAP, 10 | width: 80, 11 | height: 43, 12 | }; 13 | 14 | const LEVEL_MAP: &str = "\ 15 | ################################################################################ 16 | # ######################################################## ######### 17 | # @ ###### ######### #### ################### ####### 18 | # #### g # ############### ##### 19 | # #### # # ####### #### ############# ### 20 | ##### ######### # # ####### ######### #### ##### ### 21 | ##### ######### ###### ####### o ######### #### ## ##### ### 22 | ## #### ######### ### ## o ### 23 | ##### ######### ### #### ####### ## ##### ### 24 | ##### ######### ### #### ####### # ### ## ##### ### 25 | ##### ######### ### #### ####### ####### ##### o ### 26 | ### ## ### #### ####### ################ ### 27 | ### ## ### o ###### ########### # ############ ### 28 | ### ## ### ###### ########### ### ### 29 | ### % ###### ########### # ### ! ## ### 30 | ### ## ### ###### ## ####### ## ### 31 | ### ## ### ## ### ##### # ######################## ##### 32 | ### ## ### ## ### ##### # # ###################### ##### 33 | #### ## ####### ###### ##### ### #### o ########### ###### ##### 34 | #### ## ####### ###### #### ## #### # ######### ###### ###### 35 | # ## ####### ###### #### ## #### ############ ##### ###### 36 | # g ## ####### ###### #### ## % ########### o o #### # # 37 | # ## ### #### ## #### # ####### ## ## #### g # 38 | ####### ####### #### ###### ! ! ### # # 39 | ###### ##### #### # ###### ### ###### 40 | ##### ##### # ########## ### ###### 41 | ##### ! ### ###### # ########## o##o ### # ## 42 | ##### ### ####### ## # ###### ### g ## 43 | # ## #### ######## ### o ####### ^########^ #### # ## 44 | # g # ###### ######## ##### ####### ^ ^ #### ###### 45 | # ##g#### ###### ######## ################ ##### ###### 46 | # ## ########## ########## ######## ################# ###### # 47 | ##### ######### ########## % ######## ################### ######## ## # 48 | #### ### ######## ########## ######## #################### ########## # # 49 | ### ##### ###### ######### ######## ########### ####### # g# # 50 | ### ##### ############### ### ########### ####### #### # 51 | ### ##### #### ############## ######## g g ########### #### # ^ # 52 | #### ###^#### ############# ######## ##### #### # g# # 53 | ##### ###### ### ######## ##### g #### ! ####^^ # 54 | #!%^## ### ## ########## ######## gg g # > # 55 | #!%^ ### ### ############### ######## ##### g #### # g# # 56 | # %^## ^ ### ############### ######## ##### ################## 57 | ################################################################################ 58 | "; 59 | -------------------------------------------------------------------------------- /src/map_builders/bsp_interior.rs: -------------------------------------------------------------------------------- 1 | use super::{draw_corridor, BuilderMap, InitialMapBuilder, Rect, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | const MIN_ROOM_SIZE: i32 = 8; 5 | 6 | pub struct BspInteriorBuilder { 7 | rects: Vec, 8 | } 9 | 10 | impl InitialMapBuilder for BspInteriorBuilder { 11 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 12 | self.build(rng, build_data); 13 | } 14 | } 15 | 16 | impl BspInteriorBuilder { 17 | pub fn new() -> Box { 18 | Box::new(BspInteriorBuilder { rects: Vec::new() }) 19 | } 20 | 21 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 22 | let mut rooms: Vec = Vec::new(); 23 | self.rects.clear(); 24 | self.rects.push(Rect::new( 25 | 1, 26 | 1, 27 | build_data.map.width - 2, 28 | build_data.map.height - 2, 29 | )); // Start with a single map-sized rectangle 30 | self.partition_rects(rng); // Divide the first room 31 | 32 | for r in self.rects.clone().iter() { 33 | let room = *r; 34 | rooms.push(room); 35 | for y in room.y1..room.y2 { 36 | for x in room.x1..room.x2 { 37 | let idx = build_data.map.xy_idx(x, y); 38 | if idx > 0 39 | && idx < ((build_data.map.width * build_data.map.height) - 1) as usize 40 | { 41 | build_data.map.tiles[idx] = TileType::Floor; 42 | } 43 | } 44 | } 45 | build_data.take_snapshot(); 46 | } 47 | 48 | // Now we want corridors 49 | for i in 0..rooms.len() - 1 { 50 | let room = rooms[i]; 51 | let next_room = rooms[i + 1]; 52 | let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2)) - 1); 53 | let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2)) - 1); 54 | let end_x = 55 | next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2)) - 1); 56 | let end_y = 57 | next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2)) - 1); 58 | draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); 59 | build_data.take_snapshot(); 60 | } 61 | 62 | build_data.rooms = Some(rooms); 63 | } 64 | 65 | fn partition_rects(&mut self, rng: &mut RandomNumberGenerator) { 66 | if let Some(rect) = self.rects.pop() { 67 | // Calculate boundaries 68 | let width = rect.x2 - rect.x1; 69 | let height = rect.y2 - rect.y1; 70 | 71 | let split = rng.roll_dice(1, 4); 72 | if split <= 2 { 73 | // Horizontal split 74 | let half_width = width / 2; 75 | let h1 = Rect::new(rect.x1, rect.y1, half_width - 1, height); 76 | self.rects.push(h1); 77 | if half_width > MIN_ROOM_SIZE { 78 | self.partition_rects(rng); 79 | } 80 | let h2 = Rect::new(rect.x1 + half_width, rect.y1, half_width, height); 81 | self.rects.push(h2); 82 | if half_width > MIN_ROOM_SIZE { 83 | self.partition_rects(rng); 84 | } 85 | } else { 86 | // Vertical split 87 | let half_height = height / 2; 88 | let v1 = Rect::new(rect.x1, rect.y1, width, half_height - 1); 89 | self.rects.push(v1); 90 | if half_height > MIN_ROOM_SIZE { 91 | self.partition_rects(rng); 92 | } 93 | let v2 = Rect::new(rect.x1, rect.y1 + half_height, width, half_height); 94 | self.rects.push(v2); 95 | if half_height > MIN_ROOM_SIZE { 96 | self.partition_rects(rng); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/camera.rs: -------------------------------------------------------------------------------- 1 | use super::{map::tile_glyph, Hidden, Map, Position, Renderable}; 2 | use legion::prelude::*; 3 | use rltk::{Point, Rltk, RGB}; 4 | 5 | const SHOW_BOUNDARIES: bool = true; 6 | const MAP_OFFSET_X: i32 = 1; 7 | const MAP_OFFSET_Y: i32 = 1; 8 | 9 | pub fn get_screen_bounds(resources: &Resources, _ctx: &Rltk) -> (i32, i32, i32, i32) { 10 | let player_pos = resources.get::().unwrap(); 11 | let (x_chars, y_chars) = (48, 44); 12 | 13 | let center_x = (x_chars / 2) as i32; 14 | let center_y = (y_chars / 2) as i32; 15 | 16 | let min_x = player_pos.x - center_x; 17 | let max_x = min_x + x_chars as i32; 18 | let min_y = player_pos.y - center_y; 19 | let max_y = min_y + y_chars as i32; 20 | 21 | (min_x, max_x, min_y, max_y) 22 | } 23 | 24 | pub fn render_camera(world: &World, resources: &Resources, ctx: &mut Rltk) { 25 | let map = resources.get::().unwrap(); 26 | let (min_x, max_x, min_y, max_y) = get_screen_bounds(resources, ctx); 27 | 28 | // Draw the Map 29 | let mut y = MAP_OFFSET_Y; 30 | for ty in min_y..max_y { 31 | let mut x = MAP_OFFSET_X; 32 | for tx in min_x..max_x { 33 | if tx >= 0 && tx < map.width && ty >= 0 && ty < map.height { 34 | let idx = map.xy_idx(tx, ty); 35 | if map.revealed_tiles[idx] { 36 | let (glyph, fg, bg) = tile_glyph(idx, &*map); 37 | ctx.set(x, y, fg, bg, glyph); 38 | } 39 | } else if SHOW_BOUNDARIES { 40 | ctx.set( 41 | x, 42 | y, 43 | RGB::named(rltk::GRAY), 44 | RGB::named(rltk::BLACK), 45 | rltk::to_cp437('·'), 46 | ); 47 | } 48 | x += 1; 49 | } 50 | y += 1; 51 | } 52 | 53 | // Draw Renderable entities 54 | let query = <(Read, Read)>::query().filter(!tag::()); 55 | let mut data = query.iter(world).collect::>(); 56 | data.sort_by(|a, b| b.1.render_order.cmp(&a.1.render_order)); 57 | for (pos, render) in data.iter() { 58 | let idx = map.xy_idx(pos.x, pos.y); 59 | if map.visible_tiles[idx] { 60 | let entity_screen_x = pos.x - min_x; 61 | let entity_screen_y = pos.y - min_y; 62 | if entity_screen_x >= 0 63 | && entity_screen_x < map.width 64 | && entity_screen_y >= 0 65 | && entity_screen_y < map.height 66 | { 67 | ctx.set( 68 | entity_screen_x + MAP_OFFSET_X, 69 | entity_screen_y + MAP_OFFSET_Y, 70 | render.fg, 71 | render.bg, 72 | render.glyph, 73 | ); 74 | } 75 | } 76 | } 77 | } 78 | 79 | pub fn render_debug_map(map: &Map, ctx: &mut Rltk) { 80 | let player_pos = Point::new(map.width / 2, map.height / 2); 81 | let (x_chars, y_chars) = ctx.get_char_size(); 82 | 83 | let center_x = (x_chars / 2) as i32; 84 | let center_y = (y_chars / 2) as i32; 85 | 86 | let min_x = player_pos.x - center_x; 87 | let max_x = min_x + x_chars as i32; 88 | let min_y = player_pos.y - center_y; 89 | let max_y = min_y + y_chars as i32; 90 | 91 | let map_width = map.width - 1; 92 | let map_height = map.height - 1; 93 | 94 | let mut y = 0; 95 | for ty in min_y..max_y { 96 | let mut x = 0; 97 | for tx in min_x..max_x { 98 | if tx >= 0 && tx < map_width && ty >= 0 && ty < map_height { 99 | let idx = map.xy_idx(tx, ty); 100 | if map.revealed_tiles[idx] { 101 | let (glyph, fg, bg) = tile_glyph(idx, &*map); 102 | ctx.set(x, y, fg, bg, glyph); 103 | } 104 | } else if SHOW_BOUNDARIES { 105 | ctx.set( 106 | x, 107 | y, 108 | RGB::named(rltk::GRAY), 109 | RGB::named(rltk::BLACK), 110 | rltk::to_cp437('·'), 111 | ); 112 | } 113 | x += 1; 114 | } 115 | y += 1; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/map/mod.rs: -------------------------------------------------------------------------------- 1 | use legion::prelude::*; 2 | use rltk::{Algorithm2D, BaseMap, Point, SmallVec}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashSet; 5 | use type_uuid::TypeUuid; 6 | 7 | mod tile_type; 8 | pub use tile_type::{tile_cost, tile_opaque, tile_walkable, TileType}; 9 | mod fov; 10 | pub use fov::field_of_view; 11 | mod astar; 12 | pub use astar::a_star_search; 13 | mod themes; 14 | pub use themes::tile_glyph; 15 | pub mod dungeon; 16 | 17 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize, Default)] 18 | #[uuid = "09e57cda-e925-47f0-a3f6-107c86fa76bd"] 19 | pub struct Map { 20 | pub tiles: Vec, 21 | pub width: i32, 22 | pub height: i32, 23 | pub revealed_tiles: Vec, 24 | pub visible_tiles: Vec, 25 | pub blocked: Vec, 26 | pub depth: i32, 27 | pub bloodstains: HashSet, 28 | pub view_blocked: HashSet, 29 | pub name: String, 30 | 31 | #[serde(skip)] 32 | pub tile_content: Vec>, 33 | } 34 | 35 | impl Map { 36 | #[inline] 37 | pub fn xy_idx(&self, x: i32, y: i32) -> usize { 38 | (y as usize * self.width as usize) + x as usize 39 | } 40 | 41 | fn is_exit_valid(&self, x: i32, y: i32) -> bool { 42 | if x < 0 || x >= self.width || y < 0 || y >= self.height { 43 | return false; 44 | } 45 | let idx = self.xy_idx(x, y); 46 | !self.blocked[idx] 47 | } 48 | 49 | pub fn populate_blocked(&mut self) { 50 | for (i, tile) in self.tiles.iter_mut().enumerate() { 51 | self.blocked[i] = !tile_walkable(*tile); 52 | } 53 | } 54 | 55 | pub fn clear_content_index(&mut self) { 56 | for content in self.tile_content.iter_mut() { 57 | content.clear(); 58 | } 59 | } 60 | 61 | /// Generates an empty map, consisting entirely of solid walls 62 | pub fn new(depth: i32, width: i32, height: i32, name: S) -> Self { 63 | let map_tile_count = (width * height) as usize; 64 | Map { 65 | tiles: vec![TileType::Wall; map_tile_count], 66 | width, 67 | height, 68 | revealed_tiles: vec![false; map_tile_count], 69 | visible_tiles: vec![false; map_tile_count], 70 | blocked: vec![false; map_tile_count], 71 | tile_content: vec![Vec::new(); map_tile_count], 72 | depth, 73 | bloodstains: HashSet::new(), 74 | view_blocked: HashSet::new(), 75 | name: name.to_string(), 76 | } 77 | } 78 | } 79 | 80 | impl BaseMap for Map { 81 | fn is_opaque(&self, idx: usize) -> bool { 82 | idx >= self.tiles.len() || tile_opaque(self.tiles[idx]) || self.view_blocked.contains(&idx) 83 | } 84 | 85 | fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> { 86 | let mut exits = SmallVec::new(); 87 | let x = idx as i32 % self.width; 88 | let y = idx as i32 / self.width; 89 | let w = self.width as usize; 90 | let tt = self.tiles[idx]; 91 | 92 | // Cardinal directions 93 | if self.is_exit_valid(x - 1, y) { 94 | exits.push((idx - 1, tile_cost(tt))); 95 | }; 96 | if self.is_exit_valid(x + 1, y) { 97 | exits.push((idx + 1, tile_cost(tt))); 98 | }; 99 | if self.is_exit_valid(x, y - 1) { 100 | exits.push((idx - w, tile_cost(tt))); 101 | }; 102 | if self.is_exit_valid(x, y + 1) { 103 | exits.push((idx + w, tile_cost(tt))); 104 | }; 105 | 106 | // Diagonals 107 | if self.is_exit_valid(x - 1, y - 1) { 108 | exits.push((idx - 1 - w, tile_cost(tt) * 1.45)); 109 | }; 110 | if self.is_exit_valid(x + 1, y - 1) { 111 | exits.push((idx + 1 - w, tile_cost(tt) * 1.45)); 112 | }; 113 | if self.is_exit_valid(x - 1, y + 1) { 114 | exits.push((idx - 1 + w, tile_cost(tt) * 1.45)); 115 | }; 116 | if self.is_exit_valid(x + 1, y + 1) { 117 | exits.push((idx + 1 + w, tile_cost(tt) * 1.45)); 118 | }; 119 | 120 | exits 121 | } 122 | } 123 | 124 | impl Algorithm2D for Map { 125 | fn dimensions(&self) -> Point { 126 | Point::new(self.width, self.height) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/map_builders/bsp_dungeon.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, InitialMapBuilder, Rect, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct BspDungeonBuilder { 5 | rects: Vec, 6 | } 7 | 8 | impl InitialMapBuilder for BspDungeonBuilder { 9 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 10 | self.build(rng, build_data); 11 | } 12 | } 13 | 14 | impl BspDungeonBuilder { 15 | pub fn new() -> Box { 16 | Box::new(BspDungeonBuilder { rects: Vec::new() }) 17 | } 18 | 19 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | let mut rooms: Vec = Vec::new(); 21 | self.rects.clear(); 22 | self.rects.push(Rect::new( 23 | 2, 24 | 2, 25 | build_data.map.width - 5, 26 | build_data.map.height - 5, 27 | )); // Start with a single map-sized rectangle 28 | let first_room = self.rects[0]; 29 | self.partition_rect(&first_room); // Divide the first room 30 | 31 | // Up to 240 times, we get a random rectangle and divide it. 32 | // If its possible to squeeze a room in there, we place it and add it to the rooms list. 33 | let mut n_rooms = 0; 34 | while n_rooms < 240 { 35 | let rect = self.get_random_rect(rng); 36 | let candidate = self.get_room_candidate(&rect, rng); 37 | 38 | if self.is_possible(&candidate, &build_data, &rooms) { 39 | rooms.push(candidate); 40 | self.partition_rect(&rect); 41 | } 42 | 43 | n_rooms += 1; 44 | } 45 | 46 | build_data.rooms = Some(rooms); 47 | } 48 | 49 | fn partition_rect(&mut self, rect: &Rect) { 50 | let width = i32::abs(rect.x1 - rect.x2); 51 | let height = i32::abs(rect.y1 - rect.y2); 52 | let half_width = i32::max(width / 2, 1); 53 | let half_height = i32::max(height / 2, 1); 54 | 55 | self.rects 56 | .push(Rect::new(rect.x1, rect.y1, half_width, half_height)); 57 | self.rects.push(Rect::new( 58 | rect.x1, 59 | rect.y1 + half_height, 60 | half_width, 61 | half_height, 62 | )); 63 | self.rects.push(Rect::new( 64 | rect.x1 + half_width, 65 | rect.y1, 66 | half_width, 67 | half_height, 68 | )); 69 | self.rects.push(Rect::new( 70 | rect.x1 + half_width, 71 | rect.y1 + half_height, 72 | half_width, 73 | half_height, 74 | )); 75 | } 76 | 77 | fn get_random_rect(&self, rng: &mut RandomNumberGenerator) -> Rect { 78 | self.rects[match self.rects.len() { 79 | 1 => 0, 80 | len => (rng.roll_dice(1, len as i32) - 1) as usize, 81 | }] 82 | } 83 | 84 | fn get_room_candidate(&self, rect: &Rect, rng: &mut RandomNumberGenerator) -> Rect { 85 | let mut result = *rect; 86 | let rect_width = i32::abs(rect.x1 - rect.x2); 87 | let rect_height = i32::abs(rect.y1 - rect.y2); 88 | 89 | let w = i32::max(3, rng.roll_dice(1, i32::min(rect_width, 10)) - 1) + 1; 90 | let h = i32::max(3, rng.roll_dice(1, i32::min(rect_height, 10)) - 1) + 1; 91 | 92 | result.x1 += rng.roll_dice(1, 6) - 1; 93 | result.y1 += rng.roll_dice(1, 6) - 1; 94 | result.x2 = result.x1 + w; 95 | result.y2 = result.y1 + h; 96 | 97 | result 98 | } 99 | 100 | fn is_possible(&self, rect: &Rect, build_data: &BuilderMap, rooms: &Vec) -> bool { 101 | let mut expanded = *rect; 102 | expanded.x1 -= 2; 103 | expanded.x2 += 2; 104 | expanded.y1 -= 2; 105 | expanded.y2 += 2; 106 | 107 | if rooms.iter().any(|r| r.intersects(&rect)) { 108 | return false; 109 | } 110 | 111 | for y in expanded.y1..=expanded.y2 { 112 | for x in expanded.x1..=expanded.x2 { 113 | if x < 1 { 114 | return false; 115 | } 116 | if y < 1 { 117 | return false; 118 | } 119 | if x > build_data.map.width - 2 { 120 | return false; 121 | } 122 | if y > build_data.map.height - 2 { 123 | return false; 124 | } 125 | 126 | let idx = build_data.map.xy_idx(x, y); 127 | if build_data.map.tiles[idx] != TileType::Wall { 128 | return false; 129 | } 130 | } 131 | } 132 | true 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/map_builders/voronoi.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, InitialMapBuilder, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | #[derive(PartialEq, Copy, Clone)] 5 | pub enum DistanceAlgorithm { 6 | Pythagoras, 7 | Manhattan, 8 | Chebyshev, 9 | } 10 | 11 | pub struct VoronoiCellBuilder { 12 | n_seeds: usize, 13 | distance_algorithm: DistanceAlgorithm, 14 | } 15 | 16 | impl InitialMapBuilder for VoronoiCellBuilder { 17 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 18 | self.build(rng, build_data); 19 | } 20 | } 21 | 22 | impl VoronoiCellBuilder { 23 | #[allow(dead_code)] 24 | pub fn new() -> Box { 25 | Box::new(VoronoiCellBuilder { 26 | n_seeds: 64, 27 | distance_algorithm: DistanceAlgorithm::Pythagoras, 28 | }) 29 | } 30 | 31 | pub fn pythagoras() -> Box { 32 | Box::new(VoronoiCellBuilder { 33 | n_seeds: 64, 34 | distance_algorithm: DistanceAlgorithm::Pythagoras, 35 | }) 36 | } 37 | 38 | pub fn manhattan() -> Box { 39 | Box::new(VoronoiCellBuilder { 40 | n_seeds: 64, 41 | distance_algorithm: DistanceAlgorithm::Manhattan, 42 | }) 43 | } 44 | 45 | pub fn chebyshev() -> Box { 46 | Box::new(VoronoiCellBuilder { 47 | n_seeds: 64, 48 | distance_algorithm: DistanceAlgorithm::Chebyshev, 49 | }) 50 | } 51 | 52 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 53 | // Make a Voronoi diagram. We'll do this the hard way to learn about the technique! 54 | let mut voronoi_seeds: Vec<(usize, rltk::Point)> = Vec::new(); 55 | 56 | while voronoi_seeds.len() < self.n_seeds { 57 | let vx = rng.roll_dice(1, build_data.map.width - 1); 58 | let vy = rng.roll_dice(1, build_data.map.height - 1); 59 | let vidx = build_data.map.xy_idx(vx, vy); 60 | let candidate = (vidx, rltk::Point::new(vx, vy)); 61 | if !voronoi_seeds.contains(&candidate) { 62 | voronoi_seeds.push(candidate); 63 | } 64 | } 65 | 66 | let mut voronoi_distance = vec![(0, 0.0f32); voronoi_seeds.len()]; 67 | let mut voronoi_membership: Vec = 68 | vec![0; build_data.map.width as usize * build_data.map.height as usize]; 69 | for (i, vid) in voronoi_membership.iter_mut().enumerate() { 70 | let x = i as i32 % build_data.map.width; 71 | let y = i as i32 / build_data.map.width; 72 | 73 | for (seed, (_, pos)) in voronoi_seeds.iter().enumerate() { 74 | let distance; 75 | match self.distance_algorithm { 76 | DistanceAlgorithm::Pythagoras => { 77 | distance = rltk::DistanceAlg::PythagorasSquared 78 | .distance2d(rltk::Point::new(x, y), *pos); 79 | } 80 | DistanceAlgorithm::Manhattan => { 81 | distance = 82 | rltk::DistanceAlg::Manhattan.distance2d(rltk::Point::new(x, y), *pos); 83 | } 84 | DistanceAlgorithm::Chebyshev => { 85 | distance = 86 | rltk::DistanceAlg::Chebyshev.distance2d(rltk::Point::new(x, y), *pos); 87 | } 88 | } 89 | voronoi_distance[seed] = (seed, distance); 90 | } 91 | 92 | voronoi_distance.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 93 | 94 | *vid = voronoi_distance[0].0 as i32; 95 | } 96 | 97 | for y in 1..build_data.map.height - 1 { 98 | for x in 1..build_data.map.width - 1 { 99 | let mut neighbors = 0; 100 | let my_idx = build_data.map.xy_idx(x, y); 101 | let my_seed = voronoi_membership[my_idx]; 102 | if voronoi_membership[build_data.map.xy_idx(x - 1, y)] != my_seed { 103 | neighbors += 1; 104 | } 105 | if voronoi_membership[build_data.map.xy_idx(x + 1, y)] != my_seed { 106 | neighbors += 1; 107 | } 108 | if voronoi_membership[build_data.map.xy_idx(x, y - 1)] != my_seed { 109 | neighbors += 1; 110 | } 111 | if voronoi_membership[build_data.map.xy_idx(x, y + 1)] != my_seed { 112 | neighbors += 1; 113 | } 114 | 115 | if neighbors < 2 { 116 | build_data.map.tiles[my_idx] = TileType::Floor; 117 | } 118 | } 119 | build_data.take_snapshot(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/map_builders/forest.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | tile_walkable, AreaStartingPosition, BuilderChain, BuilderMap, CellularAutomataBuilder, 3 | CullUnreachable, MetaMapBuilder, TileType, VoronoiSpawning, XStart, YStart, 4 | }; 5 | use crate::a_star_search; 6 | use rltk::{Point, RandomNumberGenerator}; 7 | 8 | pub fn forest_builder( 9 | depth: i32, 10 | width: i32, 11 | height: i32, 12 | _rng: &mut RandomNumberGenerator, 13 | ) -> BuilderChain { 14 | let mut chain = BuilderChain::new(depth, width, height, "Into the Woods"); 15 | chain.start_with(CellularAutomataBuilder::new()); 16 | chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); 17 | chain.with(CullUnreachable::new()); 18 | chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); 19 | chain.with(VoronoiSpawning::new()); 20 | chain.with(YellowBrickRoad::new()); 21 | chain 22 | } 23 | 24 | pub struct YellowBrickRoad {} 25 | 26 | impl MetaMapBuilder for YellowBrickRoad { 27 | fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { 28 | self.build(rng, build_data); 29 | } 30 | } 31 | 32 | impl YellowBrickRoad { 33 | pub fn new() -> Box { 34 | Box::new(YellowBrickRoad {}) 35 | } 36 | 37 | fn find_exit(&self, build_data: &mut BuilderMap, seed_x: i32, seed_y: i32) -> (i32, i32) { 38 | let mut available_floors = Vec::new(); 39 | for (idx, tile_type) in build_data.map.tiles.iter().enumerate() { 40 | if tile_walkable(*tile_type) { 41 | let x = idx as i32 % build_data.map.width; 42 | let y = idx as i32 / build_data.map.width; 43 | available_floors.push(( 44 | idx, 45 | rltk::DistanceAlg::PythagorasSquared 46 | .distance2d(Point::new(x, y), Point::new(seed_x, seed_y)), 47 | )) 48 | } 49 | } 50 | if available_floors.is_empty() { 51 | panic!("No valid floors to start on"); 52 | } 53 | 54 | available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 55 | 56 | let end_idx = available_floors[0].0; 57 | let end_x = end_idx as i32 % build_data.map.width; 58 | let end_y = end_idx as i32 / build_data.map.width; 59 | (end_x, end_y) 60 | } 61 | 62 | fn paint_road(&self, build_data: &mut BuilderMap, x: i32, y: i32) { 63 | if x < 0 || x >= build_data.map.width || y < 0 || y >= build_data.map.height { 64 | return; 65 | } 66 | 67 | let idx = build_data.map.xy_idx(x, y); 68 | if build_data.map.tiles[idx] != TileType::DownStairs { 69 | build_data.map.tiles[idx] = TileType::Road; 70 | } 71 | } 72 | 73 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 74 | let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); 75 | 76 | let (end_x, end_y) = self.find_exit( 77 | build_data, 78 | build_data.map.width - 1, 79 | build_data.map.height / 2, 80 | ); 81 | let end_idx = build_data.map.xy_idx(end_x, end_y); 82 | build_data.map.tiles[end_idx] = TileType::DownStairs; 83 | 84 | build_data.map.populate_blocked(); 85 | if let Some(result) = a_star_search( 86 | Point::new(starting_pos.x, starting_pos.y), 87 | Point::new(end_x, end_y), 88 | 0.0, 89 | &build_data.map, 90 | ) { 91 | for p in result.0.iter() { 92 | let x = p.x; 93 | let y = p.y; 94 | self.paint_road(build_data, x, y); 95 | self.paint_road(build_data, x - 1, y); 96 | self.paint_road(build_data, x + 1, y); 97 | self.paint_road(build_data, x, y - 1); 98 | self.paint_road(build_data, x, y + 1); 99 | } 100 | } 101 | build_data.take_snapshot(); 102 | 103 | // Place exit 104 | let exit_dir = rng.roll_dice(1, 2); 105 | let (seed_x, seed_y, stream_startx, stream_starty) = if exit_dir == 1 { 106 | (build_data.map.width - 1, 0, 0, build_data.map.height - 1) 107 | } else { 108 | (build_data.map.width - 1, build_data.map.height - 1, 0, 0) 109 | }; 110 | 111 | let (stairs_x, stairs_y) = self.find_exit(build_data, seed_x, seed_y); 112 | let stairs_idx = build_data.map.xy_idx(stairs_x, stairs_y); 113 | build_data.map.tiles[stairs_idx] = TileType::DownStairs; 114 | 115 | let (stream_x, stream_y) = self.find_exit(build_data, stream_startx, stream_starty); 116 | let stream = a_star_search( 117 | Point::new(stairs_x, stairs_y), 118 | Point::new(stream_x, stream_y), 119 | 0.0, 120 | &build_data.map, 121 | ); 122 | // FIXME: First trace the stream, then draw the road. 123 | // Now the stream flows using road tiles. 124 | for tile in stream.unwrap().0.iter() { 125 | let idx = build_data.map.xy_idx(tile.x, tile.y); 126 | if build_data.map.tiles[idx] == TileType::Floor { 127 | build_data.map.tiles[idx] = TileType::ShallowWater; 128 | } 129 | } 130 | build_data.take_snapshot(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/map_builders/common.rs: -------------------------------------------------------------------------------- 1 | use super::{Map, TileType}; 2 | use std::cmp::{max, min}; 3 | 4 | pub fn apply_horizontal_tunnel(map: &mut Map, x1: i32, x2: i32, y: i32) -> Vec { 5 | let mut corridor = Vec::new(); 6 | for x in min(x1, x2)..=max(x1, x2) { 7 | let idx = map.xy_idx(x, y); 8 | if idx > 0 9 | && idx < map.width as usize * map.height as usize 10 | && map.tiles[idx] != TileType::Floor 11 | { 12 | map.tiles[idx] = TileType::Floor; 13 | corridor.push(idx); 14 | } 15 | } 16 | corridor 17 | } 18 | 19 | pub fn apply_vertical_tunnel(map: &mut Map, y1: i32, y2: i32, x: i32) -> Vec { 20 | let mut corridor = Vec::new(); 21 | for y in min(y1, y2)..=max(y1, y2) { 22 | let idx = map.xy_idx(x, y); 23 | if idx > 0 24 | && idx < map.width as usize * map.height as usize 25 | && map.tiles[idx] != TileType::Floor 26 | { 27 | map.tiles[idx] = TileType::Floor; 28 | corridor.push(idx); 29 | } 30 | } 31 | corridor 32 | } 33 | 34 | pub fn draw_corridor(map: &mut Map, x1: i32, y1: i32, x2: i32, y2: i32) -> Vec { 35 | let mut corridor = Vec::new(); 36 | let mut x = x1; 37 | let mut y = y1; 38 | 39 | while x != x2 || y != y2 { 40 | if x < x2 { 41 | x += 1; 42 | } else if x > x2 { 43 | x -= 1; 44 | } else if y < y2 { 45 | y += 1; 46 | } else if y > y2 { 47 | y -= 1; 48 | } 49 | 50 | let idx = map.xy_idx(x, y); 51 | if map.tiles[idx] != TileType::Floor { 52 | map.tiles[idx] = TileType::Floor; 53 | corridor.push(idx); 54 | } 55 | } 56 | corridor 57 | } 58 | 59 | #[allow(dead_code)] 60 | #[derive(Debug, Copy, Clone, PartialEq)] 61 | pub enum Symmetry { 62 | None, 63 | Horizontal, 64 | Vertical, 65 | SemiBoth, 66 | Both, 67 | } 68 | 69 | pub fn paint(map: &mut Map, mode: Symmetry, brush_size: i32, x: i32, y: i32) { 70 | match mode { 71 | Symmetry::None => apply_paint(map, brush_size, x, y), 72 | Symmetry::Horizontal => { 73 | let center_x = map.width / 2; 74 | let dist_x = i32::abs(center_x - x); 75 | if dist_x == 0 { 76 | apply_paint(map, brush_size, x, y); 77 | } else { 78 | apply_paint(map, brush_size, center_x + dist_x, y); 79 | apply_paint(map, brush_size, center_x - dist_x, y); 80 | } 81 | } 82 | Symmetry::Vertical => { 83 | let center_y = map.height / 2; 84 | let dist_y = i32::abs(center_y - y); 85 | if dist_y == 0 { 86 | apply_paint(map, brush_size, x, y); 87 | } else { 88 | apply_paint(map, brush_size, x, center_y + dist_y); 89 | apply_paint(map, brush_size, x, center_y - dist_y); 90 | } 91 | } 92 | Symmetry::SemiBoth => { 93 | let center_x = map.width / 2; 94 | let center_y = map.height / 2; 95 | let dist_x = i32::abs(center_x - x); 96 | let dist_y = i32::abs(center_y - y); 97 | if dist_x == 0 && dist_y == 0 { 98 | apply_paint(map, brush_size, x, y); 99 | } else { 100 | // This gives only 3 symmetric points, as 2 of the points 101 | // will get the same and painted twice 102 | apply_paint(map, brush_size, center_x + dist_x, y); 103 | apply_paint(map, brush_size, center_x - dist_x, y); 104 | apply_paint(map, brush_size, x, center_y + dist_y); 105 | apply_paint(map, brush_size, x, center_y - dist_y); 106 | } 107 | } 108 | Symmetry::Both => { 109 | let center_x = map.width / 2; 110 | let center_y = map.height / 2; 111 | let dist_x = i32::abs(center_x - x); 112 | let dist_y = i32::abs(center_y - y); 113 | if dist_x == 0 && dist_y == 0 { 114 | apply_paint(map, brush_size, x, y); 115 | } else { 116 | apply_paint(map, brush_size, center_x + dist_x, center_y + dist_y); 117 | apply_paint(map, brush_size, center_x - dist_x, center_y - dist_y); 118 | apply_paint(map, brush_size, center_x - dist_x, center_y + dist_y); 119 | apply_paint(map, brush_size, center_x + dist_x, center_y - dist_y); 120 | } 121 | } 122 | } 123 | } 124 | 125 | pub fn apply_paint(map: &mut Map, brush_size: i32, mut x: i32, mut y: i32) { 126 | match brush_size { 127 | 1 => { 128 | let idx = map.xy_idx(x, y); 129 | map.tiles[idx] = TileType::Floor; 130 | } 131 | brush_size => { 132 | let half_brush_size = brush_size / 2; 133 | x -= half_brush_size; 134 | y -= half_brush_size; 135 | for brush_y in y..y + brush_size { 136 | for brush_x in x..x + brush_size { 137 | if brush_x > 1 138 | && brush_x < map.width - 1 139 | && brush_y > 1 140 | && brush_y < map.height - 1 141 | { 142 | let idx = map.xy_idx(brush_x, brush_y); 143 | map.tiles[idx] = TileType::Floor; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/map/themes.rs: -------------------------------------------------------------------------------- 1 | use super::{Map, TileType}; 2 | use rltk::{FontCharType, RGB}; 3 | 4 | pub fn tile_glyph(idx: usize, map: &Map) -> (FontCharType, RGB, RGB) { 5 | let (glyph, mut fg, mut bg) = match map.depth { 6 | 2 => get_forest_glyph(idx, map), 7 | _ => get_tile_glyph_default(idx, map), 8 | }; 9 | 10 | if map.visible_tiles[idx] { 11 | if map.bloodstains.contains(&idx) { 12 | bg = RGB::from_f32(0.75, 0.0, 0.0); 13 | } 14 | } else { 15 | fg = fg.to_greyscale() 16 | } 17 | 18 | (glyph, fg, bg) 19 | } 20 | 21 | fn get_tile_glyph_default(idx: usize, map: &Map) -> (FontCharType, RGB, RGB) { 22 | let glyph; 23 | let fg; 24 | let bg = RGB::from_f32(0.0, 0.0, 0.0); 25 | 26 | match map.tiles[idx] { 27 | TileType::Floor => { 28 | glyph = rltk::to_cp437('.'); 29 | fg = RGB::from_f32(0.0, 0.5, 0.5); 30 | } 31 | TileType::WoodFloor => { 32 | glyph = rltk::to_cp437('░'); 33 | fg = RGB::named(rltk::CHOCOLATE); 34 | } 35 | TileType::Wall => { 36 | let x = idx as i32 % map.width; 37 | let y = idx as i32 / map.width; 38 | glyph = wall_glyph(&map, x, y); 39 | fg = RGB::from_f32(0.0, 1.0, 0.0); 40 | } 41 | TileType::DownStairs => { 42 | glyph = rltk::to_cp437('>'); 43 | fg = RGB::from_f32(0.0, 1.0, 1.0); 44 | } 45 | TileType::UpStairs => { 46 | glyph = rltk::to_cp437('<'); 47 | fg = RGB::from_f32(0.0, 1.0, 1.0); 48 | } 49 | TileType::Bridge => { 50 | glyph = rltk::to_cp437('▒'); 51 | fg = RGB::named(rltk::CHOCOLATE); 52 | } 53 | TileType::Road => { 54 | glyph = rltk::to_cp437('≡'); 55 | fg = RGB::named(rltk::GRAY); 56 | } 57 | TileType::Grass => { 58 | glyph = rltk::to_cp437('"'); 59 | fg = RGB::named(rltk::GREEN); 60 | } 61 | TileType::ShallowWater => { 62 | glyph = rltk::to_cp437('~'); 63 | fg = RGB::named(rltk::CYAN); 64 | } 65 | TileType::DeepWater => { 66 | glyph = rltk::to_cp437('~'); 67 | fg = RGB::named(rltk::BLUE); 68 | } 69 | TileType::Gravel => { 70 | glyph = rltk::to_cp437(';'); 71 | fg = RGB::named(rltk::GRAY); 72 | } 73 | } 74 | 75 | (glyph, fg, bg) 76 | } 77 | 78 | fn wall_glyph(map: &Map, x: i32, y: i32) -> FontCharType { 79 | if x < 0 || x >= map.width || y < 0 || y >= map.height { 80 | return 35; 81 | } 82 | let mut mask: u8 = 0; 83 | 84 | if is_revealed_and_wall(map, x, y - 1) { 85 | mask += 1; 86 | } 87 | if is_revealed_and_wall(map, x, y + 1) { 88 | mask += 2; 89 | } 90 | if is_revealed_and_wall(map, x - 1, y) { 91 | mask += 4; 92 | } 93 | if is_revealed_and_wall(map, x + 1, y) { 94 | mask += 8; 95 | } 96 | 97 | match mask { 98 | 0 => 9, // Pillar because we can't see neighbors 99 | 1 => 186, // Wall only to the north 100 | 2 => 186, // Wall only to the south 101 | 3 => 186, // Wall to the north and south 102 | 4 => 205, // Wall only to the west 103 | 5 => 188, // Wall to the north and west 104 | 6 => 187, // Wall to the south and west 105 | 7 => 185, // Wall to the north, south and west 106 | 8 => 205, // Wall only to the east 107 | 9 => 200, // Wall to the north and east 108 | 10 => 201, // Wall to the south and east 109 | 11 => 204, // Wall to the north, south and east 110 | 12 => 205, // Wall to the east and west 111 | 13 => 202, // Wall to the east, west, and south 112 | 14 => 203, // Wall to the east, west, and north 113 | 15 => 206, // ╬ Wall on all sides 114 | _ => 35, // We missed one? 115 | } 116 | } 117 | 118 | fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool { 119 | if x < 0 || x >= map.width || y < 0 || y >= map.height { 120 | return false; 121 | } 122 | let idx = map.xy_idx(x, y); 123 | map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx] 124 | } 125 | 126 | fn get_forest_glyph(idx: usize, map: &Map) -> (FontCharType, RGB, RGB) { 127 | let glyph; 128 | let fg; 129 | let bg = RGB::from_f32(0.0, 0.0, 0.0); 130 | 131 | match map.tiles[idx] { 132 | TileType::Wall => { 133 | glyph = rltk::to_cp437('♣'); 134 | fg = RGB::from_f32(0.0, 0.6, 0.0); 135 | } 136 | TileType::Bridge => { 137 | glyph = rltk::to_cp437('.'); 138 | fg = RGB::named(rltk::CHOCOLATE); 139 | } 140 | TileType::Road => { 141 | glyph = rltk::to_cp437('≡'); 142 | fg = RGB::named(rltk::YELLOW); 143 | } 144 | TileType::Grass => { 145 | glyph = rltk::to_cp437('"'); 146 | fg = RGB::named(rltk::GREEN); 147 | } 148 | TileType::ShallowWater => { 149 | glyph = rltk::to_cp437('~'); 150 | fg = RGB::named(rltk::CYAN); 151 | } 152 | TileType::DeepWater => { 153 | glyph = rltk::to_cp437('~'); 154 | fg = RGB::named(rltk::BLUE); 155 | } 156 | TileType::Gravel => { 157 | glyph = rltk::to_cp437(';'); 158 | fg = RGB::from_f32(0.5, 0.5, 0.5); 159 | } 160 | TileType::DownStairs => { 161 | glyph = rltk::to_cp437('>'); 162 | fg = RGB::from_f32(0.0, 1.0, 1.0); 163 | } 164 | _ => { 165 | glyph = rltk::to_cp437('"'); 166 | fg = RGB::from_f32(0.0, 0.6, 0.0); 167 | } 168 | } 169 | 170 | (glyph, fg, bg) 171 | } 172 | -------------------------------------------------------------------------------- /src/animal_ai_system.rs: -------------------------------------------------------------------------------- 1 | use super::{Carnivore, Herbivore, Item, Map, Point, Position, RunState, Viewshed, WantsToMelee}; 2 | use legion::prelude::*; 3 | 4 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 5 | SystemBuilder::new("animal_ai") 6 | .with_query(<(Write, Write)>::query().filter(tag::())) 7 | .with_query(<(Write, Write)>::query().filter(tag::())) 8 | .write_resource::() 9 | .read_resource::() 10 | .read_resource::() 11 | .read_component::() 12 | .build( 13 | |command_buffer, 14 | world, 15 | (map, runstate, player_entity), 16 | (query_herbivore, query_carnivore)| unsafe { 17 | if **runstate != RunState::MonsterTurn { 18 | return; 19 | } 20 | 21 | // Herbivores run away a lot 22 | for (mut viewshed, mut pos) in query_herbivore.iter_unchecked(world) { 23 | let mut run_away_from = Vec::new(); 24 | for other_tile in viewshed.visible_tiles.iter() { 25 | let view_idx = map.xy_idx(other_tile.x, other_tile.y); 26 | for other_entity in map.tile_content[view_idx].iter() { 27 | // They don't run away from items 28 | if world.get_component::(*other_entity).is_none() { 29 | // TODO: log.debug(entity is running away from other_entity) 30 | run_away_from.push(view_idx); 31 | } 32 | } 33 | } 34 | 35 | if !run_away_from.is_empty() { 36 | let entity_idx = map.xy_idx(pos.x, pos.y); 37 | map.populate_blocked(); 38 | let flee_map = rltk::DijkstraMap::new( 39 | map.width as usize, 40 | map.height as usize, 41 | &run_away_from, 42 | &**map, 43 | 10.0, 44 | ); 45 | let flee_target = 46 | rltk::DijkstraMap::find_highest_exit(&flee_map, entity_idx, &**map); 47 | if let Some(flee_target) = flee_target { 48 | if !map.blocked[flee_target] { 49 | map.blocked[entity_idx] = false; 50 | map.blocked[flee_target] = true; 51 | viewshed.dirty = true; 52 | pos.x = flee_target as i32 % map.width; 53 | pos.y = flee_target as i32 / map.width; 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Carnivores just want to eat everything 60 | for (entity, (mut viewshed, mut pos)) in 61 | query_carnivore.iter_entities_unchecked(world) 62 | { 63 | let mut run_towards = Vec::new(); 64 | let mut attacked = false; 65 | for other_tile in viewshed.visible_tiles.iter() { 66 | let view_idx = map.xy_idx(other_tile.x, other_tile.y); 67 | for other_entity in map.tile_content[view_idx].iter() { 68 | if world.get_tag::(*other_entity).is_some() 69 | || *other_entity == **player_entity 70 | { 71 | let distance = rltk::DistanceAlg::Pythagoras 72 | .distance2d(Point::new(pos.x, pos.y), *other_tile); 73 | if distance < 1.5 { 74 | command_buffer.add_component( 75 | entity, 76 | WantsToMelee { 77 | target: *other_entity, 78 | }, 79 | ); 80 | attacked = true; 81 | } else { 82 | run_towards.push(view_idx); 83 | } 84 | } 85 | } 86 | } 87 | 88 | if !run_towards.is_empty() && !attacked { 89 | let entity_idx = map.xy_idx(pos.x, pos.y); 90 | map.populate_blocked(); 91 | let chase_map = rltk::DijkstraMap::new( 92 | map.width as usize, 93 | map.height as usize, 94 | &run_towards, 95 | &**map, 96 | 10.0, 97 | ); 98 | let chase_target = 99 | rltk::DijkstraMap::find_lowest_exit(&chase_map, entity_idx, &**map); 100 | if let Some(chase_target) = chase_target { 101 | if !map.blocked[chase_target] { 102 | map.blocked[entity_idx] = false; 103 | map.blocked[chase_target] = true; 104 | viewshed.dirty = true; 105 | pos.x = chase_target as i32 % map.width; 106 | pos.y = chase_target as i32 / map.width; 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/map_builders/maze.rs: -------------------------------------------------------------------------------- 1 | use super::{BuilderMap, InitialMapBuilder, Map, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | pub struct MazeBuilder {} 5 | 6 | impl InitialMapBuilder for MazeBuilder { 7 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 8 | self.build(rng, build_data); 9 | } 10 | } 11 | 12 | impl MazeBuilder { 13 | pub fn new() -> Box { 14 | Box::new(MazeBuilder {}) 15 | } 16 | 17 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 18 | // Maze gen 19 | let mut maze = Grid::new( 20 | (build_data.map.width / 2) - 2, 21 | (build_data.map.height / 2) - 2, 22 | rng, 23 | ); 24 | maze.generate_maze(build_data); 25 | } 26 | } 27 | 28 | /* Maze code taken under MIT from https://github.com/cyucelen/mazeGenerator/ */ 29 | 30 | const TOP: usize = 0; 31 | const RIGHT: usize = 1; 32 | const BOTTOM: usize = 2; 33 | const LEFT: usize = 3; 34 | 35 | #[derive(Copy, Clone)] 36 | struct Cell { 37 | row: i32, 38 | column: i32, 39 | walls: [bool; 4], 40 | visited: bool, 41 | } 42 | 43 | impl Cell { 44 | fn new(row: i32, column: i32) -> Self { 45 | Cell { 46 | row, 47 | column, 48 | walls: [true, true, true, true], 49 | visited: false, 50 | } 51 | } 52 | 53 | fn remove_walls(&mut self, next: &mut Cell) { 54 | let x = self.column - next.column; 55 | let y = self.row - next.row; 56 | 57 | if x == 1 { 58 | self.walls[LEFT] = false; 59 | next.walls[RIGHT] = false; 60 | } else if x == -1 { 61 | self.walls[RIGHT] = false; 62 | next.walls[LEFT] = false; 63 | } else if y == 1 { 64 | self.walls[TOP] = false; 65 | next.walls[BOTTOM] = false; 66 | } else if y == -1 { 67 | self.walls[BOTTOM] = false; 68 | next.walls[TOP] = false; 69 | } 70 | } 71 | } 72 | 73 | struct Grid<'a> { 74 | width: i32, 75 | height: i32, 76 | cells: Vec, 77 | backtrace: Vec, 78 | current: usize, 79 | rng: &'a mut RandomNumberGenerator, 80 | } 81 | 82 | impl<'a> Grid<'a> { 83 | fn new(width: i32, height: i32, rng: &'a mut RandomNumberGenerator) -> Self { 84 | let mut grid = Grid { 85 | width, 86 | height, 87 | cells: Vec::new(), 88 | backtrace: Vec::new(), 89 | current: 0, 90 | rng, 91 | }; 92 | 93 | for row in 0..height { 94 | for col in 0..width { 95 | grid.cells.push(Cell::new(row, col)); 96 | } 97 | } 98 | 99 | grid 100 | } 101 | 102 | fn calculate_index(&self, row: i32, col: i32) -> i32 { 103 | if row < 0 || col < 0 || col >= self.width || row >= self.height { 104 | -1 105 | } else { 106 | col + row * self.width 107 | } 108 | } 109 | 110 | fn get_available_neighbors(&self) -> Vec { 111 | let mut neighbors = Vec::new(); 112 | 113 | let current_row = self.cells[self.current].row; 114 | let current_col = self.cells[self.current].column; 115 | 116 | let neighbor_indices = [ 117 | self.calculate_index(current_row - 1, current_col), 118 | self.calculate_index(current_row, current_col + 1), 119 | self.calculate_index(current_row + 1, current_col), 120 | self.calculate_index(current_row, current_col - 1), 121 | ]; 122 | 123 | for i in neighbor_indices.iter() { 124 | if *i != -1 && !self.cells[*i as usize].visited { 125 | neighbors.push(*i as usize); 126 | } 127 | } 128 | 129 | neighbors 130 | } 131 | 132 | fn find_next_cell(&mut self) -> Option { 133 | let neighbors = self.get_available_neighbors(); 134 | match neighbors.len() { 135 | 0 => None, 136 | 1 => Some(neighbors[0]), 137 | len => Some(neighbors[(self.rng.roll_dice(1, len as i32) - 1) as usize]), 138 | } 139 | } 140 | 141 | fn generate_maze(&mut self, build_data: &mut BuilderMap) { 142 | let mut i = 0; 143 | loop { 144 | self.cells[self.current].visited = true; 145 | let next = self.find_next_cell(); 146 | 147 | match next { 148 | Some(next) => { 149 | self.backtrace.push(self.current); 150 | let (lower_part, higher_part) = 151 | self.cells.split_at_mut(std::cmp::max(self.current, next)); 152 | let cell1 = &mut lower_part[std::cmp::min(self.current, next)]; 153 | let cell2 = &mut higher_part[0]; 154 | cell1.remove_walls(cell2); 155 | self.current = next; 156 | } 157 | None => { 158 | if let Some(back) = self.backtrace.pop() { 159 | self.current = back; 160 | } else { 161 | break; 162 | } 163 | } 164 | } 165 | 166 | if i % 50 == 0 { 167 | self.copy_to_map(&mut build_data.map); 168 | build_data.take_snapshot(); 169 | } 170 | i += 1; 171 | } 172 | } 173 | 174 | fn copy_to_map(&self, map: &mut Map) { 175 | // Clear the map 176 | for i in map.tiles.iter_mut() { 177 | *i = TileType::Wall; 178 | } 179 | 180 | for cell in self.cells.iter() { 181 | let x = cell.column + 1; 182 | let y = cell.row + 1; 183 | let idx = map.xy_idx(x * 2, y * 2); 184 | 185 | map.tiles[idx] = TileType::Floor; 186 | if !cell.walls[TOP] { 187 | map.tiles[idx - map.width as usize] = TileType::Floor; 188 | } 189 | if !cell.walls[RIGHT] { 190 | map.tiles[idx + 1] = TileType::Floor; 191 | } 192 | if !cell.walls[BOTTOM] { 193 | map.tiles[idx + map.width as usize] = TileType::Floor; 194 | } 195 | if !cell.walls[LEFT] { 196 | map.tiles[idx - 1] = TileType::Floor; 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/spawner.rs: -------------------------------------------------------------------------------- 1 | use super::{components::*, prefabs::*, random_table::RandomTable, Map, Rect, TileType}; 2 | use crate::{attr_bonus, mana_at_level, player_hp_at_level}; 3 | use legion::prelude::*; 4 | use rltk::{RandomNumberGenerator, RGB}; 5 | use std::collections::HashMap; 6 | 7 | const MAX_MONSTERS: i32 = 4; 8 | const BASE_ATTRIBUTE: i32 = 11; 9 | 10 | // Spawns the player and returns the entity object. 11 | pub fn player(world: &mut World, x: i32, y: i32) -> Entity { 12 | let mut skills = Skills { 13 | skills: HashMap::new(), 14 | }; 15 | skills.skills.insert(Skill::Melee, 1); 16 | skills.skills.insert(Skill::Defense, 1); 17 | skills.skills.insert(Skill::Magic, 1); 18 | 19 | let player = world.insert( 20 | (Player, BlocksTile), 21 | vec![( 22 | Position { x, y }, 23 | Renderable { 24 | glyph: rltk::to_cp437('@'), 25 | fg: RGB::named(rltk::YELLOW), 26 | bg: RGB::named(rltk::BLACK), 27 | render_order: 0, 28 | }, 29 | Viewshed { 30 | visible_tiles: Vec::new(), 31 | range: 8, 32 | dirty: true, 33 | }, 34 | Name { 35 | name: "Player".to_string(), 36 | }, 37 | HungerClock { 38 | state: HungerState::WellFed, 39 | duration: 20, 40 | }, 41 | Attributes { 42 | might: Attribute { 43 | base: 11, 44 | modifiers: 0, 45 | bonus: attr_bonus(BASE_ATTRIBUTE), 46 | }, 47 | fitness: Attribute { 48 | base: BASE_ATTRIBUTE, 49 | modifiers: 0, 50 | bonus: attr_bonus(BASE_ATTRIBUTE), 51 | }, 52 | quickness: Attribute { 53 | base: BASE_ATTRIBUTE, 54 | modifiers: 0, 55 | bonus: attr_bonus(BASE_ATTRIBUTE), 56 | }, 57 | intelligence: Attribute { 58 | base: BASE_ATTRIBUTE, 59 | modifiers: 0, 60 | bonus: attr_bonus(BASE_ATTRIBUTE), 61 | }, 62 | }, 63 | skills, 64 | Pools { 65 | hit_points: Pool { 66 | current: player_hp_at_level(BASE_ATTRIBUTE, 1), 67 | max: player_hp_at_level(BASE_ATTRIBUTE, 1), 68 | }, 69 | mana: Pool { 70 | current: mana_at_level(BASE_ATTRIBUTE, 1), 71 | max: mana_at_level(BASE_ATTRIBUTE, 1), 72 | }, 73 | experience: 0, 74 | level: 1, 75 | }, 76 | )], 77 | )[0]; 78 | 79 | // Starting equipment 80 | spawn_named_entity( 81 | &PREFABS.lock().unwrap(), 82 | world, 83 | "Rusty Longsword", 84 | SpawnType::Equipped { by: player }, 85 | ); 86 | spawn_named_entity( 87 | &PREFABS.lock().unwrap(), 88 | world, 89 | "Dried Sausage", 90 | SpawnType::Carried { by: player }, 91 | ); 92 | spawn_named_entity( 93 | &PREFABS.lock().unwrap(), 94 | world, 95 | "Beer", 96 | SpawnType::Carried { by: player }, 97 | ); 98 | spawn_named_entity( 99 | &PREFABS.lock().unwrap(), 100 | world, 101 | "Stained Tunic", 102 | SpawnType::Equipped { by: player }, 103 | ); 104 | spawn_named_entity( 105 | &PREFABS.lock().unwrap(), 106 | world, 107 | "Torn Trousers", 108 | SpawnType::Equipped { by: player }, 109 | ); 110 | spawn_named_entity( 111 | &PREFABS.lock().unwrap(), 112 | world, 113 | "Old Boots", 114 | SpawnType::Equipped { by: player }, 115 | ); 116 | 117 | player 118 | } 119 | 120 | fn room_table(map_depth: i32) -> RandomTable { 121 | get_spawn_table_for_depth(&PREFABS.lock().unwrap(), map_depth) 122 | } 123 | 124 | pub fn spawn_room( 125 | map: &Map, 126 | rng: &mut RandomNumberGenerator, 127 | room: &Rect, 128 | map_depth: i32, 129 | spawn_list: &mut Vec<(usize, String)>, 130 | ) { 131 | let mut possible_targets = Vec::new(); 132 | 133 | // Borrow scope - to keep access to the map isolated 134 | { 135 | for y in room.y1 + 1..room.y2 { 136 | for x in room.x1 + 1..room.x2 { 137 | let idx = map.xy_idx(x, y); 138 | if map.tiles[idx] == TileType::Floor { 139 | possible_targets.push(idx); 140 | } 141 | } 142 | } 143 | } 144 | 145 | spawn_region(map, rng, &possible_targets, map_depth, spawn_list); 146 | } 147 | 148 | pub fn spawn_region( 149 | _map: &Map, 150 | rng: &mut RandomNumberGenerator, 151 | area: &[usize], 152 | map_depth: i32, 153 | spawn_list: &mut Vec<(usize, String)>, 154 | ) { 155 | let spawn_table = room_table(map_depth); 156 | let mut spawn_points = HashMap::new(); 157 | let mut areas = Vec::from(area); 158 | 159 | let num_spawns = i32::min( 160 | areas.len() as i32, 161 | rng.roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3, 162 | ); 163 | if num_spawns == 0 { 164 | return; 165 | } 166 | 167 | for _i in 0..num_spawns { 168 | let array_index = if area.len() == 1 { 169 | 0 170 | } else { 171 | (rng.roll_dice(1, areas.len() as i32) - 1) as usize 172 | }; 173 | let map_idx = areas[array_index]; 174 | spawn_points.insert(map_idx, spawn_table.roll(rng)); 175 | areas.remove(array_index); 176 | } 177 | 178 | // Actually spawn the monsters 179 | for (idx, spawn) in spawn_points.iter() { 180 | if let Some(spawn) = spawn { 181 | spawn_list.push((*idx, (*spawn).clone())); 182 | } 183 | } 184 | } 185 | 186 | // Spawn a named entity at the location 187 | pub fn spawn_entity(world: &mut World, map: &Map, idx: &usize, name: &str) { 188 | let x = *idx as i32 % map.width; 189 | let y = *idx as i32 / map.width; 190 | 191 | let item_result = spawn_named_entity( 192 | &PREFABS.lock().unwrap(), 193 | world, 194 | name, 195 | SpawnType::AtPosition { x, y }, 196 | ); 197 | if item_result.is_some() { 198 | return; 199 | } 200 | 201 | rltk::console::log(format!("WARNING: Don't know how to spawn [{}]!", name)); 202 | } 203 | -------------------------------------------------------------------------------- /src/map_builders/drunkard.rs: -------------------------------------------------------------------------------- 1 | use super::{paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | #[derive(PartialEq, Copy, Clone)] 5 | pub enum DrunkSpawnMode { 6 | StartingPoint, 7 | Random, 8 | } 9 | 10 | pub struct DrunkardSettings { 11 | pub spawn_mode: DrunkSpawnMode, 12 | pub drunken_lifetime: i32, 13 | pub floor_percent: f32, 14 | pub brush_size: i32, 15 | pub symmetry: Symmetry, 16 | } 17 | 18 | pub struct DrunkardsWalkBuilder { 19 | settings: DrunkardSettings, 20 | } 21 | 22 | impl InitialMapBuilder for DrunkardsWalkBuilder { 23 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 24 | self.build(rng, build_data); 25 | } 26 | } 27 | 28 | impl MetaMapBuilder for DrunkardsWalkBuilder { 29 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 30 | self.build(rng, build_data); 31 | } 32 | } 33 | 34 | impl DrunkardsWalkBuilder { 35 | pub fn new(settings: DrunkardSettings) -> Box { 36 | Box::new(DrunkardsWalkBuilder { settings }) 37 | } 38 | 39 | pub fn open_area() -> Box { 40 | Self::new(DrunkardSettings { 41 | spawn_mode: DrunkSpawnMode::StartingPoint, 42 | drunken_lifetime: 400, 43 | floor_percent: 0.5, 44 | brush_size: 1, 45 | symmetry: Symmetry::None, 46 | }) 47 | } 48 | 49 | pub fn open_halls() -> Box { 50 | Self::new(DrunkardSettings { 51 | spawn_mode: DrunkSpawnMode::Random, 52 | drunken_lifetime: 400, 53 | floor_percent: 0.5, 54 | brush_size: 1, 55 | symmetry: Symmetry::None, 56 | }) 57 | } 58 | 59 | pub fn winding_passages() -> Box { 60 | Self::new(DrunkardSettings { 61 | spawn_mode: DrunkSpawnMode::Random, 62 | drunken_lifetime: 100, 63 | floor_percent: 0.4, 64 | brush_size: 1, 65 | symmetry: Symmetry::None, 66 | }) 67 | } 68 | 69 | pub fn fat_passages() -> Box { 70 | Self::new(DrunkardSettings { 71 | spawn_mode: DrunkSpawnMode::Random, 72 | drunken_lifetime: 100, 73 | floor_percent: 0.4, 74 | brush_size: 2, 75 | symmetry: Symmetry::None, 76 | }) 77 | } 78 | 79 | pub fn fearful_symmetry() -> Box { 80 | Self::new(DrunkardSettings { 81 | spawn_mode: DrunkSpawnMode::Random, 82 | drunken_lifetime: 100, 83 | floor_percent: 0.4, 84 | brush_size: 1, 85 | symmetry: Symmetry::Both, 86 | }) 87 | } 88 | 89 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 90 | // Set a central starting point 91 | let starting_position = Position { 92 | x: build_data.map.width / 2, 93 | y: build_data.map.height / 2, 94 | }; 95 | let start_idx = build_data 96 | .map 97 | .xy_idx(starting_position.x, starting_position.y); 98 | build_data.map.tiles[start_idx] = TileType::Floor; 99 | 100 | let total_tiles = build_data.map.width * build_data.map.height; 101 | let desired_floor_tiles = (self.settings.floor_percent * total_tiles as f32) as usize; 102 | let mut digger_count = 0; 103 | 104 | while { 105 | let floor_tile_count = build_data 106 | .map 107 | .tiles 108 | .iter() 109 | .filter(|tile| **tile == TileType::Floor) 110 | .count(); 111 | floor_tile_count < desired_floor_tiles 112 | } { 113 | let mut did_something = false; 114 | let mut drunk_x; 115 | let mut drunk_y; 116 | match self.settings.spawn_mode { 117 | DrunkSpawnMode::StartingPoint => { 118 | drunk_x = starting_position.x; 119 | drunk_y = starting_position.y; 120 | } 121 | DrunkSpawnMode::Random => { 122 | if digger_count == 0 { 123 | drunk_x = starting_position.x; 124 | drunk_y = starting_position.y; 125 | } else { 126 | drunk_x = rng.roll_dice(1, build_data.map.width - 3) + 1; 127 | drunk_y = rng.roll_dice(1, build_data.map.height - 3) + 1; 128 | } 129 | } 130 | } 131 | let mut drunk_life = self.settings.drunken_lifetime; 132 | 133 | while drunk_life > 0 { 134 | let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y); 135 | if build_data.map.tiles[drunk_idx] == TileType::Wall { 136 | did_something = true; 137 | } 138 | paint( 139 | &mut build_data.map, 140 | self.settings.symmetry, 141 | self.settings.brush_size, 142 | drunk_x, 143 | drunk_y, 144 | ); 145 | build_data.map.tiles[drunk_idx] = TileType::DownStairs; 146 | 147 | let stagger_direction = rng.roll_dice(1, 4); 148 | match stagger_direction { 149 | 1 => { 150 | if drunk_x > 2 { 151 | drunk_x -= 1; 152 | } 153 | } 154 | 2 => { 155 | if drunk_x < build_data.map.width - 2 { 156 | drunk_x += 1; 157 | } 158 | } 159 | 3 => { 160 | if drunk_y > 2 { 161 | drunk_y -= 1; 162 | } 163 | } 164 | _ => { 165 | if drunk_y < build_data.map.height - 2 { 166 | drunk_y += 1; 167 | } 168 | } 169 | } 170 | 171 | drunk_life -= 1; 172 | } 173 | if did_something { 174 | build_data.take_snapshot(); 175 | } 176 | 177 | digger_count += 1; 178 | for t in build_data.map.tiles.iter_mut() { 179 | if *t == TileType::DownStairs { 180 | *t = TileType::Floor; 181 | } 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/damage_system.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | gamelog::GameLog, mana_at_level, particle_system::ParticleBuilder, player_hp_at_level, 3 | Attributes, Equipped, InBackpack, LootTable, Map, Name, Player, Point, Pools, Position, 4 | RunState, SufferDamage, 5 | }; 6 | use crate::prefabs::{get_item_drop, spawn_named_item, SpawnType, PREFABS}; 7 | use legion::prelude::*; 8 | 9 | pub fn build() -> Box<(dyn Schedulable + 'static)> { 10 | SystemBuilder::new("damage") 11 | .with_query(Write::::query()) 12 | .write_component::() 13 | .read_component::() 14 | .write_resource::() 15 | .read_resource::() 16 | .write_resource::() 17 | .read_component::() 18 | .write_resource::() 19 | .read_resource::() 20 | .build( 21 | |command_buffer, 22 | world, 23 | (map, player_entity, log, particles, player_pos), 24 | query| unsafe { 25 | let mut xp_gain = 0; 26 | for (entity, mut damage) in query.iter_entities_unchecked(world) { 27 | if let Some(mut stats) = world.get_component_mut_unchecked::(entity) { 28 | for (dmg, from_player) in damage.amount.iter() { 29 | stats.hit_points.current -= dmg; 30 | 31 | if stats.hit_points.current < 1 && *from_player { 32 | xp_gain += stats.level * 100; 33 | } 34 | } 35 | 36 | if let Some(pos) = world.get_component::(entity) { 37 | let idx = map.xy_idx(pos.x, pos.y); 38 | map.bloodstains.insert(idx); 39 | } 40 | } 41 | 42 | damage.amount.clear(); 43 | command_buffer.remove_component::(entity); 44 | } 45 | 46 | if xp_gain != 0 { 47 | let player_attributes = 48 | *(world.get_component::(**player_entity).unwrap()); 49 | let mut player_stats = 50 | world.get_component_mut::(**player_entity).unwrap(); 51 | player_stats.experience += xp_gain; 52 | if player_stats.experience >= player_stats.level * 1000 { 53 | // We've gone up a level! 54 | player_stats.level = player_stats.experience / 1000 + 1; 55 | log.entries.push(format!( 56 | "Congratulations, you are now level {}", 57 | player_stats.level 58 | )); 59 | player_stats.hit_points.max = player_hp_at_level( 60 | player_attributes.fitness.base + player_attributes.fitness.modifiers, 61 | player_stats.level, 62 | ); 63 | player_stats.hit_points.current = player_stats.hit_points.max; 64 | player_stats.mana.max = mana_at_level( 65 | player_attributes.intelligence.base 66 | + player_attributes.intelligence.modifiers, 67 | player_stats.level, 68 | ); 69 | player_stats.mana.current = player_stats.mana.max; 70 | 71 | for i in 0..10 { 72 | if player_pos.y - i > 1 { 73 | particles.request( 74 | player_pos.x, 75 | player_pos.y - i, 76 | rltk::RGB::named(rltk::GOLD), 77 | rltk::RGB::named(rltk::BLACK), 78 | rltk::to_cp437('░'), 79 | 400.0, 80 | ); 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | ) 87 | } 88 | 89 | pub fn delete_the_dead(world: &mut World, resources: &mut Resources) { 90 | let mut dead = Vec::new(); 91 | for (victim, stats) in Read::::query().iter_entities(world) { 92 | if stats.hit_points.current < 1 { 93 | if let Some(_player) = world.get_tag::(victim) { 94 | resources.insert(RunState::GameOver); 95 | } else { 96 | dead.push(victim); 97 | } 98 | } 99 | } 100 | 101 | // Drop everything held by dead people 102 | let mut to_drop = Vec::new(); 103 | let mut to_spawn = Vec::new(); 104 | for victim in dead.iter() { 105 | if let Some(pos) = world.get_component::(*victim) { 106 | // Drop their stuff 107 | for (entity, equipped) in Read::::query().iter_entities(world) { 108 | if equipped.owner == *victim { 109 | to_drop.push((entity, (*pos).clone())); 110 | } 111 | } 112 | for (entity, backpack) in Read::::query().iter_entities(world) { 113 | if backpack.owner == *victim { 114 | to_drop.push((entity, (*pos).clone())); 115 | } 116 | } 117 | 118 | if let Some(loot) = world.get_component::(*victim) { 119 | let mut rng = resources.get_mut::().unwrap(); 120 | if let Some(drop) = get_item_drop(&PREFABS.lock().unwrap(), &mut rng, &loot.table) { 121 | to_spawn.push((drop, (*pos).clone())); 122 | } 123 | } 124 | } 125 | } 126 | for (entity, pos) in to_drop.drain(..) { 127 | world 128 | .remove_components::<(Equipped, InBackpack)>(entity) 129 | .expect("Dropping item failed"); 130 | world 131 | .add_component(entity, pos) 132 | .expect("Positioning dropped item failed"); 133 | } 134 | 135 | for (tag, pos) in to_spawn.drain(..) { 136 | spawn_named_item( 137 | &PREFABS.lock().unwrap(), 138 | world, 139 | &tag, 140 | SpawnType::AtPosition { x: pos.x, y: pos.y }, 141 | ); 142 | } 143 | 144 | let mut log = resources.get_mut::().unwrap(); 145 | for victim in dead.iter() { 146 | let name = if let Some(name) = world.get_component::(*victim) { 147 | name.name.clone() 148 | } else { 149 | "-Unnamed-".to_string() 150 | }; 151 | log.entries 152 | .push(format!("{} is pushing up the daisies.", name)); 153 | world.delete(*victim); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/map/fov.rs: -------------------------------------------------------------------------------- 1 | use super::Map; 2 | use num_rational::Ratio; 3 | use rltk::{Algorithm2D, BaseMap, BresenhamCircleNoDiag, Point}; 4 | 5 | pub fn field_of_view(origin: Point, range: usize, map: &Map) -> Vec { 6 | let mut visible_tiles = compute_fov(origin, range, map); 7 | // build a vector of strides of FoV circle rows 8 | let mut ranges: Vec<(i32, i32)> = vec![(origin.x, origin.x); range * 2 + 1]; 9 | BresenhamCircleNoDiag::new(origin, range as i32).for_each(|point| { 10 | let idx = point.y - (origin.y - range as i32); 11 | if idx >= 0 { 12 | if ranges[idx as usize].0 > point.x { 13 | ranges[idx as usize].0 = point.x 14 | } 15 | if ranges[idx as usize].1 < point.x { 16 | ranges[idx as usize].1 = point.x 17 | } 18 | } 19 | }); 20 | // Retail tiles in visibility circle 21 | visible_tiles.retain(|point| { 22 | if point.x < 0 || point.x >= map.width || point.y < 0 || point.y >= map.height { 23 | return false; 24 | } 25 | 26 | let idx = point.y - (origin.y - range as i32); 27 | if idx < 0 { 28 | return false; 29 | } 30 | 31 | let (min, max) = ranges[idx as usize]; 32 | min <= point.x && point.x <= max 33 | }); 34 | visible_tiles 35 | } 36 | 37 | // Symmetric Shadowcasting FoV implementation 38 | // Based on https://www.albertford.com/shadowcasting/ 39 | 40 | fn scan( 41 | row: &mut Row, 42 | quadrant: &Quadrant, 43 | range: usize, 44 | map: &Map, 45 | visible_tiles: &mut Vec, 46 | ) { 47 | if row.depth > range { 48 | return; 49 | } 50 | 51 | let mut prev_tile = None; 52 | let (depth, min_col, max_col) = row.tiles(); 53 | for col in min_col..=max_col { 54 | let tile = (depth as i32, col); 55 | if is_wall(Some(tile), quadrant, map) || is_symmetric(&row, tile) { 56 | reveal(tile, quadrant, visible_tiles); 57 | } 58 | if is_wall(prev_tile, quadrant, map) && is_floor(Some(tile), quadrant, map) { 59 | row.start_slope = slope(tile); 60 | } 61 | if is_floor(prev_tile, quadrant, map) && is_wall(Some(tile), quadrant, map) { 62 | let mut next_row = row.next(); 63 | next_row.end_slope = slope(tile); 64 | scan(&mut next_row, quadrant, range, map, visible_tiles); 65 | } 66 | prev_tile = Some(tile); 67 | } 68 | if is_floor(prev_tile, quadrant, map) { 69 | let mut next_row = row.next(); 70 | scan(&mut next_row, quadrant, range, map, visible_tiles); 71 | } 72 | } 73 | /* 74 | fn scan_iterative(row: Row) { 75 | let mut rows = vec![row]; 76 | while !rows.isEmpty() { 77 | let row = rows.pop(); 78 | let prev_tile = None; 79 | for tile in row.tiles() { 80 | if is_wall(tile) || is_symmetric(row, tile) { 81 | reveal(tile); 82 | } 83 | if is_wall(prev_tile) && is_floor(tile) { 84 | row.start_slope = slope(tile); 85 | } 86 | if is_floor(prev_tile) && is_wall(tile) { 87 | next_row = row.next(); 88 | next_row.end_slope = slope(tile); 89 | rows.append(next_row); 90 | } 91 | prev_tile = tile; 92 | } 93 | if is_floor(prev_tile) { 94 | rows.append(row.next()); 95 | } 96 | } 97 | } 98 | */ 99 | 100 | #[inline] 101 | fn reveal(tile: (i32, i32), quadrant: &Quadrant, visible_tiles: &mut Vec) { 102 | let (x, y) = quadrant.transform(tile); 103 | visible_tiles.push(Point::new(x, y)); 104 | } 105 | 106 | #[inline] 107 | fn is_wall(tile: Option<(i32, i32)>, quadrant: &Quadrant, map: &Map) -> bool { 108 | match tile { 109 | None => false, 110 | Some(tile) => { 111 | let (x, y) = quadrant.transform(tile); 112 | if x < 0 || x >= map.width || y < 0 || y >= map.height { 113 | return true; 114 | }; 115 | map.is_opaque(map.point2d_to_index(Point::new(x, y))) 116 | } 117 | } 118 | } 119 | 120 | #[inline] 121 | fn is_floor(tile: Option<(i32, i32)>, quadrant: &Quadrant, map: &Map) -> bool { 122 | match tile { 123 | None => false, 124 | Some(tile) => { 125 | let (x, y) = quadrant.transform(tile); 126 | if x < 0 || x >= map.width || y < 0 || y >= map.height { 127 | return false; 128 | }; 129 | !map.is_opaque(map.point2d_to_index(Point::new(x, y))) 130 | } 131 | } 132 | } 133 | 134 | fn compute_fov(origin: Point, range: usize, map: &Map) -> Vec { 135 | let mut visible_tiles = Vec::new(); 136 | 137 | // mark_visible(origin); 138 | visible_tiles.push(origin); 139 | 140 | for i in &[ 141 | Cardinal::North, 142 | Cardinal::East, 143 | Cardinal::South, 144 | Cardinal::West, 145 | ] { 146 | let quadrant = Quadrant::new(*i, origin); 147 | 148 | let mut first_row = Row::new(1, Ratio::from_integer(-1), Ratio::from_integer(1)); 149 | scan(&mut first_row, &quadrant, range, map, &mut visible_tiles); 150 | } 151 | 152 | visible_tiles 153 | } 154 | 155 | #[derive(Clone, Copy, Debug, PartialEq)] 156 | enum Cardinal { 157 | North, 158 | East, 159 | South, 160 | West, 161 | } 162 | 163 | struct Quadrant { 164 | cardinal: Cardinal, 165 | ox: i32, 166 | oy: i32, 167 | } 168 | 169 | impl Quadrant { 170 | pub fn new(cardinal: Cardinal, origin: Point) -> Self { 171 | Self { 172 | cardinal, 173 | ox: origin.x, 174 | oy: origin.y, 175 | } 176 | } 177 | 178 | fn transform(&self, tile: (i32, i32)) -> (i32, i32) { 179 | let (row, col) = tile; 180 | match self.cardinal { 181 | Cardinal::North => (self.ox + col, self.oy - row), 182 | Cardinal::South => (self.ox + col, self.oy + row), 183 | Cardinal::East => (self.ox + row, self.oy + col), 184 | Cardinal::West => (self.ox - row, self.oy + col), 185 | } 186 | } 187 | } 188 | 189 | struct Row { 190 | depth: usize, 191 | start_slope: Ratio, 192 | end_slope: Ratio, 193 | } 194 | 195 | impl Row { 196 | pub fn new(depth: usize, start_slope: Ratio, end_slope: Ratio) -> Self { 197 | Self { 198 | depth, 199 | start_slope, 200 | end_slope, 201 | } 202 | } 203 | 204 | fn tiles(&self) -> (usize, i32, i32) { 205 | let min_col = round_ties_up(self.depth, self.start_slope); 206 | let max_col = round_ties_down(self.depth, self.end_slope); 207 | (self.depth, min_col, max_col) 208 | } 209 | 210 | fn next(&self) -> Row { 211 | Row::new(self.depth + 1, self.start_slope, self.end_slope) 212 | } 213 | } 214 | 215 | #[inline] 216 | fn slope(tile: (i32, i32)) -> Ratio { 217 | let (row_depth, col) = tile; 218 | Ratio::new(2 * col - 1, 2 * row_depth) 219 | } 220 | 221 | fn is_symmetric(row: &Row, tile: (i32, i32)) -> bool { 222 | let (_row_depth, col) = tile; 223 | Ratio::from_integer(col) >= Ratio::from_integer(row.depth as i32) * row.start_slope 224 | && Ratio::from_integer(col) <= Ratio::from_integer(row.depth as i32) * row.end_slope 225 | } 226 | fn round_ties_up(d: usize, n: Ratio) -> i32 { 227 | let multiplied = Ratio::from_integer(d as i32) * n; 228 | let ratio = *multiplied.numer() as f32 / *multiplied.denom() as f32; 229 | f32::floor(ratio + 0.5) as i32 230 | } 231 | fn round_ties_down(d: usize, n: Ratio) -> i32 { 232 | let multiplied = Ratio::from_integer(d as i32) * n; 233 | let ratio = *multiplied.numer() as f32 / *multiplied.denom() as f32; 234 | f32::ceil(ratio - 0.5) as i32 235 | } 236 | -------------------------------------------------------------------------------- /src/map_builders/waveform_collapse/constraints.rs: -------------------------------------------------------------------------------- 1 | use super::{tile_idx_in_chunk, Map, MapChunk, TileType}; 2 | use std::collections::HashSet; 3 | 4 | pub fn build_patterns( 5 | map: &Map, 6 | chunk_size: i32, 7 | include_flipping: bool, 8 | dedupe: bool, 9 | ) -> Vec> { 10 | let chunks_x = map.width / chunk_size; 11 | let chunks_y = map.height / chunk_size; 12 | let mut patterns = Vec::new(); 13 | 14 | for cy in 0..chunks_y { 15 | for cx in 0..chunks_x { 16 | // Normal orientation 17 | let mut pattern = Vec::new(); 18 | let start_x = cx * chunk_size; 19 | let end_x = start_x + chunk_size; 20 | let start_y = cy * chunk_size; 21 | let end_y = start_y + chunk_size; 22 | 23 | for y in start_y..end_y { 24 | for x in start_x..end_x { 25 | let idx = map.xy_idx(x, y); 26 | pattern.push(map.tiles[idx]); 27 | } 28 | } 29 | patterns.push(pattern); 30 | 31 | if include_flipping { 32 | // Flip horizontal 33 | pattern = Vec::new(); 34 | for y in start_y..end_y { 35 | for x in (start_x..end_x).rev() { 36 | let idx = map.xy_idx(x, y); 37 | pattern.push(map.tiles[idx]); 38 | } 39 | } 40 | patterns.push(pattern); 41 | 42 | // Flip vertical 43 | pattern = Vec::new(); 44 | for y in (start_y..end_y).rev() { 45 | for x in start_x..end_x { 46 | let idx = map.xy_idx(x, y); 47 | pattern.push(map.tiles[idx]); 48 | } 49 | } 50 | patterns.push(pattern); 51 | 52 | // Flip both 53 | pattern = Vec::new(); 54 | for y in (start_y..end_y).rev() { 55 | for x in (start_x..end_x).rev() { 56 | let idx = map.xy_idx(x, y); 57 | pattern.push(map.tiles[idx]); 58 | } 59 | } 60 | patterns.push(pattern); 61 | } 62 | } 63 | } 64 | 65 | // Dedupe 66 | if dedupe { 67 | rltk::console::log(format!( 68 | "Pre de-duplication, there are {} patterns", 69 | patterns.len() 70 | )); 71 | let set: HashSet> = patterns.drain(..).collect(); 72 | patterns.extend(set); 73 | rltk::console::log(format!("There are {} patterns", patterns.len())); 74 | } 75 | 76 | patterns 77 | } 78 | 79 | pub fn render_pattern_to_map(map: &mut Map, chunk: &MapChunk, chunk_size: i32, x: i32, y: i32) { 80 | let mut i = 0usize; 81 | for tile_y in 0..chunk_size { 82 | for tile_x in 0..chunk_size { 83 | let idx = map.xy_idx(x + tile_x, y + tile_y); 84 | map.tiles[idx] = chunk.pattern[i]; 85 | map.visible_tiles[idx] = true; 86 | i += 1; 87 | } 88 | } 89 | 90 | for (i, northbound) in chunk.exits[0].iter().enumerate() { 91 | if *northbound { 92 | let idx = map.xy_idx(x + i as i32, y); 93 | map.tiles[idx] = TileType::DownStairs; 94 | } 95 | } 96 | for (i, southbound) in chunk.exits[1].iter().enumerate() { 97 | if *southbound { 98 | let idx = map.xy_idx(x + i as i32, y + chunk_size - 1); 99 | map.tiles[idx] = TileType::DownStairs; 100 | } 101 | } 102 | for (i, westbound) in chunk.exits[2].iter().enumerate() { 103 | if *westbound { 104 | let idx = map.xy_idx(x, y + i as i32); 105 | map.tiles[idx] = TileType::DownStairs; 106 | } 107 | } 108 | for (i, eastbound) in chunk.exits[3].iter().enumerate() { 109 | if *eastbound { 110 | let idx = map.xy_idx(x + chunk_size - 1, y + i as i32); 111 | map.tiles[idx] = TileType::DownStairs; 112 | } 113 | } 114 | } 115 | 116 | pub fn patterns_to_constraints(patterns: Vec>, chunk_size: i32) -> Vec { 117 | // Move into new constraints object 118 | let mut constraints = Vec::new(); 119 | for p in patterns { 120 | let mut new_chunk = MapChunk { 121 | pattern: p, 122 | exits: [ 123 | vec![false; chunk_size as usize], 124 | vec![false; chunk_size as usize], 125 | vec![false; chunk_size as usize], 126 | vec![false; chunk_size as usize], 127 | ], 128 | has_exits: false, 129 | compatible_with: [Vec::new(), Vec::new(), Vec::new(), Vec::new()], 130 | }; 131 | 132 | let mut n_exits = 0; 133 | for x in 0..chunk_size { 134 | // Check for north-bound exits 135 | let north_idx = tile_idx_in_chunk(chunk_size, x, 0); 136 | if new_chunk.pattern[north_idx] == TileType::Floor { 137 | new_chunk.exits[0][x as usize] = true; 138 | n_exits += 1; 139 | } 140 | 141 | // Check for south-bound exits 142 | let south_idx = tile_idx_in_chunk(chunk_size, x, chunk_size - 1); 143 | if new_chunk.pattern[south_idx] == TileType::Floor { 144 | new_chunk.exits[1][x as usize] = true; 145 | n_exits += 1; 146 | } 147 | 148 | // Check for west-bound exits 149 | let west_idx = tile_idx_in_chunk(chunk_size, 0, x); 150 | if new_chunk.pattern[west_idx] == TileType::Floor { 151 | new_chunk.exits[2][x as usize] = true; 152 | n_exits += 1; 153 | } 154 | 155 | // Check for east-bound exits 156 | let east_idx = tile_idx_in_chunk(chunk_size, chunk_size - 1, x); 157 | if new_chunk.pattern[east_idx] == TileType::Floor { 158 | new_chunk.exits[3][x as usize] = true; 159 | n_exits += 1; 160 | } 161 | } 162 | 163 | new_chunk.has_exits = n_exits > 0; 164 | 165 | constraints.push(new_chunk); 166 | } 167 | 168 | // Build compatibility matrix 169 | let ch = constraints.clone(); 170 | for (i, c) in constraints.iter_mut().enumerate() { 171 | for (j, potential) in ch.iter().enumerate() { 172 | // Evaluate compatibility by direction 173 | for (direction, exit_list) in c.exits.iter_mut().enumerate() { 174 | let opposite = match direction { 175 | 0 => 1, // Our North, Their South 176 | 1 => 0, // Our South, Their North 177 | 2 => 3, // Our West, Their East 178 | _ => 2, // Our East, Their West 179 | }; 180 | 181 | let mut it_fits = false; 182 | let mut has_any = false; 183 | for (slot, can_enter) in exit_list.iter().enumerate() { 184 | if *can_enter { 185 | has_any = true; 186 | if potential.exits[opposite][slot] { 187 | it_fits = true; 188 | } 189 | } 190 | } 191 | if it_fits { 192 | c.compatible_with[direction].push(j); 193 | } else if !has_any { 194 | // There's no exits on this side, let's match if 195 | // the other edge also has no exits 196 | let exit_count = potential.exits[opposite].iter().filter(|a| **a).count(); 197 | if exit_count == 0 { 198 | c.compatible_with[direction].push(j); 199 | } 200 | } 201 | } 202 | } 203 | for (direction, compatible) in c.compatible_with.iter_mut().enumerate() { 204 | if compatible.len() == 0 { 205 | rltk::console::log(format!( 206 | "Eek! No compatibles for chunk {} in direction {}", 207 | i, direction 208 | )); 209 | // insert self 210 | compatible.push(i); 211 | } 212 | } 213 | } 214 | 215 | constraints 216 | } 217 | -------------------------------------------------------------------------------- /src/map_builders/waveform_collapse/solver.rs: -------------------------------------------------------------------------------- 1 | use super::{Map, MapChunk}; 2 | use std::collections::HashSet; 3 | 4 | pub struct Solver { 5 | constraints: Vec, 6 | chunk_size: i32, 7 | chunks: Vec>, 8 | chunks_x: usize, 9 | chunks_y: usize, 10 | remaining: Vec<(usize, i32)>, // (index, # neighbors) 11 | pub possible: bool, 12 | } 13 | 14 | impl Solver { 15 | pub fn new(constraints: Vec, chunk_size: i32, map: &Map) -> Self { 16 | let chunks_x = (map.width / chunk_size) as usize; 17 | let chunks_y = (map.height / chunk_size) as usize; 18 | let n_chunks = chunks_x * chunks_y; 19 | 20 | Solver { 21 | constraints, 22 | chunk_size, 23 | chunks: vec![None; n_chunks], 24 | chunks_x, 25 | chunks_y, 26 | remaining: (0..n_chunks).map(|n| (n, 0)).collect(), 27 | possible: true, 28 | } 29 | } 30 | 31 | fn chunk_idx(&self, chunk_x: usize, chunk_y: usize) -> usize { 32 | (chunk_y * self.chunks_x + chunk_x) as usize 33 | } 34 | 35 | fn count_neighbors(&self, chunk_x: usize, chunk_y: usize) -> i32 { 36 | let mut neighbors = 0; 37 | 38 | if chunk_x > 0 { 39 | let left_idx = self.chunk_idx(chunk_x - 1, chunk_y); 40 | match self.chunks[left_idx] { 41 | None => {} 42 | Some(_) => { 43 | neighbors += 1; 44 | } 45 | } 46 | } 47 | 48 | if chunk_x < self.chunks_x - 1 { 49 | let right_idx = self.chunk_idx(chunk_x + 1, chunk_y); 50 | match self.chunks[right_idx] { 51 | None => {} 52 | Some(_) => { 53 | neighbors += 1; 54 | } 55 | } 56 | } 57 | 58 | if chunk_y > 0 { 59 | let up_idx = self.chunk_idx(chunk_x, chunk_y - 1); 60 | match self.chunks[up_idx] { 61 | None => {} 62 | Some(_) => { 63 | neighbors += 1; 64 | } 65 | } 66 | } 67 | 68 | if chunk_y < self.chunks_y - 1 { 69 | let down_idx = self.chunk_idx(chunk_x, chunk_y + 1); 70 | match self.chunks[down_idx] { 71 | None => {} 72 | Some(_) => { 73 | neighbors += 1; 74 | } 75 | } 76 | } 77 | 78 | neighbors 79 | } 80 | 81 | pub fn iteration(&mut self, map: &mut Map, rng: &mut super::RandomNumberGenerator) -> bool { 82 | if self.remaining.is_empty() { 83 | return true; 84 | } 85 | 86 | // Populate the neighbor count of the remaining list 87 | let mut remain_copy = self.remaining.clone(); 88 | let mut neighbors_exist = false; 89 | for r in remain_copy.iter_mut() { 90 | let idx = r.0; 91 | let chunk_x = idx % self.chunks_x; 92 | let chunk_y = idx / self.chunks_x; 93 | let neighbor_count = self.count_neighbors(chunk_x, chunk_y); 94 | if neighbor_count > 0 { 95 | neighbors_exist = true; 96 | } 97 | *r = (r.0, neighbor_count); 98 | } 99 | remain_copy.sort_by(|a, b| b.1.cmp(&a.1)); 100 | self.remaining = remain_copy; 101 | 102 | // Pick a random chunk we haven't dealt with yet and get its index, remove from remaining list 103 | let remaining_index = if !neighbors_exist { 104 | (rng.roll_dice(1, self.remaining.len() as i32) - 1) as usize 105 | } else { 106 | 0usize 107 | }; 108 | let chunk_index = self.remaining[remaining_index].0; 109 | self.remaining.remove(remaining_index); 110 | 111 | let chunk_x = chunk_index % self.chunks_x; 112 | let chunk_y = chunk_index / self.chunks_x; 113 | 114 | let mut neighbors = 0; 115 | let mut options = Vec::new(); 116 | 117 | if chunk_x > 0 { 118 | let left_idx = self.chunk_idx(chunk_x - 1, chunk_y); 119 | match self.chunks[left_idx] { 120 | None => {} 121 | Some(nt) => { 122 | neighbors += 1; 123 | options.push(self.constraints[nt].compatible_with[3].clone()); 124 | } 125 | } 126 | } 127 | 128 | if chunk_x < self.chunks_x - 1 { 129 | let right_idx = self.chunk_idx(chunk_x + 1, chunk_y); 130 | match self.chunks[right_idx] { 131 | None => {} 132 | Some(nt) => { 133 | neighbors += 1; 134 | options.push(self.constraints[nt].compatible_with[2].clone()); 135 | } 136 | } 137 | } 138 | 139 | if chunk_y > 0 { 140 | let up_idx = self.chunk_idx(chunk_x, chunk_y - 1); 141 | match self.chunks[up_idx] { 142 | None => {} 143 | Some(nt) => { 144 | neighbors += 1; 145 | options.push(self.constraints[nt].compatible_with[1].clone()); 146 | } 147 | } 148 | } 149 | 150 | if chunk_y < self.chunks_y - 1 { 151 | let down_idx = self.chunk_idx(chunk_x, chunk_y + 1); 152 | match self.chunks[down_idx] { 153 | None => {} 154 | Some(nt) => { 155 | neighbors += 1; 156 | options.push(self.constraints[nt].compatible_with[0].clone()); 157 | } 158 | } 159 | } 160 | 161 | if neighbors == 0 { 162 | // There is nothing nearby, so we can have anything! 163 | let new_chunk_idx = (rng.roll_dice(1, self.constraints.len() as i32) - 1) as usize; 164 | self.chunks[chunk_index] = Some(new_chunk_idx); 165 | 166 | // Copy chunk to map 167 | let left_x = chunk_x as i32 * self.chunk_size as i32; 168 | let right_x = (chunk_x as i32 + 1) * self.chunk_size as i32; 169 | let top_y = chunk_y as i32 * self.chunk_size as i32; 170 | let bottom_y = (chunk_y as i32 + 1) * self.chunk_size as i32; 171 | 172 | let mut i: usize = 0; 173 | for y in top_y..bottom_y { 174 | for x in left_x..right_x { 175 | let map_idx = map.xy_idx(x, y); 176 | let tile = self.constraints[new_chunk_idx].pattern[i]; 177 | map.tiles[map_idx] = tile; 178 | i += 1; 179 | } 180 | } 181 | } else { 182 | // There are neighbors, so we try to be compatible with them 183 | let mut options_to_check = HashSet::new(); 184 | for o in options.iter() { 185 | for i in o.iter() { 186 | options_to_check.insert(*i); 187 | } 188 | } 189 | 190 | let mut possible_options = Vec::new(); 191 | for new_chunk_idx in options_to_check.iter() { 192 | let mut possible = true; 193 | for o in options.iter() { 194 | if !o.contains(new_chunk_idx) { 195 | possible = false; 196 | } 197 | } 198 | if possible { 199 | possible_options.push(*new_chunk_idx); 200 | } 201 | } 202 | 203 | if possible_options.is_empty() { 204 | self.possible = false; // TODO: remove chunks around and try again 205 | return true; 206 | } else { 207 | let new_chunk_idx = if possible_options.len() == 1 { 208 | 0 209 | } else { 210 | (rng.roll_dice(1, possible_options.len() as i32) - 1) as usize 211 | }; 212 | let new_chunk = possible_options[new_chunk_idx]; 213 | self.chunks[chunk_index] = Some(new_chunk); 214 | 215 | // Copy chunk to map 216 | let left_x = chunk_x as i32 * self.chunk_size as i32; 217 | let right_x = (chunk_x as i32 + 1) * self.chunk_size as i32; 218 | let top_y = chunk_y as i32 * self.chunk_size as i32; 219 | let bottom_y = (chunk_y as i32 + 1) * self.chunk_size as i32; 220 | 221 | let mut i: usize = 0; 222 | for y in top_y..bottom_y { 223 | for x in left_x..right_x { 224 | let map_idx = map.xy_idx(x, y); 225 | let tile = self.constraints[new_chunk].pattern[i]; 226 | map.tiles[map_idx] = tile; 227 | i += 1; 228 | } 229 | } 230 | } 231 | } 232 | 233 | false 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/map_builders/dla.rs: -------------------------------------------------------------------------------- 1 | use super::{paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType}; 2 | use rltk::RandomNumberGenerator; 3 | 4 | #[derive(PartialEq, Copy, Clone)] 5 | pub enum DLAAlgorithm { 6 | WalkInwards, 7 | WalkOutwards, 8 | CentralAttractor, 9 | } 10 | 11 | pub struct DLABuilder { 12 | algorithm: DLAAlgorithm, 13 | brush_size: i32, 14 | symmetry: Symmetry, 15 | floor_percent: f32, 16 | } 17 | 18 | impl InitialMapBuilder for DLABuilder { 19 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 20 | self.build(rng, build_data); 21 | } 22 | } 23 | 24 | impl MetaMapBuilder for DLABuilder { 25 | fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 26 | self.build(rng, build_data); 27 | } 28 | } 29 | 30 | impl DLABuilder { 31 | #[allow(dead_code)] 32 | pub fn new() -> Box { 33 | Box::new(DLABuilder { 34 | algorithm: DLAAlgorithm::WalkInwards, 35 | brush_size: 2, 36 | symmetry: Symmetry::None, 37 | floor_percent: 0.25, 38 | }) 39 | } 40 | 41 | pub fn walk_inwards() -> Box { 42 | Box::new(DLABuilder { 43 | algorithm: DLAAlgorithm::WalkInwards, 44 | brush_size: 1, 45 | symmetry: Symmetry::None, 46 | floor_percent: 0.25, 47 | }) 48 | } 49 | 50 | pub fn walk_outwards() -> Box { 51 | Box::new(DLABuilder { 52 | algorithm: DLAAlgorithm::WalkOutwards, 53 | brush_size: 2, 54 | symmetry: Symmetry::None, 55 | floor_percent: 0.25, 56 | }) 57 | } 58 | 59 | pub fn central_attractor() -> Box { 60 | Box::new(DLABuilder { 61 | algorithm: DLAAlgorithm::CentralAttractor, 62 | brush_size: 2, 63 | symmetry: Symmetry::None, 64 | floor_percent: 0.25, 65 | }) 66 | } 67 | 68 | pub fn insectoid() -> Box { 69 | Box::new(DLABuilder { 70 | algorithm: DLAAlgorithm::CentralAttractor, 71 | brush_size: 2, 72 | symmetry: Symmetry::Horizontal, 73 | floor_percent: 0.25, 74 | }) 75 | } 76 | 77 | #[allow(dead_code)] 78 | pub fn heavy_erosion() -> Box { 79 | Box::new(DLABuilder { 80 | algorithm: DLAAlgorithm::WalkInwards, 81 | brush_size: 2, 82 | symmetry: Symmetry::None, 83 | floor_percent: 0.35, 84 | }) 85 | } 86 | 87 | fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { 88 | // Carve a starting seed 89 | let starting_position = Position { 90 | x: build_data.map.width / 2, 91 | y: build_data.map.height / 2, 92 | }; 93 | let start_idx = build_data 94 | .map 95 | .xy_idx(starting_position.x, starting_position.y); 96 | build_data.map.tiles[start_idx] = TileType::Floor; 97 | build_data.map.tiles[start_idx - 1] = TileType::Floor; 98 | build_data.map.tiles[start_idx + 1] = TileType::Floor; 99 | build_data.map.tiles[start_idx - build_data.map.width as usize] = TileType::Floor; 100 | build_data.map.tiles[start_idx + build_data.map.width as usize] = TileType::Floor; 101 | build_data.take_snapshot(); 102 | 103 | // Random walker 104 | let total_tiles = build_data.map.width * build_data.map.height; 105 | let desired_floor_tiles = (self.floor_percent * total_tiles as f32) as usize; 106 | while { 107 | let floor_tile_count = build_data 108 | .map 109 | .tiles 110 | .iter() 111 | .filter(|tile| **tile == TileType::Floor) 112 | .count(); 113 | floor_tile_count < desired_floor_tiles 114 | } { 115 | match self.algorithm { 116 | DLAAlgorithm::WalkInwards => { 117 | let mut digger_x = rng.roll_dice(1, build_data.map.width - 3) + 1; 118 | let mut digger_y = rng.roll_dice(1, build_data.map.height - 3) + 1; 119 | let mut prev_x = digger_x; 120 | let mut prev_y = digger_y; 121 | while { 122 | let digger_idx = build_data.map.xy_idx(digger_x, digger_y); 123 | build_data.map.tiles[digger_idx] == TileType::Wall 124 | } { 125 | prev_x = digger_x; 126 | prev_y = digger_y; 127 | let stagger_direction = rng.roll_dice(1, 4); 128 | match stagger_direction { 129 | 1 => { 130 | if digger_x > 2 { 131 | digger_x -= 1; 132 | } 133 | } 134 | 2 => { 135 | if digger_x < build_data.map.width - 2 { 136 | digger_x += 1; 137 | } 138 | } 139 | 3 => { 140 | if digger_y > 2 { 141 | digger_y -= 1; 142 | } 143 | } 144 | _ => { 145 | if digger_y < build_data.map.height - 2 { 146 | digger_y += 1; 147 | } 148 | } 149 | } 150 | } 151 | paint( 152 | &mut build_data.map, 153 | self.symmetry, 154 | self.brush_size, 155 | prev_x, 156 | prev_y, 157 | ); 158 | } 159 | 160 | DLAAlgorithm::WalkOutwards => { 161 | let mut digger_x = starting_position.x; 162 | let mut digger_y = starting_position.y; 163 | while { 164 | let digger_idx = build_data.map.xy_idx(digger_x, digger_y); 165 | build_data.map.tiles[digger_idx] == TileType::Floor 166 | } { 167 | let stagger_direction = rng.roll_dice(1, 4); 168 | match stagger_direction { 169 | 1 => { 170 | if digger_x > 2 { 171 | digger_x -= 1; 172 | } 173 | } 174 | 2 => { 175 | if digger_x < build_data.map.width - 2 { 176 | digger_x += 1; 177 | } 178 | } 179 | 3 => { 180 | if digger_y > 2 { 181 | digger_y -= 1; 182 | } 183 | } 184 | _ => { 185 | if digger_y < build_data.map.height - 2 { 186 | digger_y += 1; 187 | } 188 | } 189 | } 190 | } 191 | paint( 192 | &mut build_data.map, 193 | self.symmetry, 194 | self.brush_size, 195 | digger_x, 196 | digger_y, 197 | ); 198 | } 199 | 200 | DLAAlgorithm::CentralAttractor => { 201 | let mut digger_x = rng.roll_dice(1, build_data.map.width - 3) + 1; 202 | let mut digger_y = rng.roll_dice(1, build_data.map.height - 3) + 1; 203 | let mut prev_x = digger_x; 204 | let mut prev_y = digger_y; 205 | 206 | let mut path = rltk::line2d( 207 | rltk::LineAlg::Bresenham, 208 | rltk::Point::new(digger_x, digger_y), 209 | rltk::Point::new(starting_position.x, starting_position.y), 210 | ); 211 | 212 | while { 213 | let digger_idx = build_data.map.xy_idx(digger_x, digger_y); 214 | build_data.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() 215 | } { 216 | prev_x = digger_x; 217 | prev_y = digger_y; 218 | digger_x = path[0].x; 219 | digger_y = path[0].y; 220 | path.remove(0); 221 | } 222 | paint( 223 | &mut build_data.map, 224 | self.symmetry, 225 | self.brush_size, 226 | prev_x, 227 | prev_y, 228 | ); 229 | } 230 | } 231 | 232 | build_data.take_snapshot(); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | activate_item, components::*, gamelog::GameLog, Map, RunState, State, TileType, Viewshed, 3 | }; 4 | use legion::prelude::*; 5 | use rltk::{Point, Rltk, VirtualKeyCode}; 6 | use std::cmp::{max, min}; 7 | 8 | pub fn try_move_player(delta_x: i32, delta_y: i32, gs: &mut State) { 9 | let map = gs.resources.get::().unwrap(); 10 | 11 | let mut wants_to_melee = Vec::new(); 12 | let mut open_doors = Vec::new(); 13 | unsafe { 14 | let players_query = <(Write, Write)>::query().filter(tag::()); 15 | for (player_entity, (mut pos, mut viewshed)) in 16 | players_query.iter_entities_unchecked(&gs.world) 17 | { 18 | let dest_x = pos.x + delta_x; 19 | let dest_y = pos.y + delta_y; 20 | if dest_x < 0 || dest_x > map.width - 1 || dest_y < 0 || dest_y > map.height - 1 { 21 | return; 22 | } 23 | let dest_idx = map.xy_idx(dest_x, dest_y); 24 | 25 | let mut recompute_blocked = false; 26 | for potential_target in map.tile_content[dest_idx].iter() { 27 | let bystander = gs.world.get_tag::(*potential_target); 28 | let vendor = gs.world.get_tag::(*potential_target); 29 | if bystander.is_some() || vendor.is_some() { 30 | if let Some(mut target_position) = gs 31 | .world 32 | .get_component_mut_unchecked::(*potential_target) 33 | { 34 | target_position.x = pos.x; 35 | target_position.y = pos.y; 36 | recompute_blocked = true; 37 | } 38 | } else if let Some(_target) = gs.world.get_component::(*potential_target) { 39 | // Store attack target 40 | wants_to_melee.push((player_entity, *potential_target)); 41 | } 42 | 43 | if let Some(_door) = gs.world.get_component::(*potential_target) { 44 | open_doors.push(*potential_target); 45 | viewshed.dirty = true; 46 | } 47 | } 48 | 49 | // FIXME: recompute blocked tiles, but this needs to wait until spatial indexing service 50 | if !map.blocked[dest_idx] || recompute_blocked { 51 | pos.x = min(map.width - 1, max(0, dest_x)); 52 | pos.y = min(map.height - 1, max(0, dest_y)); 53 | 54 | viewshed.dirty = true; 55 | 56 | // Update Player position resource 57 | let mut p_pos = gs.resources.get_mut::().unwrap(); 58 | p_pos.x = pos.x; 59 | p_pos.y = pos.y; 60 | } 61 | } 62 | } 63 | 64 | // Add WantsToMelee component to all stored entities 65 | for (entity, target) in wants_to_melee.iter() { 66 | gs.world 67 | .add_component(*entity, WantsToMelee { target: *target }) 68 | .expect("Add target failed"); 69 | } 70 | 71 | // open doors 72 | for entity in open_doors.iter() { 73 | if let Some(mut door) = gs.world.get_component_mut::(*entity) { 74 | door.open = true; 75 | } 76 | gs.world 77 | .remove_tag::(*entity) 78 | .expect("Cannot remove BlocksVisibility tag"); 79 | gs.world 80 | .remove_tag::(*entity) 81 | .expect("Cannot remove BlocksTile tag"); 82 | if let Some(mut glyph) = gs.world.get_component_mut::(*entity) { 83 | glyph.glyph = rltk::to_cp437('/'); 84 | } 85 | } 86 | } 87 | 88 | pub fn try_next_level(resources: &mut Resources) -> bool { 89 | let player_pos = resources.get::().unwrap(); 90 | let map = resources.get::().unwrap(); 91 | let player_idx = map.xy_idx(player_pos.x, player_pos.y); 92 | if map.tiles[player_idx] == TileType::DownStairs { 93 | true 94 | } else { 95 | let mut gamelog = resources.get_mut::().unwrap(); 96 | gamelog 97 | .entries 98 | .push("There is no way down from here.".to_string()); 99 | false 100 | } 101 | } 102 | 103 | fn get_item(gs: &mut State) { 104 | let player_pos = gs.resources.get::().unwrap(); 105 | let player_entity = gs.resources.get::().unwrap(); 106 | let mut gamelog = gs.resources.get_mut::().unwrap(); 107 | 108 | let mut target_item = None; 109 | let query = Read::::query().filter(tag::()); 110 | for (item_entity, position) in query.iter_entities(&gs.world) { 111 | if position.x == player_pos.x && position.y == player_pos.y { 112 | target_item = Some(item_entity); 113 | } 114 | } 115 | 116 | match target_item { 117 | None => gamelog 118 | .entries 119 | .push("There is nothing here to pick up.".to_string()), 120 | Some(item) => gs 121 | .world 122 | .add_component( 123 | *player_entity, 124 | WantsToPickupItem { 125 | collected_by: *player_entity, 126 | item, 127 | }, 128 | ) 129 | .expect("Unable to insert want to pickup"), 130 | } 131 | } 132 | 133 | fn skip_turn(gs: &mut State) -> RunState { 134 | let player_entity = gs.resources.get::().unwrap(); 135 | let map = gs.resources.get::().unwrap(); 136 | 137 | let mut can_heal = true; 138 | { 139 | let viewshed = gs.world.get_component::(*player_entity).unwrap(); 140 | for tile in viewshed.visible_tiles.iter() { 141 | let idx = map.xy_idx(tile.x, tile.y); 142 | for entity in map.tile_content[idx].iter() { 143 | if let Some(_monster) = gs.world.get_tag::(*entity) { 144 | can_heal = false; 145 | } 146 | } 147 | } 148 | } 149 | 150 | if let Some(hc) = gs.world.get_component::(*player_entity) { 151 | match hc.state { 152 | HungerState::Hungry | HungerState::Starving => can_heal = false, 153 | _ => {} 154 | } 155 | } 156 | 157 | if can_heal { 158 | if let Some(mut player_stats) = gs.world.get_component_mut::(*player_entity) { 159 | player_stats.hit_points.current = i32::min( 160 | player_stats.hit_points.current + 1, 161 | player_stats.hit_points.max, 162 | ); 163 | } 164 | } 165 | 166 | RunState::PlayerTurn 167 | } 168 | 169 | fn use_consumable_hotkey(gs: &mut State, key: i32) -> RunState { 170 | let mut carried_consumables = Vec::new(); 171 | let player = gs.resources.get::().unwrap(); 172 | // FIXME: this has to be the same query as in gui.rs - this may become nondeterministic! 173 | let query = <(Read, Read)>::query().filter(tag::()); 174 | for (entity, (carried_by, _item_name)) in query.iter_entities(&gs.world) { 175 | if carried_by.owner == *player { 176 | carried_consumables.push(entity); 177 | } 178 | } 179 | 180 | if (key as usize) < carried_consumables.len() { 181 | return activate_item( 182 | &mut gs.world, 183 | &gs.resources, 184 | carried_consumables[key as usize], 185 | ); 186 | } 187 | 188 | RunState::PlayerTurn 189 | } 190 | 191 | pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { 192 | // Hotkeys 193 | if ctx.shift && ctx.key.is_some() { 194 | let key = match ctx.key.unwrap() { 195 | VirtualKeyCode::Key1 => Some(1), 196 | VirtualKeyCode::Key2 => Some(2), 197 | VirtualKeyCode::Key3 => Some(3), 198 | VirtualKeyCode::Key4 => Some(4), 199 | VirtualKeyCode::Key5 => Some(5), 200 | VirtualKeyCode::Key6 => Some(6), 201 | VirtualKeyCode::Key7 => Some(7), 202 | VirtualKeyCode::Key8 => Some(8), 203 | VirtualKeyCode::Key9 => Some(9), 204 | _ => None, 205 | }; 206 | if let Some(key) = key { 207 | return use_consumable_hotkey(gs, key - 1); 208 | } 209 | } 210 | 211 | // Player movement 212 | match ctx.key { 213 | None => return RunState::AwaitingInput, // Nothing happened 214 | Some(key) => match key { 215 | VirtualKeyCode::Left | VirtualKeyCode::Numpad4 | VirtualKeyCode::H => { 216 | try_move_player(-1, 0, gs) 217 | } 218 | VirtualKeyCode::Right | VirtualKeyCode::Numpad6 | VirtualKeyCode::L => { 219 | try_move_player(1, 0, gs) 220 | } 221 | VirtualKeyCode::Up | VirtualKeyCode::Numpad8 | VirtualKeyCode::K => { 222 | try_move_player(0, -1, gs) 223 | } 224 | VirtualKeyCode::Down | VirtualKeyCode::Numpad2 | VirtualKeyCode::J => { 225 | try_move_player(0, 1, gs) 226 | } 227 | // Diagonals 228 | VirtualKeyCode::Numpad9 | VirtualKeyCode::Y => try_move_player(1, -1, gs), 229 | VirtualKeyCode::Numpad7 | VirtualKeyCode::U => try_move_player(-1, -1, gs), 230 | VirtualKeyCode::Numpad3 | VirtualKeyCode::N => try_move_player(1, 1, gs), 231 | VirtualKeyCode::Numpad1 | VirtualKeyCode::B => try_move_player(-1, 1, gs), 232 | 233 | VirtualKeyCode::G => get_item(gs), 234 | VirtualKeyCode::I => return RunState::ShowInventory, 235 | VirtualKeyCode::D => return RunState::ShowDropItem, 236 | VirtualKeyCode::R => return RunState::ShowRemoveItem, 237 | 238 | VirtualKeyCode::Escape => return RunState::SaveGame, 239 | 240 | // Level changes 241 | VirtualKeyCode::Period => { 242 | if try_next_level(&mut gs.resources) { 243 | return RunState::NextLevel; 244 | } 245 | } 246 | 247 | // Skip turn 248 | VirtualKeyCode::Numpad5 | VirtualKeyCode::Space => return skip_turn(gs), 249 | 250 | _ => return RunState::AwaitingInput, 251 | }, 252 | } 253 | RunState::PlayerTurn 254 | } 255 | -------------------------------------------------------------------------------- /src/components.rs: -------------------------------------------------------------------------------- 1 | use legion::prelude::*; 2 | use rltk::{FontCharType, Point, RGB}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use type_uuid::TypeUuid; 6 | 7 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 8 | #[uuid = "f4e159d6-63de-4a3c-a21b-63f8f2bd19c9"] 9 | pub struct Position { 10 | pub x: i32, 11 | pub y: i32, 12 | } 13 | 14 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 15 | #[uuid = "f24fa790-5156-4d0b-bf36-10421caee6d9"] 16 | pub struct Renderable { 17 | pub glyph: FontCharType, 18 | pub fg: RGB, 19 | pub bg: RGB, 20 | pub render_order: i32, 21 | } 22 | 23 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 24 | #[uuid = "70d29e4c-8cd9-40a3-8329-b95124bc53f2"] 25 | pub struct Player; 26 | 27 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 28 | #[uuid = "cd7ba29f-2434-4c7f-8d00-ac1370b2c287"] 29 | pub struct Viewshed { 30 | pub visible_tiles: Vec, 31 | pub range: i32, 32 | pub dirty: bool, 33 | } 34 | 35 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 36 | #[uuid = "b2f34c7c-63fa-4e6d-96a2-bbbeca8bedac"] 37 | pub struct Monster; 38 | 39 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 40 | #[uuid = "886ff52d-3052-467e-aa9a-2f628a463e86"] 41 | pub struct Name { 42 | pub name: String, 43 | } 44 | 45 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 46 | #[uuid = "8b2e566c-2e72-48b0-954b-dffb83051683"] 47 | pub struct BlocksTile; 48 | 49 | #[derive(Clone, Copy, Debug, PartialEq)] 50 | pub struct WantsToMelee { 51 | pub target: Entity, 52 | } 53 | 54 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 55 | #[uuid = "56a6359c-d947-438a-a13f-dfe65174cb6d"] 56 | pub struct SufferDamage { 57 | pub amount: Vec<(i32, bool)>, 58 | } 59 | 60 | impl SufferDamage { 61 | pub fn new_damage( 62 | command_buffer: &CommandBuffer, 63 | victim: Entity, 64 | amount: i32, 65 | from_player: bool, 66 | ) { 67 | command_buffer.exec_mut(move |world| { 68 | let mut dmg = if let Some(suffering) = world.get_component::(victim) { 69 | (*suffering).clone() 70 | } else { 71 | SufferDamage { amount: Vec::new() } 72 | }; 73 | 74 | dmg.amount.push((amount, from_player)); 75 | world 76 | .add_component(victim, dmg) 77 | .expect("Unable to insert damage"); 78 | }); 79 | } 80 | } 81 | 82 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 83 | #[uuid = "d9a3d242-2918-4241-9342-4d49a6e54f7c"] 84 | pub struct Item; 85 | 86 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 87 | #[uuid = "e878ef86-1af2-426f-abf5-49e810f7061e"] 88 | pub struct Consumable; 89 | 90 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq)] 91 | #[uuid = "98a23186-8084-40fb-938e-f0fa6b983286"] 92 | pub struct InBackpack { 93 | pub owner: Entity, 94 | } 95 | 96 | #[derive(Clone, Copy, Debug, PartialEq)] 97 | pub struct WantsToPickupItem { 98 | pub collected_by: Entity, 99 | pub item: Entity, 100 | } 101 | 102 | #[derive(Clone, Copy, Debug, PartialEq)] 103 | pub struct WantsToUseItem { 104 | pub item: Entity, 105 | pub target: Option, 106 | } 107 | 108 | #[derive(Clone, Copy, Debug, PartialEq)] 109 | pub struct WantsToDropItem { 110 | pub item: Entity, 111 | } 112 | 113 | #[derive(Clone, Copy, Debug, PartialEq)] 114 | pub struct WantsToRemoveItem { 115 | pub item: Entity, 116 | } 117 | 118 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 119 | #[uuid = "fde630bf-14fc-46e6-8cd9-36a2cbb0734a"] 120 | pub struct ProvidesHealing { 121 | pub heal_amount: i32, 122 | } 123 | 124 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 125 | #[uuid = "84480f6e-697a-4cd6-8d34-3aa0c7116d05"] 126 | pub struct Ranged { 127 | pub range: i32, 128 | } 129 | 130 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 131 | #[uuid = "707b602d-12ed-4f49-8f1a-94adea423f71"] 132 | pub struct InflictsDamage { 133 | pub damage: i32, 134 | } 135 | 136 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 137 | #[uuid = "d55f9594-4993-43d6-b536-c4228189abf7"] 138 | pub struct AreaOfEffect { 139 | pub radius: i32, 140 | } 141 | 142 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 143 | #[uuid = "aa447dea-909f-4a99-81fa-412ec6f5317c"] 144 | pub struct Confusion { 145 | pub turns: i32, 146 | } 147 | 148 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 149 | pub enum EquipmentSlot { 150 | Melee, 151 | Shield, 152 | Head, 153 | Torso, 154 | Legs, 155 | Feet, 156 | Hands, 157 | } 158 | 159 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 160 | #[uuid = "4239ace9-a158-4802-b797-591953c39ef3"] 161 | pub struct Equippable { 162 | pub slot: EquipmentSlot, 163 | } 164 | 165 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq)] 166 | #[uuid = "9aa18630-5131-45a7-a6b8-3878c4e25973"] 167 | pub struct Equipped { 168 | pub owner: Entity, 169 | pub slot: EquipmentSlot, 170 | } 171 | 172 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 173 | #[uuid = "667de082-404a-438d-8d8b-2c4f217a1017"] 174 | pub struct ParticleLifetime { 175 | pub lifetime_ms: f32, 176 | } 177 | 178 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 179 | pub enum HungerState { 180 | WellFed, 181 | Normal, 182 | Hungry, 183 | Starving, 184 | } 185 | 186 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 187 | #[uuid = "7242e30f-b971-4ad0-bcae-cc8ad67a5852"] 188 | pub struct HungerClock { 189 | pub state: HungerState, 190 | pub duration: i32, 191 | } 192 | 193 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 194 | #[uuid = "0457c4c2-e26a-4d05-a4cb-d4322a41e876"] 195 | pub struct ProvidesFood; 196 | 197 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 198 | #[uuid = "74ea7770-fd58-43ce-a0b5-8ef6f8610d48"] 199 | pub struct MagicMapper; 200 | 201 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 202 | #[uuid = "8e5c82a0-f62a-46f8-95ea-b2531da310b1"] 203 | pub struct Hidden; 204 | 205 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 206 | #[uuid = "1f0408cb-f7f2-45aa-b2d6-0aac7bae3a7d"] 207 | pub struct EntryTrigger; 208 | 209 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 210 | #[uuid = "bfd72dfa-446e-49bc-99ab-259eb6e0cbf3"] 211 | pub struct SingleActivation; 212 | 213 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 214 | #[uuid = "4e87fd9c-ce64-4464-bf0f-ea1bca2d3827"] 215 | pub struct BlocksVisibility; 216 | 217 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 218 | #[uuid = "9998e1d1-bc60-4ebc-8edd-cf7b112df34b"] 219 | pub struct Door { 220 | pub open: bool, 221 | } 222 | 223 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 224 | #[uuid = "8937d655-3173-4646-9ff2-de4cce96285f"] 225 | pub struct Bystander; 226 | 227 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 228 | #[uuid = "401102d1-3cbb-451f-8989-c5b9aa7539bb"] 229 | pub struct Vendor; 230 | 231 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 232 | #[uuid = "a15abace-8292-4203-88e8-c2ba0093e789"] 233 | pub struct Quips { 234 | pub available: Vec, 235 | } 236 | 237 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 238 | pub struct Attribute { 239 | pub base: i32, 240 | pub modifiers: i32, 241 | pub bonus: i32, 242 | } 243 | 244 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 245 | #[uuid = "614c79e7-c29f-4f46-9ed8-1dd2979ffc34"] 246 | pub struct Attributes { 247 | pub might: Attribute, 248 | pub fitness: Attribute, 249 | pub quickness: Attribute, 250 | pub intelligence: Attribute, 251 | } 252 | 253 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 254 | pub enum Skill { 255 | Melee, 256 | Defense, 257 | Magic, 258 | } 259 | 260 | #[derive(TypeUuid, Clone, Debug, Serialize, Deserialize)] 261 | #[uuid = "28e5dc44-b610-4152-a3be-ce4e466f94a5"] 262 | pub struct Skills { 263 | pub skills: HashMap, 264 | } 265 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 266 | pub struct Pool { 267 | pub max: i32, 268 | pub current: i32, 269 | } 270 | 271 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 272 | #[uuid = "f2f8a991-a90c-46e0-b986-25da16ff384e"] 273 | pub struct Pools { 274 | pub hit_points: Pool, 275 | pub mana: Pool, 276 | pub experience: i32, 277 | pub level: i32, 278 | } 279 | 280 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 281 | pub enum WeaponAttribute { 282 | Might, 283 | Quickness, 284 | } 285 | 286 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 287 | #[uuid = "43a4e3d5-dc45-465a-9c42-4672dfca6c16"] 288 | pub struct MeleeWeapon { 289 | pub attribute: WeaponAttribute, 290 | pub damage_n_dice: i32, 291 | pub damage_die_type: i32, 292 | pub damage_bonus: i32, 293 | pub hit_bonus: i32, 294 | } 295 | 296 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 297 | #[uuid = "64f8327b-24cc-409e-8567-aa73ac9923ce"] 298 | pub struct Wearable { 299 | pub armor_class: f32, 300 | pub slot: EquipmentSlot, 301 | } 302 | 303 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 304 | pub struct NaturalAttack { 305 | pub name: String, 306 | pub damage_n_dice: i32, 307 | pub damage_die_type: i32, 308 | pub damage_bonus: i32, 309 | pub hit_bonus: i32, 310 | } 311 | 312 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 313 | #[uuid = "3ad801a0-461e-4af3-81c5-3925eba81a6f"] 314 | pub struct NaturalAttackDefense { 315 | pub armor_class: Option, 316 | pub attacks: Vec, 317 | } 318 | 319 | #[derive(TypeUuid, Clone, Debug, PartialEq, Serialize, Deserialize)] 320 | #[uuid = "4c71d15e-1263-4505-ab49-e4e18abbac22"] 321 | pub struct LootTable { 322 | pub table: String, 323 | } 324 | 325 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 326 | #[uuid = "1da8957d-829f-4155-b111-339369eaebf4"] 327 | pub struct Carnivore; 328 | 329 | #[derive(TypeUuid, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 330 | #[uuid = "53e61565-1f50-4559-980b-fffe8d9406af"] 331 | pub struct Herbivore; 332 | --------------------------------------------------------------------------------