TC101: How Stats Are Calculated

Primary attribute calculations seem like they should be a pretty simple topic. So simple, in fact, that most players don’t even think about how they’re done. Most theorycrafters don’t either until they try to write a spreadsheet that models a character and notice that their math doesn’t work out.

I wonder how many times the following scenario has been re-enacted over the past few years:

Okay, my Paladin has 1455 base strength and 2378 strength from gear, for a total of 3833. With the 5% bonus for wearing plate armor, that should give me 3833\times 1.05=4024.7. The 5% stats raid buff should raise that to 3833\times 1.05\times 1.05=4225.9. My character sheet only gives an integer, so it should round that to 4226. Right?

4224 is not equal to 4226

4224 is not equal to 4226

Wait, what??

As it turns out, stat calculations are one of the more convoluted things in the game, and I suspect that (until now) few theorycrafters have modeled all the nuances with complete accuracy. Blizzard tosses a few floor() and round() functions into the mix at seemingly arbitrary places, which makes it tougher to reverse engineer. Over the past month or so, I’ve been collecting data from beta and working on determining exactly where and how the stat calculations are rounded.

This is a Theorycrafting 101 article because this process is a great example of the sort of thing I spoke about in part 1: starting with a basic model and adding complexity until all the details work. After we go over the formulas for calculating stats, we’ll go step-by-step through the process I used to test and determine the formulas.

Primary Attribute Formulas

Here’s how your character sheet attributes are calculated. First, we define some conditional values.

$\text{match} = \begin{cases}1.05 & \text{if armor matches class} \\ 1.00 & \text{otherwise} \end{cases}$

$\text{epicurean} = \begin{cases} 2 & \text{if pandaren} \\ 1 & \text{otherwise} \end{cases}$

$\text{alchemy} = \begin{cases} 2 & \text{if alchemist} \\ 1 & \text{otherwise} \end{cases}$

$\text{multiplier}$ is the total multiplier from buffs and other effects. So for example, if the only buff you have active is Blessing of Kings,

$\text{multiplier} = \begin{cases} 1.05 & \text{for STR/INT/AGI} \\ 1.00 & \text{otherwise} \end{cases}$

Similarly, Fortitude would be a $\text{multiplier}$ of 1.10, Guarded By The Light would give 1.25, and so on. Multiple effects are multiplicative, so a protection paladin with Fortitude active would have a stamina multiplier of $1.10\times 1.25=1.375$.

We then calculate the total base $B$ and gear $G$ contributions before multipliers:

$B = \text{race_base}+\text{class_base} + \text{heroic_presence} + \text{endurance}$

$\begin{align} G = \text{gear_stat} &+ \text{round}[~\text{food_stat}~] \times \text{epicurean} \\ &+ \text{round}[~\text{flask_stat}~] \\ &+ \text{round}[~\text{potion_stat}~] \\ &+ \text{round}[~\text{trinket_proc_stat}~] \end{align}$

And generate a “composite” value $C$ that incorporates the matching multiplier:

$C = \text{floor}[~G\times \text{match}~]+B\times \text{match}$

Finally, your character sheet mouseover tooltip reads:

Strength CS_Total ( CS_Base + CS_Bonus ),

with:

$\text{CS_Total} = \text{floor}[~C \times \text{multiplier}~ ]$
$\text{CS_Base} = \text{floor}[ ~B \times \text{match}~ ]$
$\text{CS_Bonus} = \text{CS_Total} – \text{CS_Base}$

Building The Model

Now that we’ve got the math out of the way, let’s see how we determined it. First of all, let’s go back to our original example. If we log on to the beta PvP server and create a new level 100 Paladin, this is what we get:

When I grow up, I want to run a camel farm.

When I grow up, I want to run a camel farm.

As you can see, the character has 1455 base strength and 2378 “bonus” strength.  Since we haven’t chosen a spec yet, and we’re completely unbuffed, these values should properly reflect our total base strength and strength from gear, respectively. We can double-check this, because Celestalon gave us the full list of base stats for each class ($\text{class_base}$)as well as the racial base stat modifiers ($\text{race_base}$). A human’s $\text{race_base}=0$ and a paladin’s $\text{class_base}=1455$, so it’s quite clear that our character sheet’s giving us the correct base value. Likewise, you could go through and add up all the strength on each piece of gear to confirm that the sum is 2378. Together, that makes the 3833 total given on the character sheet.

