diff --git a/spec/System/TestTreeTab_spec.lua b/spec/System/TestTreeTab_spec.lua new file mode 100644 index 0000000000..e9418b8921 --- /dev/null +++ b/spec/System/TestTreeTab_spec.lua @@ -0,0 +1,63 @@ +describe("TreeTab", function() + local originalClusterNodeMap + local originalMasteryEffects + + before_each(function() + newBuild() + originalClusterNodeMap = build.spec.tree.clusterNodeMap + originalMasteryEffects = build.spec.tree.masteryEffects + end) + + after_each(function() + build.spec.tree.clusterNodeMap = originalClusterNodeMap + build.spec.tree.masteryEffects = originalMasteryEffects + 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 ce9303856d..690103ba09 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -479,6 +479,7 @@ function CalcsTabClass:PowerBuilder() local cache = { } local distanceMap = { } local distanceList = { } + local masteryNodeList = { } local newPowerMax = { singleStat = 0, offence = 0, @@ -492,6 +493,47 @@ 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 function masteryEffectCanBeAssignedToNode(node, masteryEffect) + local assignedNodeId = isValueInTable(self.build.spec.masterySelections, masteryEffect.effect) + return not assignedNodeId or assignedNodeId == node.id + end + + local function calculateAddNodePower(power, node, output, buildPathNodes) + if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then + power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase) + if node.path and not node.ascendancyName then + newPowerMax.singleStat = m_max(newPowerMax.singleStat, power.singleStat) + power.pathPower = power.singleStat + if node.pathDist > 1 then + power.pathPower = self:CalculatePowerStat(self.powerStat, calcFunc({ addNodes = buildPathNodes() }, useFullDPS), calcBase) + end + end + elseif not self.powerStat or not self.powerStat.ignoreForNodes then + power.offence, power.defence = self:CalculateCombinedOffDefStat(output, calcBase) + power.singleStat = power.offence + if node.path and not node.ascendancyName then + newPowerMax.offence = m_max(newPowerMax.offence, power.offence) + newPowerMax.defence = m_max(newPowerMax.defence, power.defence) + newPowerMax.offencePerPoint = m_max(newPowerMax.offencePerPoint, power.offence / node.pathDist) + newPowerMax.defencePerPoint = m_max(newPowerMax.defencePerPoint, power.defence / node.pathDist) + end + end + end local start = GetTime() local nodeIndex = 0 @@ -499,11 +541,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 + t_insert(masteryNodeList, node) + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + if masteryEffectCanBeAssignedToNode(node, masteryEffect) 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 @@ -530,29 +586,13 @@ function CalcsTabClass:PowerBuilder() 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 node.path and not node.ascendancyName then - newPowerMax.singleStat = m_max(newPowerMax.singleStat, node.power.singleStat) - node.power.pathPower = node.power.singleStat - local pathNodes = { } - for _, node in pairs(node.path) do - pathNodes[node] = true - end - if node.pathDist > 1 then - node.power.pathPower = self:CalculatePowerStat(self.powerStat, calcFunc({ addNodes = pathNodes }, useFullDPS), calcBase) - end - end - elseif not self.powerStat or not self.powerStat.ignoreForNodes then - node.power.offence, node.power.defence = self:CalculateCombinedOffDefStat(output, calcBase) - node.power.singleStat = node.power.offence - if node.path and not node.ascendancyName then - newPowerMax.offence = m_max(newPowerMax.offence, node.power.offence) - newPowerMax.defence = m_max(newPowerMax.defence, node.power.defence) - newPowerMax.offencePerPoint = m_max(newPowerMax.offencePerPoint, node.power.offence / node.pathDist) - newPowerMax.defencePerPoint = m_max(newPowerMax.defencePerPoint, node.power.defence / node.pathDist) + calculateAddNodePower(node.power, node, output, function() + local pathNodes = { } + for _, pathNode in pairs(node.path) do + pathNodes[pathNode] = true end - end + 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) @@ -572,6 +612,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 +634,53 @@ function CalcsTabClass:PowerBuilder() end end + for _, node in ipairs(masteryNodeList) do + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + if masteryEffectCanBeAssignedToNode(node, masteryEffect) 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] = { } + local effectPower = node.power.masteryEffects[effect.id] + calculateAddNodePower(effectPower, node, output, function() + local pathNodes = { + [effectNode] = true + } + for _, pathNode in pairs(node.path) do + if pathNode ~= node then + pathNodes[pathNode] = true + end + end + return pathNodes + end) + if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then + effectPower.pathPower = effectPower.pathPower or effectPower.singleStat + node.power.singleStat = m_max(node.power.singleStat or 0, effectPower.singleStat) + node.power.pathPower = m_max(node.power.pathPower or 0, effectPower.pathPower) + elseif not self.powerStat or not self.powerStat.ignoreForNodes then + node.power.offence = m_max(node.power.offence or 0, effectPower.offence) + node.power.defence = m_max(node.power.defence or 0, effectPower.defence) + node.power.singleStat = m_max(node.power.singleStat or 0, effectPower.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 + -- 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 d33ed88456..bd2c1d2cd3 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 6a981533a3..77bfe3ffdb 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1077,50 +1077,61 @@ function TreeTabClass:BuildPowerReportList(currentStat) fmt = ".1f" } end + local powerMultiplier = (displayStat.pc or displayStat.mod) and 100 or 1 + local function formatPower(power) + local powerStr = formatNumSep(s_format("%"..displayStat.fmt, power)) + if (power > 0 and not displayStat.lowerIsBetter) or (power < 0 and displayStat.lowerIsBetter) then + return colorCodes.POSITIVE .. powerStr + elseif (power < 0 and not displayStat.lowerIsBetter) or (power > 0 and displayStat.lowerIsBetter) then + return colorCodes.NEGATIVE .. powerStr + end + return powerStr + end + local function getNodePathDist(node, isAlloc) + if isAlloc then + return #(node.depends or { }) == 0 and 1 or #node.depends + end + return #(node.path or { }) == 0 and 1 or #node.path + end + local function addReportEntry(node, name, nodePower, pathPower, pathDist, isAlloc, pathPowerStr) + t_insert(report, { + name = name, + power = nodePower, + powerStr = formatPower(nodePower), + pathPower = pathPower, + pathPowerStr = pathPowerStr or formatPower(pathPower), + allocated = isAlloc, + id = node.id, + x = node.x, + y = node.y, + type = node.type, + sd = node.sd, + pathDist = pathDist + }) + end -- search all nodes, ignoring ascendancies, sockets, etc. for nodeId, node in pairs(self.build.spec.nodes) do local isAlloc = node.alloc or self.build.calcsTab.mainEnv.grantedPassives[nodeId] if (node.type == "Normal" or node.type == "Keystone" or node.type == "Notable") 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 - local nodePower = (node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1) - local pathPower = (node.power.pathPower 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 + local pathDist = getNodePathDist(node, isAlloc) + local nodePower = (node.power.singleStat or 0) * powerMultiplier + local pathPower = (node.power.pathPower or 0) / pathDist * powerMultiplier + addReportEntry(node, node.dn, nodePower, pathPower, pathDist, isAlloc) + elseif node.type == "Mastery" and node.power.masteryEffects and not node.ascendancyName then + local pathDist = getNodePathDist(node, isAlloc) + + 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 effectLabelParts = isAlloc and not node.allMasteryOptions and node.sd or effect.stats or effect.sd + local name = effectLabelParts and node.dn..": "..t_concat(effectLabelParts, " / ") or node.dn + local nodePower = (effectPower.singleStat or 0) * powerMultiplier + local pathPower = ((effectPower.pathPower or effectPower.singleStat or 0) / pathDist) * powerMultiplier + addReportEntry(node, name, nodePower, pathPower, pathDist, isAlloc) + end end - - t_insert(report, { - name = node.dn, - power = nodePower, - powerStr = nodePowerStr, - pathPower = pathPower, - pathPowerStr = pathPowerStr, - allocated = isAlloc, - id = node.id, - x = node.x, - y = node.y, - type = node.type, - sd = node.sd, - pathDist = pathDist - }) end end @@ -1128,28 +1139,8 @@ function TreeTabClass:BuildPowerReportList(currentStat) for nodeName, node in pairs(self.build.spec.tree.clusterNodeMap) do local isAlloc = node.alloc if not isAlloc then - local nodePower = (node.power and node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1) - local nodePowerStr = s_format("%"..displayStat.fmt, nodePower) - - nodePowerStr = formatNumSep(nodePowerStr) - - 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 - - t_insert(report, { - name = node.dn, - power = nodePower, - powerStr = nodePowerStr, - pathPower = 0, - pathPowerStr = "--", - id = node.id, - type = node.type, - sd = node.sd, - pathDist = "Cluster" - }) + local nodePower = (node.power and node.power.singleStat or 0) * powerMultiplier + addReportEntry(node, node.dn, nodePower, 0, "Cluster", isAlloc, "--") end end