Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - extra 5: more generic items"

From RogueBasin
Jump to navigation Jump to search
(Add syntaxhighlight.)
 
Line 27: Line 27:
All target selection algorithms but the one selecting the wearer use a range. So we create a class with an enum for the algorithm and a range field.
All target selection algorithms but the one selecting the wearer use a range. So we create a class with an enum for the algorithm and a range field.


class TargetSelector {
<syntaxhighlight lang="C++">
public :
class TargetSelector {
public :
     enum SelectorType {
     enum SelectorType {
         CLOSEST_MONSTER,
         CLOSEST_MONSTER,
Line 37: Line 38:
     TargetSelector(SelectorType type, float range);
     TargetSelector(SelectorType type, float range);
     void selectTargets(Actor *wearer, TCODList<Actor *> & list);
     void selectTargets(Actor *wearer, TCODList<Actor *> & list);
protected :
protected :
     SelectorType type;
     SelectorType type;
     float range;
     float range;
};
};
</syntaxhighlight>


There's no WEARER selector type. Well we'll simply assume that if the pickable has no TargetSelector, the effect applies to the wearer.
There's no WEARER selector type. Well we'll simply assume that if the pickable has no TargetSelector, the effect applies to the wearer.
Line 46: Line 48:
As usual, the constructor is trivial :
As usual, the constructor is trivial :


TargetSelector::TargetSelector(SelectorType type, float range)  
<syntaxhighlight lang="C++">
TargetSelector::TargetSelector(SelectorType type, float range)  
     : type(type), range(range) {
     : type(type), range(range) {
}
}
</syntaxhighlight>


The selectTargets method populate the list with all selected actors, depending on the algorithm. CLOSEST_MONSTER grabs the monster closest to the wearer :
The selectTargets method populate the list with all selected actors, depending on the algorithm. CLOSEST_MONSTER grabs the monster closest to the wearer :


void TargetSelector::selectTargets(Actor *wearer, TCODList<Actor *> & list) {
<syntaxhighlight lang="C++">
void TargetSelector::selectTargets(Actor *wearer, TCODList<Actor *> & list) {
     switch(type) {
     switch(type) {
         case CLOSEST_MONSTER :
         case CLOSEST_MONSTER :
Line 62: Line 67:
         }
         }
         break;
         break;
</syntaxhighlight>


SELECTED_MONSTER asks the player to chose an actor (possibly the player himself) :
SELECTED_MONSTER asks the player to chose an actor (possibly the player himself) :


case SELECTED_MONSTER :
<syntaxhighlight lang="C++">
{  
case SELECTED_MONSTER :
{  
     int x,y;
     int x,y;
     engine.gui->message(TCODColor::cyan, "Left-click to select an enemy,\nor right-click to cancel.");
     engine.gui->message(TCODColor::cyan, "Left-click to select an enemy,\nor right-click to cancel.");
Line 75: Line 82:
         }
         }
     }
     }
}
}
break;
break;
</syntaxhighlight>


WEARER_RANGE picks all actors close enough from the wearer (excluding the wearer himself) :
WEARER_RANGE picks all actors close enough from the wearer (excluding the wearer himself) :


case WEARER_RANGE :
<syntaxhighlight lang="C++">
case WEARER_RANGE :
     for (Actor **iterator=engine.actors.begin();
     for (Actor **iterator=engine.actors.begin();
             iterator != engine.actors.end(); iterator++) {
             iterator != engine.actors.end(); iterator++) {
Line 89: Line 98:
         }
         }
     }
     }
break;
break;
</syntaxhighlight>


and SELECTED_RANGE asks the player to pick a tile, then selects all actors close enough from this tile (possibly including the player)  :
and SELECTED_RANGE asks the player to pick a tile, then selects all actors close enough from this tile (possibly including the player)  :


case SELECTED_RANGE :
<syntaxhighlight lang="C++">
case SELECTED_RANGE :
     int x,y;
     int x,y;
     engine.gui->message(TCODColor::cyan, "Left-click to select a tile,\nor right-click to cancel.");
     engine.gui->message(TCODColor::cyan, "Left-click to select a tile,\nor right-click to cancel.");
