Friday, September 23, 2011

roguelike tutorial 11: hunger and food

Now that we've got monsters to kill and the ability to pick up and use things, how about we add some corpses and the ability to eat them?

We first need to update our Item class to support some nutritional value.
private int foodValue;
public int foodValue() { return foodValue; }
public void modifyFoodValue(int amount) { foodValue += amount; }

And update our creature to leave corpses.

public void modifyHp(int amount) {
hp += amount;
if (hp < 1) {
doAction("die");
leaveCorpse();
world.remove(this);
}
}

private void leaveCorpse(){
Item corpse = new Item('%', color, name + " corpse");
corpse.modifyFoodValue(maxHp * 3);
world.addAtEmptySpace(corpse, x, y, z);
}

Update creatures to also have hunger.

private int maxFood;
public int maxFood() { return maxFood; }

private int food;
public int food() { return food; }

public void modifyFood(int amount) {
food += amount;
if (food > maxFood) {
food = maxFood;
} else if (food < 1 && glyph == '@') {
modifyHp(-1000);
}
}

Do you see the terrible hack there? We only want the player to be able to die of starvation since it would be boring if every monster dropped dead of starvation and if they need to eat they'd have to go around killing each other. We could have an entire ecosystem of bats farming fungus, that would introduce some neat gameplay options, but that's quite a bit more complicated than I'd like to do right now. Anyway dying only if you look like a @ is still an ugly hack — a hack so ugly our children's children will feel the shame. Let's fix it right now:

public void modifyFood(int amount) {
food += amount;
if (food > maxFood) {
food = maxFood;
} else if (food < 1 && isPlayer()) {
modifyHp(-1000);
}
}

public boolean isPlayer(){
return glyph == '@';
}

The hack is still there but it's isolated for now. Later if we have other creatures with an @ glyph or if the player can assume other forms, we can update this one isolated place. One thing I've learned from real life software is that although ugly hacks are inevitable, you can always isolate them so the callers don't need to deal with it.

But enough preaching, our Creatures also need a method to eat.

public void eat(Item item){
modifyFood(item.foodValue());
inventory.remove(item);
}

Don't forget that creatures should start with decent belly full. Add this to the creature constructor:

this.maxFood = 1000;
this.food = maxFood / 3 * 2;

Now add an EatScreen so we can eat something in our inventory.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EatScreen extends InventoryBasedScreen {

public EatScreen(Creature player) {
super(player);
}

protected String getVerb() {
return "eat";
}

protected boolean isAcceptable(Item item) {
return item.foodValue() != 0;
}

protected Screen use(Item item) {
player.eat(item);
return null;
}
}

Wow, that was easy. InventoryBasedScreen is paying off already.


For the PlayScreen we need to map the 'e' key to the EatScreen.
case KeyEvent.VK_E: subscreen = new EatScreen(player); break;
We should also let the player know how hungry he is. Change the stats in displayOutput to this:
String stats = String.format(" %3d/%3d hp %8s", player.hp(), player.maxHp(), hunger());
And add a helper method. You can use whatever text and amounts you want.
private String hunger(){
if (player.food() < player.maxFood() * 0.1)
return "Starving";
else if (player.food() < player.maxFood() * 0.2)
return "Hungry";
else if (player.food() > player.maxFood() * 0.9)
return "Stuffed";
else if (player.food() > player.maxFood() * 0.8)
return "Full";
else
return "";
}

Of course none of this will do anything if we don't use up the food we've eaten. Go ahead and add a call to modifyFood in the relevant creature methods. Here's a couple examples:
public void dig(int wx, int wy, int wz) {
modifyFood(-10);
world.dig(wx, wy, wz);
doAction("dig");
}
public void update(){
modifyFood(-1);
ai.onUpdate();
}

Go ahead and use the food values you want. You should play around with it for a while to decide what feels right. Maybe you want starvation to be a serious problem and hunting bats is the only way to stay alive or maybe you want starvation to hardly ever happen. Maybe heroes start with an inventory full of supplies or maybe they start with an empty and growling belly — as the designer it's up to you.

While looking at the modifyFood method I noticed we don't prevent hp from going higher than maxHp. Even though we don't have a way to do that yet you should add a check for that.


If we eat more than the maxFood shouldn't our stomach stretch and increase our maxFood? Or maybe the user should explode from overeating? Here's my implementation:
public void modifyFood(int amount) {
food += amount;

if (food > maxFood) {
maxFood = maxFood + food / 2;
food = maxFood;
notify("You can't believe your stomach can hold that much!");
modifyHp(-1);
} else if (food < 1 && isPlayer()) {
modifyHp(-1000);
}
}

It's a subtle effect but it gives the player a decision to make when full and carrying a lot of food and under the right circumstances overeating may become a useful strategy.

Now go ahead and add some food to your world. Bread, meat, apples, whatever.

download the code

No comments:

Post a Comment

Popular Posts