Browse Source

Add chapter 6 - monsters who yell a lot.

Herbert Wolverson 4 years ago
parent
commit
c5f14dc9ed

+ 9 - 0
Cargo.lock

@@ -160,6 +160,15 @@ dependencies = [
  "specs-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "chapter-06-monsters"
+version = "0.1.0"
+dependencies = [
+ "rltk 0.2.5 (git+https://github.com/thebracket/rltk_rs)",
+ "specs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "specs-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "cloudabi"
 version = "0.0.3"

+ 2 - 1
Cargo.toml

@@ -17,5 +17,6 @@ members = [
     "chapter-02-helloecs", 
     "chapter-03-walkmap", 
     "chapter-04-newmap",
-    "chapter-05-fov"
+    "chapter-05-fov",
+    "chapter-06-monsters"
     ]

+ 1 - 0
book/src/SUMMARY.md

@@ -5,3 +5,4 @@
 - [Chapter 3 - Walking A Map](./chapter_3.md)
 - [Chapter 4 - A More Interesting Map](./chapter_4.md)
 - [Chapter 5 - Field of View](./chapter_5.md)
+- [Chapter 6 - Monsters](./chapter_6.md)

BIN
book/src/c6-s1.png


BIN
book/src/c6-s2.gif


BIN
book/src/c6-s3.gif


+ 340 - 0
book/src/chapter_6.md

@@ -0,0 +1,340 @@
+# Chapter 6 - Monsters
+
+---
+
+***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).*
+
+---
+
+A roguelike with no monsters is quite unusual, so lets add some! The good news is that we've already done some of the work for this: we can render them, and we can calculate what they can see. We'll build on the source from the previous chapter, and get some harmless monsters into play.
+
+# Rendering a monster in the center of each room
+
+We can simply add a `Renderable` component for each monster (we'll also add a `Viewshed` since we'll use it later). In our `main` function (in `main.rs`), add the following:
+
+```rust
+for room in map.rooms.iter().skip(1) {
+    let (x,y) = room.center();
+    gs.ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('g'),
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+        })
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .build();
+}
+
+gs.ecs.insert(map);
+```
+
+Notice the `skip(1)` to ignore the first room - we don't want the player starting with a mob on top of him/her/it! Running this (with `cargo run`) produces something like this:
+
+![Screenshot](./c6-s1.png)
+
+That's a really good start! However, we're rendering monsters even if we can't see them. We probably only want to render the ones we can see. We can do this by modifying our render loop:
+
+```rust
+let positions = self.ecs.read_storage::<Position>();
+let renderables = self.ecs.read_storage::<Renderable>();
+let map = self.ecs.fetch::<Map>();
+
+for (pos, render) in (&positions, &renderables).join() {
+    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) }
+}
+```
+
+We get the map from the ECS, and use it to obtain an index - and check if the tile is visible. If it is - we render the renderable. There's no need for a special case for the player - since they can generally be expected to see themselves! The result is pretty good:
+
+![Screenshot](./c6-s2.gif)
+
+# Add some monster variety
+
+It's rather dull to only have one monster type, so we'll amend our monster spawner to be able to create `g`oblins and `o`rcs.
+
+Here's the spawner code:
+
+```rust
+let mut rng = rltk::RandomNumberGenerator::new();
+for room in map.rooms.iter().skip(1) {
+    let (x,y) = room.center();
+
+    let glyph : u8;
+    let roll = rng.roll_dice(1, 2);
+    match roll {
+        1 => { glyph = rltk::to_cp437('g') }
+        _ => { glyph = rltk::to_cp437('o') }
+    }
+
+    gs.ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: glyph,
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+        })
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .build();
+}
+```
+
+Obviously, when we start adding in combat we'll want more variety - but it's a good start. Run the program (`cargo run`), and you'll see a roughly 50/50 split between orcs and goblins.
+
+# Making the monsters think
+
+Now to start making the monsters think! For now, they won't actually *do* much, beyond pondering their lonely existence. We should start by adding a tag component to indicate that an entity *is* a monster. In `components.rs` we add a simple struct:
+
+```rust
+#[derive(Component, Debug)]
+pub struct Monster {}
+```
+
+Of course, we need to register it in `main.rs`: `gs.ecs.register::<Monster>();`. We should also amend our spawning code to apply it to monsters:
+
+```rust
+gs.ecs.create_entity()
+    .with(Position{ x, y })
+    .with(Renderable{
+        glyph: glyph,
+        fg: RGB::named(rltk::RED),
+        bg: RGB::named(rltk::BLACK),
+    })
+    .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+    .with(Monster{})
+    .build();
+```
+
+Now we make a system for monster thought. We'll make a new file, `monster_ai_system.rs`. We'll give it some basically non-existent intelligence:
+
+```rust
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Position, Map, Monster};
+extern crate rltk;
+use rltk::{field_of_view, Point};
+
+pub struct MonsterAI {}
+
+impl<'a> System<'a> for MonsterAI {
+    type SystemData = ( ReadStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, Monster>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (viewshed, pos, monster) = data;
+
+        for (viewshed,pos,_monster) in (&viewshed, &pos, &monster).join() {
+            println!("Monster considers their own existence");
+        }
+    }
+}
+```
+
+We'll also extend the system builder in `main.rs` to call it:
+
+```rust
+systems : DispatcherBuilder::new()
+    .with(VisibilitySystem{}, "visibility_system", &[])
+    .with(MonsterAI{}, "monster_ai", &["visibility_system"])
+    .build()
+```
+
+The `&["visibility_system"]` is new - it says "run this after visibility, since we depend upon its results. At this point, we don't actually care - but we will, so we'll put it in there now.
+
+If you `cargo run` your project now, it will be very slow - and your console will fill up with "Monster considers their own existence". The AI is running - but it's running every tick!
+
+# Turn-based game, in a tick-based world
+
+To prevent this - and make a turn-based game - we introduce a new concept to the game state. The game is either "running" or "waiting for input" - so we make an `enum` to handle this:
+
+```rust
+pub enum RunState { Paused, Running }
+```
+
+We add it to the State type:
+
+```rust
+pub struct State {
+    pub ecs: World,
+    pub systems: Dispatcher<'static, 'static>,
+    pub runstate : RunState
+}
+```
+
+We also amend our State creator to include a `runstate: RunState::Running`.
+
+Now, we change our `tick` function to only run the simulation when the game isn't paused - and otherwise to ask for user input:
+
+```rust
+if self.runstate == RunState::Running {
+    self.systems.dispatch(&self.ecs);
+    self.runstate = RunState::Paused;
+} else {
+    self.runstate = player_input(self, ctx);
+}
+```
+
+As you can see, `player_input` now returns a state. Here's the new code for it:
+
+```rust
+pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
+    // Player movement
+    match ctx.key {
+        None => { return RunState::Paused } // Nothing happened
+        Some(key) => match key {
+            VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::Numpad6 => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::Numpad8 => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
+            VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs),
+            VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
+            _ => { return RunState::Paused }
+        },
+    }
+    RunState::Running
+}
+```
+
+If you launch `cargo run` now, the game is back up to speed - and the monsters only think about what to do when you move. That's a basic turn-based tick loop!
+
+# Quiet monsters until they see you
+
+You *could* let monsters think every time anything moves (and you probably will when you get into deeper simulation), but for now lets quiet them down a bit - and have them react if they can see the player.
+
+It's *highly* likely that systems will often want to know where the player is - so lets add that as a resource. In `main.rs`, one line puts it in (I don't recommend doing this for non-player entities; there are only so many resources available - but the player is one we use over and over again):
+
+```rust
+gs.ecs.insert(Point::new(player_x, player_y));
+```
+
+In `player.rs`, `try_move_player()`, update the resource when the player moves:
+
+```rust
+let mut ppos = ecs.write_resource::<Point>();
+ppos.x = pos.x;
+ppos.y = pos.y;
+```
+
+We can then use that in our `monster_ai_system`. Here's a working version:
+
+```rust
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Monster};
+extern crate rltk;
+use rltk::{Point};
+
+pub struct MonsterAI {}
+
+impl<'a> System<'a> for MonsterAI {
+    type SystemData = ( ReadExpect<'a, Point>,
+                        ReadStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_pos, viewshed, monster) = data;
+
+        for (viewshed,_monster) in (&viewshed, &monster).join() {
+            if viewshed.visible_tiles.contains(&*player_pos) {
+                println!("Monster shouts insults");
+            }
+        }
+    }
+}
+```
+
+If you `cargo run` this, you'll be able to move around - and your console will gain "Monster shouts insults" from time to time when a monster can see you.
+
+# Differentiating our monsters
+
+Monsters should have names, so we know who is yelling at us! So we create a new component, `Name`. In `components.rs`, we add:
+
+```rust
+#[derive(Component, Debug)]
+pub struct Name {
+    pub name : String
+}
+```
+
+We also register it in `main.rs`, which you should be comfortable with by now! We'll also add some commands to add names to our monsters and the player. So our monster spawner looks like this:
+
+```rust
+for (i,room) in map.rooms.iter().skip(1).enumerate() {
+    let (x,y) = room.center();
+
+    let glyph : u8;
+    let name : String;
+    let roll = rng.roll_dice(1, 2);
+    match roll {
+        1 => { glyph = rltk::to_cp437('g'); name = "Goblin".to_string(); }
+        _ => { glyph = rltk::to_cp437('o'); name = "Orc".to_string(); }
+    }
+
+    gs.ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: glyph,
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+        })
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Monster{})
+        .with(Name{ name: format!("{} #{}", &name, i) })
+        .build();
+}
+```
+
+Now we adjust the `monster_ai_system` to include the monster's name. The new AI looks like this:
+
+```rust
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Monster, Name};
+extern crate rltk;
+use rltk::{Point};
+
+pub struct MonsterAI {}
+
+impl<'a> System<'a> for MonsterAI {
+    type SystemData = ( ReadExpect<'a, Point>,
+                        ReadStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>,
+                        ReadStorage<'a, Name>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_pos, viewshed, monster, name) = data;
+
+        for (viewshed,_monster,name) in (&viewshed, &monster, &name).join() {
+            if viewshed.visible_tiles.contains(&*player_pos) {
+                println!("{} shouts insults", name.name);
+            }
+        }
+    }
+}
+```
+
+If you `cargo run` the project, you now see things like *Goblin #9 shouts insults* - so you can tell who is shouting.
+
+![Screenshot](./c6-s3.gif)
+
+And that's a wrap for chapter 6; we've added a variety of foul-mouthed monsters to hurl insults at your fragile ego!
+
+**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-06-monsters)**
+
+---
+
+Copyright (C) 2019, Herbert Wolverson.
+
+---