Line 106: Line 117:
         }
         }
     }
     }
break;
break;
</syntaxhighlight>


Finally, if no monster has been selected, we display a message :
Finally, if no monster has been selected, we display a message :


<syntaxhighlight lang="C++">
     }
     }
     if ( list.isEmpty() ) {
     if ( list.isEmpty() ) {
         engine.gui->message(TCODColor::lightGrey,"No enemy is close enough");
         engine.gui->message(TCODColor::lightGrey,"No enemy is close enough");
     }
     }
}
}
</syntaxhighlight>


==Effects==
==Effects==
Line 121: Line 135:
Now that we can pick targets using a generic class, let's see what happens to those targets when the item is used. First things first, let's define an abstract effect class :
Now that we can pick targets using a generic class, let's see what happens to those targets when the item is used. First things first, let's define an abstract effect class :


class Effect {
<syntaxhighlight lang="C++">
public :
class Effect {
public :
     virtual bool applyTo(Actor *actor) = 0;
     virtual bool applyTo(Actor *actor) = 0;
};
};
</syntaxhighlight>


The method returns false if the effect could not be applied (for example healing someone who is not wounded).
The method returns false if the effect could not be applied (for example healing someone who is not wounded).
Line 132: Line 148:
Now let's define the effect that modify the health points :
Now let's define the effect that modify the health points :


class HealthEffect : public Effect {
<syntaxhighlight lang="C++">
public :
class HealthEffect : public Effect {
public :
     float amount;
     float amount;
     const char *message;
     const char *message;
 
     HealthEffect(float amount, const char *message);
     HealthEffect(float amount, const char *message);
     bool applyTo(Actor *actor);  
     bool applyTo(Actor *actor);  
};
};
</syntaxhighlight>


The effect is healing the target if amount is positive or hurting him if amount is negative. The message makes it possible to display some custom message. To keep things simple, we suppose that the message string always contains a parameter for the target name (%s) and a parameter for the amount (%g). But you can use NULL if you don't want a message to be displayed.
The effect is healing the target if amount is positive or hurting him if amount is negative. The message makes it possible to display some custom message. To keep things simple, we suppose that the message string always contains a parameter for the target name (%s) and a parameter for the amount (%g). But you can use NULL if you don't want a message to be displayed.
Line 145: Line 163:
The applyTo function first checks that we're dealing with a destructible actor :
The applyTo function first checks that we're dealing with a destructible actor :


HealthEffect::HealthEffect(float amount, const char *message)  
<syntaxhighlight lang="C++">
HealthEffect::HealthEffect(float amount, const char *message)  
     : amount(amount), message(message) {
     : amount(amount), message(message) {
}
}
 
