RPG Battle Stats FMPOV

Posted on
game-design rpg

Hello again!

Welcome to another entry in my RPG analysis series (wait I started a series right?)

With a brief memory lapse aside, and a obvious non-existent series mention, let’s look at RPG stats.

In this post I’ll go over some rather common RPG stats.

Please note, these are opinions from my point of view and are in no way backed by any real research outside of actual game examples.

P.S. please don’t hurt me.

Stats

For your convenience, I present:

Health Points - HP

Let’s begin with one of the easiest understood and non easliy recognized stat.

Health Points, often abbreviated as HP.

HP can be defined as “how many hits can I take before I die.”

In most RPGs this is represented by a visible numerical value, hence easily recognized.

But in other cases sometimes it’s not displayed as a number, but displayed as apart of the gameplay mechanics itself.

Take for example, Super Mario World, Mario in his smallest form has: 1 HP, that is, he will die after 1 hit/attack/damage, however he manages to obtain the Super Mushroom, he now grows in size and his HP now increases to 2, further more, obtaining the Fire Flower, gives him yet another point, raising him up to 3 HP.

Now given, Super Mario World is not an RPG, for that, there is Super Mario RPG, which is a great game btw.

But okay, maybe not Super Mario, how about The Legend of Zelda, those hearts in the top left corner, yup, Link’s HP; but there is one thing about those hearts, they aren’t 3 HP (3 Hearts), it’s actually 12 HP, the hearts themselves are divided into 4 quadrants, it’s just that most enemies end up removing an entire heart upon damage (4 HP), so it appears as if it does 1 heart of damage, of course to the player they wouldn’t think in terms of actual numerical values, but instead think in terms of hearts (i.e ‘I have 3 and a half hearts left.’ vs I have ‘14 HP left’).

As for it’s value, it tends to be a non-negative integer (i.e. unsigned integer), however newer games have started using Floats to represent it (e.g. Dota 2).

Magic Points - MP

Whether it’s spun as Magic Points, Mana Points, or it’s variant Special Points (SP), sometimes both are present and are used for a different set of magical/special abilities.

In most games, MP = how many points you have left to cast powerful or useful abilities.

It’s also a stat which is difficult to recover (e.g. mana potions tend to be more expensive than their health counterparts), or the magic available for “leeching” mana tends to fall on very niche characters (e.g. Witches from the Tactics Ogre series), though some games give a leeching spell for free to their magicians (e.g. Lufia, gave it’s magicians the Sap spell which could leech MP from enemies).

Similar to HP, it tends to use a non-negative integer (i.e. unsigned integer), but in newer games, sometimes it will be a Float.

Attack - ATK

Depending on the game, Attack can also be represented as strength, damage or any other stat someone thinks up.

But attack is usually always associated with the damage dealt from a physical source.

Because of attack’s myriad of different definitions, it’s sometimes a computed value, rather than a static value.

e.g. An entity has 4 Strength, and equipped with a stick that does 3 Damage.

# an arbitrary attack damage formula
atk = entity.strength * 2 + entity.weapon.damage

If the entity hits something, it will deal 11 damage, and we can say it’s Attack is 11 points.

Note this example completely ignores resistances and defense.

Defense - DEF

Defense is normally associated with physical resistance and is normally subtracted from the incoming physical attack damage.

Most times this is also a computed value similar to attack.

e.g. An entity has 4 Constitution, and armour which offers 2 points of physical resistance.

# an arbitrary defense formula
def = entity.constitution * 2 + entity.armour_value

This gives the entity 10 points of defense, when attacked those 10 points will be subtracted from the damage.

# the arbitrary attack damage formula from before
atk = entity_a.strength * 2 + entity_a.weapon.damage
# the arbitrary defense formula
def = entity_b.constitution * 2 + entity_b.armour_value
# you don't want to accidentally heal an entity from physical damage, though it is only a single point
dmg = max(atk - def, 0)

entity_b.hp = max(entity_b.hp - dmg, 0)

if entity_b.hp == 0 do
  entity_b.dead = true
end

Intermission I

