Complete roguelike tutorial using C++ and libtcod - part 8: items and inventory

From RogueBasin
Jump to navigation Jump to search
Complete roguelike tutorial using C++ and libtcod
-originally written by Jice
Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21.


In this article, we will start to add items to the dungeon, in the form of health potions. This will make it possible to kill every monster in the dungeon for the first time, even if the engine won't yet detect the end of game.

libtcod functions used in this article :

TCODConsole::printFrame

TCODSystem::waitForEvent

Actors that can be healed

For the health potion to work, we first need to be able to heal a Destructible. Let's add this. Destructible.hpp :

float heal(float amount);

Destructible.cpp :

float Destructible::heal(float amount) {
   hp += amount;
   if ( hp > maxHp ) {
       amount -= hp-maxHp;
       hp=maxHp;
   }
   return amount;
}

The function returns the amount of health point actually restored.

Pickable actors and containers

Now, we can start to make items that we can pick. Roguelikes often make a strong separation between creatures and items. Creatures have inventory and items can be picked. This can lead to unnecessary complications. For example, a chest is an item, has an inventory and cannot be picked.

Here we will break this separation and simply create actors that can be picked (and used) and actors that can contain other actors (a chest or a creature with an inventory). Let's add those two new features to our almighty Actor class :

Ai *ai; // something self-updating
Pickable *pickable; // something that can be picked and used
Container *container; // something that can contain actors

Include the headers (yet to be written) in main.hpp :

#include "Ai.hpp"
#include "Pickable.hpp"
#include "Container.hpp"
#include "Actor.hpp"

And initialize the features in the Actor constructor :

Actor::Actor(int x, int y, int ch, const char *name,
   const TCODColor &col) :
   x(x),y(y),ch(ch),col(col), name(name),
   blocks(true),attacker(NULL),destructible(NULL),ai(NULL),
   pickable(NULL),container(NULL) {
}   

Ok. So far we didn't bother with what happens when an actor is destroyed (because it never happens in the game since even a dead creature is an Actor). But when we're going to drink a health potion, we expect it to disappear from our inventory, so we're actually doing to delete the Actor object. Let's add a destructor that cleans the features :

Actor.hpp:

Actor(int x, int y, int ch, const char *name, const TCODColor &col);
~Actor();
void update();

Actor.cpp :

Actor::~Actor() {
   if ( attacker ) delete attacker;
   if ( destructible ) delete destructible;
   if ( ai ) delete ai;
   if ( pickable ) delete pickable;
   if ( container ) delete container;
}

Container implementation

Our container will obviously contain a list of actors and a capacity : Container.hpp :

class Container {
public :
   int size; // maximum number of actors. 0=unlimited
   TCODList<Actor *> inventory;       

   Container(int size);
   ~Container();
   bool add(Actor *actor);
   void remove(Actor *actor);
};

The constructor initializes the size field :

Container::Container(int size) : size(size) {
}

The destructor deletes all actors in the container :

Container::~Container() {
   inventory.clearAndDelete();
}

The add method checks that the container is not full.

bool Container::add(Actor *actor) {
   if ( size > 0 && inventory.size() >= size ) {
       // inventory full
       return false;
   }

Then add the actor :

   inventory.push(actor);
   return true;
}

The remove method... well you already know :

void Container::remove(Actor *actor) {
   inventory.remove(actor);
}

Ok that was really easy. Let's add a 26 slots inventory to the player. Why 26 ? To be able to affect a letter shortcut to each slot. In the Engine constructor :

player->ai = new PlayerAi();
player->container = new Container(26);
actors.push(player);

Pickable actors implementation

We want to be able to pick them (remove them from the dungeon and put them in our inventory), and use them (whatever that means) :

class Pickable {
public :
   bool pick(Actor *owner, Actor *wearer);
   virtual bool use(Actor *owner, Actor *wearer);
};

Obviously, the picking action is the same for every object whereas using depends on the object type. That's why the use function is virtual. Every function has a parameter for the Actor containing the Pickable feature (the owner) and the Actor that picks/uses the Pickable (for now, this will always be the player).

The pick function checks that the wearer has a container and tries to add the pickable actor to the wearer's container. It returns true if it worked, false if the container is full.

bool Pickable::pick(Actor *owner, Actor *wearer) {
   if ( wearer->container && wearer->container->add(owner) ) {
       engine.actors.remove(owner);
       return true;
   }
   return false;
}

Even though the use function depends on the type of item, the part where we destroy the used object might be used by most of them so we implement it in the base class :

bool Pickable::use(Actor *owner, Actor *wearer) {
   if ( wearer->container ) {
       wearer->container->remove(owner);
       delete owner;
       return true;
   }
   return false;
}

An item cannot always be used. For example, we'll keep the player from drinking a health potion if he already has all his health points. That's why the use function returns a boolean.

Ok now that we have some abstract pickable item, let's implement some real item : the health potion. This is simply a class extending Pickable and overloading the use function to heal the wearer. We're not using HealthPotion as class name because it's more generic. It can be used by most food items, healing scrolls and whatever may give HP to the player.

class Healer : public Pickable {
public :
   float amount; // how many hp

   Healer(float amount);
   bool use(Actor *owner, Actor *wearer);
};

Guess what the constructor implementation is ?

Healer::Healer(float amount) : amount(amount) {
}

Now the use function :

bool Healer::use(Actor *owner, Actor *wearer) {
   if ( wearer->destructible ) {
       float amountHealed = wearer->destructible->heal(amount);
       if ( amountHealed > 0 ) {
           return Pickable::use(owner,wearer);
       }
   }
   return false;
}

First we check that the wearer is destructible (thus can be healed). Then we try to heal it. If it worked, we use the Pickable::use function to remove the health potion from the inventory and destroy it.

Now all we have to do is disseminate some health potions on the map. Let's add a addItem function in Map.hpp :

void addItem(int x, int y);

And the implementation :

static const int MAX_ROOM_ITEMS = 2;
void Map::addItem(int x, int y) {
   Actor *healthPotion=new Actor(x,y,'!',"health potion",
       TCODColor::violet);
   healthPotion->blocks=false;
   healthPotion->pickable=new Healer(4);
   engine.actors.push(healthPotion);
}

And we add another loop in Map::createRoom :

// add items
int nbItems=rng->getInt(0,MAX_ROOM_ITEMS);
while (nbItems > 0) {
   int x=rng->getInt(x1,x2);
   int y=rng->getInt(y1,y2);
   if ( canWalk(x,y) ) {
       addItem(x,y);
   }
   nbItems--;
}