Browse Source

Chapter 32 - voronoi placeholder.

Herbert Wolverson 4 years ago
parent
commit
c5dbd1d6d2
34 changed files with 4522 additions and 2 deletions
  1. 13 0
      Cargo.lock
  2. 2 1
      Cargo.toml
  3. 1 1
      book/src/SUMMARY.md
  4. 23 0
      book/src/chapter_32.md
  5. 18 0
      chapter-32-voronoi/Cargo.toml
  6. 392 0
      chapter-32-voronoi/src/components.rs
  7. 62 0
      chapter-32-voronoi/src/damage_system.rs
  8. 3 0
      chapter-32-voronoi/src/gamelog.rs
  9. 371 0
      chapter-32-voronoi/src/gui.rs
  10. 74 0
      chapter-32-voronoi/src/hunger_system.rs
  11. 310 0
      chapter-32-voronoi/src/inventory_system.rs
  12. 474 0
      chapter-32-voronoi/src/main.rs
  13. 187 0
      chapter-32-voronoi/src/map.rs
  14. 172 0
      chapter-32-voronoi/src/map_builders/bsp_dungeon.rs
  15. 143 0
      chapter-32-voronoi/src/map_builders/bsp_interior.rs
  16. 126 0
      chapter-32-voronoi/src/map_builders/cellular_automota.rs
  17. 168 0
      chapter-32-voronoi/src/map_builders/common.rs
  18. 229 0
      chapter-32-voronoi/src/map_builders/dla.rs
  19. 238 0
      chapter-32-voronoi/src/map_builders/drunkard.rs
  20. 257 0
      chapter-32-voronoi/src/map_builders/maze.rs
  21. 49 0
      chapter-32-voronoi/src/map_builders/mod.rs
  22. 105 0
      chapter-32-voronoi/src/map_builders/simple_map.rs
  23. 32 0
      chapter-32-voronoi/src/map_indexing_system.rs
  24. 74 0
      chapter-32-voronoi/src/melee_combat_system.rs
  25. 73 0
      chapter-32-voronoi/src/monster_ai_system.rs
  26. 73 0
      chapter-32-voronoi/src/particle_system.rs
  27. 177 0
      chapter-32-voronoi/src/player.rs
  28. 49 0
      chapter-32-voronoi/src/random_table.rs
  29. 24 0
      chapter-32-voronoi/src/rect.rs
  30. 18 0
      chapter-32-voronoi/src/rex_assets.rs
  31. 126 0
      chapter-32-voronoi/src/saveload_system.rs
  32. 330 0
      chapter-32-voronoi/src/spawner.rs
  33. 72 0
      chapter-32-voronoi/src/trigger_system.rs
  34. 57 0
      chapter-32-voronoi/src/visibility_system.rs

+ 13 - 0
Cargo.lock