+ 12 - 0
chapter-06-monsters/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "chapter-06-monsters"
+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" }
+specs = "0.15.0"
+specs-derive = "0.4.0"

+ 36 - 0
chapter-06-monsters/src/components.rs

@@ -0,0 +1,36 @@
+extern crate specs;
+use specs::prelude::*;
+extern crate specs_derive;
+extern crate rltk;
+use rltk::{RGB};
+
+#[derive(Component)]
+pub struct Position {
+    pub x: i32,
+    pub y: i32,
+}
+
+#[derive(Component)]
+pub struct Renderable {
+    pub glyph: u8,
+    pub fg: RGB,
+    pub bg: RGB,
+}
+ 
+#[derive(Component, Debug)]
+pub struct Player {}
+
+#[derive(Component)]
+pub struct Viewshed {
+    pub visible_tiles : Vec<rltk::Point>,
+    pub range : i32,
+    pub dirty : bool
+}
+
+#[derive(Component, Debug)]
+pub struct Monster {}
+
+#[derive(Component, Debug)]
+pub struct Name {
+    pub name : String
+}

+ 115 - 0
chapter-06-monsters/src/main.rs

@@ -0,0 +1,115 @@
+extern crate rltk;
+use rltk::{Console, GameState, Rltk, RGB, Point};
+extern crate specs;
+use specs::prelude::*;
+#[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;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum RunState { Paused, Running }
+
+pub struct State {
+    pub ecs: World,
+    pub systems: Dispatcher<'static, 'static>,
+    pub runstate : RunState
+}
+
+impl GameState for State {
+    fn tick(&mut self, ctx : &mut Rltk) {
+        ctx.cls();
+        
+        if self.runstate == RunState::Running {
+            self.systems.dispatch(&self.ecs);
+            self.runstate = RunState::Paused;
+        } else {
+            self.runstate = player_input(self, ctx);
+        }
+
+        draw_map(&self.ecs, ctx);
+
+        let positions = self.ecs.read_storage::<Position>();
+        let renderables = self.ecs.read_storage::<Renderable>();
+        let map = self.ecs.fetch::<Map>();
+
+        for (pos, render) in (&positions, &renderables).join() {
+            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) }
+        }
+    }
+}
+
+fn main() {
+    let context = Rltk::init_simple8x8(80, 50, "Hello Rust World", "../resources");
+    let mut gs = State {
+        ecs: World::new(),
+        systems : DispatcherBuilder::new()
+            .with(VisibilitySystem{}, "visibility_system", &[])
+            .with(MonsterAI{}, "monster_ai", &["visibility_system"])
+            .build(),
+        runstate : RunState::Running
+    };
+    gs.ecs.register::<Position>();
+    gs.ecs.register::<Renderable>();
+    gs.ecs.register::<Player>();
+    gs.ecs.register::<Viewshed>();
+    gs.ecs.register::<Monster>();
+    gs.ecs.register::<Name>();
+
+    let map : Map = Map::new_map_rooms_and_corridors();
+    let (player_x, player_y) = map.rooms[0].center();
+
+    gs.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),
+        })
+        .with(Player{})
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Name{name: "Player".to_string() })
+        .build();
+
+    let mut rng = rltk::RandomNumberGenerator::new();
+    for (i,room) in map.rooms.iter().skip(1).enumerate() {
+        let (x,y) = room.center();
+
+        let glyph : u8;
+        let name : String;
+        let roll = rng.roll_dice(1, 2);
+        match roll {
+            1 => { glyph = rltk::to_cp437('g'); name = "Goblin".to_string(); }
+            _ => { glyph = rltk::to_cp437('o'); name = "Orc".to_string(); }
+        }
+
+        gs.ecs.create_entity()
+            .with(Position{ x, y })
+            .with(Renderable{
+                glyph: glyph,
+                fg: RGB::named(rltk::RED),
+                bg: RGB::named(rltk::BLACK),
+            })
+            .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+            .with(Monster{})
+            .with(Name{ name: format!("{} #{}", &name, i) })
+            .build();
+    }
+
+    gs.ecs.insert(map);
+    gs.ecs.insert(Point::new(player_x, player_y));
+
+    rltk::main_loop(context, gs);
+}

