Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 9: spells and ranged combat"

From RogueBasin
Jump to navigation Jump to search
(added source link)
(Add syntaxhighlight, fix some block formatting.)
 
Line 19: Line 19:
Actor.hpp
Actor.hpp


float getDistance(int cx, int cy) const;
<syntaxhighlight lang="C++">
float getDistance(int cx, int cy) const;
</syntaxhighlight>


Actor.cpp
Actor.cpp


float Actor::getDistance(int cx, int cy) const {
<syntaxhighlight lang="C++">
float Actor::getDistance(int cx, int cy) const {
     int dx=x-cx;
     int dx=x-cx;
     int dy=y-cy;
     int dy=y-cy;
     return sqrtf(dx*dx+dy*dy);
     return sqrtf(dx*dx+dy*dy);
}
}
</syntaxhighlight>


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


Actor *getClosestMonster(int x, int y, float range) const;
<syntaxhighlight lang="C++">
Actor *getClosestMonster(int x, int y, float range) const;
</syntaxhighlight>


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.
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.
Line 39: Line 45:
Engine.cpp
Engine.cpp


Actor *Engine::getClosestMonster(int x, int y, float range) const {
<syntaxhighlight lang="C++">
Actor *Engine::getClosestMonster(int x, int y, float range) const {
     Actor *closest=NULL;
     Actor *closest=NULL;
     float bestDistance=1E6f;
     float bestDistance=1E6f;
</syntaxhighlight>


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 :
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();
<syntaxhighlight lang="C++">
for (Actor **iterator=actors.begin();
     iterator != actors.end(); iterator++) {
     iterator != actors.end(); iterator++) {
     Actor *actor=*iterator;
     Actor *actor=*iterator;
     if ( actor != player && actor->destructible  
     if ( actor != player && actor->destructible  
         && !actor->destructible->isDead() ) {
         && !actor->destructible->isDead() ) {
Now let's check if the guy is within range and closer than what we found so far.
</syntaxhighlight>


Now let's check if the guy is within range and closer than what we found so far.
<syntaxhighlight lang="C++">
             float distance=actor->getDistance(x,y);
             float distance=actor->getDistance(x,y);
             if ( distance < bestDistance && ( distance <= range || range == 0.0f ) ) {
             if ( distance < bestDistance && ( distance <= range || range == 0.0f ) ) {
Line 60: Line 71:
     }
     }
     return closest;
     return closest;
}
}
</syntaxhighlight>


===LightningBolt Pickable===
===LightningBolt Pickable===
Line 68: Line 80:
Pickable.hpp
Pickable.hpp


class LightningBolt: public Pickable {
<syntaxhighlight lang="C++">
public :
class LightningBolt: public Pickable {
public :
     float range,damage;
     float range,damage;
     LightningBolt(float range, float damage);
     LightningBolt(float range, float damage);
     bool use(Actor *owner, Actor *wearer);
     bool use(Actor *owner, Actor *wearer);
};
};
</syntaxhighlight>


As usual, the constructor is trivial :
As usual, the constructor is trivial :


LightningBolt::LightningBolt(float range, float damage)
<syntaxhighlight lang="C++">
LightningBolt::LightningBolt(float range, float damage)
     : range(range),damage(damage) {
     : range(range),damage(damage) {
}
}
</syntaxhighlight>


The implementation tries to find a monster within range :
The implementation tries to find a monster within range :


bool LightningBolt::use(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
bool LightningBolt::use(Actor *owner, Actor *wearer) {
     Actor *closestMonster=engine.getClosestMonster(wearer->x,wearer->y,range);
     Actor *closestMonster=engine.getClosestMonster(wearer->x,wearer->y,range);
     if (! closestMonster ) {
     if (! closestMonster ) {
Line 89: Line 106:
         return false;
         return false;
     }
     }
</syntaxhighlight>


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


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


<syntaxhighlight lang="C++">
     return Pickable::use(owner,wearer);
     return Pickable::use(owner,wearer);
}
}
</syntaxhighlight>


===Updating the map===
===Updating the map===
Line 108: Line 130:
That's it. We only need to change the Map::addItem function to put a few scrolls of lightning bolt here and there :
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) {
<syntaxhighlight lang="C++">
void Map::addItem(int x, int y) {
     TCODRandom *rng=TCODRandom::getInstance();
     TCODRandom *rng=TCODRandom::getInstance();
     int dice = rng->getInt(0,100);
     int dice = rng->getInt(0,100);
Line 126: Line 149:
         engine.actors.push(scrollOfLightningBolt);
         engine.actors.push(scrollOfLightningBolt);
     }
     }
}
}
</syntaxhighlight>
70% of the items are health potions. 10% are scrolls of lightning bolt. The remaining 20% are for the 2 other spells.
70% of the items are health potions. 10% are scrolls of lightning bolt. The remaining 20% are for the 2 other spells.