@@ -523,6 +523,19 @@ dependencies = [
  "web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "chapter-32-voronoi"
+version = "0.1.0"
+dependencies = [
+ "rltk 0.4.1 (git+https://github.com/thebracket/rltk_rs)",
+ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
+ "specs 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "specs-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
+ "web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "cloudabi"
 version = "0.0.3"

+ 2 - 1
Cargo.toml

@@ -46,7 +46,8 @@ members = [
     "chapter-28-drunkards-walk",
     "chapter-29-mazes",
     "chapter-30-dla",
-    "chapter-31-symmetry"
+    "chapter-31-symmetry",
+    "chapter-32-voronoi"
     ]
 
 [profile.dev]

+ 1 - 1
book/src/SUMMARY.md

@@ -34,7 +34,7 @@
     - [Mazes and Labyrinths](./chapter_29.md)
     - [Diffusion-limited aggregation maps](./chapter_30.md)
     - [Add symmetry and brushes to the library](./chapter_31.md)
-    - Voronoi
+    - [Voronoi Hive Maps](./chapter_32.md)
     - Noise
     - Quantum Waveform Collapse
 [Contributors](./contributors.md)

+ 23 - 0
book/src/chapter_32.md

@@ -0,0 +1,23 @@
+# Voronoi Hive/Cell Maps
+
+---
+
+***About this tutorial***
+
+*This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!*
+
+*If you enjoy this and would like me to keep writing, please consider supporting [my Patreon](https://www.patreon.com/blackfuture).*
+
+---
+
+
+
+**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-32-voronoi)**
+
+
+[Run this chapter's example with web assembly, in your browser (WebGL2 required)](http://bfnightly.bracketproductions.com/rustbook/wasm/chapter-32-voronoi/)
+---
+
+Copyright (C) 2019, Herbert Wolverson.
+
+---

+ 18 - 0
chapter-32-voronoi/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "chapter-32-voronoi"
+version = "0.1.0"
+authors = ["Herbert Wolverson <herberticus@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+rltk = { git = "https://github.com/thebracket/rltk_rs", features = ["serialization"] }
+specs = { version = "0.15.0", features = ["serde"] }
+specs-derive = "0.4.0"
+serde= { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+
+[target.'cfg(any(target_arch = "wasm32"))'.dependencies]
+web-sys = { version = "0.3", features=["console"] }
+wasm-bindgen = "0.2"

+ 392 - 0
chapter-32-voronoi/src/components.rs

@@ -0,0 +1,392 @@
+extern crate specs;
+use specs::prelude::*;
+extern crate specs_derive;
+extern crate rltk;
+use rltk::{RGB};
+use serde::{Serialize, Deserialize};
+use specs::saveload::{Marker, ConvertSaveload};
+use specs::error::NoError;
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct Position {
+    pub x: i32,
+    pub y: i32,
+}
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct Renderable {
+    pub glyph: u8,
+    pub fg: RGB,
+    pub bg: RGB,
+    pub render_order : i32
+}
+ 
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Player {}
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct Viewshed {
+    pub visible_tiles : Vec<rltk::Point>,
+    pub range : i32,
+    pub dirty : bool
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Monster {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Name {
+    pub name : String
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct BlocksTile {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct CombatStats {
+    pub max_hp : i32,
+    pub hp : i32,
+    pub defense : i32,
+    pub power : i32
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct WantsToMelee {
+    pub target : Entity
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct SufferDamage {
+    pub amount : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Item {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Consumable {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Ranged {
+    pub range : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct InflictsDamage {
+    pub damage : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct AreaOfEffect {
+    pub radius : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Confusion {
+    pub turns : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct ProvidesHealing {
+    pub heal_amount : i32
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct InBackpack {
+    pub owner : Entity
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct WantsToPickupItem {
+    pub collected_by : Entity,
+    pub item : Entity
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct WantsToUseItem {
+    pub item : Entity,
+    pub target : Option<rltk::Point>
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct WantsToDropItem {
+    pub item : Entity
+}
+
+// See wrapper below for serialization
+#[derive(Component, Debug)]
+pub struct WantsToRemoveItem {
+    pub item : Entity
+}
+
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
+pub enum EquipmentSlot { Melee, Shield }
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct Equippable {
+    pub slot : EquipmentSlot
+}
+
+// See wrapper below for serialization
+#[derive(Component)]
+pub struct Equipped {
+    pub owner : Entity,
+    pub slot : EquipmentSlot
+}
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct MeleePowerBonus {
+    pub power : i32
+}
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct DefenseBonus {
+    pub defense : i32
+}
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct ParticleLifetime {
+    pub lifetime_ms : f32
+}
+
+#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
+pub enum HungerState { WellFed, Normal, Hungry, Starving }
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct HungerClock {
+    pub state : HungerState,
+    pub duration : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct ProvidesFood {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct MagicMapper {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct Hidden {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct EntryTrigger {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct EntityMoved {}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct SingleActivation {}
+
+// Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an
+// Entity.
+
+pub struct SerializeMe;
+
+// Special component that exists to help serialize the game data
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct SerializationHelper {
+    pub map : super::map::Map
+}
+
+// WantsToMelee wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct WantsToMeleeData<M>(M);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for WantsToMelee
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = WantsToMeleeData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.target).unwrap();
+        Ok(WantsToMeleeData(marker))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let entity = ids(data.0).unwrap();
+        Ok(WantsToMelee{target: entity})
+    }
+}
+
+// InBackpack wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct InBackpackData<M>(M);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for InBackpack
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = InBackpackData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.owner).unwrap();
+        Ok(InBackpackData(marker))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let entity = ids(data.0).unwrap();
+        Ok(InBackpack{owner: entity})
+    }
+}
+
+// WantsToPickupItem wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct WantsToPickupItemData<M>(M, M);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for WantsToPickupItem
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = WantsToPickupItemData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.collected_by).unwrap();
+        let marker2 = ids(self.item).unwrap();
+        Ok(WantsToPickupItemData(marker, marker2))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let collected_by = ids(data.0).unwrap();
+        let item = ids(data.1).unwrap();
+        Ok(WantsToPickupItem{collected_by, item})
+    }
+}
+
+// WantsToUseItem wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct WantsToUseItemData<M>(M, Option<rltk::Point>);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for WantsToUseItem
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = WantsToUseItemData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.item).unwrap();
+        Ok(WantsToUseItemData(marker, self.target))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let item = ids(data.0).unwrap();
+        let target = data.1;
+        Ok(WantsToUseItem{item, target})
+    }
+}
+
+// WantsToDropItem wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct WantsToDropItemData<M>(M);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for WantsToDropItem
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = WantsToDropItemData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.item).unwrap();
+        Ok(WantsToDropItemData(marker))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let entity = ids(data.0).unwrap();
+        Ok(WantsToDropItem{item: entity})
+    }
+}
+
+// WantsToRemoveItem wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct WantsToRemoveItemData<M>(M);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for WantsToRemoveItem
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = WantsToRemoveItemData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.item).unwrap();
+        Ok(WantsToRemoveItemData(marker))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let entity = ids(data.0).unwrap();
+        Ok(WantsToRemoveItem{item: entity})
+    }
+}
+
+// Equipped wrapper
+#[derive(Serialize, Deserialize, Clone)]
+pub struct EquippedData<M>(M, EquipmentSlot);
+
+impl<M: Marker + Serialize> ConvertSaveload<M> for Equipped
+where
+    for<'de> M: Deserialize<'de>,
+{
+    type Data = EquippedData<M>;
+    type Error = NoError;
+
+    fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
+    where
+        F: FnMut(Entity) -> Option<M>,
+    {
+        let marker = ids(self.owner).unwrap();
+        Ok(EquippedData(marker, self.slot))
+    }
+
+    fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
+    where
+        F: FnMut(M) -> Option<Entity>,
+    {
+        let entity = ids(data.0).unwrap();
+        Ok(Equipped{owner: entity, slot : data.1})
+    }
+}

+ 62 - 0
chapter-32-voronoi/src/damage_system.rs

@@ -0,0 +1,62 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, SufferDamage, Player, Name, gamelog::GameLog, RunState, Position, Map};
+
+pub struct DamageSystem {}
+
+impl<'a> System<'a> for DamageSystem {
+    type SystemData = ( WriteStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, Position>,
+                        WriteExpect<'a, Map>,
+                        Entities<'a> );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut stats, mut damage, positions, mut map, entities) = data;
+
+        for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() {
+            stats.hp -= damage.amount;
+            let pos = positions.get(entity);
+            if let Some(pos) = pos {
+                let idx = map.xy_idx(pos.x, pos.y);
+                map.bloodstains.insert(idx);
+            }
+        }
+
+        damage.clear();
+    }
+}
+
+pub fn delete_the_dead(ecs : &mut World) {
+    let mut dead : Vec<Entity> = Vec::new();
+    // Using a scope to make the borrow checker happy
+    {
+        let combat_stats = ecs.read_storage::<CombatStats>();
+        let players = ecs.read_storage::<Player>();
+        let names = ecs.read_storage::<Name>();
+        let entities = ecs.entities();
+        let mut log = ecs.write_resource::<GameLog>();
+        for (entity, stats) in (&entities, &combat_stats).join() {
+            if stats.hp < 1 { 
+                let player = players.get(entity);
+                match player {
+                    None => {
+                        let victim_name = names.get(entity);
+                        if let Some(victim_name) = victim_name {
+                            log.entries.insert(0, format!("{} is dead", &victim_name.name));
+                        }
+                        dead.push(entity)
+                    }
+                    Some(_) => {
+                        let mut runstate = ecs.write_resource::<RunState>();
+                        *runstate = RunState::GameOver;
+                    }
+                }
+            }
+        }
+    }
+
+    for victim in dead {
+        ecs.delete_entity(victim).expect("Unable to delete");
+    }    
+}

+ 3 - 0
chapter-32-voronoi/src/gamelog.rs

@@ -0,0 +1,3 @@
+pub struct GameLog {
+    pub entries : Vec<String>
+}

+ 371 - 0
chapter-32-voronoi/src/gui.rs

@@ -0,0 +1,371 @@
+extern crate rltk;
+use rltk::{ RGB, Rltk, Console, Point, VirtualKeyCode };
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, Player, gamelog::GameLog, Map, Name, Position, State, InBackpack, 
+    Viewshed, RunState, Equipped, HungerClock, HungerState, rex_assets::RexAssets,
+    Hidden };
+
+pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
+    ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+
+    let combat_stats = ecs.read_storage::<CombatStats>();
+    let players = ecs.read_storage::<Player>();
+    let hunger = ecs.read_storage::<HungerClock>();
+    for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() {
+        let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
+        ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
+
+        ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));
+
+        match hc.state {
+            HungerState::WellFed => ctx.print_color(71, 42, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"),
+            HungerState::Normal => {}
+            HungerState::Hungry => ctx.print_color(71, 42, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"),
+            HungerState::Starving => ctx.print_color(71, 42, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"),
+        }
+    }
+
+    let map = ecs.fetch::<Map>();
+    let depth = format!("Depth: {}", map.depth);
+    ctx.print_color(2, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &depth);
+
+
+    let log = ecs.fetch::<GameLog>();
+    let mut y = 44;
+    for s in log.entries.iter() {
+        if y < 49 { ctx.print(2, y, &s.to_string()); }
+        y += 1;
+    }
+
+    // Draw mouse cursor
+    let mouse_pos = ctx.mouse_pos();
+    ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::MAGENTA));
+    draw_tooltips(ecs, ctx);
+}
+
+fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {
+    let map = ecs.fetch::<Map>();
+    let names = ecs.read_storage::<Name>();
+    let positions = ecs.read_storage::<Position>();
+    let hidden = ecs.read_storage::<Hidden>();
+
+    let mouse_pos = ctx.mouse_pos();
+    if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; }
+    let mut tooltip : Vec<String> = Vec::new();
+    for (name, position, _hidden) in (&names, &positions, !&hidden).join() {
+        if position.x == mouse_pos.0 && position.y == mouse_pos.1 {
+            tooltip.push(name.name.to_string());
+        }
+    }
+
+    if !tooltip.is_empty() {
+        let mut width :i32 = 0;
+        for s in tooltip.iter() {
+            if width < s.len() as i32 { width = s.len() as i32; }
+        }
+        width += 3;
+
+        if mouse_pos.0 > 40 {
+            let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1);
+            let left_x = mouse_pos.0 - width;
+            let mut y = mouse_pos.1;
+            for s in tooltip.iter() {
+                ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &s.to_string());
+                let padding = (width - s.len() as i32)-1;
+                for i in 0..padding {
+                    ctx.print_color(arrow_pos.x - i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+                }
+                y += 1;
+            }
+            ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"->".to_string());
+        } else {
+            let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1);
+            let left_x = mouse_pos.0 +3;
+            let mut y = mouse_pos.1;
+            for s in tooltip.iter() {
+                ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &s.to_string());
+                let padding = (width - s.len() as i32)-1;
+                for i in 0..padding {
+                    ctx.print_color(left_x + s.len() as i32 + i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+                }
+                y += 1;
+            }
+            ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"<-".to_string());
+        }
+    }
+}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum ItemMenuResult { Cancel, NoResponse, Selected }
+
+pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let names = gs.ecs.read_storage::<Name>();
+    let backpack = gs.ecs.read_storage::<InBackpack>();
+    let entities = gs.ecs.entities();
+
+    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+    let count = inventory.count();
+
+    let mut y = (25 - (count / 2)) as i32;
+    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Inventory");
+    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+    let mut equippable : Vec<Entity> = Vec::new();
+    let mut j = 0;
+    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+        ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+        ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as u8);
+        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+        ctx.print(21, y, &name.name.to_string());
+        equippable.push(entity);
+        y += 1;
+        j += 1;
+    }
+
+    match ctx.key {
+        None => (ItemMenuResult::NoResponse, None),
+        Some(key) => {
+            match key {
+                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+                _ => { 
+                    let selection = rltk::letter_to_option(key);
+                    if selection > -1 && selection < count as i32 {
+                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+                    }  
+                    (ItemMenuResult::NoResponse, None)
+                }
+            }
+        }
+    }
+}
+
+pub fn drop_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let names = gs.ecs.read_storage::<Name>();
+    let backpack = gs.ecs.read_storage::<InBackpack>();
+    let entities = gs.ecs.entities();
+
+    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+    let count = inventory.count();
+
+    let mut y = (25 - (count / 2)) as i32;
+    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Drop Which Item?");
+    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+    let mut equippable : Vec<Entity> = Vec::new();
+    let mut j = 0;
+    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+        ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+        ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as u8);
+        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+        ctx.print(21, y, &name.name.to_string());
+        equippable.push(entity);
+        y += 1;
+        j += 1;
+    }
+
+    match ctx.key {
+        None => (ItemMenuResult::NoResponse, None),
+        Some(key) => {
+            match key {
+                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+                _ => { 
+                    let selection = rltk::letter_to_option(key);
+                    if selection > -1 && selection < count as i32 {
+                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+                    }  
+                    (ItemMenuResult::NoResponse, None)
+                }
+            }
+        }
+    }
+}
+
+pub fn remove_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let names = gs.ecs.read_storage::<Name>();
+    let backpack = gs.ecs.read_storage::<Equipped>();
+    let entities = gs.ecs.entities();
+
+    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+    let count = inventory.count();
+
+    let mut y = (25 - (count / 2)) as i32;
+    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Remove Which Item?");
+    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+    let mut equippable : Vec<Entity> = Vec::new();
+    let mut j = 0;
+    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+        ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+        ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as u8);
+        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+        ctx.print(21, y, &name.name.to_string());
+        equippable.push(entity);
+        y += 1;
+        j += 1;
+    }
+
+    match ctx.key {
+        None => (ItemMenuResult::NoResponse, None),
+        Some(key) => {
+            match key {
+                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+                _ => { 
+                    let selection = rltk::letter_to_option(key);
+                    if selection > -1 && selection < count as i32 {
+                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+                    }  
+                    (ItemMenuResult::NoResponse, None)
+                }
+            }
+        }
+    }
+}
+
+pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option<Point>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let player_pos = gs.ecs.fetch::<Point>();
+    let viewsheds = gs.ecs.read_storage::<Viewshed>();
+
+    ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Select Target:");
+
+    // Highlight available target cells
+    let mut available_cells = Vec::new();
+    let visible = viewsheds.get(*player_entity);
+    if let Some(visible) = visible {
+        // We have a viewshed
+        for idx in visible.visible_tiles.iter() {
+            let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx);
+            if distance <= range as f32 {
+                ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE));
+                available_cells.push(idx);
+            }
+        }
+    } else {
+        return (ItemMenuResult::Cancel, None);
+    }
+
+    // Draw mouse cursor
+    let mouse_pos = ctx.mouse_pos();
+    let mut valid_target = false;
+    for idx in available_cells.iter() { if idx.x == mouse_pos.0 && idx.y == mouse_pos.1 { valid_target = true; } }
+    if valid_target {
+        ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN));
+        if ctx.left_click {
+            return (ItemMenuResult::Selected, Some(Point::new(mouse_pos.0, mouse_pos.1)));
+        }
+    } else {
+        ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED));
+        if ctx.left_click {
+            return (ItemMenuResult::Cancel, None);
+        }
+    }
+
+    (ItemMenuResult::NoResponse, None)
+}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum MainMenuSelection { NewGame, LoadGame, Quit }
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum MainMenuResult { NoSelection{ selected : MainMenuSelection }, Selected{ selected: MainMenuSelection } }
+
+pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
+    let save_exists = super::saveload_system::does_save_exist();
+    let runstate = gs.ecs.fetch::<RunState>();
+    let assets = gs.ecs.fetch::<RexAssets>();
+    ctx.render_xp_sprite(&assets.menu, 0, 0);
+
+    ctx.draw_box_double(24, 18, 31, 10, RGB::named(rltk::WHEAT), RGB::named(rltk::BLACK));
+
+    ctx.print_color_centered(20, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");
+    ctx.print_color_centered(21, RGB::named(rltk::CYAN), RGB::named(rltk::BLACK), "by Herbert Wolverson");
+    ctx.print_color_centered(22, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK), "Use Up/Down Arrows and Enter");
+    
+    let mut y = 24;
+    if let RunState::MainMenu{ menu_selection : selection } = *runstate {
+        if selection == MainMenuSelection::NewGame {
+            ctx.print_color_centered(y, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Begin New Game");
+        } else {
+            ctx.print_color_centered(y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Begin New Game");
+        }
+        y += 1;
+
+        if save_exists {
+            if selection == MainMenuSelection::LoadGame {
+                ctx.print_color_centered(y, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Load Game");
+            } else {
+                ctx.print_color_centered(y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Load Game");
+            }
+            y += 1;
+        }
+
+        if selection == MainMenuSelection::Quit {
+            ctx.print_color_centered(y, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Quit");
+        } else {
+            ctx.print_color_centered(y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Quit");
+        }
+
+        match ctx.key {
+            None => return MainMenuResult::NoSelection{ selected: selection },
+            Some(key) => {
+                match key {
+                    VirtualKeyCode::Escape => { return MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
+                    VirtualKeyCode::Up => {
+                        let mut newselection;
+                        match selection {
+                            MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit,
+                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame,
+                            MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame
+                        }
+                        if newselection == MainMenuSelection::LoadGame && !save_exists {
+                            newselection = MainMenuSelection::NewGame;
+                        }
+                        return MainMenuResult::NoSelection{ selected: newselection }
+                    }
+                    VirtualKeyCode::Down => {
+                        let mut newselection;
+                        match selection {
+                            MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame,
+                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit,
+                            MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame
+                        }
+                        if newselection == MainMenuSelection::LoadGame && !save_exists {
+                            newselection = MainMenuSelection::Quit;
+                        }
+                        return MainMenuResult::NoSelection{ selected: newselection }
+                    }
+                    VirtualKeyCode::Return => return MainMenuResult::Selected{ selected : selection },
+                    _ => return MainMenuResult::NoSelection{ selected: selection }
+                }
+            }
+        }
+    }
+
+    MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }
+}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum GameOverResult { NoSelection, QuitToMenu }
+
+pub fn game_over(ctx : &mut Rltk) -> GameOverResult {
+    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Your journey has ended!");
+    ctx.print_color_centered(17, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "One day, we'll tell you all about how you did.");
+    ctx.print_color_centered(18, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter..");
+
+    ctx.print_color_centered(20, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Press any key to return to the menu.");
+
+    match ctx.key {
+        None => GameOverResult::NoSelection,
+        Some(_) => GameOverResult::QuitToMenu
+    }
+}

+ 74 - 0
chapter-32-voronoi/src/hunger_system.rs

@@ -0,0 +1,74 @@
+use specs::prelude::*;
+use super::{HungerClock, RunState, HungerState, SufferDamage, gamelog::GameLog};
+
+pub struct HungerSystem {}
+
+impl<'a> System<'a> for HungerSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( 
+                        Entities<'a>,
+                        WriteStorage<'a, HungerClock>,
+                        ReadExpect<'a, Entity>, // The player
+                        ReadExpect<'a, RunState>,
+                        WriteStorage<'a, SufferDamage>,
+                        WriteExpect<'a, GameLog>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut hunger_clock, player_entity, runstate, mut inflict_damage, mut log) = data;
+
+        for (entity, mut clock) in (&entities, &mut hunger_clock).join() {
+            let mut proceed = false;
+
+            match *runstate {
+                RunState::PlayerTurn => {
+                    if entity == *player_entity {
+                        proceed = true;
+                    }
+                }
+                RunState::MonsterTurn => {
+                    if entity != *player_entity {
+                        proceed = false;
+                    }
+                }
+                _ => proceed = false
+            }
+
+            if proceed {
+                clock.duration -= 1;
+                if clock.duration < 1 {
+                    match clock.state {
+                        HungerState::WellFed => {
+                            clock.state = HungerState::Normal;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are no longer well fed.".to_string());
+                            }
+                        }
+                        HungerState::Normal => {
+                            clock.state = HungerState::Hungry;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are hungry.".to_string());
+                            }
+                        }
+                        HungerState::Hungry => {
+                            clock.state = HungerState::Starving;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are starving!".to_string());
+                            }
+                        }
+                        HungerState::Starving => {
+                            // Inflict damage from hunger
+                            if entity == *player_entity {
+                                log.entries.insert(0, "Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string());
+                            }
+                            inflict_damage.insert(entity, SufferDamage{ amount: 1 }).expect("Unable to do damage");  
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 310 - 0
chapter-32-voronoi/src/inventory_system.rs

@@ -0,0 +1,310 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog, WantsToUseItem, 
+    Consumable, ProvidesHealing, CombatStats, WantsToDropItem, InflictsDamage, Map, SufferDamage,
+    AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, particle_system::ParticleBuilder,
+    ProvidesFood, HungerClock, HungerState, MagicMapper, RunState};
+
+pub struct ItemCollectionSystem {}
+
+impl<'a> System<'a> for ItemCollectionSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        WriteStorage<'a, WantsToPickupItem>,
+                        WriteStorage<'a, Position>,
+                        ReadStorage<'a, Name>,
+                        WriteStorage<'a, InBackpack>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack) = data;
+
+        for pickup in wants_pickup.join() {
+            positions.remove(pickup.item);
+            backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry");
+
+            if pickup.collected_by == *player_entity {
+                gamelog.entries.insert(0, format!("You pick up the {}.", names.get(pickup.item).unwrap().name));
+            }
+        }
+
+        wants_pickup.clear();
+    }
+}
+
+pub struct ItemUseSystem {}
+
+impl<'a> System<'a> for ItemUseSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        WriteExpect<'a, Map>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToUseItem>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, Consumable>,
+                        ReadStorage<'a, ProvidesHealing>,
+                        ReadStorage<'a, InflictsDamage>,
+                        WriteStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, AreaOfEffect>,
+                        WriteStorage<'a, Confusion>,
+                        ReadStorage<'a, Equippable>,
+                        WriteStorage<'a, Equipped>,
+                        WriteStorage<'a, InBackpack>,
+                        WriteExpect<'a, ParticleBuilder>,
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, ProvidesFood>,
+                        WriteStorage<'a, HungerClock>,
+                        ReadStorage<'a, MagicMapper>,
+                        WriteExpect<'a, RunState>
+                      );
+
+    #[allow(clippy::cognitive_complexity)]
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, map, entities, mut wants_use, names, 
+            consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, 
+            aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions,
+            provides_food, mut hunger_clocks, magic_mapper, mut runstate) = data;
+
+        for (entity, useitem) in (&entities, &wants_use).join() {
+            let mut used_item = true;
+
+            // Targeting
+            let mut targets : Vec<Entity> = Vec::new();
+            match useitem.target {
+                None => { targets.push( *player_entity ); }
+                Some(target) => {
+                    let area_effect = aoe.get(useitem.item);
+                    match area_effect {
+                        None => {
+                            // Single target in tile
+                            let idx = map.xy_idx(target.x, target.y);
+                            for mob in map.tile_content[idx].iter() {
+                                targets.push(*mob);
+                            }
+                        }
+                        Some(area_effect) => {
+                            // AoE
+                            let blast_tiles = rltk::field_of_view(target, area_effect.radius, &*map);
+                            for tile_idx in blast_tiles.iter() {
+                                let idx = map.xy_idx(tile_idx.x, tile_idx.y);
+                                for mob in map.tile_content[idx].iter() {
+                                    targets.push(*mob);
+                                }
+                                particle_builder.request(tile_idx.x, tile_idx.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('░'), 200.0);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // If it is equippable, then we want to equip it - and unequip whatever else was in that slot
+            let item_equippable = equippable.get(useitem.item);
+            match item_equippable {
+                None => {}
+                Some(can_equip) => {
+                    let target_slot = can_equip.slot;
+                    let target = targets[0];
+
+                    // Remove any items the target has in the item's slot
+                    let mut to_unequip : Vec<Entity> = Vec::new();
+                    for (item_entity, already_equipped, name) in (&entities, &equipped, &names).join() {
+                        if already_equipped.owner == target && already_equipped.slot == target_slot {
+                            to_unequip.push(item_entity);
+                            if target == *player_entity {
+                                gamelog.entries.insert(0, format!("You unequip {}.", name.name));
+                            }
+                        }
+                    }
+                    for item in to_unequip.iter() {
+                        equipped.remove(*item);
+                        backpack.insert(*item, InBackpack{ owner: target }).expect("Unable to insert backpack entry");
+                    }
+
+                    // Wield the item
+                    equipped.insert(useitem.item, Equipped{ owner: target, slot: target_slot }).expect("Unable to insert equipped component");
+                    backpack.remove(useitem.item);
+                    if target == *player_entity {
+                        gamelog.entries.insert(0, format!("You equip {}.", names.get(useitem.item).unwrap().name));
+                    }
+                }
+            }
+
+            // It it is edible, eat it!
+            let item_edible = provides_food.get(useitem.item);
+            match item_edible {
+                None => {}
+                Some(_) => {
+                    used_item = true;
+                    let target = targets[0];
+                    let hc = hunger_clocks.get_mut(target);
+                    if let Some(hc) = hc {
+                        hc.state = HungerState::WellFed;
+                        hc.duration = 20;
+                        gamelog.entries.insert(0, format!("You eat the {}.", names.get(useitem.item).unwrap().name));
+                    }
+                }
+            }
+
+            // If its a magic mapper...
+            let is_mapper = magic_mapper.get(useitem.item);
+            match is_mapper {
+                None => {}
+                Some(_) => {
+                    used_item = true;
+                    gamelog.entries.insert(0, "The map is revealed to you!".to_string());
+                    *runstate = RunState::MagicMapReveal{ row : 0};
+                }
+            }
+
+            // If it heals, apply the healing
+            let item_heals = healing.get(useitem.item);
+            match item_heals {
+                None => {}
+                Some(healer) => {
+                    used_item = false;
+                    for target in targets.iter() {
+                        let stats = combat_stats.get_mut(*target);
+                        if let Some(stats) = stats {
+                            stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);
+                            if entity == *player_entity {
+                                gamelog.entries.insert(0, format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
+                            }
+                            used_item = true;
+
+                            let pos = positions.get(*target);
+                            if let Some(pos) = pos {
+                                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::GREEN), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('♥'), 200.0);
+                            }   
+                        }            
+                    }
+                }
+            }
+
+            // If it inflicts damage, apply it to the target cell
+            let item_damages = inflict_damage.get(useitem.item);
+            match item_damages {
+                None => {}
+                Some(damage) => {
+                    used_item = false;
+                    for mob in targets.iter() {
+                        suffer_damage.insert(*mob, SufferDamage{ amount : damage.damage }).expect("Unable to insert");
+                        if entity == *player_entity {
+                            let mob_name = names.get(*mob).unwrap();
+                            let item_name = names.get(useitem.item).unwrap();
+                            gamelog.entries.insert(0, format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage));
+
+                            let pos = positions.get(*mob);
+                            if let Some(pos) = pos {
+                                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::RED), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
+                            }
+                        }
+
+                        used_item = true;
+                    }
+                }
+            }
+
+            // Can it pass along confusion? Note the use of scopes to escape from the borrow checker!
+            let mut add_confusion = Vec::new();
+            {
+                let causes_confusion = confused.get(useitem.item);
+                match causes_confusion {
+                    None => {}
+                    Some(confusion) => {
+                        used_item = false;
+                        for mob in targets.iter() {
+                            add_confusion.push((*mob, confusion.turns ));
+                            if entity == *player_entity {
+                                let mob_name = names.get(*mob).unwrap();
+                                let item_name = names.get(useitem.item).unwrap();
+                                gamelog.entries.insert(0, format!("You use {} on {}, confusing them.", item_name.name, mob_name.name));
+
+                                let pos = positions.get(*mob);
+                                if let Some(pos) = pos {
+                                    particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 200.0);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            for mob in add_confusion.iter() {
+                confused.insert(mob.0, Confusion{ turns: mob.1 }).expect("Unable to insert status");
+            }
+
+            // If its a consumable, we delete it on use
+            if used_item {
+                let consumable = consumables.get(useitem.item);
+                match consumable {
+                    None => {}
+                    Some(_) => {
+                        entities.delete(useitem.item).expect("Delete failed");
+                    }
+                }
+            }
+        }
+
+        wants_use.clear();
+    }
+}
+
+pub struct ItemDropSystem {}
+
+impl<'a> System<'a> for ItemDropSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToDropItem>,
+                        ReadStorage<'a, Name>,
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, InBackpack>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack) = data;
+
+        for (entity, to_drop) in (&entities, &wants_drop).join() {
+            let mut dropper_pos : Position = Position{x:0, y:0};
+            {
+                let dropped_pos = positions.get(entity).unwrap();
+                dropper_pos.x = dropped_pos.x;
+                dropper_pos.y = dropped_pos.y;
+            }
+            positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position");
+            backpack.remove(to_drop.item);
+
+            if entity == *player_entity {
+                gamelog.entries.insert(0, format!("You drop up the {}.", names.get(to_drop.item).unwrap().name));
+            }
+        }
+
+        wants_drop.clear();
+    }
+}
+
+pub struct ItemRemoveSystem {}
+
+impl<'a> System<'a> for ItemRemoveSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( 
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToRemoveItem>,
+                        WriteStorage<'a, Equipped>,
+                        WriteStorage<'a, InBackpack>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut wants_remove, mut equipped, mut backpack) = data;
+
+        for (entity, to_remove) in (&entities, &wants_remove).join() {
+            equipped.remove(to_remove.item);
+            backpack.insert(to_remove.item, InBackpack{ owner: entity }).expect("Unable to insert backpack");
+        }
+
+        wants_remove.clear();
+    }
+}

+ 474 - 0
chapter-32-voronoi/src/main.rs

@@ -0,0 +1,474 @@
+extern crate serde;
+extern crate rltk;
+use rltk::{Console, GameState, Rltk, Point};
+extern crate specs;
+use specs::prelude::*;
+use specs::saveload::{SimpleMarker, SimpleMarkerAllocator};
+#[macro_use]
+extern crate specs_derive;
+mod components;
+pub use components::*;
+mod map;
+pub use map::*;
+mod player;
+use player::*;
+mod rect;
+pub use rect::Rect;
+mod visibility_system;
+use visibility_system::VisibilitySystem;
+mod monster_ai_system;
+use monster_ai_system::MonsterAI;
+mod map_indexing_system;
+use map_indexing_system::MapIndexingSystem;
+mod melee_combat_system;
+use melee_combat_system::MeleeCombatSystem;
+mod damage_system;
+use damage_system::DamageSystem;
+mod gui;
+mod gamelog;
+mod spawner;
+mod inventory_system;
+use inventory_system::{ ItemCollectionSystem, ItemUseSystem, ItemDropSystem, ItemRemoveSystem };
+pub mod saveload_system;
+pub mod random_table;
+pub mod particle_system;
+pub mod hunger_system;
+pub mod rex_assets;
+pub mod trigger_system;
+pub mod map_builders;
+
+rltk::add_wasm_support!();
+
+const SHOW_MAPGEN_VISUALIZER : bool = true;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum RunState { AwaitingInput, 
+    PreRun, 
+    PlayerTurn, 
+    MonsterTurn, 
+    ShowInventory, 
+    ShowDropItem, 
+    ShowTargeting { range : i32, item : Entity},
+    MainMenu { menu_selection : gui::MainMenuSelection },
+    SaveGame,
+    NextLevel,
+    ShowRemoveItem,
+    GameOver,
+    MagicMapReveal { row : i32 },
+    MapGeneration
+}
+
+pub struct State {
+    pub ecs: World,
+    mapgen_next_state : Option<RunState>,
+    mapgen_history : Vec<Map>,
+    mapgen_index : usize,
+    mapgen_timer : f32
+}
+
+impl State {
+    fn run_systems(&mut self) {
+        let mut mapindex = MapIndexingSystem{};
+        mapindex.run_now(&self.ecs);
+        let mut vis = VisibilitySystem{};
+        vis.run_now(&self.ecs);
+        let mut mob = MonsterAI{};
+        mob.run_now(&self.ecs);
+        let mut triggers = trigger_system::TriggerSystem{};
+        triggers.run_now(&self.ecs);
+        let mut melee = MeleeCombatSystem{};
+        melee.run_now(&self.ecs);
+        let mut damage = DamageSystem{};
+        damage.run_now(&self.ecs);
+        let mut pickup = ItemCollectionSystem{};
+        pickup.run_now(&self.ecs);
+        let mut itemuse = ItemUseSystem{};
+        itemuse.run_now(&self.ecs);
+        let mut drop_items = ItemDropSystem{};
+        drop_items.run_now(&self.ecs);
+        let mut item_remove = ItemRemoveSystem{};
+        item_remove.run_now(&self.ecs);
+        let mut hunger = hunger_system::HungerSystem{};
+        hunger.run_now(&self.ecs);
+        let mut particles = particle_system::ParticleSpawnSystem{};
+        particles.run_now(&self.ecs);
+
+        self.ecs.maintain();
+    }
+}
+
+impl GameState for State {
+    fn tick(&mut self, ctx : &mut Rltk) {
+        let mut newrunstate;
+        {
+            let runstate = self.ecs.fetch::<RunState>();
+            newrunstate = *runstate;
+        }
+
+        ctx.cls();        
+        particle_system::cull_dead_particles(&mut self.ecs, ctx);
+
+        match newrunstate {
+            RunState::MainMenu{..} => {}
+            RunState::GameOver{..} => {}
+            _ => {
+                draw_map(&self.ecs.fetch::<Map>(), ctx);
+                let positions = self.ecs.read_storage::<Position>();
+                let renderables = self.ecs.read_storage::<Renderable>();
+                let hidden = self.ecs.read_storage::<Hidden>();
+                let map = self.ecs.fetch::<Map>();
+
+                let mut data = (&positions, &renderables, !&hidden).join().collect::<Vec<_>>();
+                data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
+                for (pos, render, _hidden) in data.iter() {
+                    let idx = map.xy_idx(pos.x, pos.y);
+                    if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
+                }
+                gui::draw_ui(&self.ecs, ctx);                
+            }
+        }
+        
+        match newrunstate {
+            RunState::MapGeneration => {
+                if !SHOW_MAPGEN_VISUALIZER {
+                    newrunstate = self.mapgen_next_state.unwrap();
+                }
+                ctx.cls();                
+                draw_map(&self.mapgen_history[self.mapgen_index], ctx);
+
+                self.mapgen_timer += ctx.frame_time_ms;
+                if self.mapgen_timer > 200.0 {
+                    self.mapgen_timer = 0.0;
+                    self.mapgen_index += 1;
+                    if self.mapgen_index == self.mapgen_history.len() {
+                        //self.mapgen_index -= 1;
+                        newrunstate = self.mapgen_next_state.unwrap();
+                    }
+                }
+            }
+            RunState::PreRun => {
+                self.run_systems();
+                self.ecs.maintain();
+                newrunstate = RunState::AwaitingInput;
+            }
+            RunState::AwaitingInput => {
+                newrunstate = player_input(self, ctx);
+            }
+            RunState::PlayerTurn => {
+                self.run_systems();
+                self.ecs.maintain();
+                match *self.ecs.fetch::<RunState>() {
+                    RunState::MagicMapReveal{ .. } => newrunstate = RunState::MagicMapReveal{ row: 0 },
+                    _ => newrunstate = RunState::MonsterTurn
+                }                
+            }
+            RunState::MonsterTurn => {
+                self.run_systems();
+                self.ecs.maintain();
+                newrunstate = RunState::AwaitingInput;
+            }
+            RunState::ShowInventory => {
+                let result = gui::show_inventory(self, ctx);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let item_entity = result.1.unwrap();
+                        let is_ranged = self.ecs.read_storage::<Ranged>();
+                        let is_item_ranged = is_ranged.get(item_entity);
+                        if let Some(is_item_ranged) = is_item_ranged {
+                            newrunstate = RunState::ShowTargeting{ range: is_item_ranged.range, item: item_entity };
+                        } else {
+                            let mut intent = self.ecs.write_storage::<WantsToUseItem>();
+                            intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{ item: item_entity, target: None }).expect("Unable to insert intent");
+                            newrunstate = RunState::PlayerTurn;
+                        }
+                    }
+                }
+            }
+            RunState::ShowDropItem => {
+                let result = gui::drop_item_menu(self, ctx);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let item_entity = result.1.unwrap();
+                        let mut intent = self.ecs.write_storage::<WantsToDropItem>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem{ item: item_entity }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
+                    }
+                }
+            }
+            RunState::ShowRemoveItem => {
+                let result = gui::remove_item_menu(self, ctx);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let item_entity = result.1.unwrap();
+                        let mut intent = self.ecs.write_storage::<WantsToRemoveItem>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem{ item: item_entity }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
+                    }
+                }
+            }
+            RunState::ShowTargeting{range, item} => {
+                let result = gui::ranged_target(self, ctx, range);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let mut intent = self.ecs.write_storage::<WantsToUseItem>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{ item, target: result.1 }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
+                    }
+                }
+            }
+            RunState::MainMenu{ .. } => {
+                let result = gui::main_menu(self, ctx);
+                match result {
+                    gui::MainMenuResult::NoSelection{ selected } => newrunstate = RunState::MainMenu{ menu_selection: selected },
+                    gui::MainMenuResult::Selected{ selected } => {
+                        match selected {
+                            gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun,
+                            gui::MainMenuSelection::LoadGame => {
+                                saveload_system::load_game(&mut self.ecs);
+                                newrunstate = RunState::AwaitingInput;
+                                saveload_system::delete_save();
+                            }
+                            gui::MainMenuSelection::Quit => { ::std::process::exit(0); }
+                        }
+                    }
+                }
+            }
+            RunState::GameOver => {
+                let result = gui::game_over(ctx);
+                match result {
+                    gui::GameOverResult::NoSelection => {}
+                    gui::GameOverResult::QuitToMenu => {
+                        self.game_over_cleanup();
+                        newrunstate = RunState::MapGeneration;
+                        self.mapgen_next_state = Some(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame });
+                    }
+                }
+            }
+            RunState::SaveGame => {
+                saveload_system::save_game(&mut self.ecs);
+                newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
+            }
+            RunState::NextLevel => {
+                self.goto_next_level();
+                self.mapgen_next_state = Some(RunState::PreRun);
+                newrunstate = RunState::MapGeneration;
+            }
+            RunState::MagicMapReveal{row} => {
+                let mut map = self.ecs.fetch_mut::<Map>();
+                for x in 0..MAPWIDTH {
+                    let idx = map.xy_idx(x as i32,row);
+                    map.revealed_tiles[idx] = true;
+                }
+                if row as usize == MAPHEIGHT-1 {
+                    newrunstate = RunState::MonsterTurn;
+                } else {
+                    newrunstate = RunState::MagicMapReveal{ row: row+1 };
+                }
+            }
+        }
+
+        {
+            let mut runwriter = self.ecs.write_resource::<RunState>();
+            *runwriter = newrunstate;
+        }
+        damage_system::delete_the_dead(&mut self.ecs);
+    }
+}
+
+impl State {
+    fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {
+        let entities = self.ecs.entities();
+        let player = self.ecs.read_storage::<Player>();
+        let backpack = self.ecs.read_storage::<InBackpack>();
+        let player_entity = self.ecs.fetch::<Entity>();
+        let equipped = self.ecs.read_storage::<Equipped>();
+
+        let mut to_delete : Vec<Entity> = Vec::new();
+        for entity in entities.join() {
+            let mut should_delete = true;
+
+            // Don't delete the player
+            let p = player.get(entity);
+            if let Some(_p) = p {
+                should_delete = false;
+            }
+
+            // Don't delete the player's equipment
+            let bp = backpack.get(entity);
+            if let Some(bp) = bp {
+                if bp.owner == *player_entity {
+                    should_delete = false;
+                }
+            }
+
+            let eq = equipped.get(entity);
+            if let Some(eq) = eq {
+                if eq.owner == *player_entity {
+                    should_delete = false;
+                }
+            }
+
+            if should_delete { 
+                to_delete.push(entity);
+            }
+        }
+
+        to_delete
+    }
+
+    fn goto_next_level(&mut self) {
+        // Delete entities that aren't the player or his/her equipment
+        let to_delete = self.entities_to_remove_on_level_change();
+        for target in to_delete {
+            self.ecs.delete_entity(target).expect("Unable to delete entity");
+        }
+
+        // Build a new map and place the player
+        let current_depth;
+        {
+            let worldmap_resource = self.ecs.fetch::<Map>();
+            current_depth = worldmap_resource.depth;
+        }
+        self.generate_world_map(current_depth + 1);
+
+        // Notify the player and give them some health
+        let player_entity = self.ecs.fetch::<Entity>();
+        let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
+        gamelog.entries.insert(0, "You descend to the next level, and take a moment to heal.".to_string());
+        let mut player_health_store = self.ecs.write_storage::<CombatStats>();
+        let player_health = player_health_store.get_mut(*player_entity);
+        if let Some(player_health) = player_health {
+            player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2);
+        }
+    }
+
+    fn game_over_cleanup(&mut self) {
+        // Delete everything
+        let mut to_delete = Vec::new();
+        for e in self.ecs.entities().join() {
+            to_delete.push(e);
+        }
+        for del in to_delete.iter() {
+            self.ecs.delete_entity(*del).expect("Deletion failed");
+        }
+
+        // Spawn a new player
+        {
+            let player_entity = spawner::player(&mut self.ecs, 0, 0);
+            let mut player_entity_writer = self.ecs.write_resource::<Entity>();
+            *player_entity_writer = player_entity;
+        }
+
+        // Build a new map and place the player
+        self.generate_world_map(1);                                          
+    }
+
+    fn generate_world_map(&mut self, new_depth : i32) {
+        self.mapgen_index = 0;
+        self.mapgen_timer = 0.0;
+        self.mapgen_history.clear();
+        let mut builder = map_builders::random_builder(new_depth);
+        builder.build_map();
+        self.mapgen_history = builder.get_snapshot_history();
+        let player_start;
+        {
+            let mut worldmap_resource = self.ecs.write_resource::<Map>();
+            *worldmap_resource = builder.get_map();
+            player_start = builder.get_starting_position();
+        }
+
+        // Spawn bad guys
+        builder.spawn_entities(&mut self.ecs);
+
+        // Place the player and update resources
+        let (player_x, player_y) = (player_start.x, player_start.y);
+        let mut player_position = self.ecs.write_resource::<Point>();
+        *player_position = Point::new(player_x, player_y);
+        let mut position_components = self.ecs.write_storage::<Position>();
+        let player_entity = self.ecs.fetch::<Entity>();
+        let player_pos_comp = position_components.get_mut(*player_entity);
+        if let Some(player_pos_comp) = player_pos_comp {
+            player_pos_comp.x = player_x;
+            player_pos_comp.y = player_y;
+        }
+
+        // Mark the player's visibility as dirty
+        let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
+        let vs = viewshed_components.get_mut(*player_entity);
+        if let Some(vs) = vs {
+            vs.dirty = true;
+        } 
+    }
+}
+
+fn main() {
+    let mut context = Rltk::init_simple8x8(80, 50, "Hello Rust World", "resources");
+    context.with_post_scanlines(true);
+    let mut gs = State {
+        ecs: World::new(),
+        mapgen_next_state : Some(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame }),
+        mapgen_index : 0,
+        mapgen_history: Vec::new(),
+        mapgen_timer: 0.0
+    };
+    gs.ecs.register::<Position>();
+    gs.ecs.register::<Renderable>();
+    gs.ecs.register::<Player>();
+    gs.ecs.register::<Viewshed>();
+    gs.ecs.register::<Monster>();
+    gs.ecs.register::<Name>();
+    gs.ecs.register::<BlocksTile>();
+    gs.ecs.register::<CombatStats>();
+    gs.ecs.register::<WantsToMelee>();
+    gs.ecs.register::<SufferDamage>();
+    gs.ecs.register::<Item>();
+    gs.ecs.register::<ProvidesHealing>();
+    gs.ecs.register::<InflictsDamage>();
+    gs.ecs.register::<AreaOfEffect>();
+    gs.ecs.register::<Consumable>();
+    gs.ecs.register::<Ranged>();
+    gs.ecs.register::<InBackpack>();
+    gs.ecs.register::<WantsToPickupItem>();
+    gs.ecs.register::<WantsToUseItem>();
+    gs.ecs.register::<WantsToDropItem>();
+    gs.ecs.register::<Confusion>();
+    gs.ecs.register::<SimpleMarker<SerializeMe>>();
+    gs.ecs.register::<SerializationHelper>();
+    gs.ecs.register::<Equippable>();
+    gs.ecs.register::<Equipped>();
+    gs.ecs.register::<MeleePowerBonus>();
+    gs.ecs.register::<DefenseBonus>();
+    gs.ecs.register::<WantsToRemoveItem>();
+    gs.ecs.register::<ParticleLifetime>();
+    gs.ecs.register::<HungerClock>();
+    gs.ecs.register::<ProvidesFood>();
+    gs.ecs.register::<MagicMapper>();
+    gs.ecs.register::<Hidden>();
+    gs.ecs.register::<EntryTrigger>();
+    gs.ecs.register::<EntityMoved>();
+    gs.ecs.register::<SingleActivation>();
+
+    gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
+
+    gs.ecs.insert(Map::new(1));
+    gs.ecs.insert(Point::new(0, 0));
+    gs.ecs.insert(rltk::RandomNumberGenerator::new());
+    let player_entity = spawner::player(&mut gs.ecs, 0, 0);
+    gs.ecs.insert(player_entity);
+    gs.ecs.insert(RunState::MapGeneration{} );
+    gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty Roguelike".to_string()] });
+    gs.ecs.insert(particle_system::ParticleBuilder::new());
+    gs.ecs.insert(rex_assets::RexAssets::new());
+
+    gs.generate_world_map(1);
+
+    rltk::main_loop(context, gs);
+}

