Partie 26: Alcoves et porte-torches

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.

Objets sur les murs

Dans le premier niveau, il y a une alcove dans un mur et un porte-torche.



Ils ont qulque chose en commun: ils retiennent tous les deux des objets dans un mur.
Pour l'instant, on n'avait que des objets sur le sol. Un tas d'objets est défini par ses coordonnées (les
coordonnées du quart de case sur lequel il est).
Si on veut mettre un tas d'objets dans un mur, on aura besoin des coordonnées de sa case et du coté du mur.
Alors ajoutons un coté à CObjectStack:

		class CObjectStack
		{
			[...]

			CVec2       mPos;
			EWallSide   mSide;

			[...]
		};
				
On utilisera cette convention: Bien sur, on va devoir changer les fonctions pour les tas d'objets dans CMap pour prendre en compte ce coté.

		CObjectStack*   addObjectsStack(CVec2 pos, EWallSide side);
		CObjectStack*   findObjectsStack(CVec2 pos, EWallSide side);
		void            removeObjectsStack(CVec2 pos, EWallSide side);
				
Ce n'est pas un gros travail parce que la plupart de ces fonctions dépendent de findObjectsStackIndex() et c'est
facile de la modifier pour retrouver un tas par ses coordonnées et son coté:

		int CMap::findObjectsStackIndex(CVec2 pos, EWallSide side)
		{
			for (size_t i = 0; i < mObjectsStacks.size(); ++i)
				if (mObjectsStacks[i].mPos == pos && mObjectsStacks[i].mSide == side)
					return i;

			return -1;
		}
				
Et naturellement, le format du fichier map va changer un peu pour stocker ce coté et les nouveaux types de murs
qu'on va ajouter.

Le mur à alcove

Dans walls.xml on définit un nouveau mur:

		<wall name="Alcove">
			<image>Alcove.png</image>
			<param type="enum" values="Arrondie;Carree;Autel Vi">Type</param>
			<param type="stack"/>
		</wall>
				
Il y 3 types d'alcove dans le jeu: Elles sont simplement affichées comme des décors de murs classiques alors on les définit dans ornates.xml:

		<ornate name="Alcove_Arrondie">
			<image_front>Alcove_Arched_front.png</image_front>
			<image_side>Alcove_Arched_side.png</image_side>
			<pos_front x="64" y="69"/>
			<pos_side x="34" y="69"/>
		</ornate>

		<ornate name="Alcove_Carree">
			<image_front>Alcove_Square_front.png</image_front>
			<image_side>Alcove_Square_side.png</image_side>
			<pos_front x="67" y="69"/>
			<pos_side x="38" y="70"/>
		</ornate>

		<ornate name="Alcove_Vi">
			<image_front>Alcove_Vi_front.png</image_front>
			<image_side>Alcove_Vi_side.png</image_side>
			<pos_front x="64" y="69"/>
			<pos_side x="36" y="70"/>
		</ornate>
				
Le 2ième paramètre de ce type de mur ("stack") est une sorte de "faux" paramètre.
On ne veut pas hardcoder dans l'éditeur que les murs de type 5 contiennent un tas d'objets, donc on a besoin d'un
paramètre.
Mais contrairement aux autres paramètres, comme les booléeens ou les entiers, il ne sera pas associé à une valeur.
Le tas sera stocké avec les autres et les coordonnées du mur seront suffisantes pour les associer.
De toute façon, on a besoin de ce paramètre pour dire à l'éditeur d'afficher des boutons spécifiques dans la boîte
de sélection.

Les alcoves dans l'éditeur

Les informations des alcoves dans l'éditeur vont ressembler à ca:


Le premier paramètre est le type d'alcove (arrondie, carrée ou autel de Vi).
Et les boutons suivants sont exactement les mêmes que pour un tas d'objets, avec des boutons pour ajouter un nouvel
objet, en enlever un de la liste, et des combo box pour modifier leur type.

