Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions spec/System/TestPowerReport_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,196 @@ describe("PowerReportListControl", function()
assert.are.equal("allocated", list[1].name)
end)
end)

local function findPowerStat(stat)
for _, powerStat in ipairs(data.powerStatList) do
if powerStat.stat == stat then
return powerStat
end
end
end

describe("Power report calculation requirements", function()
before_each(function()
newBuild()
end)

it("marks only the metrics that require eHP or Full DPS", function()
local expectedEHPStats = {
TotalEHP = true,
SecondMinimalMaximumHitTaken = true,
PhysicalTakenHit = true,
LightningTakenHit = true,
ColdTakenHit = true,
FireTakenHit = true,
ChaosTakenHit = true,
}
local eHPStatCount = 0
local fullDPSStatCount = 0
for _, powerStat in ipairs(data.powerStatList) do
if powerStat.requiresEHP then
eHPStatCount = eHPStatCount + 1
assert.is_true(expectedEHPStats[powerStat.stat:gsub("^Minion", "")])
end
if powerStat.requiresFullDPS then
fullDPSStatCount = fullDPSStatCount + 1
assert.are.equal("FullDPS", powerStat.stat)
end
end

assert.are.equal(14, eHPStatCount)
assert.are.equal(1, fullDPSStatCount)
assert.is_true(findPowerStat("MinionTotalEHP").requiresEHP)
assert.is_nil(findPowerStat("MeleeAvoidChance").requiresEHP)
assert.is_nil(findPowerStat("SpellAvoidChance").requiresEHP)
assert.is_nil(findPowerStat("ProjectileAvoidChance").requiresEHP)
end)
end)

describe("Power report calculator options", function()
local calcs
local originalCalcFullDPS
local originalPerform

before_each(function()
newBuild()
calcs = build.calcsTab.calcs
originalCalcFullDPS = calcs.calcFullDPS
originalPerform = calcs.perform
end)

after_each(function()
calcs.calcFullDPS = originalCalcFullDPS
calcs.perform = originalPerform
end)

local function makeCalculator()
local state = {
fullDPSCalls = 0,
performSkipEHP = { },
}
calcs.calcFullDPS = function()
state.fullDPSCalls = state.fullDPSCalls + 1
return { skills = { { } }, combinedDPS = 1, TotalDotDPS = 0 }
end
calcs.perform = function(env, skipEHP)
table.insert(state.performSkipEHP, skipEHP == nil and "nil" or skipEHP)
return originalPerform(env, skipEHP)
end

local calcFunc = calcs.getMiscCalculator(build)
state.fullDPSCalls = 0
state.performSkipEHP = { }
return calcFunc, state
end

it("honors explicit stage skips for each report type", function()
build.viewMode = "TREE"
local calcFunc, state = makeCalculator()

calcFunc({ }, nil, { skipEHP = true, skipFullDPS = true })
assert.are.same({ true }, state.performSkipEHP)
assert.are.equal(0, state.fullDPSCalls)

state.performSkipEHP = { }
calcFunc({ }, true, { skipEHP = true, skipFullDPS = false })
assert.are.same({ true }, state.performSkipEHP)
assert.are.equal(1, state.fullDPSCalls)

state.performSkipEHP = { }
calcFunc({ }, false, { skipEHP = false, skipFullDPS = true })
assert.are.same({ false }, state.performSkipEHP)
assert.are.equal(1, state.fullDPSCalls)
end)

it("preserves legacy Tree calculations when options are omitted", function()
build.viewMode = "TREE"
local calcFunc, state = makeCalculator()

calcFunc({ }, false)
assert.are.same({ "nil" }, state.performSkipEHP)
assert.are.equal(1, state.fullDPSCalls)
end)

it("preserves representative report values", function()
build.viewMode = "TREE"
local calcFunc, calcBase = calcs.getMiscCalculator(build)
local testNode
for nodeId, node in pairs(build.spec.nodes) do
if not node.alloc and node.type ~= "Mastery" and node.modKey ~= "" and not build.calcsTab.mainEnv.grantedPassives[nodeId] then
testNode = node
break
end
end
assert(testNode)
local override = { addNodes = { [testNode] = true } }

for _, stat in ipairs({ "Life", "TotalDPS", "FullDPS", "TotalEHP" }) do
local powerStat = findPowerStat(stat)
local useFullDPS = powerStat.requiresFullDPS or false
local legacyOutput = calcFunc(override, useFullDPS)
local optimizedOutput = calcFunc(override, useFullDPS, {
skipEHP = not powerStat.requiresEHP,
skipFullDPS = not useFullDPS,
})
assert.are.near(
data.powerStatList.GetFromOutput(legacyOutput, powerStat),
data.powerStatList.GetFromOutput(optimizedOutput, powerStat),
10 ^ -9
)
end

local legacyOutput = calcFunc(override, false)
local optimizedOutput = calcFunc(override, false, { skipEHP = true, skipFullDPS = true })
local legacyOffence, legacyDefence = build.calcsTab:CalculateCombinedOffDefStat(legacyOutput, calcBase)
local optimizedOffence, optimizedDefence = build.calcsTab:CalculateCombinedOffDefStat(optimizedOutput, calcBase)
assert.are.near(legacyOffence, optimizedOffence, 10 ^ -9)
assert.are.near(legacyDefence, optimizedDefence, 10 ^ -9)
end)
end)

