Part 42: Monsters part 4: Hit probability and damages

Downloads

Source code
Executable for the map editor - exactly the same as in part 40 (Windows 32bits)
Executable for the game (Windows 32bits)

Before you try to compile the code go to the "Projects" tab on the left menu, select the "Run" settings for your kit,
and set the "Working directory" to the path of "editor\data" for the editor or "game\data" for the game.

Monsters parameters

In this part, we will study the hit probability and the damages dealt by the monsters when they attack.
First we will add a few parameters to the monsters.
These values come from The Dungeon Master Encyclopaedia but the names used here are confusing.
I preferred to used the parameters names from the source code.

		<monsters>
			[...]

			<!-- 01 Giant Scorpion -->
			<monster name = "MONSTER01">
				[...]
				<defense>55</defense>
				<health>150</health>
				<attack>150</attack>
				<dexterity>55</dexterity>
				[...]
			</monster>

			<!-- 02 Swamp Slime -->
			<monster name = "MONSTER02">
				[...]
				<defense>20</defense>
				<health>110</health>
				<attack>80</attack>
				<dexterity>20</dexterity>
				[...]
			</monster>
				

The monster's starting health is computed with this formula:

		int CMonsters::getStartingLife(int type)
		{
			int baseHealth = monstersDatas[type].baseHealth;

			return baseHealth * game.levelMultiplier() + RANDOM((baseHealth / 4) + 1);
		}
				
game.levelMultiplier() is a value that will be used in various calculations throughout the game.
Each level has a difficulty coefficient. That's an easy way to tune the global difficulty of the game.
In the original game, these coefficients were like that:

		int CGame::levelMultiplier()
		{
			static const int multipliers[] =
			{
				0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 5, 6, 6
			};
			return multipliers[currentLevel - 1];
		}
				

Front monsters' row

In the last part, every monster of a group tried to attack.
But when the group is near the player, only the monsters of the front row should attack.
The monsters in the back are too far to reach the champions with melee attacks.
The only exception where back row monsters can attack is when all the monsters of the front row are dead.
So, we will first need a function to get the row of a given monster depending on the player's position.

		// returns the row of a monster depending on the player position (0 = front, 1 = center, 2 = back)
		uint8_t CMonsterGroup2::getMonsterRow(CVec2 mapPos, EMonsterPos monsterPos)
		{
			static const uint8_t monsterRow[4][5] =
			{
				// NW NE SW SE C
				{  0, 0, 2, 2, 1},   // up
				{  0, 2, 0, 2, 1},   // left
				{  2, 2, 0, 0, 1},   // down
				{  2, 0, 2, 0, 1}    // right
			};

			int playerPosFromMonster;

			if (player.pos.x < mapPos.x)
				playerPosFromMonster = 1; // left
			else if (player.pos.x > mapPos.x)
				playerPosFromMonster = 3; // right
			else if (player.pos.y < mapPos.y)
				playerPosFromMonster = 0; // up
			else
				playerPosFromMonster = 2; // down

			return monsterRow[playerPosFromMonster][monsterPos - eMonsterPosNorthWest];
		}
				
