Part 27: Scrolls and texts on the walls
Downloads
Multilines texts
<text id="SCROLL00">INVOKE FUL_FOR A MAGIC_TORCH</text>
<text id="SCROLL01">NEW LIVES_FOR_OLD BONES</text>
Now we need to write a function to split these texts:
std::vector<std::string> CInterface::getTextLines(std::string text)
{
std::vector<std::string> lines;
size_t start = 0;
size_t end = 0;
while (true)
{
end = text.find('_', start);
if (end != std::string::npos)
{
lines.push_back(text.substr(start, end - start));
start = end + 1;
}
else
{
lines.push_back(text.substr(start, end));
break;
}
}
return lines;
}
Here we used std::string functions to break the lines and store them in a vector of std::string.
//------------------------------------------------------------
int CInterface::getCharSpacing(EFonts font)
{
if (font == eFontStandard)
return 6;
else if (font == eFontScroll)
return 6;
else
return 8;
}
//------------------------------------------------------------
#define CHAR_WIDTH 8
void CInterface::drawText(QImage* image, CVec2 pos, EFonts font, const char* text, QColor color)
{
[...]
int spacing = getCharSpacing(font);
[...]
int i = 0;
while (text[i] != 0)
{
int c = codes[text[i] - 32];
CRect rect(CVec2(c * CHAR_WIDTH, 0),
CVec2((c + 1) * CHAR_WIDTH - 1, file.height() - 1));
CVec2 cPos(pos.x + i * spacing, pos.y);
graph2D.drawImageAtlas(image, cPos, file, rect, (font != eFontWalls ? color : QColor(0, 0, 0, 0)));
i++;
}
}
Now, before we write our multi-line function, look again at the result we want to get.
void CInterface::drawTextMultiLines(QImage* image, CVec2 pos, EFonts font, std::string text, QColor color, int height, int lineHeight)
{
std::vector<std::string> lines = getTextLines(text);
// center the text vertically
if (height != 0)
pos.y += (height - lines.size() * lineHeight) / 2;
// draw each line of text
for (size_t i = 0; i < lines.size(); ++i)
{
CVec2 pos2 = pos;
pos2.x -= lines[i].size() * getCharSpacing(font) / 2;
drawText(image, pos2, font, lines[i].c_str(), color);
pos.y += lineHeight;
}
}
We will see that this is not the final version of this function, as we will have to make a small change for the walls.
Objects' parameters
<!-- SCROLL -->
<item name="ITEM105">
[...]
<param type="string">Text</param>
</item>
The objects' parameters work exactly the same as the ones in the tiles and walls:
class CMap
{
[...]
std::vector<std::vector<CParamType>> mTilesParams; // list of the params for each type of tile
std::vector<std::vector<CParamType>> mWallsParams; // list of the params for each type of wall
std::vector<std::vector<CParamType>> mObjectsParams; // list of the params for each type of object
[...]
};
class CObject
{
[...]
std::vector<CParam*> mParams;
[...]
};
Objects' parameters in the editor
void CEditor::addStackParamsItems(CObjectStack* stack, int startId, QQuickItem* parent, std::vector<QQuickItem *> *list)
{
size_t size = 0;
if (stack != NULL)
{
for (size_t i = 0; i < stack->getSize(); ++i)
{
CObject& obj = stack->getObject(i);
int objType = obj.getType();
addButton(parent, "delete", list, i);
addComboBox(parent, bridge.itemsList, list, startId + i, objType);
if (objType != 0)
{
std::vector<CParamType>& sourceList = map.mObjectsParams[objType - 1];
for (size_t j = 0; j < sourceList.size(); ++j)
{
CParam* param = obj.mParams[j];
if (sourceList[j].mType == eParamString)
{
CParamString* par = (CParamString*)param;
addLabel(parent, sourceList[j].mName + ":", list);
addTextField(parent, QString::fromStdString(par->mValue), list, (j + 1) * 0x10000 + i);
}
}
}
}
size = stack->getSize();
}
addButton(parent, "add", list, size);
}
You can see that we use a little trick for the id that we give to the TextField.
<parameter index + 1> * 0x10000 + <object index>
Then in CBridge::setSelParamText() we can retrieve the right parameter when the user changes the text of a scroll:
void CBridge::setSelParamText(qint32 id, QString value)
{
[...]
if (mTabIndex == eTabTiles)
{
[...]
}
else if (mTabIndex == eTabWalls)
{
CVec2 pos = editor.mSelectStart / TILE_SIZE;
EWallSide side = editor.getWallSideAbs(editor.mSelectStart);
if (id >= 0x10000)
{
CObjectStack* stack = map.findObjectsStack(pos, side);
int objNum = id & 0xFFFF;
int paramNum = (id / 0x10000) - 1;
CObject& obj = stack->getObject(objNum);
CParamType paramInfos = map.mObjectsParams[obj.getType() - 1][paramNum];
if (paramInfos.mType == eParamString)
{
obj.setStringParam(paramInfos.mName, value.toLocal8Bit().constData());
}
}
else
{
[...]
}
}
else if (mTabIndex == eTabObjects)
{
[same code for the stacks on the ground]
}
}
Displaying the scrolls
void CInterface::drawScroll(QImage* image, CObject& object)
{
QImage scrollBg = fileCache.getImage("gfx/interface/Scroll.png");
graph2D.drawImage(image, CVec2(80, 85), scrollBg);
if (isPressingEye == true)
{
QImage eye = fileCache.getImage("gfx/interface/Eye.png");
graph2D.drawImage(image, CVec2(83, 90), eye);
}
else
{
QImage arrow = fileCache.getImage("gfx/interface/Arrow.png");
graph2D.drawImage(image, CVec2(83, 90), arrow);
}
std::string text = object.getStringParam("Text");
drawTextMultiLines(image, CVec2(162, 91), eFontScroll, getText(text), SCROLL_TEXT_COLOR, 59, 7);
}
You may have noticed that there is a test where we either draw an eye or an arrow.
void CInterface::drawBodyPart(QImage* image, CVec2 pos, int championNum, CCharacter::EBodyParts part, bool enableArea)
{
CCharacter* c = &game.characters[championNum];
int objType = c->bodyObjects[part].getType();
if (objType != 0)
{
// draw the object in this slot
CObjects::CObjectInfo object = objects.mObjectInfos[objType - 1];
QImage objImage = fileCache.getImage(object.imageFile.toLocal8Bit().constData());
int imageNum = object.imageNum;
// objects opened in hand
if (part == CCharacter::eBodyPartRightHand)
{
if (objType == OBJECT_TYPE_SCROLL)
imageNum = 30;
}
CRect rect = getItemRect(imageNum);
graph2D.drawImageAtlas(image, pos, objImage, rect);
}
[...]
}
Walls' texts
<wall name="Text">
<image>WallText.png</image>
<param type="string">Text</param>
</wall>
Here are the texts for the 2 walls in the first level:
<text id="WALL_TEXT00">HALL OF_CHAMPIONS</text>
<text id="WALL_TEXT01">VI_ALTAR OF_REBIRTH</text>
As for the other types of walls, this one is displayed in CGame::drawWall().
else if (wall->getType() == eWallText)
{
//------------------------------------------------------------------------------
// wall text
std::string text = wall->getStringParam("Text");
text = interface.getText(text);
if (tablePos == CVec2(2, 3) && side == eWallSideUp)
{
interface.drawTextMultiLines(image, CVec2(112, 74), CInterface::eFontWalls, text, QColor(), 0, 11);
}
}
But if you run the code like this, this is what you will see:
else if (wall->getType() == eWallText)
{
[...]
if (tablePos == CVec2(2, 3) && side == eWallSideUp)
{
QImage patch = fileCache.getImage("gfx/3DView/TextPatch.png");
graph2D.drawImage(image, CVec2(111, 97), patch);
interface.drawTextMultiLines(image, CVec2(112, 74), CInterface::eFontWalls, text, QColor(), 0, 11);
}
}
void CInterface::drawTextMultiLines(QImage* image, CVec2 pos, EFonts font, std::string text, QColor color, int height, int lineHeight)
{
[...]
// draw each line of text
for (size_t i = 0; i < lines.size(); ++i)
{
[...]
drawText(image, pos2, font, lines[i].c_str(), color);
pos.y += lineHeight;
if (font == eFontWalls && i == 1)
pos.y += 3;
}
}
And see what we get:
Text ornate
else if (wall->getType() == eWallText)
{
[...]
if (tablePos == CVec2(2, 3) && side == eWallSideUp)
{
[...]
}
else
{
std::vector<std::string> lines = interface.getTextLines(text);
walls.drawOrnate(image, tablePos, side, ORNATE_TEXT, lines.size());
}
}
And there is another little trick here.
CRect CWalls::drawOrnate(QImage* image, CVec2 tablePos, EWallSide side, int ornate, int textLines)
{
if (ornate != 0)
{
[...]
if (gWallsInfos[tablePos.y][tablePos.x][tableSide].size.x != 0)
{
[...]
// calculate the position and scale factor and draw the ornate in a temporary image
float scale = (float)ornateTabData->size.y / 111.0f;
QImage ornateImage = fileCache.getImage(ornateFileName.toLocal8Bit().constData());
if (ornate == ORNATE_TEXT)
{
CRect rect;
rect.tl = CVec2(0, 0);
rect.br.x = ornateImage.width() - 1;
if (side == eWallSideUp)
{
static int heights[] = {0, 14, 27, 41, 55};
rect.br.y = heights[textLines];
}
else
{
static int heights[] = {0, 10, 20, 31, 41};
rect.br.y = heights[textLines];
}
ornateImage = graph2D.cropImage(ornateImage, rect);
}
[...]
Here we use a new function of graph2D that crops an image to a given rect:
QImage CGraphics2D::cropImage(const QImage& srcImage, CRect rect)
{
QRect qrect(rect.tl.x, rect.tl.y, rect.br.x - rect.tl.x + 1, rect.br.y - rect.tl.y + 1);
return srcImage.copy(qrect);
}