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

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


The Third Side (Battle for the Solar System, #2)

The White Knights have had their wings clipped. Shot down and stranded on a planet in independent space, the five pilots find themselves sitting directly in the path of the Pandoran war machine as it prepares to advance The Mission. But if they can somehow survive and find a way home, they might just discover something far more worrisome than that which destroyed an empire.

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.

Comparing stats. Equipping this microchip will reduce our hp by 1 point, but will increase our min and max attacks by 1 point. Negative effects are shown in red, while positive effects are shown in blue.

IntroductionInspecting the codePurchase

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.

^ Back to top

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.

^ Back to top

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:

^ Back to top

Mobile site