Complete roguelike tutorial using C++ and libtcod - part 9: spells and ranged combat

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'll start to add diversity to the gameplay by adding three new spells :

  • lightning bolt : deals damages to the closest enemy
  • fireball : powerful area effect spell
  • confusion : turns the selected enemy into a randomly wandering zombie for a few turns

The scroll of lightning bolt

This dreadful spell will deal 20 damage to the closest monster, up to a distance of 5 tiles. So the first thing we need is some helper functions to get the closest monster within a range.

Some helpers

The easiest is a function to get the distance from an actor to a specific tile of the map :

Actor.hpp

float getDistance(int cx, int cy) const;

Actor.cpp

float Actor::getDistance(int cx, int cy) const {
   int dx=x-cx;
   int dy=y-cy;
   return sqrtf(dx*dx+dy*dy);
}

Well that was a small warmup. Don't forget to include math.h for the sqrtf function. Now let's do something manlier.

Engine.hpp

Actor *getClosestMonster(int x, int y, float range) const;

This function returns the closest monster from position x,y within range. If range is 0, it's considered infinite. If no monster is found within range, it returns NULL.

Engine.cpp

Actor *Engine::getClosestMonster(int x, int y, float range) const {
   Actor *closest=NULL;
   float bestDistance=1E6f;

First we declare some variable. closest is the closest monster found so far. bestDistance is the distance for the closest monster found so far. We initialize it with a value higher than any possible value (1E6 == 1000000) so that the first monster in the range will be a candidate. So let's iterate over the actors list and check alive monsters :

for (Actor **iterator=actors.begin();
   iterator != actors.end(); iterator++) {
   Actor *actor=*iterator;
   if ( actor != player && actor->destructible 
       && !actor->destructible->isDead() ) {
Now let's check if the guy is within range and closer than what we found so far.
           float distance=actor->getDistance(x,y);
           if ( distance < bestDistance && ( distance <= range || range == 0.0f ) ) {
               bestDistance=distance;
               closest=actor;
           }
       }
   }
   return closest;
}

LightningBolt Pickable

Now we can create the new Pickable :

Pickable.hpp

class LightningBolt: public Pickable {
public :
   float range,damage;
   LightningBolt(float range, float damage);
   bool use(Actor *owner, Actor *wearer);
};

As usual, the constructor is trivial :

LightningBolt::LightningBolt(float range, float damage)
   : range(range),damage(damage) {
}

The implementation tries to find a monster within range :

bool LightningBolt::use(Actor *owner, Actor *wearer) {
   Actor *closestMonster=engine.getClosestMonster(wearer->x,wearer->y,range);
   if (! closestMonster ) {
       engine.gui->message(TCODColor::lightGrey,"No enemy is close enough to strike.");
       return false;
   }

Remember that the owner is the actor that contains the LightningBolt (the scroll of lightning bolt) while the wearer is the actor having the owner in its inventory. If we found a monster, we display a appropriate message and deal damage.

// hit closest monster for <damage> hit points
engine.gui->message(TCODColor::lightBlue,
   "A lighting bolt strikes the %s with a loud thunder!\n"
   "The damage is %g hit points.",
   closestMonster->name,damage);
closestMonster->destructible->takeDamage(closestMonster,damage);

And don't forget to call the father's class use method to consume the item and remove it from the inventory :

   return Pickable::use(owner,wearer);
}

Updating the map

That's it. We only need to change the Map::addItem function to put a few scrolls of lightning bolt here and there :

void Map::addItem(int x, int y) {
   TCODRandom *rng=TCODRandom::getInstance();
   int dice = rng->getInt(0,100);
   if ( dice < 70 ) {
       // create a health potion
       Actor *healthPotion=new Actor(x,y,'!',"health potion",
           TCODColor::violet);
       healthPotion->blocks=false;
       healthPotion->pickable=new Healer(4);
       engine.actors.push(healthPotion);
   } else if ( dice < 70+10 ) {
       // create a scroll of lightning bolt 
       Actor *scrollOfLightningBolt=new Actor(x,y,'#',"scroll of lightning bolt",
           TCODColor::lightYellow);
       scrollOfLightningBolt->blocks=false;
       scrollOfLightningBolt->pickable=new LightningBolt(5,20);
       engine.actors.push(scrollOfLightningBolt);
   }
}

70% of the items are health potions. 10% are scrolls of lightning bolt. The remaining 20% are for the 2 other spells.

Now you can compile and start to zap everyone in the dungeon !

The scroll of fireball

This one will add two interesting features : targetting and area effects. Targetting requires to be able to select a position on the map with the mouse.