Browse Source

Chapter 7 is basically working.

Herbert Wolverson 4 years ago
parent
commit
0908ae7edc

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


+ 18 - 0
chapter-07-damage/src/components.rs

@@ -37,3 +37,21 @@ pub struct Name {
 
 #[derive(Component, Debug)]
 pub struct BlocksTile {}
+
+#[derive(Component, Debug)]
+pub struct CombatStats {
+    pub max_hp : i32,
+    pub hp : i32,
+    pub defense : i32,
+    pub power : i32
+}
+
+#[derive(Component, Debug)]
+pub struct WantsToMelee {
+    pub target : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct SufferDamage {
+    pub amount : i32
+}

+ 43 - 0
chapter-07-damage/src/damage_system.rs

@@ -0,0 +1,43 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, SufferDamage, Player};
+
+pub struct DamageSystem {}
+
+impl<'a> System<'a> for DamageSystem {
+    type SystemData = ( WriteStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage> );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut stats, mut damage) = data;
+
+        for (mut stats, damage) in (&mut stats, &damage).join() {
+            stats.hp -= damage.amount;
+        }
+
+        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 entities = ecs.entities();
+        for (entity, stats) in (&entities, &combat_stats).join() {
+            if stats.hp < 1 { 
+                let player = players.get(entity);
+                match player {
+                    None => dead.push(entity),
+                    Some(_) => println!("You are dead")
+                }
+            }
+        }
+    }
+
+    for victim in dead {
+        ecs.delete_entity(victim).expect("Unable to delete");
+    }    
+}

+ 43 - 10
chapter-07-damage/src/main.rs

@@ -18,26 +18,51 @@ 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;
 
 #[derive(PartialEq, Copy, Clone)]
