User:Trang Oul/Utils
Jump to navigation
Jump to search
Calculating skill params formulae and values per level
This tutorial's goal is to obtain two results:
- a formula for a given skill's property (damage, range, duration and so on)
- values for that property at different levels (1-60, as on skill pages).
1. Obtaining data
First we need to find the formulae. They are stored in data files, in skills.txt
and in skillcalc.txt
.
They are ordinary TSV files, and while there are specialized tools for modding Diablo 2, plain old MS Excel suffices, with features like freezing headers, filtering or searching.
This part is based on Phrozen Keep.
File: skills.txt
Columns
Formulae
Formulae are stored in the following columns (this list may not be exhaustive):
calcX
- general skill formulae - seecalcX desc
for descriptionprgcalcX
- Assassin finishers; also Shock Web, Blade Furyauralencalc
- length of aura/curse/buff/debuff, in framesaurarangecalc
- range of aura/curse/buff/debuff; also used by other skillsaurastatcalcX
- stats of aura/curse/buff/debuff - seeaurastatX
for stat namepassivecalcX
- passive skills - seepassivestatX
for stat namepetmax
- max number of petsDmgSymPerCalc
- synergy to physical damageEDmgSymPerCalc
- synergy to elemental damageELenSymPerCalc
- synergy to the duration of elemental damage- client-side calculations - not used for our purposecltcalcX
Params
ParamX
- numeric param used in formula - seeParamX Description
for description- other columns (including, but not limited to
ELen...
,EMin...
,EMax...
) - referenced by formulae below.
Formula details
- hardcoded numbers
- basic math operators and functions: arithmetic,
min
,max
- conditional operator:
condition ? value_if_true : value_if_false
- functions and params as defined in
skillcalc.txt
:
lnXY
,dmXY
- linear and diminishing (rational) functions. We will be the most interested in these formulae.
- All of them use skill level and additional params.
- Currently all those functions are the same per category.
parX
- parameter (columnParamX
)lvl
- soft skill level (i.e. with bonuses)blvl
- base skill level (i.e. only hard points); used mostly for synergiesulvl
- character level; used by Druid summons only- references to other column or columns - used mostly for progression based on level ranges
- Examples:
edns
- "Elemental Damage Min (256ths)"
- Used by Prayer to calculate life regen.
- As per Phrozen Keep:
- EMin: and EMax: Controls the basic minimum and maximum elemental damage dealt by this skill at sLvl 1.
- EMinLev1: to EMinLev5: and EMaxLevDam1: to EMaxLevDam5: Controls the additional elemental damage added to the skills minimum and maximum damage for every additional sLvl.
- LevDam1 is used for sLvls 2-8, LevDam2 for 9-16, LevDam3 for 17-22, LevDam4 for 23-28 and LevDam5 for sLvls above 28.
- So, Prayer starts with 2 (col
EMin
) life points at level 1, increasing by 1 per level at levels 2-8 (EMinLev1
), again by 1 at levels 9-16 (EMinLev2
) and so on, ending by +3 per each level above 28.
edmn
- "Elemental Damage"
- Used for Energy Shield's' Damage % Absorption (
min(edmn,95)
) - It starts at 20% (
EMin
), increased by 5 percentage points per skill (EMinLev1
), then by 2 p.p. (EMinLev2
), then by 1 p.p. (EMinLev3
,EMinLev4
,EMinLev5
), capped at 95%.
- Notice that although these functions refer to "Elemental Damage" columns, for those skills these columns just hold the values, used for other properties than damage.
edmx
- "Elemental Damage Max"
- Used by Dire Wolf's max cold damage (in D2R); cols
EMax...
.
skill('Skill Name'.fun)
- reference to another skill - used for synergies:- Examples:
skill('Golem Mastery'.ln12)
-ln12
formula calculated using Golem Mastery params.skill('Summon Fenris'.par2)
-Param2
of Summon Fenris (i.e. Summon Dire Wolf).skill('Summon Fenris'.lvl)
- (soft) skill level of Summon Fenris.
stat('stat_name'.fun)
- used mostly to make pets inherit character stats- Examples:
stat('item_pierce_ELE_immunity'.accr)
- Sunderstat('passive_ELE_pierce'.accr)
- Enemy Resistancestat('passive_ELE_mastery'.accr)
- Skill Damage
2. Calculation
Examples:
Skill Name | Column description if present |
Formula | Params (besides lvl ) |
Simplified Formula | Values (sample) | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Jab | calc1 : Damage % |
ln34 |
Param3=-15, Param4=3 | 3*lvl - 18 |
| ||||||||||||
Battle Cry | auralencalc |
dm34 |
Param3=0, Param4=75 Since this is duration, we convert it to seconds by dividing by 25 and allowing fractional values. |
12*lvl/5 + 48/5 or 2.4*lvl + 9.6 |
| ||||||||||||
Clay Golem | aurastatcalc1 : item_slow |
ln12 |
Param1=300, Param2=60 | 165*lvl/(2*lvl + 12) |
| ||||||||||||
Blaze | auralencalc |
dm12 |
Param3=50, Param4=500 Since this is duration, we convert it to seconds by dividing by 25 and allowing fractional values. |
(109*lvl + 60)/(5*lvl + 30) |
| ||||||||||||
Chain Lightning | calc1 : # of target jump hits |
ln34/par5 |
Param3=26, Param4=1, Param5=5 | lvl/5 + 5 |
| ||||||||||||
Strafe | aurastatcalc1 : Minimum # of Missiles created |
min(par3 + lvl - 1, par4) |
Param3=0, Param4=75 | Min(10, lvl + 3) |
| ||||||||||||
Raise Skeleton | petmax |
(lvl < 4) ?lvl:(2+lvl/3) |
(none) | Piecewise((lvl, lvl < 4), (lvl/3 + 2, True)) |
| ||||||||||||
Prayer | aurastatcalc1 : hitpoints |
edns |
EMin...: 2, 1, 1, 2, 2, 3 | Piecewise((lvl + 1, (lvl <= 8) | (lvl <= 16)), (2*lvl - 15, (lvl <= 22) | (lvl <= 28)), (3*lvl - 43, True)) |
| ||||||||||||
Energy Shield | aurastatcalc1 : Damage % Absorption |
min(edmn,95) |
EMin...: 20, 5, 2, 1, 1, 1 | Min(95, Piecewise((5*lvl + 15, lvl <= 8), (2*lvl + 39, lvl <= 16), (lvl + 55, True))) |
|
The following Python script can be used to simplify the formula and calculate values per level.
One can paste it into Python's interactive console and then use the utils while editing our Wiki.
import sympy FRAMES_PER_SECOND:int = 25 lvl, par1, par2, par3, par4, par5, par6, par7, par8 = sympy.symbols('lvl, par1, par2, par3, par4, par5, par6, par7, par8', integer=True) # the formulae are the same per category, but for ease of use they are defined with names from skillcalc.txt ln12 = par1 + (lvl - 1) * par2 ln34 = par3 + (lvl - 1) * par4 ln56 = par5 + (lvl - 1) * par6 ln78 = par7 + (lvl - 1) * par8 dm12 = ((110 * lvl) * (par2 - par1)) / (100 * (lvl + 6)) + par1 dm34 = ((110 * lvl) * (par4 - par3)) / (100 * (lvl + 6)) + par3 dm56 = ((110 * lvl) * (par6 - par5)) / (100 * (lvl + 6)) + par5 dm78 = ((110 * lvl) * (par8 - par7)) / (100 * (lvl + 6)) + par7 FORMULA_PARTS = { 'lvl' : lvl, 'par1' : par1, 'par2' : par2, 'par3' : par3, 'par4' : par4, 'par5' : par5, 'par6' : par6, 'par7' : par7, 'par8' : par8, 'ln12' : ln12, 'ln34' : ln34, 'ln56' : ln56, 'ln78' : ln78, 'dm12' : dm12, 'dm34' : dm34, 'dm56' : dm56, 'dm78' : dm78, } def print_expr(expr, seconds:bool=False) -> None: """ Prints the simplified formula. For time prints two formulae, with integers and fractions - use the "better" looking one. TODO: for time automatically print integer formula if possible (109 instead of 109.0) of float otherwise. """ if not seconds: expr.expand(frac=True) print(expr) else: expr /= FRAMES_PER_SECOND expr = expr.expand(frac=True) print(expr) print(expr.evalf()) def table(expr, params:dict|None=None, maxlvl:int=60, seconds:bool=False) -> None: """ Prints the simplified formula and the table with values for each level. seconds - convert result from frames (default unit) to seconds. To be used for time values. """ if params is None: params = {} if isinstance(expr, str): expr = sympy.sympify(expr, locals=FORMULA_PARTS) expr = expr.subs(params) expr = expr.simplify() print_expr(expr, seconds) for slvl in range(1, maxlvl+1): value = expr.subs({lvl : slvl}) value = int(value) #D2 USES INTEGERS if seconds: value /= FRAMES_PER_SECOND print(slvl, value, sep='\t') #TODO: build wiki table instead of simply printing the values def levelwise(base:int, lvls1:int, lvls2:int, lvls3:int, lvls4:int, lvls5:int): """ Builds a piecewise formula based on values per each level range. Level ranges are hardcoded: 1, 2-8, 9-16, 17-22, 22-28, >28. https://d2mods.info/forum/kb/viewarticle?a=440 """ return sympy.Piecewise( (base, lvl == 1), (base + lvls1 * (lvl-1), lvl <= 8), (base + lvls1 * 7 + lvls2 * (lvl-8), lvl <= 16), (base + lvls1 * 7 + lvls2 * 8 + lvls3 * (lvl-16), lvl <= 22), (base + lvls1 * 7 + lvls2 * 8 + lvls3 * 6 + lvls4 * (lvl-22), lvl <= 28), (base + lvls1 * 7 + lvls2 * 8 + lvls3 * 6 + lvls4 * 6 + lvls5 * (lvl-28), True), ) # sample usage table(ln34, {par3 : -15, par4 : 3}, maxlvl=6) #Jab damage table(ln12, {par1 : 300, par2 : 60}, maxlvl=6, seconds=True) #Battle Cry duration table(dm34, {par3 : 0, par4 : 75}, maxlvl=6) #Clay Golem slow table(dm12, {par1 : 50, par2 : 500}, maxlvl=60, seconds=True) #Blaze duration table(ln34/par5, {par3 : 26, par4 : 1, par5:5}, maxlvl=6) #Chain Lightining hits # For min and max use sympy functions instead of Python's # or use expression from data file directly as string (does not work with all formulae, such as with conditionals - see below). table(sympy.Min(par3 + lvl - 1, par4), {par3 : 4, par4 : 10}, maxlvl=6) #Strafe min hits table( 'min(par3 + lvl - 1, par4)', {par3 : 4, par4 : 10}, maxlvl=6) #Strafe min hits # For "condition ? value_if_true : value_if_false" use Piecewise((value_if_true, condition), (value_if_false, True)). # The second True acts as "else". table(sympy.Piecewise((lvl, lvl < 4), (2 + lvl / 3, lvl >= 4)), maxlvl=6) #Max Skeletons # For progression based on level ranges, use the "levelwise" util to make a piecewise function. # Provide base and values per level groups, padding with zeroes if needed (for ELen... - there are no ELenLev4 and ELenLev5 cols). table (levelwise(2, 1, 1, 2, 2, 3), maxlvl=6) #Prayer healing table(sympy.Min(levelwise(20, 5, 2, 1, 1, 1), 95), maxlvl=6) #Energy shield absorption