Le code est aussi similaire aux tas sur le sol. Dans addWallParamsItems() on ajoute les boutons à la boite de
sélecton:

		void    CEditor::addWallParamsItems(uint8_t type)
		{
			[...]

			std::vector<CParamType>& sourceList = map.mWallsParams[type];

			for (size_t i = 0; i < sourceList.size(); ++i)
			{
				[...]

				else if (sourceList[i].mType == eParamEnum)
				{
					CParamEnum*   par = (CParamEnum*)param;
					addLabel(wallSelGrid, sourceList[i].mName + ":", &mWallParamItems);
					addComboBox(wallSelGrid, sourceList[i].mValues, &mWallParamItems, i, par->mValue);
				}
				else if (sourceList[i].mType == eParamStack)
				{
					CObjectStack*   stack = map.findObjectsStack(pos, side);
					size_t          size = 0;

					if (stack != NULL)
					{
						for (size_t i = 0; i < stack->getSize(); ++i)
						{
							addButton(wallSelGrid, "effacer", &mWallParamItems, i);
							addComboBox(wallSelGrid, bridge.itemsList, &mWallParamItems, sourceList.size() - 1 + i, stack->getObject(i).getType());
						}
						size = stack->getSize();
					}
					addButton(wallSelGrid, "ajouter", &mWallParamItems, size);
				}
			}
		}
				
Dans CBridge::setSelParamCombo() on gère les changements des combo box.

		void    CBridge::setSelParamCombo(qint32 id, qint32 value)
		{
			[...]

			else if (mTabIndex == eTabWalls)
			{
				CVec2       pos = editor.mSelectStart / TILE_SIZE;
				EWallSide   side = editor.getWallSideAbs(editor.mSelectStart);
				CWall*      wall = &map.getTile(pos)->mWalls[side];
				bool        isStack = false;

				if ((size_t)id >= wall->mParams.size())
					isStack = true;
				else if (wall->mParams[id]->mType == eParamStack)
					isStack = true;

				if (isStack == true)
				{
					CObjectStack*   stack = map.findObjectsStack(pos, side);

					if (stack != NULL)
					{
						CObject&    object = stack->getObject(id - (wall->mParams.size() - 1));
						object.setType(value);
					}
				}
				else
				{
					CParam*     param = wall->mParams[id];

					switch (param->mType)
					{
						SET_COMBO(eParamOrnate, CParamOrnate)
						SET_COMBO(eParamEnum, CParamEnum)

						default:
							break;
					}
				}
			}

			[...]
		}
				
Et dans CBridge::selButtonClicked() on s'occupe des boutons "ajouter" et "effacer":

		void    CBridge::selButtonClicked(qint32 id)
		{
			if (id == -1)
				return;

			if (mTabIndex == eTabObjects)
			{
				[...]
			}
			else if (mTabIndex == eTabWalls)
			{
				CVec2       pos = editor.mSelectStart / TILE_SIZE;
				EWallSide   side = editor.getWallSideAbs(editor.mSelectStart);
				CObjectStack*   stack = map.findObjectsStack(pos, side);

				if (stack == NULL || (size_t)id == stack->getSize())
				{
					// ajoute un nouvel objet
					if (stack == NULL)
						stack = map.addObjectsStack(pos, side);

					CObject object;
					stack->addObject(object);
				}
				else
				{
					// retire un objet
					stack->removeObject(id);

					if (stack->getSize() == 0)
						map.removeObjectsStack(pos, side);
				}

				// met à jour les boutons
				QMetaObject::invokeMethod(this, "updateSelStack", Qt::QueuedConnection);
			}
		}
				
Il y a aussi beaucoup de changements dans "clipboard.cpp" et "tools.cpp" pour ajouter les tas des murs aux outils
copier, couper et coller.

Les alcoves dans le jeu

Comme on l'a dit, l'image d'une alcove est affichée comme un décor, on n'a pas besoin d'écrire une nouvelle fonction
pour faire ça, mais on a quand même besoin d'afficher les objets.
On les affiche dans CGame::drawWall():

		else if (wall->getType() == eWallAlcove)
		{
			//------------------------------------------------------------------------------
			// alcove
			int type = wall->getEnumParam("Type");
			walls.drawOrnate(image, tablePos, side, ORNATE_ARCHED_ALCOVE + type);

			if (tablePos.y > 0 && side == eWallSideUp)
			{
				objects.drawObjectsStack(image, mapPos, playerSide, tablePos);

				if (tablePos == CVec2(2, 3) && mouse.mObjectInHand.getType() != 0)
				{
					CRect   rectLeft(CVec2(66, 84), CVec2(157, 124));
					mouse.addArea(eMouseAreaG_DropObject, rectLeft, eCursor_Hand, (void*)mapPos.x, (void*)mapPos.y, (void*)playerSide);
				}
			}
		}
				
