Partie 31: Temps, lumière et mana

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.

Repenser le temps

La dernière fois j'ai oublié la boîte de confirmation quand on quitte le jeu.


Je n'ai aussi pas eu le temps de parler du temps pendant ces dialogues.
Tant qu'on est dans ces boîtes, qu'on choisit de sauvegarder le jeu, qu'on écrit le nom du fichier, etc., on ne
veut pas être attaqué par un monstre. donc, le temps devrait être arrêté.

Il y a plusieurs façon pour le joueur d'influer sur l'écoulement du temps dans le jeu.
Vous pouvez le stopper en mettant le jeu en pause, ou vous pouvez dormir pour le faire passer plus vite (même si en
regardant le code du jeu d'origine, ce n'est pas clair que quoi que ce soit allait plus vite à part la régénération
de la vie, de la vigueur et du mana).

Contrôler le temps est très important en particulier avec des sorts comme la torche magique qui peut durer pendant
longtemps.
Dans le jeu d'origine, ils utilisaient ce qu'ils appelaient une "timeline". C'était un compteur de temps global et
une liste d'évènements qui devaient arriver à un instant donné.
Cette méthode avait certains avantages, mais aussi des inconvénients. Par exemple, le compteur général pouvait
déborder...
On va utiliser une méthode plus simple où chaque sort aura son propre timer.
Mais on gérera aussi les autres timers qu'on a déjà utilisés.

Tout au long du code, on a utilisé différents compteurs de frames que l'on a programmés de la même façon:

		if (counter != 0)
		{
			counter--;
			
			if (counter == 0)
			{
				[...]
			}
		}
				
On va les remplacer par une nouvelle classe appelée CTimer.
Sa fonction de mise à jour devrait vous paraître familière:

		bool CTimer::update()
		{
			int speed = 1;

			if (mTime != 0)
			{
				if (mFollowGameTime)
					speed = getGameTimeFactor();

				mTime -= speed;

				if (mTime <= 0)
				{
					mTime = 0;
					return true;
				}
			}
			return false;
		}
				
La seule différence ici, c'est qu'on a utilisé une fonction getGameTimeFactor() qui nous renvoie la vitesse à
laquelle le temps s'écoule.

		int CTimer::getGameTimeFactor()
		{
			if (interface.isGamePaused() == true)
				return 0;
			else if (interface.isSleeping() == true)
				return 4;

			return 1;
		}
				
J'ai aussi utilisé cette fonction pour la vitesse des portes, comme elle n'utilisent pas de timer.
Et j'ai codé les écrans de pause et de sommeil:



Lumière

Quand vous entrez dans le niveau 2 pour la première fois, si vous n'avez pas de torche, vous verrez qu'il fait noir.
Tous les niveaux sont dans le noir, excepté le premier.
Ca ressemblait à ça dans le jeu d'origine.


Il y avait seulement 6 niveaux de lumière, mais on va en utiliser plus.
J'ai écrit une fonction darkenRect() qui fonctionne comme la fonction darken() qu'on a déjà utilisé mais seulement
sur une partie de l'image.

		void    CGraphics2D::darkenRect(QImage* image, float opacity, CRect destRect)
		{
			QImage shadowImage(image->width(), image->height(), QImage::Format_ARGB32);
			shadowImage.fill(QColor(0, 0, 0, 0));
			rect(&shadowImage, destRect, QColor(0, 0, 0), true);

			QPainter painter(image);
			painter.setOpacity(opacity);
			painter.setCompositionMode(QPainter::CompositionMode_SourceAtop);
			painter.drawImage(QPoint(0, 0), shadowImage);
			painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
			painter.setOpacity(1.0f);
			painter.end();
		}
				
On va l'utiliser directement sur l'écran.

		void CGame::displayMainView(QImage* image)
		{
			if (interface.isInInventory() == false &&
				interface.isGamePaused() == false &&
				interface.isSleeping() == false)
			{
				CRect   mainRect(CVec2(0, 33), CVec2(MAINVIEW_WIDTH - 1, 33 + MAINVIEW_HEIGHT - 1));
				[...]

				// lumière
				if (currentLevel != 1)
				{
					float   level = getLightLevel();

					if (level != MAX_LIGHT_LEVEL)
						graph2D.darkenRect(image, 1.0f - level, mainRect);
				}
			}
		}
				
