Part 37: Attacks Part 1

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.

Attacks

Each weapon can have up to 3 differents attacks.


There are 40 attacks and I added their names to "texts.xml":

		<text id="ATTACK01">BLOCK</text>
		<text id="ATTACK02">CHOP</text>
		<text id="ATTACK03">BLOW HORN</text>
		<text id="ATTACK04">FLIP</text>
		<text id="ATTACK05">PUNCH</text>
		<text id="ATTACK06">KICK</text>
		<text id="ATTACK07">WAR CRY</text>
		<text id="ATTACK08">STAB</text>
		<text id="ATTACK09">CLIMB DOWN</text>
		<text id="ATTACK10">FREEZE LIFE</text>
		<text id="ATTACK11">HIT</text>
		<text id="ATTACK12">SWING</text>
		<text id="ATTACK13">THRUST</text>
		<text id="ATTACK14">JAB</text>
		<text id="ATTACK15">PARRY</text>
		<text id="ATTACK16">HACK</text>
		<text id="ATTACK17">BERZERK</text>
		<text id="ATTACK18">FIREBALL</text>
		<text id="ATTACK19">DISPELL</text>
		<text id="ATTACK20">CONFUSE</text>
		<text id="ATTACK21">LIGHTNING</text>
		<text id="ATTACK22">DISRUPT</text>
		<text id="ATTACK23">MELEE</text>
		<text id="ATTACK24">INVOKE</text>
		<text id="ATTACK25">SLASH</text>
		<text id="ATTACK26">CLEAVE</text>
		<text id="ATTACK27">BASH</text>
		<text id="ATTACK28">STUN</text>
		<text id="ATTACK29">SHOOT</text>
		<text id="ATTACK30">SPELLSHIELD</text>
		<text id="ATTACK31">FIRESHIELD</text>
		<text id="ATTACK32">FLUXCAGE</text>
		<text id="ATTACK33">HEAL</text>
		<text id="ATTACK34">CALM</text>
		<text id="ATTACK35">LIGHT</text>
		<text id="ATTACK36">WINDOW</text>
		<text id="ATTACK37">SPIT</text>
		<text id="ATTACK38">BRANDISH</text>
		<text id="ATTACK39">THROW</text>
		<text id="ATTACK40">FUSE</text>
				
I created a new database "attacks.xml" that contains some parameters for each of them, but we will talk about
that later.

Combos

You may think that we can simply add 3 attacks numbers to each weapon in the "items.xml" file, but it's a little
bit more complex.
Each group of 3 attacks that is associated to a weapon is called a combo - at least that's the name given in
Dungeon Master Encyclopaedia.
The original game used a separate database for these combos. It is necessary because the same attack can have
different parameters if it is in 2 different combos.
So I created a "combos.xml" database:

		<?xml version="1.0" encoding="utf-8"?>
		<combos>
			<!-- Combo 0 (unused) -->
			<combo>
				<attack type="0">
				</attack>
				<attack type="0">
				</attack>
				<attack type="0">
				</attack>
			</combo>

			<!-- Combo 1 Invoke/Fuse/Fluxcage -->
			<combo>
				<attack type="24">
				</attack>
				<attack type="40">
				</attack>
				<attack type="32">
				</attack>
			</combo>

			<!-- Combo 2 Punch/Kick/War Cry -->
			<combo>
				<attack type="5">
				</attack>
				<attack type="6">
				</attack>
				<attack type="7">
				</attack>
			</combo>
			[...]

		</combos>
				
Each combo contains 3 attacks with only a type. In the future, these attacks will contain more parameters.
Note that the combo 0 has no attacks. This is the combo that will be used for objects that can't be used as a
weapons - i.e. an apple.
This database is read in a new file "attacks.cpp" and the result is stored in a list of structures like that:

		class CAttacks
		{
		public:
			[...]

			struct SComboAttack
			{
				uint8_t type;
			};

			struct SCombo
			{
				SComboAttack    attacks[3];
			};

			[...]
			void        readCombosDB();
			[...]
			SCombo&     getCombo(int num);

		private:
			[...]
			std::vector<SCombo> mCombos;
		};
				