+ 164 - 0
chapter-06-monsters/src/map.rs

@@ -0,0 +1,164 @@
+extern crate rltk;
+use rltk::{ RGB, Rltk, Console, RandomNumberGenerator, BaseMap, Algorithm2D, Point };
+use super::{Rect};
+use std::cmp::{max, min};
+extern crate specs;
+use specs::prelude::*;
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum TileType {
+    Wall, Floor
+}
+
+#[derive(Default)]
+pub struct Map {
+    pub tiles : Vec<TileType>,
+    pub rooms : Vec<Rect>,
+    pub width : i32,
+    pub height : i32,
+    pub revealed_tiles : Vec<bool>,
+    pub visible_tiles : Vec<bool>
+}
+
+impl Map {
+    pub fn xy_idx(&self, x: i32, y: i32) -> usize {
+        (y as usize * self.width as usize) + x as usize
+    }
+
+    fn apply_room_to_map(&mut self, room : &Rect) {
+        for y in room.y1 +1 ..= room.y2 {
+            for x in room.x1 + 1 ..= room.x2 {
+                let idx = self.xy_idx(x, y);
+                self.tiles[idx] = TileType::Floor;
+            }
+        }
+    }
+
+    fn apply_horizontal_tunnel(&mut self, x1:i32, x2:i32, y:i32) {
+        for x in min(x1,x2) ..= max(x1,x2) {
+            let idx = self.xy_idx(x, y);
+            if idx > 0 && idx < self.width as usize * self.height as usize {
+                self.tiles[idx as usize] = TileType::Floor;
+            }
+        }
+    }
+
+    fn apply_vertical_tunnel(&mut self, y1:i32, y2:i32, x:i32) {
+        for y in min(y1,y2) ..= max(y1,y2) {
+            let idx = self.xy_idx(x, y);
+            if idx > 0 && idx < self.width as usize * self.height as usize {
+                self.tiles[idx as usize] = TileType::Floor;
+            }
+        }
+    }
+
+    /// Makes a new map using the algorithm from http://rogueliketutorials.com/tutorials/tcod/part-3/
+    /// This gives a handful of random rooms and corridors joining them together.
+    pub fn new_map_rooms_and_corridors() -> Map {
+        let mut map = Map{
+            tiles : vec![TileType::Wall; 80*50],
+            rooms : Vec::new(),
+            width : 80,
+            height: 50,
+            revealed_tiles : vec![false; 80*50],
+            visible_tiles : vec![false; 80*50]
+        };
+
+        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, map.width - w - 1) - 1;
+            let y = rng.roll_dice(1, map.height - h - 1) - 1;
+            let new_room = Rect::new(x, y, w, h);
+            let mut ok = true;
+            for other_room in map.rooms.iter() {
+                if new_room.intersect(other_room) { ok = false }
+            }
+            if ok {
+                map.apply_room_to_map(&new_room);
+
+                if !map.rooms.is_empty() {
+                    let (new_x, new_y) = new_room.center();
+                    let (prev_x, prev_y) = map.rooms[map.rooms.len()-1].center();
+                    if rng.range(0,1) == 1 {
+                        map.apply_horizontal_tunnel(prev_x, new_x, prev_y);
+                        map.apply_vertical_tunnel(prev_y, new_y, new_x);
+                    } else {
+                        map.apply_vertical_tunnel(prev_y, new_y, prev_x);
+                        map.apply_horizontal_tunnel(prev_x, new_x, new_y);
+                    }
+                }
+
+                map.rooms.push(new_room);
+            }
+        }
+
+        map
+    }
+}
+
+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)> {
+        Vec::new()
+    }
+
+    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::distance2d(rltk::DistanceAlg::Pythagoras, 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 }
+    }
+}
+
+pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
+    let map = ecs.fetch::<Map>();
+
+    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;
+            match tile {
+                TileType::Floor => {
+                    glyph = rltk::to_cp437('.');
+                    fg = RGB::from_f32(0.0, 0.5, 0.5);
+                }
+                TileType::Wall => {
+                    glyph = rltk::to_cp437('#');
+                    fg = RGB::from_f32(0., 1.0, 0.);
+                }
+            }
+            if !map.visible_tiles[idx] { fg = fg.to_greyscale() }
+            ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
+        }
+
+        // Move the coordinates
+        x += 1;
+        if x > 79 {
+            x = 0;
+            y += 1;
+        }
+    }
+}

