diff --git a/.gitignore b/.gitignore index 704714139fd..e3f53a15c52 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ manifest-updated.xml # GGPK Export src/Export/ggpk/metadata/ src/Export/ggpk/data/ +src/Export/ggpk/art/ src/Export/ggpk/*.exe src/Export/ggpk/*.dll diff --git a/spec/System/TestAbyssTimelessJewel_spec.lua b/spec/System/TestAbyssTimelessJewel_spec.lua new file mode 100644 index 00000000000..8ceca5cade5 --- /dev/null +++ b/spec/System/TestAbyssTimelessJewel_spec.lua @@ -0,0 +1,495 @@ +local function uint16(value) + return string.char(value % 256, math.floor(value / 256) % 256) +end + +local parseAbyssJewel = select(4, LoadModule("Modules/DataAbyssJewelLookUpTableHelper")) + +local function abyssModification(components) + local encoded = { string.char(#components) } + for _, component in ipairs(components) do + encoded[#encoded + 1] = string.char(component.type, component.id, #component.rolls) + for _, roll in ipairs(component.rolls) do + encoded[#encoded + 1] = uint16(roll < 0 and roll + 65536 or roll) + end + end + return table.concat(encoded) +end + +local function abyssHeader(format, jewelType, seed) + return format .. string.char(1, jewelType) .. uint16(seed) .. uint16(seed) .. uint16(1) +end + +local function kurgalExampleData() + local affectedNodes = { + { id = 4367, modification = abyssModification({ { type = 1, id = 24, rolls = { 6 } } }) }, + { id = 15117, modification = abyssModification({ { type = 1, id = 19, rolls = { 8 } } }) }, + { id = 20528, modification = abyssModification({ { type = 2, id = 207, rolls = { -15 } } }) }, + { id = 21958, modification = abyssModification({ { type = 2, id = 211, rolls = { 12, 18 } } }) }, + } + local encoded = { abyssHeader("ABYS", 9, 533), string.char(1, 60), uint16(61419), string.char(#affectedNodes) } + for _, affectedNode in ipairs(affectedNodes) do + encoded[#encoded + 1] = uint16(affectedNode.id) + encoded[#encoded + 1] = affectedNode.modification + end + return table.concat(encoded) +end + +local function zorathExampleData() + local affectedNodes = { + { id = 23027, modification = abyssModification({ { type = 1, id = 90, rolls = { 8 } } }) }, + { id = 34171, modification = abyssModification({ { type = 1, id = 116, rolls = { 19 } } }) }, + { id = 36949, modification = abyssModification({ { type = 2, id = 4, rolls = { 2 } } }) }, + { id = 41472, modification = abyssModification({ { type = 2, id = 41, rolls = { 12 } } }) }, + { id = 50692, modification = abyssModification({ { type = 1, id = 77, rolls = { 1 } } }) }, + { id = 53884, modification = abyssModification({ { type = 1, id = 78, rolls = { 1 } } }) }, + { id = 60472, modification = abyssModification({ { type = 1, id = 102, rolls = { 4 } } }) }, + } + local encoded = { abyssHeader("ABYN", 11, 6564), uint16(#affectedNodes) } + for _, affectedNode in ipairs(affectedNodes) do + encoded[#encoded + 1] = uint16(affectedNode.id) + end + for _, affectedNode in ipairs(affectedNodes) do + encoded[#encoded + 1] = affectedNode.modification + end + encoded[#encoded + 1] = "ASCS" .. uint16(2) + .. string.char(#"Inquisitor") .. "Inquisitor" .. string.char(1) .. uint16(53884) + .. string.char(#"Chieftain") .. "Chieftain" .. string.char(1) .. uint16(50692) + return table.concat(encoded) +end + +local function newAbyssTimelessJewel(name, baseName, seedLine) + return new("Item", "Rarity: UNIQUE\n" .. name .. "\n" .. baseName .. "\n" .. + "Limited to: 1 Historic\nImplicits: 0\n" .. seedLine .. "\n" .. + "Passives affected are Conquered by the Abyssal\nHistoric\n") +end + +local function allocatePathToNode(spec, targetNode) + local classStart + for _, node in pairs(spec.allocNodes) do + if node.type == "ClassStart" then + classStart = node + break + end + end + local queue = { classStart } + local visited = { [classStart.id] = true } + local parent = { } + local head = 1 + while head <= #queue and not visited[targetNode.id] do + local current = queue[head] + head = head + 1 + for _, linked in ipairs(current.linked) do + if not visited[linked.id] and linked.type ~= "Mastery" and linked.type ~= "AscendClassStart" then + visited[linked.id] = true + parent[linked.id] = current + queue[#queue + 1] = linked + end + end + end + if not visited[targetNode.id] then + return false + end + local current = targetNode + while current do + current.alloc = true + spec.allocNodes[current.id] = current + current = parent[current.id] + end + return true +end + +local function equipJewel(spec, socketId, item) + build.itemsTab:AddItem(item, true) + spec.jewels[socketId] = item.id + local slot = build.itemsTab.sockets[socketId] + if slot then + slot.selItemId = item.id + end + return slot +end + +describe("Abyss timeless jewels", function() + before_each(function() + newBuild() + end) + + after_each(function() + data.timelessJewelLUTs[9] = nil + data.timelessJewelLUTs[11] = nil + if main.popups[1] and main.popups[1].title == "Find a Timeless Jewel" then + main:ClosePopup() + end + end) + + it("parses the Kurgal and Zorath seed modifiers", function() + local kurgal = newAbyssTimelessJewel("Baleful Dominion", "Hypnotic Eye Jewel", + "Subjugating 533 souls in the thrall of Kurgal") + local zorath = newAbyssTimelessJewel("Reclaimed Malevolence", "Assembled Eye Jewel", + "Binding 6564 souls to phylacteries to sustain Zorath") + + assert.are.equal(533, kurgal.jewelData.conqueredBy.id) + assert.are.equal("abyss_hypnotic", kurgal.jewelData.conqueredBy.conqueror.type) + assert.are.equal(6564, zorath.jewelData.conqueredBy.id) + assert.are.equal("abyss_special", zorath.jewelData.conqueredBy.conqueror.type) + end) + + it("reads Kurgal seed 533 at socket 61419", function() + data.timelessJewelLUTs[9] = parseAbyssJewel(9, kurgalExampleData()) + local affectedNodes = data.readAbyssJewelLUT(533, 61419, 9) + + assert.are.same({ type = 1, id = 622, rolls = { 8 } }, affectedNodes[15117][1]) + assert.are.same({ type = 1, id = 627, rolls = { 6 } }, affectedNodes[4367][1]) + assert.are.same({ type = 2, id = 207, rolls = { -15 } }, affectedNodes[20528][1]) + assert.are.same({ type = 2, id = 211, rolls = { 12, 18 } }, affectedNodes[21958][1]) + local changedNode = data.resolveAbyssJewelComponent(affectedNodes[21958][1], build.spec.tree.legion) + local _, _, minimumRoll = data.getAbyssJewelComponentRoll(affectedNodes[21958][1], changedNode, 1) + local _, _, maximumRoll = data.getAbyssJewelComponentRoll(affectedNodes[21958][1], changedNode, 2) + assert.are.same({ 12, 18 }, { minimumRoll, maximumRoll }) + assert.is_nil(affectedNodes[53279]) + end) + + it("loads every part of the Kurgal archive in order", function() + local loadJewelFile = LoadModule("Modules/DataJewelFileLoader") + local originalInflate = _G.Inflate + local compressedSize + _G.Inflate = function(compressedData) + compressedSize = #compressedData + return compressedData:sub(1, 2) .. compressedData:sub(-4) + end + local ok, signature = pcall(loadJewelFile, "AbyssKurgal", false) + _G.Inflate = originalInflate + + assert.is_true(ok) + assert.are.equal(29419012, compressedSize) + assert.are.equal(string.char(0x78, 0xDA, 0x51, 0x9B, 0xED, 0x9E), signature) + end) + + it("applies the Kurgal example to the passive tree", function() + local spec = build.spec + assert.is_true(allocatePathToNode(spec, spec.nodes[61419])) + spec:BuildAllDependsAndPaths() + local unchangedName = spec.nodes[53279].dn + local unchangedStats = table.concat(spec.nodes[53279].sd, "\n") + + data.timelessJewelLUTs[9] = parseAbyssJewel(9, kurgalExampleData()) + equipJewel(spec, 61419, newAbyssTimelessJewel("Baleful Dominion", "Hypnotic Eye Jewel", + "Subjugating 533 souls in the thrall of Kurgal")) + spec:BuildAllDependsAndPaths() + + assert.are.equal("Fire Resistance", spec.nodes[15117].dn) + assert.are.equal("+8% to Fire Resistance", table.concat(spec.nodes[15117].sd, "\n")) + assert.are.equal("Cold and Lightning Resistance", spec.nodes[4367].dn) + assert.are.equal("+6% to Cold and Lightning Resistances", table.concat(spec.nodes[4367].sd, "\n")) + assert.matches("15%% reduced Effect of Curses on you while on Consecrated Ground", + table.concat(spec.nodes[20528].sd, "\n")) + local addedDamageStats = table.concat(spec.nodes[21958].sd, "\n") + assert.matches("12 to 18 Added Spell Cold Damage while Dual Wielding", addedDamageStats, nil, true) + local _, addedDamageLineCount = addedDamageStats:gsub("Added Spell Cold Damage while Dual Wielding", "") + assert.are.equal(1, addedDamageLineCount) + assert.are.equal(unchangedName, spec.nodes[53279].dn) + assert.are.equal(unchangedStats, table.concat(spec.nodes[53279].sd, "\n")) + end) + + it("finds Kurgal seed 533 with rolled replacement and addition weights", function() + data.timelessJewelLUTs[9] = parseAbyssJewel(9, kurgalExampleData()) + build.timelessData.jewelType = { id = 9 } + build.timelessData.conquerorType = { } + build.timelessData.jewelSocket = { id = 61419 } + build.timelessData.searchList = table.concat({ + "abyss_hypnotic_small_attribute19, 1, 0", + "abyss_hypnotic_notable_16, 1, 0", + }, "\n") + build.timelessData.searchListFallback = "" + + build.treeTab:FindTimelessJewel() + local controls = main.popups[1].controls + controls.searchButton.onClick() + + assert.are.equal(1, #build.timelessData.searchResults) + assert.are.equal(533, build.timelessData.searchResults[1].seed) + assert.are.equal(23, build.timelessData.searchResults[1].total) + + controls.searchResults:OnSelClick(1, build.timelessData.searchResults[1], true) + local addedJewel + for _, item in pairs(build.itemsTab.items) do + if item.jewelData and item.jewelData.conqueredBy and item.jewelData.conqueredBy.id == 533 then + addedJewel = item + break + end + end + assert.is_truthy(addedJewel) + assert.are.equal("Hypnotic Eye Jewel", addedJewel.baseName) + assert.are.equal("abyss_hypnotic", addedJewel.jewelData.conqueredBy.conqueror.type) + end) + + it("shows and scores both stats of an added damage notable", function() + data.timelessJewelLUTs[9] = parseAbyssJewel(9, kurgalExampleData()) + build.timelessData.jewelType = { id = 9 } + build.timelessData.conquerorType = { } + build.timelessData.jewelSocket = { id = 61419 } + build.timelessData.searchList = "abyss_hypnotic_notable_20, 1, 0, 0" + build.timelessData.searchListFallback = "" + + build.treeTab:FindTimelessJewel() + local controls = main.popups[1].controls + local addedDamageOption + for _, option in ipairs(controls.nodeSelect.list) do + if option.id == "abyss_hypnotic_notable_20" then + addedDamageOption = option + break + end + end + assert.is_truthy(addedDamageOption) + assert.are.same({ + "(12-14) to (17-19) Added Spell Cold Damage while Dual Wielding", + }, addedDamageOption.descriptions) + + controls.searchButton.onClick() + assert.are.equal(1, #build.timelessData.searchResults) + assert.are.equal(12, build.timelessData.searchResults[1].total) + + controls.searchList:SetText("abyss_hypnotic_notable_20, 0, 1, 0", true) + controls.searchButton.onClick() + assert.are.equal(1, #build.timelessData.searchResults) + assert.are.equal(18, build.timelessData.searchResults[1].total) + + local addedDamage + for _, addition in pairs(build.spec.tree.legion.additions) do + if addition.id == "abyss_hypnotic_notable_20" then + addedDamage = addition + break + end + end + addedDamage.modListGenerated = nil + controls.fallbackWeightsButton.onClick() + local primaryValues, secondaryValues = { }, { } + for _, mod in ipairs(addedDamage.modListGenerated[1].modList) do + primaryValues[mod.name] = mod.value + end + for _, mod in ipairs(addedDamage.modListGenerated[2].modList) do + secondaryValues[mod.name] = mod.value + end + assert.are.same({ ColdMin = 100, ColdMax = 0 }, primaryValues) + assert.are.same({ ColdMin = 0, ColdMax = 100 }, secondaryValues) + end) + + it("uses named notables and generates fallback weights for variable small nodes", function() + local zorathCastSpeedNotable + for _, addition in pairs(build.spec.tree.legion.additions) do + if addition.id:match("^abyss_.+_notable_%d+$") then + assert.is_nil(addition.dn:match("^Notable %d+$"), "Missing name for " .. addition.id) + end + if addition.id == "abyss_special_notable_17" then + zorathCastSpeedNotable = addition + break + end + end + assert.are.equal("Cast Speed", zorathCastSpeedNotable.dn) + + build.skillsTab:PasteSocketGroup("Ethereal Knives 20/0 1\n") + runCallback("OnFrame") + build.timelessData.jewelType = { id = 11 } + build.timelessData.conquerorType = { } + build.timelessData.jewelSocket = { id = 26196 } + build.treeTab:FindTimelessJewel() + local controls = main.popups[1].controls + controls.fallbackWeightsList.selIndex = 1 -- Full DPS + controls.fallbackWeightsButton.onClick() + + assert.matches("abyss_special_notable_17, 1, 0, 0", build.timelessData.searchListFallback, nil, true) + assert.matches("abyss_special_small_attribute25, 1, 0, 0", build.timelessData.searchListFallback, nil, true) + end) + + it("reads Zorath seed 6564 node and Inquisitor ascendancy changes", function() + data.timelessJewelLUTs[11] = parseAbyssJewel(11, zorathExampleData()) + local expected = { + [23027] = { type = 1, id = 686, rolls = { 8 } }, + [34171] = { type = 1, id = 712, rolls = { 19 } }, + [36949] = { type = 2, id = 266, rolls = { 2 } }, + [41472] = { type = 2, id = 303, rolls = { 12 } }, + [53884] = { type = 1, id = 526, rolls = { 1 } }, + [60472] = { type = 1, id = 698, rolls = { 4 } }, + } + local path = { } + for nodeId in pairs(expected) do + if nodeId ~= 53884 then + path[nodeId] = true + end + end + local affectedNodes = data.readAbyssJewelLUT(6564, nil, 11, path, "Inquisitor") + for nodeId, component in pairs(expected) do + assert.are.same(component, affectedNodes[nodeId][1]) + end + assert.is_nil(affectedNodes[50692]) + local allAscendancies = data.readAbyssJewelLUT(6564, nil, 11, path) + assert.are.same({ type = 1, id = 525, rolls = { 1 } }, allAscendancies[50692][1]) + assert.are.same(expected[53884], allAscendancies[53884][1]) + end) + + it("applies the Zorath path and selected Inquisitor ascendancy example", function() + local spec = build.spec + spec:SelectClass(spec.tree.classNameMap.Templar) + spec:SelectAscendClass(spec.tree.ascendNameMap.Inquisitor.ascendClassId) + -- This is the allocated path from socket 26196 to the Templar start in the supplied example. + for _, nodeId in ipairs({ + 26196, 34171, 60472, 23027, 6712, 36949, 10031, 15064, 12536, 30693, + 58453, 41472, 61471, 26866, 44908, 35556, 39916, 20228, 61525, + }) do + spec.nodes[nodeId].alloc = true + spec.allocNodes[nodeId] = spec.nodes[nodeId] + end + local path = spec:GetShortestPathToClassStart(26196) + assert.is_truthy(path) + for _, nodeId in ipairs({ 34171, 60472, 23027, 36949, 41472 }) do + assert.is_truthy(path[nodeId], "Expected Zorath path to contain node " .. nodeId) + end + data.timelessJewelLUTs[11] = parseAbyssJewel(11, zorathExampleData()) + equipJewel(spec, 26196, newAbyssTimelessJewel("Reclaimed Malevolence", "Assembled Eye Jewel", + "Binding 6564 souls to phylacteries to sustain Zorath")) + + spec:BuildAllDependsAndPaths() + + assert.are.equal("Lightning Ailment Effect", spec.nodes[34171].dn) + assert.are.equal("19% increased Effect of Lightning Ailments", table.concat(spec.nodes[34171].sd, "\n")) + assert.are.equal("Chaos Resistance", spec.nodes[60472].dn) + assert.are.equal("+4% to Chaos Resistance", table.concat(spec.nodes[60472].sd, "\n")) + assert.are.equal("Intelligence", spec.nodes[23027].dn) + assert.are.equal("+8 to Intelligence", table.concat(spec.nodes[23027].sd, "\n")) + assert.matches("2%% additional Physical Damage Reduction if you weren't Damaged by a Hit Recently", + table.concat(spec.nodes[36949].sd, "\n")) + assert.matches("12%% increased Damage over Time while Dual Wielding", + table.concat(spec.nodes[41472].sd, "\n")) + assert.are.equal("Spiteful Allies", spec.nodes[53884].dn) + assert.are.equal("Minions Impale on Hit", table.concat(spec.nodes[53884].sd, "\n")) + assert.are.equal("From Below", spec.nodes[50692].dn) + end) + + it("can protect an allocated ascendancy notable from Zorath", function() + local spec = build.spec + spec:SelectClass(spec.tree.classNameMap.Templar) + spec:SelectAscendClass(spec.tree.ascendNameMap.Inquisitor.ascendClassId) + assert.is_true(allocatePathToNode(spec, spec.nodes[26196])) + spec:BuildAllDependsAndPaths() + spec.nodes[53884].alloc = true + spec.allocNodes[53884] = spec.nodes[53884] + + data.timelessJewelLUTs[11] = parseAbyssJewel(11, zorathExampleData()) + local baseNodeName = spec.tree.nodes[53884].dn + -- Equipped timeless jewels rename nodes on the active tree. The protection + -- list must still show the node's original name. + spec.nodes[53884].dn = "Spiteful Allies" + assert.are.equal("Spiteful Allies", spec.nodes[53884].dn) + + build.timelessData.jewelType = { id = 11 } + build.timelessData.conquerorType = { } + build.timelessData.jewelSocket = { id = 26196 } + build.timelessData.socketFilter = true + build.timelessData.searchList = "" + build.timelessData.searchListFallback = "" + build.treeTab:FindTimelessJewel() + local controls = main.popups[1].controls + local _, protectLabelY = controls.protectAllocatedLabel:GetPos() + local _, requiredAscendancyY = controls.abyssAscendancySelect:GetPos() + assert.is_true(requiredAscendancyY + controls.abyssAscendancySelect:GetProperty("height") < protectLabelY) + for _, option in ipairs(controls.nodeSelect.list) do + assert.is_nil(option.id and option.id:match("^abyss_special_ascendancy_notable_")) + end + for index, option in ipairs(controls.abyssAscendancySelect.list) do + if option.id == "abyss_special_ascendancy_notable_3" then + controls.abyssAscendancySelect.selIndex = index + controls.abyssAscendancySelect.selFunc(index, option) + break + end + end + controls.searchButton.onClick() + assert.are.equal(0, #build.timelessData.searchResults) + for index, option in ipairs(controls.abyssAscendancySelect.list) do + if option.id == "abyss_special_ascendancy_notable_4" then + controls.abyssAscendancySelect.selIndex = index + controls.abyssAscendancySelect.selFunc(index, option) + break + end + end + assert.matches("abyss_special_ascendancy_notable_4, 1, 0, 1", build.timelessData.searchList, nil, true) + + controls.searchButton.onClick() + assert.are.equal(1, #build.timelessData.searchResults) + + local protectedOption + for index, option in ipairs(controls.protectAllocatedSelect.list) do + if option.label == baseNodeName then + controls.protectAllocatedSelect.selIndex = index + protectedOption = option + break + end + end + assert.are.equal(baseNodeName, controls.protectAllocatedSelect:GetSelValue().label) + local tooltip = new("Tooltip") + controls.protectAllocatedSelect.tooltipFunc(tooltip, "DROP", controls.protectAllocatedSelect.selIndex, protectedOption) + assert.is_true(#tooltip.lines > 0) + assert.are.same(spec.tree.nodes[53884].sd, protectedOption.descriptions) + controls.protectAllocatedButtonAdd.onClick() + local _, requiredAscendancyYAfterAdd = controls.abyssAscendancySelect:GetPos() + assert.are.equal(requiredAscendancyY, requiredAscendancyYAfterAdd) + controls.searchButton.onClick() + assert.are.equal(0, #build.timelessData.searchResults) + end) + + it("shows passive stat differences for an Abyss timeless jewel", function() + local spec = build.spec + assert.is_true(allocatePathToNode(spec, spec.nodes[61419])) + assert.is_true(allocatePathToNode(spec, spec.nodes[15117])) + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + + data.timelessJewelLUTs[9] = parseAbyssJewel(9, kurgalExampleData()) + local item = newAbyssTimelessJewel("Baleful Dominion", "Hypnotic Eye Jewel", + "Subjugating 533 souls in the thrall of Kurgal") + local slot = equipJewel(spec, 61419, item) + assert.is_truthy(slot) + assert.is_nil(item.jewelRadiusIndex) + spec:BuildAllDependsAndPaths() + build.buildFlag = true + runCallback("OnFrame") + assert.are.equal("Fire Resistance", spec.nodes[15117].dn) + + local tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + + local hasComparison + for _, line in ipairs(tooltip.lines) do + if (line.text or ""):find("Removing this item", 1, true) then + hasComparison = true + break + end + end + assert.is_true(hasComparison) + end) + + it("applies Zorath seed 4050 to the selected Chieftain notable", function() + local spec = build.spec + spec:SelectClass(spec.tree.classNameMap.Marauder) + spec:SelectAscendClass(spec.tree.ascendNameMap.Chieftain.ascendClassId) + assert.is_true(allocatePathToNode(spec, spec.nodes[61419])) + spec:BuildAllDependsAndPaths() + + local affectedNode = { + id = 50692, + modification = abyssModification({ { type = 1, id = 77, rolls = { 1 } } }), + } + data.timelessJewelLUTs[11] = parseAbyssJewel(11, table.concat({ + abyssHeader("ABYN", 11, 4050), + uint16(1), + uint16(affectedNode.id), + affectedNode.modification, + "ASCS", uint16(1), string.char(#"Chieftain"), "Chieftain", string.char(1), uint16(affectedNode.id), + })) + equipJewel(spec, 61419, newAbyssTimelessJewel("Reclaimed Malevolence", "Assembled Eye Jewel", + "Binding 4050 souls to phylacteries to sustain Zorath")) + spec:BuildAllDependsAndPaths() + + assert.is_truthy(data.readAbyssJewelLUT(4050, nil, 11, { }, "Chieftain")[50692]) + assert.are.equal("From Below", spec.nodes[50692].dn) + assert.are.equal("Sione, Sun's Roar", spec.nodes[31667].dn) + end) +end) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 1d5bf22ebc1..6ca70529cf3 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -3990,13 +3990,14 @@ function ItemsTabClass:FormatItemSource(text) :gsub("prophecy{([^}]+)}",colorCodes.PROPHECY.."%1"..colorCodes.SOURCE) end -local function itemChangesPassiveTreeRadius(item) - return not not (item and item.type == "Jewel" and item.jewelData and item.jewelRadiusIndex - and (item.jewelData.conqueredBy or item.jewelData.intuitiveLeapLike or item.jewelData.impossibleEscapeKeystone)) +local function itemChangesPassiveTree(item) + return not not (item and item.type == "Jewel" and item.jewelData + and (item.jewelData.conqueredBy or item.jewelRadiusIndex + and (item.jewelData.intuitiveLeapLike or item.jewelData.impossibleEscapeKeystone))) end --- Radius jewels can change conquered nodes and orphaned allocations, so compare --- against a rebuilt spec instead of approximating the diff with removeNodes. +-- These jewels can replace passive nodes or disconnect allocated passives, so +-- rebuild the passive tree before comparing their stats. -- Keep this list in sync with PassiveSpec's constructor, Init, and Select* -- methods; omitted fields fail safe as nil on the comparison spec. local sharedSpecKeysForJewelComparison = { @@ -4804,7 +4805,7 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) local function getReplacedItemAndOutput(compareSlot) local selItem = self.items[compareSlot.selItemId] local override = { repSlotName = compareSlot.slotName, repItem = item ~= selItem and item or nil } - if compareSlot.nodeId and (itemChangesPassiveTreeRadius(selItem) or itemChangesPassiveTreeRadius(item)) then + if compareSlot.nodeId and (itemChangesPassiveTree(selItem) or itemChangesPassiveTree(item)) then override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem) end local output = calcFunc(override) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 49b4dbc2ce3..dbed5d05a8f 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -990,7 +990,7 @@ end -- Only allocated nodes can be traversed function PassiveSpecClass:GetShortestPathToClassStart(rootId) local root = self.nodes[rootId] - if not root or not root.alloc or not root.connectedToStart then + if not root or not root.alloc then return nil end @@ -1096,9 +1096,40 @@ end -- Rebuilds dependencies and paths for all nodes function PassiveSpecClass:BuildAllDependsAndPaths() + local timelessJewelTypeByConqueror = { + vaal = 1, + karui = 2, + maraketh = 3, + templar = 4, + eternal = 5, + kalguur = 6, + abyss_murderous = 7, + abyss_searching = 8, + abyss_hypnotic = 9, + abyss_ghastly = 10, + abyss_special = 11, + } -- This table will keep track of which nodes have been visited during each path-finding attempt local visited = { } local attributes = { "Dexterity", "Intelligence", "Strength" } + -- Read Abyss changes before resetting the nodes. Zorath needs the currently + -- allocated path from its socket to the class starting node. + local abyssConquests = { } + for socketId, itemId in pairs(self.jewels) do + local item = self.build.itemsTab.items[itemId] + local conqueredBy = item and item.jewelData and item.jewelData.conqueredBy + local jewelType = conqueredBy and timelessJewelTypeByConqueror[conqueredBy.conqueror.type] + if jewelType and jewelType >= 7 and self.allocNodes[socketId] and not item.jewelData.limitDisabled then + local path = jewelType == 11 and self:GetShortestPathToClassStart(socketId) + for nodeId, modification in pairs(data.readAbyssJewelLUT(conqueredBy.id, socketId, jewelType, path)) do + abyssConquests[nodeId] = { + id = conqueredBy.id, + conqueror = conqueredBy.conqueror, + modification = modification, + } + end + end + end -- Check all nodes for other nodes which depend on them (i.e. are only connected to the tree through that node) for id, node in pairs(self.nodes) do node.depends = wipeTable(node.depends) @@ -1109,6 +1140,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths() if self.tree.nodes[id] then self:ReplaceNode(node,self.tree.nodes[id]) end + node.conqueredBy = abyssConquests[id] if node.type ~= "ClassStart" and node.type ~= "Socket" and not node.ascendancyName then for nodeId, itemId in pairs(self.jewels) do @@ -1125,7 +1157,10 @@ function PassiveSpecClass:BuildAllDependsAndPaths() t_insert(node.intuitiveLeapLikesAffecting, self.nodes[nodeId]) end if item.jewelData.conqueredBy then - node.conqueredBy = item.jewelData.conqueredBy + local radiusJewelType = timelessJewelTypeByConqueror[item.jewelData.conqueredBy.conqueror.type] + if not radiusJewelType or radiusJewelType < 7 then + node.conqueredBy = item.jewelData.conqueredBy + end end end end @@ -1159,29 +1194,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths() local legionNodes = self.tree.legion.nodes local legionAdditions = self.tree.legion.additions - -- FIXME - continue implementing - local jewelType = 5 - if conqueredBy.conqueror.type == "vaal" then - jewelType = 1 - elseif conqueredBy.conqueror.type == "karui" then - jewelType = 2 - elseif conqueredBy.conqueror.type == "maraketh" then - jewelType = 3 - elseif conqueredBy.conqueror.type == "templar" then - jewelType = 4 - elseif conqueredBy.conqueror.type == "kalguur" then - jewelType = 6 - elseif conqueredBy.conqueror.type == "abyssTecrod" then - jewelType = 7 - elseif conqueredBy.conqueror.type == "abyssUlaman" then - jewelType = 8 - elseif conqueredBy.conqueror.type == "abyssKurgal" then - jewelType = 9 - elseif conqueredBy.conqueror.type == "abyssAmanamu" then - jewelType = 10 - elseif conqueredBy.conqueror.type == "abyssZorath" then - jewelType = 11 - end + local jewelType = timelessJewelTypeByConqueror[conqueredBy.conqueror.type] or 5 local seed = conqueredBy.id if jewelType == 5 then seed = seed / 20 @@ -1206,7 +1219,24 @@ function PassiveSpecClass:BuildAllDependsAndPaths() return statToFix -- if it doesn't need to be changed end - if node.type == "Notable" then + if jewelType >= 7 then + for _, component in ipairs(conqueredBy.modification) do + local changedNode, replacesNode = data.resolveAbyssJewelComponent(component, self.tree.legion) + if changedNode then + if replacesNode then + self:ReplaceNode(node, changedNode) + end + for statIndex, statLine in ipairs(changedNode.sd) do + for statKey, statMod in pairs(changedNode.stats) do + statLine = replaceHelperFunc(statLine, statKey, statMod, component.rolls[statMod.index]) + end + self:NodeAdditionOrReplacementFromString(node, (replacesNode and "" or " \n") .. statLine, replacesNode and statIndex == 1) + end + else + ConPrintf("Unhandled Abyss component ID: " .. component.id) + end + end + elseif node.type == "Notable" then local jewelDataTbl = { } if seed ~= m_max(m_min(seed, data.timelessJewelSeedMax[jewelType]), data.timelessJewelSeedMin[jewelType]) then ConPrintf("ERROR: Seed " .. seed .. " is outside of valid range [" .. data.timelessJewelSeedMin[jewelType] .. " - " .. data.timelessJewelSeedMax[jewelType] .. "] for jewel type: " .. data.timelessJewelTypes[jewelType]) diff --git a/src/Classes/PassiveTree.lua b/src/Classes/PassiveTree.lua index beb25d84ebd..d6e244b1583 100644 --- a/src/Classes/PassiveTree.lua +++ b/src/Classes/PassiveTree.lua @@ -365,31 +365,51 @@ local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion) -- 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] + local function loadLegionSheet(data) + local sheet = spriteSheets[data.filename] if not sheet then sheet = { } sheet.handle = NewImageHandle() - sheet.handle:Load("TreeData/legion/"..maxZoom.filename) + sheet.handle:Load("TreeData/legion/"..data.filename) sheet.width, sheet.height = sheet.handle:ImageSize() - spriteSheets[maxZoom.filename] = sheet + spriteSheets[data.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] = { + return sheet + end + for _, data in ipairs(legionSprites.treeAssets or { }) do + local sheet = loadLegionSheet(data) + for name, coords in pairs(data.coords) do + self.assets[name] = { 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 + [4] = (coords.y + coords.h) / sheet.height, } end end + for type, data in pairs(legionSprites) do + if type ~= "treeAssets" then + local maxZoom = data[#data] + local sheet = loadLegionSheet(maxZoom) + 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 + end local classArt = { [0] = "centerscion", @@ -676,6 +696,14 @@ local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion) end end + -- The game only gives these notables numbered names. Use the manual name for + -- their first stat, or show the full stat description when no name exists yet. + for _, addition in pairs(self.legion.additions) do + if addition.id:match("^abyss_.+_notable_%d+$") and addition.dn:match("^Notable %d+$") then + addition.dn = data.abyssNotableNames[addition.sortedStats[1]] or addition.sd[1] + end + end + -- Build ModList for legion jewels for _, node in pairs(self.legion.nodes) do -- Determine node type diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 5d3cb60a524..17a3394de0a 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -19,6 +19,12 @@ local JEWEL_RADIUS_TINT_PRIMARY_ONLY = { 1, 0, 0, 0.7 } local JEWEL_RADIUS_TINT_COMPARE_ONLY = { 0, 1, 0, 0.7 } local gemTooltip = LoadModule("Classes/GemTooltip") + +local function isAbyssConquered(node) + local conqueror = node and node.conqueredBy and node.conqueredBy.conqueror + return conqueror and conqueror.type and conqueror.type:match("^abyss_") +end + local PassiveTreeViewClass = newClass("PassiveTreeView", function(self) self.ring = NewImageHandle() self.ring:Load("Assets/ring.png", "CLAMP") @@ -707,7 +713,22 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) setConnectorColor(0.75, 0.75, 0.75) end SetDrawColor(unpack(connectorColor)) - DrawImageQuad(tree.assets[connector.type..state].handle, unpack(connector.c)) + local assetName = connector.type .. state + -- The game uses Abyss connector art only when both connected nodes are conquered. + if isAbyssConquered(node1) and isAbyssConquered(node2) then + assetName = "Abyss" .. assetName + end + local asset = tree.assets[assetName] or tree.assets[connector.type..state] + -- Atlas assets provide bounds that map the usual connector coordinates into one sprite. + local left, top = asset[1] or 0, asset[2] or 0 + local width, height = (asset[3] or 1) - left, (asset[4] or 1) - top + local c = connector.c + DrawImageQuad(asset.handle, + c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], + left + c[9] * width, top + c[10] * height, + left + c[11] * width, top + c[12] * height, + left + c[13] * width, top + c[14] * height, + left + c[15] * width, top + c[16] * height) end -- Draw the connecting lines between nodes @@ -856,6 +877,16 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) base = node.sprites[node.type:lower()..(isAlloc and "Active" or "Inactive")] local overlayKey = state .. (node.ascendancyName and "Ascend" or "") .. (node.isBlighted and "Blighted" or "") local overlayName = node.overlay[overlayKey] + if isAbyssConquered(node) and (node.type == "Notable" or node.type == "Keystone") then + -- AlternateTreeArt gives Abyss replacements their own notable, + -- keystone, and ascendancy-notable frames. + local frameType = node.ascendancyName and "Ascendancy" or node.type + local frameState = state == "alloc" and "Allocated" or state == "path" and "CanAllocate" or "Unallocated" + local abyssOverlay = "Abyss" .. frameType .. "Frame" .. frameState + if tree.assets[abyssOverlay] then + overlayName = abyssOverlay + end + end if node.ascendancyName then local prefix = node.bloodlineOverlayPrefix or (tree.bloodlineSpritePrefixes and tree.bloodlineSpritePrefixes[node.ascendancyName]) if prefix and overlayName then @@ -1125,6 +1156,10 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) -- Draw ring overlays for jewel sockets local function drawJewelRadius(jewel, scrX, scrY, tint) + -- Abyss jewels do not show radius art in game. + if isAbyssConquered(jewel.jewelData) then + return + end local radData = build.data.jewelRadius[jewel.jewelRadiusIndex] local outerSize = radData.outer * scale local innerSize = radData.inner * scale * 1.06 diff --git a/src/Classes/TimelessJewelListControl.lua b/src/Classes/TimelessJewelListControl.lua index c1a25a720fd..e86ba6b8099 100644 --- a/src/Classes/TimelessJewelListControl.lua +++ b/src/Classes/TimelessJewelListControl.lua @@ -100,23 +100,7 @@ function TimelessJewelListControlClass:OnSelClick(index, data, doubleClick) local socketInfo = data.socketLabel or (self.sharedList.socket and self.sharedList.socket.keystone) or "Unknown" local label = "[" .. data.seed .. "; " .. data.total.. "; " .. socketInfo .. "]\n" local variant = self.sharedList.conqueror.id == 1 and 1 or (self.sharedList.conqueror.id - 1) .. "\n" - local itemData = [[ -Heroic Tragedy ]] .. label .. [[ -Timeless Jewel -League: Legion -Limited to: 1 Historic -Variant: Vorana (Black Scythe Training) -Variant: Uhtred (Celestial Mathematics) -Variant: Medved (The Unbreaking Circle) -Selected Variant: ]] .. variant .. "\n" .. [[ -Radius: Large -Implicits: 0 -{variant:1}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Vorana -{variant:2}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Uhtred -{variant:3}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Medved -Passives in radius are Conquered by the Kalguur -Historic -]] + local itemData if self.sharedList.type.id == 1 then itemData = [[ Glorious Vanity ]] .. label .. [[ @@ -228,7 +212,7 @@ Implicits: 0 Passives in radius are Conquered by the Templars Historic ]] - elseif self.sharedList.type.id == 5 then + elseif self.sharedList.type.id == 5 then itemData = [[ Elegant Hubris ]] .. label .. [[ Timeless Jewel @@ -245,6 +229,79 @@ Implicits: 0 {variant:3}Commissioned ]] .. data.seed .. [[ coins to commemorate Caspiro Passives in radius are Conquered by the Eternal Empire Historic +]] + elseif self.sharedList.type.id == 6 then + itemData = [[ +Heroic Tragedy ]] .. label .. [[ +Timeless Jewel +League: Legion +Limited to: 1 Historic +Variant: Vorana (Black Scythe Training) +Variant: Uhtred (Celestial Mathematics) +Variant: Medved (The Unbreaking Circle) +Selected Variant: ]] .. variant .. "\n" .. [[ +Radius: Large +Implicits: 0 +{variant:1}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Vorana +{variant:2}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Uhtred +{variant:3}Remembrancing ]] .. data.seed .. [[ songworthy deeds by the line of Medved +Passives in radius are Conquered by the Kalguur +Historic +]] + elseif self.sharedList.type.id == 7 then + itemData = [[ +Festering Vengeance ]] .. label .. [[ +Murderous Eye Jewel +League: Allflame +Limited to: 1 Historic +Implicits: 0 +Subjugating ]] .. data.seed .. [[ souls in the thrall of Tecrod +Passives affected are Conquered by the Abyssal +Historic +]] + elseif self.sharedList.type.id == 8 then + itemData = [[ +Extinguishing Grasp ]] .. label .. [[ +Searching Eye Jewel +League: Allflame +Limited to: 1 Historic +Implicits: 0 +Subjugating ]] .. data.seed .. [[ souls in the thrall of Ulaman +Passives affected are Conquered by the Abyssal +Historic +]] + elseif self.sharedList.type.id == 9 then + itemData = [[ +Baleful Dominion ]] .. label .. [[ +Hypnotic Eye Jewel +League: Allflame +Limited to: 1 Historic +Implicits: 0 +Subjugating ]] .. data.seed .. [[ souls in the thrall of Kurgal +Passives affected are Conquered by the Abyssal +Historic +]] + elseif self.sharedList.type.id == 10 then + itemData = [[ +Destructive Aspiration ]] .. label .. [[ +Ghastly Eye Jewel +League: Allflame +Limited to: 1 Historic +Implicits: 0 +Subjugating ]] .. data.seed .. [[ souls in the thrall of Amanamu +Passives affected are Conquered by the Abyssal +Historic +]] + elseif self.sharedList.type.id == 11 then + itemData = [[ +Reclaimed Malevolence ]] .. label .. [[ +Assembled Eye Jewel +League: Allflame +Limited to: 1 Historic +Implicits: 0 +Binding ]] .. data.seed .. [[ souls to phylacteries to sustain Zorath +Passives affected are Conquered by the Abyssal +Historic ]] end local item = new("Item", itemData) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 254ca5ffe57..9db3f378b44 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1190,7 +1190,12 @@ function TreeTabClass:FindTimelessJewel() { label = "Brutal Restraint", name = "maraketh", id = 3 }, { label = "Militant Faith", name = "templar", id = 4 }, { label = "Elegant Hubris", name = "eternal", id = 5 }, - { label = "Heroic Tragedy", name = "kalguur", id = 6 } + { label = "Heroic Tragedy", name = "kalguur", id = 6 }, + { label = "Festering Vengeance", name = "abyss_murderous", id = 7 }, + { label = "Extinguishing Grasp", name = "abyss_searching", id = 8 }, + { label = "Baleful Dominion", name = "abyss_hypnotic", id = 9 }, + { label = "Destructive Aspiration", name = "abyss_ghastly", id = 10 }, + { label = "Reclaimed Malevolence", name = "abyss_special", id = 11 } } -- rebuild `timelessData.jewelType` as we only store the minimum amount of `jewelType` data in build XML if next(timelessData.jewelType) then @@ -1239,7 +1244,12 @@ function TreeTabClass:FindTimelessJewel() { label = "Vorana (Black Scythe Training)", id = 2 }, { label = "Uhtred (Celestial Mathematics)", id = 3 }, { label = "Medved (The Unbreaking Circle)", id = 4 } - } + }, + [7] = { { label = "Tecrod (Overwhelming Hate)", id = 1 } }, + [8] = { { label = "Ulaman (Weighted Exchange)", id = 1 } }, + [9] = { { label = "Kurgal (Reconstructed Essence)", id = 1 } }, + [10] = { { label = "Amanamu (The Loyal Few)", id = 1 } }, + [11] = { { label = "Zorath", id = 1 } } } -- rebuild `timelessData.conquerorType` as we only store the minimum amount of `conquerorType` data in build XML if next(timelessData.conquerorType) then @@ -1270,6 +1280,18 @@ function TreeTabClass:FindTimelessJewel() { id = 15, label = "Non-Curse Aura Effect" }, { id = 16, label = "Defences from Shield" } } + local abyssAscendancyOptions = { } + for _, node in pairs(legionNodes) do + if node.id:match("^abyss_special_ascendancy_notable_%d+$") then + t_insert(abyssAscendancyOptions, { + label = node.dn, + descriptions = copyTable(node.sd), + id = node.id, + }) + end + end + t_sort(abyssAscendancyOptions, function(a, b) return a.label < b.label end) + t_insert(abyssAscendancyOptions, 1, { label = "Any" }) local jewelSockets = { } t_insert(jewelSockets, { label = "All Sockets", @@ -1328,7 +1350,9 @@ function TreeTabClass:FindTimelessJewel() wipeTable(modData) local smallModData = { } for _, node in pairs(legionNodes) do - if node.id:match("^" .. timelessData.jewelType.name .. "_.+") and not isValueInArray(ignoredMods, node.dn) and not node.ks then + if node.id:match("^" .. timelessData.jewelType.name .. "_.+") + and not node.id:match("^abyss_special_ascendancy_notable_") + and not isValueInArray(ignoredMods, node.dn) and not node.ks then if node["not"] then t_insert(modData, { label = node.dn .. " " .. node.sd[1], @@ -1480,7 +1504,7 @@ function TreeTabClass:FindTimelessJewel() self.build.modFlag = true end - controls.devotionSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {820, 25, 0, 16}, "^7Devotion modifiers:") + controls.devotionSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {920, 25, 0, 16}, "^7Devotion modifiers:") controls.devotionSelectLabel.shown = timelessData.jewelType.id == 4 controls.devotionSelect1 = new("DropDownControl", {"TOP", controls.devotionSelectLabel, "BOTTOM"}, {0, 8, 200, 18}, devotionVariants, function(index, value) timelessData.devotionVariant1 = index @@ -1495,11 +1519,19 @@ function TreeTabClass:FindTimelessJewel() local rowHeight = 17 local labelHeight = 16 local labelSpacing = 4 + local allocatedNodes = { } + local protectedNodes = { } + local protectedNodesCount = 0 + local setAllocatedNodes + local clearProtected + self.allocatedNodesInRadiusCount = 0 - controls.jewelSelect = new("DropDownControl", {"TOPLEFT", nil, "TOPLEFT"}, {380, 25, 200, rowHeight}, jewelTypes, function(index, value) + controls.jewelSelect = new("DropDownControl", {"TOPLEFT", nil, "TOPLEFT"}, {520, 25, 200, rowHeight}, jewelTypes, function(index, value) timelessData.jewelType = value controls.devotionSelectLabel.shown = value.id == 4 -- Militant Faith - controls.protectAllocatedLabel.shown = (value.id == 4 and controls.socketFilter.state) + controls.abyssAscendancyLabel.shown = value.id == 11 + controls.abyssAscendancySelect.selIndex = 1 + controls.protectAllocatedLabel.shown = (value.id == 4 or value.id == 11) and controls.socketFilter.state controls.conquerorSelect.list = conquerorTypes[timelessData.jewelType.id] controls.conquerorSelect.selIndex = 1 timelessData.conquerorType = conquerorTypes[timelessData.jewelType.id][1] @@ -1507,6 +1539,10 @@ function TreeTabClass:FindTimelessJewel() buildMods() updateSearchList("", false) updateSearchList("", true) + if controls.socketFilter.state then + clearProtected() + setAllocatedNodes() + end end) controls.jewelSelect.selIndex = timelessData.jewelType.id controls.jewelSelectLabel = new("LabelControl", {"RIGHT", controls.jewelSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Jewel Type:") @@ -1519,26 +1555,36 @@ function TreeTabClass:FindTimelessJewel() controls.conquerorSelect.selIndex = timelessData.conquerorType.id controls.conquerorSelectLabel = new("LabelControl", {"RIGHT", controls.conquerorSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Conqueror:") - local allocatedNodes = { } - local protectedNodes = { } - local protectedNodesCount = 0 - self.allocatedNodesInRadiusCount = 0 - local function setAllocatedNodes() -- find allocated nodes in radius for Militant Faith filtering / protected nodes dropdown + setAllocatedNodes = function() if timelessData.jewelSocket.id == -1 or not treeData.nodes[timelessData.jewelSocket.id] then return end - local nodeNames = { } - local radiusNodes = treeData.nodes[timelessData.jewelSocket.id].nodesInRadius[3] -- large radius around timelessData.jewelSocket.id - for nodeId in pairs(radiusNodes) do - if self.build.calcsTab.mainEnv.grantedPassives[nodeId] ~= nil or self.build.spec.allocNodes[nodeId] ~= nil then - allocatedNodes[nodeId] = true - if treeData.nodes[nodeId] and treeData.nodes[nodeId].isNotable then - t_insert(nodeNames, treeData.nodes[nodeId].dn) + wipeTable(allocatedNodes) + local nodeOptions = { } + if timelessData.jewelType.id == 11 then + -- Reclaimed Malevolence can replace an allocated notable in the selected ascendancy. + for nodeId in pairs(self.build.spec.allocNodes) do + local baseNode = treeData.nodes[nodeId] + if baseNode and baseNode.ascendancyName == self.build.spec.curAscendClassName and baseNode.type == "Notable" then + allocatedNodes[nodeId] = true + t_insert(nodeOptions, { label = baseNode.dn, descriptions = copyTable(baseNode.sd) }) + end + end + else + local radiusNodes = treeData.nodes[timelessData.jewelSocket.id].nodesInRadius[3] + for nodeId in pairs(radiusNodes) do + if self.build.calcsTab.mainEnv.grantedPassives[nodeId] ~= nil or self.build.spec.allocNodes[nodeId] ~= nil then + allocatedNodes[nodeId] = true + if treeData.nodes[nodeId] and treeData.nodes[nodeId].isNotable then + local baseNode = treeData.nodes[nodeId] + t_insert(nodeOptions, { label = baseNode.dn, descriptions = copyTable(baseNode.sd) }) + end end end end - controls.protectAllocatedSelect:SetList(nodeNames) - self.allocatedNodesInRadiusCount = #nodeNames + t_sort(nodeOptions, function(a, b) return a.label < b.label end) + controls.protectAllocatedSelect:SetList(nodeOptions) + self.allocatedNodesInRadiusCount = #nodeOptions end @@ -1556,7 +1602,7 @@ function TreeTabClass:FindTimelessJewel() end controls.socketSelectLabel = new("LabelControl", {"RIGHT", controls.socketSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Jewel Socket:") - local function clearProtected() -- clear all controls, nodes related to Militant Faith filtering + clearProtected = function() protectedNodesCount = 0 protectedNodes = { } for index, _ in pairs(controls) do @@ -1572,7 +1618,7 @@ function TreeTabClass:FindTimelessJewel() controls.socketFilterAdditionalDistanceLabel.shown = value controls.socketFilterAdditionalDistance.shown = value controls.socketFilterAdditionalDistanceValue.shown = value - controls.protectAllocatedLabel.shown = (value and timelessData.jewelType.label == "Militant Faith") + controls.protectAllocatedLabel.shown = value and (timelessData.jewelType.id == 4 or timelessData.jewelType.id == 11) if value then setAllocatedNodes() @@ -1588,15 +1634,23 @@ function TreeTabClass:FindTimelessJewel() end controls.socketFilter.state = timelessData.socketFilter - -- Militant Faith protect notables controls - controls.protectAllocatedLabel = new("LabelControl", { "TOPLEFT", nil, "TOPLEFT" }, { 15, 25, 0, 16 }, "^7Protect allocated nodes from changing:") + -- Protect notables that must not be replaced by Militant Faith or Reclaimed Malevolence. + controls.protectAllocatedLabel = new("LabelControl", { "TOPLEFT", nil, "TOPLEFT" }, { + 15, + function() + return timelessData.jewelType.id == 11 and 85 or 25 + end, + 0, + 16, + }, "^7Protect allocated nodes from changing:") controls.protectAllocatedSelect = new("DropDownControl", { "TOPLEFT", controls.protectAllocatedLabel, "BOTTOMLEFT" }, { 0, 8, 200, 18 }, nil, nil) controls.protectAllocatedButtonAdd = new("ButtonControl", { "LEFT", controls.protectAllocatedSelect, "RIGHT" }, { 5, 0, 44, 18 }, "Add", function() local selValue = controls.protectAllocatedSelect:GetSelValue() - if selValue and not controls["protected:"..selValue] then + local nodeName = selValue and selValue.label + if nodeName and not controls["protected:"..nodeName] then protectedNodesCount = protectedNodesCount + 1 - t_insert(protectedNodes, selValue) - controls["protected:"..selValue] = new("LabelControl", { "TOPLEFT", controls.protectAllocatedSelect, "BOTTOMLEFT" }, { 0, 16 * protectedNodesCount - 10, 0, 16 }, "^7"..selValue) + t_insert(protectedNodes, nodeName) + controls["protected:"..nodeName] = new("LabelControl", { "TOPLEFT", controls.protectAllocatedSelect, "BOTTOMLEFT" }, { 0, 16 * protectedNodesCount - 10, 0, 16 }, "^7"..nodeName) end end) controls.protectAllocatedButtonClear = new("ButtonControl", { "LEFT", controls.protectAllocatedButtonAdd, "RIGHT" }, { 5, 0, 44, 18 }, "Clear", function() @@ -1606,7 +1660,49 @@ function TreeTabClass:FindTimelessJewel() if controls.socketFilter.state then setAllocatedNodes() end - controls.protectAllocatedLabel.shown = controls.jewelSelect.selIndex == 4 and controls.socketFilter.state + controls.protectAllocatedLabel.shown = (controls.jewelSelect.selIndex == 4 or controls.jewelSelect.selIndex == 11) and controls.socketFilter.state + + -- This requirement is separate from ordinary node weights and remains above + -- the protection list so adding protected nodes does not move the selector. + controls.abyssAscendancyLabel = new("LabelControl", {"TOPLEFT", nil, "TOPLEFT"}, {15, 25, 0, 16}, "^7Required ascendancy outcome:") + controls.abyssAscendancyLabel.shown = timelessData.jewelType.id == 11 + controls.abyssAscendancySelect = new("DropDownControl", {"TOPLEFT", controls.abyssAscendancyLabel, "BOTTOMLEFT"}, {0, 8, 200, 18}, abyssAscendancyOptions, function(index, value) + local searchLines = { } + for _, searchRow in ipairs(searchListTbl) do + if not searchRow[1]:match("^abyss_special_ascendancy_notable_") then + t_insert(searchLines, t_concat(searchRow, ", ")) + end + end + if value.id then + t_insert(searchLines, value.id .. ", 1, 0, 1") + end + updateSearchList(t_concat(searchLines, "\n"), false) + end) + controls.abyssAscendancySelect.selIndex = 1 + for _, searchRow in ipairs(searchListTbl) do + for optionIndex, option in ipairs(abyssAscendancyOptions) do + if option.id == searchRow[1] then + controls.abyssAscendancySelect.selIndex = optionIndex + break + end + end + end + controls.abyssAscendancySelect.tooltipFunc = function(tooltip, mode, index, value) + tooltip:Clear() + if mode ~= "OUT" and value.descriptions then + for _, line in ipairs(value.descriptions) do + tooltip:AddLine(16, "^7" .. line) + end + end + end + controls.protectAllocatedSelect.tooltipFunc = function(tooltip, mode, index, value) + tooltip:Clear() + if mode ~= "OUT" and value and value.descriptions then + for _, line in ipairs(value.descriptions) do + tooltip:AddLine(16, "^7" .. line) + end + end + end controls.protectAllocatedButtonAdd.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() @@ -1756,6 +1852,15 @@ function TreeTabClass:FindTimelessJewel() end buildMods() + local function getLegionStatLabels(legionPassive) + local statCount = timelessData.jewelType.id >= 7 and #legionPassive.sortedStats or #legionPassive.sd + if statCount > #legionPassive.sd then + return statCount, "Minimum value: " .. legionPassive.sd[1], "Maximum value: " .. legionPassive.sd[1] + end + return statCount, + statCount == 1 and t_concat(legionPassive.sd, " + ") or legionPassive.sd[1] or "None", + legionPassive.sd[2] or "None" + end controls.nodeSelect = new("DropDownControl", {"TOPLEFT", controls.nodeSlider3, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, modData, function(index, value) nodeSliderStatLabel = "None" nodeSlider2StatLabel = "None" @@ -1763,18 +1868,14 @@ function TreeTabClass:FindTimelessJewel() local statCount = 0 for _, legionNode in ipairs(legionNodes) do if legionNode.id == value.id then - statCount = #legionNode.sd - nodeSliderStatLabel = legionNode.sd[1] or "None" - nodeSlider2StatLabel = legionNode.sd[2] or "None" + statCount, nodeSliderStatLabel, nodeSlider2StatLabel = getLegionStatLabels(legionNode) break end end if statCount == 0 then for _, legionAddition in ipairs(legionAdditions) do if legionAddition.id == value.id then - statCount = #legionAddition.sd - nodeSliderStatLabel = legionAddition.sd[1] or "None" - nodeSlider2StatLabel = legionAddition.sd[2] or "None" + statCount, nodeSliderStatLabel, nodeSlider2StatLabel = getLegionStatLabels(legionAddition) break end end @@ -1844,7 +1945,7 @@ function TreeTabClass:FindTimelessJewel() local powerGain = (nodePower - basePower) / -- normalize with absolute base power so that the result isn't negative math.abs(basePower) - powerEntry["weight" .. i] = powerGain / (newNode.divisor or 1) + powerEntry["weight" .. i] = powerGain / (node.divisor or newNode.divisor or 1) end end t_insert(newList, powerEntry) @@ -1872,8 +1973,22 @@ function TreeTabClass:FindTimelessJewel() end return statToFix -- if it doesn't need to be changed end + local function buildStatModLists(legionPassive) + -- Give each stat its own mod list even when several stats share one display line. + local modLists = { } + for statIndex, statKey in ipairs(legionPassive.sortedStats) do + local statValues = { } + for key in pairs(legionPassive.stats) do + statValues[key] = key == statKey and 100 or 0 + end + local line = data.describeStats(statValues, "stat_descriptions")[1] + modLists[statIndex] = { modList = modLib.parseMod(line), divisor = 100 } + end + return modLists + end local nodes = { } + local usesVariableRolls = timelessData.jewelType.id == 1 or timelessData.jewelType.id >= 7 for _, modNode in ipairs(modData) do if modNode.id then local newNode = nil @@ -1881,32 +1996,34 @@ function TreeTabClass:FindTimelessJewel() if legionNode.id == modNode.id or (totalModIDs[modNode.id] and totalModIDs[modNode.id][legionNode.id]) then newNode = { } newNode.id = modNode.id - if modNode.type == "vaal" then - if #legionNode.sd == 2 then + if usesVariableRolls then + if #legionNode.sortedStats > 1 then newNode.calcMultiple = true if legionNode.modListGenerated then newNode.node = copyTable(legionNode.modListGenerated) else - -- generate modList - local modList1, extra1 = modLib.parseMod(replaceHelperFunc(legionNode.sd[1], legionNode.sortedStats[1], legionNode.stats[legionNode.sortedStats[1]], 100)) - local modList2, extra2 = modLib.parseMod(replaceHelperFunc(legionNode.sd[2], legionNode.sortedStats[2], legionNode.stats[legionNode.sortedStats[2]], 100)) - local modLists = { { modList = modList1 }, { modList = modList2 } } + local modLists = buildStatModLists(legionNode) legionNode.modListGenerated = copyTable(modLists) newNode.node = copyTable(modLists) end - newNode.node[1].id = legionNode.id - newNode.node[2].id = legionNode.id + for _, node in ipairs(newNode.node) do + node.id = legionNode.id + end else - if legionNode.modListGenerated then + local originalLine = legionNode.sd[1] + local line = replaceHelperFunc(originalLine, legionNode.sortedStats[1], legionNode.stats[legionNode.sortedStats[1]], 100) + if line == originalLine and #legionNode.sd > 1 then + -- Some fixed game stats represent several display lines; score the complete effect together. + newNode.modList = legionNode.modList + elseif legionNode.modListGenerated then newNode.modList = copyTable(legionNode.modListGenerated) else - -- generate modList - local modList, extra = modLib.parseMod(replaceHelperFunc(legionNode.sd[1], legionNode.sortedStats[1], legionNode.stats[legionNode.sortedStats[1]], 100)) + local modList, extra = modLib.parseMod(line) legionNode.modListGenerated = modList newNode.modList = modList end + newNode.divisor = line ~= originalLine and 100 or 1 end - newNode.divisor = 100 else newNode.modList = legionNode.modList if modNode.totalMod then @@ -1921,25 +2038,37 @@ function TreeTabClass:FindTimelessJewel() if legionAddition.id == modNode.id or (totalModIDs[modNode.id] and totalModIDs[modNode.id][legionAddition.id]) then newNode = { } newNode.id = modNode.id - if legionAddition.modList then + if usesVariableRolls and #legionAddition.sortedStats > 1 then + newNode.calcMultiple = true + if legionAddition.modListGenerated then + newNode.node = copyTable(legionAddition.modListGenerated) + else + local modLists = buildStatModLists(legionAddition) + legionAddition.modListGenerated = copyTable(modLists) + newNode.node = copyTable(modLists) + end + for _, node in ipairs(newNode.node) do + node.id = legionAddition.id + end + elseif legionAddition.modList then newNode.modList = legionAddition.modList elseif legionAddition.modListGenerated then newNode.modList = legionAddition.modListGenerated else -- generate modList - local line = legionAddition.sd[1] - if modNode.type == "vaal" then - for key, stat in legionAddition.stats do -- should only be length 1 + local originalLine = legionAddition.sd[1] + local line = originalLine + if usesVariableRolls then + for key, stat in pairs(legionAddition.stats) do -- should only be length 1 line = replaceHelperFunc(line, key, stat, 100) end end local modList, extra = modLib.parseMod(line) legionAddition.modListGenerated = modList newNode.modList = modList + newNode.divisor = line ~= originalLine and 100 or 1 end - if modNode.type == "vaal" then - newNode.divisor = 100 - elseif modNode.totalMod then + if not usesVariableRolls and modNode.totalMod then newNode.divisor = newNode.modList[1].value end break @@ -1999,7 +2128,7 @@ function TreeTabClass:FindTimelessJewel() tooltip:AddLine(16, "^7Optional: Only show results where total weight meets or exceeds this value.") end - local listWidth = 440 + local listWidth = 500 local listHeight = 200 local buttonHeight = 20 local edgePadding = 12 @@ -2102,9 +2231,13 @@ function TreeTabClass:FindTimelessJewel() local result = timelessData.searchResults[i] local conquerorKeystoneTradeIds = data.timelessJewelTradeIDs[timelessData.jewelType.id].keystone - local conquerorTradeIds = { conquerorKeystoneTradeIds[1], conquerorKeystoneTradeIds[2], conquerorKeystoneTradeIds[3] } - if timelessData.sharedResults.conqueror.id > 1 then + local conquerorTradeIds + if timelessData.jewelType.id >= 7 then + conquerorTradeIds = { conquerorKeystoneTradeIds[1] } + elseif timelessData.sharedResults.conqueror.id > 1 then conquerorTradeIds = { conquerorKeystoneTradeIds[timelessData.sharedResults.conqueror.id - 1] } + else + conquerorTradeIds = { conquerorKeystoneTradeIds[1], conquerorKeystoneTradeIds[2], conquerorKeystoneTradeIds[3] } end for _, tradeId in ipairs(conquerorTradeIds) do @@ -2289,6 +2422,11 @@ function TreeTabClass:FindTimelessJewel() end local radiusNodes = treeData.nodes[socketId].nodesInRadius[3] + local isAbyssJewel = timelessData.jewelType.id >= 7 + local zorathPath = timelessData.jewelType.id == 11 and self.build.spec:GetShortestPathToClassStart(socketId) + if timelessData.jewelType.id == 11 and not zorathPath then + return nil + end local allocatedNodes = { } local unAllocatedNodesDistance = { } local targetNodes = { } @@ -2323,7 +2461,7 @@ function TreeTabClass:FindTimelessJewel() for _, legionNode in ipairs(legionNodes) do if legionNode.id == desiredNode[1] then -- non-vaal replacements only support one nodeWeight - if timelessData.jewelType.id > 1 then + if timelessData.jewelType.id > 1 and timelessData.jewelType.id < 7 then singleStat = true end displayName = t_concat(legionNode.sd, " + ") @@ -2334,8 +2472,8 @@ function TreeTabClass:FindTimelessJewel() if displayName == nil then for _, legionAddition in ipairs(legionAdditions) do if legionAddition.id == desiredNode[1] then - -- additions only support one nodeWeight - singleStat = true + -- Original timeless additions only store an ID; Abyss records also store their rolls. + singleStat = timelessData.jewelType.id < 7 displayName = t_concat(legionAddition.sd, " + ") break end @@ -2358,7 +2496,7 @@ function TreeTabClass:FindTimelessJewel() end -- if we're protecting a node and the number of protected nodes is less than the total allocated in radius and the total desired nodes is less than the total allocated in radius -- these constraints avoid a blank result in the case where you set a min weight of 1 onto a non devotion stat with zero unprotected nodes - if protectedNodesCount > 0 and protectedNodesCount < self.allocatedNodesInRadiusCount and (#searchListCombinedTbl < self.allocatedNodesInRadiusCount) then + if timelessData.jewelType.id == 4 and protectedNodesCount > 0 and protectedNodesCount < self.allocatedNodesInRadiusCount and (#searchListCombinedTbl < self.allocatedNodesInRadiusCount) then t_insert(minimumWeights, { reqNode = desiredNode[1], weight = 1 }) end if desiredNodes[desiredNode[1]] then @@ -2386,7 +2524,7 @@ function TreeTabClass:FindTimelessJewel() rootNodes[class.startNodeId] = true end - if controls.socketFilter.state then + if controls.socketFilter.state and not isAbyssJewel then timelessData.socketFilterDistance = timelessData.socketFilterDistance or 0 for nodeId in pairs(radiusNodes) do allocatedNodes[nodeId] = self.build.calcsTab.mainEnv.grantedPassives[nodeId] ~= nil or self.build.spec.allocNodes[nodeId] ~= nil @@ -2396,28 +2534,69 @@ function TreeTabClass:FindTimelessJewel() end end - for nodeId in pairs(radiusNodes) do - if not rootNodes[nodeId] - and not treeData.nodes[nodeId].isJewelSocket - and not treeData.nodes[nodeId].isKeystone - and (not controls.socketFilter.state or allocatedNodes[nodeId] or (timelessData.socketFilterDistance > 0 and unAllocatedNodesDistance[nodeId] <= timelessData.socketFilterDistance)) then - if (treeData.nodes[nodeId].isNotable or timelessData.jewelType.id == 1) then - targetNodes[nodeId] = true - elseif desiredNodes["totalStat"] and not treeData.nodes[nodeId].isNotable then - if isValueInArray({ "Strength", "Intelligence", "Dexterity" }, treeData.nodes[nodeId].dn) then - targetSmallNodes.attributeSmalls = targetSmallNodes.attributeSmalls + 1 - else - targetSmallNodes.otherSmalls = targetSmallNodes.otherSmalls + 1 + if not isAbyssJewel then + for nodeId in pairs(radiusNodes) do + if not rootNodes[nodeId] + and not treeData.nodes[nodeId].isJewelSocket + and not treeData.nodes[nodeId].isKeystone + and (not controls.socketFilter.state or allocatedNodes[nodeId] or (timelessData.socketFilterDistance > 0 and unAllocatedNodesDistance[nodeId] <= timelessData.socketFilterDistance)) then + if (treeData.nodes[nodeId].isNotable or timelessData.jewelType.id == 1) then + targetNodes[nodeId] = true + elseif desiredNodes["totalStat"] and not treeData.nodes[nodeId].isNotable then + if isValueInArray({ "Strength", "Intelligence", "Dexterity" }, treeData.nodes[nodeId].dn) then + targetSmallNodes.attributeSmalls = targetSmallNodes.attributeSmalls + 1 + else + targetSmallNodes.otherSmalls = targetSmallNodes.otherSmalls + 1 + end end end end end - + local seedWeights = { } local seedMultiplier = timelessData.jewelType.id == 5 and 20 or 1 -- Elegant Hubris for curSeed = data.timelessJewelSeedMin[timelessData.jewelType.id] * seedMultiplier, data.timelessJewelSeedMax[timelessData.jewelType.id] * seedMultiplier, seedMultiplier do seedWeights[curSeed] = 0 resultNodes[curSeed] = { } + if isAbyssJewel then + -- Abyss files name the affected passives directly, so score those entries + -- instead of checking every passive in a jewel radius. + for targetNode, modification in pairs(data.readAbyssJewelLUT(curSeed, socketId, timelessData.jewelType.id, zorathPath, self.build.spec.curAscendClassName)) do + local treeNode = treeData.nodes[targetNode] + local scoreNode = treeNode and not rootNodes[targetNode] and not treeNode.isJewelSocket and not treeNode.isKeystone + if scoreNode and treeNode.ascendancyName and isValueInTable(protectedNodes, treeNode.dn) then + resultNodes[curSeed] = nil + break + end + if scoreNode and controls.socketFilter.state and not zorathPath then + timelessData.socketFilterDistance = timelessData.socketFilterDistance or 0 + local allocated = self.build.calcsTab.mainEnv.grantedPassives[targetNode] ~= nil or self.build.spec.allocNodes[targetNode] ~= nil + local distance = self.build.spec.nodes[targetNode].pathDist or 1000 + scoreNode = allocated or timelessData.socketFilterDistance > 0 and distance <= timelessData.socketFilterDistance + end + if scoreNode then + for _, component in ipairs(modification) do + local changedNode = data.resolveAbyssJewelComponent(component, treeData.legion) + local changedNodeId = changedNode and changedNode.id + local desiredNode = desiredNodes[changedNodeId] + if desiredNode then + local _, statMod1, roll1 = data.getAbyssJewelComponentRoll(component, changedNode, 1) + local _, statMod2, roll2 = data.getAbyssJewelComponentRoll(component, changedNode, 2) + local weight = desiredNode.nodeWeight * (roll1 or 1) + if statMod2 then + weight = weight + desiredNode.nodeWeight2 * (roll2 or 1) + end + resultNodes[curSeed][changedNodeId] = resultNodes[curSeed][changedNodeId] or { targetNodeNames = { }, totalWeight = 0 } + resultNodes[curSeed][changedNodeId].totalWeight = resultNodes[curSeed][changedNodeId].totalWeight + weight + t_insert(resultNodes[curSeed][changedNodeId], targetNode) + t_insert(resultNodes[curSeed][changedNodeId].targetNodeNames, treeNode.name) + seedWeights[curSeed] = seedWeights[curSeed] + weight + end + end + end + end + end + -- This list is empty for Abyss jewels because they were scored above. for targetNode in pairs(targetNodes) do local jewelDataTbl = data.readLUT(curSeed, targetNode, timelessData.jewelType.id) if not next(jewelDataTbl) then @@ -2558,8 +2737,8 @@ function TreeTabClass:FindTimelessJewel() result.socketId = socketInfo.id result.socketLabel = socketInfo.label end - if timelessData.jewelType.id == 1 or timelessData.jewelType.id == 3 then - -- Glorious Vanity [100-8000], Brutal Restraint [500-8000] + if timelessData.jewelType.id == 1 or timelessData.jewelType.id == 3 or timelessData.jewelType.id >= 7 then + -- These jewel types all use seeds below 10000. if seedMatch < 1000 then result.label = " " .. result.label end @@ -2604,6 +2783,7 @@ function TreeTabClass:FindTimelessJewel() controls.resetButton = new("ButtonControl", {"BOTTOMLEFT", nil, "BOTTOMLEFT"}, {panelWidth / 2 - buttonWidth/2, -edgePadding, buttonWidth, buttonHeight}, "Reset", function() updateSearchList("", true) updateSearchList("", false) + controls.abyssAscendancySelect.selIndex = 1 wipeTable(timelessData.searchResults) controls.searchTradeButton.enabled = false clearProtected() diff --git a/src/Data/Bases/jewel.lua b/src/Data/Bases/jewel.lua index 522d736a64c..93ea85a30c9 100644 --- a/src/Data/Bases/jewel.lua +++ b/src/Data/Bases/jewel.lua @@ -55,6 +55,13 @@ itemBases["Searching Eye Jewel"] = { implicitModTypes = { }, req = { }, } +itemBases["Assembled Eye Jewel"] = { + type = "Jewel", + subType = "Abyss", + tags = { abyss_jewel = true, abyss_jewel_special = true, default = true, not_for_sale = true, }, + implicitModTypes = { }, + req = { }, +} itemBases["Large Cluster Jewel"] = { type = "Jewel", diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 6b9ecf04c11..23b434cc392 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -7892,6 +7892,8 @@ c["Bathed in the blood of 8000 sacrificed in the name of Doryani"]={{[1]={flags= c["Bathed in the blood of 8000 sacrificed in the name of Xibaqua"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="vaal"},id=8000}}}},nil} c["Bathed in the blood of 8000 sacrificed in the name of Zerphi"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=2,type="vaal"},id=8000}}}},nil} c["Battlemage"]={{[1]={flags=0,keywordFlags=0,name="Battlemage",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="MainHandWeaponDamageAppliesToSpells",type="MAX",value=100}},nil} +c["Binding 4050 souls to phylacteries to sustain Zorath"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_special"},id=4050}}}},nil} +c["Binding 8000 souls to phylacteries to sustain Zorath"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_special"},id=8000}}}},nil} c["Bitter Frost"]={{[1]={flags=0,keywordFlags=0,name="Keystone",type="LIST",value="Bitter Frost"},[2]={flags=0,keywordFlags=0,name="Condition:HaveBitterFrost",type="FLAG",value=true}},nil} c["Bleeding Enemies you Kill Explode, dealing 20% of"]={nil,"Bleeding Enemies you Kill Explode, dealing 20% of "} c["Bleeding Enemies you Kill Explode, dealing 20% of their Maximum Life as Physical Damage"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="Bleeding"},flags=0,keywordFlags=0,name="ExplodeMod",type="LIST",value={amount=20,chance=1,keyOfScaledMod="chance",type="Physical"}},[2]={flags=0,keywordFlags=0,name="CanExplode",type="FLAG",value=true}},nil} @@ -10525,6 +10527,7 @@ c["Passive Skills in radius of Voracious Flame can be allocated without being co c["Passive Skills in radius of Wicked Ward can be allocated without being connected to your tree"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="impossibleEscapeKeystone",value="wicked ward"}},[2]={flags=0,keywordFlags=0,name="ImpossibleEscapeKeystones",type="LIST",value={key="wicked ward",value=true}}},nil} c["Passive Skills in radius of Wind Dancer can be allocated without being connected to your tree"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="impossibleEscapeKeystone",value="wind dancer"}},[2]={flags=0,keywordFlags=0,name="ImpossibleEscapeKeystones",type="LIST",value={key="wind dancer",value=true}}},nil} c["Passive Skills in radius of Zealot's Oath can be allocated without being connected to your tree"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="impossibleEscapeKeystone",value="zealot's oath"}},[2]={flags=0,keywordFlags=0,name="ImpossibleEscapeKeystones",type="LIST",value={key="zealot's oath",value=true}}},nil} +c["Passives affected are Conquered by the Abyssal"]={{},nil} c["Passives granting Cold Resistance or all Elemental Resistances in Radius"]={nil,"Passives granting Cold Resistance or all Elemental Resistances in Radius "} c["Passives granting Cold Resistance or all Elemental Resistances in Radius also grant an equal chance to gain a Frenzy Charge on Kill"]={nil,"Passives granting Cold Resistance or all Elemental Resistances in Radius also grant an equal chance to gain a Frenzy Charge on Kill "} c["Passives granting Fire Resistance or all Elemental Resistances in Radius"]={nil,"Passives granting Fire Resistance or all Elemental Resistances in Radius "} @@ -12186,6 +12189,14 @@ c["Strike Skills which target additional Enemies can do so from 40% further away c["Stun Threshold is based on 500% of your Mana instead of Life"]={{[1]={flags=0,keywordFlags=0,name="StunThresholdBasedOnManaInsteadOfLife",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="StunThresholdManaPercent",type="BASE",value=500}},nil} c["Stun Threshold is based on Energy Shield instead of Life"]={{[1]={flags=0,keywordFlags=0,name="StunThresholdBasedOnEnergyShieldInsteadOfLife",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="StunThresholdEnergyShieldPercent",type="BASE",value=100}},nil} c["Stuns from Critical Strikes have 100% increased Duration"]={{[1]={flags=0,keywordFlags=0,name="EnemyStunDurationOnCrit",type="INC",value=100}},nil} +c["Subjugating 4050 souls in the thrall of Amanamu"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_ghastly"},id=4050}}}},nil} +c["Subjugating 4050 souls in the thrall of Kurgal"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_hypnotic"},id=4050}}}},nil} +c["Subjugating 4050 souls in the thrall of Tecrod"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_murderous"},id=4050}}}},nil} +c["Subjugating 4050 souls in the thrall of Ulaman"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_searching"},id=4050}}}},nil} +c["Subjugating 8000 souls in the thrall of Amanamu"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_ghastly"},id=8000}}}},nil} +c["Subjugating 8000 souls in the thrall of Kurgal"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_hypnotic"},id=8000}}}},nil} +c["Subjugating 8000 souls in the thrall of Tecrod"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_murderous"},id=8000}}}},nil} +c["Subjugating 8000 souls in the thrall of Ulaman"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id=1,type="abyss_searching"},id=8000}}}},nil} c["Summon 2 additional Skeletons with Summon Skeletons"]={nil,"Summon 2 additional Skeletons with Summon Skeletons "} c["Summon 2 additional Skeletons with Summon Skeletons Summon 4 additional Skeletons with Summon Skeletons"]={nil,"Summon 2 additional Skeletons with Summon Skeletons Summon 4 additional Skeletons with Summon Skeletons "} c["Summon 4 additional Skeletons with Summon Skeletons"]={nil,"Summon 4 additional Skeletons with Summon Skeletons "} diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip b/src/Data/TimelessJewelData/AbyssAmanamu.zip deleted file mode 100644 index 8e60de73e12..00000000000 Binary files a/src/Data/TimelessJewelData/AbyssAmanamu.zip and /dev/null differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part0 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part0 new file mode 100644 index 00000000000..5764c3cd49a Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part0 differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part1 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part1 new file mode 100644 index 00000000000..5b2604e0498 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part1 differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part2 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part2 new file mode 100644 index 00000000000..3c9fd1a4235 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part2 differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part3 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part3 new file mode 100644 index 00000000000..fa46b451ecc Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part3 differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part4 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part4 new file mode 100644 index 00000000000..eb95d6a6c42 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part4 differ diff --git a/src/Data/TimelessJewelData/AbyssAmanamu.zip.part5 b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part5 new file mode 100644 index 00000000000..e3ac91fdb45 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssAmanamu.zip.part5 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip b/src/Data/TimelessJewelData/AbyssKurgal.zip deleted file mode 100644 index f0e518cb916..00000000000 Binary files a/src/Data/TimelessJewelData/AbyssKurgal.zip and /dev/null differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part0 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part0 new file mode 100644 index 00000000000..04d5e737bb6 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part0 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part1 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part1 new file mode 100644 index 00000000000..e3e576e9493 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part1 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part2 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part2 new file mode 100644 index 00000000000..f84453bee28 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part2 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part3 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part3 new file mode 100644 index 00000000000..9acf12ac26d Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part3 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part4 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part4 new file mode 100644 index 00000000000..995ae789558 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part4 differ diff --git a/src/Data/TimelessJewelData/AbyssKurgal.zip.part5 b/src/Data/TimelessJewelData/AbyssKurgal.zip.part5 new file mode 100644 index 00000000000..27aa9b63961 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssKurgal.zip.part5 differ diff --git a/src/Data/TimelessJewelData/AbyssNotableNames.lua b/src/Data/TimelessJewelData/AbyssNotableNames.lua new file mode 100644 index 00000000000..2ef60fd7eb2 --- /dev/null +++ b/src/Data/TimelessJewelData/AbyssNotableNames.lua @@ -0,0 +1,85 @@ +-- Manual display names for Abyss timeless jewel notables. +-- The game only calls them "Notable N". Using the stat ID as the key lets the +-- same effect keep the same name on every jewel type. +return { + ["accuracy_rating"] = "Accuracy", + ["additional_all_attributes"] = "All Attributes", + ["additional_dexterity"] = "Dexterity", + ["additional_intelligence"] = "Intelligence", + ["additional_physical_damage_reduction_%_if_not_damaged_by_a_hit_recently"] = "Physical Damage Reduction if not Hit Recently", + ["additional_strength"] = "Strength", + ["attack_speed_+%"] = "Attack Speed", + ["attack_speed_+%_if_have_crit_recently"] = "Attack Speed if Crit Recently", + ["attacks_chance_to_blind_on_hit_%"] = "Blind chance with Attacks", + ["attacks_chance_to_taunt_on_hit_%"] = "Taunt chance with Attacks", + ["base_avoid_bleed_%"] = "Bleed Avoid", + ["base_avoid_freeze_%"] = "Freeze Avoid", + ["base_avoid_ignite_%"] = "Ignite Avoid", + ["base_avoid_poison_%"] = "Poison Avoid", + ["base_avoid_shock_%"] = "Shock Avoid", + ["base_avoid_stun_%"] = "Stun Avoid", + ["base_cast_speed_+%"] = "Cast Speed", + ["base_chaos_damage_resistance_%"] = "Chaos Resistance", + ["base_cold_damage_resistance_%"] = "Cold Resistance", + ["base_cooldown_speed_+%"] = "Cooldown Recovery Rate", + ["base_critical_strike_multiplier_+"] = "Critical Strike Multiplier", + ["base_evasion_rating"] = "Evasion", + ["base_fire_damage_resistance_%"] = "Fire Resistance", + ["base_life_regeneration_rate_per_minute"] = "Life Regeneration", + ["base_lightning_damage_resistance_%"] = "Lightning Resistance", + ["base_mana_regeneration_rate_per_minute"] = "Mana Regeneration", + ["base_maximum_life"] = "Life", + ["base_maximum_mana"] = "Mana", + ["base_physical_damage_reduction_rating"] = "Armour", + ["base_resist_all_elements_%"] = "Elemental Resistances", + ["block_chance_on_damage_taken_%"] = "Attack Block if Hit Recently", + ["cast_speed_+%_if_have_crit_recently"] = "Cast Speed if Crit Recently", + ["cast_speed_+%_if_player_minion_has_been_killed_recently"] = "Cast Speed if a Minion was Killed Recently", + ["chance_to_block_spells_%_if_damaged_by_a_hit_recently"] = "Spell Block if Hit Recently", + ["cold_ailment_effect_+%"] = "Cold Ailment Effect", + ["critical_strike_chance_+%"] = "Critical Strike Chance", + ["critical_strike_chance_+%_if_have_not_crit_recently"] = "Critical Strike Chance if Not Crit Recently", + ["critical_strike_multiplier_+_if_enemy_killed_recently"] = "Critical Strike Multiplier if Killed Recently", + ["curse_effect_+%"] = "Curse Effect", + ["curse_effect_on_self_+%_while_on_consecrated_ground"] = "Reduced Curse Effect on Consecrated Ground", + ["damage_+%_if_enemy_killed_recently"] = "Damage if Killed Recently", + ["damage_over_time_+%_while_dual_wielding"] = "Damage over Time while Dual Wielding", + ["damage_over_time_+%_while_holding_a_shield"] = "Damage over Time while Holding a Shield", + ["damage_over_time_+%_while_wielding_two_handed_weapon"] = "Damage over Time with a Two Handed Weapon", + ["damage_penetrates_%_elemental_resistance_if_enemy_not_killed_recently"] = "Elemental Penetration if Not Killed Recently", + ["damage_taken_goes_to_mana_%"] = "Damage Recoup as Mana", + ["enemies_you_blind_have_critical_strike_chance_+%"] = "Reduced Critical Strike Chance for Blinded Enemies", + ["enemies_you_hinder_have_life_regeneration_rate_+%"] = "Reduced Life Regeneration for Hindered Enemies", + ["enemies_you_intimidate_have_stun_duration_on_self_+%"] = "Stun Duration against Intimidated Enemies", + ["enemies_you_maim_have_damage_taken_over_time_+%"] = "Damage over Time Taken by Maimed Enemies", + ["enemies_you_wither_have_all_resistances_%"] = "Reduced Resistances for Withered Enemies", + ["energy_shield_regeneration_rate_per_second"] = "Energy Shield Regeneration", + ["evasion_rating_+%_while_moving"] = "Evasion Rating while Moving", + ["impale_debuff_effect_+%"] = "Impale Effect", + ["life_regeneration_rate_per_minute_%_while_moving"] = "Life Regeneration while Moving", + ["lightning_ailment_effect_+%"] = "Lightning Ailment Effect", + ["mana_regeneration_rate_+%_while_moving"] = "Mana Regeneration while Moving", + ["minion_attack_and_cast_speed_+%_if_you_or_minions_have_killed_enemy_recently"] = "Minion Attack and Cast Speed if Killed Recently", + ["minion_attacks_chance_to_blind_on_hit_%"] = "Minion Blind Chance", + ["minion_attacks_chance_to_taunt_on_hit_%"] = "Minion Taunt Chance", + ["minion_bleed_on_hit_with_attacks_%"] = "Minion Bleed Chance", + ["minion_chance_to_ignite_%"] = "Minion Ignite Chance", + ["minion_chaos_resistance_%"] = "Minion Chaos Resistance", + ["minion_damage_+%_if_have_used_a_minion_skill_recently"] = "Minion Damage if a Minion Skill was Used Recently", + ["minion_elemental_resistance_%"] = "Minion Elemental Resistances", + ["minion_global_maximum_added_chaos_damage"] = "Added Minion Chaos Damage", + ["minion_global_maximum_added_cold_damage"] = "Added Minion Cold Damage", + ["minion_global_maximum_added_fire_damage"] = "Added Minion Fire Damage", + ["minion_global_maximum_added_lightning_damage"] = "Added Minion Lightning Damage", + ["minion_global_maximum_added_physical_damage"] = "Added Minion Physical Damage", + ["minion_life_leech_from_any_damage_permyriad"] = "Minion Life Leech", + ["minion_life_regeneration_rate_per_minute_%"] = "Minion Life Regeneration", + ["minion_life_regeneration_rate_per_second"] = "Minion Life Regeneration", + ["minion_maximum_life_+%"] = "Minion Life", + ["minion_movement_speed_+%"] = "Minion Movement Speed", + ["minion_spells_chance_to_hinder_on_hit_%"] = "Minion Hinder Chance", + ["minions_chance_to_poison_on_hit_%"] = "Minion Poison Chance", + ["movement_speed_+%_if_have_not_taken_damage_recently"] = "Movement Speed if Not Hit Recently", + ["phasing_for_4_seconds_on_kill_%"] = "Phasing on Kill", + ["spells_chance_to_hinder_on_hit_%"] = "Hinder chance with Spells", +} diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip b/src/Data/TimelessJewelData/AbyssTecrod.zip deleted file mode 100644 index 6425f12951e..00000000000 Binary files a/src/Data/TimelessJewelData/AbyssTecrod.zip and /dev/null differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part0 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part0 new file mode 100644 index 00000000000..4eec30134ff Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part0 differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part1 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part1 new file mode 100644 index 00000000000..72fc1729881 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part1 differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part2 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part2 new file mode 100644 index 00000000000..105d606fac4 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part2 differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part3 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part3 new file mode 100644 index 00000000000..89297e117a4 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part3 differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part4 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part4 new file mode 100644 index 00000000000..f87eccafedb Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part4 differ diff --git a/src/Data/TimelessJewelData/AbyssTecrod.zip.part5 b/src/Data/TimelessJewelData/AbyssTecrod.zip.part5 new file mode 100644 index 00000000000..e308d236419 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssTecrod.zip.part5 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip b/src/Data/TimelessJewelData/AbyssUlaman.zip deleted file mode 100644 index f141c8e83bd..00000000000 Binary files a/src/Data/TimelessJewelData/AbyssUlaman.zip and /dev/null differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part0 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part0 new file mode 100644 index 00000000000..4ea69b71d9c Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part0 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part1 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part1 new file mode 100644 index 00000000000..a11c0dcf1db Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part1 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part2 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part2 new file mode 100644 index 00000000000..98a8042b894 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part2 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part3 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part3 new file mode 100644 index 00000000000..58671594483 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part3 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part4 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part4 new file mode 100644 index 00000000000..0e1f3d7eeaa Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part4 differ diff --git a/src/Data/TimelessJewelData/AbyssUlaman.zip.part5 b/src/Data/TimelessJewelData/AbyssUlaman.zip.part5 new file mode 100644 index 00000000000..628d2a80db6 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssUlaman.zip.part5 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip b/src/Data/TimelessJewelData/AbyssZorath.zip deleted file mode 100644 index 963b1e53298..00000000000 Binary files a/src/Data/TimelessJewelData/AbyssZorath.zip and /dev/null differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part0 b/src/Data/TimelessJewelData/AbyssZorath.zip.part0 new file mode 100644 index 00000000000..e2fa752eecd Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part0 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part1 b/src/Data/TimelessJewelData/AbyssZorath.zip.part1 new file mode 100644 index 00000000000..a3d637b7e22 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part1 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part2 b/src/Data/TimelessJewelData/AbyssZorath.zip.part2 new file mode 100644 index 00000000000..e61b509a040 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part2 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part3 b/src/Data/TimelessJewelData/AbyssZorath.zip.part3 new file mode 100644 index 00000000000..82ad530aa24 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part3 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part4 b/src/Data/TimelessJewelData/AbyssZorath.zip.part4 new file mode 100644 index 00000000000..e21af76425d Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part4 differ diff --git a/src/Data/TimelessJewelData/AbyssZorath.zip.part5 b/src/Data/TimelessJewelData/AbyssZorath.zip.part5 new file mode 100644 index 00000000000..e0d9a667198 Binary files /dev/null and b/src/Data/TimelessJewelData/AbyssZorath.zip.part5 differ diff --git a/src/Data/TimelessJewelData/LegionPassives.lua b/src/Data/TimelessJewelData/LegionPassives.lua index 2a6934fb569..5ba6dbe7b24 100644 --- a/src/Data/TimelessJewelData/LegionPassives.lua +++ b/src/Data/TimelessJewelData/LegionPassives.lua @@ -2154,12 +2154,11 @@ return { ["dn"] = "Notable 18", ["id"] = "abyss_murderous_notable_18", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Dagger Attacks", - [2] = "0 to (7-8) Added Physical Damage with Dagger Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Dagger Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_daggers", - [2] = "attack_minimum_added_physical_damage_with_daggers", + [1] = "attack_minimum_added_physical_damage_with_daggers", + [2] = "attack_maximum_added_physical_damage_with_daggers", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_daggers"] = { @@ -2182,12 +2181,11 @@ return { ["dn"] = "Notable 19", ["id"] = "abyss_murderous_notable_19", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Claw Attacks", - [2] = "0 to (7-8) Added Physical Damage with Claw Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Claw Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_claws", - [2] = "attack_minimum_added_physical_damage_with_claws", + [1] = "attack_minimum_added_physical_damage_with_claws", + [2] = "attack_maximum_added_physical_damage_with_claws", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_claws"] = { @@ -2210,12 +2208,11 @@ return { ["dn"] = "Notable 20", ["id"] = "abyss_murderous_notable_20", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Sword Attacks", - [2] = "0 to (7-8) Added Physical Damage with Sword Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Sword Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_swords", - [2] = "attack_minimum_added_physical_damage_with_swords", + [1] = "attack_minimum_added_physical_damage_with_swords", + [2] = "attack_maximum_added_physical_damage_with_swords", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_swords"] = { @@ -2238,12 +2235,11 @@ return { ["dn"] = "Notable 21", ["id"] = "abyss_murderous_notable_21", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Axe Attacks", - [2] = "0 to (7-8) Added Physical Damage with Axe Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Axe Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_axes", - [2] = "attack_minimum_added_physical_damage_with_axes", + [1] = "attack_minimum_added_physical_damage_with_axes", + [2] = "attack_maximum_added_physical_damage_with_axes", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_axes"] = { @@ -2266,12 +2262,11 @@ return { ["dn"] = "Notable 22", ["id"] = "abyss_murderous_notable_22", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Mace or Sceptre Attacks", - [2] = "0 to (7-8) Added Physical Damage with Mace or Sceptre Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Mace or Sceptre Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_maces", - [2] = "attack_minimum_added_physical_damage_with_maces", + [1] = "attack_minimum_added_physical_damage_with_maces", + [2] = "attack_maximum_added_physical_damage_with_maces", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_maces"] = { @@ -2294,12 +2289,11 @@ return { ["dn"] = "Notable 23", ["id"] = "abyss_murderous_notable_23", ["sd"] = { - [1] = "(5-6) to 0 Added Physical Damage with Staff Attacks", - [2] = "0 to (7-8) Added Physical Damage with Staff Attacks", + [1] = "(5-6) to (7-8) Added Physical Damage with Staff Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_staves", - [2] = "attack_minimum_added_physical_damage_with_staves", + [1] = "attack_minimum_added_physical_damage_with_staves", + [2] = "attack_maximum_added_physical_damage_with_staves", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_staves"] = { @@ -2322,12 +2316,11 @@ return { ["dn"] = "Notable 24", ["id"] = "abyss_murderous_notable_24", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Dagger Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Dagger Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Dagger Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_daggers", - [2] = "attack_minimum_added_lightning_damage_with_daggers", + [1] = "attack_minimum_added_lightning_damage_with_daggers", + [2] = "attack_maximum_added_lightning_damage_with_daggers", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_daggers"] = { @@ -2350,12 +2343,11 @@ return { ["dn"] = "Notable 25", ["id"] = "abyss_murderous_notable_25", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Claw Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Claw Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Claw Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_claws", - [2] = "attack_minimum_added_lightning_damage_with_claws", + [1] = "attack_minimum_added_lightning_damage_with_claws", + [2] = "attack_maximum_added_lightning_damage_with_claws", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_claws"] = { @@ -2378,12 +2370,11 @@ return { ["dn"] = "Notable 26", ["id"] = "abyss_murderous_notable_26", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Sword Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Sword Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Sword Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_swords", - [2] = "attack_minimum_added_lightning_damage_with_swords", + [1] = "attack_minimum_added_lightning_damage_with_swords", + [2] = "attack_maximum_added_lightning_damage_with_swords", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_swords"] = { @@ -2406,12 +2397,11 @@ return { ["dn"] = "Notable 27", ["id"] = "abyss_murderous_notable_27", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Axe Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Axe Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Axe Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_axes", - [2] = "attack_minimum_added_lightning_damage_with_axes", + [1] = "attack_minimum_added_lightning_damage_with_axes", + [2] = "attack_maximum_added_lightning_damage_with_axes", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_axes"] = { @@ -2434,12 +2424,11 @@ return { ["dn"] = "Notable 28", ["id"] = "abyss_murderous_notable_28", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Mace or Sceptre Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Mace or Sceptre Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Mace or Sceptre Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_maces", - [2] = "attack_minimum_added_lightning_damage_with_maces", + [1] = "attack_minimum_added_lightning_damage_with_maces", + [2] = "attack_maximum_added_lightning_damage_with_maces", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_maces"] = { @@ -2462,12 +2451,11 @@ return { ["dn"] = "Notable 29", ["id"] = "abyss_murderous_notable_29", ["sd"] = { - [1] = "(1-4) to 0 Added Lightning Damage with Staff Attacks", - [2] = "0 to (33-35) Added Lightning Damage with Staff Attacks", + [1] = "(1-4) to (33-35) Added Lightning Damage with Staff Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_staves", - [2] = "attack_minimum_added_lightning_damage_with_staves", + [1] = "attack_minimum_added_lightning_damage_with_staves", + [2] = "attack_maximum_added_lightning_damage_with_staves", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_staves"] = { @@ -2490,12 +2478,11 @@ return { ["dn"] = "Notable 30", ["id"] = "abyss_murderous_notable_30", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Dagger Attacks", - [2] = "0 to (20-22) Added Fire Damage with Dagger Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Dagger Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_daggers", - [2] = "attack_minimum_added_fire_damage_with_daggers", + [1] = "attack_minimum_added_fire_damage_with_daggers", + [2] = "attack_maximum_added_fire_damage_with_daggers", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_daggers"] = { @@ -2518,12 +2505,11 @@ return { ["dn"] = "Notable 31", ["id"] = "abyss_murderous_notable_31", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Claw Attacks", - [2] = "0 to (20-22) Added Fire Damage with Claw Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Claw Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_claws", - [2] = "attack_minimum_added_fire_damage_with_claws", + [1] = "attack_minimum_added_fire_damage_with_claws", + [2] = "attack_maximum_added_fire_damage_with_claws", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_claws"] = { @@ -2546,12 +2532,11 @@ return { ["dn"] = "Notable 32", ["id"] = "abyss_murderous_notable_32", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Sword Attacks", - [2] = "0 to (20-22) Added Fire Damage with Sword Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Sword Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_swords", - [2] = "attack_minimum_added_fire_damage_with_swords", + [1] = "attack_minimum_added_fire_damage_with_swords", + [2] = "attack_maximum_added_fire_damage_with_swords", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_swords"] = { @@ -2574,12 +2559,11 @@ return { ["dn"] = "Notable 33", ["id"] = "abyss_murderous_notable_33", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Axe Attacks", - [2] = "0 to (20-22) Added Fire Damage with Axe Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Axe Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_axes", - [2] = "attack_minimum_added_fire_damage_with_axes", + [1] = "attack_minimum_added_fire_damage_with_axes", + [2] = "attack_maximum_added_fire_damage_with_axes", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_axes"] = { @@ -2602,12 +2586,11 @@ return { ["dn"] = "Notable 34", ["id"] = "abyss_murderous_notable_34", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Mace or Sceptre Attacks", - [2] = "0 to (20-22) Added Fire Damage with Mace or Sceptre Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Mace or Sceptre Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_maces", - [2] = "attack_minimum_added_fire_damage_with_maces", + [1] = "attack_minimum_added_fire_damage_with_maces", + [2] = "attack_maximum_added_fire_damage_with_maces", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_maces"] = { @@ -2630,12 +2613,11 @@ return { ["dn"] = "Notable 35", ["id"] = "abyss_murderous_notable_35", ["sd"] = { - [1] = "(12-13) to 0 Added Fire Damage with Staff Attacks", - [2] = "0 to (20-22) Added Fire Damage with Staff Attacks", + [1] = "(12-13) to (20-22) Added Fire Damage with Staff Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_staves", - [2] = "attack_minimum_added_fire_damage_with_staves", + [1] = "attack_minimum_added_fire_damage_with_staves", + [2] = "attack_maximum_added_fire_damage_with_staves", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_staves"] = { @@ -2658,12 +2640,11 @@ return { ["dn"] = "Notable 36", ["id"] = "abyss_murderous_notable_36", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Dagger Attacks", - [2] = "0 to (17-20) Added Cold Damage with Dagger Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Dagger Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_daggers", - [2] = "attack_minimum_added_cold_damage_with_daggers", + [1] = "attack_minimum_added_cold_damage_with_daggers", + [2] = "attack_maximum_added_cold_damage_with_daggers", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_daggers"] = { @@ -2686,12 +2667,11 @@ return { ["dn"] = "Notable 37", ["id"] = "abyss_murderous_notable_37", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Claw Attacks", - [2] = "0 to (17-20) Added Cold Damage with Claw Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Claw Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_claws", - [2] = "attack_minimum_added_cold_damage_with_claws", + [1] = "attack_minimum_added_cold_damage_with_claws", + [2] = "attack_maximum_added_cold_damage_with_claws", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_claws"] = { @@ -2714,12 +2694,11 @@ return { ["dn"] = "Notable 38", ["id"] = "abyss_murderous_notable_38", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Sword Attacks", - [2] = "0 to (17-20) Added Cold Damage with Sword Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Sword Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_swords", - [2] = "attack_minimum_added_cold_damage_with_swords", + [1] = "attack_minimum_added_cold_damage_with_swords", + [2] = "attack_maximum_added_cold_damage_with_swords", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_swords"] = { @@ -2742,12 +2721,11 @@ return { ["dn"] = "Notable 39", ["id"] = "abyss_murderous_notable_39", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Axe Attacks", - [2] = "0 to (17-20) Added Cold Damage with Axe Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Axe Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_axes", - [2] = "attack_minimum_added_cold_damage_with_axes", + [1] = "attack_minimum_added_cold_damage_with_axes", + [2] = "attack_maximum_added_cold_damage_with_axes", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_axes"] = { @@ -2770,12 +2748,11 @@ return { ["dn"] = "Notable 40", ["id"] = "abyss_murderous_notable_40", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Mace or Sceptre Attacks", - [2] = "0 to (17-20) Added Cold Damage with Mace or Sceptre Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Mace or Sceptre Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_maces", - [2] = "attack_minimum_added_cold_damage_with_maces", + [1] = "attack_minimum_added_cold_damage_with_maces", + [2] = "attack_maximum_added_cold_damage_with_maces", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_maces"] = { @@ -2798,12 +2775,11 @@ return { ["dn"] = "Notable 41", ["id"] = "abyss_murderous_notable_41", ["sd"] = { - [1] = "(10-11) to 0 Added Cold Damage with Staff Attacks", - [2] = "0 to (17-20) Added Cold Damage with Staff Attacks", + [1] = "(10-11) to (17-20) Added Cold Damage with Staff Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_staves", - [2] = "attack_minimum_added_cold_damage_with_staves", + [1] = "attack_minimum_added_cold_damage_with_staves", + [2] = "attack_maximum_added_cold_damage_with_staves", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_staves"] = { @@ -2826,12 +2802,11 @@ return { ["dn"] = "Notable 42", ["id"] = "abyss_murderous_notable_42", ["sd"] = { - [1] = "(10-11) to 0 Added Chaos Damage with Dagger Attacks", - [2] = "0 to (17-20) Added Chaos Damage with Dagger Attacks", + [1] = "(10-11) to (17-20) Added Chaos Damage with Dagger Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_chaos_damage_with_daggers", - [2] = "attack_minimum_added_chaos_damage_with_daggers", + [1] = "attack_minimum_added_chaos_damage_with_daggers", + [2] = "attack_maximum_added_chaos_damage_with_daggers", }, ["stats"] = { ["attack_maximum_added_chaos_damage_with_daggers"] = { @@ -2854,12 +2829,11 @@ return { ["dn"] = "Notable 43", ["id"] = "abyss_murderous_notable_43", ["sd"] = { - [1] = "(10-11) to 0 Added Chaos Damage with Claw Attacks", - [2] = "0 to (17-20) Added Chaos Damage with Claw Attacks", + [1] = "(10-11) to (17-20) Added Chaos Damage with Claw Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_chaos_damage_with_claws", - [2] = "attack_minimum_added_chaos_damage_with_claws", + [1] = "attack_minimum_added_chaos_damage_with_claws", + [2] = "attack_maximum_added_chaos_damage_with_claws", }, ["stats"] = { ["attack_maximum_added_chaos_damage_with_claws"] = { @@ -2901,12 +2875,11 @@ return { ["dn"] = "Notable 45", ["id"] = "abyss_murderous_notable_45", ["sd"] = { - [1] = "Adds (6-7) to 0 Fire Damage to Attacks", - [2] = "Adds 0 to (13-16) Fire Damage to Attacks", + [1] = "Adds (6-7) to (13-16) Fire Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage", - [2] = "attack_minimum_added_fire_damage", + [1] = "attack_minimum_added_fire_damage", + [2] = "attack_maximum_added_fire_damage", }, ["stats"] = { ["attack_maximum_added_fire_damage"] = { @@ -2929,12 +2902,11 @@ return { ["dn"] = "Notable 46", ["id"] = "abyss_murderous_notable_46", ["sd"] = { - [1] = "Adds (6-7) to 0 Cold Damage to Attacks", - [2] = "Adds 0 to (11-13) Cold Damage to Attacks", + [1] = "Adds (6-7) to (11-13) Cold Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage", - [2] = "attack_minimum_added_cold_damage", + [1] = "attack_minimum_added_cold_damage", + [2] = "attack_maximum_added_cold_damage", }, ["stats"] = { ["attack_maximum_added_cold_damage"] = { @@ -2957,12 +2929,11 @@ return { ["dn"] = "Notable 47", ["id"] = "abyss_murderous_notable_47", ["sd"] = { - [1] = "Adds (1-2) to 0 Lightning Damage to Attacks", - [2] = "Adds 0 to (25-27) Lightning Damage to Attacks", + [1] = "Adds (1-2) to (25-27) Lightning Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage", - [2] = "attack_minimum_added_lightning_damage", + [1] = "attack_minimum_added_lightning_damage", + [2] = "attack_maximum_added_lightning_damage", }, ["stats"] = { ["attack_maximum_added_lightning_damage"] = { @@ -2985,12 +2956,11 @@ return { ["dn"] = "Notable 48", ["id"] = "abyss_murderous_notable_48", ["sd"] = { - [1] = "Adds (2-3) to 0 Physical Damage to Attacks", - [2] = "Adds 0 to (4-5) Physical Damage to Attacks", + [1] = "Adds (2-3) to (4-5) Physical Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage", - [2] = "attack_minimum_added_physical_damage", + [1] = "attack_minimum_added_physical_damage", + [2] = "attack_maximum_added_physical_damage", }, ["stats"] = { ["attack_maximum_added_physical_damage"] = { @@ -3013,12 +2983,11 @@ return { ["dn"] = "Notable 49", ["id"] = "abyss_murderous_notable_49", ["sd"] = { - [1] = "Adds (8-9) to 0 Chaos Damage to Attacks", - [2] = "Adds 0 to (14-17) Chaos Damage to Attacks", + [1] = "Adds (8-9) to (14-17) Chaos Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_chaos_damage", - [2] = "attack_minimum_added_chaos_damage", + [1] = "attack_minimum_added_chaos_damage", + [2] = "attack_maximum_added_chaos_damage", }, ["stats"] = { ["attack_maximum_added_chaos_damage"] = { @@ -3592,12 +3561,11 @@ return { ["dn"] = "Notable 27", ["id"] = "abyss_searching_notable_27", ["sd"] = { - [1] = "Adds (6-7) to 0 Fire Damage to Attacks", - [2] = "Adds 0 to (13-16) Fire Damage to Attacks", + [1] = "Adds (6-7) to (13-16) Fire Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage", - [2] = "attack_minimum_added_fire_damage", + [1] = "attack_minimum_added_fire_damage", + [2] = "attack_maximum_added_fire_damage", }, ["stats"] = { ["attack_maximum_added_fire_damage"] = { @@ -3620,12 +3588,11 @@ return { ["dn"] = "Notable 28", ["id"] = "abyss_searching_notable_28", ["sd"] = { - [1] = "Adds (6-7) to 0 Cold Damage to Attacks", - [2] = "Adds 0 to (11-13) Cold Damage to Attacks", + [1] = "Adds (6-7) to (11-13) Cold Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage", - [2] = "attack_minimum_added_cold_damage", + [1] = "attack_minimum_added_cold_damage", + [2] = "attack_maximum_added_cold_damage", }, ["stats"] = { ["attack_maximum_added_cold_damage"] = { @@ -3648,12 +3615,11 @@ return { ["dn"] = "Notable 29", ["id"] = "abyss_searching_notable_29", ["sd"] = { - [1] = "Adds (1-2) to 0 Lightning Damage to Attacks", - [2] = "Adds 0 to (25-27) Lightning Damage to Attacks", + [1] = "Adds (1-2) to (25-27) Lightning Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage", - [2] = "attack_minimum_added_lightning_damage", + [1] = "attack_minimum_added_lightning_damage", + [2] = "attack_maximum_added_lightning_damage", }, ["stats"] = { ["attack_maximum_added_lightning_damage"] = { @@ -3676,12 +3642,11 @@ return { ["dn"] = "Notable 30", ["id"] = "abyss_searching_notable_30", ["sd"] = { - [1] = "Adds (2-3) to 0 Physical Damage to Attacks", - [2] = "Adds 0 to (4-5) Physical Damage to Attacks", + [1] = "Adds (2-3) to (4-5) Physical Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage", - [2] = "attack_minimum_added_physical_damage", + [1] = "attack_minimum_added_physical_damage", + [2] = "attack_maximum_added_physical_damage", }, ["stats"] = { ["attack_maximum_added_physical_damage"] = { @@ -3704,12 +3669,11 @@ return { ["dn"] = "Notable 31", ["id"] = "abyss_searching_notable_31", ["sd"] = { - [1] = "Adds (8-9) to 0 Chaos Damage to Attacks", - [2] = "Adds 0 to (14-17) Chaos Damage to Attacks", + [1] = "Adds (8-9) to (14-17) Chaos Damage to Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_chaos_damage", - [2] = "attack_minimum_added_chaos_damage", + [1] = "attack_minimum_added_chaos_damage", + [2] = "attack_maximum_added_chaos_damage", }, ["stats"] = { ["attack_maximum_added_chaos_damage"] = { @@ -3732,12 +3696,11 @@ return { ["dn"] = "Notable 32", ["id"] = "abyss_searching_notable_32", ["sd"] = { - [1] = "4 to 0 Added Physical Damage with Wand Attacks", - [2] = "0 to (5-6) Added Physical Damage with Wand Attacks", + [1] = "4 to (5-6) Added Physical Damage with Wand Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_wands", - [2] = "attack_minimum_added_physical_damage_with_wands", + [1] = "attack_minimum_added_physical_damage_with_wands", + [2] = "attack_maximum_added_physical_damage_with_wands", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_wands"] = { @@ -3760,12 +3723,11 @@ return { ["dn"] = "Notable 33", ["id"] = "abyss_searching_notable_33", ["sd"] = { - [1] = "4 to 0 Added Physical Damage with Bow Attacks", - [2] = "0 to (5-6) Added Physical Damage with Bow Attacks", + [1] = "4 to (5-6) Added Physical Damage with Bow Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_physical_damage_with_bow", - [2] = "attack_minimum_added_physical_damage_with_bow", + [1] = "attack_minimum_added_physical_damage_with_bow", + [2] = "attack_maximum_added_physical_damage_with_bow", }, ["stats"] = { ["attack_maximum_added_physical_damage_with_bow"] = { @@ -3788,12 +3750,11 @@ return { ["dn"] = "Notable 34", ["id"] = "abyss_searching_notable_34", ["sd"] = { - [1] = "(1-3) to 0 Added Lightning Damage with Wand Attacks", - [2] = "0 to (28-30) Added Lightning Damage with Wand Attacks", + [1] = "(1-3) to (28-30) Added Lightning Damage with Wand Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_wand", - [2] = "attack_minimum_added_lightning_damage_with_wand", + [1] = "attack_minimum_added_lightning_damage_with_wand", + [2] = "attack_maximum_added_lightning_damage_with_wand", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_wand"] = { @@ -3816,12 +3777,11 @@ return { ["dn"] = "Notable 35", ["id"] = "abyss_searching_notable_35", ["sd"] = { - [1] = "(1-3) to 0 Added Lightning Damage with Bow Attacks", - [2] = "0 to (28-30) Added Lightning Damage with Bow Attacks", + [1] = "(1-3) to (28-30) Added Lightning Damage with Bow Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_lightning_damage_with_bows", - [2] = "attack_minimum_added_lightning_damage_with_bows", + [1] = "attack_minimum_added_lightning_damage_with_bows", + [2] = "attack_maximum_added_lightning_damage_with_bows", }, ["stats"] = { ["attack_maximum_added_lightning_damage_with_bows"] = { @@ -3844,12 +3804,11 @@ return { ["dn"] = "Notable 36", ["id"] = "abyss_searching_notable_36", ["sd"] = { - [1] = "(9-11) to 0 Added Fire Damage with Wand Attacks", - [2] = "0 to (16-19) Added Fire Damage with Wand Attacks", + [1] = "(9-11) to (16-19) Added Fire Damage with Wand Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_wand", - [2] = "attack_minimum_added_fire_damage_with_wand", + [1] = "attack_minimum_added_fire_damage_with_wand", + [2] = "attack_maximum_added_fire_damage_with_wand", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_wand"] = { @@ -3872,12 +3831,11 @@ return { ["dn"] = "Notable 37", ["id"] = "abyss_searching_notable_37", ["sd"] = { - [1] = "(9-11) to 0 Added Fire Damage with Bow Attacks", - [2] = "0 to (16-19) Added Fire Damage with Bow Attacks", + [1] = "(9-11) to (16-19) Added Fire Damage with Bow Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_fire_damage_with_bow", - [2] = "attack_minimum_added_fire_damage_with_bow", + [1] = "attack_minimum_added_fire_damage_with_bow", + [2] = "attack_maximum_added_fire_damage_with_bow", }, ["stats"] = { ["attack_maximum_added_fire_damage_with_bow"] = { @@ -3900,12 +3858,11 @@ return { ["dn"] = "Notable 38", ["id"] = "abyss_searching_notable_38", ["sd"] = { - [1] = "(8-9) to 0 Added Cold Damage with Wand Attacks", - [2] = "0 to (14-16) Added Cold Damage with Wand Attacks", + [1] = "(8-9) to (14-16) Added Cold Damage with Wand Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_wand", - [2] = "attack_minimum_added_cold_damage_with_wand", + [1] = "attack_minimum_added_cold_damage_with_wand", + [2] = "attack_maximum_added_cold_damage_with_wand", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_wand"] = { @@ -3928,12 +3885,11 @@ return { ["dn"] = "Notable 39", ["id"] = "abyss_searching_notable_39", ["sd"] = { - [1] = "(8-9) to 0 Added Cold Damage with Bow Attacks", - [2] = "0 to (14-16) Added Cold Damage with Bow Attacks", + [1] = "(8-9) to (14-16) Added Cold Damage with Bow Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_cold_damage_with_bows", - [2] = "attack_minimum_added_cold_damage_with_bows", + [1] = "attack_minimum_added_cold_damage_with_bows", + [2] = "attack_maximum_added_cold_damage_with_bows", }, ["stats"] = { ["attack_maximum_added_cold_damage_with_bows"] = { @@ -3956,12 +3912,11 @@ return { ["dn"] = "Notable 40", ["id"] = "abyss_searching_notable_40", ["sd"] = { - [1] = "(8-9) to 0 Added Chaos Damage with Bow Attacks", - [2] = "0 to (14-16) Added Chaos Damage with Bow Attacks", + [1] = "(8-9) to (14-16) Added Chaos Damage with Bow Attacks", }, ["sortedStats"] = { - [1] = "attack_maximum_added_chaos_damage_with_bows", - [2] = "attack_minimum_added_chaos_damage_with_bows", + [1] = "attack_minimum_added_chaos_damage_with_bows", + [2] = "attack_maximum_added_chaos_damage_with_bows", }, ["stats"] = { ["attack_maximum_added_chaos_damage_with_bows"] = { @@ -4402,12 +4357,11 @@ return { ["dn"] = "Notable 19", ["id"] = "abyss_hypnotic_notable_19", ["sd"] = { - [1] = "(13-16) to 0 Added Spell Fire Damage while Dual Wielding", - [2] = "0 to (18-21) Added Spell Fire Damage while Dual Wielding", + [1] = "(13-16) to (18-21) Added Spell Fire Damage while Dual Wielding", }, ["sortedStats"] = { - [1] = "spell_maximum_added_fire_damage_while_dual_wielding", - [2] = "spell_minimum_added_fire_damage_while_dual_wielding", + [1] = "spell_minimum_added_fire_damage_while_dual_wielding", + [2] = "spell_maximum_added_fire_damage_while_dual_wielding", }, ["stats"] = { ["spell_maximum_added_fire_damage_while_dual_wielding"] = { @@ -4430,12 +4384,11 @@ return { ["dn"] = "Notable 20", ["id"] = "abyss_hypnotic_notable_20", ["sd"] = { - [1] = "(12-14) to 0 Added Spell Cold Damage while Dual Wielding", - [2] = "0 to (17-19) Added Spell Cold Damage while Dual Wielding", + [1] = "(12-14) to (17-19) Added Spell Cold Damage while Dual Wielding", }, ["sortedStats"] = { - [1] = "spell_maximum_added_cold_damage_while_dual_wielding", - [2] = "spell_minimum_added_cold_damage_while_dual_wielding", + [1] = "spell_minimum_added_cold_damage_while_dual_wielding", + [2] = "spell_maximum_added_cold_damage_while_dual_wielding", }, ["stats"] = { ["spell_maximum_added_cold_damage_while_dual_wielding"] = { @@ -4458,12 +4411,11 @@ return { ["dn"] = "Notable 21", ["id"] = "abyss_hypnotic_notable_21", ["sd"] = { - [1] = "(1-4) to 0 Added Spell Lightning Damage while Dual Wielding", - [2] = "0 to (29-34) Added Spell Lightning Damage while Dual Wielding", + [1] = "(1-4) to (29-34) Added Spell Lightning Damage while Dual Wielding", }, ["sortedStats"] = { - [1] = "spell_maximum_added_lightning_damage_while_dual_wielding", - [2] = "spell_minimum_added_lightning_damage_while_dual_wielding", + [1] = "spell_minimum_added_lightning_damage_while_dual_wielding", + [2] = "spell_maximum_added_lightning_damage_while_dual_wielding", }, ["stats"] = { ["spell_maximum_added_lightning_damage_while_dual_wielding"] = { @@ -4486,12 +4438,11 @@ return { ["dn"] = "Notable 22", ["id"] = "abyss_hypnotic_notable_22", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Physical Damage while Dual Wielding", - [2] = "0 to (14-17) Added Spell Physical Damage while Dual Wielding", + [1] = "(9-12) to (14-17) Added Spell Physical Damage while Dual Wielding", }, ["sortedStats"] = { - [1] = "spell_maximum_added_physical_damage_while_dual_wielding", - [2] = "spell_minimum_added_physical_damage_while_dual_wielding", + [1] = "spell_minimum_added_physical_damage_while_dual_wielding", + [2] = "spell_maximum_added_physical_damage_while_dual_wielding", }, ["stats"] = { ["spell_maximum_added_physical_damage_while_dual_wielding"] = { @@ -4514,12 +4465,11 @@ return { ["dn"] = "Notable 23", ["id"] = "abyss_hypnotic_notable_23", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Chaos Damage while Dual Wielding", - [2] = "0 to (14-17) Added Spell Chaos Damage while Dual Wielding", + [1] = "(9-12) to (14-17) Added Spell Chaos Damage while Dual Wielding", }, ["sortedStats"] = { - [1] = "spell_maximum_added_chaos_damage_while_dual_wielding", - [2] = "spell_minimum_added_chaos_damage_while_dual_wielding", + [1] = "spell_minimum_added_chaos_damage_while_dual_wielding", + [2] = "spell_maximum_added_chaos_damage_while_dual_wielding", }, ["stats"] = { ["spell_maximum_added_chaos_damage_while_dual_wielding"] = { @@ -4542,12 +4492,11 @@ return { ["dn"] = "Notable 24", ["id"] = "abyss_hypnotic_notable_24", ["sd"] = { - [1] = "(13-16) to 0 Added Spell Fire Damage while wielding a Two Handed Weapon", - [2] = "0 to (18-21) Added Spell Fire Damage while wielding a Two Handed Weapon", + [1] = "(13-16) to (18-21) Added Spell Fire Damage while wielding a Two Handed Weapon", }, ["sortedStats"] = { - [1] = "spell_maximum_added_fire_damage_while_wielding_two_handed_weapon", - [2] = "spell_minimum_added_fire_damage_while_wielding_two_handed_weapon", + [1] = "spell_minimum_added_fire_damage_while_wielding_two_handed_weapon", + [2] = "spell_maximum_added_fire_damage_while_wielding_two_handed_weapon", }, ["stats"] = { ["spell_maximum_added_fire_damage_while_wielding_two_handed_weapon"] = { @@ -4570,12 +4519,11 @@ return { ["dn"] = "Notable 25", ["id"] = "abyss_hypnotic_notable_25", ["sd"] = { - [1] = "(12-14) to 0 Added Spell Cold Damage while wielding a Two Handed Weapon", - [2] = "0 to (17-19) Added Spell Cold Damage while wielding a Two Handed Weapon", + [1] = "(12-14) to (17-19) Added Spell Cold Damage while wielding a Two Handed Weapon", }, ["sortedStats"] = { - [1] = "spell_maximum_added_cold_damage_while_wielding_two_handed_weapon", - [2] = "spell_minimum_added_cold_damage_while_wielding_two_handed_weapon", + [1] = "spell_minimum_added_cold_damage_while_wielding_two_handed_weapon", + [2] = "spell_maximum_added_cold_damage_while_wielding_two_handed_weapon", }, ["stats"] = { ["spell_maximum_added_cold_damage_while_wielding_two_handed_weapon"] = { @@ -4598,12 +4546,11 @@ return { ["dn"] = "Notable 26", ["id"] = "abyss_hypnotic_notable_26", ["sd"] = { - [1] = "(1-4) to 0 Added Spell Lightning Damage while wielding a Two Handed Weapon", - [2] = "0 to (29-34) Added Spell Lightning Damage while wielding a Two Handed Weapon", + [1] = "(1-4) to (29-34) Added Spell Lightning Damage while wielding a Two Handed Weapon", }, ["sortedStats"] = { - [1] = "spell_maximum_added_lightning_damage_while_wielding_two_handed_weapon", - [2] = "spell_minimum_added_lightning_damage_while_wielding_two_handed_weapon", + [1] = "spell_minimum_added_lightning_damage_while_wielding_two_handed_weapon", + [2] = "spell_maximum_added_lightning_damage_while_wielding_two_handed_weapon", }, ["stats"] = { ["spell_maximum_added_lightning_damage_while_wielding_two_handed_weapon"] = { @@ -4626,12 +4573,11 @@ return { ["dn"] = "Notable 27", ["id"] = "abyss_hypnotic_notable_27", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Physical Damage while wielding a Two Handed Weapon", - [2] = "0 to (14-17) Added Spell Physical Damage while wielding a Two Handed Weapon", + [1] = "(9-12) to (14-17) Added Spell Physical Damage while wielding a Two Handed Weapon", }, ["sortedStats"] = { - [1] = "spell_maximum_added_physical_damage_while_wielding_two_handed_weapon", - [2] = "spell_minimum_added_physical_damage_while_wielding_two_handed_weapon", + [1] = "spell_minimum_added_physical_damage_while_wielding_two_handed_weapon", + [2] = "spell_maximum_added_physical_damage_while_wielding_two_handed_weapon", }, ["stats"] = { ["spell_maximum_added_physical_damage_while_wielding_two_handed_weapon"] = { @@ -4654,12 +4600,11 @@ return { ["dn"] = "Notable 28", ["id"] = "abyss_hypnotic_notable_28", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Chaos Damage while wielding a Two Handed Weapon", - [2] = "0 to (14-17) Added Spell Chaos Damage while wielding a Two Handed Weapon", + [1] = "(9-12) to (14-17) Added Spell Chaos Damage while wielding a Two Handed Weapon", }, ["sortedStats"] = { - [1] = "spell_maximum_added_chaos_damage_while_wielding_two_handed_weapon", - [2] = "spell_minimum_added_chaos_damage_while_wielding_two_handed_weapon", + [1] = "spell_minimum_added_chaos_damage_while_wielding_two_handed_weapon", + [2] = "spell_maximum_added_chaos_damage_while_wielding_two_handed_weapon", }, ["stats"] = { ["spell_maximum_added_chaos_damage_while_wielding_two_handed_weapon"] = { @@ -4682,12 +4627,11 @@ return { ["dn"] = "Notable 29", ["id"] = "abyss_hypnotic_notable_29", ["sd"] = { - [1] = "(13-16) to 0 Added Spell Fire Damage while holding a Shield", - [2] = "0 to (18-21) Added Spell Fire Damage while holding a Shield", + [1] = "(13-16) to (18-21) Added Spell Fire Damage while holding a Shield", }, ["sortedStats"] = { - [1] = "spell_maximum_added_fire_damage_while_holding_a_shield", - [2] = "spell_minimum_added_fire_damage_while_holding_a_shield", + [1] = "spell_minimum_added_fire_damage_while_holding_a_shield", + [2] = "spell_maximum_added_fire_damage_while_holding_a_shield", }, ["stats"] = { ["spell_maximum_added_fire_damage_while_holding_a_shield"] = { @@ -4710,12 +4654,11 @@ return { ["dn"] = "Notable 30", ["id"] = "abyss_hypnotic_notable_30", ["sd"] = { - [1] = "(12-14) to 0 Added Spell Cold Damage while holding a Shield", - [2] = "0 to (17-19) Added Spell Cold Damage while holding a Shield", + [1] = "(12-14) to (17-19) Added Spell Cold Damage while holding a Shield", }, ["sortedStats"] = { - [1] = "spell_maximum_added_cold_damage_while_holding_a_shield", - [2] = "spell_minimum_added_cold_damage_while_holding_a_shield", + [1] = "spell_minimum_added_cold_damage_while_holding_a_shield", + [2] = "spell_maximum_added_cold_damage_while_holding_a_shield", }, ["stats"] = { ["spell_maximum_added_cold_damage_while_holding_a_shield"] = { @@ -4738,12 +4681,11 @@ return { ["dn"] = "Notable 31", ["id"] = "abyss_hypnotic_notable_31", ["sd"] = { - [1] = "(1-4) to 0 Added Spell Lightning Damage while holding a Shield", - [2] = "0 to (29-34) Added Spell Lightning Damage while holding a Shield", + [1] = "(1-4) to (29-34) Added Spell Lightning Damage while holding a Shield", }, ["sortedStats"] = { - [1] = "spell_maximum_added_lightning_damage_while_holding_a_shield", - [2] = "spell_minimum_added_lightning_damage_while_holding_a_shield", + [1] = "spell_minimum_added_lightning_damage_while_holding_a_shield", + [2] = "spell_maximum_added_lightning_damage_while_holding_a_shield", }, ["stats"] = { ["spell_maximum_added_lightning_damage_while_holding_a_shield"] = { @@ -4766,12 +4708,11 @@ return { ["dn"] = "Notable 32", ["id"] = "abyss_hypnotic_notable_32", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Physical Damage while holding a Shield", - [2] = "0 to (14-17) Added Spell Physical Damage while holding a Shield", + [1] = "(9-12) to (14-17) Added Spell Physical Damage while holding a Shield", }, ["sortedStats"] = { - [1] = "spell_maximum_added_physical_damage_while_holding_a_shield", - [2] = "spell_minimum_added_physical_damage_while_holding_a_shield", + [1] = "spell_minimum_added_physical_damage_while_holding_a_shield", + [2] = "spell_maximum_added_physical_damage_while_holding_a_shield", }, ["stats"] = { ["spell_maximum_added_physical_damage_while_holding_a_shield"] = { @@ -4794,12 +4735,11 @@ return { ["dn"] = "Notable 33", ["id"] = "abyss_hypnotic_notable_33", ["sd"] = { - [1] = "(9-12) to 0 Added Spell Chaos Damage while holding a Shield", - [2] = "0 to (14-17) Added Spell Chaos Damage while holding a Shield", + [1] = "(9-12) to (14-17) Added Spell Chaos Damage while holding a Shield", }, ["sortedStats"] = { - [1] = "spell_maximum_added_chaos_damage_while_holding_a_shield", - [2] = "spell_minimum_added_chaos_damage_while_holding_a_shield", + [1] = "spell_minimum_added_chaos_damage_while_holding_a_shield", + [2] = "spell_maximum_added_chaos_damage_while_holding_a_shield", }, ["stats"] = { ["spell_maximum_added_chaos_damage_while_holding_a_shield"] = { @@ -4822,12 +4762,11 @@ return { ["dn"] = "Notable 34", ["id"] = "abyss_hypnotic_notable_34", ["sd"] = { - [1] = "Adds (9-12) to 0 Fire Damage to Spells", - [2] = "Adds 0 to (14-17) Fire Damage to Spells", + [1] = "Adds (9-12) to (14-17) Fire Damage to Spells", }, ["sortedStats"] = { - [1] = "spell_maximum_added_fire_damage", - [2] = "spell_minimum_added_fire_damage", + [1] = "spell_minimum_added_fire_damage", + [2] = "spell_maximum_added_fire_damage", }, ["stats"] = { ["spell_maximum_added_fire_damage"] = { @@ -4850,12 +4789,11 @@ return { ["dn"] = "Notable 35", ["id"] = "abyss_hypnotic_notable_35", ["sd"] = { - [1] = "Adds (8-11) to 0 Cold Damage to Spells", - [2] = "Adds 0 to (13-16) Cold Damage to Spells", + [1] = "Adds (8-11) to (13-16) Cold Damage to Spells", }, ["sortedStats"] = { - [1] = "spell_maximum_added_cold_damage", - [2] = "spell_minimum_added_cold_damage", + [1] = "spell_minimum_added_cold_damage", + [2] = "spell_maximum_added_cold_damage", }, ["stats"] = { ["spell_maximum_added_cold_damage"] = { @@ -4878,12 +4816,11 @@ return { ["dn"] = "Notable 36", ["id"] = "abyss_hypnotic_notable_36", ["sd"] = { - [1] = "Adds (1-3) to 0 Lightning Damage to Spells", - [2] = "Adds 0 to (22-27) Lightning Damage to Spells", + [1] = "Adds (1-3) to (22-27) Lightning Damage to Spells", }, ["sortedStats"] = { - [1] = "spell_maximum_added_lightning_damage", - [2] = "spell_minimum_added_lightning_damage", + [1] = "spell_minimum_added_lightning_damage", + [2] = "spell_maximum_added_lightning_damage", }, ["stats"] = { ["spell_maximum_added_lightning_damage"] = { @@ -4906,12 +4843,11 @@ return { ["dn"] = "Notable 37", ["id"] = "abyss_hypnotic_notable_37", ["sd"] = { - [1] = "Adds (4-7) to 0 Physical Damage to Spells", - [2] = "Adds 0 to (10-13) Physical Damage to Spells", + [1] = "Adds (4-7) to (10-13) Physical Damage to Spells", }, ["sortedStats"] = { - [1] = "spell_maximum_added_physical_damage", - [2] = "spell_minimum_added_physical_damage", + [1] = "spell_minimum_added_physical_damage", + [2] = "spell_maximum_added_physical_damage", }, ["stats"] = { ["spell_maximum_added_physical_damage"] = { @@ -4934,12 +4870,11 @@ return { ["dn"] = "Notable 38", ["id"] = "abyss_hypnotic_notable_38", ["sd"] = { - [1] = "Adds (4-7) to 0 Chaos Damage to Spells", - [2] = "Adds 0 to (10-13) Chaos Damage to Spells", + [1] = "Adds (4-7) to (10-13) Chaos Damage to Spells", }, ["sortedStats"] = { - [1] = "spell_maximum_added_chaos_damage", - [2] = "spell_minimum_added_chaos_damage", + [1] = "spell_minimum_added_chaos_damage", + [2] = "spell_maximum_added_chaos_damage", }, ["stats"] = { ["spell_maximum_added_chaos_damage"] = { @@ -5456,12 +5391,11 @@ return { ["dn"] = "Notable 23", ["id"] = "abyss_ghastly_notable_23", ["sd"] = { - [1] = "Minions deal (15-18) to 0 additional Fire Damage", - [2] = "Minions deal 0 to (21-24) additional Fire Damage", + [1] = "Minions deal (15-18) to (21-24) additional Fire Damage", }, ["sortedStats"] = { - [1] = "minion_global_maximum_added_fire_damage", - [2] = "minion_global_minimum_added_fire_damage", + [1] = "minion_global_minimum_added_fire_damage", + [2] = "minion_global_maximum_added_fire_damage", }, ["stats"] = { ["minion_global_maximum_added_fire_damage"] = { @@ -5484,12 +5418,11 @@ return { ["dn"] = "Notable 24", ["id"] = "abyss_ghastly_notable_24", ["sd"] = { - [1] = "Minions deal (15-18) to 0 additional Cold Damage", - [2] = "Minions deal 0 to (21-24) additional Cold Damage", + [1] = "Minions deal (15-18) to (21-24) additional Cold Damage", }, ["sortedStats"] = { - [1] = "minion_global_maximum_added_cold_damage", - [2] = "minion_global_minimum_added_cold_damage", + [1] = "minion_global_minimum_added_cold_damage", + [2] = "minion_global_maximum_added_cold_damage", }, ["stats"] = { ["minion_global_maximum_added_cold_damage"] = { @@ -5512,12 +5445,11 @@ return { ["dn"] = "Notable 25", ["id"] = "abyss_ghastly_notable_25", ["sd"] = { - [1] = "Minions deal (1-3) to 0 additional Lightning Damage", - [2] = "Minions deal 0 to (33-39) additional Lightning Damage", + [1] = "Minions deal (1-3) to (33-39) additional Lightning Damage", }, ["sortedStats"] = { - [1] = "minion_global_maximum_added_lightning_damage", - [2] = "minion_global_minimum_added_lightning_damage", + [1] = "minion_global_minimum_added_lightning_damage", + [2] = "minion_global_maximum_added_lightning_damage", }, ["stats"] = { ["minion_global_maximum_added_lightning_damage"] = { @@ -5540,12 +5472,11 @@ return { ["dn"] = "Notable 26", ["id"] = "abyss_ghastly_notable_26", ["sd"] = { - [1] = "Minions deal (9-12) to 0 additional Physical Damage", - [2] = "Minions deal 0 to (15-18) additional Physical Damage", + [1] = "Minions deal (9-12) to (15-18) additional Physical Damage", }, ["sortedStats"] = { - [1] = "minion_global_maximum_added_physical_damage", - [2] = "minion_global_minimum_added_physical_damage", + [1] = "minion_global_minimum_added_physical_damage", + [2] = "minion_global_maximum_added_physical_damage", }, ["stats"] = { ["minion_global_maximum_added_physical_damage"] = { @@ -5568,12 +5499,11 @@ return { ["dn"] = "Notable 27", ["id"] = "abyss_ghastly_notable_27", ["sd"] = { - [1] = "Minions deal (9-12) to 0 additional Chaos Damage", - [2] = "Minions deal 0 to (15-18) additional Chaos Damage", + [1] = "Minions deal (9-12) to (15-18) additional Chaos Damage", }, ["sortedStats"] = { - [1] = "minion_global_maximum_added_chaos_damage", - [2] = "minion_global_minimum_added_chaos_damage", + [1] = "minion_global_minimum_added_chaos_damage", + [2] = "minion_global_maximum_added_chaos_damage", }, ["stats"] = { ["minion_global_maximum_added_chaos_damage"] = { diff --git a/src/Data/TimelessJewelData/LegionTradeIds.lua b/src/Data/TimelessJewelData/LegionTradeIds.lua index 50b12b6ba16..44fe6788eeb 100644 --- a/src/Data/TimelessJewelData/LegionTradeIds.lua +++ b/src/Data/TimelessJewelData/LegionTradeIds.lua @@ -61,4 +61,9 @@ return { [3] = "explicit.pseudo_timeless_jewel_medved" } }, + [7] = { keystone = { [1] = "explicit.pseudo_timeless_jewel_tecrod" } }, + [8] = { keystone = { [1] = "explicit.pseudo_timeless_jewel_ulaman" } }, + [9] = { keystone = { [1] = "explicit.pseudo_timeless_jewel_kurgal" } }, + [10] = { keystone = { [1] = "explicit.pseudo_timeless_jewel_amanamu" } }, + [11] = { keystone = { [1] = "explicit.pseudo_timeless_jewel_zorath" } }, } \ No newline at end of file diff --git a/src/Data/Uniques/jewel.lua b/src/Data/Uniques/jewel.lua index 7e08b85ddf9..ced15ff27c2 100644 --- a/src/Data/Uniques/jewel.lua +++ b/src/Data/Uniques/jewel.lua @@ -1961,5 +1961,55 @@ Implicits: 0 {variant:5}4% increased Totem Damage per 10 Devotion Passives in radius are Conquered by the Templars Historic +]],[[ +Festering Vengeance +Murderous Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Tecrod +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Extinguishing Grasp +Searching Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Ulaman +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Baleful Dominion +Hypnotic Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Kurgal +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Destructive Aspiration +Ghastly Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Amanamu +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Reclaimed Malevolence +Assembled Eye Jewel +League: Abyss +Source: Vendor Recipe +Limited to: 1 Historic +Implicits: 0 +Binding (100-8000) souls to phylacteries to sustain Zorath +Passives affected are Conquered by the Abyssal +Historic ]], } diff --git a/src/Export/Bases/jewel.txt b/src/Export/Bases/jewel.txt index e8afa8787df..b2f0efa551c 100644 --- a/src/Export/Bases/jewel.txt +++ b/src/Export/Bases/jewel.txt @@ -12,6 +12,7 @@ local itemBases = ... #base Metadata/Items/Jewels/JewelAbyssCaster #base Metadata/Items/Jewels/JewelAbyssMelee #base Metadata/Items/Jewels/JewelAbyssRanged +#base Metadata/Items/Jewels/JewelAbyssConglomerate #subType Cluster #base Metadata/Items/Jewels/JewelPassiveTreeExpansionLarge diff --git a/src/Export/Classes/GGPKData.lua b/src/Export/Classes/GGPKData.lua index ec2cec1f170..a8037121648 100644 --- a/src/Export/Classes/GGPKData.lua +++ b/src/Export/Classes/GGPKData.lua @@ -214,11 +214,15 @@ function GGPKClass:GetNeededFiles() "Data/PantheonPanelLayout.dat", "Data/AlternatePassiveAdditions.dat", "Data/AlternatePassiveSkills.dat", + "Data/AlternateTreeArt.dat", "Data/AlternateTreeVersions.dat", "Data/GrantedEffectQualityStats.dat", "Data/AegisVariations.dat", "Data/CostTypes.dat", "Data/PassiveJewelRadii.dat", + "Data/PassiveJewelRadiiArt.dat", + "Data/PassiveSkillTreeConnectionArt.dat", + "Data/PassiveSkillTreeNodeFrameArt.dat", "Data/SoundEffects.dat", "Data/MavenJewelRadiusKeystones.dat", "Data/GrantedEffectStatSets.dat", diff --git a/src/Export/Scripts/legionPassives.lua b/src/Export/Scripts/legionPassives.lua index c1d75777561..6049af059d8 100644 --- a/src/Export/Scripts/legionPassives.lua +++ b/src/Export/Scripts/legionPassives.lua @@ -107,35 +107,34 @@ function stringify(thing) end function parseStats(datFileRow, legionPassive) - local descOrders = {} for idx,statKey in ipairs(datFileRow.StatsKeys) do local refRow = type(statKey) == "number" and statKey + 1 or statKey._rowIndex local statId = stats:ReadCell(refRow, 1) local range = datFileRow["Stat"..idx] - local stat = {} - stat[statId] = { + local stat = { ["min"] = range[1], ["max"] = range[2], ["index"] = idx } - -- Describing stats here to get the orders - local statLines, orders = describeStats(stat) - stat[statId].statOrder = orders[1] - legionPassive.stats[statId] = stat[statId] - for i, line in ipairs(statLines) do - table.insert(legionPassive.sd, line) - descOrders[line] = orders[i] - end + -- describeStats changes values while formatting them, so use a copy when only finding the order. + local _, orders = describeStats({ [statId] = { min = stat.min, max = stat.max } }) + stat.statOrder = orders[1] + legionPassive.stats[statId] = stat end - -- Have to re-sort since we described the stats earlier - table.sort(legionPassive.sd, function(a, b) return descOrders[a] < descOrders[b] end) + -- A description can combine several stats, such as minimum and maximum + -- added damage, so describe the complete set together. + legionPassive.sd = describeStats(legionPassive.stats) local sortedStats = {} for stat in pairsSortByKey(legionPassive.stats) do table.insert(sortedStats, stat) end -- Finally get what we want, sorted stats by order - table.sort(sortedStats, function(a, b) return legionPassive.stats[a].statOrder and legionPassive.stats[a].statOrder < legionPassive.stats[b].statOrder end) + table.sort(sortedStats, function(a, b) + local statA, statB = legionPassive.stats[a], legionPassive.stats[b] + local orderA, orderB = statA.statOrder or math.huge, statB.statOrder or math.huge + return orderA < orderB or orderA == orderB and statA.index < statB.index + end) legionPassive.sortedStats = sortedStats end diff --git a/src/Export/Scripts/legionSprites.lua b/src/Export/Scripts/legionSprites.lua new file mode 100644 index 00000000000..75da8c8ed46 --- /dev/null +++ b/src/Export/Scripts/legionSprites.lua @@ -0,0 +1,263 @@ +-- Run this from the exporter's Scripts screen after selecting the current game data. +-- Lua builds the sheet data and GIMP writes PNGs using the same structure as PoB 2. + +local gimpBatch = require("Tree/GimpBatch/gimp_batch") + +local function round(value) + return math.floor(value + 0.5) +end + +-- The source UI images are made for the game's larger passive-tree canvas. +local treeArtScale = 0.3835 + +local function addFile(files, path) + if path and path ~= "" then + files[path] = true + end +end + +-- Keep images of the same height on one row. This is the layout used by the +-- PoB 2 PNG exporter and keeps coordinates stable when new rows are added. +local function packEntries(entries, maximumWidth) + table.sort(entries, function(a, b) + if a.height ~= b.height then + return a.height < b.height + elseif a.width ~= b.width then + return a.width < b.width + end + return a.name:lower() < b.name:lower() + end) + + local x, y, rowHeight, usedWidth = 0, 0, 0, 0 + local coords = { } + for _, entry in ipairs(entries) do + if x > 0 and (x + entry.width > maximumWidth or entry.height ~= rowHeight) then + x = 0 + y = y + rowHeight + rowHeight = 0 + end + entry.x = x + entry.y = y + coords[entry.name] = entry + x = x + entry.width + rowHeight = math.max(rowHeight, entry.height) + usedWidth = math.max(usedWidth, x) + end + return coords, usedWidth, y + rowHeight +end + +local function writeSheet(output, name, filename, coords, names) + output:write(string.format('\t["%s"] = {{\n', name)) + output:write(string.format('\t\t["filename"] = "%s",\n', filename)) + output:write('\t\t["coords"] = {\n') + table.sort(names, function(a, b) return a:lower() < b:lower() end) + for _, assetName in ipairs(names) do + local coord = coords[assetName] + output:write(string.format( + '\t\t\t["%s"] = { ["x"] = %d, ["y"] = %d, ["w"] = %d, ["h"] = %d },\n', + assetName, coord.x, coord.y, coord.width, coord.height + )) + end + output:write('\t\t},\n\t}},\n') +end + +main.ggpk:ExtractList({ "art/uiimages1.txt" }, { }) +local uiImages = { } +for line in convertUTF16to8(getFile("art/uiimages1.txt")):gmatch("[^\r\n]+") do + local name, path, x, y, right, bottom = line:match( + '^"([^"]+)"%s+"([^"]+)"%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)' + ) + if name then + uiImages[name:lower()] = { + path = path, + x = tonumber(x), + y = tonumber(y), + width = tonumber(right) - tonumber(x) + 1, + height = tonumber(bottom) - tonumber(y) + 1, + } + end +end + +local alternateArt +for row in dat("AlternateTreeArt"):Rows() do + if row.TreeVersion and row.TreeVersion.Id:match("^Abyss") then + if not alternateArt then + alternateArt = row + else + -- All current Abyss jewels use one art set. Stop the export if a future + -- jewel needs another set instead of silently giving it the wrong art. + assert(row.ConnectionArt.Id == alternateArt.ConnectionArt.Id + and row.KeystoneFrameArt.Id == alternateArt.KeystoneFrameArt.Id + and row.NotableFrameArt.Id == alternateArt.NotableFrameArt.Id + and row.AscendancyFrameArt.Id == alternateArt.AscendancyFrameArt.Id, + "Abyss alternate tree art sets do not match") + end + end +end +assert(alternateArt, "Abyss alternate tree art was not found") + +local files = { } +local iconEntries = { } +local iconSections = { normal = { }, notable = { }, keystone = { } } +local iconSizes = { normal = 27, notable = 38, keystone = 64 } +local seenIcons = { } +for passive in dat("AlternatePassiveSkills"):Rows() do + local icon = passive.DDSIcon + if icon and icon ~= "" and not seenIcons[icon] then + local section = "normal" + if isValueInTable(passive.PassiveType, 4) then + section = "keystone" + elseif isValueInTable(passive.PassiveType, 3) or isValueInTable(passive.PassiveType, 5) then + section = "notable" + end + seenIcons[icon] = true + iconSections[section][#iconSections[section] + 1] = icon + iconEntries[#iconEntries + 1] = { + name = icon, + file = main.ggpk.oozPath .. icon:lower(), + width = iconSizes[section], + height = iconSizes[section], + } + addFile(files, icon) + end +end + +local frameEntries = { } +local frameGroups = { + { art = alternateArt.NotableFrameArt, prefix = "AbyssNotableFrame" }, + { art = alternateArt.KeystoneFrameArt, prefix = "AbyssKeystoneFrame" }, + { art = alternateArt.AscendancyFrameArt, prefix = "AbyssAscendancyFrame" }, +} +local frameStates = { + { column = "Normal", suffix = "Unallocated" }, + { column = "CanAllocate", suffix = "CanAllocate" }, + { column = "Active", suffix = "Allocated" }, +} +for _, group in ipairs(frameGroups) do + for _, state in ipairs(frameStates) do + local image = assert(uiImages[group.art[state.column]:lower()], "Missing UIImages row for Abyss frame") + frameEntries[#frameEntries + 1] = { + name = group.prefix .. state.suffix, + file = main.ggpk.oozPath .. image.path:lower(), + sourceX = image.x, + sourceY = image.y, + sourceWidth = image.width, + sourceHeight = image.height, + width = round(image.width * treeArtScale), + height = round(image.height * treeArtScale), + } + addFile(files, image.path) + end +end + +local connectionArt = alternateArt.ConnectionArt +local connectionSources = { + { state = "normal", file = connectionArt.Normal }, + -- Active is an effect mask. The game colors it gold over the normal curve + -- while previewing a path; it is not itself a displayed connector texture. + { state = "intermediate", file = connectionArt.Normal, effect = connectionArt.Active }, + -- Despite the column name, Mask contains the completed green curve art. + { state = "active", file = connectionArt.Mask }, +} +for _, source in ipairs(connectionSources) do + addFile(files, source.file) + addFile(files, source.effect) + source.file = main.ggpk.oozPath .. source.file:lower() + if source.effect then + source.effect = main.ggpk.oozPath .. source.effect:lower() + end +end + +local filesToExtract = { } +for file in pairs(files) do + filesToExtract[#filesToExtract + 1] = file +end +table.sort(filesToExtract) +main.ggpk:ExtractList(filesToExtract, { }) + +for _, entry in ipairs(iconEntries) do + local file = assert(io.open(entry.file, "rb")) + file:seek("set", 12) + local dimensions = file:read(8) + file:close() + entry.sourceWidth, entry.sourceHeight = bytesToUInt(dimensions, 5), bytesToUInt(dimensions, 1) + entry.sourceX, entry.sourceY = 0, 0 +end + +local outputPath = GetScriptPath() .. "/../TreeData/legion/" +local temporaryPath = main.ggpk.oozPath .. "data/" +local iconCoords, iconWidth, iconHeight = packEntries(iconEntries, 512) +local combineImagesScript = GetScriptPath() .. "/Tree/GimpBatch/combine_images.scm" +local orbitSizes = { + normal = { 33, 64, 130, 191, 256, 322 }, + intermediate = { 33, 64, 130, 191, 256, 322 }, + active = { 39, 70, 136, 197, 262, 329 }, +} +for state, sizes in pairs(orbitSizes) do + for orbit, size in ipairs(sizes) do + frameEntries[#frameEntries + 1] = { + name = "AbyssOrbit" .. orbit .. state:gsub("^%l", string.upper), + file = temporaryPath .. "abyss-orbit" .. orbit .. "-" .. state .. ".png", + sourceX = 0, + sourceY = 0, + sourceWidth = size, + sourceHeight = size, + width = size, + height = size, + } + end +end +local artCoords, artWidth, artHeight = packEntries(frameEntries, 512) +local batch = gimpBatch.new_batch(temporaryPath .. "legion-sprites.scm", { + combineImagesScript, + GetScriptPath() .. "/Tree/GimpBatch/extract_abyss_lines.scm", +}) +gimpBatch.combine_images_to_sprite( + batch, { filename = "legion-active.png", w = iconWidth, h = iconHeight, coords = iconEntries }, outputPath, 100 +) +gimpBatch.combine_images_to_sprite( + batch, { filename = "legion-inactive.png", w = iconWidth, h = iconHeight, coords = iconEntries }, outputPath, 40 +) +gimpBatch.extract_abyss_lines(batch, connectionSources, temporaryPath, outputPath) +gimpBatch.combine_images_to_sprite( + batch, { filename = "legion-art.png", w = artWidth, h = artHeight, coords = frameEntries }, outputPath, 100 +) +local result = gimpBatch.run_batch(batch) +assert(result == 0 or result == true, "GIMP failed to create the Legion sprite sheets") + +local output = assert(io.open(outputPath .. "tree-legion.lua", "w")) +output:write("-- This file is automatically generated by Export/Scripts/legionSprites.lua.\n") +output:write("return {\n") +writeSheet(output, "keystoneActive", "legion-active.png", iconCoords, iconSections.keystone) +writeSheet(output, "keystoneInactive", "legion-inactive.png", iconCoords, iconSections.keystone) +writeSheet(output, "notableActive", "legion-active.png", iconCoords, iconSections.notable) +writeSheet(output, "notableInactive", "legion-inactive.png", iconCoords, iconSections.notable) +writeSheet(output, "normalActive", "legion-active.png", iconCoords, iconSections.normal) +writeSheet(output, "normalInactive", "legion-inactive.png", iconCoords, iconSections.normal) + +local artNames = { } +for name in pairs(artCoords) do + artNames[#artNames + 1] = name +end +output:write('\t["treeAssets"] = {\n') +output:write('\t\t{ ["filename"] = "legion-art.png", ["coords"] = {\n') +table.sort(artNames) +for _, name in ipairs(artNames) do + local coord = artCoords[name] + output:write(string.format( + '\t\t\t["%s"] = { ["x"] = %d, ["y"] = %d, ["w"] = %d, ["h"] = %d },\n', + name, coord.x, coord.y, coord.width, coord.height + )) +end +output:write('\t\t} },\n') +for _, state in ipairs({ "Normal", "Intermediate", "Active" }) do + output:write(string.format( + '\t\t{ ["filename"] = "abyss-line-connector-%s.png", ["coords"] = { ["AbyssLineConnector%s"] = { ["x"] = 0, ["y"] = 0, ["w"] = 368, ["h"] = 13 } } },\n', + state:lower(), state + )) +end +output:write('\t},\n') +output:write("}\n") +output:close() + +printf("Legion passive sprites and Abyss tree art exported.") diff --git a/src/Export/Tree/GimpBatch/combine_images.scm b/src/Export/Tree/GimpBatch/combine_images.scm new file mode 100644 index 00000000000..53d4ff3e33f --- /dev/null +++ b/src/Export/Tree/GimpBatch/combine_images.scm @@ -0,0 +1,29 @@ +(define (combine-images-into-sprite-sheet output-path width height saturation entries) + (let* ((sprite-sheet (car (gimp-image-new width height RGB))) + (background (car (gimp-layer-new sprite-sheet "Background" width height RGBA-IMAGE 100 LAYER-MODE-NORMAL)))) + (gimp-image-insert-layer sprite-sheet background 0 0) + + (for-each + (lambda (entry) + (let* ((file (list-ref entry 0)) + (source-x (list-ref entry 1)) + (source-y (list-ref entry 2)) + (source-width (list-ref entry 3)) + (source-height (list-ref entry 4)) + (target-x (list-ref entry 5)) + (target-y (list-ref entry 6)) + (target-width (list-ref entry 7)) + (target-height (list-ref entry 8)) + (layer (car (gimp-file-load-layer RUN-NONINTERACTIVE sprite-sheet file)))) + (gimp-image-insert-layer sprite-sheet layer -1 0) + ; UIImages share a texture, so crop before scaling and placing the layer. + (gimp-layer-resize layer source-width source-height (- source-x) (- source-y)) + (gimp-layer-scale layer target-width target-height TRUE) + (if (< saturation 100) + (gimp-drawable-hue-saturation layer 0 0 0 (- saturation 100) 0)) + (gimp-layer-set-offsets layer target-x target-y))) + entries) + + (gimp-image-merge-visible-layers sprite-sheet CLIP-TO-IMAGE) + (file-png-export RUN-NONINTERACTIVE sprite-sheet output-path -1 0 9 1 0 1 1 1 0 "auto") + (gimp-image-delete sprite-sheet))) diff --git a/src/Export/Tree/GimpBatch/extract_abyss_lines.scm b/src/Export/Tree/GimpBatch/extract_abyss_lines.scm new file mode 100644 index 00000000000..5df586825a9 --- /dev/null +++ b/src/Export/Tree/GimpBatch/extract_abyss_lines.scm @@ -0,0 +1,67 @@ +(define (make-abyss-preview base-path effect-path output-path) + (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE base-path))) + (effect-layer (car (gimp-file-load-layer RUN-NONINTERACTIVE image effect-path)))) + (gimp-image-insert-layer image effect-layer -1 0) + ; The effect file is a cyan mask on black. Shift it to gold and use screen + ; blending so only its textured highlight is drawn over the normal curve. + (gimp-drawable-hue-saturation effect-layer 0 -135 0 0 0) + (gimp-layer-set-mode effect-layer LAYER-MODE-SCREEN) + (gimp-layer-set-opacity effect-layer 55) + (gimp-image-merge-visible-layers image CLIP-TO-IMAGE) + (file-png-export RUN-NONINTERACTIVE image output-path -1 0 9 1 0 1 1 1 0 "auto") + (gimp-image-delete image))) + +(define (export-abyss-line-part source-path output-path crop-x crop-y crop-size target-size inner-radius outer-radius) + (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE source-path))) + (layer (vector-ref (car (gimp-image-get-layers image)) 0)) + (image-width (car (gimp-drawable-get-width layer))) + (image-height (car (gimp-drawable-get-height layer)))) + (gimp-layer-add-alpha layer) + (if (> outer-radius 0) + (begin + ; Each curve is a quarter of a circle centred on the bottom-right corner. + ; Keep only the band containing this orbit before cropping the square. + (gimp-image-select-ellipse image CHANNEL-OP-REPLACE + (- image-width outer-radius) (- image-height outer-radius) + (* outer-radius 2) (* outer-radius 2)) + (gimp-image-select-ellipse image CHANNEL-OP-SUBTRACT + (- image-width inner-radius) (- image-height inner-radius) + (* inner-radius 2) (* inner-radius 2)) + (gimp-selection-invert image) + (gimp-drawable-edit-clear layer) + (gimp-selection-none image))) + (gimp-image-crop image crop-size crop-size crop-x crop-y) + (gimp-image-scale image target-size target-size) + (file-png-export RUN-NONINTERACTIVE image output-path -1 0 9 1 0 1 1 1 0 "auto") + (gimp-image-delete image))) + +(define (extract-abyss-lines source-path temporary-directory output-directory state active) + (let* ((line-height 60) + (line-image (car (gimp-file-load RUN-NONINTERACTIVE source-path))) + (line-layer (vector-ref (car (gimp-image-get-layers line-image)) 0)) + (image-size (car (gimp-drawable-get-width line-layer))) + (line-path (string-append output-directory "abyss-line-connector-" state ".png")) + (radii '(82 164 334 489 658 839)) + (inner '(40 123 292 411 573 748)) + (outer '(123 207 411 573 748 958)) + (normal-sizes '(33 64 130 191 256 322)) + (active-sizes '(39 70 136 197 262 329)) + (sizes (if (= active 1) active-sizes normal-sizes))) + ; The straight connector is the strip across the top of the source image. + (gimp-layer-add-alpha line-layer) + (gimp-image-crop line-image image-size line-height 0 10) + (gimp-image-scale line-image 368 13) + (file-png-export RUN-NONINTERACTIVE line-image line-path -1 0 9 1 0 1 1 1 0 "auto") + (gimp-image-delete line-image) + + (let loop ((orbit 1) (radius-list radii) (inner-list inner) (outer-list outer) (size-list sizes)) + (if (not (null? radius-list)) + (let* ((radius (car radius-list)) + (target-size (car size-list)) + ; PoB renders the source art at 38.35% of its game size. + (crop-size (inexact->exact (round (/ target-size 0.3835)))) + (output-path (string-append temporary-directory "abyss-orbit" (number->string orbit) "-" state ".png"))) + (export-abyss-line-part source-path output-path + (- image-size crop-size) (- image-size crop-size) crop-size target-size + (car inner-list) (car outer-list)) + (loop (+ orbit 1) (cdr radius-list) (cdr inner-list) (cdr outer-list) (cdr size-list))))))) diff --git a/src/Export/Tree/GimpBatch/gimp_batch.lua b/src/Export/Tree/GimpBatch/gimp_batch.lua new file mode 100644 index 00000000000..9fdd165695a --- /dev/null +++ b/src/Export/Tree/GimpBatch/gimp_batch.lua @@ -0,0 +1,78 @@ +local module = { } + +local function schemePath(path) + return path:gsub("\\", "/"):gsub('"', '\\"') +end + +local function gimpExecutable() + local programFiles = os.getenv("ProgramFiles") + local installed = programFiles and programFiles .. "\\GIMP 3\\bin\\gimp-console-3.exe" + local file = installed and io.open(installed, "rb") + if file then + file:close() + return installed + end + return "gimp-console-3.exe" +end + +-- Put all sprite operations in one script so GIMP only needs to start once. +function module.new_batch(scriptPath, dependencies) + local batch = { + path = scriptPath, + script = assert(io.open(scriptPath, "w")), + } + for _, dependency in ipairs(dependencies) do + batch.script:write(string.format('(load "%s")\n', schemePath(dependency))) + end + return batch +end + +-- GIMP does the same PNG sprite composition used by the PoB 2 exporter. Each +-- entry contains the source crop followed by its position and size in the atlas. +function module.combine_images_to_sprite(batch, sheetData, outputPath, saturation) + local coords = { } + for _, entry in ipairs(sheetData.coords) do + coords[#coords + 1] = string.format( + '("%s" %d %d %d %d %d %d %d %d)', + schemePath(entry.file), + entry.sourceX, entry.sourceY, entry.sourceWidth, entry.sourceHeight, + entry.x, entry.y, entry.width, entry.height + ) + end + + batch.script:write(string.format( + '(combine-images-into-sprite-sheet "%s" %d %d %d \'(%s))\n', + schemePath(outputPath .. sheetData.filename), sheetData.w, sheetData.h, saturation, table.concat(coords, " ") + )) +end + +-- The game stores every orbit in one image. This uses the shared centre and +-- radii of those curves to produce the six orbit images PoB renders. +function module.extract_abyss_lines(batch, sources, temporaryPath, outputPath) + for _, source in ipairs(sources) do + local sourceFile = source.file + if source.effect then + sourceFile = temporaryPath .. "abyss-preview.png" + batch.script:write(string.format( + '(make-abyss-preview "%s" "%s" "%s")\n', + schemePath(source.file), schemePath(source.effect), schemePath(sourceFile) + )) + end + batch.script:write(string.format( + '(extract-abyss-lines "%s" "%s" "%s" "%s" %d)\n', + schemePath(sourceFile), schemePath(temporaryPath), schemePath(outputPath), + source.state, source.state == "active" and 1 or 0 + )) + end +end + +function module.run_batch(batch) + batch.script:close() + local command = string.format( + 'cmd /c ""%s" -n -i -s -c --batch-interpreter=plug-in-script-fu-eval -b "(load \\\"%s\\\")" --quit"', + gimpExecutable(), schemePath(batch.path) + ) + return os.execute(command) +end + +return module diff --git a/src/Export/Uniques/jewel.lua b/src/Export/Uniques/jewel.lua index 41520953a19..355cc73ac26 100644 --- a/src/Export/Uniques/jewel.lua +++ b/src/Export/Uniques/jewel.lua @@ -1865,5 +1865,55 @@ Implicits: 0 {variant:19}ShieldDefencesPerDevotion Passives in radius are Conquered by the Templars Historic +]],[[ +Festering Vengeance +Murderous Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Tecrod +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Extinguishing Grasp +Searching Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Ulaman +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Baleful Dominion +Hypnotic Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Kurgal +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Destructive Aspiration +Ghastly Eye Jewel +League: Abyss +Source: Drops from unique{Zorath, Vile Assembled} +Limited to: 1 Historic +Implicits: 0 +Subjugating (100-8000) souls in the thrall of Amanamu +Passives affected are Conquered by the Abyssal +Historic +]],[[ +Reclaimed Malevolence +Assembled Eye Jewel +League: Abyss +Source: Vendor Recipe +Limited to: 1 Historic +Implicits: 0 +Binding (100-8000) souls to phylacteries to sustain Zorath +Passives affected are Conquered by the Abyssal +Historic ]], } diff --git a/src/Export/spec.lua b/src/Export/spec.lua index f3dba3510e6..6c707a43f6c 100644 --- a/src/Export/spec.lua +++ b/src/Export/spec.lua @@ -871,13 +871,55 @@ return { } }, alternatetreeart={ + [1]={ + list=false, + name="TreeVersion", + refTo="AlternateTreeVersions", + type="Key", + width=150 + }, + [2]={ + list=false, + name="ConnectionArt", + refTo="PassiveSkillTreeConnectionArt", + type="Key", + width=180 + }, + [3]={ + list=false, + name="KeystoneFrameArt", + refTo="PassiveSkillTreeNodeFrameArt", + type="Key", + width=180 + }, + [4]={ + list=false, + name="RadiiArt", + refTo="PassiveJewelRadiiArt", + type="Key", + width=150 + }, + [5]={ + list=false, + name="NotableFrameArt", + refTo="PassiveSkillTreeNodeFrameArt", + type="Key", + width=180 + }, + [6]={ + list=false, + name="AscendancyFrameArt", + refTo="PassiveSkillTreeNodeFrameArt", + type="Key", + width=200 + } }, alternatetreepassivesizes={ }, alternatetreeversions={ [1]={ list=false, - name="ConquerorType", + name="Id", refTo="", type="String", width=150 @@ -10856,6 +10898,36 @@ return { width=150 } }, + passivejewelradiiart={ + [1]={ + list=false, + name="Id", + refTo="", + type="String", + width=120 + }, + [2]={ + list=false, + name="Circle1", + refTo="", + type="String", + width=450 + }, + [3]={ + list=false, + name="Circle2", + refTo="", + type="String", + width=450 + }, + [4]={ + list=false, + name="Glow", + refTo="", + type="String", + width=450 + } + }, passivejewelslots={ [1]={ list=false, @@ -11648,6 +11720,108 @@ return { width=150 } }, + passiveskilltreeconnectionart={ + [1]={ + list=false, + name="Id", + refTo="", + type="String", + width=200 + }, + [2]={ + list=false, + name="Normal", + refTo="", + type="String", + width=500 + }, + [3]={ + list=false, + name="Intermediate2", + refTo="", + type="String", + width=500 + }, + [4]={ + list=false, + name="Intermediate", + refTo="", + type="String", + width=500 + }, + [5]={ + list=false, + name="Active", + refTo="", + type="String", + width=500 + }, + [6]={ + list=false, + name="Mask", + refTo="", + type="String", + width=500 + }, + [7]={ + list=false, + name="Ornament1", + refTo="", + type="String", + width=400 + }, + [8]={ + list=false, + name="Ornament2", + refTo="", + type="String", + width=400 + } + }, + passiveskilltreenodeframeart={ + [1]={ + list=false, + name="Id", + refTo="", + type="String", + width=250 + }, + [2]={ + list=false, + name="Normal", + refTo="", + type="String", + width=550 + }, + [3]={ + list=false, + name="CanAllocate", + refTo="", + type="String", + width=550 + }, + [4]={ + list=false, + name="Active", + refTo="", + type="String", + width=550 + }, + [5]={ + list=false, + name="Mask", + refTo="", + type="String", + width=550 + }, + [6]={ + list=false, + name="Header", + refTo="", + type="String", + width=450 + } + }, passiveskilltrees={ }, passiveskilltreetutorial={ diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index a598972bce8..10459584341 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -890,6 +890,11 @@ data.timelessJewelTypes = { [4] = "Militant Faith", [5] = "Elegant Hubris", [6] = "Heroic Tragedy", + [7] = "Abyss Tecrod", + [8] = "Abyss Ulaman", + [9] = "Abyss Kurgal", + [10] = "Abyss Amanamu", + [11] = "Abyss Zorath", } data.timelessJewelSeedMin = { [1] = 100, @@ -920,8 +925,10 @@ data.timelessJewelSeedMax = { data.timelessJewelTradeIDs = LoadModule("Data/TimelessJewelData/LegionTradeIds") data.timelessJewelAdditions = 337 -- #legionAdditions data.nodeIDList = LoadModule("Data/TimelessJewelData/NodeIndexMapping") +data.abyssNotableNames = LoadModule("Data/TimelessJewelData/AbyssNotableNames") data.timelessJewelLUTs = { } data.readLUT, data.repairLUTs = LoadModule("Modules/DataLegionLookUpTableHelper") +data.readAbyssJewelLUT, data.resolveAbyssJewelComponent, data.getAbyssJewelComponentRoll = LoadModule("Modules/DataAbyssJewelLookUpTableHelper") -- this runs if the "size" key is missing from nodeIDList and attempts to rebuild all jewel LUTs and the nodeIDList -- note this should only run in dev mode diff --git a/src/Modules/DataAbyssJewelLookUpTableHelper.lua b/src/Modules/DataAbyssJewelLookUpTableHelper.lua new file mode 100644 index 00000000000..85674cb4cb8 --- /dev/null +++ b/src/Modules/DataAbyssJewelLookUpTableHelper.lua @@ -0,0 +1,275 @@ +-- Path of Building +-- +-- Module: Data Abyss Jewel Look Up Table Helper +-- Reads Abyss jewel data grouped by socket and Zorath data grouped by passive node. +-- +local s_byte = string.byte +local s_sub = string.sub +local loadJewelFile = LoadModule("Modules/DataJewelFileLoader") + +local function readUInt16(jewelData, offset) + return bytesToUShort(jewelData, offset), offset + 2 +end + +-- A modification starts with its number of parts. Each part then stores whether +-- it replaces or adds stats, its stat ID, its roll count, and its two-byte rolls. +local function skipModification(jewelData, offset) + local componentCount = s_byte(jewelData, offset) + offset = offset + 1 + for _ = 1, componentCount do + local statCount = s_byte(jewelData, offset + 2) + offset = offset + 3 + statCount * 2 + end + return offset +end + +local function readModification(jewelData, offset, jewelType) + local componentCount = s_byte(jewelData, offset) + local modification = { } + offset = offset + 1 + for componentIndex = 1, componentCount do + local componentType, localId, statCount = s_byte(jewelData, offset, offset + 2) + offset = offset + 3 + local mapping = data.nodeIDList.localIdToGlobalId + local component = { + type = componentType, + id = mapping and mapping[jewelType] and mapping[jewelType][localId] or localId, + rolls = { }, + } + for statIndex = 1, statCount do + local roll + roll, offset = readUInt16(jewelData, offset) + component.rolls[statIndex] = roll >= 32768 and roll - 65536 or roll + end + modification[componentIndex] = component + end + return modification, offset +end + +-- The file uses type 1 for a full node replacement and type 2 for stats added +-- to the original node. Return the matching existing Legion node data. +local function resolveAbyssJewelComponent(component, legion) + if component.type == 1 then + return legion.nodes[component.id + 1 - data.timelessJewelAdditions], true + elseif component.type == 2 then + return legion.additions[component.id + 1], false + end +end + +local function getAbyssJewelComponentRoll(component, node, statIndex) + local statKey, statMod + for key, mod in pairs(node.stats) do + if mod.index == statIndex then + statKey, statMod = key, mod + break + end + end + local roll = statMod and component.rolls[statMod.index] + -- A negative stored value is shown as a positive number in "reduced" descriptions. + if roll and roll < 0 and statMod.min >= 0 then + roll = -roll + end + return statKey, statMod, roll +end + +local function skipSocketRecord(jewelData, offset) + local affectedNodeCount = s_byte(jewelData, offset) + offset = offset + 1 + for _ = 1, affectedNodeCount do + offset = skipModification(jewelData, offset + 2) + end + return offset +end + +local function skipAscendancyRecord(jewelData, offset) + return offset + 1 + s_byte(jewelData, offset) * 2 +end + +local function parseAbyssJewel(jewelType, jewelData) + -- The header stores the four-letter format, version, jewel type, first and + -- last seed, and the amount between seeds. + local format = s_sub(jewelData, 1, 4) + local formatVersion = s_byte(jewelData, 5) + local storedJewelType = s_byte(jewelData, 6) + local seedMinimum, offset = readUInt16(jewelData, 7) + local seedMaximum + seedMaximum, offset = readUInt16(jewelData, offset) + local seedIncrement + seedIncrement, offset = readUInt16(jewelData, offset) + if formatVersion ~= 1 or storedJewelType ~= jewelType or seedIncrement == 0 then + error("Invalid Abyss timeless jewel header for jewel type " .. jewelType) + end + + local jewelLUT = { + data = jewelData, + format = format, + seedMinimum = seedMinimum, + seedMaximum = seedMaximum, + seedIncrement = seedIncrement, + seedCount = (seedMaximum - seedMinimum) / seedIncrement + 1, + seedOffsets = { }, + blockOffsets = { }, + } + + if format == "ABYS" then + -- Each socket has one entry for every seed. + local socketCount = s_byte(jewelData, offset) + jewelLUT.abyssSize = s_byte(jewelData, offset + 1) + offset = offset + 2 + local socketIds = { } + for socketIndex = 1, socketCount do + socketIds[socketIndex], offset = readUInt16(jewelData, offset) + end + for _, socketId in ipairs(socketIds) do + jewelLUT.blockOffsets[socketId] = offset + for _ = 1, jewelLUT.seedCount do + offset = skipSocketRecord(jewelData, offset) + end + end + elseif format == "ABYN" then + -- Zorath has one block per passive node, so only allocated path nodes are read later. + local nodeCount + nodeCount, offset = readUInt16(jewelData, offset) + local nodeIds = { } + for nodeIndex = 1, nodeCount do + nodeIds[nodeIndex], offset = readUInt16(jewelData, offset) + end + for _, nodeId in ipairs(nodeIds) do + jewelLUT.blockOffsets[nodeId] = offset + for _ = 1, jewelLUT.seedCount do + offset = skipModification(jewelData, offset) + end + end + + if s_sub(jewelData, offset, offset + 3) ~= "ASCS" then + error("Missing Abyss ascendancy section for jewel type " .. jewelType) + end + offset = offset + 4 + local ascendancyCount + ascendancyCount, offset = readUInt16(jewelData, offset) + jewelLUT.ascendancyOffsets = { } + for _ = 1, ascendancyCount do + local nameLength = s_byte(jewelData, offset) + local ascendancyName = s_sub(jewelData, offset + 1, offset + nameLength) + offset = offset + nameLength + 1 + jewelLUT.ascendancyOffsets[ascendancyName] = offset + for _ = 1, jewelLUT.seedCount do + offset = skipAscendancyRecord(jewelData, offset) + end + end + else + error("Unknown Abyss timeless jewel format " .. format) + end + return jewelLUT +end + +local function loadAbyssJewel(jewelType) + if data.timelessJewelLUTs[jewelType] and data.timelessJewelLUTs[jewelType].data then + return data.timelessJewelLUTs[jewelType] + end + + local jewelTypeName = data.timelessJewelTypes[jewelType]:gsub("%s+", "") + local jewelData = loadJewelFile(jewelTypeName, false) + if not jewelData then + return + end + data.timelessJewelLUTs[jewelType] = parseAbyssJewel(jewelType, jewelData) + return data.timelessJewelLUTs[jewelType] +end + +local function getSeedIndex(jewelLUT, seed) + local seedOffset = seed - jewelLUT.seedMinimum + if seedOffset < 0 or seed > jewelLUT.seedMaximum or seedOffset % jewelLUT.seedIncrement ~= 0 then + return + end + return seedOffset / jewelLUT.seedIncrement + 1 +end + +local function getRecordOffsets(jewelLUT, blockKey, blockOffset, skipRecord) + if not blockOffset then + return + end + if not jewelLUT.seedOffsets[blockKey] then + -- Entry sizes vary, so remember where each seed starts after the first scan. + local offsets = { } + local offset = blockOffset + for seedIndex = 1, jewelLUT.seedCount do + offsets[seedIndex] = offset + offset = skipRecord(jewelLUT.data, offset) + end + jewelLUT.seedOffsets[blockKey] = offsets + end + return jewelLUT.seedOffsets[blockKey] +end + +local function readNode(seed, nodeId, jewelType) + local jewelLUT = loadAbyssJewel(jewelType) + local seedIndex = jewelLUT and getSeedIndex(jewelLUT, seed) + if not seedIndex or jewelLUT.format ~= "ABYN" then + return { } + end + local offsets = getRecordOffsets(jewelLUT, nodeId, jewelLUT.blockOffsets[nodeId], skipModification) + return offsets and readModification(jewelLUT.data, offsets[seedIndex], jewelType) or { } +end + +local function readAbyssJewelLUT(seed, socketId, jewelType, path, ascendancyName) + if jewelType >= 7 and jewelType <= 10 then + local jewelLUT = loadAbyssJewel(jewelType) + local seedIndex = jewelLUT and getSeedIndex(jewelLUT, seed) + if not seedIndex or jewelLUT.format ~= "ABYS" then + return { } + end + local offsets = getRecordOffsets(jewelLUT, socketId, jewelLUT.blockOffsets[socketId], skipSocketRecord) + if not offsets then + return { } + end + + local offset = offsets[seedIndex] + local affectedNodeCount = s_byte(jewelLUT.data, offset) + local affectedNodes = { } + offset = offset + 1 + for _ = 1, affectedNodeCount do + local nodeId + nodeId, offset = readUInt16(jewelLUT.data, offset) + affectedNodes[nodeId], offset = readModification(jewelLUT.data, offset, jewelType) + end + return affectedNodes + elseif jewelType ~= 11 or not path then + return { } + end + + local affectedNodes = { } + for nodeId in pairs(path) do + local modification = readNode(seed, nodeId, jewelType) + if next(modification) then + affectedNodes[nodeId] = modification + end + end + local jewelLUT = loadAbyssJewel(jewelType) + local seedIndex = jewelLUT and getSeedIndex(jewelLUT, seed) + if seedIndex and jewelLUT.format == "ABYN" then + -- A Zorath seed selects one node in every ascendancy. A name limits the + -- result to one ascendancy when a caller only needs that section. + local ascendancyOffsets = jewelLUT.ascendancyOffsets + if ascendancyName then + ascendancyOffsets = { [ascendancyName] = ascendancyOffsets[ascendancyName] } + end + for name, blockOffset in pairs(ascendancyOffsets) do + local blockKey = "ascendancy:" .. name + local offsets = getRecordOffsets(jewelLUT, blockKey, blockOffset, skipAscendancyRecord) + if offsets then + local offset = offsets[seedIndex] + local selectedCount = s_byte(jewelLUT.data, offset) + offset = offset + 1 + for _ = 1, selectedCount do + local nodeId + nodeId, offset = readUInt16(jewelLUT.data, offset) + affectedNodes[nodeId] = readNode(seed, nodeId, jewelType) + end + end + end + end + return affectedNodes +end + +return readAbyssJewelLUT, resolveAbyssJewelComponent, getAbyssJewelComponentRoll, parseAbyssJewel diff --git a/src/Modules/DataJewelFileLoader.lua b/src/Modules/DataJewelFileLoader.lua new file mode 100644 index 00000000000..07ce80e52eb --- /dev/null +++ b/src/Modules/DataJewelFileLoader.lua @@ -0,0 +1,95 @@ +-- Path of Building +-- +-- Module: Jewel Data File Loader +-- Loads compressed jewel data from one file or a numbered set of parts. +-- +local t_concat = table.concat + +local function loadJewelFile(jewelTypeName, cacheUncompressed) + local jewelPath = "/Data/TimelessJewelData/" .. jewelTypeName + local scriptPath = GetScriptPath() + if scriptPath == "" then + -- The desktop app supplies its script folder. Headless tests may start in + -- either the repository root or the src folder, so check both locations. + local relativePath = "." .. jewelPath + local file = io.open(relativePath .. ".zip", "rb") or io.open(relativePath .. ".zip.part0", "rb") or io.open(relativePath .. ".bin", "rb") + if file then + file:close() + scriptPath = "." + else + scriptPath = "./src" + end + end + cacheUncompressed = cacheUncompressed ~= false + + local uncompressedFileAttr = { } + if cacheUncompressed then + local fileHandle = NewFileSearch(scriptPath .. jewelPath .. ".bin") + if fileHandle then + uncompressedFileAttr.fileName = fileHandle:GetFileName() + uncompressedFileAttr.modified = fileHandle:GetFileModifiedTime() + end + end + + local compressedFileAttr = { } + local fileHandle = NewFileSearch(scriptPath .. jewelPath .. ".zip") + if fileHandle then + compressedFileAttr.modified = fileHandle:GetFileModifiedTime() + end + fileHandle = NewFileSearch(scriptPath .. jewelPath .. ".zip.part*") + if fileHandle then + compressedFileAttr.modified = fileHandle:GetFileModifiedTime() + end + + if uncompressedFileAttr.modified and uncompressedFileAttr.modified > (compressedFileAttr.modified or 0) then + ConPrintf("Uncompressed jewel data is up-to-date, loading " .. uncompressedFileAttr.fileName) + local uncompressedFile = io.open(scriptPath .. jewelPath .. ".bin", "rb") + if uncompressedFile then + local jewelData = uncompressedFile:read("*a") + uncompressedFile:close() + if jewelData then + return jewelData + end + end + end + + if cacheUncompressed then + ConPrintf("Failed to load " .. scriptPath .. jewelPath .. ".bin, or data is out of date, falling back to compressed file") + end + local compressedFile = io.open(scriptPath .. jewelPath .. ".zip", "rb") + local compressedData + if compressedFile then + compressedData = compressedFile:read("*a") + compressedFile:close() + else + local splitFile = { } + local part = 0 + while true do + local file = io.open(scriptPath .. jewelPath .. ".zip.part" .. part, "rb") + if not file then + break + end + splitFile[part + 1] = file:read("*a") + file:close() + part = part + 1 + end + compressedData = t_concat(splitFile, "") + end + + if not compressedData or compressedData == "" then + ConPrintf("Failed to load jewel data: " .. jewelTypeName) + return + end + + local jewelData = Inflate(compressedData) + if cacheUncompressed then + local uncompressedFile = io.open(scriptPath .. jewelPath .. ".bin", "wb+") + if uncompressedFile then + uncompressedFile:write(jewelData) + uncompressedFile:close() + end + end + return jewelData +end + +return loadJewelFile diff --git a/src/Modules/DataLegionLookUpTableHelper.lua b/src/Modules/DataLegionLookUpTableHelper.lua index 05b6982ff9e..fc05511b147 100644 --- a/src/Modules/DataLegionLookUpTableHelper.lua +++ b/src/Modules/DataLegionLookUpTableHelper.lua @@ -3,78 +3,7 @@ -- Module: Data Legion Look Up Table Helper -- Contains functions for managing the LUTs for the data module -- -local t_concat = table.concat - --- Load legion jewel data -local function loadJewelFile(jewelTypeName) - jewelTypeName = "/Data/TimelessJewelData/" .. jewelTypeName - local jewelData - - local scriptPath = GetScriptPath() - - local fileHandle = NewFileSearch(scriptPath .. jewelTypeName .. ".bin") - local uncompressedFileAttr = { } - if fileHandle then - uncompressedFileAttr.fileName = fileHandle:GetFileName() - uncompressedFileAttr.modified = fileHandle:GetFileModifiedTime() - end - - fileHandle = NewFileSearch(scriptPath .. jewelTypeName .. ".zip") - local compressedFileAttr = { } - if fileHandle then - compressedFileAttr.fileName = fileHandle:GetFileName() - compressedFileAttr.modified = fileHandle:GetFileModifiedTime() - end - - fileHandle = NewFileSearch(scriptPath .. jewelTypeName .. ".zip.part*") - local splitFile = { } - if fileHandle then - compressedFileAttr.modified = fileHandle:GetFileModifiedTime() - end - while fileHandle do - local fileName = fileHandle:GetFileName() - local file = io.open(scriptPath .. "/Data/TimelessJewelData/" .. fileName, "rb") - local part = tonumber(fileName:match("%.part(%d)")) or 0 - splitFile[part + 1] = file:read("*a") - file:close() - if not fileHandle:NextFile() then - break - end - end - splitFile = t_concat(splitFile, "") - - if uncompressedFileAttr.modified and uncompressedFileAttr.modified > (compressedFileAttr.modified or 0) then - ConPrintf("Uncompressed jewel data is up-to-date, loading " .. uncompressedFileAttr.fileName) - local uncompressedFile = io.open(scriptPath .. jewelTypeName .. ".bin", "rb") - if uncompressedFile then - jewelData = uncompressedFile:read("*a") - uncompressedFile:close() - end - if jewelData then - return jewelData - end - end - - ConPrintf("Failed to load " .. scriptPath .. jewelTypeName .. ".bin, or data is out of date, falling back to compressed file") - local compressedFile = io.open(scriptPath .. jewelTypeName .. ".zip", "rb") - if compressedFile then - jewelData = Inflate(compressedFile:read("*a")) - compressedFile:close() - elseif splitFile ~= "" then - jewelData = Inflate(splitFile) - end - - if jewelData == nil then - ConPrintf("Failed to load either file: " .. jewelTypeName .. ".zip, " .. jewelTypeName .. ".bin") - else - local uncompressedFile = io.open(scriptPath .. jewelTypeName .. ".bin", "wb+") - if uncompressedFile then - uncompressedFile:write(jewelData) - uncompressedFile:close() - end - end - return jewelData -end +local loadJewelFile = LoadModule("Modules/DataJewelFileLoader") -- lazy load a specific timeless jewel type -- valid values: "Glorious Vanity", "Lethal Pride", "Brutal Restraint", "Militant Faith", "Elegant Hubris" @@ -330,4 +259,4 @@ local function readLUT(seed, nodeID, jewelType) return { } end -return readLUT, repairLUTs \ No newline at end of file +return readLUT, repairLUTs diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 484a5adb592..bee5687cebf 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -62,6 +62,11 @@ local conquerorList = { ["vorana"] = { id = 1, type = "kalguur" }, ["uhtred"] = { id = 2, type = "kalguur" }, ["medved"] = { id = 3, type = "kalguur" }, + ["tecrod"] = { id = 1, type = "abyss_murderous" }, + ["ulaman"] = { id = 1, type = "abyss_searching" }, + ["kurgal"] = { id = 1, type = "abyss_hypnotic" }, + ["amanamu"] = { id = 1, type = "abyss_ghastly" }, + ["zorath"] = { id = 1, type = "abyss_special" }, } -- List of modifier forms local formList = { @@ -5723,7 +5728,14 @@ local specialModList = { ["remembrancing (%d+) songworthy deeds by the line of (.+)"] = function(num, _, name) return { mod("JewelData", "LIST", { key = "conqueredBy", value = { id = num, conqueror = conquerorList[name:lower()] } }) } end, + ["subjugating (%d+) souls in the thrall of (.+)"] = function(num, _, name) + return { mod("JewelData", "LIST", + { key = "conqueredBy", value = { id = num, conqueror = conquerorList[name:lower()] } }) } end, + ["binding (%d+) souls to phylacteries to sustain (.+)"] = function(num, _, name) + return { mod("JewelData", "LIST", + { key = "conqueredBy", value = { id = num, conqueror = conquerorList[name:lower()] } }) } end, ["passives in radius are conquered by the (%D+)"] = { }, + ["passives affected are conquered by the abyssal"] = { }, ["historic"] = { }, -- Tattoos ["+(%d+) to maximum life per allocated journey tattoo of the body"] = function(num) return { diff --git a/src/TreeData/legion/abyss-line-connector-active.png b/src/TreeData/legion/abyss-line-connector-active.png new file mode 100644 index 00000000000..41e5f56bb6a Binary files /dev/null and b/src/TreeData/legion/abyss-line-connector-active.png differ diff --git a/src/TreeData/legion/abyss-line-connector-intermediate.png b/src/TreeData/legion/abyss-line-connector-intermediate.png new file mode 100644 index 00000000000..276b2fd18a7 Binary files /dev/null and b/src/TreeData/legion/abyss-line-connector-intermediate.png differ diff --git a/src/TreeData/legion/abyss-line-connector-normal.png b/src/TreeData/legion/abyss-line-connector-normal.png new file mode 100644 index 00000000000..f794f1c2313 Binary files /dev/null and b/src/TreeData/legion/abyss-line-connector-normal.png differ diff --git a/src/TreeData/legion/keystone-additional-3.png b/src/TreeData/legion/keystone-additional-3.png deleted file mode 100644 index b8233970bd5..00000000000 Binary files a/src/TreeData/legion/keystone-additional-3.png and /dev/null differ diff --git a/src/TreeData/legion/keystone-additional-disabled-3.png b/src/TreeData/legion/keystone-additional-disabled-3.png deleted file mode 100644 index 31c126a71e1..00000000000 Binary files a/src/TreeData/legion/keystone-additional-disabled-3.png and /dev/null differ diff --git a/src/TreeData/legion/legion-active.png b/src/TreeData/legion/legion-active.png new file mode 100644 index 00000000000..40672b4b09e Binary files /dev/null and b/src/TreeData/legion/legion-active.png differ diff --git a/src/TreeData/legion/legion-art.png b/src/TreeData/legion/legion-art.png new file mode 100644 index 00000000000..3a4858345fc Binary files /dev/null and b/src/TreeData/legion/legion-art.png differ diff --git a/src/TreeData/legion/legion-inactive.png b/src/TreeData/legion/legion-inactive.png new file mode 100644 index 00000000000..82b143bc38b Binary files /dev/null and b/src/TreeData/legion/legion-inactive.png differ diff --git a/src/TreeData/legion/skills-additional-3.jpg b/src/TreeData/legion/skills-additional-3.jpg deleted file mode 100644 index 53615636ab6..00000000000 Binary files a/src/TreeData/legion/skills-additional-3.jpg and /dev/null differ diff --git a/src/TreeData/legion/skills-additional-disabled-3.jpg b/src/TreeData/legion/skills-additional-disabled-3.jpg deleted file mode 100644 index 113e2ecf6be..00000000000 Binary files a/src/TreeData/legion/skills-additional-disabled-3.jpg and /dev/null differ diff --git a/src/TreeData/legion/tree-legion.lua b/src/TreeData/legion/tree-legion.lua index e604affb75f..b983543e17a 100644 --- a/src/TreeData/legion/tree-legion.lua +++ b/src/TreeData/legion/tree-legion.lua @@ -1,119 +1,163 @@ -return { - ["keystoneActive"]={{ - ["filename"]="keystone-additional-3.png", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/DivineFlesh.dds"]={["x"]=128,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/EternalYouth.dds"]={["x"]=192,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/CorruptedDefences.dds"]={["x"]=256,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SoulTetherKeystone.dds"]={["x"]=320,["y"]=64,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/StrengthOfBlood.dds"]={["x"]=0,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/GlancingBlows.dds"]={["x"]=64,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TemperedByWar.dds"]={["x"]=128,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/FocusedRage.dds"]={["x"]=192,["y"]=128,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/WindDancer.dds"]={["x"]=256,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SharpandBrittle.dds"]={["x"]=320,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TheBlindMonk.dds"]={["x"]=0,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/OasisKeystone.dds"]={["x"]=64,["y"]=192,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/MiracleMaker.dds"]={["x"]=128,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/InnerConviction.dds"]={["x"]=192,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/PowerOfPurpose.dds"]={["x"]=256,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TranscendenceKeystone.dds"]={["x"]=320,["y"]=192,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/SupremeDecadence.dds"]={["x"]=0,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeGrandstand.dds"]={["x"]=64,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeEgo.dds"]={["x"]=128,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeProdigy.dds"]={["x"]=192,["y"]=256,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/KalguuranDexKeystone.dds"]={["x"]=256,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/KalguuranIntKeystone.dds"]={["x"]=320,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/KalguuranStrKeystone.dds"]={["x"]=0,["y"]=320,["w"]=64,["h"]=64}, - } - } +-- This file is automatically generated by Export/Scripts/legionSprites.lua. +return { + ["keystoneActive"] = {{ + ["filename"] = "legion-active.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/CorruptedDefences.dds"] = { ["x"] = 0, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/DivineFlesh.dds"] = { ["x"] = 64, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/EternalYouth.dds"] = { ["x"] = 128, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/FocusedRage.dds"] = { ["x"] = 192, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlancingBlows.dds"] = { ["x"] = 256, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairGhastly1.dds"] = { ["x"] = 320, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairHypnotic1.dds"] = { ["x"] = 384, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairMurderous1.dds"] = { ["x"] = 448, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairSearching1.dds"] = { ["x"] = 0, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/InnerConviction.dds"] = { ["x"] = 64, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranDexKeystone.dds"] = { ["x"] = 128, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranIntKeystone.dds"] = { ["x"] = 192, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranStrKeystone.dds"] = { ["x"] = 256, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/MiracleMaker.dds"] = { ["x"] = 320, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/OasisKeystone.dds"] = { ["x"] = 384, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/PowerOfPurpose.dds"] = { ["x"] = 448, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SharpandBrittle.dds"] = { ["x"] = 0, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SoulTetherKeystone.dds"] = { ["x"] = 64, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/StrengthOfBlood.dds"] = { ["x"] = 128, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeDecadence.dds"] = { ["x"] = 192, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeEgo.dds"] = { ["x"] = 256, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeGrandstand.dds"] = { ["x"] = 320, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeProdigy.dds"] = { ["x"] = 384, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TemperedByWar.dds"] = { ["x"] = 448, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TheBlindMonk.dds"] = { ["x"] = 0, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TranscendenceKeystone.dds"] = { ["x"] = 64, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/WindDancer.dds"] = { ["x"] = 128, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + }, + }}, + ["keystoneInactive"] = {{ + ["filename"] = "legion-inactive.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/CorruptedDefences.dds"] = { ["x"] = 0, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/DivineFlesh.dds"] = { ["x"] = 64, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/EternalYouth.dds"] = { ["x"] = 128, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/FocusedRage.dds"] = { ["x"] = 192, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlancingBlows.dds"] = { ["x"] = 256, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairGhastly1.dds"] = { ["x"] = 320, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairHypnotic1.dds"] = { ["x"] = 384, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairMurderous1.dds"] = { ["x"] = 448, ["y"] = 65, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairSearching1.dds"] = { ["x"] = 0, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/InnerConviction.dds"] = { ["x"] = 64, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranDexKeystone.dds"] = { ["x"] = 128, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranIntKeystone.dds"] = { ["x"] = 192, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/KalguuranStrKeystone.dds"] = { ["x"] = 256, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/MiracleMaker.dds"] = { ["x"] = 320, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/OasisKeystone.dds"] = { ["x"] = 384, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/PowerOfPurpose.dds"] = { ["x"] = 448, ["y"] = 129, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SharpandBrittle.dds"] = { ["x"] = 0, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SoulTetherKeystone.dds"] = { ["x"] = 64, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/StrengthOfBlood.dds"] = { ["x"] = 128, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeDecadence.dds"] = { ["x"] = 192, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeEgo.dds"] = { ["x"] = 256, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeGrandstand.dds"] = { ["x"] = 320, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/SupremeProdigy.dds"] = { ["x"] = 384, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TemperedByWar.dds"] = { ["x"] = 448, ["y"] = 193, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TheBlindMonk.dds"] = { ["x"] = 0, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/TranscendenceKeystone.dds"] = { ["x"] = 64, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + ["Art/2DArt/SkillIcons/passives/WindDancer.dds"] = { ["x"] = 128, ["y"] = 257, ["w"] = 64, ["h"] = 64 }, + }, + }}, + ["notableActive"] = {{ + ["filename"] = "legion-active.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/DevotionNotable.dds"] = { ["x"] = 0, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireDefensiveNotable.dds"] = { ["x"] = 38, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireOffensiveNotable.dds"] = { ["x"] = 76, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairGhastly2.dds"] = { ["x"] = 114, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairHypnotic2.dds"] = { ["x"] = 152, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairMurderous2.dds"] = { ["x"] = 190, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairSearching2.dds"] = { ["x"] = 228, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranDexNotable.dds"] = { ["x"] = 266, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranIntNotable.dds"] = { ["x"] = 304, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranStrNotable.dds"] = { ["x"] = 342, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/VaalNotableDefensive.dds"] = { ["x"] = 380, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/VaalNotableOffensive.dds"] = { ["x"] = 418, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + }, + }}, + ["notableInactive"] = {{ + ["filename"] = "legion-inactive.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/DevotionNotable.dds"] = { ["x"] = 0, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireDefensiveNotable.dds"] = { ["x"] = 38, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireOffensiveNotable.dds"] = { ["x"] = 76, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairGhastly2.dds"] = { ["x"] = 114, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairHypnotic2.dds"] = { ["x"] = 152, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairMurderous2.dds"] = { ["x"] = 190, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/GlintingDespairSearching2.dds"] = { ["x"] = 228, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranDexNotable.dds"] = { ["x"] = 266, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranIntNotable.dds"] = { ["x"] = 304, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/KalguuranStrNotable.dds"] = { ["x"] = 342, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/VaalNotableDefensive.dds"] = { ["x"] = 380, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + ["Art/2DArt/SkillIcons/passives/VaalNotableOffensive.dds"] = { ["x"] = 418, ["y"] = 27, ["w"] = 38, ["h"] = 38 }, + }, + }}, + ["normalActive"] = {{ + ["filename"] = "legion-active.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLich.dds"] = { ["x"] = 0, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode1.dds"] = { ["x"] = 27, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode2.dds"] = { ["x"] = 54, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode3.dds"] = { ["x"] = 81, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode4.dds"] = { ["x"] = 108, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/DevotionNode.dds"] = { ["x"] = 135, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireBlank.dds"] = { ["x"] = 162, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/VaalDefensive.dds"] = { ["x"] = 189, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/VaalOffensive.dds"] = { ["x"] = 216, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + }, + }}, + ["normalInactive"] = {{ + ["filename"] = "legion-inactive.png", + ["coords"] = { + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLich.dds"] = { ["x"] = 0, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode1.dds"] = { ["x"] = 27, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode2.dds"] = { ["x"] = 54, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode3.dds"] = { ["x"] = 81, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/AbyssRevampGazeoftheLichNode4.dds"] = { ["x"] = 108, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/DevotionNode.dds"] = { ["x"] = 135, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/EternalEmpireBlank.dds"] = { ["x"] = 162, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/VaalDefensive.dds"] = { ["x"] = 189, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + ["Art/2DArt/SkillIcons/passives/VaalOffensive.dds"] = { ["x"] = 216, ["y"] = 0, ["w"] = 27, ["h"] = 27 }, + }, + }}, + ["treeAssets"] = { + { ["filename"] = "legion-art.png", ["coords"] = { + ["AbyssAscendancyFrameAllocated"] = { ["x"] = 0, ["y"] = 265, ["w"] = 79, ["h"] = 79 }, + ["AbyssAscendancyFrameCanAllocate"] = { ["x"] = 79, ["y"] = 265, ["w"] = 79, ["h"] = 79 }, + ["AbyssAscendancyFrameUnallocated"] = { ["x"] = 158, ["y"] = 265, ["w"] = 79, ["h"] = 79 }, + ["AbyssKeystoneFrameAllocated"] = { ["x"] = 0, ["y"] = 344, ["w"] = 84, ["h"] = 85 }, + ["AbyssKeystoneFrameCanAllocate"] = { ["x"] = 84, ["y"] = 344, ["w"] = 84, ["h"] = 85 }, + ["AbyssKeystoneFrameUnallocated"] = { ["x"] = 168, ["y"] = 344, ["w"] = 84, ["h"] = 85 }, + ["AbyssNotableFrameAllocated"] = { ["x"] = 0, ["y"] = 72, ["w"] = 58, ["h"] = 59 }, + ["AbyssNotableFrameCanAllocate"] = { ["x"] = 58, ["y"] = 72, ["w"] = 58, ["h"] = 59 }, + ["AbyssNotableFrameUnallocated"] = { ["x"] = 116, ["y"] = 72, ["w"] = 58, ["h"] = 59 }, + ["AbyssOrbit1Active"] = { ["x"] = 0, ["y"] = 33, ["w"] = 39, ["h"] = 39 }, + ["AbyssOrbit1Intermediate"] = { ["x"] = 0, ["y"] = 0, ["w"] = 33, ["h"] = 33 }, + ["AbyssOrbit1Normal"] = { ["x"] = 33, ["y"] = 0, ["w"] = 33, ["h"] = 33 }, + ["AbyssOrbit2Active"] = { ["x"] = 0, ["y"] = 195, ["w"] = 70, ["h"] = 70 }, + ["AbyssOrbit2Intermediate"] = { ["x"] = 0, ["y"] = 131, ["w"] = 64, ["h"] = 64 }, + ["AbyssOrbit2Normal"] = { ["x"] = 64, ["y"] = 131, ["w"] = 64, ["h"] = 64 }, + ["AbyssOrbit3Active"] = { ["x"] = 0, ["y"] = 559, ["w"] = 136, ["h"] = 136 }, + ["AbyssOrbit3Intermediate"] = { ["x"] = 0, ["y"] = 429, ["w"] = 130, ["h"] = 130 }, + ["AbyssOrbit3Normal"] = { ["x"] = 130, ["y"] = 429, ["w"] = 130, ["h"] = 130 }, + ["AbyssOrbit4Active"] = { ["x"] = 0, ["y"] = 886, ["w"] = 197, ["h"] = 197 }, + ["AbyssOrbit4Intermediate"] = { ["x"] = 0, ["y"] = 695, ["w"] = 191, ["h"] = 191 }, + ["AbyssOrbit4Normal"] = { ["x"] = 191, ["y"] = 695, ["w"] = 191, ["h"] = 191 }, + ["AbyssOrbit5Active"] = { ["x"] = 0, ["y"] = 1339, ["w"] = 262, ["h"] = 262 }, + ["AbyssOrbit5Intermediate"] = { ["x"] = 0, ["y"] = 1083, ["w"] = 256, ["h"] = 256 }, + ["AbyssOrbit5Normal"] = { ["x"] = 256, ["y"] = 1083, ["w"] = 256, ["h"] = 256 }, + ["AbyssOrbit6Active"] = { ["x"] = 0, ["y"] = 2245, ["w"] = 329, ["h"] = 329 }, + ["AbyssOrbit6Intermediate"] = { ["x"] = 0, ["y"] = 1601, ["w"] = 322, ["h"] = 322 }, + ["AbyssOrbit6Normal"] = { ["x"] = 0, ["y"] = 1923, ["w"] = 322, ["h"] = 322 }, + } }, + { ["filename"] = "abyss-line-connector-normal.png", ["coords"] = { ["AbyssLineConnectorNormal"] = { ["x"] = 0, ["y"] = 0, ["w"] = 368, ["h"] = 13 } } }, + { ["filename"] = "abyss-line-connector-intermediate.png", ["coords"] = { ["AbyssLineConnectorIntermediate"] = { ["x"] = 0, ["y"] = 0, ["w"] = 368, ["h"] = 13 } } }, + { ["filename"] = "abyss-line-connector-active.png", ["coords"] = { ["AbyssLineConnectorActive"] = { ["x"] = 0, ["y"] = 0, ["w"] = 368, ["h"] = 13 } } }, }, - ["keystoneInactive"]={{ - ["filename"]="keystone-additional-disabled-3.png", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/DivineFlesh.dds"]={["x"]=128,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/EternalYouth.dds"]={["x"]=192,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/CorruptedDefences.dds"]={["x"]=256,["y"]=64,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SoulTetherKeystone.dds"]={["x"]=320,["y"]=64,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/StrengthOfBlood.dds"]={["x"]=0,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/GlancingBlows.dds"]={["x"]=64,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TemperedByWar.dds"]={["x"]=128,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/FocusedRage.dds"]={["x"]=192,["y"]=128,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/WindDancer.dds"]={["x"]=256,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SharpandBrittle.dds"]={["x"]=320,["y"]=128,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TheBlindMonk.dds"]={["x"]=0,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/OasisKeystone.dds"]={["x"]=64,["y"]=192,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/MiracleMaker.dds"]={["x"]=128,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/InnerConviction.dds"]={["x"]=192,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/PowerOfPurpose.dds"]={["x"]=256,["y"]=192,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/TranscendenceKeystone.dds"]={["x"]=320,["y"]=192,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/SupremeDecadence.dds"]={["x"]=0,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeGrandstand.dds"]={["x"]=64,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeEgo.dds"]={["x"]=128,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/SupremeProdigy.dds"]={["x"]=192,["y"]=256,["w"]=64,["h"]=64}, - - ["Art/2DArt/SkillIcons/passives/KalguuranDexKeystone.dds"]={["x"]=256,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/KalguuranIntKeystone.dds"]={["x"]=320,["y"]=256,["w"]=64,["h"]=64}, - ["Art/2DArt/SkillIcons/passives/KalguuranStrKeystone.dds"]={["x"]=0,["y"]=320,["w"]=64,["h"]=64}, - } - } - }, - ["notableActive"]={{ - ["filename"]="keystone-additional-3.png", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/VaalNotableOffensive.dds"]={["x"]=0,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/VaalNotableDefensive.dds"]={["x"]=64,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranStrNotable.dds"]={["x"]=128,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranIntNotable.dds"]={["x"]=192,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranDexNotable.dds"]={["x"]=256,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/DevotionNotable.dds"]={["x"]=64,["y"]=64,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireOffensiveNotable.dds"]={["x"]=320,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireDefensiveNotable.dds"]={["x"]=0,["y"]=64,["w"]=38,["h"]=38}, - - } - } - }, - ["notableInactive"]={{ - ["filename"]="keystone-additional-disabled-3.png", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/VaalNotableOffensive.dds"]={["x"]=0,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/VaalNotableDefensive.dds"]={["x"]=64,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranStrNotable.dds"]={["x"]=128,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranIntNotable.dds"]={["x"]=192,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/KalguuranDexNotable.dds"]={["x"]=256,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/DevotionNotable.dds"]={["x"]=64,["y"]=64,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireOffensiveNotable.dds"]={["x"]=320,["y"]=0,["w"]=38,["h"]=38}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireDefensiveNotable.dds"]={["x"]=0,["y"]=64,["w"]=38,["h"]=38}, - } - } - }, - ["normalActive"]={{ - ["filename"]="skills-additional-3.jpg", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/VaalOffensive.dds"]={["x"]=0,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/VaalDefensive.dds"]={["x"]=27,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/DevotionNode.dds"]={["x"]=54,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireBlank.dds"]={["x"]=81,["y"]=0,["w"]=27,["h"]=27}, - } - } - }, - ["normalInactive"]={{ - ["filename"]="skills-additional-disabled-3.jpg", - ["coords"]={ - ["Art/2DArt/SkillIcons/passives/VaalOffensive.dds"]={["x"]=0,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/VaalDefensive.dds"]={["x"]=27,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/DevotionNode.dds"]={["x"]=54,["y"]=0,["w"]=27,["h"]=27}, - ["Art/2DArt/SkillIcons/passives/EternalEmpireBlank.dds"]={["x"]=81,["y"]=0,["w"]=27,["h"]=27}, - } - } - }, -} \ No newline at end of file +}