In other words, so far we’ve got the following skeletal formulas:

$B = \text{class_base}+\text{race_base}$
$G = \text{gear_stat}$
$\text{CS_Base} = B$
$\text{CS_Bonus}=G$
$\text{CS_Total}=B+G$

Those aren’t final, of course – we’re going to be adding to them and correcting them as we go.

This is also useful because it means when we go to test other classes, we can look at the unbuffed values before we chose a spec to grab our $B$ and $\text{gear_stat}$ values.

Adding Armor Skills

Now let’s choose a spec. When we spec Retribution, we get the 5% increase to strength from the Armor Skills passive. Which gives us slightly different numbers:

We can respec her. We have the technology. We can make her ...stronger...faster.

We can respec her. We have the technology. We can make her …stronger…faster.

The only thing we’ve changed is to add a multiplier of 1.05 thanks to the armor specialization passive. Yet, as you might have guessed from our earlier example, our new value is not just 1.05 times our un-specced value of 3833. $3833\times 1.05 = 4024.7$, yet our character sheet reads 4023! Let’s see what’s going on here.

First, let’s consider the base value. We started with 1455, and $1455\times 1.05=1527.8$. The character sheet reads 1527, though, which tells us that the character sheet is taking the result of the calculation and applying a floor() function. In fact, this isn’t much of a surprise – it’s been known for a while that all of the values on the character sheet are floored rather than rounded. The game still uses the full-precision values when it does calculations though, so you don’t have to worry about stat points being “wasted” due to rounding/flooring.

Similarly, the 2378 strength from gear has become 2496 bonus strength. If we check the math, $2378\times 1.05 = 2496.9$. So the bonus strength is also being floored, not rounded. Our total strength is just the sum of the two floored values, which tells us that some of this flooring is happening before it’s displayed on the character sheet, otherwise we should have 4024 strength, as mentioned earlier. This is also useful information: since we know that the three character sheet values are linked through basic addition, we really only need to find correct formulas for two of them, and the third will fall into place automatically.

Now, none of this is really news. These details have been known for some time, since it’s very easy to stumble across. But this is how someone, at some point, had to go about determining it.

So now we can update our skeleton formulas slightly to incorporate the new details:

$B = \text{class_base}+\text{race_base}$
$G = \text{gear_stat}$
$\text{CS_Base} = \text{floor}[ ~B \times \text{match}~]$
$\text{CS_Bonus}=\text{floor}[~ G\times \text{match}~]$
$\text{CS_Total}=\text{CS_Base}+\text{CS_Bonus}$

The trick here is that there’s some ambiguity about those floor functions. For example, according to this, our $\text{CS_Total}$ could be expressed as:

$\text{CS_Total} = \text{floor}[~B\times \text{match}~] + \text{floor}[~G\times \text{match}~],$

but in reality, we’d get the same result from either of the following formulas as well:

$\text{CS_Total} = \text{floor}[~\text{floor}[~B\times \text{match}~] + G\times \text{match}~]$
$\text{CS_Total} = \text{floor}[~B\times \text{match} + \text{floor}[~G\times \text{match}~]~]$

So we don’t really know which is correct yet. The one thing we can rule out is having no floors.

Incorporating Multipliers

Now let’s apply Blessing of Kings to see how that interacts with these formulas:

It's good to be the King.

It’s good to be the King.

Our first guess might have been that Kings would work the same way as the matching multiplier. In other words, our character sheet base should be $1455\times 1.05\times 1.05=1604.1$, or 1604. But it’s clear that’s not how it works, because our base value hasn’t changed – it’s still 1527. Likewise, if we treat the bonus strength contribution the same way, we’d get $2378\times 1.05\times 1.05 = 2621.7$, which is far too low. Something else is happening here.

If we naively take our total strength before Kings (4023) and multiply by the Kings modifier, we get $4023\times 1.05=4224.2$, which is exactly right after applying a floor function. Interesting! This actually gives us a hint as to how to proceed, but I’m going to blithely ignore it for instructional purposes.

