Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 8: items and inventory"

From RogueBasin
Jump to navigation Jump to search
(Add syntaxhighlight.)
 
(2 intermediate revisions by one other user not shown)
Line 16: Line 16:
Destructible.hpp :
Destructible.hpp :


float heal(float amount);
<syntaxhighlight lang="C++">
float heal(float amount);
</syntaxhighlight>


Destructible.cpp :
Destructible.cpp :


float Destructible::heal(float amount) {
<syntaxhighlight lang="C++">
float Destructible::heal(float amount) {
     hp += amount;
     hp += amount;
     if ( hp > maxHp ) {
     if ( hp > maxHp ) {
Line 27: Line 30:
     }
     }
     return amount;
     return amount;
}
}
</syntaxhighlight>


The function returns the amount of health point actually restored.
The function returns the amount of health point actually restored.
Line 37: Line 41:
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 :
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
<syntaxhighlight lang="C++" highlight="2-3">
<span style="color:green">Pickable *pickable; // something that can be picked and used
Ai *ai; // something self-updating
Container *container; // something that can contain actors</span>
Pickable *pickable; // something that can be picked and used
Container *container; // something that can contain actors
</syntaxhighlight>


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


#include "Ai.hpp"
<syntaxhighlight lang="C++" highlight="2-3">
<span style="color:green">#include "Pickable.hpp"
#include "Ai.hpp"
#include "Container.hpp"</span>
#include "Pickable.hpp"
#include "Actor.hpp"
#include "Container.hpp"
#include "Actor.hpp"
</syntaxhighlight>


And initialize the features in the Actor constructor :
And initialize the features in the Actor constructor :


Actor::Actor(int x, int y, int ch, const char *name,
<syntaxhighlight lang="C++" highlight="5">
Actor::Actor(int x, int y, int ch, const char *name,
     const TCODColor &col) :
     const TCODColor &col) :
     x(x),y(y),ch(ch),col(col), name(name),
     x(x),y(y),ch(ch),col(col), name(name),
     blocks(true),attacker(NULL),destructible(NULL),ai(NULL),
     blocks(true),attacker(NULL),destructible(NULL),ai(NULL),
     <span style="color:green">pickable(NULL),container(NULL) {</span>
     pickable(NULL),container(NULL) {
}
</syntaxhighlight>


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  
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  
Line 62: Line 72:
Actor.hpp:
Actor.hpp:


Actor(int x, int y, int ch, const char *name, const TCODColor &col);
<syntaxhighlight lang="C++" highlight="2">
<span style="color:green">~Actor();</span>
Actor(int x, int y, int ch, const char *name, const TCODColor &col);
void update();
~Actor();
void update();
</syntaxhighlight>


Actor.cpp :
Actor.cpp :


Actor::~Actor() {
<syntaxhighlight lang="C++">
Actor::~Actor() {
     if ( attacker ) delete attacker;
     if ( attacker ) delete attacker;
     if ( destructible ) delete destructible;
     if ( destructible ) delete destructible;
Line 74: Line 87:
     if ( pickable ) delete pickable;
     if ( pickable ) delete pickable;
     if ( container ) delete container;
     if ( container ) delete container;
}
}
</syntaxhighlight>
 
Since Destructible, Ai, and Pickable will be derived classes, we need to define a virtual destructor for each in their respective hpp files in order to safely delete them:
 
<syntaxhighlight lang="C++">
virtual ~Destructible() {};
</syntaxhighlight>
 
<syntaxhighlight lang="C++">
virtual ~Ai() {};
</syntaxhighlight>
 
<syntaxhighlight lang="C++">
virtual ~Pickable() {};
</syntaxhighlight>


==Container implementation==
==Container implementation==
Line 81: Line 109:
Container.hpp :
Container.hpp :


class Container {
<syntaxhighlight lang="C++">
public :
class Container {
public :
     int size; // maximum number of actors. 0=unlimited
     int size; // maximum number of actors. 0=unlimited
     TCODList<Actor *> inventory;       
     TCODList<Actor *> inventory;       
Line 90: Line 119:
     bool add(Actor *actor);
     bool add(Actor *actor);
     void remove(Actor *actor);
     void remove(Actor *actor);
};
};
</syntaxhighlight>


The constructor initializes the size field :
The constructor initializes the size field :


Container::Container(int size) : size(size) {
<syntaxhighlight lang="C++">
}
Container::Container(int size) : size(size) {
}
</syntaxhighlight>


The destructor deletes all actors in the container :
The destructor deletes all actors in the container :


Container::~Container() {
<syntaxhighlight lang="C++">
Container::~Container() {
     inventory.clearAndDelete();
     inventory.clearAndDelete();
}
}
</syntaxhighlight>


The add method checks that the container is not full.
The add method checks that the container is not full.


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


Then add the actor :   
Then add the actor :   


<syntaxhighlight lang="C++">
     inventory.push(actor);
     inventory.push(actor);
     return true;
     return true;
}
}
</syntaxhighlight>


