Parcourir la source

More crash fixes on previous chapters, add chapter 19 - hunger clock.

Herbert Wolverson il y a 4 ans
Parent
commit
8935130bb2

+ 7 - 0
Cargo.lock

@@ -285,6 +285,13 @@ dependencies = [
 [[package]]
 name = "chapter-19-food"
 version = "0.1.0"
+dependencies = [
+ "rltk 0.2.5 (git+https://github.com/thebracket/rltk_rs)",
+ "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "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"

BIN
book/src/c19-s1.jpg


BIN
book/src/c19-s2.jpg


BIN
book/src/c19-s3.jpg


+ 1 - 1
book/src/chapter_16.md

@@ -27,7 +27,7 @@ This requires the `wall_glyph` function, so lets write it:
 
 ```rust
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 305 - 0
book/src/chapter_19.md

@@ -12,8 +12,313 @@
 
 Hunger clocks are a controversial feature of a lot of roguelikes. They can really irritate the player if you are spending all of your time looking for food, but they also drive you forward - so you can't sit around without exploring more. Resting to heal becomes more of a risk/reward system, in particular. This chapter will implement a basic hunger clock for the player.
 
+## Adding a hunger clock component
 
+We'll be adding a hunger clock to the player, so the first step is to make a component to represent it. In `components.rs`:
 
+```rust
+#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
+pub enum HungerState { WellFed, Normal, Hungry, Starving }
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct HungerClock {
+    pub state : HungerState,
+    pub duration : i32
+}
+```
+
+As with all components, it needs to be registered in `main.rs` and `saveload_system.rs`. In `spawners.rs`, we'll extend the `player` function to add a hunger clock to the player:
+
+```rust
+pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
+    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),
+            render_order: 0
+        })
+        .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 })
+        .with(HungerClock{ state: HungerState::WellFed, duration: 20 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build()
+}
+```
+
+There's now a hunger clock component in place, but it doesn't *do* anything!
+
+## Adding a hunger system
+
+We'll make a new file, `hunger_system.rs` and implement a hunger clock system. It's quite straightforward:
+
+```rust
+use specs::prelude::*;
+use super::{HungerClock, RunState, HungerState, SufferDamage, gamelog::GameLog};
+
+pub struct HungerSystem {}
+
+impl<'a> System<'a> for HungerSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( 
+                        Entities<'a>,
+                        WriteStorage<'a, HungerClock>,
+                        ReadExpect<'a, Entity>, // The player
+                        ReadExpect<'a, RunState>,
+                        WriteStorage<'a, SufferDamage>,
+                        WriteExpect<'a, GameLog>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut hunger_clock, player_entity, runstate, mut inflict_damage, mut log) = data;
+
+        for (entity, mut clock) in (&entities, &mut hunger_clock).join() {
+            let mut proceed = false;
+
+            match *runstate {
+                RunState::PlayerTurn => {
+                    if entity == *player_entity {
+                        proceed = true;
+                    }
+                }
+                RunState::MonsterTurn => {
+                    if entity != *player_entity {
+                        proceed = false;
+                    }
+                }
+                _ => proceed = false
+            }
+
+            if proceed {
+                clock.duration -= 1;
+                if clock.duration < 1 {
+                    match clock.state {
+                        HungerState::WellFed => {
+                            clock.state = HungerState::Normal;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are no longer well fed.".to_string());
+                            }
+                        }
+                        HungerState::Normal => {
+                            clock.state = HungerState::Hungry;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are hungry.".to_string());
+                            }
+                        }
+                        HungerState::Hungry => {
+                            clock.state = HungerState::Starving;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are starving!".to_string());
+                            }
+                        }
+                        HungerState::Starving => {
+                            // Inflict damage from hunger
+                            if entity == *player_entity {
+                                log.entries.insert(0, "Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string());
+                            }
+                            inflict_damage.insert(entity, SufferDamage{ amount: 1 }).expect("Unable to do damage");  
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+```
+
+It works by iterating all entities that have a `HungerClock`. If they are the player, it only takes effect in the `PlayerTurn` state; likewise, if they are a monster, it only takes place in their turn (in case we want hungry monsters later!). The duration of the current state is reduced on each run-through. If it hits 0, it moves one state down - or if you are starving, damages you.
+
+Now we need to add it to the list of systems running in `main.rs`:
+
+```rust
+let mut gs = State {
+    ecs: World::new(),
+    systems : DispatcherBuilder::new()
+        .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"])
+        .with(ItemCollectionSystem{}, "pickup", &["melee_combat"])
+        .with(ItemUseSystem{}, "potions", &["melee_combat"])
+        .with(ItemDropSystem{}, "drop_items", &["melee_combat"])
+        .with(ItemRemoveSystem{}, "remove_items", &["melee_combat"])
+        .with(hunger_system::HungerSystem{}, "hunger", &["melee_combat", "potions"])
+        .with(particle_system::ParticleSpawnSystem{}, "spawn_particles", &["potions", "melee_combat"])
+        .build(),
+};
+```
+
+If you `cargo run` now, and hit wait a *lot* - you'll starve to death.
+
+![Screenshot](./c19-s1.jpg)
+
+## Displaying the status
+
+It would be nice to *know* your hunger state! We'll modify `draw_ui` in `gui.rs` to show it:
+
+```rust
+pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
+    ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+
+    let combat_stats = ecs.read_storage::<CombatStats>();
+    let players = ecs.read_storage::<Player>();
+    let hunger = ecs.read_storage::<HungerClock>();
+    for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() {
+        let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
+        ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
+
+        ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));
+
+        match hc.state {
+            HungerState::WellFed => ctx.print_color(71, 42, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"),
+            HungerState::Normal => {}
+            HungerState::Hungry => ctx.print_color(71, 42, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"),
+            HungerState::Starving => ctx.print_color(71, 42, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"),
+        }
+    }
+    ...
+```
+
+If you `cargo run` your project, this gives quite a pleasant display:
+![Screenshot](./c19-s2.jpg)
+
+## Adding in food
+
+It's all well and good starving to death, but players will find it frustrating if they always start do die after 620 turns (and suffer consequences before that! 620 may sound like a lot, but it's common to use a few hundred moves on a level, and we aren't trying to make food the primary game focus). We'll introduce a new item, `Rations`. We have most of the components needed for this already, but we need a new one to indicate that an item `ProvidesFood`. In `components.rs`:
+
+```rust
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct ProvidesFood {}
+```
+
+We will, as always, need to register this in `main.rs` and `saveload_system.rs`.
+
+Now, in `spawner.rs` we'll create a new function to make rations:
+
+```rust
+fn rations(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('%'),
+            fg: RGB::named(rltk::GREEN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Rations".to_string() })
+        .with(Item{})
+        .with(ProvidesFood{})
+        .with(Consumable{})
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+```
+
+We'll also add it to the spawn table (quite common):
+
+```rust
+fn room_table(map_depth: i32) -> RandomTable {
+    RandomTable::new()
+        .add("Goblin", 10)
+        .add("Orc", 1 + map_depth)
+        .add("Health Potion", 7)
+        .add("Fireball Scroll", 2 + map_depth)
+        .add("Confusion Scroll", 2 + map_depth)
+        .add("Magic Missile Scroll", 4)
+        .add("Dagger", 3)
+        .add("Shield", 3)
+        .add("Longsword", map_depth - 1)
+        .add("Tower Shield", map_depth - 1)
+        .add("Rations", 10)
+}
+```
+
+And to the spawn code:
+```rust
+match spawn.1.as_ref() {
+    "Goblin" => goblin(ecs, x, y),
+    "Orc" => orc(ecs, x, y),
+    "Health Potion" => health_potion(ecs, x, y),
+    "Fireball Scroll" => fireball_scroll(ecs, x, y),
+    "Confusion Scroll" => confusion_scroll(ecs, x, y),
+    "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
+    "Dagger" => dagger(ecs, x, y),
+    "Shield" => shield(ecs, x, y),
+    "Longsword" => longsword(ecs, x, y),
+    "Tower Shield" => tower_shield(ecs, x, y),
+    "Rations" => rations(ecs, x, y),
+    _ => {}
+}
+```
+
+If you `cargo run` now, you will encounter rations that you can pickup and drop. You can't, however, eat them! We'll add that to `inventory_system.rs`. Here's the relevant portion (see the tutorial source for the full version):
+
+```rust
+// It it is edible, eat it!
+let item_edible = provides_food.get(useitem.item);
+match item_edible {
+    None => {}
+    Some(_) => {
+        used_item = true;
+        let target = targets[0];
+        let hc = hunger_clocks.get_mut(target);
+        if let Some(hc) = hc {
+            hc.state = HungerState::WellFed;
+            hc.duration = 20;
+            gamelog.entries.insert(0, format!("You eat the {}.", names.get(useitem.item).unwrap().name));
+        }
+    }
+}
+```
+
+If you `cargo run` now, you can run around - find rations, and eat them to reset the hunger clock!
+
+![Screenshot](./c19-s3.jpg)
+
+## Adding a bonus for being well fed
+
+It would be nice if being `Well Fed` does something! We'll give you a temporary +1 to your power when you are fed. This encourages the player to eat - even though they don't have to (sneakily making it harder to survive on lower levels as food becomes less plentiful). In `melee_combat_system.rs` we add:
+
+```rust
+let hc = hunger_clock.get(entity);
+if let Some(hc) = hc {
+    if hc.state == HungerState::WellFed {
+        offensive_bonus += 1;
+    }
+}
+```
+
+And that's it! You get a +1 power bonus for being full of rations.
+
+## Preventing healing when hungry or starving
+
+As another benefit to food, we'll prevent you from wait-healing while hungry or starving (this also balances the healing system we added earlier). In `player.rs`, we modify `skip_turn`:
+
+```rust
+let hunger_clocks = ecs.read_storage::<HungerClock>();
+let hc = hunger_clocks.get(*player_entity);
+if let Some(hc) = hc {
+    match hc.state {
+        HungerState::Hungry => can_heal = false,
+        HungerState::Starving => can_heal = false,
+        _ => {}
+    }
+}
+
+if can_heal {
+```
+
+## Wrap-Up
+
+We now have a working hunger clock system. You may want to tweak the durations to suit your taste (or skip it completely if it isn't your cup of tea) - but it's a mainstay of the genre, so it's good to have it included in the tutorials.
 
 **The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-19-food)**
 

+ 1 - 1
chapter-16-nicewalls/src/map.rs

@@ -187,7 +187,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 1 - 1
chapter-17-blood/src/map.rs

@@ -190,7 +190,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 1 - 1
chapter-18-particles/src/map.rs

@@ -190,7 +190,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 12 - 0
chapter-19-food/src/components.rs

@@ -154,6 +154,18 @@ pub struct ParticleLifetime {
     pub lifetime_ms : f32
 }
 
+#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
+pub enum HungerState { WellFed, Normal, Hungry, Starving }
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct HungerClock {
+    pub state : HungerState,
+    pub duration : i32
+}
+
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
+pub struct ProvidesFood {}
+
 // Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an
 // Entity.
 

+ 10 - 2
chapter-19-food/src/gui.rs

@@ -3,18 +3,26 @@ use rltk::{ RGB, Rltk, Console, Point, VirtualKeyCode };
 extern crate specs;
 use specs::prelude::*;
 use super::{CombatStats, Player, gamelog::GameLog, Map, Name, Position, State, InBackpack, 
-    Viewshed, RunState, Equipped};
+    Viewshed, RunState, Equipped, HungerClock, HungerState};
 
 pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
     ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
 
     let combat_stats = ecs.read_storage::<CombatStats>();
     let players = ecs.read_storage::<Player>();
-    for (_player, stats) in (&players, &combat_stats).join() {
+    let hunger = ecs.read_storage::<HungerClock>();
+    for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() {
         let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
         ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
 
         ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));
+
+        match hc.state {
+            HungerState::WellFed => ctx.print_color(71, 42, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"),
+            HungerState::Normal => {}
+            HungerState::Hungry => ctx.print_color(71, 42, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"),
+            HungerState::Starving => ctx.print_color(71, 42, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"),
+        }
     }
 
     let map = ecs.fetch::<Map>();

+ 74 - 0
chapter-19-food/src/hunger_system.rs

@@ -0,0 +1,74 @@
+use specs::prelude::*;
+use super::{HungerClock, RunState, HungerState, SufferDamage, gamelog::GameLog};
+
+pub struct HungerSystem {}
+
+impl<'a> System<'a> for HungerSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( 
+                        Entities<'a>,
+                        WriteStorage<'a, HungerClock>,
+                        ReadExpect<'a, Entity>, // The player
+                        ReadExpect<'a, RunState>,
+                        WriteStorage<'a, SufferDamage>,
+                        WriteExpect<'a, GameLog>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut hunger_clock, player_entity, runstate, mut inflict_damage, mut log) = data;
+
+        for (entity, mut clock) in (&entities, &mut hunger_clock).join() {
+            let mut proceed = false;
+
+            match *runstate {
+                RunState::PlayerTurn => {
+                    if entity == *player_entity {
+                        proceed = true;
+                    }
+                }
+                RunState::MonsterTurn => {
+                    if entity != *player_entity {
+                        proceed = false;
+                    }
+                }
+                _ => proceed = false
+            }
+
+            if proceed {
+                clock.duration -= 1;
+                if clock.duration < 1 {
+                    match clock.state {
+                        HungerState::WellFed => {
+                            clock.state = HungerState::Normal;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are no longer well fed.".to_string());
+                            }
+                        }
+                        HungerState::Normal => {
+                            clock.state = HungerState::Hungry;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are hungry.".to_string());
+                            }
+                        }
+                        HungerState::Hungry => {
+                            clock.state = HungerState::Starving;
+                            clock.duration = 200;
+                            if entity == *player_entity {
+                                log.entries.insert(0, "You are starving!".to_string());
+                            }
+                        }
+                        HungerState::Starving => {
+                            // Inflict damage from hunger
+                            if entity == *player_entity {
+                                log.entries.insert(0, "Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string());
+                            }
+                            inflict_damage.insert(entity, SufferDamage{ amount: 1 }).expect("Unable to do damage");  
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 23 - 3
chapter-19-food/src/inventory_system.rs

@@ -2,7 +2,8 @@ extern crate specs;
 use specs::prelude::*;
 use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog, WantsToUseItem, 
     Consumable, ProvidesHealing, CombatStats, WantsToDropItem, InflictsDamage, Map, SufferDamage,
-    AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, particle_system::ParticleBuilder};
+    AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, particle_system::ParticleBuilder,
+    ProvidesFood, HungerClock, HungerState};
 
 pub struct ItemCollectionSystem {}
 
@@ -53,14 +54,17 @@ impl<'a> System<'a> for ItemUseSystem {
                         WriteStorage<'a, Equipped>,
                         WriteStorage<'a, InBackpack>,
                         WriteExpect<'a, ParticleBuilder>,
-                        ReadStorage<'a, Position>
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, ProvidesFood>,
+                        WriteStorage<'a, HungerClock>
                       );
 
     #[allow(clippy::cognitive_complexity)]
     fn run(&mut self, data : Self::SystemData) {
         let (player_entity, mut gamelog, map, entities, mut wants_use, names, 
             consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, 
-            aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions) = data;
+            aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions,
+            provides_food, mut hunger_clocks) = data;
 
         for (entity, useitem) in (&entities, &wants_use).join() {
             let mut used_item = true;
@@ -126,6 +130,22 @@ impl<'a> System<'a> for ItemUseSystem {
                 }
             }
 
+            // It it is edible, eat it!
+            let item_edible = provides_food.get(useitem.item);
+            match item_edible {
+                None => {}
+                Some(_) => {
+                    used_item = true;
+                    let target = targets[0];
+                    let hc = hunger_clocks.get_mut(target);
+                    if let Some(hc) = hc {
+                        hc.state = HungerState::WellFed;
+                        hc.duration = 20;
+                        gamelog.entries.insert(0, format!("You eat the {}.", names.get(useitem.item).unwrap().name));
+                    }
+                }
+            }
+
             // If it heals, apply the healing
             let item_heals = healing.get(useitem.item);
             match item_heals {

+ 4 - 0
chapter-19-food/src/main.rs

@@ -32,6 +32,7 @@ use inventory_system::{ ItemCollectionSystem, ItemUseSystem, ItemDropSystem, Ite
 pub mod saveload_system;
 pub mod random_table;
 pub mod particle_system;
+pub mod hunger_system;
 
 #[derive(PartialEq, Copy, Clone)]
 pub enum RunState { AwaitingInput, 
@@ -357,6 +358,7 @@ fn main() {
             .with(ItemUseSystem{}, "potions", &["melee_combat"])
             .with(ItemDropSystem{}, "drop_items", &["melee_combat"])
             .with(ItemRemoveSystem{}, "remove_items", &["melee_combat"])
+            .with(hunger_system::HungerSystem{}, "hunger", &["melee_combat", "potions"])
             .with(particle_system::ParticleSpawnSystem{}, "spawn_particles", &["potions", "melee_combat"])
             .build(),
     };
@@ -389,6 +391,8 @@ fn main() {
     gs.ecs.register::<DefenseBonus>();
     gs.ecs.register::<WantsToRemoveItem>();
     gs.ecs.register::<ParticleLifetime>();
+    gs.ecs.register::<HungerClock>();
+    gs.ecs.register::<ProvidesFood>();
 
     gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
 

+ 1 - 1
chapter-19-food/src/map.rs

@@ -190,7 +190,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 11 - 3
chapter-19-food/src/melee_combat_system.rs

@@ -1,7 +1,7 @@
 extern crate specs;
 use specs::prelude::*;
 use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog, MeleePowerBonus, DefenseBonus, Equipped,
-    particle_system::ParticleBuilder, Position};
+    particle_system::ParticleBuilder, Position, HungerClock, HungerState};
 
 pub struct MeleeCombatSystem {}
 
@@ -17,12 +17,13 @@ impl<'a> System<'a> for MeleeCombatSystem {
                         ReadStorage<'a, DefenseBonus>,
                         ReadStorage<'a, Equipped>,
                         WriteExpect<'a, ParticleBuilder>,
-                        ReadStorage<'a, Position>
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, HungerClock>
                       );
 
     fn run(&mut self, data : Self::SystemData) {
         let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, 
-            melee_power_bonuses, defense_bonuses, equipped, mut particle_builder, positions) = data;
+            melee_power_bonuses, defense_bonuses, equipped, mut particle_builder, positions, hunger_clock) = data;
 
         for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
             if stats.hp > 0 {
@@ -33,6 +34,13 @@ impl<'a> System<'a> for MeleeCombatSystem {
                     }
                 }
 
+                let hc = hunger_clock.get(entity);
+                if let Some(hc) = hc {
+                    if hc.state == HungerState::WellFed {
+                        offensive_bonus += 1;
+                    }
+                }
+
                 let target_stats = combat_stats.get(wants_melee.target).unwrap();
                 if target_stats.hp > 0 {
                     let target_name = names.get(wants_melee.target).unwrap();

+ 11 - 1
chapter-19-food/src/player.rs

@@ -3,7 +3,7 @@ use rltk::{VirtualKeyCode, Rltk, Point};
 extern crate specs;
 use specs::prelude::*;
 use super::{Position, Player, Viewshed, State, Map, RunState, CombatStats, WantsToMelee, Item,
-    gamelog::GameLog, WantsToPickupItem, TileType, Monster};
+    gamelog::GameLog, WantsToPickupItem, TileType, Monster, HungerClock, HungerState};
 
 pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
     let mut positions = ecs.write_storage::<Position>();
@@ -99,6 +99,16 @@ fn skip_turn(ecs: &mut World) -> RunState {
         }
     }
 
+    let hunger_clocks = ecs.read_storage::<HungerClock>();
+    let hc = hunger_clocks.get(*player_entity);
+    if let Some(hc) = hc {
+        match hc.state {
+            HungerState::Hungry => can_heal = false,
+            HungerState::Starving => can_heal = false,
+            _ => {}
+        }
+    }
+
     if can_heal {
         let mut health_components = ecs.write_storage::<CombatStats>();
         let player_hp = health_components.get_mut(*player_entity).unwrap();

+ 2 - 2
chapter-19-food/src/saveload_system.rs

@@ -39,7 +39,7 @@ pub fn save_game(ecs : &mut World) {
             Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
             AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
             WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus,
-            WantsToRemoveItem, ParticleLifetime
+            WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood
         );
     }
 
@@ -88,7 +88,7 @@ pub fn load_game(ecs: &mut World) {
             Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
             AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
             WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus,
-            WantsToRemoveItem, ParticleLifetime
+            WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood
         );
     }
 

+ 22 - 1
chapter-19-food/src/spawner.rs

@@ -4,7 +4,8 @@ extern crate specs;
 use specs::prelude::*;
 use super::{CombatStats, Player, Renderable, Name, Position, Viewshed, Monster, BlocksTile, Rect, Item, 
     Consumable, Ranged, ProvidesHealing, map::MAPWIDTH, InflictsDamage, AreaOfEffect, Confusion, SerializeMe,
-    random_table::RandomTable, EquipmentSlot, Equippable, MeleePowerBonus, DefenseBonus };
+    random_table::RandomTable, EquipmentSlot, Equippable, MeleePowerBonus, DefenseBonus, HungerClock,
+    HungerState, ProvidesFood };
 use crate::specs::saveload::{MarkedBuilder, SimpleMarker};
 use std::collections::HashMap;
 
@@ -23,6 +24,7 @@ pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
         .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 })
+        .with(HungerClock{ state: HungerState::WellFed, duration: 20 })
         .marked::<SimpleMarker<SerializeMe>>()
         .build()
 }
@@ -41,6 +43,7 @@ fn room_table(map_depth: i32) -> RandomTable {
         .add("Shield", 3)
         .add("Longsword", map_depth - 1)
         .add("Tower Shield", map_depth - 1)
+        .add("Rations", 10)
 }
 
 /// Fills a room with stuff!
@@ -87,6 +90,7 @@ pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) {
             "Shield" => shield(ecs, x, y),
             "Longsword" => longsword(ecs, x, y),
             "Tower Shield" => tower_shield(ecs, x, y),
+            "Rations" => rations(ecs, x, y),
             _ => {}
         }
     }
@@ -252,3 +256,20 @@ fn tower_shield(ecs: &mut World, x: i32, y: i32) {
         .marked::<SimpleMarker<SerializeMe>>()
         .build();
 }
+
+fn rations(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('%'),
+            fg: RGB::named(rltk::GREEN),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Rations".to_string() })
+        .with(Item{})
+        .with(ProvidesFood{})
+        .with(Consumable{})
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}