725 lines
23 KiB
Lua
725 lines
23 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Class: Passive Tree
|
|
-- Passive skill tree class.
|
|
-- Responsible for downloading and loading the passive tree data and assets
|
|
-- Also pre-calculates and pre-parses most of the data need to use the passive tree, including the node modifiers
|
|
--
|
|
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_pi = math.pi
|
|
local m_rad = math.rad
|
|
local m_sin = math.sin
|
|
local m_cos = math.cos
|
|
local m_tan = math.tan
|
|
local m_sqrt = math.sqrt
|
|
|
|
|
|
local classArt = {
|
|
[0] = "centerscion",
|
|
[1] = "centermarauder",
|
|
[2] = "centerranger",
|
|
[3] = "centerwitch",
|
|
[4] = "centerduelist",
|
|
[5] = "centertemplar",
|
|
[6] = "centershadow"
|
|
}
|
|
|
|
local orbit4Angle = { [0] = 0, 10, 20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120, 130, 135, 140, 150, 160, 170, 180, 190, 200, 210, 220, 225, 230, 240, 250, 260, 270, 280, 290, 300, 310, 315, 320, 330, 340, 350 }
|
|
for i, ang in ipairs(orbit4Angle) do
|
|
orbit4Angle[i] = m_rad(ang)
|
|
end
|
|
local orbitMult = { [0] = 0, m_pi / 3, m_pi / 6, m_pi / 6 }
|
|
local orbitDist = { [0] = 0, 82, 162, 335, 493 }
|
|
|
|
|
|
-- Retrieve the file at the given URL
|
|
local function getFile(URL)
|
|
local page = ""
|
|
local easy = common.curl.easy()
|
|
easy:setopt_url(URL)
|
|
easy:setopt_writefunction(function(data)
|
|
page = page..data
|
|
return true
|
|
end)
|
|
easy:perform()
|
|
easy:close()
|
|
return #page > 0 and page
|
|
end
|
|
|
|
local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion)
|
|
self.treeVersion = treeVersion
|
|
self.targetVersion = treeVersions[treeVersion].targetVersion
|
|
local versionNum = treeVersions[treeVersion].num
|
|
|
|
self.legion = LoadModule("Data/3_0/LegionPassives")
|
|
|
|
MakeDir("TreeData")
|
|
|
|
ConPrintf("Loading passive tree data for version '%s'...", treeVersions[treeVersion].short)
|
|
local treeText
|
|
local treeFile = io.open("TreeData/"..treeVersion.."/tree.lua", "r")
|
|
if treeFile then
|
|
treeText = treeFile:read("*a")
|
|
treeFile:close()
|
|
else
|
|
ConPrintf("Downloading passive tree data...")
|
|
local page
|
|
local pageFile = io.open("TreeData/"..treeVersion.."/tree.txt", "r")
|
|
if pageFile then
|
|
page = pageFile:read("*a")
|
|
pageFile:close()
|
|
else
|
|
page = getFile("https://www.pathofexile.com/passive-skill-tree/")
|
|
end
|
|
local treeData = page:match("var passiveSkillTreeData = (%b{})")
|
|
if treeData then
|
|
treeText = "local tree=" .. jsonToLua(page:match("var passiveSkillTreeData = (%b{})"))
|
|
treeText = treeText .. "tree.classes=" .. jsonToLua(page:match("ascClasses: (%b{})"))
|
|
treeText = "return tree"
|
|
else
|
|
treeText = "return " .. jsonToLua(page)
|
|
end
|
|
treeFile = io.open("TreeData/"..treeVersion.."/tree.lua", "w")
|
|
treeFile:write(treeText)
|
|
treeFile:close()
|
|
end
|
|
for k, v in pairs(assert(loadstring(treeText))()) do
|
|
self[k] = v
|
|
end
|
|
|
|
local cdnRoot = versionNum >= 3.08 and versionNum <= 3.09 and "https://web.poecdn.com" or ""
|
|
|
|
self.size = m_min(self.max_x - self.min_x, self.max_y - self.min_y) * 1.1
|
|
|
|
if versionNum >= 3.10 then
|
|
-- Migrate to old format
|
|
for i = 0, 6 do
|
|
self.classes[i] = self.classes[i + 1]
|
|
self.classes[i + 1] = nil
|
|
end
|
|
end
|
|
|
|
-- Build maps of class name -> class table
|
|
self.classNameMap = { }
|
|
self.ascendNameMap = { }
|
|
for classId, class in pairs(self.classes) do
|
|
if versionNum >= 3.10 then
|
|
-- Migrate to old format
|
|
class.classes = class.ascendancies
|
|
end
|
|
class.classes[0] = { name = "None" }
|
|
self.classNameMap[class.name] = classId
|
|
for ascendClassId, ascendClass in pairs(class.classes) do
|
|
self.ascendNameMap[ascendClass.name] = {
|
|
classId = classId,
|
|
class = class,
|
|
ascendClassId = ascendClassId,
|
|
ascendClass = ascendClass
|
|
}
|
|
end
|
|
end
|
|
|
|
ConPrintf("Loading passive tree assets...")
|
|
for name, data in pairs(self.assets) do
|
|
self:LoadImage(name..".png", cdnRoot..(data[0.3835] or data[1]), data, not name:match("[OL][ri][bn][ie][tC]") and "ASYNC" or nil)--, not name:match("[OL][ri][bn][ie][tC]") and "MIPMAP" or nil)
|
|
end
|
|
|
|
-- Load sprite sheets and build sprite map
|
|
self.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 = { }
|
|
self:LoadImage(maxZoom.filename:gsub("%?%x+$",""), "https://web.poecdn.com"..(self.imageRoot or "/image/")..(versionNum >= 3.08 and "passive-skill/" or "build-gen/passive-skill-sprite/")..maxZoom.filename, sheet, "CLAMP")--, "MIPMAP")
|
|
spriteSheets[maxZoom.filename] = sheet
|
|
end
|
|
for name, coords in pairs(maxZoom.coords) do
|
|
if not self.spriteMap[name] then
|
|
self.spriteMap[name] = { }
|
|
end
|
|
self.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
|
|
|
|
-- Load legion sprite sheets and build sprite map
|
|
local legionSprites = LoadModule("TreeData/legion/tree-legion.lua")
|
|
for type, data in pairs(legionSprites) do
|
|
local maxZoom = data[#data]
|
|
local sheet = spriteSheets[maxZoom.filename]
|
|
if not sheet then
|
|
sheet = { }
|
|
sheet.handle = NewImageHandle()
|
|
sheet.handle:Load("TreeData/legion/"..maxZoom.filename)
|
|
sheet.width, sheet.height = sheet.handle:ImageSize()
|
|
spriteSheets[maxZoom.filename] = sheet
|
|
end
|
|
for name, coords in pairs(maxZoom.coords) do
|
|
if not self.spriteMap[name] then
|
|
self.spriteMap[name] = { }
|
|
end
|
|
self.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 classArt = {
|
|
[0] = "centerscion",
|
|
[1] = "centermarauder",
|
|
[2] = "centerranger",
|
|
[3] = "centerwitch",
|
|
[4] = "centerduelist",
|
|
[5] = "centertemplar",
|
|
[6] = "centershadow"
|
|
}
|
|
self.nodeOverlay = {
|
|
Normal = {
|
|
artWidth = 40,
|
|
alloc = "PSSkillFrameActive",
|
|
path = "PSSkillFrameHighlighted",
|
|
unalloc = "PSSkillFrame",
|
|
allocAscend = versionNum >= 3.10 and "AscendancyFrameSmallAllocated" or "PassiveSkillScreenAscendancyFrameSmallAllocated",
|
|
pathAscend = versionNum >= 3.10 and "AscendancyFrameSmallCanAllocate" or "PassiveSkillScreenAscendancyFrameSmallCanAllocate",
|
|
unallocAscend = versionNum >= 3.10 and "AscendancyFrameSmallNormal" or "PassiveSkillScreenAscendancyFrameSmallNormal"
|
|
},
|
|
Notable = {
|
|
artWidth = 58,
|
|
alloc = "NotableFrameAllocated",
|
|
path = "NotableFrameCanAllocate",
|
|
unalloc = "NotableFrameUnallocated",
|
|
allocAscend = versionNum >= 3.10 and "AscendancyFrameLargeAllocated" or "PassiveSkillScreenAscendancyFrameLargeAllocated",
|
|
pathAscend = versionNum >= 3.10 and "AscendancyFrameLargeCanAllocate" or "PassiveSkillScreenAscendancyFrameLargeCanAllocate",
|
|
unallocAscend = versionNum >= 3.10 and "AscendancyFrameLargeNormal" or "PassiveSkillScreenAscendancyFrameLargeNormal",
|
|
allocBlighted = "BlightedNotableFrameAllocated",
|
|
pathBlighted = "BlightedNotableFrameCanAllocate",
|
|
unallocBlighted = "BlightedNotableFrameUnallocated",
|
|
},
|
|
Keystone = {
|
|
artWidth = 84,
|
|
alloc = "KeystoneFrameAllocated",
|
|
path = "KeystoneFrameCanAllocate",
|
|
unalloc = "KeystoneFrameUnallocated"
|
|
},
|
|
Socket = {
|
|
artWidth = 58,
|
|
alloc = "JewelFrameAllocated",
|
|
path = "JewelFrameCanAllocate",
|
|
unalloc = "JewelFrameUnallocated",
|
|
allocAlt = "JewelSocketAltActive",
|
|
pathAlt = "JewelSocketAltCanAllocate",
|
|
unallocAlt = "JewelSocketAltNormal",
|
|
}
|
|
}
|
|
for type, data in pairs(self.nodeOverlay) do
|
|
local size = data.artWidth * 1.33
|
|
data.size = size
|
|
data.rsq = size * size
|
|
end
|
|
|
|
if versionNum >= 3.10 then
|
|
-- Migrate groups to old format
|
|
for _, group in pairs(self.groups) do
|
|
group.n = group.nodes
|
|
group.oo = { }
|
|
for _, orbit in ipairs(group.orbits) do
|
|
group.oo[orbit] = true
|
|
end
|
|
end
|
|
|
|
-- Go away
|
|
self.nodes.root = nil
|
|
end
|
|
|
|
ConPrintf("Processing tree...")
|
|
self.keystoneMap = { }
|
|
self.notableMap = { }
|
|
self.clusterNodeMap = { }
|
|
self.sockets = { }
|
|
local nodeMap = { }
|
|
local orbitMult = { [0] = 0, m_pi / 3, m_pi / 6, m_pi / 6 }
|
|
local orbitMultFull = {
|
|
[0] = 0,
|
|
[1] = 10 * m_pi / 180,
|
|
[2] = 20 * m_pi / 180,
|
|
[3] = 30 * m_pi / 180,
|
|
[4] = 40 * m_pi / 180,
|
|
[5] = 45 * m_pi / 180,
|
|
[6] = 50 * m_pi / 180,
|
|
[7] = 60 * m_pi / 180,
|
|
[8] = 70 * m_pi / 180,
|
|
[9] = 80 * m_pi / 180,
|
|
[10] = 90 * m_pi / 180,
|
|
[11] = 100 * m_pi / 180,
|
|
[12] = 110 * m_pi / 180,
|
|
[13] = 120 * m_pi / 180,
|
|
[14] = 130 * m_pi / 180,
|
|
[15] = 135 * m_pi / 180,
|
|
[16] = 140 * m_pi / 180,
|
|
[17] = 150 * m_pi / 180,
|
|
[18] = 160 * m_pi / 180,
|
|
[19] = 170 * m_pi / 180,
|
|
[20] = 180 * m_pi / 180,
|
|
[21] = 190 * m_pi / 180,
|
|
[22] = 200 * m_pi / 180,
|
|
[23] = 210 * m_pi / 180,
|
|
[24] = 220 * m_pi / 180,
|
|
[25] = 225 * m_pi / 180,
|
|
[26] = 230 * m_pi / 180,
|
|
[27] = 240 * m_pi / 180,
|
|
[28] = 250 * m_pi / 180,
|
|
[29] = 260 * m_pi / 180,
|
|
[30] = 270 * m_pi / 180,
|
|
[31] = 280 * m_pi / 180,
|
|
[32] = 290 * m_pi / 180,
|
|
[33] = 300 * m_pi / 180,
|
|
[34] = 310 * m_pi / 180,
|
|
[35] = 315 * m_pi / 180,
|
|
[36] = 320 * m_pi / 180,
|
|
[37] = 330 * m_pi / 180,
|
|
[38] = 340 * m_pi / 180,
|
|
[39] = 350 * m_pi / 180
|
|
}
|
|
local orbitDist = { [0] = 0, 82, 162, 335, 493 }
|
|
for _, node in pairs(self.nodes) do
|
|
-- Migration...
|
|
if versionNum < 3.10 then
|
|
-- To new format
|
|
node.classStartIndex = node.spc[0] and node.spc[0]
|
|
else
|
|
-- To old format
|
|
node.id = node.skill
|
|
node.g = node.group
|
|
node.o = node.orbit
|
|
node.oidx = node.orbitIndex
|
|
node.dn = node.name
|
|
node.sd = node.stats
|
|
node.passivePointsGranted = node.grantedPassivePoints or 0
|
|
end
|
|
|
|
if versionNum <= 3.09 and node.passivePointsGranted > 0 then
|
|
t_insert(node.sd, "Grants "..node.passivePointsGranted.." Passive Skill Point"..(node.passivePointsGranted > 1 and "s" or ""))
|
|
end
|
|
node.conquered = false
|
|
node.alternative = {}
|
|
node.__index = node
|
|
node.linkedId = { }
|
|
nodeMap[node.id] = node
|
|
|
|
-- Determine node type
|
|
if node.classStartIndex then
|
|
node.type = "ClassStart"
|
|
local class = self.classes[node.classStartIndex]
|
|
class.startNodeId = node.id
|
|
node.startArt = classArt[node.classStartIndex]
|
|
elseif node.isAscendancyStart then
|
|
node.type = "AscendClassStart"
|
|
local ascendClass = self.ascendNameMap[node.ascendancyName].ascendClass
|
|
ascendClass.startNodeId = node.id
|
|
elseif node.m or node.isMastery then
|
|
node.type = "Mastery"
|
|
elseif node.isJewelSocket then
|
|
node.type = "Socket"
|
|
self.sockets[node.id] = node
|
|
elseif node.ks or node.isKeystone then
|
|
node.type = "Keystone"
|
|
self.keystoneMap[node.dn] = node
|
|
elseif node["not"] or node.isNotable then
|
|
node.type = "Notable"
|
|
if not node.ascendancyName then
|
|
self.notableMap[node.dn:lower()] = node
|
|
end
|
|
else
|
|
node.type = "Normal"
|
|
end
|
|
|
|
-- Find the node group
|
|
local group = self.groups[node.g]
|
|
if group then
|
|
node.group = group
|
|
group.ascendancyName = node.ascendancyName
|
|
if node.isAscendancyStart then
|
|
group.isAscendancyStart = true
|
|
end
|
|
if node.o ~= 4 then
|
|
node.angle = node.oidx * orbitMult[node.o]
|
|
else
|
|
node.angle = orbitMultFull[node.oidx]
|
|
end
|
|
local dist = orbitDist[node.o]
|
|
node.x = group.x + m_sin(node.angle) * dist
|
|
node.y = group.y - m_cos(node.angle) * dist
|
|
elseif node.type == "Notable" or node.type == "Keystone" then
|
|
self.clusterNodeMap[node.dn] = node
|
|
end
|
|
|
|
self:ProcessNode(node)
|
|
end
|
|
|
|
-- Pregenerate the polygons for the node connector lines
|
|
self.connectors = { }
|
|
for _, node in pairs(self.nodes) do
|
|
for _, otherId in pairs(node.out or {}) do
|
|
if type(otherId) == "string" then
|
|
otherId = tonumber(otherId)
|
|
end
|
|
local other = nodeMap[otherId]
|
|
t_insert(node.linkedId, otherId)
|
|
t_insert(other.linkedId, node.id)
|
|
if node.type ~= "ClassStart" and other.type ~= "ClassStart"
|
|
and node.type ~= "Mastery" and other.type ~= "Mastery"
|
|
and node.ascendancyName == other.ascendancyName
|
|
and not node.isProxy and not other.isProxy
|
|
and not node.group.isProxy and not node.group.isProxy then
|
|
t_insert(self.connectors, self:BuildConnector(node, other))
|
|
end
|
|
end
|
|
end
|
|
-- Precalculate the lists of nodes that are within each radius of each socket
|
|
for nodeId, socket in pairs(self.sockets) do
|
|
socket.nodesInRadius = { }
|
|
socket.attributesInRadius = { }
|
|
for radiusIndex, radiusInfo in ipairs(data[self.targetVersion].jewelRadius) do
|
|
socket.nodesInRadius[radiusIndex] = { }
|
|
socket.attributesInRadius[radiusIndex] = { }
|
|
local outerRadiusSquared = radiusInfo.outer * radiusInfo.outer
|
|
local innerRadiusSquared = radiusInfo.inner * radiusInfo.inner
|
|
for _, node in pairs(self.nodes) do
|
|
if node ~= socket and not node.isBlighted and node.group and not node.isProxy and not node.group.isProxy then
|
|
local vX, vY = node.x - socket.x, node.y - socket.y
|
|
local euclideanDistanceSquared = vX * vX + vY * vY
|
|
if innerRadiusSquared <= euclideanDistanceSquared then
|
|
if euclideanDistanceSquared <= outerRadiusSquared then
|
|
socket.nodesInRadius[radiusIndex][node.id] = node
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for classId, class in pairs(self.classes) do
|
|
local startNode = nodeMap[class.startNodeId]
|
|
for _, nodeId in ipairs(startNode.linkedId) do
|
|
local node = nodeMap[nodeId]
|
|
if node.type == "Normal" then
|
|
node.modList:NewMod("Condition:ConnectedTo"..class.name.."Start", "FLAG", true, "Tree:"..nodeId)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Build ModList for legion jewels
|
|
for _, node in pairs(self.legion.nodes) do
|
|
-- Determine node type
|
|
if node.m then
|
|
node.type = "Mastery"
|
|
elseif node.ks then
|
|
node.type = "Keystone"
|
|
self.keystoneMap[node.dn] = node
|
|
elseif node["not"] then
|
|
node.type = "Notable"
|
|
else
|
|
node.type = "Normal"
|
|
end
|
|
|
|
-- Assign node artwork assets
|
|
node.sprites = self.spriteMap[node.icon]
|
|
if not node.sprites then
|
|
--error("missing sprite "..node.icon)
|
|
node.sprites = { }
|
|
end
|
|
|
|
-- Parse node modifier lines
|
|
node.mods = { }
|
|
node.modKey= ""
|
|
local i = 1
|
|
if node.passivePointsGranted > 0 then
|
|
t_insert(node.sd, "Grants "..node.passivePointsGranted.." Passive Skill Point"..(node.passivePointsGranted > 1 and "s" or ""))
|
|
end
|
|
while node.sd[i] do
|
|
if node.sd[i]:match("\n") then
|
|
local line = node.sd[i]
|
|
local il = i
|
|
t_remove(node.sd, i)
|
|
for line in line:gmatch("[^\n]+") do
|
|
t_insert(node.sd, il, line)
|
|
il = il + 1
|
|
end
|
|
end
|
|
local line = node.sd[i]
|
|
local list, extra = modLib.parseMod[self.targetVersion](line)
|
|
if not list or extra then
|
|
-- Try to combine it with one or more of the lines that follow this one
|
|
local endI = i + 1
|
|
while node.sd[endI] do
|
|
local comb = line
|
|
for ci = i + 1, endI do
|
|
comb = comb .. " " .. node.sd[ci]
|
|
end
|
|
list, extra = modLib.parseMod[self.targetVersion](comb, true)
|
|
if list and not extra then
|
|
-- Success, add dummy mod lists to the other lines that were combined with this one
|
|
for ci = i + 1, endI do
|
|
node.mods[ci] = { list = { } }
|
|
end
|
|
break
|
|
end
|
|
endI = endI + 1
|
|
end
|
|
end
|
|
if not list then
|
|
-- Parser had no idea how to read this modifier
|
|
node.unknown = true
|
|
elseif extra then
|
|
-- Parser recognised this as a modifier but couldn't understand all of it
|
|
node.extra = true
|
|
else
|
|
for _, mod in ipairs(list) do
|
|
node.modKey = node.modKey.."["..modLib.formatMod(mod).."]"
|
|
end
|
|
end
|
|
node.mods[i] = { list = list, extra = extra }
|
|
i = i + 1
|
|
while node.mods[i] do
|
|
-- Skip any lines with dummy lists added by the line combining code
|
|
i = i + 1
|
|
end
|
|
end
|
|
node.modList = new("ModList")
|
|
for _, mod in pairs(node.mods) do
|
|
if mod.list and not mod.extra then
|
|
for i, mod in ipairs(mod.list) do
|
|
mod.source = "Tree:"..node.id
|
|
if type(mod.value) == "table" and mod.value.mod then
|
|
mod.value.mod.source = mod.source
|
|
end
|
|
node.modList:AddMod(mod)
|
|
end
|
|
end
|
|
end
|
|
if node.type == "Keystone" then
|
|
node.keystoneMod = modLib.createMod("Keystone", "LIST", node.dn, "Tree"..node.id)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Common processing code for nodes (used for both real tree nodes and subgraph nodes)
|
|
function PassiveTreeClass:ProcessNode(node)
|
|
-- Assign node artwork assets
|
|
node.sprites = self.spriteMap[node.icon]
|
|
if not node.sprites then
|
|
--error("missing sprite "..node.icon)
|
|
node.sprites = self.spriteMap["Art/2DArt/SkillIcons/passives/MasteryBlank.png"]
|
|
end
|
|
node.overlay = self.nodeOverlay[node.type]
|
|
if node.overlay then
|
|
node.rsq = node.overlay.rsq
|
|
node.size = node.overlay.size
|
|
end
|
|
|
|
-- Derive the true position of the node
|
|
if node.group then
|
|
node.angle = node.o == 4 and orbit4Angle[node.oidx] or node.oidx * orbitMult[node.o]
|
|
local dist = orbitDist[node.o]
|
|
node.x = node.group.x + m_sin(node.angle) * dist
|
|
node.y = node.group.y - m_cos(node.angle) * dist
|
|
end
|
|
|
|
node.modKey = ""
|
|
if not node.sd then
|
|
return
|
|
end
|
|
|
|
-- Parse node modifier lines
|
|
node.mods = { }
|
|
local i = 1
|
|
while node.sd[i] do
|
|
if node.sd[i]:match("\n") then
|
|
local line = node.sd[i]
|
|
local il = i
|
|
t_remove(node.sd, i)
|
|
for line in line:gmatch("[^\n]+") do
|
|
t_insert(node.sd, il, line)
|
|
il = il + 1
|
|
end
|
|
end
|
|
local line = node.sd[i]
|
|
local list, extra = modLib.parseMod[self.targetVersion](line)
|
|
if not list or extra then
|
|
-- Try to combine it with one or more of the lines that follow this one
|
|
local endI = i + 1
|
|
while node.sd[endI] do
|
|
local comb = line
|
|
for ci = i + 1, endI do
|
|
comb = comb .. " " .. node.sd[ci]
|
|
end
|
|
list, extra = modLib.parseMod[self.targetVersion](comb, true)
|
|
if list and not extra then
|
|
-- Success, add dummy mod lists to the other lines that were combined with this one
|
|
for ci = i + 1, endI do
|
|
node.mods[ci] = { list = { } }
|
|
end
|
|
break
|
|
end
|
|
endI = endI + 1
|
|
end
|
|
end
|
|
if not list then
|
|
-- Parser had no idea how to read this modifier
|
|
node.unknown = true
|
|
elseif extra then
|
|
-- Parser recognised this as a modifier but couldn't understand all of it
|
|
node.extra = true
|
|
else
|
|
for _, mod in ipairs(list) do
|
|
node.modKey = node.modKey.."["..modLib.formatMod(mod).."]"
|
|
end
|
|
end
|
|
node.mods[i] = { list = list, extra = extra }
|
|
i = i + 1
|
|
while node.mods[i] do
|
|
-- Skip any lines with dummy lists added by the line combining code
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
-- Build unified list of modifiers from all recognised modifier lines
|
|
node.modList = new("ModList")
|
|
for _, mod in pairs(node.mods) do
|
|
if mod.list and not mod.extra then
|
|
for i, mod in ipairs(mod.list) do
|
|
mod.source = "Tree:"..node.id
|
|
if type(mod.value) == "table" and mod.value.mod then
|
|
mod.value.mod.source = mod.source
|
|
end
|
|
node.modList:AddMod(mod)
|
|
end
|
|
end
|
|
end
|
|
if node.type == "Keystone" then
|
|
node.keystoneMod = modLib.createMod("Keystone", "LIST", node.dn, "Tree"..node.id)
|
|
end
|
|
end
|
|
|
|
-- Checks if a given image is present and downloads it from the given URL if it isn't there
|
|
function PassiveTreeClass:LoadImage(imgName, url, data, ...)
|
|
local imgFile = io.open("TreeData/"..imgName, "r")
|
|
if imgFile then
|
|
imgFile:close()
|
|
else
|
|
imgFile = io.open("TreeData/"..self.treeVersion.."/"..imgName, "r")
|
|
if imgFile then
|
|
imgFile:close()
|
|
imgName = self.treeVersion.."/"..imgName
|
|
elseif main.allowTreeDownload then -- Enable downloading with Ctrl+Shift+F5
|
|
ConPrintf("Downloading '%s'...", imgName)
|
|
local data = getFile(url)
|
|
if data and not data:match("<!DOCTYPE html>") then
|
|
imgFile = io.open("TreeData/"..imgName, "wb")
|
|
imgFile:write(data)
|
|
imgFile:close()
|
|
else
|
|
ConPrintf("Failed to download: %s", url)
|
|
end
|
|
end
|
|
end
|
|
data.handle = NewImageHandle()
|
|
data.handle:Load("TreeData/"..imgName, ...)
|
|
data.width, data.height = data.handle:ImageSize()
|
|
end
|
|
|
|
-- Generate the quad used to render the line between the two given nodes
|
|
function PassiveTreeClass:BuildConnector(node1, node2)
|
|
local connector = {
|
|
ascendancyName = node1.ascendancyName,
|
|
nodeId1 = node1.id,
|
|
nodeId2 = node2.id,
|
|
c = { } -- This array will contain the quad's data: 1-8 are the vertex coordinates, 9-16 are the texture coordinates
|
|
-- Only the texture coords are filled in at this time; the vertex coords need to be converted from tree-space to screen-space first
|
|
-- This will occur when the tree is being drawn; .vert will map line state (Normal/Intermediate/Active) to the correct tree-space coordinates
|
|
}
|
|
if node1.g == node2.g and node1.o == node2.o then
|
|
-- Nodes are in the same orbit of the same group
|
|
-- Calculate the starting angle (node1.angle) and arc angle
|
|
if node1.angle > node2.angle then
|
|
node1, node2 = node2, node1
|
|
end
|
|
local arcAngle = node2.angle - node1.angle
|
|
if arcAngle > m_pi then
|
|
node1, node2 = node2, node1
|
|
arcAngle = m_pi * 2 - arcAngle
|
|
end
|
|
if arcAngle < m_pi * 0.9 then
|
|
-- Angle is less than 180 degrees, draw an arc
|
|
connector.type = "Orbit" .. node1.o
|
|
-- This is an arc texture mapped onto a kite-shaped quad
|
|
-- Calculate how much the arc needs to be clipped by
|
|
-- Both ends of the arc will be clipped by this amount, so 90 degree arc angle = no clipping and 30 degree arc angle = 75 degrees of clipping
|
|
-- The clipping is accomplished by effectively moving the bottom left and top right corners of the arc texture towards the top left corner
|
|
-- The arc texture only shows 90 degrees of an arc, but some arcs must go for more than 90 degrees
|
|
-- Fortunately there's nowhere on the tree where we can't just show the middle 90 degrees and rely on the node artwork to cover the gaps :)
|
|
local clipAngle = m_pi / 4 - arcAngle / 2
|
|
local p = 1 - m_max(m_tan(clipAngle), 0)
|
|
local angle = node1.angle - clipAngle
|
|
connector.vert = { }
|
|
for _, state in pairs({"Normal","Intermediate","Active"}) do
|
|
-- The different line states have differently-sized artwork, so the vertex coords must be calculated separately for each one
|
|
local art = self.assets[connector.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
|
|
local vert = { }
|
|
vert[1], vert[2] = node1.group.x, node1.group.y
|
|
vert[3], vert[4] = cX + (size * m_sin(angle) - oX) * p, cY + (size * -m_cos(angle) - oY) * p
|
|
vert[5], vert[6] = cX, cY
|
|
vert[7], vert[8] = cX + (size * m_cos(angle) - oX) * p, cY + (size * m_sin(angle) - oY) * p
|
|
connector.vert[state] = vert
|
|
end
|
|
connector.c[9], connector.c[10] = 1, 1
|
|
connector.c[11], connector.c[12] = 0, p
|
|
connector.c[13], connector.c[14] = 0, 0
|
|
connector.c[15], connector.c[16] = p, 0
|
|
return connector
|
|
end
|
|
end
|
|
|
|
-- Generate a straight line
|
|
connector.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)
|
|
connector[1], connector[2] = node1.x - nY, node1.y + nX
|
|
connector[3], connector[4] = node1.x + nY, node1.y - nX
|
|
connector[5], connector[6] = node2.x + nY, node2.y - nX
|
|
connector[7], connector[8] = node2.x - nY, node2.y + nX
|
|
connector.c[9], connector.c[10] = 0, 1
|
|
connector.c[11], connector.c[12] = 0, 0
|
|
connector.c[13], connector.c[14] = endS, 0
|
|
connector.c[15], connector.c[16] = endS, 1
|
|
connector.vert = { Normal = connector, Intermediate = connector, Active = connector }
|
|
return connector
|
|
end
|