Part 27: Scrolls and texts on the walls

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.

Multilines texts

In this part, we will talk about the texts on the scrolls and on the walls.



These texts are special. When they are displayed, they are split into several lines.
At first I thought I would use a width parameter and split the text when it is larger than this width using a word-
wrapping algorithm.
But I changed my mind when I saw this text in an emulator:


Here we can see that in the original game, they did not use an algorithm to limit the width of the text.
If they did so, the second line of the text would at least contain "FOR OLD" because it is shorter than the first line.
They must have inserted line breaks by hand in the source text.
So that's what I did. I chose to use the "_" character as a line break and here are what the texts of the 2 scrolls in the
first level look like:

		<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.

Before writing a function to display that strings vector, I wanted to clean up a little bit the drawText() function.
until now, it used an "8" value which denoted the width of the characters in the font image. I replaced this value
by a define "CHAR_WIDTH".
It also used a strange variable called "offset" which was substracted from CHAR_WIDTH to get the spacing between 2 characters
on the screen. I replaced it by a function.
So here is the "cleaned up code":

		//------------------------------------------------------------
		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.
You will notice that each line of text is centered horizontally.
Additionally for the scrolls the text is centered vertically in the scroll image.
So our function will need a parameter called "height" to know the height of the scroll. In the case of a wall, we will set
this parameter to 0.
Additionally we will need a parameter for the height of a line - the value that we will add to the y coordinate between
each line and that will be different for the wall and the scroll fonts.
Apart from those parameters, the function will have the same parameters as the drawText() function: position, font, color...
But as the lines are centered horizontally, the x component of the position we will give will be the position of the "center"
line of the text.


So here is the code of our function:

		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

Before we can display the scrolls' texts we neeed to define them in the editor.
Until now, the "items.xml" in the editor only contained the names of the objects.
We will now use the same file as in the game and the objects names will be in a "texts.xml" file - we didn't have one in
the editor.
So these 2 files will work exactly as in the game.

Then we will need to add a "Text" parameter to the scrolls in "items.xml".

		<!-- 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:

Objects' parameters in the editor

The parameters will appear under each object name:


The routine to create these buttons was duplicated for the objects stacks on the ground and in the walls. So I put it
in a new function:

		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.
We will need to retrieve both the index of the object in the stack and the index of the parameter in this object.
So the id has this form:

	<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

Drawing the scroll is simple. We just have to draw the background image and call our multi-line text function.
This is done in the drawScroll() function:

		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.
That is because there are two ways to read a scroll:
In the first case, drawScroll() is called by drawInfos().
And in the second case, it is called by drawInventory().
In this case, we also need to add a text in drawBodyPart() to display the opened scroll in the hand:

		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

For the walls with text we create a new type of walls in "walls.xml":

		<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().
Note that we only draw it if we are right in front of the wall:

		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:


Obviously the wall's graphic is annoying to read the text correcly.
Expecially for the 3rd line.
In the original game there was a trick: a part of the wall was copied over the vertical line between the "bricks".
I did not found an image for that in the original graphics, so I created one myself - it's "TextPatch.png" in
"gfx/3DView".
Once we draw it, this is the result:

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


The 3rd line is still a bit misplaced so let's modify drawTextMultiLines() to lower it:

		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

When we are not right in front of the text, it appears as an unreadable ornate image.


		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.
In the original game, the size of this ornate was based on the number of lines of text.


So we will have to modify drawOrnate to achieve this:

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