Part 43: Champions' life and death

Downloads

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

Damages and death

In the last part the monsters could attack, but it had no effect on the champion's life.
Now we we will really reduce their life and see how they die.
First we need to complete the hitChampion() function.

		void CCharacter::hitChampion(int num, int damages)
		{
			// TODO: wound, damage types

			// already dead or empty ?
			if (portrait == -1 || isDead() == true)
				return;

			displayedDamages = damages;
			damagesTimer.set(90, false);
			stats[eStatHealth].value -= damages;

			static CVec2    championPos[4][4] =
			{
				{{0, 0}, {1, 0}, {0, 1}, {1, 1}},   // up
				{{0, 1}, {0, 0}, {1, 1}, {1, 0}},   // left
				{{1, 1}, {0, 1}, {1, 0}, {0, 0}},   // down
				{{1, 0}, {1, 1}, {0, 0}, {0, 1}}    // right
			};

			// dead ?
			if (stats[eStatHealth].value <= 0)
			{
				stats[eStatHealth].value = 0;

				// drop all objects on the ground
				CVec2   stackPos = player.pos * 2 + championPos[player.dir][num];
				CObjectStack*   stack = map.addObjectsStack(stackPos, eWallSideMax);

				for (int i = 0; i < eBodyPartCount; ++i)
				{
					CObject&    obj = bodyObjects[i];

					if (obj.getType() != 0)
					{
						stack->addObject(obj);
						obj.setType(0);
					}
				}

				// drop the champion's bones
				CObject bones;
				bones.setType(OBJECT_TYPE_BONES);
				bones.setIntParam("Champion", num);
				stack->addObject(bones);
			}
		}
				
We subtract the damages from the champion's health.
When he's dead, we drop all of its objects and its bones on the ground.
the championPos table gives us the positions where we drop these items based on the player's direction.
This looks like that:


You can see that I added a "Champion" parameter to the bones object.

		<!-- # BONES -->
		<item name="ITEM149">
			[...]
			<param type="int">Champion</param>
		</item>
				
Because we need to know the name of the champion to display the bones' name when we pick them up.


We use the same function we wrote for the messages at the bottom of the screen to insert this name in the string.

		void CInterface::drawObjectName(QImage* image)
		{
			int type = mouse.mObjectInHand.getType();

			if (type != 0)
			{
				std::string name;

				if (type == OBJECT_TYPE_BONES)
					name = setChampionNameInString(mouse.mObjectInHand.getIntParam("Champion"), "ITEM149");
				else
					name = objects.mObjectInfos[type].name;

				drawText(image, CVec2(233, 33), eFontStandard, name.c_str(), MAIN_INTERFACE_COLOR);
			}
		}
				

Hitting the walls


When the party hits a wall, the 2 champions in the front row relatively to the wall take damages.
The value of the damages in the original game were computed with a strange formula based on the champions' stats,
but I replaced it with a simple random value between 1 and 2.

		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]

			if (canMove == true)
			{
				[...]
			}
			else
			{
				if (game.characters[0].portrait != -1)
				{
					static const int championsHit[eWallSideMax][2] =
					{
						{0, 1}, // eWallSideUp
						{2, 3}, // eWallSideDown
						{0, 2}, // eWallSideLeft
						{1, 3}  // eWallSideRight
					};

					for (int i = 0; i < 2; ++i)
					{
						int champNum = championsHit[invWallSide(side)][i];
						game.characters[champNum].hitChampion(champNum, (RANDOM(4) == 0 ? 2 : 1));
					}
					sound->play(newPos, "sound/Party_Damaged.wav");
				}
			}
		}
				
I should probably have used the getChampionRow() from the last part here and take into account the case when the
two players in the front row are dead.
But we will have to rewrite those functions when we add the characters placement inside the party.

Falling into a pit


When the party falls into a pit, every champion takes damage.
It's a random value around 20. But this value will change when we add wounds to the characters.

		void CPlayer::fall()
		{
			[...]
			hitAllChampions(20);
		}

		//-----------------------------------------------------------------------------------------
		void CPlayer::hitAllChampions(int damages)
		{
			int rnd = damages / 8 + 1;

			sound->play(pos, "sound/Party_Damaged.wav");

			for (int i = 0; i < 4; ++i)
			{
				int dmg = damages - rnd + RANDOM(rnd * 2);
				dmg = MAX(1, dmg);
				game.characters[i].hitChampion(i, dmg);
			}
		}
				

Vi altar


