Files
PathOfBuilding/Modules/Build.lua
Jack Lockwood 4810093798 Release 1.4.151
- Added/updated skills for 3.8
- Applied minion/mine changes
- Updated bases and mods
- Added support for Self flat damage on enemies
- Fixed Assassin's Mark crit
- Fixed "less Mana Cost" applying to reservations
- Fixed "+x to level of all Minion Skill Gems" applying to supports
2019-09-09 21:13:35 +10:00

1326 lines
53 KiB
Lua

-- Path of Building
--
-- Module: Build
-- Loads and manages the current build.
--
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
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 normalBanditDropList = {
{ label = "Passive point", banditId = "None" },
{ label = "Oak (Life)", banditId = "Oak" },
{ label = "Kraityn (Resists)", banditId = "Kraityn" },
{ label = "Alira (Mana)", banditId = "Alira" },
}
local cruelBanditDropList = {
{ label = "Passive point", banditId = "None" },
{ label = "Oak (Endurance)", banditId = "Oak" },
{ label = "Kraityn (Frenzy)", banditId = "Kraityn" },
{ label = "Alira (Power)", banditId = "Alira" },
}
local mercilessBanditDropList = {
{ label = "Passive point", banditId = "None" },
{ label = "Oak (Phys Dmg)", banditId = "Oak" },
{ label = "Kraityn (Att. Speed)", banditId = "Kraityn" },
{ label = "Alira (Cast Speed)", banditId = "Alira" },
}
local fooBanditDropList = {
{ label = "2 Passive Points", banditId = "None" },
{ label = "Oak (Life Regen, Phys.Dmg. Reduction, Phys.Dmg)", banditId = "Oak" },
{ label = "Kraityn (Attack/Cast Speed, Attack Dodge, Move Speed)", banditId = "Kraityn" },
{ label = "Alira (Mana Regen, Crit Multiplier, Resists)", banditId = "Alira" },
}
local PantheonMajorGodDropList = {
{ label = "Nothing", id = "None" },
{ label = "Soul of the Brine King", id = "TheBrineKing" },
{ label = "Soul of Lunaris", id = "Lunaris" },
{ label = "Soul of Solaris", id = "Solaris" },
{ label = "Soul of Arakaali", id = "Arakaali" },
}
local PantheonMinorGodDropList = {
{ label = "Nothing", id = "None" },
{ label = "Soul of Gruthkul", id = "Gruthkul" },
{ label = "Soul of Yugul", id = "Yugul" },
{ label = "Soul of Abberath", id = "Abberath" },
{ label = "Soul of Tukohama", id = "Tukohama" },
{ label = "Soul of Garukhan", id = "Garukhan" },
{ label = "Soul of Ralakesh", id = "Ralakesh" },
{ label = "Soul of Ryslatha", id = "Ryslatha" },
{ label = "Soul of Shakari", id = "Shakari" },
}
local buildMode = new("ControlHost")
function buildMode:Init(dbFileName, buildName, buildXML, targetVersion)
self.dbFileName = dbFileName
self.buildName = buildName
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
if not dbFileName and not targetVersion and not buildXML then
targetVersion = liveTargetVersion
--self.targetVersion = nil
--self:OpenTargetVersionPopup(true)
--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)
local used, ascUsed = self.spec:CountAllocNodes()
local usedMax = 99 + (self.targetVersion == "2_6" and 21 or 22) + (self.calcsTab.mainOutput.ExtraPoints or 0)
local ascMax = 8
control.str = string.format("%s%3d / %3d %s%d / %d", used > usedMax and "^1" or "^7", used, usedMax, ascUsed > ascMax and "^1" or "^7", ascUsed, ascMax)
control.req = "Required level: "..m_max(1, (100 + used - usedMax))
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.characterLevel = new("EditControl", {"LEFT",self.controls.pointDisplay,"RIGHT"}, 12, 0, 106, 20, "", "Level", "%D", 3, function(buf)
self.characterLevel = m_min(tonumber(buf) or 1, 100)
self.modFlag = true
self.buildFlag = true
end)
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
mult = mult * (1 / (1 + 0.1 * (playerLevel - 94)))
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.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.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.buildFlag = true
end)
-- List of display stats
-- This defines the stats in the side bar, and also which stats show in node/item comparisons
-- This may be user-customisable in the future
self.displayStats = {
{ stat = "ActiveMinionLimit", label = "Active Minion Limit", fmt = "d" },
{ stat = "AverageHit", label = "Average Hit", fmt = ".1f", compPercent = true },
{ stat = "AverageDamage", label = "Average Damage", fmt = ".1f", compPercent = true, flag = "attack" },
{ stat = "Speed", label = "Attack Rate", fmt = ".2f", compPercent = true, flag = "attack" },
{ stat = "Speed", label = "Cast Rate", fmt = ".2f", compPercent = true, flag = "spell" },
{ stat = "HitSpeed", label = "Hit Rate", fmt = ".2f", compPercent = true },
{ stat = "TrapThrowingTime", label = "Trap Throwing Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, },
{ stat = "TrapCooldown", label = "Trap Cooldown", fmt = ".2fs", lowerIsBetter = true },
{ stat = "MineLayingTime", label = targetVersion == "2_6" and "Mine Laying Time" or "Mine Throwing Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, },
{ stat = "TotemPlacementTime", label = "Totem Placement Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, },
{ stat = "PreEffectiveCritChance", label = "Crit Chance", fmt = ".2f%%" },
{ stat = "CritChance", label = "Effective Crit Chance", fmt = ".2f%%", condFunc = function(v,o) return v ~= o.PreEffectiveCritChance end },
{ stat = "CritMultiplier", label = "Crit Multiplier", fmt = "d%%", pc = true, condFunc = function(v,o) return (o.CritChance or 0) > 0 end },
{ stat = "HitChance", label = "Hit Chance", fmt = ".0f%%", flag = "attack" },
{ stat = "TotalDPS", label = "Total DPS", fmt = ".1f", compPercent = true, flag = "notAverage" },
{ stat = "TotalDot", label = "DoT DPS", fmt = ".1f", compPercent = true },
{ stat = "BleedDPS", label = "Bleed DPS", fmt = ".1f", compPercent = true },
{ stat = "IgniteDPS", label = "Ignite DPS", fmt = ".1f", compPercent = true },
{ stat = "IgniteDamage", label = "Total Damage per Ignite", fmt = ".1f", compPercent = true },
{ stat = "WithIgniteDPS", label = "Total DPS inc. Ignite", fmt = ".1f", compPercent = true },
{ stat = "WithIgniteAverageDamage", label = "Average Dmg. inc. Ignite", fmt = ".1f", compPercent = true },
{ stat = "PoisonDPS", label = "Poison DPS", fmt = ".1f", compPercent = true },
{ stat = "PoisonDamage", label = "Total Damage per Poison", fmt = ".1f", compPercent = true },
{ stat = "WithPoisonDPS", label = "Total DPS inc. Poison", fmt = ".1f", compPercent = true, flag = "poison", condFunc = function(v,o) return v ~= o.TotalDPS end },
{ stat = "WithPoisonAverageDamage", label = "Average Dmg. inc. Poison", fmt = ".1f", compPercent = true, flag = "poison", condFunc = function(v,o) return v ~= o.AverageDamage end },
{ stat = "DecayDPS", label = "Decay DPS", fmt = ".1f", compPercent = true },
{ stat = "Cooldown", label = "Skill Cooldown", fmt = ".2fs", lowerIsBetter = true },
{ stat = "AreaOfEffectRadius", label = "AoE Radius", fmt = "d" },
{ stat = "ManaCost", label = "Mana Cost", fmt = "d", compPercent = true, lowerIsBetter = true, condFunc = function() return true end },
{ },
{ stat = "Str", label = "Strength", fmt = "d" },
{ stat = "ReqStr", label = "Strength Required", fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Str end },
{ stat = "Dex", label = "Dexterity", fmt = "d" },
{ stat = "ReqDex", label = "Dexterity Required", fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Dex end },
{ stat = "Int", label = "Intelligence", fmt = "d" },
{ stat = "ReqInt", label = "Intelligence Required", fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Int end },
{ },
{ stat = "Life", label = "Total Life", fmt = "d", compPercent = true },
{ stat = "Spec:LifeInc", label = "%Inc Life from Tree", fmt = "d%%", condFunc = function(v,o) return v > 0 and o.Life > 1 end },
{ stat = "LifeUnreserved", label = "Unreserved Life", fmt = "d", condFunc = function(v,o) return v < o.Life end, compPercent = true },
{ stat = "LifeUnreservedPercent", label = "Unreserved Life", fmt = "d%%", condFunc = function(v,o) return v < 100 end },
{ stat = "LifeRegen", label = "Life Regen", fmt = ".1f" },
{ stat = "LifeLeechGainRate", label = "Life Leech/On Hit Rate", fmt = ".1f", compPercent = true },
{ stat = "LifeLeechGainPerHit", label = "Life Leech/Gain per Hit", fmt = ".1f", compPercent = true },
{ },
{ stat = "Mana", label = "Total Mana", fmt = "d", compPercent = true },
{ stat = "Spec:ManaInc", label = "%Inc Mana from Tree", fmt = "d%%" },
{ stat = "ManaUnreserved", label = "Unreserved Mana", fmt = "d", condFunc = function(v,o) return v < o.Mana end, compPercent = true },
{ stat = "ManaUnreservedPercent", label = "Unreserved Mana", fmt = "d%%", condFunc = function(v,o) return v < 100 end },
{ stat = "ManaRegen", label = "Mana Regen", fmt = ".1f" },
{ stat = "ManaLeechGainRate", label = "Mana Leech/On Hit Rate", fmt = ".1f", compPercent = true },
{ stat = "ManaLeechGainPerHit", label = "Mana Leech/Gain per Hit", fmt = ".1f", compPercent = true },
{ },
{ stat = "TotalDegen", label = "Total Degen", fmt = ".1f", lowerIsBetter = true },
{ stat = "NetRegen", label = "Net Regen", fmt = "+.1f" },
{ stat = "NetLifeRegen", label = "Net Life Regen", fmt = "+.1f" },
{ stat = "NetManaRegen", label = "Net Mana Regen", fmt = "+.1f" },
{ },
{ stat = "EnergyShield", label = "Energy Shield", fmt = "d", compPercent = true },
{ stat = "Spec:EnergyShieldInc", label = "%Inc ES from Tree", fmt = "d%%" },
{ stat = "EnergyShieldRegen", label = "Energy Shield Regen", fmt = ".1f" },
{ stat = "EnergyShieldLeechGainRate", label = "ES Leech/On Hit Rate", fmt = ".1f", compPercent = true },
{ stat = "EnergyShieldLeechGainPerHit", label = "ES Leech/Gain per Hit", fmt = ".1f", compPercent = true },
{ stat = "Evasion", label = "Evasion rating", fmt = "d", compPercent = true },
{ stat = "Spec:EvasionInc", label = "%Inc Evasion from Tree", fmt = "d%%" },
{ stat = "MeleeEvadeChance", label = "Evade Chance", fmt = "d%%", condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance == o.ProjectileEvadeChance end },
{ stat = "MeleeEvadeChance", label = "Melee Evade Chance", fmt = "d%%", condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance ~= o.ProjectileEvadeChance end },
{ stat = "ProjectileEvadeChance", label = "Projectile Evade Chance", fmt = "d%%", condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance ~= o.ProjectileEvadeChance end },
{ stat = "Armour", label = "Armour", fmt = "d", compPercent = true },
{ stat = "Spec:ArmourInc", label = "%Inc Armour from Tree", fmt = "d%%" },
{ stat = "PhysicalDamageReduction", label = "Phys. Damage Reduction", fmt = "d%%" },
{ stat = "EffectiveMovementSpeedMod", label = "Movement Speed Modifier", fmt = "+d%%", mod = true, condFunc = function() return true end },
{ stat = "BlockChance", label = "Block Chance", fmt = "d%%" },
{ stat = "SpellBlockChance", label = "Spell Block Chance", fmt = "d%%" },
{ stat = "AttackDodgeChance", label = "Attack Dodge Chance", fmt = "d%%" },
{ stat = "SpellDodgeChance", label = "Spell Dodge Chance", fmt = "d%%" },
{ },
{ stat = "FireResist", label = "Fire Resistance", fmt = "d%%", condFunc = function() return true end },
{ stat = "ColdResist", label = "Cold Resistance", fmt = "d%%", condFunc = function() return true end },
{ stat = "LightningResist", label = "Lightning Resistance", fmt = "d%%", condFunc = function() return true end },
{ stat = "ChaosResist", label = "Chaos Resistance", fmt = "d%%", condFunc = function() return true end },
{ stat = "FireResistOverCap", label = "Fire Res. Over Max", fmt = "d%%" },
{ stat = "ColdResistOverCap", label = "Cold Res. Over Max", fmt = "d%%" },
{ stat = "LightningResistOverCap", label = "Lightning Res. Over Max", fmt = "d%%" },
{ stat = "ChaosResistOverCap", label = "Chaos Res. Over Max", fmt = "d%%" },
}
self.minionDisplayStats = {
{ stat = "AverageDamage", label = "Average Damage", fmt = ".1f", compPercent = true },
{ stat = "Speed", label = "Attack/Cast Rate", fmt = ".2f", compPercent = true },
{ stat = "HitSpeed", label = "Hit Rate", fmt = ".2f" },
{ stat = "TotalDPS", label = "Total DPS", fmt = ".1f", compPercent = true },
{ stat = "TotalDot", label = "DoT DPS", fmt = ".1f", compPercent = true },
{ stat = "BleedDPS", label = "Bleed DPS", fmt = ".1f", compPercent = true },
{ stat = "IgniteDPS", label = "Ignite DPS", fmt = ".1f", compPercent = true },
{ stat = "WithPoisonDPS", label = "DPS inc. Poison", fmt = ".1f", compPercent = true },
{ stat = "DecayDPS", label = "Decay DPS", fmt = ".1f", compPercent = true },
{ stat = "Cooldown", label = "Skill Cooldown", fmt = ".2fs", lowerIsBetter = true },
{ stat = "Life", label = "Total Life", fmt = ".1f", compPercent = true },
{ stat = "LifeRegen", label = "Life Regen", fmt = ".1f" },
{ stat = "LifeLeechGainRate", label = "Life Leech/On Hit Rate", fmt = ".1f", compPercent = true },
}
self.extraSaveStats = {
"PowerCharges",
"PowerChargesMax",
"FrenzyCharges",
"FrenzyChargesMax",
"EnduranceCharges",
"EnduranceChargesMax",
"ActiveTotemLimit",
"ActiveMinionLimit",
}
self.viewMode = "TREE"
self.targetVersion = defaultTargetVersion
self.characterLevel = 1
self.controls.characterLevel:SetText(tostring(self.characterLevel))
self.banditNormal = "None"
self.banditCruel = "None"
self.banditMerciless = "None"
self.pantheonMajorGod = "None"
self.pantheonMinorGod = "None"
self.spectreList = { }
-- Load build file
self.xmlSectionList = { }
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 targetVersion then
self.targetVersion = targetVersion
end
self.targetVersionData = targetVersions[self.targetVersion]
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
-- 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
if self.targetVersion == "2_6" then
self.controls.banditNormal = new("DropDownControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, 0, 70, 100, 16, normalBanditDropList, function(index, value)
self.banditNormal = value.banditId
self.modFlag = true
self.buildFlag = true
end)
self.controls.banditNormalLabel = new("LabelControl", {"BOTTOMLEFT",self.controls.banditNormal,"TOPLEFT"}, 0, 0, 0, 14, "^7Normal Bandit:")
self.controls.banditCruel = new("DropDownControl", {"LEFT",self.controls.banditNormal,"RIGHT"}, 0, 0, 100, 16, mercilessBanditDropList, function(index, value)
self.banditCruel = value.banditId
self.modFlag = true
self.buildFlag = true
end)
self.controls.banditCruelLabel = new("LabelControl", {"BOTTOMLEFT",self.controls.banditCruel,"TOPLEFT"}, 0, 0, 0, 14, "^7Cruel Bandit:")
self.controls.banditMerciless = new("DropDownControl", {"LEFT",self.controls.banditCruel,"RIGHT"}, 0, 0, 100, 16, cruelBanditDropList, function(index, value)
self.banditMerciless = value.banditId
self.modFlag = true
self.buildFlag = true
end)
self.controls.banditMercilessLabel = new("LabelControl", {"BOTTOMLEFT",self.controls.banditMerciless,"TOPLEFT"}, 0, 0, 0, 14, "^7Merciless Bandit:")
else
self.controls.bandit = new("DropDownControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, 0, 70, 300, 16, fooBanditDropList, function(index, value)
self.bandit = value.banditId
self.modFlag = true
self.buildFlag = true
end)
self.controls.banditLabel = new("LabelControl", {"BOTTOMLEFT",self.controls.bandit,"TOPLEFT"}, 0, 0, 0, 14, "^7Bandit:")
-- The Pantheon
local function applyPantheonDescription(tooltip, mode, index, value)
tooltip:Clear()
if value.id == "None" then
return
end
local applyModes = { BODY = true, HOVER = true }
if applyModes[mode] then
local god = self.data.pantheons[value.id]
for _, soul in ipairs(god.souls) do
local name = soul.name
local lines = { }
for _, mod in ipairs(soul.mods) do
t_insert(lines, mod.line)
end
tooltip:AddLine(20, '^8'..name)
tooltip:AddLine(14, '^6'..table.concat(lines, '\n'))
tooltip:AddSeparator(10)
end
end
end
self.controls.pantheonMajorGod = new("DropDownControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, 0, 110, 300, 16, PantheonMajorGodDropList, function(index, value)
self.pantheonMajorGod = value.id
self.modFlag = true
self.buildFlag = true
end)
self.controls.pantheonMajorGod.tooltipFunc = applyPantheonDescription
self.controls.pantheonMinorGod = new("DropDownControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, 0, 130, 300, 16, PantheonMinorGodDropList, function(index, value)
self.pantheonMinorGod = value.id
self.modFlag = true
self.buildFlag = true
end)
self.controls.pantheonMinorGod.tooltipFunc = applyPantheonDescription
self.controls.pantheonLabel = new("LabelControl", {"BOTTOMLEFT",self.controls.pantheonMajorGod,"TOPLEFT"}, 0, 0, 0, 14, "^7The Pantheon:")
end
local mainSkillPosY = (self.targetVersion == "2_6") and 95 or 155 -- The Pantheon's DropDown space
self.controls.mainSkillLabel = new("LabelControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, 0, mainSkillPosY, 300, 16, "^7Main Skill:")
self.controls.mainSocketGroup = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, 0, 2, 300, 16, nil, function(index, value)
self.mainSocketGroup = index
self.modFlag = true
self.buildFlag = true
end)
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, 16, 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, 200, 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.mainSkillMineCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillPart,"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()
return main.screenH - main.mainBarHeight - 4 - y
end
-- Initialise build components
self.data = data[self.targetVersion]
self.latestTree = main.tree[self.targetVersionData.latestTreeVersion]
self.importTab = new("ImportTab", self)
self.notesTab = new("NotesTab", 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,
["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,
}
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 saver:Load(node, self.dbFileName) then
self:CloseBuild()
return
end
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
-- Initialise class dropdown
for classId, class in pairs(self.latestTree.classes) do
t_insert(self.controls.classDrop.list, {
label = class.name,
classId = classId,
})
end
table.sort(self.controls.classDrop.list, function(a, b) return a.label < b.label end)
-- Build calculation output tables
self.outputRevision = 1
self.calcsTab:BuildOutput()
self:RefreshStatList()
self.buildFlag = false
--[[
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 msec", GetTime() - start)
--]]
self.abortSave = false
end
function buildMode:CanExit(mode)
if not self.targetVersion or not self.unsaved then
return true
end
self:OpenSavePopup(mode)
return false
end
function buildMode:Shutdown()
if launch.devMode 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:SetMode("LIST", self.dbFileName and self.buildName, self.dbFileSubPath)
end
function buildMode:Load(xml, fileName)
self.targetVersion = data[xml.attrib.targetVersion] and xml.attrib.targetVersion or defaultTargetVersion
if xml.attrib.viewMode then
self.viewMode = xml.attrib.viewMode
end
self.characterLevel = tonumber(xml.attrib.level) or 1
self.controls.characterLevel:SetText(tostring(self.characterLevel))
for _, diff in pairs({"bandit","banditNormal","banditCruel","banditMerciless","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[self.targetVersion].minions[child.attrib.id] then
t_insert(self.spectreList, child.attrib.id)
end
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.bandit,
banditNormal = self.banditNormal,
banditCruel = self.banditCruel,
banditMerciless = self.banditMerciless,
pantheonMajorGod = self.pantheonMajorGod,
pantheonMinorGod = self.pantheonMinorGod,
mainSocketGroup = tostring(self.mainSocketGroup),
}
for _, id in ipairs(self.spectreList) do
t_insert(xml, { elem = "Spectre", attrib = { id = id } })
end
for index, statData in ipairs(self.displayStats) do
if statData.stat then
local statVal = self.calcsTab.mainOutput[statData.stat]
if statVal then
t_insert(xml, { elem = "PlayerStat", attrib = { stat = statData.stat, value = tostring(statVal) } })
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
self.modFlag = false
end
function buildMode:OnFrame(inputEvents)
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 == "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"
end
end
end
end
self:ProcessControlsInput(inputEvents, main.viewPort)
-- Update contents of ascendancy class dropdown
wipeTable(self.controls.ascendDrop.list)
for i = 0, #self.spec.curClass.classes do
local ascendClass = self.spec.curClass.classes[i]
t_insert(self.controls.ascendDrop.list, {
label = ascendClass.name,
ascendClassId = i,
})
end
self.controls.classDrop:SelByValue(self.spec.curClassId, "classId")
self.controls.ascendDrop:SelByValue(self.spec.curAscendClassId, "ascendClassId")
for _, diff in pairs({"bandit","banditNormal","banditCruel","banditMerciless"}) do
if self.controls[diff] then
self.controls[diff]:SelByValue(self[diff], "banditId")
end
end
for _, diff in pairs({"pantheonMajorGod","pantheonMinorGod"}) do
if self.controls[diff] then
self.controls[diff]:SelByValue(self[diff], "id")
end
end
if self.buildFlag then
-- Rebuild calculation output tables
self.outputRevision = self.outputRevision + 1
self.buildFlag = false
self.calcsTab:BuildOutput()
self:RefreshStatList()
end
if main.showThousandsSidebar ~= self.lastShowThousandsSidebar then
self:RefreshStatList()
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 == "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.configTab.modFlag or self.treeTab.modFlag 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 selection popup
function buildMode:OpenTargetVersionPopup(initial)
local controls = { }
local function setVersion(version)
if version == self.targetVersion then
main:ClosePopup()
return
end
if initial then
main:ClosePopup()
self:Shutdown()
self:Init(false, self.buildName, nil, version)
end
end
controls.label = new("LabelControl", nil, 0, 20, 0, 16, "^7Which game version will this build use?")
controls.version2_6 = new("ButtonControl", nil, -90, 50, 170, 20, "2.6 (Atlas of Worlds)", function()
setVersion("2_6")
end)
controls.version3_0 = new("ButtonControl", nil, 90, 50, 170, 20, "3.0 (Fall of Oriath)", function()
setVersion("3_0")
end)
controls.note = new("LabelControl", nil, 0, 80, 0, 14, "^7Tip: Existing builds can be converted between versions\nusing the 'Game Version' option in the Configuration tab.")
controls.cancel = new("ButtonControl", nil, 0, 120, 80, 20, "Cancel", function()
main:ClosePopup()
if initial then
self:CloseBuild()
end
end)
main:OpenPopup(370, 150, "Game Version", controls, nil, nil, "cancel")
end
function buildMode:OpenSavePopup(mode, newVersion)
local modeDesc = {
["LIST"] = "now?",
["EXIT"] = "before exiting?",
["UPDATE"] = "before updating?",
["VERSION"] = "before converting?",
}
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.versionOnSave = newVersion
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)
elseif mode == "VERSION" then
self:Shutdown()
self:Init(self.dbFileName, self.buildName, nil, newVersion)
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, self.dbFileName and self.buildName, 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()
end)
controls.save.enabled = false
controls.close = new("ButtonControl", nil, 45, 225, 80, 20, "Cancel", function()
main:ClosePopup()
self.actionOnSave = nil
self.versionOnSave = nil
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("MinionListControl", nil, 100, 40, 190, 250, self.data, sourceList, controls.list)
controls.save = new("ButtonControl", nil, -45, 300, 80, 20, "Save", function()
self.spectreList = destList
self.modFlag = true
self.buildFlag = true
main:ClosePopup()
end)
controls.cancel = new("ButtonControl", nil, 45, 300, 80, 20, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(410, 330, "Spectre Library", controls)
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
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.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
t_insert(controls.mainSkill.list, { val = i, label = 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.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
end
if activeSkill.skillFlags.mine then
controls.mainSkillMineCount.shown = true
controls.mainSkillMineCount.buf = tostring(activeEffect.srcInstance["skillMineCount"..suffix] or "")
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)
local val = statVal * ((statData.pc or statData.mod) and 100 or 1) - (statData.mod and 100 or 0)
local color = (statVal >= 0 and "^7" or colorCodes.NEGATIVE)
local valStr = s_format("%"..statData.fmt, val)
if main.showThousandsSidebar then
valStr = color .. formatNumSep(valStr)
else
valStr = color .. valStr
end
self.lastShowThousandsSidebar = main.showThousandsSidebar
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 statData.stat then
if not statData.flag or actor.mainSkill.skillFlags[statData.flag] then
local statVal = actor.output[statData.stat]
if statVal and ((statData.condFunc and statData.condFunc(statVal,actor.output)) or (not statData.condFunc and statVal ~= 0)) then
t_insert(statBoxList, {
height = 16,
"^7"..statData.label..":",
self:FormatStat(statData, statVal),
})
end
end
elseif not statBoxList[#statBoxList] or statBoxList[#statBoxList][1] then
t_insert(statBoxList, { height = 10 })
end
end
end
-- Build list of side bar stats
function buildMode:RefreshStatList()
local statBoxList = wipeTable(self.controls.statBox.list)
if self.calcsTab.mainEnv.minion then
t_insert(statBoxList, { height = 18, "^7Minion:" })
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)
end
function buildMode:CompareStatList(tooltip, statList, actor, baseOutput, compareOutput, header, nodeCount)
local count = 0
for _, statData in ipairs(statList) do
if statData.stat and (not statData.flag or actor.mainSkill.skillFlags[statData.flag]) then
local statVal1 = compareOutput[statData.stat] or 0
local statVal2 = baseOutput[statData.stat] or 0
local diff = statVal1 - statVal2
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 line = string.format("%s%+"..statData.fmt.." %s", color, diff * ((statData.pc or statData.mod) and 100 or 1), statData.label)
local pcPerPt = ""
if statData.compPercent and statVal1 ~= 0 and statVal2 ~= 0 then
local pc = statVal1 / statVal2 * 100 - 100
line = line .. string.format(" (%+.1f%%)", pc)
if nodeCount then
pcPerPt = string.format(" (%+.1f%%)", pc / nodeCount)
end
end
if nodeCount then
line = line .. string.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 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
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
if req[1] then
tooltip:AddLine(16, "^x7F7F7FRequires "..table.concat(req, "^x7F7F7F, "))
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[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
-- 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
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
if self.versionOnSave then
self.targetVersion = self.versionOnSave
self.versionOnSave = nil
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
if action == "LIST" then
self:CloseBuild()
elseif action == "EXIT" then
Exit()
elseif action == "UPDATE" then
launch:ApplyUpdate(launch.updateAvailable)
elseif action == "VERSION" then
self:Shutdown()
self:Init(self.dbFileName, self.buildName)
end
end
return buildMode