Partie 43: Vie et mort des champions

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.

Dégâts et mort

Dans la dernière partie les monstres pouvaient attaquer, mais ça n'avait pas d'effet sur la vie des champions.
Maintenant on va vraiment réduire leur vie et voir comment ils meurent.
D'abord il faut qu'on termine la fonction hitChampion().

		void CCharacter::hitChampion(int num, int damages)
		{
			// déjà mort ou vide ?
			if (portrait == -1 || isDead() == true)
				return;

			displayedDamages = damages;
			damagesTimer.set(90, false);
			stats[eStatHealth].value -= damages;

			static CVec2    championPos[4][4] =
			{
				{{0, 0}, {1, 0}, {0, 1}, {1, 1}},   // up
				{{0, 1}, {0, 0}, {1, 1}, {1, 0}},   // left
				{{1, 1}, {0, 1}, {1, 0}, {0, 0}},   // down
				{{1, 0}, {1, 1}, {0, 0}, {0, 1}}    // right
			};

			// mort ?
			if (stats[eStatHealth].value <= 0)
			{
				stats[eStatHealth].value = 0;

				// jette tous les objet sur le sol
				CVec2   stackPos = player.pos * 2 + championPos[player.dir][num];
				CObjectStack*   stack = map.addObjectsStack(stackPos, eWallSideMax);

				for (int i = 0; i < eBodyPartCount; ++i)
				{
					CObject&    obj = bodyObjects[i];

					if (obj.getType() != 0)
					{
						stack->addObject(obj);
						obj.setType(0);
					}
				}

				// jette les os du champion
				CObject bones;
				bones.setType(OBJECT_TYPE_BONES);
				bones.setIntParam("Champion", num);
				stack->addObject(bones);
			}
		}
				
On soustrait les dégâts à la santé du champion.
Quand il est mort, on fait tomber tous ses objets sur le sol.
La table championPos nous donne les positions où faire tomber ces objets en fonction de la direction du joueur.
Ca ressemble à ça:


Vous pouvez voir que j'ai ajouté un paramètre "Champion" à l'objet os.

		<!-- OS DE # -->
		<item name="ITEM149">
			[...]
			<param type="int">Champion</param>
		</item>
				
Parce qu'on doit connaître le nom du champion pour afficher le nom des os quand on les ramasse.


On utilise la fonction qu'on a écrite pour les messages en bas de l'écran pour insérer ce nom dans le texte.

		void CInterface::drawObjectName(QImage* image)
		{
			int type = mouse.mObjectInHand.getType();

			if (type != 0)
			{
				std::string name;

				if (type == OBJECT_TYPE_BONES)
					name = setChampionNameInString(mouse.mObjectInHand.getIntParam("Champion"), "ITEM149");
				else
					name = objects.mObjectInfos[type].name;

				drawText(image, CVec2(233, 33), eFontStandard, name.c_str(), MAIN_INTERFACE_COLOR);
			}
		}
				

Se cogner contre les murs


Quand l'équipe se cogne dans un mur, les 2 champions dans la rangée de devant par rapport au mur reçoivent des
dégâts.
La valeur de ces dégâts dans le jeu d'origine était calculée avec une formule étrange basée sur les stats du
champion, mais je l'ai remplacée par un simple random entre 1 et 2.

		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]

			if (canMove == true)
			{
				[...]
			}
			else
			{
				if (game.characters[0].portrait != -1)
				{
					static const int championsHit[eWallSideMax][2] =
					{
						{0, 1}, // eWallSideUp
						{2, 3}, // eWallSideDown
						{0, 2}, // eWallSideLeft
						{1, 3}  // eWallSideRight
					};

					for (int i = 0; i < 2; ++i)
					{
						int champNum = championsHit[invWallSide(side)][i];
						game.characters[champNum].hitChampion(champNum, (RANDOM(4) == 0 ? 2 : 1));
					}
					sound->play(newPos, "sound/Party_Damaged.wav");
				}
			}
		}
				