+ 187 - 0
chapter-32-voronoi/src/map.rs

@@ -0,0 +1,187 @@
+extern crate rltk;
+use rltk::{ RGB, Rltk, Console, BaseMap, Algorithm2D, Point };
+extern crate specs;
+use specs::prelude::*;
+use serde::{Serialize, Deserialize};
+use std::collections::HashSet;
+
+pub const MAPWIDTH : usize = 80;
+pub const MAPHEIGHT : usize = 43;
+pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
+
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
+pub enum TileType {
+    Wall, Floor, DownStairs
+}
+
+#[derive(Default, Serialize, Deserialize, Clone)]
+pub struct Map {
+    pub tiles : Vec<TileType>,
+    pub width : i32,
+    pub height : i32,
+    pub revealed_tiles : Vec<bool>,
+    pub visible_tiles : Vec<bool>,
+    pub blocked : Vec<bool>,
+    pub depth : i32,
+    pub bloodstains : HashSet<usize>,
+
+    #[serde(skip_serializing)]
+    #[serde(skip_deserializing)]
+    pub tile_content : Vec<Vec<Entity>>
+}
+
+impl Map {
+    pub fn xy_idx(&self, x: i32, y: i32) -> usize {
+        (y as usize * self.width as usize) + x as usize
+    }
+
+    fn is_exit_valid(&self, x:i32, y:i32) -> bool {
+        if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; }
+        let idx = (y * self.width) + x;
+        !self.blocked[idx as usize]
+    }
+
+    pub fn populate_blocked(&mut self) {        
+        for (i,tile) in self.tiles.iter_mut().enumerate() {
+            self.blocked[i] = *tile == TileType::Wall;
+        }
+    }
+
+    pub fn clear_content_index(&mut self) {
+        for content in self.tile_content.iter_mut() {
+            content.clear();
+        }
+    }
+
+    /// Generates an empty map, consisting entirely of solid walls
+    pub fn new(new_depth : i32) -> Map {
+        Map{
+            tiles : vec![TileType::Wall; MAPCOUNT],
+            width : MAPWIDTH as i32,
+            height: MAPHEIGHT as i32,
+            revealed_tiles : vec![false; MAPCOUNT],
+            visible_tiles : vec![false; MAPCOUNT],
+            blocked : vec![false; MAPCOUNT],
+            tile_content : vec![Vec::new(); MAPCOUNT],
+            depth: new_depth,
+            bloodstains: HashSet::new()
+        }
+    }
+}
+
+impl BaseMap for Map {
+    fn is_opaque(&self, idx:i32) -> bool {
+        self.tiles[idx as usize] == TileType::Wall
+    }
+
+    fn get_available_exits(&self, idx:i32) -> Vec<(i32, f32)> {
+        let mut exits : Vec<(i32, f32)> = Vec::new();
+        let x = idx % self.width;
+        let y = idx / self.width;
+
+        // Cardinal directions
+        if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) };
+        if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) };
+        if self.is_exit_valid(x, y-1) { exits.push((idx-self.width, 1.0)) };
+        if self.is_exit_valid(x, y+1) { exits.push((idx+self.width, 1.0)) };
+
+        // Diagonals
+        if self.is_exit_valid(x-1, y-1) { exits.push(((idx-self.width)-1, 1.45)); }
+        if self.is_exit_valid(x+1, y-1) { exits.push(((idx-self.width)+1, 1.45)); }
+        if self.is_exit_valid(x-1, y+1) { exits.push(((idx+self.width)-1, 1.45)); }
+        if self.is_exit_valid(x+1, y+1) { exits.push(((idx+self.width)+1, 1.45)); }
+
+        exits
+    }
+
+    fn get_pathing_distance(&self, idx1:i32, idx2:i32) -> f32 {
+        let p1 = Point::new(idx1 % self.width, idx1 / self.width);
+        let p2 = Point::new(idx2 % self.width, idx2 / self.width);
+        rltk::DistanceAlg::Pythagoras.distance2d(p1, p2)
+    }
+}
+
+impl Algorithm2D for Map {
+    fn point2d_to_index(&self, pt: Point) -> i32 {
+        (pt.y * self.width) + pt.x
+    }
+
+    fn index_to_point2d(&self, idx:i32) -> Point {
+        Point{ x: idx % self.width, y: idx / self.width }
+    }
+}
+
+fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
+    let idx = map.xy_idx(x, y);
+    map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx]
+}
+
+fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
+    let mut mask : u8 = 0;
+
+    if is_revealed_and_wall(map, x, y - 1) { mask +=1; }
+    if is_revealed_and_wall(map, x, y + 1) { mask +=2; }
+    if is_revealed_and_wall(map, x - 1, y) { mask +=4; }
+    if is_revealed_and_wall(map, x + 1, y) { mask +=8; }
+
+    match mask {
+        0 => { 9 } // Pillar because we can't see neighbors
+        1 => { 186 } // Wall only to the north
+        2 => { 186 } // Wall only to the south
+        3 => { 186 } // Wall to the north and south
+        4 => { 205 } // Wall only to the west
+        5 => { 188 } // Wall to the north and west
+        6 => { 187 } // Wall to the south and west
+        7 => { 185 } // Wall to the north, south and west
+        8 => { 205 } // Wall only to the east
+        9 => { 200 } // Wall to the north and east
+        10 => { 201 } // Wall to the south and east
+        11 => { 204 } // Wall to the north, south and east
+        12 => { 205 } // Wall to the east and west
+        13 => { 202 } // Wall to the east, west, and south
+        14 => { 203 } // Wall to the east, west, and north
+        _ => { 35 } // We missed one?
+    }
+}
+
+pub fn draw_map(map : &Map, ctx : &mut Rltk) {
+    let mut y = 0;
+    let mut x = 0;
+    for (idx,tile) in map.tiles.iter().enumerate() {
+        // Render a tile depending upon the tile type
+
+        if map.revealed_tiles[idx] {
+            let glyph;
+            let mut fg;
+            let mut bg = RGB::from_f32(0., 0., 0.);
+            match tile {
+                TileType::Floor => {
+                    glyph = rltk::to_cp437('.');
+                    fg = RGB::from_f32(0.0, 0.5, 0.5);
+                }
+                TileType::Wall => {
+                    glyph = wall_glyph(&*map, x, y);
+                    fg = RGB::from_f32(0., 1.0, 0.);
+                }
+                TileType::DownStairs => {
+                    glyph = rltk::to_cp437('>');
+                    fg = RGB::from_f32(0., 1.0, 1.0);
+                }
+            }
+            if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.); }
+            if !map.visible_tiles[idx] { 
+                fg = fg.to_greyscale();
+                bg = RGB::from_f32(0., 0., 0.); // Don't show stains out of visual range
+            }
+            ctx.set(x, y, fg, bg, glyph);
+        }
+
+        // Move the coordinates
+        x += 1;
+        if x > MAPWIDTH as i32-1 {
+            x = 0;
+            y += 1;
+        }
+    }
+}