describe("PowerBuilder calculation options", function()
before_each(function()
newBuild()
end)

it("uses one selected-metric option set for all candidate calculations", function()
local output = {
Life = 101,
TotalEHP = 101,
FullDPS = 101,
CombinedDPS = 101,
LifeUnreserved = 101,
Armour = 101,
EnergyShield = 101,
Evasion = 101,
LifeRegenRecovery = 101,
EnergyShieldRegenRecovery = 101,
Minion = { TotalEHP = 101, CombinedDPS = 101 },
}
local baseOutput = output

local function runPowerBuilder(powerStat, expectedOptions)
local callCount = 0
build.calcsTab.powerStat = powerStat
build.calcsTab.powerMax = nil
build.calcsTab.nodePowerMaxDepth = 1
build.calcsTab.miscCalculator = { function(override, useFullDPS, options)
callCount = callCount + 1
assert.are.same(expectedOptions, options)
assert.are.equal(not expectedOptions.skipFullDPS, useFullDPS)
return output
end, baseOutput }

build.calcsTab:PowerBuilder()
assert.is_true(callCount > 0)
end

runPowerBuilder(findPowerStat("Life"), { skipEHP = true, skipFullDPS = true })
runPowerBuilder(findPowerStat("FullDPS"), { skipEHP = true, skipFullDPS = false })
runPowerBuilder(findPowerStat("TotalEHP"), { skipEHP = false, skipFullDPS = true })
runPowerBuilder(findPowerStat("MinionTotalEHP"), { skipEHP = false, skipFullDPS = true })
runPowerBuilder(findPowerStat("MeleeAvoidChance"), { skipEHP = true, skipFullDPS = true })
runPowerBuilder(data.powerStatList[1], { skipEHP = true, skipFullDPS = true })
end)
end)
21 changes: 14 additions & 7 deletions src/Classes/CalcsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,15 @@ end
-- Estimate the offensive and defensive power of all unallocated nodes
function CalcsTabClass:PowerBuilder()
-- local timer_start = GetTime()
local useFullDPS = self.powerStat and self.powerStat.stat == "FullDPS"
local useFullDPS = self.powerStat and self.powerStat.requiresFullDPS or false
local calcOptions = {
skipEHP = not (self.powerStat and self.powerStat.requiresEHP),
skipFullDPS = not useFullDPS,
}
local calcFunc, calcBase = self:GetMiscCalculator()
local function calcPower(override)
return calcFunc(override, useFullDPS, calcOptions)
end
local cache = { }
local distanceMap = { }
local distanceList = { }
Expand Down Expand Up @@ -541,7 +548,7 @@ function CalcsTabClass:PowerBuilder()
newPowerMax.singleStat = m_max(newPowerMax.singleStat, power.singleStat)
power.pathPower = power.singleStat
if distance > 1 then
power.pathPower = self:CalculatePowerStat(self.powerStat, calcFunc({ addNodes = buildPathNodes() }, useFullDPS), calcBase)
power.pathPower = self:CalculatePowerStat(self.powerStat, calcPower({ addNodes = buildPathNodes() }), calcBase)
end
end
elseif not self.powerStat or not self.powerStat.ignoreForNodes then
Expand Down Expand Up @@ -611,7 +618,7 @@ function CalcsTabClass:PowerBuilder()
for nodeId, node in pairs(nodes) do
if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] then
if not cache[node.modKey] then
cache[node.modKey] = calcFunc({ addNodes = { [node] = true } }, useFullDPS)
cache[node.modKey] = calcPower({ addNodes = { [node] = true } })
end
local output = cache[node.modKey]
calculateAddNodePower(node.power, distance, node, output, function()
Expand All @@ -623,7 +630,7 @@ function CalcsTabClass:PowerBuilder()
end)
elseif node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] then
if not cache[node.modKey.."_remove"] then
cache[node.modKey.."_remove"] = calcFunc({ removeNodes = { [node] = true } }, useFullDPS)
cache[node.modKey.."_remove"] = calcPower({ removeNodes = { [node] = true } })
end
local output = cache[node.modKey.."_remove"]
if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then
Expand All @@ -635,7 +642,7 @@ function CalcsTabClass:PowerBuilder()
pathNodes[node] = true
end
if #node.depends > 1 then
node.power.pathPower = self:CalculatePowerStat(self.powerStat, calcFunc({ removeNodes = pathNodes }, useFullDPS), calcBase)
node.power.pathPower = self:CalculatePowerStat(self.powerStat, calcPower({ removeNodes = pathNodes }), calcBase)
end
end
end
Expand Down Expand Up @@ -670,7 +677,7 @@ function CalcsTabClass:PowerBuilder()
local effectNode = buildMasteryEffectNode(node, effect)
if effectNode.modKey ~= "" then
if not cache[effectNode.modKey] then
cache[effectNode.modKey] = calcFunc({ addNodes = { [effectNode] = true } }, useFullDPS)
cache[effectNode.modKey] = calcPower({ addNodes = { [effectNode] = true } })
end
local output = cache[effectNode.modKey]
node.power.masteryEffects[effect.id] = { }
Expand Down Expand Up @@ -718,7 +725,7 @@ function CalcsTabClass:PowerBuilder()
wipeTable(node.power)
if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then
if not cache[node.modKey] then
cache[node.modKey] = calcFunc({ addNodes = { [node] = true } }, useFullDPS)
cache[node.modKey] = calcPower({ addNodes = { [node] = true } })
end
local output = cache[node.modKey]
if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then
Expand Down
13 changes: 9 additions & 4 deletions src/Modules/Calcs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,13 @@ end
---@field conditions string[]?
---@field extraJewelFuncs ModList?

