Part 49: Stats and skills Part 2: Skills in melee

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.

Revisiting skills levels

In the last part we talked about the skills levels and specificallly the differences between the main and the
hidden skills.
But after digging into the code of the original game I found that it's not exaclty how it works.
For a main skill we get its level from the formula we talked about.
But for a hidden skill we don't use its level directly. In fact we take the average of its experience and the
experience of the corresponding main skill. Then we apply the same formula as the main skill to get level of this
hidden one.

So I removed the "value" of the SSkill structure and I wrote a function that returns the level of a given skill:

		int CCharacter::getSkillLevel(ESkillsNames skillName)
		{
			if (interface.isSleeping() == true)
				return 1;

			int exp = skills[skillName].experience;

			// Hidden skill
			if (skillName >= 4)
			{
				int mainSkill = (skillName - 4) / 4;
				exp = (exp + skills[mainSkill].experience) / 2;
			}

			// compute level from experience
			int level = 0;

			while (exp >= 500)
			{
				exp /= 2;
				level++;
			}

			return level;
		}
				

Skills in attacks

Each attack in the game is linked to a skill and a skill level.
The skill's number is contained in each attack.
So I added it to the "attacks.xml" file.

		<attacks>
			<!-- 0: Nothing -->
			<attack>
				<type>Melee</type>
				[...]
				<skill>0</skill>
			</attack>

			<!-- 1: BLOCK -->
			<attack>
				<type>Melee</type>
				[...]
				<skill>7</skill>
			</attack>

			<!-- 2: CHOP -->
			<attack>
				<type>Melee</type>
				[...]
				<skill>6</skill>
			</attack>
				
The skill's level you need for an attack is stored in the combos.
This way the same attack can need different levels if it is available in different weapons.

		<combos>
			<!-- Combo 0 (unused) -->
			<combo>
				<attack type="0">
					<skill_level>0</skill_level>
				</attack>
				<attack type="0">
					<skill_level>0</skill_level>
				</attack>
				<attack type="0">
					<skill_level>0</skill_level>
				</attack>
			</combo>

			<!-- Combo 1 Invoke/Fuse/Fluxcage -->
			<combo>
				<attack type="24">
					<skill_level>0</skill_level>
				</attack>
				<attack type="40">
					<skill_level>0</skill_level>
				</attack>
				<attack type="32">
					<skill_level>0</skill_level>
				</attack>
			</combo>
				
These skills were used in the original game to add a "bonus" to the damages we do when we attack a monster.
This is used in the getMeleeDamage() function.

		int CCharacter::getMeleeDamage(CVec2 monsterPos, CAttacks::SAttack& attack)
		{
			[...]

			// do we hit ?
			[...]

			if (championDexterity > monsterDexterity ||
			    ((RANDOM(75) + 1) <= attack.hitProbability) ||
			    RANDOM(4) == 0)
			{
				[...]

				// compute damages
				[...]

				// last chance ?
				[...]

				// randomize the damages
				[...]

				// skill bonus
				if (getSkillLevel((ESkillsNames)attack.skill) >= RANDOM(64))
				{
					damages += 10;
				}

				return damages;
				

Attacks list

The attacks' skills also defines the attacks we can do with a weapon.
Generally the last attacks in the list need a higher skill level.
So I slightly modified drawAttacks() function to take that into account.
In particular, I first create a list of the available attacks, then I use it to display the attacks box.

		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);
			CCharacter* c = &game.characters[num];

			// make a list of possible attacks
			mAttacksList.clear();

			for (int i = 0; i < 3; ++i)
			{
				if (combo.attacks[i].type != 0)
				{
					int attackType = combo.attacks[i].type;
					CAttacks::SAttack&  attack = attacks.getAttack(attackType);
					int skillLevel = c->getSkillLevel((CCharacter::ESkillsNames)attack.skill) + 1;

					if (skillLevel >= combo.attacks[i].skillLevel)
						mAttacksList.push_back(attackType);
				}
			}

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

			// hide unused slots
			for (int i = 2; i >= mAttacksList.size(); i--)
			{
				CRect   rect(CVec2(233, 86 + i * 12),
				             CVec2(319, 97 + i * 12));
				graph2D.rect(image, rect, BLACK, true);
			}

			// draw character's name
			drawText(image, CVec2(235, 79), eFontStandard, c->firstName.c_str(), BLACK);

			// draw attacks' names
			for (int i = 0; i < mAttacksList.size(); ++i)
			{
				static char attackName[16];
				sprintf(attackName, "ATTACK%02d", mAttacksList[i]);
				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);
			}
		}
				

