Part 28: Game interface - Part 4: Messages, spells and weapons

Downloads

Source code
Executable for the map editor - exactly the same as in part 27 (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.

Messages

The bottom of the screen is used for messages appearing when various events are triggered.


There can be up to 4 lines of text.
The messages appears from the bottom of the screen and scroll up, along with the ones above them.
Each message disappear after about 18 seconds.
And they are written with the color of a champion, or the blue color of the interface.

So let's see how we will implement them in the CInterface class.

		struct SMessage
		{
			std::string text;
			int champion;
			int time;
		};

		std::list<SMessage>   mMessages;
		int     messagePos;
				
We will store the messages' structures in a list.
The message structure contains:
messagePos will hold the position of the last message that is added. We only need one coordinate, as the other messages
will be displayed right above this one.

Adding a message to the list

To add a message to the list, we will write a function that will take as parameters the text string and the number of the
champion used for the color:

		void CInterface::addMessage(std::string text, int champion)
		{
				
First, we have to check if the list already contains 4 lines. If it is the case, we remove the oldest message:

			if (mMessages.size() == 4)
				mMessages.pop_back();
				
Then we simply fill a new message structure with our values and add it to the list.

			SMessage    newMessage;
			newMessage.text = text;
			newMessage.champion = champion;
			newMessage.time = MSG_TIME;
			mMessages.push_front(newMessage);
				
Finally, we set the position of the last message, which is the one we just added.

			messagePos = MSG_START_POS;
		}
				
MSG_START_POS is defined to be "200 * MSG_POS_FACTOR".
MSG_POS_FACTOR is simply a scaling factor as we used for the doors to get a smooth scrolling.
So this message's y coordinate is 200 pixel. That is right outside of the screen.
Later, we will scroll it upwards to make it appear on the screen.

Drawing the messages

To display the messages, we simply loop through the list from the bottom-most messge to the top-most and draw them one
above the other, using the right color.

		void CInterface::drawMessages(QImage* image)
		{
			std::list<SMessage>::iterator it;
			int pos = messagePos;

			for (it = mMessages.begin(); it != mMessages.end(); ++it)
			{
				QColor  color;

				if (it->champion < 4)
				{
					color = QColor(championsColors[it->champion][0],
					               championsColors[it->champion][1],
					               championsColors[it->champion][2]);
				}
				else
				{
					color = MAIN_INTERFACE_COLOR;
				}

				drawText(image, CVec2(0, pos / MSG_POS_FACTOR), eFontStandard, (it->text).c_str(), color);
				pos -= 7 * MSG_POS_FACTOR;
			}
		}
				

Updating the messages

Following the logic we used for the arrows, to handle the scrolling and the timeout, we will write an update function
that will be called every frame.
Note that it is a good practice to call these update functions at the same place, because it will make things easier
when we will implement functions like the pause.

		void CInterface::updateMessages()
		{
			// scrolling
			if (messagePos > MSG_DEST_POS)
				messagePos -= MSG_SCROLL_SPEED;

			if (messagePos < MSG_DEST_POS)
				messagePos = MSG_DEST_POS;

			// decrement times
			std::list<SMessage>::iterator it;

			for (it = mMessages.begin(); it != mMessages.end();)
			{
				it->time--;

				if (it->time == 0)
					it = mMessages.erase(it);
				else
					++it;
			}
		}
				

The resurrect message


The resurrect message, like most of the messages we will see later contains the name of a champion.
We won't add a message to the database for each champion. So we will use a placeholder character that will be replaced
by the champion's name.

		<text id="MESSAGE00"># RESURRECTED.</text>
				
Here is the code to replace the "#" character:

		std::string CInterface::setChampionNameInString(int champion, std::string stringId)
		{
			std::string result = getText(stringId);
			size_t  pos = result.find('#');

			if (pos != std::string::npos)
				result.replace(pos, 1, game.characters[champion].firstName);

			return result;
		}
				
Before adding the text to the list, I had to write another function to find the last champion added to the party - in
fact, this code was already used somewhere else.

		int CInterface::findLastChampion()
		{
			for (int i = 3; i >= 0; --i)
				if (game.characters[i].portrait != -1)
					return i;

			return -1;
		}
				
Now, we can add the message when we resurrect a character in CInterface::update():

		// resurrect button
		case eMouseArea_Resurrect:
		{
			game.lastMirrorClicked->setBoolParam("isEmpty", true);
			isResurrecting = false;
			mainState = eMainGame;
			int champ = findLastChampion();
			std::string message = setChampionNameInString(champ, "MESSAGE00");
			addMessage(message, champ);
		}
		break;
				

The spells area


The spells area is quite complex, with many buttons to click on.
We will cover it in several parts. Each time I will highlight in red the part we are talking about.

But before that, a few words about the variables we will use.
The current caster will be stored in a variable called "currentSpellCaster" in CInterface.
The current spells of each champion will be stored as a characters string "spell" in CCharacter - remember when I write
the text routine, I choose to associate the spells symbols to the lowercase letters.

The drawing of all the elements of this area will take place in the CInterface::drawSpells() function.

The caster selection area


We will draw it in 3 times.
First, we draw the small buttons on the left of the selected caster.

		CCharacter* c = &game.characters[currentSpellCaster];

		for (int i = 0; i < currentSpellCaster; ++i)
		{
			if (isChampionEmpty(i) == false && isChampionDead(i) == false)
			{
				CRect   rect(CVec2(233 + 14 * i, 42),
				             CVec2(244 + 14 * i, 48));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				if (isSpellsGreyed() == false)
					mouse.addArea(eMouseArea_SpellCaster, rect, eCursor_Arrow, (void*)i);
			}
		}
				
Then the large caster's tab with it's name.

		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			CRect   rect(CVec2(233 + 14 * currentSpellCaster, 42),
			             CVec2(277 + 14 * currentSpellCaster, 49));
			graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

			drawText(image, rect.tl + CVec2(2, 2), eFontStandard, c->firstName.c_str(), BLACK);
		}
				