---@class MiscCalculatorOptions
---@field skipEHP boolean? Skip effective hit pool and maximum hit estimations.
---@field skipFullDPS boolean? Skip the Full DPS roll-up.

-- Get calculator for other changes (adding/removing nodes, items, gems, etc)
---@param build Build
---@return fun(override?: CalcOverride, useFullDPS?: boolean): Output calcFunc
---@return fun(override?: CalcOverride, useFullDPS?: boolean, options?: MiscCalculatorOptions): Output calcFunc
---@return Output output
function calcs.getMiscCalculator(build)
-- Run base calculation pass
Expand All @@ -97,10 +101,11 @@ function calcs.getMiscCalculator(build)
env.player.output.FullDPS = fullDPS.combinedDPS
env.player.output.FullDotDPS = fullDPS.TotalDotDPS
end
return function(override, useFullDPS)
return function(override, useFullDPS, options)
local env, cachedPlayerDB, cachedEnemyDB, cachedMinionDB = calcs.initEnv(build, "CALCULATOR", override)
calcs.perform(env)
if (useFullDPS ~= false or build.viewMode == "TREE") and usedFullDPS then
calcs.perform(env, options and options.skipEHP)
local calculateFullDPS = not (options and options.skipFullDPS) and (useFullDPS ~= false or build.viewMode == "TREE")
if calculateFullDPS and usedFullDPS then
-- prevent upcoming calculation from using Cached Data and thus forcing it to re-calculate new FullDPS roll-up
-- without this, FullDPS increase/decrease when for node/item/gem comparison would be all 0 as it would be comparing
-- A with A (due to cache reuse) instead of A with B
Expand Down
18 changes: 10 additions & 8 deletions src/Modules/Data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@ end
---@field ignoreForItems? boolean
---@field reverseSort? boolean
---@field itemField string?
---@field requiresEHP? boolean Whether this stat requires effective health and maximum hit estimations
---@field requiresFullDPS? boolean Whether this stat requires the Full DPS roll-up