+ 172 - 0
chapter-32-voronoi/src/map_builders/bsp_dungeon.rs

@@ -0,0 +1,172 @@
+use super::{MapBuilder, Map, Rect, apply_room_to_map, 
+    TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER, draw_corridor};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+
+pub struct BspDungeonBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    rooms: Vec<Rect>,
+    history: Vec<Map>,
+    rects: Vec<Rect>
+}
+
+impl MapBuilder for BspDungeonBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for room in self.rooms.iter().skip(1) {
+            spawner::spawn_room(ecs, room, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl BspDungeonBuilder {
+    pub fn new(new_depth : i32) -> BspDungeonBuilder {
+        BspDungeonBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            rooms: Vec::new(),
+            history: Vec::new(),
+            rects: Vec::new()
+        }
+    }
+
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        self.rects.clear();
+        self.rects.push( Rect::new(2, 2, self.map.width-5, self.map.height-5) ); // Start with a single map-sized rectangle
+        let first_room = self.rects[0];
+        self.add_subrects(first_room); // Divide the first room
+
+        // Up to 240 times, we get a random rectangle and divide it. If its possible to squeeze a
+        // room in there, we place it and add it to the rooms list.
+        let mut n_rooms = 0;
+        while n_rooms < 240 {
+            let rect = self.get_random_rect(&mut rng);
+            let candidate = self.get_random_sub_rect(rect, &mut rng);
+
+            if self.is_possible(candidate) {
+                apply_room_to_map(&mut self.map, &candidate);
+                self.rooms.push(candidate);
+                self.add_subrects(rect);
+                self.take_snapshot();
+            }
+
+            n_rooms += 1;
+        }
+
+        // Now we sort the rooms
+        self.rooms.sort_by(|a,b| a.x1.cmp(&b.x1) );
+
+        // Now we want corridors
+        for i in 0..self.rooms.len()-1 {
+            let room = self.rooms[i];
+            let next_room = self.rooms[i+1];
+            let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2))-1);
+            let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2))-1);
+            let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1);
+            let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1);
+            draw_corridor(&mut self.map, start_x, start_y, end_x, end_y);
+            self.take_snapshot();
+        }
+
+        // Don't forget the stairs
+        let stairs = self.rooms[self.rooms.len()-1].center();
+        let stairs_idx = self.map.xy_idx(stairs.0, stairs.1);
+        self.map.tiles[stairs_idx] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Set player start
+        let start = self.rooms[0].center();
+        self.starting_position = Position{ x: start.0, y: start.1 };
+    }
+
+    fn add_subrects(&mut self, rect : Rect) {
+        let width = i32::abs(rect.x1 - rect.x2);
+        let height = i32::abs(rect.y1 - rect.y2);
+        let half_width = i32::max(width / 2, 1);
+        let half_height = i32::max(height / 2, 1);
+
+        self.rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height ));
+        self.rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width, half_height ));
+        self.rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width, half_height ));
+        self.rects.push(Rect::new( rect.x1 + half_width, rect.y1 + half_height, half_width, half_height ));
+    }
+
+    fn get_random_rect(&mut self, rng : &mut RandomNumberGenerator) -> Rect {
+        if self.rects.len() == 1 { return self.rects[0]; }
+        let idx = (rng.roll_dice(1, self.rects.len() as i32)-1) as usize;
+        self.rects[idx]
+    }
+
+    fn get_random_sub_rect(&self, rect : Rect, rng : &mut RandomNumberGenerator) -> Rect {
+        let mut result = rect;
+        let rect_width = i32::abs(rect.x1 - rect.x2);
+        let rect_height = i32::abs(rect.y1 - rect.y2);
+
+        let w = i32::max(3, rng.roll_dice(1, i32::min(rect_width, 10))-1) + 1;
+        let h = i32::max(3, rng.roll_dice(1, i32::min(rect_height, 10))-1) + 1;
+
+        result.x1 += rng.roll_dice(1, 6)-1;
+        result.y1 += rng.roll_dice(1, 6)-1;
+        result.x2 = result.x1 + w;
+        result.y2 = result.y1 + h;
+
+        result
+    }
+
+    fn is_possible(&self, rect : Rect) -> bool {
+        let mut expanded = rect;
+        expanded.x1 -= 2;
+        expanded.x2 += 2;
+        expanded.y1 -= 2;
+        expanded.y2 += 2;
+
+        let mut can_build = true;
+
+        for y in expanded.y1 ..= expanded.y2 {
+            for x in expanded.x1 ..= expanded.x2 {
+                if x > self.map.width-2 { can_build = false; }
+                if y > self.map.height-2 { can_build = false; }
+                if x < 1 { can_build = false; }
+                if y < 1 { can_build = false; }
+                if can_build {
+                    let idx = self.map.xy_idx(x, y);
+                    if self.map.tiles[idx] != TileType::Wall { 
+                        can_build = false; 
+                    }
+                }
+            }
+        }
+
+        can_build
+    }
+}

+ 143 - 0
chapter-32-voronoi/src/map_builders/bsp_interior.rs

@@ -0,0 +1,143 @@
+use super::{MapBuilder, Map, Rect, TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER,
+    draw_corridor};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+
+const MIN_ROOM_SIZE : i32 = 8;
+
+pub struct BspInteriorBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    rooms: Vec<Rect>,
+    history: Vec<Map>,
+    rects: Vec<Rect>
+}
+
+impl MapBuilder for BspInteriorBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for room in self.rooms.iter().skip(1) {
+            spawner::spawn_room(ecs, room, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl BspInteriorBuilder {
+    pub fn new(new_depth : i32) -> BspInteriorBuilder {
+        BspInteriorBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            rooms: Vec::new(),
+            history: Vec::new(),
+            rects: Vec::new()
+        }
+    }
+
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        self.rects.clear();
+        self.rects.push( Rect::new(1, 1, self.map.width-2, self.map.height-2) ); // Start with a single map-sized rectangle
+        let first_room = self.rects[0];
+        self.add_subrects(first_room, &mut rng); // Divide the first room
+
+        let rooms = self.rects.clone();
+        for r in rooms.iter() {
+            let room = *r;
+            //room.x2 -= 1;
+            //room.y2 -= 1;
+            self.rooms.push(room);
+            for y in room.y1 .. room.y2 {
+                for x in room.x1 .. room.x2 {
+                    let idx = self.map.xy_idx(x, y);
+                    if idx > 0 && idx < ((self.map.width * self.map.height)-1) as usize {
+                        self.map.tiles[idx] = TileType::Floor;
+                    }
+                }
+            }
+            self.take_snapshot();
+        }
+
+        // Now we want corridors
+        for i in 0..self.rooms.len()-1 {
+            let room = self.rooms[i];
+            let next_room = self.rooms[i+1];
+            let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2))-1);
+            let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2))-1);
+            let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1);
+            let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1);
+            draw_corridor(&mut self.map, start_x, start_y, end_x, end_y);
+            self.take_snapshot();
+        }
+
+        // Don't forget the stairs
+        let stairs = self.rooms[self.rooms.len()-1].center();
+        let stairs_idx = self.map.xy_idx(stairs.0, stairs.1);
+        self.map.tiles[stairs_idx] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Place the player
+        let start = self.rooms[0].center();
+        self.starting_position = Position{ x: start.0, y: start.1 };
+    }
+
+    fn add_subrects(&mut self, rect : Rect, rng : &mut RandomNumberGenerator) {
+        // Remove the last rect from the list
+        if !self.rects.is_empty() {
+            self.rects.remove(self.rects.len() - 1);
+        }
+
+        // Calculate boundaries
+        let width  = rect.x2 - rect.x1;
+        let height = rect.y2 - rect.y1;
+        let half_width = width / 2;
+        let half_height = height / 2;
+
+        let split = rng.roll_dice(1, 4);
+
+        if split <= 2 {
+            // Horizontal split
+            let h1 = Rect::new( rect.x1, rect.y1, half_width-1, height );
+            self.rects.push( h1 );
+            if half_width > MIN_ROOM_SIZE { self.add_subrects(h1, rng); }
+            let h2 = Rect::new( rect.x1 + half_width, rect.y1, half_width, height );
+            self.rects.push( h2 );
+            if half_width > MIN_ROOM_SIZE { self.add_subrects(h2, rng); }
+        } else {
+            // Vertical split
+            let v1 = Rect::new( rect.x1, rect.y1, width, half_height-1 );
+            self.rects.push(v1);
+            if half_height > MIN_ROOM_SIZE { self.add_subrects(v1, rng); }
+            let v2 = Rect::new( rect.x1, rect.y1 + half_height, width, half_height );
+            self.rects.push(v2);
+            if half_height > MIN_ROOM_SIZE { self.add_subrects(v2, rng); }
+        }
+    }
+}