The remove method... well you already know :
The remove method... well you already know :


void Container::remove(Actor *actor) {
<syntaxhighlight lang="C++">
void Container::remove(Actor *actor) {
     inventory.remove(actor);
     inventory.remove(actor);
}
}
</syntaxhighlight>


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


player->ai = new PlayerAi();
<syntaxhighlight lang="C++" highlight="2">
<span style="color:green">player->container = new Container(26);</span>
player->ai = new PlayerAi();
actors.push(player);
player->container = new Container(26);
actors.push(player);
</syntaxhighlight>


==Pickable actors implementation==
==Pickable actors implementation==
Line 133: Line 175:
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) :
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 {
<syntaxhighlight lang="C++">
public :
class Pickable {
public :
     bool pick(Actor *owner, Actor *wearer);
     bool pick(Actor *owner, Actor *wearer);
     virtual bool use(Actor *owner, Actor *wearer);
     virtual bool use(Actor *owner, Actor *wearer);
};
};
</syntaxhighlight>


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.
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.
Line 144: Line 188:
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.
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) {
<syntaxhighlight lang="C++">
bool Pickable::pick(Actor *owner, Actor *wearer) {
     if ( wearer->container && wearer->container->add(owner) ) {
     if ( wearer->container && wearer->container->add(owner) ) {
         engine.actors.remove(owner);
         engine.actors.remove(owner);
Line 150: Line 195:
     }
     }
     return false;
     return false;
}
}
</syntaxhighlight>


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 :
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) {
<syntaxhighlight lang="C++">
bool Pickable::use(Actor *owner, Actor *wearer) {
     if ( wearer->container ) {
     if ( wearer->container ) {
         wearer->container->remove(owner);
         wearer->container->remove(owner);
Line 161: Line 208:
     }
     }
     return false;
     return false;
}
}
</syntaxhighlight>


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.
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.
Line 167: Line 215:
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.
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 {
<syntaxhighlight lang="C++">
public :
class Healer : public Pickable {
public :
     float amount; // how many hp
     float amount; // how many hp
   
   
     Healer(float amount);
     Healer(float amount);
     bool use(Actor *owner, Actor *wearer);
     bool use(Actor *owner, Actor *wearer);
};
};
</syntaxhighlight>


Guess what the constructor implementation is ?
Guess what the constructor implementation is ?


Healer::Healer(float amount) : amount(amount) {
<syntaxhighlight lang="C++">
}
Healer::Healer(float amount) : amount(amount) {
}
</syntaxhighlight>


Now the use function :
Now the use function :


bool Healer::use(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
bool Healer::use(Actor *owner, Actor *wearer) {
     if ( wearer->destructible ) {
     if ( wearer->destructible ) {
         float amountHealed = wearer->destructible->heal(amount);
         float amountHealed = wearer->destructible->heal(amount);
Line 190: Line 243:
     }
     }
     return false;
     return false;
}
}
</syntaxhighlight>


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.
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.
Line 196: Line 250:
Now all we have to do is disseminate some health potions on the map. Let's add a addItem function in Map.hpp :
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);
<syntaxhighlight lang="C++">
void addItem(int x, int y);
</syntaxhighlight>


