Part 31: Time, light and mana

Downloads

Source code
Executable for the map editor (Windows 32bits)
Executable for the game (Windows 32bits)

Before you try to compile the code go to the "Projects" tab on the left menu, select the "Run" settings for your kit,
and set the "Working directory" to the path of "editor\data" for the editor or "game\data" for the game.

Rethinking the time

Last time I forgot the confirmation box before you quit the game.


I also didn't have time to talk about the time in these dialogs.
While we are int these boxes, choosing to save the game, typing the file name, and so on, we don't want to be attacked
by a monster at the same time. So the game time should be paused.

There are a few ways for the player to act on the time flow in the game.
You can stop the time by pausing the game, or you can sleep to make it run faster - even though after looking at the code
of the original game, it is not clear that anything was faster during sleep except the health/stamina/mana regeneration.

Controling the time is particularly important with spells like "magic torch" that can last for a long time.
In the original game they used what they called a "timeline". It was a global time counter and a list of events that should happen
at a given time.
This method had some advantages, but also drawbacks. For example the main counter could overflow...
We will use a much simpler method where every spell has it's own timer.
But we also will handle the other timers we are already using.

Throughout the code, we used several frame counters that were coded the same way:

		if (counter != 0)
		{
			counter--;
			
			if (counter == 0)
			{
				[...]
			}
		}
				
We will replace them with a new class called CTimer.
Its update function should be familiar:

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

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

				mTime -= speed;

				if (mTime <= 0)
				{
					mTime = 0;
					return true;
				}
			}
			return false;
		}
				
The only difference here is that we use a getGameTimeFactor function that returns us the speed at which the time
flows.

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

			return 1;
		}
				
I also used this function for the speed of the doors, as they don't use timers.
And I coded the pause and the sleep screens:



Light

When you enter level 2 for the first time, if you don't have a torch, you will see that it's dark.
Every level except the first one are dark.
This is what it looked like in the original game.


There was only 6 levels of lighting, but we will use more.
I wrote a darkenRect() function that work as the darken() we already used but only on a rectangle of the 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();
		}
				
We will use it directly on the screen image.

		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));
				[...]

				// light
				if (currentLevel != 1)
				{
					float   level = getLightLevel();

					if (level != MAX_LIGHT_LEVEL)
						graph2D.darkenRect(image, 1.0f - level, mainRect);
				}
			}
		}
				
There are 2 main sources of light in the game: torches and spells - and yes, I forgot the illumulet...

Torches

Torches have a number of "charges" that decrease slowly while they are in a champion's hand.
The light they produce is proportionnal to these charges. So it will slowly decrease.
First we have to add a charges parameter to "items.xml":

		<!-- TORCH -->
		<item name="ITEM003">
			[...]
			<param type="int">Charges</param>
		</item>
				
This implies small modifications to the map editor as it didn't handle int parameters in objects.
But the only torches that we can use are either the one in the ZED's inventory or torches on torch-holders.
These ones do not appear in the editor. So I wrote a function to initialize their charges at the begining.

		void CObjects::setDefaultParams(CObject* obj)
		{
			if (obj->getType() == OBJECT_TYPE_TORCH)
			{
				obj->setIntParam("Charges", TORCH_MAX_CHARGES);
			}
		}
				
To update their charges I had to convert them to a CTimer to use the time control that we talked about.

		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());
			}
			[...]
		}
				
Torches also have 4 different images when you hold them in your hand depending on their level, so I had to
change the way they are displayed.


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

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

				// torch
				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);
			}

			[...]
		}
				
Now, to calculate the light power, the original game took the 5 most powerful torches in the champions' hands.
Well in fact, there was a little bug in the code, and the 5th torch was not always the 5th more powerful...
Then they added the powers, taking the full power of the 1st one, half of the power of the 2nd, a quarter of
the power of the 3rd, and so on.
So here is the code to do that:

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

			// get the powers of the torches in che champions' hands
			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");
			}

			// sort the 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;
					}

			// get the 5 most powerful torches (as in the original game)
			for (int i = 0; i < MIN(numTorches, 5); ++i)
				power += (torches[i] >> i) / 545;

			[...]
		}
				

