Partie 24: Objets - Partie 4: Dans l'inventaire

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 21 (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.

Positions des objets

On ne peut pas mettre un objet n'importe où dans la feuille de personnage.
Une paire de bottes n'ira pas sur votre tête, certains objets sont trop grands pour rentrer dans votre poche, etc...
Alors chaque objet va être associé à une liste de positions possibles.
D'abord, regardons toutes les positions possibles.
Cette image vient de the Dungeon Master Encyclopaedia


Vous pouvez voir que chaque slot d'objet fait partie d'un groupe spécifique.
Pour faciliter les choses dans le code, on va identifier ces groupes par une seule lettre:
La liste des positions où un objet donné peut aller sera stockée comme une chaine de caractères dans items.xml, et
chaque caractère de cette chaine correspondra à cette liste.

		<items>
			<!-- ======================== 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>
			</item>

			[...]
		</items>
				

Les parties du corps

Maintenant, modifions quelques choses dans "characters.h".
Jusqu'à maintenant, on avait un enum avec toutes les parties du corps du personnage. On va le compléter avec tous
les slots d'objets possibles, y compris les poches, le carquois et le sac à dos (mais pas le coffre, parce qu'on en
parlera dans une autre partie).

		#define BACKPACK_SIZE   17

		class CCharacter
		{
		public:
			enum EBodyParts
			{
				eBodyPartHead = 0,	// tête
				eBodyPartNeck,		// cou
				eBodyPartTorso,		// torse
				eBodyPartLeftHand,	// main gauche
				eBodyPartRightHand,	// main droite
				eBodyPartLegs,		// jambes
				eBodyPartFeet,		// pieds
				eBodyPartPouch1,	// poche 1
				eBodyPartPouch2,	// poche 2
				eBodyPartQuiver1,	// carquois 1
				eBodyPartQuiver2,	// carquois 2
				eBodyPartQuiver3,	// carquois 3
				eBodyPartQuiver4,	// carquois 4
				eBodyPartBackpack,	// sac à dos
				eBodyPartCount = eBodyPartBackpack + BACKPACK_SIZE
			};
				
Le tableau qui contient les objets de notre personnage va changer aussi. Auparavant on avait un tableau temporaire
d'entiers, maintenant on va le remplacer par un tableau de CObject.

		CObject     bodyObjects[eBodyPartCount];
				

Définir les slots d'objets

Maintenant, définissons les slots d'objets dans la feuille de personnage.
On aura besoin de leur position, d'un caractère pour définir leur type d'après la liste qu'on a donnée au début, et
d'un entier pour dire si on doit dessiner un sprite par défaut quand il n'y a pas d'objet.
Ces sprites par défaut sont pour les images de la silhouette du corps, de la poche, du carquois et du sac à dos.


Comme ces sprites sont tous dans la même planche, on a juste besoin d'un entier pour le numéro du sprite.

Donc dans inventory.h, on aura un tableau de structures comme ça:

		class CInterface
		{
		public:
			[...]

			struct SBodyInfos
			{
				CVec2   pos;
				char    type;
				int     defaultImage;
			};

			[...]

		private:
			[...]
			SBodyInfos  bodyInfos[CCharacter::eBodyPartCount];
		};
				
Ce tableau est initialisé dans le constructeur de CInterface:

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

			// initialise les positions des objets dans la feuille de personnage
			bodyInfos[CCharacter::eBodyPartHead].pos      = CVec2(34,  59);
			bodyInfos[CCharacter::eBodyPartNeck].pos      = CVec2( 6,  66);
			bodyInfos[CCharacter::eBodyPartTorso].pos     = CVec2(34,  79);
			bodyInfos[CCharacter::eBodyPartLeftHand].pos  = CVec2( 6,  86);
			bodyInfos[CCharacter::eBodyPartRightHand].pos = CVec2(62,  86);
			bodyInfos[CCharacter::eBodyPartLegs].pos      = CVec2(34,  99);
			bodyInfos[CCharacter::eBodyPartFeet].pos      = CVec2(34, 119);
			bodyInfos[CCharacter::eBodyPartPouch1].pos    = CVec2( 6, 106);
			bodyInfos[CCharacter::eBodyPartPouch2].pos    = CVec2( 6, 123);
			bodyInfos[CCharacter::eBodyPartQuiver1].pos   = CVec2(62, 106);
			bodyInfos[CCharacter::eBodyPartQuiver2].pos   = CVec2(79, 106);
			bodyInfos[CCharacter::eBodyPartQuiver3].pos   = CVec2(62, 123);
			bodyInfos[CCharacter::eBodyPartQuiver4].pos   = CVec2(79, 123);

			bodyInfos[CCharacter::eBodyPartHead].type      = 'H';
			bodyInfos[CCharacter::eBodyPartNeck].type      = 'N';
			bodyInfos[CCharacter::eBodyPartTorso].type     = 'T';
			bodyInfos[CCharacter::eBodyPartLeftHand].type  = 'h';
			bodyInfos[CCharacter::eBodyPartRightHand].type = 'h';
			bodyInfos[CCharacter::eBodyPartLegs].type      = 'L';
			bodyInfos[CCharacter::eBodyPartFeet].type      = 'F';
			bodyInfos[CCharacter::eBodyPartPouch1].type    = 'P';
			bodyInfos[CCharacter::eBodyPartPouch2].type    = 'P';
			bodyInfos[CCharacter::eBodyPartQuiver1].type   = 'Q';
			bodyInfos[CCharacter::eBodyPartQuiver2].type   = 'q';
			bodyInfos[CCharacter::eBodyPartQuiver3].type   = 'q';
			bodyInfos[CCharacter::eBodyPartQuiver4].type   = 'q';

			for (int i = 0; i < BACKPACK_SIZE; ++i)
			{
				int x = 66 + ((i + 1) % 9) * 17;
				int y = 49 + ((i + 1) / 9) * 17;
				bodyInfos[CCharacter::eBodyPartBackpack + i].pos = CVec2(x, y);
				bodyInfos[CCharacter::eBodyPartBackpack + i].type = 'B';
			}

			// images par défaut pour les parties du corps
			for (int i = 0; i < CCharacter::eBodyPartCount; ++i)
				bodyInfos[i].defaultImage = -1;

			bodyInfos[CCharacter::eBodyPartHead].defaultImage         = 24;
			bodyInfos[CCharacter::eBodyPartNeck].defaultImage         = 16;
			bodyInfos[CCharacter::eBodyPartTorso].defaultImage        = 26;
			bodyInfos[CCharacter::eBodyPartLeftHand].defaultImage     = 20;
			bodyInfos[CCharacter::eBodyPartRightHand].defaultImage    = 22;
			bodyInfos[CCharacter::eBodyPartLegs].defaultImage         = 28;
			bodyInfos[CCharacter::eBodyPartFeet].defaultImage         = 30;
			bodyInfos[CCharacter::eBodyPartPouch1].defaultImage       = 17;
			bodyInfos[CCharacter::eBodyPartQuiver1].defaultImage      = 18;
			bodyInfos[CCharacter::eBodyPartBackpack + 8].defaultImage = 19;
		}
				