bool HealthEffect::applyTo(Actor *actor) {
bool HealthEffect::applyTo(Actor *actor) {
     if (!actor->destructible) return false;
     if (!actor->destructible) return false;
</syntaxhighlight>


Then deals with the healing part :
Then deals with the healing part :


if ( amount > 0 ) {
<syntaxhighlight lang="C++">
if ( amount > 0 ) {
     float pointsHealed=actor->destructible->heal(amount);
     float pointsHealed=actor->destructible->heal(amount);
     if (pointsHealed > 0) {
     if (pointsHealed > 0) {
Line 162: Line 183:
         return true;
         return true;
     }
     }
</syntaxhighlight>
We display the message only if some health points were actually restored. Now the hurting part :
We display the message only if some health points were actually restored. Now the hurting part :


<syntaxhighlight lang="C++">
     } else {
     } else {
             if ( message && -amount-actor->destructible->defense > 0 ) {
             if ( message && -amount-actor->destructible->defense > 0 ) {
Line 173: Line 196:
             }
             }
     }
     }
</syntaxhighlight>


Here we have to display the message before calling takeDamage because after, the actor might already be dead. You wouldn't want to read "the dead orc get burnt for x hit points".
Here we have to display the message before calling takeDamage because after, the actor might already be dead. You wouldn't want to read "the dead orc get burnt for x hit points".
Line 178: Line 202:
If the target could not be healed/wounded, we return false :
If the target could not be healed/wounded, we return false :


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


==Some class to officialize temporary Ai==
==Some class to officialize temporary Ai==
Line 185: Line 211:
The second effect has to temporary replace the target's  Ai. It can only be replaced by an Ai class that is capable of saving the previous Ai to get back to it once the effect ends. Let's improve the Ai class hierarchy by defining a TemporaryAi abstract class :
The second effect has to temporary replace the target's  Ai. It can only be replaced by an Ai class that is capable of saving the previous Ai to get back to it once the effect ends. Let's improve the Ai class hierarchy by defining a TemporaryAi abstract class :


class TemporaryAi : public Ai {
<syntaxhighlight lang="C++">
public :
class TemporaryAi : public Ai {
public :
     TemporaryAi(int nbTurns);
     TemporaryAi(int nbTurns);
     void update(Actor *owner);
     void update(Actor *owner);
     void applyTo(Actor *actor);
     void applyTo(Actor *actor);
protected :
protected :
     int nbTurns;
     int nbTurns;
     Ai *oldAi;
     Ai *oldAi;
};
};
</syntaxhighlight>
The TemporaryAi class stores the number of turns and the previous Ai. The implementation is trivial as it mostly reuses code from the ConfusedMonsterAi class :
The TemporaryAi class stores the number of turns and the previous Ai. The implementation is trivial as it mostly reuses code from the ConfusedMonsterAi class :


TemporaryAi::TemporaryAi(int nbTurns) : nbTurns(nbTurns) {
<syntaxhighlight lang="C++">
}
TemporaryAi::TemporaryAi(int nbTurns) : nbTurns(nbTurns) {
}
void TemporaryAi::update(Actor *owner) {
 
void TemporaryAi::update(Actor *owner) {
     nbTurns--;
     nbTurns--;
     if ( nbTurns == 0 ) {
     if ( nbTurns == 0 ) {
Line 205: Line 234:
         delete this;
         delete this;
     }
     }
}
}
 
void TemporaryAi::applyTo(Actor *actor) {
void TemporaryAi::applyTo(Actor *actor) {
     oldAi=actor->ai;
     oldAi=actor->ai;
     actor->ai=this;
     actor->ai=this;
}
}
</syntaxhighlight>
Now we can simplify the ConfusedMonsterAi class by making it inherit from TemporaryAi :
Now we can simplify the ConfusedMonsterAi class by making it inherit from TemporaryAi :


class ConfusedMonsterAi : public TemporaryAi {
<syntaxhighlight lang="C++">
public :
class ConfusedMonsterAi : public TemporaryAi {
public :
     ConfusedMonsterAi(int nbTurns);
     ConfusedMonsterAi(int nbTurns);
     void update(Actor *owner);
     void update(Actor *owner);
};
};
</syntaxhighlight>


ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns)  
<syntaxhighlight lang="C++">
ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns)  
     : TemporaryAi(nbTurns) {
     : TemporaryAi(nbTurns) {
}
}
   
   
void ConfusedMonsterAi::update(Actor *owner) {
void ConfusedMonsterAi::update(Actor *owner) {
     TCODRandom *rng=TCODRandom::getInstance();
     TCODRandom *rng=TCODRandom::getInstance();
     int dx=rng->getInt(-1,1);
     int dx=rng->getInt(-1,1);
Line 241: Line 274:
     }
     }
     TemporaryAi::update(owner);
     TemporaryAi::update(owner);
}
}
</syntaxhighlight>


==An effect that changes the Ai==
==An effect that changes the Ai==
Line 247: Line 281:
We can know define our second effect :
We can know define our second effect :


class AiChangeEffect : public Effect {
<syntaxhighlight lang="C++">
public :
class AiChangeEffect : public Effect {
public :
     TemporaryAi *newAi;
     TemporaryAi *newAi;
     const char *message;
     const char *message;
 
     AiChangeEffect(TemporaryAi *newAi, const char *message);
     AiChangeEffect(TemporaryAi *newAi, const char *message);
     bool applyTo(Actor *actor);
     bool applyTo(Actor *actor);
};
};
</syntaxhighlight>