That detail does make it pretty clear that base strength is affected by Kings, though. But the game’s accounting adds that extra strength to the green bonus strength value rather than the white base strength value in the tooltips.

For the moment, let’s go with our earlier (and unstated) assumption that the game calculates things the way we have, such that “base” and “bonus” strength are calculated independently and then summed to get the total. We’ll find out shortly if this is a good assumption or not. The bonus strength value is then, roughly speaking,

$G\times \text{match}\times \text{multiplier} + B\times \text{match}\times (\text{multiplier}-1)$

with the caveat that there may be some floors going on in there. We can turn that “may” into a “must” by checking the math:

$2378\times 1.05 \times 1.05 + 1455\times 1.05\times 0.05 = 2698.1$

which should give us a bonus strength value of 2698, not 2697. So we know that in order for this formulation to be correct, there has to be a floor happening somewhere before we get to the value that the game floors to show on the character sheet. We can also rule out any uses of round() here, because that would always give 2698.

This is the same ambiguity I spoke of earlier – we knew that the character sheet values were being floored, but we weren’t 100% sure where. Including the Kings multiplier here has clarified that there needs to be at least one extra floor() floating around in our hypothetical formula, but doesn’t tell us exactly where. So we have to try them all, and see if we can rule any of them out.

There are four logical scenarios that use only one floor (in addition to the final floor used to display the value on the character sheet). They are:

$F1=\text{floor}[~G\times \text{match}~]\times \text{multiplier} + B\times \text{match}\times (\text{multiplier}-1)$
$F2=\text{floor}[~G\times \text{match}\times \text{multiplier}~] + B\times \text{match}\times (\text{multiplier}-1)$
$F3=G\times \text{match}\times \text{multiplier} + \text{floor}[~B\times \text{match}~]\times (\text{multiplier}-1)$
$F4=G\times \text{match}\times \text{multiplier} + \text{floor}[~B\times \text{match}\times (\text{multiplier}-1)~]$

If we put our data into this, with $G=2378$, $B=1455$, $\text{match}=1.05$, and $\text{multiplier}=1.05$, they give us the following results:

$F1=2697.19$
$F2=2697.39$
$F3=2698.10$
$F4=2697.75$

This rules out $F3$, because the final floor() on the character sheet would leave that as 2698, which is wrong. But this test can’t distinguish between $F1$, $F2$, and $F4$. Any of those could still be correct. Unfortunately, that’s all the information we can extract from our premade Ret paladin.

We learn a little more if we swap specs to protection. In protection spec, we get a 5% increase to stamina from the Armor Skills passive along with the 25% increase to stamina from Guarded By The Light. Starting with $B=890$ base stamina and $G=3250$ from gear, and using $\text{multiplier}=1.25$, the formulas give us:

$F1=4498.63$
$F2=4498.63$
$F3=4499.13$
$F4=4498.63$

And looking at the character sheet….

One of these things is not like the others.

One of these things is not like the others.

Uh oh. The only one that worked was $F3$, which we’ve already ruled out with our ret paladin. So this tells us that none of those formulas are correct. And increasing the number of floors doesn’t help, because it just makes the values even smaller, which won’t satisfy the protection data. For example,

$F5=\text{floor}[~G\times \text{match}\times \text{mult}~]+\text{floor}[~B\times \text{match}~]\times (\text{mult}-1) = 4498.50$

Still no dice. We can’t add round functions either, because then the ret data isn’t satisfied. As it turns out, we’re going about this the wrong way. Our original hypothesis – that the game calculate the base and bonus values individually and sums them to get the total – has to be false.

This isn’t really news, because theorycrafters have been using an alternative formulation for years. But it’s a good example of coming up with a hypothesis, testing it, and ultimately ruling it out, which is something that happens all the time in theorycrafting. Now that we’ve done so, let’s see if we have more luck with another hypothesis.

A Change Of Approach

The traditional approach goes something like this: rather than trying to calculate base and bonus strength individually, let’s try deriving correct formulas for the base and total values. Then the bonus value will be determined using basic subtraction. As we’ll see, this approach is much more successful.

Just as we did for bonus strength, we’ll construct formulas for total strength using $B$, $G$, $\text{match}$, and $\text{multiplier}$. We also know there has to be at least one floor in there somewhere based on our armor matching modifier test.

