Partie 44: Les Attaques Partie 2: Tuer les monstres avec des armes de mêlée

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 40 (Windows 32bits)
Exécutable du jeu (Windows 32bits)

Avant d'essayer de compiler le code, allez dans l'onglet "Projets" dans le menu de gauche, séléctionnez l'onglet "Run" pour votre kit,
et mettez dans "Working directory" le chemin de "editor\data" pour l'éditeur ou "game\data" pour le jeu.

Nettoyage des fonctions de rang

Dans les parties précédentes on a utilisé getMonsterRow() et getChampionRow() pour trouver le rang d'un monstre ou
d'un champion en fonction de la position de l'ennemi (soit l'équipe soit le groupe de monstres).
Pour plus de clarté j'ai déplacé getChampionRow dans CPlayer plutôt que dans CMonsterGroup2.

		// retourne le rang d'un monstre en fonction de la position du joueur (0 = avant, 1 = centre, 2 = arrière)
		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},   // haut
				{  0, 2, 0, 2, 1},   // gauche
				{  2, 2, 0, 0, 1},   // bas
				{  2, 0, 2, 0, 1}    // droite
			};

			int playerPosFromMonster;

			if (player.pos.x < mapPos.x)
				playerPosFromMonster = 1; // gauche
			else if (player.pos.x > mapPos.x)
				playerPosFromMonster = 3; // droite
			else if (player.pos.y < mapPos.y)
				playerPosFromMonster = 0; // haut
			else
				playerPosFromMonster = 2; // bas

			return monsterRow[playerPosFromMonster][monsterPos - eMonsterPosNorthWest];
		}
		
		// renvoie le rang d'un champion en fonction de la position du groupe de mosntres (0 = devant, 1 = derrière)
		uint8_t CPlayer::getChampionRow(CVec2 monsterPos, int championNum)
		{
			static const uint8_t championRow[4][4] =
			{
				// 1  2  3  4
				{  0, 0, 1, 1},   // haut
				{  0, 1, 0, 1},   // gauche
				{  1, 1, 0, 0},   // bas
				{  1, 0, 1, 0}    // droite
			};

			CVec2   localMonsterPos = getLocalFromPos(monsterPos);
			int monsterPosFromPlayer;

			if (localMonsterPos.x < 0)
				monsterPosFromPlayer = 1; // gauche
			else if (localMonsterPos.x > 0)
				monsterPosFromPlayer = 3; // droite
			else if (localMonsterPos.y < 0)
				monsterPosFromPlayer = 0; // haut
			else
				monsterPosFromPlayer = 2; // bas

			return championRow[monsterPosFromPlayer][championNum];
		}
				
On devait aussi tester si les monstres de la première ligne d'un groupe étaient tous morts pour que la ligne
arrière puisse attaquer l'équipe.
Et bien sûr la même chose doit être testé pour la première ligne de l'équipe.
Alors pour simplifier ça, j'ai écrit 2 autres fonctions qui disent si un champion ou un monstre donnés est en
contact avec l'ennemi en prenant ça en compte:

		// retourne true si un monstre est en première ligne de son groupe, en prenant en compte les morts
		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);
		}

		// retourne true si un champion est en première ligne, en prenant en compte les morts.
		bool CPlayer::isChampionInFront(CVec2 monsterPos, int championNum)
		{
			int rowToSearch;

			// teste s'il y a au moins un champion vivant en première ligne
			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);
		}
				
Maintenant ça simplifie les fonctions qui utilisaient les rangs. Par exemple la fonction CMonster::chooseTarget():

		// choisit le champion que ce monstre va attaquer
		uint8_t CMonster::chooseTarget(CVec2 mapPos)
		{
			// liste les cibles possibles
			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;
				}
			}

			// choisit une cible
			return possibleTargets[RANDOM(nbTargets)];
		}
				
Et comme je l'ai dit, j'aurais dû utiliser ces fonctions quand l'équipe touche un mur, maintenant c'est fait:

		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]
			
			// mur
			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");
			}
				

Les attaques de mêlée

Dans cette partie, on va essayer de toucher les monstres avec des armes de mêlée.
Mais d'abord on doit définir ce qu'est une arme de "mêlée", ou plutôt une "attaque de mêlée" parce qu'une arme
comme un bâton vous permet de frapper les ennemis, mais aussi de lancer des sorts.
Donc dans attacks.xml j'ai ajouté un type:

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

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

			[...]
				
Ce type peut prendre différentes valeurs:

Choisir une cible

Comme pour les monstres, seuls les champions en première ligne peuvent attaquer avec des armes de mêlée.
Dans le code suivant on va d'abord tester s'il y a un groupe de monstres dans la case devant nous.
Ensuite, si le joueur est en première ligne, il choisit une cible.

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

			if (frontTile != NULL)
			{
				// porte cassable
				if (frontTile->getType() == eTileDoor &&
					[...]
				}
				else if (attack.type == "Melee")
				{
					// attaque de melee: cherche un groupe de monstres en face de nous
					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];

						// seuls les champions de devant peuvent attaquer avec des armes de melee
						if (player.isChampionInFront(monsterPos, currentWeapon) == true)
						{
							// choisit une cible
							uint8_t target = chooseTarget(monsterPos);
							[...]
						}
						else
						{
							// trop loin
							weaponsAreaState = eWeaponsAreaWeapons;
						}
					}
					else
					{
						// rien à frapper
						weaponsAreaState = eWeaponsAreaWeapons;
					}
				}
				