Line 141: Line 165:
Engine.hpp
Engine.hpp


bool pickATile(int *x, int *y, float maxRange = 0.0f);
<syntaxhighlight lang="C++">
bool pickATile(int *x, int *y, float maxRange = 0.0f);
</syntaxhighlight>


The function returns a boolean to allow the player to cancel by pressing a key or right clicking. A range of 0 means that we allow the tile to be picked anywhere in the player's field of view. This function uses a default value for the maxRange parameter so that we can omit the parameter :
The function returns a boolean to allow the player to cancel by pressing a key or right clicking. A range of 0 means that we allow the tile to be picked anywhere in the player's field of view. This function uses a default value for the maxRange parameter so that we can omit the parameter :


engine.pickATile(&x,&y);
<syntaxhighlight lang="C++">
engine.pickATile(&x,&y);
</syntaxhighlight>


is the same as
is the same as


engine.pickATile(&x,&y, 0.0f);
<syntaxhighlight lang="C++">
engine.pickATile(&x,&y, 0.0f);
</syntaxhighlight>


We're not going to use the main loop from main.cpp while picking a tile. This would require to add a flag in the engine to know if we're in standard play mode or tile picking mode. Instead, we create a alternative game loop.
We're not going to use the main loop from main.cpp while picking a tile. This would require to add a flag in the engine to know if we're in standard play mode or tile picking mode. Instead, we create a alternative game loop.
Line 155: Line 185:
Since we want the mouse look to keep working while targetting, we need to render the game screen in the loop
Since we want the mouse look to keep working while targetting, we need to render the game screen in the loop


