Part 44: Attacks Part 2: killing monsters with melee weapons

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.

Cleaning up the row functions

In the previous parts, we used the getMonsterRow() and getChampionRow() to get the row of a monster or a champion
depending on the position of the enemy - either the party or the monsters' group.
For clarity I moved getChampionRow to CPlayer instead of CMonsterGroup2.

		// 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];
		}
		
		// returns the row of champions depending on the monsters group position (0 = front, 1 = back)
		uint8_t CPlayer::getChampionRow(CVec2 monsterPos, 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 = getLocalFromPos(monsterPos);
			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];
		}
				
We also needed to test if the monsters of the front row in a group are all dead so that the back row could hit the
party.
And obviously the same thing needs to be tested for the front row of the party.
So to simplify this, I wrote 2 other functions that tells if a given champion or monster is in contact with the
enemy taking this into account:

		// returns if a monster is in the fron row of its group, taking into account the dead ones
		bool CMonsterGroup2::isMonsterInFront(CVec2 mapPos, EMonsterPos monsterPos)
		{
			bool    isFrontRowRowAlive = false;

			for (int i = 0; i < 4; ++i)
			{
				if (mMonsters[i].pos != eMonsterPosNone &&
					getMonsterRow(mapPos, mMonsters[i].pos) < 2)
				{
					isFrontRowRowAlive = true;
					break;
				}
			}

			uint8_t row = getMonsterRow(mapPos, monsterPos);
			return (isFrontRowRowAlive == true && row < 2) ||
				   (isFrontRowRowAlive == false && row == 2);
		}

		// returns if a champion is in the front row, taking into account the dead ones.
		bool CPlayer::isChampionInFront(CVec2 monsterPos, int championNum)
		{
			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 &&
					player.getChampionRow(monsterPos, i) == 0)
				{
					rowToSearch = 0;
					break;
				}
			}

			return (player.getChampionRow(monsterPos, championNum) == rowToSearch);
		}
				
Now this simplifies the functions that used the rows. I.e. the CMonster::chooseTarget() function:

		// choose the player this monster will attack
		uint8_t CMonster::chooseTarget(CVec2 mapPos)
		{
			// 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 &&
					player.isChampionInFront(mapPos, i) == true)
				{
					possibleTargets[nbTargets++] = i;
				}
			}

			// choose a target
			return possibleTargets[RANDOM(nbTargets)];
		}
				
And as I said that I should have used these function when the party is hitting a wall, now I did it:

		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]
			
			// wall
			if (game.characters[0].portrait != -1)
			{
				for (int i = 0; i < 4; ++i)
				{
					if (player.isChampionInFront(newPos, i) == true)
						game.characters[i].hitChampion(i, (RANDOM(4) == 0 ? 2 : 1));
				}

				sound->play(newPos, "sound/Party_Damaged.wav");
			}
				

The melee attacks

In this part we will try to hit monsters with melee weapons.
But first we need to define what is a "melee" weapon, or rather a "melee attack" as a weapon like a staff allows
you to hit enemies, and also can cast spells too.
So in attacks.xml I added a type:

		<attacks>
			<!-- 0: Nothing -->
			<attack>
				<type>Melee</type>
				[...]
			</attack>

			<!-- 1: BLOCK -->
			<attack>
				<type>Melee</type>
				[...]
			</attack>

			[...]
				
This type parameter can take different values:

Choosing a target