Once again, it's a very simple class :
Once again, it's a very simple class :


AiChangeEffect::AiChangeEffect(TemporaryAi *newAi, const char *message)  
<syntaxhighlight lang="C++">
AiChangeEffect::AiChangeEffect(TemporaryAi *newAi, const char *message)  
     : newAi(newAi), message(message) {
     : newAi(newAi), message(message) {
}
}
 
bool AiChangeEffect::applyTo(Actor *actor) {
bool AiChangeEffect::applyTo(Actor *actor) {
     newAi->applyTo(actor);
     newAi->applyTo(actor);
     if ( message ) {
     if ( message ) {
Line 268: Line 305:
     }
     }
     return true;
     return true;
}
}
</syntaxhighlight>


==A more generic Pickable class==
==A more generic Pickable class==
Line 274: Line 312:
It's time to use our new TargetSelector and Effect classes to improve the Pickable class.
It's time to use our new TargetSelector and Effect classes to improve the Pickable class.


class Pickable {
<syntaxhighlight lang="C++">
public :
class Pickable {
public :
     Pickable(TargetSelector *selector, Effect *effect);
     Pickable(TargetSelector *selector, Effect *effect);
     virtual ~Pickable();
     virtual ~Pickable();
Line 281: Line 320:
     void drop(Actor *owner, Actor *wearer);
     void drop(Actor *owner, Actor *wearer);
     bool use(Actor *owner, Actor *wearer);
     bool use(Actor *owner, Actor *wearer);
protected :
protected :
     TargetSelector *selector;
     TargetSelector *selector;
     Effect *effect;
     Effect *effect;
};
};
</syntaxhighlight>


The class will store a TargetSelector (NULL if the target is the wearer) and an Effect. We add a destructor to delete those fields when the Pickable is destroyed.
The class will store a TargetSelector (NULL if the target is the wearer) and an Effect. We add a destructor to delete those fields when the Pickable is destroyed.


Pickable::Pickable(TargetSelector *selector, Effect *effect) :
<syntaxhighlight lang="C++">
Pickable::Pickable(TargetSelector *selector, Effect *effect) :
     selector(selector), effect(effect) {
     selector(selector), effect(effect) {
}
}
 
Pickable::~Pickable() {
Pickable::~Pickable() {
     if ( selector ) delete selector;
     if ( selector ) delete selector;
     if ( effect ) delete effect;
     if ( effect ) delete effect;
}
}
</syntaxhighlight>


Now the use function determines the list of targets using the selector :
Now the use function determines the list of targets using the selector :


