Part 28: Game interface - Part 4: Messages, spells and weapons
Downloads
Messages
				
		struct SMessage
		{
			std::string text;
			int champion;
			int time;
		};
		std::list<SMessage>   mMessages;
		int     messagePos;
				
				We will store the messages' structures in a list.
Adding a message to the list
		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".
Drawing the messages
		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
		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
				
		<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
		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 caster selection area
				
		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
				
		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.
		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.
		// 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
				
		// 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
				
		// 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.
		// 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 buttons
		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.
				// 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.
		// click on a weapon
		case eMouseArea_Weapon:
			currentWeapon = (int)clickedArea->param1;
			weaponsAreaState = eWeaponsAreaAttacks;
		break;
				
			
			
			
The attacks' list
				
		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.
			// 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
		// click on an attack
		case eMouseArea_Attack:
			currentAttack = (int)clickedArea->param1;
			attackHighlightTime = ATTACK_HIGHLIGHT_TIME;
		break;
				
				
				
		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
				
		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
				
		// 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.