From b6b954dbfd7519f050ecf213804436ffd26a2759 Mon Sep 17 00:00:00 2001 From: Ken Snyder <5116385+ksnyder9801@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:32:22 -0700 Subject: [PATCH] add masteries to power report --- spec/System/TestTreeTab_spec.lua | 57 +++++++++++++ src/Classes/CalcsTab.lua | 110 ++++++++++++++++++++++++- src/Classes/PowerReportListControl.lua | 9 ++ src/Classes/TreeTab.lua | 47 +++++++++++ 4 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 spec/System/TestTreeTab_spec.lua diff --git a/spec/System/TestTreeTab_spec.lua b/spec/System/TestTreeTab_spec.lua new file mode 100644 index 0000000000..5312e603eb --- /dev/null +++ b/spec/System/TestTreeTab_spec.lua @@ -0,0 +1,57 @@ +describe("TreeTab", function() + before_each(function() + newBuild() + end) + + teardown(function() + -- newBuild() resets the shared build state for the next test. + end) + + it("adds separate power report entries for mastery effects", function() + local treeTab = build.treeTab + local parentNode = { id = 2 } + local masteryNode = { + id = 1, + type = "Mastery", + dn = "Two Hand Mastery", + power = { + masteryEffects = { + [101] = { singleStat = 10, pathPower = 10 }, + [102] = { singleStat = 20, pathPower = 20 }, + }, + }, + masteryEffects = { + { effect = 101 }, + { effect = 102 }, + }, + path = { parentNode, false }, + x = 10, + y = 20, + } + masteryNode.path[2] = masteryNode + + treeTab.build.displayStats = { + { stat = "Damage", label = "Damage", fmt = ".1f" }, + } + treeTab.build.spec.nodes = { + [masteryNode.id] = masteryNode, + } + treeTab.build.spec.masterySelections = { } + treeTab.build.spec.tree.clusterNodeMap = { } + treeTab.build.spec.tree.masteryEffects = { + [101] = { id = 101, sd = { "Gain 10 Damage" }, stats = { "Gain 10 Damage" } }, + [102] = { id = 102, sd = { "Gain 20 Damage" }, stats = { "Gain 20 Damage" } }, + } + treeTab.build.calcsTab.mainEnv = { grantedPassives = { } } + + local report = treeTab:BuildPowerReportList({ stat = "Damage", label = "Damage" }) + + assert.are.same(2, #report) + assert.are.same("Mastery", report[1].type) + assert.are.same("Two Hand Mastery: Gain 20 Damage", report[1].name) + assert.are.same(20, report[1].power) + assert.are.same(2, report[1].pathDist) + assert.are.same(10, report[2].power) + assert.are.same("Two Hand Mastery: Gain 10 Damage", report[2].name) + end) +end) diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index fe751b4674..9e06e2e368 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -492,6 +492,20 @@ function CalcsTabClass:PowerBuilder() if coroutine.running() then coroutine.yield() end + + local function buildMasteryEffectNode(node, effect) + local effectNode = { + id = node.id, + type = node.type, + name = node.name, + sd = { }, + } + for i, sd in ipairs(effect.sd or { }) do + effectNode.sd[i] = sd + end + self.build.spec.tree:ProcessStats(effectNode) + return effectNode + end local start = GetTime() local nodeIndex = 0 @@ -499,11 +513,25 @@ function CalcsTabClass:PowerBuilder() for nodeId, node in pairs(self.build.spec.nodes) do wipeTable(node.power) + if node.type == "Mastery" then + node.power.masteryEffects = { } + end if node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] then - distanceMap[node.pathDist or 1000] = distanceMap[node.pathDist or 1000] or { } - distanceMap[node.pathDist or 1000][nodeId] = node - if not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < node.pathDist) then - total = total + 1 + if node.type == "Mastery" and node.allMasteryOptions then + if not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < node.pathDist) then + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + local assignedNodeId = isValueInTable(self.build.spec.masterySelections, masteryEffect.effect) + if not assignedNodeId or assignedNodeId == node.id then + total = total + 1 + end + end + end + else + distanceMap[node.pathDist or 1000] = distanceMap[node.pathDist or 1000] or { } + distanceMap[node.pathDist or 1000][nodeId] = node + if not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < node.pathDist) then + total = total + 1 + end end end end @@ -572,6 +600,17 @@ function CalcsTabClass:PowerBuilder() end end end + if node.type == "Mastery" then + local selectedEffectId = self.build.spec.masterySelections[node.id] + if selectedEffectId then + node.power.masteryEffects[selectedEffectId] = { + singleStat = node.power.singleStat, + pathPower = node.power.pathPower, + offence = node.power.offence, + defence = node.power.defence, + } + end + end nodeIndex = nodeIndex + 1 if coroutine.running() and GetTime() - start > 100 then if self.build.powerBuilderProgressCallback then @@ -583,6 +622,69 @@ function CalcsTabClass:PowerBuilder() end end + for nodeId, node in pairs(self.build.spec.nodes) do + if node.type == "Mastery" and node.allMasteryOptions and node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] then + if not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < node.pathDist) then + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + local assignedNodeId = isValueInTable(self.build.spec.masterySelections, masteryEffect.effect) + if not assignedNodeId or assignedNodeId == node.id then + local effect = self.build.spec.tree.masteryEffects[masteryEffect.effect] + 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) + end + local output = cache[effectNode.modKey] + node.power.masteryEffects[effect.id] = { } + if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then + node.power.masteryEffects[effect.id].singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase) + node.power.masteryEffects[effect.id].pathPower = node.power.masteryEffects[effect.id].singleStat + if node.path and not node.ascendancyName then + newPowerMax.singleStat = m_max(newPowerMax.singleStat, node.power.masteryEffects[effect.id].singleStat) + local pathNodes = { + [effectNode] = true + } + for _, pathNode in pairs(node.path) do + if pathNode ~= node then + pathNodes[pathNode] = true + end + end + if node.pathDist > 1 then + node.power.masteryEffects[effect.id].pathPower = self:CalculatePowerStat(self.powerStat, calcFunc({ addNodes = pathNodes }, useFullDPS), calcBase) + end + end + node.power.singleStat = m_max(node.power.singleStat or 0, node.power.masteryEffects[effect.id].singleStat) + node.power.pathPower = m_max(node.power.pathPower or 0, node.power.masteryEffects[effect.id].pathPower) + elseif not self.powerStat or not self.powerStat.ignoreForNodes then + node.power.masteryEffects[effect.id].offence, node.power.masteryEffects[effect.id].defence = self:CalculateCombinedOffDefStat(output, calcBase) + node.power.masteryEffects[effect.id].singleStat = node.power.masteryEffects[effect.id].offence + if node.path and not node.ascendancyName then + newPowerMax.offence = m_max(newPowerMax.offence, node.power.masteryEffects[effect.id].offence) + newPowerMax.defence = m_max(newPowerMax.defence, node.power.masteryEffects[effect.id].defence) + newPowerMax.offencePerPoint = m_max(newPowerMax.offencePerPoint, node.power.masteryEffects[effect.id].offence / node.pathDist) + newPowerMax.defencePerPoint = m_max(newPowerMax.defencePerPoint, node.power.masteryEffects[effect.id].defence / node.pathDist) + end + node.power.offence = m_max(node.power.offence or 0, node.power.masteryEffects[effect.id].offence) + node.power.defence = m_max(node.power.defence or 0, node.power.masteryEffects[effect.id].defence) + node.power.singleStat = m_max(node.power.singleStat or 0, node.power.masteryEffects[effect.id].singleStat) + end + end + nodeIndex = nodeIndex + 1 + if coroutine.running() and GetTime() - start > 100 then + if self.build.powerBuilderProgressCallback then + self.build.powerBuilderProgressCallback(m_floor(nodeIndex/total*100)) + end + coroutine.yield() + start = GetTime() + end + end + end + end + end + end + end + -- Calculate the impact of every cluster notable -- used for the power report screen for nodeName, node in pairs(self.build.spec.tree.clusterNodeMap) do diff --git a/src/Classes/PowerReportListControl.lua b/src/Classes/PowerReportListControl.lua index 0b08bdd60f..4c27b97912 100644 --- a/src/Classes/PowerReportListControl.lua +++ b/src/Classes/PowerReportListControl.lua @@ -23,6 +23,7 @@ local PowerReportListClass = newClass("PowerReportListControl", "ListControl", f self.colLabels = true self.nodeSelectCallback = nodeSelectCallback self.showClusters = false + self.showMasteries = true self.allocated = false self.label = "Building Tree..." @@ -34,6 +35,11 @@ local PowerReportListClass = newClass("PowerReportListControl", "ListControl", f self:ReList() self:ReSort(3) -- Sort by power end) + self.controls.masteryCheck = new("CheckBoxControl", {"RIGHT", self.controls.filterSelect, "LEFT"}, {-120, 0, 18}, "Show Masteries:", function(state) + self.showMasteries = state + self:ReList() + self:ReSort(3) -- Sort by power + end, nil, true) end) function PowerReportListClass:SetReport(stat, report) @@ -103,6 +109,9 @@ function PowerReportListClass:ReList() if self.allocated then insert = item.allocated end + if not self.showMasteries and item.type == "Mastery" then + insert = false + end if insert then t_insert(self.list, item) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 99a342cd85..194d5312c8 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1093,6 +1093,53 @@ function TreeTabClass:BuildPowerReportList(currentStat) type = node.type, pathDist = pathDist }) + elseif node.type == "Mastery" and node.power.masteryEffects and not node.ascendancyName then + local pathDist + if isAlloc then + pathDist = #(node.depends or { }) == 0 and 1 or #node.depends + else + pathDist = #(node.path or { }) == 0 and 1 or #node.path + end + + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + local effect = self.build.spec.tree.masteryEffects[masteryEffect.effect] + local effectPower = node.power.masteryEffects[masteryEffect.effect] + if effect and effectPower then + local nodePower = (effectPower.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1) + local pathPower = ((effectPower.pathPower or effectPower.singleStat or 0) / pathDist) * ((displayStat.pc or displayStat.mod) and 100 or 1) + local nodePowerStr = s_format("%"..displayStat.fmt, nodePower) + local pathPowerStr = s_format("%"..displayStat.fmt, pathPower) + + nodePowerStr = formatNumSep(nodePowerStr) + pathPowerStr = formatNumSep(pathPowerStr) + + if (nodePower > 0 and not displayStat.lowerIsBetter) or (nodePower < 0 and displayStat.lowerIsBetter) then + nodePowerStr = colorCodes.POSITIVE .. nodePowerStr + elseif (nodePower < 0 and not displayStat.lowerIsBetter) or (nodePower > 0 and displayStat.lowerIsBetter) then + nodePowerStr = colorCodes.NEGATIVE .. nodePowerStr + end + if (pathPower > 0 and not displayStat.lowerIsBetter) or (pathPower < 0 and displayStat.lowerIsBetter) then + pathPowerStr = colorCodes.POSITIVE .. pathPowerStr + elseif (pathPower < 0 and not displayStat.lowerIsBetter) or (pathPower > 0 and displayStat.lowerIsBetter) then + pathPowerStr = colorCodes.NEGATIVE .. pathPowerStr + end + + local effectLabelParts = isAlloc and not node.allMasteryOptions and node.sd or effect.stats or effect.sd + t_insert(report, { + name = effectLabelParts and node.dn..": "..t_concat(effectLabelParts, " / ") or node.dn, + power = nodePower, + powerStr = nodePowerStr, + pathPower = pathPower, + pathPowerStr = pathPowerStr, + allocated = isAlloc, + id = node.id, + x = node.x, + y = node.y, + type = node.type, + pathDist = pathDist + }) + end + end end end