Files
PathOfBuilding/src/Modules/Build.lua
LocalIdentity 91a5571210 Fix calculation when using Unexciting Runegraft (#9251)
I implemented the Unexciting mod incorrectly in the past making it negate any lucky or unlucky mod instead of applying the correct formula
Unexciting rolls three times and keeps the median result -> 3p^2 - 2p^3

This affects Block Chance, Suppress chance and crit chance

Also changed the sidebar for Block and Suppress chance to use 3 and 2 significant figures as when using azadi's crest for double lucky the block chance values can easily get to 99 and require more precision
Made sure so add a new handler that trims off 0 after the decimal so regular block builds don't show 75.000% block

Co-authored-by: LocalIdentity <localidentity2@gmail.com>
2025-11-15 14:42:34 +11:00

1938 lines
75 KiB
Lua

-- Path of Building
--
-- Module: Build
-- Loads and manages the current build.
--
local pairs = pairs
local ipairs = ipairs
local next = next
local t_insert = table.insert
local t_sort = table.sort
local m_min = math.min
local m_max = math.max
local m_floor = math.floor
local m_abs = math.abs
local s_format = string.format
local buildMode = new("ControlHost")
local function InsertIfNew(t, val)
if (not t) then return end
for i,v in ipairs(t) do
if v == val then return end
end
table.insert(t, val)
end
---matchFlags
--- Compares the skill flags table against the line flag settings
--- Required enabling flags check takes precedence over disabling flags check
---@param reqFlags table containing the required flags
---@param notFlags table containing the disabling flags
---@param flags table containing the flags to match against
local function matchFlags(reqFlags, notFlags, flags)
if type(reqFlags) == "string" then
reqFlags = { reqFlags }
end
if reqFlags then
for _, flag in ipairs(reqFlags) do
if not flags[flag] then
return
end
end
end
if type(notFlags) == "string" then
notFlags = { notFlags }
end
if notFlags then
for _, flag in ipairs(notFlags) do
if flags[flag] then
return
end
end
end
-- Both flag checks passed, default true
return true
end
function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLink)
self.dbFileName = dbFileName
self.buildName = buildName
self.importLink = importLink
if dbFileName then
self.dbFileSubPath = self.dbFileName:sub(#main.buildPath + 1, -#self.buildName - 5)
else
self.dbFileSubPath = main.modes.LIST.subPath or ""
end
if not buildName then
main:SetMode("LIST")
end
-- Load build file
self.xmlSectionList = { }
self.spectreList = { }
self.timelessData = { jewelType = { }, conquerorType = { }, devotionVariant1 = 1, devotionVariant2 = 1, jewelSocket = { }, fallbackWeightMode = { }, searchList = "", searchListFallback = "", searchResults = { }, sharedResults = { } }
self.viewMode = "TREE"
self.characterLevel = m_min(m_max(main.defaultCharLevel or 1, 1), 100)
self.targetVersion = liveTargetVersion
self.bandit = "None"
self.pantheonMajorGod = "None"
self.pantheonMinorGod = "None"
self.characterLevelAutoMode = main.defaultCharLevel == 1 or main.defaultCharLevel == nil
if buildXML then
if self:LoadDB(buildXML, "Unnamed build") then
self:CloseBuild()
return
end
self.modFlag = true
else
if self:LoadDBFile() then
self:CloseBuild()
return
end
self.modFlag = false
end
if convertBuild then
self.targetVersion = liveTargetVersion
end
if self.targetVersion ~= liveTargetVersion then
self.targetVersion = nil
self:OpenConversionPopup()
return
end
self.abortSave = true
wipeTable(self.controls)
local miscTooltip = new("Tooltip")
-- Controls: top bar, left side
self.anchorTopBarLeft = new("Control", nil, {4, 4, 0, 20})
self.controls.back = new("ButtonControl", {"LEFT",self.anchorTopBarLeft,"RIGHT"}, {0, 0, 60, 20}, "<< Back", function()
if self.unsaved then
self:OpenSavePopup("LIST")
else
self:CloseBuild()
end
end)
self.controls.buildName = new("Control", {"LEFT",self.controls.back,"RIGHT"}, {8, 0, 0, 20})
self.controls.buildName.width = function(control)
local limit = self.anchorTopBarRight:GetPos() - 98 - 40 - self.controls.back:GetSize() - self.controls.save:GetSize() - self.controls.saveAs:GetSize()
local bnw = DrawStringWidth(16, "VAR", self.buildName)
self.strWidth = m_min(bnw, limit)
self.strLimited = bnw > limit
return self.strWidth + 98
end
self.controls.buildName.Draw = function(control)
local x, y = control:GetPos()
local width, height = control:GetSize()
SetDrawColor(0.5, 0.5, 0.5)
DrawImage(nil, x + 91, y, self.strWidth + 6, 20)
SetDrawColor(0, 0, 0)
DrawImage(nil, x + 92, y + 1, self.strWidth + 4, 18)
SetDrawColor(1, 1, 1)
SetViewport(x, y + 2, self.strWidth + 94, 16)
DrawString(0, 0, "LEFT", 16, "VAR", "Current build: "..self.buildName)
SetViewport()
if control:IsMouseInBounds() then
SetDrawLayer(nil, 10)
miscTooltip:Clear()
if self.dbFileSubPath and self.dbFileSubPath ~= "" then
miscTooltip:AddLine(16, self.dbFileSubPath..self.buildName)
elseif self.strLimited then
miscTooltip:AddLine(16, self.buildName)
end
miscTooltip:Draw(x, y, width, height, main.viewPort)
SetDrawLayer(nil, 0)
end
end
self.controls.save = new("ButtonControl", {"LEFT",self.controls.buildName,"RIGHT"}, {8, 0, 50, 20}, "Save", function()
self:SaveDBFile()
end)
self.controls.save.enabled = function()
return not self.dbFileName or self.unsaved
end
self.controls.saveAs = new("ButtonControl", {"LEFT",self.controls.save,"RIGHT"}, {8, 0, 70, 20}, "Save As", function()
self:OpenSaveAsPopup()
end)
self.controls.saveAs.enabled = function()
return self.dbFileName
end
-- Controls: top bar, right side
self.anchorTopBarRight = new("Control", nil, {function() return main.screenW / 2 + 6 end, 4, 0, 20})
self.controls.pointDisplay = new("Control", {"LEFT",self.anchorTopBarRight,"RIGHT"}, {-12, 0, 0, 20})
self.controls.pointDisplay.x = function(control)
local width, height = control:GetSize()
if self.controls.saveAs:GetPos() + self.controls.saveAs:GetSize() < self.anchorTopBarRight:GetPos() - width - 16 then
return -12 - width
else
return 0
end
end
self.controls.pointDisplay.width = function(control)
control.str, control.req = self:EstimatePlayerProgress()
return DrawStringWidth(16, "FIXED", control.str) + 8
end
self.controls.pointDisplay.Draw = function(control)
local x, y = control:GetPos()
local width, height = control:GetSize()
SetDrawColor(1, 1, 1)
DrawImage(nil, x, y, width, height)
SetDrawColor(0, 0, 0)
DrawImage(nil, x + 1, y + 1, width - 2, height - 2)
SetDrawColor(1, 1, 1)
DrawString(x + 4, y + 2, "LEFT", 16, "FIXED", control.str)
if control:IsMouseInBounds() then
SetDrawLayer(nil, 10)
miscTooltip:Clear()
miscTooltip:AddLine(16, control.req)
miscTooltip:Draw(x, y, width, height, main.viewPort)
SetDrawLayer(nil, 0)
end
end
self.controls.levelScalingButton = new("ButtonControl", {"LEFT",self.controls.pointDisplay,"RIGHT"}, {12, 0, 50, 20}, self.characterLevelAutoMode and "Auto" or "Manual", function()
self.characterLevelAutoMode = not self.characterLevelAutoMode
self.controls.levelScalingButton.label = self.characterLevelAutoMode and "Auto" or "Manual"
self.configTab:BuildModList()
self.modFlag = true
self.buildFlag = true
end)
self.controls.characterLevel = new("EditControl", {"LEFT",self.controls.levelScalingButton,"RIGHT"}, {8, 0, 106, 20}, "", "Level", "%D", 3, function(buf)
self.characterLevel = m_min(m_max(tonumber(buf) or 1, 1), 100)
self.configTab:BuildModList()
self.modFlag = true
self.buildFlag = true
self.characterLevelAutoMode = false
self.controls.levelScalingButton.label = "Manual"
end)
self.controls.characterLevel:SetText(self.characterLevel)
self.controls.characterLevel.tooltipFunc = function(tooltip)
if tooltip:CheckForUpdate(self.characterLevel) then
tooltip:AddLine(16, "Experience multiplier:")
local playerLevel = self.characterLevel
local safeZone = 3 + m_floor(playerLevel / 16)
for level, expLevel in ipairs(self.data.monsterExperienceLevelMap) do
local diff = m_abs(playerLevel - expLevel) - safeZone
local mult
if diff <= 0 then
mult = 1
else
mult = ((playerLevel + 5) / (playerLevel + 5 + diff ^ 2.5)) ^ 1.5
end
if playerLevel >= 95 then
local xpPenalty = ({0.935, 0.885, 0.813, 0.7175, 0.6})[playerLevel - 94] or 0
mult = mult * (1 / (1 + 0.1 * (playerLevel - 94))) * xpPenalty
end
if mult > 0.01 then
local line = level
if level >= 68 then
line = line .. string.format(" (Tier %d)", level - 67)
end
line = line .. string.format(": %.1f%%", mult * 100)
tooltip:AddLine(14, line)
end
end
end
end
self.controls.classDrop = new("DropDownControl", {"LEFT",self.controls.characterLevel,"RIGHT"}, {8, 0, 100, 20}, nil, function(index, value)
if value.classId ~= self.spec.curClassId then
if self.spec:CountAllocNodes() == 0 or self.spec:IsClassConnected(value.classId) then
self.spec:SelectClass(value.classId)
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
else
main:OpenConfirmPopup("Class Change", "Changing class to "..value.label.." will reset your passive tree.\nThis can be avoided by connecting one of the "..value.label.." starting nodes to your tree.", "Continue", function()
self.spec:SelectClass(value.classId)
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
end)
end
end
end)
self.controls.ascendDrop = new("DropDownControl", {"LEFT",self.controls.classDrop,"RIGHT"}, {8, 0, 120, 20}, nil, function(index, value)
self.spec:SelectAscendClass(value.ascendClassId)
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
end)
self.controls.secondaryAscendDrop = new("DropDownControl", {"LEFT",self.controls.ascendDrop,"RIGHT"}, {8, 0, 160, 20}, {
{ label = "None", ascendClassId = 0 },
}, function(index, value)
if not value or not self.spec then
return
end
self.spec:SelectSecondaryAscendClass(value.ascendClassId)
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
end)
self.controls.secondaryAscendDrop.enableDroppedWidth = true
self.controls.secondaryAscendDrop.maxDroppedWidth = 360
local initialSecondarySelection = (self.spec and self.spec.curSecondaryAscendClassId) or 0
self.controls.secondaryAscendDrop:SelByValue(initialSecondarySelection, "ascendClassId")
self.controls.buildLoadouts = new("DropDownControl", {"LEFT",self.controls.secondaryAscendDrop,"RIGHT"}, {8, 0, 190, 20}, {}, function(index, value)
if value == "^7^7Loadouts:" or value == "^7^7-----" then
self.controls.buildLoadouts:SetSel(1)
return
end
if value == "^7^7Sync" then
self:SyncLoadouts()
self.controls.buildLoadouts:SetSel(1)
return
end
if value == "^7^7Help >>" then
main:OpenAboutPopup(7)
self.controls.buildLoadouts:SetSel(1)
return
end
if value == "^7^7New Loadout" then
local controls = { }
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:")
controls.edit = new("EditControl", nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf)
controls.save.enabled = buf:match("%S")
end)
controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function()
local loadout = controls.edit.buf
local newSpec = new("PassiveSpec", self, latestTreeVersion)
newSpec.title = loadout
t_insert(self.treeTab.specList, newSpec)
local itemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1)
t_insert(self.itemsTab.itemSetOrderList, itemSet.id)
itemSet.title = loadout
local skillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1)
t_insert(self.skillsTab.skillSetOrderList, skillSet.id)
skillSet.title = loadout
local configSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1)
t_insert(self.configTab.configSetOrderList, configSet.id)
configSet.title = loadout
self:SyncLoadouts()
self.modFlag = true
main:ClosePopup()
end)
controls.save.enabled = false
controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel")
self.controls.buildLoadouts:SetSel(1)
return
end
-- item, skill, and config sets have identical structure
-- return id as soon as it's found
local function findSetId(setOrderList, value, sets, setSpecialLinks)
for _, setOrder in ipairs(setOrderList) do
if value == (sets[setOrder].title or "Default") then
return setOrder
else
local linkMatch = string.match(value, "%{(%w+)%}")
if linkMatch then
return setSpecialLinks[linkMatch]["setId"]
end
end
end
return nil
end
-- trees have a different structure with id/name pairs
-- return id as soon as it's found
local function findNamedSetId(treeList, value, setSpecialLinks)
for id, spec in ipairs(treeList) do
if value == spec then
return id
else
local linkMatch = string.match(value, "%{(%w+)%}")
if linkMatch then
return setSpecialLinks[linkMatch]["setId"]
end
end
end
return nil
end
local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1
local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1
local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1
local newSpecId = findNamedSetId(self.treeTab:GetSpecList(), value, self.treeListSpecialLinks)
local newItemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, value, self.itemsTab.itemSets, self.itemListSpecialLinks)
local newSkillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, value, self.skillsTab.skillSets, self.skillListSpecialLinks)
local newConfigId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, value, self.configTab.configSets, self.configListSpecialLinks)
-- if exact match nor special grouping cannot find setIds, bail
if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then
return
end
if newSpecId ~= self.treeTab.activeSpec then
self.treeTab:SetActiveSpec(newSpecId)
end
if newItemId ~= self.itemsTab.activeItemSetId then
self.itemsTab:SetActiveItemSet(newItemId)
end
if newSkillId ~= self.skillsTab.activeSkillSetId then
self.skillsTab:SetActiveSkillSet(newSkillId)
end
if newConfigId ~= self.configTab.activeConfigSetId then
self.configTab:SetActiveConfigSet(newConfigId)
end
self.controls.buildLoadouts:SelByValue(value)
end)
--self.controls.similarBuilds = new("ButtonControl", {"LEFT",self.controls.buildLoadouts,"RIGHT"}, {8, 0, 100, 20}, "Similar Builds", function()
-- self:OpenSimilarPopup()
--end)
--self.controls.similarBuilds.tooltipFunc = function(tooltip)
-- tooltip:Clear()
-- tooltip:AddLine(16, "Search for builds similar to your current character.")
-- tooltip:AddLine(16, "For best results, make sure to select your main item set, tree, and skills before opening the popup.")
--end
if buildName == "~~temp~~" then
-- Remove temporary build file
os.remove(self.dbFileName)
self.buildName = "Unnamed build"
self.dbFileName = false
self.dbFileSubPath = nil
self.modFlag = true
end
-- List of display stats
self.displayStats, self.minionDisplayStats, self.extraSaveStats = LoadModule("Modules/BuildDisplayStats")
-- Controls: Side bar
self.anchorSideBar = new("Control", nil, {4, 36, 0, 0})
self.controls.modeImport = new("ButtonControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 0, 134, 20}, "Import/Export Build", function()
self.viewMode = "IMPORT"
end)
self.controls.modeImport.locked = function() return self.viewMode == "IMPORT" end
self.controls.modeNotes = new("ButtonControl", {"LEFT",self.controls.modeImport,"RIGHT"}, {4, 0, 58, 20}, "Notes", function()
self.viewMode = "NOTES"
end)
self.controls.modeNotes.locked = function() return self.viewMode == "NOTES" end
self.controls.modeConfig = new("ButtonControl", {"TOPRIGHT",self.anchorSideBar,"TOPLEFT"}, {300, 0, 100, 20}, "Configuration", function()
self.viewMode = "CONFIG"
end)
self.controls.modeConfig.locked = function() return self.viewMode == "CONFIG" end
self.controls.modeTree = new("ButtonControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 26, 72, 20}, "Tree", function()
self.viewMode = "TREE"
end)
self.controls.modeTree.locked = function() return self.viewMode == "TREE" end
self.controls.modeSkills = new("ButtonControl", {"LEFT",self.controls.modeTree,"RIGHT"}, {4, 0, 72, 20}, "Skills", function()
self.viewMode = "SKILLS"
end)
self.controls.modeSkills.locked = function() return self.viewMode == "SKILLS" end
self.controls.modeItems = new("ButtonControl", {"LEFT",self.controls.modeSkills,"RIGHT"}, {4, 0, 72, 20}, "Items", function()
self.viewMode = "ITEMS"
end)
self.controls.modeItems.locked = function() return self.viewMode == "ITEMS" end
self.controls.modeCalcs = new("ButtonControl", {"LEFT",self.controls.modeItems,"RIGHT"}, {4, 0, 72, 20}, "Calcs", function()
self.viewMode = "CALCS"
end)
self.controls.modeCalcs.locked = function() return self.viewMode == "CALCS" end
self.controls.modeParty = new("ButtonControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 52, 72, 20}, "Party", function()
self.viewMode = "PARTY"
end)
self.controls.modeParty.locked = function() return self.viewMode == "PARTY" end
-- Skills
self.controls.mainSkillLabel = new("LabelControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 80, 300, 16}, "^7Main Skill:")
self.controls.mainSocketGroup = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value)
self.mainSocketGroup = index
self.modFlag = true
self.buildFlag = true
end)
self.controls.mainSocketGroup.maxDroppedWidth = 500
self.controls.mainSocketGroup.tooltipFunc = function(tooltip, mode, index, value)
local socketGroup = self.skillsTab.socketGroupList[index]
if socketGroup and tooltip:CheckForUpdate(socketGroup, self.outputRevision) then
self.skillsTab:AddSocketGroupTooltip(tooltip, socketGroup)
end
end
self.controls.mainSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
mainSocketGroup.mainActiveSkill = index
self.modFlag = true
self.buildFlag = true
end)
self.controls.mainSkillPart = new("DropDownControl", {"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT",true}, {0, 2, 300, 18}, nil, function(index, value)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillPart = index
self.modFlag = true
self.buildFlag = true
end)
self.controls.mainSkillStageCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Stages:") {
shown = function()
return self.controls.mainSkillStageCount:IsShown()
end,
}
self.controls.mainSkillStageCount = new("EditControl", {"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillStageCount = tonumber(buf)
self.modFlag = true
self.buildFlag = true
end)
self.controls.mainSkillMineCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillStageCountLabel,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Active Mines:") {
shown = function()
return self.controls.mainSkillMineCount:IsShown()
end,
}
self.controls.mainSkillMineCount = new("EditControl", {"LEFT",self.controls.mainSkillMineCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillMineCount = tonumber(buf)
self.modFlag = true
self.buildFlag = true
end)
self.controls.mainSkillMinion = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 3, 178, 18}, nil, function(index, value)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
if value.itemSetId then
srcInstance.skillMinionItemSet = value.itemSetId
else
srcInstance.skillMinion = value.minionId
end
self.modFlag = true
self.buildFlag = true
end)
function self.controls.mainSkillMinion.CanReceiveDrag(control, type, value)
if type == "Item" and control.list[control.selIndex] and control.list[control.selIndex].itemSetId then
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local minionUses = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.grantedEffect.minionUses
return minionUses and minionUses[value:GetPrimarySlot()] -- O_O
end
end
function self.controls.mainSkillMinion.ReceiveDrag(control, type, value, source)
self.itemsTab:EquipItemInSet(value, control.list[control.selIndex].itemSetId)
end
function self.controls.mainSkillMinion.tooltipFunc(tooltip, mode, index, value)
tooltip:Clear()
if value.itemSetId then
self.itemsTab:AddItemSetTooltip(tooltip, self.itemsTab.itemSets[value.itemSetId])
tooltip:AddSeparator(14)
tooltip:AddLine(14, colorCodes.TIP.."Tip: You can drag items from the Items tab onto this dropdown to equip them onto the minion.")
end
end
self.controls.mainSkillMinionLibrary = new("ButtonControl", {"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 18}, "Manage Spectres...", function()
self:OpenSpectreLibrary()
end)
self.controls.mainSkillMinionSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {0, 2, 200, 16}, nil, function(index, value)
local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillMinionSkill = index
self.modFlag = true
self.buildFlag = true
end)
self.controls.statBoxAnchor = new("Control", {"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 2, 0, 0})
self.controls.statBox = new("TextListControl", {"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=170,align="RIGHT_X"},{x=174,align="LEFT"}})
self.controls.statBox.height = function(control)
local x, y = control:GetPos()
local warnHeight = main.showWarnings and #self.controls.warnings.lines > 0 and 18 or 0
return main.screenH - main.mainBarHeight - 4 - y - warnHeight
end
self.controls.warnings = new("Control",{"TOPLEFT",self.controls.statBox,"BOTTOMLEFT",true}, {0, 0, 0, 18})
self.controls.warnings.lines = {}
self.controls.warnings.width = function(control)
return control.str and DrawStringWidth(16, "FIXED", control.str) + 8 or 0
end
self.controls.warnings.Draw = function(control)
if #self.controls.warnings.lines > 0 then
local count = 0
for _ in pairs(self.controls.warnings.lines) do count = count + 1 end
control.str = string.format(colorCodes.NEGATIVE.."%d Warnings", count)
local x, y = control:GetPos()
local width, height = control:GetSize()
DrawString(x, y + 2, "LEFT", 16, "FIXED", control.str)
if control:IsMouseInBounds() then
SetDrawLayer(nil, 10)
miscTooltip:Clear()
for k,v in pairs(self.controls.warnings.lines) do miscTooltip:AddLine(16, v) end
miscTooltip:Draw(x, y, width, height, main.viewPort)
SetDrawLayer(nil, 0)
end
else
control.str = {}
end
end
-- Initialise build components
self.latestTree = main.tree[latestTreeVersion]
data.setJewelRadiiGlobally(latestTreeVersion)
self.data = data
self.importTab = new("ImportTab", self)
self.notesTab = new("NotesTab", self)
self.partyTab = new("PartyTab", self)
self.configTab = new("ConfigTab", self)
self.itemsTab = new("ItemsTab", self)
self.treeTab = new("TreeTab", self)
self.skillsTab = new("SkillsTab", self)
self.calcsTab = new("CalcsTab", self)
-- Load sections from the build file
self.savers = {
["Config"] = self.configTab,
["Notes"] = self.notesTab,
["Party"] = self.partyTab,
["Tree"] = self.treeTab,
["TreeView"] = self.treeTab.viewer,
["Items"] = self.itemsTab,
["Skills"] = self.skillsTab,
["Calcs"] = self.calcsTab,
["Import"] = self.importTab,
}
self.legacyLoaders = { -- Special loaders for legacy sections
["Spec"] = self.treeTab,
}
--special rebuild to properly initialise boss placeholders
self.configTab:BuildModList()
self:UpdateClassDropdowns()
-- Load legacy bandit and pantheon choices from build section
for _, control in ipairs({ "bandit", "pantheonMajorGod", "pantheonMinorGod" }) do
self.configTab.input[control] = self[control]
end
-- so we ran into problems with converted trees, trying to check passive tree routes and also consider thread jewels
-- but we can't check jewel info because items have not been loaded yet, and they come after passives in the xml.
-- the simplest solution seems to be making sure passive trees (which contain jewel sockets) are loaded last.
local deferredPassiveTrees = { }
for _, node in ipairs(self.xmlSectionList) do
-- Check if there is a saver that can load this section
local saver = self.savers[node.elem] or self.legacyLoaders[node.elem]
if saver then
-- if the saver is treeTab, defer it until everything is loaded
if saver == self.treeTab then
t_insert(deferredPassiveTrees, node)
else
if saver:Load(node, self.dbFileName) then
self:CloseBuild()
return
end
end
end
end
for _, node in ipairs(deferredPassiveTrees) do
-- Check if there is a saver that can load this section
if self.treeTab:Load(node, self.dbFileName) then
self:CloseBuild()
return
end
end
for _, saver in pairs(self.savers) do
if saver.PostLoad then
saver:PostLoad()
end
end
if next(self.configTab.input) == nil then
-- Check for old calcs tab settings
self.configTab:ImportCalcSettings()
end
-- Build calculation output tables
self.outputRevision = 1
self.calcsTab:BuildOutput()
self:RefreshStatList()
self.buildFlag = false
self.spec:SetWindowTitleWithBuildClass()
--[[
local testTooltip = new("Tooltip")
for _, item in pairs(main.uniqueDB.list) do
ConPrintf("%s", item.name)
self.itemsTab:AddItemTooltip(testTooltip, item)
testTooltip:Clear()
end
for _, item in pairs(main.rareDB.list) do
ConPrintf("%s", item.name)
self.itemsTab:AddItemTooltip(testTooltip, item)
testTooltip:Clear()
end
--]]
--[[
local start = GetTime()
SetProfiling(true)
for i = 1, 10 do
self.calcsTab:PowerBuilder()
end
SetProfiling(false)
ConPrintf("Power build time: %d ms", GetTime() - start)
--]]
self.abortSave = false
self:SyncLoadouts()
end
local acts = {
-- https://www.poewiki.net/wiki/Passive_skill
[1] = { level = 1, questPoints = 0 },
-- Act 1 : The Dweller of the Deep
-- Act 1 : The Marooned Mariner
[2] = { level = 12, questPoints = 2 },
-- Act 1,2 : The Way Forward (Reward after reaching Act 2)
-- Act 2 : Through Sacred Ground (Fellshrine Reward 3.25)
[3] = { level = 22, questPoints = 4 },
-- Act 3 : Victario's Secrets
-- Act 3 : Piety's Pets
[4] = { level = 32, questPoints = 6 },
-- Act 4 : An Indomitable Spirit
[5] = { level = 40, questPoints = 7 },
-- Act 5 : In Service to Science
-- Act 5 : Kitava's Torments
[6] = { level = 44, questPoints = 9 },
-- Act 6 : The Father of War
-- Act 6 : The Puppet Mistress
-- Act 6 : The Cloven One
[7] = { level = 50, questPoints = 12 },
-- Act 7 : The Master of a Million Faces
-- Act 7 : Queen of Despair
-- Act 7 : Kishara's Star
[8] = { level = 54, questPoints = 15 },
-- Act 8 : Love is Dead
-- Act 8 : Reflection of Terror
-- Act 8 : The Gemling Legion
[9] = { level = 60, questPoints = 18 },
-- Act 9 : Queen of the Sands
-- Act 9 : The Ruler of Highgate
[10] = { level = 64, questPoints = 20 },
-- Act 10 : Vilenta's Vengeance
-- Act 10 : An End to Hunger (+2)
[11] = { level = 67, questPoints = 23 },
}
local function actExtra(act, extra)
-- Act 2 : Deal With The Bandits (+1 if the player kills all bandits)
return act > 2 and extra or 0
end
function buildMode:SyncLoadouts()
self.controls.buildLoadouts.list = {"No Loadouts"}
local filteredList = {"^7^7Loadouts:"}
local treeList = {}
local itemList = {}
local skillList = {}
local configList = {}
-- used when clicking on the dropdown to set the correct setId for each SetActiveSet()
self.treeListSpecialLinks, self.itemListSpecialLinks, self.skillListSpecialLinks, self.configListSpecialLinks = {}, {}, {}, {}
local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1
local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1
local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1
if self.treeTab ~= nil and self.itemsTab ~= nil and self.skillsTab ~= nil and self.configTab ~= nil then
local transferTable = {}
local sortedTreeListSpecialLinks = {}
for id, spec in ipairs(self.treeTab.specList) do
local specTitle = spec.title or "Default"
-- only alphanumeric and comma are allowed in the braces { }
local linkIdentifier = string.match(specTitle, "%{([%w,]+)%}")
if linkIdentifier then
local setName = specTitle:gsub("%{" .. linkIdentifier .. "%}", ""):gsub("^%s*", ""):gsub("%s*$", "")
if not setName or setName == "" then
setName = "Default"
end
-- iterate over each identifier, delimited by comma, and set the index so we can grab it later
-- setId index is the id of the set in the global list needed for SetActiveSet
-- setName is only used for Tree currently and we strip the braces to get the plain name of the set, this is used as the name of the loadout
for linkId in string.gmatch(linkIdentifier, "[^%,]+") do
transferTable["setId"] = id
transferTable["setName"] = setName
transferTable["linkId"] = linkId
self.treeListSpecialLinks[linkId] = transferTable
t_insert(sortedTreeListSpecialLinks, transferTable)
transferTable = {}
end
else
t_insert(treeList, (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(specTitle))
end
end
-- item, skill, and config sets have identical structure
local function identifyLinks(setOrderList, tabSets, setList, specialLinks, treeLinks)
for id, set in ipairs(setOrderList) do
local setTitle = tabSets[set].title or "Default"
local linkIdentifier = string.match(setTitle, "%{([%w,]+)%}")
-- this if/else prioritizes group identifier in case the user creates sets with same name AND same identifiers
-- result is only the group is recognized and one loadout is created rather than a duplicate from each condition met
if linkIdentifier then
local setName = setTitle:gsub("%{" .. linkIdentifier .. "%}", ""):gsub("^%s*", ""):gsub("%s*$", "")
if not setName or setName == "" then
setName = "Default"
end
for linkId in string.gmatch(linkIdentifier, "[^%,]+") do
transferTable["setId"] = set
transferTable["setName"] = setName
specialLinks[linkId] = transferTable
transferTable = {}
end
else
setList[setTitle] = true
end
end
end
identifyLinks(self.itemsTab.itemSetOrderList, self.itemsTab.itemSets, itemList, self.itemListSpecialLinks, self.treeListSpecialLinks)
identifyLinks(self.skillsTab.skillSetOrderList, self.skillsTab.skillSets, skillList, self.skillListSpecialLinks, self.treeListSpecialLinks)
identifyLinks(self.configTab.configSetOrderList, self.configTab.configSets, configList, self.configListSpecialLinks, self.treeListSpecialLinks)
-- loop over all for exact match loadouts
for id, tree in ipairs(treeList) do
if (oneItem or itemList[tree]) and (oneSkill or skillList[tree]) and (oneConfig or configList[tree]) then
t_insert(filteredList, tree)
end
end
-- loop over the identifiers found within braces and set the loadout name to the TreeSet
for _, tree in ipairs(sortedTreeListSpecialLinks) do
local treeLinkId = tree.linkId
if ((oneItem or self.itemListSpecialLinks[treeLinkId]) and (oneSkill or self.skillListSpecialLinks[treeLinkId]) and (oneConfig or self.configListSpecialLinks[treeLinkId])) then
t_insert(filteredList, tree.setName .." {"..treeLinkId.."}")
end
end
end
-- giving the options unique formatting so it can not match with user-created sets
t_insert(filteredList, "^7^7-----")
t_insert(filteredList, "^7^7New Loadout")
t_insert(filteredList, "^7^7Sync")
t_insert(filteredList, "^7^7Help >>")
if #filteredList > 0 then
self.controls.buildLoadouts.list = filteredList
end
-- Try to select loadout in dropdown based on currently selected tree
if self.treeTab then
local treeName = self.treeTab.specList[self.treeTab.activeSpec].title or "Default"
for i, loadout in ipairs(filteredList) do
if loadout == treeName then
local linkMatch = string.match(treeName, "%{(%w+)%}") or treeName
if linkMatch then
local skillName = self.skillsTab.skillSets[self.skillsTab.activeSkillSetId].title or "Default"
local skillMatch = oneSkill or skillName:find(linkMatch, 1, true)
local itemName = self.itemsTab.itemSets[self.itemsTab.activeItemSetId].title or "Default"
local itemMatch = oneItem or itemName:find(linkMatch, 1, true)
local configName = self.configTab.configSets[self.configTab.activeConfigSetId].title or "Default"
local configMatch = oneConfig or configName:find(linkMatch, 1, true)
if skillMatch and itemMatch and configMatch then
self.controls.buildLoadouts:SetSel(i)
return treeList, itemList, skillList, configList
end
end
break
end
end
end
self.controls.buildLoadouts:SetSel(1)
return treeList, itemList, skillList, configList
end
function buildMode:EstimatePlayerProgress()
local PointsUsed, AscUsed, SecondaryAscUsed = self.spec:CountAllocNodes()
local extra = self.calcsTab.mainOutput and self.calcsTab.mainOutput.ExtraPoints or 0
local usedMax, ascMax, secondaryAscMax, level, act = 99 + 23 + extra, 8, 8, 1, 0
-- Find estimated act and level based on points used
repeat
act = act + 1
level = m_min(m_max(PointsUsed + 1 - acts[act].questPoints - actExtra(act, extra), acts[act].level), 100)
until act == 11 or level <= acts[act + 1].level
if self.characterLevelAutoMode and self.characterLevel ~= level then
self.characterLevel = level
self.controls.characterLevel:SetText(self.characterLevel)
self.configTab:BuildModList()
end
-- Ascendancy points for lab
-- this is a recommendation for beginners who are using Path of Building for the first time and trying to map out progress in PoB
local labSuggest = level < 33 and ""
or level < 55 and "\nLabyrinth: Normal Lab"
or level < 68 and "\nLabyrinth: Cruel Lab"
or level < 75 and "\nLabyrinth: Merciless Lab"
or level < 90 and "\nLabyrinth: Uber Lab"
or ""
if PointsUsed > usedMax then InsertIfNew(self.controls.warnings.lines, "You have too many passive points allocated") end
if AscUsed > ascMax then InsertIfNew(self.controls.warnings.lines, "You have too many ascendancy points allocated") end
if SecondaryAscUsed > secondaryAscMax then InsertIfNew(self.controls.warnings.lines, "You have too many secondary ascendancy points allocated") end
self.Act = level < 90 and act <= 10 and act or "Endgame"
return string.format("%s%3d / %3d %s%d / %d", PointsUsed > usedMax and colorCodes.NEGATIVE or "^7", PointsUsed, usedMax, AscUsed > ascMax and colorCodes.NEGATIVE or "^7", AscUsed, ascMax),
"Required Level: "..level.."\nEstimated Progress:\nAct: "..self.Act.."\nQuestpoints: "..acts[act].questPoints.."\nExtra Skillpoints: "..actExtra(act, extra)..labSuggest
end
function buildMode:CanExit(mode)
if not self.unsaved then
return true
end
self:OpenSavePopup(mode)
return false
end
function buildMode:Shutdown()
if launch.devMode and (not main.disableDevAutoSave) and self.targetVersion and not self.abortSave then
if self.dbFileName then
self:SaveDBFile()
elseif self.unsaved then
self.dbFileName = main.buildPath.."~~temp~~.xml"
self.buildName = "~~temp~~"
self.dbFileSubPath = ""
self:SaveDBFile()
end
end
self.abortSave = nil
self.savers = nil
end
function buildMode:GetArgs()
return self.dbFileName, self.buildName
end
function buildMode:CloseBuild()
main:SetWindowTitleSubtext()
main:SetMode("LIST", self.dbFileName and self.buildName, self.dbFileSubPath)
end
function buildMode:Load(xml, fileName)
self.targetVersion = xml.attrib.targetVersion or legacyTargetVersion
if xml.attrib.viewMode then
self.viewMode = xml.attrib.viewMode
end
self.characterLevel = tonumber(xml.attrib.level) or 1
self.characterLevelAutoMode = xml.attrib.characterLevelAutoMode == "true"
for _, diff in pairs({ "bandit", "pantheonMajorGod", "pantheonMinorGod" }) do
self[diff] = xml.attrib[diff] or "None"
end
self.mainSocketGroup = tonumber(xml.attrib.mainSkillIndex) or tonumber(xml.attrib.mainSocketGroup) or 1
wipeTable(self.spectreList)
for _, child in ipairs(xml) do
if child.elem == "Spectre" then
if child.attrib.id and data.minions[child.attrib.id] then
t_insert(self.spectreList, child.attrib.id)
end
elseif child.elem == "TimelessData" then
self.timelessData.jewelType = {
id = tonumber(child.attrib.jewelTypeId)
}
self.timelessData.conquerorType = {
id = tonumber(child.attrib.conquerorTypeId)
}
self.timelessData.devotionVariant1 = tonumber(child.attrib.devotionVariant1) or 1
self.timelessData.devotionVariant2 = tonumber(child.attrib.devotionVariant2) or 1
self.timelessData.jewelSocket = {
id = tonumber(child.attrib.jewelSocketId)
}
self.timelessData.fallbackWeightMode = {
idx = tonumber(child.attrib.fallbackWeightModeIdx)
}
self.timelessData.socketFilter = child.attrib.socketFilter == "true"
self.timelessData.socketFilterDistance = tonumber(child.attrib.socketFilterDistance) or 0
self.timelessData.searchList = child.attrib.searchList
self.timelessData.searchListFallback = child.attrib.searchListFallback
end
end
end
function buildMode:Save(xml)
xml.attrib = {
targetVersion = self.targetVersion,
viewMode = self.viewMode,
level = tostring(self.characterLevel),
className = self.spec.curClassName,
ascendClassName = self.spec.curAscendClassName,
bandit = self.configTab.input.bandit,
pantheonMajorGod = self.configTab.input.pantheonMajorGod,
pantheonMinorGod = self.configTab.input.pantheonMinorGod,
mainSocketGroup = tostring(self.mainSocketGroup),
characterLevelAutoMode = tostring(self.characterLevelAutoMode)
}
for _, id in ipairs(self.spectreList) do
t_insert(xml, { elem = "Spectre", attrib = { id = id } })
end
local addedStatNames = { }
for index, statData in ipairs(self.displayStats) do
if matchFlags(statData.flag, statData.notFlag, self.calcsTab.mainEnv.player.mainSkill.skillFlags) then
local statName = statData.stat and statData.stat..(statData.childStat or "")
if statName and not addedStatNames[statName] then
if statData.stat == "SkillDPS" then
local statVal = self.calcsTab.mainOutput[statData.stat]
for _, skillData in ipairs(statVal) do
local triggerStr = ""
if skillData.trigger and skillData.trigger ~= "" then
triggerStr = skillData.trigger
end
local lhsString = skillData.name
if skillData.count >= 2 then
lhsString = tostring(skillData.count).."x "..skillData.name
end
t_insert(xml, { elem = "FullDPSSkill", attrib = { stat = lhsString, value = tostring(skillData.dps * skillData.count), skillPart = skillData.skillPart or "", source = skillData.source or skillData.trigger or "" } })
end
addedStatNames[statName] = true
else
local statVal = self.calcsTab.mainOutput[statData.stat]
if statVal and statData.childStat then
statVal = statVal[statData.childStat]
end
if statVal and (statData.condFunc and statData.condFunc(statVal, self.calcsTab.mainOutput) or true) then
t_insert(xml, { elem = "PlayerStat", attrib = { stat = statName, value = tostring(statVal) } })
addedStatNames[statName] = true
end
end
end
end
end
for index, stat in ipairs(self.extraSaveStats) do
local statVal = self.calcsTab.mainOutput[stat]
if statVal then
t_insert(xml, { elem = "PlayerStat", attrib = { stat = stat, value = tostring(statVal) } })
end
end
if self.calcsTab.mainEnv.minion then
for index, statData in ipairs(self.minionDisplayStats) do
if statData.stat then
local statVal = self.calcsTab.mainOutput.Minion[statData.stat]
if statVal then
t_insert(xml, { elem = "MinionStat", attrib = { stat = statData.stat, value = tostring(statVal) } })
end
end
end
end
local timelessData = {
elem = "TimelessData",
attrib = {
jewelTypeId = next(self.timelessData.jewelType) and tostring(self.timelessData.jewelType.id),
conquerorTypeId = next(self.timelessData.conquerorType) and tostring(self.timelessData.conquerorType.id),
devotionVariant1 = tostring(self.timelessData.devotionVariant1),
devotionVariant2 = tostring(self.timelessData.devotionVariant2),
jewelSocketId = next(self.timelessData.jewelSocket) and tostring(self.timelessData.jewelSocket.id),
fallbackWeightModeIdx = next(self.timelessData.fallbackWeightMode) and tostring(self.timelessData.fallbackWeightMode.idx),
socketFilter = self.timelessData.socketFilter and "true",
socketFilterDistance = self.timelessData.socketFilterDistance and tostring(self.timelessData.socketFilterDistance),
searchList = self.timelessData.searchList and tostring(self.timelessData.searchList),
searchListFallback = self.timelessData.searchListFallback and tostring(self.timelessData.searchListFallback)
}
}
t_insert(xml, timelessData)
end
function buildMode:ResetModFlags()
self.modFlag = false
self.notesTab.modFlag = false
self.partyTab.modFlag = false
self.configTab.modFlag = false
self.treeTab.modFlag = false
self.treeTab.searchFlag = false
self.spec.modFlag = false
self.skillsTab.modFlag = false
self.itemsTab.modFlag = false
self.calcsTab.modFlag = false
end
function buildMode:OnFrame(inputEvents)
-- Stop at drawing the background if the loaded build needs to be converted
if not self.targetVersion then
main:DrawBackground(main.viewPort)
return
end
if self.abortSave and not launch.devMode then
self:CloseBuild()
end
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "MOUSE4" then
if self.unsaved then
self:OpenSavePopup("LIST")
else
self:CloseBuild()
end
elseif IsKeyDown("CTRL") then
if event.key == "i" then
self.viewMode = "IMPORT"
self.importTab:SelectControl(self.importTab.controls.importCodeIn)
elseif event.key == "s" then
self:SaveDBFile()
inputEvents[id] = nil
elseif event.key == "w" then
if self.unsaved then
self:OpenSavePopup("LIST")
else
self:CloseBuild()
end
elseif event.key == "1" then
self.viewMode = "TREE"
elseif event.key == "2" then
self.viewMode = "SKILLS"
elseif event.key == "3" then
self.viewMode = "ITEMS"
elseif event.key == "4" then
self.viewMode = "CALCS"
elseif event.key == "5" then
self.viewMode = "CONFIG"
elseif event.key == "6" then
self.viewMode = "NOTES"
elseif event.key == "7" then
self.viewMode = "PARTY"
end
end
end
end
self:ProcessControlsInput(inputEvents, main.viewPort)
self.controls.classDrop:SelByValue(self.spec.curClassId, "classId")
self.controls.ascendDrop.list = self.controls.classDrop:GetSelValueByKey("ascendancies")
self.controls.ascendDrop:SelByValue(self.spec.curAscendClassId, "ascendClassId")
self.controls.ascendDrop:CheckDroppedWidth(true)
local secondaryDrop = self.controls.secondaryAscendDrop
if secondaryDrop then
local legacyAlternateAscendancyIds = {
Warden = true,
Warlock = true,
Primalist = true,
}
local entries = {
{ label = "None", ascendClassId = 0 },
}
local selection = (self.spec and self.spec.curSecondaryAscendClassId) or 0
if self.spec and self.spec.tree then
local altAscendancies = self.spec.tree.alternate_ascendancies
if altAscendancies then
local sortable = { }
for ascendClassId, ascendClass in pairs(altAscendancies) do
if ascendClass and ascendClass.id then
if not legacyAlternateAscendancyIds[ascendClass.id] or ascendClassId == selection then
t_insert(sortable, { label = ascendClass.name, ascendClassId = ascendClassId })
end
end
end
t_sort(sortable, function(a, b)
return a.label < b.label
end)
for _, entry in ipairs(sortable) do
t_insert(entries, entry)
end
end
end
secondaryDrop:SetList(entries)
secondaryDrop:SelByValue(selection, "ascendClassId")
secondaryDrop:CheckDroppedWidth(true)
secondaryDrop.enabled = self.spec ~= nil and #entries > 1
end
if self.buildFlag then
-- Wipe Global Cache
wipeGlobalCache()
-- Rebuild calculation output tables
self.outputRevision = self.outputRevision + 1
self.buildFlag = false
self.calcsTab:BuildOutput()
self:RefreshStatList()
end
if main.showThousandsSeparators ~= self.lastShowThousandsSeparators then
self:RefreshStatList()
end
if main.thousandsSeparator ~= self.lastShowThousandsSeparator then
self:RefreshStatList()
end
if main.decimalSeparator ~= self.lastShowDecimalSeparator then
self:RefreshStatList()
end
if main.showTitlebarName ~= self.lastShowTitlebarName then
self.spec:SetWindowTitleWithBuildClass()
end
-- Update contents of main skill dropdowns
self:RefreshSkillSelectControls(self.controls, self.mainSocketGroup, "")
-- Draw contents of current tab
local sideBarWidth = 312
local tabViewPort = {
x = sideBarWidth,
y = 32,
width = main.screenW - sideBarWidth,
height = main.screenH - 32
}
if self.viewMode == "IMPORT" then
self.importTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "NOTES" then
self.notesTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "PARTY" then
self.partyTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "CONFIG" then
self.configTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "TREE" then
self.treeTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "SKILLS" then
self.skillsTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "ITEMS" then
self.itemsTab:Draw(tabViewPort, inputEvents)
elseif self.viewMode == "CALCS" then
self.calcsTab:Draw(tabViewPort, inputEvents)
end
self.unsaved = self.modFlag or self.notesTab.modFlag or self.partyTab.modFlag or self.configTab.modFlag or self.treeTab.modFlag or self.treeTab.searchFlag or self.spec.modFlag or self.skillsTab.modFlag or self.itemsTab.modFlag or self.calcsTab.modFlag
SetDrawLayer(5)
-- Draw top bar background
SetDrawColor(0.2, 0.2, 0.2)
DrawImage(nil, 0, 0, main.screenW, 28)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, 0, 28, main.screenW, 4)
DrawImage(nil, main.screenW/2 - 2, 0, 4, 28)
-- Draw side bar background
SetDrawColor(0.1, 0.1, 0.1)
DrawImage(nil, 0, 32, sideBarWidth - 4, main.screenH - 32)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, sideBarWidth - 4, 32, 4, main.screenH - 32)
self:DrawControls(main.viewPort)
end
-- Opens the game version conversion popup
function buildMode:OpenConversionPopup()
local controls = { }
local currentVersion = treeVersions[latestTreeVersion].display
controls.note = new("LabelControl", nil, {0, 20, 0, 16}, colorCodes.TIP..[[
Info:^7 You are trying to load a build created for a version of Path of Exile that is
not supported by us. You will have to convert it to the current game version to load it.
To use a build newer than the current supported game version, you may have to update.
To use a build older than the current supported game version, we recommend loading it
in an older version of Path of Building Community instead.
]])
controls.label = new("LabelControl", nil, {0, 110, 0, 16}, colorCodes.WARNING..[[
Warning:^7 Converting a build to a different game version may have side effects.
For example, if the passive tree has changed, then some passives may be deallocated.
You should create a backup copy of the build before proceeding.
]])
controls.convert = new("ButtonControl", nil, {-40, 170, 120, 20}, "Convert to ".. currentVersion, function()
main:ClosePopup()
self:Shutdown()
self:Init(self.dbFileName, self.buildName, nil, true)
end)
controls.cancel = new("ButtonControl", nil, {60, 170, 70, 20}, "Cancel", function()
main:ClosePopup()
self:CloseBuild()
end)
main:OpenPopup(580, 200, "Game Version", controls, "convert", nil, "cancel")
end
function buildMode:OpenSavePopup(mode)
local modeDesc = {
["LIST"] = "now?",
["EXIT"] = "before exiting?",
["UPDATE"] = "before updating?",
}
local controls = { }
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7This build has unsaved changes.\nDo you want to save them "..modeDesc[mode])
controls.save = new("ButtonControl", nil, {-90, 70, 80, 20}, "Save", function()
main:ClosePopup()
self.actionOnSave = mode
self:SaveDBFile()
end)
controls.noSave = new("ButtonControl", nil, {0, 70, 80, 20}, "Don't Save", function()
main:ClosePopup()
if mode == "LIST" then
self:CloseBuild()
elseif mode == "EXIT" then
Exit()
elseif mode == "UPDATE" then
launch:ApplyUpdate(launch.updateAvailable)
end
end)
controls.close = new("ButtonControl", nil, {90, 70, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(300, 100, "Save Changes", controls)
end
function buildMode:OpenSaveAsPopup()
local newFileName, newBuildName
local controls = { }
local function updateBuildName()
local buf = controls.edit.buf
newFileName = main.buildPath..controls.folder.subPath..buf..".xml"
newBuildName = buf
controls.save.enabled = false
if buf:match("%S") then
local out = io.open(newFileName, "r")
if out then
out:close()
else
controls.save.enabled = true
end
end
end
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter new build name:")
controls.edit = new("EditControl", nil, {0, 40, 450, 20},
not self.dbFileName and main.predefinedBuildName or (self.buildName or self.dbFileName):gsub("[\\/:%*%?\"<>|%c]", "-"), nil, "\\/:%*%?\"<>|%c", 100, function(buf)
updateBuildName()
end)
controls.folderLabel = new("LabelControl", {"TOPLEFT",nil,"TOPLEFT"}, {10, 70, 0, 16}, "^7Folder:")
controls.newFolder = new("ButtonControl", {"TOPLEFT",nil,"TOPLEFT"}, {100, 67, 94, 20}, "New Folder...", function()
main:OpenNewFolderPopup(main.buildPath..controls.folder.subPath, function(newFolderName)
if newFolderName then
controls.folder:OpenFolder(newFolderName)
end
end)
end)
controls.folder = new("FolderListControl", nil, {0, 115, 450, 100}, self.dbFileSubPath, function(subPath)
updateBuildName()
end)
controls.save = new("ButtonControl", nil, {-45, 225, 80, 20}, "Save", function()
main:ClosePopup()
self.dbFileName = newFileName
self.buildName = newBuildName
self.dbFileSubPath = controls.folder.subPath
self:SaveDBFile()
self.spec:SetWindowTitleWithBuildClass()
end)
controls.close = new("ButtonControl", nil, {45, 225, 80, 20}, "Cancel", function()
main:ClosePopup()
self.actionOnSave = nil
end)
if self.dbFileName or self.buildName then
controls.save.enabled = self.dbFileName or self.buildName
updateBuildName()
else
controls.save.enabled = false
end
main:OpenPopup(470, 255, self.dbFileName and "Save As" or "Save", controls, "save", "edit", "close")
end
-- Open the spectre library popup
function buildMode:OpenSpectreLibrary()
local destList = copyTable(self.spectreList)
local sourceList = { }
for id in pairs(self.data.spectres) do
t_insert(sourceList, id)
end
table.sort(sourceList, function(a,b)
if self.data.minions[a].name == self.data.minions[b].name then
return a < b
else
return self.data.minions[a].name < self.data.minions[b].name
end
end)
local controls = { }
controls.list = new("MinionListControl", nil, {-100, 40, 190, 250}, self.data, destList)
controls.source = new("MinionSearchListControl", nil, {100, 60, 190, 230}, self.data, sourceList, controls.list)
controls.save = new("ButtonControl", nil, {-45, 330, 80, 20}, "Save", function()
self.spectreList = destList
self.modFlag = true
self.buildFlag = true
main:ClosePopup()
end)
controls.cancel = new("ButtonControl", nil, {45, 330, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
controls.noteLine1 = new("LabelControl", {"TOPLEFT",controls.list,"BOTTOMLEFT"}, {24, 2, 0, 16}, "^7Spectres in your Library must be assigned to an active")
controls.noteLine2 = new("LabelControl", {"TOPLEFT",controls.list,"BOTTOMLEFT"}, {20, 18, 0, 16}, "^7Raise Spectre gem for their buffs and curses to activate")
local spectrePopup = main:OpenPopup(410, 360, "Spectre Library", controls)
spectrePopup:SelectControl(spectrePopup.controls.source.controls.searchText)
end
function buildMode:UpdateClassDropdowns(treeVersion)
local classes = main.tree[treeVersion or latestTreeVersion].classes
wipeTable(self.controls.classDrop.list)
-- Initialise class dropdown
for classId, class in pairs(classes) do
local ascendancies = {}
-- Initialise ascendancy dropdown
for i = 0, #class.classes do
local ascendClass = class.classes[i]
t_insert(ascendancies, {
label = ascendClass.name,
ascendClassId = i,
})
end
t_insert(self.controls.classDrop.list, {
label = class.name,
classId = classId,
ascendancies = ascendancies,
})
end
table.sort(self.controls.classDrop.list, function(a, b) return a.label < b.label end)
end
function buildMode:OpenSimilarPopup()
local controls = { }
-- local width, height = self:GetSize()
local buildProviders = {
{
name = "PoB Archives",
impl = new("PoBArchivesProvider", "similar")
}
}
local width = 600
local height = function()
return main.screenH * 0.8
end
local padding = 50
controls.similarBuildList = new("ExtBuildListControl", nil, {0, padding, width, height() - 2 * padding}, buildProviders)
controls.similarBuildList.shown = true
controls.similarBuildList.height = function()
return height() - 2 * padding
end
controls.similarBuildList.width = function ()
return width - padding
end
controls.similarBuildList:SetImportCode(common.base64.encode(Deflate(self:SaveDB("code"))):gsub("+","-"):gsub("/","_"))
controls.similarBuildList:Init("PoB Archives")
-- controls.similarBuildList.shown = not controls.similarBuildList:IsShown()
controls.close = new("ButtonControl", nil, {0, height() - (padding + 20) / 2, 80, 20}, "Close", function()
main:ClosePopup()
end)
-- used in PopupDialog to dynamically size the popup
local function resizeFunc()
main.popups[1].height = height()
main.popups[1].y = function()
return m_floor((main.screenH - height()) / 2)
end
controls.close.y = height() - 35
end
main:OpenPopup(width, height(), "Similar Builds", controls, nil, nil, nil, nil, resizeFunc)
end
-- Refresh the set of controls used to select main group/skill/minion
function buildMode:RefreshSkillSelectControls(controls, mainGroup, suffix)
controls.mainSocketGroup.selIndex = mainGroup
wipeTable(controls.mainSocketGroup.list)
for i, socketGroup in pairs(self.skillsTab.socketGroupList) do
controls.mainSocketGroup.list[i] = { val = i, label = socketGroup.displayLabel }
end
controls.mainSocketGroup:CheckDroppedWidth(true)
if controls.warnings then controls.warnings.shown = #controls.warnings.lines > 0 end
if #controls.mainSocketGroup.list == 0 then
controls.mainSocketGroup.list[1] = { val = 1, label = "<No skills added yet>" }
controls.mainSkill.shown = false
controls.mainSkillPart.shown = false
controls.mainSkillMineCount.shown = false
controls.mainSkillStageCount.shown = false
controls.mainSkillMinion.shown = false
controls.mainSkillMinionSkill.shown = false
else
local mainSocketGroup = self.skillsTab.socketGroupList[mainGroup]
local displaySkillList = mainSocketGroup["displaySkillList"..suffix]
local mainActiveSkill = mainSocketGroup["mainActiveSkill"..suffix] or 1
wipeTable(controls.mainSkill.list)
for i, activeSkill in ipairs(displaySkillList) do
local explodeSource = activeSkill.activeEffect.srcInstance.explodeSource
local explodeSourceName = explodeSource and (explodeSource.name or explodeSource.dn)
local colourCoded = explodeSourceName and ("From "..colorCodes[explodeSource.rarity or "NORMAL"]..explodeSourceName)
t_insert(controls.mainSkill.list, { val = i, label = colourCoded or activeSkill.activeEffect.grantedEffect.name })
end
controls.mainSkill.enabled = #displaySkillList > 1
controls.mainSkill.selIndex = mainActiveSkill
controls.mainSkill.shown = true
controls.mainSkillPart.shown = false
controls.mainSkillMineCount.shown = false
controls.mainSkillStageCount.shown = false
controls.mainSkillMinion.shown = false
controls.mainSkillMinionLibrary.shown = false
controls.mainSkillMinionSkill.shown = false
if displaySkillList[1] then
local activeSkill = displaySkillList[mainActiveSkill]
local activeEffect = activeSkill.activeEffect
if activeEffect then
if activeEffect.grantedEffect.parts and #activeEffect.grantedEffect.parts > 1 then
controls.mainSkillPart.shown = true
wipeTable(controls.mainSkillPart.list)
for i, part in ipairs(activeEffect.grantedEffect.parts) do
t_insert(controls.mainSkillPart.list, { val = i, label = part.name })
end
controls.mainSkillPart.selIndex = activeEffect.srcInstance["skillPart"..suffix] or 1
if activeEffect.grantedEffect.parts[controls.mainSkillPart.selIndex].stages then
controls.mainSkillStageCount.shown = true
controls.mainSkillStageCount.buf = tostring(activeEffect.srcInstance["skillStageCount"..suffix] or activeEffect.grantedEffect.parts[controls.mainSkillPart.selIndex].stagesMin or 1)
end
end
if activeSkill.skillFlags.mine then
controls.mainSkillMineCount.shown = true
controls.mainSkillMineCount.buf = tostring(activeEffect.srcInstance["skillMineCount"..suffix] or "")
end
if activeSkill.skillFlags.multiStage and not (activeEffect.grantedEffect.parts and #activeEffect.grantedEffect.parts > 1) then
controls.mainSkillStageCount.shown = true
controls.mainSkillStageCount.buf = tostring(activeEffect.srcInstance["skillStageCount"..suffix] or activeSkill.skillData.stagesMin or 1)
end
if not activeSkill.skillFlags.disable and (activeEffect.grantedEffect.minionList or activeSkill.minionList[1]) then
wipeTable(controls.mainSkillMinion.list)
if activeEffect.grantedEffect.minionHasItemSet then
for _, itemSetId in ipairs(self.itemsTab.itemSetOrderList) do
local itemSet = self.itemsTab.itemSets[itemSetId]
t_insert(controls.mainSkillMinion.list, {
label = itemSet.title or "Default Item Set",
itemSetId = itemSetId,
})
end
controls.mainSkillMinion:SelByValue(activeEffect.srcInstance["skillMinionItemSet"..suffix] or 1, "itemSetId")
else
controls.mainSkillMinionLibrary.shown = (activeEffect.grantedEffect.minionList and not activeEffect.grantedEffect.minionList[1])
for _, minionId in ipairs(activeSkill.minionList) do
t_insert(controls.mainSkillMinion.list, {
label = self.data.minions[minionId].name,
minionId = minionId,
})
end
controls.mainSkillMinion:SelByValue(activeEffect.srcInstance["skillMinion"..suffix] or controls.mainSkillMinion.list[1], "minionId")
end
controls.mainSkillMinion.enabled = #controls.mainSkillMinion.list > 1
controls.mainSkillMinion.shown = true
wipeTable(controls.mainSkillMinionSkill.list)
if activeSkill.minion then
for _, minionSkill in ipairs(activeSkill.minion.activeSkillList) do
t_insert(controls.mainSkillMinionSkill.list, minionSkill.activeEffect.grantedEffect.name)
end
controls.mainSkillMinionSkill.selIndex = activeEffect.srcInstance["skillMinionSkill"..suffix] or 1
controls.mainSkillMinionSkill.shown = true
controls.mainSkillMinionSkill.enabled = #controls.mainSkillMinionSkill.list > 1
else
t_insert(controls.mainSkillMinion.list, "<No spectres in build>")
end
end
end
end
end
end
function buildMode:FormatStat(statData, statVal, overCapStatVal, colorOverride)
if type(statVal) == "table" then return "" end
local val = statVal * ((statData.pc or statData.mod) and 100 or 1) - (statData.mod and 100 or 0)
local color = colorOverride or (statVal >= 0 and "^7" or statData.chaosInoc and "^8" or colorCodes.NEGATIVE)
if statData.label == "Unreserved Life" and statVal == 0 then
color = colorCodes.NEGATIVE
end
local valStr = s_format("%"..statData.fmt, val)
local number, suffix = valStr:match("^([%+%-]?%d+%.%d+)(%D*)$")
if number then
valStr = number:gsub("0+$", ""):gsub("%.$", "") .. suffix
end
valStr:gsub("%.", main.decimalSeparator)
valStr = color .. formatNumSep(valStr)
if overCapStatVal and overCapStatVal > 0 then
valStr = valStr .. "^x808080" .. " (+" .. s_format("%d", overCapStatVal) .. "%)"
end
self.lastShowThousandsSeparators = main.showThousandsSeparators
self.lastShowThousandsSeparator = main.thousandsSeparator
self.lastShowDecimalSeparator = main.decimalSeparator
self.lastShowTitlebarName = main.showTitlebarName
return valStr
end
-- Add stat list for given actor
function buildMode:AddDisplayStatList(statList, actor)
local statBoxList = self.controls.statBox.list
for index, statData in ipairs(statList) do
if matchFlags(statData.flag, statData.notFlag, actor.mainSkill.skillFlags) then
local labelColor = "^7"
if statData.color then
labelColor = statData.color
end
if statData.stat then
local statVal = actor.output[statData.stat]
-- access output values that are one node deeper (statData.stat is a table e.g. output.MainHand.Accuracy vs output.Life)
if statVal and statData.childStat then
statVal = statVal[statData.childStat]
end
if statVal and ((statData.condFunc and statData.condFunc(statVal,actor.output)) or (not statData.condFunc and statVal ~= 0)) then
local overCapStatVal = actor.output[statData.overCapStat] or nil
if statData.stat == "SkillDPS" then
labelColor = colorCodes.CUSTOM
table.sort(actor.output.SkillDPS, function(a,b) return (a.dps * a.count) > (b.dps * b.count) end)
for _, skillData in ipairs(actor.output.SkillDPS) do
local triggerStr = ""
if skillData.trigger and skillData.trigger ~= "" then
triggerStr = colorCodes.WARNING.." ("..skillData.trigger..")"..labelColor
end
local lhsString = labelColor..skillData.name..triggerStr..":"
if skillData.count >= 2 then
lhsString = labelColor..tostring(skillData.count).."x "..skillData.name..triggerStr..":"
end
t_insert(statBoxList, {
height = 16,
lhsString,
self:FormatStat({fmt = "1.f"}, skillData.dps * skillData.count, overCapStatVal),
})
if skillData.skillPart then
t_insert(statBoxList, {
height = 14,
align = "CENTER_X", x = 140,
"^8"..skillData.skillPart,
})
end
if skillData.source then
t_insert(statBoxList, {
height = 14,
align = "CENTER_X", x = 140,
colorCodes.WARNING.."from " ..skillData.source,
})
end
end
elseif not (statData.hideStat) then
-- Change the color of the stat label to red if cost exceeds pool
local colorOverride = nil
if actor.output[statData.stat.."Warning"] or (statData.warnFunc and statData.warnFunc(statVal, actor.output) and statData.warnColor) then
colorOverride = colorCodes.NEGATIVE
end
t_insert(statBoxList, {
height = 16,
labelColor..statData.label..":",
self:FormatStat(statData, statVal, overCapStatVal, colorOverride),
})
end
end
if statData.warnFunc and statVal and ((statData.condFunc and statData.condFunc(statVal, actor.output)) or not statData.condFunc) then
local v = statData.warnFunc(statVal, actor.output)
if v then
InsertIfNew(self.controls.warnings.lines, v)
end
end
elseif statData.label and statData.condFunc and statData.condFunc(actor.output) then
t_insert(statBoxList, {
height = 16, labelColor..statData.label..":",
"^7"..actor.output[statData.labelStat].."%^x808080" .. " (" .. statData.val .. ")",})
elseif not statBoxList[#statBoxList] or statBoxList[#statBoxList][1] then
t_insert(statBoxList, { height = 6 })
end
end
end
for pool, warningFlag in pairs({["Life"] = "LifeCostWarningList", ["Mana"] = "ManaCostWarningList", ["Rage"] = "RageCostWarningList", ["Energy Shield"] = "ESCostWarningList"}) do
if actor.output[warningFlag] then
local line = "You do not have enough "..(actor.output.EnergyShieldProtectsMana and pool == "Mana" and "Energy Shield and Mana" or pool).." to use: "
for _, skill in ipairs(actor.output[warningFlag]) do
line = line..skill..", "
end
line = line:sub(1, -3)
InsertIfNew(self.controls.warnings.lines, line)
end
end
for pool, warningFlag in pairs({["Unreserved life"] = "LifePercentCostPercentCostWarningList", ["Unreserved Mana"] = "ManaPercentCostPercentCostWarningList"}) do
if actor.output[warningFlag] then
local line = "You do not have enough ".. pool .."% to use: "
for _, skill in ipairs(actor.output[warningFlag]) do
line = line..skill..", "
end
line = line:sub(1, -3)
InsertIfNew(self.controls.warnings.lines, line)
end
end
if actor.output.VixensTooMuchCastSpeedWarn then
InsertIfNew(self.controls.warnings.lines, "You may have too much cast speed or too little cooldown reduction to effectively use Vixen's Curse replacement")
end
if actor.output.VixenModeNoVixenGlovesWarn then
InsertIfNew(self.controls.warnings.lines, "Vixen's calculation mode for Doom Blast is selected but you do not have Vixen's Entrapment Embroidered Gloves equipped")
end
do
local aspectCount = 0
aspectCount = aspectCount + (actor.output.CrabBarriersMax > 0 and actor.output.CrabBarriers > 0 and 1 or 0)
aspectCount = aspectCount + (aspectCount < 2 and actor.modDB:Flag(nil, "Condition:AspectOfTheSpiderActive") and 1 or 0)
aspectCount = aspectCount + (aspectCount < 2 and (actor.modDB:Flag(nil, "Condition:CatsAgilityActive") or actor.modDB:Flag(nil, "Condition:CatsStealthActive")) and 1 or 0)
aspectCount = aspectCount + (aspectCount < 2 and (actor.modDB:Flag(nil, "Condition:AviansFlightActive") or actor.modDB:Flag(nil, "Condition:AviansMightActive")) and 1 or 0)
if aspectCount > 1 then
InsertIfNew(self.controls.warnings.lines, "You have more than one Aspect skill active")
end
end
end
function buildMode:InsertItemWarnings()
if self.calcsTab.mainEnv.itemWarnings.jewelLimitWarning then
for _, warning in ipairs(self.calcsTab.mainEnv.itemWarnings.jewelLimitWarning) do
InsertIfNew(self.controls.warnings.lines, "You are exceeding jewel limit with the jewel "..warning)
end
end
if self.calcsTab.mainEnv.itemWarnings.socketLimitWarning then
for _, warning in ipairs(self.calcsTab.mainEnv.itemWarnings.socketLimitWarning) do
InsertIfNew(self.controls.warnings.lines, "You have too many gems in your "..warning.." slot")
end
end
end
-- Build list of side bar stats
function buildMode:RefreshStatList()
self.controls.warnings.lines = {}
local statBoxList = wipeTable(self.controls.statBox.list)
if self.calcsTab.mainEnv.player.mainSkill.infoMessage then
if #self.calcsTab.mainEnv.player.mainSkill.infoMessage > 40 then
for line in string.gmatch(self.calcsTab.mainEnv.player.mainSkill.infoMessage, "([^:]+)") do
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, colorCodes.CUSTOM .. line})
end
else
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, colorCodes.CUSTOM .. self.calcsTab.mainEnv.player.mainSkill.infoMessage})
end
if self.calcsTab.mainEnv.player.mainSkill.infoMessage2 then
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, "^8" .. self.calcsTab.mainEnv.player.mainSkill.infoMessage2})
end
end
if self.calcsTab.mainEnv.minion then
t_insert(statBoxList, { height = 18, "^7Minion:" })
if self.calcsTab.mainEnv.minion.mainSkill.infoMessage then
-- Split the line if too long
if #self.calcsTab.mainEnv.minion.mainSkill.infoMessage > 40 then
for line in string.gmatch(self.calcsTab.mainEnv.minion.mainSkill.infoMessage, "([^:]+)") do
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, colorCodes.CUSTOM .. line})
end
else
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, colorCodes.CUSTOM .. self.calcsTab.mainEnv.minion.mainSkill.infoMessage})
end
if self.calcsTab.mainEnv.minion.mainSkill.infoMessage2 then
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, "^8" .. self.calcsTab.mainEnv.minion.mainSkill.infoMessage2})
end
end
self:AddDisplayStatList(self.minionDisplayStats, self.calcsTab.mainEnv.minion)
t_insert(statBoxList, { height = 10 })
t_insert(statBoxList, { height = 18, "^7Player:" })
end
if self.calcsTab.mainEnv.player.mainSkill.skillFlags.disable then
t_insert(statBoxList, { height = 16, "^7Skill disabled:" })
t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, self.calcsTab.mainEnv.player.mainSkill.disableReason })
end
self:AddDisplayStatList(self.displayStats, self.calcsTab.mainEnv.player)
self:InsertItemWarnings()
end
function buildMode:CompareStatList(tooltip, statList, actor, baseOutput, compareOutput, header, nodeCount)
local count = 0
for _, statData in ipairs(statList) do
if statData.stat and matchFlags(statData.flag, statData.notFlag, actor.mainSkill.skillFlags) and not statData.childStat and statData.stat ~= "SkillDPS" then
local statVal1 = compareOutput[statData.stat] or 0
local statVal2 = baseOutput[statData.stat] or 0
local diff = statVal1 - statVal2
if statData.stat == "FullDPS" and not compareOutput[statData.stat] then
diff = 0
end
if (diff > 0.001 or diff < -0.001) and (not statData.condFunc or statData.condFunc(statVal1,compareOutput) or statData.condFunc(statVal2,baseOutput)) then
if count == 0 then
tooltip:AddLine(14, header)
end
local color = ((statData.lowerIsBetter and diff < 0) or (not statData.lowerIsBetter and diff > 0)) and colorCodes.POSITIVE or colorCodes.NEGATIVE
local val = diff * ((statData.pc or statData.mod) and 100 or 1)
local valStr = s_format("%+"..statData.fmt, val) -- Can't use self:FormatStat, because it doesn't have %+. Adding that would have complicated a simple function
valStr = formatNumSep(valStr)
local line = s_format("%s%s %s", color, valStr, statData.label)
local pcPerPt = ""
if statData.compPercent and statVal1 ~= 0 and statVal2 ~= 0 then
local pc = statVal1 / statVal2 * 100 - 100
line = line .. s_format(" (%+.1f%%)", pc)
if nodeCount then
pcPerPt = s_format(" (%+.1f%%)", pc / nodeCount)
end
end
if nodeCount then
line = line .. s_format(" ^8[%+"..statData.fmt.."%s per point]", diff * ((statData.pc or statData.mod) and 100 or 1) / nodeCount, pcPerPt)
end
tooltip:AddLine(14, line)
count = count + 1
end
end
end
return count
end
-- Compare values of all display stats between the two output tables, and add any changed stats to the tooltip
-- Adds the provided header line before the first stat line, if any are added
-- Returns the number of stat lines added
function buildMode:AddStatComparesToTooltip(tooltip, baseOutput, compareOutput, header, nodeCount)
local count = 0
if self.calcsTab.mainEnv.player.mainSkill.minion and baseOutput.Minion and compareOutput.Minion then
count = count + self:CompareStatList(tooltip, self.minionDisplayStats, self.calcsTab.mainEnv.minion, baseOutput.Minion, compareOutput.Minion, header.."\n^7Minion:", nodeCount)
if count > 0 then
header = "^7Player:"
else
header = header.."\n^7Player:"
end
end
count = count + self:CompareStatList(tooltip, self.displayStats, self.calcsTab.mainEnv.player, baseOutput, compareOutput, header, nodeCount)
return count
end
-- Add requirements to tooltip
do
local req = { }
function buildMode:AddRequirementsToTooltip(tooltip, level, str, dex, int, strBase, dexBase, intBase)
if level and level > 0 then
t_insert(req, s_format("^x7F7F7FLevel %s%d", main:StatColor(level, nil, self.characterLevel), level))
end
-- Convert normal attributes to Omni attributes
if self.calcsTab.mainEnv.modDB:Flag(nil, "OmniscienceRequirements") then
local omniSatisfy = self.calcsTab.mainEnv.modDB:Sum("INC", nil, "OmniAttributeRequirements")
local highestAttribute = 0
for i, stat in ipairs({str, dex, int}) do
if((stat or 0) > highestAttribute) then
highestAttribute = stat
end
end
local omni = math.floor(highestAttribute * (100/omniSatisfy))
if omni and (omni > 0 or omni > self.calcsTab.mainOutput.Omni) then
t_insert(req, s_format("%s%d ^x7F7F7FOmni", main:StatColor(omni, 0, self.calcsTab.mainOutput.Omni), omni))
end
else
if str and (str > 14 or str > self.calcsTab.mainOutput.Str) then
t_insert(req, s_format("%s%d ^x7F7F7FStr", main:StatColor(str, strBase, self.calcsTab.mainOutput.Str), str))
end
if dex and (dex > 14 or dex > self.calcsTab.mainOutput.Dex) then
t_insert(req, s_format("%s%d ^x7F7F7FDex", main:StatColor(dex, dexBase, self.calcsTab.mainOutput.Dex), dex))
end
if int and (int > 14 or int > self.calcsTab.mainOutput.Int) then
t_insert(req, s_format("%s%d ^x7F7F7FInt", main:StatColor(int, intBase, self.calcsTab.mainOutput.Int), int))
end
end
if req[1] then
local fontSizeBig = main.showFlavourText and 18 or 16
tooltip:AddLine(fontSizeBig, "^x7F7F7FRequires "..table.concat(req, "^x7F7F7F, "), "FONTIN SC")
tooltip:AddSeparator(10)
end
wipeTable(req)
end
end
function buildMode:LoadDB(xmlText, fileName)
-- Parse the XML
local dbXML, errMsg = common.xml.ParseXML(xmlText)
if not dbXML then
launch:ShowErrMsg("^1Error loading '%s': %s", fileName, errMsg)
return true
elseif #dbXML == 0 then
main:OpenMessagePopup("Error", "Build file is empty, or error parsing xml.\n\n"..fileName)
return true
elseif dbXML[1].elem ~= "PathOfBuilding" then
launch:ShowErrMsg("^1Error parsing '%s': 'PathOfBuilding' root element missing", fileName)
return true
end
-- Load Build section first
for _, node in ipairs(dbXML[1]) do
if type(node) == "table" and node.elem == "Build" then
self:Load(node, self.dbFileName)
break
end
end
-- Check if xml has an import link
for _, node in ipairs(dbXML[1]) do
if type(node) == "table" and node.elem == "Import" then
if node.attrib.importLink and not self.importLink then
self.importLink = node.attrib.importLink
end
break
end
end
-- Store other sections for later processing
for _, node in ipairs(dbXML[1]) do
if type(node) == "table" then
t_insert(self.xmlSectionList, node)
end
end
end
function buildMode:LoadDBFile()
if not self.dbFileName then
return
end
ConPrintf("Loading '%s'...", self.dbFileName)
local file = io.open(self.dbFileName, "r")
if not file then
self.dbFileName = nil
return true
end
local xmlText = file:read("*a")
file:close()
return self:LoadDB(xmlText, self.dbFileName)
end
function buildMode:SaveDB(fileName)
local dbXML = { elem = "PathOfBuilding" }
-- Save Build section first
do
local node = { elem = "Build" }
self:Save(node)
t_insert(dbXML, node)
end
-- Call on all savers to save their data in their respective sections
for elem, saver in pairs(self.savers) do
local node = { elem = elem }
saver:Save(node)
t_insert(dbXML, node)
end
-- Compose the XML
local xmlText, errMsg = common.xml.ComposeXML(dbXML)
if not xmlText then
launch:ShowErrMsg("Error saving '%s': %s", fileName, errMsg)
else
return xmlText
end
end
function buildMode:SaveDBFile()
if not self.dbFileName then
self:OpenSaveAsPopup()
return
end
local xmlText = self:SaveDB(self.dbFileName)
if not xmlText then
return true
end
local file = io.open(self.dbFileName, "w+")
if not file then
main:OpenMessagePopup("Error", "Couldn't save the build file:\n"..self.dbFileName.."\nMake sure the save folder exists and is writable.")
return true
end
file:write(xmlText)
file:close()
local action = self.actionOnSave
self.actionOnSave = nil
-- Reset all modFlags
self:ResetModFlags()
if action == "LIST" then
self:CloseBuild()
elseif action == "EXIT" then
Exit()
elseif action == "UPDATE" then
launch:ApplyUpdate(launch.updateAvailable)
end
end
return buildMode