There are six obvious ways to do this using one or two floor functions:

$T1 = \text{floor}[~G\times \text{match}~]\times \text{multiplier}+B\times \text{match}\times \text{multiplier}$
$T2 = \text{floor}[~G\times \text{match}\times \text{multiplier}~]+B\times \text{match}\times \text{multiplier}$
$T3 = G\times \text{match}\times \text{multiplier}+\text{floor}[~B\times \text{match}~]\times \text{multiplier}$
$T4 = G\times \text{match}\times \text{multiplier}+\text{floor}[~B\times \text{match}\times \text{multiplier}~]$
$T5 = \text{floor}[~G\times \text{match}~]\times \text{multiplier}+\text{floor}[~B\times \text{match}~]\times \text{multiplier}$
$T6 = \text{floor}[~G\times \text{match}\times \text{multiplier}~]+\text{floor}[~B\times \text{match}\times \text{multiplier}~]$

We could also come up with two more methods using two floors, but they would require that we be flooring before the $\text{multiplier}$ in one term but not in the other, which seems unlikely. If none of these hold up, we’ll revisit that idea.

Plugging in our ret paladin stats of $G=2378$, $B=1455$, $\text{match}=1.05$, and $\text{multiplier}=1.05$, we get the following results:

$T1=4224.94$
$T2=4225.14$
$T3=4225.10$
$T4=4225.75$
$T5=4224.15$
$T6=4225.00$

So right out of the gate, we can cross off four of the formulas (2, 3, 4, and 6). Only $T1$ and $T5$ give a value consistent with the character sheet. And there’s something notable about those two formulas: we can factor out $\text{multiplier}$ in each of them:

$ T1  = \left (~ \text{floor}[~G\times \text{match}~]+B\times \text{match}~ \right )\times \text{multiplier}$

$T5 = \left (~ \text{floor}[~G\times \text{match}~]+\text{floor}[~B\times \text{match}~] ~ \right ) \times \text{multiplier}$

that observation will come in handy later. For now, we need to figure out which one of these two formulas correct.

It’s worth noting that $T5$ is what’s frequently been used for calculating base stats in most theorycrafting works. This is what Simulationcraft has been using throughout MoP, for example. It’s pretty close, and generally gives you answers that are correct.

But as we noticed while working on the WoD code, on some rare occasions, it’s off by one. You can see why, as well: Let’s say that $B=919$. Then $B\times \text{multiplier}=964.95$. If we just multiply by $\text{multiplier}$ like we do in formula $T1$, we’d get a subtotal of 1013.20. But if we floor that 964.95 before multiplying, we get 1012.20. The two formulas would give answers that differ by one, with $T1$ giving a slightly higher value than $T5$.

So to test this discrepancy, we need to find a character with just the right amount of stat bonus. This gets easier the higher $\text{multiplier}$ is, so let’s try with our prot paladin, whose $\text{multiplier}=1.3750$ for stamina once we apply the 10% stamina buff.  Again using $B=890$, $G=3250$, and our $\text{match}=1.05$, we get:

$T1 = 5976.44$
$T2 = 5975.75$

Demonstrating the “off-by-one” error. And what does the character sheet say?

Conclusive Evidence!

We’ve already shown earlier that this value has to be floored rather than rounded, so this removes any ambiguity. $T1$ is the only formula that’s still standing. We now know conclusively how our total stats are calculated, at least so far.

Since we can factor the $\text{multiplier}$ out, it makes some sense to define a “composite” subtotal $C$ to make the math a little easier. In other words, we define it such that

$C = \text{floor}[~G\times \text{match}~]+B\times \text{match}$,

and then our character sheet base and total values are just

$\text{CS_Base} = \text{floor}[~ B\times \text{match}~]$
$\text{CS_Total} = \text{floor}[~C\times \text{multiplier}~]$

And of course, $\text{CS_Bonus} = \text{CS_Total}-\text{CS_Base}$. This gives you the bulk of the formulation provided in the beginning of this post.

The Nitty-Gritty Details

We’re not finished yet, though. Because even after this, we were able to observe some odd off-by-one errors due to certain special effects. For example, how are flat stat-bonus buffs like potions, trinket procs, flasks, and food incorporated into this? What about racial effects like Endurance, Heroic Presence, and Epicurean? So I set out to do some more testing.

