Partie 42: Monstres partie 4: Probabilité de touche et dégats

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.

Paramètres des monstres

Dans cette partie on va étudier la probabilité de toucher et les dégats infligés par les monstres quand ils attaquent.
D'abord on va ajouter quelques paramètres aux monstres.
Ces valeurs viennent de The Dungeon Master Encyclopaedia mais les noms ici sont un peu confus.
J'ai préféré utiliser les noms de paramètres du code source.

		<monsters>
			[...]

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


			<!-- 02 Démon Baveux -->
			<monster name = "MONSTER02">
				[...]
				<defense>20</defense>
				<health>110</health>
				<attack>80</attack>
				<dexterity>20</dexterity>
				[...]
			</monster>
				

La santé de départ du monstre est calculée avec cette formule:

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

			return baseHealth * game.levelMultiplier() + RANDOM((baseHealth / 4) + 1);
		}
				
game.levelMultiplier() est une valeur qui sera utilisée dans divers calculs tout au long du jeu.
Chaque niveau a un coefficient de difficulté. C'est une méthode facile pour régler la difficulté globale du jeu.
Dans le jeu d'origine, ces coefficients étaient les suivants:

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

La ligne "avant" des monstres

Dans la dernière partie, chaque monstre d'un groupe essayait d'attaquer.
Mais quand le groupe est à coté du joueur, seuls les monstres en première ligne devraient attaquer.
Les monstres en arrière sont trop loin pour atteindre les champions avec des attaques au corps à corps.
La seule exception où les monstres de l'arrière peuvent attaquer c'est quand tous les monstres en première ligne
sont morts.
Alors on aura d'abord besoin d'une fonction pour trouver la ligne d'un monstre donné en fonction de la position du
joueur.

		// 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];
		}
				
Ensuite, dans le groupe, avant d'appeler les updates de chaque monstre, on va utiliser ça pour tester si la
première ligne est morte ou pas:

		void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
		{
			// vérifie qu'il y a ait au moins un monstre vivant dans la ligne de front du groupe (ou au centre)
			bool    isFrontRowRowAlive = false;

			for (int i = 0; i < 4; ++i)
			{
				if (monsters[i].pos != eMonsterPosNone &&
				    getMonsterRow(mapPos, monsters[i].pos) < 2)
				{
					isFrontRowRowAlive = true;
					break;
				}
			}
				
Puis quand on update les monstes, on peut leur dire s'ils peuvent attaquer.

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

			// met à jour tous les monstres du groupe
			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);
				}
				

Choisir une cible

Dans la fonction fight() d'un monstre, maintenant qu'on sait qu'on est en première ligne, on peut choisir un
champion pour cible.

		void CMonster::fight(CVec2 mapPos, uint8_t type, bool isInFront)
		{
			if (nextAttackTimer.update() == true)
			{
				// si le monstre est dans la ligne avant du groupe
				if (isInFront == true)
				{
					// joue le son d'attaqued
					[...]

					// affiche l'animation du monstre
					[...]

					// choisit un champion cible
					int target = chooseTarget(mapPos);
				
Ce champion devra être en première ligne du groupe du joueur.
Comme on n'a pas parlé des positions des champions à l'intérieur du groupe, on va assumer que les 2 premiers sont
devant et les 2 derniers sont derrière.
Alors on aura besoin d'une fonction pour trouver la rangée d'un champion en fonction de la position relative des
monstres.

		// retourne le rang d'un champion en fonction de la position d'un groupe de monstres (0 = avant, 1 = arrière)
		uint8_t CMonsterGroup2::getChampionRow(CVec2 mapPos, 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 = player.getLocalFromPos(mapPos);
			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];
		}
				
Ensuite, dans la fonction chooseTarget() on a aussi besoin de tester si la première ligne de champions est toujours
vivante.
Et on choisira un champion au hasard dans la rangée qu'on attaquera.

		// choisit le champion que ce monstre va attaquer
		uint8_t CMonster::chooseTarget(CVec2 mapPos)
		{
			int rowToSearch;

			// vérifie qu'il y a au moins un champion vivant dans la ligne de front
			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;
				}
			}

			// 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 &&
				    CMonsterGroup2::getChampionRow(mapPos, i) == rowToSearch)
				{
					possibleTargets[nbTargets++] = i;
				}
			}

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

