Moving stuff around

This commit is contained in:
Openarl
2016-05-05 01:15:55 +10:00
parent db114eb23f
commit c70be7cc43
16 changed files with 30 additions and 22 deletions

310
Modules/Build.lua Normal file
View File

@@ -0,0 +1,310 @@
local launch, cfg, main = ...
local ipairs = ipairs
local t_insert = table.insert
local buildMode = { }
buildMode.controls = { }
t_insert(buildMode.controls, common.newButton(4, 4, 60, 20, "<< Back", function()
main:SetMode("LIST", buildMode.dbFileName)
end))
t_insert(buildMode.controls, common.newButton(4 + 68, 4, 60, 20, "Tree", function()
buildMode.viewMode = "TREE"
end, function()
return buildMode.viewMode ~= "TREE"
end))
t_insert(buildMode.controls, common.newButton(4 + 68*2, 4, 60, 20, "Items", function()
buildMode.viewMode = "ITEMS"
end, function()
return buildMode.viewMode ~= "ITEMS"
end))
t_insert(buildMode.controls, common.newButton(4 + 68*3, 4, 60, 20, "Calcs", function()
buildMode.viewMode = "CALCS"
end, function()
return buildMode.viewMode ~= "CALCS"
end))
t_insert(buildMode.controls, {
x = 4 + 68*4,
y = 4,
Draw = function(self)
local buildName = buildMode.dbFileName:gsub(".xml","")
local bnw = DrawStringWidth(16, "VAR", buildName)
SetDrawColor(0.5, 0.5, 0.5)
DrawImage(nil, self.x + 91, self.y, bnw + 6, 20)
SetDrawColor(0, 0, 0)
DrawImage(nil, self.x + 92, self.y + 1, bnw + 4, 18)
SetDrawColor(1, 1, 1)
DrawString(self.x, self.y + 2, "LEFT", 16, "VAR", "Current build: "..buildName.." "..((buildMode.calcs.modFlag or buildMode.spec.modFlag or buildMode.items.modFlag) and "(Unsaved)" or ""))
end,
})
buildMode.controls.pointDisplay = {
x = 0,
y = 4,
Draw = function(self)
local used, ascUsed = buildMode.spec:CountAllocNodes()
local usedMax = 120 + (buildMode.calcs.output.total_extraPoints or 0)
local ascMax = 6
local 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)
local strW = DrawStringWidth(16, "FIXED", str) + 6
SetDrawColor(1, 1, 1)
DrawImage(nil, self.x, self.y, strW + 2, 20)
SetDrawColor(0, 0, 0)
DrawImage(nil, self.x + 1, self.y + 1, strW, 18)
SetDrawColor(1, 1, 1)
DrawString(self.x + 4, self.y + 2, "LEFT", 16, "FIXED", str)
end,
}
buildMode.controls.classDrop = common.newDropDown(0, 4, 100, 20, nil, function(index, val)
local classId = main.tree.classNameMap[val]
if classId ~= buildMode.spec.curClassId then
if buildMode.spec:IsClassConnected(classId) or buildMode.spec:CountAllocNodes() == 0 then
buildMode.spec:SelectClass(classId)
buildMode.spec:AddUndoState()
else
launch:ShowPrompt(0, 0, 0, "Changing class to "..val.." will reset your tree.\nThis can be avoided by connecting one of the "..val.." starting nodes to your tree.\n\nPress Y to continue.", function(key)
if key == "y" then
buildMode.spec:SelectClass(classId)
buildMode.spec:AddUndoState()
end
return true
end)
end
end
end, function()
return buildMode.viewMode == "TREE"
end)
buildMode.controls.ascendDrop = common.newDropDown(0, 4, 100, 20, nil, function(index, val)
local ascendClassId = main.tree.ascendNameMap[val].ascendClassId
buildMode.spec:SelectAscendClass(ascendClassId)
buildMode.spec:AddUndoState()
end, function()
return buildMode.viewMode == "TREE"
end)
buildMode.savers = {
["Build"] = "",
["Calcs"] = "calcs",
["Items"] = "items",
["Spec"] = "spec",
["TreeView"] = "treeView",
}
function buildMode:LoadDB()
local dbXML, errMsg = common.xml.LoadXMLFile(self.dbFileName)
if not dbXML then
launch:ShowErrMsg("^1Error loading '%s': %s", self.dbFileName, errMsg)
return true
elseif dbXML[1].elem ~= "PathOfBuilding" then
launch:ShowErrMsg("^1Error parsing '%s': 'PathOfBuilding' root element missing", self.dbFileName)
return true
end
for _, node in ipairs(dbXML[1]) do
if type(node) == "table" then
local key = self.savers[node.elem]
if key then
local saver = self[key] or self
if saver:Load(node, self.dbFileName) then
return true
end
end
end
end
end
function buildMode:SaveDB()
local dbXML = { elem = "PathOfBuilding" }
for elem, key in pairs(self.savers) do
local saver = self[key] or self
local node = { elem = elem }
saver:Save(node)
t_insert(dbXML, node)
end
local res, errMsg = common.xml.SaveXMLFile(dbXML, self.dbFileName)
if not res then
launch:ShowErrMsg("Error saving '%s': %s", self.dbFileName, errMsg)
return true
end
end
function buildMode:Load(xml, fileName)
if xml.attrib.viewMode then
self.viewMode = xml.attrib.viewMode
end
end
function buildMode:Save(xml)
xml.attrib = {
viewMode = self.viewMode,
className = self.tree.classes[self.spec.curClassId].name,
ascendClassName = self.spec.curAscendClassId > 0 and self.tree.classes[self.spec.curClassId].classes[tostring(self.spec.curAscendClassId)].name,
level = tostring(self.calcs.input.player_level or 1)
}
end
function buildMode:Init(dbFileName)
self.dbFileName = dbFileName
ConPrintf("Loading '%s'...", dbFileName)
self.abortSave = true
self.items = LoadModule("Items", launch, cfg, main)
self.items:Init(self)
self.calcs = LoadModule("Calcs", launch, cfg, main)
self.calcs:Init(self)
self.tree = main.tree
self.spec = main.SpecClass.NewSpec(main.tree)
self.treeView = main.TreeViewClass.NewTreeView()
wipeTable(self.controls.classDrop.list)
for classId, class in pairs(self.tree.classes) do
t_insert(self.controls.classDrop.list, class.name)
end
table.sort(self.controls.classDrop.list)
self.displayStats = {
{ mod = "total_avg", label = "Average Hit", fmt = ".1f" },
{ mod = "total_speed", label = "Speed", fmt = ".2f" },
{ mod = "total_critChance", label = "Crit Chance", fmt = ".2f%%", pc = true },
{ mod = "total_critMultiplier", label = "Crit Multiplier", fmt = "d%%", pc = true },
{ mod = "total_hitChance", label = "Hit Chance", fmt = "d%%", pc = true },
{ mod = "total_dps", label = "Total DPS", fmt = ".1f" },
{ mod = "ignite_dps", label = "Ignite DPS", fmt = ".1f" },
{ mod = "poison_dps", label = "Poison DPS", fmt = ".1f" },
{ },
{ mod = "spec_lifeInc", label = "Life %", fmt = "d" },
{ mod = "total_life", label = "Total Life", fmt = "d" },
{ },
{ mod = "total_mana", label = "Total Mana", fmt = "d" },
{ mod = "total_manaRegen", label = "Mana Regen", fmt = ".1f" },
{ },
{ mod = "total_energyShield", label = "Energy Shield", fmt = "d" },
{ mod = "total_evasion", label = "Evasion rating", fmt = "d" },
{ mod = "total_armour", label = "Armour", fmt = "d" },
{ mod = "total_blockChance", label = "Block Chance", fmt = "d%%" },
{ },
{ mod = "total_fireResist", label = "Fire Resistance", fmt = "d%%" },
{ mod = "total_coldResist", label = "Cold Resistance", fmt = "d%%" },
{ mod = "total_lightningResist", label = "Lightning Resistance", fmt = "d%%" },
{ mod = "total_chaosResist", label = "Chaos Resistance", fmt = "d%%" },
}
self.viewMode = "TREE"
if self:LoadDB() then
main:SetMode("LIST", dbFileName)
return
end
--[[local start = GetTime()
SetProfiling(true)
for i = 1, 10 do
self.calcs:BuildPower(self)
end
SetProfiling(false)
ConPrintf("Power build time: %d msec", GetTime() - start)]]
self.abortSave = false
end
function buildMode:Shutdown()
if not self.abortSave then
self:SaveDB()
end
self.abortSave = nil
self.calcs:Shutdown()
self.calcs = nil
self.items:Shutdown()
self.items = nil
self.spec = nil
self.treeView = nil
end
function buildMode:OnFrame(inputEvents)
common.controlsInput(self, inputEvents)
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "s" and IsKeyDown("CTRL") then
self:SaveDB()
inputEvents[id] = nil
end
end
end
local class = main.tree.classes[self.spec.curClassId]
local ascendClass = class.classes[tostring(self.spec.curAscendClassId)]
wipeTable(self.controls.ascendDrop.list)
for _, ascendClass in pairs(main.tree.classes[self.spec.curClassId].classes) do
t_insert(self.controls.ascendDrop.list, ascendClass.name)
end
table.sort(self.controls.ascendDrop.list)
self.controls.classDrop:SelByValue(class.name)
self.controls.ascendDrop:SelByValue(ascendClass and ascendClass.name or "None")
self.controls.pointDisplay.x = cfg.screenW / 2 + 6
self.controls.classDrop.x = self.controls.pointDisplay.x + 154
self.controls.ascendDrop.x = self.controls.classDrop.x + self.controls.classDrop.width + 8
self.calcs:RunControl(self)
if self.viewMode == "TREE" then
local viewPort = {
x = 258,
y = 32,
width = cfg.screenW - 258,
height = cfg.screenH - 32
}
self.treeView:DrawTree(self, viewPort, inputEvents)
elseif self.viewMode == "CALCS" then
local viewPort = {
x = 0,
y = 32,
width = cfg.screenW,
height = cfg.screenH - 32
}
self.calcs:DrawGrid(viewPort, inputEvents)
elseif self.viewMode == "ITEMS" then
local viewPort = {
x = 258,
y = 32,
width = cfg.screenW - 258,
height = cfg.screenH - 32
}
self.items:DrawItems(viewPort, inputEvents)
end
SetDrawColor(0.2, 0.2, 0.2)
DrawImage(nil, 0, 0, cfg.screenW, 28)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, 0, 28, cfg.screenW, 4)
DrawImage(nil, cfg.screenW/2 - 2, 0, 4, 28)
common.controlsDraw(self, viewPort)
if self.viewMode ~= "CALCS" then
SetDrawColor(0.1, 0.1, 0.1)
DrawImage(nil, 0, 32, 254, cfg.screenH - 32)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, 254, 32, 4, cfg.screenH - 32)
local y = 36
for index, data in ipairs(self.displayStats) do
if data.mod then
if self.calcs.output[data.mod] and self.calcs.output[data.mod] ~= 0 then
DrawString(150, y, "RIGHT_X", 16, "VAR", data.label..":")
DrawString(154, y, "LEFT", 16, "VAR", string.format("%"..data.fmt, self.calcs.output[data.mod] * (data.pc and 100 or 1)))
y = y + 16
end
else
y = y + 12
end
end
end
end
return buildMode

665
Modules/Calcs.lua Normal file
View File