bool Engine::pickATile(int *x, int *y, float maxRange) {
<syntaxhighlight lang="C++">
bool Engine::pickATile(int *x, int *y, float maxRange) {
     while ( !TCODConsole::isWindowClosed() ) {
     while ( !TCODConsole::isWindowClosed() ) {
         render();
         render();
</syntaxhighlight>


Now the player might not be aware of where he's allowed to click. Let's highlight the zone for him. We scan the whole map and look for tiles in FOV and within range :
Now the player might not be aware of where he's allowed to click. Let's highlight the zone for him. We scan the whole map and look for tiles in FOV and within range :


// highlight the possible range
<syntaxhighlight lang="C++">
for (int cx=0; cx < map->width; cx++) {
// highlight the possible range
for (int cx=0; cx < map->width; cx++) {
     for (int cy=0; cy < map->height; cy++) {
     for (int cy=0; cy < map->height; cy++) {
         if ( map->isInFov(cx,cy)
         if ( map->isInFov(cx,cy)
             && ( maxRange == 0 || player->getDistance(cx,cy) <= maxRange) ) {
             && ( maxRange == 0 || player->getDistance(cx,cy) <= maxRange) ) {
</syntaxhighlight>


Remember how we darkened the oldest message log by multiplying its color by a float smaller than 1 ? Well we can highlight a color using the same trick :
Remember how we darkened the oldest message log by multiplying its color by a float smaller than 1 ? Well we can highlight a color using the same trick :


<syntaxhighlight lang="C++">
             TCODColor col=TCODConsole::root->getCharBackground(cx,cy);
             TCODColor col=TCODConsole::root->getCharBackground(cx,cy);
             col = col * 1.2f;
             col = col * 1.2f;
Line 174: Line 209:
         }
         }
     }
     }
}
}
</syntaxhighlight>


Now we need to update the mouse coordinate in Engine::mouse, so let's duplicate the checkForEvent call from Engine::update :
Now we need to update the mouse coordinate in Engine::mouse, so let's duplicate the checkForEvent call from Engine::update :


<syntaxhighlight lang="C++">
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
</syntaxhighlight>


We're going to do one more thing to help the player select his tile : fill the tile under the mouse cursor with white :
We're going to do one more thing to help the player select his tile : fill the tile under the mouse cursor with white :


if ( map->isInFov(mouse.cx,mouse.cy)
<syntaxhighlight lang="C++">
if ( map->isInFov(mouse.cx,mouse.cy)
     && ( maxRange == 0 || player->getDistance(mouse.cx,mouse.cy) <= maxRange )) {
     && ( maxRange == 0 || player->getDistance(mouse.cx,mouse.cy) <= maxRange )) {
     TCODConsole::root->setCharBackground(mouse.cx,mouse.cy,TCODColor::white);
     TCODConsole::root->setCharBackground(mouse.cx,mouse.cy,TCODColor::white);
</syntaxhighlight>


And if the player presses the left button while a valid tile is selected, return the tile coordinates :
And if the player presses the left button while a valid tile is selected, return the tile coordinates :


<syntaxhighlight lang="C++">
     if ( mouse.lbutton_pressed ) {
     if ( mouse.lbutton_pressed ) {
         *x=mouse.cx;
         *x=mouse.cx;
Line 193: Line 234:
         return true;
         return true;
     }
     }
}  
}
</syntaxhighlight>


If the player pressed a key or right clicked, we exit  :
If the player pressed a key or right clicked, we exit  :


if (mouse.rbutton_pressed || lastKey.vk != TCODK_NONE) {
<syntaxhighlight lang="C++">
if (mouse.rbutton_pressed || lastKey.vk != TCODK_NONE) {
     return false;
     return false;
}
}
</syntaxhighlight>


Finally we flush the screen. If the player exits the loop by closing the game window, we also return false :
Finally we flush the screen. If the player exits the loop by closing the game window, we also return false :


<syntaxhighlight lang="C++">
         TCODConsole::flush();
         TCODConsole::flush();
     }
     }
     return false;
     return false;
}
}
</syntaxhighlight>


===Fireball Pickable===
===Fireball Pickable===
Line 212: Line 258:
Now that we can pick a tile, we can create the new Pickable. Since it requires a range and a damage amount, we can inherit from the LightnintBolt class :
Now that we can pick a tile, we can create the new Pickable. Since it requires a range and a damage amount, we can inherit from the LightnintBolt class :


class Fireball : public LightningBolt {
<syntaxhighlight lang="C++">
public :
class Fireball : public LightningBolt {
public :
     Fireball(float range, float damage);
     Fireball(float range, float damage);
     bool use(Actor *owner, Actor *wearer);       
     bool use(Actor *owner, Actor *wearer);       
};
};
</syntaxhighlight>


Implementation :
Implementation :


Fireball::Fireball(float range, float damage)
<syntaxhighlight lang="C++">
Fireball::Fireball(float range, float damage)
     : LightningBolt(range,damage) {     
     : LightningBolt(range,damage) {     
}
}
</syntaxhighlight>


The use function displays a message and waits for the player to pick a tile :
The use function displays a message and waits for the player to pick a tile :


bool Fireball::use(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
bool Fireball::use(Actor *owner, Actor *wearer) {
     engine.gui->message(TCODColor::cyan, "Left-click a target tile for the fireball,\nor right-click to cancel.");
     engine.gui->message(TCODColor::cyan, "Left-click a target tile for the fireball,\nor right-click to cancel.");
     int x,y;
     int x,y;
Line 232: Line 283:
         return false;
         return false;
     }
     }
</syntaxhighlight>