Notez que les objets sont affichés seulement quand le mur est face à nous.
On dessine les objets en appelant la fonction drawObjectsStack() que j'ai modifiée pour prendre en compte le coté
du mur et le fait que les coordonnées sont celles d'une case, pas d'un quart de case.

		void CObjects::drawObjectsStack(QImage* image, CVec2 mapPos, EWallSide side, CVec2 tablePos)
		{
			[...]

			CObjectStack*   stack = map.findObjectsStack(mapPos, side);

			if (stack != NULL)
			{
				for (size_t i = 0; i < stack->getSize(); ++i)
				{
					int type = stack->getObject(i).getType();

					if (type != 0)
					{
						// récupère l'image de l'objet
						[...]

						// récupère la position de l'objet et ajoute une valeur pseudo-aléatoire
						CVec2   pos;

						if (side == eWallSideMax)
							pos = getObjectPos(tablePos);
						else
							pos = getAlcovePos(tablePos);
						[...]

						// calcule la taille par rapport à la position de référence (la plus proche)
						CVec2   pos0, pos1;
						float   scale;

						if (side == eWallSideMax)
						{
							pos0 = getObjectPos(CVec2(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2));
							pos1 = getObjectPos(CVec2(WALL_TABLE_WIDTH, tablePos.y));
							scale = (float)(pos1.x - 112) / (float)(pos0.x - 112);
						}
						else
						{
							static float scalesTable[] = {0.36, 0.53, 0.82};
							scale = scalesTable[tablePos.y - 1];
						}

						// redimensionne l'objet dans une image temporaire
						[...]

						// assombrit l'objet en fonction de sa distance
						float shadow;

						if (side == eWallSideMax)
							shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y) * 0.13f;
						else
							shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y * 2) * 0.13f;
						graph2D.darken(&scaledObject, shadow);

						// dessine l'objet à l'écran
						[...]

						// ajoute la zone souris
						if (mouse.mObjectInHand.getType() == 0)
						{
							bool    isAreaOK = false;

							if (side == eWallSideMax)
							{
								if (tablePos.x >= WALL_TABLE_WIDTH - 1 && tablePos.x <= WALL_TABLE_WIDTH)
								{
									if (tablePos.y == (WALL_TABLE_HEIGHT - 1) * 2 ||
										(tablePos.y == (WALL_TABLE_HEIGHT - 2) * 2 + 1 && isWallInFront() == false))
									{
										isAreaOK = true;
									}
								}
							}
							else
							{
								if (tablePos.x == WALL_TABLE_WIDTH / 2 && tablePos.y == WALL_TABLE_HEIGHT - 1)
									isAreaOK = true;
							}

							if (isAreaOK)
							{
								CRect   mouseRect(pos, CVec2(pos.x + scaledObject.width() - 1, pos.y + scaledObject.height() - 1));
								mouse.addArea(eMouseAreaG_PickObject, mouseRect, eCursor_Hand, (void*)stack, (void*)i);
							}
						}
					}
				}
			}
		}
				
La position de l'objet à l'écran est calculée par une nouvelle fonction:

		CVec2 CObjects::getAlcovePos(CVec2 tablePos)
		{
			CVec2   pos;
			pos.x = 250 * (tablePos.x * 2 - 4) / (8.5 - tablePos.y * 2) + 112;
			static int  yTable[] = {91, 104, 120};
			pos.y = yTable[tablePos.y - 1];
			return pos;
		}
				
Comme vous pouvez le voir, pour les zones souris, j'ai utilisé les mêmes identifiants que pour les tas au sol
(eMouseAreaG_PickObject et eMouseAreaG_DropObject). Donc on a le même comportement quand on clique sur un objet
dans une alcove ou au sol.

Les porte-torches dans l'éditeur