And finally, a loop that looks like the first one for the buttons on the right of the caster.

		for (int i = currentSpellCaster + 1; i < 4; ++i)
		{
			if (isChampionEmpty(i) == false && isChampionDead(i) == false)
			{
				CRect   rect(CVec2(266 + 14 * i, 42),
				             CVec2(277 + 14 * i, 48));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				if (isSpellsGreyed() == false)
					mouse.addArea(eMouseArea_SpellCaster, rect, eCursor_Arrow, (void*)i);
			}
		}
				
In CInterface::update(), we handle the mouse areas we added by simply changing the current caster.

		// change the spell caster
		case eMouseArea_SpellCaster:
			currentSpellCaster = (int)clickedArea->param1;
		break;
				

The symbols buttons


There are 4 differents pages of symbols.
We know in which one we are by counting the characters of the current spell.

		int currentPage = 0;

		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
			currentPage = strlen(c->spell) % 4;
				
Then we draw each symbol like we did in the placeholder we wrote in a previous part.
Note that the symbols change depending on the page number.

		char word[2] = "a";

		for (int i = 0; i < 6; ++i)
		{
			CVec2 pos(239 + i * 14, 54);
			word[0] = 'a' + currentPage * 6 + i;
			drawText(image, pos, eFontStandard, word, MAIN_INTERFACE_COLOR);

			if (isChampionEmpty(currentSpellCaster) == false &&
				isChampionDead(currentSpellCaster) == false &&
				isSpellsGreyed() == false)
			{
				CRect   rect(CVec2(235 + 14 * i, 51),
							 CVec2(247 + 14 * i, 61));
				mouse.addArea(eMouseArea_SpellLetter, rect, eCursor_Arrow, (void*)((int)word[0]));
			}
		}
				
When one of these mouse areas is pressed, we add the corresponding symbol to the spell.
But if the spells already contains 4 symbols, we clear it before.

		// add a symbol to the spell
		case eMouseArea_SpellLetter:
		{
			CCharacter* c = &game.characters[currentSpellCaster];
			if (strlen(c->spell) == 4)
				c->spell[0] = 0;

			int i = strlen(c->spell);
			c->spell[i] = (int)clickedArea->param1;
			c->spell[i + 1] = 0;
		}
		break;
				

The backspace button


It's only a mouse area we have to define.

		// back arrow
		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			CRect   rect(CVec2(305, 63), CVec2(318, 73));
			mouse.addArea(eMouseArea_SpellBack, rect, eCursor_Arrow);
		}
				
