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.