Pour le porte-torches, notre approche sera un peu différente.
Ce n'est pas vraiment comme une alcove où on peut avoir tout un tas d'objets.
Ici on ne peut avoir qu'un seul objet qui est toujours du même type, alors on n'a pas besoin d'autant de boutons
que pour l'alcove.
Dans l'éditeur, on aura seulement besoin d'un booleen pour dire si le porte-torche est vide ou s'il contient une
torche au départ.
Alors voilà ce qu'on va ajouter à "walls.xml"...

		<wall name="Torche">
			<image>Torch.png</image>
			<param type="bool">estVide</param>
		</wall>
				
...et comment ça va apparaitre dans l'éditeur:


Les porte-torches dans le jeu

Même si on a simplifié l'interface pour les porte-torches dans l'éditeur, On va quand même avoir besoin de les
lier à un tas d'objets dans le jeu.
Comme ces tas ne sont pas créés par l'éditeur, on va devoir les créer dans une fonction init() qui sera appelée
juste après qu'on aura chargé la map:

		void CWalls::init()
		{
			// crée les tas d'objets pour les porte-torches
			for (int y = 0; y < map.mSize.y; ++y)
				for (int x = 0; x < map.mSize.x; ++x)
				{
					CVec2   pos(x, y);
					CTile*  t = map.getTile(pos);

					for (int side = 0; side < eWallSideMax; ++side)
					{
						CWall* w = &t->mWalls[side];

						if (w->getType() == eWallTorch)
						{
							bool    isEmpty = w->getBoolParam("estVide");

							map.removeObjectsStack(pos, (EWallSide)side);

							if (isEmpty == false)
							{
								CObjectStack*   stack = map.addObjectsStack(pos, (EWallSide)side);
								CObject object;
								object.setType(3);
								stack->addObject(object);
							}
						}
					}
				}
		}
				
On parcourt simplement tous les "murs torche" dans la map, et on crée un tas si leur flag "estVide" est faux.

Les porte-torches apparaissent dans le jeu comme des décors de murs.
Il y a 2 décors: un quand le porte-torche est vide...


...et un quand il n'est aps vide:


Comme pour les autres murs, celui-ci est dessiné dans CGame::drawWall(). On dessine un décor ou l'autre en fonction
de l'existence d'un tas d'objets associé au mur:

		else if (wall->getType() == eWallTorch)
		{
			//------------------------------------------------------------------------------
			// porte-torche
			CObjectStack*   stack = map.findObjectsStack(mapPos, playerSide);
			CRect   rect = walls.drawOrnate(image, tablePos, side, (stack != NULL ? ORNATE_TORCH_FULL : ORNATE_TORCH_EMPTY));

			if (tablePos == CVec2(2, 3) && side == eWallSideUp)
				mouse.addArea(eMouseAreaG_WallTorch, rect, eCursor_Hand, (void*)mapPos.x, (void*)mapPos.y, (void*)playerSide);
		}
				
Remarquez que j'ai modifié drawOrnate pour qu'elle renvoie le rectangle du décor qu'elle dessine. De cette façon,
on n'a pas besoin de le recalculer pour la zone souris.

Enfin, dans CGame::update() On teste la zone souris. Soit on prends la torche si notre main est vide, ou, si on est
en train de tenir une torche et que le porte-torche est vide, on le "remplit".

		else if (clickedArea->type == eMouseAreaG_WallTorch)
		{
			CVec2   pos;
			EWallSide   side;

			pos.x = (int)clickedArea->param1;
			pos.y = (int)clickedArea->param2;
			side = (EWallSide)((int)clickedArea->param3);

			int type = mouse.mObjectInHand.getType();
			CObjectStack* stack = map.findObjectsStack(pos, side);

			if (type == 0 && stack != NULL)
			{
				mouse.mObjectInHand = stack->getObject(0);
				map.removeObjectsStack(pos, side);
			}
			else if (type == 3 && stack == NULL)
			{
				stack = map.addObjectsStack(pos, side);
				stack->addObject(mouse.mObjectInHand);
				mouse.mObjectInHand.setType(0);
			}
			characters[interface.selectedChampion].updateLoad();
		}
				

Derniers mots

Quelques remarques en conclusion de cette partie: