-- 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 m_ceil = math.ceil local m_floor = math.floor local m_sin = math.sin local m_cos = math.cos local m_pi = math.pi 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.tooltip = common.New("Tooltip") self.nodeViewer = common.New("PassiveTreeView") self.rangeGuide = NewImageHandle() self.rangeGuide:Load("Assets/range_guide.png") self.uiOverlay = NewImageHandle() self.uiOverlay:Load("Assets/game_ui_small.png") 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 self.calcsTab:CheckFlag(sectionData) then if sectionData.breakdown then self:AddBreakdownSection(sectionData) elseif sectionData.modName then self:AddModSection(sectionData) end 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 if section.label then section.width = m_max(section.width, 6 + DrawStringWidth(16, "VAR", section.label..":")) 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 actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player local breakdown local ns, name = sectionData.breakdown:match("^(%a+)%.(%a+)$") if ns then breakdown = actor.breakdown[ns] and actor.breakdown[ns][name] else breakdown = actor.breakdown[sectionData.breakdown] end 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.radius then -- Radius visualiser t_insert(self.sectionList, { type = "RADIUS", radius = breakdown.radius, width = 8 + 1920/4, height = 4 + 1080/4, }) end if breakdown.rowList and #breakdown.rowList > 0 then -- Generic table local section = { type = "TABLE", label = breakdown.label, rowList = breakdown.rowList, colList = breakdown.colList, } 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.damageTypes and #breakdown.damageTypes > 0 then local section = { type = "TABLE", rowList = breakdown.damageTypes, 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.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 = colorCodes[row.item.rarity]..row.item.name row.sourceLabelTooltip = function(tooltip) self.calcsTab.build.itemsTab:AddItemTooltip(tooltip, row.item, row.source) end else row.sourceLabel = row.sourceName end end end if breakdown.modList and #breakdown.modList > 0 then -- Provided mod list self:AddModSection(sectionData, breakdown.modList) end end -- Add a table section showing a list of modifiers function CalcBreakdownClass:AddModSection(sectionData, modList) local actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player local build = self.calcsTab.build -- Build list of modifiers to display local cfg = (sectionData.cfg and actor.mainSkill[sectionData.cfg.."Cfg"] and copyTable(actor.mainSkill[sectionData.cfg.."Cfg"], true)) or { } cfg.source = sectionData.modSource local rowList local modDB = sectionData.enemy and actor.enemy.modDB or actor.modDB if modList then rowList = modList else if type(sectionData.modName) == "table" then rowList = modDB:Tabulate(sectionData.modType, cfg, unpack(sectionData.modName)) else rowList = modDB:Tabulate(sectionData.modType, cfg, sectionData.modName) end end if #rowList == 0 then return end -- Create section data local section = { type = "TABLE", label = sectionData.label, rowList = rowList, colList = { { label = "Value", key = "displayValue" }, { 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 modList and not sectionData.modType then -- Sort modifiers by type for i, row in ipairs(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 modList and not sectionData.modSource then -- Build list of totals from each modifier source local types = { } local typeList = { } for i, row in ipairs(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 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 and 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 and total ~= 0 then t_insert(lines, self:FormatModValue(total, modType)) end end end end end -- Process modifier data for _, row in ipairs(rowList) do if not sectionData.modType then -- No modifier type specified, so format the value to convey type row.displayValue = self:FormatModValue(row.value, row.mod.type) else section.colList[1].right = true row.displayValue = formatRound(row.value, 2) end if modList or 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 modList and not sectionData.modSource then -- No modifier source specified, add the source type to the table row.source = sourceType row.sourceTooltip = function(tooltip) tooltip:AddLine(16, "Total from "..sourceType..":") for _, line in ipairs(sourceTotals[sourceType]) do tooltip:AddLine(14, line) end 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.items[tonumber(itemId)] if item then row.sourceName = colorCodes[item.rarity]..item.name row.sourceNameTooltip = function(tooltip) build.itemsTab:AddItemTooltip(tooltip, item, row.mod.sourceSlot) 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 end elseif sourceType == "Skill" then -- Extract skill name row.sourceName = build.data.skills[row.mod.source:match("Skill:(.+)")].name 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 row.tags = nil if row.mod[1] then -- Format modifier tags local baseVal = type(row.mod.value) == "number" and (self:FormatModBase(row.mod, row.mod.value) .. "") for _, tag in ipairs(row.mod) do local desc if tag.type == "Condition" or tag.type == "ActorCondition" then desc = (tag.actor and (tag.actor:sub(1,1):upper()..tag.actor:sub(2).." ") or "").."Condition: "..(tag.neg and "Not " or "")..self:FormatVarNameOrList(tag.var, tag.varList) elseif tag.type == "Multiplier" then local base = tag.base and (self:FormatModBase(row.mod, tag.base).."+ "..math.abs(row.mod.value).." ") or baseVal desc = base.."per "..(tag.div and (tag.div.." ") or "")..self:FormatVarNameOrList(tag.var, tag.varList) baseVal = "" elseif tag.type == "PerStat" then local base = tag.base and (self:FormatModBase(row.mod, tag.base).."+ "..math.abs(row.mod.value).." ") or baseVal desc = base.."per "..(tag.div or 1).." "..self:FormatVarNameOrList(tag.stat, tag.statList) baseVal = "" elseif tag.type == "MultiplierThreshold" or tag.type == "StatThreshold" then desc = "If "..self:FormatVarNameOrList(tag.var or tag.stat, tag.varList or tag.statList)..(tag.upper and " <= " or " >= ")..(tag.threshold or self:FormatModName(tag.thresholdVar or tag.thresholdStat)) elseif tag.type == "SkillName" then desc = "Skill: "..tag.skillName elseif tag.type == "SkillId" then desc = "Skill: "..build.data.skills[tag.skillId].name elseif tag.type == "SkillType" then for name, type in pairs(SkillType) do if type == tag.skillType then desc = "Skill type: "..(tag.neg and "Not " or "")..self:FormatModName(name) break end end if not desc then desc = "Skill type: "..(tag.neg and "Not " or "").."?" 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:FormatVarNameOrList(var, varList) return var and self:FormatModName(var) or table.concat(varList, "/") end function CalcBreakdownClass:FormatModBase(mod, base) return mod.type == "BASE" and string.format("%+g ", math.abs(base)) or math.abs(base).."% " 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.."% reduced" end elseif modType == "MORE" then if value >= 0 then return value.."% more" else return -value.."% less" end elseif modType == "OVERRIDE" then return "Override: "..value 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 self.tooltip:Clear() ttFunc(self.tooltip) self.tooltip:Draw(col.x, rowY, col.width, 12, viewPort) 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 }, { }) SetDrawLayer(nil, 30) 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:DrawRadiusVisual(x, y, width, height, radius) SetDrawColor(0.75, 0.75, 0.75) DrawImage(self.rangeGuide, x, y, width, height) --SetDrawColor(0, 0, 0) --DrawImage(nil, x, y, width, height) --[[SetDrawColor(0.5, 0.5, 0.75) for r = 10, 130, 20 do main:RenderRing(x, y, width, height, 0, 0, r, 3) end SetDrawColor(1, 1, 1) for r = 20, 120, 20 do main:RenderRing(x, y, width, height, 0, 0, r, 3) end main:RenderCircle(x, y, width, height, 0, 0, 2)]] SetDrawColor(0.5, 1, 0.5, 0.33) main:RenderCircle(x, y, width, height, 0, 0, radius) --[[SetDrawColor(1, 0.5, 0.5, 0.33) if not self.foo1 then self.foo1, self.foo2 = 0, 0 end if IsKeyDown("LEFT") then self.foo1 = self.foo1 - 0.3 elseif IsKeyDown("RIGHT") then self.foo1 = self.foo1 + 0.3 end if IsKeyDown("UP") then self.foo2 = self.foo2 + 0.3 elseif IsKeyDown("DOWN") then self.foo2 = self.foo2 - 0.3 end main:RenderCircle(x, y, width, height, self.foo1, self.foo2, 30)]] SetDrawColor(1, 1, 1) DrawImage(self.uiOverlay, x, y, width, height) 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) elseif section.type == "RADIUS" then SetDrawColor(1, 1, 1) DrawImage(nil, x + 2, sectionY, section.width - 4, section.height) self:DrawRadiusVisual(x + 4, sectionY + 2, section.width - 8, section.height - 4, section.radius) 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