« Back to tutorial listing

— Creating a simple roguelike —
Part 11: XP and levelling

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

There are plenty of things that we can do in our roguelike right now, such as fighting monsters, collecting items, and equipping weapons and armour. Another important aspect of roguelikes is the ability to earn experience points (XP) from defeating enemies, and leveling up. In this part, we'll look at how we can achieve that. We've made the map much larger now and also added in many more mice.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue11 to run the code. You will see a window open like the one above, showing our main character in a dungeon environment. Use the same controls as before to move around and open the inventory. Play the game as before, battling the mice, and picking up and using equipment. For each mouse you defeat, you will earn 10 experience points. You will also earn 1 experience point for each item you collect in the dungeon. Once you earn 100 experience points, you will advance to level 2, meaning your HP will increase, as well as your attack and defence stats. Once you're finished, close the window to exit.

Inspecting the code

As stated before, we've increased the size of our map. This is done in defs.h:


#define MAP_WIDTH                 85
#define MAP_HEIGHT                48

All that's need to do is increase the values of MAP_WIDTH and MAP_HEIGHT, to create a larger dungeon floor. The map generation will take care of the rest.

Onto structs.h, we've made a tweak to the Monster struct:


typedef struct {
	int hp, maxHP;
	int minAttack;
	int maxAttack;
	int defence;
	int alert;
	int visRange;
	int level;
	int xp;
	int requiredXP;
	SDL_Point patrolDest;
} Monster;

We've added in three new fields. `level` is the level of the Monster. This really only applies to the player. `xp` is the amount of experience points the Monster has. In the case of the player, it will be cumulative amount of XP, while for the Micro Mice, etc. it will be how much they are worth and what will be awarded to the player upon their defeat. requiredXP is the amount of XP required to advance to the next level. Again, this only applies to the player.

Moving onto monsters.c now, we've made a couple of minor tweaks. Starting with addMonsters:


void addMonsters(void)
{
	int i;

	for (i = 0 ; i < 16 ; i++)
	{
		addEntityToDungeon(initEntity("Micro Mouse"));
	}
}

We're now adding in 16 Micro Mice to the dungeon.

The initMicroMouse function has also been updated:


void initMicroMouse(Entity *e)
{
	Monster *m;

	m = createMonster(e);
	m->hp = m->maxHP = 1 + rand() % 4;
	m->defence = 1;
	m->minAttack = 1;
	m->maxAttack = 3;
	m->visRange = 12;
	m->xp = 10;

	STRCPY(e->name, "Micro Mouse");
	STRCPY(e->description, "A white mouse with a microchip and antenna embedded in its head.");
	e->texture = getAtlasImage("gfx/entities/microMouse.png", 1);
}

We're now setting the Monster's `xp` to a value of 10. This will be the number of experience points each Micro Mouse will award us when we defeat it.

Sticking with small changes, if we turn next to items.c, we can see we've made an update to the `touch` function:


static void touch(Entity *self, Entity *other)
{
	char text[MAX_DESCRIPTION_LENGTH];

	if (other == dungeon.player)
	{
		addToInventory(self);

		sprintf(text, "Picked up a %s.", self->name);

		addHudMessage(HUD_MSG_NORMAL, text);

		addPlayerXP(1);
	}
}

We're now adding a function named addPlayerXP and passing over 1. We'll get to the function in a little later on, but for now it means that the player will be awarded 1 experience point (xp) for collecting the item. Note that this line has been added to the `touch` functions in armour.c, weapons.c, and microchips.c.

Changes to combat.c come next. We've made another small tweak to doMeleeAttack:


void doMeleeAttack(Entity *attacker, Entity *target)
{
	// snipped

	addHudMessage(type, combatMessage);

	if (target->dead == 1 && attacker == dungeon.player)
	{
		addPlayerXP(tarMonster->xp);
	}
}

After we've printed the combat message, we're then testing to see if `target` (the monster that was attacked) has had its `dead` flag set to 1. If so, and `attacker` was the player, we'll call addPlayerXP and pass over tarMonster's `xp`. Remember that tarMonster is the Monster data of the target entity. So, this would be the 10 xp of the Micro Mouse in this part.

Now that we've covered off the small calls, we can turn to the more interesting parts. Shifting over to player.c, we've first made changes to updatePlayerAttributes:


void updatePlayerAttributes(Monster *m, int ignoreEquipmentSlot)
{
	int i, lvl;
	Equipment *eq;

	m->requiredXP = 0;

	m->level = 0;

	do {
		m->level++;
		m->requiredXP += (m->level * 100) + ((m->level - 1) * 50);
	} while (m->xp >= m->requiredXP);

	lvl = m->level - 1;

	m->maxHP = 25 + (lvl * 10);
	m->defence = 4 + lvl;
	m->minAttack = 1 + lvl;
	m->maxAttack = 4 + (lvl * 1.5);

	for (i = 0 ; i < EQUIP_MAX ; i++)
	{
		if (game.equipment[i] != NULL && i != ignoreEquipmentSlot)
		{
			eq = (Equipment*) game.equipment[i]->data;

			m->maxHP += eq->hp;
			m->minAttack += eq->minAttack;
			m->maxAttack += eq->maxAttack;
			m->defence += eq->defence;
		}
	}

	m->maxHP = MAX(m->maxHP, 1);
	m->hp = MIN(m->hp, m->maxHP);
	m->minAttack = MAX(1, m->minAttack);
	m->maxAttack = MAX(1, m->maxAttack);
	m->defence = MAX(m->defence, 0);
}

This function has been responsible for updating the player's stats when they add or remove a piece of equipment. We're now going to factor in the player's xp and level, to increase their stats accordingly.