Linking the combos to the weapons

In the "items.xml" file, we will add a combo number to each weapon.

		<?xml version="1.0" encoding="utf-8"?>
		<items>
			<!-- BARE HAND -->
			<item name="ITEM000">
				[...]
				<combo>2</combo>
			</item>

			<!-- ======================== Weapons ======================== -->
			<!-- EYE OF TIME -->
			<item name="ITEM001">
				[...]
				<combo>43</combo>
			</item>

			<!-- STORMRING -->
			<item name="ITEM002">
				[...]
				<combo>7</combo>
			</item>
				
You can see that I added a "bare hand" object because when there is no weapon in the champion's hand, it is linked
to a combo anyways.
But there is also another reason to that. As the objects in "items.xml" started at "1", every time we had to
access these datas in the code, we substracted 1 to the object's type.
For example we had:

        CObjects::CObjectInfo&  infos = objects.mObjectInfos[objType - 1];
				
This was confusing and could lead to bugs. So now we don't need the "-1".

Now we can change the weapons display according to these datas.
First, if the object held by a champion is linked to the combo 0, we don't need to display it in the weapon's area,
and there will be no mouse area on it.


The drawWeapon() function now looks like that:

		void    CInterface::drawWeapon(QImage* image, int num)
		{
			if (isWeaponEmpty(num) == false)
			{
				// draw the background rectangle
				CRect   rect(CVec2(233 + 22 * num, 86),
							 CVec2(252 + 22 * num, 120));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				// draw the weapon
				CVec2       pos = CVec2(235 + 22 * num, 96);
				CObject&    weapon = getWeapon(num);
				int         weaponType = weapon.getType();
				CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];

				if (weaponInfo.combo != 0)
				{
					std::string imageFile;
					int         imageNum;

					CObjects::CObjectInfo  object = objects.mObjectInfos[weaponType];
					imageFile = object.imageFile.toLocal8Bit().constData();
					imageNum = object.imageNum;

					QImage  objImage = fileCache.getImage(imageFile);
					CRect   objRect = getItemRect(imageNum);
					graph2D.drawImageAtlas(image, pos, objImage, objRect, BLACK);
				}

				// grey out or mouse area
				if (isWeaponGreyed(num) == true)
					graph2D.patternRectangle(image, rect);
				else if (weaponInfo.combo != 0)
					mouse.addArea(eMouseArea_Weapon, rect, eCursor_Arrow, (void*)num);
			}
		}
				
Of course we also use the combo datas to find how many attacks a weapon has and display their names in
drawAttacks():

		void    CInterface::drawAttacks(QImage* image, int num)
		{
			CObject&    weapon = getWeapon(num);
			int         weaponType = weapon.getType();
			CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];
			CAttacks::SCombo&       combo = attacks.getCombo(weaponInfo.combo);

			// draw the background image
			QImage  attacksBg = fileCache.getImage("gfx/interface/WeaponsActions.png");
			CVec2   pos(233, 77);
			graph2D.drawImage(image, pos, attacksBg);

			// hide unused slots
			for (int i = 0; i < 3; ++i)
			{
				if (combo.attacks[i].type == 0)
				{
					CRect   rect(CVec2(233, 86 + i * 12),
					             CVec2(319, 97 + i * 12));
					graph2D.rect(image, rect, BLACK, true);
				}
			}

			// draw character's name
			CCharacter* c = &game.characters[num];
			drawText(image, CVec2(235, 79), eFontStandard, c->firstName.c_str(), BLACK);

			// draw attacks' names
			for (int i = 0; i < 3; ++i)
			{
				if (combo.attacks[i].type != 0)
				{
					static char attackName[16];
					sprintf(attackName, "ATTACK%02d", combo.attacks[i].type);
					drawText(image, CVec2(241, 89 + i * 12), eFontStandard, getText(attackName).c_str(), MAIN_INTERFACE_COLOR);

					if (attackHighlightTime.isRunning() == true && currentAttack == i)
					{
						CRect   rect(CVec2(234, 86 + i * 12),
						             CVec2(318, 96 + i * 12));
						graph2D.Xor(image, rect, MAIN_INTERFACE_COLOR);
					}
				}
			}

			// mouse areas
			if (isWeaponGreyed(num) == false && attackHighlightTime.isRunning() == false)
			{
				for (int i = 0; i < 3; ++i)
				{
					if (combo.attacks[i].type != 0)
					{
						CRect   rect(CVec2(234, 86 + i * 12),
						             CVec2(318, 96 + i * 12));
						mouse.addArea(eMouseArea_Attack, rect, eCursor_Arrow, (void*)i);
					}
				}

				CRect   rect(CVec2(290, 77), CVec2(314, 83));
				mouse.addArea(eMouseArea_CloseAttack, rect, eCursor_Arrow);
			}
		}
				