+ 126 - 0
chapter-32-voronoi/src/map_builders/cellular_automota.rs

@@ -0,0 +1,126 @@
+use super::{MapBuilder, Map,  
+    TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER,
+    remove_unreachable_areas_returning_most_distant, generate_voronoi_spawn_regions};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+use std::collections::HashMap;
+
+pub struct CellularAutomotaBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    history: Vec<Map>,
+    noise_areas : HashMap<i32, Vec<usize>>
+}
+
+impl MapBuilder for CellularAutomotaBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for area in self.noise_areas.iter() {
+            spawner::spawn_region(ecs, area.1, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl CellularAutomotaBuilder {
+    pub fn new(new_depth : i32) -> CellularAutomotaBuilder {
+        CellularAutomotaBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new()
+        }
+    }
+
+    #[allow(clippy::map_entry)]
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        // First we completely randomize the map, setting 55% of it to be floor.
+        for y in 1..self.map.height-1 {
+            for x in 1..self.map.width-1 {
+                let roll = rng.roll_dice(1, 100);
+                let idx = self.map.xy_idx(x, y);
+                if roll > 55 { self.map.tiles[idx] = TileType::Floor } 
+                else { self.map.tiles[idx] = TileType::Wall }
+            }
+        }
+        self.take_snapshot();
+
+        // Now we iteratively apply cellular automota rules
+        for _i in 0..15 {
+            let mut newtiles = self.map.tiles.clone();
+
+            for y in 1..self.map.height-1 {
+                for x in 1..self.map.width-1 {
+                    let idx = self.map.xy_idx(x, y);
+                    let mut neighbors = 0;
+                    if self.map.tiles[idx - 1] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx + 1] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx - self.map.width as usize] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx + self.map.width as usize] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx - (self.map.width as usize - 1)] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx - (self.map.width as usize + 1)] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx + (self.map.width as usize - 1)] == TileType::Wall { neighbors += 1; }
+                    if self.map.tiles[idx + (self.map.width as usize + 1)] == TileType::Wall { neighbors += 1; }
+
+                    if neighbors > 4 || neighbors == 0 {
+                        newtiles[idx] = TileType::Wall;
+                    }
+                    else {
+                        newtiles[idx] = TileType::Floor;
+                    }
+                }
+            }
+
+            self.map.tiles = newtiles.clone();
+            self.take_snapshot();
+        }
+
+        // Find a starting point; start at the middle and walk left until we find an open tile
+        self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
+        let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
+        while self.map.tiles[start_idx] != TileType::Floor {
+            self.starting_position.x -= 1;
+            start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
+        }
+        self.take_snapshot();
+
+        // Find all tiles we can reach from the starting point
+        let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
+        self.take_snapshot();
+
+        // Place the stairs
+        self.map.tiles[exit_tile] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Now we build a noise map for use in spawning entities later
+        self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);
+    }
+}

+ 168 - 0
chapter-32-voronoi/src/map_builders/common.rs

@@ -0,0 +1,168 @@
+use super::{Map, Rect, TileType};
+use std::cmp::{max, min};
+use std::collections::HashMap;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum Symmetry { None, Horizontal, Vertical, Both }
+
+pub fn apply_room_to_map(map : &mut Map, room : &Rect) {
+    for y in room.y1 +1 ..= room.y2 {
+        for x in room.x1 + 1 ..= room.x2 {
+            let idx = map.xy_idx(x, y);
+            if idx > 0 && idx < ((map.width * map.height)-1) as usize {
+                map.tiles[idx] = TileType::Floor;
+            }
+        }
+    }
+}
+
+pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) {
+    for x in min(x1,x2) ..= max(x1,x2) {
+        let idx = map.xy_idx(x, y);
+        if idx > 0 && idx < map.width as usize * map.height as usize {
+            map.tiles[idx as usize] = TileType::Floor;
+        }
+    }
+}
+
+pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) {
+    for y in min(y1,y2) ..= max(y1,y2) {
+        let idx = map.xy_idx(x, y);
+        if idx > 0 && idx < map.width as usize * map.height as usize {
+            map.tiles[idx as usize] = TileType::Floor;
+        }
+    }
+}
+
+/// Searches a map, removes unreachable areas and returns the most distant tile.
+pub fn remove_unreachable_areas_returning_most_distant(map : &mut Map, start_idx : usize) -> usize {
+    let map_starts : Vec<i32> = vec![start_idx as i32];
+    let dijkstra_map = rltk::DijkstraMap::new(map.width, map.height, &map_starts , map, 300.0);
+    let mut exit_tile = (0, 0.0f32);
+    for (i, tile) in map.tiles.iter_mut().enumerate() {
+        if *tile == TileType::Floor {
+            let distance_to_start = dijkstra_map.map[i];
+            // We can't get to this tile - so we'll make it a wall
+            if distance_to_start == std::f32::MAX {
+                *tile = TileType::Wall;
+            } else {
+                // If it is further away than our current exit candidate, move the exit
+                if distance_to_start > exit_tile.1 {
+                    exit_tile.0 = i;
+                    exit_tile.1 = distance_to_start;
+                }
+            }
+        }
+    }
+
+    exit_tile.0
+}
+
+/// Generates a Voronoi/cellular noise map of a region, and divides it into spawn regions.
+#[allow(clippy::map_entry)]
+pub fn generate_voronoi_spawn_regions(map: &Map, rng : &mut rltk::RandomNumberGenerator) -> HashMap<i32, Vec<usize>> {
+    let mut noise_areas : HashMap<i32, Vec<usize>> = HashMap::new();
+    let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as u64);
+    noise.set_noise_type(rltk::NoiseType::Cellular);
+    noise.set_frequency(0.08);
+    noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan);
+
+    for y in 1 .. map.height-1 {
+        for x in 1 .. map.width-1 {
+            let idx = map.xy_idx(x, y);
+            if map.tiles[idx] == TileType::Floor {
+                let cell_value_f = noise.get_noise(x as f32, y as f32) * 10240.0;
+                let cell_value = cell_value_f as i32;
+
+                if noise_areas.contains_key(&cell_value) {
+                    noise_areas.get_mut(&cell_value).unwrap().push(idx);
+                } else {
+                    noise_areas.insert(cell_value, vec![idx]);
+                }
+            }
+        }
+    }
+
+    noise_areas
+}
+
+pub fn draw_corridor(map: &mut Map, x1:i32, y1:i32, x2:i32, y2:i32) {
+    let mut x = x1;
+    let mut y = y1;
+
+    while x != x2 || y != y2 {
+        if x < x2 {
+            x += 1;
+        } else if x > x2 {
+            x -= 1;
+        } else if y < y2 {
+            y += 1;
+        } else if y > y2 {
+            y -= 1;
+        }
+
+        let idx = map.xy_idx(x, y);
+        map.tiles[idx] = TileType::Floor;
+    }
+}
+
+pub fn paint(map: &mut Map, mode: Symmetry, brush_size: i32, x: i32, y:i32) {
+    match mode {
+        Symmetry::None => apply_paint(map, brush_size, x, y),
+        Symmetry::Horizontal => {
+            let center_x = map.width / 2;
+            if x == center_x {
+                apply_paint(map, brush_size, x, y);                    
+            } else {
+                let dist_x = i32::abs(center_x - x);
+                apply_paint(map, brush_size, center_x + dist_x, y);
+                apply_paint(map, brush_size, center_x - dist_x, y);
+            }
+        }
+        Symmetry::Vertical => {
+            let center_y = map.height / 2;
+            if y == center_y {
+                apply_paint(map, brush_size, x, y);
+            } else {
+                let dist_y = i32::abs(center_y - y);
+                apply_paint(map, brush_size, x, center_y + dist_y);
+                apply_paint(map, brush_size, x, center_y - dist_y);
+            }
+        }
+        Symmetry::Both => {
+            let center_x = map.width / 2;
+            let center_y = map.height / 2;
+            if x == center_x && y == center_y {
+                apply_paint(map, brush_size, x, y);
+            } else {
+                let dist_x = i32::abs(center_x - x);
+                apply_paint(map, brush_size, center_x + dist_x, y);
+                apply_paint(map, brush_size, center_x - dist_x, y);
+                let dist_y = i32::abs(center_y - y);
+                apply_paint(map, brush_size, x, center_y + dist_y);
+                apply_paint(map, brush_size, x, center_y - dist_y);
+            }
+        }
+    }
+}
+
+fn apply_paint(map: &mut Map, brush_size: i32, x: i32, y: i32) {
+    match brush_size {
+        1 => {
+            let digger_idx = map.xy_idx(x, y);
+            map.tiles[digger_idx] = TileType::Floor;
+        }
+
+        _ => {
+            let half_brush_size = brush_size / 2;
+            for brush_y in y-half_brush_size .. y+half_brush_size {
+                for brush_x in x-half_brush_size .. x+half_brush_size {
+                    if brush_x > 1 && brush_x < map.width-1 && brush_y > 1 && brush_y < map.height-1 {
+                        let idx = map.xy_idx(brush_x, brush_y);
+                        map.tiles[idx] = TileType::Floor;
+                    }
+                }
+            }
+        }
+    }
+}

+ 229 - 0
chapter-32-voronoi/src/map_builders/dla.rs

@@ -0,0 +1,229 @@
+use super::{MapBuilder, Map,  
+    TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER,
+    remove_unreachable_areas_returning_most_distant, generate_voronoi_spawn_regions,
+    Symmetry, paint};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+use std::collections::HashMap;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum DLAAlgorithm { WalkInwards, WalkOutwards, CentralAttractor }
+
+pub struct DLABuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    history: Vec<Map>,
+    noise_areas : HashMap<i32, Vec<usize>>,
+    algorithm : DLAAlgorithm,
+    brush_size: i32,
+    symmetry: Symmetry,
+    floor_percent: f32
+}
+
+impl MapBuilder for DLABuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for area in self.noise_areas.iter() {
+            spawner::spawn_region(ecs, area.1, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl DLABuilder {
+    pub fn new(new_depth : i32) -> DLABuilder {
+        DLABuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            algorithm: DLAAlgorithm::WalkInwards,
+            brush_size: 2,
+            symmetry: Symmetry::None,
+            floor_percent: 0.25
+        }
+    }
+
+    pub fn walk_inwards(new_depth : i32) -> DLABuilder {
+        DLABuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            algorithm: DLAAlgorithm::WalkInwards,
+            brush_size: 1,
+            symmetry: Symmetry::None,
+            floor_percent: 0.25
+        }
+    }
+
+    pub fn walk_outwards(new_depth : i32) -> DLABuilder {
+        DLABuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            algorithm: DLAAlgorithm::WalkOutwards,
+            brush_size: 2,
+            symmetry: Symmetry::None,
+            floor_percent: 0.25
+        }
+    }
+
+    pub fn central_attractor(new_depth : i32) -> DLABuilder {
+        DLABuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            algorithm: DLAAlgorithm::CentralAttractor,
+            brush_size: 2,
+            symmetry: Symmetry::None,
+            floor_percent: 0.25
+        }
+    }
+
+    pub fn insectoid(new_depth : i32) -> DLABuilder {
+        DLABuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            algorithm: DLAAlgorithm::CentralAttractor,
+            brush_size: 2,
+            symmetry: Symmetry::Horizontal,
+            floor_percent: 0.25
+        }
+    }
+
+    #[allow(clippy::map_entry)]
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        // Carve a starting seed
+        self.starting_position = Position{ x: self.map.width/2, y : self.map.height/2 };
+        let start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
+        self.take_snapshot();
+        self.map.tiles[start_idx] = TileType::Floor;
+        self.map.tiles[start_idx-1] = TileType::Floor;
+        self.map.tiles[start_idx+1] = TileType::Floor;
+        self.map.tiles[start_idx-self.map.width as usize] = TileType::Floor;
+        self.map.tiles[start_idx+self.map.width as usize] = TileType::Floor;
+
+        // Random walker
+        let total_tiles = self.map.width * self.map.height;
+        let desired_floor_tiles = (self.floor_percent * total_tiles as f32) as usize;
+        let mut floor_tile_count = self.map.tiles.iter().filter(|a| **a == TileType::Floor).count();
+        while floor_tile_count  < desired_floor_tiles {
+
+            match self.algorithm {
+                DLAAlgorithm::WalkInwards => {
+                    let mut digger_x = rng.roll_dice(1, self.map.width - 3) + 1;
+                    let mut digger_y = rng.roll_dice(1, self.map.height - 3) + 1;
+                    let mut prev_x = digger_x;
+                    let mut prev_y = digger_y;
+                    let mut digger_idx = self.map.xy_idx(digger_x, digger_y);
+                    while self.map.tiles[digger_idx] == TileType::Wall {
+                        prev_x = digger_x;
+                        prev_y = digger_y;
+                        let stagger_direction = rng.roll_dice(1, 4);
+                        match stagger_direction {
+                            1 => { if digger_x > 2 { digger_x -= 1; } }
+                            2 => { if digger_x < self.map.width-2 { digger_x += 1; } }
+                            3 => { if digger_y > 2 { digger_y -=1; } }
+                            _ => { if digger_y < self.map.height-2 { digger_y += 1; } }
+                        }
+                        digger_idx = self.map.xy_idx(digger_x, digger_y);
+                    }
+                    paint(&mut self.map, self.symmetry, self.brush_size, prev_x, prev_y);
+                }
+
+                DLAAlgorithm::WalkOutwards => {
+                    let mut digger_x = self.starting_position.x;
+                    let mut digger_y = self.starting_position.y;
+                    let mut digger_idx = self.map.xy_idx(digger_x, digger_y);
+                    while self.map.tiles[digger_idx] == TileType::Floor {
+                        let stagger_direction = rng.roll_dice(1, 4);
+                        match stagger_direction {
+                            1 => { if digger_x > 2 { digger_x -= 1; } }
+                            2 => { if digger_x < self.map.width-2 { digger_x += 1; } }
+                            3 => { if digger_y > 2 { digger_y -=1; } }
+                            _ => { if digger_y < self.map.height-2 { digger_y += 1; } }
+                        }
+                        digger_idx = self.map.xy_idx(digger_x, digger_y);
+                    }
+                    paint(&mut self.map, self.symmetry, self.brush_size, digger_x, digger_y);
+                }
+
+                DLAAlgorithm::CentralAttractor => {
+                    let mut digger_x = rng.roll_dice(1, self.map.width - 3) + 1;
+                    let mut digger_y = rng.roll_dice(1, self.map.height - 3) + 1;
+                    let mut prev_x = digger_x;
+                    let mut prev_y = digger_y;
+                    let mut digger_idx = self.map.xy_idx(digger_x, digger_y);
+
+                    let mut path = rltk::line2d(
+                        rltk::LineAlg::Bresenham, 
+                        rltk::Point::new( digger_x, digger_y ), 
+                        rltk::Point::new( self.starting_position.x, self.starting_position.y )
+                    );
+
+                    while self.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() {
+                        prev_x = digger_x;
+                        prev_y = digger_y;
+                        digger_x = path[0].x;
+                        digger_y = path[0].y;
+                        path.remove(0);
+                        digger_idx = self.map.xy_idx(digger_x, digger_y);
+                    }
+                    paint(&mut self.map, self.symmetry, self.brush_size, prev_x, prev_y);
+                }
+            }
+
+            self.take_snapshot();
+
+            floor_tile_count = self.map.tiles.iter().filter(|a| **a == TileType::Floor).count();
+        }
+
+        // Find all tiles we can reach from the starting point
+        let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
+        self.take_snapshot();
+
+        // Place the stairs
+        self.map.tiles[exit_tile] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Now we build a noise map for use in spawning entities later
+        self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);
+    }    
+}

+ 238 - 0
chapter-32-voronoi/src/map_builders/drunkard.rs