We're first zeroing both the player Monster's requiredXP and `level`. We're then setting up a do-loop, which will continue to run while the player's `xp` is equal to or greater than requiredXP. For each cycle of our do-loop, we're increasing the player's `level`. We're then performing a calculation to determine the amount of xp that is needed to advance a level. The equation makes use of the player's current level and their previous level (note: this equation is nothing special, just something random made up for this tutorial). We're assigning the result of the equation to the player Monster's requiredXP. So, the first time it runs, the player will have 0 xp. The loop will run once and set them to level 1 and require 100 xp to advance.

With that done, we then come to working out the stats. We're going to factor in the player's `level` as we do so. First, we assign the player's `level` less 1 to a variable called `lvl`. We're then using the value of `lvl` along with the player's maxHP, `defence`, minAttack, and maxAttack. The player's maxHP will increase by 10 points for each level earned, while the `defence` and minAttack stats will add 1 point. maxAttack will gain 1.5 points for each level. Note that since `lvl` is the player's level less 1, no bonus stats will be awarded to the starting player (since `lvl` will be 0).

The rest of the function remains the same. Putting the level stat calculation here gives us a central place at which to work out all our player's attributes scores, while factoring in their level and equipment. Pretty neat!

We've also added in a new function - addPlayerXP:


void addPlayerXP(int amount)
{
	Monster *m;
	int oldLevel;
	char text[MAX_DESCRIPTION_LENGTH];

	m = (Monster*) dungeon.player->data;

	oldLevel = m->level;

	m->xp += amount;

	updatePlayerAttributes(m, -1);

	if (m->level > oldLevel)
	{
		sprintf(text, "You are now level #%d! Your stats have increased!", m->level);

		addHudMessage(HUD_MSG_GOOD, text);
	}
}

The purpose of this function is to update the player's `xp` and also raise their `level` and update their stats, as needed. The function takes a single argument. `amount` is the amount of xp we want to add to the player. We start by extracting the Monster data from the player, and then assigning the Monster's `level` to a variable named `oldLevel`. We then add `amount` to the Monster's `xp` and call updatePlayerAttributes. With that done, we test the Monster's `level`. If it's now higher than `oldLevel`, the player has levelled up. We'll print a hud message to reflect this.

A simple function, but one that does the job of increasing our xp, checking if we've gone up a level, and updating our stats all in one.

With all that done, we just need to visit dungeon.c and update createDungeon:


static void createDungeon(void)
{
	initEntities();

	initEntity("Player");

	if (1)
	{
		generateMap();
	}
	else
	{
		generateEmptyMap();
	}

	addMonsters();

	addItems();

	addWeapons();

	addArmour();

	addMicrochips();

	updateFogOfWar();

	dungeon.currentEntity = dungeon.player;
}

We're now calling all our population functions: addMonsters, addItems, addWeapons, addArmour, and addMicrochips. This will ensure all monsters and items are added to the dungeon. We now have lots of monsters to battle and items to pickup and use.

Before we finish this part, we should take a look at some of the bugs that we've also fixed; these bugs have made themselves more prominent, now that we have more monsters on the map. Starting with monsters.c, we've fixed a bug in patrol:


static void patrol(Entity *e, Monster *m)
{
	int dx, dy;
	Entity *other;

	if (dungeon.map[m->patrolDest.x][m->patrolDest.y].tile >= TILE_GROUND && dungeon.map[m->patrolDest.x][m->patrolDest.y].tile < TILE_WALL)
	{
		other = getEntityAt(m->patrolDest.x, m->patrolDest.y);

		if (other == NULL || other == dungeon.player || !other->solid)
		{
			createAStarRoute(e, m->patrolDest.x, m->patrolDest.y, &dx, &dy);

			moveEntity(e, dx, dy);

			if (e->x == m->patrolDest.x && e->y == m->patrolDest.y)
			{
				m->patrolDest.x = rand() % MAP_WIDTH;

				m->patrolDest.y = rand() % MAP_HEIGHT;
			}
		}
	}
	else
	{
		m->patrolDest.x = rand() % MAP_WIDTH;

		m->patrolDest.y = rand() % MAP_HEIGHT;
	}
}

We've added in a check for our patrol code, to ensure that the square the Monster is attempting to move into is clear or contains the player. This check is needed because Monsters can head towards the last place they saw the player and may then attack another Monster by mistake, if they were standing right now to it to begin with. Testing that the path is clear before the move fixes this issue.

We've also fixed another bug entities.c, in moveEntity:


void moveEntity(Entity *e, int dx, int dy)
{
	int x, y;

	if (dx != 0 || dy != 0)
	{
		x = e->x + dx;
		y = e->y + dy;

		if (dx < 0)
		{
			e->facing = FACING_LEFT;
		}
		else if (dx > 0)
		{
			e->facing = FACING_RIGHT;
		}

		if (x >= 0 && y >= 0 && x < MAP_WIDTH && y < MAP_HEIGHT && dungeon.map[x][y].tile >= TILE_GROUND && dungeon.map[x][y].tile < TILE_WALL && !isBlocked(e, x, y))
		{
			e->x = x;
			e->y = y;
		}
	}
}

Before moving, we're testing to see that `dx` and `dy` are not both zero. This is to fix a bug where it was possible for Monsters to hit themselves, which isn't something anyone wants to be doing ...

And that's it for leveling up! It wasn't too hard to add in and we've got a system that increases the amount of xp that is required as we progress. Our stats also increase in tandem with our level, which is something we also want.

Now, what we need next is to be able to move up and down floors. In our next part, we'll introduce stairs.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase:

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Desktop site