If a valid tile was picked, a message is displayed and we start to scan all creatures alive and within range (including the player himself !) :
If a valid tile was picked, a message is displayed and we start to scan all creatures alive and within range (including the player himself !) :


// burn everything in <range> (including player)
<syntaxhighlight lang="C++">
engine.gui->message(TCODColor::orange,"The fireball explodes, burning everything within %g tiles!",range);
// burn everything in <range> (including player)
for (Actor **iterator=engine.actors.begin();
engine.gui->message(TCODColor::orange,"The fireball explodes, burning everything within %g tiles!",range);
for (Actor **iterator=engine.actors.begin();
     iterator != engine.actors.end(); iterator++) {
     iterator != engine.actors.end(); iterator++) {
     Actor *actor=*iterator;
     Actor *actor=*iterator;
     if ( actor->destructible && !actor->destructible->isDead()
     if ( actor->destructible && !actor->destructible->isDead()
         && actor->getDistance(x,y) <= range) {
         && actor->getDistance(x,y) <= range) {
</syntaxhighlight>


The poor guys get burned :
The poor guys get burned :


<syntaxhighlight lang="C++">
         engine.gui->message(TCODColor::orange,"The %s gets burned for %g hit points.",
         engine.gui->message(TCODColor::orange,"The %s gets burned for %g hit points.",
             actor->name,damage);
             actor->name,damage);
         actor->destructible->takeDamage(actor,damage);
         actor->destructible->takeDamage(actor,damage);
     }
     }
}
}
</syntaxhighlight>
and the scroll disappears :
and the scroll disappears :


<syntaxhighlight lang="C++">
     return Pickable::use(owner,wearer);
     return Pickable::use(owner,wearer);
}
}
</syntaxhighlight>
Now let's put some scrolls of fireball in the dungeon, at the end of Map::addItem :
Now let's put some scrolls of fireball in the dungeon, at the end of Map::addItem :


} else if ( dice < 70+10+10 ) {
<syntaxhighlight lang="C++">
} else if ( dice < 70+10+10 ) {
     // create a scroll of fireball
     // create a scroll of fireball
     Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
     Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
Line 263: Line 322:
     scrollOfFireball->pickable=new Fireball(3,12);
     scrollOfFireball->pickable=new Fireball(3,12);
     engine.actors.push(scrollOfFireball);
     engine.actors.push(scrollOfFireball);
</syntaxhighlight>


You can compile. With all the new powers you get, monsters may start to fear you.
You can compile. With all the new powers you get, monsters may start to fear you.
Line 276: Line 336:
Engine.hpp
Engine.hpp


Actor *getActor(int x, int y) const;
<syntaxhighlight lang="C++">
Actor *getActor(int x, int y) const;
</syntaxhighlight>


Note that the function is called getActor and not getMonster. Yes, you'll be able to confuse yourself ! The function just scans the actors and tries to find someone alive on the specified tile :
Note that the function is called getActor and not getMonster. Yes, you'll be able to confuse yourself ! The function just scans the actors and tries to find someone alive on the specified tile :


Actor *Engine::getActor(int x, int y) const {
<syntaxhighlight lang="C++">
Actor *Engine::getActor(int x, int y) const {
     for (Actor **iterator=actors.begin();
     for (Actor **iterator=actors.begin();
         iterator != actors.end(); iterator++) {
         iterator != actors.end(); iterator++) {
Line 290: Line 353:
     }
     }
     return NULL;
     return NULL;
}
}
</syntaxhighlight>


===Artificial confusion===
===Artificial confusion===
Line 298: Line 362:
Ai.hpp
Ai.hpp


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


Ai.cpp:
Ai.cpp:


ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns, Ai *oldAi)  
<syntaxhighlight lang="C++">
ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns, Ai *oldAi)  
     : nbTurns(nbTurns),oldAi(oldAi) {
     : nbTurns(nbTurns),oldAi(oldAi) {
}
}
</syntaxhighlight>


First we get some random direction.
First we get some random direction.