And the implementation :
And the implementation :


static const int MAX_ROOM_ITEMS = 2;
<syntaxhighlight lang="C++">
static const int MAX_ROOM_ITEMS = 2;


void Map::addItem(int x, int y) {
void Map::addItem(int x, int y) {
     Actor *healthPotion=new Actor(x,y,'!',"health potion",
     Actor *healthPotion=new Actor(x,y,'!',"health potion",
         TCODColor::violet);
         TCODColor::violet);
Line 208: Line 265:
     healthPotion->pickable=new Healer(4);
     healthPotion->pickable=new Healer(4);
     engine.actors.push(healthPotion);
     engine.actors.push(healthPotion);
}
}
</syntaxhighlight>


And we add another loop in Map::createRoom :
And we add another loop in Map::createRoom :


// add items
<syntaxhighlight lang="C++">
int nbItems=rng->getInt(0,MAX_ROOM_ITEMS);
// add items
while (nbItems > 0) {
int nbItems=rng->getInt(0,MAX_ROOM_ITEMS);
while (nbItems > 0) {
     int x=rng->getInt(x1,x2);
     int x=rng->getInt(x1,x2);
     int y=rng->getInt(y1,y2);
     int y=rng->getInt(y1,y2);
Line 221: Line 280:
     }
     }
     nbItems--;
     nbItems--;
}
}
</syntaxhighlight>


==Interface with the player==
==Interface with the player==
Line 229: Line 289:
Ai.hpp :
Ai.hpp :


void handleActionKey(Actor *owner, int ascii);
<syntaxhighlight lang="C++">
void handleActionKey(Actor *owner, int ascii);
</syntaxhighlight>


In PlayerAi::update, we call this method when the key is a non special key :
In PlayerAi::update, we call this method when the key is a non special key :


case TCODK_RIGHT : dx=1; break;
<syntaxhighlight lang="C++" highlight="2">
<span style="color:green">case TCODK_CHAR : handleActionKey(owner, engine.lastKey.c); break;</span>
case TCODK_RIGHT : dx=1; break;
default:break;
case TCODK_CHAR : handleActionKey(owner, engine.lastKey.c); break;
default:break;
</syntaxhighlight>


The implementation scans the actors list and look for pickable actors on the player's tile.
The implementation scans the actors list and look for pickable actors on the player's tile.


void PlayerAi::handleActionKey(Actor *owner, int ascii) {
<syntaxhighlight lang="C++">
void PlayerAi::handleActionKey(Actor *owner, int ascii) {
     switch(ascii) {
     switch(ascii) {
         case 'g' : // pickup item
         case 'g' : // pickup item
Line 248: Line 313:
                 Actor *actor=*iterator;
                 Actor *actor=*iterator;
                 if ( actor->pickable && actor->x == owner->x && actor->y == owner->y ) {
                 if ( actor->pickable && actor->x == owner->x && actor->y == owner->y ) {
</syntaxhighlight>


Then it tries to pick the item (it might fail if the inventory is full) :
Then it tries to pick the item (it might fail if the inventory is full) :


<syntaxhighlight lang="C++">
         if (actor->pickable->pick(actor,owner)) {
         if (actor->pickable->pick(actor,owner)) {
             found=true;
             found=true;
             engine.gui->message(TCODColor::lightGrey,"You pick the %s.",
             engine.gui->message(TCODColor::lightGrey,"You pick up the %s.",
                 actor->name);
                 actor->name);
             break;
             break;
Line 261: Line 328:
         }
         }
     }
     }
}
}
if (!found) {
if (!found) {
     engine.gui->message(TCODColor::lightGrey,"There's nothing here that you can pick.");
     engine.gui->message(TCODColor::lightGrey,"There's nothing here that you can pick up.");
}
}
</syntaxhighlight>


The found boolean serves two purposes here :
The found boolean serves two purposes here :
Line 275: Line 343:
We could fix this by replacing the loop ending condition
We could fix this by replacing the loop ending condition


iterator != engine.actors.end()
<syntaxhighlight lang="C++">
iterator != engine.actors.end()
</syntaxhighlight>


with
with


iterator < engine.actors.end()
<syntaxhighlight lang="C++">
iterator < engine.actors.end()
</syntaxhighlight>


but I find it nice and more realistic to have to spend one turn for each item to pick. Moreover, this only concerns tiles where there are more than one item. This shouldn't happen too often.
but I find it nice and more realistic to have to spend one turn for each item to pick. Moreover, this only concerns tiles where there are more than one item. This shouldn't happen too often.
Whether we pick an item or not, we allow the monsters to update. This means that even trying to pick something on an empty tile will trigger a new turn :
Whether we pick an item or not, we allow the monsters to update. This means that even trying to pick something on an empty tile will trigger a new turn :


<syntaxhighlight lang="C++">
             engine.gameStatus=Engine::NEW_TURN;
             engine.gameStatus=Engine::NEW_TURN;
         }
         }
         break;
         break;
     }
     }
}
}
</syntaxhighlight>
There one last thing we need to change. So far, we display the corpses present on the player's tile in the message log. Let's change this to display also items (or rather pickable actors). This is in PlayerAi::moveOrAttack :
There one last thing we need to change. So far, we display the corpses present on the player's tile in the message log. Let's change this to display also items (or rather pickable actors). This is in PlayerAi::moveOrAttack :


