forked from PathOfBuildingCommunity/PathOfBuilding
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCalcTools.lua
274 lines (258 loc) · 9.85 KB
/
CalcTools.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
-- Path of Building
--
-- Module: Calc Tools
-- Various functions used by the calculation modules
--
local pairs = pairs
local t_insert = table.insert
local t_remove = table.remove
local m_floor = math.floor
local m_min = math.min
local m_max = math.max
calcLib = { }
-- Calculate and combine INC/MORE modifiers for the given modifier names
function calcLib.mod(modStore, cfg, ...)
return (1 + (modStore:Sum("INC", cfg, ...)) / 100) * modStore:More(cfg, ...)
end
---Calculates additive and multiplicative modifiers for specified modifier names
---@param modStore table
---@param cfg table
---@param ... string @Mod name(s)
---@return number, number @increased, more
function calcLib.mods(modStore, cfg, ...)
local inc = 1 + modStore:Sum("INC", cfg, ...) / 100
local more = modStore:More(cfg, ...)
return inc, more
end
-- Calculate value
function calcLib.val(modStore, name, cfg)
local baseVal = modStore:Sum("BASE", cfg, name)
if baseVal ~= 0 then
return baseVal * calcLib.mod(modStore, cfg, name)
else
return 0
end
end
-- Validate the level of the given gem
function calcLib.validateGemLevel(gemInstance)
local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect
if not grantedEffect.levels[gemInstance.level] then
-- Try limiting to the level range of the skill
gemInstance.level = m_max(1, gemInstance.level)
if #grantedEffect.levels > 0 then
gemInstance.level = m_min(#grantedEffect.levels, gemInstance.level)
end
end
if not grantedEffect.levels[gemInstance.level] and gemInstance.gemData and gemInstance.gemData.naturalMaxLevel then
gemInstance.level = gemInstance.gemData.naturalMaxLevel
end
if not grantedEffect.levels[gemInstance.level] then
-- That failed, so just grab any level
gemInstance.level = next(grantedEffect.levels)
end
end
-- Evaluate a skill type postfix expression
function calcLib.doesTypeExpressionMatch(checkTypes, skillTypes, minionTypes)
local stack = { }
for _, skillType in pairs(checkTypes) do
if skillType == SkillType.OR then
local other = t_remove(stack)
stack[#stack] = stack[#stack] or other
elseif skillType == SkillType.AND then
local other = t_remove(stack)
stack[#stack] = stack[#stack] and other
elseif skillType == SkillType.NOT then
stack[#stack] = not stack[#stack]
else
t_insert(stack, skillTypes[skillType] or (minionTypes and minionTypes[skillType]) or false)
end
end
for _, val in ipairs(stack) do
if val then
return true
end
end
return false
end
-- Check if given support skill can support the given active skill
function calcLib.canGrantedEffectSupportActiveSkill(grantedEffect, activeSkill)
if grantedEffect.unsupported or activeSkill.activeEffect.grantedEffect.cannotBeSupported then
return false
end
if grantedEffect.supportGemsOnly and not activeSkill.activeEffect.gemData then
return false
end
-- Special case for things like Forbidden Shako or Hungry Loop with for example Prismatic Burst and another compatible support
if grantedEffect.fromItem and grantedEffect.support and (activeSkill.activeEffect.grantedEffect.fromItem or activeSkill.activeEffect.grantedEffect.modSource:sub(1, #"Item") == "Item" or (activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.fromItem)) then
return false
end
-- if the activeSkill is a Minion's skill like "Default Attack", use minion's skillTypes instead for exclusions
-- otherwise compare support to activeSkill directly
if grantedEffect.excludeSkillTypes[1] and calcLib.doesTypeExpressionMatch(grantedEffect.excludeSkillTypes, (activeSkill.summonSkill and activeSkill.summonSkill.skillTypes) or activeSkill.skillTypes) then
return false
end
if grantedEffect.isTrigger and activeSkill.actor.enemy.player ~= activeSkill.actor then
return false
end
return not grantedEffect.requireSkillTypes[1] or calcLib.doesTypeExpressionMatch(grantedEffect.requireSkillTypes, activeSkill.skillTypes, not grantedEffect.ignoreMinionTypes and activeSkill.minionSkillTypes)
end
-- Check if given gem is of the given type ("all", "strength", "melee", etc)
function calcLib.gemIsType(gem, type, includeTransfigured)
return (type == "all" or
(type == "elemental" and (gem.tags.fire or gem.tags.cold or gem.tags.lightning)) or
(type == "aoe" and gem.tags.area) or
(type == "trap or mine" and (gem.tags.trap or gem.tags.mine)) or
((type == "active skill" or type == "grants_active_skill" or type == "skill") and gem.tags.grants_active_skill and not gem.tags.support) or
(type == "non-vaal" and not gem.tags.vaal) or
(type == gem.name:lower()) or
(type == gem.name:lower():gsub("^vaal ", "")) or
(includeTransfigured and calcLib.isGemIdSame(gem.name, type, true)) or
((type ~= "active skill" and type ~= "grants_active_skill" and type ~= "skill") and gem.tags[type]))
end
-- From PyPoE's formula.py
function calcLib.getGemStatRequirement(level, isSupport, multi)
if multi == 0 then
return 0
end
local a, b
if isSupport then
b = 6 * multi / 100
if multi == 100 then
a = 1.495
elseif multi == 60 then
a = 0.945
elseif multi == 40 then
a = 0.6575
else
return 0
end
else
b = 8 * multi / 100
if multi == 100 then
a = 2.1
b = 7.75
elseif multi == 75 then
a = 1.619
elseif multi == 60 then
a = 1.325
elseif multi == 40 then
a = 0.924
else
return 0
end
end
local req = round(level * a + b)
return req < 14 and 0 or req
end
-- Build table of stats for the given skill instance
function calcLib.buildSkillInstanceStats(skillInstance, grantedEffect)
local stats = { }
if skillInstance.quality > 0 and grantedEffect.qualityStats then
local qualityId = skillInstance.qualityId or "Default"
local qualityStats = grantedEffect.qualityStats[qualityId]
if not qualityStats then
qualityStats = grantedEffect.qualityStats
end
for _, stat in ipairs(qualityStats) do
stats[stat[1]] = (stats[stat[1]] or 0) + math.modf(stat[2] * skillInstance.quality)
end
end
local level = grantedEffect.levels[skillInstance.level] or { }
local availableEffectiveness
local actorLevel = skillInstance.actorLevel or level.levelRequirement or 1
for index, stat in ipairs(grantedEffect.stats) do
-- Static value used as default (assumes statInterpolation == 1)
local statValue = level[index] or 1
if level.statInterpolation then
if level.statInterpolation[index] == 3 then
-- Effectiveness interpolation
if not availableEffectiveness then
availableEffectiveness =
(data.gameConstants["SkillDamageBaseEffectiveness"] + data.gameConstants["SkillDamageIncrementalEffectiveness"] * (actorLevel - 1)) * (grantedEffect.baseEffectiveness or 1)
* (1 + (grantedEffect.incrementalEffectiveness or 0)) ^ (actorLevel - 1)
end
statValue = round(availableEffectiveness * level[index])
elseif level.statInterpolation[index] == 2 then
-- Linear interpolation; I'm actually just guessing how this works
-- Order the levels, since sometimes they skip around
local orderedLevels = { }
local currentLevelIndex
for level, _ in pairs(grantedEffect.levels) do
t_insert(orderedLevels, level)
end
table.sort(orderedLevels)
for idx, level in ipairs(orderedLevels) do
if skillInstance.level == level then
currentLevelIndex = idx
end
end
if #orderedLevels > 1 then
local nextLevelIndex = m_min(currentLevelIndex + 1, #orderedLevels)
local nextReq = grantedEffect.levels[orderedLevels[nextLevelIndex]].levelRequirement
local prevReq = grantedEffect.levels[orderedLevels[nextLevelIndex - 1]].levelRequirement
local nextStat = grantedEffect.levels[orderedLevels[nextLevelIndex]][index]
local prevStat = grantedEffect.levels[orderedLevels[nextLevelIndex - 1]][index]
statValue = round(prevStat + (nextStat - prevStat) * (actorLevel - prevReq) / (nextReq - prevReq))
else
statValue = round(grantedEffect.levels[orderedLevels[currentLevelIndex]][index])
end
end
end
stats[stat] = (stats[stat] or 0) + statValue
end
if grantedEffect.constantStats then
for _, stat in ipairs(grantedEffect.constantStats) do
stats[stat[1]] = (stats[stat[1]] or 0) + (stat[2] or 0)
end
end
return stats
end
--- Correct the tags on conversion with multipliers so they carry over correctly
--- @param mod table
--- @param multiplier number
--- @param minionMods bool @convert ActorConditions pointing at parent to normal Conditions
--- @return table @converted multipliers
function calcLib.getConvertedModTags(mod, multiplier, minionMods)
local modifiers = { }
for k, value in ipairs(mod) do
if minionMods and value.type == "ActorCondition" and value.actor == "parent" then
modifiers[k] = { type = "Condition", var = value.var }
elseif value.limitTotal then
-- LimitTotal can apply to 'per stat' or 'multiplier', so just copy the whole and update the limit
local copy = copyTable(value)
copy.limit = copy.limit * multiplier
modifiers[k] = copy
else
modifiers[k] = copyTable(value)
end
end
return modifiers
end
--- Get the gameId from the gemName which will be the same as the base gem for transfigured gems
--- @param gemName string
--- @param dropVaal boolean
--- @return string
function calcLib.getGameIdFromGemName(gemName, dropVaal)
if type(gemName) ~= "string" then
return
end
local gemId = data.gemForBaseName[gemName:lower()]
if not gemId then return end
local gameId
if dropVaal and data.gems[gemId].vaalGem then
gameId = data.gems[data.gemVaalGemIdForBaseGemId[gemId]].gameId
else
gameId = data.gems[gemId].gameId
end
return gameId
end
--- Use getGameIdFromGemName to get gameId from the gemName and passed in type. Return true if they're the same and not nil
--- @param gemName string
--- @param type string
--- @param dropVaal boolean
--- @return boolean
function calcLib.isGemIdSame(gemName, typeName, dropVaal)
local gemNameId = calcLib.getGameIdFromGemName(gemName, dropVaal)
local typeId = calcLib.getGameIdFromGemName(typeName, dropVaal)
return gemNameId and typeId and gemNameId == typeId
end