Endurance was the easiest to test. Consulting our handy tables, all of the classes have 890 base stamina, and taurens get a racial modifier of +1. So before we choose a spec, our tauren paladin test subject (lovingly named Testbeef) should have 891 stamina. Instead…

There's some extra beef here...

There’s some extra beef here…

Endurance gives 197.055 stamina at level 100. The tooltip is, as usual, floored, but we can get an exact value directly from the spell data using Simulationcraft’s spell_query tool:

SimC to the rescue.

SimC to the rescue.

It’s clear from this that Endurance is being counted as base stamina by the game (890+1+197 = 1088). In other words, we can update our formula for $B$:

$B = \text{race_base}+\text{class_base}+\text{endurance}$

And it’s fairly simple to confirm that all of the other formulas work out to accurate values as given.

Next up is Heroic Presence, so we create a draenei paladin. It isn’t hard to determine how this works:

Heroic base strength.

The tooltip for Heroic Presence reads 65, but spell_query reveals that the actual value is 65.25. Draenei get a 1-point racial strength modifier, so it’s pretty clear that our base strength is just 1455+1+65=1521, meaning that Heroic Presence is also added into $B$:

$B = \text{race_base}+\text{class_base}+\text{endurance}+\text{heroic_presence}$

There’s one caveat here, which is that we can’t know for sure from this data whether Endurance or Heroic Presence are being floored before they’re added to $B$. Both of them have low enough decimal values that it would be unlikely to matter and difficult to test. Heroic Presence’s value was 130.5 several beta builds ago, and I was able to confirm that it isn’t being rounded (though that was unlikely anyway). But I couldn’t rule out a floor(). To do so now, we’d need modifiers such that $0.25\times \text{match}\times \text{multiplier} > 1$ to test Heroic Presence (or $0.055\times \text{match}\times \text{multiplier}>1$ for Endurance), which we don’t have. So it probably doesn’t matter very much, but it’s worth noting in case we find a situation where we can explicitly test that.

Epicurean was the most interesting test, because first we had to figure out how stat buffs worked. For example, is the amount of stat given by food floored or rounded? It turns out that by being clever, we can test both at once.

First, we searched through to find some foods that would be useful to us. The two we ended up using were Serpent Brew of Serenity and Hearty Elekk Steak. Serpent Brew has an intellect bonus of 24 according to the buff it grants. However, if we check the spell data…

Sneaky Sneaky

Sneaky Sneaky

… it apparently gives a buff of 23.816 intellect. Which is a strong case for flat stat bonus buffs being rounded, not floored, and making them the exception to just about everything else. We can double-check this by rolling a level 100 monk, using Zen Pilgrimage to visit Master Chang at the Peak of Serenity, and testing this:

I took off all of poor Foodtest's gear and didn't give her a spec to make this easier to see. I'm sort of a jerk like that.

I took off all of poor Foodtest’s gear and didn’t give her a spec to make this easier to see. I’m sort of a jerk like that.

This tells us that the buff is definitely rounded, not floored. Because there are only two ways to get 48 intellect from a 23.816-intellect buff on a pandaren, and neither of them involve a floor:

$F1 = \text{round}[~\text{food_stat}\times 2~]$
$F2 = \text{round}[~\text{food_stat}~]\times 2$

To figure out which one we have, we employ the new Hearty Elekk Steak, which is conveniently available from Savage Flaskataur, Esq. in Stormwind or Orgrimmar. This food is almost magical, in that the spell data says it grants a 187.49999 stamina buff. Even spell_query rounds this to 187.5, which led to a little confusion until we sussed out the issue. But what it means for us is that if $F1$ is correct, we’ll see a buff that’s 375 stamina on our pandaren, but if formula $F2$ is correct it will only be 374. And what is it?

The steaks are low.

So apparently, food buffs are rounded, and Epicurean is applied after the rounding occurs. It’s also fairly easy to test that these are affected by $\text{match}$ and $\text{multiplier}$ just like gear contributions are, so we can fold this directly into our definition of $G$:

$G = \text{gear_stat} + \text{round}[~\text{food_stat}~] \times \text{epicurean}$