bool Pickable::use(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
bool Pickable::use(Actor *owner, Actor *wearer) {
     TCODList<Actor *> list;
     TCODList<Actor *> list;
     if ( selector ) {
     if ( selector ) {
Line 306: Line 349:
         list.push(wearer);
         list.push(wearer);
     }
     }
</syntaxhighlight>


Then tries to apply the effect to the targets. We consider that the action is a success if the effect succeeds on at least one of the targets.
Then tries to apply the effect to the targets. We consider that the action is a success if the effect succeeds on at least one of the targets.


bool succeed=false;
<syntaxhighlight lang="C++">
for (Actor **it=list.begin(); it!=list.end(); it++) {
bool succeed=false;
for (Actor **it=list.begin(); it!=list.end(); it++) {
     if ( effect->applyTo(*it) ) {
     if ( effect->applyTo(*it) ) {
         succeed=true;
         succeed=true;
     }
     }
}
}
</syntaxhighlight>


In case of a success, we destroy the item :
In case of a success, we destroy the item :


<syntaxhighlight lang="C++">
     if ( succeed ) {
     if ( succeed ) {
         if ( wearer->container ) {
         if ( wearer->container ) {
Line 325: Line 372:
     }
     }
     return succeed;  
     return succeed;  
}
}
</syntaxhighlight>


With this new powerful Pickable class, you can now safely remove all the previous item classes from the code :
With this new powerful Pickable class, you can now safely remove all the previous item classes from the code :
Line 340: Line 388:
The health potion :
The health potion :


// create a health potion
<syntaxhighlight lang="C++">
Actor *healthPotion=new Actor(x,y,'!',"health potion",
// create a health potion
Actor *healthPotion=new Actor(x,y,'!',"health potion",
     TCODColor::violet);
     TCODColor::violet);
healthPotion->blocks=false;
healthPotion->blocks=false;
healthPotion->pickable=new Pickable(NULL,new HealthEffect(4,NULL));
healthPotion->pickable=new Pickable(NULL,new HealthEffect(4,NULL));
engine.actors.push(healthPotion);
engine.actors.push(healthPotion);
</syntaxhighlight>


The scroll of lightning bolt :
The scroll of lightning bolt :


// create a scroll of lightning bolt  
<syntaxhighlight lang="C++">
Actor *scrollOfLightningBolt=new Actor(x,y,'#',"scroll of lightning bolt",
// create a scroll of lightning bolt  
Actor *scrollOfLightningBolt=new Actor(x,y,'#',"scroll of lightning bolt",
     TCODColor::lightYellow);
     TCODColor::lightYellow);
scrollOfLightningBolt->blocks=false;
scrollOfLightningBolt->blocks=false;
scrollOfLightningBolt->pickable=new Pickable(
scrollOfLightningBolt->pickable=new Pickable(
     new TargetSelector(TargetSelector::CLOSEST_MONSTER,5),
     new TargetSelector(TargetSelector::CLOSEST_MONSTER,5),
     new HealthEffect(-20,"A lighting bolt strikes the %s with a loud thunder!\n"
     new HealthEffect(-20,"A lighting bolt strikes the %s with a loud thunder!\n"
         "The damage is %g hit points."));
         "The damage is %g hit points."));
engine.actors.push(scrollOfLightningBolt);
engine.actors.push(scrollOfLightningBolt);
</syntaxhighlight>


The scroll of fireball :
The scroll of fireball :


// create a scroll of fireball
<syntaxhighlight lang="C++">
Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
// create a scroll of fireball
Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
     TCODColor::lightYellow);
     TCODColor::lightYellow);
scrollOfFireball->blocks=false;
scrollOfFireball->blocks=false;
scrollOfFireball->pickable=new Pickable(
scrollOfFireball->pickable=new Pickable(
     new TargetSelector(TargetSelector::SELECTED_RANGE,3),
     new TargetSelector(TargetSelector::SELECTED_RANGE,3),
     new HealthEffect(-12,"The %s gets burned for %g hit points."));
     new HealthEffect(-12,"The %s gets burned for %g hit points."));
engine.actors.push(scrollOfFireball);
engine.actors.push(scrollOfFireball);
</syntaxhighlight>


The scroll of confusion :
The scroll of confusion :


// create a scroll of confusion
<syntaxhighlight lang="C++">
Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
// create a scroll of confusion
Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
     TCODColor::lightYellow);
     TCODColor::lightYellow);
scrollOfConfusion->blocks=false;
scrollOfConfusion->blocks=false;
scrollOfConfusion->pickable=new Pickable(
scrollOfConfusion->pickable=new Pickable(
     new TargetSelector(TargetSelector::SELECTED_MONSTER,5),
     new TargetSelector(TargetSelector::SELECTED_MONSTER,5),
     new AiChangeEffect(new ConfusedMonsterAi(10),
     new AiChangeEffect(new ConfusedMonsterAi(10),
         "The eyes of the %s look vacant,\nas he starts to stumble around!"));
         "The eyes of the %s look vacant,\nas he starts to stumble around!"));
engine.actors.push(scrollOfConfusion);
engine.actors.push(scrollOfConfusion);
</syntaxhighlight>


That's it. You can now compile and well... the game is almost exactly the same. We lost some details in the process (for example having specific colors for the messages but we could easily add a TCODColor parameter to the effects' constructors), but now, you can add a new category of item we only a few lines of code, by either mixing existing selectors and effect or just adding a new effect.
That's it. You can now compile and well... the game is almost exactly the same. We lost some details in the process (for example having specific colors for the messages but we could easily add a TCODColor parameter to the effects' constructors), but now, you can add a new category of item we only a few lines of code, by either mixing existing selectors and effect or just adding a new effect.

