PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Android Games

DDDDD
Number Blocks
Match 3 Warriors

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
2D run and gun
Roguelike
Medals (Achievements)
2D turn-based strategy game
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

android (3)
battle-for-the-solar-system (10)
blob-wars (10)
brexit (1)
code (6)
edgar (9)
games (43)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (17)
water-closet (4)

Books


Project Starfighter

In his fight back against the ruthless Wade-Ellen Asset Protection Corporation, pilot Chris Bainfield finds himself teaming up with the most unlikely of allies - a sentient starfighter known as Athena.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a simple roguelike —
Part 10: Comparing stats

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

Introduction

Something that many RPGs do these days is show you a comparison of stats for the equipment you're using against the equipment you wish to use. In this part, we'll be adding such a feature to the inventory screen, so we can see what benefits, if any, adding armour, weapons, and microchips have on our stats.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue10 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. Pick up the items as you find them. Equip and remove the items as you please. Notice how the comparison is displayed when clicking on an item in the inventory and also when selecting it in the equipment box. When clicking on the item in the equipment box, we're displaying the effect of removing the item, rather than adding it. Once you're finished, close the window to exit.

Inspecting the code

This part is quite short, as comparing the item stats is pretty straightforward; we've only had to make changes to two files to do so.

We'll start with player.c, where we've updated two functions. initPlayer comes first:


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

	m = malloc(sizeof(Monster));
	memset(m, 0, sizeof(Monster));

	STRCPY(e->name, "Player");
	STRCPY(e->description, "A brave lab technician, hunting for escaped mice.");
	e->type = ET_PLAYER;
	e->texture = getAtlasImage("gfx/entities/girl.png", 1);
	e->data = m;
	e->solid = 1;

	dungeon.player = e;

	updatePlayerAttributes(m, -1);

	m->hp = m->maxHP;

	moveDelay = 0;
}

We've made a change to the updatePlayerAttributes call, since the function's signature has changed. It now takes two arguments. We're passing across the player's Monster struct, as well as -1.

If we jump straight to updatePlayerAttributes now, we can see what that means:


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

	m->maxHP = 25;
	m->defence = 4;
	m->minAttack = 1;
	m->maxAttack = 4;

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

As already stated, updatePlayerAttributes now takes two arguments. `m` is the Monster we want to work with for updating the stats, while ignoreEquipmentSlot is the index number of the equipment slot we want to ignore when adding up the equipment stats. Other than not explictly working with dungeon's player's monster data and also testing in our equipment stats for-loop that `i` is not equal to ignoreEquipmentSlot before using the Equipment's data, the rest of the code remains the same.

What this means for us is that we're no longer tied to using the player's monster object, and can pass through any monster object we wish. Being able to specify an equipment slot to ignore also helps us when it comes to removing items, as we'll see.

Now, let's turn to inventory.c, where we've made further use of this new function. Starting with `use`:


static void use(void)
{
	Item *i;

	if (selectedInventoryItem != NULL)
	{
		switch (selectedInventoryItem->type)
		{
			case ET_ITEM:
				i = (Item*) selectedInventoryItem->data;

				if (i->use != NULL && i->use())
				{
					trash();
				}
				break;

			case ET_WEAPON:
			case ET_ARMOUR:
			case ET_MICROCHIP:
				game.equipment[selectedEquipmentSlot] = selectedInventoryItem;
				removeFromInventory(selectedInventoryItem);
				selectedInventoryItem = NULL;
				selectedEquipmentSlot = -1;

				updatePlayerAttributes(dungeon.player->data, -1);
				break;

			default:
				break;
		}
	}
}

