Files
PathOfBuilding/Classes/PassiveTreeView.lua
2016-05-15 16:38:13 +10:00

564 lines
18 KiB
Lua

-- Path of Building
--
-- Class: Passive Tree View
-- Passive skill tree viewer.
--
local launch, 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 TreeViewClass = common.NewClass("PassiveTreeView", function(self)
self.ring = NewImageHandle()
self.ring:Load("Assets/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.New("ButtonControl", 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.New("ButtonControl", 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.New("ButtonControl", 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.New("EditControl", 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)
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: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.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)
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 ~= spec.curAscendClassName then
SetDrawColor(1, 1, 1, 0.25)
end
self:DrawAsset(tree.assets["Classes"..group.ascendancyName], scrX, scrY, scale)
SetDrawColor(1, 1, 1)
end
elseif group.oo["3"] then
self:DrawAsset(tree.assets.PSGroupBackground3, scrX, scrY, scale, true)
elseif group.oo["2"] then
self:DrawAsset(tree.assets.PSGroupBackground2, scrX, scrY, scale)
elseif group.oo["1"] then
self: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 ~= spec.curAscendClassName 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 ~= spec.curAscendClassName 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
self: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
self: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: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
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