<span style="color:green">// look for corpses or items</span>
<syntaxhighlight lang="C++" highlight="1,5-6">
for (Actor **iterator=engine.actors.begin();
// look for corpses or items
for (Actor **iterator=engine.actors.begin();
     iterator != engine.actors.end(); iterator++) {
     iterator != engine.actors.end(); iterator++) {
     Actor *actor=*iterator;
     Actor *actor=*iterator;
     <span style="color:green">bool corpseOrItem=(actor->destructible && actor->destructible->isDead())
     bool corpseOrItem=(actor->destructible && actor->destructible->isDead())
         || actor->pickable;</span>
         || actor->pickable;
     if ( corpseOrItem
     if ( corpseOrItem
         && actor->x == targetx && actor->y == targety ) {
         && actor->x == targetx && actor->y == targety ) {
         engine.gui->message(TCODColor::lightGrey,"There's a %s here.",actor->name);
         engine.gui->message(TCODColor::lightGrey,"There's a %s here.",actor->name);
     }
     }
}
}
</syntaxhighlight>


Now you can compile and pick all the health potions ! But you can't use them yet.
Now you can compile and pick all the health potions ! But you can't use them yet.
Line 309: Line 385:
We will use the 'i' key to display the player inventory. Each item will have a shortcut that will allow to use it. Let's add a protected function in PlayerAi to select an item from the inventory :
We will use the 'i' key to display the player inventory. Each item will have a shortcut that will allow to use it. Let's add a protected function in PlayerAi to select an item from the inventory :


Actor *choseFromInventory(Actor *owner);
<syntaxhighlight lang="C++">
Actor *choseFromInventory(Actor *owner);
</syntaxhighlight>


If the player presses a key that is not an item shortcut, the function returns NULL.
If the player presses a key that is not an item shortcut, the function returns NULL.
Line 315: Line 393:
The function defines some constant for the inventory screen and creates an offscreen console :
The function defines some constant for the inventory screen and creates an offscreen console :