@@ -0,0 +1,238 @@
+use super::{MapBuilder, Map,  
+    TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER, 
+    remove_unreachable_areas_returning_most_distant, generate_voronoi_spawn_regions,
+    paint, Symmetry};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+use std::collections::HashMap;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum DrunkSpawnMode { StartingPoint, Random }
+
+pub struct DrunkardSettings {
+    pub spawn_mode : DrunkSpawnMode,
+    pub drunken_lifetime : i32,
+    pub floor_percent: f32,
+    pub brush_size: i32,
+    pub symmetry: Symmetry
+}
+
+pub struct DrunkardsWalkBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    history: Vec<Map>,
+    noise_areas : HashMap<i32, Vec<usize>>,
+    settings : DrunkardSettings
+}
+
+impl MapBuilder for DrunkardsWalkBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for area in self.noise_areas.iter() {
+            spawner::spawn_region(ecs, area.1, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl DrunkardsWalkBuilder {
+    pub fn new(new_depth : i32, settings: DrunkardSettings) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings
+        }
+    }
+
+    pub fn open_area(new_depth : i32) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings : DrunkardSettings{
+                spawn_mode: DrunkSpawnMode::StartingPoint,
+                drunken_lifetime: 400,
+                floor_percent: 0.5,
+                brush_size: 1,
+                symmetry: Symmetry::None
+            }
+        }
+    }
+
+    pub fn open_halls(new_depth : i32) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings : DrunkardSettings{
+                spawn_mode: DrunkSpawnMode::Random,
+                drunken_lifetime: 400,
+                floor_percent: 0.5,
+                brush_size: 1,
+                symmetry: Symmetry::None
+            }
+        }
+    }
+
+    pub fn winding_passages(new_depth : i32) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings : DrunkardSettings{
+                spawn_mode: DrunkSpawnMode::Random,
+                drunken_lifetime: 100,
+                floor_percent: 0.4,
+                brush_size: 1,
+                symmetry: Symmetry::None
+            }
+        }
+    }
+
+    pub fn fat_passages(new_depth : i32) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings : DrunkardSettings{
+                spawn_mode: DrunkSpawnMode::Random,
+                drunken_lifetime: 100,
+                floor_percent: 0.4,
+                brush_size: 2,
+                symmetry: Symmetry::None
+            }
+        }
+    }
+
+    pub fn fearful_symmetry(new_depth : i32) -> DrunkardsWalkBuilder {
+        DrunkardsWalkBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new(),
+            settings : DrunkardSettings{
+                spawn_mode: DrunkSpawnMode::Random,
+                drunken_lifetime: 100,
+                floor_percent: 0.4,
+                brush_size: 1,
+                symmetry: Symmetry::Both
+            }
+        }
+    }
+    
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        // Set a central starting point
+        self.starting_position = Position{ x: self.map.width / 2, y: self.map.height / 2 };
+        let start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
+        self.map.tiles[start_idx] = TileType::Floor;
+
+        let total_tiles = self.map.width * self.map.height;
+        let desired_floor_tiles = (self.settings.floor_percent * total_tiles as f32) as usize;
+        let mut floor_tile_count = self.map.tiles.iter().filter(|a| **a == TileType::Floor).count();
+        let mut digger_count = 0;
+        let mut active_digger_count = 0;
+        while floor_tile_count  < desired_floor_tiles {
+            let mut did_something = false;
+            let mut drunk_x;
+            let mut drunk_y;
+            match self.settings.spawn_mode {
+                DrunkSpawnMode::StartingPoint => {
+                    drunk_x = self.starting_position.x;
+                    drunk_y = self.starting_position.y;
+                }
+                DrunkSpawnMode::Random => {
+                    if digger_count == 0 {
+                        drunk_x = self.starting_position.x;
+                        drunk_y = self.starting_position.y;
+                    } else {
+                        drunk_x = rng.roll_dice(1, self.map.width - 3) + 1;
+                        drunk_y = rng.roll_dice(1, self.map.height - 3) + 1;
+                    }
+                }
+            }
+            let mut drunk_life = self.settings.drunken_lifetime;
+
+            while drunk_life > 0 {
+                let drunk_idx = self.map.xy_idx(drunk_x, drunk_y);
+                if self.map.tiles[drunk_idx] == TileType::Wall {
+                    did_something = true;
+                }
+                paint(&mut self.map, self.settings.symmetry, self.settings.brush_size, drunk_x, drunk_y);
+                self.map.tiles[drunk_idx] = TileType::DownStairs;
+
+                let stagger_direction = rng.roll_dice(1, 4);
+                match stagger_direction {
+                    1 => { if drunk_x > 2 { drunk_x -= 1; } }
+                    2 => { if drunk_x < self.map.width-2 { drunk_x += 1; } }
+                    3 => { if drunk_y > 2 { drunk_y -=1; } }
+                    _ => { if drunk_y < self.map.height-2 { drunk_y += 1; } }
+                }
+
+                drunk_life -= 1;
+            }
+            if did_something { 
+                self.take_snapshot(); 
+                active_digger_count += 1;
+            }
+
+            digger_count += 1;
+            for t in self.map.tiles.iter_mut() {
+                if *t == TileType::DownStairs {
+                    *t = TileType::Floor;
+                }
+            }
+            floor_tile_count = self.map.tiles.iter().filter(|a| **a == TileType::Floor).count();
+        }
+        println!("{} dwarves gave up their sobriety, of whom {} actually found a wall.", digger_count, active_digger_count);
+
+        // Find all tiles we can reach from the starting point
+        let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
+        self.take_snapshot();
+
+        // Place the stairs
+        self.map.tiles[exit_tile] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Now we build a noise map for use in spawning entities later
+        self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);
+    }
+}

+ 257 - 0
chapter-32-voronoi/src/map_builders/maze.rs

@@ -0,0 +1,257 @@
+use super::{MapBuilder, Map,  
+    TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER,
+    remove_unreachable_areas_returning_most_distant, generate_voronoi_spawn_regions};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+use std::collections::HashMap;
+
+pub struct MazeBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    history: Vec<Map>,
+    noise_areas : HashMap<i32, Vec<usize>>
+}
+
+impl MapBuilder for MazeBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.build();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for area in self.noise_areas.iter() {
+            spawner::spawn_region(ecs, area.1, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl MazeBuilder {
+    pub fn new(new_depth : i32) -> MazeBuilder {
+        MazeBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            history: Vec::new(),
+            noise_areas : HashMap::new()
+        }
+    }
+
+    #[allow(clippy::map_entry)]
+    fn build(&mut self) {
+        let mut rng = RandomNumberGenerator::new();
+
+        // Maze gen
+        let mut maze = Grid::new((self.map.width / 2)-2, (self.map.height / 2)-2, &mut rng);
+        maze.generate_maze(self);
+
+        // Find a starting point; start at the middle and walk left until we find an open tile
+        self.starting_position = Position{ x: 2, y : 2 };
+        let start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
+        self.take_snapshot();
+
+        // Find all tiles we can reach from the starting point
+        let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
+        self.take_snapshot();
+
+        // Place the stairs
+        self.map.tiles[exit_tile] = TileType::DownStairs;
+        self.take_snapshot();
+
+        // Now we build a noise map for use in spawning entities later
+        self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);
+    }
+}
+
+/* Maze code taken under MIT from https://github.com/cyucelen/mazeGenerator/ */
+
+const TOP : usize = 0;
+const RIGHT : usize = 1;
+const BOTTOM : usize = 2;
+const LEFT : usize = 3;
+
+#[derive(Copy, Clone)]
+struct Cell {
+    row: i32,
+    column: i32,
+    walls: [bool; 4],
+    visited: bool,
+}
+
+impl Cell {
+    fn new(row: i32, column: i32) -> Cell {
+        Cell{
+            row,
+            column,
+            walls: [true, true, true, true],
+            visited: false
+        }
+    }
+
+    unsafe fn remove_walls(&mut self, next : *mut Cell) {
+        let x = self.column - (*(next)).column;
+        let y = self.row - (*(next)).row;
+
+        if x == 1 {
+            self.walls[LEFT] = false;
+            (*(next)).walls[RIGHT] = false;
+        }
+        else if x == -1 {
+            self.walls[RIGHT] = false;
+            (*(next)).walls[LEFT] = false;
+        }
+        else if y == 1 {
+            self.walls[TOP] = false;
+            (*(next)).walls[BOTTOM] = false;
+        }
+        else if y == -1 {
+            self.walls[BOTTOM] = false;
+            (*(next)).walls[TOP] = false;
+        }
+    }
+}
+
+struct Grid<'a> {
+    width: i32,
+    height: i32,
+    cells: Vec<Cell>,
+    backtrace: Vec<usize>,
+    current: usize,
+    rng : &'a mut RandomNumberGenerator
+}
+
+impl<'a> Grid<'a> {
+    fn new(width: i32, height:i32, rng: &mut RandomNumberGenerator) -> Grid {
+        let mut grid = Grid{
+            width,
+            height,
+            cells: Vec::new(),
+            backtrace: Vec::new(),
+            current: 0,
+            rng
+        };
+
+        for row in 0..height {
+            for column in 0..width {
+                grid.cells.push(Cell::new(row, column));
+            }
+        }
+
+        grid
+    }
+
+    fn calculate_index(&self, row: i32, column: i32) -> i32 {
+        if row < 0 || column < 0 || column > self.width-1 || row > self.height-1 {
+            -1
+        } else {
+            column + (row * self.width)
+        }
+    }
+
+    fn get_available_neighbors(&self) -> Vec<usize> {
+        let mut neighbors : Vec<usize> = Vec::new();
+
+        let current_row = self.cells[self.current].row;
+        let current_column = self.cells[self.current].column;
+
+        let neighbor_indices : [i32; 4] = [
+            self.calculate_index(current_row -1, current_column),
+            self.calculate_index(current_row, current_column + 1),
+            self.calculate_index(current_row + 1, current_column),
+            self.calculate_index(current_row, current_column - 1)
+        ];
+
+        for i in neighbor_indices.iter() {
+            if *i != -1 && !self.cells[*i as usize].visited {
+                neighbors.push(*i as usize);
+            }
+        }
+
+        neighbors
+    }
+
+    fn find_next_cell(&mut self) -> Option<usize> {
+        let neighbors = self.get_available_neighbors();
+        if !neighbors.is_empty() {
+            if neighbors.len() == 1 {
+                return Some(neighbors[0]);
+            } else {
+                return Some(neighbors[(self.rng.roll_dice(1, neighbors.len() as i32)-1) as usize]);
+            }
+        }
+        None
+    }
+
+    fn generate_maze(&mut self, generator : &mut MazeBuilder) {
+        let mut i = 0;
+        loop {
+            self.cells[self.current].visited = true;
+            let next = self.find_next_cell();
+
+            match next {
+                Some(next) => {
+                    self.cells[next].visited = true;
+                    self.backtrace.insert(0, self.current);
+                    unsafe {
+                        let next_cell : *mut Cell = &mut self.cells[next];
+                        let current_cell = &mut self.cells[self.current];
+                        current_cell.remove_walls(next_cell);
+                    }
+                    self.current = next;
+                }
+                None => {
+                    if !self.backtrace.is_empty() {
+                        self.current = self.backtrace[0];
+                        self.backtrace.remove(0);
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            if i % 50 == 0 {
+                self.copy_to_map(&mut generator.map);
+                generator.take_snapshot();    
+            }
+            i += 1;
+        }
+    }
+
+    fn copy_to_map(&self, map : &mut Map) {
+        // Clear the map
+        for i in map.tiles.iter_mut() { *i = TileType::Wall; }
+
+        for cell in self.cells.iter() {
+            let x = cell.column + 1;
+            let y = cell.row + 1;
+            let idx = map.xy_idx(x * 2, y * 2);
+
+            map.tiles[idx] = TileType::Floor;
+            if !cell.walls[TOP] { map.tiles[idx - map.width as usize] = TileType::Floor }
+            if !cell.walls[RIGHT] { map.tiles[idx + 1] = TileType::Floor }
+            if !cell.walls[BOTTOM] { map.tiles[idx + map.width as usize] = TileType::Floor }
+            if !cell.walls[LEFT] { map.tiles[idx - 1] = TileType::Floor }
+        }
+    }
+}

+ 49 - 0
chapter-32-voronoi/src/map_builders/mod.rs

@@ -0,0 +1,49 @@
+use super::{Map, Rect, TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER};
+mod simple_map;
+use simple_map::SimpleMapBuilder;
+mod bsp_dungeon;
+use bsp_dungeon::BspDungeonBuilder;
+mod bsp_interior;
+use bsp_interior::BspInteriorBuilder;
+mod cellular_automota;
+use cellular_automota::CellularAutomotaBuilder;
+mod drunkard;
+use drunkard::*;
+mod maze;
+use maze::*;
+mod dla;
+use dla::*;
+mod common;
+use common::*;
+use specs::prelude::*;
+
+pub trait MapBuilder {
+    fn build_map(&mut self);
+    fn spawn_entities(&mut self, ecs : &mut World);
+    fn get_map(&self) -> Map;
+    fn get_starting_position(&self) -> Position;
+    fn get_snapshot_history(&self) -> Vec<Map>;
+    fn take_snapshot(&mut self);
+}
+
+pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
+    let mut rng = rltk::RandomNumberGenerator::new();
+    let builder = rng.roll_dice(1, 14);
+    match builder {
+        1 => Box::new(BspDungeonBuilder::new(new_depth)),
+        2 => Box::new(BspInteriorBuilder::new(new_depth)),
+        3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
+        4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
+        5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
+        6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
+        7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)),
+        8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)),
+        9 => Box::new(MazeBuilder::new(new_depth)),
+        10 => Box::new(DLABuilder::walk_inwards(new_depth)),
+        11 => Box::new(DLABuilder::walk_outwards(new_depth)),
+        12 => Box::new(DLABuilder::central_attractor(new_depth)),
+        13 => Box::new(DLABuilder::insectoid(new_depth)),
+        _ => Box::new(SimpleMapBuilder::new(new_depth))
+    }
+}
+

+ 105 - 0
chapter-32-voronoi/src/map_builders/simple_map.rs

@@ -0,0 +1,105 @@
+use super::{MapBuilder, Map, Rect, apply_room_to_map, 
+    apply_horizontal_tunnel, apply_vertical_tunnel, TileType,
+    Position, spawner, SHOW_MAPGEN_VISUALIZER};
+use rltk::RandomNumberGenerator;
+use specs::prelude::*;
+
+pub struct SimpleMapBuilder {
+    map : Map,
+    starting_position : Position,
+    depth: i32,
+    rooms: Vec<Rect>,
+    history: Vec<Map>
+}
+
+impl MapBuilder for SimpleMapBuilder {
+    fn get_map(&self) -> Map {
+        self.map.clone()
+    }
+
+    fn get_starting_position(&self) -> Position {
+        self.starting_position.clone()
+    }
+
+    fn get_snapshot_history(&self) -> Vec<Map> {
+        self.history.clone()
+    }
+
+    fn build_map(&mut self)  {
+        self.rooms_and_corridors();
+    }
+
+    fn spawn_entities(&mut self, ecs : &mut World) {
+        for room in self.rooms.iter().skip(1) {
+            spawner::spawn_room(ecs, room, self.depth);
+        }
+    }
+
+    fn take_snapshot(&mut self) {
+        if SHOW_MAPGEN_VISUALIZER {
+            let mut snapshot = self.map.clone();
+            for v in snapshot.revealed_tiles.iter_mut() {
+                *v = true;
+            }
+            self.history.push(snapshot);
+        }
+    }
+}
+
+impl SimpleMapBuilder {
+    pub fn new(new_depth : i32) -> SimpleMapBuilder {
+        SimpleMapBuilder{
+            map : Map::new(new_depth),
+            starting_position : Position{ x: 0, y : 0 },
+            depth : new_depth,
+            rooms: Vec::new(),
+            history: Vec::new()
+        }
+    }
+
+    fn rooms_and_corridors(&mut self) {
+        const MAX_ROOMS : i32 = 30;
+        const MIN_SIZE : i32 = 6;
+        const MAX_SIZE : i32 = 10;
+
+        let mut rng = RandomNumberGenerator::new();
+
+        for _i in 0..MAX_ROOMS {
+            let w = rng.range(MIN_SIZE, MAX_SIZE);
+            let h = rng.range(MIN_SIZE, MAX_SIZE);
+            let x = rng.roll_dice(1, self.map.width - w - 1) - 1;
+            let y = rng.roll_dice(1, self.map.height - h - 1) - 1;
+            let new_room = Rect::new(x, y, w, h);
+            let mut ok = true;
+            for other_room in self.rooms.iter() {
+                if new_room.intersect(other_room) { ok = false }
+            }
+            if ok {
+                apply_room_to_map(&mut self.map, &new_room);
+                self.take_snapshot();
+
+                if !self.rooms.is_empty() {
+                    let (new_x, new_y) = new_room.center();
+                    let (prev_x, prev_y) = self.rooms[self.rooms.len()-1].center();
+                    if rng.range(0,1) == 1 {
+                        apply_horizontal_tunnel(&mut self.map, prev_x, new_x, prev_y);
+                        apply_vertical_tunnel(&mut self.map, prev_y, new_y, new_x);
+                    } else {
+                        apply_vertical_tunnel(&mut self.map, prev_y, new_y, prev_x);
+                        apply_horizontal_tunnel(&mut self.map, prev_x, new_x, new_y);
+                    }
+                }
+
+                self.rooms.push(new_room);
+                self.take_snapshot();
+            }
+        }
+
+        let stairs_position = self.rooms[self.rooms.len()-1].center();
+        let stairs_idx = self.map.xy_idx(stairs_position.0, stairs_position.1);
+        self.map.tiles[stairs_idx] = TileType::DownStairs;
+
+        let start_pos = self.rooms[0].center();
+        self.starting_position = Position{ x: start_pos.0, y: start_pos.1 };
+    }
+}