@@ -0,0 +1,665 @@
local launch, cfg, main = ...
cfg.gridHeight = 18
cfg.defGridWidth = 50
cfg.defBorderCol = { 0.1, 0.1, 0.1 }
cfg.defCellCol = { 0, 0, 0 }
local pairs = pairs
local t_insert = table.insert
local m_max = math.max
local m_floor = math.floor
local function alignCellText(e)
if e.align == "RIGHT" then
return cfg.screenW - (e.grid.offX + e.x + e.grid[e.gx].width - 2)
elseif e.align == "CENTER" then
return - cfg.screenW / 2 + e.grid.offX + e.x + e.grid[e.gx].width / 2
else
return e.grid.offX + e.x + 2
end
end
local function formatCellText(fn, val)
if fn then
local errMsg, text = PCall(fn, val)
if errMsg then
launch:ShowErrMsg("Error formatting cell: %s", errMsg)
return ""
else
return text
end
else
return tostring(val)
end
end
local elem = {}
elem.input = {}
elem.input.__index = elem.input
elem.input.borderCol = { 0.9, 0.9, 0.9 }
elem.input.cellCol = { 0.1, 0.1, 0.4 }
function elem.input:Init()
local grid = self.grid
if self.format == "choice" then
self.dropDown = common.newDropDown(0, 0, 0, 0, self.list, function(index, val)
if val ~= grid.input[self.name] then
grid.input[self.name] = val
grid.changeFlag = true
end
end)
self.dropDown.sel = 1
end
end
function elem.input:Draw()
local grid = self.grid
if self.format == "check" then
DrawString(alignCellText(self), grid.offY + self.y + 2, self.align, cfg.gridHeight - 4, "FIXED", grid.input[self.name] and "^x33FF33Yes" or "^xFF3333No")
elseif grid.focus == self then
if self.edit then
self.edit:Draw()
else
self.dropDown:Draw()
end
elseif grid.input[self.name] then
DrawString(alignCellText(self), grid.offY + self.y + 2, self.align, cfg.gridHeight - 4, "VAR", "^7"..formatCellText(self.formatFunc, grid.input[self.name]))
end
end
function elem.input:OnKeyDown(key, doubleClick)
local grid = self.grid
if grid.focus == self then
if key == "RETURN" or key == "TAB" then
if self.edit then
local newVal = #self.edit.buf and self.edit.buf or nil
if self.format == "number" then
newVal = tonumber((newVal:gsub(",",""):gsub("%*","e")))
end
if newVal ~= grid.input[self.name] then
grid.input[self.name] = newVal
grid.changeFlag = true
end
end
grid:SetFocus()
grid:MoveSel(key == "TAB" and "RIGHT" or "DOWN", true)
elseif self.edit then
self.edit:OnKeyDown(key)
elseif self.dropDown then
if self.dropDown:OnKeyDown(key) then
grid:SetFocus()
end
end
elseif key == "RIGHTBUTTON" or (key == "LEFTBUTTON" and doubleClick) then
if self.format == "check" then
grid.input[self.name] = not grid.input[self.name]
grid.changeFlag = true
elseif self.format == "choice" then
grid:SetFocus(self)
else
grid:SetFocus(self)
self.edit:SetText(grid.input[self.name] or "")
end
elseif key == "WHEELUP" then
if self.format == "number" then
grid.input[self.name] = (grid.input[self.name] or 0) + 1
grid.changeFlag = true
end
elseif key == "WHEELDOWN" then
if self.format == "number" then
grid.input[self.name] = (grid.input[self.name] or 0) - 1
grid.changeFlag = true
end
elseif self.edit then
if key == "c" and IsKeyDown("CTRL") then
if grid.input[self.name] then
Copy(tostring(grid.input[self.name]))
end
elseif key == "v" and IsKeyDown("CTRL") then
local newVal = Paste()
if newVal then
if self.format == "number" then
newVal = tonumber(newVal)
end
if newVal ~= grid.input[self.name] then
grid.input[self.name] = newVal
grid.changeFlag = true
end
end
end
end
end
function elem.input:OnKeyUp(key)
local grid = self.grid
if grid.focus == self then
if self.dropDown then
if self.dropDown:OnKeyUp(key) then
grid:SetFocus()
end
else
self.edit:OnKeyUp(key)
end
end
end
function elem.input:OnChar(key)
local grid = self.grid
if self.format == "check" then
if key == " " then
grid.input[self.name] = not grid.input[self.name]
grid.changeFlag = true
end
return
elseif self.format == "choice" then
return
end
if key == "\r" then
return
elseif not grid.focus and key == "\b" then
grid.input[self.name] = nil
grid.changeFlag = true
return
elseif key:match("%c") then
return
end
if not grid.focus then
if self.format == "number" and key == "+" then
grid.input[self.name] = (grid.input[self.name] or 0) + 1
grid.changeFlag = true
return
end
grid:SetFocus(self)
end
self.edit:OnChar(key)
end
function elem.input:OnFocusGained()
local grid = self.grid
if self.format == "choice" then
self.dropDown.x = grid.offX + self.x
self.dropDown.y = grid.offY + self.y
self.dropDown.width = grid:GetElemWidth(self)
self.dropDown.height = cfg.gridHeight
self.dropDown:SelByValue(grid.input[self.name])
self.dropDown:OnKeyDown("LEFTBUTTON")
else
local fmtFilter = { number = "[-%d%.e,*]", string = "." }
self.edit = common.newEditField(nil, nil, fmtFilter[self.format])
self.edit.x = grid.offX + self.x + 2
self.edit.y = grid.offY + self.y + 2
self.edit.width = grid:GetElemWidth(self)
self.edit.height = cfg.gridHeight - 4
end
end
function elem.input:OnFocusLost()
self.edit = nil
if self.dropDown then
self.dropDown.dropped = false
end
end
elem.output = {}
elem.output.__index = elem.output
elem.output.borderCol = { 0.7, 0.7, 0.7 }
function elem.output:Draw()
local grid = self.grid
if grid.output[self.name] then
DrawString(alignCellText(self), grid.offY + self.y + 2, self.align, cfg.gridHeight - 4, self.font or "VAR", "^7"..formatCellText(self.formatFunc, grid.output[self.name]))
end
end
function elem.output:OnKeyDown(key)
local grid = self.grid
if key == "c" and IsKeyDown("CTRL") and grid.output[self.name] then
Copy(tostring(grid.output[self.name]))
end
end
elem.label = {}
elem.label.__index = elem.label
function elem.label:Draw()
local grid = self.grid
DrawString(alignCellText(self), grid.offY + self.y + 2, self.align, cfg.gridHeight - 4, "VAR BOLD", "^xD0D5D0"..self.text)
end
local grid = { }
function grid:Clear()
for gx in ipairs(self) do
self[gx] = nil
end
self.width = 1
self.height = 1
self[1] = { width = cfg.defGridWidth, x = 0 }
self:CalcCoords()
end
function grid:SetSize(w, h)
if w < self.width then
for gx = w + 1, self.width do
self[gx] = nil
end
end
self.width = w
self.height = h
for gx = 1, w do
self[gx] = self[gx] or { width = cfg.defGridWidth }
end
self:CalcCoords()
end
function grid:CalcCoords()
local x = 0
for gx = 1, self.width do
self[gx].x = x
for gy = 1, self.height do
if self[gx][gy] and self[gx][gy].gx == gx then
self[gx][gy].x = x
end
end
x = x + self[gx].width
end
self.realWidth = x
self.realHeight = self.height * cfg.gridHeight
end
function grid:CheckSize()
local mx, my = 1, 1
for gx = 1, self.width do
for gy = 1, self.height do
if self[gx][gy] then
mx = math.max(mx, gx + self[gx][gy].width - 1)
my = math.max(my, gy)
end
end
end
grid:SetSize(mx, my)
end
function grid:SetColWidth(gx, sz)
if self[gx] then
self[gx].width = sz
end
self:CalcCoords()
end
function grid:GetElem(gx, gy)
return self[gx] and self[gx][gy]
end
function grid:SetElem(gx, gy, e)
if e then
e.grid = self
e.gx = gx
e.gy = gy
e.width = e.width or 1
if gx + e.width - 1 > self.width then
grid:SetSize(gx + e.width - 1, self.height)
end
if gy > self.height then
grid:SetSize(self.width, gy)
end
for i = 1, e.width do
grid[gx + i - 1][gy] = e
end
e.x = grid[gx].x
e.y = (gy - 1) * cfg.gridHeight
if elem[e.type] then
setmetatable(e, elem[e.type])
end
if e.Init then
e:Init()
end
elseif self:GetElem(gx, gy) then
self[gx][gy] = nil
self:CheckSize()
end
end
function grid:GetElemWidth(e)
local width = 0
for gx = e.gx, e.gx + e.width - 1 do
width = width + self[gx].width
end
return width
end
function grid:SetFocus(e)
if self.focus and self.focus.OnFocusLost then
self.focus:OnFocusLost()
end
self.focus = e
if self.focus and self.focus.OnFocusGained then
self.focus:OnFocusGained()
end
end
function grid:MoveSel(dir, force)
if not self.sel or not self.sel.gx then
return
end
local selX, selY = self.sel.gx, self.sel.gy
local s, e, i
if dir == "LEFT" or dir == "RIGHT" then
if dir == "LEFT" then
s, e, i = selX - 1, 1, -1
else
s, e, i = selX + 1, self.width, 1
end
for gx = s, e, i do
if self[gx][selY] then
self.sel = self[gx][selY]
return
end
end
else
if dir == "UP" then
s, e, i = selY - 1, 1, -1
else
s, e, i = selY + 1, self.height, 1
end
for gy = s, e, i do
if self[selX][gy] then
self.sel = self[selX][gy]
return
end
end
end
if force then
self.sel = nil
end
end
function grid:Draw()
local x = self.offX
local h = cfg.gridHeight
for gx = 1, self.width do
local y = self.offY
local w = self[gx].width
for gy = 1, self.height do
local e = self[gx][gy]
if not e or e.gx == gx then
local ew = w
if e and e.width and e.width > 1 then
for i = 1, e.width - 1 do
ew = ew + self[gx + i].width
end
end
SetDrawColor(unpack(e and e.borderCol or cfg.defBorderCol))
DrawImage(nil, x, y, ew, h)
SetDrawColor(unpack(e and e.cellCol or cfg.defCellCol))
DrawImage(nil, x + 1, y + 1, ew - 2, h - 2)
end
y = y + cfg.gridHeight
end
x = x + self[gx].width
end
for gx = 1, self.width do
for gy = 1, self.height do
local e = self[gx][gy]
if e and e.gx == gx and e.Draw and e ~= self.focus then
e:Draw()
end
end
end
if self.focus then
self.focus:Draw()
end
if self.sel and self.sel.gx then
local selX, selY = self.sel.gx, self.sel.gy
SetDrawColor(1, 1, 1)
local x, y = self.sel.x + self.offX, self.sel.y + self.offY
local w, h = self:GetElemWidth(self.sel), cfg.gridHeight
DrawImage(nil, x - 2, y - 2, w + 4, 4)
DrawImage(nil, x - 2, y + h - 2, w + 4, 4)
DrawImage(nil, x - 2, y - 2, 4, h + 4)
DrawImage(nil, x + w - 2, y - 2, 4, h + 4)
end
end
function grid:OnKeyDown(key, doubleClick)
if self.focus then
if self.focus.OnKeyDown then
self.focus:OnKeyDown(key, doubleClick)
end
elseif key == "LEFTBUTTON" or key == "RIGHTBUTTON" then
self.sel = nil
local cx, cy = GetCursorPos()
local gcx, gcy = cx - self.offX, cy - self.offY
local gy = math.floor(gcy / cfg.gridHeight) + 1
if gcx >= 0 and gcy >= 0 and gcx < self.realWidth and gcy < self.realHeight then
local x = 0
for gx = 1, self.width do
if gcx >= x and gcx < x + self[gx].width then
if self[gx][gy] then
local e = self[gx][gy]
self.sel = e
if e.OnKeyDown then
e:OnKeyDown(key, doubleClick)
end
end
return
end
x = x + self[gx].width
end
end
elseif key == "LEFT" or key == "RIGHT" or key == "UP" or key == "DOWN" then
self:MoveSel(key)
elseif self.sel then
if self.sel.OnKeyDown then
self.sel:OnKeyDown(key)
end
end
end
function grid:OnKeyUp(key)
if self.focus then
if key == "ESCAPE" then
self:SetFocus()
elseif self.focus.OnKeyUp then
self.focus:OnKeyUp(key)
end
elseif key == "ESCAPE" then
self.sel = nil
elseif self.sel then
if self.sel.OnKeyUp then
self.sel:OnKeyUp(key)
end
end
end
function grid:OnChar(key)
if self.focus then
if self.focus.OnChar then
self.focus:OnChar(key)
end
elseif self.sel then
if self.sel.OnChar then
self.sel:OnChar(key)
end
end
end
local calcs = { }
function calcs:Init(build)
self.build = build
self.input = { }
self.output = { }
grid.input = self.input
grid.output = self.output
grid:Clear()
self.undo = { }
self.redo = { }
self:LoadControl()
end
function calcs:Shutdown()
grid:SetFocus()
grid:Clear()
self.redo = nil
self.undo = nil
end
function calcs:Load(xml, dbFileName)
for _, node in ipairs(xml) do
if type(node) == "table" then
if node.elem == "Input" then
if not node.attrib.name then
launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing name attribute", fileName)
return true
end
if node.attrib.number then
self.input[node.attrib.name] = tonumber(node.attrib.number)
elseif node.attrib.string then
self.input[node.attrib.name] = node.attrib.string
elseif node.attrib.boolean then
self.input[node.attrib.name] = node.attrib.boolean == "true"
else
launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing number, string or boolean attribute", fileName)
return true
end
end
end
end
self:AddUndoState()
self.buildFlag = true
end
function calcs:Save(xml)
self.modFlag = false
for k, v in pairs(self.input) do
local child = {elem = "Input", attrib = {name = k}}
if type(v) == "number" then
child.attrib.number = tostring(v)
elseif type(v) == "boolean" then
child.attrib.boolean = tostring(v)
else
child.attrib.string = tostring(v)
end
t_insert(xml, child)
end
end
function calcs:AddUndoState()
t_insert(self.undo, 1, copyTable(self.input))
self.undo[102] = nil
end
function calcs:LoadControl()
grid:Clear()
local errMsg
errMsg, self.control = PLoadModule("CalcsControl", grid)
if errMsg then
launch:ShowErrMsg("Error loading control script: %s", errMsg)
elseif not self.control then
launch:ShowErrMsg("Error loading control script: no object returned")
end
end
function calcs:RunControl()
if grid.changeFlag then
grid.changeFlag = false
self.modFlag = true
self.buildFlag = true
self:AddUndoState()
if not self.noClearRedo then
self.redo = {}
end
self.noClearRedo = false
end
if self.buildFlag or self.build.spec.buildFlag or self.build.items.buildFlag then
self.buildFlag = false
self.build.spec.buildFlag = false
self.build.items.buildFlag = false
wipeTable(self.output)
if self.control and self.control.buildOutput then
local errMsg, otherMsg = PCall(self.control.buildOutput, grid.input, grid.output, self.build)
if errMsg then
launch:ShowErrMsg("Error building output: %s", errMsg)
elseif otherMsg then
launch:ShowPrompt(1, 0.5, 0, otherMsg.."\n\nEnter/Escape to Dismiss", function(key)
if key == "RETURN" or key == "ESCAPE" then
return true
end
end)
end
end
self.powerBuildFlag = true
end
end
function calcs:BuildPower()
local calcFunc, base = self:GetNodeCalculator()
if not calcFunc then
return
end
local cache = { }
self.powerMax = { }
for _, node in pairs(self.build.spec.nodes) do
node.power = wipeTable(node.power)
if not node.alloc and node.modKey ~= "" then
if not cache[node.modKey] then
cache[node.modKey] = calcFunc({node})
end
local output = cache[node.modKey]
local dpsKey = base.mode_average and "total_avg" or "total_dps"
node.power.dps = (output[dpsKey] - base[dpsKey]) / base[dpsKey]
node.power.def = (output.total_life - base.total_life) / m_max(2000, base.total_life) * 0.5 +
(output.total_armour - base.total_armour) / m_max(10000, base.total_armour) +
(output.total_energyShield - base.total_energyShield) / m_max(2000, base.total_energyShield) +
(output.total_evasion - base.total_evasion) / m_max(10000, base.total_evasion) +
(output.total_lifeRegen - base.total_lifeRegen) / 500
if node.path then
self.powerMax.dps = m_max(self.powerMax.dps or 0, node.power.dps)
self.powerMax.def = m_max(self.powerMax.def or 0, node.power.def)
end
end
end
self.powerBuildFlag = false
end
function calcs:GetNodeCalculator()
if self.control and self.control.getNodeCalculator then
local errMsg, calcFunc, calcBase = PCall(self.control.getNodeCalculator, grid.input, self.build)
if errMsg then
launch:ShowErrMsg("Error creating calculator: %s", errMsg)
elseif otherMsg then
launch:ShowPrompt(1, 0.5, 0, otherMsg.."\n\nEnter/Escape to Dismiss")
end
return calcFunc, calcBase
end
end
function calcs:GetItemCalculator()
if self.control and self.control.getItemCalculator then
local errMsg, calcFunc, calcBase = PCall(self.control.getItemCalculator, grid.input, self.build)
if errMsg then
launch:ShowErrMsg("Error creating calculator: %s", errMsg)
elseif otherMsg then
launch:ShowPrompt(1, 0.5, 0, otherMsg.."\n\nEnter/Escape to Dismiss")
end
return calcFunc, calcBase
end
end
function calcs:DrawGrid(viewPort, inputEvents)
grid.offX = viewPort.x + m_floor((viewPort.width - grid.realWidth) / 2)
grid.offY = viewPort.y + 2
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "r" and IsKeyDown("CTRL") then
self:LoadControl()
self.buildFlag = true
elseif event.key == "z" and IsKeyDown("CTRL") then
if self.undo[2] then
t_insert(self.redo, 1, table.remove(self.undo, 1))
wipeTable(self.input)
for k, v in pairs(table.remove(self.undo, 1)) do
self.input[k] = v
end
grid.changeFlag = true
self.noClearRedo = true
end
elseif event.key == "y" and IsKeyDown("CTRL") then
if self.redo[1] then
wipeTable(self.input)
for k, v in pairs(table.remove(self.redo, 1)) do
self.input[k] = v
end
grid.changeFlag = true
self.noClearRedo = true
end
else
grid:OnKeyDown(event.key, event.doubleClick)
end
elseif event.type == "KeyUp" then
grid:OnKeyUp(event.key)
elseif event.type == "Char" then
grid:OnChar(event.key)
end
end
grid:Draw()
end
return calcs

1399
Modules/CalcsControl.lua Normal file

File diff suppressed because it is too large Load Diff

505
Modules/CalcsView.lua Normal file
View File