void ConfusedMonsterAi::update(Actor *owner) {
<syntaxhighlight lang="C++">
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);
     int dy=rng->getInt(-1,1);
     int dy=rng->getInt(-1,1);
</syntaxhighlight>


If there is a movement to a walkable cell, move the monster :
If there is a movement to a walkable cell, move the monster :


<syntaxhighlight lang="C++">
if ( dx != 0 || dy != 0 ) {
if ( dx != 0 || dy != 0 ) {
     int destx=owner->x+dx;
     int destx=owner->x+dx;
Line 328: Line 399:
         owner->x = destx;
         owner->x = destx;
         owner->y = desty;
         owner->y = desty;
</syntaxhighlight>


But if there's someone on the destination cell, attack him !
But if there's someone on the destination cell, attack him !


<syntaxhighlight lang="C++">
     } else {
     } else {
         Actor *actor=engine.getActor(destx, desty);
         Actor *actor=engine.getActor(destx, desty);
Line 337: Line 410:
         }
         }
     }
     }
}
}
</syntaxhighlight>
Finally, decrease the number of confusion turns and restore the previous Ai when the last turn has gone.
Finally, decrease the number of confusion turns and restore the previous Ai when the last turn has gone.


<syntaxhighlight lang="C++">
     nbTurns--;
     nbTurns--;
     if ( nbTurns == 0 ) {
     if ( nbTurns == 0 ) {
Line 345: Line 420:
         delete this;
         delete this;
     }
     }
}
}
</syntaxhighlight>
Note that since no Actor will contain the ConfusedMonsterAi reference anymore, we can safely delete it. Each scroll of confusion will create a new instance of ConfusedMonsterAi.
Note that since no Actor will contain the ConfusedMonsterAi reference anymore, we can safely delete it. Each scroll of confusion will create a new instance of ConfusedMonsterAi.


Line 352: Line 428:
Our last Pickable contains the number of turns of the effect and a range (we don't want to be able to confuse a monster at the other side of the map).
Our last Pickable contains the number of turns of the effect and a range (we don't want to be able to confuse a monster at the other side of the map).


class Confuser : public Pickable {
<syntaxhighlight lang="C++">
public :
class Confuser : public Pickable {
public :
     int nbTurns;
     int nbTurns;
     float range;
     float range;
     Confuser(int nbTurns, float range);
     Confuser(int nbTurns, float range);
     bool use(Actor *owner, Actor *wearer);   
     bool use(Actor *owner, Actor *wearer);   
};
};
</syntaxhighlight>


The constructor is the most complex thing we've seen so far.
The constructor is the most complex thing we've seen so far.


Confuser::Confuser(int nbTurns, float range)
<syntaxhighlight lang="C++">
Confuser::Confuser(int nbTurns, float range)
     : nbTurns(nbTurns), range(range) {
     : nbTurns(nbTurns), range(range) {
}
}
</syntaxhighlight>


Ok, I was just kidding.
Ok, I was just kidding.
Line 370: Line 450:
As with the fireball, the use function first picks a tile :
As with the fireball, the use function first picks a tile :


bool Confuser::use(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
bool Confuser::use(Actor *owner, Actor *wearer) {
     engine.gui->message(TCODColor::cyan, "Left-click an enemy to confuse it,\nor right-click to cancel.");
     engine.gui->message(TCODColor::cyan, "Left-click an enemy to confuse it,\nor right-click to cancel.");
     int x,y;
     int x,y;
Line 376: Line 457:
         return false;
         return false;
     }
     }
</syntaxhighlight>


then checks if there is an actor on the selected tile :
then checks if there is an actor on the selected tile :


Actor *actor=engine.getActor(x,y);
<syntaxhighlight lang="C++">
if (! actor ) {
Actor *actor=engine.getActor(x,y);
if (! actor ) {
     return false;
     return false;
}
}
</syntaxhighlight>


And now, time for brain washing :
And now, time for brain washing :


// confuse the monster for <nbTurns> turns
<syntaxhighlight lang="C++">
Ai *confusedAi=new ConfusedMonsterAi( nbTurns, actor->ai );
// confuse the monster for <nbTurns> turns
actor->ai = confusedAi;
Ai *confusedAi=new ConfusedMonsterAi( nbTurns, actor->ai );
actor->ai = confusedAi;
</syntaxhighlight>


Finally, display some message and destroy the scroll :
Finally, display some message and destroy the scroll :


<syntaxhighlight lang="C++">
     engine.gui->message(TCODColor::lightGreen,"The eyes of the %s look vacant,\nas he starts to stumble around!",
     engine.gui->message(TCODColor::lightGreen,"The eyes of the %s look vacant,\nas he starts to stumble around!",
         actor->name);
         actor->name);
     return Pickable::use(owner,wearer);
     return Pickable::use(owner,wearer);
}
}
</syntaxhighlight>


