5 Commits a49ef5b665 ... 8cbc339f87

Author SHA1 Message Date
  Getty Ritter 8cbc339f87 start adding monsters 1 year ago
  Getty Ritter fcc12f3735 some cleanup 1 year ago
  Getty Ritter 1d448034df implement having-seen-ness 1 year ago
  Getty Ritter 06127bde2d fmt 1 year ago
  Getty Ritter fc3e7c2607 fix shadow bleed issue 1 year ago
11 changed files with 452 additions and 58 deletions
  1. 12 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 98 45
      carpet/src/vis.rs
  4. 2 1
      ch5/src/main.rs
  5. 27 11
      ch5/src/map.rs
  6. 8 1
      ch5/src/systems.rs
  7. 15 0
      ch6/Cargo.toml
  8. 37 0
      ch6/src/components.rs
  9. 81 0
      ch6/src/main.rs
  10. 107 0
      ch6/src/map.rs
  11. 64 0
      ch6/src/systems.rs

+ 12 - 0
Cargo.lock

@@ -373,6 +373,18 @@ dependencies = [
  "specs-system-macro",
 ]
 
+[[package]]
+name = "ch6"
+version = "0.1.0"
+dependencies = [
+ "carpet",
+ "ggez",
+ "rand",
+ "specs",
+ "specs-derive",
+ "specs-system-macro",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.3.3"

+ 1 - 0
Cargo.toml

@@ -7,4 +7,5 @@ members = [
   "ch3",
   "ch4",
   "ch5",
+  "ch6",
 ]

+ 98 - 45
carpet/src/vis.rs

@@ -91,16 +91,11 @@ impl ShadowLine {
     }
 
     fn add(&mut self, other: Shadow) {
-        let index = {
-            let mut index = 0;
-            while index < self.shadows.len() {
-                if self.shadows[index].start >= other.start {
-                    break;
-                }
-                index += 1;
-            }
-            index
-        };
+        let index = self
+            .shadows
+            .iter()
+            .position(|sh| sh.start >= other.start)
+            .unwrap_or(self.shadows.len());
 
         // find whether there's an overlapping previous and next
         // shadow segment
@@ -114,27 +109,42 @@ impl ShadowLine {
                 .get(index - 1)
                 .filter(|sh| sh.end > other.start)
         };
-        let next = if index < self.shadows.len() {
-            None
-        } else {
-            self.shadows.get(index).filter(|sh| sh.start < other.end)
-        };
+        let next = self.shadows.get(index).filter(|sh| sh.start < other.end);
 
         match (previous, next) {
-            // two overlapping segments: join them together
+            // 1 2 3 4 5 6 7      1 2 3 4 5 6 7
+            // [prev] [next]  =>  [prev.......]
+            //    [other]
+            //
+            // two overlapping segments: join them together,
+            // specifically extending the previous one and deleting
+            // the second
             (Some(_), Some(n)) => {
                 self.shadows[index - 1].end = n.end;
                 self.shadows.remove(index);
             }
-            // just one overlapping segment: extend the segment in the
-            // appropriate direction
+            // 1 2 3 4 5 6 7 8 9      1 2 3 4 5 6 7 8 9
+            // [prev]     [next]  =>  [prev]  [next...]
+            //         [other]
+            //
+            // just overlapping a later segment: pull the later
+            // segment's start point earlier
             (None, Some(_)) => {
                 self.shadows[index].start = other.start;
             }
+            // 1 2 3 4 5 6 7 8 9      1 2 3 4 5 6 7 8 9
+            // [prev]     [next]  =>  [prev...]  [next]
+            //   [other]
+            //
+            // just overlapping an earlier segment: pull the earlier
+            // segment's end point later
             (Some(_), None) => {
                 self.shadows[index - 1].end = other.end;
             }
-            // no overlapping segments: add this one
+            // 1 2 3 4 5 6 7 8    1 2 3 4 5 6 7 8
+            // [p]         [n] => [p] [other] [n]
+            //     [other]
+            // no overlapping segments: just add this one in between
             (None, None) => {
                 self.shadows.insert(index, other);
             }
@@ -145,15 +155,19 @@ impl ShadowLine {
 pub struct Viewshed<T> {
     pub vis: Board<bool>,
     blocking: Box<fn(&T) -> bool>,
+    pub range: Option<usize>,
 }
 
 impl<T> Viewshed<T> {
     pub fn create(original: &Board<T>, blocking: fn(&T) -> bool) -> Viewshed<T> {
-        let vis = Board::new_from(original.width(), original.height(), |_, _| {
-            false
-        });
+        let vis = Board::new_from(original.width(), original.height(), |_, _| false);
         let blocking = Box::new(blocking);
-        Viewshed { vis, blocking }
+        let range = None;
+        Viewshed {
+            vis,
+            blocking,
+            range,
+        }
     }
 
     pub fn visibility(&self, coord: impl Into<Coord>) -> bool {
@@ -200,9 +214,16 @@ impl<T> Viewshed<T> {
                 let visible = !line.in_shadow(&projection);
                 self.vis[pos] = visible;
 
-                if visible && (self.blocking)(&board[pos]) {
-                    line.add(projection);
-                    full_shadow = line.is_full_shadow();
+                if visible {
+                    let out_of_range = if let Some(r) = self.range {
+                        ((row.pow(2) + col.pow(2)) as f32).sqrt() >= r as f32
+                    } else {
+                        false
+                    };
+                    if out_of_range || (self.blocking)(&board[pos]) {
+                        line.add(projection);
+                        full_shadow = line.is_full_shadow();
+                    }
                 }
             }
         }
@@ -230,9 +251,13 @@ mod test {
     const V: bool = true;
     const U: bool = false;
 
-    fn assert_same_vis(exp: &Board<bool>, actual: &Board<bool>) {
+    fn assert_same_vis(p: Coord, exp: &Board<bool>, actual: &Board<bool>) {
         if exp != actual {
-            panic!("Expected:\n{}\n========\nActual:\n{}\n", to_vis(exp), to_vis(actual));
+            panic!(
+                "Expected:\n{}\n========\nActual:\n{}\n",
+                to_vis(p, exp),
+                to_vis(p, actual)
+            );
         }
     }
 
@@ -289,14 +314,36 @@ mod test {
         assert_eq!(line.shadows[1], super::Shadow::new(0.9, 1.0));
     }
 
-    fn to_vis(vis: &super::Board<bool>) -> String {
+    #[test]
+    fn add_shadow_line_several() {
+        let mut line = super::ShadowLine {
+            shadows: Vec::new(),
+        };
+
+        assert_eq!(line.shadows.len(), 0);
+
+        line.add(super::Shadow::new(0.5, 0.8));
+        assert_eq!(line.shadows.len(), 1);
+
+        line.add(super::Shadow::new(0.0, 0.667));
+        assert_eq!(line.shadows.len(), 1);
+
+        line.add(super::Shadow::new(0.6, 1.0));
+        assert_eq!(line.shadows.len(), 1);
+    }
+
+    fn to_vis(p: Coord, vis: &super::Board<bool>) -> String {
         let mut buf = String::new();
         for y in 0..vis.height() {
             for x in 0..vis.width() {
-                buf.push(match vis[(x, y)] {
-                    true  => '#',
-                    false => '.',
-                })
+                if p.x == x && p.y == y {
+                    buf.push('@')
+                } else {
+                    buf.push(match vis[(x, y)] {
+                        true => '#',
+                        false => '.',
+                    })
+                }
             }
             buf.push('\n')
         }
@@ -316,7 +363,8 @@ mod test {
             ]
         ];
         let mut v = Viewshed::create(&b, |n| *n == 1);
-        v.calculate_from(&b, Coord::new(2, 2));
+        let p = Coord::new(2, 2);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             5,5;
             [
@@ -327,7 +375,7 @@ mod test {
                 U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
     }
 
     #[test]
@@ -345,7 +393,8 @@ mod test {
             ]
         ];
         let mut v = Viewshed::create(&b, |n| *n == 1);
-        v.calculate_from(&b, Coord::new(3, 3));
+        let p = Coord::new(3, 3);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             7,7;
             [
@@ -358,7 +407,7 @@ mod test {
                 U, U, U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
     }
 
     #[test]
@@ -376,7 +425,8 @@ mod test {
             ]
         ];
         let mut v = Viewshed::create(&b, |n| *n == 1);
-        v.calculate_from(&b, Coord::new(3, 3));
+        let p = Coord::new(3, 3);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             13,7;
             [
@@ -389,7 +439,7 @@ mod test {
                 U, U, U, U, U, U, U, U, U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
     }
 
     #[test]
@@ -406,7 +456,8 @@ mod test {
         ];
 
         let mut v = Viewshed::create(&b, |n| *n == 1);
-        v.calculate_from(&b, Coord::new(2, 2));
+        let p = Coord::new(2, 2);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             7,5;
             [
@@ -417,9 +468,10 @@ mod test {
                 U, U, U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
 
-        v.calculate_from(&b, Coord::new(4, 2));
+        let p = Coord::new(4, 2);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             7,5;
             [
@@ -430,7 +482,7 @@ mod test {
                 U, U, U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
     }
 
     #[test]
@@ -449,7 +501,8 @@ mod test {
         ];
 
         let mut v = Viewshed::create(&b, |n| *n == 1);
-        v.calculate_from(&b, Coord::new(2, 2));
+        let p = Coord::new(2, 2);
+        v.calculate_from(&b, p);
         let exp: Board<bool> = board_from_vec![
             9,7;
             [
@@ -462,6 +515,6 @@ mod test {
                 U, U, U, U, U, U, U, U, U,
             ]
         ];
-        assert_same_vis(&exp, &v.vis);
+        assert_same_vis(p, &exp, &v.vis);
     }
 }

+ 2 - 1
ch5/src/main.rs

@@ -38,7 +38,8 @@ fn main() -> Result<(), GameError> {
         .first()
         .map(|r| r.center())
         .unwrap_or_else(|| [40, 25].into());
-    let vs = carpet::Viewshed::create(&map.tiles, map::TileType::blocks_view);
+    let mut vs = carpet::Viewshed::create(&map.tiles, map::Cell::blocks_view);
+    vs.range = Some(20);
     game.insert(map::Viewshed { vs, dirty: true });
     game.insert(map);
 

+ 27 - 11
ch5/src/map.rs

@@ -3,29 +3,35 @@ use rand::Rng;
 use specs::prelude::*;
 
 #[derive(PartialEq, Clone, Copy, Debug)]
-pub enum TileType {
-    Wall,
-    Floor,
+pub struct Cell {
+    pub tile: TileType,
+    pub seen: bool,
 }
 
-impl TileType {
+impl Cell {
     pub fn glyph(&self) -> carpet::CP437 {
-        match self {
+        match self.tile {
             TileType::Wall => carpet::CP437::from_char('#'),
             TileType::Floor => carpet::CP437::from_u8(0),
         }
     }
 
     pub fn blocks_view(&self) -> bool {
-        match self {
+        match self.tile {
             TileType::Wall => true,
             TileType::Floor => false,
         }
     }
 }
 
+#[derive(PartialEq, Clone, Copy, Debug)]
+pub enum TileType {
+    Wall,
+    Floor,
+}
+
 pub struct Map {
-    pub tiles: carpet::Board<TileType>,
+    pub tiles: carpet::Board<Cell>,
     pub rooms: Vec<carpet::Rect>,
 }
 
@@ -33,7 +39,14 @@ impl Map {
     pub fn new() -> Map {
         let mut rng = rand::thread_rng();
         let mut map = Map {
-            tiles: carpet::Board::new_with_default(80, 50, TileType::Wall),
+            tiles: carpet::Board::new_with_default(
+                80,
+                50,
+                Cell {
+                    tile: TileType::Wall,
+                    seen: false,
+                },
+            ),
             rooms: Vec::new(),
         };
 
@@ -75,17 +88,20 @@ impl Map {
             .window_iter_mut(rect)
             .unwrap_or_else(|| panic!("Rect {:?} of map bounds", rect));
         for (_, _, t) in iter {
-            *t = TileType::Floor;
+            t.tile = TileType::Floor;
         }
     }
 
     pub fn passable(&self, (x, y): (usize, usize)) -> bool {
-        Some(&TileType::Floor) == self.tiles.get(x, y)
+        if let Some(cell) = self.tiles.get(x, y) {
+            return cell.tile == TileType::Floor;
+        }
+        false
     }
 }
 
 #[derive(Component)]
 pub struct Viewshed {
-    pub vs: carpet::Viewshed<TileType>,
+    pub vs: carpet::Viewshed<Cell>,
     pub dirty: bool,
 }

+ 8 - 1
ch5/src/systems.rs

@@ -13,6 +13,8 @@ system_impl! {
         for (x, y, t) in map.tiles.iter() {
             if vs.vs.visibility([x, y]) {
                 game_board.set([x, y], t.glyph());
+            } else if t.seen {
+                game_board.set_with_color([x, y], t.glyph(), (0.5, 0.5, 0.5));
             }
         }
         for (p, r) in (&pos, &renderable).join() {
@@ -42,13 +44,18 @@ system! {
 
 system! {
     Visibility(
-        resource map: Map,
+        resource mut map: Map,
         resource mut viewshed: Viewshed,
         pos: carpet::Coord,
         _player: Player,
     ) {
         if viewshed.dirty {
             viewshed.vs.calculate_from(&map.tiles, *pos);
+            for (x, y, vis) in viewshed.vs.vis.iter() {
+                if *vis {
+                    map.tiles[(x, y)].seen = true;
+                }
+            }
             viewshed.dirty = false;
         }
     }

+ 15 - 0
ch6/Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "ch6"
+version = "0.1.0"
+authors = ["Getty Ritter <gettylefou@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+ggez = "*"
+carpet = { path = "../carpet" }
+specs = "*"
+specs-derive = "*"
+specs-system-macro = { git = "https://git.infinitenegativeutility.com/getty/specs-system-macro.git" }
+rand = "*"

+ 37 - 0
ch6/src/components.rs

@@ -0,0 +1,37 @@
+use specs::prelude::*;
+
+#[derive(Component)]
+pub struct Renderable {
+    pub glyph: carpet::CP437,
+    pub color: carpet::Color,
+}
+
+#[derive(Component)]
+pub struct Player;
+
+impl Player {
+    fn get_entity(world: &mut specs::World) -> Entity {
+        let storage = (&world.read_component::<Player>(), &world.entities());
+        storage
+            .join()
+            .next()
+            .expect("No entities tagged as Player")
+            .1
+    }
+}
+
+#[derive(Component)]
+pub struct Motion {
+    pub down: i8,
+    pub right: i8,
+}
+
+impl Motion {
+    pub fn move_player(world: &mut specs::World, down: i8, right: i8) {
+        let player = Player::get_entity(world);
+        world
+            .write_component::<Motion>()
+            .insert(player, Motion { down, right })
+            .unwrap();
+    }
+}

+ 81 - 0
ch6/src/main.rs

@@ -0,0 +1,81 @@
+#[macro_use]
+extern crate specs_derive;
+#[macro_use]
+extern crate specs_system_macro;
+
+mod components;
+mod map;
+mod systems;
+
+use ggez::GameError;
+use specs::prelude::{Builder, RunNow};
+
+use components as com;
+
+fn main() -> Result<(), GameError> {
+    let mut game: carpet::Game<carpet::CP437> = carpet::GameBuilder::new()
+        .name("game")
+        .author("me")
+        .resource_path({
+            let base = std::env::var("CARGO_MANIFEST_DIR").unwrap();
+            let mut path = std::path::PathBuf::from(base);
+            path.pop();
+            path.push("resources");
+            path
+        })
+        .tileset("/haberdash.gif", [12, 12])
+        .map_size(80, 50)
+        .build()?;
+
+    game.register::<carpet::Coord>();
+    game.register::<com::Renderable>();
+    game.register::<com::Motion>();
+    game.register::<com::Player>();
+
+    let map = map::Map::new();
+    let player_start = map
+        .rooms
+        .first()
+        .map(|r| r.center())
+        .unwrap_or_else(|| [40, 25].into());
+    let mut vs = carpet::Viewshed::create(&map.tiles, map::Cell::blocks_view);
+    vs.range = Some(20);
+    game.insert(map::Viewshed { vs, dirty: true });
+
+    for r in map.rooms.iter().skip(1) {
+        game.create_entity()
+            .with(r.center())
+            .with(com::Renderable {
+                glyph: carpet::CP437::from_char('g'),
+                color: carpet::Color::Red,
+            })
+            .build();
+    }
+
+    game.insert(map);
+
+    game.create_entity()
+        .with(player_start)
+        .with(com::Player)
+        .with(com::Renderable {
+            glyph: carpet::CP437::from_char('@'),
+            color: carpet::Color::Blue,
+        })
+        .build();
+
+    {
+        // set up all the keybindings
+        use carpet::KeyCode::*;
+        let none = carpet::KeyMods::NONE;
+        game.on_key((W, none), |world| com::Motion::move_player(world, -1, 0));
+        game.on_key((A, none), |world| com::Motion::move_player(world, 0, -1));
+        game.on_key((S, none), |world| com::Motion::move_player(world, 1, 0));
+        game.on_key((D, none), |world| com::Motion::move_player(world, 0, 1));
+    }
+
+    game.run_with_systems(|world| {
+        systems::Visibility.run_now(world);
+        systems::Draw.run_now(world);
+        systems::Move.run_now(world);
+    })
+}

+ 107 - 0
ch6/src/map.rs

@@ -0,0 +1,107 @@
+use rand::Rng;
+
+use specs::prelude::*;
+
+#[derive(PartialEq, Clone, Copy, Debug)]
+pub struct Cell {
+    pub tile: TileType,
+    pub seen: bool,
+}
+
+impl Cell {
+    pub fn glyph(&self) -> carpet::CP437 {
+        match self.tile {
+            TileType::Wall => carpet::CP437::from_char('#'),
+            TileType::Floor => carpet::CP437::from_u8(0),
+        }
+    }
+
+    pub fn blocks_view(&self) -> bool {
+        match self.tile {
+            TileType::Wall => true,
+            TileType::Floor => false,
+        }
+    }
+}
+
+#[derive(PartialEq, Clone, Copy, Debug)]
+pub enum TileType {
+    Wall,
+    Floor,
+}
+
+pub struct Map {
+    pub tiles: carpet::Board<Cell>,
+    pub rooms: Vec<carpet::Rect>,
+}
+
+impl Map {
+    pub fn new() -> Map {
+        let mut rng = rand::thread_rng();
+        let mut map = Map {
+            tiles: carpet::Board::new_with_default(
+                80,
+                50,
+                Cell {
+                    tile: TileType::Wall,
+                    seen: false,
+                },
+            ),
+            rooms: Vec::new(),
+        };
+
+        const MAX_ROOMS: usize = 30;
+        const MIN_SIZE: usize = 6;
+        const MAX_SIZE: usize = 10;
+
+        for _ in 0..MAX_ROOMS {
+            let w = rng.gen_range(MIN_SIZE..MAX_SIZE);
+            let h = rng.gen_range(MIN_SIZE..MAX_SIZE);
+            let x = rng.gen_range(1..80 - w);
+            let y = rng.gen_range(1..50 - h);
+            let room = carpet::Rect::new([x, y], [w, h]);
+            if map.rooms.iter().any(|r| r.overlaps(room)) {
+                continue;
+            }
+            map.carve(room);
+            if let Some(prev) = map.rooms.first() {
+                let c1 = room.center();
+                let c2 = prev.center();
+                let join = if rng.gen() {
+                    carpet::Coord { x: c1.x, y: c2.y }
+                } else {
+                    carpet::Coord { x: c2.x, y: c1.y }
+                };
+                map.carve(carpet::Rect::from_points(c1, join));
+                map.carve(carpet::Rect::from_points(join, c2));
+            }
+
+            map.rooms.push(room);
+        }
+
+        map
+    }
+
+    fn carve(&mut self, rect: carpet::Rect) {
+        let iter = self
+            .tiles
+            .window_iter_mut(rect)
+            .unwrap_or_else(|| panic!("Rect {:?} of map bounds", rect));
+        for (_, _, t) in iter {
+            t.tile = TileType::Floor;
+        }
+    }
+
+    pub fn passable(&self, (x, y): (usize, usize)) -> bool {
+        if let Some(cell) = self.tiles.get(x, y) {
+            return cell.tile == TileType::Floor;
+        }
+        false
+    }
+}
+
+#[derive(Component)]
+pub struct Viewshed {
+    pub vs: carpet::Viewshed<Cell>,
+    pub dirty: bool,
+}

+ 64 - 0
ch6/src/systems.rs

@@ -0,0 +1,64 @@
+use crate::components::{Motion, Player, Renderable};
+use crate::map::{Map, Viewshed};
+
+system_impl! {
+    Draw(
+        resource mut game_board: carpet::GameBoard<carpet::CP437>,
+        resource map: Map,
+        resource vs: Viewshed,
+        renderable: Renderable,
+        pos: carpet::Coord,
+    ) {
+        game_board.clear();
+        for (x, y, t) in map.tiles.iter() {
+            if vs.vs.visibility([x, y]) {
+                game_board.set([x, y], t.glyph());
+            } else if t.seen {
+                game_board.set_with_color([x, y], t.glyph(), (0.5, 0.5, 0.5));
+            }
+        }
+        for (p, r) in (&pos, &renderable).join() {
+            if vs.vs.visibility([p.x, p.y]) {
+                game_board.set_with_color([p.x, p.y], r.glyph, r.color);
+            }
+        }
+    }
+}
+
+system! {
+    Move (
+        resource map: Map,
+        resource mut viewshed: Viewshed,
+        mut motion: Motion,
+        mut pos: carpet::Coord,
+    ) {
+        let tgt_x = (pos.x as i8 + motion.right) as usize;
+        let tgt_y = (pos.y as i8 + motion.down) as usize;
+        if map.passable((tgt_x, tgt_y)) {
+            viewshed.dirty = true;
+            pos.x = tgt_x;
+            pos.y = tgt_y;
+        }
+    } finally {
+        motion.clear();
+    }
+}
+
+system! {
+    Visibility(
+        resource mut map: Map,
+        resource mut viewshed: Viewshed,
+        pos: carpet::Coord,
+        _player: Player,
+    ) {
+        if viewshed.dirty {
+            viewshed.vs.calculate_from(&map.tiles, *pos);
+            for (x, y, vis) in viewshed.vs.vis.iter() {
+                if *vis {
+                    map.tiles[(x, y)].seen = true;
+                }
+            }
+            viewshed.dirty = false;
+        }
+    }
+}