We've made two small changes here - when using an ET_WEAPON, ET_ARMOUR, or ET_MICROCHIP we're setting the selectedEquipmentSlot to -1 and also calling updatePlayerAttributes with the new parameters. In this case, we're passing over our player's Monster data and -1, to say that we wish to sum up all the current equipment for the player (obviously we can't access slot -1). So, the same as before. The reason for setting selectedEquipmentSlot to -1 is to prevent our comparison code from immediately displaying the effect of removing the item we added (which would look a bit odd).

removeItem has seen a similar tweak:


static void removeItem(void)
{
	if (selectedEquipmentSlot != -1 && game.equipment[selectedEquipmentSlot] != NULL)
	{
		addToInventory(game.equipment[selectedEquipmentSlot]);

		game.equipment[selectedEquipmentSlot] = NULL;

		selectedEquipmentSlot = -1;

		updatePlayerAttributes(dungeon.player->data, -1);
	}
}

Here, too, we're calling updatePlayerAttributes with the player's monster data and -1, to sum up the player's monster data as before.

Now we come to drawStats. We've made a number of changes to this function:


static void drawStats(void)
{
	Monster *m1, m2;
	Equipment *eq;
	char text[MAX_DESCRIPTION_LENGTH];
	int compare;

	compare = 0;

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

	eq = NULL;

	if (selectedInventoryItem != NULL && (selectedInventoryItem->type == ET_WEAPON || selectedInventoryItem->type == ET_ARMOUR || selectedInventoryItem->type == ET_MICROCHIP))
	{
		eq = (Equipment*) selectedInventoryItem->data;

		compare = 1;
	}
	else if (selectedEquipmentSlot != -1 && game.equipment[selectedEquipmentSlot] != NULL)
	{
		compare = 1;
	}

	memcpy(&m2, m1, sizeof(Monster));

	if (compare)
	{
		updatePlayerAttributes(&m2, selectedEquipmentSlot);

		if (eq != NULL)
		{
			m2.maxHP += eq->hp;
			m2.minAttack += eq->minAttack;
			m2.maxAttack += eq->maxAttack;
			m2.defence += eq->defence;

			m2.maxHP = MAX(1, m2.maxHP);
			m2.hp = MIN(m2.hp, m2.maxHP);
			m2.minAttack = MAX(1, m2.minAttack);
			m2.maxAttack = MAX(1, m2.maxAttack);
			m2.defence = MAX(0, m2.defence);
		}
	}

	app.fontScale = 1.8;

	sprintf(text, "HP: %d / %d", m2.hp, m2.maxHP);
	drawDiffValue(text, 1100, 100, m2.maxHP, m1->maxHP);

	sprintf(text, "Min attack: %d", m2.minAttack);
	drawDiffValue(text, 1100, 150, m2.minAttack, m1->minAttack);

	sprintf(text, "Max attack: %d", m2.maxAttack);
	drawDiffValue(text, 1100, 200, m2.maxAttack, m1->maxAttack);

	sprintf(text, "Defence: %d", m2.defence);
	drawDiffValue(text, 1100, 250, m2.defence, m1->defence);

	app.fontScale = 1.0;
}

The function has been been changed quite a lot, to now support comparing stats. We'll work through this bit by bit.

We're first setting a variable called `compare` to 0, to tell our function we're not comparing stats. Next, we extract the player's Monster data and assign it to a variable called `m1`. We're also setting an Equipment pointer called `eq` to NULL.

With that done, we're testing whether we have an inventory item selected, by checking if selectedInventoryItem is not NULL. If it's not and it's `type` is ET_WEAPON, ET_ARMOUR, or ET_MICROCHIP, we're going to extract the Equipment data from it and assign it to `eq`. We're also going to set the `compare` variable to 1, since we have something we want to compare.

If we don't have an inventory item selected, but do have an equipment slot selected (selectedEquipmentSlot is not -1 and the entity at game's equipment at slot selectedEquipmentSlot is not NULL), we'll be setting `compare` to 1. Notice how we're using an else if statement here; we're currently not comparing new equipment to existing equipment. This could be changed in the future (or perhaps left as an exercise for the reader..!).

With that done, we're then using memcpy to copy all of `m1`'s data (the player's Monster data) in a variable called `m2` (another Monster object). We now have variables called `m1` and `m2`, that both contain all the player's Monster data. We're then checking if `compare` is set. If so, we're calling updatePlayerAttributes and passing over `m2` and selectedEquipmentSlot. This will mean that we'll be ignoring the equipment currently set at selectedEquipmentSlot. If we have an item of equipment highlighted, `m2`'s stats will be set WITHOUT the item.

Next, we're testing if `eq` (the Equipment) is not NULL. If it's not, we'll be manually adding the equipment's data to `m2` in the same way it happens in updatePlayerAttributes. Since updatePlayerAttributes operates on the player's currently equipped items, this step is needed to show what the new item's stats will do for us.

We now have two Monster variables: `m1` and `m2`. `m1` contains the player's data, with their current equipment set. `m2`, however, contains the data for our player with either the new item added in or an item removed. We therefore have two Monsters that we can compare to one another. We then start printing out the player's stats, using sprintf and `m2`'s data (the new data). For each one of the stats, we're calling a new function named drawDiffValue, passing over the text string of `text`, the x and y position we want to draw the text, `m2`'s stat (such as maxHP) and `m1`'s stat. We're using `m2`'s data for the sprintf call because we want to display the new data, rather than the old.

If that sounds a bit confusing, consider this - we're just comparing old (m1) with new (m2), with and without the changes in equipment.

The next function is drawDiffValue. It is very easy to understand:


static void drawDiffValue(char *stat, int x, int y, int new, int old)
{
	int diff;
	char text[32];

	diff = new - old;

	if (diff < 0)
	{
		sprintf(text, "%s (%d)", stat, diff);
		drawText(text, x, y, 255, 0, 0, TEXT_ALIGN_LEFT, 0);
	}
	else if (diff > 0)
	{
		sprintf(text, "%s (+%d)", stat, diff);
		drawText(text, x, y, 64, 200, 255, TEXT_ALIGN_LEFT, 0);
	}
	else
	{
		drawText(stat, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	}
}

As we've seen already, it takes five parameters - `stat`, the text string; `x`, the text's horizontal position; `y`, the text's vertical position; `new`, the value of the new stat (`m2`); and `old`, the value of the old stat (`m1`). The idea behind the function is simply to see if the new stat is better than the old stat, and render the approrpiate string.

We're first calculating the difference between new and old, by subtracting `old` from `new` and assigning it to a variable called `diff`. Next, we're testing the value of `diff`. If it's less than 0, we'll consider this a bad stat. We're using sprintf to draw the `stat` text, along with the difference in brackets afterwards (using a char array called `text`). With that done, we're calling drawText, passing over `text`, `x`, `y`, and a red RGB value. In short, if the value is negative, we'll print the stat line in red.

We're then testing if `diff` is greater than 0. If so, we're rendering the line in a light blue, and with the text line showing a positive number (note the addition of the plus symbol in backets).

Finally, if `diff` is 0, we're just calling drawText and passing through `stat`, rendering the text in white.

And there we have it. Some short code for comparing the before and after stats of adding or removing equipment. A small change, but a very welcome one, as players can now immediately see what that brand new microchip they found is going to do. Now that our inventory is more or less done, it's time to return to the game proper. We'll be expanding the size of our dungeon in the next part and adding in many more mice to hunt down. We'll also be introducing xp and leveling, with enough mice to take the player from level 1 to level 2.

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:

Mobile site