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
631 changes: 631 additions & 0 deletions spec/System/TestPowerReport_spec.lua

Large diffs are not rendered by default.

100 changes: 77 additions & 23 deletions src/Classes/CalcsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -492,12 +492,50 @@ function CalcsTabClass:BuildPower()
end
end

local function nodePowerCacheKey(node, radiusNodes)
if radiusNodes[node.id] or node.conqueredBy or node.isTattoo or node.overrideType
or (node.type ~= "Normal" and node.type ~= "Notable" and node.type ~= "Mastery") then
return node
end
local key = { node.alloc and "remove" or "add", node.type, node.type == "Mastery" and node.name or "" }
for _, mod in ipairs(node.modList) do
-- Granted skills and other structured effects can depend on their source node.
if mod.type == "LIST" then
return node
end
-- modKey omits modifiers added later, such as cluster small-passive effect.
t_insert(key, "[" .. modLib.formatMod(mod) .. "]")
end
return table.concat(key, "|")
end

-- Estimate the offensive and defensive power of all unallocated nodes
function CalcsTabClass:PowerBuilder()
---@param disableRelevance boolean? Private validation opt-out; never persisted.
function CalcsTabClass:PowerBuilder(disableRelevance)
-- local timer_start = GetTime()
local useFullDPS = self.powerStat and self.powerStat.stat == "FullDPS"
local useFullDPS = self.powerStat and self.powerStat.requiresFullDPS or false
local useClusterPower = self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes
local calcOptions = {
skipEHP = not (self.powerStat and self.powerStat.requiresEHP),
skipFullDPS = not useFullDPS,
}
local calcFunc, calcBase = self:GetMiscCalculator()
local relevance
local function calcPower(override)
if relevance and relevance:CanSkip(override) then
return relevance.output
end
return calcFunc(override, useFullDPS, calcOptions)
end
local cache = { }
local radiusNodes = { }
for _, jewel in ipairs(self.mainEnv.radiusJewelList) do
for nodeId, node in pairs(jewel.nodes) do
if node.type ~= "Mastery" then
radiusNodes[nodeId] = true
end
end
end
local distanceMap = { }
local distanceList = { }
local masteryNodeList = { }
Expand All @@ -515,6 +553,18 @@ function CalcsTabClass:PowerBuilder()
coroutine.yield()
end

if not disableRelevance then
local observation = self.calcs.newQueryObserver()
calcOptions.queryObserver = observation
local ok, nullOutput = pcall(calcPower, { })
observation:Seal()
calcOptions.queryObserver = nil
if not ok then
error(nullOutput, 0)
end
relevance = self.calcs.newNodeRelevance(self.mainEnv, observation, nullOutput)
end

local function buildMasteryEffectNode(node, effect)
local effectNode = {
id = node.id,
Expand All @@ -541,7 +591,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 @@ -597,9 +647,11 @@ function CalcsTabClass:PowerBuilder()
distanceMap = nil
table.sort(distanceList, function(a, b) return a[1] < b[1] end)
-- Count eligible cluster nodes
for _, node in pairs(self.build.spec.tree.clusterNodeMap) do
if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then
total = total + 1
if useClusterPower then
for _, node in pairs(self.build.spec.tree.clusterNodeMap) do
if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then
total = total + 1
end
end
end

Expand All @@ -610,10 +662,11 @@ function CalcsTabClass:PowerBuilder()
end
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)
local key = nodePowerCacheKey(node, radiusNodes)
if not cache[key] then
cache[key] = calcPower({ addNodes = { [node] = true } })
end
local output = cache[node.modKey]
local output = cache[key]
calculateAddNodePower(node.power, distance, node, output, function()
local pathNodes = { }
for _, pathNode in pairs(node.path) do
Expand All @@ -622,10 +675,11 @@ function CalcsTabClass:PowerBuilder()
return pathNodes
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)
local key = nodePowerCacheKey(node, radiusNodes)
if not cache[key] then
cache[key] = calcPower({ removeNodes = { [node] = true } })
end
local output = cache[node.modKey.."_remove"]
local output = cache[key]
if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then
node.power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase)
if node.depends and not node.ascendancyName then
Expand All @@ -635,7 +689,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 @@ -669,10 +723,11 @@ function CalcsTabClass:PowerBuilder()
if effect then
local effectNode = buildMasteryEffectNode(node, effect)
if effectNode.modKey ~= "" then
if not cache[effectNode.modKey] then
cache[effectNode.modKey] = calcFunc({ addNodes = { [effectNode] = true } }, useFullDPS)
local key = nodePowerCacheKey(effectNode, radiusNodes)
if not cache[key] then
cache[key] = calcPower({ addNodes = { [effectNode] = true } })
end
local output = cache[effectNode.modKey]
local output = cache[key]
node.power.masteryEffects[effect.id] = { }
local effectPower = node.power.masteryEffects[effect.id]
calculateAddNodePower(effectPower, node.pathDist, node, output, function()
Expand Down Expand Up @@ -716,14 +771,13 @@ function CalcsTabClass:PowerBuilder()
node.power = {}
end
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)
end
local output = cache[node.modKey]
if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then
node.power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase)
if useClusterPower and not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then
local key = nodePowerCacheKey(node, radiusNodes)
if not cache[key] then
cache[key] = calcPower({ addNodes = { [node] = true } })
end
local output = cache[key]
node.power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase)
nodeIndex = nodeIndex + 1
if coroutine.running() and GetTime() - start > 100 then
if self.build.powerBuilderProgressCallback then
Expand Down
4 changes: 2 additions & 2 deletions src/Classes/ModDB.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ local mod_createMod = modLib.createMod
---@class ModDB: ModStore
local ModDBClass = newClass("ModDB", "ModStore")

