551 lines
17 KiB
Lua
551 lines
17 KiB
Lua
-- 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: "..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 == "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 |