Latest revision as of 08:48, 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.


This article is an optional "extra" that implements more generic items, making it possible to create new items with less code. It can be applied on article 9 source code.

In article 9, we brought more variety to the items, but each new item required a new class inheriting from Pickable. Yet, we can see some pattern in the way pickable actors behave :

They all select one or several targets :

  • the wearer of the item (health potion)
  • the wearer's closest enemy (lightning bolt)
  • a selected actor (confusion)
  • all actors close to a selected tile (fireball)

A fifth way that is not yet used but might be useful would be :

  • all actors close to the wearer of the item

They all apply some effect to the selected targets :

  • increase or decrease health (all but confusion)
  • temporary replace the target's Ai (confusion)

So instead of implementing the target selection and the effect in a class inheriting from Pickable, why not add a TargetSelector and an Effect directly to the Pickable class ?

A generic target selector

All target selection algorithms but the one selecting the wearer use a range. So we create a class with an enum for the algorithm and a range field.

class TargetSelector {
public :
    enum SelectorType {
        CLOSEST_MONSTER,
        SELECTED_MONSTER,
        WEARER_RANGE,
        SELECTED_RANGE      
    };
    TargetSelector(SelectorType type, float range);
    void selectTargets(Actor *wearer, TCODList<Actor *> & list);
protected :
    SelectorType type;
    float range;
};

There's no WEARER selector type. Well we'll simply assume that if the pickable has no TargetSelector, the effect applies to the wearer.

As usual, the constructor is trivial :

TargetSelector::TargetSelector(SelectorType type, float range) 
    : type(type), range(range) {
}

The selectTargets method populate the list with all selected actors, depending on the algorithm. CLOSEST_MONSTER grabs the monster closest to the wearer :

void TargetSelector::selectTargets(Actor *wearer, TCODList<Actor *> & list) {
    switch(type) {
        case CLOSEST_MONSTER :
        {
            Actor *closestMonster=engine.getClosestMonster(wearer->x,wearer->y,range);
            if ( closestMonster ) {
                list.push(closestMonster);
            }
        }
        break;

SELECTED_MONSTER asks the player to chose an actor (possibly the player himself) :

case SELECTED_MONSTER :
{ 
    int x,y;
    engine.gui->message(TCODColor::cyan, "Left-click to select an enemy,\nor right-click to cancel.");
    if ( engine.pickATile(&x,&y,range)) {
        Actor *actor=engine.getActor(x,y);
        if ( actor ) {
            list.push(actor);
        }
    }
}
break;

WEARER_RANGE picks all actors close enough from the wearer (excluding the wearer himself) :

case WEARER_RANGE :
    for (Actor **iterator=engine.actors.begin();
            iterator != engine.actors.end(); iterator++) {
        Actor *actor=*iterator;
        if ( actor != wearer && actor->destructible && !actor->destructible->isDead()
            && actor->getDistance(wearer->x,wearer->y) <= range) {              
            list.push(actor);
        }
    }
break;

and SELECTED_RANGE asks the player to pick a tile, then selects all actors close enough from this tile (possibly including the player)  :

case SELECTED_RANGE :
    int x,y;
    engine.gui->message(TCODColor::cyan, "Left-click to select a tile,\nor right-click to cancel.");
    if ( engine.pickATile(&x,&y)) {
        for (Actor **iterator=engine.actors.begin();
                iterator != engine.actors.end(); iterator++) {
            Actor *actor=*iterator;
            if ( actor->destructible && !actor->destructible->isDead()
                && actor->getDistance(x,y) <= range) {                
                list.push(actor);
            }
        }
    }
break;

Finally, if no monster has been selected, we display a message :

    }
    if ( list.isEmpty() ) {
        engine.gui->message(TCODColor::lightGrey,"No enemy is close enough");
    }
}

Effects

Now that we can pick targets using a generic class, let's see what happens to those targets when the item is used. First things first, let's define an abstract effect class :

