Part 26: Alcoves and torch holders

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.

Objects on the walls

In the first level there is an alcove in a wall and a torch holder.



They have something in common: they both hold objects in a wall.
For now we only had objects on the ground. An obect stack is defined by its coordinates - the coordinates of the
quarter of tile where it lies.
If we want to put a stack of objects on a wall, we will need the coordinates of its tile and the side of the wall.
So let's add a side to CObjectStack:

		class CObjectStack
		{
			[...]

			CVec2       mPos;
			EWallSide   mSide;

			[...]
		};
				
We will use this convention: Of course, we will have to change the object stacks functions in CMap to take this side into account.

		CObjectStack*   addObjectsStack(CVec2 pos, EWallSide side);
		CObjectStack*   findObjectsStack(CVec2 pos, EWallSide side);
		void            removeObjectsStack(CVec2 pos, EWallSide side);
				
This is not a huge work, as all these functions mostly rely on the findObjectsStackIndex() one, and it's easy to modify
it to find a stack both by its coordinates and its side:

		int CMap::findObjectsStackIndex(CVec2 pos, EWallSide side)
		{
			for (size_t i = 0; i < mObjectsStacks.size(); ++i)
				if (mObjectsStacks[i].mPos == pos && mObjectsStacks[i].mSide == side)
					return i;

			return -1;
		}
				
And obviously, the format of the map file will change a little bit to store this side and the new types of walls we will add.

The alcove wall

In walls.xml we define a new wall:

		<wall name="Alcove">
			<image>Alcove.png</image>
			<param type="enum" values="Arched;Square;Vi Altar">Type</param>
			<param type="stack"/>
		</wall>
				
There are 3 types of alcoves in this game: These are simply displayed like standard wall ornates so we define them in ornates.xml:

		<ornate name="Alcove_Arched">
			<image_front>Alcove_Arched_front.png</image_front>
			<image_side>Alcove_Arched_side.png</image_side>
			<pos_front x="64" y="69"/>
			<pos_side x="34" y="69"/>
		</ornate>

		<ornate name="Alcove_Square">
			<image_front>Alcove_Square_front.png</image_front>
			<image_side>Alcove_Square_side.png</image_side>
			<pos_front x="67" y="69"/>
			<pos_side x="38" y="70"/>
		</ornate>

		<ornate name="Alcove_Vi">
			<image_front>Alcove_Vi_front.png</image_front>
			<image_side>Alcove_Vi_side.png</image_side>
			<pos_front x="64" y="69"/>
			<pos_side x="36" y="70"/>
		</ornate>
				
The second parameter of this type of wall - "stack" - is a sort of "fake" parameter.
We don't want to hardcode in the editor that the walls of type 5 holds a stack of objects, so we need this parameter.
But unlike the other parameters, like bool or int, it will not be associated to a value.
The stack will be stored along with the others and the coordinates of the wall will be sufficient to associate them.
Anyways we need this parameter to tell the editor to display specific buttons in the selection box.

Alcoves in the editor

The alcove informations in the editor will look like this:


The first parameter is the type of alcove - arched, squared or Vi altar.
And the following buttons are exactly the same as an object stack, with buttons to "add" a new object, "delete" an object
in the list, and combo boxes to modify it's type.

The code is also similar to the ground stacks. In addWallParamsItems() we add the buttons to the selection box:

		void    CEditor::addWallParamsItems(uint8_t type)
		{
			[...]

			std::vector<CParamType>& sourceList = map.mWallsParams[type];

			for (size_t i = 0; i < sourceList.size(); ++i)
			{
				[...]

				else if (sourceList[i].mType == eParamEnum)
				{
					CParamEnum*   par = (CParamEnum*)param;
					addLabel(wallSelGrid, sourceList[i].mName + ":", &mWallParamItems);
					addComboBox(wallSelGrid, sourceList[i].mValues, &mWallParamItems, i, par->mValue);
				}
				else if (sourceList[i].mType == eParamStack)
				{
					CObjectStack*   stack = map.findObjectsStack(pos, side);
					size_t          size = 0;

					if (stack != NULL)
					{
						for (size_t i = 0; i < stack->getSize(); ++i)
						{
							addButton(wallSelGrid, "delete", &mWallParamItems, i);
							addComboBox(wallSelGrid, bridge.itemsList, &mWallParamItems, sourceList.size() - 1 + i, stack->getObject(i).getType());
						}
						size = stack->getSize();
					}
					addButton(wallSelGrid, "add", &mWallParamItems, size);
				}
			}
		}
				