-pub enum RunState { Paused, Running }
+pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn }
 
 pub struct State {
     pub ecs: World,
-    pub systems: Dispatcher<'static, 'static>,
-    pub runstate : RunState
+    pub systems: Dispatcher<'static, 'static>
 }
 
 impl GameState for State {
     fn tick(&mut self, ctx : &mut Rltk) {
         ctx.cls();
+        let mut newrunstate;
+        {
+            let runstate = self.ecs.fetch::<RunState>();
+            newrunstate = *runstate;
+        }
         
-        if self.runstate == RunState::Running {
-            self.systems.dispatch(&self.ecs);
-            self.runstate = RunState::Paused;
-        } else {
-            self.runstate = player_input(self, ctx);
+        match newrunstate {
+            RunState::PreRun => {
+                self.systems.dispatch(&self.ecs);
+                newrunstate = RunState::AwaitingInput;
+            }
+            RunState::AwaitingInput => {
+                newrunstate = player_input(self, ctx);
+            }
+            RunState::PlayerTurn => {
+                self.systems.dispatch(&self.ecs);
+                newrunstate = RunState::MonsterTurn;
+            }
+            RunState::MonsterTurn => {
+                self.systems.dispatch(&self.ecs);
+                newrunstate = RunState::AwaitingInput;
+            }
+        }
+
+        {
+            let mut runwriter = self.ecs.write_resource::<RunState>();
+            *runwriter = newrunstate;
         }
+        damage_system::delete_the_dead(&mut self.ecs);
 
         draw_map(&self.ecs, ctx);
 
@@ -60,8 +85,9 @@ fn main() {
             .with(MapIndexingSystem{}, "map_indexing_system", &[])
             .with(VisibilitySystem{}, "visibility_system", &[])
             .with(MonsterAI{}, "monster_ai", &["visibility_system", "map_indexing_system"])
+            .with(MeleeCombatSystem{}, "melee_combat", &["monster_ai"])
+            .with(DamageSystem{}, "damage", &["melee_combat"])
             .build(),
-        runstate : RunState::Running
     };
     gs.ecs.register::<Position>();
     gs.ecs.register::<Renderable>();
@@ -70,11 +96,14 @@ fn main() {
     gs.ecs.register::<Monster>();
     gs.ecs.register::<Name>();
     gs.ecs.register::<BlocksTile>();
+    gs.ecs.register::<CombatStats>();
+    gs.ecs.register::<WantsToMelee>();
+    gs.ecs.register::<SufferDamage>();
 
     let map : Map = Map::new_map_rooms_and_corridors();
     let (player_x, player_y) = map.rooms[0].center();
 
-    gs.ecs
+    let player_entity = gs.ecs
         .create_entity()
         .with(Position { x: player_x, y: player_y })
         .with(Renderable {
@@ -85,6 +114,7 @@ fn main() {
         .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 })
         .build();
 
     let mut rng = rltk::RandomNumberGenerator::new();
@@ -110,11 +140,14 @@ fn main() {
             .with(Monster{})
             .with(Name{ name: format!("{} #{}", &name, i) })
             .with(BlocksTile{})
+            .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
             .build();
     }
 
     gs.ecs.insert(map);
     gs.ecs.insert(Point::new(player_x, player_y));
+    gs.ecs.insert(player_entity);
+    gs.ecs.insert(RunState::PreRun);
 
     rltk::main_loop(context, gs);
 }

+ 10 - 2
chapter-07-damage/src/map.rs

@@ -18,7 +18,8 @@ pub struct Map {
     pub height : i32,
     pub revealed_tiles : Vec<bool>,
     pub visible_tiles : Vec<bool>,
-    pub blocked : Vec<bool>
+    pub blocked : Vec<bool>,
+    pub tile_content : Vec<Vec<Entity>>
 }
 
 impl Map {
@@ -65,6 +66,12 @@ impl Map {
         }
     }
 
+    pub fn clear_content_index(&mut self) {
+        for content in self.tile_content.iter_mut() {
+            content.clear();
+        }
+    }
+
     /// 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 {
@@ -75,7 +82,8 @@ impl Map {
             height: 50,
             revealed_tiles : vec![false; 80*50],
             visible_tiles : vec![false; 80*50],
-            blocked : vec![false; 80*50]
+            blocked : vec![false; 80*50],
+            tile_content : vec![Vec::new(); 80*50]
         };
 
         const MAX_ROOMS : i32 = 30;

+ 15 - 4
chapter-07-damage/src/map_indexing_system.rs

@@ -7,15 +7,26 @@ pub struct MapIndexingSystem {}
 impl<'a> System<'a> for MapIndexingSystem {
     type SystemData = ( WriteExpect<'a, Map>,
                         ReadStorage<'a, Position>,
-                        ReadStorage<'a, BlocksTile>);
+                        ReadStorage<'a, BlocksTile>,
+                        Entities<'a>,);
 
     fn run(&mut self, data : Self::SystemData) {
-        let (mut map, position, blockers) = data;
+        let (mut map, position, blockers, entities) = data;
 
         map.populate_blocked();
-        for (position, _blocks) in (&position, &blockers).join() {
+        map.clear_content_index();
+        for (entity, position) in (&entities, &position).join() {
             let idx = map.xy_idx(position.x, position.y);
-            map.blocked[idx] = true;
+
+            // 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);
         }
     }
 }

+ 38 - 0
chapter-07-damage/src/melee_combat_system.rs

@@ -0,0 +1,38 @@
+extern crate specs;
+use specs::prelude::*;
+use super::{CombatStats, WantsToMelee, Name, SufferDamage};
+
+pub struct MeleeCombatSystem {}
+
+impl<'a> System<'a> for MeleeCombatSystem {
+    type SystemData = ( Entities<'a>,
+                        WriteStorage<'a, WantsToMelee>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
+
+        for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
+            if stats.hp > 0 {
+                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 damage = i32::max(0, stats.power - target_stats.defense);
+
+                    if damage == 0 {
+                        println!("{} is unable to hurt {}", &name.name, &target_name.name);
+                    } else {
+                        println!("{} 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();
+    }
+}

+ 12 - 10
chapter-07-damage/src/monster_ai_system.rs

@@ -1,6 +1,6 @@
 extern crate specs;
 use specs::prelude::*;
-use super::{Viewshed, Monster, Name, Map, Position};
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState};
 extern crate rltk;
 use rltk::{Point};
 
@@ -10,23 +10,25 @@ 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>,
-                        ReadStorage<'a, Name>,
-                        WriteStorage<'a, Position>);
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, WantsToMelee>);
 
     fn run(&mut self, data : Self::SystemData) {
-        let (mut map, player_pos, mut viewshed, monster, name, mut position) = data;
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = data;
 
-        for (mut viewshed,_monster,name,mut pos) in (&mut viewshed, &monster, &name, &mut position).join() {
+        if *runstate != RunState::MonsterTurn { return; }
+
+        for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() {
             let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
             if distance < 1.5 {
-                // Attack goes here
-                println!("{} shouts insults", name.name);
-                return;
+                wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
             }
-
-            if viewshed.visible_tiles.contains(&*player_pos) {
+            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, 

+ 18 - 6
chapter-07-damage/src/player.rs

@@ -2,16 +2,28 @@ extern crate rltk;
 use rltk::{VirtualKeyCode, Rltk, Point};
 extern crate specs;
 use specs::prelude::*;
-use super::{Position, Player, Viewshed, State, Map, RunState};
+use super::{Position, Player, Viewshed, State, Map, RunState, CombatStats, WantsToMelee};
 
 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 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>();
 
-    for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
+    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 += delta_x;
             pos.y += delta_y;
@@ -32,7 +44,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
 pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
     // Player movement
     match ctx.key {
-        None => { return RunState::Paused } // Nothing happened
+        None => { return RunState::AwaitingInput } // 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),
@@ -63,8 +75,8 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
             VirtualKeyCode::Numpad1 => try_move_player(-1, 1, &mut gs.ecs),
             VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs),
 
-            _ => { return RunState::Paused }
+            _ => { return RunState::AwaitingInput }
         },
     }
-    RunState::Running
+    RunState::PlayerTurn
 }