When it is clicked we delete the last letter of the spell if it is not empty.

		// spell backspace
		case eMouseArea_SpellBack:
		{
			CCharacter* c = &game.characters[currentSpellCaster];
			int i = strlen(c->spell);

			if (i != 0)
				c->spell[i - 1] = 0;
		}
		break;
				

The "cast spell" button


In this area, we write the current spell.
I added a parameter to the drawText() function to increase the space between the symbols because they were too close
to each other.
Note that I didn't check if the position of the symbols were exactly at the same place as the original game.

		// current spell
		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			drawText(image, CVec2(237, 66), eFontStandard, c->spell, MAIN_INTERFACE_COLOR, 1);

			if (isSpellsGreyed() == false)
			{
				CRect   rect(CVec2(234, 63), CVec2(303, 73));
				mouse.addArea(eMouseArea_CastSpell, rect, eCursor_Arrow);
			}
		}
				
When we click on this button, we cast the spell.
Here I simply write a message. We won't check the spells for now.

		// cast a spell
		case eMouseArea_CastSpell:
		{
			CCharacter* c = &game.characters[currentSpellCaster];

			if (strlen(c->spell) != 0)
			{
				std::string message = setChampionNameInString(currentSpellCaster, "MESSAGE01");
				addMessage(message, 4);
				c->spell[0] = 0;
			}
		}
		break;
				

The weapons area


The weapons area is composed of the weapons buttons, the attacks list and the damages splash.
We will explain them separately, but here is a quick look of the variables that can be found in CInterface:

The weapons buttons

We already had the background rectangle for a long time.

		void    CInterface::drawWeapon(QImage* image, int num)
		{
			if (isWeaponEmpty(num) == false)
			{
				// draw the background rectangle
				CRect   rect(CVec2(233 + 22 * num, 86),
				             CVec2(252 + 22 * num, 120));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);
				
Now to draw the weapons in black, we use the same function as to draw the text with a given color.
You can see that there is a special case when the champion's hand is empty.
We should also avoid drawing some objects. For example, an apple is not a weapon, but we will see that in another part.

				// draw the weapon
				CVec2       pos = CVec2(235 + 22 * num, 96);
				int         weapon = getWeapon(num).getType();
				std::string imageFile;
				int         imageNum;

				if (weapon != 0)
				{
					CObjects::CObjectInfo  object = objects.mObjectInfos[weapon - 1];
					imageFile = object.imageFile.toLocal8Bit().constData();
					imageNum = object.imageNum;
				}
				else
				{
					// empty hand
					imageFile = "gfx/interface/Items6.png";
					imageNum = 9;
				}

				QImage  objImage = fileCache.getImage(imageFile);
				CRect   objRect = getItemRect(imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, objRect, BLACK);
				
Finally, we draw the pattern when the weapon is greyed out, or add a mouse area when it is not.

				// grey out or mouse area
				if (isWeaponGreyed(num) == true)
					graph2D.patternRectangle(image, rect);
				else
					mouse.addArea(eMouseArea_Weapon, rect, eCursor_Arrow, (void*)num);
			}
		}
				
Note that the isWeaponGreyed() function depends on the cooldown variables.
We will see these variables later.

When we test the mouse area in CInterface::update(), we change the state of the weapons area to display
the attack list.

		// click on a weapon
		case eMouseArea_Weapon:
			currentWeapon = (int)clickedArea->param1;
			weaponsAreaState = eWeaponsAreaAttacks;
		break;
				

The attacks' list


The attacks' list shows up to 3 attack for the weapon on which we clicked.
The names of the attacks will change with the weapons. For now I only used the names of the 3 attacks for the
bare hand.
There is not always 3 attacks. That depends on the skills of the character. To simulate that I used a simple
formula: The first champion has 1 attack, the second has 2 attacks, the 3rd has 3 attacks, and the 4th has 1
attack.