Light spells

There are 3 spells related to the light: "magic torch", "light" and "darkness".
The light spell is only a stronger magic torch, and the darkness spell reduces the light for a short amount
of time.

Spells produce the same light power during their whole duration. At the end, in the original game, they used
a trick of the timeline to decrease their power quickly but not instantly.
I simulated this with a "decrease time".
But let's see the class we will use to store a spell in the new file "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;
		};
				
The variables name are self explantory:

The getPower() function returns the current power depending on the time elapsed:

		int CSpell::getPower()
		{
			if (time.get() >= decreaseTime)
				return power;
			else
				return (power * time.get()) / decreaseTime;
		}
				
We will keep a list of all the active spells in a new class CSpells:

		class CSpells
		{
		public:
			CSpells();

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

			std::vector<CSpell>    mActiveSpells;
		};

		extern CSpells  spells;
				
The update() function obviously decreases the timer of each spell and removes it from the list when it
gets to 0.
The cast() function will decode the "spell string" of the given champion and add the corresponding spell
to the list.

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

			// magic torch
			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;
			}

			// light
			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;
			}

			// darkness
			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);
		}
				
The spells will never fail - at the moment we don't check the skill level of the caster.
I tried to set power levels and times as close as possible as the original game, but it was difficult
to tell because of the only 6 levels of darkness.

Spells powers are simply added to the torches in getLightLevel():

		// magical spells
		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();
			}
		}
				

Final light calculation

Now that we have the powers of the torches and the magic spells, we can compute the final light level:

		// compute the global light level
		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;
				
Note that I didn't set the MIN_LIGHT_LEVEL to 0 to avoid a completely black screen.
Here is what level 2 looks like with no light at all:


And with a simple torch:

Mana

As we are really casting spells now, I wanted to limit them by taking into account their mana cost.
The cost of each symbol was easy to code thanks to 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;
			}
		}
				
And now that we can lose mana, we also need to regenerate it.
In the original game, the formula to regenerate the mana was quite complex.
It implied 3 different timers, the wizard skill, the wisdom skill, the priest skill, and the maximum mana
value of a character. An it also consumed stamina.
I will perhabs code this one day - once we will have the character's skills at least. But for now I wanted
a quick thing to test.
Anyways, this formula was only important when the character had a high level.

I started the game and played a little bit with ZED and I found that when he was awake, he gained 1 point of
mana every 30 seconds. So I used a simple timer to simulate this:

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

			// mana regeneration
			if (manaRegenTimer.update() == true)
			{
				if (portrait != -1)
					if (stats[eStatMana].value < stats[eStatMana].maxValue)
						stats[eStatMana].value++;

				[...]
					manaRegenTimer.set(MANA_REGEN_TIMER, true);
				[...]
			}
		}
				
But even if the time runs 4 times faster when the champions sleep, that was not enough.
ZED regains its full mana in about 10 seconds while sleeping. That's 30 time the speed of the
regeneration when he's awake.
So I had to set another value for the timer and to write function so that CInterface signals
CCharacter when we enter or leave the sleep mode. And I set the timers to one value or another in
these functions.

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

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

Save and other things

Of course, when we save the game, now we also need to save the list of the active spells.
So I added save and load functions to all the implied classes.
They are not more complex that the other save functions we already saw.

There must be a bug in the save as the file is getting very big when we save in level 2.
I'll have to investigate that.

As I said, I forgot to talk about the illumulet in this part. It will be for another one.

There is also a few other thing to say about the sleep, as it modifies some other parameters.
I.e. the dexterity of the champions are greatly reduced.

But that's already a big part. So, see you soon.