---@type PowerStat[]
data.powerStatList = {
{ stat=nil, label="Offence/Defence", combinedOffDef=true, ignoreForItems=true },
{ stat=nil, label="Name", itemField="Name", ignoreForNodes=true, reverseSort=true, transform=function(value) return value:gsub("^The ","") end},
{ stat="FullDPS", label="Full DPS" },
{ stat="FullDPS", label="Full DPS", requiresFullDPS=true },
{ stat="CombinedDPS", label="Combined DPS" },
{ stat="TotalDPS", label="Hit DPS" },
{ stat="WithImpaleDPS", label="Impale + Hit DPS" },
Expand Down Expand Up @@ -159,13 +161,13 @@ data.powerStatList = {
{ stat="MeleeAvoidChance", label="Melee avoid chance" },
{ stat="SpellAvoidChance", label="Spell avoid chance" },
{ stat="ProjectileAvoidChance", label="Projectile avoid chance" },
{ stat="TotalEHP", label="Effective Hit Pool" },
{ stat="SecondMinimalMaximumHitTaken", label="Eff. Maximum Hit Taken" },
{ stat="PhysicalTakenHit", label="Taken Phys dmg", transform=function(value) return -value end },
{ stat="LightningTakenHit", label="Taken Lightning dmg", transform=function(value) return -value end },
{ stat="ColdTakenHit", label="Taken Cold dmg", transform=function(value) return -value end },
{ stat="FireTakenHit", label="Taken Fire dmg", transform=function(value) return -value end },
{ stat="ChaosTakenHit", label="Taken Chaos dmg", transform=function(value) return -value end },
{ stat="TotalEHP", label="Effective Hit Pool", requiresEHP=true },
{ stat="SecondMinimalMaximumHitTaken", label="Eff. Maximum Hit Taken", requiresEHP=true },
{ stat="PhysicalTakenHit", label="Taken Phys dmg", transform=function(value) return -value end, requiresEHP=true },
{ stat="LightningTakenHit", label="Taken Lightning dmg", transform=function(value) return -value end, requiresEHP=true },
{ stat="ColdTakenHit", label="Taken Cold dmg", transform=function(value) return -value end, requiresEHP=true },
{ stat="FireTakenHit", label="Taken Fire dmg", transform=function(value) return -value end, requiresEHP=true },
{ stat="ChaosTakenHit", label="Taken Chaos dmg", transform=function(value) return -value end, requiresEHP=true },
{ stat="CritChance", label="Crit Chance" },
{ stat="CritMultiplier", label="Crit Multiplier" },
{ stat="BleedChance", label="Bleed Chance" },
Expand Down
Loading