Dessin et zones souris

J'ai écrit une fonction pour dessiner un objet dans un slot et définir sa zone souris.

		void    CInterface::drawBodyPart(QImage* image, CVec2 pos, int championNum, CCharacter::EBodyParts part, bool enableArea)
				
Elle commence avec la partie dessin. On teste si il y a un objet dans le tableau bodyObjects du personnage donné.
S'il y a un objet, on le dessine comme on a dessiné le curseur dans la partie précédente.
S'il n'y a pas d'objet dans ce slot, on dessine l'image par défaut définie dans bodyInfos.
J'expliquerai plus tard pourquoi on doit passer la position en paramètre à cette fonction au lieu de la récupérer
directement dans bodyInfos.

		{
			CCharacter* c = &game.characters[championNum];
			int     objType = c->bodyObjects[part].getType();

			if (objType != 0)
			{
				// dessine l'objet dans ce slot
				CObjects::CObjectInfo  object = objects.mObjectInfos[objType - 1];
				QImage  objImage = fileCache.getImage(object.imageFile.toLocal8Bit().constData());
				CRect   rect = getItemRect(object.imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, rect);
			}
			else if (bodyInfos[part].defaultImage != -1)
			{
				// dessine l'image par défaut
				QImage  bodyParts = fileCache.getImage("gfx/interface/Items6.png");
				CRect   rect = getItemRect(bodyInfos[part].defaultImage);
				graph2D.drawImageAtlas(image, pos, bodyParts, rect);
			}
				
La deuxième partie de la fonction définit la zone souris.
Ici on doit vérifier si l'objet que l'on tient peut aller dans ce slot en testant si le caractère qui définit ce
type de slot est contenu dans la chaine "positions" de l'objet.
Un paramètre supplémentaire "enableArea" permet de passer outre ce test. Je l'expliquerai plus tard aussi.

			// zone souris
			int     objHandType = mouse.mObjectInHand.getType();
			CRect   mouseRect(pos, pos + CVec2(ITEM_WIDTH - 1, ITEM_HEIGHT - 1));
			bool    isPositionCompatible = true;

			// Teste si l'objet qu'on tient peut être mis dans ce type de slot
			if (objHandType != 0)
			{
				QString&    positions = objects.mObjectInfos[objHandType - 1].positions;
				isPositionCompatible = positions.contains(bodyInfos[part].type);
			}

			if (isPositionCompatible == true && enableArea == true)
				mouse.addArea(eMouseArea_InvObject, mouseRect, eCursor_Hand, (void*)championNum, (void*)part);
		}
				