@@ -0,0 +1,505 @@
local grid = ...
local s_format = string.format
local m_abs = math.abs
local m_floor = math.floor
local m_min = math.min
local m_max = math.max
local pairs = pairs
local ipairs = ipairs
function formatNumSep(val, dec)
dec = dec or 0
val = val or 0
local neg = val < 0
val = m_floor(m_abs(val * 10 ^ dec))
local str = string.reverse(s_format("%.0f", val))
if #str < (dec + 1) then
str = str .. string.rep("0", dec + 1 - #str)
end
local ret = ""
local pDec, pThou = dec, 3
for ci = 1, #str do
local c = str:sub(ci, ci)
ret = c .. ret
if pDec > 0 then
pDec = pDec - 1
if pDec == 0 then
ret = "." .. ret
end
else
pThou = pThou - 1
if pThou == 0 and ci < #str then
ret = "," .. ret
pThou = 3
end
end
end
return (neg and "-" or "") .. ret
end
function getFormatNumSep(dec)
return function(val)
return formatNumSep(val, dec)
end
end
function formatRound(val, dec)
dec = dec or 0
return m_floor(val * 10 ^ dec + 0.5) / 10 ^ dec
end
function getFormatRound(dec)
return function(val)
return formatRound(val, dec)
end
end
function formatPercent(val, dec)
dec = dec or 0
return m_floor((val or 0) * 100 * 10 ^ dec) / 10 ^ dec .. "%"
end
function getFormatPercent(dec)
return function(val)
return formatPercent(val, dec)
end
end
function formatSec(val)
if val == 0 then
return "0s"
else
return s_format("%.2fs", val)
end
end
local function mkField(x, y, fieldType, name, format, width, list)
local isFunc = type(format) == "function"
grid:SetElem(x, y, {
type = fieldType,
name = name,
format = (isFunc or not format) and "number" or format,
formatFunc = isFunc and format,
align = (format == "string" or format == "choice") and "LEFT" or "RIGHT",
width = width,
list = list,
})
end
local function mkFieldWithLabel(x, y, fieldType, label, name, format, width, list)
grid:SetElem(x, y, {
type = "label",
text = label,
align = "RIGHT"
})
if type(name) == "table" then
for i, n in ipairs(name) do
if n then
mkField(x + i, y, fieldType, n, format)
end
end
else
mkField(x + 1, y, fieldType, name, format, width, list)
end
end
local function mkFieldTable(x, y, tbl)
for i, v in ipairs(tbl) do
if #v == 1 then
if type(v[1]) == "table" then
for c, l in ipairs(v[1]) do
grid:SetElem(x + c - 1, y + i - 1, { type = "label", text = l, align = c == 1 and "RIGHT" or "CENTER" })
end
else
grid:SetElem(x, y + i - 1, { type = "label", text = v[1], align = "RIGHT" })
end
elseif #v > 1 then
mkFieldWithLabel(x, y + i - 1, unpack(v))
end
end
end
local function fieldNames(pre, suf, spec)
return {
spec:match("p") and (pre.."_physical"..suf) or false,
spec:match("l") and (pre.."_lightning"..suf) or false,
spec:match("c") and (pre.."_cold"..suf) or false,
spec:match("f") and (pre.."_fire"..suf) or false,
spec:match("h") and (pre.."_chaos"..suf) or false,
spec:match("a") and (pre.."_damage"..suf) or false,
spec:match("e") and (pre.."_elem"..suf) or false
}
end
local columnWidths = {
120, 60,
150, 60,
150, 60,
160, 90, 90, 90, 90, 90, 90, 70
}
local columns = { }
columns[1] = {
{
{ "Player:" },
{ "input", "Level:", "player_level" },
{ "output", "Gear Strength:", "gear_strBase" },
{ "output", "Gear Dexterity:", "gear_dexBase" },
{ "output", "Gear Intelligence:", "gear_intBase" },
{ "output", "^xFF7700Strength^7:", "total_str" },
{ "output", "^x33FF33Dexterity^7:", "total_dex" },
{ "output", "^x7777FFIntelligence^7:", "total_int" },
{ },
{ "Monsters:" },
{ "input", "Monster level:", "monster_level" },
{ "output", "Experience:", "monster_xp", formatPercent },
{ },
{ "Life:" },
{ "output", "Spec +:", "spec_lifeBase" },
{ "output", "Spec %:", "spec_lifeInc" },
{ "output", "Gear +:", "gear_lifeBase" },
{ "output", "Gear %:", "gear_lifeInc" },
{ "output", "Total:", "total_life", formatRound },
{ "output", "Spec Regen %:", "spec_lifeRegenPercent" },
{ "output", "Gear Regen +:", "gear_lifeRegenBase" },
{ "output", "Gear Regen %:", "gear_lifeRegenPercent" },
{ "output", "Total Regen:", "total_lifeRegen", getFormatRound(1) },
{ },
{ "Mana:" },
{ "output", "Spec +:", "spec_manaBase" },
{ "output", "Spec %:", "spec_manaInc" },
{ "output", "Gear +:", "gear_manaBase" },
{ "output", "Gear %:", "gear_manaInc" },
{ "output", "Total:", "total_mana", formatRound },
{ "output", "Spec Regen %:", "spec_manaRegenInc" },
{ "output", "Gear Regen +:", "gear_manaRegenBase" },
{ "output", "Gear Regen %:", "gear_manaRegenInc" },
{ "output", "Total Regen:", "total_manaRegen", getFormatRound(1) },
{ },
{ "Auras and Buffs:" },
{ "input", "Skill 1:", "buff_spec1", "string", 2 },
{ "input", "Skill 2:", "buff_spec2", "string", 2 },
{ "input", "Skill 3:", "buff_spec3", "string", 2 },
{ "input", "Skill 4:", "buff_spec4", "string", 2 },
{ "input", "Skill 5:", "buff_spec5", "string", 2 },
{ "input", "Skill 6:", "buff_spec6", "string", 2 },
{ "input", "Skill 7:", "buff_spec7", "string", 2 },
{ "input", "Skill 8:", "buff_spec8", "string", 2 },
{ "input", "Skill 9:", "buff_spec9", "string", 2 },
{ "input", "Skill 10:", "buff_spec10", "string", 2 },
}
}
columns[3] = {
{
{ "Energy Shield:" },
{ "output", "Spec +:", "spec_energyShieldBase" },
{ "output", "Spec %:", "spec_energyShieldInc" },
{ "output", "Gear +:", "total_gear_energyShieldBase" },
{ "output", "Gear %:", "gear_energyShieldInc" },
{ "output", "Total:", "total_energyShield", formatRound },
{ "output", "Recharge rate:", "total_energyShieldRecharge", getFormatRound(1) },
{ "output", "Recharge delay:", "total_energyShieldRechargeDelay", formatSec },
{ },
{ "Evasion:" },
{ "output", "Spec +:", "spec_evasionBase" },
{ "output", "Spec %:", "spec_evasionInc" },
{ "output", "Gear +:", "total_gear_evasionBase" },
{ "output", "Gear %:", "gear_evasionInc" },
{ "output", "Total:", "total_evasion", formatRound },
{ },
{ "Armour:" },
{ "output", "Spec +:", "spec_armourBase" },
{ "output", "Spec %:", "spec_armourInc" },
{ "output", "Gear +:", "total_gear_armourBase" },
{ "output", "Gear %:", "gear_armourInc" },
{ "output", "Total:", "total_armour", formatRound },
{ },
{ "Misc:" },
{ "input", "Normal Bandit:", "misc_banditNormal", "choice", 1, { "None", "Alira", "Kraityn", "Oak" } },
{ "input", "Cruel Bandit:", "misc_banditCruel", "choice", 1, { "None", "Alira", "Kraityn", "Oak" } },
{ "input", "Merciless Bandit:", "misc_banditMerc", "choice", 1, { "None", "Alira", "Kraityn", "Oak" } },
{ "input", "Always on Low Life?", "cond_LowLife", "check" },
{ "input", "Always on Full Life?", "cond_FullLife", "check" },
}
}
columns[5] = {
{
{ "Buffs:" },
{ "input", "Power Charges?", "buff_power", "check" },
}, {
flag = "havePower",
{ "output", "Max Power:", "powerMax" },
}, {
{ "input", "Frenzy Charges?", "buff_frenzy", "check" },
}, {
flag = "haveFrenzy",
{ "output", "Max Frenzy:", "frenzyMax" },
}, {
{ "input", "Endurance Charges?", "buff_endurance", "check" },
}, {
flag = "haveEndurance",
{ "output", "Max Endurance:", "enduranceMax" },
}, {
{ "input", "Onslaught?", "condBuff_Onslaught", "check" },
{ "input", "Fortify?", "condBuff_Fortify", "check" },
{ "input", "Using a Flask?", "condBuff_UsingFlask", "check" },
}, {
{ },
{ "For Effective DPS:" },
{ "input", "Enemy is Bleeding?", "condEff_EnemyBleeding", "check" },
{ "input", "Enemy is Poisoned?", "condEff_EnemyPoisoned", "check" },
{ "input", "Enemy is Burning?", "condEff_EnemyBurning", "check" },
{ "input", "Enemy is Ignited?", "condEff_EnemyIgnited", "check" },
{ "input", "Enemy is Chilled?", "condEff_EnemyChilled", "check" },
{ "input", "Enemy is Frozen?", "condEff_EnemyFrozen", "check" },
{ "input", "Enemy is Shocked?", "condEff_EnemyShocked", "check" },
{ "input", "Enemy Elem. Resist:", "effective_elemResist" },
{ },
{ "Crit Chance:" },
}, {
flag = "attack",
{ "output", "Weapon Crit %:", "gear_weap1_critChanceBase" },
}, {
{ "output", "Spec Global Crit %:", "spec_critChanceInc" },
{ "output", "Gear Global Crit %:", "gear_global_critChanceInc" },
}, {
flag = "spell",
{ "output", "Spec Spell Crit %:", "spec_spell_critChanceInc" },
{ "output", "Gear Spell Crit %:", "gear_spell_critChanceInc" },
}, {
flag = "melee",
{ "output", "Spec Melee Crit %:", "spec_melee_critChanceInc" },
}, {
flag = "totem",
{ "output", "Spec Totem Crit %:", "spec_totem_critChanceInc" },
}, {
flag = "trap",
{ "output", "Spec Trap Crit %:", "spec_trap_critChanceInc" },
}, {
flag = "mine",
{ "output", "Spec Mine Crit %:", "spec_mine_critChanceInc" },
}, {
{ "output", "Crit Chance:", "total_critChance", getFormatPercent(2) },
{ "output", "Spec Global Multi %:", "spec_critMultiplier" },
{ "output", "Gear Global Multi %:", "gear_critMultiplier" },
}, {
flag = "spell",
{ "output", "Spec Spell Multi %:", "spec_spell_critMultiplier" },
}, {
flag = "melee",
{ "output", "Spec Melee Multi %:", "spec_melee_critMultiplier" },
}, {
flag = "totem",
{ "output", "Spec Totem Multi %:", "spec_totem_critMultiplier" },
}, {
flag = "trap",
{ "output", "Spec Trap Multi %:", "spec_trap_critMultiplier" },
}, {
flag = "mine",
{ "output", "Spec Mine Multi %:", "spec_mine_critMultiplier" },
}, {
{ "output", "Multiplier:", "total_critMultiplier", formatPercent },
}, {
flag = "attack",
{ },
{ "Accuracy:" },
{ "output", "Spec Accuracy+:", "spec_accuracyBase" },
{ "output", "Spec Accuracy %:", "spec_accuracyInc" },
{ "output", "Gear Accuracy+:", "gear_accuracyBase" },
{ "output", "Gear Accuracy %:", "gear_accuracyInc" },
{ "output", "Total Accuracy:", "total_accuracy", formatRound },
{ "input", "Use Monster Level?", "misc_hitMonsterLevel", "check" },
{ "output", "Chance to Hit:", "total_hitChance", formatPercent },
}, {
{ },
{ "Stun:" },
{ "output", "Stun Duration on You:", "stun_duration", formatSec },
{ "output", "Block Duration on You:", "stun_blockDuration", formatSec },
{ "output", "Duration on Enemies:", "stun_enemyDuration", formatSec },
{ "output", "Enemy Threshold Mod:", "stun_enemyThresholdMod", formatPercent },
}
}
columns[7] = {
{
{ "input", "Skill:", "skill_spec", "string", 7 },
}, {
flag = "multiPart",
{ "input", "Skill Part #:", "skill_part" },
{ "output", "Part:", "skill_partName", "string", 2 },
}, {
{ },
{ "input", "Mode:", "misc_buffMode", "choice", 2, { "Unbuffed", "With buffs", "Effective DPS with buffs" } },
{ },
}, {
flag = "attack",
{ { "Attack:", "Physical", "Lightning", "Cold", "Fire", "Chaos", "Combined", "Elemental" } },
}, {
flag = "weapon1Attack",
{ "output", "Main Hand:", "gear_weapon1_name", "string", 3 },
{ "output", "Weapon Min:", fieldNames("gear_weapon1", "Min", "plcfh") },
{ "output", "Weapon Max:", fieldNames("gear_weapon1", "Max", "plcfh") },
{ "output", "Weapon APS:", "gear_weapon1_attackRate" },
{ "output", "Weapon DPS:", fieldNames("weapon1", "DPS", "plcfhae"), getFormatRound(2) },
}, {
flag = "weapon2Attack",
{ "output", "Off Hand:", "gear_weapon2_name", "string", 3 },
{ "output", "Weapon Min:", fieldNames("gear_weapon2", "Min", "plcfh") },
{ "output", "Weapon Max:", fieldNames("gear_weapon2", "Max", "plcfh") },
{ "output", "Weapon APS:", "gear_weapon2_attackRate" },
{ "output", "Weapon DPS:", fieldNames("weaponon2", "DPS", "plcfhae"), getFormatRound(2) },
}, {
flag = "attack",
{ "output", "Spec Attack Dmg %:", fieldNames("spec_attack", "Inc", "pa") },
{ "output", "Spec Weapon Dmg %:", fieldNames("spec_weapon", "Inc", "plcfae") },
{ "output", "Gear Weapon Dmg %:", fieldNames("gear_weapon", "Inc", "plcfae") },
}, {
flag = "spell",
{ { "Spell:", "Physical", "Lightning", "Cold", "Fire", "Chaos", "Combined", "Elemental" } },
{ "output", "Spec Spell Dmg %:", fieldNames("spec_spell", "Inc", "a") },
{ "output", "Gear Spell Dmg %:", fieldNames("gear_spell", "Inc", "a") },
}, {
flag = "projectile",
{ "output", "Spec Projectile Dmg %:", fieldNames("spec_projectile", "Inc", "a") },
{ "output", "Gear Projectile Dmg %:", fieldNames("gear_projectile", "Inc", "a") },
}, {
flag = "aoe",
{ "output", "Spec Area Dmg %:", fieldNames("spec_aoe", "Inc", "a") },
{ "output", "Gear Area Dmg %:", fieldNames("gear_aoe", "Inc", "a") },
}, {
flag = "totem",
{ "output", "Spec Totem Dmg %:", fieldNames("spec_totem", "Inc", "a") },
{ "output", "Gear Totem Dmg %:", fieldNames("gear_totem", "Inc", "a") },
}, {
flag = "trap",
{ "output", "Spec Trap Dmg %:", fieldNames("spec_trap", "Inc", "a") },
{ "output", "Gear Trap Dmg %:", fieldNames("gear_trap", "Inc", "a") },
}, {
flag = "mine",
{ "output", "Spec Mine Dmg %:", fieldNames("spec_mine", "Inc", "a") },
{ "output", "Gear Mine Dmg %:", fieldNames("gear_mine", "Inc", "a") },
}, {
{ "output", "Spec Global %:", fieldNames("spec", "Inc", "plcfhe") },
{ "output", "Gear Global %:", fieldNames("gear", "Inc", "plcfhae") },
}, {
flag = "attack",
{ "output", "Gear Attack Min+:", fieldNames("gear_attack", "Min", "plcfh") },
{ "output", "Gear Attack Max+:", fieldNames("gear_attack", "Max", "plcfh") },
}, {
flag = "spell",
{ "output", "Gear Spell Min+:", fieldNames("gear_spell", "Min", "plcfh") },
{ "output", "Gear Spell Max+:", fieldNames("gear_spell", "Max", "plcfh") },
}, {
flag = "attack",
{ "output", "Spec Attack Speed %:", "spec_attackSpeedInc" },
{ "output", "Gear Attack Speed %:", "gear_attackSpeedInc" },
{ "output", "Spec Attack&Cast Sp. %:", "spec_speedInc" },
{ "output", "Gear Attack&Cast Sp. %:", "gear_speedInc" },
{ "output", "Attack Damage:", fieldNames("total", "", "plcfha") },
{ "output", "Average Damage:", "total_avg", getFormatRound(1) },
{ "output", "Attack Speed:", "total_speed", getFormatRound(2) },
{ "output", "Attack Time:", "total_time", getFormatRound(2) },
{ "output", "Attack DPS:", "total_dps", getFormatRound(1) },
}, {
flag = "spell",
{ "output", "Spec Cast Speed %:", "spec_castSpeedInc" },
{ "output", "Gear Cast Speed %:", "gear_castSpeedInc" },
{ "output", "Spec Attack&Cast Sp. %:", "spec_speedInc" },
{ "output", "Gear Attack&Cast Sp. %:", "gear_speedInc" },
{ "output", "Spell Damage:", fieldNames("total", "", "plcfha") },
{ "output", "Average Damage:", "total_avg", getFormatRound(1) },
{ "output", "Cast Rate:", "total_speed", getFormatRound(2) },
{ "output", "Cast Time:", "total_time", getFormatRound(2) },
{ "output", "Spell DPS:", "total_dps", getFormatRound(1) },
}, {
flag = "cast",
{ "output", "Secondary Damage:", fieldNames("total", "", "plcfha") },
{ "output", "Average Damage:", "total_avg", getFormatRound(1) },
}, {
{ "output", "Mana Cost:", "total_manaCost", formatRound }
}, {
flag = "projectile",
{ "output", "Spec Pierce Chance %:", "spec_pierceChance" },
{ "output", "Gear Pierce Chance %:", "gear_pierceChance" },
{ "output", "Pierce Chance:", "total_pierce", formatPercent },
}, {
flag = "duration",
{ "output", "Spec Duration %:", "spec_durationInc" },
{ "output", "Skill Duration:", "total_duration", formatSec },
}, {
flag = "trap",
{ "output", "Trap Cooldown:", "total_trapCooldown", formatSec },
}, {
flag = "dot",
{ "output", "Spec DoT Dmg %:", fieldNames("spec_dot", "Inc", "pfa") },
{ "output", "Gear DoT Dmg %:", fieldNames("gear_dot", "Inc", "pfa") },
{ "output", "DoT:", fieldNames("total", "Dot", "plcfh"), getFormatRound(1) },
}, {
flag = "canBleed",
{ "output", "Spec Bleed Chance %:", "spec_bleedChance" },
{ "output", "Gear Bleed Chance %:", "gear_bleedChance" },
{ "input", "Other Bleed Chance %:", "other_bleedChance" },
}, {
flag = "bleed",
{ "output", "Bleed Chance:", "bleed_chance", formatPercent },
{ "output", "Bleed DPS:", "bleed_dps", getFormatRound(1) },
{ "output", "Bleed Duration:", "bleed_duration", formatSec },
}, {
flag = "canPoison",
{ "output", "Spec Poison Chance %:", "spec_poisonChance" },
{ "output", "Gear Poison Chance %:", "gear_poisonChance" },
{ "input", "Other Poison Chance %:", "other_poisonChance" },
}, {
flag = "poison",
{ "output", "Spec Poison Dmg %:", "spec_poison_damageInc" },
{ "output", "Poison Chance:", "poison_chance", formatPercent },
{ "output", "Poison DPS:", "poison_dps", getFormatRound(1) },
{ "output", "Poison Duration:", "poison_duration", formatSec },
}, {
flag = "canIgnite",
{ "output", "Spec Ignite Chance %:", "spec_igniteChance" },
{ "output", "Gear Ignite Chance %:", "gear_igniteChance" },
{ "input", "Other Ignite Chance %:", "other_igniteChance" },
}, {
flag = "ignite",
{ "output", "Ignite Chance:", "ignite_chance", formatPercent },
{ "output", "Ignite DPS:", "ignite_dps", getFormatRound(1) },
{ "output", "Ignite Duration:", "ignite_duration", formatSec },
}
}
local curFlags
return function(newFlags)
if curFlags then
local noNewFlags = true
local sub = copyTable(curFlags)
for flag in pairs(newFlags) do
if curFlags[flag] then
sub[flag] = nil
else
noNewFlags = false
break
end
end
if noNewFlags and not next(sub) then
return
end
end
curFlags = copyTable(newFlags)
grid:Clear()
for colX, colTables in pairs(columns) do
local y = 1
for _, data in ipairs(colTables) do
if not data.flag or curFlags[data.flag] then
mkFieldTable(colX, y, data)
y = y + #data
end
end
end
for col, width in ipairs(columnWidths) do
grid:SetColWidth(col, width)
end
end

94
Modules/Data.lua Normal file
View File

@@ -0,0 +1,94 @@
data = { }
data.gems = { }
data.gems["_default"] = {
attack = true,
melee = true,
base = {
},
quality = {
},
levels = {
[1] = { }
},
}
LoadModule("Gems/act_str", data.gems)
LoadModule("Gems/act_dex", data.gems)
LoadModule("Gems/act_int", data.gems)
LoadModule("Gems/sup_str", data.gems)
LoadModule("Gems/sup_dex", data.gems)
LoadModule("Gems/sup_int", data.gems)
data.colorCodes = {
NORMAL = "^xC8C8C8",
MAGIC = "^x8888FF",
RARE = "^xFFFF77",
UNIQUE = "^xAF6025",
FIRE = "^x960000",
COLD = "^x366492",
LIGHT = "^xFFD700",
CHAOS = "^xD02090",
}
data.jewelRadius = {
{ rad = 800, col = "^xFFBB33", label = "Small" },
{ rad = 1200, col = "^x33FF66", label = "Medium" },
{ rad = 1500, col = "^x3333FF", label = "Large" }
}
data.evasionTable = { 36, 42, 49, 56, 64, 72, 80, 89, 98, 108,
118, 128, 140, 151, 164, 177, 190, 204, 219, 235,
251, 268, 286, 305, 325, 345, 367, 389, 412, 437,
463, 489, 517, 546, 577, 609, 642, 676, 713, 750,
790, 831, 873, 918, 964, 1013, 1063, 1116, 1170, 1227,
1287, 1349, 1413, 1480, 1550, 1623, 1698, 1777, 1859, 1944,
2033, 2125, 2221, 2321, 2425, 2533, 2645, 2761, 2883, 3009,
3140, 3276, 3418, 3565, 3717, 3876, 4041, 4213, 4391, 4574,
4767, 4969, 5179, 5398 }
data.weaponTypeInfo = {
["None"] = { oneHand = true, melee = true, space = "unarmed" },
["Bow"] = { oneHand = false, melee = false, space = "bow" },
["Claw"] = { oneHand = true, melee = true, space = "claw" },
["Dagger"] = { oneHand = true, melee = true, space = "dagger" },
["Staff"] = { oneHand = false, melee = true, space = "staff" },
["Wand"] = { oneHand = true, melee = false, space = "wand" },
["One Handed Axe"] = { oneHand = true, melee = true, space = "axe" },
["One Handed Mace"] = { oneHand = true, melee = true, space = "mace" },
["One Handed Sword"] = { oneHand = true, melee = true, space = "sword" },
["Two Handed Axe"] = { oneHand = false, melee = true, space = "axe" },
["Two Handed Mace"] = { oneHand = false, melee = true, space = "mace" },
["Two Handed Sword"] = { oneHand = false, melee = true, space = "sword" },
}
data.unarmedWeap = {
[0] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 6 }, -- Scion
[1] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 8 }, -- Marauder
[2] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 5 }, -- Ranger
[3] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 5 }, -- Witch
[4] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 6 }, -- Duelist
[5] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 6 }, -- Templar
[6] = { attackRate = 1.2, critChanceBase = 0, physicalMin = 2, physicalMax = 5 }, -- Shadow
}
data.itemBases = { }
local itemTypes = {
"axe",
"bow",
"claw",
"dagger",
"mace",
"staff",
"sword",
"wand",
"helm",
"body",
"glove",
"boots",
"shield",
"misc"
}
for _, type in pairs(itemTypes) do
LoadModule("items/"..type, data.itemBases)
end

533
Modules/Items.lua Normal file
View File

