Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 11: dungeon levels and character progression"
(Fix a typo in a note about a relevant API change that breaks some tutorial code) |
HexDecimal (talk | contribs) (Add syntaxhighlight. Fix blocks.) |
||
Line 9: | Line 9: | ||
Obviously, the first thing we need is some stairs to go deeper into the dungeon. Where as other actors are only displayed when they are in the field of view, we would like the stairs to be always visible. We need another boolean on the Actor class for this : | Obviously, the first thing we need is some stairs to go deeper into the dungeon. Where as other actors are only displayed when they are in the field of view, we would like the stairs to be always visible. We need another boolean on the Actor class for this : | ||
<syntaxhighlight lang="C++" highlight="2"> | |||
bool blocks; // can we walk on this actor? | |||
bool fovOnly; // only display when in fov | |||
Attacker *attacker; // something that deals damages | |||
</syntaxhighlight> | |||
Of course this field must be initialized in the constructor : | Of course this field must be initialized in the constructor : | ||
<syntaxhighlight lang="C++" highlight="4"> | |||
Actor::Actor(int x, int y, int ch, const char *name, | |||
const TCODColor &col) : | const TCODColor &col) : | ||
x(x),y(y),ch(ch),col(col),name(name), | x(x),y(y),ch(ch),col(col),name(name), | ||
blocks(true),fovOnly(true),attacker(NULL),destructible(NULL),ai(NULL), | |||
pickable(NULL),container(NULL) { | pickable(NULL),container(NULL) { | ||
} | |||
</syntaxhighlight> | |||
The engine rendering code must be updated to use this new field : | The engine rendering code must be updated to use this new field : | ||
<syntaxhighlight lang="C++" highlight="6-7"> | |||
// draw the actors | |||
for (Actor **iterator=actors.begin(); | |||
iterator != actors.end(); iterator++) { | iterator != actors.end(); iterator++) { | ||
Actor *actor=*iterator; | Actor *actor=*iterator; | ||
if ( actor != player | if ( actor != player | ||
&& ((!actor->fovOnly && map->isExplored(actor->x,actor->y)) | |||
|| map->isInFov(actor->x,actor->y)) ) { | || map->isInFov(actor->x,actor->y)) ) { | ||
actor->render(); | actor->render(); | ||
} | } | ||
} | |||
</syntaxhighlight> | |||
Now we can create the stairs. But we need to keep a pointer on them to be able to detect when the player stands on their cell, so let's add an Actor pointer in the Engine class : | Now we can create the stairs. But we need to keep a pointer on them to be able to detect when the player stands on their cell, so let's add an Actor pointer in the Engine class : | ||
<syntaxhighlight lang="C++" highlight="2"> | |||
Actor *player; | |||
Actor *stairs; | |||
Map *map; | |||
</syntaxhighlight> | |||
We create the stair in the Engine::init function : | We create the stair in the Engine::init function : | ||
<syntaxhighlight lang="C++" highlight="2-5"> | |||
actors.push(player); | |||
stairs = new Actor(0,0,'>',"stairs",TCODColor::white); | |||
stairs->blocks=false; | |||
stairs->fovOnly=false; | |||
actors.push(stairs); | |||
map = new Map(80,43); | |||
</syntaxhighlight> | |||
In the end of Map::createRoom, we put the stair in the middle of the room. This will be called for every room in the dungeon. In the end, the stairs will be in the last room of the BSP tree, far from the player who is in the first room. | In the end of Map::createRoom, we put the stair in the middle of the room. This will be called for every room in the dungeon. In the end, the stairs will be in the last room of the BSP tree, far from the player who is in the first room. | ||
<syntaxhighlight lang="C++"> | |||
// set stairs position | |||
engine.stairs->x=(x1+x2)/2; | |||
engine.stairs->y=(y1+y2)/2; | |||
</syntaxhighlight> | |||
The stairs must be saved along with other actors in Engine::save : | The stairs must be saved along with other actors in Engine::save : | ||
<syntaxhighlight lang="C++" highlight="3-4,6,8"> | |||
// then the player | |||
player->save(zip); | |||
// then the stairs | |||
stairs->save(zip); | |||
// then all the other actors | |||
zip.putInt(actors.size()-2); | |||
for (Actor **it=actors.begin(); it!=actors.end(); it++) { | |||
if ( *it != player && *it != stairs ) { | |||
</syntaxhighlight> | |||
And restored in Engine::load : | And restored in Engine::load : | ||
<syntaxhighlight lang="C++" highlight="2-5"> | |||
player->load(zip); | |||
// the stairs | |||
stairs=new Actor(0,0,0,NULL,TCODColor::white); | |||
stairs->load(zip); | |||
actors.push(stairs); | |||
// then all other actors | |||
</syntaxhighlight> | |||
For this tutorial, we create one-way stairs. Once you go down, you can't go back up. But you can improve this by adding stairs going up. You don't even need a new class, you can use the actor character (either > or <) to detect the type of stair. | For this tutorial, we create one-way stairs. Once you go down, you can't go back up. But you can improve this by adding stairs going up. You don't even need a new class, you can use the actor character (either > or <) to detect the type of stair. | ||
Line 79: | Line 95: | ||
When the player is on the stair, he can push '>' to go down to the next level. We check this in PlayerAi::handleActionKey : | When the player is on the stair, he can push '>' to go down to the next level. We check this in PlayerAi::handleActionKey : | ||
<syntaxhighlight lang="C++"> | |||
case '>' : | |||
if ( engine.stairs->x == owner->x && engine.stairs->y == owner->y ) { | if ( engine.stairs->x == owner->x && engine.stairs->y == owner->y ) { | ||
engine.nextLevel(); | engine.nextLevel(); | ||
Line 85: | Line 102: | ||
engine.gui->message(TCODColor::lightGrey,"There are no stairs here."); | engine.gui->message(TCODColor::lightGrey,"There are no stairs here."); | ||
} | } | ||
break; | |||
</syntaxhighlight> | |||
Note that this seems not to work anymore as lastKey.c is always lowercase. In that case check lastKey.shift. | Note that this seems not to work anymore as lastKey.c is always lowercase. In that case check lastKey.shift. | ||
Line 93: | Line 111: | ||
Engine.hpp: | Engine.hpp: | ||
<syntaxhighlight lang="C++"> | |||
int level; | |||
void nextLevel(); | |||
</syntaxhighlight> | |||
This level starts with 1 : | This level starts with 1 : | ||
<syntaxhighlight lang="C++" highlight="3"> | |||
Engine::Engine(int screenWidth, int screenHeight) : gameStatus(STARTUP), | |||
player(NULL),map(NULL),fovRadius(10), | player(NULL),map(NULL),fovRadius(10), | ||
screenWidth(screenWidth),screenHeight(screenHeight),level(1) { | |||
</syntaxhighlight> | |||
and is increased in nextLevel() : | and is increased in nextLevel() : | ||
<syntaxhighlight lang="C++"> | |||
void Engine::nextLevel() { | |||
level++; | level++; | ||
</syntaxhighlight> | |||
We also display some messages and heal the player : | We also display some messages and heal the player : | ||
<syntaxhighlight lang="C++"> | |||
gui->message(TCODColor::lightViolet,"You take a moment to rest, and recover your strength."); | |||
player->destructible->heal(player->destructible->maxHp/2); | |||
gui->message(TCODColor::red,"After a rare moment of peace, you descend\ndeeper into the heart of the dungeon..."); | |||
</syntaxhighlight> | |||
Now we clean the dungeon, removing all the monsters and items, but not the player. There's also no point deleting and recreating the stairs : | Now we clean the dungeon, removing all the monsters and items, but not the player. There's also no point deleting and recreating the stairs : | ||
<syntaxhighlight lang="C++"> | |||
delete map; | delete map; | ||
// delete all actors but player and stairs | // delete all actors but player and stairs | ||
Line 123: | Line 150: | ||
} | } | ||
} | } | ||
</syntaxhighlight> | |||
We're using a special function of TCODList here that makes it possible to remove an element from the list while iterating over it. It's very important to use the remove function that takes the iterator as parameter and not the function that takes a list element. Doing this : | We're using a special function of TCODList here that makes it possible to remove an element from the list while iterating over it. It's very important to use the remove function that takes the iterator as parameter and not the function that takes a list element. Doing this : | ||
<syntaxhighlight lang="C++"> | |||
actors.remove(*it) | |||
</syntaxhighlight> | |||
on the last element of the list would result in it getting bigger than actors.end(). The loop would keep rolling until a SIGSEGV occurs (see the extra article about debugging). | on the last element of the list would result in it getting bigger than actors.end(). The loop would keep rolling until a SIGSEGV occurs (see the extra article about debugging). | ||
Line 134: | Line 164: | ||
Back to the nextLevel function, we can now recreate a map (including actors) : | Back to the nextLevel function, we can now recreate a map (including actors) : | ||
<syntaxhighlight lang="C++"> | |||
// create a new map | // create a new map | ||
map = new Map(80,43); | map = new Map(80,43); | ||
map->init(true); | map->init(true); | ||
gameStatus=STARTUP; | gameStatus=STARTUP; | ||
} | |||
</syntaxhighlight> | |||
One last thing we want to do is to display the dungeon level, in Gui::render, just before blitting the gui console : | One last thing we want to do is to display the dungeon level, in Gui::render, just before blitting the gui console : | ||
<syntaxhighlight lang="C++"> | |||
// dungeon level | |||
con->setDefaultForeground(TCODColor::white); | |||
con->print(3,3,"Dungeon level %d",engine.level); | |||
</syntaxhighlight> | |||
==Rewarding monster kills== | ==Rewarding monster kills== | ||
Line 152: | Line 186: | ||
Destructible.hpp : | Destructible.hpp : | ||
<syntaxhighlight lang="C++"> | |||
int xp; // XP gained when killing this monster (or player xp) | |||
Destructible(float maxHp, float defense, const char *corpseName, int xp); | |||
</syntaxhighlight> | |||
Destructible.cpp : | Destructible.cpp : | ||
<syntaxhighlight lang="C++"> | |||
Destructible::Destructible(float maxHp, float defense, const char *corpseName, int xp) : | |||
maxHp(maxHp),hp(maxHp),defense(defense),corpseName(corpseName),xp(xp) { | maxHp(maxHp),hp(maxHp),defense(defense),corpseName(corpseName),xp(xp) { | ||
} | |||
</syntaxhighlight> | |||
We also add the xp value to the monster constructor : | We also add the xp value to the monster constructor : | ||
Line 166: | Line 204: | ||
Destructible.hpp : | Destructible.hpp : | ||
<syntaxhighlight lang="C++"> | |||
MonsterDestructible(float maxHp, float defense, const char *corpseName, int xp); | |||
</syntaxhighlight> | |||
Destructible.cpp : | Destructible.cpp : | ||
<syntaxhighlight lang="C++"> | |||
MonsterDestructible::MonsterDestructible(float maxHp, float defense, const char *corpseName,int xp) : | |||
Destructible(maxHp,defense,corpseName,xp) { | Destructible(maxHp,defense,corpseName,xp) { | ||
} | |||
</syntaxhighlight> | |||
Now we change the MonsterDestructible::die function to handle the xp : | Now we change the MonsterDestructible::die function to handle the xp : | ||
<syntaxhighlight lang="C++" highlight="4-6"> | |||
void MonsterDestructible::die(Actor *owner) { | |||
// transform it into a nasty corpse! it doesn't block, can't be | // transform it into a nasty corpse! it doesn't block, can't be | ||
// attacked and doesn't move | // attacked and doesn't move | ||
engine.gui->message(TCODColor::lightGrey,"%s is dead. You gain %d xp", | |||
owner->name, xp); | owner->name, xp); | ||
engine.player->destructible->xp += xp; | engine.player->destructible->xp += xp; | ||
Destructible::die(owner); | Destructible::die(owner); | ||
} | |||
</syntaxhighlight> | |||
==Player's XP level== | ==Player's XP level== | ||
Line 193: | Line 237: | ||
Ai.hpp : | Ai.hpp : | ||
<syntaxhighlight lang="C++" highlight="2-5"> | |||
class PlayerAi : public Ai { | |||
public : | |||
int xpLevel; | int xpLevel; | ||
PlayerAi(); | PlayerAi(); | ||
int getNextLevelXp();</ | int getNextLevelXp(); | ||
</syntaxhighlight> | |||
Ai.cpp : | Ai.cpp : | ||
<syntaxhighlight lang="C++"> | |||
PlayerAi::PlayerAi() : xpLevel(1) { | |||
} | |||
const int LEVEL_UP_BASE=200; | |||
const int LEVEL_UP_FACTOR=150; | |||
int PlayerAi::getNextLevelXp() { | |||
return LEVEL_UP_BASE + xpLevel*LEVEL_UP_FACTOR; | return LEVEL_UP_BASE + xpLevel*LEVEL_UP_FACTOR; | ||
} | |||
</syntaxhighlight> | |||
The player starts at level 1. Level 2 requires 350 xp, then every new level requires 150 more xp than the previous. We can update the xp level in the PlayerAi::update function : | The player starts at level 1. Level 2 requires 350 xp, then every new level requires 150 more xp than the previous. We can update the xp level in the PlayerAi::update function : | ||
<syntaxhighlight lang="C++" highlight="2-7"> | |||
void PlayerAi::update(Actor *owner) { | |||
int levelUpXp = getNextLevelXp(); | |||
if ( owner->destructible->xp >= levelUpXp ) { | if ( owner->destructible->xp >= levelUpXp ) { | ||
xpLevel++; | xpLevel++; | ||
owner->destructible->xp -= levelUpXp; | owner->destructible->xp -= levelUpXp; | ||
engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel); | engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel); | ||
} | } | ||
if ( owner->destructible && owner->destructible->isDead() ) { | if ( owner->destructible && owner->destructible->isDead() ) { | ||
return; | return; | ||
} | } | ||
</syntaxhighlight> | |||
Another change in Gui::render() to display the player xp bar : | Another change in Gui::render() to display the player xp bar : | ||
<syntaxhighlight lang="C++"> | |||
// draw the XP bar | |||
PlayerAi *ai=(PlayerAi *)engine.player->ai; | |||
char xpTxt[128]; | |||
sprintf(xpTxt,"XP(%d)",ai->xpLevel); | |||
renderBar(1,5,BAR_WIDTH,xpTxt,engine.player->destructible->xp, | |||
ai->getNextLevelXp(), | ai->getNextLevelXp(), | ||
TCODColor::lightViolet,TCODColor::darkerViolet); | TCODColor::lightViolet,TCODColor::darkerViolet); | ||
</syntaxhighlight> | |||
Now we also want the player to chose some upgrade when he reaches a new xp level. For that, we need to improve the menu class. | Now we also want the player to chose some upgrade when he reaches a new xp level. For that, we need to improve the menu class. | ||
Line 240: | Line 292: | ||
Now we can finally ask the player what he wants to improve when he reaches a new xp level. Let's define some new menu items in Gui.hpp: | Now we can finally ask the player what he wants to improve when he reaches a new xp level. Let's define some new menu items in Gui.hpp: | ||
<syntaxhighlight lang="C++" highlight="6-8"> | |||
enum MenuItemCode { | enum MenuItemCode { | ||
NONE, | NONE, | ||
Line 245: | Line 298: | ||
CONTINUE, | CONTINUE, | ||
EXIT, | EXIT, | ||
CONSTITUTION, | |||
STRENGTH, | STRENGTH, | ||
AGILITY</ | AGILITY | ||
}; | |||
</syntaxhighlight> | |||
In PlayerAi::update, we build a custom menu and use the pick function : | In PlayerAi::update, we build a custom menu and use the pick function : | ||
<syntaxhighlight lang="C++" highlight="7-11"> | |||
void PlayerAi::update(Actor *owner) { | |||
int levelUpXp = getNextLevelXp(); | int levelUpXp = getNextLevelXp(); | ||
if ( owner->destructible->xp >= levelUpXp ) { | if ( owner->destructible->xp >= levelUpXp ) { | ||
Line 258: | Line 313: | ||
owner->destructible->xp -= levelUpXp; | owner->destructible->xp -= levelUpXp; | ||
engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel); | engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel); | ||
engine.gui->menu.clear(); | |||
engine.gui->menu.addItem(Menu::CONSTITUTION,"Constitution (+20HP)"); | engine.gui->menu.addItem(Menu::CONSTITUTION,"Constitution (+20HP)"); | ||
engine.gui->menu.addItem(Menu::STRENGTH,"Strength (+1 attack)"); | engine.gui->menu.addItem(Menu::STRENGTH,"Strength (+1 attack)"); | ||
engine.gui->menu.addItem(Menu::AGILITY,"Agility (+1 defense)"); | engine.gui->menu.addItem(Menu::AGILITY,"Agility (+1 defense)"); | ||
Menu::MenuItemCode menuItem=engine.gui->menu.pick(Menu::PAUSE);</ | Menu::MenuItemCode menuItem=engine.gui->menu.pick(Menu::PAUSE); | ||
</syntaxhighlight> | |||
Then we update the player's stats depending on what was chosen : | Then we update the player's stats depending on what was chosen : | ||
<syntaxhighlight lang="C++"> | |||
switch (menuItem) { | |||
case Menu::CONSTITUTION : | case Menu::CONSTITUTION : | ||
owner->destructible->maxHp+=20; | owner->destructible->maxHp+=20; | ||
Line 278: | Line 335: | ||
break; | break; | ||
default:break; | default:break; | ||
} | |||
</syntaxhighlight> | |||
==A fancy pause menu== | ==A fancy pause menu== | ||
Line 288: | Line 346: | ||
The display mode is defined in Gui.hpp : | The display mode is defined in Gui.hpp : | ||
<syntaxhighlight lang="C++" highlight="9-12,16"> | |||
class Menu { | |||
public : | |||
enum MenuItemCode { | enum MenuItemCode { | ||
NONE, | NONE, | ||
Line 296: | Line 355: | ||
EXIT | EXIT | ||
}; | }; | ||
enum DisplayMode { | |||
MAIN, | MAIN, | ||
PAUSE | PAUSE | ||
}; | }; | ||
~Menu(); | ~Menu(); | ||
void clear(); | void clear(); | ||
void addItem(MenuItemCode code, const char *label); | void addItem(MenuItemCode code, const char *label); | ||
MenuItemCode pick(DisplayMode mode=MAIN); | |||
</syntaxhighlight> | |||
Now the implementation, in Gui.cpp. First we define the size of the pause menu using some constants : | Now the implementation, in Gui.cpp. First we define the size of the pause menu using some constants : | ||
<syntaxhighlight lang="C++"> | |||
const int PAUSE_MENU_WIDTH=30; | |||
const int PAUSE_MENU_HEIGHT=15; | |||
Menu::MenuItemCode Menu::pick(DisplayMode mode) { | |||
</syntaxhighlight> | |||
Since the menu position depends on the display mode, we define two variables to store the menu position : | Since the menu position depends on the display mode, we define two variables to store the menu position : | ||
<syntaxhighlight lang="C++" highlight="2"> | |||
int selectedItem=0; | |||
int menux,menuy; | |||
</syntaxhighlight> | |||
When we render the pause menu, we want to center the menu on the screen : | When we render the pause menu, we want to center the menu on the screen : | ||
<syntaxhighlight lang="C++"> | |||
if (mode == PAUSE) { | |||
menux=engine.screenWidth/2-PAUSE_MENU_WIDTH/2; | menux=engine.screenWidth/2-PAUSE_MENU_WIDTH/2; | ||
menuy=engine.screenHeight/2-PAUSE_MENU_HEIGHT/2; | menuy=engine.screenHeight/2-PAUSE_MENU_HEIGHT/2; | ||
</syntaxhighlight> | |||
Then we use the printFrame helper function to draw an empty dialog box. We use the TCOD_BKGND_ALPHA flag so that the background is transparent. The value 70 means alpha is 70/255 = 0.27. The dialog is almost opaque and let us just see a bit of the background. | Then we use the printFrame helper function to draw an empty dialog box. We use the TCOD_BKGND_ALPHA flag so that the background is transparent. The value 70 means alpha is 70/255 = 0.27. The dialog is almost opaque and let us just see a bit of the background. | ||
<syntaxhighlight lang="C++"> | |||
TCODConsole::root->setDefaultForeground(TCODColor(200,180,50)); | |||
TCODConsole::root->printFrame(menux,menuy,PAUSE_MENU_WIDTH,PAUSE_MENU_HEIGHT,true, | |||
TCOD_BKGND_ALPHA(70),"menu"); | TCOD_BKGND_ALPHA(70),"menu"); | ||
</syntaxhighlight> | |||
Then we slightly offset the position of the menu so that it doesn't render on top of the dialog frame : | Then we slightly offset the position of the menu so that it doesn't render on top of the dialog frame : | ||
<syntaxhighlight lang="C++"> | |||
menux+=2; | |||
menuy+=3; | |||
</syntaxhighlight> | |||
If we're rendering the main menu, we just display the background image and define the menu position : | If we're rendering the main menu, we just display the background image and define the menu position : | ||
<syntaxhighlight lang="C++"> | |||
} else { | |||
static TCODImage img("menu_background1.png"); | static TCODImage img("menu_background1.png"); | ||
img.blit2x(TCODConsole::root,0,0); | img.blit2x(TCODConsole::root,0,0); | ||
menux=10; | menux=10; | ||
menuy=TCODConsole::root->getHeight()/3; | menuy=TCODConsole::root->getHeight()/3; | ||
} | |||
</syntaxhighlight> | |||
Then we just have to update the menu render code to use the menux, menuy variables : | Then we just have to update the menu render code to use the menux, menuy variables : | ||
<syntaxhighlight lang="C++" highlight="9"> | |||
while( !TCODConsole::isWindowClosed() ) { | |||
int currentItem=0; | int currentItem=0; | ||
for (MenuItem **it=items.begin(); it!=items.end(); it++) { | for (MenuItem **it=items.begin(); it!=items.end(); it++) { | ||
Line 352: | Line 425: | ||
TCODConsole::root->setDefaultForeground(TCODColor::lightGrey); | TCODConsole::root->setDefaultForeground(TCODColor::lightGrey); | ||
} | } | ||
TCODConsole::root->print(menux,menuy+currentItem*3,(*it)->label); | |||
currentItem++; | currentItem++; | ||
} | } | ||
</syntaxhighlight> | |||
[[Category:Developing]] | [[Category:Developing]] |
Latest revision as of 10:18, 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 turn the single level prototype into a real game by adding multi-level dungeons (in fact infinite levels) and experience level for the player.
Stairs
Obviously, the first thing we need is some stairs to go deeper into the dungeon. Where as other actors are only displayed when they are in the field of view, we would like the stairs to be always visible. We need another boolean on the Actor class for this :
bool blocks; // can we walk on this actor?
bool fovOnly; // only display when in fov
Attacker *attacker; // something that deals damages
Of course this field must be initialized in the constructor :
Actor::Actor(int x, int y, int ch, const char *name,
const TCODColor &col) :
x(x),y(y),ch(ch),col(col),name(name),
blocks(true),fovOnly(true),attacker(NULL),destructible(NULL),ai(NULL),
pickable(NULL),container(NULL) {
}
The engine rendering code must be updated to use this new field :
// draw the actors
for (Actor **iterator=actors.begin();
iterator != actors.end(); iterator++) {
Actor *actor=*iterator;
if ( actor != player
&& ((!actor->fovOnly && map->isExplored(actor->x,actor->y))
|| map->isInFov(actor->x,actor->y)) ) {
actor->render();
}
}
Now we can create the stairs. But we need to keep a pointer on them to be able to detect when the player stands on their cell, so let's add an Actor pointer in the Engine class :
Actor *player;
Actor *stairs;
Map *map;
We create the stair in the Engine::init function :
actors.push(player);
stairs = new Actor(0,0,'>',"stairs",TCODColor::white);
stairs->blocks=false;
stairs->fovOnly=false;
actors.push(stairs);
map = new Map(80,43);
In the end of Map::createRoom, we put the stair in the middle of the room. This will be called for every room in the dungeon. In the end, the stairs will be in the last room of the BSP tree, far from the player who is in the first room.
// set stairs position
engine.stairs->x=(x1+x2)/2;
engine.stairs->y=(y1+y2)/2;
The stairs must be saved along with other actors in Engine::save :
// then the player
player->save(zip);
// then the stairs
stairs->save(zip);
// then all the other actors
zip.putInt(actors.size()-2);
for (Actor **it=actors.begin(); it!=actors.end(); it++) {
if ( *it != player && *it != stairs ) {
And restored in Engine::load :
player->load(zip);
// the stairs
stairs=new Actor(0,0,0,NULL,TCODColor::white);
stairs->load(zip);
actors.push(stairs);
// then all other actors
For this tutorial, we create one-way stairs. Once you go down, you can't go back up. But you can improve this by adding stairs going up. You don't even need a new class, you can use the actor character (either > or <) to detect the type of stair.
When the player is on the stair, he can push '>' to go down to the next level. We check this in PlayerAi::handleActionKey :
case '>' :
if ( engine.stairs->x == owner->x && engine.stairs->y == owner->y ) {
engine.nextLevel();
} else {
engine.gui->message(TCODColor::lightGrey,"There are no stairs here.");
}
break;
Note that this seems not to work anymore as lastKey.c is always lowercase. In that case check lastKey.shift.
The Engine::nextLevel function deals with the dungeon regeneration. We'll keep the current level number in a field of the Engine.
Engine.hpp:
int level;
void nextLevel();
This level starts with 1 :
Engine::Engine(int screenWidth, int screenHeight) : gameStatus(STARTUP),
player(NULL),map(NULL),fovRadius(10),
screenWidth(screenWidth),screenHeight(screenHeight),level(1) {
and is increased in nextLevel() :
void Engine::nextLevel() {
level++;
We also display some messages and heal the player :
gui->message(TCODColor::lightViolet,"You take a moment to rest, and recover your strength.");
player->destructible->heal(player->destructible->maxHp/2);
gui->message(TCODColor::red,"After a rare moment of peace, you descend\ndeeper into the heart of the dungeon...");
Now we clean the dungeon, removing all the monsters and items, but not the player. There's also no point deleting and recreating the stairs :
delete map;
// delete all actors but player and stairs
for (Actor **it=actors.begin(); it!=actors.end(); it++) {
if ( *it != player && *it != stairs ) {
delete *it;
it = actors.remove(it);
}
}
We're using a special function of TCODList here that makes it possible to remove an element from the list while iterating over it. It's very important to use the remove function that takes the iterator as parameter and not the function that takes a list element. Doing this :
actors.remove(*it)
on the last element of the list would result in it getting bigger than actors.end(). The loop would keep rolling until a SIGSEGV occurs (see the extra article about debugging).
Another very important thing when iterating over a TCODList : never add an element to the list inside the loop. Adding an element might result in a reallocation of the list. The iterator value would not be correct anymore after that. If you need to add an element to the list, create another toAdd list, fill it with the elements to be added. Once you finished iterating over the first list, call myList.addAll(toAdd).
Back to the nextLevel function, we can now recreate a map (including actors) :
// create a new map
map = new Map(80,43);
map->init(true);
gameStatus=STARTUP;
}
One last thing we want to do is to display the dungeon level, in Gui::render, just before blitting the gui console :
// dungeon level
con->setDefaultForeground(TCODColor::white);
con->print(3,3,"Dungeon level %d",engine.level);
Rewarding monster kills
We need some xp value on the creatures. When the player kills a creature, the creature's xp is added the player's one. We put this in the Destructible class, so that both creatures and the player get it :
Destructible.hpp :
int xp; // XP gained when killing this monster (or player xp)
Destructible(float maxHp, float defense, const char *corpseName, int xp);
Destructible.cpp :
Destructible::Destructible(float maxHp, float defense, const char *corpseName, int xp) :
maxHp(maxHp),hp(maxHp),defense(defense),corpseName(corpseName),xp(xp) {
}
We also add the xp value to the monster constructor :
Destructible.hpp :
MonsterDestructible(float maxHp, float defense, const char *corpseName, int xp);
Destructible.cpp :
MonsterDestructible::MonsterDestructible(float maxHp, float defense, const char *corpseName,int xp) :
Destructible(maxHp,defense,corpseName,xp) {
}
Now we change the MonsterDestructible::die function to handle the xp :
void MonsterDestructible::die(Actor *owner) {
// transform it into a nasty corpse! it doesn't block, can't be
// attacked and doesn't move
engine.gui->message(TCODColor::lightGrey,"%s is dead. You gain %d xp",
owner->name, xp);
engine.player->destructible->xp += xp;
Destructible::die(owner);
}
Player's XP level
Now we need a way to associate some XP level with the player's xp value. Each time the player reaches a new level, he will get some candies.
We add an xpLevel field in the PlayerAi class, a constructor to initialize it and some helper function to compute the xp value for the next xp level :
Ai.hpp :
class PlayerAi : public Ai {
public :
int xpLevel;
PlayerAi();
int getNextLevelXp();
Ai.cpp :
PlayerAi::PlayerAi() : xpLevel(1) {
}
const int LEVEL_UP_BASE=200;
const int LEVEL_UP_FACTOR=150;
int PlayerAi::getNextLevelXp() {
return LEVEL_UP_BASE + xpLevel*LEVEL_UP_FACTOR;
}
The player starts at level 1. Level 2 requires 350 xp, then every new level requires 150 more xp than the previous. We can update the xp level in the PlayerAi::update function :
void PlayerAi::update(Actor *owner) {
int levelUpXp = getNextLevelXp();
if ( owner->destructible->xp >= levelUpXp ) {
xpLevel++;
owner->destructible->xp -= levelUpXp;
engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel);
}
if ( owner->destructible && owner->destructible->isDead() ) {
return;
}
Another change in Gui::render() to display the player xp bar :
// draw the XP bar
PlayerAi *ai=(PlayerAi *)engine.player->ai;
char xpTxt[128];
sprintf(xpTxt,"XP(%d)",ai->xpLevel);
renderBar(1,5,BAR_WIDTH,xpTxt,engine.player->destructible->xp,
ai->getNextLevelXp(),
TCODColor::lightViolet,TCODColor::darkerViolet);
Now we also want the player to chose some upgrade when he reaches a new xp level. For that, we need to improve the menu class.
Distributing candies
Now we can finally ask the player what he wants to improve when he reaches a new xp level. Let's define some new menu items in Gui.hpp:
enum MenuItemCode {
NONE,
NEW_GAME,
CONTINUE,
EXIT,
CONSTITUTION,
STRENGTH,
AGILITY
};
In PlayerAi::update, we build a custom menu and use the pick function :
void PlayerAi::update(Actor *owner) {
int levelUpXp = getNextLevelXp();
if ( owner->destructible->xp >= levelUpXp ) {
xpLevel++;
owner->destructible->xp -= levelUpXp;
engine.gui->message(TCODColor::yellow,"Your battle skills grow stronger! You reached level %d",xpLevel);
engine.gui->menu.clear();
engine.gui->menu.addItem(Menu::CONSTITUTION,"Constitution (+20HP)");
engine.gui->menu.addItem(Menu::STRENGTH,"Strength (+1 attack)");
engine.gui->menu.addItem(Menu::AGILITY,"Agility (+1 defense)");
Menu::MenuItemCode menuItem=engine.gui->menu.pick(Menu::PAUSE);
Then we update the player's stats depending on what was chosen :
switch (menuItem) {
case Menu::CONSTITUTION :
owner->destructible->maxHp+=20;
owner->destructible->hp+=20;
break;
case Menu::STRENGTH :
owner->attacker->power += 1;
break;
case Menu::AGILITY :
owner->destructible->defense += 1;
break;
default:break;
}
So far, the in-game pause menu looks like the main game menu. That's not something very usual. The pause menu is generally rendered on top of the game screen.
We're going to add some displayMode parameter to the menu class to enable an alternative rendering method to get this look :
The display mode is defined in Gui.hpp :
class Menu {
public :
enum MenuItemCode {
NONE,
NEW_GAME,
CONTINUE,
EXIT
};
enum DisplayMode {
MAIN,
PAUSE
};
~Menu();
void clear();
void addItem(MenuItemCode code, const char *label);
MenuItemCode pick(DisplayMode mode=MAIN);
Now the implementation, in Gui.cpp. First we define the size of the pause menu using some constants :
const int PAUSE_MENU_WIDTH=30;
const int PAUSE_MENU_HEIGHT=15;
Menu::MenuItemCode Menu::pick(DisplayMode mode) {
Since the menu position depends on the display mode, we define two variables to store the menu position :
int selectedItem=0;
int menux,menuy;
When we render the pause menu, we want to center the menu on the screen :
if (mode == PAUSE) {
menux=engine.screenWidth/2-PAUSE_MENU_WIDTH/2;
menuy=engine.screenHeight/2-PAUSE_MENU_HEIGHT/2;
Then we use the printFrame helper function to draw an empty dialog box. We use the TCOD_BKGND_ALPHA flag so that the background is transparent. The value 70 means alpha is 70/255 = 0.27. The dialog is almost opaque and let us just see a bit of the background.
TCODConsole::root->setDefaultForeground(TCODColor(200,180,50));
TCODConsole::root->printFrame(menux,menuy,PAUSE_MENU_WIDTH,PAUSE_MENU_HEIGHT,true,
TCOD_BKGND_ALPHA(70),"menu");
Then we slightly offset the position of the menu so that it doesn't render on top of the dialog frame :
menux+=2;
menuy+=3;
If we're rendering the main menu, we just display the background image and define the menu position :
} else {
static TCODImage img("menu_background1.png");
img.blit2x(TCODConsole::root,0,0);
menux=10;
menuy=TCODConsole::root->getHeight()/3;
}
Then we just have to update the menu render code to use the menux, menuy variables :
while( !TCODConsole::isWindowClosed() ) {
int currentItem=0;
for (MenuItem **it=items.begin(); it!=items.end(); it++) {
if ( currentItem == selectedItem ) {
TCODConsole::root->setDefaultForeground(TCODColor::lighterOrange);
} else {
TCODConsole::root->setDefaultForeground(TCODColor::lightGrey);
}
TCODConsole::root->print(menux,menuy+currentItem*3,(*it)->label);
currentItem++;
}