class Effect {
public :
    virtual bool applyTo(Actor *actor) = 0;
};

The method returns false if the effect could not be applied (for example healing someone who is not wounded).

Health effect

Now let's define the effect that modify the health points :

class HealthEffect : public Effect {
public :
    float amount;
    const char *message;

    HealthEffect(float amount, const char *message);
    bool applyTo(Actor *actor); 
};

The effect is healing the target if amount is positive or hurting him if amount is negative. The message makes it possible to display some custom message. To keep things simple, we suppose that the message string always contains a parameter for the target name (%s) and a parameter for the amount (%g). But you can use NULL if you don't want a message to be displayed.

The applyTo function first checks that we're dealing with a destructible actor :

HealthEffect::HealthEffect(float amount, const char *message) 
    : amount(amount), message(message) {
}

bool HealthEffect::applyTo(Actor *actor) {
    if (!actor->destructible) return false;

Then deals with the healing part :

if ( amount > 0 ) {
    float pointsHealed=actor->destructible->heal(amount);
    if (pointsHealed > 0) {
        if ( message ) {
            engine.gui->message(TCODColor::lightGrey,message,actor->name,pointsHealed);
        }
        return true;
    }

We display the message only if some health points were actually restored. Now the hurting part :

    } else {
            if ( message && -amount-actor->destructible->defense > 0 ) {
                engine.gui->message(TCODColor::lightGrey,message,actor->name,
                    -amount-actor->destructible->defense);
            }
            if ( actor->destructible->takeDamage(actor,-amount) > 0 ) {
                return true;
            }
    }

Here we have to display the message before calling takeDamage because after, the actor might already be dead. You wouldn't want to read "the dead orc get burnt for x hit points".

If the target could not be healed/wounded, we return false :

    return false;
}

Some class to officialize temporary Ai

The second effect has to temporary replace the target's Ai. It can only be replaced by an Ai class that is capable of saving the previous Ai to get back to it once the effect ends. Let's improve the Ai class hierarchy by defining a TemporaryAi abstract class :

class TemporaryAi : public Ai {
public :
    TemporaryAi(int nbTurns);
    void update(Actor *owner);
    void applyTo(Actor *actor);
protected :
    int nbTurns;
    Ai *oldAi;
};

The TemporaryAi class stores the number of turns and the previous Ai. The implementation is trivial as it mostly reuses code from the ConfusedMonsterAi class :

TemporaryAi::TemporaryAi(int nbTurns) : nbTurns(nbTurns) {
}

void TemporaryAi::update(Actor *owner) {
    nbTurns--;
    if ( nbTurns == 0 ) {
        owner->ai = oldAi;
        delete this;
    }
}

void TemporaryAi::applyTo(Actor *actor) {
    oldAi=actor->ai;
    actor->ai=this;
}

Now we can simplify the ConfusedMonsterAi class by making it inherit from TemporaryAi :

class ConfusedMonsterAi : public TemporaryAi {
public :
    ConfusedMonsterAi(int nbTurns);
    void update(Actor *owner);
};
ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns) 
    : TemporaryAi(nbTurns) {
}
 
void ConfusedMonsterAi::update(Actor *owner) {
    TCODRandom *rng=TCODRandom::getInstance();
    int dx=rng->getInt(-1,1);
    int dy=rng->getInt(-1,1);
    if ( dx != 0 || dy != 0 ) {
        int destx=owner->x+dx;
        int desty=owner->y+dy;
        if ( engine.map->canWalk(destx, desty) ) {
            owner->x = destx;
            owner->y = desty;
        } else {
            Actor *actor=engine.getActor(destx, desty);
            if ( actor ) {
                owner->attacker->attack(owner, actor);
            }
        }
    }
    TemporaryAi::update(owner);
}

An effect that changes the Ai

We can know define our second effect :

class AiChangeEffect : public Effect {
public :
    TemporaryAi *newAi;
    const char *message;

    AiChangeEffect(TemporaryAi *newAi, const char *message);
    bool applyTo(Actor *actor);
};

Once again, it's a very simple class :

