User:Trang Oul/Utils

From Basin Wiki
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 - see calcX desc for description
  • prgcalcX - Assassin finishers; also Shock Web, Blade Fury
  • auralencalc - length of aura/curse/buff/debuff, in frames
  • aurarangecalc - range of aura/curse/buff/debuff; also used by other skills
  • aurastatcalcX - stats of aura/curse/buff/debuff - see aurastatX for stat name
  • passivecalcX - passive skills - see passivestatX for stat name
  • petmax - max number of pets
  • DmgSymPerCalc - synergy to physical damage
  • EDmgSymPerCalc - synergy to elemental damage
  • ELenSymPerCalc - synergy to the duration of elemental damage
  • cltcalcX - client-side calculations - not used for our purpose


Params

  • ParamX - numeric param used in formula - see ParamX Description for description
  • other columns (including, but not limited toELen..., 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 (column ParamX)
  • lvl - soft skill level (i.e. with bonuses)
  • blvl - base skill level (i.e. only hard points); used mostly for synergies
  • ulvl - 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:


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
Level Value
1 -15
2 -12
3 -9
Battle Cry auralencalc ln12 Param1=300, Param2=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
Level Value
1 12.0
2 14.4
3 16.8
Clay Golem aurastatcalc1 : item_slow dm34 Param3=0, Param4=75 165*lvl/(2*lvl + 12)
Level Value
1 11
2 20
3 27
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)
Level Value
1 4.8
2 6.92
3 8.6
Chain Lightning calc1 : # of target jump hits ln34/par5 Param3=26, Param4=1, Param5=5 lvl/5 + 5
Level Value
1 5
2 5
5 6
25 10
Strafe aurastatcalc1 : Minimum # of Missiles created min(par3 + lvl - 1, par4) Param3=0, Param4=75 Min(10, lvl + 3)
Level Value
1 4
2 5
7 10
Raise Skeleton petmax (lvl < 4) ?lvl:(2+lvl/3) (none) Piecewise((lvl, lvl < 4), (lvl/3 + 2, True))
Level Value
1 1
2 2
3 3
4 3
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))
Level Value
1 2
2 3
3 4
60 137
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)))
Level Value
1 20
2 25
3 30
10 59
40 95


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 True in the second param acts as "else" (i.e. the first value when the first condition is met, or the second otherwise - hence the second condition is always met).
table(sympy.Piecewise((lvl, lvl < 4), (2 + lvl / 3, True)), 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