Part 44: Attacks Part 2: killing monsters with melee weapons
Downloads
Cleaning up the row functions
// returns the row of a monster depending on the player position (0 = front, 1 = center, 2 = back)
uint8_t CMonsterGroup2::getMonsterRow(CVec2 mapPos, EMonsterPos monsterPos)
{
static const uint8_t monsterRow[4][5] =
{
// NW NE SW SE C
{ 0, 0, 2, 2, 1}, // up
{ 0, 2, 0, 2, 1}, // left
{ 2, 2, 0, 0, 1}, // down
{ 2, 0, 2, 0, 1} // right
};
int playerPosFromMonster;
if (player.pos.x < mapPos.x)
playerPosFromMonster = 1; // left
else if (player.pos.x > mapPos.x)
playerPosFromMonster = 3; // right
else if (player.pos.y < mapPos.y)
playerPosFromMonster = 0; // up
else
playerPosFromMonster = 2; // down
return monsterRow[playerPosFromMonster][monsterPos - eMonsterPosNorthWest];
}
// returns the row of champions depending on the monsters group position (0 = front, 1 = back)
uint8_t CPlayer::getChampionRow(CVec2 monsterPos, int championNum)
{
static const uint8_t championRow[4][4] =
{
// 1 2 3 4
{ 0, 0, 1, 1}, // up
{ 0, 1, 0, 1}, // left
{ 1, 1, 0, 0}, // down
{ 1, 0, 1, 0} // right
};
CVec2 localMonsterPos = getLocalFromPos(monsterPos);
int monsterPosFromPlayer;
if (localMonsterPos.x < 0)
monsterPosFromPlayer = 1; // left
else if (localMonsterPos.x > 0)
monsterPosFromPlayer = 3; // right
else if (localMonsterPos.y < 0)
monsterPosFromPlayer = 0; // up
else
monsterPosFromPlayer = 2; // down
return championRow[monsterPosFromPlayer][championNum];
}
We also needed to test if the monsters of the front row in a group are all dead so that the back row could hit the
// returns if a monster is in the fron row of its group, taking into account the dead ones
bool CMonsterGroup2::isMonsterInFront(CVec2 mapPos, EMonsterPos monsterPos)
{
bool isFrontRowRowAlive = false;
for (int i = 0; i < 4; ++i)
{
if (mMonsters[i].pos != eMonsterPosNone &&
getMonsterRow(mapPos, mMonsters[i].pos) < 2)
{
isFrontRowRowAlive = true;
break;
}
}
uint8_t row = getMonsterRow(mapPos, monsterPos);
return (isFrontRowRowAlive == true && row < 2) ||
(isFrontRowRowAlive == false && row == 2);
}
// returns if a champion is in the front row, taking into account the dead ones.
bool CPlayer::isChampionInFront(CVec2 monsterPos, int championNum)
{
int rowToSearch;
// check if there is at least one living champion in the front row
rowToSearch = 1;
for (int i = 0; i < 4; ++i)
{
if (interface.isChampionEmpty(i) == false &&
interface.isChampionDead(i) == false &&
player.getChampionRow(monsterPos, i) == 0)
{
rowToSearch = 0;
break;
}
}
return (player.getChampionRow(monsterPos, championNum) == rowToSearch);
}
Now this simplifies the functions that used the rows. I.e. the CMonster::chooseTarget() function:
// choose the player this monster will attack
uint8_t CMonster::chooseTarget(CVec2 mapPos)
{
// list the possible targets
uint8_t possibleTargets[2];
int nbTargets = 0;
for (int i = 0; i < 4; ++i)
{
if (interface.isChampionEmpty(i) == false &&
interface.isChampionDead(i) == false &&
player.isChampionInFront(mapPos, i) == true)
{
possibleTargets[nbTargets++] = i;
}
}
// choose a target
return possibleTargets[RANDOM(nbTargets)];
}
And as I said that I should have used these function when the party is hitting a wall, now I did it:
void CPlayer::moveIfPossible(EWallSide side)
{
[...]
// wall
if (game.characters[0].portrait != -1)
{
for (int i = 0; i < 4; ++i)
{
if (player.isChampionInFront(newPos, i) == true)
game.characters[i].hitChampion(i, (RANDOM(4) == 0 ? 2 : 1));
}
sound->play(newPos, "sound/Party_Damaged.wav");
}
The melee attacks
<attacks>
<!-- 0: Nothing -->
<attack>
<type>Melee</type>
[...]
</attack>
<!-- 1: BLOCK -->
<attack>
<type>Melee</type>
[...]
</attack>
[...]
This type parameter can take different values:
Choosing a target
void CInterface::updateWeapons()
{
[...]
if (frontTile != NULL)
{
// breakable door
if (frontTile->getType() == eTileDoor &&
[...]
}
else if (attack.type == "Melee")
{
// melee attack: look for a monster's group in front of us
CVec2 monsterPos = player.pos;
monsterPos.x += (player.dir == 3) - (player.dir == 1);
monsterPos.y += (player.dir == 2) - (player.dir == 0);
int groupIndex = map.findMonsterGroupIndex(monsterPos);
if (groupIndex != -1)
{
CMonsterGroup2& group = monsters.monsterGroups[groupIndex];
// only front players can attack with melee weapon
if (player.isChampionInFront(monsterPos, currentWeapon) == true)
{
// choose a target
uint8_t target = chooseTarget(monsterPos);
[...]
}
else
{
// can't reach
weaponsAreaState = eWeaponsAreaWeapons;
}
}
else
{
// nothing to hit
weaponsAreaState = eWeaponsAreaWeapons;
}
}
The chooseTarget function looks like the one we used for the monsters.
uint8_t CInterface::chooseTarget(CVec2 mapPos)
{
// list the possible targets
int groupIndex = map.findMonsterGroupIndex(mapPos);
CMonsterGroup2& group = monsters.monsterGroups[groupIndex];
uint8_t possibleTargets[2];
int nbTargets = 0;
for (int i = 0; i < 4; ++i)
{
CMonster& monster = group.mMonsters[i];
if (monster.pos != eMonsterPosNone &&
group.isMonsterInFront(mapPos, monster.pos) == true)
{
possibleTargets[nbTargets++] = i;
}
}
// choose a target
return possibleTargets[RANDOM(nbTargets)];
}
Attacking the target
damages = game.characters[currentWeapon].getMeleeDamage(monsterPos, attack);
if (damages != 0)
{
// hit
damagesDisplayTime.set(DAMAGES_DISPLAY_TIME, false);
weaponsAreaState = eWeaponsAreaDamage;
group.hitMonster(monsterPos, target, damages);
}
else
{
// miss
weaponsAreaState = eWeaponsAreaWeapons;
}
The getMeleeDamage() function not only computes damages, but also checks if we hit the target.
int CCharacter::getMeleeDamage(CVec2 monsterPos, CAttacks::SAttack& attack)
{
int groupIndex = map.findMonsterGroupIndex(monsterPos);
CMonsterGroup& mapGroup = map.mMonsterGroups[groupIndex];
int monsterType = mapGroup.getType();
CMonsters::SMonsterData& monsterData = monsters.monstersDatas[monsterType];
int weaponType = bodyObjects[eBodyPartRightHand].getType();
// do we hit ?
int monsterDexterity = monsterData.dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
int championDexterity = getDexterity();
if (championDexterity > monsterDexterity ||
((RANDOM(75) + 1) <= attack.hitProbability) ||
RANDOM(4) == 0)
{
Then we compute the value of the damages taken by the monster.
int damages = 0;
int monsterDefense = 0;
// compute damages
damages = getStrength(eBodyPartRightHand);
We tune these damages with a coefficient for the attack used.
if (damages != 0)
{
damages += RANDOM(damages / 2 + 1);
damages = (damages * attack.damages) / 32;
monsterDefense = monsterData.defense + game.levelMultiplier() * 2 + RANDOM(32);
if (weaponType == OBJECT_TYPE_DIAMOND_EDGE)
{
monsterDefense -= monsterDefense / 4;
}
else if (weaponType == OBJECT_TYPE_HARDCLEAVE)
{
monsterDefense -= monsterDefense / 8;
}
damages = damages - monsterDefense - RANDOM(32);
}
Then there is a strange part of the code that looks like an attempt to "tune" the damages and raise a little the
// last chance ?
if (damages <= 1)
{
int damages2 = damages;
damages = RANDOM(4);
if (damages != 0)
{
damages++;
damages2 += RANDOM(16);
if (damages2 > 0 || RANDOM(2))
{
damages += RANDOM(4);
if (RANDOM(4) == 0)
damages += MAX(0, damages2 + RANDOM(16));
}
}
}
Finally, we "randomize" the damages the same way we did it for the monsters, in order to increase the probability
// randomize the damages
if (damages != 0)
{
damages /= 2;
damages += RANDOM(damages) + RANDOM(4);
damages += RANDOM(damages);
damages /= 4;
damages += RANDOM(4) + 1;
}
return damages;
}
return 0;
}
The champion's "strength"
int CCharacter::getStrength(int objType)
{
int strength = stats[eStatStrength].value + RANDOM(16);
CObjects::CObjectInfo& objInfo = objects.mObjectInfos[objType];
Then we modulate it with the weight of the weapon.
// object weight
int loadLimit = stats[eStatLoad].maxValue / 16;
int loadLimit2 = loadLimit + (loadLimit - 12) / 2;
if (objInfo.weight <= loadLimit)
{
strength += objInfo.weight - 12;
}
else if (objInfo.weight <= loadLimit2)
{
strength += (objInfo.weight - loadLimit) / 2;
}
else
{
strength -= (objInfo.weight - loadLimit2) * 2;
}
The weapons have each a "damage" coefficient that is added to the strength.
// weapon's damages
strength += objInfo.damages;
And finally we divide the strength by 2 and bound this value between 0 and 100.
// bound the value
strength /= 2;
if (strength < 0)
strength = 0;
if (strength > 100)
strength = 100;
return strength;
}
Note that it is not the exact formula used in the original game, as it also took into account the skill of the
Monsters' life and death
void CMonsterGroup2::hitMonster(CVec2 groupPos, int monsterNum, int damages)
{
mMonsters[monsterNum].life -= damages;
// dead ?
if (mMonsters[monsterNum].life <= 0)
{
mMonsters[monsterNum].pos = eMonsterPosNone;
// if all monsters are dead, we remove the group
int i;
for (i = 0; i < 4; ++i)
{
if (mMonsters[i].pos != eMonsterPosNone)
break;
}
if (i == 4)
{
int groupIndex = map.findMonsterGroupIndex(groupPos);
map.removeMonsterGroup(groupPos);
monsters.monsterGroups.erase(monsters.monsterGroups.begin() + groupIndex);
}
}
}
Solid monsters
void CPlayer::moveIfPossible(EWallSide side)
{
[...]
int canMove = 0; // 0=OK, 1=wall, 2=monster
if (tile->mWalls[side].getType() != 0)
{
canMove = 1;
}
else if (destTile == NULL)
{
canMove = 1;
}
else if (destTile->getType() == eTileDoor && doors.isOpened(destTile) == false)
{
canMove = 1;
}
else if (map.findMonsterGroupIndex(newPos) != -1)
{
canMove = 2;
}
if (canMove == 0)
{
// no obstacle
pos = newPos;
[...]
}
else if (canMove == 1)
{
// wall
[...]
}
}