Whew, let’s take a breather, so far we’ve covered HP, MP, ATK and DEF, with just HP, ATK and DEF we have a basic damage system.

Damage Formula

dmg = atk - def
hp = max(hp - dmg, 0)

And that’s great for the basics of an rpg system, however there are other stats, such as hit rates, critical rates, weapon/armor affinity, so much.

So for the next section I’ll go over things like critical, hit and evasion rates.

Critical Rate - CRIT

Of the rate-modifiers critical is possibly the easiest to understand, depending on how the system is designed, criticals can be a fixed multiplier applied to physical damage, or that multiplier could be a stat of it’s own, it could also be some other factors, it’s up to the designer.

Critical Hits, sometimes shortened as “Crits”, are damages usually several times larger than their normal counter parts.

However these “Crits” don’t happen often, they’re usually randomized, getting hit with several crits back to back is asking for pain.

Critical rates can be represented by a normalized decimal or by a whole number, the only thing it requires is that it has a “cap” that is a maximum value, this maximum value will be used as the domain for the random number generator.

is_crit = Random.random(100) <= crit_rate

Here we cap our crit_rate at 100, if the crit_rate is 5 (5%), and the generator produces a number 5 or less, then it’s considered a critical hit.

Some sharp folks may point out a glaring flaw, if the generator produces numbers from 0 to 100, it’s actually 101 different values, and since we used <= (less than or equal to), it’s actually 6% (0..5 that is, 0, 1, 2, 3, 4, 5).

In these cases if the random generator produces values “inclusive” of it’s cap, then subtracting 1 from the cap will fix the first problem, the second problem with it being 6% can be alleviated by changing from <= to simply < (less than).

is_crit = Random.random(99) < crit_rate
# Random produces between 0 and 99 (inclusive)
# so any value less than 5 is considered a critical hit

New Damage Formula

We’ll set our critical multiplier to 3.

crit_multiplier = 3
# the arbitrary attack damage formula from before
atk = entity_a.strength * 2 + entity_a.weapon.damage
is_crit = Random.random(99) < entity_a.crit_rate
atk = if is_crit do
  atk * crit_multiplier
else
  atk
end
# the arbitrary defense formula
def = entity_b.constitution * 2 + entity_b.armour_value
# you don't want to accidentally heal an entity from physical damage, though it is only a single point
dmg = max(atk - def, 0)

entity_b.hp = max(entity_b.hp - dmg, 0)

if entity_b.hp == 0 do
  entity_b.dead = true
end

Hit Rate - HIT

Critical hits are pretty scary if you’re on the receiving end, but what if you could avoid it?

Or avoid any hit for that matter, this is where hit rates come in, they affect the attacking entities ability to land a successful hit on a target, sometimes hit rates are called accuracy.

Just like the critical rate, it’s calculated in the same manner, except the result is binary, either it hits, or it doesn’t.

will_hit = Random.random(99) < hit_rate

New Damage Formula

crit_multiplier = 3
will_hit = Random.random(99) < entity_a.hit_rate
if will_hit do
  # the arbitrary attack damage formula from before
  atk = entity_a.strength * 2 + entity_a.weapon.damage
  is_crit = Random.random(99) < entity_a.crit_rate
  atk = if is_crit do
    atk * crit_multiplier
  else
    atk
  end
  # the arbitrary defense formula
  def = entity_b.constitution * 2 + entity_b.armour_value
  # don't want to accidentally heal an entity from physical damage, though it is only a single point
  dmg = max(atk - def, 0)

  entity_b.hp = max(entity_b.hp - dmg, 0)

  if entity_b.hp == 0 do
    entity_b.dead = true
  end
else
  # nobody got hurt today
end

With the introduction of the hit rate the entity can now avoid getting mauled sometimes, assuming the attacker has a hit rate less than 100.

Even with 99 hit rate, that gives the defender a 1% chance of not being hit.

Evasion Rate - EVA

Sadly the attacker just equipped his BFS with a red dot sight and its hit rate just capped at 100, looks like it’s game over for the defender.

But wait, evasion, it can still dodge the attacks!

