Part 24: Objects - Part 4: In the character's sheet

Downloads

Source code
Executable for the map editor - exactly the same as in part 21 (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 positions

We can't put objects anywhere in the character's sheet.
A pair of boots won't go on your head, some objects are too large to fit in your pouch, etc...
So each object will be associated with a list of possible positions.
First, let's see all the possible positions.
This image comes from the Dungeon Master Encyclopaedia


You can see that each object slot is part of a specific group.
To ease things in the code, we will identify these groups by a single letter:
The list of positions where a given object can go will be stored as a string in items.xml, and each character of this
string will be one of the list above.

		<items>
			<!-- ======================== Weapons ======================== -->
			<!-- EYE OF TIME -->
			<item name="ITEM001">
				<category>Weapon</category>
				<floor_image>00W_Ring.png</floor_image>
				<image_file>Items0.png</image_file>
				<image_num>17</image_num>
				<position>hBCP</position>
			</item>

			[...]
		</items>
				

Body parts

Now let's change a few things in "characters.h".
Until now we had an enum with all the body parts of the character. We will complete it with all the possible objects slots,
including pouch, quiver, and backpack - but not the chest as we will cover it in another part.

		#define BACKPACK_SIZE   17

		class CCharacter
		{
		public:
			enum EBodyParts
			{
				eBodyPartHead = 0,
				eBodyPartNeck,
				eBodyPartTorso,
				eBodyPartLeftHand,
				eBodyPartRightHand,
				eBodyPartLegs,
				eBodyPartFeet,
				eBodyPartPouch1,
				eBodyPartPouch2,
				eBodyPartQuiver1,
				eBodyPartQuiver2,
				eBodyPartQuiver3,
				eBodyPartQuiver4,
				eBodyPartBackpack,
				eBodyPartCount = eBodyPartBackpack + BACKPACK_SIZE
			};
				
The array that holds the objects of our character will change too. previously we had a placeholder array of ints, now we
will change it to an array of CObject.

		CObject     bodyObjects[eBodyPartCount];
				

Defining the objects slots

Now let's define the objects slots in the character's sheet.
We will need their position, a character to define their types following the list we gave at the beginning, and an int to
tell if we have to draw a default sprite when there is no objects.
These default sprites are for the body outline, the pouch, the quiver and the backpack graphics.


As these sprites are all in the same sheet, we only need an int for the sprite number.

So in inventory.h, we will have an array of structures like that:

		class CInterface
		{
		public:
			[...]

			struct SBodyInfos
			{
				CVec2   pos;
				char    type;
				int     defaultImage;
			};

			[...]

		private:
			[...]
			SBodyInfos  bodyInfos[CCharacter::eBodyPartCount];
		};
				
This array is initialized in the constructor of CInterface:

		CInterface::CInterface()
		{
			[...]

			// initialize the positions of objects in the character's sheet
			bodyInfos[CCharacter::eBodyPartHead].pos      = CVec2(34,  59);
			bodyInfos[CCharacter::eBodyPartNeck].pos      = CVec2( 6,  66);
			bodyInfos[CCharacter::eBodyPartTorso].pos     = CVec2(34,  79);
			bodyInfos[CCharacter::eBodyPartLeftHand].pos  = CVec2( 6,  86);
			bodyInfos[CCharacter::eBodyPartRightHand].pos = CVec2(62,  86);
			bodyInfos[CCharacter::eBodyPartLegs].pos      = CVec2(34,  99);
			bodyInfos[CCharacter::eBodyPartFeet].pos      = CVec2(34, 119);
			bodyInfos[CCharacter::eBodyPartPouch1].pos    = CVec2( 6, 106);
			bodyInfos[CCharacter::eBodyPartPouch2].pos    = CVec2( 6, 123);
			bodyInfos[CCharacter::eBodyPartQuiver1].pos   = CVec2(62, 106);
			bodyInfos[CCharacter::eBodyPartQuiver2].pos   = CVec2(79, 106);
			bodyInfos[CCharacter::eBodyPartQuiver3].pos   = CVec2(62, 123);
			bodyInfos[CCharacter::eBodyPartQuiver4].pos   = CVec2(79, 123);

			bodyInfos[CCharacter::eBodyPartHead].type      = 'H';
			bodyInfos[CCharacter::eBodyPartNeck].type      = 'N';
			bodyInfos[CCharacter::eBodyPartTorso].type     = 'T';
			bodyInfos[CCharacter::eBodyPartLeftHand].type  = 'h';
			bodyInfos[CCharacter::eBodyPartRightHand].type = 'h';
			bodyInfos[CCharacter::eBodyPartLegs].type      = 'L';
			bodyInfos[CCharacter::eBodyPartFeet].type      = 'F';
			bodyInfos[CCharacter::eBodyPartPouch1].type    = 'P';
			bodyInfos[CCharacter::eBodyPartPouch2].type    = 'P';
			bodyInfos[CCharacter::eBodyPartQuiver1].type   = 'Q';
			bodyInfos[CCharacter::eBodyPartQuiver2].type   = 'q';
			bodyInfos[CCharacter::eBodyPartQuiver3].type   = 'q';
			bodyInfos[CCharacter::eBodyPartQuiver4].type   = 'q';

			for (int i = 0; i < BACKPACK_SIZE; ++i)
			{
				int x = 66 + ((i + 1) % 9) * 17;
				int y = 49 + ((i + 1) / 9) * 17;
				bodyInfos[CCharacter::eBodyPartBackpack + i].pos = CVec2(x, y);
				bodyInfos[CCharacter::eBodyPartBackpack + i].type = 'B';
			}

			// default images for the body parts
			for (int i = 0; i < CCharacter::eBodyPartCount; ++i)
				bodyInfos[i].defaultImage = -1;

			bodyInfos[CCharacter::eBodyPartHead].defaultImage         = 24;
			bodyInfos[CCharacter::eBodyPartNeck].defaultImage         = 16;
			bodyInfos[CCharacter::eBodyPartTorso].defaultImage        = 26;
			bodyInfos[CCharacter::eBodyPartLeftHand].defaultImage     = 20;
			bodyInfos[CCharacter::eBodyPartRightHand].defaultImage    = 22;
			bodyInfos[CCharacter::eBodyPartLegs].defaultImage         = 28;
			bodyInfos[CCharacter::eBodyPartFeet].defaultImage         = 30;
			bodyInfos[CCharacter::eBodyPartPouch1].defaultImage       = 17;
			bodyInfos[CCharacter::eBodyPartQuiver1].defaultImage      = 18;
			bodyInfos[CCharacter::eBodyPartBackpack + 8].defaultImage = 19;
		}
				

Drawing and mouse area

I wrote a function to draw an object in a slot and define it's mouse area.

		void    CInterface::drawBodyPart(QImage* image, CVec2 pos, int championNum, CCharacter::EBodyParts part, bool enableArea)
				
It starts with the drawing part. We check if there is an object int the bodyObjects array for the given character.
If there is an object, we draw it like we drew the cursor in the last part.
If there is no object in this slot, we draw the default image defined in bodyInfos.
I will explain later why we have to pass the position as a parameter instead of reading it directly from bodyInfos
in this function.

		{
			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());
				CRect   rect = getItemRect(object.imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, rect);
			}
			else if (bodyInfos[part].defaultImage != -1)
			{
				// draw the default image
				QImage  bodyParts = fileCache.getImage("gfx/interface/Items6.png");
				CRect   rect = getItemRect(bodyInfos[part].defaultImage);
				graph2D.drawImageAtlas(image, pos, bodyParts, rect);
			}
				
The second part of the function defines the mouse area.
Here, we have to check if the object we are holding can go in this slot by testing if the character that defines
the type of this slot is contained in the "positions" string of the object.
An additionnal parameter "enableArea" can override this test. I'll explain that later too.

			// mouse area
			int     objHandType = mouse.mObjectInHand.getType();
			CRect   mouseRect(pos, pos + CVec2(ITEM_WIDTH - 1, ITEM_HEIGHT - 1));
			bool    isPositionCompatible = true;

			// check if the object we are holding can be put in this type of slot
			if (objHandType != 0)
			{
				QString&    positions = objects.mObjectInfos[objHandType - 1].positions;
				isPositionCompatible = positions.contains(bodyInfos[part].type);
			}

			if (isPositionCompatible == true && enableArea == true)
				mouse.addArea(eMouseArea_InvObject, mouseRect, eCursor_Hand, (void*)championNum, (void*)part);
		}
				
Now in drawInventory(), we can draw all the objects with this simple loop:

		for (int i = 0; i < CCharacter::eBodyPartCount; ++i)
			drawBodyPart(image, bodyInfos[i].pos, currentChampion, (CCharacter::EBodyParts)i);
				

Checking the mouse area

In CInterface::update() we check the mouse areas for the slots like that:

		void   CInterface::update(SMouseArea* clickedArea)
		{
			[...]

			if (clickedArea != NULL)
			{
				if (mouse.mButtonPressing == true)
				{
					switch (clickedArea->type)
					{
						[...]

						// object slot in inventory
						case eMouseArea_InvObject:
						{
							// get current character and position in inventory
							int champNum = (int)clickedArea->param1;
							CCharacter* c = &game.characters[champNum];
							int objectPos = (int)clickedArea->param2;

							// swap the object we are holding with the one in the inventory
							CObject temp = mouse.mObjectInHand;
							mouse.mObjectInHand = c->bodyObjects[objectPos];
							c->bodyObjects[objectPos] = temp;
						}
						break;

						default:
						break;
					}
				}

			[...]
		}
				
We simply exchange the object we are holding with the one in the slot.
It works even if the hand or the slot are empty. In this case there is a normal object with only it's type set to 0.

We could have saved time by not setting up a mouse area if both the slot and the hand are empty, but this should not
slow down the game too much. Anyways, we are in the same case when the inventory is full of objects.

Champions' starting objects

When you resurrect a champion he/she already has a few objects in it's inventory.


We will define these object in "champions.xml" like this:

	<!-- ELIJA -->
	<champion firstName="CHAMP_FNAME00" lastName="CHAMP_LNAME00">
		[...]
		<object pos="T">52</object>
		<object pos="L">53</object>
		<object pos="F">50</object>
		<object pos="21">133</object>
	</champion>
				
For the position, you can either put the slot number following the EBodyParts enum or use one of these shortcuts:
The object's number follows the order of "items.xml".

These objects are read in CCharacter::readCharactersDB() and stored in a list of structures:

		class CCharacter
		{
			[...]
			
		private:
			struct SDBObject
			{
				int pos;
				int type;
			};

			struct SChampDBData
			{
				[...]
				std::vector<SDBObject>  objects;
			};

			[...]
		};
				
When we call fromDB() to initialize a character, its objects are created from this list:

		void CCharacter::fromDB(int num)
		{
			[...]

			for (size_t i = 0; i < eBodyPartCount; ++i)
				bodyObjects[i].setType(0);

			for (size_t i = 0; i < db.objects.size(); ++i)
			{
				int pos = db.objects[i].pos;
				int type = db.objects[i].type;
				bodyObjects[pos].setType(type);
			}
		}
				

Hands at the top

The hands in the "characters portraits area" at the top of the screen are a special case of slots.


They are a copy of the hands slots in the character's sheet.
So we will use the same id to draw them but they are not at the same position.
That's the reason why we had to pass the position as a parameter to drawBodyPart().

		void    CInterface::drawChampion(QImage* image, int num)
		{
			[...]

			// draw hands
			mouse.addArea(eMouseArea_None, handMouseRect, eCursor_Hand);
			drawBodyPart(image, CVec2(69 * num +  4, 10), num, CCharacter::eBodyPartLeftHand, !isResurrecting);
			drawBodyPart(image, CVec2(69 * num + 24, 10), num, CCharacter::eBodyPartRightHand, !isResurrecting);
				
You can see that we also set the last parameterof this function - the enableArea flag.
We must disable these slots when we resurrect a champion to prevent the player from stealing its objects
by putting them in the hands of another champion.


Additionally I added a test to avoid clicking on the "resurrect" or "cancel" buttons while we are holding
an object. This follows the behavior in the original game.