As for the monsters, only champions in the front row can attack with melee weapons.
In the following code we first check if there is a group of monsters in the tile ahead of us.
Then if the player player is in the front row, he chooses a target.

		void CInterface::updateWeapons()
		{
			[...]

			if (frontTile != NULL)
			{
				// breakable door
				if (frontTile->getType() == eTileDoor &&
					[...]
				}
				else if (attack.type == "Melee")
				{
					// melee attack: look for a monster's group in front of us
					CVec2   monsterPos = player.pos;

					monsterPos.x += (player.dir == 3) - (player.dir == 1);
					monsterPos.y += (player.dir == 2) - (player.dir == 0);

					int groupIndex = map.findMonsterGroupIndex(monsterPos);

					if (groupIndex != -1)
					{
						CMonsterGroup2& group = monsters.monsterGroups[groupIndex];

						// only front players can attack with melee weapon
						if (player.isChampionInFront(monsterPos, currentWeapon) == true)
						{
							// choose a target
							uint8_t target = chooseTarget(monsterPos);
							[...]
						}
						else
						{
							// can't reach
							weaponsAreaState = eWeaponsAreaWeapons;
						}
					}
					else
					{
						// nothing to hit
						weaponsAreaState = eWeaponsAreaWeapons;
					}
				}
				
The chooseTarget function looks like the one we used for the monsters.
We pick a random monster in the front row of the group:

		uint8_t CInterface::chooseTarget(CVec2 mapPos)
		{
			// list the possible targets
			int groupIndex = map.findMonsterGroupIndex(mapPos);
			CMonsterGroup2& group = monsters.monsterGroups[groupIndex];

			uint8_t possibleTargets[2];
			int nbTargets = 0;

			for (int i = 0; i < 4; ++i)
			{
				CMonster&   monster = group.mMonsters[i];

				if (monster.pos != eMonsterPosNone &&
					group.isMonsterInFront(mapPos, monster.pos) == true)
				{
					possibleTargets[nbTargets++] = i;
				}
			}

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

Attacking the target

In updateWeapons() here is what will replace the [...] after we chose a target:

		damages = game.characters[currentWeapon].getMeleeDamage(monsterPos, attack);

		if (damages != 0)
		{
			// hit
			damagesDisplayTime.set(DAMAGES_DISPLAY_TIME, false);
			weaponsAreaState = eWeaponsAreaDamage;
			group.hitMonster(monsterPos, target, damages);
		}
		else
		{
			// miss
			weaponsAreaState = eWeaponsAreaWeapons;
		}
				
The getMeleeDamage() function not only computes damages, but also checks if we hit the target.
you can see that we compares the dexterities of the champion and the monster to see if the attack is avoided,
exactly as we did when a monster hits a champion.
There is also the random test to get a minimum probability of 25%.
And in the middle, there is attack's hit probability test that we used before - between 0 and 75

		int CCharacter::getMeleeDamage(CVec2 monsterPos, CAttacks::SAttack& attack)
		{
			int groupIndex = map.findMonsterGroupIndex(monsterPos);
			CMonsterGroup&  mapGroup = map.mMonsterGroups[groupIndex];
			int monsterType = mapGroup.getType();
			CMonsters::SMonsterData&    monsterData = monsters.monstersDatas[monsterType];
			int weaponType = bodyObjects[eBodyPartRightHand].getType();

			// do we hit ?
			int monsterDexterity = monsterData.dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
			int championDexterity = getDexterity();

			if (championDexterity > monsterDexterity ||
				((RANDOM(75) + 1) <= attack.hitProbability) ||
				RANDOM(4) == 0)
			{
				
Then we compute the value of the damages taken by the monster.
First we get the champion's "strength" which is in fact the base damage value given the weapon he wield in its
right hand.
I will explain this function later.

				int damages = 0;
				int monsterDefense = 0;

				// compute damages
				damages = getStrength(eBodyPartRightHand);
				
We tune these damages with a coefficient for the attack used.
It's a value that I added for each attack in attacks.xml.
This coefficient is in 1/32th meaning that i.e. a value of 16 yields to half the normal damages.

We also take into account the monster's "defense", and you can see that there are special cases for some weapons
that "ignore" a fraction of the monster's "armor".

				if (damages != 0)
				{
					damages += RANDOM(damages / 2 + 1);
					damages = (damages * attack.damages) / 32;
					monsterDefense = monsterData.defense + game.levelMultiplier() * 2 + RANDOM(32);

					if (weaponType == OBJECT_TYPE_DIAMOND_EDGE)
					{
						monsterDefense -= monsterDefense / 4;
					}
					else if (weaponType == OBJECT_TYPE_HARDCLEAVE)
					{
						monsterDefense -= monsterDefense / 8;
					}

					damages = damages - monsterDefense - RANDOM(32);
				}
				
Then there is a strange part of the code that looks like an attempt to "tune" the damages and raise a little the
probability to hit. Though it seems quite complex.

				// last chance ?
				if (damages <= 1)
				{
					int damages2 = damages;
					damages = RANDOM(4);

					if (damages != 0)
					{
						damages++;
						damages2 += RANDOM(16);

						if (damages2 > 0 || RANDOM(2))
						{
							damages += RANDOM(4);

							if (RANDOM(4) == 0)
								damages += MAX(0, damages2 + RANDOM(16));
						}
					}
				}
				
Finally, we "randomize" the damages the same way we did it for the monsters, in order to increase the probability
of low damages.

				// randomize the damages
				if (damages != 0)
				{
					damages /= 2;
					damages += RANDOM(damages) + RANDOM(4);
					damages += RANDOM(damages);
					damages /= 4;
					damages += RANDOM(4) + 1;
				}

				return damages;
			}
			return 0;
		}
				

The champion's "strength"

To compute the champion's base damage, we first get the strength stat:

		int CCharacter::getStrength(int objType)
		{
			int strength = stats[eStatStrength].value + RANDOM(16);
			CObjects::CObjectInfo&    objInfo = objects.mObjectInfos[objType];
				
Then we modulate it with the weight of the weapon.
Light weapons - comparatively to the champion's max load - will increase the strength.
Whereas heavy weapon will decrease the strength as they are harder to handle.

			// object weight
			int loadLimit = stats[eStatLoad].maxValue / 16;
			int loadLimit2 = loadLimit + (loadLimit - 12) / 2;

			if (objInfo.weight <= loadLimit)
			{
				strength += objInfo.weight - 12;
			}
			else if (objInfo.weight <= loadLimit2)
			{
				strength += (objInfo.weight - loadLimit) / 2;
			}
			else
			{
				strength -= (objInfo.weight - loadLimit2) * 2;
			}
				
The weapons have each a "damage" coefficient that is added to the strength.

			// weapon's damages
			strength += objInfo.damages;
				
And finally we divide the strength by 2 and bound this value between 0 and 100.

			// bound the value
			strength /= 2;

			if (strength < 0)
				strength = 0;

			if (strength > 100)
				strength = 100;

			return strength;
		}
				
Note that it is not the exact formula used in the original game, as it also took into account the skill of the
champion to handle the weapon.
In getMeleeDamage the original game also modulated the hit probability with the champion's luck.
But we will add these parameters laters when we talk about the character's stats and skills.

Monsters' life and death

Now that we have the damages, we can talk about the hitMonster() function we saw earlier.
This function decreases the monster's life, and when it's dead it makes it disappear by setting its position to
eMonsterPosNone.
At this moment we also check if all the monsters of this group are dead to remove it from the lists.
Remember that the groups datas are in 2 lists: map.mMonsterGroups and monsters.monsterGroups.

		void CMonsterGroup2::hitMonster(CVec2 groupPos, int monsterNum, int damages)
		{
			mMonsters[monsterNum].life -= damages;

			// dead ?
			if (mMonsters[monsterNum].life <= 0)
			{
				mMonsters[monsterNum].pos = eMonsterPosNone;

				// if all monsters are dead, we remove the group
				int i;
				for (i = 0; i < 4; ++i)
				{
					if (mMonsters[i].pos != eMonsterPosNone)
						break;
				}

				if (i == 4)
				{
					int groupIndex = map.findMonsterGroupIndex(groupPos);
					map.removeMonsterGroup(groupPos);
					monsters.monsterGroups.erase(monsters.monsterGroups.begin() + groupIndex);
				}
			}
		}
				

Solid monsters

Finally to make the game more realistic, now that the fights begin to work, we will avoid to walk through the
monsters as we did before.
To block the movements of the party we will modify the moveIfPossible().
Up to now it used a flag "canMove" that told us if the player was hitting a wall or not.
We will change this flag to an int that takes the following values:
We need to differentiate the 2 last cases as when we hit a monster we don't take damages.

		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]
			int canMove = 0;    // 0=OK, 1=wall, 2=monster

			if (tile->mWalls[side].getType() != 0)
			{
				canMove = 1;
			}
			else if (destTile == NULL)
			{
				canMove = 1;
			}
			else if (destTile->getType() == eTileDoor && doors.isOpened(destTile) == false)
			{
				canMove = 1;
			}
			else if (map.findMonsterGroupIndex(newPos) != -1)
			{
				canMove = 2;
			}

			if (canMove == 0)
			{
				// no obstacle
				pos = newPos;

				[...]
			}
			else if (canMove == 1)
			{
				// wall
				[...]
			}
		}