And we can finally handle the last 10% in Map::addItem :
And we can finally handle the last 10% in Map::addItem :


} else {
<syntaxhighlight lang="C++">
} else {
     // create a scroll of confusion
     // create a scroll of confusion
     Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
     Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
Line 406: Line 495:
     scrollOfConfusion->pickable=new Confuser(10,8);
     scrollOfConfusion->pickable=new Confuser(10,8);
     engine.actors.push(scrollOfConfusion);
     engine.actors.push(scrollOfConfusion);
</syntaxhighlight>


==Bonus: drop item==
==Bonus: drop item==
Line 413: Line 503:
Pickable.hpp
Pickable.hpp


void drop(Actor *owner, Actor *wearer);
<syntaxhighlight lang="C++">
void drop(Actor *owner, Actor *wearer);
</syntaxhighlight>


Pickable.cpp
Pickable.cpp


void Pickable::drop(Actor *owner, Actor *wearer) {
<syntaxhighlight lang="C++">
void Pickable::drop(Actor *owner, Actor *wearer) {
     if ( wearer->container ) {
     if ( wearer->container ) {
         wearer->container->remove(owner);
         wearer->container->remove(owner);
Line 426: Line 519:
             wearer->name,owner->name);
             wearer->name,owner->name);
     }
     }
}
}
</syntaxhighlight>


The only subtlety here is that we have to remember to update the item position before dropping it on the ground else it will reappear on the tile where it was picked !
The only subtlety here is that we have to remember to update the item position before dropping it on the ground else it will reappear on the tile where it was picked !
Line 432: Line 526:
The input is processed in PlayerAi::handleActionKey :
The input is processed in PlayerAi::handleActionKey :


case 'd' : // drop item  
<syntaxhighlight lang="C++">
{
case 'd' : // drop item  
{
     Actor *actor=choseFromInventory(owner);
     Actor *actor=choseFromInventory(owner);
     if ( actor ) {
     if ( actor ) {
Line 439: Line 534:
         engine.gameStatus=Engine::NEW_TURN;
         engine.gameStatus=Engine::NEW_TURN;
     }           
     }           
}
}
break;
break;
</syntaxhighlight>


Dropping an item also costs you a turn.
Dropping an item also costs you a turn.

Latest revision as of 09:32, 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'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
View source here

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.

Targetting helper

Let's create a helper function for that in the Engine class.

Engine.hpp

bool pickATile(int *x, int *y, float maxRange = 0.0f);

The function returns a boolean to allow the player to cancel by pressing a key or right clicking. A range of 0 means that we allow the tile to be picked anywhere in the player's field of view. This function uses a default value for the maxRange parameter so that we can omit the parameter :

engine.pickATile(&x,&y);

is the same as

engine.pickATile(&x,&y, 0.0f);

We're not going to use the main loop from main.cpp while picking a tile. This would require to add a flag in the engine to know if we're in standard play mode or tile picking mode. Instead, we create a alternative game loop.

Since we want the mouse look to keep working while targetting, we need to render the game screen in the loop

