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 Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a simple roguelike —
Part 13: More monsters, more loot!

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

Introduction

Now that we can traverse floors, it's about time that we introduced some more Monsters (just variations on the Micro Mouse, to be honest) and equipment into the game. This part will focus on introducing those things, and also offering more powerful monsters and equipment as we rise through the floors.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue13 to run the code. You will see a window open displaying the player character in a small room, with just a stair case (leading up). Play the game as normal and battle the new monsters as you encounter them. New weapons, armour, and microchips can be equipped as before. Once you're finished, close the window to exit.

Inspecting the code

Starting out, we've made an update to defs.h:


#define MAX_FLOORS                13

We've set the maximum number of floors to 13. This will be the goal of the our little game and where we must reach to defeat the Mouse King (who's incharge of this whole operation!).

Returning next to structs.h, we've introduced a new struct:


typedef struct {
	char name[MAX_NAME_LENGTH];
	int chance;
} MonsterTypeChance;

MonsterTypeChance will be used to determine the chance that a monster will appear on a floor. `name` is the name of the Monster (as used with the entity factory), while `chance` will be the percent chance of the Monster being added to the floor.

Moving over to monsters.c now, we've updated addMonsters:


void addMonsters(void)
{
	int i, n, adj;

	adj = 10 * (dungeon.floor - 1);

	STRCPY(monsterTypeChance[0].name, "Micro Mouse");
	monsterTypeChance[0].chance = 100 - adj;

	STRCPY(monsterTypeChance[1].name, "Neo Mouse");
	monsterTypeChance[1].chance = 110 - adj;

	STRCPY(monsterTypeChance[2].name, "Tough Mouse");
	monsterTypeChance[2].chance = 130 - adj;

	STRCPY(monsterTypeChance[3].name, "Rabid Mouse");
	monsterTypeChance[3].chance = 150 - adj;

	STRCPY(monsterTypeChance[4].name, "Meta Mouse");
	monsterTypeChance[4].chance = 180 - adj;

	n = 8 + rand() % 16;

	for (i = 0 ; i < n ; i++)
	{
		addEntityToDungeon(initEntity(getRandomMonster()), 0);
	}
}

We've more or less completely rewritten this function. To start with, we're setting a variable called `adj` to 10, multiplied by the value of dungeon's `floor` less 1. So, if we're on floor 1, adj will be 0; floor 2, and adj will be 10, etc. We're then setting up our MonsterTypeChances. monsterTypeChance is a static array in monsters.c, of MAX_MONSTER_TYPES in length (defined as 5). We set each index in the array to a different monster type, starting with the easier monsters (Micro Mouse) and ending with the hardest (Meta Mouse). Note how the `chance` value of the MonsterTypeChance increases as we go along, moving beyond 100. These are our default values. We're subtracting `adj` from each value, however. This means that as we go higher up, the more the value of `chance` will decrease. We'll see what this does in a moment.

With our monsterTypeChance array setup, we then assign a variable called `n` a random number between 8 and 15. We're next using a for-loop from 0 to `n`, and calling addEntityToDungeon, using the result of initEntity, which is using the result of a new function called getRandomMonster.

getRandomMonster is where we're making use of our monsterTypeChance array:


static char *getRandomMonster(void)
{
	int i, r;

	r = rand() % 100;

	for (i = 0 ; i < MAX_MONSTER_TYPES ; i++)
	{
		if (monsterTypeChance[i].chance >= 0 && r < monsterTypeChance[i].chance)
		{
			return monsterTypeChance[i].name;
		}
	}

	return "Tough Mouse";
}

The idea behind this function is to grab a random monster that is appropriate to the dungeon floor we're on. We start by assigning a variable called `r` a random value between 0 and 99. We're then looping through our monsterTypeChance array and testing whether the `chance` is greater than 0 and also whether `r` is less than `chance`. If so, we'll return the name of the monster.

What that means is that we'll be looking for a monsterTypeChance whose `chance` fits our floor. So, for floor 1, the monsterTypeChance array would look like this:

  • Micro mouse: 100
  • Neo mouse: 110
  • Tough mouse: 130
  • Rabid mouse: 150
  • Meta mouse: 180
`r` cannot be greater than 99 and so we'll always return Micro Mouse. The same would be true of floor 2. However, floor 3's array would look like this:
  • Micro mouse: 80
  • Neo mouse: 90
  • Tough mouse: 110
  • Rabid mouse: 130
  • Meta mouse: 160
