Stat Rebalancing
This is an introduction to manipulating stat values and calculations on AzerothCore for WoW version 3.3.5.
-
We will cover a brief theory of how stat calculations work, what files and source code to edit, as well as a basic introduction to the kind of mathematical transformations involved.
-
We will then go on and provide specific pointers to where common item and stat calculations can be modified in AzerothCore as of revision 0773ed2. I will attempt to keep descriptions version-agnostic and broad enough that variations to the exact source layout can be navigated around, but only so far that this is realistically possible. Major rewrites of the stat system would render this section of the guide outdated, and readers should be aware of potential discrepancies when applying it to emulator versions far into the future.
-
This guide was originally written as a commission, so it focuses mostly on aspects requested by that client, but should still work as a good introduction to the stat system as a whole.
The kind of stats we will be discussing involve:
- Primary stats, such as strength, stamina and intellect
- Secondary stats, such as attack power and armor
- Combat/Percentage-based ratings, such as haste rating, defense rating, critical strike rating and so on
- Pet stats
- “Final” stats, such as hitpoints, mana, mana regeneration and damage reductions.
Prerequisites
This guide assumes a basic knowledge of the following topics:
- Compiling the core
- Reading (mostly basic) C++ code
- Editing databases with a tool like HeidiSQL or MySQL Workbench
- Editing DBC files with an editor such as WDBX.
Disclaimers
Many of the stat covered in this guide should work and apply predictably as described, granted the proper emulator version and input data is used. However, wow emulators are not perfect and especially anything relating to spells has a tendency to contain specific hardcodes in wildly different places around the core, causing unexpected behavior for specific items or spells. This guide does not include many pointers for navigating such hardcodes, but is better covered in a separate tutorial altogether. It is not possible to provide a comprehensive map to every such individual hardcode, there are simply too many of them.
This guide also does not include any pointers for manipulating spell data modifiers to stat calculations, and is better covered in a tutorial about spell creation and manipulation. However, if navigating source code around locations given in this tutorial, it is often possible to find and apply simpler transformations even to spell input for some stat types, usually via calls to GetModifierValue
and similar functions. Keep in mind potential spell hardcodes if you decide to attempt this.
Theory
This section includes introductions to the basic concepts we will be discussing in future sections, such as what kind of files may need to be edited, what kind of stats exist in the game, and what kind of mathematical transformations we can apply to them.
Types of Stats
The stats we will discuss here all have various different mathematical traits and are defined in various ways throughout the game.
Explicit Values
These are values in the game that are explicitly saved to the player, such as their experience, race or class. These values do not depend on any other type of value, but serve as “pure inputs” to other calculations.
Data Tables
These are values that depend on other values via some kind of lookup, typically lookups from explicit values, and most often the players level.
For example, players primary stats (without items/spells) depend on explicit lookups into the player_levelstats
server database table, where a players race, class and level directly correlate to values for primary stats. In mathematical expressions for this guide, I will use the square bracket syntax for these types of lookups: base_stats[class,race,level]
, reading “base_stats, depending on the players class, race and level”.
Hidden / Shared Tables
Not all data tables correspond to a visible final stat in the game. In this guide, I will refer to these as “hidden tables”. These tables are very important to keep in mind, as they often influence wildly different stats and can cause very unexpected behavior if edited carelessly, but often serve a very important role in how the players level affects their final stats. Examples of hidden data tables include DBC tables starting with Gt
, as well as many hardcoded ratings coefficients found around the cores source code, in particular StatSystem.cpp
.
Intermediate and “Final” Stats
These are values that depend on other stats in some way, usually involving multiplication, addition or other mathematical transformations. These stats make up the bulk of visible stats in the game, and can both be affected by and affect other stats directly.
For example, players level is technically an intermediate stat depending on their total experience, but is also used as a lookup in many different data tables and calculations on their own.
Other values, such as a players max hp, is typically understood to be a “final” stat that do not influence any other stats in the game. This is almost always incorrect, and stats thought to be “final” are almost always intermediate. Players max hp influence plenty of spells and scripts in the game.
It is very important to remember that any stat you change in the game can have unexpected consequences for spell effects and scripts, both generic and hard-coded. In this guide, I will not be referring to any stat as final, but instead as “visible”.
Maths
Stat calculations in WoW can be complicated and involve a lot of variables and maths. This section will aim to explain in simple terms where and how you can often best achieve predictable results without necessarily understanding the full formula.
Specifically, we will look at where we often want to manipulate stat values, and what kind of transformations we want to apply to them.
Inputs and Outputs
As an example, let’s look at how players max hp is calculated in the core as a function of their stamina, class, level and aura bonues:
// Mathematical definition:
// MAX_HEALTH() = ((player_classlevelstats[class,level] + base_unit_value) * BASE_PCT_MODIFIERS) + (max(stamina,20) + (stamina-max(stamina,20)*10 )) * TOTAL_VALUE_MODIFIERS) * TOTAL_PCT_MODIFIERS`
void Player::UpdateMaxHealth()
{
UnitMods unitMod = UNIT_MOD_HEALTH;
float value =
// base_unit_value
GetModifierValue(unitMod, BASE_VALUE)
// player_classlevelstats[class,level]
+ GetCreateHealth();
// spells: base hp percentage
value *= GetModifierValue(unitMod, BASE_PCT);
// stamina, (after base spell pct, before total pct)
value += GetHealthBonusFromStamina();
// spells: flat spell modifier (after base spell pct, before total pct)
value += GetModifierValue(unitMod, TOTAL_VALUE)
// spells: total hp percentage modifier
value *= GetModifierValue(unitMod, TOTAL_PCT);
// custom scripts, may apply additional changes
sScriptMgr->OnAfterUpdateMaxHealth(this, value);
// writes the value, not part of calculation
SetMaxHealth((uint32)value);
}
...
float Player::GetHealthBonusFromStamina()
{
// How much stamina the player is assumed to have for this calculation
float stamina = GetStat(STAT_STAMINA);
float baseStam = stamina < 20 ? stamina : 20;
float moreStam = stamina - baseStam;
// Base stamina contribution to hp before modifiers apply.
return baseStam + (moreStam * 10.0f);
}
As you can see, there are many places where we can manipulate this formula, but I want to bring to your attention a few important places:
-
Inputs: where we directly read another stat value, such as the line
float stamina = GetStat(STAT_STAMINA);
-
Outputs: where a stat directly contributes to the total value, such as the line
return baseStam + (moreStam * 10.0f);
It is usually advisable to apply transformations close either to a calculations inputs or its outputs. The middle parts may contain very intricate logic that are not always very intuitive, and have often been developed out of necessity to balance the game or handle edge cases (such as the checks for very low stamina values above).
We can usually achieve more powerful expressions by applying transformations close to the input of a calculation, but this also means that it tends to cause changes to behave more unpredictably than if we apply it close to the output.
In Summary
- We usually want to apply transformations close to a functions inputs or its outputs
- Input transformations are more powerful but less stable than output transformations
Transformations
In mathematics, a transformation is simply a way that we take one value and transform it into a another value. Common ways to transform stat values is to add and multiply them, but more complex transformations such as polynomials, exponentials and logarithmics are also possible.
Polynomials and exponentials in particular are crucial to understand how progression in WoW works, and they show up very frequently in level-based data tables such as experience per level, spell damage, primary stats and gold rewards. However, inside calculations we usually stick to additions and multiplications, as more exotic transformations are usually so unpredictable that they rarely change the game in a useful way. A common way to express these in wow is “bonuses” for additions, and “multipliers/percentages/scalars” for multiplication.
In Summary
- Data tables tend to contain polynomial and exponential curves
- Calculations usually consist of addition (bonuses) and multiplication (multipliers/percentages/scalars) of values
Example: Stamina Transformation
Let’s illustrate the above points by applying multiplication to the amount of stamina assumed when calculating players max health.
In particular, let’s look at two deceptively similar statements:
- Double the amount of stamina used for calculating max health
- Double the amount of max health gained from stamina
At first glance, these statements might look equivalent, we double the stamina variable somewhere and it will “double” whatever we think of as the “output”. However, because the stamina function contains logical comparisons and additions, these are not equivalent, and where exactly we double the “stamina” value matters. In mathematical terms, we say that the stamina expression is non-commutative.
Statement 1
float Player::GetHealthBonusFromStamina()
{
// "Double the amount of stamina used for calculating max health"
float stamina = GetStat(STAT_STAMINA) * 2;
float baseStam = stamina < 20 ? stamina : 20;
float moreStam = stamina - baseStam;
return baseStam + (moreStam * 10.0f);
}
Statement 2
float Player::GetHealthBonusFromStamina()
{
float stamina = GetStat(STAT_STAMINA);
float baseStam = stamina < 20 ? stamina : 20;
float moreStam = stamina - baseStam;
// "Double the amount of health gained from stamina"
return (baseStam + (moreStam * 10.0f)) * 2;
}
Exercise: Attempt to run a few different stamina values into these two functions and observe the output. In particular, try out stamina values both below and above 20.
In Summary
- Changing stat calculations is a very delicate process, and it’s very important to analyze what exactly your transformations express.
Data sources
This sections aims to lay out the different data sources that need to be taken into account when re-balancing the game.
The “Hidden” DBC Tables (Gt*)
DBC tables with the prefix gt
serve an important role in how many level-based calculations take place in the game, but because many don’t have any direct visible equivalent, they are often unknown to many players and even some developers.
These files contain important percentage data for various calculations based on players levels and/or class. Contrary to many other data tables, these tables do not contain any columns indicating what level or class they actually map to, instead this is inferred from their index/id.
There are four types of gt
DBC tables with different layouts:
Class Indices
These tables have only a single value per class. The first row is for warriors (class id 1), the second for paladins (class id 2), the third row for hunters (class id 3) and so on.
GtChanceToMeleeCritBase.dbc
(base melee crit ratios)GtChanceToSpellCritBase.dbc
(base spell crit ratios)
Class / Level Indices
These tables contain 100 values per class, going from level 1 to level 100. The first 100 rows correspond to warriors, the following 100 rows to paladins and so on.
GtChanceToMeleeCrit.dbc
(level-based melee crit ratios)GtChanceToSpellCrit.dbc
(level-based spell crit ratios)GtOCTRegenHP.dbc
(non-spirit based hp regeneration)GtOCTRegenMP.dbc
(non-spirit based mana regeneration)GtRegenHPPerSpt.dbc
(spirit-based hp regeneration)GtRegenMPPerSpt.dbc
(spirit-based hp regeneration)
Combat Rating/Level Indices
These tables contain 100 level values for 32 different combat ratings. Note that these values are not indexed by class, instead the first 100 rows correspond to CR_WEAPON_SKILL
combat rating from level 1 to 100 for all classes, the following 100 rows to CR_DEFENSE_SKILL
and so on.
The full list of combat rating types is as follows:
enum CombatRating
{
CR_WEAPON_SKILL = 0,
CR_DEFENSE_SKILL = 1,
CR_DODGE = 2,
CR_PARRY = 3,
CR_BLOCK = 4,
CR_HIT_MELEE = 5,
CR_HIT_RANGED = 6,
CR_HIT_SPELL = 7,
CR_CRIT_MELEE = 8,
CR_CRIT_RANGED = 9,
CR_CRIT_SPELL = 10,
CR_HIT_TAKEN_MELEE = 11,
CR_HIT_TAKEN_RANGED = 12,
CR_HIT_TAKEN_SPELL = 13,
CR_CRIT_TAKEN_MELEE = 14,
CR_CRIT_TAKEN_RANGED = 15,
CR_CRIT_TAKEN_SPELL = 16,
CR_HASTE_MELEE = 17,
CR_HASTE_RANGED = 18,
CR_HASTE_SPELL = 19,
CR_WEAPON_SKILL_MAINHAND = 20,
CR_WEAPON_SKILL_OFFHAND = 21,
CR_WEAPON_SKILL_RANGED = 22,
CR_EXPERTISE = 23,
CR_ARMOR_PENETRATION = 24
};
Note that out of the 32 possible tables, only 25 are actually used by the game and the rest were probably left in for future use. 32 is a nice round number if you happen to count in binary.
Combat Ratings / Class Indices
This is another special table that concern combat ratings, but instead of being indexed by level they are indexed by class and contain 32 values per class. The first 32 rows correspond to warriors, the following 32 rows to paladins and so on. In any given 32 row chunk, the first row corresponds to CR_WEAPON_SKILL
, the second row to CR_DEFENSE_SKILL
and so on.
In Summary
- Gt tables contain important percentage data for different calculations, and are especially important when re-balancing calculations based on levels or classes.
- For most intents, it’s probably enough to know these tables exist and where such level/class-based ratios come from.
Exercise 1: If you open the file gtRegenHPPerSept.dbc in WDBX, you may notice that ids between 901 and 1000 are just 0. Why do you think this is? (hint: try looking at the class table in ChrClasses.dbc
)
Exercise 2: Gt
tables are the cause for the infamous crashes that druids experience on levels above 100, since they only contain 100 values per class. Druids, having the highest class id in the game, causes the character frame to almost always read uninitialized memory and crash the game if you open it. Knowing this, what kind of values do you think are displayed if you level up a mage to 101?
Server Database Tables
Non-percentage data is stored in tables in the servers mysql database. These tables contains players base hp/mana and primary stats depending on their level, class, and/or race.
player_xp_for_level
: How much experience is needed for each levelplayer_levelstats
: Primary stats for each level depending on race and classplayer_classlevelstats
: Base hp/mana for each level depending on class.
Hardcoded Source Tables
A few data tables are (for some reason) hardcoded into StatSystem.cpp
, but these might move into database tables one day. These tables contain class-specific ratios that don’t correspond to anything very intuitive, and their use is very local to the functions and file they are defined in.
These values typically refers to “diminishing” and “cap” values, but should not be confused with the common in-game “item caps” usually going by the same name, though these values do influence such caps even if indirectly.
note: if these tables change name, you can likely still find them by searching the files for the “[MAX_CLASSES]” array size specifier.
m_diminishing_k
- diminishing ratios for multiple percentage calculationsmiss_cap
- caps for diminishing miss valuesparry_cap
- caps for diminishing parry valuesdodge_cap
- caps for diminishing dodge values
Reading Source Code
In this section, we will look closer at how these values are actually handled by the emulator core, as well as some pointers for how to do future research and how they are found.
Learning Source Code
When navigating C++, a proper IDE is invaluable to find things you are looking for. If you are not editing files directly, modern versions of Visual Studio work very well to easily click on identifiers to find their declarations and definitions.
Knowing well what things to search for is also important, because not all values can be found just by following references around.
For example, say that we want to find calculations relating to expertise. This is a combat rating that uses the enum value CR_EXPERTISE
, and if we search the entire core repository for this string we can see that it shows up in only a few source files. This gives us functions such as Player::_ApplyItemMods
, Player::ApplyEnchantment
, Player::UpdateRating
, Player::UpdateExpertise
to investigate. This might seem overwhelming already, but remember that most stats in the game work very similarly to other stats similar to itself, so if we just start reading functions one by one we will eventually build familiarty with the system as we eventually reach a point of re-visiting the same functions over and over again.
It is important to remember that not every function will do exactly what it is we’re trying to get it to do, and sometimes their names are deceptive. If investigating source code on your own, it is important to work carefully and analyze what the current function is actually doing. Many values in WoW emulators are cached whenever something that influence them changes, and very few values are ever calculated straight from all its sources directly. This can cause many calculations to be disjointed, and it is usually necessary to search further for such cached data if you encounter it.
In Summary
- Searching and following references are fundamental building blocks for learning to navigate source code.
- Searching for enum values is a highly effective way to find stat calculations
- WoW emulators cache many of its values, so many calculations are disjointed.
Update Data
Update data refers to a big block of special fields stored on different game entities, such as creatures and players, meant to be sent to clients when they change. Any time you see source code that reads entity->SetUInt32Value
, entity->SetFloatValue
, entity->GetUInt32Value
and so on, this refers to updatedata fields.
These fields are very important in many stat calculations, as they are both used to send stat data to the client and to cache values when stat inputs change. A very common pattern for stat updates is that one functions write to an updatedata field that another function reads shortly after.
All update fields for different object types can be found in UpdateFields.h
, and especially interesting for stat re-balancing are Unit
and Player
fields.
There is little point in studying these fields on their own, and their purpose can be deceptive out of context, but it is important to be aware of what they are if you do encounter them. Many times stat calculations do not call SetXValue
functions directly, but via some wrapper function, some of which we will encounter in the following sections.
It is also important to be aware that some update data fields can contain multiple values inside a single field. For example, PLAYER_FIELD_COMBAT_RATING_1
has a size of 25, and contains separate values for each combat rating type.
Entry points
Following are some general starting points for reading about stat-related calculations specifically, some files and some functions. These will feature heavily in the following sections.
StatSystem.cpp
- Probably the most important file for stat calculations.Player.cpp
- Contains some offshoots fromStatSystem.cpp
SharedDefines.h
- Contains most stat-related enumsPlayer::ApplyRatingMod
Player::ApplyEnchantment
Player::_ApplyItemBonuses
Pet calculations
Pet.cpp
Stat Transformations
This sections contain a long list of specific stat transformation examples that can serve as an introduction to the stat system.
In this section, I will commonly use a convention of pointing out what Function a value is supposed to be changed in, as well as what Expression is supposed to be transformed for the desired effect.
For example, if we had the following (example) function:
void MyClass::GetHealthFromStamina()
{
int val1 = GetStat(STAT_STAMINA);
if(val1 < 20) val1 = 20;
return val1 * 10;
}
I might describe transforming “the amount of stamina used for calculating max health” as follows:
-
Function:
MyClass::GetHealthFromStamina
-
Expression:
GetStat(STAT_STAMINA)
Meaning, in plain english, that to apply the desired transformation, you should find the function called MyClass::GetHealthFromStamina
, and within it you should find and transform the expression GetStat(STAT_STAMINA)
. For example, if you wanted to double the amount of stamina used for calculating the maximum health, you would change this function to:
void MyClass::GetHealthFromStamina()
{
int val1 = (GetStat(STAT_STAMINA) * 2);
if(val1 < 20) val1 = 20;
return val1 * 10;
}
For some calculations, there may be multiple such expressions you need to change, and this will be expressed in the notation if so. For other calculations, the process of transforming it is more complicated and contains a more verbose description of how to achieve the desired effect.
Primary Stats
Primary stats refer to the typical rpg stats strength, agility, intellect, spirit and stamina.
When working with primary stats, you will commonly encounter constants from the Stats
enum, defined in SharedDefines.h
. Its members have the prefix STAT_
, so whenever you stumble upon identifiers like STAT_STRENGTH
, they refer to this enum.
Commonly, primary stat values are read with the Player method GetStat(StatType)
, such as GetStat(STAT_STRENGTH)
. This function returns the effective value of this stat on the player with spell modifiers applied.
Agility -> Armor
-
Function:
Player::UpdateArmor
-
Expression:
GetStat(STAT_AGILITY)
-
Example: Adding a flat +20 bonus to agility used for armor
void Player::UpdateArmor() ... value *= GetModifierValue(unitMod, BASE_PCT); // armor percent from items value += (GetStat(STAT_AGILITY) + 20) * 2.0f; // armor bonus from stats value += GetModifierValue(unitMod, TOTAL_VALUE); ...
Agility / Strength -> Attack Power
This transformation is complex, and requires reading source code
-
Function:
Player::UpdateAttackPowerAndDamage
-
Expression: Any instance of
GetStat(STAT_AGILITY)
andGetStat(STAT_STRENGTH)
respectively. -
Note: This function is divided into ranged/non-ranged attack power and switches between each individual class. Individual formulas are relatively straight forward, but because of the combinatorics the function is relatively complex. Druids in particular have some nasty hardcodes here, so changing their formulas requires some reading.
-
Note: If you wish to apply generic transformations to the calculated attack power in this function, you can track the
val2
variable and write to it before the first call toSetModifierValue(...)
Armor -> Damage Reduction
-
Function:
Unit::CalcArmorReducedDamage
-
Expression: Look for the statement
float armor = float(victim->GetArmor());
and transform thevictim->GetArmor()
part -
Note: Armor is further modified inside the
if(attacker)
for things like armor penetration and ignore auras. -
Note: Post-modifier armor is below the
if (armor < 0.0f) armor = 0;
check, and is applied to damage through a final formula below it. Note that this function cannot normally return values below 1.
Block Value -> Blocked Damage
-
Function:
Unit::CalculateMeleeDamage
-
Expression: In the switch case for
MELEE_HIT_BLOCK
, after statementdamageInfo->blocked_amount = damageInfo->target->GetShieldBlockValue();
, the expressiondamageInfo->blocked_amount
contains the effective shield block value, which also factors in things like strength stat. -
Note: If you wish to only affect shield block value itself without the strength stat factored in, you need to dissect
Player::GetShieldBlockValue
because it is called by multiple other functions in the core, including one that writes to update data.
Agility -> Critical Hit
Despite its name, this function is used for both melee and ranged critical hit ratings
-
Function:
Player::GetMeleeCritFromAgility
-
Expression:
GetStat(STAT_AGILITY)
-
Further reading:
Player::UpdateAllCritPercentages
Stamina -> Max Health
-
Function:
Player::GetHealthBonusFromStamina
-
Expression:
GetStat(STAT_STAMINA)
-
Further reading:
Player::UpdateMaxHealth
for base data and other modifiers.
Intellect -> Max Mana
-
Function:
Player::GetManaBonusFromIntellect
-
Expression:
GetStat(STAT_INTELLECT)
-
Further reading:
Player::UpdateMaxPower
for base data and other modifiers.
Intellect -> Spell Crit
-
Function:
Player::GetSpellCritFromIntellect
-
Expression:
GetStat(STAT_INTELLECT)
-
Further Reading:
Player::UpdateSpellCritChance
Spirit -> Mana Regen
-
Function:
Player::OCTRegenMPPerSpirit
-
Expression:
GetStat(STAT_SPIRIT)
Intellect -> Mana Regen
-
Function:
Player::UpdateManaRegen
-
Expression:
GetStat(STAT_INTELLECT)
Cast State Mana Regen
This transformation is complex, and requires reading source code
-
Function:
Player::UpdateManaRegen
-
Expression: At the bottom of these functions, two update data fields are written:
UNIT_FIELD_POWER_REGEN_INTERRUPTED_FLAT_MODIFIER
- In-combat mana regen regenerationUNIT_FIELD_POWER_REGEN_FLAT_MODIFIER
- Out-of-combat mana regeneration
Either value can be transformed or swapped before writing to change their relationship.
Secondary Stats
Secondary stats, in this guide, refer to non-combat rating stats like attack power.
Attack Power
This transformation is complex, and requires reading source code
Attack power used for virtually all damage calculations is calculated in Unit::GetTotalAttackPowerValue
. Transforming any returned value here will transform effective attack power for virtually any calculation in the game. It is also possible to transform this value as used by individual calculations by simply searching the entire source for GetTotalAttackPowerValue
and transforming the function call expression.
- Side-Effects:
Unit::GetTotalAttackPowerValue
is also read inLFGMgr
power calculations, so transforming it might break LFG calculations if not compensated for.
Combat Ratings
As we’ve seen in the above sections, combat ratings are values that tends to calculate into a percentage value of some kind. These include stats such as hit rating, haste rating, critical strike rating and defense rating.
Combat ratings are defined in the CombatRating
struct in Unit.h
, and its members all contain the CR_
prefix. If you see an all-uppercase identifier starting with CR_
, it is typically a reference to this enum:
enum CombatRating
{
CR_WEAPON_SKILL = 0,
CR_DEFENSE_SKILL = 1,
CR_DODGE = 2,
CR_PARRY = 3,
CR_BLOCK = 4,
CR_HIT_MELEE = 5,
CR_HIT_RANGED = 6,
CR_HIT_SPELL = 7,
CR_CRIT_MELEE = 8,
CR_CRIT_RANGED = 9,
CR_CRIT_SPELL = 10,
CR_HIT_TAKEN_MELEE = 11,
CR_HIT_TAKEN_RANGED = 12,
CR_HIT_TAKEN_SPELL = 13,
CR_CRIT_TAKEN_MELEE = 14,
CR_CRIT_TAKEN_RANGED = 15,
CR_CRIT_TAKEN_SPELL = 16,
CR_HASTE_MELEE = 17,
CR_HASTE_RANGED = 18,
CR_HASTE_SPELL = 19,
CR_WEAPON_SKILL_MAINHAND = 20,
CR_WEAPON_SKILL_OFFHAND = 21,
CR_WEAPON_SKILL_RANGED = 22,
CR_EXPERTISE = 23,
CR_ARMOR_PENETRATION = 24
};
Combat ratings are among the more complex stats in the game, and transforming them requires understanding the different steps at which they are applied, where their bonuses come from and how the core itself transforms them. There is no simple way to transform “a players flat combat rating” into almost any bonus.
Generic Combat Rating Storage and Functions
This transformation is complex, and requires reading source code
Combat rating data is stored in a few distinct fields on players, and these change whenever players equip/unequip items or gains/loses auras that modify them:
-
Player::m_baseRatingValue
- contains a raw tally of all combat rating stats gained from items without any modifiers applied. Haste ratings read directly from this value when calculating their bonuses. -
Update data fields
PLAYER_FIELD_COMBAT_RATING_1
- contains combat stat points read fromPlayer::m_baseRatingValue
after spell modifiers have been applied. With the exception of haste ratings, this is what most combat rating calculations actually read.
There are also a few generic functions that keep showing up in combat rating calculations that are worth being aware of:
-
Player::GetRatingMultiplier
- This function readsGtCombatRatings.dbc
andGtOCTClassCombatRatingScalar.dbc
data into a single scalar modifier for the players level and stats. The input and out of this function can be transformed to manipulate the dbc stats without actually changing the underlying tables. -
Player::GetRatingBonusValue
- This function reads thePLAYER_FIELD_COMBAT_RATING_1
update data field and multiplies it byPlayer::GetRatingMultiplier
for a specific combat rating. This is typically the function used whenever combat rating bonuses are applied around the core, and can be modified to apply generic modifiers from displayed combat ratings to effective combat rating values used in calculations. Keep in mind that not all combat ratings use this field, such as haste ratings.For example, if you wanted to apply a generic +10 bonus to all defense rating calculations for hunters you could change the function into something like this:
float Player::GetRatingBonusValue(CombatRating cr) const { return float(GetUInt32Value(static_cast<uint16>(PLAYER_FIELD_COMBAT_RATING_1) + cr)) * GetRatingMultiplier(cr) + (cr == CR_DEFENSE_SKILL && getClass() == CLASS_HUNTER) ? 10 : 0; }
Player::ApplyRatingMod
- This function is called whenever a player changes their equipment so that a combat rating stat changes. The purpose of this function is to:- Set raw combat rating data into
Player::m_baseRatingValue
- Handle the three haste rating types raw from this value
- Call
Player::UpdateRating
- Set raw combat rating data into
Player::UpdateRating
- This is the main function calculating effective combat rating values. It can be used for applying base combat rating data (m_baseRatingValues
) into post-spell-modifier values (PLAYER_FIELD_COMBAT_RATING_1
) stored in update data, and as a generic entrypoint into more specific target update data fields, particularly percentage-based ones.
Defense Rating
This stat is complex, and requires reading source code to understand fully
Defense rating is a combat rating that commonly acts both as a distinct value and as a modifier to the players defense skill.
An important function to be aware of for defense rating calculations is Player::GetDefenseSkillValue
, which is used as a sort of generic modifier adding together the players defense skill and the combat rating bonus on top. It is called when calculating most stats that are influenced by defense rating, but not all of them.
Modifying defense rating itself can be most easily done inside Player::GetRatingBonusValue
with a specific check for defense rating, as all defense rating stats calls this function at some point, either directly or through Player::GetDefenseSkillValue
. Just like with other combat ratings that depend on dbc data, this can be used both to transform the input base combat rating bonus and the output bonus with dbc data applied.
Defense -> Crit Percentages
-
Functions:
Unit::GetUnitCriticalChance
,Unit::SpellTakenCritChance
(both contain the same formula) -
Expression: Look for a line that contains
victim->GetDefenseSkillValue
. This line contains the full formula for how defense skill and rating affects crit.
Defense -> Dodge/Parry Percentages
These two stats are calculated near identically, so we will cover them in a single section.
-
Functions:
Player::UpdateDodgePercentage
,Player::UpdateParryPercentage
for each stat respectiverly. -
Expression: Look for a call to
GetRatingBonusValue(CR_DEFENSE_SKILL)
- note how these are among the few combat rating stats that do not callGetDefenseSkillValue
but instead reads the rating directly.
Defense -> Block Percentage
-
Function:
Player::UpdateDodgePercentage
-
Expression:
Player::GetDefenseSkillValue()
Dodge Rating -> Dodge Percentage
-
Function:
Player::UpdateDodgePercentage
-
Expression:
GetRatingBonusValue(CR_DODGE)
Parry Rating -> Parry Percentage
-
Function:
Player::UpdateParryPercentage
-
Expression:
GetRatingBonusValue(CR_PARRY)
Block Rating -> Block Percentage
-
Function:
Player::UpdateBlockPercentage
-
Expression:
GetRatingBonusValue(CR_BLOCK)
Haste Ratings -> Attack / Cast Time Percentages
-
Function:
Player::ApplyRatingMod
-
Expression: Inside the check for haste ratings, transform
mult
before it is being read into any other variables (e.g.oldVal
/newVal
). -
Notes: This transformations has very strict rules and must not behave in such a way that can ever change for a single character (such as reading any other stats, randomness or character state). Things that can typically be used for this transformation include player class, race and the
CombatRating
enum itself.
Haste -> GCD Reduction
-
Function:
Spell::TriggerGlobalCooldown
-
Expression:
m_caster->GetFloatValue(UNIT_MOD_CAST_SPEED))
Haste -> Cast Time
-
Function:
Unit::ModSpellCastTime
-
Input: All instances of
GetFloatValue(UNIT_MOD_CAST_SPEED)
-
Further reading:
SpellInfo::CalcCastTime
Spell Crit
-
Expression:
Unit::SpellDoneCritChance
-
Input: All instances of
GetFloatValue(...)
with enum valuesPLAYER_CRIT_PERCENTAGE
,PLAYER_OFFHAND_CRIT_PERCENTAGE
,PLAYER_RANGED_CRIT_PERCENTAGE
.
Resilience
This transformation is complex, and requires reading source code
Resilience is a type of combat rating written into the following three ratings identically:
CR_CRIT_TAKEN_MELEE
CR_CRIT_TAKEN_RANGED
CR_CRIT_TAKEN_SPELL
Despite their names, these three rating values are used for all resilience calculations, including non-crit damage, and the names are thus misleading.
The most important function for reading resilience is Unit::ApplyResilience
, and serves as the entrypoint to more specific calculations.
The way these functions work is somewhat convoluted and there is no easy way to explain how to change individual stats with a single transformation, rather the whole subsystem must be understood to know how to modify the individual functions to do what you want. There are two functions that must be understood at the bottom:
-
Unit::GetCombatRatingReduction(CombatRating)
- Returns the bonus value for a specific combat rating for a player or their pet. -
Unit::GetCombatRatingDamageReduction(CombatRating,rate,cap,damage)
- Calculates a damage reduction for a specific combat rating (by callingUnit::GetCombatRatingReduction
) and multiplying it by arate
, capping it off and returning a value between 0 and 1.
Now that we know these two functions, we can follow the outline of Unit::ApplyResilience
to understand how different resilience reductions are calculated:
Unit::ApplyResilience
Unit::GetMeleeCritDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetRangedCritDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetSpellCritDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetMeleeDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetRangedDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetSpellDamageReduction
Unit::GetCombatRatingDamageReduction
Unit::GetMeleeCritChanceReduction
Unit::GetCombatRatingReduction
Unit::GetRangedCritChanceReduction
Unit::GetCombatRatingReduction
Unit::GetSpellCritChanceReduction
Unit::GetCombatRatingReduction
As you can see, because of the extensive use of these two generic functions they need to be extensively changed to allow transforming individual resilience values for individual damage types, since the type of reduction or critical hit status is not passed into them by default, and the resilience values themselves are so extensively transformed by them that their output have very little in common with it.
Expertise Rating
This transformation is complex, and requires reading source code
Expertise is stored as a normal combat rating value in update data, but also splits and writes into distinct update data fields PLAYER_EXPERTISE
and PLAYER_OFFHAND_EXPERTISE
in the function Player::UpdateExpertise
Expertise -> Dodge/Parry reduction
-
Function:
Player::GetExpertiseDodgeOrParryReduction
-
Expression: Calls to
GetUInt32Value
for each weapon attack type respectively. -
Note: To separate calculations for either dodge or parry, search the entire source code for calls to
GetExpertiseDodgeOrParryReduction
, it should give you 4 results where the expression is clearly read into either a dodge or parry variable. -
Further Reading:
Unit::RollMeleeOutcomeAgainst
,Unit::MeleeSpellHitResult
Armor Penetration -> Armor Ignored
-
Function:
Unit::CalcArmorReducedDamage
-
Expression:
attacker->ToPlayer()->GetRatingBonusValue(CR_ARMOR_PENETRATION)
-
Note: There is a cap for how much armor can be reduced calculated into the
maxArmorPen
variable right above the expression.
Pet Stats
Note: Emulators refer to player pets (both warlock-style and hunter pets) as “Guardians”, which should not be confused with the engineering pets commonly called guardians in the game.
Pet stat handling is very complex and involves both database tables, hardcoded C++ values and pet-specific aura spells.
Generally, these are divided into:
-
Base pet-specific stat data - Stored in the
pet_levelstats
database table -
Default pet stat data - Hardcoded into various places of
Pet.cpp
-
Pet-specific stat scalars - Hardcoded into various places of
Pet.cpp
-
Player to pet stat bonuses - Exclusively done using aura effects that are applied in various ways.
Pet Primary Stats
This transformation is complex, and requires reading source code
-
Base stat data per level and pet is stored in the
pet_levelstats
database table. -
If no primary stats are found
pet_levelstats
, default values are found inGuardian::InitStatsForLevel
(when not found in the above table), by searching forSetCreateStat
and looking for a chunk that looks similar to this:
SetCreateStat(STAT_STRENGTH, 22 + 2 * petlevel);
SetCreateStat(STAT_AGILITY, 22 + 1.5f * petlevel);
SetCreateStat(STAT_STAMINA, 25 + 2 * petlevel);
SetCreateStat(STAT_INTELLECT, 28 + 2 * petlevel);
SetCreateStat(STAT_SPIRIT, 27 + 1.5f * petlevel);
Pet Attack Damage
-
Function:
Guardian::InitStatsForLevel
-
Expression: Below the stat code there is a big switch statement on the petType like this:
switch (petType)
. In this statement individual “weapon” damage are set up and can be manipulated per pet type.
Pet Strength/Agility -> Attack Power
-
Function:
Guardian::UpdateAttackPowerAndDamage
-
Expression: calls to
GetStat
with enums likeSTAT_STRENGTH
andSTAT_AGILITY
throughout the function.
Pet Happiness -> Damage
-
Function:
Guardian::UpdateDamagePhysical
-
Expression: Near bottom of the function is a switch statement over the different happiness states.
Pet Agility -> Armor
-
Function:
Guardian::UpdateArmor
-
Expression:
GetStat(STAT_AGILITY)
Pet Stamina -> Max Health
-
Function:
Guardian::UpdateMaxHealth
-
Expression:
GetStat(STAT_STAMINA)
-
Note: In the middle of the function are various other pet-specific transformations applied.
Pet Intellect -> Max Mana
-
Function:
Guardian::UpdateMaxPower
- Where:
- Effective intellect:
GetStat(STAT_INTELLECT)
- Base intellect:
GetCreateStat(STAT_INTELLECT)
- Effective intellect:
- Note: Pet-specific transformations in the middle of the function
Player Stats -> Pet Stats
This transformation is very complex and requires reading source code in multiple files as well as DBC tools
Player -> Pet stats are generally handled by pet-specific aura effects for both primary and secondary stats. For this reason, this is probably one of the most complicated transformations that can be done, and knowledge of reading spell effect handlers is necessary.
Finding Pet Stat Scaling Auras
There is no documentation for exactly what spell auras are used for what pets, however there are three common methods to find them:
-
Many of these auras have the spell name
Tamed Pet Passive (DND)
, and often contain an icon indicating what pet they are for. - Many auras are applied through CreatureFamily entries. The connection works as follows:
- Entries in
creature_template
contains a field “family” that references an entry inCreatureFamily.dbc
- Entries in
CreatureFamily.dbc
contain two skilline fields,SkillLine_1
andSkillLine_2
- Entries in
SkillLineAbility.dbc
contains fields referencing a spell fromSpell.dbc
, as well as a SkillLine fromSkillLine.dbc
- An aura spell is automatically applied to a pet if it meets the following requirements:
- The spell is a passive aura
- The spell has no spell level set
- The spell has an ability with acquire method set to 2
- One of the two skilline fields in the pets creature family entry matches the above abilitys skilline.
- This happens in
LoadDBCStores
function inDBCStores.cpp
, specifically the part looping overCreatureFamilyEntry
values
- Entries in
- Other auras not covered by pet family entries are applied manually to different pet types inside
Guardian::InitStatsForLevel
inside the bigpetType
switch statement with calls toAddAura
Reading Hardcoded Scripts
Much of pet scaling uses the scaling auras mentioned above, but generally goes through custom scripts for classes like hunter or warlock. Manipulating these often come down to finding the matching function handling them inside the class script files and looking for the individual spell ids.
-
Figure out what spell you are modifying, either by inspecting the pets active auras or the methods mentioned above.
-
Find your class script file. These are located in
src\server\scripts\Spells/spell_yourclass.cpp
-
For most classes there is a dedicated function handling most scaling, usually found by searching for
scaling
. -
Read the code. Pet scaling is so hardcoded that there isn’t much else to do, usually with specific formulas for specific pets under specific classes.
Reading Spell Aura Effects
Most of the aura effects are properly implemented in the actual spell effects in dbc, but for some of them it is necessary to be able to read spell effect handlers to handle hardcodes. This section will briefly explain how to go from an aura effect in DBC to an effect handler in the core.
-
Find your aura effect. This can be done with a DBC editor like WDBX or the Spell Editor, I would recommend the latter as it includes spell aura names. For this example, let’s assume we find the spell aura
SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE
. - Find the aura effect handler. This is commonly done by searching for
SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE
insideSpellAuraEffects.cpp
. This will give you a line similar to&AuraEffect::HandleModTotalPercentStat, //137 SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE
, and the function pointer on this line is your aura effect handler.- Alternatively, if you only have the numeric aura id, you can usually search for
//aura-id
(//137
in this case) to find the same line.
- Alternatively, if you only have the numeric aura id, you can usually search for
- Read the effect handler. Either search for the effect handler function or shift-click it in Visual Studio to go to the implementation. The contents here are completely dependent on the aura effect and needs to be read in their entirety to be understood.
Threat
Threat is a relatively simple and generic calculation in the game. Whenever a unit deals damage or casts a spell they generate threat to the target or nearby enemies, and the player with the most threat (save for taunts) is the one that creatures attacks.
The interesting files and tables for threat are:
ThreatMgr.cpp
/ThreatMgr.h
Unit.cpp
/Unit.h
Spell.dbc
(for threat-related effects)spell_threat
world table
Spell Threat (database)
-
World Table:
spell_threat
-
Note: This table is perfect for arbitrarily modifying threat generated by spells on a per-spell basis
Threat -> Threat (core)
-
Function:
ThreatCalcHelper::calcThreat
-
Expression: the
threat
variable anywhere in this function. Early is before spell modifiers, late is after spell modifiers. -
Note: If you want to apply a transformation after school-specific modifiers, you can follow the tail call to
hatedUnit->ApplyTotalThreatModifier
and do your transform in that function as well. -
Further Reading:
Unit::AddThreat
,ThreatMgr::addThreat
Taunts
-
Function:
Unit::TauntApply
,Unit::TauntFadeOut
-
Note: These are the functions that ultimately handles taunts on AzerothCore. As of the commit this article is written for, you can also see that the ThreatMgrs taunt handling is commented out and completely unused.