@@ -0,0 +1,533 @@
local launch, cfg, main = ...
local t_insert = table.insert
local m_floor = math.floor
local s_format = string.format
local items = { }
local function applyRange(line, range)
return line:gsub("%((%d+)%-(%d+) to (%d+)%-(%d+)%)", function(minMin, maxMin, minMax, maxMax) return string.format("%d-%d", tonumber(minMin) + range * (tonumber(minMax) - tonumber(minMin)), tonumber(maxMin) + range * (tonumber(maxMax) - tonumber(maxMin))) end)
:gsub("%((%d+) to (%d+)%)", function(min, max) return tostring(tonumber(min) + range * (tonumber(max) - tonumber(min))) end)
end
items.slots = { }
items.controls = { }
items.controls.addDisplayItem = common.newButton(0, 0, 60, 20, "Add", function()
items:AddDisplayItem()
end)
local function mkItemSlot(x, y, slotName, slotLabel)
local slot = { }
slot.items = { }
slot.list = { }
slot.x = x
slot.y = y
slot.width = 320
slot.height = 20
slot.slotName = slotName
slot.label = slotLabel or slotName
slot.dropDown = common.newDropDown(x, y, slot.width, slot.height, slot.list, function(sel)
if slot.items[sel] ~= slot.selItem then
slot.selItem = slot.items[sel]
items:PopulateSlots()
items.buildFlag = true
items.modFlag = true
end
end)
function slot:Populate()
wipeTable(self.items)
wipeTable(self.list)
self.items[1] = 0
self.list[1] = "None"
self.dropDown.sel = 1
for _, item in ipairs(items.list) do
if items:IsItemValidForSlot(item, slotName) then
t_insert(self.items, item.id)
t_insert(self.list, data.colorCodes[item.rarity]..item.name)
if item.id == self.selItem then
self.dropDown.sel = #self.list
end
end
end
if not self.selItem or not items.list[self.selItem] or not items:IsItemValidForSlot(items.list[self.selItem], slotName) then
self.selItem = 0
end
end
function slot:Draw(viewPort)
self.dropDown.x = viewPort.x + self.x
self.dropDown.y = viewPort.y + self.y
DrawString(self.dropDown.x - 2, self.dropDown.y + 2, "RIGHT_X", self.height - 4, "VAR", "^7"..slot.label..":")
self.dropDown:Draw()
if self.dropDown:IsMouseOver() then
local ttItem
if self.dropDown.dropped then
if self.dropDown.hoverSel then
ttItem = items.list[self.items[self.dropDown.hoverSel]]
end
elseif self.selItem and not items.selControl then
ttItem = items.list[self.selItem]
end
if ttItem then
items:AddItemTooltip(ttItem)
main:DrawTooltip(self.dropDown.x, self.dropDown.y, self.width, self.height, viewPort, data.colorCodes[ttItem.rarity], true)
end
end
end
function slot:IsMouseOver()
return self.dropDown:IsMouseOver()
end
function slot:OnKeyDown(key)
return self.dropDown:OnKeyDown(key)
end
function slot:OnKeyUp(key)
return self.dropDown:OnKeyUp(key)
end
items.slots[slotName] = slot
return slot
end
local baseSlots = { "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Belt", "Weapon 1", "Weapon 2" }
for index, slotName in pairs(baseSlots) do
t_insert(items.controls, mkItemSlot(400, (index - 1) * 20, slotName))
end
function items:IsItemValidForSlot(item, slotName)
if item.type == slotName:gsub(" %d+","") then
return true
elseif slotName == "Weapon 1" or slotName == "Weapon" then
return data.itemBases[item.baseName].weapon ~= nil
elseif slotName == "Weapon 2" then
local weapon1Sel = self.slots["Weapon 1"].selItem
local weapon1Type = weapon1Sel > 0 and data.itemBases[self.list[weapon1Sel].baseName].type or "None"
if weapon1Type == "Bow" then
return item.type == "Quiver"
elseif data.weaponTypeInfo[weapon1Type].oneHand then
return item.type == "Shield" or (data.weaponTypeInfo[item.type] and data.weaponTypeInfo[item.type].oneHand and (weapon1Type == "None" or (weapon1Type == "Wand" and item.type == "Wand") or (weapon1Type ~= "Wand" and item.type ~= "Wand")))
end
end
end
function items:PopulateSlots()
for _, slot in pairs(self.slots) do
slot:Populate()
end
end
function items:UpdateJewels()
local spec = self.build.spec
for nodeId, slot in pairs(self.sockets) do
if not spec.allocNodes[nodeId] then
slot.inactive = true
self.controls["socket"..nodeId] = nil
end
end
local socketList = { }
for nodeId, node in pairs(spec.allocNodes) do
if node.type == "socket" then
t_insert(socketList, nodeId)
end
end
table.sort(socketList)
for index, nodeId in pairs(socketList) do
local slot = self.sockets[nodeId]
self.controls["socket"..nodeId] = slot
slot.inactive = false
slot.y = (#baseSlots + index - 1) * 20
end
end
function items:GetSocketJewel(nodeId)
return self.sockets[nodeId], self.list[self.sockets[nodeId].selItem]
end
function items:ParseItemRaw(item)
if not item.id then
item.id = 1
while self.list[item.id] do
item.id = item.id + 1
end
end
item.name = "?"
item.rarity = "UNIQUE"
item.rawLines = { }
for line in string.gmatch(item.raw .. "\r\n", "([^\r\n]*)\r?\n") do
line = line:gsub("^%s+",""):gsub("%s+$","")
if #line > 0 then
t_insert(item.rawLines, line)
end
end
local mode = "WIKI"
local l = 1
if item.rawLines[l] then
local rarity = item.rawLines[l]:match("^Rarity: (%a+)")
if rarity then
mode = "GAME"
item.rarity = rarity:upper()
l = l + 1
end
end
if item.rawLines[l] then
item.name = item.rawLines[l]
l = l + 1
end
if item.rarity == "NORMAL" or item.rarity == "MAGIC" then
for baseName, baseData in pairs(data.itemBases) do
if item.name:find(baseName, 1, true) then
item.baseName = baseName
item.type = baseData.type
break
end
end
elseif item.rawLines[l] and not item.rawLines[l]:match("^%-") and data.itemBases[item.rawLines[l]] then
item.baseName = item.rawLines[l]
item.title = item.name
item.name = item.title .. ", " .. item.baseName
item.type = data.itemBases[item.baseName].type
end
item.modLines = { }
while item.rawLines[l] do
local line = item.rawLines[l]
if data.weaponTypeInfo[line] then
item.weaponType = line
else
local specName, specVal = line:match("^([%a ]+): %+?([%d%-%.]+)")
if not specName then
specName, specVal = line:match("^([%a ]+): (.+)")
end
if specName then
if specName == "Radius" and item.type == "Jewel" then
for index, data in pairs(data.jewelRadius) do
if specVal == data.label then
item.radius = index
break
end
end
end
else
local rangedLine
if line:match("%(%d+%-%d+ to %d+%-%d+%)") or line:match("%(%d+ to %d+%)") then
rangedLine = applyRange(line, 1)
end
local modList, extra = mod.parseMod(rangedLine or line)
if modList then
t_insert(item.modLines, { line = line, extra = extra, mods = modList, range = rangedLine and 1 })
end
end
end
l = l + 1
end
self:BuildItemModList(item)
end
function items:BuildItemModList(item)
local modList = { }
item.modList = modList
for _, modLine in ipairs(item.modLines) do
if not modLine.extra then
if modLine.range then
local line = applyRange(modLine.line, modLine.range)
local list, extra = mod.parseMod(line)
if list and not extra then
mod.mods = list
end
end
for k, v in pairs(modLine.mods) do
mod.listMerge(modList, k, v)
end
end
end
local base = data.itemBases[item.baseName]
if not base then
return
end
if base.weapon then
modList.weaponX_type = base.type
modList.weaponX_name = item.name
for _, elem in pairs({"physical","lightning","cold","fire","chaos"}) do
local min = (base.weapon[elem.."Min"] or 0) + (modList["attack_"..elem.."Min"] or 0)
local max = (base.weapon[elem.."Max"] or 0) + (modList["attack_"..elem.."Max"] or 0)
if elem == "physical" then
if modList.weaponNoPhysical then
min, max = 0, 0
else
min = m_floor(min * (1 + (modList["physicalInc"] or 0) / 100 + .2) + 0.5)
max = m_floor(max * (1 + (modList["physicalInc"] or 0) / 100 + .2) + 0.5)
end
modList["physicalInc"] = nil
end
if min > 0 and max > 0 then
modList["weaponX_"..elem.."Min"] = min
modList["weaponX_"..elem.."Max"] = max
end
modList["attack_"..elem.."Min"] = nil
modList["attack_"..elem.."Max"] = nil
end
modList.weaponX_attackRate = m_floor(base.weapon.attackRateBase * (1 + (modList.attackSpeedInc or 0) / 100) * 100 + 0.5) / 100
modList.attackSpeedInc = nil
if modList.weaponAlwaysCrit then
modList.weaponX_critChanceBase = 100
else
modList.weaponX_critChanceBase = m_floor(base.weapon.critChanceBase * (1 + (modList.critChanceInc or 0) / 100) * 100 + 0.5) / 100
end
modList.critChanceInc = nil
elseif base.armour then
if base.type == "Shield" then
modList.weaponX_type = "Shield"
end
if base.armour.armourBase then
modList.armourBase = m_floor((base.armour.armourBase + (modList.armourBase or 0)) * (1 + ((modList.armourInc or 0) + (modList.armourAndEvasionInc or 0) + (modList.armourAndESInc or 0) + 20) / 100) + 0.5)
end
if base.armour.evasionBase then
modList.evasionBase = m_floor((base.armour.evasionBase + (modList.evasionBase or 0)) * (1 + ((modList.evasionInc or 0) + (modList.armourAndEvasionInc or 0) + (modList.evasionAndEnergyShieldInc or 0) + 20) / 100) + 0.5)
end
if base.armour.energyShieldBase then
modList.energyShieldBase = m_floor((base.armour.energyShieldBase + (modList.energyShieldBase or 0)) * (1 + ((modList.energyShieldInc or 0) + (modList.armourAndEnergyShieldInc or 0) + (modList.evasionAndEnergyShieldInc or 0) + 20) / 100) + 0.5)
end
if base.armour.blockChance then
if modList.shieldNoBlock then
modList.blockChance = 0
else
modList.blockChance = base.armour.blockChance + (modList.blockChance or 0)
end
end
modList.armourInc = nil
modList.evasionInc = nil
modList.energyShieldInc = nil
modList.armourAndEvasionInc = nil
modList.armourAndESInc = nil
modList.evasionAndEnergyShieldInc = nil
elseif item.type == "Jewel" then
item.jewelFunc = modList.jewelFunc
modList.jewelFunc = nil
end
end
function items:AddItemTooltip(item)
local rarityCode = data.colorCodes[item.rarity]
if item.title then
main:AddTooltipLine(20, rarityCode..item.title)
main:AddTooltipLine(20, rarityCode..item.baseName)
else
main:AddTooltipLine(20, rarityCode..item.name)
end
local base = data.itemBases[item.baseName]
modList = item.modList
if base.weapon then
main:AddTooltipSeperator(10)
main:AddTooltipLine(16, s_format("^x7F7F7F%s", base.type))
main:AddTooltipLine(16, "^x7F7F7FQuality: "..data.colorCodes.MAGIC.."+20%")
if modList.weaponX_physicalMin then
main:AddTooltipLine(16, s_format("^x7F7F7FPhysical Damage: "..data.colorCodes.MAGIC.."%d-%d", modList.weaponX_physicalMin, modList.weaponX_physicalMax))
end
local elemLine
for _, var in ipairs({"fire","cold","lightning"}) do
if modList["weaponX_"..var.."Min"] then
elemLine = elemLine and elemLine.."^x7F7F7F, " or "^x7F7F7FElemental Damage: "
elemLine = elemLine..s_format("%s%d-%d", data.colorCodes[var:upper()], modList["weaponX_"..var.."Min"], modList["weaponX_"..var.."Max"])
end
end
if elemLine then
main:AddTooltipLine(16, elemLine)
end
if modList.weaponX_chaosMin then
main:AddTooltipLine(16, s_format("^x7F7F7FChaos Damage: "..data.colorCodes.CHAOS.."%d-%d", modList.weaponX_chaosMin, modList.weaponX_chaosMax))
end
main:AddTooltipLine(16, s_format("^x7F7F7FCritical Strike Chance: %s%.2f%%", modList.weaponX_critChanceBase ~= base.weapon.critChanceBase and data.colorCodes.MAGIC or "^7", modList.weaponX_critChanceBase))
main:AddTooltipLine(16, s_format("^x7F7F7FAttacks per Second: %s%.2f", modList.weaponX_attackRate ~= base.weapon.attackRateBase and data.colorCodes.MAGIC or "^7", modList.weaponX_attackRate))
elseif base.armour then
main:AddTooltipSeperator(10)
main:AddTooltipLine(16, "^x7F7F7FQuality: "..data.colorCodes.MAGIC.."+20%")
if base.armour.blockChance and modList.blockChance > 0 then
main:AddTooltipLine(16, s_format("^x7F7F7FChance to Block: %s%d%%", modList.blockChance ~= base.armour.blockChance and data.colorCodes.MAGIC or "^7", modList.blockChance))
end
for _, def in ipairs({{var="armour",label="Armour"},{var="evasion",label="Evasion Rating"},{var="energyShield",label="Energy Shield"}}) do
local itemVal = modList[def.var.."Base"]
if itemVal then
main:AddTooltipLine(16, s_format("^x7F7F7F%s: %s%d", def.label, itemVal ~= base.armour[def.var.."Base"] and data.colorCodes.MAGIC or "^7", itemVal))
end
end
elseif item.radius then
main:AddTooltipSeperator(10)
main:AddTooltipLine(16, "^x7F7F7FRadius: ^7"..data.jewelRadius[item.radius].label)
end
if item.modLines[1] then
main:AddTooltipSeperator(10)
for index, modLine in pairs(item.modLines) do
local line = modLine.range and applyRange(modLine.line, modLine.range) or modLine.line
main:AddTooltipLine(16, (modLine.extra and data.colorCodes.NORMAL or data.colorCodes.MAGIC)..line)
if index == 1 and base.implicit and item.modLines[2] then
main:AddTooltipSeperator(10)
end
end
end
self:UpdateJewels()
for slotName, slot in pairs(self.slots) do
local selItem = self.list[slot.selItem]
if items:IsItemValidForSlot(item, slotName) and not slot.inactive and (item ~= selItem or item.type == "Jewel") then
local calcFunc, calcBase = self.build.calcs:GetItemCalculator()
if calcFunc then
local output = calcFunc(slotName, item ~= selItem and item)
local header = false
for _, statData in ipairs(self.build.displayStats) do
if statData.mod then
local diff = (output[statData.mod] or 0) - (calcBase[statData.mod] or 0)
if diff > 0.001 or diff < -0.001 then
if not header then
main:AddTooltipSeperator(14)
if item == selItem then
main:AddTooltipLine(14, "^7Removing this jewel will give you:")
else
main:AddTooltipLine(14, string.format("^7Equipping this item in %s%s will give you:", slot.label, selItem and " (replacing "..data.colorCodes[selItem.rarity]..selItem.name.."^7)" or ""))
end
header = true
end
main:AddTooltipLine(14, string.format("%s%+"..statData.fmt.." %s", diff > 0 and "^x00FF44" or "^xFF3300", diff * (statData.pc and 100 or 1), statData.label))
end
end
end
end
end
end
if IsKeyDown("ALT") then
main:AddTooltipSeperator(10)
local nameList = { }
for k in pairs(modList) do
t_insert(nameList, k)
end
table.sort(nameList)
for _, name in ipairs(nameList) do
main:AddTooltipLine(16, "^7"..name.." = "..tostring(modList[name]))
end
end
end
function items:AddDisplayItem()
for _, item in pairs(self.list) do
if item.raw == self.displayItem.raw then
self.displayItem = nil
return
end
end
if not self.list[self.displayItem.id] then
t_insert(self.orderList, self.displayItem.id)
end
self.list[self.displayItem.id] = self.displayItem
self.displayItem = nil
self:PopulateSlots()
end
function items:Init(build)
self.build = build
self.list = { }
self.sockets = { }
for _, node in pairs(main.tree.nodes) do
if node.type == "socket" then
self.sockets[node.id] = mkItemSlot(400, 0, "Jewel "..node.id, "Socket")
end
end
self.orderList = { }
end
function items:Shutdown()
end
function items:Load(xml, dbFileName)
for _, node in ipairs(xml) do
if node.elem == "Item" then
local item = { }
item.raw = ""
item.id = tonumber(node.attrib.id)
self:ParseItemRaw(item)
for _, child in ipairs(node) do
if type(child) == "string" then
item.raw = child
self:ParseItemRaw(item)
elseif child.elem == "ModRange" then
local id = tonumber(child.attrib.id) or 0
local range = tonumber(child.attrib.range) or 1
if item.modLines[id] then
item.modLines[id].range = range
end
end
end
self:BuildItemModList(item)
self.list[item.id] = item
t_insert(self.orderList, item.id)
elseif node.elem == "Slot" then
if self.slots[node.attrib.name or ""] then
self.slots[node.attrib.name].selItem = tonumber(node.attrib.itemId)
end
end
end
self:PopulateSlots()
end
function items:Save(xml)
self.modFlag = false
for _, id in ipairs(self.orderList) do
local item = self.list[id]
local child = { elem = "Item", attrib = { id = tostring(id) } }
t_insert(child, item.raw)
for id, modLine in ipairs(item.modLines) do
if modLine.range then
t_insert(child, { elem = "ModRange", attrib = { id = tostring(id), range = tostring(modLine.range) } })
end
end
t_insert(xml, child)
end
for name, slot in pairs(self.slots) do
t_insert(xml, { elem = "Slot", attrib = { name = name, itemId = tostring(slot.selItem) }})
end
end
function items:DrawItems(viewPort, inputEvents)
common.controlsInput(self, inputEvents)
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "v" and IsKeyDown("CTRL") then
local newItem = Paste()
if newItem then
self.displayItem = {
raw = newItem:gsub("^%s+",""):gsub("%s+$",""):gsub("<EFBFBD>","-"):gsub("%b<>",""):gsub("<EFBFBD>","o")
}
self:ParseItemRaw(self.displayItem)
if not self.displayItem.baseName then
self.displayItem = nil
end
end
end
end
end
if self.displayItem then
self.controls.addDisplayItem.x = viewPort.x + viewPort.width - 530
self.controls.addDisplayItem.y = viewPort.y + 4
self.controls.addDisplayItem.hidden = false
self.controls.addDisplayItem.label = self.list[self.displayItem.id] and "Save" or "Add"
self:AddItemTooltip(self.displayItem)
main:DrawTooltip(viewPort.x + viewPort.width - 500, viewPort.y + 28, nil, nil, viewPort, data.colorCodes[self.displayItem.rarity], true)
else
self.controls.addDisplayItem.hidden = true
end
self:UpdateJewels()
common.controlsDraw(self, viewPort)
for index, id in pairs(self.orderList) do
local item = self.list[id]
local rarityCode = data.colorCodes[item.rarity]
SetDrawColor(rarityCode)
local x = viewPort.x + 2
local y = viewPort.y + 2 + 16 * (index - 1)
DrawString(x, y, "LEFT", 16, "VAR", item.name)
local cx, cy = GetCursorPos()
if cx >= x and cx < x + 250 and cy >= y and cy < y + 16 then
self:AddItemTooltip(item)
main:DrawTooltip(x, y, 250, 16, viewPort, rarityCode, true)
end
end
end
return items

311
Modules/List.lua Normal file
View File

@@ -0,0 +1,311 @@
local launch, cfg, main = ...
local vfs = require("vfs")
local listMode = { }
listMode.controls = {
common.newButton(2, 2, 60, 20, "New", function()
listMode:New()
end),
common.newButton(66, 2, 60, 20, "Copy", function()
listMode:CopySel()
end, function()
return listMode.sel ~= nil
end),
common.newButton(130, 2, 60, 20, "Rename", function()
listMode:RenameSel()
end, function()
return listMode.sel ~= nil
end),
common.newButton(194, 2, 60, 20, "Delete", function()
listMode:DeleteSel()
end, function()
return listMode.sel ~= nil
end),
}
function listMode:BuildList()
self.list = { }
vfs.scan(true)
for _, file in ipairs(vfs.root.files) do
if file.name:lower():match("%.xml$") and file.name:lower() ~= "settings.xml" then
local build = { }
build.fileName = file.name
local dbXML, errMsg = common.xml.LoadXMLFile(file.name)
if dbXML and dbXML[1].elem == "PathOfBuilding" then
for _, node in ipairs(dbXML[1]) do
if type(node) == "table" and node.elem == "Build" then
build.className = node.attrib.className
build.ascendClassName = node.attrib.ascendClassName
build.level = tonumber(node.attrib.level) or 1
end
end
end
table.insert(self.list, build)
end
end
self:SortList()
end
function listMode:SortList()
local oldSelFileName = self.sel and self.list[self.sel].fileName
table.sort(self.list, function(a, b) return a.fileName:upper() < b.fileName:upper() end)
if oldSelFileName then
self:SelFileByName(oldSelFileName)
end
end
function listMode:EditInit(finFunc)
self.edit = self.sel
self.editFinFunc = finFunc
self.editField = common.newEditField(self.list[self.sel].fileName:gsub(".xml$",""), nil, "[%w _+.()]")
self.editField.x = 2
self.editField.y = 6 + self.sel * 20
self.editField.width = cfg.screenW
self.editField.height = 16
end
function listMode:EditFinish()
local msg = self.editFinFunc(self.editField.buf)
if msg then
launch:ShowPrompt(1, 0.5, 0, msg.."\n\nEnter/Escape to dismiss")
return
end
self.edit = nil
self.editField = nil
end
function listMode:EditCancel()
self.sel = nil
self.edit = nil
self.editField = nil
self:BuildList()
end
function listMode:New()
table.insert(self.list, 1, { fileName = "", level = 1 })
self.sel = 1
self:EditInit(function(buf)
if #buf < 1 then
return "No name entered"
end
local fileName = buf .. ".xml"
local outFile, msg = io.open(fileName, "r")
if outFile then
outFile:close()
return "'"..fileName.."' already exists"
end
outFile, msg = io.open(fileName, "w")
if not outFile then
return "Couldn't create '"..fileName.."': "..msg
end
outFile:write('<?xml version="1.0" encoding="UTF-8"?>\n<PathOfBuilding>\n</PathOfBuilding>')
outFile:close()
self.list[self.sel].fileName = fileName
self:SortList()
end)
end
function listMode:CopySel()
local srcName = self.list[self.sel].fileName
table.insert(self.list, self.sel + 1, copyTable(self.list[self.sel]))
self.sel = self.sel + 1
self.list[self.sel].fileName = srcName:gsub(".xml$","") .. " (copy)"
self:EditInit(function(buf)
if #buf < 1 then
return "No name entered"
end
local inFile, msg = io.open(srcName, "r")
if not inFile then
return "Couldn't copy '"..srcName.."': "..msg
end
local dstName = buf .. ".xml"
local outFile, msg = io.open(dstName, "r")
if outFile then
outFile:close()
return "'"..dstName.."' already exists"
end
outFile, msg = io.open(dstName, "w")
if not outFile then
return "Couldn't create '"..dstName.."': "..msg
end
outFile:write(inFile:read("*a"))
inFile:close()
outFile:close()
self.list[self.sel].fileName = dstName
self:SortList()
end)
end
function listMode:RenameSel()
local oldName = self.list[self.sel].fileName
self:EditInit(function(buf)
if #buf < 1 then
return "No name entered"
end
local newName = buf .. ".xml"
if newName == oldName then
return
end
if newName:lower() ~= oldName:lower() then
local newFile = io.open(newName, "r")
if newFile then
newFile:close()
return "'"..newName.."' already exists"
end
end
local res, msg = os.rename(oldName, newName)
if not res then
return "Couldn't rename '"..oldName.."' to '"..newName.."': "..msg
end
self.list[self.sel].fileName = newName
self:SortList()
end)
end
function listMode:DeleteSel()
launch:ShowPrompt(1, 0, 0, "Are you sure you want to delete\n'"..self.list[self.sel].fileName.."' ? (y/n)", function(key)
if key == "y" then
os.remove(self.list[self.sel].fileName)
self:BuildList()
self.sel = nil
end
return true
end)
end
function listMode:SelFileByName(selFileName)
self.sel = nil
for index, build in ipairs(self.list) do
if build.fileName == selFileName then
self.sel = index
break
end
end
end
function listMode:Init(selFileName)
self:BuildList()
self:SelFileByName(selFileName)
end
function listMode:Shutdown()
if self.edit then
self:EditCancel()
end
end
function listMode:OnFrame(inputEvents)
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
self:OnKeyDown(event.key, event.doubleClick)
elseif event.type == "KeyUp" then
self:OnKeyUp(event.key)
elseif event.type == "Char" then
self:OnChar(event.key)
end
end
common.controlsDraw(self)
for index, build in ipairs(self.list) do
local y = 4 + index * 20
if self.sel == index then
SetDrawColor(1, 1, 1)
else
SetDrawColor(0.5, 0.5, 0.5)
end
DrawImage(nil, 0, y, cfg.screenW, 20)
if self.sel == index then
SetDrawColor(0.33, 0.33, 0.33)
else
SetDrawColor(0, 0, 0)
end
DrawImage(nil, 0, y + 1, cfg.screenW, 18)
if self.edit == index then
self.editField:Draw(2, y + 2, 16)
else
if self.sel == index then
SetDrawColor(1, 1, 1)
else
SetDrawColor(0.8, 0.8, 0.8)
end
DrawString(4, y + 2, "LEFT", 16, "VAR", build.fileName:gsub(".xml",""))
DrawString(304, y + 2, "LEFT", 16, "VAR", string.format("Level %d %s", build.level, build.ascendClassName or build.className or "?"))
end
end
end
function listMode:OnKeyDown(key, doubleClick)
if self.edit then
if key == "RETURN" then
self:EditFinish()
elseif key == "ESCAPE" then
self:EditCancel()
else
self.editField:OnKeyDown(key)
end
elseif key == "LEFTBUTTON" then
self.selControl = nil
local cx, cy = GetCursorPos()
for _, control in pairs(self.controls) do
if control.IsMouseOver and control:IsMouseOver() then
control:OnKeyDown(key)
self.selControl = control
return
end
end
self.sel = nil
for index, fileName in ipairs(self.list) do
local y = 4 + index * 20
if cy >= y and cy < y + 20 then
if doubleClick then
main:SetMode("BUILD", self.list[index].fileName)
else
self.sel = index
end
return
end
end
elseif key == "RETURN" then
if self.sel then
main:SetMode("BUILD", self.list[self.sel].fileName)
end
elseif key == "UP" then
if not self.sel then
self.sel = #self.list
else
self.sel = (self.sel - 2) % #self.list + 1
end
elseif key == "DOWN" then
if not self.sel then
self.sel = 1
else
self.sel = self.sel % #self.list + 1
end
elseif key == "F2" then
if self.sel then
self:RenameSel()
end
elseif key == "DELETE" then
if self.sel then
self:DeleteSel()
end
end
end
function listMode:OnKeyUp(key)
if self.edit then
self.editField:OnKeyUp(key)
elseif self.selControl then
self.selControl:OnKeyUp(key)
self.selControl = nil
end
end
function listMode:OnChar(key)
if self.edit then
self.editField:OnChar(key)
end
end
return listMode

203
Modules/Main.lua Normal file
View File

@@ -0,0 +1,203 @@
local launch = ...
local ipairs = ipairs
local m_floor = math.floor
local m_max = math.max
local t_insert = table.insert
local cfg = { }
local main = { }
main.tooltipLines = { }
function main:AddTooltipLine(size, text)
for line in string.gmatch(text .. "\n", "([^\n]*)\n") do
t_insert(self.tooltipLines, { size = size, text = line })
end
end
function main:AddTooltipSeperator(size)
t_insert(self.tooltipLines, { size = size })
end
function main:DrawTooltip(x, y, w, h, viewPort, col, center)
local ttW, ttH = 0, 0
for _, data in ipairs(self.tooltipLines) do
ttH = ttH + data.size + 2
if data.text then
ttW = m_max(ttW, DrawStringWidth(data.size, "VAR", data.text))
end
end
ttW = ttW + 12
ttH = ttH + 10
local ttX = x
local ttY = y
if w and h then
ttX = ttX + w + 5
if ttX + ttW > viewPort.x + viewPort.width then
ttX = m_max(viewPort.x, x - 5 - ttW)
end
if ttY + ttH > viewPort.y + viewPort.height then
ttY = m_max(viewPort.y, y + h - ttH)
end
elseif center then
ttX = m_floor(x - ttW/2)
end
col = col or { 0.5, 0.3, 0 }
if type(col) == "string" then
SetDrawColor(col)
else
SetDrawColor(unpack(col))
end
DrawImage(nil, ttX, ttY, ttW, 3)
DrawImage(nil, ttX, ttY, 3, ttH)
DrawImage(nil, ttX, ttY + ttH - 3, ttW, 3)
DrawImage(nil, ttX + ttW - 3, ttY, 3, ttH)
SetDrawColor(0, 0, 0, 0.75)
DrawImage(nil, ttX + 3, ttY + 3, ttW - 6, ttH - 6)
SetDrawColor(1, 1, 1)
local y = ttY + 6
for i, data in ipairs(self.tooltipLines) do
if data.text then
if center then
DrawString(ttX + ttW/2, y, "CENTER_X", data.size, "VAR", data.text)
else
DrawString(ttX + 6, y, "LEFT", data.size, "VAR", data.text)
end
else
if type(col) == "string" then
SetDrawColor(col)
else
SetDrawColor(unpack(col))
end
DrawImage(nil, ttX + 3, y - 1 + data.size / 2, ttW - 6, 2)
end
y = y + data.size + 2
self.tooltipLines[i] = nil
end
end
LoadModule("Data")
LoadModule("ModTools")
main.TreeClass = LoadModule("Tree", launch, cfg, main)
main.SpecClass = LoadModule("Spec", launch, cfg, main)
main.TreeViewClass = LoadModule("TreeView", launch, cfg, main)
main.modes = { }
main.modes["LIST"] = LoadModule("List", launch, cfg, main)
main.modes["BUILD"] = LoadModule("Build", launch, cfg, main)
function main:SetMode(newMode, ...)
self.newMode = newMode
self.newModeArgs = {...}
end
function main:CallMode(func, ...)
local modeTbl = self.modes[self.mode]
if modeTbl[func] then
modeTbl[func](modeTbl, ...)
end
end
function main:LoadSettings()
local setXML, errMsg = common.xml.LoadXMLFile("Settings.xml")
if not setXML then
return true
elseif setXML[1].elem ~= "PathOfBuilding" then
launch:ShowErrMsg("^1Error parsing 'Settings.xml': 'PathOfBuilding' root element missing")
return true
end
for _, node in ipairs(setXML[1]) do
if type(node) == "table" then
if node.elem == "Mode" then
if not node.attrib.mode or not self.modes[node.attrib.mode] then
launch:ShowErrMsg("^1Error parsing 'Settings.xml': Invalid mode attribute in 'Mode' element")
return true
end
local args = { }
for _, child in ipairs(node) do
if type(child) == "table" then
if child.elem == "Arg" then
if child.attrib.number then
t_insert(args, tonumber(child.attrib.number))
elseif child.attrib.string then
t_insert(args, child.attrib.string)
elseif child.attrib.boolean then
t_insert(args, child.attrib.boolean == "true")
end
end
end
end
self:SetMode(node.attrib.mode, unpack(args))
end
end
end
end
function main:SaveSettings()
local setXML = { elem = "PathOfBuilding" }
local mode = { elem = "Mode", attrib = { mode = self.mode } }
for _, val in ipairs(self.modeArgs) do
local child = { elem = "Arg", attrib = {} }
if type(val) == "number" then
child.attrib.number = tostring(val)
elseif type(val) == "boolean" then
child.attrib.boolean = tostring(val)
else
child.attrib.string = tostring(val)
end
t_insert(mode, child)
end
t_insert(setXML, mode)
local res, errMsg = common.xml.SaveXMLFile(setXML, "Settings.xml")
if not res then
launch:ShowErrMsg("Error saving 'Settings.xml': %s", errMsg)
return true
end
end
function main:OnFrame()
cfg.screenW, cfg.screenH = GetScreenSize()
if self.newMode then
if self.mode then
self:CallMode("Shutdown")
end
self.mode = self.newMode
self.modeArgs = self.newModeArgs
self.newMode = nil
self:CallMode("Init", unpack(self.modeArgs))
end
self:CallMode("OnFrame", self.inputEvents)
wipeTable(self.inputEvents)
end
function main:OnKeyDown(key, doubleClick)
t_insert(self.inputEvents, { type = "KeyDown", key = key, doubleClick = doubleClick })
end
function main:OnKeyUp(key)
t_insert(self.inputEvents, { type = "KeyUp", key = key })
end
function main:OnChar(key)
t_insert(self.inputEvents, { type = "Char", key = key })
end
function main:Init()
self.inputEvents = { }
self.tree = self.TreeClass.NewTree()
self:SetMode("LIST")
self:LoadSettings()
end
function main:Shutdown()
self:CallMode("Shutdown")
self:SaveSettings()
end
return main

545
Modules/ModParser.lua Normal file
View File

@@ -0,0 +1,545 @@
-- List of modifier forms
local formList = {
["^(%d+)%% increased"] = "INC",
["^(%d+)%% reduced"] = "RED",
["^(%d+)%% more"] = "MORE",
["^(%d+)%% less"] = "LESS",
["^([%+%-][%d%.]+)%%?"] = "BASE",
["^([%+%-][%d%.]+)%%? to"] = "BASE",
["^you gain ([%d%.]+)"] = "BASE",
["^([%+%-]?%d+)%% chance"] = "CHANCE",
["^([%+%-]?%d+)%% additional chance"] = "CHANCE",
["^([%d%.]+)%% of"] = "CONV",
["^gain ([%d%.]+)%% of"] = "CONV",
["penetrates (%d+)%%"] = "PEN",
["penetrates (%d+)%% of enemy"] = "PEN",
["^([%d%.]+)%% of (.+) regenerated per second"] = "REGENPERCENT",
["^([%d%.]+) (.+) regenerated per second"] = "REGENFLAT",
}
-- Map of modifier names
-- '{suf}' is replaced with modifier suffix ('Base', 'Inc', 'More')
local modNameList = {
-- Attributes
["strength"] = "str{suf}",
["dexterity"] = "dex{suf}",
["intelligence"] = "int{suf}",
["strength and dexterity"] = { "str{suf}", "dex{suf}" },
["strength and intelligence"] = { "str{suf}", "int{suf}" },
["dexterity and intelligence"] = { "dex{suf}", "int{suf}" },
["all attributes"] = { "str{suf}", "dex{suf}", "int{suf}" },
-- Life/mana
["maximum life"] = "life{suf}",
["maximum mana"] = "mana{suf}",
["mana regeneration rate"] = "manaRegen{suf}",
["mana cost"] = "manaCost{suf}",
["mana cost of skills"] = "manaCost{suf}",
["mana reserved"] = "manaReserved{suf}",
["mana reservation"] = "manaReserved{suf}",
-- Primary defences
["maximum energy shield"] = "energyShield{suf}",
["energy shield recharge rate"] = "energyShieldRecharge{suf}",
["armour"] = "armour{suf}",
["evasion rating"] = "evasion{suf}",
["energy shield"] = "energyShield{suf}",
["armour and evasion"] = "armourAndEvasion{suf}",
["armour and evasion rating"] = "armourAndEvasion{suf}",
["evasion rating and armour"] = "armourAndEvasion{suf}",
["armour and energy shield"] = "armourAndEnergyShield{suf}",
["evasion and energy shield"] = "evasionAndEnergyShield{suf}",
["defences"] = "defences{suf}",
["global defences"] = "defences{suf}",
-- Resistances
["fire resistance"] = "fireResist",
["maximum fire resistance"] = "fireResistMax",
["cold resistance"] = "coldResist",
["maximum cold resistance"] = "coldResistMax",
["lightning resistance"] = "lightningResist",
["maximum lightning resistance"] = "lightningResistMax",
["fire and cold resistances"] = { "fireResist", "coldResist" },
["fire and lightning resistances"] = { "fireResist", "lightningResist" },
["cold and lightning resistances"] = { "coldResist", "lightningResist" },
["elemental resistances"] = "elemResist",
["all elemental resistances"] = "elemResist",
["chaos resistance"] = "chaosResist",
-- Other defences
["to dodge attacks"] = "dodgeAttack",
["to dodge spells"] = "dodgeSpell",
["to dodge spell damage"] = "dodgeSpell",
["to block"] = "blockChance",
["to block spells"] = "spellBlockChance",
["maximum block chance"] = "blockChanceMax",
["to avoid being shocked"] = "avoidShock",
["to avoid being frozen"] = "avoidFrozen",
["to avoid being chilled"] = "avoidChilled",
["to avoid being ignited"] = "avoidIgnite",
["to avoid elemental status ailments"] = { "avoidShock", "avoidFrozen", "avoidChilled", "avoidIgnite" },
-- Stun modifiers
["stun recovery"] = "stunRecovery{suf}",
["stun threshold"] = "stunThreshold{suf}",
["block recovery"] = "blockRecovery{suf}",
["enemy stun threshold"] = "stunEnemyThreshold{suf}",
["stun duration on enemies"] = "stunEnemyDuration{suf}",
-- Auras/curses
["radius of aura skills"] = "auraRadius{suf}",
["effect of non-curse auras you cast"] = "auraEffect{suf}",
-- Charges
["maximum power charge"] = "powerMax",
["maximum power charges"] = "powerMax",
["power charge duration"] = "powerDuration{suf}",
["maximum frenzy charge"] = "frenzyMax",
["maximum frenzy charges"] = "frenzyMax",
["frenzy charge duration"] = "frenzyDuration{suf}",
["maximum endurance charge"] = "enduranceMax",
["maximum endurance charges"] = "enduranceMax",
["endurance charge duration"] = "enduranceDuration{suf}",
["endurance, frenzy and power charge duration"] = { "powerDuration{suf}", "frenzyDuration{suf}", "enduranceDuration{suf}" },
-- On hit/kill effects
["life gained on kill"] = "lifeOnKill",
["mana gained on kill"] = "manaOnKill",
["life gained for each enemy hit by attacks"] = "attack_lifeOnHit",
["life gained for each enemy hit by your attacks"] = "attack_lifeOnHit",
["life gained for each enemy hit by spells"] = "spell_lifeOnHit",
["life gained for each enemy hit by your spells"] = "spell_lifeOnHit",
["mana gained for each enemy hit by attacks"] = "attack_manaOnHit",
["mana gained for each enemy hit by your attacks"] = "attack_manaOnHit",
["energy shield gained for each enemy hit by attacks"] = "attack_energyShieldOnHit",
["energy shield gained for each enemy hit by your attacks"] = "attack_energyShieldOnHit",
["life and mana gained for each enemy hit"] = { "attack_lifeOnHit", "attack_manaOnHit" },
-- Projectile modifiers
["pierce chance"] = "pierceChance",
["of projectiles piercing"] = "pierceChance",
["of arrows piercing"] = "pierceChance",
["projectile speed"] = "projectileSpeed{suf}",
["arrow speed"] = "bow_projectileSpeed{suf}",
-- Totem/trap/mine modifiers
["totem placement speed"] = "totemPlacementSpeed{suf}",
["totem life"] = "totemLife{suf}",
["totem duration"] = "totemDuration{suf}",
["trap throwing speed"] = "trapThrowingSpeed{suf}",
["trap trigger radius"] = "trapTriggerRadius{suf}",
["trap duration"] = "trapDuration{suf}",
["cooldown recovery speed for throwing traps"] = "trapCooldownRecovery{suf}",
["mine laying speed"] = "mineLayingSpeed{suf}",
["mine detonation radius"] = "mineDetonationRad{suf}",
["mine duration"] = "mineDuration{suf}",
-- Other skill modifiers
["radius"] = "aoeRadius{suf}",
["radius of area skills"] = "aoeRadius{suf}",
["duration"] = "duration{suf}",
["skill effect duration"] = "duration{suf}",
-- Buffs
["onslaught effect"] = "onslaughtEffect{suf}",
["fortify duration"] = "fortifyDuration{suf}",
["effect of fortify on you"] = "fortifyEffect{suf}",
-- Basic damage types
["damage"] = "damage{suf}",
["physical damage"] = "physical{suf}",
["lightning damage"] = "lightning{suf}",
["cold damage"] = "cold{suf}",
["fire damage"] = "fire{suf}",
["chaos damage"] = "chaos{suf}",
["elemental damage"] = "elem{suf}",
-- Other damage forms
["attack damage"] = "attack_damage{suf}",
["physical attack damage"] = "attack_physical{suf}",
["physical weapon damage"] = "weapon_physical{suf}",
["physical melee damage"] = "melee_physical{suf}",
["melee physical damage"] = "melee_physical{suf}",
["wand damage"] = "wand_damage{suf}",
["wand physical damage"] = "wand_physical{suf}",
["damage over time"] = "dot_damage{suf}",
["physical damage over time"] = "dot_physical{suf}",
["burning damage"] = "degen_fire{suf}",
-- Crit/accuracy/speed modifiers
["critical strike chance"] = "critChance{suf}",
["global critical strike chance"] = "global_critChance{suf}",
["critical strike multiplier"] = "critMultiplier",
["global critical strike multiplier"] = "critMultiplier",
["accuracy rating"] = "accuracy{suf}",
["attack speed"] = "attackSpeed{suf}",
["cast speed"] = "castSpeed{suf}",
["attack and cast speed"] = "speed{suf}",
-- Elemental status ailments
["to shock"] = "shockChance",
["shock chance"] = "shockChance",
["to freeze"] = "freezeChance",
["freeze chance"] = "freezeChance",
["to ignite"] = "igniteChance",
["ignite chance"] = "igniteChance",
["to freeze, shock and ignite"] = { "freezeChance", "shockChance", "igniteChance" },
["shock duration on enemies"] = "shock_duration{suf}",
["freeze duration on enemies"] = "freeze_duration{suf}",
["chill duration on enemies"] = "chill_duration{suf}",
["ignite duration on enemies"] = "ignite_duration{suf}",
["duration of elemental status ailments on enemies"] = { "shock_duration{suf}", "freeze_duration{suf}", "chill_duration{suf}", "ignite_duration{suf}" },
-- Other debuffs
["to poison on hit"] = "poisonChance",
["poison duration"] = "poison_duration{suf}",
["to cause bleeding"] = "bleedChance",
["to cause bleeding on hit"] = "bleedChance",
-- Misc modifiers
["movement speed"] = "movementSpeed{suf}",
["light radius"] = "lightRadius{suf}",
["rarity of items found"] = "lootRarity{suf}",
["quantity of items found"] = "lootQuantity{suf}",
}
-- List of modifier namespaces
local namespaceList = {
-- Weapon types
["with axes"] = "axe_",
["with bows"] = "bow_",
["with claws"] = "claw_",
["with daggers"] = "dagger_",
["with maces"] = "mace_",
["with staves"] = "staff_",
["with swords"] = "sword_",
["with wands"] = "wand_",
["unarmed"] = "unarmed_",
["with one handed weapons"] = "weapon1h_",
["with one handed melee weapons"] = "weapon1hMelee_",
["with two handed weapons"] = "weapon2h_",
["with two handed melee weapons"] = "weapon2hMelee_",
-- Skill types
["spell"] = "spell_",
["for spells"] = "spell_",
["melee"] = "melee_",
["with weapons"] = "weapon_",
["weapon"] = "weapon_",
["with poison"] = "poison_",
["with attacks"] = "attack_",
["projectile"] = "projectile_",
["area"] = "aoe_",
["mine"] = "mine_",
["with mines"] = "mine_",
["trap"] = "trap_",
["with traps"] = "trap_",
["totem"] = "totem_",
["with totem skills"] = "totem_",
["with lightning skills"] = "lightning_",
["with cold skills"] = "cold_",
["with fire skills"] = "fire_",
["with chaos skills"] = "chaos_",
-- Other
["from equipped shield"] = "Shield_",
}
-- List of namespaces that appear at the start of a line
local preSpaceList = {
["^minions have "] = "minion_",
["^minions deal "] = "minion_",
["^attacks used by totems have "] = "totem_",
["^spells cast by totems have "] = "totem_",
["^melee attacks have "] = "melee_",
}
-- List of special namespaces
local specialSpaceList = {
-- Per charge modifiers
["per power charge"] = "power_",
["per frenzy charge"] = "frenzy_",
["per endurance charge"] = "endurance_",
-- Equipment conditions
["while holding a shield"] = "condMod_UsingShield_",
["with shields"] = "condMod_UsingShield_",
["while dual wielding"] = "condMod_DualWielding_",
["while wielding a staff"] = "condMod_UsingStaff_",
-- Player status conditions
["when on low life"] = "condMod_LowLife_",
["while on low life"] = "condMod_LowLife_",
["when not on low life"] = "condMod_notLowLife_",
["while not on low life"] = "condMod_notLowLife_",
["when on full life"] = "condMod_FullLife_",
["when not on full life"] = "condMod_notFullLife_",
["while you have fortify"] = "condMod_Fortify_",
["during onslaught"] = "condMod_Onslaught_",
["while you have onslaught"] = "condMod_Onslaught_",
["while phasing"] = "condMod_Phasing_",
["while using a flask"] = "condMod_UsingFlask_",
["while on consecrated ground"] = "condMod_OnConsecratedGround_",
["if you've attacked recently"] = "condMod_AttackedRecently_",
["if you've cast a spell recently"] = "condMod_CastSpellRecently_",
["if you've summoned a totem recently"] = "condMod_SummonedTotemRecently_",
["if you've used a movement skill recently"] = "condMod_UsedMovementSkillRecently_",
["if you detonated mines recently"] = "condMod_DetonatedMinesRecently_",
["if you've crit in the past 8 seconds"] = "condMod_CritInPast8Sec_",
["if energy shield recharge has started recently"] = "condMod_EnergyShieldRechargeRecently_",
-- Enemy status conditions
["against bleeding enemies"] = "condMod_EnemyBleeding_",
["against poisoned enemies"] = "condMod_EnemyPoisoned_",
["against burning enemies"] = "condMod_EnemyBurning_",
["enemies which are chilled"] = "condMod_EnemyChilled_",
["against frozen, shocked or ignited enemies"] = "condMod_EnemyFrozenShockedIgnited_",
["against enemies that are affected by elemental status ailments"] = "condMod_EnemyElementalStatus_",
["against enemies that are affected by no elemental status ailments"] = "condMod_notEnemyElementalStatus_",
}
-- List of special modifiers
local specialModList = {
-- Keystones
["your hits can't be evaded"] = { noEvade = true },
["never deal critical strikes"] = { noCrit = true },
["no critical strike multiplier"] = { noCritMult = true },
["the increase to physical damage from strength applies to projectile attacks as well as melee attacks"] = { ironGrip = true },
["converts all evasion rating to armour%. dexterity provides no bonus to evasion rating"] = { ironReflexes = true },
["30%% chance to dodge attacks%. 50%% less armour and energy shield, 30%% less chance to block spells and attacks"] = { dodgeAttack = 30, armourMore = 0.5, energyShieldMore = 0.5 },
["maximum life becomes 1, immune to chaos damage"] = { chaosInoculation = true },
["deal no non%-fire damage"] = { physicalFinalMore = 0, lightningFinalMore = 0, coldFinalMore = 0, chaosFinalMore = 0 },
-- Ascendancy notables
["movement skills cost no mana"] = { movement_manaCostMore = 0 },
["projectiles have 100%% additional chance to pierce targets at the start of their movement, losing this chance as the projectile travels farther"] = { pierceChance = 100 },
["projectile critical strike chance increased by arrow pierce chance"] = { projectile_critChanceInc = 100 },
["always poison on hit while using a flask"] = { condMod_UsingFlask_poisonChance = 100 },
["armour received from body armour is doubled"] = { ["Body Armour_armourMore"] = 2 },
["gain (%d+)%% of maximum mana as extra maximum energy shield"] = function(num) return { manaGainAsES = num } end,
["you have fortify"] = { cond_Fortify = true },
-- Special node types
["(%d+)%% additional block chance with staves"] = function(num) return { condMod_UsingStaff_blockChance = num } end,
["(%d+)%% additional block chance with shields"] = function(num) return { condMod_UsingShield_blockChance = num } end,
["(%d+)%% additional block chance while dual wielding"] = function(num) return { condMod_DualWielding_blockChance = num } end,
["(%d+)%% faster start of energy shield recharge"] = function(num) return { energyShieldRechargeFaster = num } end,
["(%d+)%% additional block chance while dual wielding or holding a shield"] = function(num) return { condMod_DualWielding_blockChance = num, condMod_UsingShield_blockChance = num } end,
-- Other modifiers
["adds (%d+)%-(%d+) (%a+) damage ?t?o? ?a?t?t?a?c?k?s?"] = function(_, min, max, type) local pre = "attack_"..type return { [pre.."Min"] = tonumber(min), [pre.."Max"] = tonumber(max) } end,
["adds (%d+)%-(%d+) (%a+) damage to attacks with bows"] = function(_, min, max, type) local pre = "bow_"..type return { [pre.."Min"] = tonumber(min), [pre.."Max"] = tonumber(max) } end,
["adds (%d+)%-(%d+) (%a+) damage to spells"] = function(_, min, max, type) local pre = "spell_"..type return { [pre.."Min"] = tonumber(min), [pre.."Max"] = tonumber(max) } end,
["cannot be shocked"] = { avoidShock = 100 },
["cannot be frozen"] = { avoidFreeze = 100 },
["cannot be chilled"] = { avoidChill = 100 },
["cannot be ignited"] = { avoidIgnite = 100 },
["cannot be stunned"] = { stunImmunity = true },
["deal no physical damage"] = { physicalFinalMore = 0 },
["iron will"] = { ironWill = true },
["extra gore"] = { },
-- Special item local modifiers
["no physical damage"] = { weaponNoPhysical = true },
["all attacks with this weapon are critical strikes"] = { weaponAlwaysCrit = true },
["hits can't be evaded"] = { weaponX_noEvade = true },
["no block chance"] = { shieldNoBlock = true },
["has 1 socket"] = { },
["socketed gems have (.+)"] = { },
["socketed gems are Supported by (.+)"] = { },
["+(%d) to level of socketed (%a+) gems"] = { },
["grants level (%d+) (.+) skill"] = { },
-- Unique item modifiers
["projectile damage increased by arrow pierce chance"] = { drillneck = true },
}
-- Special lookups used for various modifier forms
local convTypes = {
["as extra lightning damage"] = "GainAslightning",
["added as lightning damage"] = "GainAslightning",
["as extra cold damage"] = "GainAscold",
["added as cold damage"] = "GainAscold",
["as extra fire damage"] = "GainAsfire",
["added as fire damage"] = "GainAsfire",
["as extra chaos damage"] = "GainAschaos",
["added as chaos damage"] = "GainAschaos",
["converted to lightning damage"] = "ConvertTolightning",
["converted to cold damage"] = "ConvertTocold",
["converted to fire damage"] = "ConvertTofire",
["converted to chaos damage"] = "ConvertTochaos",
}
local penTypes = {
["lightning resistance"] = "lightningPen",
["cold resistance"] = "coldPen",
["fire resistance"] = "firePen",
["elemental resistance"] = "elemPen",
["elemental resistances"] = "elemPen",
}
local regenTypes = {
["life"] = "lifeRegen{suf}",
["maximum life"] = "lifeRegen{suf}",
["mana"] = "manaRegen{suf}",
["energyShield"] = "energyShieldRegen{suf}",
}
-- Build active skill name lookup
local skillNameList = { }
for skillName, data in pairs(data.gems) do
if not data.support then
skillNameList[skillName:lower()] = "skill:" .. skillName .. "_"
end
end
local function getSimpleConv(src, dst, factor)
return function(mods, allMods, data)
if mods and mods[src] then
mod.listMerge(allMods, dst, mods[src] * factor)
mods[src] = nil
end
end
end
local function getMatchConv(others, dst)
return function(mods, allMods, data)
if mods then
for k, v in pairs(mods) do
for _, other in pairs(others) do
if k:match(other) then
mod.listMerge(allMods, k:gsub(other, dst), v)
mods[k] = nil
end
end
end
end
end
end
local function getPerStat(dst, stat, factor)
return function(mods, allMods, data)
if mods then
data[stat] = (data[stat] or 0) + (mods[stat] or 0)
else
mod.listMerge(allMods, dst, math.floor(data[stat] * factor + 0.5))
end
end
end
-- List of radius jewel functions
local jewelFuncs = {
["Strength from Passives in Radius is Transformed to Dexterity"] = getSimpleConv("strBase", "dexBase", 1),
["Dexterity from Passives in Radius is Transformed to Strength"] = getSimpleConv("dexBase", "strBase", 1),
["Strength from Passives in Radius is Transformed to Intelligence"] = getSimpleConv("strBase", "intBase", 1),
["Intelligence from Passives in Radius is Transformed to Strength"] = getSimpleConv("intBase", "strBase", 1),
["Dexterity from Passives in Radius is Transformed to Intelligence"] = getSimpleConv("dexBase", "intBase", 1),
["Intelligence from Passives in Radius is Transformed to Dexterity"] = getSimpleConv("intBase", "dexBase", 1),
["Increases and Reductions to Life in Radius are Transformed to apply to Energy Shield"] = getSimpleConv("lifeInc", "energyShieldInc", 1),
["Increases and Reductions to Energy Shield in Radius are Transformed to apply to Armour at 200% of their value"] = getSimpleConv("energyShieldInc", "armourInc", 2),
["Increases and Reductions to Life in Radius are Transformted to apply to Mana at 200% of their value"] = getSimpleConv("lifeInc", "manaInc", 2),
["Increases and Reductions to Physical Damage in Radius are transformed to apply to Cold Damage"] = getMatchConv({"physicalInc"}, "coldInc"),
["Increases and Reductions to Cold Damage in Radius are transformed to apply to Physical Damage"] = getMatchConv({"coldInc"}, "physicalInc"),
["Increases and Reductions to other Damage Types in Radius are Transformed to apply to Fire Damage"] = getMatchConv({"physicalInc","coldInc","lightningInc","chaosInc"}, "fireInc"),
["Melee and Melee Weapon Type Modifiers in Radius are Transformed to Bow Modifiers"] = getMatchConv({"melee_","axe_","claw_","dagger_","mace_","staff_","sword_"}, "bow_"),
["Adds 1 to maximum Life per 3 Intelligence in Radius"] = getPerStat("lifeBase", "intBase", 1 / 3),
["1% increased Evasion Rating per 3 Dexterity Allocated in Radius"] = getPerStat("evasionInc", "dexBase", 1 / 3),
["1% increased Claw Physical Damage per 3 Dexterity Allocated in Radius"] = getPerStat("claw_physicalInc", "dexBase", 1 / 3),
["1% increased Melee Physical Damage while Unarmed per 3 Dexterity Allocated in Radius"] = getPerStat("unarmed_physicalInc", "dexBase", 1 / 3),
["3% increased Totem Life per 10 Strength in Radius"] = getPerStat("totemLifeInc", "strBase", 3 / 10),
["Adds 1 maximum Lightning Damage to Attacks per 1 Dexterity Allocated in Radius"] = getPerStat("attack_lightningMax", "dexBase", 1),
["5% increased Chaos damage per 10 Intelligence from Allocated Passives in Radius"] = getPerStat("chaosInc", "intBase", 5 / 10),
["Dexterity and Intelligence from passives in Radius count towards Strength Melee Damage bonus"] = function(mods, allMods, data)
if mods then
data.dexBase = (data.dexBase or 0) + (mods.dexBase or 0)
data.intBase = (data.intBase or 0) + (mods.intBase or 0)
else
mod.listMerge(allMods, "dexIntToMeleeBonus", data.dexBase + data.intBase)
end
end,
}
-- Scan a line for the earliest and longest match from the pattern list
-- If a match is found, returns the corresponding value from the pattern table, plus the remainder of the line and a table of captures
local function scan(line, patternList, plain)
local bestIndex, bestEndIndex
local bestMatch = { nil, line, nil }
for pattern, patternVal in pairs(patternList) do
local index, endIndex, cap1, cap2, cap3, cap4, cap5 = line:lower():find(pattern, 1, plain)
if index and (not bestIndex or index < bestIndex or (index == bestIndex and endIndex > bestEndIndex)) then
bestIndex = index
bestEndIndex = endIndex
bestMatch = { patternVal, line:sub(1, index - 1)..line:sub(endIndex + 1, -1), { cap1, cap2, cap3, cap4, cap5 } }
end
end
return bestMatch[1], bestMatch[2], bestMatch[3]
end
return function(line)
-- Check if this is a special modifier
local specialMod, specialLine, cap = scan(line, specialModList)
if specialMod and #specialLine == 0 then
if type(specialMod) == "function" then
return specialMod(tonumber(cap[1]), unpack(cap))
else
return copyTable(specialMod)
end
end
if jewelFuncs[line] then
return { jewelFunc = jewelFuncs[line] }
end
-- Check for a namespace at the start of the line
local space
space, line = scan(line, preSpaceList)
-- Scan for modifier form
local modForm, formCap
modForm, line, formCap = scan(line, formList)
if not modForm then
return
end
local num = tonumber(formCap[1])
-- Check for special namespaces (per-charge, conditionals)
local specialSpace
specialSpace, line = scan(line, specialSpaceList, true)
-- Scan for modifier name
local modName
modName, line = scan(line, modNameList, true)
if not modName and line:match("%S") then
return { }, line
end
-- Scan for skill name
local skillSpace
skillSpace, line = scan(line, skillNameList, true)
-- Scan for namespace if one hasn't been found already
if not space then
space, line = scan(line, namespaceList, true)
end
-- Find modifier value and suffix according to form
local val, suffix
if modForm == "INC" then
val = num
suffix = "Inc"
elseif modForm == "RED" then
val = -num
suffix = "Inc"
elseif modForm == "MORE" then
val = 1 + num / 100
suffix = "More"
elseif modForm == "LESS" then
val = 1 - num / 100
suffix = "More"
elseif modForm == "BASE" then
val = num
suffix = "Base"
elseif modForm == "CHANCE" then
val = num
elseif modForm == "CONV" then
val = num
suffix, line = scan(line, convTypes, true)
if not suffix then
return { }, line
end
elseif modForm == "PEN" then
val = num
modName, line = scan(line, penTypes, true)
if not modName then
return { }, line
end
elseif modForm == "REGENPERCENT" then
val = num
suffix = "Percent"
modName = regenTypes[formCap[2]:lower()]
if not modName then
return { }, line
end
elseif modForm == "REGENFLAT" then
val = num
suffix = "Base"
modName = regenTypes[formCap[2]:lower()]
if not modName then
return { }, line
end
end
-- Generate modifier list
local nameList = modName or ""
local modList = { }
for i, name in ipairs(type(nameList) == "table" and nameList or { nameList }) do
modList[(skillSpace or "") .. (specialSpace or "") .. (space or "") .. name:gsub("{suf}", suffix or "")] = val
end
return modList, line:match("%S") and line
end

155
Modules/ModTools.lua Normal file
View File

@@ -0,0 +1,155 @@
local t_insert = table.insert
mod = { }
mod.parseMod = LoadModule("ModParser")
-- Break modifier name into namespace and mod name
local spaceLookup = { }
function mod.getSpaceName(modName)
if not spaceLookup[modName] then
local space, mod = modName:match("^([^_]+)_(.+)$")
if not space then
space = "global"
mod = modName
end
spaceLookup[modName] = { space, mod }
return space, mod
end
return unpack(spaceLookup[modName])
end
-- Extract condition name from modifier name
local condLookup = { }
function mod.getCondName(modName)
if not condLookup[modName] then
local isNot, condName, mod = modName:match("^(n?o?t?)(%w+)_(.+)$")
isNot = (isNot == "not")
condLookup[modName] = { isNot, condName, mod }
return isNot, condName, mod
end
return unpack(condLookup[modName])
end
-- Magic table to check if a modifier is multiplicative (contains 'More' in the modifier name)
mod.isModMult = { }
mod.isModMult.__index = function(t, modName)
local val = (modName:match("More") ~= nil)
t[modName] = val
return val
end
setmetatable(mod.isModMult, mod.isModMult)
-- Merge modifier with existing mod list, respecting additivity/multiplicativity
function mod.listMerge(modList, modName, modVal)
if modList[modName] then
if type(modVal) == "boolean" then
modList[modName] = modList[modName] or modVal
elseif type(modVal) == "function" then
local orig = modList[modName]
modList[modName] = function(...) orig(...) modVal(...) end
elseif mod.isModMult[modName] then
modList[modName] = modList[modName] * modVal
else
modList[modName] = modList[modName] + modVal
end
else
modList[modName] = modVal
end
end
-- Unmerge modifier from existing mod list, respecting additivity/multiplicativity
function mod.listUnmerge(modList, modName, modVal)
if type(modVal) == "boolean" then
if modVal == true then
modList[modName] = false
end
elseif type(modVal) == "string" then
modList[modName] = nil
elseif mod.isModMult[modName] then
if modVal == 0 then
modList[modName] = 1
else
modList[modName] = (modList[modName] or 1) / modVal
end
else
modList[modName] = (modList[modName] or 0) - modVal
end
end
-- Merge modifier with mod database
function mod.dbMerge(modDB, spaceName, modName, modVal)
if not spaceName then
spaceName, modName = mod.getSpaceName(modName)
elseif spaceName == "" then
spaceName = "global"
end
if not modDB[spaceName] then
modDB[spaceName] = { }
end
mod.listMerge(modDB[spaceName], modName, modVal)
end
-- Unmerge modifier from mod database
function mod.dbUnmerge(modDB, spaceName, modName, modVal)
if not spaceName then
spaceName, modName = mod.getSpaceName(modName)
elseif spaceName == "" then
spaceName = "global"
end
if not modDB[spaceName] then
modDB[spaceName] = { }
end
mod.listUnmerge(modDB[spaceName], modName, modVal)
end
-- Merge modifier list with mod database
function mod.dbMergeList(modDB, modList)
for k, modVal in pairs(modList) do
local spaceName, modName = mod.getSpaceName(k)
if not modDB[spaceName] then
modDB[spaceName] = { }
end
mod.listMerge(modDB[spaceName], modName, modVal)
end
end
-- Unmerge modifier list from mod database
function mod.dbUnmergeList(modDB, modList)
for k, modVal in pairs(modList) do
local spaceName, modName = mod.getSpaceName(k)
if not modDB[spaceName] then
modDB[spaceName] = { }
end
mod.listUnmerge(modDB[spaceName], modName, modVal)
end
end
-- Print modifier list to the console
function mod.listPrint(modList, tab)
local names = { }
for k in pairs(modList) do
if type(k) == "string" then
t_insert(names, k)
end
end
table.sort(names)
for _, name in pairs(names) do
ConPrintf("%s%s = %s", string.rep("\t", tab or 0), name, modList[name])
end
end
-- Print modifier database to the console
function mod.dbPrint(modDB)
local spaceNames = { }
for k in pairs(modDB) do
t_insert(spaceNames, k)
end
table.sort(spaceNames)
for _, spaceName in pairs(spaceNames) do
ConPrintf("%s = {", spaceName)
mod.listPrint(modDB[spaceName], 1)
ConPrintf("},")
end
end

310
Modules/Spec.lua Normal file
View File

@@ -0,0 +1,310 @@
local launch, cfg, main = ...
local pairs = pairs
local ipairs = ipairs
local m_min = math.min
local m_max = math.max
local m_floor = math.floor
local t_insert = table.insert
local SpecClass = { }
SpecClass.__index = SpecClass
local function buildPathFromNode(root)
root.pathDist = 0
root.path = { }
local queue = { root }
local o, i = 1, 2
while o < i do
local node = queue[o]
o = o + 1
local curDist = node.pathDist + 1
for _, other in ipairs(node.linked) do
if other.type ~= "class" and other.type ~= "ascendClass" and other.pathDist > curDist and (node.ascendancyName == other.ascendancyName or (curDist == 1 and not other.ascendancyName)) then
other.pathDist = curDist
other.path = wipeTable(other.path)
other.path[1] = other
for i, n in ipairs(node.path) do
other.path[i+1] = n
end
queue[i] = other
i = i + 1
end
end
end
end
function SpecClass:BuildAllPaths()
for id, node in pairs(self.nodes) do
node.pathDist = node.alloc and 0 or 1000
node.path = nil
end
for id, node in pairs(self.allocNodes) do
buildPathFromNode(node)
end
end
local function findStart(node, visited, noAscend)
node.visited = true
t_insert(visited, node)
for _, other in ipairs(node.linked) do
if other.alloc and (other.type == "class" or other.type == "ascendClass" or (not other.visited and findStart(other, visited, noAscend))) then
if not noAscend or other.type ~= "ascendClass" then
return true
end
end
end
end
function SpecClass:BuildAllDepends()
local visited = { }
for id, node in pairs(self.nodes) do
node.depends = wipeTable(node.depends)
if node.alloc then
node.depends[1] = node
node.visited = true
local anyStartFound = (node.type == "class" or node.type == "ascendClass")
for _, other in ipairs(node.linked) do
if other.alloc then
if other.type == "class" or other.type == "ascendClass" then
anyStartFound = true
elseif findStart(other, visited) then
anyStartFound = true
for i, n in ipairs(visited) do
n.visited = false
visited[i] = nil
end
else
for i, n in ipairs(visited) do
t_insert(node.depends, n)
n.visited = false
visited[i] = nil
end
end
end
end
node.visited = false
if not anyStartFound then
for _, depNode in ipairs(node.depends) do
depNode.alloc = false
self.allocNodes[depNode.id] = nil
end
end
end
end
end
function SpecClass:AllocNode(node, altPath)
if not node.path then
return
end
for _, pathNode in ipairs(altPath or node.path) do
pathNode.alloc = true
self.allocNodes[pathNode.id] = pathNode
buildPathFromNode(pathNode)
end
if node.isMultipleChoiceOption then
local parent = node.linked[1]
for _, optNode in ipairs(parent.linked) do
if optNode.isMultipleChoiceOption and optNode.alloc and optNode ~= node then
optNode.alloc = false
self.allocNodes[optNode.id] = nil
self:BuildAllPaths()
end
end
end
self:BuildAllDepends()
end
function SpecClass:DeallocNode(node)
for _, depNode in ipairs(node.depends) do
depNode.alloc = false
self.allocNodes[depNode.id] = nil
end
self:BuildAllDepends()
self:BuildAllPaths()
end
function SpecClass:CountAllocNodes()
local used, ascUsed = 0, 0
for _, node in pairs(self.allocNodes) do
if node.type ~= "class" and node.type ~= "ascendClass" then
if node.ascendancyName then
if not node.isMultipleChoiceOption then
ascUsed = ascUsed + 1
end
else
used = used + 1
end
end
end
return used, ascUsed
end
function SpecClass:ResetNodes()
for id, node in pairs(self.nodes) do
if node.type ~= "class" and node.type ~= "ascendClass" then
node.alloc = false
self.allocNodes[id] = nil
end
end
end
function SpecClass:IsClassConnected(classId)
for _, other in ipairs(self.nodes[self.tree.classes[classId].startNodeId].linked) do
if other.alloc then
other.visited = true
local visited = { }
local found = findStart(other, visited, true)
for i, n in ipairs(visited) do
n.visited = false
end
other.visited = false
if found then
return true
end
end
end
return false
end
function SpecClass:SelectClass(classId)
if self.curClassId then
local oldStartNodeId = self.tree.classes[self.curClassId].startNodeId
self.nodes[oldStartNodeId].alloc = false
self.allocNodes[oldStartNodeId] = nil
end
self.curClassId = classId
local class = self.tree.classes[classId]
local startNode = self.nodes[class.startNodeId]
startNode.alloc = true
self.allocNodes[startNode.id] = startNode
self:SelectAscendClass(0)
end
function SpecClass:SelectAscendClass(ascendClassId)
self.curAscendClassId = ascendClassId
local ascendClass = self.tree.classes[self.curClassId].classes[tostring(ascendClassId)] or { name = "" }
for id, node in pairs(self.allocNodes) do
if node.ascendancyName and node.ascendancyName ~= ascendClass.name then
node.alloc = false
self.allocNodes[id] = nil
end
end
if ascendClass.startNodeId then
local startNode = self.nodes[ascendClass.startNodeId]
startNode.alloc = true
self.allocNodes[startNode.id] = startNode
end
self:BuildAllDepends()
self:BuildAllPaths()
end
function SpecClass:DecodeURL(url)
self:ResetNodes()
local b = common.base64.decode(url:gsub("^.+/",""):gsub("-","+"):gsub("_","/"))
local ver = b:byte(4)
local classId = b:byte(5)
local ascendClassId = (ver >= 4) and b:byte(6) or 0
self:SelectClass(classId)
for i = (ver >= 4) and 8 or 7, #b-1, 2 do
local id = b:byte(i) * 256 + b:byte(i + 1)
local node = self.nodes[id]
if node then
node.alloc = true
self.allocNodes[id] = node
if ascendClassId == 0 and node.ascendancyName then
ascendClassId = self.tree.ascendNameMap[node.ascendancyName].ascendClassId
end
end
end
self:SelectAscendClass(ascendClassId)
end
function SpecClass:EncodeURL(prefix)
local a = { 0, 0, 0, 4, self.curClassId, self.curAscendClassId, 0 }
for id, node in pairs(self.allocNodes) do
if node.type ~= "class" and node.type ~= "ascendClass" then
t_insert(a, m_floor(id / 256))
t_insert(a, id % 256)
end
end
return (prefix or "")..common.base64.encode(string.char(unpack(a))):gsub("+","-"):gsub("/","_")
end
function SpecClass:AddUndoState(noClearRedo)
t_insert(self.undo, 1, self:EncodeURL())
self.undo[102] = nil
self.modFlag = true
self.buildFlag = true
if not noClearRedo then
self.redo = { }
end
end
function SpecClass:Undo()
if self.undo[2] then
t_insert(self.redo, 1, table.remove(self.undo, 1))
self:DecodeURL(table.remove(self.undo, 1))
self:AddUndoState(true)
self.modFlag = true
self.buildFlag = true
end
end
function SpecClass:Redo()
if self.redo[1] then
self:DecodeURL(table.remove(self. redo, 1))
self:AddUndoState(true)
self.modFlag = true
self.buildFlag = true
end
end
function SpecClass:Load(xml, dbFileName)
for _, node in pairs(xml) do
if type(node) == "table" then
if node.elem == "URL" then
if type(node[1]) ~= "string" then
launch:ShowErrMsg("^1Error parsing '%s': 'URL' element missing content", fileName)
return true
end
self:DecodeURL(node[1])
self.undo = { node[1] }
self.redo = { }
end
end
end
self.modFlag = false
end
function SpecClass:Save(xml)
t_insert(xml, {
elem = "URL",
[1] = self:EncodeURL("https://www.pathofexile.com/passive-skill-tree/")
})
self.modFlag = false
end
function SpecClass.NewSpec(tree)
local self = setmetatable({}, SpecClass)
self.tree = tree
self.nodes = { }
for _, treeNode in ipairs(tree.nodes) do
self.nodes[treeNode.id] = setmetatable({
rsq = treeNode.overlay and treeNode.overlay.rsq,
size = treeNode.overlay and treeNode.overlay.size,
linked = { }
}, { __index = treeNode })
end
for id, node in pairs(self.nodes) do
for _, otherId in ipairs(node.linkedId) do
t_insert(node.linked, self.nodes[otherId])
end
end
self.allocNodes = { }
self.undo = { }
self.redo = { }
self:SelectClass(0)
return self
end
return SpecClass

338
Modules/Tree.lua Normal file
View File

@@ -0,0 +1,338 @@
local launch, cfg, main = ...
local pairs = pairs
local ipairs = ipairs
local m_min = math.min
local m_max = math.max
local m_pi = math.pi
local m_sin = math.sin
local m_cos = math.cos
local m_tan = math.tan
local m_sqrt = math.sqrt
local t_insert = table.insert
local function cacheImage(imgName, url)
local imgFile = io.open(imgName, "r")
if imgFile then
imgFile:close()
else
ConPrintf("Downloading '%s'...", imgName)
imgFile = io.open(imgName, "wb")
local easy = common.curl.easy()
easy:setopt_url(url)
easy:setopt_writefunction(imgFile)
easy:perform()
easy:close()
imgFile:close()
end
end
local TreeClass = { }
TreeClass.__index = TreeClass
function TreeClass:BuildConnector(node1, node2)
local conn = {
ascendancyName = node1.ascendancyName,
nodeId1 = node1.id,
nodeId2 = node2.id,
c = { }
}
if node1.g == node2.g and node1.o == node2.o then
conn.type = "Orbit" .. node1.o
if node1.angle > node2.angle then
node1, node2 = node2, node1
end
local span = node2.angle - node1.angle
if span > m_pi then
node1, node2 = node2, node1
span = m_pi * 2 - span
end
local clipAngle = m_pi / 4 - span / 2
local p = 1 - m_max(m_tan(clipAngle), 0)
local angle = node1.angle - clipAngle
local norm, act = { }, { }
for tbl, state in pairs({[norm] = "Normal", [act] = "Active"}) do
local art = self.assets[conn.type..state]
local size = art.width * 2 * 1.33
local oX, oY = size * m_sqrt(2) * m_sin(angle + m_pi/4), size * m_sqrt(2) * -m_cos(angle + m_pi/4)
local cX, cY = node1.group.x + oX, node1.group.y + oY
tbl[1], tbl[2] = node1.group.x, node1.group.y
tbl[3], tbl[4] = cX + (size * m_sin(angle) - oX) * p, cY + (size * -m_cos(angle) - oY) * p
tbl[5], tbl[6] = cX, cY
tbl[7], tbl[8] = cX + (size * m_cos(angle) - oX) * p, cY + (size * m_sin(angle) - oY) * p
end
conn.vert = { Normal = norm, Intermediate = norm, Active = act }
conn.c[9], conn.c[10] = 1, 1
conn.c[11], conn.c[12] = 0, p
conn.c[13], conn.c[14] = 0, 0
conn.c[15], conn.c[16] = p, 0
else
conn.type = "LineConnector"
local art = self.assets.LineConnectorNormal
local vX, vY = node2.x - node1.x, node2.y - node1.y
local dist = m_sqrt(vX * vX + vY * vY)
local scale = art.height * 1.33 / dist
local nX, nY = vX * scale, vY * scale
local endS = dist / (art.width * 1.33)
conn[1], conn[2] = node1.x - nY, node1.y + nX
conn[3], conn[4] = node1.x + nY, node1.y - nX
conn[5], conn[6] = node2.x + nY, node2.y - nX
conn[7], conn[8] = node2.x - nY, node2.y + nX
conn.vert = { Normal = conn, Intermediate = conn, Active = conn }
conn.c[9], conn.c[10] = 0, 1
conn.c[11], conn.c[12] = 0, 0
conn.c[13], conn.c[14] = endS, 0
conn.c[15], conn.c[16] = endS, 1
end
return conn
end
function TreeClass.NewTree()
os.execute("mkdir Data")
ConPrintf("Loading JSON...")
local treeText, classText
local treeFile = io.open("Data/tree.json", "r")
local classFile = io.open("Data/classes.json", "r")
if treeFile and classFile then
treeText = treeFile:read("*a")
treeFile:close()
classText = classFile:read("*a")
classFile:close()
else
if treeFile then
treeFile:close()
elseif classFile then
classFile:close()
end
ConPrintf("Downloading JSON...")
local page = ""
local easy = common.curl.easy()
easy:setopt_url("https://www.pathofexile.com/passive-skill-tree/")
easy:setopt_writefunction(function(data)
page = page..data
return true
end)
easy:perform()
easy:close()
treeText = page:match("var passiveSkillTreeData = (%b{})")
treeFile = io.open("Data/tree.json", "w")
treeFile:write(treeText)
treeFile:close()
classText = page:match("ascClasses: (%b{})")
classFile = io.open("Data/classes.json", "w")
classFile:write(classText)
classFile:close()
end
local self = common.json.decode(treeText)
self.classes = { }
for id, data in pairs(common.json.decode(classText)) do
self.classes[tonumber(id)] = data
data.classes["0"] = { name = "None "}
end
setmetatable(self, TreeClass)
self.size = m_min(self.max_x - self.min_x, self.max_y - self.min_y) * 1.1
ConPrintf("Loading assets...")
for name, data in pairs(self.assets) do
local imgName = "Data/"..name..".png"
cacheImage(imgName, data["0.3835"] or data["1"])
data.handle = NewImageHandle()
data.handle:Load(imgName)
data.width, data.height = data.handle:ImageSize()
end
local spriteMap = { }
local spriteSheets = { }
for type, data in pairs(self.skillSprites) do
local maxZoom = data[#data]
local sheet = spriteSheets[maxZoom.filename]
if not sheet then
sheet = { }
local imgName = "Data/"..maxZoom.filename
cacheImage(imgName, self.imageRoot.."build-gen/passive-skill-sprite/"..maxZoom.filename)
sheet.handle = NewImageHandle()
sheet.handle:Load(imgName, "CLAMP")
sheet.width, sheet.height = sheet.handle:ImageSize()
spriteSheets[maxZoom.filename] = sheet
end
for name, coords in pairs(maxZoom.coords) do
if not spriteMap[name] then
spriteMap[name] = { }
end
spriteMap[name][type] = {
handle = sheet.handle,
width = coords.w,
height = coords.h,
[1] = coords.x / sheet.width,
[2] = coords.y / sheet.height,
[3] = (coords.x + coords.w) / sheet.width,
[4] = (coords.y + coords.h) / sheet.height
}
end
end
local nodeOverlay = {
normal = {
alloc = "PSSkillFrameActive",
path = "PSSkillFrameHighlighted",
unalloc = "PSSkillFrame",
allocAscend = "PassiveSkillScreenAscendancyFrameSmallAllocated",
pathAscend = "PassiveSkillScreenAscendancyFrameSmallCanAllocate",
unallocAscend = "PassiveSkillScreenAscendancyFrameSmallNormal"
},
notable = {
alloc = "NotableFrameAllocated",
path = "NotableFrameCanAllocate",
unalloc = "NotableFrameUnallocated",
allocAscend = "PassiveSkillScreenAscendancyFrameLargeAllocated",
pathAscend = "PassiveSkillScreenAscendancyFrameLargeCanAllocate",
unallocAscend = "PassiveSkillScreenAscendancyFrameLargeNormal"
},
keystone = {
alloc = "KeystoneFrameAllocated",
path = "KeystoneFrameCanAllocate",
unalloc = "KeystoneFrameUnallocated"
},
socket = {
alloc = "JewelFrameAllocated",
path = "JewelFrameCanAllocate",
unalloc = "JewelFrameUnallocated"
}
}
for type, data in pairs(nodeOverlay) do
local size = self.assets[data.unalloc].width * 1.33
data.size = size
data.rsq = size * size
end
ConPrintf("Building nodes...")
local nodeMap = { }
local orbitMult = { [0] = 0, m_pi / 3, m_pi / 6, m_pi / 6, m_pi / 20 }
local orbitDist = { [0] = 0, 82, 162, 335, 493 }
for _, node in ipairs(self.nodes) do
nodeMap[node.id] = node
if node.spc[1] then
node.type = "class"
elseif node.isAscendancyStart then
node.type = "ascendClass"
elseif node.m then
node.type = "mastery"
elseif node.isJewelSocket then
node.type = "socket"
elseif node.ks then
node.type = "keystone"
elseif node["not"] then
node.type = "notable"
else
node.type = "normal"
end
node.sprites = spriteMap[node.icon]
node.overlay = nodeOverlay[node.type]
node.linkedId = { }
local group = self.groups[tostring(node.g)]
group.ascendancyName = node.ascendancyName
if node.isAscendancyStart then
group.isAscendancyStart = true
end
node.group = group
node.angle = node.oidx * orbitMult[node.o]
local dist = orbitDist[node.o]
node.x = group.x + m_sin(node.angle) * dist
node.y = group.y - m_cos(node.angle) * dist
node.mods = { }
node.modKey = ""
local i = 1
while node.sd[i] do
local line = node.sd[i]
local list, extra
if line:match("\n") then
list, extra = mod.parseMod(line:gsub("\n", " "))
if list and not extra then
node.sd[i] = line:gsub("\n", " ")
else
table.remove(node.sd, i)
local si = i
for subLine in line:gmatch("[^\n]+") do
table.insert(node.sd, si, subLine)
si = si + 1
end
list, extra = mod.parseMod(node.sd[i])
end
else
list, extra = mod.parseMod(line)
end
if not list then
node.unknown = true
elseif extra then
node.extra = true
else
for k, v in pairs(list) do
node.modKey = node.modKey..k.."="..tostring(v)..","
end
end
node.mods[i] = { list = list, extra = extra }
i = i + 1
end
end
ConPrintf("Building connections...")
self.conn = { }
for _, node in ipairs(self.nodes) do
for _, otherId in ipairs(node.out) do
local other = nodeMap[otherId]
t_insert(node.linkedId, otherId)
t_insert(other.linkedId, node.id)
if node.type ~= "class" and other.type ~= "class" and node.ascendancyName == other.ascendancyName then
t_insert(self.conn, self:BuildConnector(node, other))
end
end
end
self.classNameMap = { }
self.ascendNameMap = { ["None"] = { classId = 0, ascendClassId = 0 } }
for classId, class in pairs(self.classes) do
self.classNameMap[class.name] = classId
for ascendClassId, ascendClass in pairs(class.classes) do
self.ascendNameMap[ascendClass.name] = {
classId = classId,
ascendClassId = ascendClassId
}
end
end
local classArt = {
[0] = "centerscion",
[1] = "centermarauder",
[2] = "centerranger",
[3] = "centerwitch",
[4] = "centerduelist",
[5] = "centertemplar",
[6] = "centershadow"
}
for _, node in ipairs(self.nodes) do
if node.type == "class" then
local class = self.classes[node.spc[1]]
class.startNodeId = node.id
node.startArt = classArt[node.spc[1]]
for _, otherId in ipairs(node.linkedId) do
local other = nodeMap[otherId]
if other.type == "ascendClass" then
for _, ascendClass in pairs(class.classes) do
if other.ascendancyName == ascendClass.name then
ascendClass.startNodeId = otherId
break
end
end
end
end
end
end
return self
end
return TreeClass

568
Modules/TreeView.lua Normal file
View File

@@ -0,0 +1,568 @@
local launch, cfg, main = ...
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 function drawAsset(data, x, y, scale, isHalf)
local width = data.width * scale * 1.33
local height = data.height * scale * 1.33
if isHalf then
DrawImage(data.handle, x - width, y - height * 2, width * 2, height * 2)
DrawImage(data.handle, x - width, y, width * 2, height * 2, 0, 1, 1, 0)
else
DrawImage(data.handle, x - width, y - height, width * 2, height * 2, unpack(data))
end
end
local TreeViewClass = { }
TreeViewClass.__index = TreeViewClass
function TreeViewClass:Zoom(level, viewPort)
self.zoomLevel = m_max(0, m_min(12, self.zoomLevel + level))
local oldZoom = self.zoom
self.zoom = 1.2 ^ self.zoomLevel
local factor = self.zoom / oldZoom
local cursorX, cursorY = GetCursorPos()
local relX = cursorX - viewPort.x - viewPort.width/2
local relY = cursorY - viewPort.y - viewPort.height/2
self.zoomX = relX + (self.zoomX - relX) * factor
self.zoomY = relY + (self.zoomY - relY) * factor
end
function TreeViewClass:AddNodeTooltip(node, build)
-- Special case for sockets
if node.type == "socket" and node.alloc then
local socket, jewel = build.items:GetSocketJewel(node.id)
if jewel then
build.items:AddItemTooltip(jewel, build)
else
main:AddTooltipLine(24, "^7"..node.dn..(IsKeyDown("ALT") and " ["..node.id.."]" or ""))
end
main:AddTooltipSeperator(14)
main:AddTooltipLine(14, "^x80A080Tip: Right click this socket to go to the items page and choose the jewel for this socket.")
return
end
-- Node name
main:AddTooltipLine(24, "^7"..node.dn..(IsKeyDown("ALT") and " ["..node.id.."]" or ""))
if IsKeyDown("ALT") and node.power and node.power.dps then
main:AddTooltipLine(16, string.format("DPS power: %g Defence power: %g", node.power.dps, node.power.def))
end
-- Node description
if node.sd[1] then
main:AddTooltipLine(16, "")
for i, line in ipairs(node.sd) do
if node.mods[i].list then
if IsKeyDown("ALT") then
local modStr
for k, v in pairs(node.mods[i].list) do
modStr = (modStr and modStr..", " or "^2") .. string.format("%s = %s", k, tostring(v))
end
if node.mods[i].extra then
modStr = (modStr and modStr.." " or "") .. "^1" .. node.mods[i].extra
end
if modStr then
line = line .. " " .. modStr
end
end
end
main:AddTooltipLine(16, "^7"..line)
end
end
-- Reminder text
if node.reminderText then
main:AddTooltipSeperator(14)
for _, line in ipairs(node.reminderText) do
main:AddTooltipLine(14, "^xA0A080"..line)
end
end
-- Mod differences
local calcFunc, calcBase = build.calcs:GetNodeCalculator(build)
if calcFunc then
main:AddTooltipSeperator(14)
local count
local nodeOutput, pathOutput
if node.alloc then
count = #node.depends
nodeOutput = calcFunc({node}, true)
pathOutput = calcFunc(node.depends, true)
else
local path = self.tracePath or node.path or { }
count = #path
nodeOutput = calcFunc({node})
pathOutput = calcFunc(path)
end
local none = true
local header = false
for _, data in ipairs(build.displayStats) do
if data.mod then
local diff = (nodeOutput[data.mod] or 0) - (calcBase[data.mod] or 0)
if diff > 0.001 or diff < -0.001 then
none = false
if not header then
main:AddTooltipLine(14, string.format("^7%s this node will give you:", node.alloc and "Unallocating" or "Allocating"))
header = true
end
main:AddTooltipLine(14, string.format("%s%+"..data.fmt.." %s", diff > 0 and "^x00FF44" or "^xFF3300", diff * (data.pc and 100 or 1), data.label))
end
end
end
if count > 1 then
header = false
for _, data in ipairs(build.displayStats) do
if data.mod then
local diff = (pathOutput[data.mod] or 0) - (calcBase[data.mod] or 0)
if diff > 0.001 or diff < -0.001 then
none = false
if not header then
main:AddTooltipLine(14, string.format("^7%s this node and all nodes %s will give you:", node.alloc and "Unallocating" or "Allocating", node.alloc and "depending on it" or "leading to it"))
header = true
end
main:AddTooltipLine(14, string.format("%s%+"..data.fmt.." %s", diff > 0 and "^x00FF44" or "^xFF3300", diff * (data.pc and 100 or 1), data.label))
end
end
end
end
if none then
main:AddTooltipLine(14, string.format("^7No changes from %s this node%s.", node.alloc and "unallocating" or "allocating", count > 1 and " or the nodes leading to it" or ""))
end
end
-- Pathing distance
if node.path and #node.path > 0 then
main:AddTooltipSeperator(14)
main:AddTooltipLine(14, "^7"..#node.path .. " points to node")
if #node.path > 1 then
main:AddTooltipLine(14, "^x80A080")
main:AddTooltipLine(14, "Tip: To reach this node by a different path, hold Shift, then trace the path and click this node")
end
end
end
function TreeViewClass:DrawTree(build, viewPort, inputEvents)
local tree = build.tree
local spec = build.spec
self.build = build
self.viewPort = viewPort
viewPort.height = viewPort.height - 32
local treeClick
common.controlsInput(self, inputEvents)
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "LEFTBUTTON" then
self.dragX, self.dragY = GetCursorPos()
elseif event.key == "z" and IsKeyDown("CTRL") then
spec:Undo()
elseif event.key == "y" and IsKeyDown("CTRL") then
spec:Redo()
elseif event.key == "h" then
self.showHeatMap = not self.showHeatMap
end
elseif event.type == "KeyUp" then
if event.key == "LEFTBUTTON" then
if self.dragX and not self.dragging then
treeClick = "LEFT"
end
elseif event.key == "RIGHTBUTTON" then
treeClick = "RIGHT"
elseif event.key == "WHEELUP" then
self:Zoom(IsKeyDown("SHIFT") and 3 or 1, viewPort)
elseif event.key == "WHEELDOWN" then
self:Zoom(IsKeyDown("SHIFT") and -3 or -1, viewPort)
end
end
end
local cursorX, cursorY = GetCursorPos()
if not IsKeyDown("LEFTBUTTON") then
self.dragging = false
self.dragX, self.dragY = nil, nil
end
if self.dragX then
if not self.dragging then
if math.abs(cursorX - self.dragX) > 5 or math.abs(cursorY - self.dragY) > 5 then
self.dragging = true
end
end
if self.dragging then
self.zoomX = self.zoomX + cursorX - self.dragX
self.zoomY = self.zoomY + cursorY - self.dragY
self.dragX, self.dragY = cursorX, cursorY
end
end
local scale = m_min(viewPort.width, viewPort.height) / tree.size * self.zoom
local function treeToScreen(x, y)
return x * scale + self.zoomX + viewPort.x + viewPort.width/2,
y * scale + self.zoomY + viewPort.y + viewPort.height/2
end
local function screenToTree(x, y)
return (x - self.zoomX - viewPort.x - viewPort.width/2) / scale,
(y - self.zoomY - viewPort.y - viewPort.height/2) / scale
end
if IsKeyDown("SHIFT") then
self.traceMode = true
self.tracePath = self.tracePath or { }
else
self.traceMode = false
self.tracePath = nil
end
local hoverNode
if cursorX >= viewPort.x and cursorX < viewPort.x + viewPort.width and cursorY >= viewPort.y and cursorY < viewPort.y + viewPort.height then
local curTreeX, curTreeY = screenToTree(cursorX, cursorY)
for id, node in pairs(spec.nodes) do
if node.rsq then
local vX = curTreeX - node.x
local vY = curTreeY - node.y
if vX * vX + vY * vY <= node.rsq then
if self.traceMode then
if not node.path then
break
elseif not self.tracePath[1] then
for _, pathNode in ipairs(node.path) do
t_insert(self.tracePath, 1, pathNode)
end
else
local lastPathNode = self.tracePath[#self.tracePath]
if node ~= lastPathNode then
if isValueInArray(self.tracePath, node) then
break
end
if not isValueInArray(node.linked, lastPathNode) then
break
end
t_insert(self.tracePath, node)
end
end
end
hoverNode = node
break
end
end
end
end
local hoverPath, hoverDep
if self.traceMode then
hoverPath = { }
for _, pathNode in pairs(self.tracePath) do
hoverPath[pathNode] = true
end
elseif hoverNode and hoverNode.path then
hoverPath = { }
for _, pathNode in pairs(hoverNode.path) do
hoverPath[pathNode] = true
end
hoverDep = { }
for _, depNode in pairs(hoverNode.depends) do
hoverDep[depNode] = true
end
end
if treeClick == "LEFT" then
if hoverNode then
if hoverNode.alloc then
spec:DeallocNode(hoverNode)
spec:AddUndoState()
elseif hoverNode.path then
spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath)
spec:AddUndoState()
end
end
elseif treeClick == "RIGHT" then
if hoverNode and hoverNode.alloc and hoverNode.type == "socket" then
build.viewMode = "ITEMS"
local slot = build.items.sockets[hoverNode.id]
slot.dropDown.dropped = true
build.items.selControl = slot
end
end
local bg = tree.assets.Background1
local bgSize = bg.width * scale * 1.33 * 2.5
SetDrawColor(1, 1, 1)
DrawImage(bg.handle, viewPort.x, viewPort.y, viewPort.width, viewPort.height, (self.zoomX + viewPort.width/2) / -bgSize, (self.zoomY + viewPort.height/2) / -bgSize, (viewPort.width/2 - self.zoomX) / bgSize, (viewPort.height/2 - self.zoomY) / bgSize)
local curAscendName = tree.classes[spec.curClassId].classes[tostring(spec.curAscendClassId)].name
for _, group in pairs(tree.groups) do
local scrX, scrY = treeToScreen(group.x, group.y)
if group.ascendancyName then
if group.isAscendancyStart then
if group.ascendancyName ~= curAscendName then
SetDrawColor(1, 1, 1, 0.25)
end
drawAsset(tree.assets["Classes"..group.ascendancyName], scrX, scrY, scale)
SetDrawColor(1, 1, 1)
end
elseif group.oo["3"] then
drawAsset(tree.assets.PSGroupBackground3, scrX, scrY, scale, true)
elseif group.oo["2"] then
drawAsset(tree.assets.PSGroupBackground2, scrX, scrY, scale)
elseif group.oo["1"] then
drawAsset(tree.assets.PSGroupBackground1, scrX, scrY, scale)
end
end
for _, conn in pairs(tree.conn) do
local state = "Normal"
local node1, node2 = spec.nodes[conn.nodeId1], spec.nodes[conn.nodeId2]
if node1.alloc and node2.alloc then
state = "Active"
elseif hoverPath then
if (node1.alloc or node1 == hoverNode or hoverPath[node1]) and (node2.alloc or node2 == hoverNode or hoverPath[node2]) then
state = "Intermediate"
end
end
local vert = conn.vert[state]
conn.c[1], conn.c[2] = treeToScreen(vert[1], vert[2])
conn.c[3], conn.c[4] = treeToScreen(vert[3], vert[4])
conn.c[5], conn.c[6] = treeToScreen(vert[5], vert[6])
conn.c[7], conn.c[8] = treeToScreen(vert[7], vert[8])
if hoverDep and hoverDep[node1] and hoverDep[node2] then
SetDrawColor(1, 0, 0)
elseif conn.ascendancyName and conn.ascendancyName ~= curAscendName then
SetDrawColor(0.75, 0.75, 0.75)
end
DrawImageQuad(tree.assets[conn.type..state].handle, unpack(conn.c))
SetDrawColor(1, 1, 1)
end
for id, node in pairs(spec.nodes) do
local base, overlay
if node.type == "class" then
overlay = node.alloc and node.startArt or "PSStartNodeBackgroundInactive"
elseif node.type == "ascendClass" then
overlay = "PassiveSkillScreenAscendancyMiddle"
elseif node.type == "mastery" then
base = node.sprites.mastery
else
local state
if self.showHeatMap or node.alloc or node == hoverNode or (self.traceMode and node == self.tracePath[#self.tracePath])then
state = "alloc"
elseif hoverPath and hoverPath[node] then
state = "path"
else
state = "unalloc"
end
if node.type == "socket" then
base = tree.assets[node.overlay[state .. (node.ascendancyName and "Ascend" or "")]]
local jewel = node.alloc and build.items.list[build.items.sockets[id].selItem]
if jewel then
if jewel.baseName == "Crimson Jewel" then
overlay = "JewelSocketActiveRed"
elseif jewel.baseName == "Viridian Jewel" then
overlay = "JewelSocketActiveGreen"
elseif jewel.baseName == "Cobalt Jewel" then
overlay = "JewelSocketActiveBlue"
end
end
else
base = node.sprites[node.type..(node.alloc and "Active" or "Inactive")]
overlay = node.overlay[state .. (node.ascendancyName and "Ascend" or "")]
end
end
local scrX, scrY = treeToScreen(node.x, node.y)
if node.ascendancyName and node.ascendancyName ~= curAscendName then
SetDrawColor(0.5, 0.5, 0.5)
end
if IsKeyDown("ALT") then
if node.extra then
SetDrawColor(1, 0, 0)
elseif node.unknown then
SetDrawColor(0, 1, 1)
else
SetDrawColor(0, 0, 0)
end
end
if self.showHeatMap then
if build.calcs.powerBuildFlag then
build.calcs:BuildPower()
end
if not node.alloc and node.type ~= "class" and node.type ~= "ascendClass" then
local dps = m_max(node.power.dps or 0, 0)
local def = m_max(node.power.def or 0, 0)
local dpsCol = (dps / build.calcs.powerMax.dps * 1.5) ^ 0.5
local defCol = (def / build.calcs.powerMax.def * 1.5) ^ 0.5
SetDrawColor(dpsCol, (dpsCol + defCol) / 4, defCol)
else
SetDrawColor(1, 1, 1)
end
end
if base then
drawAsset(base, scrX, scrY, scale)
end
if overlay then
if node.type ~= "class" and node.type ~= "ascendClass" then
if #self.searchStr > 0 then
local errMsg, match = PCall(string.match, node.dn:lower(), self.searchStr:lower())
if not match then
for index, line in ipairs(node.sd) do
errMsg, match = PCall(string.match, line:lower(), self.searchStr:lower())
if not match and node.mods[index].list then
for k in pairs(node.mods[index].list) do
errMsg, match = PCall(string.match, k, self.searchStr)
if match then
break
end
end
end
if match then
break
end
end
end
if match then
local col = math.sin((GetTime() / 100) % 360) / 2 + 0.5
SetDrawColor(col, col, col)
end
end
if hoverNode and hoverNode ~= node then
if hoverDep and hoverDep[node] then
SetDrawColor(1, 0, 0)
end
if hoverNode.type == "socket" then
local vX, vY = node.x - hoverNode.x, node.y - hoverNode.y
local dSq = vX * vX + vY * vY
for _, data in ipairs(data.jewelRadius) do
if dSq <= data.rad * data.rad then
SetDrawColor(data.col)
break
end
end
end
end
end
drawAsset(tree.assets[overlay], scrX, scrY, scale)
SetDrawColor(1, 1, 1)
end
end
for nodeId, slot in pairs(build.items.sockets) do
local node = spec.nodes[nodeId]
if node == hoverNode then
local scrX, scrY = treeToScreen(node.x, node.y)
for _, radData in ipairs(data.jewelRadius) do
local size = radData.rad * scale
SetDrawColor(radData.col)
DrawImage(self.ring, scrX - size, scrY - size, size * 2, size * 2)
end
elseif node.alloc then
local socket, jewel = build.items:GetSocketJewel(nodeId)
if jewel and jewel.radius then
local scrX, scrY = treeToScreen(node.x, node.y)
local radData = data.jewelRadius[jewel.radius]
local size = radData.rad * scale
SetDrawColor(radData.col)
DrawImage(self.ring, scrX - size, scrY - size, size * 2, size * 2)
end
end
end
if hoverNode then
self:AddNodeTooltip(hoverNode, build)
local scrX, scrY = treeToScreen(hoverNode.x, hoverNode.y)
local size = m_floor(hoverNode.size * scale)
main:DrawTooltip(m_floor(scrX - size), m_floor(scrY - size), size * 2, size * 2, viewPort)
end
SetDrawColor(0.05, 0.05, 0.05)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height + 4, viewPort.width, 28)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height, viewPort.width, 4)
common.controlsDraw(self, viewPort)
end
function TreeViewClass:Load(xml, fileName)
if xml.attrib.zoomLevel then
self.zoomLevel = tonumber(xml.attrib.zoomLevel)
self.zoom = 1.2 ^ self.zoomLevel
end
if xml.attrib.zoomX and xml.attrib.zoomY then
self.zoomX = tonumber(xml.attrib.zoomX)
self.zoomY = tonumber(xml.attrib.zoomY)
end
if xml.attrib.searchStr then
self.searchStr = xml.attrib.searchStr
self.controls.treeSearch:SetText(self.searchStr)
end
if xml.attrib.showHeatMap then
self.showHeatMap = xml.attrib.showHeatMap == "true"
end
end
function TreeViewClass:Save(xml)
xml.attrib = {
zoomLevel = tostring(self.zoomLevel),
zoomX = tostring(self.zoomX),
zoomY = tostring(self.zoomY),
searchStr = self.searchStr,
showHeatMap = tostring(self.showHeatMap),
}
end
function TreeViewClass.NewTreeView()
local self = setmetatable({}, TreeViewClass)
self.ring = NewImageHandle()
self.ring:Load("ring.png")
self.zoomLevel = 3
self.zoom = 1.2 ^ self.zoomLevel
self.zoomX = 0
self.zoomY = 0
self.searchStr = ""
self.controls = { }
t_insert(self.controls, common.newButton(function() return self.viewPort.x + 4 end, function() return self.viewPort.y + self.viewPort.height + 8 end, 60, 20, "Reset", function()
launch:ShowPrompt(1, 0, 0, "Are you sure to want to reset your tree?\nPress Y to continue.", function(key)
if key == "y" then
self.build.spec:ResetNodes()
self.build.spec:AddUndoState()
end
return true
end)
end))
t_insert(self.controls, common.newButton(function() return self.viewPort.x + 4 + 68*1 end, function() return self.viewPort.y + self.viewPort.height + 8 end, 60, 20, "Import", function()
launch:ShowPrompt(0, 0, 0, "Press Ctrl+V to import passive tree link.", function(key)
if key == "v" and IsKeyDown("CTRL") then
local url = Paste()
if url and #url > 0 then
self.build.spec:DecodeURL(url)
self.build.spec:AddUndoState()
end
return true
elseif key == "ESCAPE" then
return true
end
end)
end))
t_insert(self.controls, common.newButton(function() return self.viewPort.x + 4 + 68*2 end, function() return self.viewPort.y + self.viewPort.height + 8 end, 60, 20, "Export", function()
launch:ShowPrompt(0, 0, 0, "Press Ctrl+C to copy passive tree link.", function(key)
if key == "c" and IsKeyDown("CTRL") then
Copy(self.build.spec:EncodeURL("https://www.pathofexile.com/passive-skill-tree/"))
return true
elseif key == "ESCAPE" then
return true
end
end)
end))
self.controls.treeSearch = common.newEditControl(function() return self.viewPort.x + 4 + 68*3 end, function() return self.viewPort.y + self.viewPort.height + 8 end, 400, 20, "", "Search", "[^%c%(%)]", 100, nil, function(buf)
self.searchStr = buf
end)
return self
end
return TreeViewClass