Weapons classes

In the game we will need to differentiate the various weapons we can hold.
In the original game each weapon had a "class" value between 0 and 255 that told us two things:


So I added these values in the "items.xml" file.

		<items>
			<!-- BARE HAND -->
			<item name="ITEM000">
				[...]
				<class>0</class>
			</item>

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

			<!-- STORMRING -->
			<item name="ITEM002">
				[...]
				<class>131</class>
			</item>
				
As I said these values gives us the type of the weapons. They follow these tables:

Swing weapons (0)
Values Weapon type
0 Swing weapon: swords, maces and clubs

Throwing weapons (1 to 15)
Values Weapon type
1 Throwing star
2 Daggers and axes
3 to 9 unused
10 Bows ammunitions - arrows
11 Slings ammunitions - rocks
12 Poison darts
13 to 15 unused

Shooting weapons (16 to 111)
Values Weapon type
16 to 31 Bows and crossbows
32 to 47 Slings
48 to 111 unused

Magical weapons (112 to 255)
Values Weapon type
112 to 255 Magical weapons - rings, wands and staffs

In the game we will need only a few defines for these classes:

		#define WEAPON_CLASS_SWING            0 /* Class 0: SWING weapons */
		#define WEAPON_CLASS_DAGGER_AND_AXES  2 /* Class 1 to 15: THROW weapons */
		#define WEAPON_CLASS_BOW_AMMUNITION   10
		#define WEAPON_CLASS_SLING_AMMUNITION 11
		#define WEAPON_CLASS_POISON_DART      12
		#define WEAPON_CLASS_FIRST_BOW        16 /* Class 16 to 111: SHOOT weapons */
		#define WEAPON_CLASS_LAST_BOW         31
		#define WEAPON_CLASS_FIRST_SLING      32
		#define WEAPON_CLASS_LAST_SLING       47
		#define WEAPON_CLASS_FIRST_MAGIC      112 /* Class 112 to 255: Magic and special weapons */
				

Skills in the strength

These classes are used to add a bonus to the strength in the getStrength() function.

		int CCharacter::getStrength(int objType)
		{
			[...]

			// object weight
			[...]

			// weapon's damages
			strength += objInfo.damages;

			if (objType != 0)
			{
				int skill = 0;

				if (objInfo.weaponClass == WEAPON_CLASS_SWING || objInfo.weaponClass == WEAPON_CLASS_DAGGER_AND_AXES)
					skill = getSkillLevel(eSkillSwing) + 1;

				if (objInfo.weaponClass != WEAPON_CLASS_SWING && objInfo.weaponClass < WEAPON_CLASS_FIRST_BOW)
					skill += getSkillLevel(eSkillThrow) + 1;

				if (objInfo.weaponClass >= WEAPON_CLASS_FIRST_BOW && objInfo.weaponClass < WEAPON_CLASS_FIRST_MAGIC)
					skill += getSkillLevel(eSkillShoot) + 1;

				strength += skill * 2;
			}

			// bound the value
			[...]

			return strength;
		}
				

Parrying

The parry value is used to get a chance to avoid an attack from a monster.

		void CMonster::fight(CVec2 mapPos, uint8_t type, bool isInFront)
		{
			if (nextAttackTimer.update() == true)
			{
				initNextAttack(type);

				// if the monster is in the front line of the group
				if (isInFront == true)
				{
					// play attack sound
					[...]

					// show monster's animation
					[...]

					// choose a target champion
					[...]

					// compute monster's and champion's dexterities
					[...]

					// check if we hit
					if (interface.isSleeping() == true ||
					    monsterDexterity > championDexterity ||
					    RANDOM(4) == 0)
					{
						// compute damages
						int parry = (game.characters[target].getSkillLevel(CCharacter::eSkillParry) + 1) * 2;
						int damages = monsters.monstersDatas[type].attack + game.levelMultiplier() * 2 + RANDOM(16) - parry;

						if (damages <= 1)
						{
							// parry
							if (RANDOM(2))
								return;

							damages = RANDOM(4) + 2;
						}

						// randomize damages (quite a complicated way...)
						[...]

						// hit the champion
						[...]
					}

					// if we are sleeping, wake up
					[...]
				}
			}
		}
				

Pits' bug

I found a bug with the pits. When you fell into one of them the game entered in a forever loop.
Apparently the isFinished() function used to tell if the screaming sound was finished didn't work.
As I did not have the time to fully investigate that, I simply replaced it with a timer.

		if (fallTimer.update() == true)
			player.fall();