Actor *PlayerAi::choseFromInventory(Actor *owner) {
<syntaxhighlight lang="C++">
Actor *PlayerAi::choseFromInventory(Actor *owner) {
     static const int INVENTORY_WIDTH=50;
     static const int INVENTORY_WIDTH=50;
     static const int INVENTORY_HEIGHT=28;
     static const int INVENTORY_HEIGHT=28;
     static TCODConsole con(INVENTORY_WIDTH,INVENTORY_HEIGHT);
     static TCODConsole con(INVENTORY_WIDTH,INVENTORY_HEIGHT);
</syntaxhighlight>


Note that thanks to the static keyword, the console is only created the first time, and not every time we open the inventory. The inventory has a capacity of 26 items but the console's height is 28 because we draw a frame around it. We use some libtcod helper function to do that :
Note that thanks to the static keyword, the console is only created the first time, and not every time we open the inventory. The inventory has a capacity of 26 items but the console's height is 28 because we draw a frame around it. We use some libtcod helper function to do that :


// display the inventory frame
<syntaxhighlight lang="C++">
con.setDefaultForeground(TCODColor(200,180,50));
// display the inventory frame
con.printFrame(0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,true,
con.setDefaultForeground(TCODColor(200,180,50));
con.printFrame(0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,true,
     TCOD_BKGND_DEFAULT,"inventory");
     TCOD_BKGND_DEFAULT,"inventory");
</syntaxhighlight>


Then we scan the player's inventory and print every item with its shortcut :
Then we scan the player's inventory and print every item with its shortcut :


// display the items with their keyboard shortcut
<syntaxhighlight lang="C++">
con.setDefaultForeground(TCODColor::white);
// display the items with their keyboard shortcut
int shortcut='a';
con.setDefaultForeground(TCODColor::white);
int y=1;
int shortcut='a';
for (Actor **it=owner->container->inventory.begin();
int y=1;
for (Actor **it=owner->container->inventory.begin();
     it != owner->container->inventory.end(); it++) {
     it != owner->container->inventory.end(); it++) {
     Actor *actor=*it;
     Actor *actor=*it;
Line 339: Line 422:
     y++;
     y++;
     shortcut++;
     shortcut++;
}
}
</syntaxhighlight>


'a' is a char, but a char in C is nothing else than an integer ascii value, so we can increment it as any integer value.
'a' is a char, but a char in C is nothing else than an integer ascii value, so we can increment it as any integer value.
Line 345: Line 429:
Then we blit the inventory console on the root console, and call the flush function to make sure that it is visible on the game window :
Then we blit the inventory console on the root console, and call the flush function to make sure that it is visible on the game window :


// blit the inventory console on the root console
<syntaxhighlight lang="C++">
TCODConsole::blit(&con, 0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,
// blit the inventory console on the root console
TCODConsole::blit(&con, 0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,
     TCODConsole::root, engine.screenWidth/2 - INVENTORY_WIDTH/2,
     TCODConsole::root, engine.screenWidth/2 - INVENTORY_WIDTH/2,
     engine.screenHeight/2-INVENTORY_HEIGHT/2);
     engine.screenHeight/2-INVENTORY_HEIGHT/2);
TCODConsole::flush();
TCODConsole::flush();
</syntaxhighlight>


Then we wait for the player to press a key :
Then we wait for the player to press a key :


// wait for a key press
<syntaxhighlight lang="C++">
TCOD_key_t key;
// wait for a key press
TCODSystem::waitForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL,true);
TCOD_key_t key;
TCODSystem::waitForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL,true);
</syntaxhighlight>


Since the items shortcuts range from 'a' to 'z', we only deal with TCODK_CHAR values :
Since the items shortcuts range from 'a' to 'z', we only deal with TCODK_CHAR values :


if ( key.vk == TCODK_CHAR ) {
<syntaxhighlight lang="C++">
if ( key.vk == TCODK_CHAR ) {
     int actorIndex=key.c - 'a';
     int actorIndex=key.c - 'a';
</syntaxhighlight>


Now we did something strange. A character subtraction. Well if you subtract the ascii code of the pressed key (say 'b') with the ascii code of 'a', you get the item number in the inventory ('b' - 'a' == 1). Of course, if the player presses a key out of the 'a'-'z' range, the index will not be correct so we add another check :
Now we did something strange. A character subtraction. Well if you subtract the ascii code of the pressed key (say 'b') with the ascii code of 'a', you get the item number in the inventory ('b' - 'a' == 1). Of course, if the player presses a key out of the 'a'-'z' range, the index will not be correct so we add another check :


if ( actorIndex >= 0 && actorIndex < owner->container->inventory.size() ) {
<syntaxhighlight lang="C++">
if ( actorIndex >= 0 && actorIndex < owner->container->inventory.size() ) {
     return owner->container->inventory.get(actorIndex);
     return owner->container->inventory.get(actorIndex);
}
}
</syntaxhighlight>
If no valid item was selected, return NULL :
If no valid item was selected, return NULL :


<syntaxhighlight lang="C++">
     }
     }
     return NULL;
     return NULL;
}
}
</syntaxhighlight>