+ 24 - 0
chapter-06-monsters/src/monster_ai_system.rs

@@ -0,0 +1,24 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Monster, Name};
+extern crate rltk;
+use rltk::{Point};
+
+pub struct MonsterAI {}
+
+impl<'a> System<'a> for MonsterAI {
+    type SystemData = ( ReadExpect<'a, Point>,
+                        ReadStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>,
+                        ReadStorage<'a, Name>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_pos, viewshed, monster, name) = data;
+
+        for (viewshed,_monster,name) in (&viewshed, &monster, &name).join() {
+            if viewshed.visible_tiles.contains(&*player_pos) {
+                println!("{} shouts insults", name.name);
+            }
+        }
+    }
+}

+ 53 - 0
chapter-06-monsters/src/player.rs

@@ -0,0 +1,53 @@
+extern crate rltk;
+use rltk::{VirtualKeyCode, Rltk, Point};
+extern crate specs;
+use specs::prelude::*;
+use super::{Position, Player, Viewshed, TileType, State, Map, RunState};
+
+pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
+    let mut positions = ecs.write_storage::<Position>();
+    let mut players = ecs.write_storage::<Player>();
+    let mut viewsheds = ecs.write_storage::<Viewshed>();
+    let map = ecs.fetch::<Map>();
+
+    for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
+        let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
+        if map.tiles[destination_idx] != TileType::Wall {
+            pos.x += delta_x;
+            pos.y += delta_y;
+
+            if pos.x < 0 { pos.x = 0; }
+            if pos.x > 79 { pos.y = 79; }
+            if pos.y < 0 { pos.y = 0; }
+            if pos.y > 49 { pos.y = 49; }
+
+            viewshed.dirty = true;
+            let mut ppos = ecs.write_resource::<Point>();
+            ppos.x = pos.x;
+            ppos.y = pos.y;
+        }
+    }
+}
+
+pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
+    // Player movement
+    match ctx.key {
+        None => { return RunState::Paused } // Nothing happened
+        Some(key) => match key {
+            VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
+            VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::Numpad6 => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
+            VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::Numpad8 => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),
+            VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
+            VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs),
+            VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
+            _ => { return RunState::Paused }
+        },
+    }
+    RunState::Running
+}

+ 21 - 0
chapter-06-monsters/src/rect.rs

@@ -0,0 +1,21 @@
+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)
+    }
+}

+ 37 - 0
chapter-06-monsters/src/visibility_system.rs

@@ -0,0 +1,37 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Position, Map, Player};
+extern crate rltk;
+use rltk::{field_of_view, Point};
+
+pub struct VisibilitySystem {}
+
+impl<'a> System<'a> for VisibilitySystem {
+    type SystemData = ( WriteExpect<'a, Map>,
+                        Entities<'a>,
+                        WriteStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, Player>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, entities, mut viewshed, pos, player) = 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;
+                    }
+                }
+            }
+        }
+    }
+}