Part 42: Monsters part 4: Hit probability and damages
Downloads
Monsters parameters
<monsters>
[...]
<!-- 01 Giant Scorpion -->
<monster name = "MONSTER01">
[...]
<defense>55</defense>
<health>150</health>
<attack>150</attack>
<dexterity>55</dexterity>
[...]
</monster>
<!-- 02 Swamp Slime -->
<monster name = "MONSTER02">
[...]
<defense>20</defense>
<health>110</health>
<attack>80</attack>
<dexterity>20</dexterity>
[...]
</monster>
int CMonsters::getStartingLife(int type)
{
int baseHealth = monstersDatas[type].baseHealth;
return baseHealth * game.levelMultiplier() + RANDOM((baseHealth / 4) + 1);
}
game.levelMultiplier() is a value that will be used in various calculations throughout the game.
int CGame::levelMultiplier()
{
static const int multipliers[] =
{
0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 5, 6, 6
};
return multipliers[currentLevel - 1];
}
Front monsters' row
// 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];
}
Then in the group, before calling the updates of each monster, we will use this to check if the front row is dead
void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
{
// check if there is at least one living monster in the front row of the group (or at the center)
bool isFrontRowRowAlive = false;
for (int i = 0; i < 4; ++i)
{
if (monsters[i].pos != eMonsterPosNone &&
getMonsterRow(mapPos, monsters[i].pos) < 2)
{
isFrontRowRowAlive = true;
break;
}
}
Then when we update the monsters, we can tell them if they can attack.
void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
{
[...]
// update all the monsters of the group
for (int i = 0; i < 4; ++i)
if (monsters[i].pos != eMonsterPosNone)
{
uint8_t row = getMonsterRow(mapPos, monsters[i].pos);
bool isInFront = (isFrontRowRowAlive == true && row < 2) ||
(isFrontRowRowAlive == false && row == 2);
monsters[i].update(*this, mapPos, type, isInFront);
}
Choosing a target
void CMonster::fight(CVec2 mapPos, uint8_t type, bool isInFront)
{
if (nextAttackTimer.update() == true)
{
// 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
int target = chooseTarget(mapPos);
This champion will need to be in the front row of the player's group.
// returns the row of champions depending on the monsters group position (0 = front, 1 = back)
uint8_t CMonsterGroup2::getChampionRow(CVec2 mapPos, 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 = player.getLocalFromPos(mapPos);
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];
}
Then in the chooseTarget() function we will also need to check if the front row of champions is still alive.
// choose the player this monster will attack
uint8_t CMonster::chooseTarget(CVec2 mapPos)
{
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 &&
CMonsterGroup2::getChampionRow(mapPos, i) == 0)
{
rowToSearch = 0;
break;
}
}
// 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 &&
CMonsterGroup2::getChampionRow(mapPos, i) == rowToSearch)
{
possibleTargets[nbTargets++] = i;
}
}
// choose a target
return possibleTargets[RANDOM(nbTargets)];
}
Hit probability
#define RANDOM(_maxValue) (rand() % (_maxValue))
Now that we chose a target, there is a chance that it avoids the attack.
// compute monster's and champion's dexterities
int monsterDexterity = monsters.monstersDatas[type].dexterity + game.levelMultiplier() * 2 + RANDOM(32) - 16;
int championDexterity = game.characters[target].getDexterity();
The champion's dexterity is computed with this function.
int CCharacter::getDexterity()
{
int dexterity = stats[eStatDexterity].value + RANDOM(8);
// decrease dexterity depending on the load
dexterity -= (dexterity / 2) * stats[eStatLoad].value / stats[eStatLoad].maxValue;
// if we are sleeping...
if (interface.isSleeping() == true)
dexterity /= 2;
// bound the value
int min = 1 + RANDOM(8);
int max = 100 - RANDOM(8);
if (dexterity < min)
dexterity = min;
if (dexterity > max)
dexterity = max;
return dexterity;
}
The champion's base dexterity is reduced by the load he carries and a little bit randomized.
// check if we hit
if (interface.isSleeping() == true ||
monsterDexterity > championDexterity ||
RANDOM(4) == 0)
{
When the party is sleeping, the monster always hit.
Damages
// compute damages (here there should be a chance to parry)
int damages = monsters.monstersDatas[type].attack + game.levelMultiplier() * 2 + RANDOM(16);
As the comment says, here there should be a test to see if the champion parries the attack.
// randomize damages (quite a complicated way...)
damages /= 2;
damages += RANDOM(damages) + RANDOM(4);
damages += RANDOM(damages);
damages /= 4;
damages += RANDOM(4) + 1;
if (RANDOM(2))
damages -= RANDOM(damages / 2 + 1) - 1;
So why didn't they use a simple random.
// hit the champion
static char soundName[256];
sprintf(soundName, "sound/Champion_Damaged_%d.wav", RANDOM(4) + 1);
sound->play(player.pos, soundName);
game.characters[target].hitChampion(damages);
At this point, in the original game there was still a lot of things to take into account:
void CCharacter::hitChampion(int damages)
{
displayedDamages = damages;
damagesTimer.set(90, false);
}
These values will then be used in the interface to display the right image and number.