Complete roguelike tutorial using C++ and libtcod - extra 5: more generic items

From RogueBasin
Revision as of 13:22, 23 October 2015 by Joel Pera (talk | contribs) (pasted →‎Effects)
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.


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).