Moving stuff around
This commit is contained in:
310
Modules/Build.lua
Normal file
310
Modules/Build.lua
Normal 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
665
Modules/Calcs.lua
Normal 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
1399
Modules/CalcsControl.lua
Normal file
File diff suppressed because it is too large
Load Diff
505
Modules/CalcsView.lua
Normal file
505
Modules/CalcsView.lua
Normal 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
94
Modules/Data.lua
Normal 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
533
Modules/Items.lua
Normal 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
311
Modules/List.lua
Normal 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
203
Modules/Main.lua
Normal 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
545
Modules/ModParser.lua
Normal 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
155
Modules/ModTools.lua
Normal 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
310
Modules/Spec.lua
Normal 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
338
Modules/Tree.lua
Normal 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
568
Modules/TreeView.lua
Normal 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
|
||||
Reference in New Issue
Block a user