There's now a chance we'll return a Neo Mouse or a Tough Mouse! Floor 9 would be the point at which any Mouse is possible:
  • Micro mouse: 20
  • Neo mouse: 30
  • Tough mouse: 50
  • Rabid mouse: 70
  • Meta mouse: 100
Notice how by default we're returning a Tough Mouse at the end of the function. This is because floors higher than 9 will see maximum `chance`s being under 100, therefore leaving a gap where nothing is returned. Returning a Tough Mouse fixes this issue.

This is just one of many ways in which we could've handled our random monster creation.

Moving on, we'll quickly look at the new Monsters we've added. Starting with NeoMouse (note - these functions largely speak for themselves, so we won't linger):


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

	m = createMonster(e);
	m->hp = m->maxHP = 4 + rand() % 14;
	m->defence = 8;
	m->minAttack = 6;
	m->maxAttack = 9;
	m->visRange = 24;
	m->xp = 35;

	STRCPY(e->name, "Neo Mouse");
	STRCPY(e->description, "The next generation of mouse. Exciting times for pet owners.");
	e->texture = getAtlasImage("gfx/entities/neoMouse.png", 1);
}

A Neo Mouse is tougher than a standard Micro Mouse and is worth 35 xp.

initTouchMouse is next:


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

	m = createMonster(e);
	m->hp = m->maxHP = 20 + rand() % 30;
	m->defence = 12;
	m->minAttack = 1;
	m->maxAttack = 16;
	m->visRange = 32;
	m->xp = 50;

	STRCPY(e->name, "Tough Mouse");
	STRCPY(e->description, "A mouse that's completely ripped. Apparently spends its life at the gym.");
	e->texture = getAtlasImage("gfx/entities/toughMouse.png", 1);
}

Tougher still, and with a higher sight range (visRange). A Tough Mouse is worth 50 xp.

initRabidMouse comes next:


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

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

	STRCPY(e->name, "Rabid Mouse");
	STRCPY(e->description, "A mouse that's foaming at the mouth. Either it ate a bath bomb or it has rabies.");
	e->texture = getAtlasImage("gfx/entities/rabidMouse.png", 1);
}

In some aspects more difficult, in others, not so. We have plans for Rapid Mouse in a future part. This guy is worth 65 xp.

Finally, initMetaMouse:


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

	m = createMonster(e);
	m->hp = m->maxHP = 20 + rand() % 35;
	m->defence = 1;
	m->minAttack = 12;
	m->maxAttack = 16;
	m->visRange = 12;
	m->xp = 105;

	STRCPY(e->name, "Meta Mouse");
	STRCPY(e->description, "A mouse but also not a mouse. One could say it's currently in a superposition.");
	e->texture = getAtlasImage("gfx/entities/metaMouse.png", 1);
}

105 xp! The Meta Mouse is worth quite a lot! This is, again, because we have plans for him in a future part.

Now, over to armour.c, where we've introduced some new equipment. Starting with addArmour:


void addArmour(void)
{
	if (rand() % 2 == 0)
	{
		addEntityToDungeon(initEntity("Biker Jacket"), 0);
	}
	else
	{
		addEntityToDungeon(initEntity("Bulletproof Vest"), 0);
	}
}

For now, we'll always add armour to our dungeon. There's a 50/50 chance that we'll either add a Biker Jacket or a Bulletproof Vest.

The Bulletproof Vest is a new item, that is created via initBulletproofVest:


void initBulletproofVest(Entity *e)
{
	Equipment *eq;

	eq = createArmour(e);

	STRCPY(e->name, "Bulletproof Vest");
	STRCPY(e->description, "An item of body armor that helps absorb the impact and reduce or stop penetration to the torso.");
	e->texture = getAtlasImage("gfx/entities/bulletproofVest.png", 1);

	eq->defence = 9;
	eq->maxAttack = -1;

	applyBonus(e, eq);
}

The function is largely like initBikerJacket. Notice that the vest reduces our maxAttack by 1. This is just to demonstrate that armour doesn't have to always have a positive impact. Notice, too, that we're calling a new function at the end of initBulletproofVest named applyBonus, and passing in the entity (`e`) and the equipment (`eq`):

applyBonus is a simple function to understand:


static void applyBonus(Entity *e, Equipment *eq)
{
	char text[8];
	int bonus, chance;

	chance = MAX_FLOORS - dungeon.floor;

	if (rand() % chance == 0)
	{
		bonus = 1 + (rand() % dungeon.floor);

		eq->defence += bonus;

		sprintf(text, " +%d", bonus);

		strcat(e->name, text);

		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "%s: hp=%d, minAttack=%d, maxAttack=%d, defence=%d", e->name, eq->hp, eq->minAttack, eq->maxAttack, eq->defence);
	}
}