Then in the group, before calling the updates of each monster, we will use this to check if the front row is dead
or not:

		void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
		{
			// check if there is at least one living monster in the front row of the group (or at the center)
			bool    isFrontRowRowAlive = false;

			for (int i = 0; i < 4; ++i)
			{
				if (monsters[i].pos != eMonsterPosNone &&
					getMonsterRow(mapPos, monsters[i].pos) < 2)
				{
					isFrontRowRowAlive = true;
					break;
				}
			}
				
Then when we update the monsters, we can tell them if they can attack.

		void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
		{
			[...]

			// update all the monsters of the group
			for (int i = 0; i < 4; ++i)
				if (monsters[i].pos != eMonsterPosNone)
				{
					uint8_t row = getMonsterRow(mapPos, monsters[i].pos);
					bool    isInFront = (isFrontRowRowAlive == true && row < 2) ||
					                    (isFrontRowRowAlive == false && row == 2);
					monsters[i].update(*this, mapPos, type, isInFront);
				}
				

Choosing a target

In the monster's fight() function, now that we know if we are in the front row, we can choose a target champion.

		void CMonster::fight(CVec2 mapPos, uint8_t type, bool isInFront)
		{
			if (nextAttackTimer.update() == true)
			{
				// if the monster is in the front line of the group
				if (isInFront == true)
				{
					// play attack sound
					[...]

					// show monster's animation
					[...]

					// choose a target champion
					int target = chooseTarget(mapPos);
				
This champion will need to be in the front row of the player's group.
As we didn't talk about the positions of the champions in the group, we will assume that the 2 firsts are in the
front row and the 2 last are in the back row.
So we will need a function to get the row of a champion depending on the monsters relative position.

		// returns the row of champions depending on the monsters group position (0 = front, 1 = back)
		uint8_t CMonsterGroup2::getChampionRow(CVec2 mapPos, int championNum)
		{
			static const uint8_t championRow[4][4] =
			{
				// 1  2  3  4
				{  0, 0, 1, 1},   // up
				{  0, 1, 0, 1},   // left
				{  1, 1, 0, 0},   // down
				{  1, 0, 1, 0}    // right
			};

			CVec2   localMonsterPos = player.getLocalFromPos(mapPos);
			int monsterPosFromPlayer;

			if (localMonsterPos.x < 0)
				monsterPosFromPlayer = 1; // left
			else if (localMonsterPos.x > 0)
				monsterPosFromPlayer = 3; // right
			else if (localMonsterPos.y < 0)
				monsterPosFromPlayer = 0; // up
			else
				monsterPosFromPlayer = 2; // down

			return championRow[monsterPosFromPlayer][championNum];
		}
				
Then in the chooseTarget() function we will also need to check if the front row of champions is still alive.
And we will choose a random champion in the row we will attack.

		// choose the player this monster will attack
		uint8_t CMonster::chooseTarget(CVec2 mapPos)
		{
			int rowToSearch;

			// check if there is at least one living champion in the front row
			rowToSearch = 1;

			for (int i = 0; i < 4; ++i)
			{
				if (interface.isChampionEmpty(i) == false &&
				    interface.isChampionDead(i) == false &&
				    CMonsterGroup2::getChampionRow(mapPos, i) == 0)
				{
					rowToSearch = 0;
					break;
				}
			}

			// list the possible targets
			uint8_t possibleTargets[2];
			int nbTargets = 0;

			for (int i = 0; i < 4; ++i)
			{
				if (interface.isChampionEmpty(i) == false &&
				    interface.isChampionDead(i) == false &&
				    CMonsterGroup2::getChampionRow(mapPos, i) == rowToSearch)
				{
					possibleTargets[nbTargets++] = i;
				}
			}

			// choose a target
			return possibleTargets[RANDOM(nbTargets)];
		}
				

Hit probability

You may have noticed the RANDOM() in the previous function.
As we will now use a lot of randoms, I wrote a macro to replace the rand() we had at various places and make the
code more readable:

		#define RANDOM(_maxValue)   (rand() % (_maxValue))
				
Now that we chose a target, there is a chance that it avoids the attack.
We have to compute the dexterity of both the monster and the champion this way:

		// compute monster's and champion's dexterities
		int monsterDexterity = monsters.monstersDatas[type].dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
		int championDexterity = game.characters[target].getDexterity();
				
The champion's dexterity is computed with this function.

		int CCharacter::getDexterity()
		{
			int dexterity = stats[eStatDexterity].value + RANDOM(8);

			// decrease dexterity depending on the load
			dexterity -= (dexterity / 2) * stats[eStatLoad].value / stats[eStatLoad].maxValue;

			// if we are sleeping...
			if (interface.isSleeping() == true)
				dexterity /= 2;

			// bound the value
			int min = 1 + RANDOM(8);
			int max = 100 - RANDOM(8);

			if (dexterity < min)
				dexterity = min;

			if (dexterity > max)
				dexterity = max;

			return dexterity;
		}
				
The champion's base dexterity is reduced by the load he carries and a little bit randomized.

Now that we have these values we can tell if the monster hits the champion with this test:

		// check if we hit
		if (interface.isSleeping() == true ||
		    monsterDexterity > championDexterity ||
		    RANDOM(4) == 0)
		{
				
When the party is sleeping, the monster always hit.
The RANDOM seems to something added to tune the difficulty. With this test, the monster alway have at least 25%
chance to hit it's target.
In the original game, there was also a chance to avoid the attack based on the "luck" statistic of the champion.

Damages

To compute the damages dealt by the hit, we first get the "base damages":

		// compute damages (here there should be a chance to parry)
		int damages = monsters.monstersDatas[type].attack + game.levelMultiplier() * 2 + RANDOM(16);
				
As the comment says, here there should be a test to see if the champion parries the attack.
We will see that later, as we didn't talk about the character's skills for now.

Then the damages are "randomized" with a strange formula

		// randomize damages (quite a complicated way...)
		damages /= 2;
		damages += RANDOM(damages) + RANDOM(4);
		damages += RANDOM(damages);
		damages /= 4;
		damages += RANDOM(4) + 1;

		if (RANDOM(2))
			damages -= RANDOM(damages / 2 + 1) - 1;
				
So why didn't they use a simple random.
Well this formula is just a strange way to get a non uniform probability distribution.
At the end, there should be slightly more probability to get a low damages value than a high one.
It would be good to make a simulation, by running this code a lot of times, to see the actual probabilities that we
can get.

Now that we have computed the damages, we can finally hit the champion.

		// hit the champion
		static char soundName[256];
		sprintf(soundName, "sound/Champion_Damaged_%d.wav", RANDOM(4) + 1);
		sound->play(player.pos, soundName);
		game.characters[target].hitChampion(damages);
				
At this point, in the original game there was still a lot of things to take into account: But we'll see all of this in another part.
For now we will simply display the damages that a character takes at the top of the screen, in the area where its
head or hands appears.

There are 2 types of damages displays.
Either the hands of the character are visible, so the damages appears over it's name:


Or the head of the character is displayed because we are in its sheet, then the damages appear over its face:


So the hitChampion() function simply stores the damage value and starts a timer.

		void CCharacter::hitChampion(int damages)
		{
			displayedDamages = damages;
			damagesTimer.set(90, false);
		}
				
These values will then be used in the interface to display the right image and number.