+ 32 - 0
chapter-32-voronoi/src/map_indexing_system.rs

@@ -0,0 +1,32 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Map, Position, BlocksTile};
+
+pub struct MapIndexingSystem {}
+
+impl<'a> System<'a> for MapIndexingSystem {
+    type SystemData = ( WriteExpect<'a, Map>,
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, BlocksTile>,
+                        Entities<'a>,);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, position, blockers, entities) = data;
+
+        map.populate_blocked();
+        map.clear_content_index();
+        for (entity, position) in (&entities, &position).join() {
+            let idx = map.xy_idx(position.x, position.y);
+
+            // If they block, update the blocking list
+            let _p : Option<&BlocksTile> = blockers.get(entity);
+            if let Some(_p) = _p {
+                map.blocked[idx] = true;
+            }
+            
+            // Push the entity to the appropriate index slot. It's a Copy
+            // type, so we don't need to clone it (we want to avoid moving it out of the ECS!)
+            map.tile_content[idx].push(entity);
+        }
+    }
+}

+ 74 - 0
chapter-32-voronoi/src/melee_combat_system.rs

@@ -0,0 +1,74 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog, MeleePowerBonus, DefenseBonus, Equipped,
+    particle_system::ParticleBuilder, Position, HungerClock, HungerState};
+
+pub struct MeleeCombatSystem {}
+
+impl<'a> System<'a> for MeleeCombatSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( Entities<'a>,
+                        WriteExpect<'a, GameLog>,
+                        WriteStorage<'a, WantsToMelee>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, MeleePowerBonus>,
+                        ReadStorage<'a, DefenseBonus>,
+                        ReadStorage<'a, Equipped>,
+                        WriteExpect<'a, ParticleBuilder>,
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, HungerClock>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, 
+            melee_power_bonuses, defense_bonuses, equipped, mut particle_builder, positions, hunger_clock) = data;
+
+        for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
+            if stats.hp > 0 {
+                let mut offensive_bonus = 0;
+                for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() {
+                    if equipped_by.owner == entity {
+                        offensive_bonus += power_bonus.power;
+                    }
+                }
+
+                let hc = hunger_clock.get(entity);
+                if let Some(hc) = hc {
+                    if hc.state == HungerState::WellFed {
+                        offensive_bonus += 1;
+                    }
+                }
+
+                let target_stats = combat_stats.get(wants_melee.target).unwrap();
+                if target_stats.hp > 0 {
+                    let target_name = names.get(wants_melee.target).unwrap();
+
+                    let mut defensive_bonus = 0;
+                    for (_item_entity, defense_bonus, equipped_by) in (&entities, &defense_bonuses, &equipped).join() {
+                        if equipped_by.owner == wants_melee.target {
+                            defensive_bonus += defense_bonus.defense;
+                        }
+                    }
+
+                    let pos = positions.get(wants_melee.target);
+                    if let Some(pos) = pos {
+                        particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
+                    }
+
+                    let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
+
+                    if damage == 0 {
+                        log.entries.insert(0, format!("{} is unable to hurt {}", &name.name, &target_name.name));
+                    } else {
+                        log.entries.insert(0, format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage));
+                        inflict_damage.insert(wants_melee.target, SufferDamage{ amount: damage }).expect("Unable to do damage");                        
+                    }
+                }
+            }
+        }
+
+        wants_melee.clear();
+    }
+}

+ 73 - 0
chapter-32-voronoi/src/monster_ai_system.rs

@@ -0,0 +1,73 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState, 
+    Confusion, particle_system::ParticleBuilder, EntityMoved};
+extern crate rltk;
+use rltk::{Point};
+
+pub struct MonsterAI {}
+
+impl<'a> System<'a> for MonsterAI {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( WriteExpect<'a, Map>,
+                        ReadExpect<'a, Point>,
+                        ReadExpect<'a, Entity>,
+                        ReadExpect<'a, RunState>,
+                        Entities<'a>,
+                        WriteStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>,
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, WantsToMelee>,
+                        WriteStorage<'a, Confusion>,
+                        WriteExpect<'a, ParticleBuilder>,
+                        WriteStorage<'a, EntityMoved>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, 
+            monster, mut position, mut wants_to_melee, mut confused, mut particle_builder,
+            mut entity_moved) = data;
+
+        if *runstate != RunState::MonsterTurn { return; }
+
+        for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() {
+            let mut can_act = true;
+
+            let is_confused = confused.get_mut(entity);
+            if let Some(i_am_confused) = is_confused {
+                i_am_confused.turns -= 1;
+                if i_am_confused.turns < 1 {
+                    confused.remove(entity);
+                }
+                can_act = false;
+
+                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), 
+                    rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 200.0);
+            }
+
+            if can_act {
+                let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
+                if distance < 1.5 {
+                    wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
+                }
+                else if viewshed.visible_tiles.contains(&*player_pos) {
+                    // Path to the player
+                    let path = rltk::a_star_search(
+                        map.xy_idx(pos.x, pos.y) as i32, 
+                        map.xy_idx(player_pos.x, player_pos.y) as i32, 
+                        &mut *map
+                    );
+                    if path.success && path.steps.len()>1 {
+                        let mut idx = map.xy_idx(pos.x, pos.y);
+                        map.blocked[idx] = false;
+                        pos.x = path.steps[1] % map.width;
+                        pos.y = path.steps[1] / map.width;
+                        entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker");
+                        idx = map.xy_idx(pos.x, pos.y);
+                        map.blocked[idx] = true;
+                        viewshed.dirty = true;
+                    }
+                }
+            }
+        }
+    }
+}

+ 73 - 0
chapter-32-voronoi/src/particle_system.rs

@@ -0,0 +1,73 @@
+use specs::prelude::*;
+use super::{ Rltk, ParticleLifetime, Position, Renderable };
+use rltk::RGB;
+
+pub fn cull_dead_particles(ecs : &mut World, ctx : &Rltk) {
+    let mut dead_particles : Vec<Entity> = Vec::new();
+    {
+        // Age out particles
+        let mut particles = ecs.write_storage::<ParticleLifetime>();
+        let entities = ecs.entities();
+        for (entity, mut particle) in (&entities, &mut particles).join() {
+            particle.lifetime_ms -= ctx.frame_time_ms;
+            if particle.lifetime_ms < 0.0 {
+                dead_particles.push(entity);
+            }
+        }                    
+    }
+    for dead in dead_particles.iter() {
+        ecs.delete_entity(*dead).expect("Particle will not die");
+    } 
+}
+
+struct ParticleRequest {
+    x: i32,
+    y: i32,
+    fg: RGB,
+    bg: RGB,
+    glyph: u8,
+    lifetime: f32
+}
+
+pub struct ParticleBuilder {
+    requests : Vec<ParticleRequest>
+}
+
+impl ParticleBuilder {
+    pub fn new() -> ParticleBuilder {
+        ParticleBuilder{ requests : Vec::new() }
+    }
+
+    pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: u8, lifetime: f32) {
+        self.requests.push(
+            ParticleRequest{
+                x, y, fg, bg, glyph, lifetime
+            }
+        );
+    }
+}
+
+pub struct ParticleSpawnSystem {}
+
+impl<'a> System<'a> for ParticleSpawnSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( 
+                        Entities<'a>,
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, Renderable>,
+                        WriteStorage<'a, ParticleLifetime>,
+                        WriteExpect<'a, ParticleBuilder>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut positions, mut renderables, mut particles, mut particle_builder) = data;
+        for new_particle in particle_builder.requests.iter() {
+            let p = entities.create();
+            positions.insert(p, Position{ x: new_particle.x, y: new_particle.y }).expect("Unable to inser position");
+            renderables.insert(p, Renderable{ fg: new_particle.fg, bg: new_particle.bg, glyph: new_particle.glyph, render_order: 0 }).expect("Unable to insert renderable");
+            particles.insert(p, ParticleLifetime{ lifetime_ms: new_particle.lifetime }).expect("Unable to insert lifetime");
+        }
+
+        particle_builder.requests.clear();
+    }
+}

+ 177 - 0
chapter-32-voronoi/src/player.rs

@@ -0,0 +1,177 @@
+extern crate rltk;
+use rltk::{VirtualKeyCode, Rltk, Point};
+extern crate specs;
+use specs::prelude::*;
+use std::cmp::{max, min};
+use super::{Position, Player, Viewshed, State, Map, RunState, CombatStats, WantsToMelee, Item,
+    gamelog::GameLog, WantsToPickupItem, TileType, Monster, HungerClock, HungerState, EntityMoved};
+
+pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
+    let mut positions = ecs.write_storage::<Position>();
+    let players = ecs.read_storage::<Player>();
+    let mut viewsheds = ecs.write_storage::<Viewshed>();
+    let entities = ecs.entities();
+    let combat_stats = ecs.read_storage::<CombatStats>();
+    let map = ecs.fetch::<Map>();
+    let mut wants_to_melee = ecs.write_storage::<WantsToMelee>();
+    let mut entity_moved = ecs.write_storage::<EntityMoved>();
+
+    for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() {
+        let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
+
+        for potential_target in map.tile_content[destination_idx].iter() {
+            let target = combat_stats.get(*potential_target);
+            if let Some(_target) = target {
+                wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed");
+                return;
+            }
+        }
+
+        if !map.blocked[destination_idx] {
+            pos.x = min(79 , max(0, pos.x + delta_x));
+            pos.y = min(49, max(0, pos.y + delta_y));
+            entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker");
+
+            viewshed.dirty = true;
+            let mut ppos = ecs.write_resource::<Point>();
+            ppos.x = pos.x;
+            ppos.y = pos.y;
+        }
+    }
+}
+
+pub fn try_next_level(ecs: &mut World) -> bool {
+    let player_pos = ecs.fetch::<Point>();
+    let map = ecs.fetch::<Map>();
+    let player_idx = map.xy_idx(player_pos.x, player_pos.y);
+    if map.tiles[player_idx] == TileType::DownStairs {
+        true
+    } else {
+        let mut gamelog = ecs.fetch_mut::<GameLog>();
+        gamelog.entries.insert(0, "There is no way down from here.".to_string());
+        false
+    }
+}
+
+fn get_item(ecs: &mut World) {
+    let player_pos = ecs.fetch::<Point>();
+    let player_entity = ecs.fetch::<Entity>();
+    let entities = ecs.entities();
+    let items = ecs.read_storage::<Item>();
+    let positions = ecs.read_storage::<Position>();
+    let mut gamelog = ecs.fetch_mut::<GameLog>();    
+
+    let mut target_item : Option<Entity> = None;
+    for (item_entity, _item, position) in (&entities, &items, &positions).join() {
+        if position.x == player_pos.x && position.y == player_pos.y {
+            target_item = Some(item_entity);
+        }
+    }
+
+    match target_item {
+        None => gamelog.entries.insert(0, "There is nothing here to pick up.".to_string()),
+        Some(item) => {
+            let mut pickup = ecs.write_storage::<WantsToPickupItem>();
+            pickup.insert(*player_entity, WantsToPickupItem{ collected_by: *player_entity, item }).expect("Unable to insert want to pickup");
+        }
+    }
+}
+
+fn skip_turn(ecs: &mut World) -> RunState {
+    let player_entity = ecs.fetch::<Entity>();
+    let viewshed_components = ecs.read_storage::<Viewshed>();
+    let monsters = ecs.read_storage::<Monster>();
+
+    let worldmap_resource = ecs.fetch::<Map>();
+
+    let mut can_heal = true;
+    let viewshed = viewshed_components.get(*player_entity).unwrap();
+    for tile in viewshed.visible_tiles.iter() {
+        let idx = worldmap_resource.xy_idx(tile.x, tile.y);
+        for entity_id in worldmap_resource.tile_content[idx].iter() {
+            let mob = monsters.get(*entity_id);
+            match mob {
+                None => {}
+                Some(_) => { can_heal = false; }
+            }
+        }
+    }
+
+    let hunger_clocks = ecs.read_storage::<HungerClock>();
+    let hc = hunger_clocks.get(*player_entity);
+    if let Some(hc) = hc {
+        match hc.state {
+            HungerState::Hungry => can_heal = false,
+            HungerState::Starving => can_heal = false,
+            _ => {}
+        }
+    }
+
+    if can_heal {
+        let mut health_components = ecs.write_storage::<CombatStats>();
+        let player_hp = health_components.get_mut(*player_entity).unwrap();
+        player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp);
+    }
+
+    RunState::PlayerTurn
+}
+
+pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
+    // Player movement
+    match ctx.key {
+        None => { return RunState::AwaitingInput } // Nothing happened
+        Some(key) => match key {
+            VirtualKeyCode::Left |
+            VirtualKeyCode::Numpad4 |
+            VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
+
+            VirtualKeyCode::Right |
+            VirtualKeyCode::Numpad6 |
+            VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
+
+            VirtualKeyCode::Up |
+            VirtualKeyCode::Numpad8 |
+            VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),
+
+            VirtualKeyCode::Down |
+            VirtualKeyCode::Numpad2 |
+            VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
+
+            // Diagonals
+            VirtualKeyCode::Numpad9 |
+            VirtualKeyCode::U => try_move_player(1, -1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad7 |
+            VirtualKeyCode::Y => try_move_player(-1, -1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad3 |
+            VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad1 |
+            VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs),
+
+            // Skip Turn
+            VirtualKeyCode::Numpad5 |
+            VirtualKeyCode::Space => return skip_turn(&mut gs.ecs),
+
+            // Level changes
+            VirtualKeyCode::Period => {
+                if try_next_level(&mut gs.ecs) {
+                    return RunState::NextLevel;
+                }
+            }
+
+            // Picking up items
+            VirtualKeyCode::G => get_item(&mut gs.ecs),
+            VirtualKeyCode::I => return RunState::ShowInventory,
+            VirtualKeyCode::D => return RunState::ShowDropItem,
+            VirtualKeyCode::R => return RunState::ShowRemoveItem,
+
+            // Save and Quit
+            VirtualKeyCode::Escape => return RunState::SaveGame,
+
+            _ => { return RunState::AwaitingInput }
+        },
+    }
+    RunState::PlayerTurn
+}

+ 49 - 0
chapter-32-voronoi/src/random_table.rs

@@ -0,0 +1,49 @@
+use rltk::RandomNumberGenerator;
+
+pub struct RandomEntry {
+    name : String,
+    weight : i32
+}
+
+impl RandomEntry {
+    pub fn new<S:ToString>(name: S, weight: i32) -> RandomEntry {
+        RandomEntry{ name: name.to_string(), weight }
+    }
+}
+
+#[derive(Default)]
+pub struct RandomTable {
+    entries : Vec<RandomEntry>,
+    total_weight : i32
+}
+
+impl RandomTable {
+    pub fn new() -> RandomTable {
+        RandomTable{ entries: Vec::new(), total_weight: 0 }
+    }
+
+    pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable {
+        if weight > 0 {
+            self.total_weight += weight;
+            self.entries.push(RandomEntry::new(name.to_string(), weight));
+        }
+        self
+    }
+
+    pub fn roll(&self, rng : &mut RandomNumberGenerator) -> String {
+        if self.total_weight == 0 { return "None".to_string(); }
+        let mut roll = rng.roll_dice(1, self.total_weight)-1;
+        let mut index : usize = 0;
+
+        while roll > 0 {
+            if roll < self.entries[index].weight {
+                return self.entries[index].name.clone();
+            }
+
+            roll -= self.entries[index].weight;
+            index += 1;
+        }
+
+        "None".to_string()
+    }
+}