The idea behind this function is to apply a bonus to our armour, by increasing the level of `defence` on offer. We want armour with bonuses to be found at higher dungeon levels, and so we subtract the dungeon's `floor` value from MAX_FLOORS (13), and assign it to `chance`. We then use the value of `chance` against rand and check if the result is 0. This means that it will be rare to find armour with a bonus at level 1, but more common at level 9.

After we've decided that we want to add a bonus to our armoud, we'll determine the bonus value to add. We take a further random of the dungeon `floor`, plus 1, and assign it to a variable called `bonus`. This means that the higher dungeon floor we're at, the greater the chance of getting a larger bonus. It does not mean, however, that finding armour with a +10 bonus is a sure thing, however. We take the value of `bonus` and add it to the armour's (`eq`) defence. We then use sprintf and a variable called `text`, to create a string reflecting the bonus (for example, +1, +5, +8, etc). Finally, we use strcat to add the bonus text to the entity's `name`.

This means that our Biker Jacket with a +2 bonus will now be called "Biker Jacket +2". Note that this applyBonus function has also been added to initCrowbar. It's not exclusive to initStunBaton.

Moving over the weapons.c now, we've made quite similar updates. Starting with addWeapons:


void addWeapons(void)
{
	if (rand() % 2 == 0)
	{
		addEntityToDungeon(initEntity("Crowbar"), 0);
	}
	else
	{
		addEntityToDungeon(initEntity("Stun Baton"), 0);
	}
}

There's now a 50/50 chance we'll add a Crowbar or Stun Baton.

We're created our stun batons in initStunBaton:


void initStunBaton(Entity *e)
{
	Equipment *eq;

	eq = createWeapon(e);

	STRCPY(e->name, "Stun Baton");
	STRCPY(e->description, "A weapon that utilizes an electric shock to destroy a target. Hurts loads.");
	e->texture = getAtlasImage("gfx/entities/stunBaton.png", 1);

	eq->minAttack = 1;
	eq->maxAttack = 9;
	eq->defence = -1;

	applyBonus(e, eq);
}

Much large initCrowbar, we're setting all the weapon's attributes. Notice how the stun baton reduces our `defence` by 1 point, again just to demonstrate that weapons can have negative impacts on stats. We're also calling applyBonus at the end of the function. This is not quite the same function as was in armour.c, however, as we're altering different stats:


static void applyBonus(Entity *e, Equipment *eq)
{
	char text[8];
	int bonus, chance;

	chance = MAX_FLOORS - dungeon.floor;

	if (rand() % chance == 0)
	{
		bonus = 1 + (rand() % dungeon.floor);

		eq->minAttack += bonus;
		eq->maxAttack += bonus;

		sprintf(text, " +%d", bonus);

		strcat(e->name, text);

		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "%s: hp=%d, minAttack=%d, maxAttack=%d, defence=%d", e->name, eq->hp, eq->minAttack, eq->maxAttack, eq->defence);
	}
}

Rather than update the weapon's defence value, we're increasing the minAttack and maxAttack values. Otherwise, the two functions are the same.

Now, let's turn to microchips.c, where we've made some updates to initMicrochip:


void initMicrochip(Entity *e)
{
	Equipment *eq;
	int i, n, amount;

	eq = createMicrochip(e);

	STRCPY(e->name, "Microchip");
	STRCPY(e->description, "A tiny wafer of semiconducting material. Randomly affects stats.");
	e->texture = getAtlasImage("gfx/entities/microchip.png", 1);

	n = dungeon.floor * 2;

	for (i = 0 ; i < n ; i++)
	{
		amount = rand() % 2 == 0 ? -1 : 1;

		switch (rand() % 4)
		{
			case 0:
				eq->hp += amount;
				break;

			case 1:
				eq->minAttack += amount;
				break;

			case 2:
				eq->maxAttack += amount;
				break;

			default:
				eq->defence += amount;
				break;
		}
	}

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "%s: hp=%d, minAttack=%d, maxAttack=%d, defence=%d", e->name, eq->hp, eq->minAttack, eq->maxAttack, eq->defence);
}

We've changed now the microchip's random attribute assignment works. We're now assigning a variable called `n` the value of dungeon's `floor` multiplied by 2. This means that the higher the floor, the higher the value of `n`. We're then using a for-loop from 0 to `n` to randomly affect the microchip's variable stats. At each step of the loop, we're assigning a variable called `amount` a value of either 1 or -1. We're then using a switch statement against a random value of 4. Depending on the outcome, we'll add amount to the microchip's `hp`, minAttack, maxAttack, or `defence`. Overall, this means that as we move higher up the dungeon, the microchip's effects (both positive and negative!) will become more extreme. It will be possible, for example, to find a microchip that offers +24 `defence`, or even -24 maxAttack.