AiChangeEffect::AiChangeEffect(TemporaryAi *newAi, const char *message) 
    : newAi(newAi), message(message) {
}

bool AiChangeEffect::applyTo(Actor *actor) {
    newAi->applyTo(actor);
    if ( message ) {
        engine.gui->message(TCODColor::lightGrey,message,actor->name);
    }
    return true;
}

A more generic Pickable class

It's time to use our new TargetSelector and Effect classes to improve the Pickable class.

class Pickable {
public :
    Pickable(TargetSelector *selector, Effect *effect);
    virtual ~Pickable();
    bool pick(Actor *owner, Actor *wearer);
    void drop(Actor *owner, Actor *wearer);
    bool use(Actor *owner, Actor *wearer);
protected :
    TargetSelector *selector;
    Effect *effect;
};

The class will store a TargetSelector (NULL if the target is the wearer) and an Effect. We add a destructor to delete those fields when the Pickable is destroyed.

Pickable::Pickable(TargetSelector *selector, Effect *effect) :
    selector(selector), effect(effect) {
}

Pickable::~Pickable() {
    if ( selector ) delete selector;
    if ( effect ) delete effect;
}

Now the use function determines the list of targets using the selector :

bool Pickable::use(Actor *owner, Actor *wearer) {
    TCODList<Actor *> list;
    if ( selector ) {
        selector->selectTargets(wearer, list);
    } else {
        list.push(wearer);
    }

Then tries to apply the effect to the targets. We consider that the action is a success if the effect succeeds on at least one of the targets.

bool succeed=false;
for (Actor **it=list.begin(); it!=list.end(); it++) {
    if ( effect->applyTo(*it) ) {
        succeed=true;
    }
}

In case of a success, we destroy the item :

    if ( succeed ) {
        if ( wearer->container ) {
            wearer->container->remove(owner);
            delete owner;
        }
    }
    return succeed; 
}

With this new powerful Pickable class, you can now safely remove all the previous item classes from the code :

  • Healer
  • Confuser
  • LightningBolt
  • Fireball

Updating the Map::addItem function

All the items now rely on the Pickable class, using appropriate target selectors and effects.

The health potion :

// create a health potion
Actor *healthPotion=new Actor(x,y,'!',"health potion",
    TCODColor::violet);
healthPotion->blocks=false;
healthPotion->pickable=new Pickable(NULL,new HealthEffect(4,NULL));
engine.actors.push(healthPotion);

The scroll of lightning bolt :

// create a scroll of lightning bolt 
Actor *scrollOfLightningBolt=new Actor(x,y,'#',"scroll of lightning bolt",
    TCODColor::lightYellow);
scrollOfLightningBolt->blocks=false;
scrollOfLightningBolt->pickable=new Pickable(
    new TargetSelector(TargetSelector::CLOSEST_MONSTER,5),
    new HealthEffect(-20,"A lighting bolt strikes the %s with a loud thunder!\n"
        "The damage is %g hit points."));
engine.actors.push(scrollOfLightningBolt);

The scroll of fireball :

// create a scroll of fireball
Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
    TCODColor::lightYellow);
scrollOfFireball->blocks=false;
scrollOfFireball->pickable=new Pickable(
    new TargetSelector(TargetSelector::SELECTED_RANGE,3),
    new HealthEffect(-12,"The %s gets burned for %g hit points."));
engine.actors.push(scrollOfFireball);

The scroll of confusion :

// create a scroll of confusion
Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
    TCODColor::lightYellow);
scrollOfConfusion->blocks=false;
scrollOfConfusion->pickable=new Pickable(
    new TargetSelector(TargetSelector::SELECTED_MONSTER,5),
    new AiChangeEffect(new ConfusedMonsterAi(10),
        "The eyes of the %s look vacant,\nas he starts to stumble around!"));
engine.actors.push(scrollOfConfusion);

That's it. You can now compile and well... the game is almost exactly the same. We lost some details in the process (for example having specific colors for the messages but we could easily add a TCODColor parameter to the effects' constructors), but now, you can add a new category of item we only a few lines of code, by either mixing existing selectors and effect or just adding a new effect.