Il y a 2 source principales de lumière dans le jeu: les torches et les sorts (ah oui, j'ai oublié l'illumulette...).

Les torches

Les torches ont un nombre de "charges" qui diminue lentement pendant qu'elles sont dans la main d'un champion.
La lumière qu'elles produisent est proportionnelle à ces charges. Donc elle va lentement décroitre.
D'abord il faut qu'on ajoute un paramètre "charges" à "items.xml":

		<!-- TORCHE -->
		<item name="ITEM003">
			[...]
			<param type="int">Charges</param>
		</item>
				
Ca implique de petites modifications dans l'éditeur de niveau comme il ne gérait pas les paramètres int dans les
objets.
Mais les seules torches qu'on peut utiliser sont soit dans l'inventaire de ZED ou sur les porte-torches.
Celles-ci n'apparaissent pas dans l'éditeur. Alors j'ai écrit une fonction pour initialiser leurs charges au début.

		void CObjects::setDefaultParams(CObject* obj)
		{
			if (obj->getType() == OBJECT_TYPE_TORCH)
			{
				obj->setIntParam("Charges", TORCH_MAX_CHARGES);
			}
		}
				
Pour mettre à jour leurs charges j'ai du les convertir en CTimer pour utiliser le contrôle du temps dont on a parlé
plus haut.

		void CCharacter::update()
		{
			if (bodyObjects[eBodyPartLeftHand].getType() == OBJECT_TYPE_TORCH)
			{
				CTimer  t;
				t.set(bodyObjects[eBodyPartLeftHand].getIntParam("Charges"), true);
				t.update();
				bodyObjects[eBodyPartLeftHand].setIntParam("Charges", t.get());
			}

			if (bodyObjects[eBodyPartRightHand].getType() == OBJECT_TYPE_TORCH)
			{
				CTimer  t;
				t.set(bodyObjects[eBodyPartRightHand].getIntParam("Charges"), true);
				t.update();
				bodyObjects[eBodyPartRightHand].setIntParam("Charges", t.get());
			}
			[...]
		}
				
Les torches ont aussi 4 images différentes quand vous les tenez dans la main, qui dépendent de leur niveau, alors
j'ai du changer la façon dont elles sont affichées.


		void    CInterface::drawBodyPart(QImage* image, CVec2 pos, int championNum, CCharacter::EBodyParts part, bool enableArea)
		{
			[...]

			if (objType != 0)
			{
				[...]

				// torche
				if (part == CCharacter::eBodyPartRightHand ||
					part == CCharacter::eBodyPartLeftHand)
				{
					if (objType == OBJECT_TYPE_TORCH)
					{
						int charges = obj->getIntParam("Charges");

						if (charges > 0)
							imageNum = 5 + ((charges - 1) * 3) / TORCH_MAX_CHARGES;
					}
				}

				CRect   rect = getItemRect(imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, rect);
			}

			[...]
		}
				
Maintenant pour calculer la puissance de la lumière, le jeu d'origine prenait les 5 torches les plus puissantes
dans les mains des champions.
Mais en fait il y a vait un petit bug dans le code, et la 5ième torche n'était pas toujours la 5ième plus
puissante...
Ensuite, ils ajoutaient les puissances, en prenant la pleine puissance de la 1ère, la moitié de la puissance de la
2ième, le quart de la puissance de la 3ième, etc..
Voila le code pour faire ça:

		float CGame::getLightLevel()
		{
			int power = 0;

			// gère la puissance des torches dans les amins des champions
			int torches[8];
			int numTorches = 0;
			CObject*    obj;

			for (int i = 0; i < 4; ++i)
			{
				CCharacter* c = &game.characters[i];

				obj = &c->bodyObjects[CCharacter::eBodyPartLeftHand];
				if (obj->getType() == OBJECT_TYPE_TORCH)
					torches[numTorches++] = obj->getIntParam("Charges");

				obj = &c->bodyObjects[CCharacter::eBodyPartRightHand];
				if (obj->getType() == OBJECT_TYPE_TORCH)
					torches[numTorches++] = obj->getIntParam("Charges");
			}

			// trie les torches
			for (int i = 0; i < numTorches - 1; ++i)
				for (int j = i + 1; j < numTorches; ++j)
					if (torches[i] < torches[j])
					{
						int temp = torches[i];
						torches[i] = torches[j];
						torches[j] = temp;
					}

			// prend les 5 torches les plus puissantes (comme dans le jeu d'origine)
			for (int i = 0; i < MIN(numTorches, 5); ++i)
				power += (torches[i] >> i) / 545;

			[...]
		}
				