The evasion rate affects the defending entity, however some attacks will never “miss” if they have some special flag or ability for that.

Once again, evasion is calculated in the same manner as the previous 2 rates:

will_evade = Random.random(99) < evasion_rate

New Damage Formula

crit_multiplier = 3
will_hit = Random.random(99) < entity_a.hit_rate
will_evade = Random.random(99) < entity_b.hit_rate
if will_hit and not will_evade do
  # the arbitrary attack damage formula from before
  atk = entity_a.strength * 2 + entity_a.weapon.damage
  is_crit = Random.random(99) < entity_a.crit_rate
  atk = if is_crit do
    atk * crit_multiplier
  else
    atk
  end
  # the arbitrary defense formula
  def = entity_b.constitution * 2 + entity_b.armour_value
  # don't want to accidentally heal an entity from physical damage, though it is only a single point
  dmg = max(atk - def, 0)

  entity_b.hp = max(entity_b.hp - dmg, 0)

  if entity_b.hp == 0 do
    entity_b.dead = true
  end
else
  # nobody got hurt today
end

Intermission II

So now we’ve managed to add various rates into our damage formula, entity_a and now “miss” or deal critical damage to entity_b, who can in turn dodge those attacks, sometimes.

But it’s really just whack a mole at this point, entity_a is always the one attacking, somehow we need the entities to take turns.

Depending on the genre of game, different stats factor into an entity’s attack order.

For action games, this is normally replaced with an attack cooldown, preventing the player from spamming attacks.

In the next section I’ll go over some turn affecting stats.

Speed - SPD

One of the first variants of turn order stats can be speed, this stat determines how quickly an entity is allowed to act, the higher, the sooner, the lower the later.

Speed can be a computed value, by factoring the entity’s strength, carrying capacity, and equipment weight, or it can be some fixed value, if thats needed.

All entities are then sorted by their speed, those with higher speed should act first.

Pokemon, is a game which uses this kind of system, faster pokemon will act first, and the slower one will go second, unless trick room is in play, in which case it’s the other way around.

turn_order =
  Enum.sort_by(entities, fn entity ->
    entity.spd
  end)
  |> Enum.reverse()

Fatigue - FTG

Fatigue is a bit different from speed, in that it accumulates and has to decrement before the entity can act, similar to the Wait Time (in everything but name), Fatigue is accumulated from actions.

And just like wait time it’s subject to the same turn order logic.

turn_order =
  Enum.sort_by(entities, fn entity ->
    entity.ftg
  end)

Unlike speed which is fixed, fatigue is an accumulated stat from every action that is performed, unless otherwise stated.

# do attack stuff
...
# fatigue should be applied regardless of the action's outcome, though it's value can change based on the result
entity_a.ftg += action.ftg

Observation

Looking at RPG stats from both the West and Japan, many will see a difference in their scale.

Western RPGs tend to start small, and get into fractional values with precision up to 2 decimal places.

However in Japanese RPGs, they forgo the decimal and keep everything in large whole numbers.

My assumptions:

  1. Technical limitations of console hardware in consoles during the 90s. Considered the old school golden era, the consoles that defined many households were limited by their CPUs. They lacked floating point operations, and if they did manage to have it, were imprecise, required additional chips to work, or were just too slow for video games. This would greatly affect what JRPGs could do on their parent hardware. On the other hand, many WRPGs appeared on home computers, which were capable of floating point operations (even if it was plagued with the previous problems mentioned for consoles), however they were ‘fast’ enough, since these processors tended to be clocked higher than their console counterparts, not to mention the greater availability of RAM.

  2. Difference in currency value. In the west, currency tends to be presented in decimals up to 2 decimal places. In the east, currency is presented in whole numbers, often omitting the decimal places. Lets working with an imaginary exchange rate, let’s say 1 USD is 100 JPY. It means an item for 1.99 USD would be for 199 JPY, or an item for 6800 JPY would be 68 USD. As you can see the difference really starts to show the higher the value goes.

  3. With both of the aforementioned assumptions, it would make sense for developers targeting their local audience to use a numerical system and value that the population was more accustomed to seeing.