Browse Source

Working potion use.

Herbert Wolverson 4 years ago
parent
commit
40d4e17c61

BIN
book/src/c9-s4.gif


+ 73 - 0
book/src/chapter_9.md

@@ -502,6 +502,79 @@ RunState::ShowInventory => {
 
 If you try to use an item in your inventory now, you'll get a log entry that you try to use it, but we haven't written that bit of code yet. That's a start!
 
+Once again, we want generic code - so that eventually monsters might use potions. We're going to cheat a little while all items are potions, and just make a potion system; we'll turn it into something more useful later. So we'll start by creating an "intent" component in `components.rs` (and registered in `main.rs`):
+
+```rust
+#[derive(Component, Debug)]
+pub struct WantsToDrinkPotion {
+    pub potion : Entity
+}
+```
+
+Add the following to `inventory.rs`:
+
+```rust
+pub struct PotionUseSystem {}
+
+impl<'a> System<'a> for PotionUseSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToDrinkPotion>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, Potion>,
+                        WriteStorage<'a, CombatStats>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data;
+
+        for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() {
+            let potion = potions.get(drink.potion);
+            match potion {
+                None => {}
+                Some(potion) => {
+                    stats.hp = i32::max(stats.max_hp, stats.hp + potion.heal_amount);
+                    if entity == *player_entity {
+                        gamelog.entries.insert(0, format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount));
+                    }
+                    entities.delete(drink.potion).expect("Delete failed");
+                }
+            }
+        }
+
+        wants_drink.clear();
+    }
+}
+```
+
+And register it in the list of systems to run:
+```rust
+.with(PotionUseSystem{}, "potions", &["melee_combat"])
+```
+
+Like other systems we've looked at, this iterates all of the `WantsToDrinkPotion` intent objects. It then heals up the drinker by the amount set in the `Potion` component, and deletes the potion. Since all of the placement information is attached to the potion itself, there's no need to chase around making sure it is removed from the appropriate backpack: the entity ceases to exist, and takes its components with it.
+
+Testing this with `cargo run` gives a surprise: the potion isn't deleted after use! This is because the ECS simply marks entities as `dead` - it doesn't delete them in systems (so as to not mess up iterators and threading). So after every call to `dispatch`, we need to add a call to `maintain`. In `main.ecs`:
+
+```rust
+RunState::PlayerTurn => {
+    self.systems.dispatch(&self.ecs);
+    self.ecs.maintain();
+    newrunstate = RunState::MonsterTurn;
+}
+RunState::MonsterTurn => {
+    self.systems.dispatch(&self.ecs);
+    self.ecs.maintain();
+    newrunstate = RunState::AwaitingInput;
+}
+```
+
+NOW if you `cargo run` the project, you can pickup and drink health potions:
+
+![Screenshot](./c9-s4.gif)
+
 # Dropping Items
 
 # Render order

+ 5 - 0
chapter-09-items/src/components.rs

@@ -74,3 +74,8 @@ pub struct WantsToPickupItem {
     pub collected_by : Entity,
     pub item : Entity
 }
+
+#[derive(Component, Debug)]
+pub struct WantsToDrinkPotion {
+    pub potion : Entity
+}

+ 36 - 1
chapter-09-items/src/inventory_system.rs

@@ -1,6 +1,7 @@
 extern crate specs;
 use specs::prelude::*;
-use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog};
+use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog, WantsToDrinkPotion, 
+    Potion, CombatStats};
 
 pub struct ItemCollectionSystem {}
 
@@ -28,4 +29,38 @@ impl<'a> System<'a> for ItemCollectionSystem {
 
         wants_pickup.clear();
     }
+}
+
+pub struct PotionUseSystem {}
+
+impl<'a> System<'a> for PotionUseSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToDrinkPotion>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, Potion>,
+                        WriteStorage<'a, CombatStats>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data;
+
+        for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() {
+            let potion = potions.get(drink.potion);
+            match potion {
+                None => {}
+                Some(potion) => {
+                    stats.hp = i32::max(stats.max_hp, stats.hp + potion.heal_amount);
+                    if entity == *player_entity {
+                        gamelog.entries.insert(0, format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount));
+                    }
+                    entities.delete(drink.potion).expect("Delete failed");
+                }
+            }
+        }
+
+        wants_drink.clear();
+    }
 }

+ 9 - 4
chapter-09-items/src/main.rs

@@ -26,7 +26,7 @@ mod gui;
 mod gamelog;
 mod spawner;
 mod inventory_system;
-use inventory_system::{ ItemCollectionSystem };
+use inventory_system::{ ItemCollectionSystem, PotionUseSystem };
 
 #[derive(PartialEq, Copy, Clone)]
 pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory }
@@ -64,6 +64,7 @@ impl GameState for State {
         match newrunstate {
             RunState::PreRun => {
                 self.systems.dispatch(&self.ecs);
+                self.ecs.maintain();
                 newrunstate = RunState::AwaitingInput;
             }
             RunState::AwaitingInput => {
@@ -71,10 +72,12 @@ impl GameState for State {
             }
             RunState::PlayerTurn => {
                 self.systems.dispatch(&self.ecs);
+                self.ecs.maintain();
                 newrunstate = RunState::MonsterTurn;
             }
             RunState::MonsterTurn => {
                 self.systems.dispatch(&self.ecs);
+                self.ecs.maintain();
                 newrunstate = RunState::AwaitingInput;
             }
             RunState::ShowInventory => {
@@ -84,9 +87,9 @@ impl GameState for State {
                     gui::ItemMenuResult::NoResponse => {}
                     gui::ItemMenuResult::Selected => {
                         let item_entity = result.1.unwrap();
-                        let names = self.ecs.read_storage::<Name>();
-                        let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
-                        gamelog.entries.insert(0, format!("You try to use {}, but it isn't written yet", names.get(item_entity).unwrap().name));
+                        let mut intent = self.ecs.write_storage::<WantsToDrinkPotion>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToDrinkPotion{ potion: item_entity }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
                     }
                 }
             }
@@ -112,6 +115,7 @@ fn main() {
             .with(MeleeCombatSystem{}, "melee_combat", &["monster_ai"])
             .with(DamageSystem{}, "damage", &["melee_combat"])
             .with(ItemCollectionSystem{}, "pickup", &["melee_combat"])
+            .with(PotionUseSystem{}, "potions", &["melee_combat"])
             .build(),
     };
     gs.ecs.register::<Position>();
@@ -128,6 +132,7 @@ fn main() {
     gs.ecs.register::<Potion>();
     gs.ecs.register::<InBackpack>();
     gs.ecs.register::<WantsToPickupItem>();
+    gs.ecs.register::<WantsToDrinkPotion>();
 
     let map : Map = Map::new_map_rooms_and_corridors();
     let (player_x, player_y) = map.rooms[0].center();