Maintenant, dans drawInventory(), on peut dessiner tous les objets avec cette simple boucle:

		for (int i = 0; i < CCharacter::eBodyPartCount; ++i)
			drawBodyPart(image, bodyInfos[i].pos, currentChampion, (CCharacter::EBodyParts)i);
				

Tester les zones souris

Dans CInterface::update() on teste les zones souris pour les slots de cette façon:

		void   CInterface::update(SMouseArea* clickedArea)
		{
			[...]

			if (clickedArea != NULL)
			{
				if (mouse.mButtonPressing == true)
				{
					switch (clickedArea->type)
					{
						[...]

						// slot d'objet dans l'inventaire
						case eMouseArea_InvObject:
						{
							// récupère le perso courant et la position dans l'inventaire
							int champNum = (int)clickedArea->param1;
							CCharacter* c = &game.characters[champNum];
							int objectPos = (int)clickedArea->param2;

							// échange l'objet que l'on tient avec celui dans l'inventaire
							CObject temp = mouse.mObjectInHand;
							mouse.mObjectInHand = c->bodyObjects[objectPos];
							c->bodyObjects[objectPos] = temp;
						}
						break;

						default:
						break;
					}
				}

			[...]
		}
				
On échange simplement l'objet que l'on tient avec celui dans le slot.
Ca marche même si la main ou le slot sont vides. Dans ce cas, il y a un objet normal avec seulement son type mis à 0.

On aurait pu gagner du temps en ne définissant pas de zone souris si à la fois le slot et notre main sont vides,
mais ça ne devrait pas trop ralentir le jeu. Quoi qu'il en soit, on sera dans le même cas si l'inventaire est plein
d'objets.

Les objets de départ des champions

Quand vous ressuscitez un champion, il/elle a déjà quelques objets dans son inventaire.


On va définir ces objets dans "champions.xml" comme ça:

	<!-- ELIJA -->
	<champion firstName="CHAMP_FNAME00" lastName="CHAMP_LNAME00">
		[...]
		<object pos="T">52</object>
		<object pos="L">53</object>
		<object pos="F">50</object>
		<object pos="21">133</object>
	</champion>
				
pour la position, vous pouvez soit mettre le numéro du slot en suivant l'enum EBodyParts, soit utiliser un de ces
raccourcis:
Le numéro de l'objet suit l'ordre de "items.xml".

Ces objets sont lus dans CCharacter::readCharactersDB() et stockés dans une liste de structures:

		class CCharacter
		{
			[...]
			
		private:
			struct SDBObject
			{
				int pos;
				int type;
			};

			struct SChampDBData
			{
				[...]
				std::vector<SDBObject>  objects;
			};

			[...]
		};
				
Quand on appelle fromDB() pour initialiser un personnages, ses objets sont créés à partir de cette liste:

		void CCharacter::fromDB(int num)
		{
			[...]

			for (size_t i = 0; i < eBodyPartCount; ++i)
				bodyObjects[i].setType(0);

			for (size_t i = 0; i < db.objects.size(); ++i)
			{
				int pos = db.objects[i].pos;
				int type = db.objects[i].type;
				bodyObjects[pos].setType(type);
			}
		}
				

Les mains en haut

Les mains dans la zone de "portrait" des personnages en haut de l'écran sont un cas particulier de slots.


Ils sont une copie des slots des mains dans la feuille de personnage.
On va donc utiliser le même identifiant pour les dessiner, mais ils ne sont pas à la même position.
C'est la raison pour laquelle on devait passer la position en paramètre à drawBodyPart().

		void    CInterface::drawChampion(QImage* image, int num)
		{
			[...]

			// dessine les mains
			mouse.addArea(eMouseArea_None, handMouseRect, eCursor_Hand);
			drawBodyPart(image, CVec2(69 * num +  4, 10), num, CCharacter::eBodyPartLeftHand, !isResurrecting);
			drawBodyPart(image, CVec2(69 * num + 24, 10), num, CCharacter::eBodyPartRightHand, !isResurrecting);
				
Vous pouvez voir qu'on positionne aussi le dernier paramètre de cette fonction (le flag enableArea).
On doit désactiver ces slots quand on ressuscite un champion pour empêcher le joueur de voler ses objets en les
mettant dans les mains d'un autre champion.


De plus, j'ai ajouté un test pour empêcher de cliquer sur les boutons "Ressusciter" ou "Annuler" quand on tient un
objet. Ca suit le comportement du jeu d'origine.