In CBridge::setSelParamCombo() we handle the changes of the combo boxes.

		void    CBridge::setSelParamCombo(qint32 id, qint32 value)
		{
			[...]

			else if (mTabIndex == eTabWalls)
			{
				CVec2       pos = editor.mSelectStart / TILE_SIZE;
				EWallSide   side = editor.getWallSideAbs(editor.mSelectStart);
				CWall*      wall = &map.getTile(pos)->mWalls[side];
				bool        isStack = false;

				if ((size_t)id >= wall->mParams.size())
					isStack = true;
				else if (wall->mParams[id]->mType == eParamStack)
					isStack = true;

				if (isStack == true)
				{
					CObjectStack*   stack = map.findObjectsStack(pos, side);

					if (stack != NULL)
					{
						CObject&    object = stack->getObject(id - (wall->mParams.size() - 1));
						object.setType(value);
					}
				}
				else
				{
					CParam*     param = wall->mParams[id];

					switch (param->mType)
					{
						SET_COMBO(eParamOrnate, CParamOrnate)
						SET_COMBO(eParamEnum, CParamEnum)

						default:
							break;
					}
				}
			}

			[...]
		}
				
And in CBridge::selButtonClicked() we handle the "add" and "delete" buttons:

		void    CBridge::selButtonClicked(qint32 id)
		{
			if (id == -1)
				return;

			if (mTabIndex == eTabObjects)
			{
				[...]
			}
			else if (mTabIndex == eTabWalls)
			{
				CVec2       pos = editor.mSelectStart / TILE_SIZE;
				EWallSide   side = editor.getWallSideAbs(editor.mSelectStart);
				CObjectStack*   stack = map.findObjectsStack(pos, side);

				if (stack == NULL || (size_t)id == stack->getSize())
				{
					// add new object
					if (stack == NULL)
						stack = map.addObjectsStack(pos, side);

					CObject object;
					stack->addObject(object);
				}
				else
				{
					// remove object
					stack->removeObject(id);

					if (stack->getSize() == 0)
						map.removeObjectsStack(pos, side);
				}

				// update the buttons
				QMetaObject::invokeMethod(this, "updateSelStack", Qt::QueuedConnection);
			}
		}
				
There is also a lot of changes in "clipboard.cpp" and "tools.cpp" to add the walls' stacks in the copy, cut and paste tools.

Alcoves in the game

As we said, the alcove image is displayed like an ornate, we don't have to add new code to do that, but we still have
to draw the objects.
We draw them in CGame::drawWall():

		else if (wall->getType() == eWallAlcove)
		{
			//------------------------------------------------------------------------------
			// alcove
			int type = wall->getEnumParam("Type");
			walls.drawOrnate(image, tablePos, side, ORNATE_ARCHED_ALCOVE + type);

			if (tablePos.y > 0 && side == eWallSideUp)
			{
				objects.drawObjectsStack(image, mapPos, playerSide, tablePos);

				if (tablePos == CVec2(2, 3) && mouse.mObjectInHand.getType() != 0)
				{
					CRect   rectLeft(CVec2(66, 84), CVec2(157, 124));
					mouse.addArea(eMouseAreaG_DropObject, rectLeft, eCursor_Hand, (void*)mapPos.x, (void*)mapPos.y, (void*)playerSide);
				}
			}
		}
				
Note that the objects are drawn only when the wall is facing us.
We draw the objects by calling the drawObjectsStack() function that I modified to take into account the side of the wall
and the fact that the coordinates are those of a tile, not a quarter of tile.

		void CObjects::drawObjectsStack(QImage* image, CVec2 mapPos, EWallSide side, CVec2 tablePos)
		{
			[...]

			CObjectStack*   stack = map.findObjectsStack(mapPos, side);

			if (stack != NULL)
			{
				for (size_t i = 0; i < stack->getSize(); ++i)
				{
					int type = stack->getObject(i).getType();

					if (type != 0)
					{
						// get object image
						[...]

						// get object position and add a pseudo random value
						CVec2   pos;

						if (side == eWallSideMax)
							pos = getObjectPos(tablePos);
						else
							pos = getAlcovePos(tablePos);
						[...]

						// compute the scale based on the reference position (the nearest)
						CVec2   pos0, pos1;
						float   scale;

						if (side == eWallSideMax)
						{
							pos0 = getObjectPos(CVec2(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2));
							pos1 = getObjectPos(CVec2(WALL_TABLE_WIDTH, tablePos.y));
							scale = (float)(pos1.x - 112) / (float)(pos0.x - 112);
						}
						else
						{
							static float scalesTable[] = {0.36, 0.53, 0.82};
							scale = scalesTable[tablePos.y - 1];
						}

						// scale the object in a temporary image
						[...]

						// darken the object based on it's distance
						float shadow;

						if (side == eWallSideMax)
							shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y) * 0.13f;
						else
							shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y * 2) * 0.13f;
						graph2D.darken(&scaledObject, shadow);

						// draw the object on screen
						[...]

						// add the mouse area
						if (mouse.mObjectInHand.getType() == 0)
						{
							bool    isAreaOK = false;

							if (side == eWallSideMax)
							{
								if (tablePos.x >= WALL_TABLE_WIDTH - 1 && tablePos.x <= WALL_TABLE_WIDTH)
								{
									if (tablePos.y == (WALL_TABLE_HEIGHT - 1) * 2 ||
										(tablePos.y == (WALL_TABLE_HEIGHT - 2) * 2 + 1 && isWallInFront() == false))
									{
										isAreaOK = true;
									}
								}
							}
							else
							{
								if (tablePos.x == WALL_TABLE_WIDTH / 2 && tablePos.y == WALL_TABLE_HEIGHT - 1)
									isAreaOK = true;
							}

							if (isAreaOK)
							{
								CRect   mouseRect(pos, CVec2(pos.x + scaledObject.width() - 1, pos.y + scaledObject.height() - 1));
								mouse.addArea(eMouseAreaG_PickObject, mouseRect, eCursor_Hand, (void*)stack, (void*)i);
							}
						}
					}
				}
			}
		}
				
