Partie 37: Les attaques Partie 1

Téléchargements

Code source
Exécutable de l'éditeur de niveaux (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.

Les attaques

Chaque arme a jusqu'à 3 attaques différentes.


Il y en a 40 et j'ai ajouté leurs noms dans "texts.xml":

		<text id="ATTACK01">FENDRE</text>
		<text id="ATTACK02">ENTAILLER</text>
		<text id="ATTACK03">SONNER FORT</text>
		<text id="ATTACK04">PILE OU FACE</text>
		<text id="ATTACK05">COGNER</text>
		<text id="ATTACK06">COUP DE PIED</text>
		<text id="ATTACK07">AMORT</text>
		<text id="ATTACK08">POIGNARDER</text>
		<text id="ATTACK09">DESCENDRE</text>
		<text id="ATTACK10">CONGELER</text>
		<text id="ATTACK11">FRAPPER</text>
		<text id="ATTACK12">TAILLADER</text>
		<text id="ATTACK13">PLONGER</text>
		<text id="ATTACK14">PIQUER</text>
		<text id="ATTACK15">PARER</text>
		<text id="ATTACK16">ABATTRE</text>
		<text id="ATTACK17">SE RUER SUR</text>
		<text id="ATTACK18">BOULE DE FEU</text>
		<text id="ATTACK19">CONJURER</text>
		<text id="ATTACK20">SUBJUGUER</text>
		<text id="ATTACK21">ECLAIR</text>
		<text id="ATTACK22">ENSORCELER</text>
		<text id="ATTACK23">MELEE</text>
		<text id="ATTACK24">INVOQUER</text>
		<text id="ATTACK25">TAILLADER</text>
		<text id="ATTACK26">POURFENDRE</text>
		<text id="ATTACK27">ASSOMMER</text>
		<text id="ATTACK28">ETOURDIR</text>
		<text id="ATTACK29">TIRER</text>
		<text id="ATTACK30">AURA MAGIQUE</text>
		<text id="ATTACK31">PARE-FEU</text>
		<text id="ATTACK32">CAGE</text>
		<text id="ATTACK33">GUERIR</text>
		<text id="ATTACK34">TRANQUILISER</text>
		<text id="ATTACK35">LUMIERE</text>
		<text id="ATTACK36">FENETRE</text>
		<text id="ATTACK37">CRACHER</text>
		<text id="ATTACK38">BRANDIR</text>
		<text id="ATTACK39">LANCER</text>
		<text id="ATTACK40">FUSIONNER</text>
				
J'ai créé une nouvelle base de données "attacks.xml" qui contient quelques paramètres pour chacune d'entre-elles,
mais on parlera de ça plus tard.

Combos

Vous pensez peut-être qu'on va simplement ajouter 3 numéros d'attaque à chaque arme dans le fichier "items.xml",
mais c'est un peu plus compliqué que ça.
Chaque groupe de 3 attaques qui est associé à une arme est s'appelle une combo (du moins c'est le nom donné par
Dungeon Master Encyclopaedia).
Le jeu d'origine utilisait une base de données à part pour ces combos. C'est necessaire parce que la même attaque
peut avoir des paramètres différents si elle est dans 2 combos differentes.
Donc j'ai créé une base de données "combos.xml":

		<?xml version="1.0" encoding="utf-8"?>
		<combos>
			<!-- Combo 0 (inutilise) -->
			<combo>
				<attack type="0">
				</attack>
				<attack type="0">
				</attack>
				<attack type="0">
				</attack>
			</combo>

			<!-- Combo 1 Invoquer/Fusionner/Cage -->
			<combo>
				<attack type="24">
				</attack>
				<attack type="40">
				</attack>
				<attack type="32">
				</attack>
			</combo>

			<!-- Combo 2 Cogner/Coup de Pied/Amort -->
			<combo>
				<attack type="5">
				</attack>
				<attack type="6">
				</attack>
				<attack type="7">
				</attack>
			</combo>
			[...]

		</combos>
				
Chaque combo contient 3 attaques qui ont seulement un type. A l'avenir ces attaques contiendront plus de paramètres.
Remarquez que la combo 0 n'a pas d'attaque. C'est la combo qui sera utilisée pour les objets qui ne peuvent pas
être utilisés comme arme (par ex. une pomme).
Cette base de données est lue dans une nouveau fichier "attacks.cpp" et le résultat est stocké dans une liste de
structures comme ça:

		class CAttacks
		{
		public:
			[...]

			struct SComboAttack
			{
				uint8_t type;
			};

			struct SCombo
			{
				SComboAttack    attacks[3];
			};

			[...]
			void        readCombosDB();
			[...]
			SCombo&     getCombo(int num);

		private:
			[...]
			std::vector<SCombo> mCombos;
		};
				

Lier les combos aux armes

Dans le fichier "items.xml" on va ajouter un numéro de combo à chaque arme.

		<?xml version="1.0" encoding="utf-8"?>
		<items>
			<!-- MAIN NUE -->
			<item name="ITEM000">
				<category>Weapon</category>
				<image_file>Items6.png</image_file>
				<image_num>9</image_num>
				<weight>0</weight>
				<combo>2</combo>
			</item>

			<!-- ======================== Armes ======================== -->
			<!-- OEIL DU TEMPS -->
			<item name="ITEM001">
				<category>Weapon</category>
				<floor_image>00W_Ring.png</floor_image>
				<image_file>Items0.png</image_file>
				<image_num>17</image_num>
				<position>hBCP</position>
				<weight>1</weight>
				<combo>43</combo>
			</item>

			<!-- ROND D'ECLAIRS -->
			<item name="ITEM002">
				<category>Weapon</category>
				<floor_image>00W_Ring.png</floor_image>
				<image_file>Items0.png</image_file>
				<image_num>19</image_num>
				<position>hBCP</position>
				<weight>1</weight>
				<combo>7</combo>
			</item>
				
Vous pouvez voir que j'ai ajouté un objet "main nue" parce que quand il n'y a pas d'arme dans la main du champion,
elle est liée à une combo quand même.
Mais il y a aussi une autre raison de faire ça. Comme les objets dans "items.xml" commençaient à "1", chaque fois
qu'on devait accéder à ces données dans le code, on devait retirer 1 au type de l'objet.
Par exemple, on avait:

        CObjects::CObjectInfo&  infos = objects.mObjectInfos[objType - 1];
				
Ca prêtait à confusion et ça pouvait créer des bugs. Alors maintenant, on n'a plus besoin du "-1".

Maintenant, on peut changer l'affichage des armes en fonction de ces données.
D'abord, si l'objet tenu par un champion est lié à la combo 0, on n'a pas besoin de l'afficher dans la zone des
armes, et il n'y aura pas de zone souris dessus.


La fonction drawWeapon() va maintenant ressembler à ça:

		void    CInterface::drawWeapon(QImage* image, int num)
		{
			if (isWeaponEmpty(num) == false)
			{
				// dessine le rectangle de fond
				CRect   rect(CVec2(233 + 22 * num, 86),
							 CVec2(252 + 22 * num, 120));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				// dessine l'arme
				CVec2       pos = CVec2(235 + 22 * num, 96);
				CObject&    weapon = getWeapon(num);
				int         weaponType = weapon.getType();
				CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];

				if (weaponInfo.combo != 0)
				{
					std::string imageFile;
					int         imageNum;

					CObjects::CObjectInfo  object = objects.mObjectInfos[weaponType];
					imageFile = object.imageFile.toLocal8Bit().constData();
					imageNum = object.imageNum;

					QImage  objImage = fileCache.getImage(imageFile);
					CRect   objRect = getItemRect(imageNum);
					graph2D.drawImageAtlas(image, pos, objImage, objRect, BLACK);
				}

				// griser ou zone souris
				if (isWeaponGreyed(num) == true)
					graph2D.patternRectangle(image, rect);
				else if (weaponInfo.combo != 0)
					mouse.addArea(eMouseArea_Weapon, rect, eCursor_Arrow, (void*)num);
			}
		}
				
Bien entendu on utilise aussi les données de la combo pour trouver combien d'attaques a une arme et afficher leurs
noms dans drawAttacks():

		void    CInterface::drawAttacks(QImage* image, int num)
		{
			CObject&    weapon = getWeapon(num);
			int         weaponType = weapon.getType();
			CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];
			CAttacks::SCombo&       combo = attacks.getCombo(weaponInfo.combo);

			// dessine l'image de fond
			QImage  attacksBg = fileCache.getImage("gfx/interface/WeaponsActions.png");
			CVec2   pos(233, 77);
			graph2D.drawImage(image, pos, attacksBg);

			// cache les slots inutiles
			for (int i = 0; i < 3; ++i)
			{
				if (combo.attacks[i].type == 0)
				{
					CRect   rect(CVec2(233, 86 + i * 12),
								 CVec2(319, 97 + i * 12));
					graph2D.rect(image, rect, BLACK, true);
				}
			}

			// écrit le nom du personnage
			CCharacter* c = &game.characters[num];
			drawText(image, CVec2(235, 79), eFontStandard, c->firstName.c_str(), BLACK);

			// écrit le nom des attaques
			for (int i = 0; i < 3; ++i)
			{
				if (combo.attacks[i].type != 0)
				{
					static char attackName[16];
					sprintf(attackName, "ATTACK%02d", combo.attacks[i].type);
					drawText(image, CVec2(241, 89 + i * 12), eFontStandard, getText(attackName).c_str(), MAIN_INTERFACE_COLOR);

					if (attackHighlightTime.isRunning() == true && currentAttack == i)
					{
						CRect   rect(CVec2(234, 86 + i * 12),
									 CVec2(318, 96 + i * 12));
						graph2D.Xor(image, rect, MAIN_INTERFACE_COLOR);
					}
				}
			}

			// zones souris
			if (isWeaponGreyed(num) == false && attackHighlightTime.isRunning() == false)
			{
				for (int i = 0; i < 3; ++i)
				{
					if (combo.attacks[i].type != 0)
					{
						CRect   rect(CVec2(234, 86 + i * 12),
									 CVec2(318, 96 + i * 12));
						mouse.addArea(eMouseArea_Attack, rect, eCursor_Arrow, (void*)i);
					}
				}

				CRect   rect(CVec2(290, 77), CVec2(314, 83));
				mouse.addArea(eMouseArea_CloseAttack, rect, eCursor_Arrow);
			}
		}
				
Toutes les attaques d'une arme ne seront pas disponibles dès le départ. Certaines nécessiteront que le personnage
ait un certain niveau dans une compétence donnée.
Mais on verra ça dans une autre partie...

La base de données des attaques

Comme je l'ai dit au début, il y a une base de données pour les attaques aussi, car il y a des paramètres
différents pour chacune d'entre-elles.
Ce fichier c'est "attacks.xml" et il est aussi chargé dans "attacks.cpp":

		<?xml version="1.0" encoding="utf-8"?>
		<attacks>
			<!-- Rien -->
			<attack>
				<cooldown>0</cooldown>
				<hitprob>0</hitprob>
			</attack>

			<!-- FENDRE -->
			<attack>
				<cooldown>60</cooldown>
				<hitprob>22</hitprob>
			</attack>

			<!-- ENTAILLER -->
			<attack>
				<cooldown>80</cooldown>
				<hitprob>48</hitprob>
			</attack>
				
Pour le moment il contient seulement le temps de cooldown en nombre de frames et la probabilité de toucher avec
cette arme, qui va de 0 à 75.
Alors pour savoir si une arme "touche, on va prendre un nombre aléatoire entre 1 et 75 et tester si il est inférieur
ou égal à cette valeur.
De cette façon:
C'est ce qu'on fait dans updateWeapons au moment où la surbrillance de l'attaque s'arrête:

		void CInterface::updateWeapons()
		{
			// surbrillance de l'attaque
			if (attackHighlightTime.update() == true)
			{
				CObject&    weapon = getWeapon(currentWeapon);
				int         weaponType = weapon.getType();
				CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];
				CAttacks::SCombo&        combo = attacks.getCombo(weaponInfo.combo);
				int         attackType = combo.attacks[currentAttack].type;
				CAttacks::SAttack&       attack = attacks.getAttack(attackType);

				weaponCoolDown[currentWeapon].set(attack.cooldown, true);

				if (((rand() % 75) + 1) <= attack.hitProbability)
				{
					damages = rand() % 200 + 1;
					damagesDisplayTime.set(DAMAGES_DISPLAY_TIME, false);
					weaponsAreaState = eWeaponsAreaDamage;
				}
				else
				{
					weaponsAreaState = eWeaponsAreaWeapons;
				}
			}

			// timer des dégats
			if (damagesDisplayTime.update() == true)
				weaponsAreaState = eWeaponsAreaWeapons;

			// cooldowns des armes
			for (int i = 0; i < 4; ++i)
				weaponCoolDown[i].update();
		}
				
Ce n'est pas la formule exacte utilisée dans le jeu d'origine, car la probabilité de toucher va varier avec les
stats du champion comme la dextérité ou la chance.
Dans un combat, la chance de toucher va aussi changer en fonction des stats du monstre qu'on touche, parce que sa
dextérité peut lui permettre d'éviter l'attaque.

Vous remarquerez que les attaques comme tirer avec un arc ou jeter un sort avec un bâton ont une probabilité de 0.
Ce sont des attaques spéciales où un projectile ou un sort sont toujours jetés. C'est seulement quand le projectile
touche la cible que l'on teste si il lui fait des dégâts.