Now let's see the drawing function. First, we draw the background image which contains the 3 attack slots.

		void    CInterface::drawAttacks(QImage* image, int num)
		{
			// draw the background image
			QImage  attacksBg = fileCache.getImage("gfx/interface/WeaponsActions.png");
			CVec2   pos(233, 77);
			graph2D.drawImage(image, pos, attacksBg);
				
We hide the unused slots by drawing a black rectangle over them.

			// hide unused slots
			int nbAttacks = (num % 3) + 1;

			if (nbAttacks != 3)
			{
				CRect   rect(CVec2(233, 98 + (nbAttacks - 1) * 12),
							 CVec2(319, 121));
				graph2D.rect(image, rect, BLACK, true);
			}
				
We draw the champion's name at the top of the list

			// draw character's name
			CCharacter* c = &game.characters[num];
			drawText(image, CVec2(235, 79), eFontStandard, c->firstName.c_str(), BLACK);
				
We draw the attacks' names. Note that one of them can be highlighted - we'll see that later.
To draw the highlighted one, we use the function to invert the colors that we used for the arrows.

			// draw attacks' names
			for (int i = 0; i < nbAttacks; ++i)
			{
				static char attackName[16];
				sprintf(attackName, "ATTACK%02d", i);
				drawText(image, CVec2(241, 89 + i * 12), eFontStandard, getText(attackName).c_str(), MAIN_INTERFACE_COLOR);

				if (attackHighlightTime != 0 && currentAttack == i)
				{
					CRect   rect(CVec2(234, 86 + i * 12),
					             CVec2(318, 96 + i * 12));
					graph2D.Xor(image, rect, MAIN_INTERFACE_COLOR);
				}
			}
				
And finally, we set up the mouse areas

			// mouse areas
			if (isWeaponGreyed(num) == false && attackHighlightTime == 0)
			{
				for (int i = 0; i < nbAttacks; ++i)
				{
					CRect   rect(CVec2(234, 86 + i * 12),
					             CVec2(318, 96 + i * 12));
					mouse.addArea(eMouseArea_Attack, rect, eCursor_Arrow, (void*)i);
				}

				CRect   rect(CVec2(290, 77), CVec2(314, 83));
				mouse.addArea(eMouseArea_CloseAttack, rect, eCursor_Arrow);
			}
		}
				
When we click on, one of these mouse areas, we don't leave the list immediately, but the clicked attack is
highlighted for a short period of time.

		// click on an attack
		case eMouseArea_Attack:
			currentAttack = (int)clickedArea->param1;
			attackHighlightTime = ATTACK_HIGHLIGHT_TIME;
		break;
				

As for the messages we write an update function to decrement this counter. When it reaches 0, two things
happen:

		void CInterface::updateWeapons()
		{
			// attack highlight
			if (attackHighlightTime > 0)
			{
				attackHighlightTime--;

				if (attackHighlightTime == 0)
				{
					weaponCoolDown[currentWeapon] = currentAttack * 60;

					if ((rand() % 2) == 0)
					{
						weaponsAreaState = eWeaponsAreaWeapons;
					}
					else
					{
						damages = rand() % 200 + 1;
						damagesDisplayTime = DAMAGES_DISPLAY_TIME;
						weaponsAreaState = eWeaponsAreaDamage;
					}
				}
			}
				

The damages splash


The damages splash is simply displayed for a short amount of time.

		void CInterface::drawDamages(QImage *image)
		{
			QImage  damagesBg = fileCache.getImage("gfx/interface/DamageDone.png");
			static char damagesStr[8];
			sprintf(damagesStr, "%d", damages);

			int     length = strlen(damagesStr);
			float   scaleX = 0.44f + (length - 1) * 0.24f;
			CVec2   pos(258 - (length - 1) * 10, 81);

			graph2D.drawImageScaled(image, pos, damagesBg, scaleX, false, 0.82f);

			CVec2   textPos(274 - (length - 1) * 3, 97);
			drawText(image, textPos, eFontStandard, damagesStr, MAIN_INTERFACE_COLOR);
		}
				
It's timer is also processed in the update function. At the end, we get back to the weapons buttons.

		// damages timer
		if (damagesDisplayTime > 0)
		{
			damagesDisplayTime--;

			if (damagesDisplayTime == 0)
				weaponsAreaState = eWeaponsAreaWeapons;
		}
				

The cooldown

When we get back to the weapons buttons, depending its cooldown value, the one we clicked may still
be greyed out.


The cooldown is also handled in updateWeapons().

		// weapons cooldowns
		for (int i = 0; i < 4; ++i)
			if (weaponCoolDown[i] > 0)
				weaponCoolDown[i]--;
				
We only have to wait that the timer ends to be able to click on it again.