+ 24 - 0
chapter-32-voronoi/src/rect.rs

@@ -0,0 +1,24 @@
+use serde::{Serialize, Deserialize};
+
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
+pub struct Rect {
+    pub x1 : i32,
+    pub x2 : i32,
+    pub y1 : i32,
+    pub y2 : i32
+}
+
+impl Rect {
+    pub fn new(x:i32, y: i32, w:i32, h:i32) -> Rect {
+        Rect{x1:x, y1:y, x2:x+w, y2:y+h}
+    }
+
+    // Returns true if this overlaps with other
+    pub fn intersect(&self, other:&Rect) -> bool {
+        self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1
+    }
+
+    pub fn center(&self) -> (i32, i32) {
+        ((self.x1 + self.x2)/2, (self.y1 + self.y2)/2)
+    }
+}

+ 18 - 0
chapter-32-voronoi/src/rex_assets.rs

@@ -0,0 +1,18 @@
+use rltk::{rex::XpFile};
+
+rltk::embedded_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
+
+pub struct RexAssets {
+    pub menu : XpFile
+}
+
+impl RexAssets {
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> RexAssets {
+        rltk::link_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
+
+        RexAssets{
+            menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap()
+        }
+    }
+}

+ 126 - 0
chapter-32-voronoi/src/saveload_system.rs

@@ -0,0 +1,126 @@
+use specs::prelude::*;
+use specs::saveload::{SimpleMarker, SimpleMarkerAllocator, SerializeComponents, DeserializeComponents, MarkedBuilder};
+use specs::error::NoError;
+use super::components::*;
+use std::fs::File;
+use std::path::Path;
+use std::fs;
+
+macro_rules! serialize_individually {
+    ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => {
+        $(
+        SerializeComponents::<NoError, SimpleMarker<SerializeMe>>::serialize(
+            &( $ecs.read_storage::<$type>(), ),
+            &$data.0,
+            &$data.1,
+            &mut $ser,
+        )
+        .unwrap();
+        )*
+    };
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn save_game(_ecs : &mut World) {
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn save_game(ecs : &mut World) {
+    // Create helper
+    let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
+    let savehelper = ecs
+        .create_entity()
+        .with(SerializationHelper{ map : mapcopy })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+
+    // Actually serialize
+    {
+        let data = ( ecs.entities(), ecs.read_storage::<SimpleMarker<SerializeMe>>() );
+
+        let writer = File::create("./savegame.json").unwrap();
+        let mut serializer = serde_json::Serializer::new(writer);
+        serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster, 
+            Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
+            AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
+            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus,
+            WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden,
+            EntryTrigger, EntityMoved, SingleActivation
+        );
+    }
+
+    // Clean up
+    ecs.delete_entity(savehelper).expect("Crash on cleanup");
+}
+
+pub fn does_save_exist() -> bool {
+    Path::new("./savegame.json").exists()
+}
+
+macro_rules! deserialize_individually {
+    ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
+        $(
+        DeserializeComponents::<NoError, _>::deserialize(
+            &mut ( &mut $ecs.write_storage::<$type>(), ),
+            &$data.0, // entities
+            &mut $data.1, // marker
+            &mut $data.2, // allocater
+            &mut $de,
+        )
+        .unwrap();
+        )*
+    };
+}
+
+pub fn load_game(ecs: &mut World) {
+    {
+        // Delete everything
+        let mut to_delete = Vec::new();
+        for e in ecs.entities().join() {
+            to_delete.push(e);
+        }
+        for del in to_delete.iter() {
+            ecs.delete_entity(*del).expect("Deletion failed");
+        }
+    }
+
+    let data = fs::read_to_string("./savegame.json").unwrap();
+    let mut de = serde_json::Deserializer::from_str(&data);
+
+    {
+        let mut d = (&mut ecs.entities(), &mut ecs.write_storage::<SimpleMarker<SerializeMe>>(), &mut SimpleMarkerAllocator::<SerializeMe>::new());
+
+        deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, 
+            Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
+            AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
+            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus,
+            WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden,
+            EntryTrigger, EntityMoved, SingleActivation
+        );
+    }
+
+    let mut deleteme : Option<Entity> = None;
+    {
+        let entities = ecs.entities();
+        let helper = ecs.read_storage::<SerializationHelper>();
+        let player = ecs.read_storage::<Player>();
+        let position = ecs.read_storage::<Position>();
+        for (e,h) in (&entities, &helper).join() {
+            let mut worldmap = ecs.write_resource::<super::map::Map>();
+            *worldmap = h.map.clone();
+            worldmap.tile_content = vec![Vec::new(); super::map::MAPCOUNT];
+            deleteme = Some(e);
+        }
+        for (e,_p,pos) in (&entities, &player, &position).join() {
+            let mut ppos = ecs.write_resource::<rltk::Point>();
+            *ppos = rltk::Point::new(pos.x, pos.y);
+            let mut player_resource = ecs.write_resource::<Entity>();
+            *player_resource = e;
+        }
+    }
+    ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper");
+}
+
+pub fn delete_save() {
+    if Path::new("./savegame.json").exists() { std::fs::remove_file("./savegame.json").expect("Unable to delete file"); } 
+}

+ 330 - 0
chapter-32-voronoi/src/spawner.rs

@@ -0,0 +1,330 @@
+extern crate rltk;
+use rltk::{ RGB, RandomNumberGenerator };
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, Player, Renderable, Name, Position, Viewshed, Monster, BlocksTile, Rect, Item, 
+    Consumable, Ranged, ProvidesHealing, map::MAPWIDTH, InflictsDamage, AreaOfEffect, Confusion, SerializeMe,
+    random_table::RandomTable, EquipmentSlot, Equippable, MeleePowerBonus, DefenseBonus, HungerClock,
+    HungerState, ProvidesFood, MagicMapper, Hidden, EntryTrigger, SingleActivation, Map, TileType };
+use crate::specs::saveload::{MarkedBuilder, SimpleMarker};
+use std::collections::HashMap;
+
+/// Spawns the player and returns his/her entity object.
+pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
+    ecs
+        .create_entity()
+        .with(Position { x: player_x, y: player_y })
+        .with(Renderable {
+            glyph: rltk::to_cp437('@'),
+            fg: RGB::named(rltk::YELLOW),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 0
+        })
+        .with(Player{})
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Name{name: "Player".to_string() })
+        .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
+        .with(HungerClock{ state: HungerState::WellFed, duration: 20 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build()
+}
+
+const MAX_MONSTERS : i32 = 4;
+
+fn room_table(map_depth: i32) -> RandomTable {
+    RandomTable::new()
+        .add("Goblin", 10)
+        .add("Orc", 1 + map_depth)
+        .add("Health Potion", 7)
+        .add("Fireball Scroll", 2 + map_depth)
+        .add("Confusion Scroll", 2 + map_depth)
+        .add("Magic Missile Scroll", 4)
+        .add("Dagger", 3)
+        .add("Shield", 3)
+        .add("Longsword", map_depth - 1)
+        .add("Tower Shield", map_depth - 1)
+        .add("Rations", 10)
+        .add("Magic Mapping Scroll", 2)
+        .add("Bear Trap", 5)
+}
+
+/// Fills a room with stuff!
+pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) {
+    let mut possible_targets : Vec<usize> = Vec::new();
+    { // Borrow scope - to keep access to the map separated
+        let map = ecs.fetch::<Map>();
+        for y in room.y1 + 1 .. room.y2 {
+            for x in room.x1 + 1 .. room.x2 {
+                let idx = map.xy_idx(x, y);
+                if map.tiles[idx] == TileType::Floor {
+                    possible_targets.push(idx);
+                }
+            }
+        }
+    }
+
+    spawn_region(ecs, &possible_targets, map_depth);
+}
+
+/// Fills a region with stuff!
+pub fn spawn_region(ecs: &mut World, area : &[usize], map_depth: i32) {
+    let spawn_table = room_table(map_depth);
+    let mut spawn_points : HashMap<usize, String> = HashMap::new();
+    let mut areas : Vec<usize> = Vec::from(area);
+
+    // Scope to keep the borrow checker happy
+    {
+        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+        let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3);
+        if num_spawns == 0 { return; }
+
+        for _i in 0 .. num_spawns {
+            let array_index = if areas.len() == 1 { 0usize } else { (rng.roll_dice(1, areas.len() as i32)-1) as usize };
+
+            let map_idx = areas[array_index];
+            spawn_points.insert(map_idx, spawn_table.roll(&mut rng));
+            areas.remove(array_index);
+        }
+    }
+
+    // Actually spawn the monsters
+    for spawn in spawn_points.iter() {
+        spawn_entity(ecs, &spawn);
+    }
+}
+
+/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)
+fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
+    let x = (*spawn.0 % MAPWIDTH) as i32;
+    let y = (*spawn.0 / MAPWIDTH) as i32;
+
+    match spawn.1.as_ref() {
+        "Goblin" => goblin(ecs, x, y),
+        "Orc" => orc(ecs, x, y),
+        "Health Potion" => health_potion(ecs, x, y),
+        "Fireball Scroll" => fireball_scroll(ecs, x, y),
+        "Confusion Scroll" => confusion_scroll(ecs, x, y),
+        "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
+        "Dagger" => dagger(ecs, x, y),
+        "Shield" => shield(ecs, x, y),
+        "Longsword" => longsword(ecs, x, y),
+        "Tower Shield" => tower_shield(ecs, x, y),
+        "Rations" => rations(ecs, x, y),
+        "Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
+        "Bear Trap" => bear_trap(ecs, x, y),
+        _ => {}
+    }
+}
+
+fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); }
+fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('g'), "Goblin"); }
+
+fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : u8, name : S) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph,
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 1
+        })
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Monster{})
+        .with(Name{ name : name.to_string() })
+        .with(BlocksTile{})
+        .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn health_potion(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('¡'),
+            fg: RGB::named(rltk::MAGENTA),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Health Potion".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(ProvidesHealing{ heal_amount: 8 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::CYAN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Magic Missile Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(InflictsDamage{ damage: 20 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::ORANGE),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Fireball Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(InflictsDamage{ damage: 20 })
+        .with(AreaOfEffect{ radius: 3 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn confusion_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::PINK),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Confusion Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(Confusion{ turns: 4 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn dagger(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('/'),
+            fg: RGB::named(rltk::CYAN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Dagger".to_string() })
+        .with(Item{})
+        .with(Equippable{ slot: EquipmentSlot::Melee })
+        .with(MeleePowerBonus{ power: 2 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn shield(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('('),
+            fg: RGB::named(rltk::CYAN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Shield".to_string() })
+        .with(Item{})
+        .with(Equippable{ slot: EquipmentSlot::Shield })
+        .with(DefenseBonus{ defense: 1 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn longsword(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('/'),
+            fg: RGB::named(rltk::YELLOW),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Longsword".to_string() })
+        .with(Item{})
+        .with(Equippable{ slot: EquipmentSlot::Melee })
+        .with(MeleePowerBonus{ power: 4 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn tower_shield(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('('),
+            fg: RGB::named(rltk::YELLOW),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Tower Shield".to_string() })
+        .with(Item{})
+        .with(Equippable{ slot: EquipmentSlot::Shield })
+        .with(DefenseBonus{ defense: 3 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn rations(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('%'),
+            fg: RGB::named(rltk::GREEN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Rations".to_string() })
+        .with(Item{})
+        .with(ProvidesFood{})
+        .with(Consumable{})
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn magic_mapping_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::CYAN3),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Scroll of Magic Mapping".to_string() })
+        .with(Item{})
+        .with(MagicMapper{})
+        .with(Consumable{})
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
+fn bear_trap(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('^'),
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Bear Trap".to_string() })
+        .with(Hidden{})
+        .with(EntryTrigger{})
+        .with(InflictsDamage{ damage: 6 })
+        .with(SingleActivation{})
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}

+ 72 - 0
chapter-32-voronoi/src/trigger_system.rs

@@ -0,0 +1,72 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{EntityMoved, Position, EntryTrigger, Hidden, Map, Name, gamelog::GameLog, 
+    InflictsDamage, particle_system::ParticleBuilder, SufferDamage, SingleActivation};
+
+pub struct TriggerSystem {}
+
+impl<'a> System<'a> for TriggerSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Map>,
+                        WriteStorage<'a, EntityMoved>,
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, EntryTrigger>,
+                        WriteStorage<'a, Hidden>,
+                        ReadStorage<'a, Name>,
+                        Entities<'a>,
+                        WriteExpect<'a, GameLog>,
+                        ReadStorage<'a, InflictsDamage>,
+                        WriteExpect<'a, ParticleBuilder>,
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, SingleActivation>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (map, mut entity_moved, position, entry_trigger, mut hidden, 
+            names, entities, mut log, inflicts_damage, mut particle_builder,
+            mut inflict_damage, single_activation) = data;
+
+        // Iterate the entities that moved and their final position
+        let mut remove_entities : Vec<Entity> = Vec::new();
+        for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() {
+            let idx = map.xy_idx(pos.x, pos.y);
+            for entity_id in map.tile_content[idx].iter() {
+                if entity != *entity_id { // Do not bother to check yourself for being a trap!
+                    let maybe_trigger = entry_trigger.get(*entity_id);
+                    match maybe_trigger {
+                        None => {},
+                        Some(_trigger) => {
+                            // We triggered it                            
+                            let name = names.get(*entity_id);
+                            if let Some(name) = name {
+                                log.entries.insert(0, format!("{} triggers!", &name.name));
+                            }
+
+                            hidden.remove(*entity_id); // The trap is no longer hidden
+
+                            // If the trap is damage inflicting, do it
+                            let damage = inflicts_damage.get(*entity_id);
+                            if let Some(damage) = damage {
+                                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
+                                inflict_damage.insert(entity, SufferDamage{ amount: damage.damage }).expect("Unable to do damage");
+                            }
+
+                            // If it is single activation, it needs to be removed
+                            let sa = single_activation.get(*entity_id);
+                            if let Some(_sa) = sa {
+                                remove_entities.push(*entity_id);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Remove any single activation traps
+        for trap in remove_entities.iter() {
+            entities.delete(*trap).expect("Unable to delete trap");
+        }
+
+        // Remove all entity movement markers
+        entity_moved.clear();
+    }
+}

+ 57 - 0
chapter-32-voronoi/src/visibility_system.rs

@@ -0,0 +1,57 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Position, Map, Player, Hidden, gamelog::GameLog, Name};
+extern crate rltk;
+use rltk::{field_of_view, Point};
+
+pub struct VisibilitySystem {}
+
+impl<'a> System<'a> for VisibilitySystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( WriteExpect<'a, Map>,
+                        Entities<'a>,
+                        WriteStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, Player>,
+                        WriteStorage<'a, Hidden>,
+                        WriteExpect<'a, rltk::RandomNumberGenerator>,
+                        WriteExpect<'a, GameLog>,
+                        ReadStorage<'a, Name>,);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, entities, mut viewshed, pos, player, 
+            mut hidden, mut rng, mut log, names) = data;
+
+        for (ent,viewshed,pos) in (&entities, &mut viewshed, &pos).join() {
+            if viewshed.dirty {
+                viewshed.dirty = false;
+                viewshed.visible_tiles = field_of_view(Point::new(pos.x, pos.y), viewshed.range, &*map);
+
+                // If this is the player, reveal what they can see
+                let _p : Option<&Player> = player.get(ent);
+                if let Some(_p) = _p {
+                    for t in map.visible_tiles.iter_mut() { *t = false };
+                    for vis in viewshed.visible_tiles.iter() {
+                        let idx = map.xy_idx(vis.x, vis.y);
+                        map.revealed_tiles[idx] = true;
+                        map.visible_tiles[idx] = true;
+
+                        // Chance to reveal hidden things
+                        for e in map.tile_content[idx].iter() {
+                            let maybe_hidden = hidden.get(*e);
+                            if let Some(_maybe_hidden) = maybe_hidden {
+                                if rng.roll_dice(1,24)==1 {
+                                    let name = names.get(*e);
+                                    if let Some(name) = name {
+                                        log.entries.insert(0, format!("You spotted a {}.", &name.name));
+                                    }
+                                    hidden.remove(*e);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}