Sorts de lumière

Il y a 3 sorts en relation avec la lumière: "torche magique", "lumière" et "obscurité".
Le sort lumière est simplement une torche magique plus forte, et le sort obscurité réduit la lumière pendant un
court instant.

Les sorts produisent la même quantité de lumière pendant toute leur durée. A la fin, dans le jeu d'origine, ils
utilisaient une astuce de la timeline pour réduire leur luminosité rapidement, mais pas instantanément.
J'ai simulé ça avec un "temps de décroissance".
Mais regardons la classe qu'on va utiliser pour stocker un sort dans le nouveau fichier "spells.h":

		enum ESpells
		{
			eSpell_MagicTorch,
			eSpell_Light,
			eSpell_Darkness,
			eSpell_Count
		};

		class CSpell
		{
		public:
			int     getPower();
			[...]

			ESpells spell;
			int     power;
			CTimer  time;
			int     decreaseTime;
		};
				
Les noms des variables parlent d'eux-même:

La fonction getPower() renvoie la puissance courante en fonction du temps écoulé:

		int CSpell::getPower()
		{
			if (time.get() >= decreaseTime)
				return power;
			else
				return (power * time.get()) / decreaseTime;
		}
				
On va stocker une liste de tous les sorts actifs dans une nouvelle classe CSpells:

		class CSpells
		{
		public:
			CSpells();

			void    cast(int champion);
			void    update();
			[...]

			std::vector<CSpell>    mActiveSpells;
		};

		extern CSpells  spells;
				
Evidemment, la fonction update() décrémente les timers de chaque sort et les retire de la liste quand ils arrivent
à 0.
La fonction cast() va décoder la "chaîne" du sort du champion donné et ajouter le sort correspondant à la liste.

		void CSpells::cast(int champion)
		{
			CCharacter* c = &game.characters[champion];
			char*   spell = c->spell;
			int     level = spell[0] - 'a';

			// torche magique
			if (strcmp(&spell[1], "j") == 0)
			{
				CSpell  s;
				s.spell = eSpell_MagicTorch;
				s.power = (level + 4) * 16;
				s.decreaseTime = (level + 4) * 60;
				s.time.set(41250 + level * 8000 + s.decreaseTime, true);
				mActiveSpells.push_back(s);
				return;
			}

			// lumière
			else if (strcmp(&spell[1], "ipw") == 0)
			{
				CSpell  s;
				s.spell = eSpell_MagicTorch;
				s.power = (level * 2 + 4) * 16;
				s.decreaseTime = (level * 2 + 4) * 60;
				s.time.set(156250 + level * 32000 + s.decreaseTime, true);
				mActiveSpells.push_back(s);
				return;
			}

			// ténèbres
			else if (strcmp(&spell[1], "kpx") == 0)
			{
				CSpell  s;
				s.spell = eSpell_MagicTorch;
				s.power = -(level + 3) * 16;
				s.decreaseTime = (level + 3) * 60;
				s.time.set(1531 + s.decreaseTime, true);
				mActiveSpells.push_back(s);
				return;
			}

			std::string message = interface.setChampionNameInString(champion, "MESSAGE01");
			interface.addMessage(message, 4);
		}
				
Les sorts ne rateront jamais (pour le moment on ne teste pas le niveau de compétence du lanceur).
J'ai essayé de positionner le niveau des sorts et les temps le plus proche possible du jeu d'origine, mais c'était
difficile à cause des simples 6 niveaux d'obscurité.

La puissance des sorts est simplement ajoutée aux torches dans getLightLevel():

		// sorts
		for (size_t i = 0; i < spells.mActiveSpells.size(); ++i)
		{
			CSpell* s = &spells.mActiveSpells[i];

			if (s->spell == eSpell_MagicTorch ||
				s->spell == eSpell_Light ||
				s->spell == eSpell_Darkness)
			{
				power += s->getPower();
			}
		}
				

Calcul final de la lumière

Maintenant qu'on a la puissance des torches et des sorts, on peut calculer la puissance finale de la lumière:

		// calcule le niveau global de lumière
		float   level = ((float)power / 256.0f) * (MAX_LIGHT_LEVEL - MIN_LIGHT_LEVEL) + MIN_LIGHT_LEVEL;

		if (level > MAX_LIGHT_LEVEL)
			level = MAX_LIGHT_LEVEL;

		return level;
				
