-- Path of Building -- -- Class: Passive Tree View -- Passive skill tree viewer. -- Draws the passive skill tree, and also maintains the current view settings (zoom level, position, etc) -- local pairs = pairs local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove local m_min = math.min local m_max = math.max local m_floor = math.floor local band = bit.band local b_rshift = bit.rshift local PassiveTreeViewClass = newClass("PassiveTreeView", function(self) self.ring = NewImageHandle() self.ring:Load("Assets/ring.png", "CLAMP") self.highlightRing = NewImageHandle() self.highlightRing:Load("Assets/small_ring.png", "CLAMP") self.jewelShadedOuterRing = NewImageHandle() self.jewelShadedOuterRing:Load("Assets/ShadedOuterRing.png", "CLAMP") self.jewelShadedOuterRingFlipped = NewImageHandle() self.jewelShadedOuterRingFlipped:Load("Assets/ShadedOuterRingFlipped.png", "CLAMP") self.jewelShadedInnerRing = NewImageHandle() self.jewelShadedInnerRing:Load("Assets/ShadedInnerRing.png", "CLAMP") self.jewelShadedInnerRingFlipped = NewImageHandle() self.jewelShadedInnerRingFlipped:Load("Assets/ShadedInnerRingFlipped.png", "CLAMP") self.eternal1 = NewImageHandle() self.eternal1:Load("TreeData/PassiveSkillScreenEternalEmpireJewelCircle1.png", "CLAMP") self.eternal2 = NewImageHandle() self.eternal2:Load("TreeData/PassiveSkillScreenEternalEmpireJewelCircle2.png", "CLAMP") self.karui1 = NewImageHandle() self.karui1:Load("TreeData/PassiveSkillScreenKaruiJewelCircle1.png", "CLAMP") self.karui2 = NewImageHandle() self.karui2:Load("TreeData/PassiveSkillScreenKaruiJewelCircle2.png", "CLAMP") self.maraketh1 = NewImageHandle() self.maraketh1:Load("TreeData/PassiveSkillScreenMarakethJewelCircle1.png", "CLAMP") self.maraketh2 = NewImageHandle() self.maraketh2:Load("TreeData/PassiveSkillScreenMarakethJewelCircle2.png", "CLAMP") self.templar1 = NewImageHandle() self.templar1:Load("TreeData/PassiveSkillScreenTemplarJewelCircle1.png", "CLAMP") self.templar2 = NewImageHandle() self.templar2:Load("TreeData/PassiveSkillScreenTemplarJewelCircle2.png", "CLAMP") self.vaal1 = NewImageHandle() self.vaal1:Load("TreeData/PassiveSkillScreenVaalJewelCircle1.png", "CLAMP") self.vaal2 = NewImageHandle() self.vaal2:Load("TreeData/PassiveSkillScreenVaalJewelCircle2.png", "CLAMP") self.tooltip = new("Tooltip") self.zoomLevel = 3 self.zoom = 1.2 ^ self.zoomLevel self.zoomX = 0 self.zoomY = 0 self.searchStr = "" self.searchStrCached = "" self.searchStrResults = {} self.showStatDifferences = true end) function PassiveTreeViewClass: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 end if xml.attrib.showHeatMap then self.showHeatMap = xml.attrib.showHeatMap == "true" end if xml.attrib.showStatDifferences then self.showStatDifferences = xml.attrib.showStatDifferences == "true" end end function PassiveTreeViewClass:Save(xml) xml.attrib = { zoomLevel = tostring(self.zoomLevel), zoomX = tostring(self.zoomX), zoomY = tostring(self.zoomY), searchStr = self.searchStr, showHeatMap = tostring(self.showHeatMap), showStatDifferences = tostring(self.showStatDifferences), } end function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) local spec = build.spec local tree = spec.tree local cursorX, cursorY = GetCursorPos() local mOver = cursorX >= viewPort.x and cursorX < viewPort.x + viewPort.width and cursorY >= viewPort.y and cursorY < viewPort.y + viewPort.height -- Process input events local treeClick for id, event in ipairs(inputEvents) do if event.type == "KeyDown" then if event.key == "LEFTBUTTON" then if mOver then -- Record starting coords of mouse drag -- Dragging won't actually commence unless the cursor moves far enough self.dragX, self.dragY = cursorX, cursorY end elseif event.key == "p" then self.showHeatMap = not self.showHeatMap elseif event.key == "d" and IsKeyDown("CTRL") then self.showStatDifferences = not self.showStatDifferences elseif event.key == "PAGEUP" then self:Zoom(IsKeyDown("SHIFT") and 3 or 1, viewPort) elseif event.key == "PAGEDOWN" then self:Zoom(IsKeyDown("SHIFT") and -3 or -1, viewPort) end elseif event.type == "KeyUp" then if event.key == "LEFTBUTTON" then if self.dragX and not self.dragging then -- Mouse button went down, but didn't move far enough to trigger drag, so register a normal click treeClick = "LEFT" end elseif mOver then if 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 end if not IsKeyDown("LEFTBUTTON") then -- Left mouse button isn't down, stop dragging if dragging was in progress self.dragging = false self.dragX, self.dragY = nil, nil end if self.dragX then -- Left mouse is down if not self.dragging then -- Check if mouse has moved more than a few pixels, and if so, initiate dragging 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 -- Ctrl-click to zoom if treeClick and IsKeyDown("CTRL") then self:Zoom(treeClick == "RIGHT" and -2 or 2, viewPort) treeClick = nil end -- Clamp zoom offset local clampFactor = self.zoom * 2 / 3 self.zoomX = m_min(m_max(self.zoomX, -viewPort.width * clampFactor), viewPort.width * clampFactor) self.zoomY = m_min(m_max(self.zoomY, -viewPort.height * clampFactor), viewPort.height * clampFactor) -- Create functions that will convert coordinates between the screen and tree coordinate spaces local scale = m_min(viewPort.width, viewPort.height) / tree.size * self.zoom local offsetX = self.zoomX + viewPort.x + viewPort.width/2 local offsetY = self.zoomY + viewPort.y + viewPort.height/2 local function treeToScreen(x, y) return x * scale + offsetX, y * scale + offsetY end local function screenToTree(x, y) return (x - offsetX) / scale, (y - offsetY) / scale end if IsKeyDown("SHIFT") then -- Enable path tracing mode self.traceMode = true self.tracePath = self.tracePath or { } else self.traceMode = false self.tracePath = nil end local hoverNode if mOver then -- Cursor is over the tree, check if it is over a node local curTreeX, curTreeY = screenToTree(cursorX, cursorY) for nodeId, node in pairs(spec.nodes) do if node.rsq and node.group and not node.isProxy and not node.group.isProxy then -- Node has a defined size (i.e. has artwork) local vX = curTreeX - node.x local vY = curTreeY - node.y if vX * vX + vY * vY <= node.rsq then hoverNode = node break end end end end -- If hovering over a node, find the path to it (if unallocated) or the list of dependent nodes (if allocated) local hoverPath, hoverDep if self.traceMode then -- Path tracing mode is enabled if hoverNode then if not hoverNode.path then -- Don't highlight the node if it can't be pathed to hoverNode = nil elseif not self.tracePath[1] then -- Initialise the trace path using this node's path for _, pathNode in ipairs(hoverNode.path) do t_insert(self.tracePath, 1, pathNode) end else local lastPathNode = self.tracePath[#self.tracePath] if hoverNode ~= lastPathNode then -- If node is directly linked to the last node in the path, add it if isValueInArray(hoverNode.linked, lastPathNode) then local index = isValueInArray(self.tracePath, hoverNode) if index then -- Node is already in the trace path, remove it first t_remove(self.tracePath, index) end t_insert(self.tracePath, hoverNode) else hoverNode = nil end end end end -- Use the trace path as the path hoverPath = { } for _, pathNode in pairs(self.tracePath) do hoverPath[pathNode] = true end elseif hoverNode and hoverNode.path then -- Use the node's own path and dependence list hoverPath = { } if not hoverNode.dependsOnIntuitiveLeapLike then for _, pathNode in pairs(hoverNode.path) do hoverPath[pathNode] = true end end hoverDep = { } for _, depNode in pairs(hoverNode.depends) do hoverDep[depNode] = true end end if treeClick == "LEFT" then if hoverNode then -- User left-clicked on a node if hoverNode.alloc then -- Node is allocated, so deallocate it spec:DeallocNode(hoverNode) spec:AddUndoState() build.buildFlag = true elseif hoverNode.path then -- Node is unallocated and can be allocated, so allocate it spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath) spec:AddUndoState() build.buildFlag = true end end elseif treeClick == "RIGHT" then if hoverNode and hoverNode.alloc and hoverNode.type == "Socket" then local slot = build.itemsTab.sockets[hoverNode.id] if slot:IsEnabled() then -- User right-clicked a jewel socket, jump to the item page and focus the corresponding item slot control slot.dropped = true build.itemsTab:SelectControl(slot) build.viewMode = "ITEMS" end --[[ Only allow node editing in these situations: Vaal (Glorious Vanity): any non-keystone Maraketh (Brutal Restraint): only notables, +dex already set Eternal (Elegant Hubris): only notables, other passives are blank Karui (Lethal Pride): only notables, +str already set Templar (Militant Faith): any non-keystone, non-notables add devotion or replace with devotion ]]-- elseif hoverNode and hoverNode.conqueredBy and (hoverNode.conqueredBy.conqueror.type == "vaal" or hoverNode.isNotable) then build.treeTab:ModifyNodePopup(hoverNode) build.buildFlag = true end end -- Draw the background artwork local bg = tree.assets.Background1 if bg.width == 0 then bg.width, bg.height = bg.handle:ImageSize() end if bg.width > 0 then 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) end -- Hack to draw class background art, the position data doesn't seem to be in the tree JSON yet if build.spec.curClassId == 1 then local scrX, scrY = treeToScreen(-2750, 1600) self:DrawAsset(tree.assets.BackgroundStr, scrX, scrY, scale) elseif build.spec.curClassId == 2 then local scrX, scrY = treeToScreen(2550, 1600) self:DrawAsset(tree.assets.BackgroundDex, scrX, scrY, scale) elseif build.spec.curClassId == 3 then local scrX, scrY = treeToScreen(-250, -2200) self:DrawAsset(tree.assets.BackgroundInt, scrX, scrY, scale) elseif build.spec.curClassId == 4 then local scrX, scrY = treeToScreen(-150, 2350) self:DrawAsset(tree.assets.BackgroundStrDex, scrX, scrY, scale) elseif build.spec.curClassId == 5 then local scrX, scrY = treeToScreen(-2100, -1500) self:DrawAsset(tree.assets.BackgroundStrInt, scrX, scrY, scale) elseif build.spec.curClassId == 6 then local scrX, scrY = treeToScreen(2350, -1950) self:DrawAsset(tree.assets.BackgroundDexInt, scrX, scrY, scale) end local function renderGroup(group, isExpansion) 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[isExpansion and "GroupBackgroundLargeHalfAlt" or "PSGroupBackground3"], scrX, scrY, scale, true) elseif group.oo[2] then self:DrawAsset(tree.assets[isExpansion and "GroupBackgroundMediumAlt" or "PSGroupBackground2"], scrX, scrY, scale) elseif group.oo[1] then self:DrawAsset(tree.assets[isExpansion and "GroupBackgroundSmallAlt" or "PSGroupBackground1"], scrX, scrY, scale) end end -- Draw the group backgrounds for _, group in pairs(tree.groups) do if not group.isProxy then renderGroup(group) end end for _, subGraph in pairs(spec.subGraphs) do renderGroup(subGraph.group, true) end local function renderConnector(connector) local node1, node2 = spec.nodes[connector.nodeId1], spec.nodes[connector.nodeId2] local connectorColor = {1,1,1} local function getState(n1, n2) -- Determine the connector state local state = "Normal" if n1.alloc and n2.alloc then state = "Active" elseif hoverPath then if (n1.alloc or n1 == hoverNode or hoverPath[n1]) and (n2.alloc or n2 == hoverNode or hoverPath[n2]) then state = "Intermediate" end end return state end local state = getState(node1, node2); local baseState = state if self.compareSpec then local cNode1, cNode2 = self.compareSpec.nodes[connector.nodeId1], self.compareSpec.nodes[connector.nodeId2] if cNode1 and cNode2 then baseState = getState(cNode1,cNode2) end end if baseState == "Active" and state ~= "Active" then state = "Active" connectorColor = {0,1,0} end if baseState ~= "Active" and state == "Active" then connectorColor = {1,0,0} end -- Convert vertex coordinates to screen-space and add them to the coordinate array local vert = connector.vert[state] connector.c[1], connector.c[2] = treeToScreen(vert[1], vert[2]) connector.c[3], connector.c[4] = treeToScreen(vert[3], vert[4]) connector.c[5], connector.c[6] = treeToScreen(vert[5], vert[6]) connector.c[7], connector.c[8] = treeToScreen(vert[7], vert[8]) if hoverDep and hoverDep[node1] and hoverDep[node2] then -- Both nodes depend on the node currently being hovered over, so color the line red SetDrawColor(1, 0, 0) elseif connector.ascendancyName and connector.ascendancyName ~= spec.curAscendClassName then -- Fade out lines in ascendancy classes other than the current one SetDrawColor(0.75, 0.75, 0.75) else SetDrawColor(connectorColor[1], connectorColor[2], connectorColor[3]) end DrawImageQuad(tree.assets[connector.type..state].handle, unpack(connector.c)) end -- Draw the connecting lines between nodes SetDrawLayer(nil, 20) for _, connector in pairs(tree.connectors) do renderConnector(connector) end for _, subGraph in pairs(spec.subGraphs) do for _, connector in pairs(subGraph.connectors) do renderConnector(connector) end end if self.showHeatMap then -- Build the power numbers if needed build.calcsTab:BuildPower() self.heatMapStat = build.calcsTab.powerStat end -- Update cached node data if self.searchStrCached ~= self.searchStr then self.searchStrCached = self.searchStr local function prepSearch(search) search = search:lower() local searchWords = {} for matchstring, v in search:gmatch('"([^"]*)"') do searchWords[#searchWords+1] = matchstring search = search:gsub('"'..matchstring..'"', "") end for matchstring, v in search:gmatch("(%S*)") do if matchstring:match("%S") ~= nil then searchWords[#searchWords+1] = matchstring end end return searchWords end self.searchParams = prepSearch(self.searchStr) for nodeId, node in pairs(spec.nodes) do self.searchStrResults[nodeId] = #self.searchParams > 0 and self:DoesNodeMatchSearchParams(node) end end -- Draw the nodes for nodeId, node in pairs(spec.nodes) do -- Determine the base and overlay images for this node based on type and state local compareNode = self.compareSpec and self.compareSpec.nodes[nodeId] or nil local base, overlay local isAlloc = node.alloc or build.calcsTab.mainEnv.grantedPassives[nodeId] or (compareNode and compareNode.alloc) SetDrawLayer(nil, 25) if node.type == "ClassStart" then overlay = isAlloc and node.startArt or "PSStartNodeBackgroundInactive" elseif node.type == "AscendClassStart" then overlay = treeVersions[tree.treeVersion].num >= 3.10 and "AscendancyMiddle" or "PassiveSkillScreenAscendancyMiddle" elseif node.type == "Mastery" then -- This is the icon that appears in the center of many groups SetDrawLayer(nil, 15) base = node.sprites.mastery else local state if self.showHeatMap or isAlloc or node == hoverNode or (self.traceMode and node == self.tracePath[#self.tracePath])then -- Show node as allocated if it is being hovered over -- Also if the heat map is turned on (makes the nodes more visible) state = "alloc" elseif hoverPath and hoverPath[node] then state = "path" else state = "unalloc" end if node.type == "Socket" then -- Node is a jewel socket, retrieve the socketed jewel (if present) so we can display the correct art base = tree.assets[node.overlay[state .. (node.expansionJewel and "Alt" or "")]] local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(nodeId) if isAlloc and jewel then if jewel.baseName == "Crimson Jewel" then overlay = node.expansionJewel and "JewelSocketActiveRedAlt" or "JewelSocketActiveRed" elseif jewel.baseName == "Viridian Jewel" then overlay = node.expansionJewel and "JewelSocketActiveGreenAlt" or "JewelSocketActiveGreen" elseif jewel.baseName == "Cobalt Jewel" then overlay = node.expansionJewel and "JewelSocketActiveBlueAlt" or "JewelSocketActiveBlue" elseif jewel.baseName == "Prismatic Jewel" then overlay = node.expansionJewel and "JewelSocketActivePrismaticAlt" or "JewelSocketActivePrismatic" elseif jewel.base.subType == "Abyss" then overlay = node.expansionJewel and "JewelSocketActiveAbyssAlt" or "JewelSocketActiveAbyss" elseif jewel.baseName == "Timeless Jewel" then overlay = node.expansionJewel and "JewelSocketActiveLegionAlt" or "JewelSocketActiveLegion" elseif jewel.baseName == "Large Cluster Jewel" then -- Temp; waiting for art :/ overlay = "JewelSocketActiveGreenAlt" elseif jewel.baseName == "Medium Cluster Jewel" then -- Temp; waiting for art :/ overlay = "JewelSocketActiveBlueAlt" elseif jewel.baseName == "Small Cluster Jewel" then -- Temp; waiting for art :/ overlay = "JewelSocketActiveRedAlt" end end else -- Normal node (includes keystones and notables) base = node.sprites[node.type:lower()..(isAlloc and "Active" or "Inactive")] overlay = node.overlay[state .. (node.ascendancyName and "Ascend" or "") .. (node.isBlighted and "Blighted" or "")] end end -- Convert node position to screen-space local scrX, scrY = treeToScreen(node.x, node.y) -- Determine color for the base artwork if self.showHeatMap then if not isAlloc and node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then if self.heatMapStat and self.heatMapStat.stat then -- Calculate color based on a single stat local stat = m_max(node.power.singleStat or 0, 0) local statCol = (stat / build.calcsTab.powerMax.singleStat * 1.5) ^ 0.5 if(stat ~= 0) then if(self.heatMapStatPerPoint and self.heatMapTopPick) then statCol = stat / node.pathDist == build.calcsTab.powerMax.singleStatPerPoint and 1.5 ^ 0.5 or 0 elseif self.heatMapStatPerPoint then statCol = statCol / node.pathDist * 4 elseif self.heatMapTopPick then statCol = stat == build.calcsTab.powerMax.singleStat and 1.5 ^ 0.5 or 0 end end if main.nodePowerTheme == "RED/BLUE" then SetDrawColor(statCol, 0, 0) elseif main.nodePowerTheme == "RED/GREEN" then SetDrawColor(0, statCol, 0) elseif main.nodePowerTheme == "GREEN/BLUE" then SetDrawColor(0, 0, statCol) end else -- Calculate color based on DPS and defensive powers local offence = m_max(node.power.offence or 0, 0) local defence = m_max(node.power.defence or 0, 0) local dpsCol = (offence / build.calcsTab.powerMax.offence * 1.5) ^ 0.5 local defCol = (defence / build.calcsTab.powerMax.defence * 1.5) ^ 0.5 if offence ~= 0 or defence ~= 0 then if(self.heatMapStatPerPoint and self.heatMapTopPick) then dpsCol = offence / node.pathDist == build.calcsTab.powerMax.offencePerPoint and 1.5 ^ 0.5 or 0 defCol = defence / node.pathDist == build.calcsTab.powerMax.defencePerPoint and 1.5 ^ 0.5 or 0 elseif self.heatMapStatPerPoint then dpsCol = dpsCol / node.pathDist * 4 defCol = defCol / node.pathDist * 4 elseif self.heatMapTopPick then dpsCol = offence == build.calcsTab.powerMax.offence and 1.5 ^ 0.5 or 0 defCol = defence == build.calcsTab.powerMax.defence and 1.5 ^ 0.5 or 0 end end local mixCol = (m_max(dpsCol - 0.5, 0) + m_max(defCol - 0.5, 0)) / 2 if main.nodePowerTheme == "RED/BLUE" then SetDrawColor(dpsCol, mixCol, defCol) elseif main.nodePowerTheme == "RED/GREEN" then SetDrawColor(dpsCol, defCol, mixCol) elseif main.nodePowerTheme == "GREEN/BLUE" then SetDrawColor(mixCol, dpsCol, defCol) end end else if compareNode then if compareNode.alloc and not node.alloc then -- Base has, current has not, color green (take these nodes to match) SetDrawColor(0, 1, 0) elseif not compareNode.alloc and node.alloc then -- Base has not, current has, color red (Remove nodes to match) SetDrawColor(1, 0, 0) else -- Both have or both have not, use white SetDrawColor(1, 1, 1) end else SetDrawColor(1, 1, 1) end end elseif launch.devModeAlt then -- Debug display if node.extra then SetDrawColor(1, 0, 0) elseif node.unknown then SetDrawColor(0, 1, 1) else SetDrawColor(0, 0, 0) end else if compareNode then if compareNode.alloc and not node.alloc then -- Base has, current has not, color green (take these nodes to match) SetDrawColor(0, 1, 0) elseif not compareNode.alloc and node.alloc then -- Base has not, current has, color red (Remove nodes to match) SetDrawColor(1, 0, 0) else -- Both have or both have not, use white SetDrawColor(1, 1, 1) end else SetDrawColor(1, 1, 1) end end -- Draw base artwork if base then self:DrawAsset(base, scrX, scrY, scale) end if overlay then -- Draw overlay if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then if hoverNode and hoverNode ~= node then -- Mouse is hovering over a different node if hoverDep and hoverDep[node] then -- This node depends on the hover node, turn it red SetDrawColor(1, 0, 0) elseif hoverNode.type == "Socket" and hoverNode.nodesInRadius then -- Hover node is a socket, check if this node falls within its radius and color it accordingly local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(hoverNode.id) local isThreadOfHope = jewel and jewel.jewelRadiusLabel == "Variable" if isThreadOfHope then -- Jewel in socket is Thread of Hope or similar for index, data in ipairs(build.data.jewelRadius) do if hoverNode.nodesInRadius[index][node.id] then -- Draw Thread of Hope's annuli if data.inner ~= 0 then SetDrawColor(data.col) break end end end else -- Jewel in socket is not Thread of Hope or similar for index, data in ipairs(build.data.jewelRadius) do if hoverNode.nodesInRadius[index][node.id] then -- Draw normal jewel radii if data.inner == 0 then SetDrawColor(data.col) break end end end end end end end self:DrawAsset(tree.assets[overlay], scrX, scrY, scale) SetDrawColor(1, 1, 1) end if self.searchStrResults[nodeId] then -- Node matches the search string, show the highlight circle SetDrawLayer(nil, 30) SetDrawColor(1, 0, 0) local size = 175 * scale / self.zoom ^ 0.4 DrawImage(self.highlightRing, scrX - size, scrY - size, size * 2, size * 2) end if node == hoverNode and (node.type ~= "Socket" or not IsKeyDown("SHIFT")) and not IsKeyDown("CTRL") and not main.popups[1] then -- Draw tooltip SetDrawLayer(nil, 100) local size = m_floor(node.size * scale) if self.tooltip:CheckForUpdate(node, self.showStatDifferences, self.tracePath, launch.devModeAlt, build.outputRevision) then self:AddNodeTooltip(self.tooltip, node, build) end self.tooltip:Draw(m_floor(scrX - size), m_floor(scrY - size), size * 2, size * 2, viewPort) end end -- Draw ring overlays for jewel sockets SetDrawLayer(nil, 25) for nodeId in pairs(tree.sockets) do local node = spec.nodes[nodeId] if node and (not node.expansionJewel or node.expansionJewel.size == 2) then local scrX, scrY = treeToScreen(node.x, node.y) local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(nodeId) if node == hoverNode then local isThreadOfHope = jewel and jewel.jewelRadiusLabel == "Variable" if isThreadOfHope then for _, radData in ipairs(build.data.jewelRadius) do local outerSize = radData.outer * scale local innerSize = radData.inner * scale -- Jewel in socket is Thread of Hope or similar, draw it's annulus if innerSize ~= 0 then SetDrawColor(radData.col) DrawImage(self.ring, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.ring, scrX - innerSize, scrY - innerSize, innerSize * 2, innerSize * 2) end end else for _, radData in ipairs(build.data.jewelRadius) do local outerSize = radData.outer * scale local innerSize = radData.inner * scale -- Jewel in socket is not Thread of Hope or similar, draw normal jewel radius if innerSize == 0 then SetDrawColor(radData.col) DrawImage(self.ring, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) end end end elseif node.alloc then if jewel and jewel.jewelRadiusIndex then -- Draw only the selected jewel radius local radData = build.data.jewelRadius[jewel.jewelRadiusIndex] local outerSize = radData.outer * scale local innerSize = radData.inner * scale * 1.06 if jewel.title == "Brutal Restraint" then DrawImage(self.maraketh1, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.maraketh2, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) elseif jewel.title == "Elegant Hubris" then DrawImage(self.eternal1, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.eternal2, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) elseif jewel.title == "Glorious Vanity" then DrawImage(self.vaal1, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.vaal2, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) elseif jewel.title == "Lethal Pride" then DrawImage(self.karui1, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.karui2, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) elseif jewel.title == "Militant Faith" then DrawImage(self.templar1, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.templar2, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) else SetDrawColor(0.9,0.9,1,0.7) DrawImage(self.jewelShadedOuterRing, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.jewelShadedOuterRingFlipped, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.jewelShadedInnerRing, scrX - innerSize, scrY - innerSize, innerSize * 2, innerSize * 2) DrawImage(self.jewelShadedInnerRingFlipped, scrX - innerSize, scrY - innerSize, innerSize * 2, innerSize * 2) end end end end end end -- Draws the given asset at the given position function PassiveTreeViewClass:DrawAsset(data, x, y, scale, isHalf) if not data then return end if data.width == 0 then data.width, data.height = data.handle:ImageSize() if data.width == 0 then return end end 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 -- Zoom the tree in or out function PassiveTreeViewClass:Zoom(level, viewPort) -- Calculate new zoom level and zoom factor self.zoomLevel = m_max(0, m_min(12, self.zoomLevel + level)) local oldZoom = self.zoom self.zoom = 1.2 ^ self.zoomLevel -- Adjust zoom center position so that the point on the tree that is currently under the mouse will remain under it 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 PassiveTreeViewClass:Focus(x, y, viewPort, build) self.zoomLevel = 12 self.zoom = 1.2 ^ self.zoomLevel local tree = build.spec.tree local scale = m_min(viewPort.width, viewPort.height) / tree.size * self.zoom self.zoomX = -x * scale self.zoomY = -y * scale end function PassiveTreeViewClass:DoesNodeMatchSearchParams(node) if node.type == "ClassStart" or node.type == "Mastery" then return end local needMatches = copyTable(self.searchParams) local err local function search(haystack, need) for i=#need, 1, -1 do if haystack:match(need[i]) then table.remove(need, i) end end return need end -- Check node name err, needMatches = PCall(search, node.dn:lower(), needMatches) if #needMatches == 0 then return true end -- Check node description for index, line in ipairs(node.sd) do -- Check display text first err, needMatches = PCall(search, line:lower(), needMatches) if #needMatches == 0 then return true end if #needMatches > 0 and node.mods[index].list then -- Then check modifiers for _, mod in ipairs(node.mods[index].list) do err, needMatches = PCall(search, mod.name, needMatches) if #needMatches == 0 then return true end end end end -- Check node type err, needMatches = PCall(search, node.type:lower(), needMatches) if #needMatches == 0 then return true end end function PassiveTreeViewClass:AddNodeName(tooltip, node, build) tooltip:SetRecipe(node.recipe) tooltip:AddLine(24, "^7"..node.dn..(launch.devModeAlt and " ["..node.id.."]" or "")) if launch.devModeAlt and node.id > 65535 then -- Decompose cluster node Id local index = band(node.id, 0xF) local size = band(b_rshift(node.id, 4), 0x3) local large = band(b_rshift(node.id, 6), 0x7) local medium = band(b_rshift(node.id, 9), 0x3) tooltip:AddLine(16, string.format("^7Cluster node index: %d, size: %d, large index: %d, medium index: %d", index, size, large, medium)) end if node.type == "Socket" and node.nodesInRadius then local attribTotals = { } for nodeId in pairs(node.nodesInRadius[2]) do local specNode = build.spec.nodes[nodeId] for _, attrib in ipairs{"Str","Dex","Int"} do attribTotals[attrib] = (attribTotals[attrib] or 0) + specNode.finalModList:Sum("BASE", nil, attrib) end end if attribTotals["Str"] >= 40 then tooltip:AddLine(16, "^7Can support "..colorCodes.STRENGTH.."Strength ^7threshold jewels") end if attribTotals["Dex"] >= 40 then tooltip:AddLine(16, "^7Can support "..colorCodes.DEXTERITY.."Dexterity ^7threshold jewels") end if attribTotals["Int"] >= 40 then tooltip:AddLine(16, "^7Can support "..colorCodes.INTELLIGENCE.."Intelligence ^7threshold jewels") end end if node.type == "Socket" and node.alloc then if node.distanceToClassStart and node.distanceToClassStart > 0 then tooltip:AddSeparator(14) tooltip:AddLine(16, string.format("^7Distance to start: %d", node.distanceToClassStart)) end end end function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build) -- Special case for sockets if node.type == "Socket" and node.alloc then local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(node.id) if jewel then build.itemsTab:AddItemTooltip(tooltip, jewel, { nodeId = node.id }) if node.distanceToClassStart and node.distanceToClassStart > 0 then tooltip:AddSeparator(14) tooltip:AddLine(16, string.format("^7Distance to start: %d", node.distanceToClassStart)) end else self:AddNodeName(tooltip, node, build) end tooltip:AddSeparator(14) if socket:IsEnabled() then tooltip:AddLine(14, colorCodes.TIP.."Tip: Right click this socket to go to the items page and choose the jewel for this socket.") end tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.") return end -- Node name self:AddNodeName(tooltip, node, build) if launch.devModeAlt then if node.power and node.power.offence then -- Power debugging info tooltip:AddLine(16, string.format("DPS power: %g Defence power: %g", node.power.offence, node.power.defence)) end end if node.sd[1] then tooltip:AddLine(16, "") for i, line in ipairs(node.sd) do if node.mods[i].list then if launch.devModeAlt then -- Modifier debugging info local modStr for _, mod in pairs(node.mods[i].list) do modStr = (modStr and modStr..", " or "^2") .. modLib.formatMod(mod) 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 tooltip:AddLine(16, ((node.mods[i].extra or not node.mods[i].list) and colorCodes.UNSUPPORTED or colorCodes.MAGIC)..line) end end -- Reminder text if node.reminderText then tooltip:AddSeparator(14) for _, line in ipairs(node.reminderText) do tooltip:AddLine(14, "^xA0A080"..line) end end -- Conqueror node editing if node and node.conqueredBy and (node.conqueredBy.conqueror.type == "vaal" or node.isNotable) then tooltip:AddSeparator(14) tooltip:AddLine(14, colorCodes.TIP.."Tip: Right click to edit the modifiers for this node") end -- Mod differences if self.showStatDifferences then local calcFunc, calcBase = build.calcsTab:GetMiscCalculator(build) tooltip:AddSeparator(14) local path = (node.alloc and node.depends) or self.tracePath or node.path or { } local pathLength = #path local pathNodes = { } for _, node in pairs(path) do pathNodes[node] = true end local nodeOutput, pathOutput if node.alloc then -- Calculate the differences caused by deallocating this node and its dependent nodes nodeOutput = calcFunc({ removeNodes = { [node] = true } }, {}) if pathLength > 1 then pathOutput = calcFunc({ removeNodes = pathNodes }, {}) end else -- Calculated the differences caused by allocating this node and all nodes along the path to it nodeOutput = calcFunc({ addNodes = { [node] = true } }, {}) if pathLength > 1 then pathOutput = calcFunc({ addNodes = pathNodes }, {}) end end local count = build:AddStatComparesToTooltip(tooltip, calcBase, nodeOutput, node.alloc and "^7Unallocating this node will give you:" or "^7Allocating this node will give you:") if pathLength > 1 then count = count + build:AddStatComparesToTooltip(tooltip, calcBase, pathOutput, node.alloc and "^7Unallocating this node and all nodes depending on it will give you:" or "^7Allocating this node and all nodes leading to it will give you:", pathLength) end if count == 0 then tooltip:AddLine(14, string.format("^7No changes from %s this node%s.", node.alloc and "unallocating" or "allocating", pathLength > 1 and " or the nodes leading to it" or "")) end tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to disable the display of stat differences.") else tooltip:AddSeparator(14) tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to enable the display of stat differences.") end -- Pathing distance tooltip:AddSeparator(14) if node.path and #node.path > 0 then if self.traceMode and isValueInArray(self.tracePath, node) then tooltip:AddLine(14, "^7"..#self.tracePath .. " nodes in trace path") tooltip:AddLine(14, colorCodes.TIP) else tooltip:AddLine(14, "^7"..#node.path .. " points to node") tooltip:AddLine(14, colorCodes.TIP) if #node.path > 1 then -- Handy hint! tooltip:AddLine(14, "Tip: To reach this node by a different path, hold Shift, then trace the path and click this node") end end end if node.depends and #node.depends > 1 then tooltip:AddSeparator(14) tooltip:AddLine(14, "^7"..#node.depends .. " points gained from unallocating these nodes") tooltip:AddLine(14, colorCodes.TIP) end if node.type == "Socket" then tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.") else tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Ctrl to hide this tooltip.") end end