function ModDBClass:ModDB(parent)
self:ModStore(parent)
function ModDBClass:ModDB(parent, queryObserver)
self:ModStore(parent, queryObserver)
self.mods = { }
return self
end
Expand Down
4 changes: 2 additions & 2 deletions src/Classes/ModList.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ local mod_createMod = modLib.createMod
---@class ModList: ModStore
local ModListClass = newClass("ModList", "ModStore")

function ModListClass:ModList(parent)
self:ModStore(parent)
function ModListClass:ModList(parent, queryObserver)
self:ModStore(parent, queryObserver)
return self
end

Expand Down
25 changes: 24 additions & 1 deletion src/Classes/ModStore.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ end })
---@class ModStore
local ModStoreClass = newClass("ModStore")

function ModStoreClass:ModStore(parent)
---@field queryObserver table? Calculation-local, sealed after observation.
function ModStoreClass:ModStore(parent, queryObserver)
self.queryObserver = queryObserver or (parent and parent.queryObserver) or false
self.parent = parent or false
self.actor = parent and parent.actor or { }
self.multipliers = { }
Expand Down Expand Up @@ -170,6 +172,9 @@ function ModStoreClass:Sum(modType, cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query(modType, flags, keywordFlags, source, ...)
end
return self:SumInternal(self, modType, cfg, flags, keywordFlags, source, ...)
end

Expand All @@ -184,6 +189,9 @@ function ModStoreClass:More(cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query("MORE", flags, keywordFlags, source, ...)
end
return self:MoreInternal(self, cfg, flags, keywordFlags, source, ...)
end

Expand All @@ -195,6 +203,9 @@ function ModStoreClass:Flag(cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query("FLAG", flags, keywordFlags, source, ...)
end
return self:FlagInternal(self, cfg, flags, keywordFlags, source, ...)
end

Expand All @@ -209,6 +220,9 @@ function ModStoreClass:Override(cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query("OVERRIDE", flags, keywordFlags, source, ...)
end
return self:OverrideInternal(self, cfg, flags, keywordFlags, source, ...)
end

Expand All @@ -223,6 +237,9 @@ function ModStoreClass:List(cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query("LIST", flags, keywordFlags, source, ...)
end
local result = { }
self:ListInternal(self, result, cfg, flags, keywordFlags, source, ...)
return result
Expand All @@ -240,6 +257,9 @@ function ModStoreClass:Tabulate(modType, cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query(modType, flags, keywordFlags, source, ...)
end
local result = { }
self:TabulateInternal(self, result, modType, cfg, flags, keywordFlags, source, ...)
return result
Expand Down Expand Up @@ -283,6 +303,9 @@ function ModStoreClass:HasMod(modType, cfg, ...)
keywordFlags = cfg.keywordFlags or 0
source = cfg.source
end
if self.queryObserver and self.queryObserver.active then
self.queryObserver:Query(modType, flags, keywordFlags, source, ...)
end
return self:HasModInternal(modType, flags, keywordFlags, source, ...)
end

Expand Down
6 changes: 3 additions & 3 deletions src/Modules/CalcActiveSkill.lua
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ function calcs.copyActiveSkill(env, mode, skill)
end

local newSkill = calcs.createActiveSkill(activeEffect, skill.supportList, skill.actor, skill.socketGroup, skill.summonSkill)
local newEnv, _, _, _ = calcs.initEnv(env.build, mode, env.override)
local newEnv, _, _, _ = calcs.initEnv(env.build, mode, env.override, nil, env.queryObserver)
calcs.buildActiveSkillModList(newEnv, newSkill)
newSkill.skillModList = new("ModList"):ModList(newSkill.baseSkillModList)
if newSkill.minion then
newSkill.minion.modDB = new("ModDB"):ModDB()
newSkill.minion.modDB = new("ModDB"):ModDB(nil, env.queryObserver)
newSkill.minion.modDB.actor = newSkill.minion
calcs.createMinionSkills(env, newSkill)
newSkill.skillPartName = newSkill.minion.mainSkill.activeEffect.grantedEffect.name
Expand Down Expand Up @@ -489,7 +489,7 @@ function calcs.buildActiveSkillModList(env, activeSkill)
end

-- Initialise skill modifier list
local skillModList = new("ModList"):ModList(activeSkill.actor.modDB)
local skillModList = new("ModList"):ModList(activeSkill.actor.modDB, env.queryObserver)
activeSkill.skillModList = skillModList
activeSkill.baseSkillModList = skillModList

Expand Down
3 changes: 3 additions & 0 deletions src/Modules/CalcMirages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ function calcs.mirages(env)
compareFunc = function(skill, env, config, mirageSkill)
if skill ~= env.player.mainSkill and skill.skillTypes[SkillType.Attack] and not skill.skillTypes[SkillType.Totem] and not skill.skillTypes[SkillType.SummonsTotem] and band(skill.skillCfg.flags, bor(ModFlag.Sword, ModFlag.Weapon1H)) == bor(ModFlag.Sword, ModFlag.Weapon1H) and not skill.skillCfg.skillCond["usedByMirage"] then
local uuid = cacheSkillUUID(skill, env)
if env.queryObserver and env.mode == "CALCULATOR" and GlobalCache.cachedData[env.mode][uuid] then
env.queryObserver.unsafe = "cached calculation"
end
if not GlobalCache.cachedData[env.mode][uuid] then
calcs.buildActiveSkill(env, env.mode, skill, uuid)
end
Expand Down
Loading
Loading