Resolving spells and variables in spell texts
The DataDragon provided by Riot Games contains info on all 158 champions. But when looking at the spell texts of these champions, you often come across variables that are seemingly unresolvable (e.g., dealing <physicalDamage>{{ wdamage }} physical damage</physicalDamage>.
). Now, if we look at the rest of champion file, there is no other mention of wdamage
. This is why in this post we will be using data from the CommunityDragon and as example champion we will use Aatrox (patch 12.2).
Our main two data sources used in this post are:
- game/data/characters/aatrox/aatrox.bin.json
for Aatrox's champion data and - game/data/menu/fontconfig_en_us.txt.json
for the English spell texts.
Finding the correct spell objects
First, we have to find the correct CharacterRecord that contains the paths to the spell objects. A champion file typically contains multiple character records for different game modes, like URF. We will use the default character record for normal summoner's rift games, which can be found at JSON key Characters/<championName>/CharacterRecords/Root
in the champion file. This character root contains a field called: spellNames
which contains all the champion's spells. If you also want the passive spell, you can find it in the mCharacterPassiveSpell
field the only difference between the spell names in the passive spell field and the spell names is that the passive spell contains the full path (Characters/Aatrox/Spells/AatroxPassiveAbility/AatroxPassive
) and the spell names field contains the following paths: "spellNames": ["AatroxQAbility/AatroxQ", "AatroxWAbility/AatroxW", "AatroxEAbility/AatroxE", "AatroxRAbility/AatroxR"]
. These spell names can be formatted to a full path as follows: Characters/<championName>/Spells/<spellName>
. We need these full paths to be able to find the spell's data, as it's located at the JSON value corresponding to the key which is said path.
Spell Maths
To be able to understand the values and ratios spells are comprised of, we must first understand the mathematics. Although a spell object contains way more fields, we will only need the following fields in this post:
- mEffectAmount(optional): Contains static data that scales per level point put into the spell.
- mDataValues: Contains static data that scales per level point put into the spell, but these are mostly used by spell calculations, which we will discuss shortly.
- mSpellCalculations: Contains dynamic game calculations that use a number of variables to determine its value.
Spell Calculations
Before we can continue resolving spell texts, we first need to understand spell calculations. Spell calculations come in 3 different flavours, namely:
- GameCalculation: A normal game calculation
- GameCalculationModified: A GameCalculation but as the name suggests it is modified by a certain modifier or:
Modified(gameCalc, multiplier) = gameCalc() * mulitplier
- GameCalculationConditional: A GameCalculation but it contains 3 other game calculations, 1 for calculating which of the other two it should use as value.
In this post we will only discuss a normal game calculation as this is forms a good base. By the time you fully understand a game calculation, you will be able to figure out the other two.
A game calculation contains the following fields:
- mFormulaParts: The parts of type IGameCalculationPart that comprise the final equation
- mDisplayAsPercent: If this is set to true, the final value has to be displayed as
(v * 100)%
- mMultiplier(optional): A IGameCalculationPart. If this field is set, the final value is multiplied by the value from this calculation part.
Calculation Parts
There are many calculation parts, of which a large section is known. As we cannot discuss every part, we will discuss the most frequently occurring ones. The other ones can be found worked out here, every file contains a separate calculation part and a method to parse them from JSON files, format the part as string and get their value based on a context containing stats and other values needed to compute some values.
NumberCalculationPart
NumberCalculationPart is the simplest of all calculation parts, as it returns a static 64-bit floating point contained in the mNumber
field.
EffectValueCalculationPart
EffectValueCalculationPart returns a value from the mEffectAmount
table based on the mEffectIndex
field and the level of the spell (mEffectAmount[mEffectIndex-1][spellLevel]
).
NamedDataValueCalculationPart
NamedDataValueCalculationPart returns a value from the mDataValues
table based on the mDataValue
field and the level of the spell (mDataValues[mDataValue][spellLevel]
).
StatByNamedDataValueCalculationPart
StatByNamedDataValueCalculationPart is very similar to NamedDataValueCalculationPart also using a named data value, but it has 2 additional fields: mStat
and mStatFormula
. This part calculates its value according to a champion stat in game. A list of all the stats and their respective integer values can be found here. The mStatFormula field is used to determine whether to use the base stat of a champion, the total stat value, or the bonus amount(0 = Base, 1 = Bonus and 2 = Total).
Resolving Spell Texts
To be able to resolve spell texts, we first have to find the correct tooltip text belonging to the spell. The key for this tooltip text can be found at in the client data object of the spell: mClientData→mTooltipData→keyTooltip
. In our case for example Spell_AatroxW_Tooltip
because the font config uses only lower case keys we first have to convert our key to lower case, so we get spell_aatroxw_tooltip
and if we try this key in the font config we indeed find Aatrox fires a chain...
now sometimes a key does not exist in the font config for example Spell_VexQ_Tooltip
then we can try to find its corresponding hash instead. The hash algorithm used in the font config is XXH64 truncated to 40 bits(a python function for this can be found in the CDTB). So if we try that again with the lower case key, we get the hash 70ad6d3fc2
we can now format enclose this hash in brackets and look for the key {70ad6d3fc2}
in the font config we find Vex launches a wave of mist...
.
Lets take our example tooltip from Aatrox's W:Aatrox fires a chain, <status>Slowing</status> the first enemy hit by @WSlowPercentage*-100@% for @WSlowDuration@ seconds and dealing <physicalDamage>@WDamage@ physical damage</physicalDamage>. Champions and large jungle monsters have @WSlowDuration@ seconds to leave the impact area or be <status>Pulled</status> back to the center and damaged again for the same amount.
We see a couple things here namely the variables in between two @ characters. In this case these variables are either named data values or spell calculations. If we look at WSlowPercentage
we can find it in the data values table with the following values [-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25]
we can also see a *
followed by -100 meaning we have to multiple the value from WSlowPercentage
by -100 resulting in 25% accros all levels. As for WDamage
we can find this in the spell calculations with two parts a NamedDataValueCalculationPart with WBaseDamage
and a StatByNamedDataValueCalculationPart with WTotalADRatio
and mStat: 2
. The calculation will be as follows: 30/40/50/60/70 + 0.4 * AttackDamage
. The first values are the values from WBaseDamage
now if we look at that field we will see seven values instead of 5 but as a ability can only be used from level 1 and not level 0 we can ignore the first value then the last value can also be ignored because the spell has a max level of 5. There are also cases where a variable will be in a form conforming to @Effect<number>Amount@
(e.g., @Effect2Amount@
) this means we have to get the corresponding value from the mEffectAmount
table based on that number and the level of the spell (mEffectAmount[<number>-1][spellLevel]
).
Sometimes it can occur that we cannot cannot find a certain variable be it a game calculation or a datavalue. This issue is similar to what was described earlier with the config but for this we have to use the FNV-1A32 hashing algorithm (a python function for this can be found in the CDTB). When you get the lower case name of the variable and get its hash you can enclose it in brackets like previously mentioned and then try finding it in the data values or spell callculations.