La fonction chooseTarget() ressemble à celle qu'on a utilisée pour les monstres.
On choisit un monstre au hasard parmi ceux qui sont en première ligne du groupe:

		uint8_t CInterface::chooseTarget(CVec2 mapPos)
		{
			// liste les cibles possibles
			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;
				}
			}

			// choisit une cible
			return possibleTargets[RANDOM(nbTargets)];
		}
				

Attaquer la cible

Dans updateWeapons() voici ce qui va remplacer le [...] après qu'on ait choisi une cible:

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

		if (damages != 0)
		{
			// frappe
			damagesDisplayTime.set(DAMAGES_DISPLAY_TIME, false);
			weaponsAreaState = eWeaponsAreaDamage;
			group.hitMonster(monsterPos, target, damages);
		}
		else
		{
			// manqué
			weaponsAreaState = eWeaponsAreaWeapons;
		}
				
La fonction getMeleeDamage() ne calcule pas seulement les dégâts, mais elle teste aussi si on touche une cible.
Vous pouvez voir qu'on compare les dextérités du champion et du monstre pour voir si l'attaque est évitée,
exactement comme on faisait quand un monstre frappait un champion.
Il y a aussi le test aléatoire pour avoir une probabilité d'au moins 25%.
Et au milieu il y a le test de probabilité de l'attaque qu'on a utilisé auparavant (entre 0 et 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();

			// est-ce qu'on touche ?
			int monsterDexterity = monsterData.dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
			int championDexterity = getDexterity();

			if (championDexterity > monsterDexterity ||
			    ((RANDOM(75) + 1) <= attack.hitProbability) ||
			    RANDOM(4) == 0)
			{
				
Ensuite on calcule la valeur des dégâts subis par le monstre.
D'abord on obtient la "force" du champion qui est en fait la valeur de base des dégâts en fonction de l'arme qu'il
porte dans sa main droite.
J'expliquerai cette fonction plus tard.

				int damages = 0;
				int monsterDefense = 0;

				// calcule les dégâts
				damages = getStrength(eBodyPartRightHand);
				
On pondère ces dégâts avec un coefficient pour l'attaque utilisée.
C'est une valeur que j'ai ajouté à chaque attaque dans "attacks.xml".
Ce coefficient est en 1/32ièmes ce qui veut dire que par exemple une valeur de 16 produit la moitié des dégâts
normaux.

On prend aussi en compte la "défense" du monstre, et vous pouvez voir qu'il y a des cas spéciaux pour certaines
armes qui "ignorent" une fraction de "l'armure" du monstre.

				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);
				}
				
Ensuite il y a une partie étrange du code qui ressemble a une tentative "d'affiner" les dégâts et d'augmenter un
peu la probabilité de toucher. Même si ça a l'air assez complexe.

				// dernière 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));
						}
					}
				}
				
Enfin on "randomise" les dégâts de la même façon qu'on l'a fait pour les monstres, dans le but d'augmenter la
probabilité d'avoir des dégâts faibles.

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

La "force" du champion

Pour calculer les dégâts de base du champion, on récupère d'abord la statistique de force:

		int CCharacter::getStrength(int objType)
		{
			int strength = stats[eStatStrength].value + RANDOM(16);
			CObjects::CObjectInfo&    objInfo = objects.mObjectInfos[objType];
				
Ensuite, on la module avec le poids de l'arme.
Les armes légères (comparativement à la charge maxi du champion) vont augmenter la force.
Alors que les armes lourdes vont diminuer la force car elles sont plus difficiles à manier.

			// poids de l'object
			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;
			}
				
Les armes ont chacune un coefficient de dégâts qui est ajouté à la force.

			// dégâts de l'arme
			strength += objInfo.damages;
				
Et finalement on divise la force par 2 et on limite cette valeur entre 0 et 100.

			// limite la valeur
			strength /= 2;

			if (strength < 0)
				strength = 0;

			if (strength > 100)
				strength = 100;

			return strength;
		}
				
Notez que ce n'est pas la formule exacte utilisée dans le jeu d'origine, car elle prenait aussi en compte
l'aptitude du champion à manier l'arme.
Dans getMeleeDamage le jeu d'origine modulait aussi la probabilité de toucher avec la chance du champion.
Mais on ajoutera ces paramètres plus tard quand on parlera des statistiques et des aptitudes des personnages.

Vie et mort des monstres

Maintenant qu'on a les dégâts ,on peut parler de la fonction hitMonster() qu'on a vue auparavant.
Cette fonction diminue la vie du monstre, et quand il est mort, il le fait disparaître en mettant sa position à
eMonsterPosNone.
A ce moment on teste aussi si tous les monstres du groupe sont morts pour le retirer des listes.
Rappelez-vous que les données des groupes sont dans 2 listes: map.mMonsterGroups et monsters.monsterGroups.

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

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

				// si tous les monstres sont morts, on supprime le groupe
				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);
				}
			}
		}
				

Monstres solides

Enfin pour rendre le jeu plus réaliste, maintenant que le combat commence à marcher, on va éviter de traverser les
monstres comme on le faisait avant.
Pour bloquer les déplacements de l'équipe on va modifier la fonction moveIfPossible().
Jusqu'à maintenant elle utilisait un flag "canMove" qui nous disait si le joueur rentrait dans un mur ou non.
On va transformer ce flag en un int qui prendra les valeurs suivantes:
On doit différencier les 2 derniers cas, car quand on rentre dans un monstre on ne prend pas de dégâts.

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

			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)
			{
				// pas d'obstacle
				pos = newPos;

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