All the attacks of a weapon won't be available at the beginning. Some of them will require that the character has a
given level in a particular skill.
But we will see that in another part...

The attacks database

As I said at the beginning, there is a database for the attacks too, as there are differents parameters for each
of them.
This file is "attacks.xml" and it is loaded in "attacks.cpp" too:

		<?xml version="1.0" encoding="utf-8"?>
		<attacks>
			<!-- Nothing -->
			<attack>
				<cooldown>0</cooldown>
				<hitprob>0</hitprob>
			</attack>

			<!-- BLOCK -->
			<attack>
				<cooldown>60</cooldown>
				<hitprob>22</hitprob>
			</attack>

			<!-- CHOP -->
			<attack>
				<cooldown>80</cooldown>
				<hitprob>48</hitprob>
			</attack>
				
At the moment it only holds the cooldown time in frames and the probability to hit with this weapon that goes
from 0 to 75.
So to check if a weapon "hits", we will get a random number between 1 and 75 and test if it is less than or equal
this value.
This way:
This is what we do in updateWeapons at the moment the attack highlight stops:

		void CInterface::updateWeapons()
		{
			// attack highlight
			if (attackHighlightTime.update() == true)
			{
				CObject&    weapon = getWeapon(currentWeapon);
				int         weaponType = weapon.getType();
				CObjects::CObjectInfo   weaponInfo = objects.mObjectInfos[weaponType];
				CAttacks::SCombo&        combo = attacks.getCombo(weaponInfo.combo);
				int         attackType = combo.attacks[currentAttack].type;
				CAttacks::SAttack&       attack = attacks.getAttack(attackType);

				weaponCoolDown[currentWeapon].set(attack.cooldown, true);

				if (((rand() % 75) + 1) <= attack.hitProbability)
				{
					damages = rand() % 200 + 1;
					damagesDisplayTime.set(DAMAGES_DISPLAY_TIME, false);
					weaponsAreaState = eWeaponsAreaDamage;
				}
				else
				{
					weaponsAreaState = eWeaponsAreaWeapons;
				}
			}

			// damages timer
			if (damagesDisplayTime.update() == true)
				weaponsAreaState = eWeaponsAreaWeapons;

			// weapons cooldowns
			for (int i = 0; i < 4; ++i)
				weaponCoolDown[i].update();
		}
				
This is not the exact formula used in the original game, as the hit probability will vary with the champion's
stats like dexterity or luck.
In a fight, the hit probability will also change depending on the stats of the monster we hit, because its
dexterity can allow him to avoid the attack.

You will notice that attacks like shooting with a bow or casting a spell with a staff have a probability of 0.
These are special attacks where a projectile or spell is always thrown. That's only when this projectile hits
the target that we check if deals damages to it.