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

From RogueBasin
Jump to navigation Jump to search
Line 241: Line 241:
     }
     }
     TemporaryAi::update(owner);
     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;
  }
  }




[[Category:Developing]]
[[Category:Developing]]

Revision as of 13:26, 23 October 2015

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