Now that we can pick an item, we can handle the 'i' key in the handleActionKey function :
Now that we can pick an item, we can handle the 'i' key in the handleActionKey function :


case 'i' : // display inventory
<syntaxhighlight lang="C++">
{
case 'i' : // display inventory
{
     Actor *actor=choseFromInventory(owner);
     Actor *actor=choseFromInventory(owner);
     if ( actor ) {
     if ( actor ) {
Line 382: Line 477:
         engine.gameStatus=Engine::NEW_TURN;
         engine.gameStatus=Engine::NEW_TURN;
     }
     }
}
}
break;
break;
</syntaxhighlight>


Again, using an item consumes a game turn, even if Pickable::use returns false. For exemple, trying to drink a health potion while having all your health points will consume a turn.
Again, using an item consumes a game turn, even if Pickable::use returns false. For exemple, trying to drink a health potion while having all your health points will consume a turn.

Latest revision as of 09:19, 20 July 2022

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

View source here

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;
}

Since Destructible, Ai, and Pickable will be derived classes, we need to define a virtual destructor for each in their respective hpp files in order to safely delete them:

virtual ~Destructible() {};
virtual ~Ai() {};
virtual ~Pickable() {};

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 effect 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--;
}

Interface with the player

Now we need some key shortcut to pick items on the ground. We add a handleKeyAction method to PlayerAi to handle all non movement key presses :

Ai.hpp :

void handleActionKey(Actor *owner, int ascii);

In PlayerAi::update, we call this method when the key is a non special key :

case TCODK_RIGHT : dx=1; break;
case TCODK_CHAR : handleActionKey(owner, engine.lastKey.c); break;
default:break;

The implementation scans the actors list and look for pickable actors on the player's tile.

void PlayerAi::handleActionKey(Actor *owner, int ascii) {
    switch(ascii) {
        case 'g' : // pickup item
        {
            bool found=false;
            for (Actor **iterator=engine.actors.begin();
                iterator != engine.actors.end(); iterator++) {
                Actor *actor=*iterator;
                if ( actor->pickable && actor->x == owner->x && actor->y == owner->y ) {

Then it tries to pick the item (it might fail if the inventory is full) :

        if (actor->pickable->pick(actor,owner)) {
            found=true;
            engine.gui->message(TCODColor::lightGrey,"You pick up the %s.",
                actor->name);
            break;
        } else if (! found) {
            found=true;
            engine.gui->message(TCODColor::red,"Your inventory is full.");
        }
    }
}
if (!found) {
    engine.gui->message(TCODColor::lightGrey,"There's nothing here that you can pick up.");
}

The found boolean serves two purposes here :

  • display a message if no pickable item was found.
  • display a message if the inventory is full (but only once).

Something very important : we only pick the first item up, then break from the loop. Why ? Picking the item removes the actor from the actors list. When we pick the last actor of the list, the iterator will go beyond the list's end() pointer. If we keep looping, the game will crash.

We could fix this by replacing the loop ending condition

iterator != engine.actors.end()

with

iterator < engine.actors.end()

but I find it nice and more realistic to have to spend one turn for each item to pick. Moreover, this only concerns tiles where there are more than one item. This shouldn't happen too often. Whether we pick an item or not, we allow the monsters to update. This means that even trying to pick something on an empty tile will trigger a new turn :

            engine.gameStatus=Engine::NEW_TURN;
        }
        break;
    }
}

