Part 45: Monsters Part 5: monsters' 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.

Bug fix

While translating part 32, I found a bug in the sound system.
We sorted the sounds to play the 4 nearest ones, but when we play them we use the unsorted list instead of the
sorted one.
This bug should not be very annoying, as it's not easy to play more than 4 sounds at the same time in the game,
but a anyways, here is the code how it should look like:

        void CSoundManager::pushTimerExpired()
        {
            [...]
            // sort the sounds
            std::vector<CSound*>    sorted;
            for (size_t i = 0; i < mSounds.size(); ++i)
                sorted.push_back(&mSounds[i]);

            for (int i = 0; i < (int)mSounds.size() - 1; ++i)
                for (int j = i + 1; j < (int)mSounds.size(); ++j)
                    if (sorted[j]->getDistance() < sorted[i]->getDistance())
                    {
                        CSound* temp = sorted[i];
                        sorted[i] = sorted[j];
                        sorted[j] = temp;
                    }

            // play the 4 nearest sounds (the others' samples are skipped)
            int leftVolume, rightVolume;

            for (size_t i = 0; i < mSounds.size(); ++i)
            {
                if (i < MIXER_COEF)
                {
                    sorted[i]->getVolumes(&leftVolume, &rightVolume);
                    sorted[i]->getData(mBuffer, len, leftVolume, rightVolume);
                }
                else
                {
                    sorted[i]->skipSamples(len);
                }
            }
				

Saving the monsters

When a group of monsters dies, it is removed from the groups' list in CMap.
So it should not reappear when you enter the level again.
But we need to save when a single monster in the group dies too.
And we also should save the life value of each monster, to avoid that they get their full life back when we
re-enter this level.

First we need to save that in the maps' snapshot images.

		class CSnapshot
		{
		public:
			[...]
			std::vector<CMonsterGroup2> monsterGroups;

			[...]
		};

		//-----------------------------------------------------------------------------------------
		CSnapshot&  CSnapshot::operator=(const CSnapshot& rhs)
		{
			[...]
			monsterGroups = rhs.monsterGroups;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void CGame::snapshotLevel()
		{
			[...]
			s.monsterGroups = monsters.monsterGroups;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void CGame::loadLevel(int level, CVec2 pos, int side)
		{
			[...]

			// search if the new level was snapshoted...
			for (size_t i = 0; i < mVisitedLevels.size(); ++i)
			{
				CSnapshot*  s = &mVisitedLevels[i];

				if (s->level == level)
				{
					[...]
					monsters.monsterGroups = s->monsterGroups;
					return;
				}
			}

			// ...else load the new level
			[...]
		}
				
When the snapshot is saved to the disk, we have to save the monsters datas too.

		//---------------------------------------------------------------------------------------------
		void    CSnapshot::save(FILE* handle)
		{
			[...]

			for (size_t i = 0; i < map.mMonsterGroups.size(); ++i)
				monsterGroups[i].save(handle);
		}

		//---------------------------------------------------------------------------------------------
		void CMonsterGroup2::save(FILE* handle)
		{
			uint8_t val8;

			val8 = dir;
			fwrite(&val8, 1, 1, handle);

			val8 = state;
			fwrite(&val8, 1, 1, handle);

			fwrite(&startPos.x, sizeof(startPos.x), 1, handle);
			fwrite(&startPos.y, sizeof(startPos.y), 1, handle);

			for (int i = 0; i < 4; ++i)
				mMonsters[i].save(handle);
		}

		//---------------------------------------------------------------------------------------------
		void CMonster::save(FILE* handle)
		{
			uint8_t val8;

			val8 = pos;
			fwrite(&val8, 1, 1, handle);

			val8 = dir;
			fwrite(&val8, 1, 1, handle);

			fwrite(&life, sizeof(life), 1, handle);
		}
				
And obviously there are corresponding load functions.

Dropping objects

Some groups of monsters in the map are linked to an objects stack.
When all the monsters of this group are dead, these objects fall on the ground.

		void CMonsterGroup2::hitMonster(CVec2 groupPos, int monsterNum, int damages)
		{
			[...]

			// dead ?
			if (mMonsters[monsterNum].life <= 0)
			{
				[...]

				// if all monsters are dead, we remove the group
				[...]

				if (i == 4)
				{
					// drop objects on the ground
					CObjectStack*   stack = map.findObjectsStack(startPos, eWallSideMonster);

					if (stack != NULL)
					{
						sound->play(groupPos, "sound/Wooden_Thud.wav");

						for (size_t j = 0; j < stack->getSize(); ++j)
						{
							CObject obj = stack->getObject(j);
							CVec2   objPos = groupPos * 2 + CVec2(RANDOM(2), RANDOM(2));
							CObjectStack*   newStack = map.addObjectsStack(objPos, eWallSideMax);
							newStack->addObject(obj);
						}
						map.removeObjectsStack(startPos, eWallSideMonster);
					}

					// delete group
					[...]
				}
			}
		}
				
The screamers drop additional objects. Each of them can drop between 0 and 2 "Screamer Slice".
We add these object to the stack when we initialize the monsters:

		void CMonsters::init()
		{
			for (size_t i = 0; i < map.mMonsterGroups.size(); ++i)
			{
				[...]

				group.startPos = mapGroup.mPos;
				[...]

				// add objects dropped by certain types of monsters
				CObject obj;

				if (mapGroup.getType() == MONSTER_TYPE_SCREAMER)
				{
					obj.setType(OBJECT_TYPE_SCREAMER_SLICE);

					for (int j = 0; j < nbMonsters; ++j)
					{
						int nbObj = RANDOM(3);

						for (int k = 0; k < nbObj; ++k)
						{
							CObjectStack*   stack = map.addObjectsStack(group.startPos, eWallSideMonster);
							stack->addObject(obj);
						}
					}
				}
				

Smoke animation

When a monster dies, a smoke animation appears.
This animation is composed of only 3 images:




These images are a scaled down version of the poison cloud image colored in grey.


But as every monster should display this animation, we have to interleave its drawing with the drawing of the
monsters. Otherwise the smoke clouds would always appear over the front-most monster of the group.

		void CGame::displayMainView(QImage* image)
		{
			[...]

			// draw monsters and clouds
			for (int row = 0; row < 3; row++)
			{
				monsters.draw(image, mapPos, tablePos, row);
				explosions.drawCloud(image, mapPos, tablePos, row);
			}

			// draw explosions
			explosions.drawExplosion(image, mapPos, tablePos);
			[...]
				
I called these smokes "clouds", as there will be similar to poison clouds.
They will be drawn in "explosions.cpp".

The beginning of the draw function sets up the image we will use and the scale factor depending on the timer.

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

				if (explo.mapPos == mapPos && getRow(explo) == row)
				{
					QImage  exploGfx;
					float   height = 0.0;
					float   scale = 0.0;
					bool    flip = false;
					QColor  color = TRANSPARENT;

					switch (explo.type)
					{
						case eExplo_MonsterDie:
							exploGfx = fileCache.getImage("gfx/3DView/explosions/Poison.png");
							height = 0.636;

							if (explo.timer.get() > explo.startTime * 2 / 3)
							{
								scale = 0.374;
							}
							else if (explo.timer.get() > explo.startTime / 3)
							{
								scale = 0.237;
								flip = true;
							}
							else
							{
								scale = 0.173;
							}

							color = MONSTER_CLOUD_COLOR;
							break;

						default:
							break;
					}

					if (scale == 0.0)
						continue;
				
Then we will compute the position of the center of the sprite.
The position will be based on the position of the corresponding monster.
But getMonsterPos() give us the position of the monster on the ground, and the smoke is above.
For example in the following image, the middle point gives the position of the smoke for the first mummy we
encounter.


It lies at 63.6% of the distance between the ground and the ceiling - that's what the height variable in the code
above tells. We will use this value for all the smokes.
So we will need the coordinates of the top and the bottom of each wall.



					// bottom of the walls (ground level)
					static const int aGround[] = {98, 106, 125, 152};
				
					// top of the walls (ceiling level)
					static const int aCeiling[] = {61, 58, 52, 42};
				
We will first get the coordinates of the coordinates of the monster.

					// calculate the position and scale factor and draw the explosion in a temporary image
					CVec2   pos = monsters.getMonsterPos2(mapPos, tablePos, explo.monsterPos);
				
Then we interpolate the coordinates to get the height of the smoke as if it was on the front or on the rear wall -
given the cell on which it is.

					//   height intepolation
					float   rearHeight  = height * (float)aCeiling[tablePos.y]     + (1.0 - height) * (float)aGround[tablePos.y];
					float   frontHeight = height * (float)aCeiling[tablePos.y + 1] + (1.0 - height) * (float)aGround[tablePos.y + 1];
				
Now we interpolate these 2 heights with the "depth" of the monster inside the tile to get the final height.

					//   depth interpolation
					float   distance = (float)(row + 1) * 0.25;
					float   meanHeight = distance * rearHeight + (1.0 - distance) * frontHeight;

					//   rounding the result
					pos.y = (int)(meanHeight + 0.5);
				
Then we compute the scale of the image and we change its color if necessary.
And that is our case as we want to draw the smokes in grey.

					// compute the scale based on the reference position (the nearest)
					CVec2   pos0, pos1;
					float tablePosY = tablePos.y * 2 + monsterPosDir[player.dir][explo.monsterPos - eMonsterPosNorthWest][1];

					pos0 = CMonsters::getMonsterPos(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2 + 1);
					pos1 = CMonsters::getMonsterPos(WALL_TABLE_WIDTH, tablePosY);
					scale *= 2.68 * (float)(pos1.x - 112) / (float)(pos0.x - 112);

					// set color
					QImage  tempImage;
					QImage* srcImage;

					if (color.alpha() == 0)
					{
						srcImage = &exploGfx;
					}
					else
					{
						tempImage = exploGfx;
						graph2D.drawImageAtlas(&tempImage, CVec2(), exploGfx, CRect(CVec2(), CVec2(exploGfx.width(), exploGfx.height())), color);
						srcImage = &tempImage;
					}
				
The following of the function is very similar to the one we use to draw the monsters: scaling, darkening and
finally drawing on the screen.

					// scale the explosion
					QImage  scaledExplo(exploGfx.size() * scale, QImage::Format_ARGB32);
					scaledExplo.fill(TRANSPARENT);
					graph2D.drawImageScaled(&scaledExplo, CVec2(), *srcImage, scale);

					// darken the explosion based on it's distance
					float shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePosY - 1) * 0.13f;
					graph2D.darken(&scaledExplo, shadow);

					// draw the explosion (pos relative to its encter)
					pos.x -= scaledExplo.width() / 2;
					pos.y -= scaledExplo.height() / 2;
					graph2D.drawImage(image, pos, scaledExplo, 0, flip, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
				}
			}
		}