Probabilité de toucher

Vous devez avoir remarqué le RANDOM() dans la fonction précédente.
Comme on va utiliser beaucoup de randoms maintenant, j'ai écrit une macro pour remplacer le rand() qu'on avait à
différents endroits et pour rendre le code plus lisible:

		#define RANDOM(_maxValue)   (rand() % (_maxValue))
				
Maintenant qu'on a choisi une cible, il y a une chance qu'elle évite l'attaque.
On doit calculer la dextérité du monstre et du champion de cette façon:

		// calcule les dextérités du monstre et du champion
		int monsterDexterity = monsters.monstersDatas[type].dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
		int championDexterity = game.characters[target].getDexterity();
				
La dextérité du champion est calculée par cette fonction.

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

			// diminue la dextérité en fonction de la charge
			dexterity -= (dexterity / 2) * stats[eStatLoad].value / stats[eStatLoad].maxValue;

			// si on dort...
			if (interface.isSleeping() == true)
				dexterity /= 2;

			// limite la valeur
			int min = 1 + RANDOM(8);
			int max = 100 - RANDOM(8);

			if (dexterity < min)
				dexterity = min;

			if (dexterity > max)
				dexterity = max;

			return dexterity;
		}
				
La dextérité de base du champion est diminuée par la charge qu'il porte et légèrement randomisée.

Maintenant qu'on a ces valeurs on peut dire si le monstre touche le champion avec ce test:

		// teste si on a touché
		if (interface.isSleeping() == true ||
		    monsterDexterity > championDexterity ||
		    RANDOM(4) == 0)
		{
				
Quand l'équipe est en train de dormir, le monstre touche toujours.
Le RANDOM a l'air d'être quelque chose ajouté pour régler la difficulté. avec ce test le monstre a toujours au
moins 25% de chance de toucher sa cible.
Dans le jeu d'origine il y avait aussi une chance d'éviter l'attaque basée sur la statistique "chance" du champion.

Dégâts

Pour calculer les dégâts infligés par l'attaque, on récupère d'abord les "dégâts de base":

		// calcule les dégats (ici, on devrait avoir une chance de parer)
		int damages = monsters.monstersDatas[type].attack + game.levelMultiplier() * 2 + RANDOM(16);
				
Comme le dit le commentaire, ici il devrait y avoir un test pour savoir si le champion pare l'attaque.
On verra ça plus tard, car on n'a pas encore parlé des talents des personnages.

les dégâts sont "randomisés" avec une formule étrange

		// randomise les dégats (d'une façon assez compliquée...)
		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;
				
Alors pourquoi n'ont ils pas utilisé un simple random ?
Eh bien cette formule est juste une façon étrange d'obtenir une distribution de probabilités non uniforme.
A la fin il y aura une plus grande probabilité d'obtenir une faible quantité de dégâts plutôt qu'une grande.
Ca serait bien de faire une simulation en exécutant ce code un grand nombre de fois pour voir les probabilités
réelles qu'on obtient.

Maintenant qu'on a calculé les dégâts, on peut enfin frapper le champion.

		// frappe le 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);
				
A ce moment, dans le jeu d'origine, il y avait encore beaucoup de choses à prendre en compte: Mais on verra tout ça dans une autre partie.
Pour l'instant on va simplement afficher les dégâts qu'un personnage subit en haut de l'écran, dans la zone où sa
tête ou ses mains apparaissent.

Il y a 2 types d'affichage des dégâts.
Soit les mains du personnage sont visible, alors les dégâts apparaissent par dessus son nom:


Ou bien la tête du personnage est affichée parce qu'on est dans son inventaire, alors les dégâts apparaissent par
dessus son visage:


Alors la fonction hitChampion() stocke simplement la valeur des dégâts et démarre un timer.

		void CCharacter::hitChampion(int damages)
		{
			displayedDamages = damages;
			damagesTimer.set(90, false);
		}
				
Ces valeurs seront ensuite utilisées dans l'interface pour afficher la bonne image et le nombre.