J'aurais probablement dû utiliser la fonction getChampionRow() de la partie précédente ici et prendre en compte le
cas où les 2 joueurs de devant sont morts.
Mais on devra réécrire ces fonctions quand on ajoutera le placement des personnage au sein de l'équipe.

Tomber dans un trou


Quand l'équipe tombe dans un trou, chaque champion reçoit des dégâts.
C'est une valeur aléatoire autour de 20. Mais ça changera quand on ajoutera les blessures aux personnages.

		void CPlayer::fall()
		{
			[...]
			hitAllChampions(20);
		}

		//-----------------------------------------------------------------------------------------
		void CPlayer::hitAllChampions(int damages)
		{
			int rnd = damages / 8 + 1;

			sound->play(pos, "sound/Party_Damaged.wav");

			for (int i = 0; i < 4; ++i)
			{
				int dmg = damages - rnd + RANDOM(rnd * 2);
				dmg = MAX(1, dmg);
				game.characters[i].hitChampion(i, dmg);
			}
		}
				

L'autel Vi


Quand vous placez les os d'un champion à l'intérieur d'un autel Vi, le processus de résurrection commence.
On va stocker la position du mur et son coté, et démarrer un timer.

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

			else if (clickedArea->type == eMouseAreaG_DropObject)
			{
				// récupère la position et le coté
				[...]

				// jette l'objet
				CObjectStack*   stack = map.addObjectsStack(pos, side);
				stack->addObject(mouse.mObjectInHand);

				// test si on ressuscite un champion
				if (mouse.mObjectInHand.getType() == OBJECT_TYPE_BONES &&
				    side != eWallSideMax)
				{
					CWall&  wall = map.getTile(pos)->mWalls[side];

					if (wall.getType() == eWallAlcove &&
					    wall.getEnumParam("Type") == 2)
					{
						// ressuscite
						sound->play(pos, "sound/Spell.wav");
						walls.resurrectWallPos = pos;
						walls.resurrectWallSide = side;
						walls.resurrectTimer.set(120, true);
					}
				}
				
Le timer est utilisé pour jouer une animation d'éclair pendant 2 secondes.
Le graphisme de l'éclair vient d'un missile "éclair", mais on parlera des missiles dans une autre partie.
L'animation consiste à retourner cette image d'éclair toutes les 1/2 secondes, et elle est affichée comme un décor
de mur.

		CWalls::COrnateData  alcoveLightning =
		{
			"",
			"gfx/3DView/missiles/Lightning_side.png",
			"",
			{82, 95},
			{0, 0}
		};

		//---------------------------------------------------------------------------------------------
		CRect CWalls::drawOrnate(QImage* image, CVec2 tablePos, EWallSide side, int ornate, int textLines)
		{
			if (ornate != 0)
			{
				int tableSide = walls.getTableSide(side, false);

				if (gWallsInfos[tablePos.y][tablePos.x][tableSide].size.x != 0)
				{
					COrnateData* ornateData = NULL;

					if (ornate == ORNATE_ALCOVE_LIGHTNING)
						ornateData = &alcoveLightning;
					else
						ornateData = &walls.ornatesDatas[ornate];

					[...]

					// dessine les décors à l'écran.
					bool    flipX = false;
					bool    flipY = false;

					if (ornate == ORNATE_ALCOVE_LIGHTNING)
					{
						int image = walls.resurrectTimer.get() / 30;
						flipX = image & 1;
						flipY = (image >> 1) & 1;
					}
					graph2D.drawImage(image, ornatePos, tempImage, 0, flipX, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT), flipY);
				

Explosion


Quand l'animation d'éclair se termine, une explosion apparaît pendant une courte durée.
J'ai créé un nouveau fichier "explosions.cpp" pour gérer ça parce que les explosions et les nuages seront très
utilisés pour les sorts.
Une fonction add() est appelée pour créer une nouvelle explosion et l'ajouter à une liste.
Les paramètres de cette fonction sont la position dans la map, le type d'explosion, et sa durée.

		void CExplosions::add(EExplosions type, CVec2 mapPos, int duration)
		{
			SExplosion  explo;
			explo.type = type;
			explo.mapPos = mapPos;
			explo.timer.set(duration, true);

			mExploList.push_back(explo);

			if (type == eExplo_Resurrect)
				sound->play(mapPos, "sound/Explosion.wav");
		}
				
Une fonction update() gère les timers de toutes les explosions et les retire de la liste quand elles sont
terminées.

		void CExplosions::update()
		{
			std::vector<SExplosion>::iterator   it = mExploList.begin();

			while(it != mExploList.end())
			{
				if (it->timer.update() == true)
					it = mExploList.erase(it);
				else
					it++;
			}
		}
				
La fonction draw() est une version simplifiée de CWalls::drawOrnate()

		void CExplosions::draw(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			for (size_t i = 0; i < mExploList.size(); ++i)
			{
				SExplosion& explo = mExploList[i];

				if (explo.mapPos == mapPos)
				{
					QImage  exploGfx;
					CVec2   pos;
					float   scale = 1.0;

					if (explo.type == eExplo_Resurrect)
					{
						exploGfx = fileCache.getImage("gfx/3DView/explosions/Fireball.png");
						pos = CVec2(57, 66);
						scale = 110.0 / 145.0;
					}

					const SWallInfos*   tabData = &gWallsInfos[tablePos.y][tablePos.x][eWallSideUp];
					if (tabData->size.x != 0)
					{
						const SWallInfos*   refWall = NULL;
						refWall = &gWallsInfos[3][2][eWallSideUp];

						// calcule la position et la taille et dessine l'explosion dans une image temporaire
						scale *= (float)tabData->size.y / 111.0f;

						QSize   scaledSize = exploGfx.size() * scale;
						pos = pos - refWall->pos;
						pos.x = pos.x * tabData->size.x / refWall->size.x;
						pos.y = pos.y * tabData->size.y / refWall->size.y;

						pos += tabData->pos;

						QImage  tempImage(scaledSize, QImage::Format_ARGB32);
						tempImage.fill(TRANSPARENT);
						graph2D.drawImageScaled(&tempImage, CVec2(), exploGfx, scale);

						// assombrit l'explosion en fonction de sa distance
						static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
						float shadow = shadowLevels[WALL_TABLE_HEIGHT - 1 - tablePos.y];

						graph2D.darken(&tempImage, shadow);

						// dessine l'explosion à l'écran.
						graph2D.drawImage(image, pos, tempImage, 0, false, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
					}
				}
			}
		}
				
Quand on crée cette explosion, le champion est aussi ressuscité et récupère la moitié de sa santé de départ.

		void CWalls::update()
		{
			// animation de l'autel vi
			if (resurrectTimer.update() == true)
			{
				// retrouve le numéro du champion
				CObjectStack*   stack = map.findObjectsStack(resurrectWallPos, resurrectWallSide);
				int         last = stack->getSize() - 1;
				CObject&    bones = stack->getObject(last);
				int         champNum = bones.getIntParam("Champion");

				// efface l'objet
				stack->removeObject(last);
				resurrectWallSide = eWallSideMax;

				// ressuscite le champion
				CCharacter::SStat&  health = game.characters[champNum].stats[CCharacter::eStatHealth];
				health.value = health.startValue / 2;

				// lance l'explosion
				explosions.add(eExplo_Resurrect, player.pos, 20);
			}
		}
				
Je n'ai pas regardé le code du jeu d'origine pour voir si d'autres stats du champion changeaient.

Régénération de vie

Comme pour le mana, la santé se régénère lentement pendant le jeu. Et elle se régénère plus vite quand l'équipe
dort.
Donc, comme pour le mana, j'ai utilisé un timer qui est initialisé avec 2 valeurs suivant qu'on dort ou pas.

		#define HEALTH_REGEN_TIMER        (30 * 60)
		#define HEALTH_REGEN_TIMER_SLEEP  90