When you put the bones of a champion inside a Vi altar, the resurrection process begins.
We will store the wall's position and side and start a timer.

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

			else if (clickedArea->type == eMouseAreaG_DropObject)
			{
				// get the pos and side
				[...]

				// drop object
				CObjectStack*   stack = map.addObjectsStack(pos, side);
				stack->addObject(mouse.mObjectInHand);

				// test if we resurrect a champion
				if (mouse.mObjectInHand.getType() == OBJECT_TYPE_BONES &&
				    side != eWallSideMax)
				{
					CWall&  wall = map.getTile(pos)->mWalls[side];

					if (wall.getType() == eWallAlcove &&
					    wall.getEnumParam("Type") == 2)
					{
						// resurrect
						sound->play(pos, "sound/Spell.wav");
						walls.resurrectWallPos = pos;
						walls.resurrectWallSide = side;
						walls.resurrectTimer.set(120, true);
					}
				}
				
The timer is used to play a lightning animation during 2 seconds.
The graphics of the lightning comes from a "lightning bolt" missile, but we will talk about missiles in another
part.
The animation consists in flipping this lightning image every 1/2 second, and it is displayed as a wall's ornate.

		CWalls::COrnateData  alcoveLightning =
		{
			"",
			"gfx/3DView/missiles/Lightning_side.png",
			"",
			{82, 95},
			{0, 0}
		};

		//---------------------------------------------------------------------------------------------
		CRect CWalls::drawOrnate(QImage* image, CVec2 tablePos, EWallSide side, int ornate, int textLines)
		{
			if (ornate != 0)
			{
				int tableSide = walls.getTableSide(side, false);

				if (gWallsInfos[tablePos.y][tablePos.x][tableSide].size.x != 0)
				{
					COrnateData* ornateData = NULL;

					if (ornate == ORNATE_ALCOVE_LIGHTNING)
						ornateData = &alcoveLightning;
					else
						ornateData = &walls.ornatesDatas[ornate];

					[...]

					// draw the ornate on screen.
					bool    flipX = false;
					bool    flipY = false;

					if (ornate == ORNATE_ALCOVE_LIGHTNING)
					{
						int image = walls.resurrectTimer.get() / 30;
						flipX = image & 1;
						flipY = (image >> 1) & 1;
					}
					graph2D.drawImage(image, ornatePos, tempImage, 0, flipX, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT), flipY);
				

Explosion


When the lightning animation stops, an explosion appears for a short duration.
I created a new file "explosions.cpp" to handle it because explosions and clouds will be used a lot for spells.
An add() function is called to create a new explosion and add it to a list.
The parameter of this function are the map position, the type of explosion, and its duration.

		void CExplosions::add(EExplosions type, CVec2 mapPos, int duration)
		{
			SExplosion  explo;
			explo.type = type;
			explo.mapPos = mapPos;
			explo.timer.set(duration, true);

			mExploList.push_back(explo);

			if (type == eExplo_Resurrect)
				sound->play(mapPos, "sound/Explosion.wav");
		}
				
An update() function handles the timers of all the explosions and removes them from the list when they are finished.

		void CExplosions::update()
		{
			std::vector<SExplosion>::iterator   it = mExploList.begin();

			while(it != mExploList.end())
			{
				if (it->timer.update() == true)
					it = mExploList.erase(it);
				else
					it++;
			}
		}
				
The draw() function is a simplified version of CWalls::drawOrnate()

		void CExplosions::draw(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			for (size_t i = 0; i < mExploList.size(); ++i)
			{
				SExplosion& explo = mExploList[i];

				if (explo.mapPos == mapPos)
				{
					QImage  exploGfx;
					CVec2   pos;
					float   scale = 1.0;

					if (explo.type == eExplo_Resurrect)
					{
						exploGfx = fileCache.getImage("gfx/3DView/explosions/Fireball.png");
						pos = CVec2(57, 66);
						scale = 110.0 / 145.0;
					}

					const SWallInfos*   tabData = &gWallsInfos[tablePos.y][tablePos.x][eWallSideUp];
					if (tabData->size.x != 0)
					{
						const SWallInfos*   refWall = NULL;
						refWall = &gWallsInfos[3][2][eWallSideUp];

						// calculate the position and scale factor and draw the explosion in a temporary image
						scale *= (float)tabData->size.y / 111.0f;

						QSize   scaledSize = exploGfx.size() * scale;
						pos = pos - refWall->pos;
						pos.x = pos.x * tabData->size.x / refWall->size.x;
						pos.y = pos.y * tabData->size.y / refWall->size.y;

						pos += tabData->pos;

						QImage  tempImage(scaledSize, QImage::Format_ARGB32);
						tempImage.fill(TRANSPARENT);
						graph2D.drawImageScaled(&tempImage, CVec2(), exploGfx, scale);

						// darken the explosion based on it's distance
						static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
						float shadow = shadowLevels[WALL_TABLE_HEIGHT - 1 - tablePos.y];

						graph2D.darken(&tempImage, shadow);

						// draw the explosion on the screen.
						graph2D.drawImage(image, pos, tempImage, 0, false, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
					}
				}
			}
		}
				
When we create this explosion, the champion is also resurrected and gets back half o its starting life.

		void CWalls::update()
		{
			// vi altar animation
			if (resurrectTimer.update() == true)
			{
				// retrieve champion's number
				CObjectStack*   stack = map.findObjectsStack(resurrectWallPos, resurrectWallSide);
				int         last = stack->getSize() - 1;
				CObject&    bones = stack->getObject(last);
				int         champNum = bones.getIntParam("Champion");

				// delete object
				stack->removeObject(last);
				resurrectWallSide = eWallSideMax;

				// resurrect champion
				CCharacter::SStat&  health = game.characters[champNum].stats[CCharacter::eStatHealth];
				health.value = health.startValue / 2;

				// launch explosion
				explosions.add(eExplo_Resurrect, player.pos, 20);
			}
		}
				
I didn't look at the code of the original game to see if other stats of the champion are changed.

Health regeneration

As for the mana, the health slowly regenerates during the game. And it regenerates faster when the party sleeps.
So, like the mana, I used a timer that is set up with 2 values depending on whether or no we are sleeping.

		#define HEALTH_REGEN_TIMER        (30 * 60)
		#define HEALTH_REGEN_TIMER_SLEEP  90