This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!
If you enjoy this and would like me to keep writing, please consider supporting my Patreon.
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.
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:
We'll make a new file, hunger_system.rs and implement a hunger clock system. It's quite straightforward:
#![allow(unused)]fnmain() {
use specs::prelude::*;
use super::{HungerClock, RunState, HungerState, SufferDamage, gamelog::GameLog};
pubstructHungerSystem {}
impl<'a> System<'a> for HungerSystem {
#[allow(clippy::type_complexity)]typeSystemData = (
Entities<'a>,
WriteStorage<'a, HungerClock>,
ReadExpect<'a, Entity>, // The player
ReadExpect<'a, RunState>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, GameLog>
);
fnrun(&mutself, 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() {
letmut proceed = false;
match *runstate {
RunState::PlayerTurn => {
if entity == *player_entity {
proceed = true;
}
}
RunState::MonsterTurn => {
if entity != *player_entity {
proceed = true;
}
}
_ => 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.push("You are no longer well fed.".to_string());
}
}
HungerState::Normal => {
clock.state = HungerState::Hungry;
clock.duration = 200;
if entity == *player_entity {
log.entries.push("You are hungry.".to_string());
}
}
HungerState::Hungry => {
clock.state = HungerState::Starving;
clock.duration = 200;
if entity == *player_entity {
log.entries.push("You are starving!".to_string());
}
}
HungerState::Starving => {
// Inflict damage from hungerif entity == *player_entity {
log.entries.push("Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string());
}
SufferDamage::new_damage(&mut inflict_damage, entity, 1);
}
}
}
}
}
}
}
}
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:
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:
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):
#![allow(unused)]fnmain() {
// 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);
ifletSome(hc) = hc {
hc.state = HungerState::WellFed;
hc.duration = 20;
gamelog.entries.push(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!
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:
#![allow(unused)]fnmain() {
let hc = hunger_clock.get(entity);
ifletSome(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.
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:
#![allow(unused)]fnmain() {
let hunger_clocks = ecs.read_storage::<HungerClock>();
let hc = hunger_clocks.get(*player_entity);
ifletSome(hc) = hc {
match hc.state {
HungerState::Hungry => can_heal = false,
HungerState::Starving => can_heal = false,
_ => {}
}
}
if can_heal {
}
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