bool Engine::pickATile(int *x, int *y, float maxRange) {
    while ( !TCODConsole::isWindowClosed() ) {
        render();

Now the player might not be aware of where he's allowed to click. Let's highlight the zone for him. We scan the whole map and look for tiles in FOV and within range :

// highlight the possible range
for (int cx=0; cx < map->width; cx++) {
    for (int cy=0; cy < map->height; cy++) {
        if ( map->isInFov(cx,cy)
            && ( maxRange == 0 || player->getDistance(cx,cy) <= maxRange) ) {

Remember how we darkened the oldest message log by multiplying its color by a float smaller than 1 ? Well we can highlight a color using the same trick :

            TCODColor col=TCODConsole::root->getCharBackground(cx,cy);
            col = col * 1.2f;
            TCODConsole::root->setCharBackground(cx,cy,col);
        }
    }
}

Now we need to update the mouse coordinate in Engine::mouse, so let's duplicate the checkForEvent call from Engine::update :

TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);

We're going to do one more thing to help the player select his tile : fill the tile under the mouse cursor with white :

if ( map->isInFov(mouse.cx,mouse.cy)
    && ( maxRange == 0 || player->getDistance(mouse.cx,mouse.cy) <= maxRange )) {
    TCODConsole::root->setCharBackground(mouse.cx,mouse.cy,TCODColor::white);

And if the player presses the left button while a valid tile is selected, return the tile coordinates :

    if ( mouse.lbutton_pressed ) {
        *x=mouse.cx;
        *y=mouse.cy;
        return true;
    }
}

If the player pressed a key or right clicked, we exit  :

if (mouse.rbutton_pressed || lastKey.vk != TCODK_NONE) {
    return false;
}

Finally we flush the screen. If the player exits the loop by closing the game window, we also return false :

        TCODConsole::flush();
    }
    return false;
}

Fireball Pickable

Now that we can pick a tile, we can create the new Pickable. Since it requires a range and a damage amount, we can inherit from the LightnintBolt class :

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

Implementation :

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

The use function displays a message and waits for the player to pick a tile :

bool Fireball::use(Actor *owner, Actor *wearer) {
    engine.gui->message(TCODColor::cyan, "Left-click a target tile for the fireball,\nor right-click to cancel.");
    int x,y;
    if (! engine.pickATile(&x,&y)) {
        return false;
    }

If a valid tile was picked, a message is displayed and we start to scan all creatures alive and within range (including the player himself !) :

// burn everything in <range> (including player)
engine.gui->message(TCODColor::orange,"The fireball explodes, burning everything within %g tiles!",range);
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) {

The poor guys get burned :

        engine.gui->message(TCODColor::orange,"The %s gets burned for %g hit points.",
            actor->name,damage);
        actor->destructible->takeDamage(actor,damage);
    }
}

and the scroll disappears :

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

Now let's put some scrolls of fireball in the dungeon, at the end of Map::addItem :

} else if ( dice < 70+10+10 ) {
    // create a scroll of fireball
    Actor *scrollOfFireball=new Actor(x,y,'#',"scroll of fireball",
        TCODColor::lightYellow);
    scrollOfFireball->blocks=false;
    scrollOfFireball->pickable=new Fireball(3,12);
    engine.actors.push(scrollOfFireball);

You can compile. With all the new powers you get, monsters may start to fear you.

The scroll of confusion

This one is very special as it will change the behaviour of a monster. For this, we need to be able to pick a specific actor.

Actor picking helper

Let's add some helper in the Engine class.

Engine.hpp

Actor *getActor(int x, int y) const;

Note that the function is called getActor and not getMonster. Yes, you'll be able to confuse yourself ! The function just scans the actors and tries to find someone alive on the specified tile :

Actor *Engine::getActor(int x, int y) const {
    for (Actor **iterator=actors.begin();
        iterator != actors.end(); iterator++) {
        Actor *actor=*iterator;
        if ( actor->x == x && actor->y ==y && actor->destructible
            && ! actor->destructible->isDead()) {
            return actor;
        }
    }
    return NULL;
}

Artificial confusion

We need a new Ai that wanders randomly and attacks anything it bumps into. This Ai have to be able to restore the previous Ai after a few turns :

Ai.hpp

class ConfusedMonsterAi : public Ai {
public :
    ConfusedMonsterAi(int nbTurns, Ai *oldAi);
    void update(Actor *owner);
protected :
    int nbTurns;
    Ai *oldAi;
};

Ai.cpp:

ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns, Ai *oldAi) 
    : nbTurns(nbTurns),oldAi(oldAi) {
}

