Browse Source

Starting on chapter 7

Herbert Wolverson 4 years ago
parent
commit
bb561289dd

+ 9 - 0
Cargo.lock

@@ -169,6 +169,15 @@ dependencies = [
  "specs-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "chapter-07-damage"
+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

@@ -18,5 +18,6 @@ members = [
     "chapter-03-walkmap", 
     "chapter-04-newmap",
     "chapter-05-fov",
-    "chapter-06-monsters"
+    "chapter-06-monsters",
+    "chapter-07-damage"
     ]

+ 1 - 0
book/src/SUMMARY.md

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

File diff suppressed because it is too large
+ 101 - 0
book/src/chapter_7.md


+ 1 - 1
chapter-06-monsters/src/main.rs

@@ -98,7 +98,7 @@ fn main() {
         gs.ecs.create_entity()
             .with(Position{ x, y })
             .with(Renderable{
-                glyph: glyph,
+                glyph,
                 fg: RGB::named(rltk::RED),
                 bg: RGB::named(rltk::BLACK),
             })

+ 12 - 0
chapter-07-damage/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "chapter-07-damage"
+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-07-damage/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-07-damage/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,
+                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);
+}

+ 180 - 0
chapter-07-damage/src/map.rs

@@ -0,0 +1,180 @@
+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;
+            }
+        }
+    }
+
+    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.tiles[idx as usize] != TileType::Wall
+    }
+
+    /// 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)> {
+        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)) };
+
+        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::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;
+        }
+    }
+}

+ 37 - 0
chapter-07-damage/src/monster_ai_system.rs

@@ -0,0 +1,37 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{Viewshed, Monster, Name, Map, Position};
+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>,
+                        WriteStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>,
+                        ReadStorage<'a, Name>,
+                        WriteStorage<'a, Position>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, player_pos, mut viewshed, monster, name, mut position) = data;
+
+        for (mut viewshed,_monster,name,mut pos) in (&mut viewshed, &monster, &name, &mut position).join() {
+            if viewshed.visible_tiles.contains(&*player_pos) {
+                println!("{} shouts insults", name.name);
+                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 {
+                    pos.x = path.steps[1] % map.width;
+                    pos.y = path.steps[1] / map.width;
+                    viewshed.dirty = true;
+                }
+            }
+        }
+    }
+}

+ 53 - 0
chapter-07-damage/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-07-damage/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-07-damage/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;
+                    }
+                }
+            }
+        }
+    }
+}