That's all our monster and equipment changes done. We can now look through all the other misc. updates we've added.

Starting with dungeon.c, we've updated createDungeon:


static void createDungeon(void)
{
	// snipped

	generateMap();

	if (dungeon.floor > 0 && dungeon.floor < MAX_FLOORS)
	{
		addMonsters();

		addItems();

		addWeapons();

		addArmour();

		addMicrochips();
	}

	addStairs(oldFloor);

	updateFogOfWar();

	dungeon.currentEntity = dungeon.player;

	sprintf(text, "Entering floor #%d", dungeon.floor);

	addHudMessage(HUD_MSG_NORMAL, text);
}

We're now only adding monsters and items to floors 1 through 12. Floors 0 and 13 will be free of those.

Turning to map.c, we've made a similar update in generateMap:


void generateMap(void)
{
	if (dungeon.floor > 0 && dungeon.floor < MAX_FLOORS)
	{
		randomWalk();

		tidyWalls();
	}
	else
	{
		generateEmptyMap();
	}
}

Again both floors 0 and 13 will be empty maps, with only 1-12 featuring mazes.

Over to items.c, where we've tweaked addItems:


void addItems(void)
{
	int i, n;

	if (rand() % 2 == 0)
	{
		addEntityToDungeon(initEntity("Key"), 0);
	}

	n = rand() % 3;

	for (i = 0 ; i < n ; i++)
	{
		addEntityToDungeon(initEntity("Health Pack"), 0);
	}
}

There is now a 50/50 chance a Key will be added to the floor, while there may be up to 2 health packs to be found (or perhaps none at all!).

We've also updated the `touch` function, renaming it from `touch` to touchItem:


void touchItem(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);
	}
}

The function is now public (no longer static), and is used in armour.c, weapons.c, and microchips.c. Since these function were all doing the same thing, making them call a global function centralizes the logic and stops us from repeating code.

Finally, we've updated inventory.c, to fix a bug when we equip an item:


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:
				if (game.equipment[selectedEquipmentSlot] != NULL)
				{
					addToInventory(game.equipment[selectedEquipmentSlot]);
				}
				game.equipment[selectedEquipmentSlot] = selectedInventoryItem;
				removeFromInventory(selectedInventoryItem);
				selectedInventoryItem = NULL;
				selectedEquipmentSlot = -1;

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

			default:
				break;
		}
	}
}

When using an ET_WEAPON, ET_ARMOUR, or ET_MICROCHIP, we're now testing to see if an entity has already been set at game's equipment array at index selectedEquipmentSlot. If so, we'll call addToInventory, passing over the entity at the slot index to put it back into the inventory, and then put the new one in its place. Not doing so means that the existing item would be lost for good (and also cause a memory leak).

The last thing we need to do is turn to entityFactory.c, and update initEntityFactory:


void initEntityFactory(void)
{
	memset(&head, 0, sizeof(InitFunc));
	tail = &head;

	addInitFunc("Player", initPlayer);
	addInitFunc("Micro Mouse", initMicroMouse);
	addInitFunc("Neo Mouse", initNeoMouse);
	addInitFunc("Tough Mouse", initToughMouse);
	addInitFunc("Rabid Mouse", initRabidMouse);
	addInitFunc("Meta Mouse", initMetaMouse);
	addInitFunc("Key", initKey);
	addInitFunc("Health Pack", initHealthPack);
	addInitFunc("Crowbar", initCrowbar);
	addInitFunc("Stun Baton", initStunBaton);
	addInitFunc("Biker Jacket", initBikerJacket);
	addInitFunc("Bulletproof Vest", initBulletproofVest);
	addInitFunc("Microchip", initMicrochip);
	addInitFunc("Stairs (Up)", initStairsUp);
	addInitFunc("Stairs (Down)", initStairsDown);
}

Adding in all the init functions for our new armour, weapons, and monsters means that they will be available to our dungeon.

And that's another part finished! Our game looks to almost be done. There are several more features that we want to put in, however. For example, what are the Keys for that we've been collecting? Keys open things, such as doors. In the next part, we'll be adding in doors and also placing the weapons, armour, and microchips behind them and in alcoves, rather than scatter them about the dungeon floor, for the player to trip over ...

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