There one last thing we need to change. So far, we display the corpses present on the player's tile in the message log. Let's change this to display also items (or rather pickable actors). This is in PlayerAi::moveOrAttack :

// look for corpses or items
for (Actor **iterator=engine.actors.begin();
    iterator != engine.actors.end(); iterator++) {
    Actor *actor=*iterator;
    bool corpseOrItem=(actor->destructible && actor->destructible->isDead())
        || actor->pickable;
    if ( corpseOrItem
         && actor->x == targetx && actor->y == targety ) {
        engine.gui->message(TCODColor::lightGrey,"There's a %s here.",actor->name);
    }
}

Now you can compile and pick all the health potions ! But you can't use them yet.

Last but not least, the inventory

We will use the 'i' key to display the player inventory. Each item will have a shortcut that will allow to use it. Let's add a protected function in PlayerAi to select an item from the inventory :

Actor *choseFromInventory(Actor *owner);

If the player presses a key that is not an item shortcut, the function returns NULL.

The function defines some constant for the inventory screen and creates an offscreen console :

Actor *PlayerAi::choseFromInventory(Actor *owner) {
    static const int INVENTORY_WIDTH=50;
    static const int INVENTORY_HEIGHT=28;
    static TCODConsole con(INVENTORY_WIDTH,INVENTORY_HEIGHT);

Note that thanks to the static keyword, the console is only created the first time, and not every time we open the inventory. The inventory has a capacity of 26 items but the console's height is 28 because we draw a frame around it. We use some libtcod helper function to do that :

// display the inventory frame
con.setDefaultForeground(TCODColor(200,180,50));
con.printFrame(0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,true,
    TCOD_BKGND_DEFAULT,"inventory");

Then we scan the player's inventory and print every item with its shortcut :

// display the items with their keyboard shortcut
con.setDefaultForeground(TCODColor::white);
int shortcut='a';
int y=1;
for (Actor **it=owner->container->inventory.begin();
    it != owner->container->inventory.end(); it++) {
    Actor *actor=*it;
    con.print(2,y,"(%c) %s", shortcut, actor->name);
    y++;
    shortcut++;
}

'a' is a char, but a char in C is nothing else than an integer ascii value, so we can increment it as any integer value.

Then we blit the inventory console on the root console, and call the flush function to make sure that it is visible on the game window :

// blit the inventory console on the root console
TCODConsole::blit(&con, 0,0,INVENTORY_WIDTH,INVENTORY_HEIGHT,
    TCODConsole::root, engine.screenWidth/2 - INVENTORY_WIDTH/2,
    engine.screenHeight/2-INVENTORY_HEIGHT/2);
TCODConsole::flush();

Then we wait for the player to press a key :

// wait for a key press
TCOD_key_t key;
TCODSystem::waitForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL,true);

Since the items shortcuts range from 'a' to 'z', we only deal with TCODK_CHAR values :

if ( key.vk == TCODK_CHAR ) {
    int actorIndex=key.c - 'a';

Now we did something strange. A character subtraction. Well if you subtract the ascii code of the pressed key (say 'b') with the ascii code of 'a', you get the item number in the inventory ('b' - 'a' == 1). Of course, if the player presses a key out of the 'a'-'z' range, the index will not be correct so we add another check :

if ( actorIndex >= 0 && actorIndex < owner->container->inventory.size() ) {
    return owner->container->inventory.get(actorIndex);
}

If no valid item was selected, return NULL :

    }
    return NULL;
}

Now that we can pick an item, we can handle the 'i' key in the handleActionKey function :

case 'i' : // display inventory
{
    Actor *actor=choseFromInventory(owner);
    if ( actor ) {
        actor->pickable->use(actor,owner);
        engine.gameStatus=Engine::NEW_TURN;
    }
}
break;

Again, using an item consumes a game turn, even if Pickable::use returns false. For exemple, trying to drink a health potion while having all your health points will consume a turn.

That's it. You can compile and try to kill all the monsters. For the first time, you have a chance of success !