Testing the others follows a similar process. Spell data tells us that a Flask of the Earth adds 170.926 stamina, but in-game both the tooltip and character sheet show that it grants 171 stamina. So flasks are clearly rounded as well. The Alchemy profession buff has already been turned off, so we can safely ignore that. A Potion of Mogu Power grants 455.793 strength according to spell data, but shows up as 456 in-game, suggesting that potions are also rounded. Testing with a few different trinket procs also showed that they were all rounded, not floored.

The one thing we haven’t shown yet is whether these effects are rounded individually or after-the-fact. The Epicurean data strongly suggests each is rounded independently, but a more rigorous test would be nice. To do so, we need a pair of buffs that give a different value when rounded separately than together – e.g. both ending in 0.5 or greater, or both ending between 0.25 and 0.49.

Digging through the spell data, we come up with Beer Basted Crocolisk, which grants 35.724 strength and stamina, and Flask of Steelskin, which grants 178.62 stamina. If we use both items, we should get 215 stamina if each is rounded individually, but only 214 stamina if they’re rounded after being added together. A few unlucky crocolisks later, we have our confirmation:

Only about four crocolisks were harmed during the filming of this test.

This is the last piece of the puzzle, and finishes confirming the equations given in the beginning of this post.

Closing Thoughts

You can see that we’ve done a pretty exhaustive job of testing edge cases to make sure our formula for base stats works correctly in all circumstances. If we were content to be accurate to within one point of stat, we could have stopped very early on. But it was important to me that the stats Simulationcraft spits out match your character sheet all the time. Ultimately that lack of accuracy reflects poorly on the sim, even if “off-by-one” errors have no significant effect on the overall simulation results.

And as you’ve now seen, trying to cover all of those edge cases often takes some careful thought about what those cases are and how you can test them. Often it means ruling out all but one hypothesis, and sometimes you need very specific items or gear combinations to distinguish between different hypotheses. Most combinations of food+flask wouldn’t reveal the difference between the two rounding schemes proposed, we had to seek out a very specific pair based on the numerical constraints of the problem.

But that’s the sort of work theorycrafters do – wading through the minutiae to build models that are as accurate as possible. And sometimes, even an “easy” task like calculating player attributes ends up having a ton of details that you never thought about before.

This entry was posted in Theck's Pounding Headaches, Theorycrafting and tagged , , , , , , . Bookmark the permalink.

10 Responses to TC101: How Stats Are Calculated

  1. Cors says:

    I am glad you enjoy doing all this. It makes my head spin :)

  2. Pingback: TC101 Homework: Character Sheet Stat Calculations | Fel Concentration

  3. Komma says:

    Finally got around to reading this in full. Just wanted to note that in the four logical scenarios for investigating stat multipliers, you listed “F1=” four times by mistake.

    Amazing sleuthing with the case elimination and reverse engineering.

  4. Pingback: TC401: Avoidance Diminishing Returns in WoD | Sacred Duty

  5. meriticus says:

    In programming, there are generally different types of numbers (integers, floats, etc). Integers do not support decimals, and just drop them if present (i.e. flooring). For example:

    int x = 2 * 1.05

    will be calculated as 2.1, but since x is an integer, the decimals will be dropped, resulting in x = 2

    My guess is a lot of the calculation done by the game uses integers instead of a type that supports decimals (probably for efficiency reasons), which is why all these ‘floors’ occur. I can’t imagine why they would purposefully floor all these things on purpose.

    • Theck says:

      It’s quite possible that they store some values as ints rather than e.g. doubles, but there’s no way to be certain without looking at their code. For our purposes, it’s entirely irrelevant whether the flooring happens because of type casting a result from double to int or whether it’s from an explicit std::floor() call.

      Also note that there are cases where they explicitly round(), which is pretty clearly not due to type casting.

  6. Jackinthegreen says:

    An excellent rundown of stats, as always!

    Question though: Is there a particular reason Blizz couldn’t have things be whole numbers instead? Is there anything to be gained by doing things this way?

    • Theck says:

      There are several things to be gained by using floating point values rather than ints, mostly range and precision. Calculations on floating points are also (usually?) a bit slower than on integers though, so there are trade-offs. I couldn’t tell you why they did it the way they did.

Leave a Reply