The position of the object on the screen is computed by a new function:

		CVec2 CObjects::getAlcovePos(CVec2 tablePos)
		{
			CVec2   pos;
			pos.x = 250 * (tablePos.x * 2 - 4) / (8.5 - tablePos.y * 2) + 112;
			static int  yTable[] = {91, 104, 120};
			pos.y = yTable[tablePos.y - 1];
			return pos;
		}
				
As you can see, for the mouse areas I used the same identifiers as for the ground stacks - eMouseAreaG_PickObject and
eMouseAreaG_DropObject. So we have the same behaviour if we click on an object in an alcove or on the ground.

Torch holders in the editor

For the torch holder, our approach will be a lille bit different.
It is not really like an alcove where we can have a full stack of objects.
Here we can only have one object that is always of the same type, so we don't need to add as much buttons as for the alcove.
In the editor we will only need a boolean that tells if the torch holder is empty or if it holds a torch at the beginning.
So here is what we will add to "walls.xml"...

		<wall name="Torch">
			<image>Torch.png</image>
			<param type="bool">isEmpty</param>
		</wall>
				
...and how it appears in the editor:


Torch holders in the game

Although we simplified the interface for the torch holders, in the editor, we will still link them to an object stack
in the game.
As these stacks are not created by the editor, we will need to create them in an init() fnction that will be called right
after we loaded the map:

		void CWalls::init()
		{
			// create stacks for wall torches
			for (int y = 0; y < map.mSize.y; ++y)
				for (int x = 0; x < map.mSize.x; ++x)
				{
					CVec2   pos(x, y);
					CTile*  t = map.getTile(pos);

					for (int side = 0; side < eWallSideMax; ++side)
					{
						CWall* w = &t->mWalls[side];

						if (w->getType() == eWallTorch)
						{
							bool    isEmpty = w->getBoolParam("isEmpty");

							map.removeObjectsStack(pos, (EWallSide)side);

							if (isEmpty == false)
							{
								CObjectStack*   stack = map.addObjectsStack(pos, (EWallSide)side);
								CObject object;
								object.setType(3);
								stack->addObject(object);
							}
						}
					}
				}
		}
				
We simply parse all the "torch walls" in the map and create a stack if their "isEmpty" flag is false.

The torch holders appear in the game as wall ornates.
There are 2 ornates: one when the holder is empty...


...and one when it's not empty:


As for the other walls, this one is drawn in CGame::drawWall(). We draw one ornate or the other depending on the existence
of an object stack associated to the wall:

		else if (wall->getType() == eWallTorch)
		{
			//------------------------------------------------------------------------------
			// torch holder
			CObjectStack*   stack = map.findObjectsStack(mapPos, playerSide);
			CRect   rect = walls.drawOrnate(image, tablePos, side, (stack != NULL ? ORNATE_TORCH_FULL : ORNATE_TORCH_EMPTY));

			if (tablePos == CVec2(2, 3) && side == eWallSideUp)
				mouse.addArea(eMouseAreaG_WallTorch, rect, eCursor_Hand, (void*)mapPos.x, (void*)mapPos.y, (void*)playerSide);
		}
				
Note that I modified drawOrnate so that it returns the rectangle of the ornate it draws. This way we don't have to recompute
it for the mouse area.

Finally in CGame::update() we check the mouse area. Either we pick up the torch if our hand is empty, or if we are holding
a torch and the holder is empty, we "fill" it.

		else if (clickedArea->type == eMouseAreaG_WallTorch)
		{
			CVec2   pos;
			EWallSide   side;

			pos.x = (int)clickedArea->param1;
			pos.y = (int)clickedArea->param2;
			side = (EWallSide)((int)clickedArea->param3);

			int type = mouse.mObjectInHand.getType();
			CObjectStack* stack = map.findObjectsStack(pos, side);

			if (type == 0 && stack != NULL)
			{
				mouse.mObjectInHand = stack->getObject(0);
				map.removeObjectsStack(pos, side);
			}
			else if (type == 3 && stack == NULL)
			{
				stack = map.addObjectsStack(pos, side);
				stack->addObject(mouse.mObjectInHand);
				mouse.mObjectInHand.setType(0);
			}
			characters[interface.selectedChampion].updateLoad();
		}
				

Last words

A few remarks as a conclusion to this part: