-- Path of Building -- -- Class: Calc Breakdown Control -- Calculation breakdown control used in the Calcs tab -- local launch, main = ... local t_insert = table.insert local m_max = math.max local m_min = math.min local band = bit.band local CalcBreakdownClass = common.NewClass("CalcBreakdown", "Control", "ControlHost", function(self, calcsTab) self.Control() self.ControlHost() self.calcsTab = calcsTab self.shown = false self.nodeViewer = common.New("PassiveTreeView") self.controls.scrollBar = common.New("ScrollBarControl", {"RIGHT",self,"RIGHT"}, -2, 0, 18, 0, 80, "VERTICAL", true) end) function CalcBreakdownClass:IsMouseOver() if not self:IsShown() then return end return self:IsMouseInBounds() or self:GetMouseOverControl() end function CalcBreakdownClass:SetBreakdownData(displayData, pinned) self.pinned = pinned if displayData == self.sourceData then return end self.sourceData = displayData self.shown = false if not displayData then return end -- Build list of sections self.sectionList = wipeTable(self.sectionList) for _, sectionData in ipairs(displayData) do if sectionData.breakdown then self:AddBreakdownSection(sectionData) elseif sectionData.modName then self:AddModSection(sectionData) end end if #self.sectionList == 0 then self.calcsTab:ClearDisplayStat() return end self.shown = true -- Determine the size of each section, and the combined content size of the breakdown self.contentWidth = 0 local offset = 2 for i, section in ipairs(self.sectionList) do if section.type == "TEXT" then section.width = 0 for _, line in ipairs(section.lines) do section.width = m_max(section.width, DrawStringWidth(section.textSize, "VAR", line) + 8) end section.height = #section.lines * section.textSize + 4 elseif section.type == "TABLE" then -- This also calculates the width of each column in the table section.width = 4 for _, col in pairs(section.colList) do for _, row in pairs(section.rowList) do if row[col.key] then col.width = m_max(col.width or 0, DrawStringWidth(16, "VAR", col.label) + 6, DrawStringWidth(12, "VAR", row[col.key]) + 6) end end if col.width then section.width = section.width + col.width end end section.height = #section.rowList * 14 + 20 if section.label then section.height = section.height + 16 end end self.contentWidth = m_max(self.contentWidth, section.width) section.offset = offset offset = offset + section.height + 8 end self.contentHeight = offset - 6 end -- Add sections based on the breakdown data generated by the Calcs module function CalcBreakdownClass:AddBreakdownSection(sectionData) local breakdown = self.calcsTab.calcsEnv.breakdown[sectionData.breakdown] if not breakdown then return end if #breakdown > 0 then -- Text lines t_insert(self.sectionList, { type = "TEXT", lines = breakdown, textSize = 16 }) end if breakdown.damageComponents and #breakdown.damageComponents > 0 then -- Damage component table, used for hit damage breakdowns local section = { type = "TABLE", rowList = breakdown.damageComponents, colList = { { label = "From", key = "source", right = true }, { label = "Base", key = "base" }, { label = "Inc/red", key = "inc" }, { label = "More/less", key = "more" }, { label = "Converted Damage", key = "convSrc" }, { label = "Total", key = "total" }, { label = "Conversion", key = "convDst" }, } } t_insert(self.sectionList, section) end if breakdown.reservations and #breakdown.reservations > 0 then -- Reservations table, used for life/mana reservation breakdowns local section = { type = "TABLE", rowList = breakdown.reservations, colList = { { label = "Skill", key = "skillName" }, { label = "Base", key = "base" }, { label = "MCM", key = "mult" }, { label = "More/less", key = "more" }, { label = "Inc/red", key = "inc" }, { label = "Reservation", key = "total" }, } } t_insert(self.sectionList, section) end if breakdown.slots and #breakdown.slots > 0 then -- Slots table, used for armour/evasion/ES total breakdowns local section = { type = "TABLE", rowList = breakdown.slots, colList = { { label = "Base", key = "base", right = true }, { label = "Inc/red", key = "inc" }, { label = "More/less", key = "more" }, { label = "Total", key = "total", right = true }, { label = "Source", key = "source" }, { label = "Name", key = "sourceLabel" }, }, } t_insert(self.sectionList, section) for _, row in pairs(section.rowList) do if row.item then row.sourceLabel = data.colorCodes[row.item.rarity]..row.item.name row.sourceLabelTooltip = function() self.calcsTab.build.itemsTab:AddItemTooltip(row.item, row.source) return data.colorCodes[row.item.rarity], true end else row.sourceLabel = row.sourceName end end end end -- Add a table section showing a list of modifiers function CalcBreakdownClass:AddModSection(sectionData) local env = self.calcsTab.calcsEnv local build = self.calcsTab.build -- Build list of modifiers to display local cfg = (sectionData.cfg and copyTable(env.mainSkill[sectionData.cfg.."Cfg"])) or { } cfg.source = sectionData.modSource cfg.tabulate = true local rowList local modDB = sectionData.enemy and env.enemyDB or env.modDB if type(sectionData.modName) == "table" then rowList = modDB:Sum(sectionData.modType, cfg, unpack(sectionData.modName)) else rowList = modDB:Sum(sectionData.modType, cfg, sectionData.modName) end if #rowList == 0 then return end -- Create section data local section = { type = "TABLE", label = sectionData.label, rowList = rowList, colList = { { label = "Value", key = "value" }, { label = "Stat", key = "name" }, { label = "Skill types", key = "flags" }, { label = "Notes", key = "tags" }, { label = "Source", key = "source" }, { label = "Source Name", key = "sourceName" }, }, } t_insert(self.sectionList, section) if not sectionData.modType then -- Sort modifiers by type for i, row in pairs(rowList) do row.index = i end table.sort(rowList, function(a, b) if a.mod.type == b.mod.type then return a.index < b.index else return a.mod.type < b.mod.type end end) end local sourceTotals = { } if not sectionData.modSource then -- Build list of totals from each modifier source local types = { } local typeList = { } for i, row in pairs(rowList) do -- Find all the modifier types and source types that are present in the modifier lsit if not types[row.mod.type] then types[row.mod.type] = true t_insert(typeList, row.mod.type) end local sourceType = row.mod.source:match("[^:]+") if not sourceTotals[sourceType] then sourceTotals[sourceType] = { } end end cfg.tabulate = false for sourceType, lines in pairs(sourceTotals) do cfg.source = sourceType for _, modType in ipairs(typeList) do if type(sectionData.modName) == "table" then -- Multiple stats, show each separately for _, modName in ipairs(sectionData.modName) do local total = modDB:Sum(modType, cfg, modName) if modType == "MORE" then total = round((total - 1) * 100) end if total ~= 0 then t_insert(lines, self:FormatModValue(total, modType) .. " " .. modName:gsub("(%l)(%u)","%1 %2")) end end else local total = modDB:Sum(modType, cfg, sectionData.modName) if modType == "MORE" then total = round((total - 1) * 100) end if total ~= 0 then t_insert(lines, self:FormatModValue(total, modType)) end end end end end -- Process modifier data for _, row in pairs(rowList) do if not sectionData.modType then -- No modifier type specified, so format the value to convey type row.value = self:FormatModValue(row.value, row.mod.type) else section.colList[1].right = true row.value = formatRound(row.value, 2) end if type(sectionData.modName) == "table" then -- Multiple stat names specified, add this modifier's stat to the table row.name = self:FormatModName(row.mod.name) end local sourceType = row.mod.source:match("[^:]+") if not sectionData.modSource then -- No modifier source specified, add the source type to the table row.source = sourceType row.sourceTooltip = function() main:AddTooltipLine(16, "Total from "..sourceType..":") for _, line in ipairs(sourceTotals[sourceType]) do main:AddTooltipLine(14, line) end return nil, false end end if sourceType == "Item" then -- Modifier is from an item, add item name and tooltip local itemId = row.mod.source:match("Item:(%d+):.+") local item = build.itemsTab.list[tonumber(itemId)] if item then row.sourceName = data.colorCodes[item.rarity]..item.name row.sourceNameTooltip = function() build.itemsTab:AddItemTooltip(item, row.mod.sourceSlot) return data.colorCodes[item.rarity], true end end elseif sourceType == "Tree" then -- Modifier is from a passive node, add node name, and add node ID (used to show node location) local nodeId = row.mod.source:match("Tree:(%d+)") if nodeId then local node = build.spec.nodes[tonumber(nodeId)] row.sourceName = node.dn row.sourceNameNode = node elseif row.mod.source == "Tree:Jewel" then row.sourceName = "Jewel conversion" end elseif sourceType == "Skill" then -- Extract skill name row.sourceName = row.mod.source:match("Skill:(.+)") end if row.mod.flags ~= 0 or row.mod.keywordFlags ~= 0 then -- Combine, sort and format modifier flags local flagNames = { } for flags, src in pairs({[row.mod.flags] = ModFlag, [row.mod.keywordFlags] = KeywordFlag}) do for name, val in pairs(src) do if band(flags, val) == val then t_insert(flagNames, name) end end end table.sort(flagNames) row.flags = table.concat(flagNames, ", ") end if row.mod.tagList[1] then -- Format modifier tags local baseVal = (row.mod.type == "BASE" and string.format("%+g", math.abs(row.mod.value)) or math.abs(row.mod.value).."%") for _, tag in ipairs(row.mod.tagList) do local desc if tag.type == "Condition" then desc = "Condition: "..(tag.neg and "Not " or "")..(tag.varList and table.concat(tag.varList, "/") or self:FormatModName(tag.var)) elseif tag.type == "Multiplier" then if tag.base then desc = (row.mod.type == "BASE" and string.format("%+g", tag.base) or tag.base.."%").." + "..math.abs(row.mod.value).." per "..self:FormatModName(tag.var) else desc = baseVal.." per "..self:FormatModName(tag.var) end elseif tag.type == "PerStat" then if tag.base then desc = (row.mod.type == "BASE" and string.format("%+g", tag.base) or tag.base.."%").." + "..math.abs(row.mod.value).." per "..tag.div.." "..self:FormatModName(tag.var) else desc = baseVal.." per "..tag.div.." "..self:FormatModName(tag.stat) end elseif tag.type == "SkillName" then desc = "Skill: "..tag.skillName elseif tag.type == "SkillType" then for name, type in pairs(SkillType) do if type == tag.skillType then desc = "Skill type: "..self:FormatModName(name) break end end if not desc then desc = "Skill type: ?" end elseif tag.type == "SlotNumber" then desc = "When in slot #"..tag.num elseif tag.type == "GlobalEffect" then desc = tag.effectType else desc = self:FormatModName(tag.type) end if desc then row.tags = (row.tags and row.tags .. ", " or "") .. desc end end end end end function CalcBreakdownClass:FormatModName(modName) return modName:gsub("([%l%d])(%u)","%1 %2"):gsub("(%l)(%d)","%1 %2") end function CalcBreakdownClass:FormatModValue(value, modType) if modType == "BASE" then return string.format("%+g base", value) elseif modType == "INC" then if value >= 0 then return value.."% increased" else return -value.."% decreased" end elseif modType == "MORE" then if value >= 0 then return value.."% more" else return -value.."% less" end elseif modType == "FLAG" then return value and "True" or "False" else return value end end function CalcBreakdownClass:DrawBreakdownTable(viewPort, x, y, section) local cursorX, cursorY = GetCursorPos() if section.label then -- Draw table lable if able DrawString(x + 2, y, "LEFT", 16, "VAR", "^7"..section.label..":") y = y + 16 end local colX = x + 4 for index, col in ipairs(section.colList) do if col.width then -- Column is present, draw the separator and label col.x = colX if index > 1 then -- Skip the separator for the first column SetDrawColor(0.5, 0.5, 0.5) DrawImage(nil, colX - 2, y, 1, section.label and section.height - 16 or section.height) end SetDrawColor(1, 1, 1) DrawString(colX, y + 2, "LEFT", 16, "VAR", col.label) colX = colX + col.width end end local rowY = y + 20 for _, row in ipairs(section.rowList) do -- Draw row separator SetDrawColor(0.5, 0.5, 0.5) DrawImage(nil, x + 2, rowY - 1, section.width - 4, 1) for _, col in ipairs(section.colList) do if col.width and row[col.key] then -- This row has an entry for this column, draw it if col.right then DrawString(col.x + col.width - 4, rowY + 1, "RIGHT_X", 12, "VAR", "^7"..row[col.key]) else DrawString(col.x, rowY + 1, "LEFT", 12, "VAR", "^7"..row[col.key]) end local ttFunc = row[col.key.."Tooltip"] local ttNode = row[col.key.."Node"] if (ttFunc or ttNode) and cursorY >= viewPort.y + 2 and cursorY < viewPort.y + viewPort.height - 2 and cursorX >= col.x and cursorY >= rowY and cursorX < col.x + col.width and cursorY < rowY + 14 then -- Mouse is over the cell, draw highlighting lines and show the tooltip/node location SetDrawLayer(nil, 15) SetDrawColor(0, 1, 0) DrawImage(nil, col.x - 2, rowY - 1, col.width, 1) DrawImage(nil, col.x - 2, rowY + 13, col.width, 1) if ttFunc then local color, center = ttFunc() main:DrawTooltip(col.x, rowY, col.width, 12, viewPort, color, center) elseif ttNode then local viewerX = col.x + col.width + 5 if viewPort.x + viewPort.width < viewerX + 304 then viewerX = col.x - 309 end local viewerY = m_min(rowY, viewPort.y + viewPort.height - 304) SetDrawColor(1, 1, 1) DrawImage(nil, viewerX, viewerY, 304, 304) local viewer = self.nodeViewer viewer.zoom = 5 viewer.zoomX = -ttNode.x / 11.85 viewer.zoomY = -ttNode.y / 11.85 SetViewport(viewerX + 2, viewerY + 2, 300, 300) viewer:Draw(self.calcsTab.build, { x = 0, y = 0, width = 300, height = 300 }, { }) SetDrawColor(1, 0, 0) DrawImage(viewer.highlightRing, 135, 135, 30, 30) SetViewport() end SetDrawLayer(nil, 10) end end end rowY = rowY + 14 end end function CalcBreakdownClass:Draw(viewPort) local sourceData = self.sourceData local scrollBar = self.controls.scrollBar local width = self.contentWidth local height = self.contentHeight if self.contentHeight > viewPort.height then -- Content won't fit the screen height, so set the scrollbar width = self.contentWidth + scrollBar.width height = viewPort.height scrollBar.height = height - 4 scrollBar:SetContentDimension(self.contentHeight - 4, viewPort.height - 4) else scrollBar:SetContentDimension(0, 0) end self.width = width self.height = height -- Calculate position based on the source cell local x = sourceData.x + sourceData.width + 5 local y = m_min(sourceData.y, viewPort.y + viewPort.height - height) if x + width > viewPort.x + viewPort.width then x = m_max(viewPort.x, sourceData.x - 5 - width) end self.x = x self.y = y -- Draw background SetDrawLayer(nil, 10) SetDrawColor(0, 0, 0, 0.9) DrawImage(nil, x + 2, y + 2, width - 4, height - 4) -- Draw border (this is put in sub layer 11 so it draws over the contents, in case they don't fit the screen) SetDrawLayer(nil, 11) if self.pinned then SetDrawColor(0.25, 1, 0.25) else SetDrawColor(0.33, 0.66, 0.33) end DrawImage(nil, x, y, width, 2) DrawImage(nil, x, y + height - 2, width, 2) DrawImage(nil, x, y, 2, height) DrawImage(nil, x + width - 2, y, 2, height) SetDrawLayer(nil, 10) self:DrawControls(viewPort) -- Draw the sections y = y - scrollBar.offset for i, section in ipairs(self.sectionList) do local sectionY = y + section.offset if section.type == "TEXT" then local lineY = sectionY + 2 for i, line in ipairs(section.lines) do SetDrawColor(1, 1, 1) DrawString(x + 4, lineY, "LEFT", section.textSize, "VAR", line) lineY = lineY + section.textSize end elseif section.type == "TABLE" then self:DrawBreakdownTable(viewPort, x, sectionY, section) end end SetDrawLayer(nil, 0) end function CalcBreakdownClass:OnKeyDown(key, doubleClick) if not self:IsShown() or not self:IsEnabled() then return end local mOverControl = self:GetMouseOverControl() if mOverControl and mOverControl.OnKeyDown then return mOverControl:OnKeyDown(key) end local mOver = self:IsMouseOver() if key:match("BUTTON") then if not mOver then -- Mouse click outside the control, hide the breakdown self.calcsTab:ClearDisplayStat() self.shown = false return end end return self end function CalcBreakdownClass:OnKeyUp(key) if not self:IsShown() or not self:IsEnabled() then return end if key == "WHEELDOWN" then self.controls.scrollBar:Scroll(1) elseif key == "WHEELUP" then self.controls.scrollBar:Scroll(-1) end return self end