Remarquez que je n'ai pas mis MIN_LIGHT_LEVEL à 0 pour éviter d'avoir un écran complètement noir.
Voila à quoi ressemble le niveau 2 sans aucune lumière:


Et avec une seule torche:

Mana

Comme on lance vraiment des sorts maintenant, je voulais les limiter en prenant en compte leur coût en mana.
Le coût de chaque symbole était facile à programmer grâce à Dungeon Master Encyclopaedia:

		int CSpells::getMana(int champion, char symbol)
		{
			CCharacter* c = &game.characters[champion];
			char*   spell = c->spell;

			if (spell[0] == 0)
			{
				return symbol - 'a' + 1;
			}
			else
			{
				int     level = spell[0] - 'a';
				static const int    costs[] =
					{
						2,  // g = Ya
						3,  // h = Vi
						4,  // i = Oh
						5,  // j = Ful
						6,  // k = Des
						7,  // l = Zo
						4,  // m = Ven
						5,  // n = Ew
						6,  // o = Kath
						7,  // p = Ir
						7,  // q = Bro
						9,  // r = Gor
						2,  // s = Ku
						2,  // t = Ros
						3,  // u = Dain
						4,  // v = Neta
						6,  // w = Ra
						7   // x = Sar
					};

				return (costs[symbol - 'g'] * (level + 2)) / 2;
			}
		}
				
Et maintenant qu'on peut perdre du mana, on doit aussi le régénérer.
Dans le jeu d'origine, la formule pour le régénérer était plutôt complexe.
Elle impliquait 3 timer différents, la compétence en sorcellerie, la sagesse, le niveau de prêtrise et la valeur
maxi du mana du personnage. Et ça consommait aussi de la vigueur.
Je coderai peut-être ça un jour (quand on aura le compétences des personnages au moins). Mais pour l'instant, je
voulais faire un truc rapide pour tester.
De toute façon cette formule n'est vraiment importante que quand le personnage a un haut niveau.

J'ai démarré le jeu d'origine et j'ai joué un peu avec ZED. J'ai remarqué que quand il était réveillé, il gagnait 1
point de mana point de mana toutes les 30 secondes, alors j'ai utilisé un simple timer pour simuler ça:

		void CCharacter::update()
		{
			[...]

			// régénération de mana
			if (manaRegenTimer.update() == true)
			{
				if (portrait != -1)
					if (stats[eStatMana].value < stats[eStatMana].maxValue)
						stats[eStatMana].value++;

				[...]
					manaRegenTimer.set(MANA_REGEN_TIMER, true);
				[...]
			}
		}
				
Mais même si le temps s'écoule 4 fois plus vite quand les champions dorment, ça ne a suffisait pas.
ZED récupère tout son mana en environ 10 secondes quand il dort. C'est 30 fois sa vitesse de régénération quand il
est réveillé.
Alors j'ai du mettre une autre valeur pour le timer et écrire des fonctions pour que CInterface signale à
CCharacter quand on entre ou qu'on sort du sommeil. Et je met le timer à l'une ou l'autre valeur dans ces fonctions.

		void CCharacter::enterSleep()
		{
			manaRegenTimer.set(MANA_REGEN_TIMER_SLEEP, true);
		}

		void CCharacter::exitSleep()
		{
			manaRegenTimer.set(MANA_REGEN_TIMER, true);
		}
				

Sauvegarde et choses diverses

Bien sur, quand on sauve le jeu maintenant on doit aussi sauver la liste des sorts actifs.
Donc j'ai ajouté des fonctions load() et save() dans toutes les classes concernées.
Elles ne sont pas plus compliquées que les autres fonctions de sauvegarde qu'on a déjà vues.

Il doit y avoir un bug dans la sauvegarde car le fichier devient très gros quand on sauvegarde dans le niveau 2.
Il faudra que j'enquête sur ça.

Comme je l'ai dit, j'ai oublié de parler de l'illumulette dans cette partie. Ca sera pour une prochaine.

Il y a encore d'autres choses à dire à propos du sommeil, car il modifie d'autres paramètres.
Par exemple, la dextérité des champions est grandement réduite.

Mais c'était déjà une grosse partie. A bientôt.