First we get some random direction.

void ConfusedMonsterAi::update(Actor *owner) {
    TCODRandom *rng=TCODRandom::getInstance();
    int dx=rng->getInt(-1,1);
    int dy=rng->getInt(-1,1);

If there is a movement to a walkable cell, move the monster :

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;

But if there's someone on the destination cell, attack him !

    } else {
        Actor *actor=engine.getActor(destx, desty);
        if ( actor ) {
            owner->attacker->attack(owner, actor);
        }
    }
}

Finally, decrease the number of confusion turns and restore the previous Ai when the last turn has gone.

    nbTurns--;
    if ( nbTurns == 0 ) {
        owner->ai = oldAi;
        delete this;
    }
}

Note that since no Actor will contain the ConfusedMonsterAi reference anymore, we can safely delete it. Each scroll of confusion will create a new instance of ConfusedMonsterAi.

Confuser Pickable

Our last Pickable contains the number of turns of the effect and a range (we don't want to be able to confuse a monster at the other side of the map).

class Confuser : public Pickable {
public :
    int nbTurns;
    float range;
    Confuser(int nbTurns, float range);
    bool use(Actor *owner, Actor *wearer);  
};

The constructor is the most complex thing we've seen so far.

Confuser::Confuser(int nbTurns, float range)
    : nbTurns(nbTurns), range(range) {
}

Ok, I was just kidding.

As with the fireball, the use function first picks a tile :

bool Confuser::use(Actor *owner, Actor *wearer) {
    engine.gui->message(TCODColor::cyan, "Left-click an enemy to confuse it,\nor right-click to cancel.");
    int x,y;
    if (! engine.pickATile(&x,&y,range)) {
        return false;
    }

then checks if there is an actor on the selected tile :

Actor *actor=engine.getActor(x,y);
if (! actor ) {
    return false;
}

And now, time for brain washing :

// confuse the monster for <nbTurns> turns
Ai *confusedAi=new ConfusedMonsterAi( nbTurns, actor->ai );
actor->ai = confusedAi;

Finally, display some message and destroy the scroll :

    engine.gui->message(TCODColor::lightGreen,"The eyes of the %s look vacant,\nas he starts to stumble around!",
        actor->name);
    return Pickable::use(owner,wearer);
}

And we can finally handle the last 10% in Map::addItem :

} else {
    // create a scroll of confusion
    Actor *scrollOfConfusion=new Actor(x,y,'#',"scroll of confusion",
        TCODColor::lightYellow);
    scrollOfConfusion->blocks=false;
    scrollOfConfusion->pickable=new Confuser(10,8);
    engine.actors.push(scrollOfConfusion);

Bonus: drop item

If you're not too impatient to use all the new magical goodies, we can add a last small feature. So far we were able to pick items but not to drop them. Let's add a drop function to the Pickable class :

Pickable.hpp

void drop(Actor *owner, Actor *wearer);

Pickable.cpp

void Pickable::drop(Actor *owner, Actor *wearer) {
    if ( wearer->container ) {
        wearer->container->remove(owner);
        engine.actors.push(owner);
        owner->x=wearer->x;
        owner->y=wearer->y;
        engine.gui->message(TCODColor::lightGrey,"%s drops a %s.",
            wearer->name,owner->name);
    }
}

The only subtlety here is that we have to remember to update the item position before dropping it on the ground else it will reappear on the tile where it was picked !

The input is processed in PlayerAi::handleActionKey :

case 'd' : // drop item 
{
    Actor *actor=choseFromInventory(owner);
    if ( actor ) {
        actor->pickable->drop(actor,owner);
        engine.gameStatus=Engine::NEW_TURN;
    }           
}
break;

Dropping an item also costs you a turn.

Now you can compile and enjoy your new mage powers !